Repository: dotnet/yarp Branch: main Commit: 6458e08b647a Files: 1031 Total size: 3.3 MB Directory structure: gitextract_y4symcy7/ ├── .azuredevops/ │ └── dependabot.yml ├── .config/ │ ├── CredScanSuppressions.json │ └── tsaoptions.json ├── .dockerignore ├── .editorconfig ├── .gitattributes ├── .github/ │ ├── CODEOWNERS │ ├── ISSUE_TEMPLATE/ │ │ ├── bug.md │ │ ├── feedback.md │ │ └── idea.md │ ├── policies/ │ │ └── resourceManagement.yml │ └── workflows/ │ ├── docker_build.yml │ ├── markdownlint-problem-matcher.json │ ├── markdownlint.yml │ └── markdownlintignore ├── .gitignore ├── .markdownlint.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Directory.Build.props ├── Directory.Build.targets ├── LICENSE.txt ├── NuGet.config ├── README.md ├── SECURITY.md ├── TFMs.props ├── THIRD-PARTY-NOTICES.TXT ├── YARP.slnx ├── activate.ps1 ├── activate.sh ├── azure-pipelines-nonprod.yml ├── azure-pipelines-pr.yml ├── azure-pipelines.yml ├── build.cmd ├── build.sh ├── docs/ │ ├── DailyBuilds.md │ ├── README.md │ ├── designs/ │ │ ├── README.md │ │ ├── config.md │ │ ├── route-extensibility.md │ │ └── yarp-tunneling.md │ ├── operations/ │ │ ├── BackportingToPreview.md │ │ ├── Branching.md │ │ ├── DependencyFlow.md │ │ ├── README.md │ │ └── Release.md │ └── roadmap.md ├── dotnet-yarp-release.yml ├── eng/ │ ├── Build.props │ ├── CodeAnalysis.src.globalconfig │ ├── CodeAnalysis.test.globalconfig │ ├── PoliCheckExclusions.xml │ ├── Publishing.props │ ├── Signing.props │ ├── Version.Details.xml │ ├── Versions.props │ ├── common/ │ │ ├── BuildConfiguration/ │ │ │ └── build-configuration.json │ │ ├── CIBuild.cmd │ │ ├── PSScriptAnalyzerSettings.psd1 │ │ ├── README.md │ │ ├── SetupNugetSources.ps1 │ │ ├── SetupNugetSources.sh │ │ ├── build.cmd │ │ ├── build.ps1 │ │ ├── build.sh │ │ ├── cibuild.sh │ │ ├── core-templates/ │ │ │ ├── job/ │ │ │ │ ├── job.yml │ │ │ │ ├── onelocbuild.yml │ │ │ │ ├── publish-build-assets.yml │ │ │ │ ├── source-build.yml │ │ │ │ └── source-index-stage1.yml │ │ │ ├── jobs/ │ │ │ │ ├── codeql-build.yml │ │ │ │ ├── jobs.yml │ │ │ │ └── source-build.yml │ │ │ ├── post-build/ │ │ │ │ ├── common-variables.yml │ │ │ │ ├── post-build.yml │ │ │ │ └── setup-maestro-vars.yml │ │ │ ├── steps/ │ │ │ │ ├── cleanup-microbuild.yml │ │ │ │ ├── component-governance.yml │ │ │ │ ├── enable-internal-runtimes.yml │ │ │ │ ├── enable-internal-sources.yml │ │ │ │ ├── generate-sbom.yml │ │ │ │ ├── get-delegation-sas.yml │ │ │ │ ├── get-federated-access-token.yml │ │ │ │ ├── install-microbuild-impl.yml │ │ │ │ ├── install-microbuild.yml │ │ │ │ ├── publish-build-artifacts.yml │ │ │ │ ├── publish-logs.yml │ │ │ │ ├── publish-pipeline-artifacts.yml │ │ │ │ ├── retain-build.yml │ │ │ │ ├── send-to-helix.yml │ │ │ │ ├── source-build.yml │ │ │ │ └── source-index-stage1-publish.yml │ │ │ └── variables/ │ │ │ └── pool-providers.yml │ │ ├── cross/ │ │ │ ├── armel/ │ │ │ │ └── tizen/ │ │ │ │ └── tizen.patch │ │ │ ├── build-android-rootfs.sh │ │ │ ├── build-rootfs.sh │ │ │ ├── install-debs.py │ │ │ ├── riscv64/ │ │ │ │ └── tizen/ │ │ │ │ └── tizen.patch │ │ │ ├── tizen-build-rootfs.sh │ │ │ ├── tizen-fetch.sh │ │ │ └── toolchain.cmake │ │ ├── darc-init.ps1 │ │ ├── darc-init.sh │ │ ├── dotnet-install.cmd │ │ ├── dotnet-install.ps1 │ │ ├── dotnet-install.sh │ │ ├── dotnet.cmd │ │ ├── dotnet.ps1 │ │ ├── dotnet.sh │ │ ├── enable-cross-org-publishing.ps1 │ │ ├── generate-locproject.ps1 │ │ ├── generate-sbom-prep.ps1 │ │ ├── generate-sbom-prep.sh │ │ ├── helixpublish.proj │ │ ├── init-tools-native.cmd │ │ ├── init-tools-native.ps1 │ │ ├── init-tools-native.sh │ │ ├── internal/ │ │ │ ├── Directory.Build.props │ │ │ ├── NuGet.config │ │ │ └── Tools.csproj │ │ ├── internal-feed-operations.ps1 │ │ ├── internal-feed-operations.sh │ │ ├── loc/ │ │ │ └── P22DotNetHtmlLocalization.lss │ │ ├── msbuild.ps1 │ │ ├── msbuild.sh │ │ ├── native/ │ │ │ ├── CommonLibrary.psm1 │ │ │ ├── common-library.sh │ │ │ ├── init-compiler.sh │ │ │ ├── init-distro-rid.sh │ │ │ ├── init-os-and-arch.sh │ │ │ ├── install-cmake-test.sh │ │ │ ├── install-cmake.sh │ │ │ ├── install-dependencies.sh │ │ │ └── install-tool.ps1 │ │ ├── pipeline-logging-functions.ps1 │ │ ├── pipeline-logging-functions.sh │ │ ├── post-build/ │ │ │ ├── check-channel-consistency.ps1 │ │ │ ├── nuget-validation.ps1 │ │ │ ├── nuget-verification.ps1 │ │ │ ├── publish-using-darc.ps1 │ │ │ ├── redact-logs.ps1 │ │ │ ├── sourcelink-validation.ps1 │ │ │ └── symbols-validation.ps1 │ │ ├── retain-build.ps1 │ │ ├── sdk-task.ps1 │ │ ├── sdk-task.sh │ │ ├── sdl/ │ │ │ ├── NuGet.config │ │ │ ├── configure-sdl-tool.ps1 │ │ │ ├── execute-all-sdl-tools.ps1 │ │ │ ├── extract-artifact-archives.ps1 │ │ │ ├── extract-artifact-packages.ps1 │ │ │ ├── init-sdl.ps1 │ │ │ ├── packages.config │ │ │ ├── run-sdl.ps1 │ │ │ ├── sdl.ps1 │ │ │ └── trim-assets-version.ps1 │ │ ├── template-guidance.md │ │ ├── templates/ │ │ │ ├── job/ │ │ │ │ ├── job.yml │ │ │ │ ├── onelocbuild.yml │ │ │ │ ├── publish-build-assets.yml │ │ │ │ ├── source-build.yml │ │ │ │ └── source-index-stage1.yml │ │ │ ├── jobs/ │ │ │ │ ├── codeql-build.yml │ │ │ │ ├── jobs.yml │ │ │ │ └── source-build.yml │ │ │ ├── post-build/ │ │ │ │ ├── common-variables.yml │ │ │ │ ├── post-build.yml │ │ │ │ └── setup-maestro-vars.yml │ │ │ ├── steps/ │ │ │ │ ├── component-governance.yml │ │ │ │ ├── enable-internal-runtimes.yml │ │ │ │ ├── enable-internal-sources.yml │ │ │ │ ├── generate-sbom.yml │ │ │ │ ├── get-delegation-sas.yml │ │ │ │ ├── get-federated-access-token.yml │ │ │ │ ├── publish-build-artifacts.yml │ │ │ │ ├── publish-logs.yml │ │ │ │ ├── publish-pipeline-artifacts.yml │ │ │ │ ├── retain-build.yml │ │ │ │ ├── send-to-helix.yml │ │ │ │ ├── source-build.yml │ │ │ │ ├── source-index-stage1-publish.yml │ │ │ │ └── vmr-sync.yml │ │ │ ├── variables/ │ │ │ │ └── pool-providers.yml │ │ │ └── vmr-build-pr.yml │ │ ├── templates-official/ │ │ │ ├── job/ │ │ │ │ ├── job.yml │ │ │ │ ├── onelocbuild.yml │ │ │ │ ├── publish-build-assets.yml │ │ │ │ ├── source-build.yml │ │ │ │ └── source-index-stage1.yml │ │ │ ├── jobs/ │ │ │ │ ├── codeql-build.yml │ │ │ │ ├── jobs.yml │ │ │ │ └── source-build.yml │ │ │ ├── post-build/ │ │ │ │ ├── common-variables.yml │ │ │ │ ├── post-build.yml │ │ │ │ └── setup-maestro-vars.yml │ │ │ ├── steps/ │ │ │ │ ├── component-governance.yml │ │ │ │ ├── enable-internal-runtimes.yml │ │ │ │ ├── enable-internal-sources.yml │ │ │ │ ├── generate-sbom.yml │ │ │ │ ├── get-delegation-sas.yml │ │ │ │ ├── get-federated-access-token.yml │ │ │ │ ├── publish-build-artifacts.yml │ │ │ │ ├── publish-logs.yml │ │ │ │ ├── publish-pipeline-artifacts.yml │ │ │ │ ├── retain-build.yml │ │ │ │ ├── send-to-helix.yml │ │ │ │ ├── source-build.yml │ │ │ │ └── source-index-stage1-publish.yml │ │ │ └── variables/ │ │ │ ├── pool-providers.yml │ │ │ └── sdl-variables.yml │ │ ├── tools.ps1 │ │ ├── tools.sh │ │ ├── vmr-sync.ps1 │ │ └── vmr-sync.sh │ ├── sdl-tsa-vars.config │ └── yarpapppack/ │ ├── Common.projitems │ ├── yarpapppack-linux-arm64.csproj │ └── yarpapppack-linux-x64.csproj ├── es-metadata.yml ├── global.json ├── pack.cmd ├── pack.sh ├── restore.cmd ├── restore.sh ├── samples/ │ ├── BasicYarpSample/ │ │ ├── BasicYarpSample.csproj │ │ ├── Program.cs │ │ ├── Properties/ │ │ │ └── launchSettings.json │ │ ├── README.md │ │ └── appsettings.json │ ├── Directory.Build.props │ ├── KubernetesIngress.Sample/ │ │ ├── Combined/ │ │ │ ├── Dockerfile │ │ │ ├── Program.cs │ │ │ ├── Properties/ │ │ │ │ └── launchSettings.json │ │ │ ├── README.md │ │ │ ├── Yarp.Kubernetes.IngressController.csproj │ │ │ ├── appsettings.Development.json │ │ │ ├── appsettings.json │ │ │ └── ingress-controller.yaml │ │ ├── Ingress/ │ │ │ ├── Dockerfile │ │ │ ├── Program.cs │ │ │ ├── Properties/ │ │ │ │ └── launchSettings.json │ │ │ ├── README.md │ │ │ ├── Yarp.Kubernetes.Ingress.csproj │ │ │ ├── appsettings.Development.json │ │ │ ├── appsettings.json │ │ │ └── ingress.yaml │ │ ├── Monitor/ │ │ │ ├── Dockerfile │ │ │ ├── Program.cs │ │ │ ├── Properties/ │ │ │ │ └── launchSettings.json │ │ │ ├── README.md │ │ │ ├── Yarp.Kubernetes.Monitor.csproj │ │ │ ├── appsettings.Development.json │ │ │ ├── appsettings.json │ │ │ └── ingress-monitor.yaml │ │ ├── README.md │ │ └── backend/ │ │ ├── Dockerfile │ │ ├── Program.cs │ │ ├── Properties/ │ │ │ └── launchSettings.json │ │ ├── README.md │ │ ├── appsettings.Development.json │ │ ├── appsettings.json │ │ ├── backend.csproj │ │ ├── backend.yaml │ │ └── ingress-sample.yaml │ ├── Prometheus/ │ │ ├── HttpLoadApp/ │ │ │ ├── HttpLoadApp.csproj │ │ │ └── Program.cs │ │ ├── README.md │ │ ├── ReverseProxy.Metrics-Prometheus.Sample/ │ │ │ ├── Program.cs │ │ │ ├── PrometheusDnsMetrics.cs │ │ │ ├── PrometheusForwarderMetrics.cs │ │ │ ├── PrometheusKestrelMetrics.cs │ │ │ ├── PrometheusOutboundHttpMetrics.cs │ │ │ ├── PrometheusServiceExtensions.cs │ │ │ ├── PrometheusSocketMetrics.cs │ │ │ ├── Properties/ │ │ │ │ └── launchSettings.json │ │ │ ├── ReverseProxy.Metrics.Prometheus.Sample.csproj │ │ │ ├── appsettings.Development.json │ │ │ └── appsettings.json │ │ ├── prometheus.yml │ │ ├── run10destinations.cmd │ │ └── run10destinations.sh │ ├── README.md │ ├── ReverseProxy.Auth.Sample/ │ │ ├── Controllers/ │ │ │ └── AccountController.cs │ │ ├── Program.cs │ │ ├── Properties/ │ │ │ └── launchSettings.json │ │ ├── README.md │ │ ├── ReverseProxy.Auth.Sample.csproj │ │ ├── TokenService.cs │ │ ├── Views/ │ │ │ └── Account/ │ │ │ ├── AccessDenied.cshtml │ │ │ ├── LoggedOut.cshtml │ │ │ └── Login.cshtml │ │ ├── appsettings.Development.json │ │ └── appsettings.json │ ├── ReverseProxy.Code.Sample/ │ │ ├── Program.cs │ │ ├── Properties/ │ │ │ └── launchSettings.json │ │ ├── README.md │ │ ├── ReverseProxy.Code.Sample.csproj │ │ ├── appsettings.Development.json │ │ └── appsettings.json │ ├── ReverseProxy.Config.Sample/ │ │ ├── Program.cs │ │ ├── Properties/ │ │ │ └── launchSettings.json │ │ ├── README.md │ │ ├── ReverseProxy.Config.Sample.csproj │ │ ├── appsettings.Development.json │ │ └── appsettings.json │ ├── ReverseProxy.ConfigFilter.Sample/ │ │ ├── CustomConfigFilter.cs │ │ ├── Program.cs │ │ ├── Properties/ │ │ │ └── launchSettings.json │ │ ├── README.md │ │ ├── ReverseProxy.ConfigFilter.Sample.csproj │ │ ├── appsettings.Development.json │ │ └── appsettings.json │ ├── ReverseProxy.Direct.Sample/ │ │ ├── Program.cs │ │ ├── Properties/ │ │ │ └── launchSettings.json │ │ ├── README.md │ │ ├── ReverseProxy.Direct.Sample.csproj │ │ ├── appsettings.Development.json │ │ └── appsettings.json │ ├── ReverseProxy.HttpSysDelegation.Sample/ │ │ ├── README.md │ │ ├── ReverseProxy/ │ │ │ ├── Program.cs │ │ │ ├── Properties/ │ │ │ │ └── launchSettings.json │ │ │ ├── ReverseProxy.HttpSysDelegation.Sample.csproj │ │ │ ├── appsettings.Development.json │ │ │ └── appsettings.json │ │ └── SampleHttpSysServer/ │ │ ├── Program.cs │ │ ├── Properties/ │ │ │ └── launchSettings.json │ │ ├── SampleHttpSysServer.csproj │ │ ├── appsettings.Development.json │ │ └── appsettings.json │ ├── ReverseProxy.LetsEncrypt.Sample/ │ │ ├── Program.cs │ │ ├── Properties/ │ │ │ └── launchSettings.json │ │ ├── README.md │ │ ├── ReverseProxy.LetsEncrypt.Sample.csproj │ │ └── appsettings.json │ ├── ReverseProxy.Metrics.Sample/ │ │ ├── ForwarderMetricsConsumer.cs │ │ ├── ForwarderTelemetryConsumer.cs │ │ ├── HttpClientTelemetryConsumer.cs │ │ ├── PerRequestMetrics.cs │ │ ├── PerRequestYarpMetricCollectionMiddleware.cs │ │ ├── Program.cs │ │ ├── Properties/ │ │ │ └── launchSettings.json │ │ ├── README.md │ │ ├── ReverseProxy.Metrics.Sample.csproj │ │ ├── WebSocketsTelemetryConsumer.cs │ │ ├── appsettings.Development.json │ │ └── appsettings.json │ ├── ReverseProxy.Transforms.Sample/ │ │ ├── MyTransformFactory.cs │ │ ├── MyTransformProvider.cs │ │ ├── Program.cs │ │ ├── Properties/ │ │ │ └── launchSettings.json │ │ ├── ReverseProxy.Transforms.Sample.csproj │ │ ├── TokenService.cs │ │ ├── appsettings.Development.json │ │ └── appsettings.json │ └── SampleServer/ │ ├── Controllers/ │ │ ├── HealthController.cs │ │ ├── HttpController.cs │ │ └── WebSocketsController.cs │ ├── Program.cs │ ├── Properties/ │ │ └── launchSettings.json │ ├── README.md │ ├── SampleServer.csproj │ ├── appsettings.Development.json │ ├── appsettings.json │ └── wwwroot/ │ └── index.html ├── src/ │ ├── Application/ │ │ ├── Extensions.cs │ │ ├── Program.cs │ │ └── Yarp.Application.csproj │ ├── Common/ │ │ └── Package.targets │ ├── Directory.Build.props │ ├── Kubernetes.Controller/ │ │ ├── Caching/ │ │ │ ├── Endpoints.cs │ │ │ ├── ICache.cs │ │ │ ├── IngressCache.cs │ │ │ ├── IngressClassData.cs │ │ │ ├── IngressData.cs │ │ │ ├── NamespaceCache.cs │ │ │ └── ServiceData.cs │ │ ├── Certificates/ │ │ │ ├── CertificateHelper.cs │ │ │ ├── ICertificateHelper.cs │ │ │ ├── IServerCertificateSelector.cs │ │ │ └── ServerCertificateSelector.cs │ │ ├── Client/ │ │ │ ├── GroupApiVersionKind.cs │ │ │ ├── IIngressResourceStatusUpdater.cs │ │ │ ├── IResourceInformer.cs │ │ │ ├── IResourceInformerRegistration.cs │ │ │ ├── KubernetesClientOptions.cs │ │ │ ├── ResourceInformer.cs │ │ │ ├── ResourceSelector.cs │ │ │ ├── V1EndpointsResourceInformer.cs │ │ │ ├── V1IngressClassResourceInformer.cs │ │ │ ├── V1IngressResourceInformer.cs │ │ │ ├── V1IngressResourceStatusUpdater.cs │ │ │ ├── V1SecretResourceInformer.cs │ │ │ └── V1ServiceResourceInformer.cs │ │ ├── ConfigProvider/ │ │ │ ├── IUpdateConfig.cs │ │ │ └── KubernetesConfigProvider.cs │ │ ├── Converters/ │ │ │ ├── ClusterTransfer.cs │ │ │ ├── YarpConfigContext.cs │ │ │ ├── YarpIngressContext.cs │ │ │ ├── YarpIngressOptions.cs │ │ │ └── YarpParser.cs │ │ ├── Hosting/ │ │ │ ├── BackgroundHostedService.cs │ │ │ ├── HostedServiceAdapter.cs │ │ │ └── ServiceCollectionHostedServiceAdapterExtensions.cs │ │ ├── Management/ │ │ │ ├── KubernetesCoreExtensions.cs │ │ │ ├── KubernetesReverseProxyServiceCollectionExtensions.cs │ │ │ └── KubernetesReverseProxyWebHostBuilderExtensions.cs │ │ ├── NamespacedName.cs │ │ ├── Properties/ │ │ │ └── AssemblyInfo.cs │ │ ├── Protocol/ │ │ │ ├── DispatchActionResult.cs │ │ │ ├── DispatchConfigProvider.cs │ │ │ ├── DispatchController.cs │ │ │ ├── Dispatcher.cs │ │ │ ├── IDispatchTarget.cs │ │ │ ├── IDispatcher.cs │ │ │ ├── Message.cs │ │ │ ├── MessageConfigProviderExtensions.cs │ │ │ ├── Receiver.cs │ │ │ └── ReceiverOptions.cs │ │ ├── Queues/ │ │ │ ├── IWorkQueue.cs │ │ │ ├── ProcessingRateLimitedQueue.cs │ │ │ └── WorkQueue.cs │ │ ├── Rate/ │ │ │ ├── Limit.cs │ │ │ ├── Limiter.cs │ │ │ └── Reservation.cs │ │ ├── Services/ │ │ │ ├── IReconciler.cs │ │ │ ├── IngressController.cs │ │ │ ├── QueueItem.cs │ │ │ ├── ReconcileData.cs │ │ │ └── Reconciler.cs │ │ ├── Yarp.Kubernetes.Controller.csproj │ │ └── YarpOptions.cs │ ├── ReverseProxy/ │ │ ├── Configuration/ │ │ │ ├── ActiveHealthCheckConfig.cs │ │ │ ├── AuthorizationConstants.cs │ │ │ ├── ClusterConfig.cs │ │ │ ├── ClusterValidators/ │ │ │ │ ├── DestinationValidator.cs │ │ │ │ ├── HealthCheckValidator.cs │ │ │ │ ├── IClusterValidator.cs │ │ │ │ ├── LoadBalancingValidator.cs │ │ │ │ ├── ProxyHttpClientValidator.cs │ │ │ │ ├── ProxyHttpRequestValidator.cs │ │ │ │ └── SessionAffinityValidator.cs │ │ │ ├── ConfigProvider/ │ │ │ │ ├── ConfigurationConfigProvider.cs │ │ │ │ ├── ConfigurationReadingExtensions.cs │ │ │ │ └── ConfigurationSnapshot.cs │ │ │ ├── ConfigValidator.cs │ │ │ ├── CorsConstants.cs │ │ │ ├── DestinationConfig.cs │ │ │ ├── HeaderMatchMode.cs │ │ │ ├── HealthCheckConfig.cs │ │ │ ├── HttpClientConfig.cs │ │ │ ├── IConfigChangeListener.cs │ │ │ ├── IConfigValidator.cs │ │ │ ├── IProxyConfig.cs │ │ │ ├── IProxyConfigFilter.cs │ │ │ ├── IProxyConfigProvider.cs │ │ │ ├── IYarpOutputCachePolicyProvider.cs │ │ │ ├── IYarpRateLimiterPolicyProvider.cs │ │ │ ├── InMemoryConfigProvider.cs │ │ │ ├── InMemoryConfigProviderExtensions.cs │ │ │ ├── PassiveHealthCheckConfig.cs │ │ │ ├── QueryParameterMatchMode.cs │ │ │ ├── RateLimitingConstants.cs │ │ │ ├── RouteConfig.cs │ │ │ ├── RouteHeader.cs │ │ │ ├── RouteMatch.cs │ │ │ ├── RouteQueryParameter.cs │ │ │ ├── RouteValidators/ │ │ │ │ ├── AuthorizationPolicyValidator.cs │ │ │ │ ├── CorsPolicyValidator.cs │ │ │ │ ├── HeadersValidator.cs │ │ │ │ ├── HostValidator.cs │ │ │ │ ├── IRouteValidator.cs │ │ │ │ ├── MethodsValidator.cs │ │ │ │ ├── OutputCachePolicyValidator.cs │ │ │ │ ├── PathValidator.cs │ │ │ │ ├── QueryParametersValidator.cs │ │ │ │ ├── RateLimitPolicyValidator.cs │ │ │ │ └── TimeoutPolicyValidator.cs │ │ │ ├── SessionAffinityConfig.cs │ │ │ ├── SessionAffinityCookieConfig.cs │ │ │ ├── TimeoutPolicyConstants.cs │ │ │ └── WebProxyConfig.cs │ │ ├── ConfigurationSchema.json │ │ ├── Delegation/ │ │ │ ├── AppBuilderDelegationExtensions.cs │ │ │ ├── DelegationExtensions.cs │ │ │ ├── DummyHttpSysDelegator.cs │ │ │ ├── HttpSysDelegator.cs │ │ │ ├── HttpSysDelegatorMiddleware.cs │ │ │ └── IHttpSysDelegator.cs │ │ ├── Forwarder/ │ │ │ ├── CallbackHttpClientFactory.cs │ │ │ ├── DirectForwardingHttpClientProvider.cs │ │ │ ├── EmptyHttpContent.cs │ │ │ ├── ForwarderError.cs │ │ │ ├── ForwarderErrorFeature.cs │ │ │ ├── ForwarderHttpClientContext.cs │ │ │ ├── ForwarderHttpClientFactory.cs │ │ │ ├── ForwarderMiddleware.cs │ │ │ ├── ForwarderRequestConfig.cs │ │ │ ├── ForwarderStage.cs │ │ │ ├── ForwarderTelemetry.cs │ │ │ ├── HttpForwarder.cs │ │ │ ├── HttpTransformer.cs │ │ │ ├── IForwarderErrorFeature.cs │ │ │ ├── IForwarderHttpClientFactory.cs │ │ │ ├── IHttpForwarder.cs │ │ │ ├── IHttpForwarderExtensions.cs │ │ │ ├── ProtocolHelper.cs │ │ │ ├── RequestTransformer.cs │ │ │ ├── RequestUtilities.cs │ │ │ ├── ReverseProxyPropagator.cs │ │ │ ├── StreamCopier.cs │ │ │ ├── StreamCopyHttpContent.cs │ │ │ └── StreamCopyResult.cs │ │ ├── Health/ │ │ │ ├── ActiveHealthCheckMonitor.Log.cs │ │ │ ├── ActiveHealthCheckMonitor.cs │ │ │ ├── ActiveHealthCheckMonitorOptions.cs │ │ │ ├── AppBuilderHealthExtensions.cs │ │ │ ├── ClusterDestinationsUpdater.cs │ │ │ ├── ConsecutiveFailuresHealthPolicy.cs │ │ │ ├── ConsecutiveFailuresHealthPolicyOptions.cs │ │ │ ├── DefaultProbingRequestFactory.cs │ │ │ ├── DestinationHealthUpdater.cs │ │ │ ├── DestinationProbingResult.cs │ │ │ ├── EntityActionScheduler.cs │ │ │ ├── HealthCheckConstants.cs │ │ │ ├── HealthyAndUnknownDestinationsPolicy.cs │ │ │ ├── HealthyOrPanicDestinationsPolicy.cs │ │ │ ├── IActiveHealthCheckMonitor.cs │ │ │ ├── IActiveHealthCheckPolicy.cs │ │ │ ├── IAvailableDestinationsPolicy.cs │ │ │ ├── IClusterDestinationsUpdater.cs │ │ │ ├── IDestinationHealthUpdater.cs │ │ │ ├── IPassiveHealthCheckPolicy.cs │ │ │ ├── IProbingRequestFactory.cs │ │ │ ├── NewActiveDestinationHealth.cs │ │ │ ├── PassiveHealthCheckMiddleware.cs │ │ │ ├── TransportFailureRateHealthPolicy.cs │ │ │ └── TransportFailureRateHealthPolicyOptions.cs │ │ ├── Limits/ │ │ │ └── LimitsMiddleware.cs │ │ ├── LoadBalancing/ │ │ │ ├── AppBuilderLoadBalancingExtensions.cs │ │ │ ├── FirstLoadBalancingPolicy.cs │ │ │ ├── ILoadBalancingPolicy.cs │ │ │ ├── LeastRequestsLoadBalancingPolicy.cs │ │ │ ├── LoadBalancingMiddleware.cs │ │ │ ├── LoadBalancingPolicies.cs │ │ │ ├── PowerOfTwoChoicesLoadBalancingPolicy.cs │ │ │ ├── RandomLoadBalancingPolicy.cs │ │ │ └── RoundRobinLoadBalancingPolicy.cs │ │ ├── Management/ │ │ │ ├── IProxyStateLookup.cs │ │ │ ├── IReverseProxyBuilder.cs │ │ │ ├── IReverseProxyBuilderExtensions.cs │ │ │ ├── ProxyConfigManager.cs │ │ │ ├── ReverseProxyBuilder.cs │ │ │ └── ReverseProxyServiceCollectionExtensions.cs │ │ ├── Model/ │ │ │ ├── ClusterDestinationsState.cs │ │ │ ├── ClusterModel.cs │ │ │ ├── ClusterState.cs │ │ │ ├── DestinationHealth.cs │ │ │ ├── DestinationHealthState.cs │ │ │ ├── DestinationModel.cs │ │ │ ├── DestinationState.cs │ │ │ ├── HttpContextFeaturesExtensions.cs │ │ │ ├── IClusterChangeListener.cs │ │ │ ├── IReverseProxyApplicationBuilder.cs │ │ │ ├── IReverseProxyFeature.cs │ │ │ ├── ProxyPipelineInitializerMiddleware.cs │ │ │ ├── README.md │ │ │ ├── ReverseProxyApplicationBuilder.cs │ │ │ ├── ReverseProxyFeature.cs │ │ │ ├── RouteModel.cs │ │ │ └── RouteState.cs │ │ ├── README.md │ │ ├── Routing/ │ │ │ ├── DirectForwardingIEndpointRouteBuilderExtensions.cs │ │ │ ├── HeaderMatcher.cs │ │ │ ├── HeaderMatcherPolicy.cs │ │ │ ├── HeaderMetadata.cs │ │ │ ├── IHeaderMetadata.cs │ │ │ ├── IQueryParameterMetadata.cs │ │ │ ├── ProxyEndpointFactory.cs │ │ │ ├── QueryParameterMatcher.cs │ │ │ ├── QueryParameterMatcherPolicy.cs │ │ │ ├── QueryParameterMetadata.cs │ │ │ ├── ReverseProxyConventionBuilder.cs │ │ │ └── ReverseProxyIEndpointRouteBuilderExtensions.cs │ │ ├── ServiceDiscovery/ │ │ │ ├── DnsDestinationResolver.cs │ │ │ ├── DnsDestinationResolverOptions.cs │ │ │ ├── IDestinationResolver.cs │ │ │ ├── NoOpDestinationResolver.cs │ │ │ └── ResolvedDestinationCollection.cs │ │ ├── SessionAffinity/ │ │ │ ├── AffinitizeTransform.cs │ │ │ ├── AffinitizeTransformProvider.cs │ │ │ ├── AffinityHelpers.cs │ │ │ ├── AffinityResult.cs │ │ │ ├── AffinityStatus.cs │ │ │ ├── AppBuilderSessionAffinityExtensions.cs │ │ │ ├── ArrCookieSessionAffinityPolicy.cs │ │ │ ├── BaseEncryptedSessionAffinityPolicy.cs │ │ │ ├── BaseHashCookieSessionAffinityPolicy.cs │ │ │ ├── CookieSessionAffinityPolicy.cs │ │ │ ├── CustomHeaderSessionAffinityPolicy.cs │ │ │ ├── HashCookieSessionAffinityPolicy.cs │ │ │ ├── IAffinityFailurePolicy.cs │ │ │ ├── ISessionAffinityPolicy.cs │ │ │ ├── Log.cs │ │ │ ├── RedistributeAffinityFailurePolicy.cs │ │ │ ├── Return503ErrorAffinityFailurePolicy.cs │ │ │ ├── SessionAffinityConstants.cs │ │ │ └── SessionAffinityMiddleware.cs │ │ ├── Transforms/ │ │ │ ├── Builder/ │ │ │ │ ├── ActionTransformProvider.cs │ │ │ │ ├── ITransformBuilder.cs │ │ │ │ ├── ITransformFactory.cs │ │ │ │ ├── ITransformProvider.cs │ │ │ │ ├── StructuredTransformer.cs │ │ │ │ ├── TransformBuilder.cs │ │ │ │ ├── TransformBuilderContext.cs │ │ │ │ ├── TransformClusterValidationContext.cs │ │ │ │ ├── TransformHelpers.cs │ │ │ │ └── TransformRouteValidationContext.cs │ │ │ ├── ForwardedTransformActions.cs │ │ │ ├── ForwardedTransformExtensions.cs │ │ │ ├── ForwardedTransformFactory.cs │ │ │ ├── HttpMethodChangeTransform.cs │ │ │ ├── HttpMethodTransformExtensions.cs │ │ │ ├── HttpMethodTransformFactory.cs │ │ │ ├── NodeFormat.cs │ │ │ ├── PathRouteValuesTransform.cs │ │ │ ├── PathStringTransform.cs │ │ │ ├── PathTransformExtensions.cs │ │ │ ├── PathTransformFactory.cs │ │ │ ├── QueryParameterFromRouteTransform.cs │ │ │ ├── QueryParameterFromStaticTransform.cs │ │ │ ├── QueryParameterRemoveTransform.cs │ │ │ ├── QueryParameterTransform.cs │ │ │ ├── QueryTransformContext.cs │ │ │ ├── QueryTransformExtensions.cs │ │ │ ├── QueryTransformFactory.cs │ │ │ ├── RequestFuncTransform.cs │ │ │ ├── RequestHeaderClientCertTransform.cs │ │ │ ├── RequestHeaderForwardedTransform.cs │ │ │ ├── RequestHeaderOriginalHostTransform.cs │ │ │ ├── RequestHeaderRemoveTransform.cs │ │ │ ├── RequestHeaderRouteValueTransform.cs │ │ │ ├── RequestHeaderTransform.cs │ │ │ ├── RequestHeaderValueTransform.cs │ │ │ ├── RequestHeaderXForwardedForTransform.cs │ │ │ ├── RequestHeaderXForwardedHostTransform.cs │ │ │ ├── RequestHeaderXForwardedPrefixTransform.cs │ │ │ ├── RequestHeaderXForwardedProtoTransform.cs │ │ │ ├── RequestHeadersAllowedTransform.cs │ │ │ ├── RequestHeadersTransformExtensions.cs │ │ │ ├── RequestHeadersTransformFactory.cs │ │ │ ├── RequestTransform.cs │ │ │ ├── RequestTransformContext.cs │ │ │ ├── ResponseCondition.cs │ │ │ ├── ResponseFuncTransform.cs │ │ │ ├── ResponseHeaderRemoveTransform.cs │ │ │ ├── ResponseHeaderValueTransform.cs │ │ │ ├── ResponseHeadersAllowedTransform.cs │ │ │ ├── ResponseTrailerRemoveTransform.cs │ │ │ ├── ResponseTrailerValueTransform.cs │ │ │ ├── ResponseTrailersAllowedTransform.cs │ │ │ ├── ResponseTrailersFuncTransform.cs │ │ │ ├── ResponseTrailersTransform.cs │ │ │ ├── ResponseTrailersTransformContext.cs │ │ │ ├── ResponseTransform.cs │ │ │ ├── ResponseTransformContext.cs │ │ │ ├── ResponseTransformExtensions.cs │ │ │ ├── ResponseTransformFactory.cs │ │ │ ├── RouteConfigTransformExtensions.cs │ │ │ └── TransformBuilderContextFuncExtensions.cs │ │ ├── Utilities/ │ │ │ ├── ActivityCancellationTokenSource.cs │ │ │ ├── AtomicCounter.cs │ │ │ ├── CaseInsensitiveEqualHelper.cs │ │ │ ├── CaseSensitiveEqualHelper.cs │ │ │ ├── CollectionEqualityHelper.cs │ │ │ ├── ConcurrentDictionaryExtensions.cs │ │ │ ├── DelegatingStream.cs │ │ │ ├── EventIds.cs │ │ │ ├── IClock.cs │ │ │ ├── IRandomFactory.cs │ │ │ ├── NullRandomFactory.cs │ │ │ ├── Observability.cs │ │ │ ├── ParsedMetadataEntry.cs │ │ │ ├── RandomFactory.cs │ │ │ ├── ServiceLookupHelper.cs │ │ │ ├── SkipLocalsInit.cs │ │ │ ├── TaskUtilities.cs │ │ │ ├── TlsFrameHelper.cs │ │ │ ├── ValueStopwatch.cs │ │ │ └── ValueStringBuilder.cs │ │ ├── WebSocketsTelemetry/ │ │ │ ├── HttpConnectFeatureWrapper.cs │ │ │ ├── HttpUpgradeFeatureWrapper.cs │ │ │ ├── WebSocketCloseReason.cs │ │ │ ├── WebSocketsParser.cs │ │ │ ├── WebSocketsTelemetry.cs │ │ │ ├── WebSocketsTelemetryExtensions.cs │ │ │ ├── WebSocketsTelemetryMiddleware.cs │ │ │ └── WebSocketsTelemetryStream.cs │ │ └── Yarp.ReverseProxy.csproj │ └── TelemetryConsumption/ │ ├── EventListenerService.cs │ ├── Forwarder/ │ │ ├── ForwarderEventListenerService.cs │ │ ├── ForwarderMetrics.cs │ │ ├── ForwarderStage.cs │ │ └── IForwarderTelemetryConsumer.cs │ ├── Http/ │ │ ├── HttpEventListenerService.cs │ │ ├── HttpMetrics.cs │ │ └── IHttpTelemetryConsumer.cs │ ├── IMetricsConsumer.cs │ ├── Kestrel/ │ │ ├── IKestrelTelemetryConsumer.cs │ │ ├── KestrelEventListenerService.cs │ │ └── KestrelMetrics.cs │ ├── MetricsOptions.cs │ ├── NameResolution/ │ │ ├── INameResolutionTelemetryConsumer.cs │ │ ├── NameResolutionEventListenerService.cs │ │ └── NameResolutionMetrics.cs │ ├── NetSecurity/ │ │ ├── INetSecurityTelemetryConsumer.cs │ │ ├── NetSecurityEventListenerService.cs │ │ └── NetSecurityMetrics.cs │ ├── README.md │ ├── Sockets/ │ │ ├── ISocketsTelemetryConsumer.cs │ │ ├── SocketsEventListenerService.cs │ │ └── SocketsMetrics.cs │ ├── TelemetryConsumptionExtensions.cs │ ├── WebSockets/ │ │ ├── IWebSocketsTelemetryConsumer.cs │ │ ├── WebSocketCloseReason.cs │ │ └── WebSocketsEventListenerService.cs │ └── Yarp.Telemetry.Consumption.csproj ├── startvs.cmd ├── test/ │ ├── Directory.Build.props │ ├── Kubernetes.Tests/ │ │ ├── Certificates/ │ │ │ ├── CertificateHelperTests.cs │ │ │ ├── cert.der │ │ │ ├── cert.pem │ │ │ ├── key.der │ │ │ └── key.pem │ │ ├── Client/ │ │ │ ├── ResourceInformerTests.cs │ │ │ ├── SyncResourceInformer.cs │ │ │ ├── V1DeploymentResourceInformer.cs │ │ │ └── V1PodResourceInformer.cs │ │ ├── Hosting/ │ │ │ ├── BackgroundHostedServiceTests.cs │ │ │ └── Fakes/ │ │ │ ├── FakeBackgroundHostedService.cs │ │ │ ├── FakeServer.cs │ │ │ ├── TestLatch.cs │ │ │ └── TestLatches.cs │ │ ├── IngressCacheTests.cs │ │ ├── IngressControllerTests.cs │ │ ├── IngressConversionTests.cs │ │ ├── KubeResourceGenerator.cs │ │ ├── Management/ │ │ │ └── KubernetesCoreExtensionsTests.cs │ │ ├── NamespacedNameTests.cs │ │ ├── Queues/ │ │ │ └── WorkQueueTests.cs │ │ ├── Rate/ │ │ │ ├── LimitTests.cs │ │ │ ├── LimiterTests.cs │ │ │ └── ReservationTests.cs │ │ ├── ReconcilerTests.cs │ │ ├── TestCluster/ │ │ │ ├── Controllers/ │ │ │ │ ├── ResourceApiController.cs │ │ │ │ └── ResourceApiGroupController.cs │ │ │ ├── ITestCluster.cs │ │ │ ├── ITestClusterHost.cs │ │ │ ├── Models/ │ │ │ │ ├── ListParameters.cs │ │ │ │ ├── ListResult.cs │ │ │ │ └── ResourceObject.cs │ │ │ ├── TestCluster.cs │ │ │ ├── TestClusterHost.cs │ │ │ ├── TestClusterHostBuilder.cs │ │ │ ├── TestClusterOptions.cs │ │ │ └── TestClusterStartup.cs │ │ ├── Utils/ │ │ │ ├── ResourceSerializers.cs │ │ │ └── TestLogger.cs │ │ ├── Yarp.Kubernetes.Tests.csproj │ │ └── testassets/ │ │ ├── annotations/ │ │ │ ├── clusters.json │ │ │ ├── ingress.yaml │ │ │ └── routes.json │ │ ├── basic-ingress/ │ │ │ ├── clusters.json │ │ │ ├── ingress.yaml │ │ │ └── routes.json │ │ ├── exact-match/ │ │ │ ├── clusters.json │ │ │ ├── ingress.yaml │ │ │ └── routes.json │ │ ├── external-name-ingress/ │ │ │ ├── clusters.json │ │ │ ├── ingress.yaml │ │ │ └── routes.json │ │ ├── hostname-routing/ │ │ │ ├── clusters.json │ │ │ ├── ingress.yaml │ │ │ └── routes.json │ │ ├── https/ │ │ │ ├── clusters.json │ │ │ ├── ingress.yaml │ │ │ └── routes.json │ │ ├── https-service-port-protocol/ │ │ │ ├── clusters.json │ │ │ ├── ingress.yaml │ │ │ └── routes.json │ │ ├── ingress-class-not-set/ │ │ │ └── ingress.yaml │ │ ├── ingress-class-set/ │ │ │ └── ingress.yaml │ │ ├── mapped-port/ │ │ │ ├── clusters.json │ │ │ ├── ingress.yaml │ │ │ └── routes.json │ │ ├── missing-svc/ │ │ │ ├── clusters.json │ │ │ ├── ingress.yaml │ │ │ └── routes.json │ │ ├── multiple-endpoints-ports/ │ │ │ ├── clusters.json │ │ │ ├── ingress.yaml │ │ │ └── routes.json │ │ ├── multiple-endpoints-same-port/ │ │ │ ├── clusters.json │ │ │ ├── ingress.yaml │ │ │ └── routes.json │ │ ├── multiple-hosts/ │ │ │ ├── clusters.json │ │ │ ├── ingress.yaml │ │ │ └── routes.json │ │ ├── multiple-ingresses/ │ │ │ ├── clusters.json │ │ │ ├── ingress.yaml │ │ │ └── routes.json │ │ ├── multiple-ingresses-one-svc/ │ │ │ ├── clusters.json │ │ │ ├── ingress.yaml │ │ │ └── routes.json │ │ ├── multiple-namespaces/ │ │ │ ├── clusters.json │ │ │ ├── ingress.yaml │ │ │ └── routes.json │ │ ├── port-diff-name/ │ │ │ ├── clusters.json │ │ │ ├── ingress.yaml │ │ │ └── routes.json │ │ ├── port-mismatch/ │ │ │ ├── clusters.json │ │ │ ├── ingress.yaml │ │ │ └── routes.json │ │ ├── resource-informer/ │ │ │ ├── ResourcesAreListedWhenReadyAsyncIsComplete/ │ │ │ │ ├── resources.yaml │ │ │ │ └── shouldbe.yaml │ │ │ └── ResourcesWithApiGroupAreListed/ │ │ │ ├── resources.yaml │ │ │ └── shouldbe.yaml │ │ ├── route-headers/ │ │ │ ├── clusters.json │ │ │ ├── ingress.yaml │ │ │ └── routes.json │ │ ├── route-metadata/ │ │ │ ├── clusters.json │ │ │ ├── ingress.yaml │ │ │ └── routes.json │ │ ├── route-methods/ │ │ │ ├── clusters.json │ │ │ ├── ingress.yaml │ │ │ └── routes.json │ │ ├── route-order/ │ │ │ ├── clusters.json │ │ │ ├── ingress.yaml │ │ │ └── routes.json │ │ └── route-queryparameters/ │ │ ├── clusters.json │ │ ├── ingress.yaml │ │ └── routes.json │ ├── ReverseProxy.FunctionalTests/ │ │ ├── Common/ │ │ │ ├── Helpers.cs │ │ │ ├── HttpSysTestEnvironment.cs │ │ │ ├── TestEnvironment.cs │ │ │ └── TestUrlHelper.cs │ │ ├── DistributedTracingTests.cs │ │ ├── Expect100ContinueTests.cs │ │ ├── HeaderTests.cs │ │ ├── HttpForwarderCancellationTests.cs │ │ ├── HttpProxyCookieTests.cs │ │ ├── HttpSysDelegationTests.cs │ │ ├── PassiveHealthCheckTests.cs │ │ ├── TelemetryConsumptionTests.cs │ │ ├── TelemetryEnumTests.cs │ │ ├── WebSocketTests.cs │ │ ├── WebSocketsTelemetryTests.cs │ │ └── Yarp.ReverseProxy.FunctionalTests.csproj │ ├── ReverseProxy.Tests/ │ │ ├── Common/ │ │ │ ├── EventAssertExtensions.cs │ │ │ ├── HttpContentExtensions.cs │ │ │ ├── MockHttpHandler.cs │ │ │ ├── TaskExtensions.cs │ │ │ ├── TestEventListener.cs │ │ │ ├── TestResources.cs │ │ │ └── TestTrailersFeature.cs │ │ ├── Configuration/ │ │ │ ├── ActiveHealthCheckConfigTests.cs │ │ │ ├── ClusterConfigTests.cs │ │ │ ├── ConfigProvider/ │ │ │ │ ├── ConfigurationConfigProviderTests.cs │ │ │ │ └── ConfigurationReadingExtensionsTests.cs │ │ │ ├── ConfigValidatorTests.cs │ │ │ ├── DestinationConfigTests.cs │ │ │ ├── HealthCheckConfigTests.cs │ │ │ ├── HttpClientConfigTests.cs │ │ │ ├── PassiveHealthCheckConfigTests.cs │ │ │ ├── RouteConfigTests.cs │ │ │ ├── RouteHeaderTests.cs │ │ │ ├── RouteMatchTests.cs │ │ │ ├── RouteQueryParameterTests.cs │ │ │ ├── SessionAffinityConfigTests.cs │ │ │ ├── YarpOutputCachePolicyProviderTests.cs │ │ │ └── YarpRateLimiterPolicyProviderTests.cs │ │ ├── Delegation/ │ │ │ ├── HttpSysDelegatorMiddlewareTests.cs │ │ │ └── HttpSysDelegatorTests.cs │ │ ├── Forwarder/ │ │ │ ├── ForwarderHttpClientFactoryTests.cs │ │ │ ├── ForwarderMiddlewareTests.cs │ │ │ ├── HttpForwarderTests.cs │ │ │ ├── HttpTransformerTests.cs │ │ │ ├── RequestUtilitiesTests.cs │ │ │ ├── ReverseProxyServiceCollectionTests.cs │ │ │ ├── StreamCopierTests.cs │ │ │ └── StreamCopyHttpContentTests.cs │ │ ├── Health/ │ │ │ ├── ActiveHealthCheckMonitorTests.cs │ │ │ ├── ClusterDestinationsUpdaterTests.cs │ │ │ ├── ConsecutiveFailuresHealthPolicyTests.cs │ │ │ ├── DefaultProbingRequestFactoryTests.cs │ │ │ ├── DestinationHealthUpdaterTests.cs │ │ │ ├── EntityActionSchedulerTests.cs │ │ │ ├── HealthyAndUnknownDestinationsPolicyTests.cs │ │ │ ├── HealthyOrPanicDestinationsPolicyTests.cs │ │ │ ├── PassiveHealthCheckMiddlewareTests.cs │ │ │ └── TransportFailureRateHealthPolicyTests.cs │ │ ├── Limits/ │ │ │ └── LimitsMiddlewareTests.cs │ │ ├── LoadBalancing/ │ │ │ ├── LoadBalancerMiddlewareTests.cs │ │ │ └── LoadBalancingPoliciesTests.cs │ │ ├── Management/ │ │ │ └── ProxyConfigManagerTests.cs │ │ ├── Model/ │ │ │ ├── DestinationStateTests.cs │ │ │ ├── HttpContextFeaturesExtensions.cs │ │ │ └── ProxyPipelineInitializerMiddlewareTests.cs │ │ ├── Routing/ │ │ │ ├── HeaderMatcherPolicyTests.cs │ │ │ ├── ProxyEndpointFactoryTests.cs │ │ │ ├── QueryMatcherPolicyTests.cs │ │ │ ├── ReverseProxyConventionBuilderTests.cs │ │ │ └── RoutingTests.cs │ │ ├── SessionAffinity/ │ │ │ ├── AffinitizeTransformProviderTests.cs │ │ │ ├── AffinitizeTransformTests.cs │ │ │ ├── AffinityTestHelper.cs │ │ │ ├── ArrCookieSessionAffinityPolicyTests.cs │ │ │ ├── BaseSessionAffinityPolicyTests.cs │ │ │ ├── CookieSessionAffinityPolicyTests.cs │ │ │ ├── CustomHeaderSessionAffinityPolicyTests.cs │ │ │ ├── HashCookieSessionAffinityPolicyTests.cs │ │ │ ├── RedistributeAffinityFailurePolicyTests.cs │ │ │ ├── Return503ErrorAffinityFailurePolicyTests.cs │ │ │ └── SessionAffinityMiddlewareTests.cs │ │ ├── Transforms/ │ │ │ ├── Builder/ │ │ │ │ └── TransformBuilderTests.cs │ │ │ ├── DestinationPrefixTransformTests.cs │ │ │ ├── ForwardedTransformExtensionsTests.cs │ │ │ ├── HttpMethodChangeTransformTests.cs │ │ │ ├── HttpMethodTransformExtensionsTests.cs │ │ │ ├── PathRouteValuesTransformTests.cs │ │ │ ├── PathStringTransformTests.cs │ │ │ ├── PathTransformExtensionsTests.cs │ │ │ ├── QueryParameterFromRouteTransformTests.cs │ │ │ ├── QueryParameterFromStaticTransformTests.cs │ │ │ ├── QueryParameterRemoveTransformTests.cs │ │ │ ├── QueryTransformContextTests.cs │ │ │ ├── QueryTransformExtensionsTests.cs │ │ │ ├── RequestHeaderClientCertTransformTests.cs │ │ │ ├── RequestHeaderForwardedTransformTests.cs │ │ │ ├── RequestHeaderRemoveTransformTests.cs │ │ │ ├── RequestHeaderRouteValueTransformTests.cs │ │ │ ├── RequestHeaderValueTransformTests.cs │ │ │ ├── RequestHeaderXForwardedForTransformTests.cs │ │ │ ├── RequestHeaderXForwardedHostTransformTests.cs │ │ │ ├── RequestHeaderXForwardedPrefixTransformTests.cs │ │ │ ├── RequestHeaderXForwardedProtoTransformTests.cs │ │ │ ├── RequestHeadersAllowedTransformTests.cs │ │ │ ├── RequestHeadersTransformExtensionsTests.cs │ │ │ ├── RequestTransformTests.cs │ │ │ ├── ResponseHeaderRemoveTransformTests.cs │ │ │ ├── ResponseHeaderValueTransformTests.cs │ │ │ ├── ResponseHeadersAllowedTransformTests.cs │ │ │ ├── ResponseTrailerRemoveTransformTests.cs │ │ │ ├── ResponseTrailerValueTransformTests.cs │ │ │ ├── ResponseTrailersAllowedTransformTests.cs │ │ │ ├── ResponseTrailersTransformTests.cs │ │ │ ├── ResponseTransformExtensionsTests.cs │ │ │ ├── ResponseTransformTests.cs │ │ │ ├── TransformBuilderContextFuncExtensionsTests.cs │ │ │ └── TransformExtensionsTestsBase.cs │ │ ├── Utilities/ │ │ │ ├── ActivityCancellationTokenSourceTests.cs │ │ │ ├── AtomicCounterTests.cs │ │ │ ├── CaseInsensitiveEqualHelperTests.cs │ │ │ ├── RandomFactoryTests.cs │ │ │ └── TlsFrameHelperTests.cs │ │ ├── WebSocketsTelemetry/ │ │ │ └── WebSocketsParserTests.cs │ │ ├── Yarp.ReverseProxy.Tests.csproj │ │ └── validSelfSignedClientEkuCertificate.cer │ ├── TestCertificates/ │ │ └── testCert.pfx │ └── Tests.Common/ │ ├── TestAutoMockBase.cs │ ├── TestLogger.cs │ ├── TestLoggerProvider.cs │ ├── TestRandom.cs │ ├── TestRandomFactory.cs │ ├── TestTimeProvider.cs │ ├── XunitLoggerFactoryExtensions.cs │ ├── XunitLoggerProvider.cs │ └── Yarp.Tests.Common.csproj ├── test.cmd ├── test.sh └── testassets/ ├── BenchmarkApp/ │ ├── BenchmarkApp.csproj │ ├── Program.cs │ ├── Properties/ │ │ └── launchSettings.json │ ├── README.md │ ├── appsettings.json │ └── testCert.pfx ├── Directory.Build.props ├── ReverseProxy.Code/ │ ├── Controllers/ │ │ └── HealthController.cs │ ├── ForwarderMetricsConsumer.cs │ ├── ForwarderTelemetryConsumer.cs │ ├── MyTransformFactory.cs │ ├── MyTransformProvider.cs │ ├── Program.cs │ ├── Properties/ │ │ └── launchSettings.json │ ├── ReverseProxy.Code.csproj │ ├── TokenService.cs │ ├── appsettings.Development.json │ └── appsettings.json ├── ReverseProxy.Config/ │ ├── Controllers/ │ │ └── HealthController.cs │ ├── CustomConfigFilter.cs │ ├── Program.cs │ ├── Properties/ │ │ └── launchSettings.json │ ├── ReverseProxy.Config.csproj │ ├── appsettings.Development.json │ └── appsettings.json ├── ReverseProxy.Direct/ │ ├── Program.cs │ ├── Properties/ │ │ └── launchSettings.json │ ├── ReverseProxy.Direct.csproj │ ├── TlsFilter.cs │ ├── appsettings.Development.json │ └── appsettings.json ├── TestClient/ │ ├── CommandLineArgs.cs │ ├── Program.cs │ ├── Properties/ │ │ └── launchSettings.json │ ├── Scenarios/ │ │ ├── Http1Scenario.cs │ │ ├── Http2PostExpectContinueScenario.cs │ │ ├── Http2Scenario.cs │ │ ├── IScenario.cs │ │ ├── RawUpgradeScenario.cs │ │ ├── SessionAffinityScenario.cs │ │ └── WebSocketsScenario.cs │ └── TestClient.csproj └── TestServer/ ├── AssemblyInfo.cs ├── Controllers/ │ ├── HealthController.cs │ ├── HttpController.cs │ ├── UpgradeController.cs │ └── WebSocketsController.cs ├── Program.cs ├── Properties/ │ └── launchSettings.json ├── TestServer.csproj ├── appsettings.Development.json └── appsettings.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .azuredevops/dependabot.yml ================================================ version: 2 # Disabling dependabot on Azure DevOps as this is a mirrored repo. Updates should go through github. enable-campaigned-updates: false enable-security-updates: false ================================================ FILE: .config/CredScanSuppressions.json ================================================ { "tool": "Credential Scanner", "suppressions": [ { "_justification": "Legitimate key/cert used for testing", "file": [ "testassets/BenchmarkApp/testCert.pfx", "test/TestCertificates/testCert.pfx", "test/Kubernetes.Tests/Certificates/key.der", "test/Kubernetes.Tests/Certificates/key.pem" ] } ] } ================================================ FILE: .config/tsaoptions.json ================================================ { "areaPath": "DevDiv\\ASP.NET Core\\YARP", "codebaseName": "ReverseProxy", "instanceUrl": "https://devdiv.visualstudio.com/", "iterationPath": "DevDiv", "notificationAliases": [ "dotnetrp@microsoft.com" ], "projectName": "DEVDIV", "repositoryName": "yarp", "template": "TFSDEVDIV" } ================================================ FILE: .dockerignore ================================================ **/.classpath **/.dockerignore **/.env **/.git **/.gitignore **/.project **/.settings **/.toolstarget **/.vs **/.vscode **/*.*proj.user **/*.dbmdl **/*.jfm **/azds.yaml **/bin **/charts **/docker-compose* **/Dockerfile* **/node_modules **/npm-debug.log **/obj **/secrets.dev.yaml **/values.dev.yaml LICENSE README.md **/ingress.yaml **/ingress-controller.yaml **/backend.yaml **/ingress-sample.yaml .dotnet TestResults ================================================ FILE: .editorconfig ================================================ ; EditorConfig to support per-solution formatting. ; Use the EditorConfig VS add-in to make this work. ; http://editorconfig.org/ ; ; Here are some resources for what's supported for .NET/C# ; https://kent-boogaart.com/blog/editorconfig-reference-for-c-developers ; https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference?view=vs-2017 ; ; Be **careful** editing this because some of the rules don't support adding a severity level ; For instance if you change to `dotnet_sort_system_directives_first = true:warning` (adding `:warning`) ; then the rule will be silently ignored. ; This is the default for the codeline. root = true [*] indent_style = space charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true [*.cs] indent_size = 4 [*.{xml,config,*proj,nuspec,props,resx,targets,yml,tasks}] indent_size = 2 [*.json] indent_size = 2 [*.sh] indent_size = 4 end_of_line = lf ## The .NET Style ## Things that are commented out are available to configure but we generally don't have a preference. [*.{cs, vb}] # Organize using directives # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-formatting-conventions?view=vs-2019#organize-using-directives dotnet_sort_system_directives_first = true # dotnet_separate_import_directive_groups = false ## TODO: Swap things back to suggestion from error before merging. Just doing this to find the violations quickly and fix them. # Naming rules # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-naming-conventions?view=vs-2019 # Inspired by, but modified from, the Roslyn style: https://github.com/dotnet/roslyn/blob/75fcec13fdaa6f0f38f8ef1d7238d947df55cf5e/.editorconfig#L59 # Non-private static fields are PascalCase dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.severity = suggestion dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.symbols = non_private_static_fields dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.style = non_private_static_field_style dotnet_naming_symbols.non_private_static_fields.applicable_kinds = field dotnet_naming_symbols.non_private_static_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected dotnet_naming_symbols.non_private_static_fields.required_modifiers = static dotnet_naming_style.non_private_static_field_style.capitalization = pascal_case # Non-private readonly fields are PascalCase dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.severity = suggestion dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.symbols = non_private_readonly_fields dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.style = non_private_readonly_field_style dotnet_naming_symbols.non_private_readonly_fields.applicable_kinds = field dotnet_naming_symbols.non_private_readonly_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected dotnet_naming_symbols.non_private_readonly_fields.required_modifiers = readonly dotnet_naming_style.non_private_readonly_field_style.capitalization = pascal_case # Constants are PascalCase dotnet_naming_rule.constants_should_be_pascal_case.severity = suggestion dotnet_naming_rule.constants_should_be_pascal_case.symbols = constants dotnet_naming_rule.constants_should_be_pascal_case.style = constant_style dotnet_naming_symbols.constants.applicable_kinds = field, local dotnet_naming_symbols.constants.required_modifiers = const dotnet_naming_style.constant_style.capitalization = pascal_case # Instance fields are camelCase and start with _ dotnet_naming_rule.instance_fields_should_be_camel_case.severity = suggestion dotnet_naming_rule.instance_fields_should_be_camel_case.symbols = instance_fields dotnet_naming_rule.instance_fields_should_be_camel_case.style = instance_field_style dotnet_naming_symbols.instance_fields.applicable_kinds = field dotnet_naming_style.instance_field_style.capitalization = camel_case dotnet_naming_style.instance_field_style.required_prefix = _ # Locals and parameters are camelCase dotnet_naming_rule.locals_should_be_camel_case.severity = suggestion dotnet_naming_rule.locals_should_be_camel_case.symbols = locals_and_parameters dotnet_naming_rule.locals_should_be_camel_case.style = camel_case_style dotnet_naming_symbols.locals_and_parameters.applicable_kinds = parameter, local dotnet_naming_style.camel_case_style.capitalization = camel_case # Local functions are PascalCase dotnet_naming_rule.local_functions_should_be_pascal_case.severity = suggestion dotnet_naming_rule.local_functions_should_be_pascal_case.symbols = local_functions dotnet_naming_rule.local_functions_should_be_pascal_case.style = local_function_style dotnet_naming_symbols.local_functions.applicable_kinds = local_function dotnet_naming_style.local_function_style.capitalization = pascal_case # By default, name items with PascalCase dotnet_naming_rule.members_should_be_pascal_case.severity = suggestion dotnet_naming_rule.members_should_be_pascal_case.symbols = all_members dotnet_naming_rule.members_should_be_pascal_case.style = pascal_case_style dotnet_naming_symbols.all_members.applicable_kinds = * dotnet_naming_style.pascal_case_style.capitalization = pascal_case # Don't use "this." anywhere. # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-language-conventions?view=vs-2019#this-and-me dotnet_style_qualification_for_field = false:error dotnet_style_qualification_for_property = false:error dotnet_style_qualification_for_method = false:error dotnet_style_qualification_for_event = false:error # Use 'string' instead of 'String', 'int' instead of 'Int32', etc. # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-language-conventions?view=vs-2019#language-keywords dotnet_style_predefined_type_for_locals_parameters_members = true:error dotnet_style_predefined_type_for_member_access = true:error # Explicitly specify modifiers and always mark fields that can be 'readonly' # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-language-conventions?view=vs-2019#normalize-modifiers dotnet_style_require_accessibility_modifiers = always:error dotnet_style_readonly_field = true:error # We generally don't have rules about parentheses. # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-language-conventions?view=vs-2019#parentheses-preferences # dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent # dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent # dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent # dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent # Expression preferences # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-language-conventions?view=vs-2019#expression-level-preferences dotnet_style_prefer_auto_properties = true:error # dotnet_style_object_initializer = true:suggestion # dotnet_style_collection_initializer = true:suggestion # dotnet_style_explicit_tuple_names = true:suggestion # dotnet_style_prefer_inferred_tuple_names = true:suggestion # dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion # dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion # dotnet_style_prefer_conditional_expression_over_return = true:suggestion # dotnet_style_prefer_compound_assignment = true:suggestion # Null-checking preferences # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-language-conventions?view=vs-2019#null-checking-preferences # dotnet_style_coalesce_expression = true:suggestion # dotnet_style_null_propagation = true:suggestion # Parameter preferences # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-language-conventions?view=vs-2019#parameter-preferences dotnet_code_quality_unused_parameters = all:error ## C#-specific style [*.cs] # License header file_header_template = Licensed to the .NET Foundation under one or more agreements.\nThe .NET Foundation licenses this file to you under the MIT license. # Preferred order of modfiers. # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-language-conventions?view=vs-2019#normalize-modifiers csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:error # Implicit and explicit types # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-language-conventions?view=vs-2019#implicit-and-explicit-types csharp_style_var_for_built_in_types = true:error csharp_style_var_when_type_is_apparent = true:error csharp_style_var_elsewhere = true:error # Expression-bodied members # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-language-conventions?view=vs-2019#expression-bodied-members # csharp_style_expression_bodied_methods = false:silent # csharp_style_expression_bodied_constructors = false:silent # csharp_style_expression_bodied_operators = false:silent # csharp_style_expression_bodied_properties = true:suggestion # csharp_style_expression_bodied_indexers = true:suggestion # csharp_style_expression_bodied_accessors = true:suggestion # csharp_style_expression_bodied_lambdas = true:silent # csharp_style_expression_bodied_local_functions = false:silent # Pattern matching # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-language-conventions?view=vs-2019#pattern-matching # csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion # csharp_style_pattern_matching_over_as_with_null_check = true:suggestion # Inlined variable declarations (out var i) # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-language-conventions?view=vs-2019#inlined-variable-declarations csharp_style_inlined_variable_declaration = true:error # Expression preferences ('default' vs 'default(T)') # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-language-conventions?view=vs-2019#c-expression-level-preferences csharp_prefer_simple_default_expression = true:error # Null-checking preferences # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-language-conventions?view=vs-2019#c-null-checking-preferences # csharp_style_throw_expression = true:suggestion csharp_style_conditional_delegate_call = true:error # Code block preferences # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-language-conventions?view=vs-2019#code-block-preferences csharp_prefer_braces = true:error # Unused value preferences # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-language-conventions?view=vs-2019#unused-value-preferences # This one is S P I C Y. Enabling either option *forces* you to put something on the left-side when you call a method that returns a value. # csharp_style_unused_value_expression_statement_preference = discard_variable:silent # csharp_style_unused_value_assignment_preference = discard_variable:suggestion # Index and Range preferences # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-language-conventions?view=vs-2019#index-and-range-preferences # csharp_style_prefer_index_operator = true:suggestion # csharp_style_prefer_range_operator = true:suggestion # Miscellaneous preferences # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-language-conventions?view=vs-2019#miscellaneous-preferences csharp_using_directive_placement = outside_namespace:error # csharp_style_deconstructed_variable_declaration = true:suggestion # csharp_style_pattern_local_over_anonymous_function = true:suggestion # csharp_prefer_static_local_function = true:suggestion # csharp_prefer_simple_using_statement = true:suggestion # csharp_style_prefer_switch_expression = true:suggestion csharp_style_namespace_declarations=file_scoped:suggestion # New-line options # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-formatting-conventions?view=vs-2019#new-line-options csharp_new_line_before_open_brace = methods, properties, control_blocks, types, anonymous_methods, lambdas, object_collection_array_initializers, accessors csharp_new_line_before_else = true csharp_new_line_before_catch = true csharp_new_line_before_finally = true csharp_new_line_before_members_in_object_initializers = true csharp_new_line_before_members_in_anonymous_types = true csharp_new_line_between_query_expression_clauses = true # Indentation options # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-formatting-conventions?view=vs-2019#indentation-options csharp_indent_case_contents = true csharp_indent_switch_labels = true csharp_indent_labels = one_less_than_current csharp_indent_block_contents = true csharp_indent_braces = false csharp_indent_case_contents_when_block = true # Spacing options # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-formatting-conventions?view=vs-2019#spacing-options csharp_space_after_cast = false csharp_space_after_keywords_in_control_flow_statements = true csharp_space_between_parentheses = false csharp_space_before_colon_in_inheritance_clause = true csharp_space_after_colon_in_inheritance_clause = true csharp_space_around_binary_operators = before_and_after csharp_space_between_method_declaration_parameter_list_parentheses = false csharp_space_between_method_declaration_empty_parameter_list_parentheses = false csharp_space_between_method_declaration_name_and_open_parenthesis = false csharp_space_between_method_call_parameter_list_parentheses = false csharp_space_between_method_call_empty_parameter_list_parentheses = false csharp_space_between_method_call_name_and_opening_parenthesis = false csharp_space_after_comma = true csharp_space_before_comma = false csharp_space_after_dot = false csharp_space_before_dot = false csharp_space_after_semicolon_in_for_statement = true csharp_space_before_semicolon_in_for_statement = false csharp_space_around_declaration_statements = false csharp_space_before_open_square_brackets = false csharp_space_between_empty_square_brackets = false csharp_space_between_square_brackets = false # Wrap options # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-formatting-conventions?view=vs-2019#wrap-options csharp_preserve_single_line_statements = true csharp_preserve_single_line_blocks = true ================================================ FILE: .gitattributes ================================================ ############################################################################### # Set default behavior to automatically normalize line endings. ############################################################################### * text=auto ############################################################################### # Make sh files under the build directory always have LF as line endings ############################################################################### *.sh eol=lf ############################################################################### # Set default behavior for command prompt diff. # # This is need for earlier builds of msysgit that does not have it on by # default for csharp files. # Note: This is only used by command line ############################################################################### #*.cs diff=csharp ############################################################################### # Set the merge driver for project and solution files # # Merging from the command prompt will add diff markers to the files if there # are conflicts (Merging from VS is not affected by the settings below, in VS # the diff markers are never inserted). Diff markers may cause the following # file extensions to fail to load in VS. An alternative would be to treat # these files as binary and thus will always conflict and require user # intervention with every merge. To do so, just uncomment the entries below ############################################################################### #*.sln merge=binary #*.csproj merge=binary #*.vbproj merge=binary #*.vcxproj merge=binary #*.vcproj merge=binary #*.dbproj merge=binary #*.fsproj merge=binary #*.lsproj merge=binary #*.wixproj merge=binary #*.modelproj merge=binary #*.sqlproj merge=binary #*.wwaproj merge=binary ############################################################################### # behavior for image files # # image files are treated as binary by default. ############################################################################### #*.jpg binary #*.png binary #*.gif binary ############################################################################### # diff behavior for common document formats # # Convert binary document formats to text before diffing them. This feature # is only available from the command line. Turn it on by uncommenting the # entries below. ############################################################################### #*.doc diff=astextplain #*.DOC diff=astextplain #*.docx diff=astextplain #*.DOCX diff=astextplain #*.dot diff=astextplain #*.DOT diff=astextplain #*.pdf diff=astextplain #*.PDF diff=astextplain #*.rtf diff=astextplain #*.RTF diff=astextplain ================================================ FILE: .github/CODEOWNERS ================================================ # Users referenced in this file will automatically be requested as reviewers for PRs that modify the given paths. # See https://help.github.com/articles/about-code-owners/ * @MihaZupan @benjaminpetit ================================================ FILE: .github/ISSUE_TEMPLATE/bug.md ================================================ --- name: Bug report about: Create a report about something that is not working labels: "Type: Bug" --- ### Describe the bug A clear and concise description of what the bug is. ### To Reproduce ### Further technical details - Include the version of the packages you are using - The platform (Linux/macOS/Windows) ================================================ FILE: .github/ISSUE_TEMPLATE/feedback.md ================================================ --- name: Feedback about: Tell us what you think! labels: "Type: Feedback" --- ### Some details **How many backends are in your application?** - [ ] 1-2 - [ ] 3-5 - [ ] 6-10 - [ ] 10+ **How do you host your application?** - [ ] Kubernetes - [ ] Azure App Service - [ ] Azure VMs - [ ] Other Cloud Provider (please include details below) - [ ] Other Hosting model (please include details below) ### What did you think of YARP? ================================================ FILE: .github/ISSUE_TEMPLATE/idea.md ================================================ --- name: Idea about: Ideas, feature requests, and wishes labels: "Type: Idea" --- ### What should we add or change to make your life better? ### Why is this important to you? ================================================ FILE: .github/policies/resourceManagement.yml ================================================ id: name: GitOps.PullRequestIssueManagement description: GitOps.PullRequestIssueManagement primitive owner: resource: repository disabled: false where: configuration: resourceManagementConfiguration: scheduledSearches: [] eventResponderTasks: - if: - payloadType: Pull_Request - hasLabel: label: Auto-Merge then: - enableAutoMerge: mergeMethod: Squash description: - if: - payloadType: Pull_Request - labelRemoved: label: Auto-Merge then: - disableAutoMerge description: - if: - payloadType: Pull_Request - isAction: action: Opened - isActivitySender: user: dotnet-maestro[bot] issueAuthor: False - titleContains: pattern: Update dependencies isRegex: False then: - approvePullRequest: comment: Auto-approving dependency update. description: onFailure: onSuccess: ================================================ FILE: .github/workflows/docker_build.yml ================================================ name: Dockerfiles Build on: push: branches: - main pull_request: branches: - main jobs: build: name: Build runs-on: ubuntu-latest steps: # Check out the branch - name: Checkout Code uses: actions/checkout@v2 # Setup up builder - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 # Run a build for the Dockerfiles but don't publish - name: Build Combined id: docker_combined uses: docker/build-push-action@v2 with: context: ./ file: ./samples/KubernetesIngress.Sample/Combined/Dockerfile push: false tags: microsoft/yarp-combined:latest - name: Build Ingress id: docker_ingress uses: docker/build-push-action@v2 with: context: ./ file: ./samples/KubernetesIngress.Sample/Ingress/Dockerfile push: false tags: microsoft/yarp-ingress:latest - name: Build Monitor id: docker_monitor uses: docker/build-push-action@v2 with: context: ./ file: ./samples/KubernetesIngress.Sample/Monitor/Dockerfile push: false tags: microsoft/yarp-monitor:latest - name: Build Backend id: docker_backend uses: docker/build-push-action@v2 with: context: ./ file: ./samples/KubernetesIngress.Sample/backend/Dockerfile push: false tags: microsoft/yarp-backend:latest ================================================ FILE: .github/workflows/markdownlint-problem-matcher.json ================================================ { "problemMatcher": [ { "owner": "markdownlint", "pattern": [ { "regexp": "^([^:]*):(\\d+):?(\\d+)?\\s([\\w-\\/]*)\\s(.*)$", "file": 1, "line": 2, "column": 3, "code": 4, "message": 5 } ] } ] } ================================================ FILE: .github/workflows/markdownlint.yml ================================================ name: Markdownlint permissions: contents: read # run even on changes without markdown changes, so that we can # make it in GitHub a required check for PR's on: pull_request: jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Use Node.js uses: actions/setup-node@v6 with: node-version: 'lts/*' - name: Run Markdownlint run: | echo "::add-matcher::.github/workflows/markdownlint-problem-matcher.json" npm i -g markdownlint-cli markdownlint --ignore-path .github/workflows/markdownlintignore "**/*.md" ================================================ FILE: .github/workflows/markdownlintignore ================================================ # Ignore files under eng/common (managed by dotnet/arcade) eng/common/ ================================================ FILE: .gitignore ================================================ ## Arcade specific things # Ignore the local '.dotnet' runtime directory and local '.packages' directory (only during CI builds, but still shouldn't ever be checked in) .dotnet/ .packages/ ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. ## ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore # User-specific files *.rsuser *.suo *.user *.userosscache *.sln.docstates # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs # Mono auto generated files mono_crash.* # Build results [Dd]ebug/ [Dd]ebugPublic/ [Rr]elease/ [Rr]eleases/ x64/ x86/ [Aa][Rr][Mm]/ [Aa][Rr][Mm]64/ bld/ [Bb]in/ [Oo]bj/ [Ll]og/ [Ll]ogs/ # Visual Studio 2015/2017 cache/options directory .vs/ # Uncomment if you have tasks that create the project's static files in wwwroot #wwwroot/ # Visual Studio 2017 auto generated files Generated\ Files/ # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* # NUnit *.VisualState.xml TestResult.xml nunit-*.xml # Build Results of an ATL Project [Dd]ebugPS/ [Rr]eleasePS/ dlldata.c # Benchmark Results BenchmarkDotNet.Artifacts/ # .NET Core project.lock.json project.fragment.lock.json artifacts/ # StyleCop StyleCopReport.xml # Files built by Visual Studio *_i.c *_p.c *_h.h *.ilk *.meta *.obj *.iobj *.pch *.pdb *.ipdb *.pgc *.pgd *.rsp *.sbr *.tlb *.tli *.tlh *.tmp *.tmp_proj *_wpftmp.csproj *.log *.vspscc *.vssscc .builds *.pidb *.svclog *.scc # Chutzpah Test files _Chutzpah* # Visual C++ cache files ipch/ *.aps *.ncb *.opendb *.opensdf *.sdf *.cachefile *.VC.db *.VC.VC.opendb # Visual Studio profiler *.psess *.vsp *.vspx *.sap # Visual Studio Trace Files *.e2e # TFS 2012 Local Workspace $tf/ # Guidance Automation Toolkit *.gpState # ReSharper is a .NET coding add-in _ReSharper*/ *.[Rr]e[Ss]harper *.DotSettings.user # TeamCity is a build add-in _TeamCity* # DotCover is a Code Coverage Tool *.dotCover # AxoCover is a Code Coverage Tool .axoCover/* !.axoCover/settings.json # Visual Studio code coverage results *.coverage *.coveragexml # NCrunch _NCrunch_* .*crunch*.local.xml nCrunchTemp_* # MightyMoose *.mm.* AutoTest.Net/ # Web workbench (sass) .sass-cache/ # Installshield output folder [Ee]xpress/ # DocProject is a documentation generator add-in DocProject/buildhelp/ DocProject/Help/*.HxT DocProject/Help/*.HxC DocProject/Help/*.hhc DocProject/Help/*.hhk DocProject/Help/*.hhp DocProject/Help/Html2 DocProject/Help/html # Click-Once directory publish/ # Publish Web Output *.[Pp]ublish.xml *.azurePubxml # Note: Comment the next line if you want to checkin your web deploy settings, # but database connection strings (with potential passwords) will be unencrypted *.pubxml *.publishproj # Microsoft Azure Web App publish settings. Comment the next line if you want to # checkin your Azure Web App publish settings, but sensitive information contained # in these scripts will be unencrypted PublishScripts/ # NuGet Packages *.nupkg # NuGet Symbol Packages *.snupkg # The packages folder can be ignored because of Package Restore **/[Pp]ackages/* # except build/, which is used as an MSBuild target. !**/[Pp]ackages/build/ # Uncomment if necessary however generally it will be regenerated when needed #!**/[Pp]ackages/repositories.config # NuGet v3's project.json files produces more ignorable files *.nuget.props *.nuget.targets # Microsoft Azure Build Output csx/ *.build.csdef # Microsoft Azure Emulator ecf/ rcf/ # Windows Store app package directories and files AppPackages/ BundleArtifacts/ Package.StoreAssociation.xml _pkginfo.txt *.appx *.appxbundle *.appxupload # Visual Studio cache files # files ending in .cache can be ignored *.[Cc]ache # but keep track of directories ending in .cache !?*.[Cc]ache/ # Others ClientBin/ ~$* *~ *.dbmdl *.dbproj.schemaview *.jfm *.publishsettings orleans.codegen.cs # Including strong name files can present a security risk # (https://github.com/github/gitignore/pull/2483#issue-259490424) #*.snk # Since there are multiple workflows, uncomment next line to ignore bower_components # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) #bower_components/ # RIA/Silverlight projects Generated_Code/ # Backup & report files from converting an old project file # to a newer Visual Studio version. Backup files are not needed, # because we have git ;-) _UpgradeReport_Files/ Backup*/ UpgradeLog*.XML UpgradeLog*.htm *.rptproj.bak # SQL Server files *.mdf *.ldf *.ndf # Business Intelligence projects *.rdl.data *.bim.layout *.bim_*.settings *.rptproj.rsuser *- [Bb]ackup.rdl *- [Bb]ackup ([0-9]).rdl *- [Bb]ackup ([0-9][0-9]).rdl # Microsoft Fakes FakesAssemblies/ # GhostDoc plugin setting file *.GhostDoc.xml # Node.js Tools for Visual Studio .ntvs_analysis.dat node_modules/ # Visual Studio 6 build log *.plg # Visual Studio 6 workspace options file *.opt # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) *.vbw # Visual Studio LightSwitch build output **/*.HTMLClient/GeneratedArtifacts **/*.DesktopClient/GeneratedArtifacts **/*.DesktopClient/ModelManifest.xml **/*.Server/GeneratedArtifacts **/*.Server/ModelManifest.xml _Pvt_Extensions # Paket dependency manager .paket/paket.exe paket-files/ # FAKE - F# Make .fake/ # CodeRush personal settings .cr/personal # Python Tools for Visual Studio (PTVS) __pycache__/ *.pyc # Cake - Uncomment if you are using it # tools/** # !tools/packages.config # Tabs Studio *.tss # Telerik's JustMock configuration file *.jmconfig # BizTalk build output *.btp.cs *.btm.cs *.odx.cs *.xsd.cs # OpenCover UI analysis results OpenCover/ # Azure Stream Analytics local run output ASALocalRun/ # MSBuild Binary and Structured Log *.binlog # NVidia Nsight GPU debugger configuration file *.nvuser # MFractors (Xamarin productivity tool) working folder .mfractor/ # Local History for Visual Studio .localhistory/ # BeatPulse healthcheck temp database healthchecksdb # Backup folder for Package Reference Convert tool in Visual Studio 2017 MigrationBackup/ # Ionide (cross platform F# VS Code tools) working folder .ionide/ # Rider .idea/ # nohup nohup.out ================================================ FILE: .markdownlint.json ================================================ { "default": true, "ul-indent": false, "no-trailing-spaces": false, "line-length": false, "blanks-around-headings": false, "no-duplicate-heading": { "siblings_only": true }, "no-trailing-punctuation": false, "ol-prefix": { "one_or_ordered": true }, "blanks-around-fences": false, "blanks-around-lists": false, "no-inline-html": { "allowed_elements": [ "summary", "details" ]}, "no-bare-urls": false, "single-trailing-newline": false, "emphasis-style": false, "first-line-heading": false, "no-space-in-code": false, // rule settings and options are documented in https://github.com/DavidAnson/markdownlint // feel free to disable more low value rules in here; get rule name from the error message. // the purpose of the linter is to catch significant issues like broken links. } ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Code of Conduct This project has adopted the code of conduct defined by the Contributor Covenant to clarify expected behavior in our community. For more information, see the [.NET Foundation Code of Conduct](https://dotnetfoundation.org/code-of-conduct). ================================================ FILE: CONTRIBUTING.md ================================================ # YARP Contribution Guide We're excited to accept contributions, but like all open source projects need to set some guidelines on how and what to contribute. There can be nothing more frustrating than working on a change for some time, only to have it sitting forever as a PR. The purpose of this doc is to set expectations for how best to contribute so that YARP can benefit from the communities skills and knowledge. ## General feedback and discussions? Start a discussion on the [repository issue tracker](https://github.com/dotnet/yarp/issues). ## Issues We love issues. Issues are the best place to discuss bugs, feature requests, ideas, designs etc. Issues are the way for everyone to communicate. We use issues to communicate across the team. Almost all contributions should start with an issue. If the conversations on an issue wander off from the initial topic, and new ideas or issues get introduced, then those should be split off into separate issues. That makes it much easier to triage the issue as the decision can/should apply to the main concept, and those separate issues won't be lost and will also be considered. So if in doubt create a new issue, and add links from both the new and original issue to each other. The new issue can always be closed if it turns out to be a duplicate. In particular, conversations on closed issues *won't cause an issue to be re-opened*, and are unlikely to be noticed, so please create a new issue and refer to the old one with a link. ## Bugs As long as humans write software, there will be bugs. If you find a bug, file an issue. The line between a bug and a feature request or design change are tricky. For the purposes of this section a bug is where the code doesn't do what was intended by the design of the feature - it may be because of human error, or not considering cases that occur in the real world. If you feel confident about the fix, create a Pull Request (PR). Bug PR's should be small and uncontroversial, and therefore easily integrated. ## Reporting security issues and bugs Security issues and bugs should be reported privately, via email, to the Microsoft Security Response Center (MSRC) secure@microsoft.com. You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Further information, including the MSRC PGP key, can be found in the [Security TechCenter](https://technet.microsoft.com/en-us/security/ff852094.aspx). ## Contributing code and content We accept fixes and features! Here are some resources to help you get started on how to contribute code or new content. * Look at the [Contributor documentation](/docs/) to get started on building the source code on your own. * ["Help wanted" issues](https://github.com/dotnet/yarp/labels/help%20wanted) - these issues are up for grabs. Comment on an issue if you want to create a fix. * ["Good first issue" issues](https://github.com/dotnet/yarp/labels/good%20first%20issue) - we think these are a good for newcomers. ### Identifying the scale If you would like to contribute to one of our repositories, first identify the scale of what you would like to contribute. If it is small (grammar/spelling or a bug fix) feel free to start working on a fix. If you are submitting a feature or substantial code contribution, please discuss it with the team and ensure it follows the product roadmap. You might also read these two blogs posts on contributing code: [Open Source Contribution Etiquette](http://tirania.org/blog/archive/2010/Dec-31.html) by Miguel de Icaza and [Don't "Push" Your Pull Requests](https://www.igvita.com/2011/12/19/dont-push-your-pull-requests/) by Ilya Grigorik. All code submissions will be rigorously reviewed and tested by the YARP team, and only those that meet an extremely high bar for both quality and design/roadmap appropriateness will be merged into the source. ### Roadmap Our primary focus is going to be on features and work items that are listed in the latest milestone for the next release. Every feature should have an issue, where scoping and design will be discussed. If you wish to contribute, we'd prefer to agree to a design in the issue, before submitting a PR. The last thing we want is for you to spend time working on a feature and then have the PR rejected or sit and get stale. There is a cost to accepting PRs. We really appreciate your help and contributions but it takes time for us to review your code, and the team will be responsible for maintaining it. We're happy to take a look at PRs contributing other features, but our focus will be on the work we already have planned. Those decisions aren't final though, and we change them over time as we learn new things. Feel free to file issues or comment on existing ones if you have new data to provide! ### Extensibility One of the primary goals of YARP is to be easily extensible. Each deployment situation will be different, and the features that are "in the box" may not do exactly what you need. The goal is for you to be able to insert additional modules in the pipeline, or replace a module to achieve the functionality that you need. The answer to many feature requests may be that it should be a custom module, rather than a change to the existing feature. In those cases, we are unlikely to want a PR for the module, but will be very interested in any changes to the core that enable the extensibility for you to achieve your scenario. ### Submitting a pull request You will need to sign a [Contributor License Agreement](https://cla.dotnetfoundation.org/) when submitting your pull request. To complete the Contributor License Agreement (CLA), you will need to follow the instructions provided by the CLA bot when you send the pull request. This needs to only be done once for any .NET Foundation OSS project. If you don't know what a pull request is read this article: https://help.github.com/articles/using-pull-requests. Make sure the repository can build and all tests pass. Familiarize yourself with the project workflow and our coding conventions. ### Feedback Your pull request will now go through extensive checks by the subject matter experts on our team. Please be patient; we have hundreds of pull requests across all of our repositories. Update your pull request according to feedback until it is approved by one of the YARP team members. All changes go through this process and a PR may go through multiple revisions until its accepted. This document has been through the same process, and if you look at the history there were probably multiple edits to each PR. After that, one of our team members may adjust the branch you merge into based on the expected release schedule. ## Code of conduct See [CODE-OF-CONDUCT.md](./CODE_OF_CONDUCT.md) ================================================ FILE: Directory.Build.props ================================================ © Microsoft Corporation. All rights reserved. 12.0 MIT Microsoft true true true 0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7 ================================================ FILE: Directory.Build.targets ================================================ ================================================ FILE: LICENSE.txt ================================================ The MIT License (MIT) Copyright (c) .NET Foundation and Contributors All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: NuGet.config ================================================ ================================================ FILE: README.md ================================================ ![YARP Icon](assets/icon.png) # Welcome to the YARP project YARP (which stands for "Yet Another Reverse Proxy") is a project to create a reverse proxy server. We found a bunch of internal teams at Microsoft who were either building a reverse proxy for their service or had been asking about APIs and tech for building one, so we decided to get them all together to work on a common solution, this project. YARP is a reverse proxy toolkit for building fast proxy servers in .NET using the infrastructure from ASP.NET and .NET. The key differentiator for YARP is that it's been designed to be easily customized and tweaked to match the specific needs of each deployment scenario. 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. # Getting started - See our [Getting Started](https://learn.microsoft.com/aspnet/core/fundamentals/servers/yarp/getting-started) docs. - Try our [previews](https://github.com/dotnet/yarp/releases). - Try our latest [daily build](/docs/DailyBuilds.md). - See our [support policy](/docs/roadmap.md). # Updates For regular updates, see our [releases page](https://github.com/dotnet/yarp/releases). Subscribe to release notifications on this repository to be notified of future updates (Watch -> Custom -> Releases). If you want to live on the bleeding edge, you can pickup the [daily builds](/docs/DailyBuilds.md). # Build To build the repo, you should only need to run `build.cmd` (on Windows) or `build.sh` (on Linux or macOS). The script will download the .NET SDK and build the solution. For VS on Windows, install the latest [VS 2022](https://visualstudio.microsoft.com/downloads/) release and then run the `startvs.cmd` script to launch Visual Studio using the appropriate local copy of the .NET SDK. To set up local development with Visual Studio, Visual Studio for Mac or Visual Studio Code, you need to put the local copy of the .NET SDK in your `PATH` environment variable. Our `Restore` script fetches the latest build of .NET and installs it to a `.dotnet` directory *within* this repository. We provide some scripts to set all this up for you. Just follow these steps: 1. Run the `restore.cmd`/`restore.sh` script to fetch the required .NET SDK locally (to the `.dotnet` directory within this repo) 1. "Dot-source" the `activate` script to put the local .NET SDK on the PATH 1. For PowerShell, run: `. .\activate.ps1` (note the leading `. `, it is required!) 1. For Linux/macOS/WSL, run: `. ./activate.sh` 1. For CMD, there is no supported script. You can manually add the `.dotnet` directory **within this repo** to your `PATH`. Ensure `where dotnet` shows a path within this repository! 1. Launch VS, VS for Mac, or VS Code! When you're done, you can run the `deactivate` function to undo the changes to your `PATH`. If you're having trouble building the project, or developing in Visual Studio, please file an issue to let us know and we'll help out (and fix our scripts/tools as needed)! # Testing The command to build and run all tests: `build.cmd/sh -test`. To run specific test you may use XunitMethodName property: `dotnet build /t:Test /p:XunitMethodName={FullyQualifiedNamespace}.{ClassName}.{MethodName}`. The tests can also be run from Visual Studio if launched using `startvs.cmd`. # Roadmap see [docs/roadmap.md](/docs/roadmap.md) # Reporting security issues and bugs Security issues and bugs should be reported privately, via email, to the Microsoft Security Response Center (MSRC) at `secure@microsoft.com`. You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Further information, including [the MSRC PGP key](https://www.microsoft.com/msrc/pgp-key-msrc), can be found at the [Microsoft Security Response Center](https://www.microsoft.com/msrc). # Contributing This project welcomes contributions and suggestions. Check out the [contributing](CONTRIBUTING.md) page for more info. This project has adopted the code of conduct defined by the Contributor Covenant to clarify expected behavior in our community. For more information, see the [.NET Foundation Code of Conduct](https://dotnetfoundation.org/code-of-conduct). ================================================ FILE: SECURITY.md ================================================ ## Security Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). If you believe you have found a security vulnerability in any Microsoft-owned repository that meets Microsoft's [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)) of a security vulnerability, please report it to us as described below. ## Reporting Security Issues **Please do not report security vulnerabilities through public GitHub issues.** Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc). You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) * Full paths of source file(s) related to the manifestation of the issue * The location of the affected source code (tag/branch/commit or direct URL) * Any special configuration required to reproduce the issue * Step-by-step instructions to reproduce the issue * Proof-of-concept or exploit code (if possible) * Impact of the issue, including how an attacker might exploit the issue This information will help us triage your report more quickly. If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. ## Preferred Languages We prefer all communications to be in English. ## Policy Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). ================================================ FILE: TFMs.props ================================================ net9.0 net8.0 net8.0;net9.0 ================================================ FILE: THIRD-PARTY-NOTICES.TXT ================================================ License notice for TlsFrame Parser ------------------------------- https://github.com/dotnet/runtime/blob/master/LICENSE.txt The MIT License (MIT) Copyright (c) .NET Foundation and Contributors All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: YARP.slnx ================================================ ================================================ FILE: activate.ps1 ================================================ # # This file must be used by invoking ". .\activate.ps1" from the command line. # You cannot run it directly. See https://docs.microsoft.com/powershell/module/microsoft.powershell.core/about/about_scripts#script-scope-and-dot-sourcing # # To exit from the environment this creates, execute the 'deactivate' function. # if ($MyInvocation.CommandOrigin -eq 'runspace') { Write-Host -f Red "This script cannot be invoked directly." Write-Host -f Red "To function correctly, this script file must be 'dot sourced' by calling `". $PSCommandPath`" (notice the dot at the beginning)." exit 1 } function deactivate ([switch]$init) { # reset old environment variables if (Test-Path variable:_OLD_PATH) { $env:PATH = $_OLD_PATH Remove-Item variable:_OLD_PATH } if (test-path function:_old_prompt) { Set-Item Function:prompt -Value $function:_old_prompt -ea ignore remove-item function:_old_prompt } Remove-Item env:DOTNET_ROOT -ea ignore Remove-Item env:DOTNET_MULTILEVEL_LOOKUP -ea ignore if (-not $init) { # Remove the deactivate function Remove-Item function:deactivate } } # Cleanup the environment deactivate -init $_OLD_PATH = $env:PATH # Tell dotnet where to find itself $env:DOTNET_ROOT = "$PSScriptRoot\.dotnet" ${env:DOTNET_ROOT(x86)} = "$PSScriptRoot\.dotnet\x86" # Tell dotnet not to look beyond the DOTNET_ROOT folder for more dotnet things $env:DOTNET_MULTILEVEL_LOOKUP = 0 # Put dotnet first on PATH $env:PATH = "${env:DOTNET_ROOT};${env:PATH}" # Set the shell prompt if (-not $env:DISABLE_CUSTOM_PROMPT) { $function:_old_prompt = $function:prompt function dotnet_prompt { # Add a prefix to the current prompt, but don't discard it. write-host "($( split-path $PSScriptRoot -leaf )) " -nonewline & $function:_old_prompt } Set-Item Function:prompt -Value $function:dotnet_prompt -ea ignore } Write-Host -f Magenta "Enabled the .NET Core environment. Execute 'deactivate' to exit." if (-not (Test-Path "${env:DOTNET_ROOT}\dotnet.exe")) { Write-Host -f Yellow ".NET Core has not been installed yet. Run $PSScriptRoot\restore.cmd to install it." } else { Write-Host "dotnet = ${env:DOTNET_ROOT}\dotnet.exe" } ================================================ FILE: activate.sh ================================================ # # This file must be used by invoking "source activate.sh" from the command line. # You cannot run it directly. # To exit from the environment this creates, execute the 'deactivate' function. _RED="\033[0;31m" _MAGENTA="\033[0;95m" _YELLOW="\033[0;33m" _RESET="\033[0m" # This detects if a script was sourced or invoked directly # See https://stackoverflow.com/a/28776166/2526265 sourced=0 if [ -n "$ZSH_EVAL_CONTEXT" ]; then case $ZSH_EVAL_CONTEXT in *:file) sourced=1;; esac THIS_SCRIPT="${0:-}" elif [ -n "$KSH_VERSION" ]; then [ "$(cd $(dirname -- $0) && pwd -P)/$(basename -- $0)" != "$(cd $(dirname -- ${.sh.file}) && pwd -P)/$(basename -- ${.sh.file})" ] && sourced=1 THIS_SCRIPT="${0:-}" elif [ -n "$BASH_VERSION" ]; then (return 2>/dev/null) && sourced=1 THIS_SCRIPT="$BASH_SOURCE" else # All other shells: examine $0 for known shell binary filenames # Detects `sh` and `dash`; add additional shell filenames as needed. case ${0##*/} in sh|dash) sourced=1;; esac THIS_SCRIPT="${0:-}" fi if [ $sourced -eq 0 ]; then printf "${_RED}This script cannot be invoked directly.${_RESET}\n" printf "${_RED}To function correctly, this script file must be sourced by calling \"source $0\".${_RESET}\n" exit 1 fi deactivate () { # reset old environment variables if [ ! -z "${_OLD_PATH:-}" ] ; then export PATH="$_OLD_PATH" unset _OLD_PATH fi if [ ! -z "${_OLD_PS1:-}" ] ; then export PS1="$_OLD_PS1" unset _OLD_PS1 fi # This should detect bash and zsh, which have a hash command that must # be called to get it to forget past commands. Without forgetting # past commands the $PATH changes we made may not be respected if [ -n "${BASH:-}" ] || [ -n "${ZSH_VERSION:-}" ] ; then hash -r 2>/dev/null fi unset DOTNET_ROOT unset DOTNET_MULTILEVEL_LOOKUP if [ ! "${1:-}" = "init" ] ; then # Remove the deactivate function unset -f deactivate fi } # Cleanup the environment deactivate init DIR="$( cd "$( dirname "$THIS_SCRIPT" )" && pwd )" _OLD_PATH="$PATH" # Tell dotnet where to find itself export DOTNET_ROOT="$DIR/.dotnet" # Tell dotnet not to look beyond the DOTNET_ROOT folder for more dotnet things export DOTNET_MULTILEVEL_LOOKUP=0 # Put dotnet first on PATH export PATH="$DOTNET_ROOT:$PATH" # Set the shell prompt if [ -z "${DISABLE_CUSTOM_PROMPT:-}" ] ; then _OLD_PS1="$PS1" export PS1="(`basename \"$DIR\"`) $PS1" fi # This should detect bash and zsh, which have a hash command that must # be called to get it to forget past commands. Without forgetting # past commands the $PATH changes we made may not be respected if [ -n "${BASH:-}" ] || [ -n "${ZSH_VERSION:-}" ] ; then hash -r 2>/dev/null fi printf "${_MAGENTA}Enabled the .NET Core environment. Execute 'deactivate' to exit.${_RESET}\n" if [ ! -f "$DOTNET_ROOT/dotnet" ]; then printf "${_YELLOW}.NET Core has not been installed yet. Run $DIR/restore.sh to install it.${_RESET}\n" else printf "dotnet = $DOTNET_ROOT/dotnet\n" fi ================================================ FILE: azure-pipelines-nonprod.yml ================================================ variables: # Needed for Arcade template - name: _TeamName value: AspNetCore # Needed for Microbuild template - name: TeamName value: AspNetCore - name: DOTNET_SKIP_FIRST_TIME_EXPERIENCE value: true - name: _PublishUsingPipelines value: true - name: _BuildConfig value: Release # Rely on task Arcade injects, not auto-injected build step. - template: /eng/common/templates/variables/pool-providers.yml@self - name: skipComponentGovernanceDetection value: true trigger: batch: true branches: exclude: - main - release/* - internal/release/* pr: autoCancel: false branches: include: - '*' resources: repositories: - repository: MicroBuildTemplate type: git name: 1ESPipelineTemplates/MicroBuildTemplate ref: refs/tags/release extends: template: azure-pipelines/MicroBuild.1ES.Unofficial.yml@MicroBuildTemplate parameters: featureFlags: autoBaseline: true sdl: sourceAnalysisPool: name: $(DncEngInternalBuildPool) image: 1es-windows-2022 os: windows policheck: enabled: true tsa: enabled: true codeql: tsaEnabled: true binskim: enabled: true # See https://dev.azure.com/securitytools/SecurityIntegration/_wiki/wikis/Guardian/1378/Glob-Format analyzeTargetGlob: '**\bin\Yarp.ReverseProxy\**.dll;**\bin\Yarp.Telemetry.Consumption\**.dll' preReleaseVersion: '4.3.1' stages: - stage: build displayName: Build jobs: - template: /eng/common/templates-official/jobs/jobs.yml@self parameters: enableMicrobuild: true enablePublishBuildArtifacts: true enablePublishTestResults: true enablePublishBuildAssets: true enablePublishUsingPipelines: ${{ variables._PublishUsingPipelines }} enableTelemetry: true mergeTestResults: true jobs: - job: Windows pool: name: $(DncEngInternalBuildPool) image: 1es-windows-2022 os: windows variables: - group: Publish-Build-Assets - name: _OfficialBuildArgs value: /p:DotNetSignType=$(_SignType) /p:TeamName=$(_TeamName) /p:DotNetPublishUsingPipelines=$(_PublishUsingPipelines) /p:OfficialBuildId=$(BUILD.BUILDNUMBER) - name: _SignType value: real steps: - checkout: self clean: true - script: eng\common\cibuild.cmd -configuration $(_BuildConfig) -prepareMachine $(_OfficialBuildArgs) displayName: Build and Publish - task: 1ES.PublishBuildArtifacts@1 displayName: Upload TestResults condition: always() continueOnError: true inputs: PathtoPublish: artifacts/TestResults/$(_BuildConfig)/ ArtifactName: $(Agent.Os)_$(Agent.JobName) TestResults PublishLocation: Container - task: 1ES.PublishBuildArtifacts@1 displayName: Upload package artifacts condition: and(succeeded(), eq(variables['system.pullrequest.isfork'], false), eq(variables['_BuildConfig'], 'Release')) inputs: PathtoPublish: artifacts/packages/ ArtifactName: artifacts PublishLocation: Container - template: /eng/common/templates-official/post-build/post-build.yml@self parameters: publishingInfraVersion: 3 enableSymbolValidation: false enableSourceLinkValidation: false enableSigningValidation: false enableNugetValidation: false SDLValidationParameters: enable: true continueOnError: false params: ' -SourceToolsList @("policheck","credscan") -TsaInstanceURL $(_TsaInstanceURL) -TsaProjectName $(_TsaProjectName) -TsaNotificationEmail $(_TsaNotificationEmail) -TsaCodebaseAdmin $(_TsaCodebaseAdmin) -TsaBugAreaPath $(_TsaBugAreaPath) -TsaIterationPath $(_TsaIterationPath) -TsaRepositoryName "ReverseProxy" -TsaCodebaseName "ReverseProxy" -TsaPublish $True -PoliCheckAdditionalRunConfigParams @("UserExclusionPath < $(Build.SourcesDirectory)/eng/PoliCheckExclusions.xml")' ================================================ FILE: azure-pipelines-pr.yml ================================================ # # See https://docs.microsoft.com/azure/devops/pipelines/yaml-schema for details # variables: - name: _TeamName value: AspNetCore - name: DOTNET_SKIP_FIRST_TIME_EXPERIENCE value: true - name: _PublishUsingPipelines value: true - name: _BuildConfig value: Release - template: /eng/common/templates/variables/pool-providers.yml # Rely on task Arcade injects, not auto-injected build step. - name: skipComponentGovernanceDetection value: true # Rely on explicit tasks, not auto-injected build steps. CodeQL3000 not enabled by default in any case. - name: Codeql.SkipTaskAutoInjection value: true pr: autoCancel: false branches: include: - '*' stages: - stage: build displayName: Build jobs: - template: /eng/common/templates/jobs/jobs.yml parameters: enableMicrobuild: true enablePublishBuildArtifacts: true enablePublishTestResults: true enablePublishBuildAssets: true enablePublishUsingPipelines: true enableTelemetry: true mergeTestResults: true jobs: - job: Windows pool: name: $(DncEngPublicBuildPool) demands: ImageOverride -equals windows.vs2019.amd64.open variables: - name: _OfficialBuildArgs value: '' - name: _SignType value: test steps: - checkout: self clean: true - script: eng\common\cibuild.cmd -configuration $(_BuildConfig) -prepareMachine $(_OfficialBuildArgs) displayName: Build and Publish - task: PublishBuildArtifacts@1 displayName: Upload TestResults condition: always() continueOnError: true inputs: pathtoPublish: artifacts/TestResults/$(_BuildConfig)/ artifactName: $(Agent.Os)_$(Agent.JobName) TestResults artifactType: Container parallel: true - task: PublishBuildArtifacts@1 displayName: Upload package artifacts condition: and(succeeded(), eq(variables['system.pullrequest.isfork'], false), eq(variables['_BuildConfig'], 'Release')) inputs: pathtoPublish: artifacts/packages/ artifactName: artifacts artifactType: Container parallel: true - job: Ubuntu pool: vmImage: ubuntu-latest variables: - name: _SignType value: none steps: - checkout: self clean: true - script: eng/common/cibuild.sh --configuration $(_BuildConfig) --prepareMachine displayName: Build - task: PublishBuildArtifacts@1 displayName: Upload TestResults condition: always() continueOnError: true inputs: pathtoPublish: artifacts/TestResults/$(_BuildConfig)/ artifactName: $(Agent.Os)_$(Agent.JobName) TestResults artifactType: Container parallel: true - job: macOS_latest displayName: 'macOS latest' pool: vmImage: macOS-latest variables: - name: _SignType value: none steps: - checkout: self clean: true - script: eng/common/cibuild.sh --configuration $(_BuildConfig) --prepareMachine displayName: Build - task: PublishBuildArtifacts@1 displayName: Upload TestResults condition: always() continueOnError: true inputs: pathtoPublish: artifacts/TestResults/$(_BuildConfig)/ artifactName: $(Agent.Os)_$(Agent.JobName) TestResults artifactType: Container parallel: true ================================================ FILE: azure-pipelines.yml ================================================ variables: # Needed for Arcade template - name: _TeamName value: AspNetCore # Needed for Microbuild template - name: TeamName value: AspNetCore - name: DOTNET_SKIP_FIRST_TIME_EXPERIENCE value: true - name: _PublishUsingPipelines value: true - name: _BuildConfig value: Release # Rely on task Arcade injects, not auto-injected build step. - template: /eng/common/templates/variables/pool-providers.yml@self - name: skipComponentGovernanceDetection value: true trigger: batch: true branches: include: - main - release/* - internal/release/* resources: repositories: - repository: MicroBuildTemplate type: git name: 1ESPipelineTemplates/MicroBuildTemplate ref: refs/tags/release extends: template: azure-pipelines/MicroBuild.1ES.Official.yml@MicroBuildTemplate parameters: featureFlags: autoBaseline: true sdl: sourceAnalysisPool: name: $(DncEngInternalBuildPool) image: 1es-windows-2022 os: windows policheck: enabled: true tsa: enabled: true codeql: tsaEnabled: true binskim: enabled: true # See https://dev.azure.com/securitytools/SecurityIntegration/_wiki/wikis/Guardian/1378/Glob-Format analyzeTargetGlob: '**\bin\Yarp.ReverseProxy\**.dll;**\bin\Yarp.Telemetry.Consumption\**.dll' preReleaseVersion: '4.3.1' stages: - stage: build displayName: Build jobs: - template: /eng/common/templates-official/jobs/jobs.yml@self parameters: enableMicrobuild: true enablePublishBuildArtifacts: true enablePublishTestResults: true enablePublishBuildAssets: true enablePublishUsingPipelines: ${{ variables._PublishUsingPipelines }} enableTelemetry: true mergeTestResults: true jobs: - job: Windows pool: name: $(DncEngInternalBuildPool) image: 1es-windows-2022 os: windows variables: - group: Publish-Build-Assets - name: _OfficialBuildArgs value: /p:DotNetSignType=$(_SignType) /p:TeamName=$(_TeamName) /p:DotNetPublishUsingPipelines=$(_PublishUsingPipelines) /p:OfficialBuildId=$(BUILD.BUILDNUMBER) - name: _SignType value: real steps: - checkout: self clean: true - script: eng\common\cibuild.cmd -configuration $(_BuildConfig) -prepareMachine $(_OfficialBuildArgs) displayName: Build and Publish - task: 1ES.PublishBuildArtifacts@1 displayName: Upload TestResults condition: always() continueOnError: true inputs: PathtoPublish: artifacts/TestResults/$(_BuildConfig)/ ArtifactName: $(Agent.Os)_$(Agent.JobName) TestResults PublishLocation: Container - task: 1ES.PublishBuildArtifacts@1 displayName: Upload package artifacts condition: and(succeeded(), eq(variables['system.pullrequest.isfork'], false), eq(variables['_BuildConfig'], 'Release')) inputs: PathtoPublish: artifacts/packages/ ArtifactName: artifacts PublishLocation: Container - template: /eng/common/templates-official/post-build/post-build.yml@self parameters: publishingInfraVersion: 3 enableSymbolValidation: false enableSourceLinkValidation: false enableSigningValidation: false enableNugetValidation: false SDLValidationParameters: enable: true continueOnError: false params: ' -SourceToolsList @("policheck","credscan") -TsaInstanceURL $(_TsaInstanceURL) -TsaProjectName $(_TsaProjectName) -TsaNotificationEmail $(_TsaNotificationEmail) -TsaCodebaseAdmin $(_TsaCodebaseAdmin) -TsaBugAreaPath $(_TsaBugAreaPath) -TsaIterationPath $(_TsaIterationPath) -TsaRepositoryName "ReverseProxy" -TsaCodebaseName "ReverseProxy" -TsaPublish $True -PoliCheckAdditionalRunConfigParams @("UserExclusionPath < $(Build.SourcesDirectory)/eng/PoliCheckExclusions.xml")' ================================================ FILE: build.cmd ================================================ @echo off powershell -ExecutionPolicy ByPass -NoProfile -command "& """%~dp0eng\common\Build.ps1""" -restore -build %*" ================================================ FILE: build.sh ================================================ #!/usr/bin/env bash source="${BASH_SOURCE[0]}" # resolve $SOURCE until the file is no longer a symlink while [[ -h $source ]]; do scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" source="$(readlink "$source")" # if $source was a relative symlink, we need to resolve it relative to the path where the # symlink file was located [[ $source != /* ]] && source="$scriptroot/$source" done scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" "$scriptroot/eng/common/build.sh" --build --restore $@ ================================================ FILE: docs/DailyBuilds.md ================================================ How to get daily builds of YARP =============================== Daily builds include the latest source code changes. They are not supported for production use and are subject to frequent changes, but we strive to make sure daily builds function correctly. If you want to download the latest daily build and use it in a project, then you need to: - Obtain the latest [build of the .NET Core SDK](https://github.com/dotnet/sdk#installing-the-sdk). - Add a NuGet.Config to your project directory with the following content: ```xml ``` *NOTE: This NuGet.Config should be with your application unless you want nightly packages to potentially start being restored for other apps on the machine.* Then follow the [Getting Started](https://learn.microsoft.com/aspnet/core/fundamentals/servers/yarp/getting-started) guide to set up a project and add the nuget package dependency. Note daily builds use a higher preview version than given in the docs. Some features, such as new target frameworks, may require prerelease tooling builds for Visual Studio. These are available in the [Visual Studio Preview](https://www.visualstudio.com/vs/preview/). To debug daily builds using Visual Studio ------------------------------------------ - *Enable Source Link support* in Visual Studio should be enabled. - *Enable source server support* in Visual should be enabled. - *Enable Just My Code* should be disabled - Under Symbols enable the *Microsoft Symbol Servers* setting. ================================================ FILE: docs/README.md ================================================ # Docs Folder This folder contains: * [Design Notes](designs/) - These are the design notes used to guide our development. They aren't designed to be usage guides but may help contributors in understanding why some patterns were used. * [Operations](operations/) - These are operational docs for running releases and other infrastructure related to the project. Public documentation is available at [learn.microsoft.com](https://learn.microsoft.com/aspnet/core/fundamentals/servers/yarp/getting-started). ================================================ FILE: docs/designs/README.md ================================================ # Design Meeting Notes Rough meeting notes from design meetings. ================================================ FILE: docs/designs/config.md ================================================ # Config based proxy apps > [!CAUTION] > These are archived design discussions. Information may be outdated and inaccurate. RE: https://github.com/dotnet/yarp/issues/9 Config based proxies are common and we'll need to support at least basic proxy scenarios from config. Here are some initial considerations: - Config sources and systems - Define routes based on host and/or path - List multiple back-ends per route for load balancing - A restart should not be needed to pick up config changes - You should be able to augment a route's configuration in code. Kestrel does something similar using named endpoints. ## Config systems: We have three relevant components that already have config systems: Kestrel, UrlRewrite, and ReverseProxy. - [Kestrel](https://learn.microsoft.com/aspnet/core/fundamentals/servers/kestrel/endpoints) - [UrlRewrite](https://github.com/dotnet/aspnetcore/blob/f4d81e3af2b969744a57d76d4d622036ac514a6a/src/Middleware/Rewrite/sample/UrlRewrite.xml#L1-L11) - [ReverseProxy](https://github.com/dotnet/yarp/blob/b2cf5bdddf7962a720672a75f2e93913d16dfee7/samples/IslandGateway.Sample/appsettings.json#L10-L34) Proposals: - The Kestrel config and the Proxy/Gateway config should remain adjacent, not merged. Inbound and outbound are distinct concerns. As long as both are available in the same broader config system then that's close enough. - UrlRewrite should also remain as is. It's not ideal that it's in a separate file and format from the rest of the config, but we'll wait and see if that is a long term blocker. ## Route config: The proxy has a [config mechanism](https://github.com/dotnet/yarp/blob/b2cf5bdddf7962a720672a75f2e93913d16dfee7/samples/IslandGateway.Sample/appsettings.json#L26-L32) to define routes and map those to back end groups. ```json "Routes": [ { "RouteId": "backend1/route1", "BackendId": "backend1", "Rule": "Host('localhost') && Path('/{**catchall}')" } ] ``` This maps to a [ProxyRoute](https://github.com/dotnet/yarp/blob/b2cf5bdddf7962a720672a75f2e93913d16dfee7/src/IslandGateway.Core/Abstractions/RouteDiscovery/Contract/GatewayRoute.cs) type. This basic structure is useful though the "Rule" [system](https://github.com/dotnet/yarp/blob/b2cf5bdddf7962a720672a75f2e93913d16dfee7/src/IslandGateway.Core/Service/Config/RuleParsing/RuleParser.cs) seems overly complex. Need to circle back with DavidN on this. We may be able to simplify that down to independent keys for matching Host, Path, Header, etc.. It's not clear that the additional `&&` or `||` aspects are necessary here. If we used separate properties then it would be implicitly `&&` based. To achieve `||` you'd define additional routes. This is also an area where augmenting with code defined constraints could be useful to handle the more complex scenarios. The ProxyRoute.Metadata dictionary may be able to be replaced or supplemented by giving direct access to the config node for that route. Compare to Kestrel's [EndpointConfig.ConfigSection](https://github.com/dotnet/aspnetcore/blob/f4d81e3af2b969744a57d76d4d622036ac514a6a/src/Servers/Kestrel/Core/src/Internal/ConfigurationReader.cs#L168-L175) property. That would allow for augmenting an endpoint with additional complex custom entries that the app code can reference for additional config actions. Update: The custom rule system was modified by [#24](https://github.com/dotnet/yarp/pull/24) so that the config now looks like this: ```json "Routes": [ { "RouteId": "backend1/route1", "BackendId": "backend1", "Match": { "Methods": [ "GET", "POST" ], "Host": "localhost", "Path": "/{**catchall}" } } ] ``` ## Backend configuration The proxy code defines the types [Backend](https://github.com/dotnet/yarp/blob/b2cf5bdddf7962a720672a75f2e93913d16dfee7/src/IslandGateway.Core/Abstractions/BackendDiscovery/Contract/Backend.cs) and [BackendEndpoint](https://github.com/dotnet/yarp/blob/b2cf5bdddf7962a720672a75f2e93913d16dfee7/src/IslandGateway.Core/Abstractions/BackendEndpointDiscovery/Contract/BackendEndpoint.cs) and allows these to be defined via config and referenced by name from routes. A BackendEndpoint defines a specific service instance with an id, address, and associated metadata. A Backend is a collection of one or more BackendEndpoints and a set of policies for choosing which endpoint to rout each request to (load balancing, circuit breakers, health checks, affinities, etc.). This seems a bit monolithic compared to our initial design explorations. We anticipate wanting to break these policies up into distinct steps in a pipeline to make them more replaceable. That said, we'll still need a config model for the default set of components and it may look very much like what's already here. Question: Why are the backends and the endpoints listed separately in config rather than nested? Object model links endpoints 1:1 with backends, so there doesn't seem to be a reason to list them separately. Existing: ```json "Backends": [ { "BackendId": "backend1" }, { "BackendId": "backend2" } ], "Endpoints": { "backend1": [ { "EndpointId": "backend1/endpoint1", "Address": "https://localhost:10000/" } ], "backend2": [ { "EndpointId": "backend2/endpoint1", "Address": "https://localhost:10001/" } ] }, ``` Nested: ```json "Backends": [ { "BackendId": "backend1", "Endpoints": [ { "EndpointId": "backend1/endpoint1", "Address": "https://localhost:10000/" } ], }, { "BackendId": "backend2" "Endpoints": [ { "EndpointId": "backend2/endpoint1", "Address": "https://localhost:10001/" } ], } ], ``` Additional feedback: Why is it using arrays instead of objects? These items are not order sensitive, and they already have id properties anyways. ```json "Backends": { "backend1" : { "Endpoints": [ "endpoint1": { "Address": "https://localhost:10000/" } }, }, "backend2": { "Endpoints": { "endpoint1": { "Address": "https://localhost:10001/" } }, } }, ``` Update: The backend and endpoint layout has been modified to the following: ```json "Backends": { "backend1": { "Endpoints": { "backend1/endpoint1": { "Address": "https://localhost:10000/" } } }, "backend2": { "Endpoints": { "backend2/endpoint1": { "Address": "https://localhost:10001/" } } } }, ``` ## Config reloading Config reloading is not yet a blocking requirement but we do expect to need it in the future. This design needs to factor in how reloading might work when it does get added. **NOTE** The proxy code has a concept of Signals that is used to convey config change. We need to see how this integrates with change notifications from our config sources and flows through the system. The Extensions config and options systems have support for change detection and reloading but very few components take advantage of it. Logging is the primary consumer today. One concern is that some change notification sources like files can trigger multiple times for a single event. The config system does not have built in handling for this, it's up to consumers to 'debounce' and filter out redundant notifications. Kestrel support for reloading config is tracked by https://github.com/dotnet/aspnetcore/issues/19376. Reloading proxy config will need to happen atomically and avoid disrupting requests already in flight. We may need to rebuild portions of the app pipeline and swap them out for new requests, drain the old requests, and clean up the old pipelines. We also want to avoid a full reset for small config changes where possible. E.g. if only one route changes then ideally we'd only rebuild that route. Reloading should be something you can opt into or out of. Right now this is only possible at the config level by opting in or out for a config source, but that affects the whole app. Updates: Config reload for proxy routes, backends, and endpoints already works. You edit appsettings.json and it automatically reloads and reconfigures the routes. Note the config change notification usually gets fired twice and logs "Applying proxy configs" each time, but the change diff logic prevents an unnecessary update the second time. We may still want to do some debounce detection to prevent extra config diffs, but that's lower priority. @halter73 raised the question of how much effort we put in to make the kestrel reload atomic with the routing reload? Conceptually it makes sense to keep the two in sync, but programmatically it's quite difficult as there's no connection between the two systems. They'd be reacting to the same change notification event in serial and requests in flight may see one set of changes without the other. We discussed this in the weekly sync and decided that since kestrel endpoint changes will be a less common scenario we won't initually worry about the atomicity here until we have customer feedback that demonstrates issues. Also, when a kestrel endpoint is modified or removed should existing connections on that endpoint be eagerly drained and closed, or should they be allowed to run a normal lifecycle? Kestrel does not currently track active connection per endpoint so additional tracking would be needed if we wanted to shut them down. Updates: The config reload code has been moved from the sample into the product assemblies. ## Augmenting config via code Some things are easier to do in code and we want to be able to support that while still pulling more transient data from config. [Kestrel](https://github.com/dotnet/aspnetcore/blob/aff01ebd7f82d641a4cfbd4a34954300311d9c2b/src/Servers/Kestrel/samples/SampleApp/Startup.cs#L138-L147) has a model where endpoints are named in config and then can be reference by name in code for additional configuration. ```json { "Kestrel": { "Endpoints": { "NamedEndpoint": { "Url": "http://*:6000" }, "NamedHttpsEndpoint": { "Url": "https://*:6443", } } } } ``` ```csharp options.Endpoint("NamedEndpoint", opt => { }) .Endpoint("NamedHttpsEndpoint", opt => { opt.HttpsOptions.SslProtocols = SslProtocols.Tls12; }); ``` The proxy code already has named routes, backends, backend endpoints, etc., so we should be able to build a similar code augmentation for those. Reloadable config complicates this pattern. The code augmentation actions will need to be captured for the lifetime of the app rather than just for startup so they can be re-run later. ================================================ FILE: docs/designs/route-extensibility.md ================================================ ## Problem Statement > [!CAUTION] > These are archived design discussions. Information may be outdated and inaccurate. Today if you want to extend the route or clusters, you can only do it through the metadata property on each, which is a Dictionary. If you want to be able to have structured data its not possible without you forcing it into a string and then parsing it when needed. There are scenarios like A/B testing, or authenticating with back end servers (not pass thru) where you want to be able to store a structure of data in config, and have it available at runtime on the route/cluster objects. If we want there to be pre-built extensions to YARP (#1714), then there needs to be a way for each of the extensions to have its own config data on routes and clusters, and for them to not step on each others toes. ## Why is this important to you? Taking a canonical example of A/B testing. In this scenario you want to be able to direct traffic to multiple clusters with the traffic patterns determined based on additional criteria. For example, You may want to be able to have a collection of clusters that are used for a route, together with percentages. So the config could look something like: ```json { "ReverseProxy": { "Routes": { "route1": { "ClusterId": "ignore", "Match": { "Path": "{**catch-all}" }, "Extensions": { "A-B": [ { "ClusterId": "c1", "Load": 0.4 }, { "ClusterId": "c2", "Load": 0.6 }, { "ClusterId": "experimental", "Load": 0.01 }, { "ClusterId": "TelemetrySample", "Load": 0.01 } ] } }, ``` The above example is adding an "A-B" extension to the route with its own data. There could be other extensions each of which have their own configuration data. ## What would this look like ### Requirements * Multiple extensions can be added to YARP, each of which can have their own config state. * Be able to have arbitrary data as part of config for routes and/or clusters * YARP should not dictate a specific data structure for the extension config * Be able to access that data as objects from middleware directly off the route and clusters * Be able to remote that data in the case of a distributed configuration server ## Proposal * Add an Extensions collection to Route and Cluster. This should follow the same pattern as http features, using an `IReadOnlyDictionary` and be accessed based on the type of the extension. This would then be accessible in proxy middleware from the route or cluster objects via an `Extensions` property: ```c# public void Configure(IApplicationBuilder app, IProxyStateLookup lookup) { app.UseRouting(); app.UseEndpoints(endpoints => { // We can customize the proxy pipeline and add/remove/replace steps endpoints.MapReverseProxy(proxyPipeline => { // Use a custom proxy middleware, defined below proxyPipeline.Use((context, next) => { var proxyFeature = context.Features.Get(); var abstate = proxyFeature.Route.Extensions[typeof(ABState)]; var newClusterName = abstate.SelectSlice(new Random().NextDouble()); if (lookup?.TryGetCluster(newClusterName, out var cluster)) { context.ReassignProxyRequest(cluster); } return next(); }); proxyPipeline.UseSessionAffinity(); proxyPipeline.UseLoadBalancing(); }); }); } ``` Using a dictionary based on object type enables easy access to the object at runtime, and a strongly typed result. * There needs to be a way for YARP to construct these strongly typed extensions based on the `IConfiguration` provider. There needs to be a mapping between the key in config, and the type that will be used to store the data. That mapping is handled by enabling factories that can be registered: ```c# services.AddReverseProxy() .LoadFromConfig(Configuration.GetSection("ReverseProxy")) .AddRouteExtension("A-B", (section, _, _) => new ABState(section)); ``` > Along with a similar mechanism for clusters. The extension registration would be something like: ```c# static IReverseProxyBuilder AddRouteExtension(this IReverseProxyBuilder builder, string sectionName, Func factory) ``` > Where the factory is passed: * The IConfigurationSection for the extension * The route object that is being extended * The existing extension instance in the case of configuration updates > > When the configuration is parsed, the factory is called based on the configuration key name, and the resultant object is added to the route/cluster objects. > > Regular YARP config will do a diff merge to handle config changes, and create new objects if applicable. The same mechanism needs to be used for extensions. If the configuration is updated, and an existing instance of the extension exists, then it will be passed to the factory. The factory can compare the current instance and re-use it, or copy its data across to a new instance based on the changes. Instances must stable, so existing instances shouldn't be modified if it would affect existing in-flight requests. YARP can't really enforce rules on how the objects are changed as we want the types to be user defined. * When using a custom config provider, the Extensions collection can be populated by the custom provider directly, or the provider can expose an `IConfigurationSection` implementation and use the factory as described below. * When YARP is integrated with 3rd party config systems, such as K8s or ServiceFabric, those systems typically have a way of expressing custom properties, some of which will be used by YARP for the definition of routes/ clusters etc. To facilitate the ability for route and cluster extensions to be expressed within those systems, the integration provider should expose an `IConfigurationSection` implementation that maps between the integrated persistence format and YARP. `IConfigurationSection` is essentially a name/value lookup API, it should map pretty reasonably to the YAML or JSON formats used by the configuration systems, and not be an undue burden to implement on these integrations. * Integration with 3rd party config systems can involve a remote process that YARP can pull its configuration from. I am [proposing another feature](https://github.com/dotnet/yarp/issues/1710), that we formalize this pattern and have the ability to create a central YARP config provider, to which multiple YARP proxies can bind. This enables scalability in terms of being able to push config to multiple instances of YARP at once. * To support this scenario, we should serialize the IConfiguration data and pass that across to the proxy instances. ================================================ FILE: docs/designs/yarp-tunneling.md ================================================ # YARP Tunneling > [!CAUTION] > These are archived design discussions. Information may be outdated and inaccurate. ## Introduction While many organizations are moving their computing to the cloud, there are occasions where you need to be able to run some services in a local datacenter. The problem then is if you need to be able to communicate with those services from the cloud. Creating a VPN network connection to Azure or other cloud provider is possible, but usually involves a lot of red tape and configuration complexity as the two networks need to be integrated. If all that the cloud needs to access is resources that are exposed over http, then a simpler solution is to have a gateway that can route traffic to the remote resource. Additionally, outbound connections over http(s) are not usually blocked, so having a on-prem gateway make an outbound connection to the cloud, is the easiest way to establish the route. This is the basis behind the [Azure Relay](https://learn.microsoft.com/azure/azure-relay/relay-what-is-it) service offering. That is the principle of the tunnel feature for YARP. You operate two instances of the YARP proxy service, configured as a tunnel. The advantage over Azure Relay is that using a reverse proxy as an on-prem gateway means that both cloud and back end services can be used without needing to update the application other than addresses. This is particularly useful for services that may have been written by a 3rd party, or are no longer under active development, and so making changes to the configuration is complicated and expensive. Relay requires the sender and receiver to be updated to use its connection protocol. ![Tunnel diagram](https://github.com/assets/95136/52d7491b-6e8a-4a2c-a51d-0734b3e41930) In the on-prem data center, you run an instance of YARP, we'll call this the back-end proxy. This is configured with routes to the resources that should be externally accessible - only routes that are configured via this proxy will be exposed. The back-end proxy is configured to create a tunnel connection to the front-end instance by specifying the connection URL and security details for the connection. The instance in the cloud, we'll refer to as the front-end, will be configured with a tunnel endpoint URL to be used by the on-prem proxy. The on-prem proxy will create a websocket connection to the tunnel endpoint, this will map the tunnel to a specific cluster. Routes can be directed to use the tunnel connection to the back-end by using the cluster that is used for the tunnel. ## Tunnel Protocol The tunnel will establish a Websockets connection between the back-end and the front-end. The back-end will establish the connection so that it can more easily break through firewalls. Once the WSS connection is created, it will be treated as a stream over which HTTP/2 traffic will be routed. HTTP/2 is used so that multiple simultaneous requests can be multiplexed over a single connection. The HTTP/2 protocol is only used between the two proxies, the connections either side can be any protocol that the proxy supports. This means we don't put any specific capability requirement on the destination servers. If the tunnel connection is broken, the back-end will attempt to reconnect to the front-end: - If the connection fails, then it will continue to reconnect every 30s until the connection is re-established. - If the connection is refused with a 500 series error, then it will be retried at the next 30s timeout. - If the connection is refused with a 400 series error then further connections for that tunnel will not be made. > Issue: Do we need an API for the tunnel? As its created from code on the back-end, the app could have additional logic for control over the duration. Does it have API for status, clean shutdown, etc. > > Issue: Will additional connections be created for scalability - H2 perf becomes limited after 100 simultaneous requests. How does the front-end know to pair a second back-end connection? The Front End should keep the WSS connection alive by sending pings every 30s if there is no other traffic. This should be done at the WSS layer. ## Moving pieces | Location | Name | Description | | --- | --- | --- | | front-end | EndPoint | The endpoint that the back-end proxy will connect to to create a tunnel. | | front-end | Cluster | The cluster that will direct to back-end proxy(ies) that have created tunnels. | | front-end | Routes | Routes need to be configured to route specific URLs to the tunnel, by using clusters that are a tunnel. | | back-end | Tunnel URL(s) | The URL(s) for the front-end endpoint that can be used to establish the tunnel. | | back-end | Routes | The back-end needs to have routes defined that will direct traffic to local resources. | ## front-end The front-end is the proxy that will be called by clients to be able to access resources via the back-end proxy. It will route traffic over a tunnel created using a WSS connection from the back-end proxy. YARP needs a mechanism to know which requests will be routed via the tunnel. This will be achieved by extending the existing cluster concept in YARP - The request to create a tunnel will specify the name of a cluster. Once the tunnel is established, it will be treated as a dynmamically created destination for the named cluster. Routes will not need to be changed, they will point at the cluster, and the tunnels will be used in the same way as destinations. Tunnel services must be enabled by the proxy server: ``` C# builder.Services.AddReverseProxy() .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy")); builder.Services.AddTunnelServices(); ``` The front-end needs to have a tunnel endpoint that the back-end will connect to. The endpoint should be parameterized to include the name of the cluster as part of the URL, and a callback that is used to validate the connection is approved: - Including the ClusterId in the URL enables the same endpoint mechanism to be used for multiple clusters. - Using a callback for authentication enables whatever scheme the proxy author(s) wish to use. - Trying to encode specific auth schemes will invariably miss a scenario that is needed. - The samples that we produce should be based around client certs as it is a good way to manage secure shared secrets. ``` C# app.MapReversProxy(); app.MapTunnel("/tunnel/{ClusterId}", async (connectionContext, cluster) => { // Use the extensions feature https://github.com/dotnet/yarp/issues/1709 to add auth data for the tunnel var tunnelAuth = cluster.Extensions[typeof(TunnelAuth)]; if (!context.Connection.ClientCertificate.Verify()) return false; foreach (var c in tunnelAuth.Certs) { if (c.ThumbPrint == context.Connection.ClientCertificate.Thumbprint) return true; } return false; }); ``` The front-end should have configuration for routes that direct to a cluster that is for the tunnel. The cluster must be marked as IsTunnel to enable tunnel capability, and must *not* include other destinations. The cluster's destinations will be supplied dynamically by back-ends creating tunnel connections. In the following case it uses the Extensions feature to enable storing thumbprints for client certs that are used to authenticate tunnel connections. The Route will direct all traffic under the path `/OnPrem/*` to the tunnel. ``` json { "ReverseProxy": { "Routes" : { "OnPrem" : { "Match" : { "Path" : "/OnPrem/{**any}" }, "ClusterId" : "MyTunnel1" } }, "Clusters" : { "MyTunnel1" : { "IsTunnel" : true, "Extensions" : { "TunnelAuth" : { "Certs" : { "name1" : "thumbprint1", "name2" : "thumbprint2" } } } } } } } ``` To ensure scalability, multiple back-end proxy instances should be able to create tunnel connections for the same cluster. When that happens, the load balancing rules for the cluster should apply, and balance between the active tunnels. Similarly multiple front-ends can be used to reduce the problems with a single point of failure. ## back-end The back-end instance is the proxy that will reside on the same network as the resources that should be exposed. The back-end will need to be able to connect to those resources, and also be able to create a WebSocket connection to the front-end proxy server(s) via whatever firewalls are between them. The back-end proxy is configured with routes and destinations that it wishes to expose to the front end. Security is maintained because only URLs matching its routes will be proxyable via it. This prevents attacks at the front-end having arbitrary access to other resources on the back-end network - they need to be explicitly included in the back-end route table. The outbound connection to the front end needs to be explicitly made for each tunnel that the back-end wishes to create. ``` C# builder.Services.AddReverseProxy() .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy")); var url = builder.Configuration["Tunnel:Url"]!; // Eg https://Myfront-end.MyCorp.com/tunnel/MyTunnel1 // Setup additional details for the connection, auth and headers var tunnelOptions = new TunnelOptions(){ TunnelClient = new SocketsHttpHandler(), ClientCertificates = new X509CertificateCollection { cert } AuthCallback = AuthServer; }; tunnelOptions.Headers.Add("MyJWTToken", tokenString); builder.WebHost.UseTunnelTransport(url, tunnelOptions); ``` The tunnel creation takes an options class that enables the HttpHandler, headers to be set for the tunnel creation to enable authentication flexibility by the app developers. It also should include a callback to enable validation of the server certificate. The back-end configuration would use the standard routes and cluster definitions. ``` json { "ReverseProxy": { "Routes": { "CNCMilling": { "Match": { "Path": "/OnPrem/CNCMilling/{**any}" }, "ClusterId": "Milling" } "3DPrinting" : { "Match": { "Path": "/OnPrem/Extrusion/{**any}" }, "ClusterId": "3dPrinting" } }, "Clusters": { "Milling": { "Destinations": { "Bay12" : "https://bay12-efd432/", "Bay15" : "https://bay15-j377d3/" } } "3dPrinting": { "Destinations": { "Bay41-controller" : "https://bay41-controller/" } } } } } ``` In the above case, requests using the paths `/OnPrem/CNCMilling/*` and `/OnPrem/Extrusion/*` will be routed by the back-end to their respective services - other paths would result in an error. Note: Active health checks probably don't make sense to be performed by the front-end against the back-end. Passive health checks will verify the overall condition of the tunnel. ## Scalability In a large deployment, there needs to be the ability to have multiple front-end and back-end proxies: - If the front-end receives multiple tunnel connections, then it should treat them as if the cluster has multiple destinations. The cluster can use the load balancing policy to select how it decides to route traffic to the back-end proxies. > Note: The front-end proxy will not be aware of the actual destinations that serve resources - each back-end should have its own cluster definition for the actual destinations, and so can include multiple servers for any route/cluster combination. - A back-end proxy should be able to create tunnels to multiple front-ends. The tunnels can be to related front-end proxies that are sharing the same load, or to front-ends in different cloud deployments. This enables the Front Ends to be very specific to particular deployments - and have constrained v-Lan configurations in the cloud. ## Authentication The authentication options for ASP.NET are diverse, and IT departments will likely have their own conditions on what is required to be able to secure a tunnel. So rather than trying to implement the combinatorial matrix of what customers could need, we should use a callback so that the proxy author can decide. Samples should be created that show best practices using a secure mechanism such as client a certificate. *Issue:* Does the back-end need additional mechanisms to validate the connection to the front-end, or is TLS/SNI sufficient? ## Security The purpose of the tunnel is to simplify service exposure by creating a tunnel through the firewall that enables external requests to be made to destination servers on the back-end network. There are a number of mitigations that reduces the risk of this feature: - No endpoints are exposed via the firewall - it does not expose any new endpoints that could act as attack vectors. The tunnel is an outbound connection made between the back-end and the front-end. - Traffic directed via the tunnel will need to have corresponding routes in the Back End configuration. Traffic will only be routed if there is a respective route and cluster configuration. Tunnel traffic can't specify arbitrary URLs that would be directed to a hostname not included in the back-end route table configuration. - Tunnel connections should only be over HTTPs ## Metrics ? What telemetry and events are needed for this? ## Error conditions | Condition | Description | | --- | --- | | No tunnel has connected | If the front end receives a request for a route that is backed by a tunnel and no tunnels have been created, then it should respond to those requests with a 502 "Bad Gateway" error | ## Web Transport Web Transport is an interesting future protocol choice for the tunnel. ================================================ FILE: docs/operations/BackportingToPreview.md ================================================ # 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/dotnet/yarp/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/dotnet/yarp/blob/main/docs/operations/Release.md#release-the-build) from the preview branch - Create a new git tag for the released commit **While still on the preview branch:** - `git tag v1.0.0-previewX.build.d` - `git push upstream --tags` - Create a new [release](https://github.com/dotnet/yarp/releases). ## Internal fixes Issues with significant security or disclosure concerns need to be fixed privately first. All of this work will happen on the internal Azdo repo and be merged to the public github repo at the time of disclosure. - Make a separate clone of https://dev.azure.com/dnceng/internal/_git/dotnet-yarp to avoid accidentally pushing to the public repo. - Create a branch named `internal/release/{version being patched}` starting from the tagged commit of the prior release. - Update versioning as needed. - Create a feature branch, fix the issue, and send a PR using Azdo. - Release the build from the internal branch. - Tag the commit and push it to the public repo. Do not push to regular `main` or `release/*` branches in the internal mirror. - Cherry pick the changes to public main as needed. - Finish the standard release checklist. ================================================ FILE: docs/operations/Branching.md ================================================ # Branching Tasks 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` 1. Run `git checkout -b release/1.1.0-previewX` where `X` is the YARP preview number. When releasing a non-preview version, use `release/1.1` instead of `release/1.1.0` so that the branch can be used for future patches. 1. If you are releasing a non-preview version: - Set the `PreReleaseVersionLabel` in [`eng/Versions.props`] to `rtw`. - Run `build.cmd -pack` - Verify that the packages in `artifacts/packages/Debug/Shipping` do not have a suffix after the intended version. For example the name should be `Yarp.ReverseProxy.1.1.0.nupkg` and not `Yarp.ReverseProxy.1.1.0-dev.nupkg`. 1. Run `git push origin release/1.1.0-previewX` to push the branch to the server. Update branding in `main`: 1. Edit the file [`eng/Versions.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 8.0 runtime versions from [the .NET 8.0 download page](https://dotnet.microsoft.com/download/dotnet/8.0), and 9.0 from [the .NET 9.0 download page](https://dotnet.microsoft.com/download/dotnet/9.0). [`eng/Versions.props`]: ../../eng/Versions.props ================================================ FILE: docs/operations/DependencyFlow.md ================================================ # Dependency Flow ## Dependency Flow Overview *For full documentation on Arcade, Maestro and `darc`, see [the Arcade documentation](https://github.com/dotnet/arcade/tree/main/Documentation)* We use the .NET Engineering System ([Arcade](https://github.com/dotnet/arcade)) to build this repo. 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. 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. 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 (3796) https://github.com/dotnet/aspnetcore @ release/6.0 -> .NET 6 (5027) https://github.com/dotnet/aspnetcore @ release/8.0 -> .NET 8 (5731) https://github.com/dotnet/aspnetcore @ release/9.0 -> .NET 9 (5732) https://github.com/dotnet/aspnetcore @ main -> .NET 10 (6050) https://github.com/dotnet/aspnetcore @ release/10.0-preview1 -> .NET 10 Preview 1 ``` 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 `dotnet/yarp` is subscribed to: ```shell > darc get-subscriptions --target-repo dotnet/yarp https://github.com/dotnet/arcade (.NET Eng - Latest) ==> 'https://github.com/dotnet/yarp' ('main') - Id: 1751e896-c0f1-4247-3909-08d8c8762e9e - Update Frequency: EveryWeek - Enabled: True - Batchable: False - PR Failure Notification tags: - Source-enabled: False - Merge Policies: Standard https://github.com/dotnet/arcade (.NET Eng - Latest) ==> 'https://github.com/dotnet/yarp' ('release/2.2') - Id: ebd75d9f-8988-4f50-bd1d-83dfc79fb7ba - Update Frequency: EveryWeek - Enabled: True - Batchable: False - PR Failure Notification tags: - Source-enabled: 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: ```yaml 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: ```yaml Channel: .NET Eng - Latest Source Repository URL: https://github.com/dotnet/arcade Target Repository URL: https://github.com/dotnet/yarp Target Branch: release/42 Update Frequency: EveryWeek 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, including having run `darc authenticate`. ## When we are ready to branch Follow the initial branching steps from [Branching.md](Branching.md). YARP uses darc to consume up-to-date Arcade bits every week. Set up dependency flow for the new branch: 1. Run `darc add-subscription` 2. Fill in the template that opens in your editor as follows: * `Channel` = `.NET Eng - Latest` * `Source Repository URL` = `https://github.com/dotnet/arcade` * `Target Repository URL` = `https://github.com/dotnet/yarp` * `Target Branch` = `release/X` (where `X` is the YARP release version) * `Update Frequency` = `EveryWeek` * `Merge Policies` is a multiline value, it should look like this: ```yaml Merge Policies: - Name: Standard Properties: {} ``` 3. Save and close the editor window. ================================================ FILE: docs/operations/README.md ================================================ # Operations Playbook *This documentation is primarily for project maintainers, though contributors are welcome to read and learn about our process!* This section has docs on various operations and tasks to be performed in the repo. * [Branching](Branching.md) * [Releasing](Release.md) * [Backporting changes onto a preview branch](BackportingToPreview.md) * [Dependency flow](DependencyFlow.md) ================================================ FILE: docs/operations/Release.md ================================================ # Releasing YARP 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/dotnet/yarp/issues/new?title=Preview%20X%20release%20checklist&body=See%20%5BRelease.md%5D%28https%3A%2F%2Fgithub.com%2Fdotnet%2Fyarp%2Fblob%2Fmain%2Fdocs%2Foperations%2FRelease.md%29%20for%20detailed%20instructions.%0A%0A-%20%5B%20%5D%20Ensure%20there%27s%20a%20release%20branch%20created%20%28see%20%5BBranching%5D%28https%3A%2F%2Fgithub.com%2Fdotnet%2Fyarp%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%60dotnet-yarp-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%20release%20notes%0A-%20%5B%20%5D%20Close%20the%20%5Bold%20milestone%5D%28https%3A%2F%2Fgithub.com%2Fdotnet%2Fyarp%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%2Fdotnet%2Fyarp%2Fbranches%29%0A-%20%5B%20%5D%20Request%20source%20code%20archival). ## Versioning Ensure the eng/Versions.props file has the expected versions and pre-release labels. For a final release set PreReleaseVersionLabel to `rtw`. ## Ensure there's a release branch created. 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 First, identify the final build of the [`dotnet-yarp-official` Azure Pipeline](https://dev.azure.com/dnceng/internal/_build?definitionId=809&_a=summary) (on dnceng/internal). The final build will be the latest successful build **in the relevant `release/x` branch**. Use the "Branches" tab on Azure DevOps to help identify it. If the branch hasn't been mirrored yet (see [`dotnet-mirror-dnceng` pipeline](https://dev.azure.com/dnceng/internal/_build?definitionId=1387)) and there are no outstanding changesets in the branch, the build of the corresponding commit from the main branch can be used. 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: ![image](https://github.com/user-attachments/assets/27ddf12d-f4b7-4faa-862e-d2d1d6eafea9) The packages can be accessed from the `PackageArtifacts` artifact: ![image](https://github.com/user-attachments/assets/264b8c6d-8108-4536-a61b-421aa652df73) ### Consume .nupkg - Visual Studio: Place it in a local folder and add that folder as a nuget feed in Visual Studio. - Command Line: `dotnet nuget add source -n local` Walk through the [Getting Started](https://learn.microsoft.com/aspnet/core/fundamentals/servers/yarp/getting-started) instructions and update them in the release branch as needed. Also validate any major new scenarios this release and their associated docs. ## Release the build Once validation has been completed, it's time to release. Go to the [`dotnet-yarp-release` pipeline](https://dev.azure.com/dnceng/internal/_build?definitionId=1448) and select "Run Pipeline". Under "Resources", select the pipeline run that you've validated artifacts for. ![image](https://github.com/user-attachments/assets/efa38319-2620-4deb-aca3-d2bf23c991cc) ![image](https://github.com/user-attachments/assets/e25419e6-498d-4cc2-bf98-b5b2bc77c251) ![image](https://github.com/user-attachments/assets/e2f7f965-b4f7-40dc-ba2f-6a91062271d5) Triple-check the version numbers of the packages in the artifacts against whatever validation was done at this point. ![image](https://github.com/user-attachments/assets/187d65d9-3c0f-4418-ab13-a77e0ad1b8e9) Select "Run". Unless you're a release approver, you're done here! ## Approve the release The Azure Pipeline will send an email to all the release approvers asking one of them to approve the release. ![image](https://github.com/user-attachments/assets/e772e31c-8aea-4bca-a21f-8bd62f61365f) Click "Review Manual Validation", or navigate to the release pipeline directly in Azure DevOps. You'll see that the stage is "Pending Approval" Enter a comment such as "release for preview X" approve finalize the release. **After approving, packages will be published automatically**. It *is* possible to cancel the pipeline, but it might be too late. See "Troubleshooting" below. The packages will be pushed and when the "NuGet.org" stage turns green, the packages are published! **Note:** NuGet publishing is quick, but there is a background indexing process that can mean it will take up to several hours for packages to become available ## 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. `git tag v1.0.0-previewX` Push the tag change to the upstream repo (**not your fork**) `git push upstream v1.0.0-previewX` ## Draft release notes Create a draft release at https://github.com/dotnet/yarp/releases using the new tag. See prior releases for the recommended content and format. ## Publish the release notes Publish the draft release notes. These should be referencing the latest docs, packages, etc.. ## Close the old milestone It should be empty now. If it's not, move the outstanding issues to the next one. ## Announce on social media David Fowler has a lot of twitter followers interested in YARP. Tweet a link to the release notes and let him retweet it. ## Set the preview branch to protected This is to avoid accidental pushes to/deletions of the preview branch. ## Delete the previous preview branch There should only be one [preview branch on the repo](https://github.com/dotnet/yarp/branches) after this point. ## Request source code archival 1. Go to the internal https://dpsopsrequestforms.azurewebsites.net portal 2. Select "Source Code Archival" 3. Proceed through steps 1,2 till the step 3 (all prerequisites are already fulfilled) 4. Fill in the required fields on the steps 3, 4, 5. Please, see the recommended values below. 5. At the last step, check all the info and and submit the request 6. Go to "My Request" tab and wait for a new ticket to appear on the list 7. Copy the ticket link from "Ticket ID" column which will look like `https://prod.******` 8. Replace the `prod` word to `portal` 9. Navigate to the fixed link and check that the ticket is actually created 10. That's all the actions needed to be done immediately. Afterwards, periodically track the ticket progress. It might take many hours. 11. [Offline] Wait for the archival completion report to arrive. Check that the size and number of archived files match the YARP repo. ### Recommended fields' values for archival request form | Field | Value | | --- | --- | | Team Alias | dotnetrp | | Business Group Name | Devdiv | | Product Name | YARP | | Version | \ | | Production Type | dotNET | | Release Type | \ | | Operating System(s) | Cross Platform | | Product Language(s) | English | | Release Date | \ | | File Count | \ | | Back Up Type | Code Repo(Git URL/AzureDevOps) | | Repo URL | \ | | OwnerAlias | dotnetrp | | File Collection | Build Scripts, Help Utility Source Code, Source Code | | Data Size | \ | ## Troubleshooting ### Authentication Errors The pipeline is authenticated via a "Service Connection" in Azure DevOps. If there are authentication errors, it's likely the API key is invalid. Follow these steps to update the API key: 1. Go to NuGet.org, log in with an account associated with an `@microsoft.com` address that has access to the `dotnetframework` organization. 2. Generate a new API key with "dotnetframework" as the Package Owner and "*" as the Package "glob". 3. Copy that API key and fill it in to the "nuget.org (dotnetframework organization)" [Service Connection](https://dev.azure.com/dnceng/internal/_settings/adminservices) in Azure DevOps. In the event you don't have access, contact `dnceng@microsoft.com` for guidance. ### Accidental Overpublish In the event you overpublish (publish a package that wasn't intended to be released), you should "unlist" the package on NuGet. It is not possible to delete packages on NuGet.org, by design, but you can remove them from search results. Users who reference the version you published directly will still be able to download it, but it won't show up in search queries or non-version-specific actions (like installing the latest). 1. Go to NuGet.org, log in with an account associated with an `@microsoft.com` address that has access to the `dotnetframework` organization. 2. Go to the package page and click "Manage package" on the "Info" sidebar on the right. 3. Expand "Listing" 4. Select the version that was accidentally published 5. Uncheck the "List in search results" box 6. Click "Save" ### Package was rejected NuGet.org has special criteria for all packages starting `Microsoft.`. If the package is rejected for not meeting one of those criteria, go to the [NuGet @ Microsoft](http://aka.ms/nuget) page for more information on required criteria and guidance for how to configure the package appropriately. ================================================ FILE: docs/roadmap.md ================================================ # YARP Roadmap ## Supported YARP versions [Latest releases](https://github.com/dotnet/yarp/releases) | Version | Release Date | Latest Patch Version | End of Support | | -- | -- | -- | -- | | [YARP 2.3](https://github.com/dotnet/yarp/releases/tag/v2.3.0) | February 27, 2025 | [2.3.0](https://github.com/dotnet/yarp/releases/tag/v2.3.0) | | ### End-of-life YARP versions | Version | Released date | Final Patch Version | End of support | | -- | -- | -- | -- | | [YARP 2.2](https://github.com/dotnet/yarp/releases/tag/v2.2.0) | September 3, 2024 | [2.2.0](https://github.com/dotnet/yarp/releases/tag/v2.2.0) | August 27, 2025 | | [YARP 2.1](https://github.com/dotnet/yarp/releases/tag/v2.1.0) | November 17, 2023 | [2.1.0](https://github.com/dotnet/yarp/releases/tag/v2.1.0) | March 3, 2025 | | [YARP 2.0](https://github.com/dotnet/yarp/releases/tag/v2.0.0) | February 14, 2023 | [2.0.1](https://github.com/dotnet/yarp/releases/tag/v2.0.1) | May 17, 2024 | | [YARP 1.1](https://github.com/dotnet/yarp/releases/tag/v1.1.0) | May 2, 2022 | [1.1.2](https://github.com/dotnet/yarp/releases/tag/v1.1.2) | August 14, 2023 | | [YARP 1.0](https://github.com/dotnet/yarp/releases/tag/v1.0.0) | November 9, 2021 | [1.0.1](https://github.com/dotnet/yarp/releases/tag/v1.0.1) | November 2, 2022 | ## Support YARP support is provided by the product team - the engineers working on YARP - which is a combination of members from ASP.NET and the .NET library teams. We do not provide 24/7 support or 'carry pagers', but we generally have good coverage. Bugs should be reported in GitHub using the issue templates, and will typically be responded to within 24hrs. If you find a security issue we ask you to [report it via the Microsoft Security Response Center (MSRC)](https://github.com/dotnet/yarp/blob/main/SECURITY.md). The support period for YARP releases is as follows: | Release | Issue Type | Support period | | --- | --- | --- | | Major or minor version | Security Bugs, Major behavior defects | Until next GA + 6 Months | | Patch version | Minor behavior defects | Until next GA | | Preview | Security Bugs, Major behavior defects | Until next preview | | | All other | None - may be addressed by next preview | For example, if 2 months after 1.3 (making up a number) is released, a security issue is found, then we will patch: - 1.3 - its the latest release - 1.2 - as it has 4 months of support remaining - 1.1 - provided that 1.2 was released less than 6 months before This support schedule is designed to provide a reasonable time period for customers to be able to update to new releases. ### Building your own copy of a release YARP is an open source project, so any customers that need fixes faster, or for older releases, are able to build their own copy of YARP. The build environment for YARP is included in the repo and maintained in sync with the source. Each release (GA and Preview) of YARP is tagged, which means that if you need to patch a specific release you can sync to the tag and build. For example you can rebuild v1.0.0 with: ```shell git clone -b v1.0.0 https://github.com/dotnet/yarp.git yarp1_0_0 cd yarp1_0_0 restore.cmd build.cmd -c release ``` This will produce the `Yarp.ReverseProxy.dll` into `artifacts/bin/Yarp.ReverseProxy/Release/net6.0`, and peer folder(s) for .NET 5 & Core 3.1. If you need to build a nuget package, that can be done with: ```shell pack.cmd -c release ``` The nuget package will be output to `artifacts/packages/release/Shipping`. ================================================ FILE: dotnet-yarp-release.yml ================================================ trigger: none name: $(Date:yyyyMMdd).$(Rev:r) variables: - name: NuGetApiKey value: - name: NuGetFeed value: https://api.nuget.org/v3/index.json resources: repositories: - repository: 1ESPipelineTemplates type: git name: 1ESPipelineTemplates/1ESPipelineTemplates ref: refs/tags/release pipelines: - pipeline: yarp-build source: dotnet\yarp\dotnet-yarp-official extends: template: v1/1ES.Official.PipelineTemplate.yml@1ESPipelineTemplates parameters: pool: name: NetCore1ESPool-Internal image: 1es-windows-2022 os: windows stages: - stage: release displayName: Release to NuGet jobs: - job: PreDeploymentApprovalJob displayName: Pre-Deployment Approval condition: succeeded() timeoutInMinutes: 2880 pool: server steps: - task: ManualValidation@1 inputs: notifyUsers: |- karelz@microsoft.com, samsp@microsoft.com, adityam@microsoft.com, mizupan@microsoft.com, bpetit@microsoft.com approvers: |- karelz@microsoft.com, samsp@microsoft.com, adityam@microsoft.com - job: NuGetPush dependsOn: PreDeploymentApprovalJob condition: succeeded() timeoutInMinutes: 30 templateContext: type: releaseJob isProduction: true inputs: - input: pipelineArtifact pipeline: yarp-build artifactName: artifacts steps: - task: NuGetToolInstaller@1 displayName: Prepare NuGet tool - task: PowerShell@2 displayName: NuGet push inputs: targetType: inline script: | tree $(Pipeline.Workspace)\Release\Shipping /f Get-ChildItem "$(Pipeline.Workspace)\Release\Shipping\*" -Filter *.nupkg -Exclude *.symbols.nupkg | ForEach-Object { $name = $_.Name Write-Host "Processing $name ..." if ($name.StartsWith("Yarp.ReverseProxy.") -or $name.StartsWith("Yarp.Telemetry.Consumption.")) { Write-Host " Publishing $name" nuget push -Source $env:NuGetFeed -ApiKey $env:NuGetApiKey $_.FullName } else { Write-Host " Skipping $name (update the script to change this)" } } env: NuGetApiKey: $(NuGetApiKey) ================================================ FILE: eng/Build.props ================================================ ================================================ FILE: eng/CodeAnalysis.src.globalconfig ================================================ is_global = true # AD0001: Analyzer threw an exception dotnet_diagnostic.AD0001.severity = warning # BCL0001: Ensure minimum API surface is respected dotnet_diagnostic.BCL0001.severity = warning # BCL0010: AppContext default value expected to be true dotnet_diagnostic.BCL0010.severity = warning # BCL0011: AppContext default value defined in if statement with incorrect pattern dotnet_diagnostic.BCL0011.severity = warning # BCL0012: AppContext default value defined in if statement at root of switch case dotnet_diagnostic.BCL0012.severity = warning # BCL0015: Invalid P/Invoke call dotnet_diagnostic.BCL0015.severity = none # BCL0020: Invalid SR.Format call dotnet_diagnostic.BCL0020.severity = warning # SYSLIB1045: Convert to 'GeneratedRegexAttribute'. dotnet_diagnostic.SYSLIB1045.severity = warning # SYSLIB1054: Use 'LibraryImportAttribute' instead of 'DllImportAttribute' to generate P/Invoke marshalling code at compile time dotnet_diagnostic.SYSLIB1054.severity = warning # SYSLIB1055: Invalid 'CustomMarshallerAttribute' usage dotnet_diagnostic.SYSLIB1055.severity = error # SYSLIB1056:Specified marshaller type is invalid dotnet_diagnostic.SYSLIB1056.severity = error # SYSLIB1057: Marshaller type does not have the required shape dotnet_diagnostic.SYSLIB1057.severity = error # SYSLIB1058: Invalid 'NativeMarshallingAttribute' usage dotnet_diagnostic.SYSLIB1058.severity = error # SYSLIB1060: Specified marshaller type is invalid dotnet_diagnostic.SYSLIB1060.severity = error # SYSLIB1061: Marshaller type has incompatible method signatures dotnet_diagnostic.SYSLIB1061.severity = error # CA1000: Do not declare static members on generic types dotnet_diagnostic.CA1000.severity = none # CA1001: Types that own disposable fields should be disposable dotnet_diagnostic.CA1001.severity = none # CA1002: Do not expose generic lists dotnet_diagnostic.CA1002.severity = none # CA1003: Use generic event handler instances dotnet_diagnostic.CA1003.severity = none # CA1005: Avoid excessive parameters on generic types dotnet_diagnostic.CA1005.severity = none # CA1008: Enums should have zero value dotnet_diagnostic.CA1008.severity = none # CA1010: Generic interface should also be implemented dotnet_diagnostic.CA1010.severity = none # CA1012: Abstract types should not have public constructors dotnet_diagnostic.CA1012.severity = none # CA1014: Mark assemblies with CLSCompliant dotnet_diagnostic.CA1014.severity = none # CA1016: Mark assemblies with assembly version dotnet_diagnostic.CA1016.severity = none # CA1017: Mark assemblies with ComVisible dotnet_diagnostic.CA1017.severity = none # CA1018: Mark attributes with AttributeUsageAttribute dotnet_diagnostic.CA1018.severity = warning # CA1019: Define accessors for attribute arguments dotnet_diagnostic.CA1019.severity = none # CA1021: Avoid out parameters dotnet_diagnostic.CA1021.severity = none # CA1024: Use properties where appropriate dotnet_diagnostic.CA1024.severity = none # CA1027: Mark enums with FlagsAttribute dotnet_diagnostic.CA1027.severity = none # CA1028: Enum Storage should be Int32 dotnet_diagnostic.CA1028.severity = none # CA1030: Use events where appropriate dotnet_diagnostic.CA1030.severity = none # CA1031: Do not catch general exception types dotnet_diagnostic.CA1031.severity = none # CA1032: Implement standard exception constructors dotnet_diagnostic.CA1032.severity = none # CA1033: Interface methods should be callable by child types dotnet_diagnostic.CA1033.severity = none # CA1034: Nested types should not be visible dotnet_diagnostic.CA1034.severity = none # CA1036: Override methods on comparable types dotnet_diagnostic.CA1036.severity = none # CA1040: Avoid empty interfaces dotnet_diagnostic.CA1040.severity = none # CA1041: Provide ObsoleteAttribute message dotnet_diagnostic.CA1041.severity = none # CA1043: Use Integral Or String Argument For Indexers dotnet_diagnostic.CA1043.severity = none # CA1044: Properties should not be write only dotnet_diagnostic.CA1044.severity = none # CA1045: Do not pass types by reference dotnet_diagnostic.CA1045.severity = none # CA1046: Do not overload equality operator on reference types dotnet_diagnostic.CA1046.severity = none # CA1047: Do not declare protected member in sealed type dotnet_diagnostic.CA1047.severity = warning # CA1050: Declare types in namespaces dotnet_diagnostic.CA1050.severity = warning # CA1051: Do not declare visible instance fields dotnet_diagnostic.CA1051.severity = none # CA1052: Static holder types should be Static or NotInheritable dotnet_diagnostic.CA1052.severity = warning dotnet_code_quality.CA1052.api_surface = private, internal # CA1054: URI-like parameters should not be strings dotnet_diagnostic.CA1054.severity = none # CA1055: URI-like return values should not be strings dotnet_diagnostic.CA1055.severity = none # CA1056: URI-like properties should not be strings dotnet_diagnostic.CA1056.severity = none # CA1058: Types should not extend certain base types dotnet_diagnostic.CA1058.severity = none # CA1060: Move pinvokes to native methods class dotnet_diagnostic.CA1060.severity = none # CA1061: Do not hide base class methods dotnet_diagnostic.CA1061.severity = none # CA1062: Validate arguments of public methods dotnet_diagnostic.CA1062.severity = none # CA1063: Implement IDisposable Correctly dotnet_diagnostic.CA1063.severity = none # CA1064: Exceptions should be public dotnet_diagnostic.CA1064.severity = none # CA1065: Do not raise exceptions in unexpected locations dotnet_diagnostic.CA1065.severity = none # CA1066: Implement IEquatable when overriding Object.Equals dotnet_diagnostic.CA1066.severity = warning # CA1067: Override Object.Equals(object) when implementing IEquatable dotnet_diagnostic.CA1067.severity = warning # CA1068: CancellationToken parameters must come last dotnet_diagnostic.CA1068.severity = none # CA1069: Enums values should not be duplicated dotnet_diagnostic.CA1069.severity = none # CA1070: Do not declare event fields as virtual dotnet_diagnostic.CA1070.severity = suggestion # CA1200: Avoid using cref tags with a prefix dotnet_diagnostic.CA1200.severity = suggestion # CA1303: Do not pass literals as localized parameters dotnet_diagnostic.CA1303.severity = none # CA1304: Specify CultureInfo dotnet_diagnostic.CA1304.severity = none # CA1305: Specify IFormatProvider dotnet_diagnostic.CA1305.severity = none # CA1307: Specify StringComparison for clarity dotnet_diagnostic.CA1307.severity = none # CA1308: Normalize strings to uppercase dotnet_diagnostic.CA1308.severity = none # CA1309: Use ordinal string comparison dotnet_diagnostic.CA1309.severity = none # CA1310: Specify StringComparison for correctness dotnet_diagnostic.CA1310.severity = suggestion # CA1311: Specify a culture or use an invariant version dotnet_diagnostic.CA1311.severity = warning # CA1401: P/Invokes should not be visible dotnet_diagnostic.CA1401.severity = warning # CA1416: Validate platform compatibility dotnet_diagnostic.CA1416.severity = warning # CA1417: Do not use 'OutAttribute' on string parameters for P/Invokes dotnet_diagnostic.CA1417.severity = warning # CA1418: Use valid platform string dotnet_diagnostic.CA1418.severity = warning # CA1419: Provide a parameterless constructor that is as visible as the containing type for concrete types derived from 'System.Runtime.InteropServices.SafeHandle' dotnet_diagnostic.CA1419.severity = warning # CA1420: Property, type, or attribute requires runtime marshalling dotnet_diagnostic.CA1420.severity = warning # CA1421: This method uses runtime marshalling even when the 'DisableRuntimeMarshallingAttribute' is applied dotnet_diagnostic.CA1421.severity = suggestion # CA1422: Validate platform compatibility dotnet_diagnostic.CA1422.severity = warning # CA1501: Avoid excessive inheritance dotnet_diagnostic.CA1501.severity = none # CA1502: Avoid excessive complexity dotnet_diagnostic.CA1502.severity = none # CA1505: Avoid unmaintainable code dotnet_diagnostic.CA1505.severity = none # CA1506: Avoid excessive class coupling dotnet_diagnostic.CA1506.severity = none # CA1507: Use nameof to express symbol names dotnet_diagnostic.CA1507.severity = warning # CA1508: Avoid dead conditional code dotnet_diagnostic.CA1508.severity = none # CA1509: Invalid entry in code metrics rule specification file dotnet_diagnostic.CA1509.severity = none # CA1510: Use ArgumentNullException throw helper # dotnet_diagnostic.CA1510.severity = warning # CA1511: Use ArgumentException throw helper dotnet_diagnostic.CA1511.severity = warning # CA1512: Use ArgumentOutOfRangeException throw helper dotnet_diagnostic.CA1512.severity = warning # CA1513: Use ObjectDisposedException throw helper dotnet_diagnostic.CA1513.severity = warning # CA1514: Avoid redundant length argument dotnet_diagnostic.CA1514.severity = warning # CA1515: Consider making public types internal dotnet_diagnostic.CA1515.severity = none # CA1700: Do not name enum values 'Reserved' dotnet_diagnostic.CA1700.severity = none # CA1707: Identifiers should not contain underscores dotnet_diagnostic.CA1707.severity = none # CA1708: Identifiers should differ by more than case dotnet_diagnostic.CA1708.severity = none # CA1710: Identifiers should have correct suffix dotnet_diagnostic.CA1710.severity = none # CA1711: Identifiers should not have incorrect suffix dotnet_diagnostic.CA1711.severity = none # CA1712: Do not prefix enum values with type name dotnet_diagnostic.CA1712.severity = none # CA1713: Events should not have 'Before' or 'After' prefix dotnet_diagnostic.CA1713.severity = none # CA1715: Identifiers should have correct prefix dotnet_diagnostic.CA1715.severity = none # CA1716: Identifiers should not match keywords dotnet_diagnostic.CA1716.severity = none # CA1720: Identifier contains type name dotnet_diagnostic.CA1720.severity = none # CA1721: Property names should not match get methods dotnet_diagnostic.CA1721.severity = none # CA1724: Type names should not match namespaces dotnet_diagnostic.CA1724.severity = none # CA1725: Parameter names should match base declaration dotnet_diagnostic.CA1725.severity = suggestion # CA1727: Use PascalCase for named placeholders # dotnet_diagnostic.CA1727.severity = warning # CA1802: Use literals where appropriate dotnet_diagnostic.CA1802.severity = warning dotnet_code_quality.CA1802.api_surface = private, internal # CA1805: Do not initialize unnecessarily dotnet_diagnostic.CA1805.severity = warning # CA1806: Do not ignore method results dotnet_diagnostic.CA1806.severity = none # CA1810: Initialize reference type static fields inline dotnet_diagnostic.CA1810.severity = warning # CA1812: Avoid uninstantiated internal classes dotnet_diagnostic.CA1812.severity = none # CA1813: Avoid unsealed attributes dotnet_diagnostic.CA1813.severity = none # CA1814: Prefer jagged arrays over multidimensional dotnet_diagnostic.CA1814.severity = none # CA1815: Override equals and operator equals on value types dotnet_diagnostic.CA1815.severity = none # CA1816: Dispose methods should call SuppressFinalize dotnet_diagnostic.CA1816.severity = none # CA1819: Properties should not return arrays dotnet_diagnostic.CA1819.severity = none # CA1820: Test for empty strings using string length dotnet_diagnostic.CA1820.severity = suggestion # CA1821: Remove empty Finalizers dotnet_diagnostic.CA1821.severity = warning # CA1822: Mark members as static dotnet_diagnostic.CA1822.severity = warning dotnet_code_quality.CA1822.api_surface = private, internal # CA1823: Avoid unused private fields dotnet_diagnostic.CA1823.severity = warning # CA1824: Mark assemblies with NeutralResourcesLanguageAttribute dotnet_diagnostic.CA1824.severity = warning # CA1825: Avoid zero-length array allocations dotnet_diagnostic.CA1825.severity = warning # CA1826: Do not use Enumerable methods on indexable collections dotnet_diagnostic.CA1826.severity = warning # CA1827: Do not use Count() or LongCount() when Any() can be used dotnet_diagnostic.CA1827.severity = warning # CA1828: Do not use CountAsync() or LongCountAsync() when AnyAsync() can be used dotnet_diagnostic.CA1828.severity = warning # CA1829: Use Length/Count property instead of Count() when available dotnet_diagnostic.CA1829.severity = warning # CA1830: Prefer strongly-typed Append and Insert method overloads on StringBuilder dotnet_diagnostic.CA1830.severity = warning # CA1831: Use AsSpan or AsMemory instead of Range-based indexers when appropriate dotnet_diagnostic.CA1831.severity = warning # CA1832: Use AsSpan or AsMemory instead of Range-based indexers when appropriate dotnet_diagnostic.CA1832.severity = warning # CA1833: Use AsSpan or AsMemory instead of Range-based indexers when appropriate dotnet_diagnostic.CA1833.severity = warning # CA1834: Consider using 'StringBuilder.Append(char)' when applicable dotnet_diagnostic.CA1834.severity = warning # CA1835: Prefer the 'Memory'-based overloads for 'ReadAsync' and 'WriteAsync' dotnet_diagnostic.CA1835.severity = warning # CA1836: Prefer IsEmpty over Count dotnet_diagnostic.CA1836.severity = warning # CA1837: Use 'Environment.ProcessId' dotnet_diagnostic.CA1837.severity = warning # CA1838: Avoid 'StringBuilder' parameters for P/Invokes dotnet_diagnostic.CA1838.severity = warning # CA1839: Use 'Environment.ProcessPath' dotnet_diagnostic.CA1839.severity = warning # CA1840: Use 'Environment.CurrentManagedThreadId' dotnet_diagnostic.CA1840.severity = warning # CA1841: Prefer Dictionary.Contains methods dotnet_diagnostic.CA1841.severity = warning # CA1842: Do not use 'WhenAll' with a single task dotnet_diagnostic.CA1842.severity = warning # CA1843: Do not use 'WaitAll' with a single task dotnet_diagnostic.CA1843.severity = warning # CA1844: Provide memory-based overrides of async methods when subclassing 'Stream' dotnet_diagnostic.CA1844.severity = warning # CA1845: Use span-based 'string.Concat' dotnet_diagnostic.CA1845.severity = warning # CA1846: Prefer 'AsSpan' over 'Substring' dotnet_diagnostic.CA1846.severity = warning # CA1847: Use char literal for a single character lookup dotnet_diagnostic.CA1847.severity = warning # CA1848: Use the LoggerMessage delegates dotnet_diagnostic.CA1848.severity = none # CA1849: Call async methods when in an async method dotnet_diagnostic.CA1849.severity = suggestion # CA1850: Prefer static 'HashData' method over 'ComputeHash' dotnet_diagnostic.CA1850.severity = warning # CA1851: Possible multiple enumerations of 'IEnumerable' collection dotnet_diagnostic.CA1851.severity = suggestion # CA1852: Seal internal types dotnet_diagnostic.CA1852.severity = warning # CA1853: Unnecessary call to 'Dictionary.ContainsKey(key)' dotnet_diagnostic.CA1853.severity = warning # CA1854: Prefer the 'IDictionary.TryGetValue(TKey, out TValue)' method dotnet_diagnostic.CA1854.severity = warning # CA1855: Prefer 'Clear' over 'Fill' dotnet_diagnostic.CA1855.severity = warning # CA1856: Incorrect usage of ConstantExpected attribute dotnet_diagnostic.CA1856.severity = error # CA1857: A constant is expected for the parameter dotnet_diagnostic.CA1857.severity = warning # CA1858: Use 'StartsWith' instead of 'IndexOf' dotnet_diagnostic.CA1858.severity = warning # CA1859: Use concrete types when possible for improved performance dotnet_diagnostic.CA1859.severity = warning # CA1860: Avoid using 'Enumerable.Any()' extension method dotnet_diagnostic.CA1860.severity = warning # CA1861: Avoid constant arrays as arguments dotnet_diagnostic.CA1861.severity = warning # CA1862: Prefer using 'StringComparer'/'StringComparison' to perform case-insensitive string comparisons dotnet_diagnostic.CA1862.severity = suggestion # CA1863: Use 'CompositeFormat' dotnet_diagnostic.CA1863.severity = suggestion # CA1864: Prefer the 'IDictionary.TryAdd(TKey, TValue)' method dotnet_diagnostic.CA1864.severity = warning # CA1865: Use char overload dotnet_diagnostic.CA1865.severity = warning # CA1866: Use char overload dotnet_diagnostic.CA1866.severity = warning # CA1867: Use char overload dotnet_diagnostic.CA1867.severity = warning # CA1868: Unnecessary call to 'Contains' for sets dotnet_diagnostic.CA1868.severity = warning # CA1869: Cache and reuse 'JsonSerializerOptions' instances dotnet_diagnostic.CA1869.severity = warning # CA1870: Use a cached 'SearchValues' instance dotnet_diagnostic.CA1870.severity = warning # CA1871: Do not pass a nullable struct to 'ArgumentNullException.ThrowIfNull' dotnet_diagnostic.CA1871.severity = warning # CA1872: Prefer 'Convert.ToHexString' and 'Convert.ToHexStringLower' over call chains based on 'BitConverter.ToString' dotnet_diagnostic.CA1872.severity = warning # CA2000: Dispose objects before losing scope dotnet_diagnostic.CA2000.severity = none # CA2002: Do not lock on objects with weak identity dotnet_diagnostic.CA2002.severity = none # CA2007: Consider calling ConfigureAwait on the awaited task # dotnet_diagnostic.CA2007.severity = warning # CA2008: Do not create tasks without passing a TaskScheduler dotnet_diagnostic.CA2008.severity = warning # CA2009: Do not call ToImmutableCollection on an ImmutableCollection value dotnet_diagnostic.CA2009.severity = warning # CA2011: Avoid infinite recursion dotnet_diagnostic.CA2011.severity = warning # CA2012: Use ValueTasks correctly dotnet_diagnostic.CA2012.severity = warning # CA2013: Do not use ReferenceEquals with value types dotnet_diagnostic.CA2013.severity = warning # CA2014: Do not use stackalloc in loops dotnet_diagnostic.CA2014.severity = warning # CA2015: Do not define finalizers for types derived from MemoryManager dotnet_diagnostic.CA2015.severity = warning # CA2016: Forward the 'CancellationToken' parameter to methods dotnet_diagnostic.CA2016.severity = warning # CA2017: Parameter count mismatch dotnet_diagnostic.CA2017.severity = warning # CA2018: 'Buffer.BlockCopy' expects the number of bytes to be copied for the 'count' argument dotnet_diagnostic.CA2018.severity = warning # CA2019: Improper 'ThreadStatic' field initialization dotnet_diagnostic.CA2019.severity = warning # CA2020: Prevent behavioral change dotnet_diagnostic.CA2020.severity = warning # CA2021: Do not call Enumerable.Cast or Enumerable.OfType with incompatible types dotnet_diagnostic.CA2021.severity = warning # CA2022: Avoid inexact read with 'Stream.Read' dotnet_diagnostic.CA2022.severity = warning # CA2100: Review SQL queries for security vulnerabilities dotnet_diagnostic.CA2100.severity = none # CA2101: Specify marshaling for P/Invoke string arguments dotnet_diagnostic.CA2101.severity = none # CA2119: Seal methods that satisfy private interfaces dotnet_diagnostic.CA2119.severity = none # CA2153: Do Not Catch Corrupted State Exceptions dotnet_diagnostic.CA2153.severity = none # CA2200: Rethrow to preserve stack details dotnet_diagnostic.CA2200.severity = warning # CA2201: Do not raise reserved exception types dotnet_diagnostic.CA2201.severity = none # CA2207: Initialize value type static fields inline dotnet_diagnostic.CA2207.severity = warning # CA2208: Instantiate argument exceptions correctly dotnet_diagnostic.CA2208.severity = warning dotnet_code_quality.CA2208.api_surface = public # CA2211: Non-constant fields should not be visible dotnet_diagnostic.CA2211.severity = none # CA2213: Disposable fields should be disposed dotnet_diagnostic.CA2213.severity = none # CA2214: Do not call overridable methods in constructors dotnet_diagnostic.CA2214.severity = none # CA2215: Dispose methods should call base class dispose dotnet_diagnostic.CA2215.severity = none # CA2216: Disposable types should declare finalizer dotnet_diagnostic.CA2216.severity = none # CA2217: Do not mark enums with FlagsAttribute dotnet_diagnostic.CA2217.severity = none # CA2218: Override GetHashCode on overriding Equals dotnet_diagnostic.CA2218.severity = none # CA2219: Do not raise exceptions in finally clauses dotnet_diagnostic.CA2219.severity = none # CA2224: Override Equals on overloading operator equals dotnet_diagnostic.CA2224.severity = none # CA2225: Operator overloads have named alternates dotnet_diagnostic.CA2225.severity = none # CA2226: Operators should have symmetrical overloads dotnet_diagnostic.CA2226.severity = none # CA2227: Collection properties should be read only dotnet_diagnostic.CA2227.severity = none # CA2231: Overload operator equals on overriding value type Equals dotnet_diagnostic.CA2231.severity = none # CA2234: Pass system uri objects instead of strings dotnet_diagnostic.CA2234.severity = none # CA2235: Mark all non-serializable fields dotnet_diagnostic.CA2235.severity = none # CA2237: Mark ISerializable types with serializable dotnet_diagnostic.CA2237.severity = none # CA2241: Provide correct arguments to formatting methods dotnet_diagnostic.CA2241.severity = warning # CA2242: Test for NaN correctly dotnet_diagnostic.CA2242.severity = warning # CA2243: Attribute string literals should parse correctly dotnet_diagnostic.CA2243.severity = warning # CA2244: Do not duplicate indexed element initializations dotnet_diagnostic.CA2244.severity = warning # CA2245: Do not assign a property to itself dotnet_diagnostic.CA2245.severity = warning # CA2246: Assigning symbol and its member in the same statement dotnet_diagnostic.CA2246.severity = warning # CA2247: Argument passed to TaskCompletionSource constructor should be TaskCreationOptions enum instead of TaskContinuationOptions enum dotnet_diagnostic.CA2247.severity = warning # CA2248: Provide correct 'enum' argument to 'Enum.HasFlag' dotnet_diagnostic.CA2248.severity = warning # CA2249: Consider using 'string.Contains' instead of 'string.IndexOf' dotnet_diagnostic.CA2249.severity = warning # CA2250: Use 'ThrowIfCancellationRequested' dotnet_diagnostic.CA2250.severity = warning # CA2251: Use 'string.Equals' dotnet_diagnostic.CA2251.severity = warning # CA2252: This API requires opting into preview features dotnet_diagnostic.CA2252.severity = error # CA2253: Named placeholders should not be numeric values dotnet_diagnostic.CA2253.severity = warning # CA2254: Template should be a static expression dotnet_diagnostic.CA2254.severity = none # CA2255: The 'ModuleInitializer' attribute should not be used in libraries dotnet_diagnostic.CA2255.severity = warning # CA2256: All members declared in parent interfaces must have an implementation in a DynamicInterfaceCastableImplementation-attributed interface dotnet_diagnostic.CA2256.severity = warning # CA2257: Members defined on an interface with the 'DynamicInterfaceCastableImplementationAttribute' should be 'static' dotnet_diagnostic.CA2257.severity = warning # CA2258: Providing a 'DynamicInterfaceCastableImplementation' interface in Visual Basic is unsupported dotnet_diagnostic.CA2258.severity = warning # CA2259: 'ThreadStatic' only affects static fields dotnet_diagnostic.CA2259.severity = warning # CA2260: Use correct type parameter dotnet_diagnostic.CA2260.severity = warning # CA2261: Do not use ConfigureAwaitOptions.SuppressThrowing with Task dotnet_diagnostic.CA2261.severity = warning # CA2262: Set 'MaxResponseHeadersLength' properly dotnet_diagnostic.CA2262.severity = warning # CA2263: Prefer generic overload when type is known dotnet_diagnostic.CA2263.severity = suggestion # CA2264: Do not pass a non-nullable value to 'ArgumentNullException.ThrowIfNull' dotnet_diagnostic.CA2264.severity = warning # CA2265: Do not compare Span to 'null' or 'default' dotnet_diagnostic.CA2265.severity = warning # CA2300: Do not use insecure deserializer BinaryFormatter dotnet_diagnostic.CA2300.severity = none # CA2301: Do not call BinaryFormatter.Deserialize without first setting BinaryFormatter.Binder dotnet_diagnostic.CA2301.severity = none # CA2302: Ensure BinaryFormatter.Binder is set before calling BinaryFormatter.Deserialize dotnet_diagnostic.CA2302.severity = none # CA2305: Do not use insecure deserializer LosFormatter dotnet_diagnostic.CA2305.severity = none # CA2310: Do not use insecure deserializer NetDataContractSerializer dotnet_diagnostic.CA2310.severity = none # CA2311: Do not deserialize without first setting NetDataContractSerializer.Binder dotnet_diagnostic.CA2311.severity = none # CA2312: Ensure NetDataContractSerializer.Binder is set before deserializing dotnet_diagnostic.CA2312.severity = none # CA2315: Do not use insecure deserializer ObjectStateFormatter dotnet_diagnostic.CA2315.severity = none # CA2321: Do not deserialize with JavaScriptSerializer using a SimpleTypeResolver dotnet_diagnostic.CA2321.severity = none # CA2322: Ensure JavaScriptSerializer is not initialized with SimpleTypeResolver before deserializing dotnet_diagnostic.CA2322.severity = none # CA2326: Do not use TypeNameHandling values other than None dotnet_diagnostic.CA2326.severity = none # CA2327: Do not use insecure JsonSerializerSettings dotnet_diagnostic.CA2327.severity = none # CA2328: Ensure that JsonSerializerSettings are secure dotnet_diagnostic.CA2328.severity = none # CA2329: Do not deserialize with JsonSerializer using an insecure configuration dotnet_diagnostic.CA2329.severity = none # CA2330: Ensure that JsonSerializer has a secure configuration when deserializing dotnet_diagnostic.CA2330.severity = none # CA2350: Do not use DataTable.ReadXml() with untrusted data dotnet_diagnostic.CA2350.severity = none # CA2351: Do not use DataSet.ReadXml() with untrusted data dotnet_diagnostic.CA2351.severity = none # CA2352: Unsafe DataSet or DataTable in serializable type can be vulnerable to remote code execution attacks dotnet_diagnostic.CA2352.severity = none # CA2353: Unsafe DataSet or DataTable in serializable type dotnet_diagnostic.CA2353.severity = none # CA2354: Unsafe DataSet or DataTable in deserialized object graph can be vulnerable to remote code execution attacks dotnet_diagnostic.CA2354.severity = none # CA2355: Unsafe DataSet or DataTable type found in deserializable object graph dotnet_diagnostic.CA2355.severity = none # CA2356: Unsafe DataSet or DataTable type in web deserializable object graph dotnet_diagnostic.CA2356.severity = none # CA2361: Ensure auto-generated class containing DataSet.ReadXml() is not used with untrusted data dotnet_diagnostic.CA2361.severity = none # CA2362: Unsafe DataSet or DataTable in auto-generated serializable type can be vulnerable to remote code execution attacks dotnet_diagnostic.CA2362.severity = none # CA3001: Review code for SQL injection vulnerabilities dotnet_diagnostic.CA3001.severity = none # CA3002: Review code for XSS vulnerabilities dotnet_diagnostic.CA3002.severity = none # CA3003: Review code for file path injection vulnerabilities dotnet_diagnostic.CA3003.severity = none # CA3004: Review code for information disclosure vulnerabilities dotnet_diagnostic.CA3004.severity = none # CA3005: Review code for LDAP injection vulnerabilities dotnet_diagnostic.CA3005.severity = none # CA3006: Review code for process command injection vulnerabilities dotnet_diagnostic.CA3006.severity = none # CA3007: Review code for open redirect vulnerabilities dotnet_diagnostic.CA3007.severity = none # CA3008: Review code for XPath injection vulnerabilities dotnet_diagnostic.CA3008.severity = none # CA3009: Review code for XML injection vulnerabilities dotnet_diagnostic.CA3009.severity = none # CA3010: Review code for XAML injection vulnerabilities dotnet_diagnostic.CA3010.severity = none # CA3011: Review code for DLL injection vulnerabilities dotnet_diagnostic.CA3011.severity = none # CA3012: Review code for regex injection vulnerabilities dotnet_diagnostic.CA3012.severity = none # CA3061: Do Not Add Schema By URL dotnet_diagnostic.CA3061.severity = warning # CA3075: Insecure DTD processing in XML dotnet_diagnostic.CA3075.severity = warning # CA3076: Insecure XSLT script processing dotnet_diagnostic.CA3076.severity = warning # CA3077: Insecure Processing in API Design, XmlDocument and XmlTextReader dotnet_diagnostic.CA3077.severity = warning # CA3147: Mark Verb Handlers With Validate Antiforgery Token dotnet_diagnostic.CA3147.severity = warning # CA5350: Do Not Use Weak Cryptographic Algorithms dotnet_diagnostic.CA5350.severity = warning # CA5351: Do Not Use Broken Cryptographic Algorithms dotnet_diagnostic.CA5351.severity = warning # CA5358: Review cipher mode usage with cryptography experts dotnet_diagnostic.CA5358.severity = none # CA5359: Do Not Disable Certificate Validation dotnet_diagnostic.CA5359.severity = warning # CA5360: Do Not Call Dangerous Methods In Deserialization dotnet_diagnostic.CA5360.severity = warning # CA5361: Do Not Disable SChannel Use of Strong Crypto dotnet_diagnostic.CA5361.severity = warning # CA5362: Potential reference cycle in deserialized object graph dotnet_diagnostic.CA5362.severity = none # CA5363: Do Not Disable Request Validation dotnet_diagnostic.CA5363.severity = warning # CA5364: Do Not Use Deprecated Security Protocols dotnet_diagnostic.CA5364.severity = warning # CA5365: Do Not Disable HTTP Header Checking dotnet_diagnostic.CA5365.severity = warning # CA5366: Use XmlReader for 'DataSet.ReadXml()' dotnet_diagnostic.CA5366.severity = none # CA5367: Do Not Serialize Types With Pointer Fields dotnet_diagnostic.CA5367.severity = none # CA5368: Set ViewStateUserKey For Classes Derived From Page dotnet_diagnostic.CA5368.severity = warning # CA5369: Use XmlReader for 'XmlSerializer.Deserialize()' dotnet_diagnostic.CA5369.severity = none # CA5370: Use XmlReader for XmlValidatingReader constructor dotnet_diagnostic.CA5370.severity = warning # CA5371: Use XmlReader for 'XmlSchema.Read()' dotnet_diagnostic.CA5371.severity = none # CA5372: Use XmlReader for XPathDocument constructor dotnet_diagnostic.CA5372.severity = none # CA5373: Do not use obsolete key derivation function dotnet_diagnostic.CA5373.severity = warning # CA5374: Do Not Use XslTransform dotnet_diagnostic.CA5374.severity = warning # CA5375: Do Not Use Account Shared Access Signature dotnet_diagnostic.CA5375.severity = none # CA5376: Use SharedAccessProtocol HttpsOnly dotnet_diagnostic.CA5376.severity = warning # CA5377: Use Container Level Access Policy dotnet_diagnostic.CA5377.severity = warning # CA5378: Do not disable ServicePointManagerSecurityProtocols dotnet_diagnostic.CA5378.severity = warning # CA5379: Ensure Key Derivation Function algorithm is sufficiently strong dotnet_diagnostic.CA5379.severity = warning # CA5380: Do Not Add Certificates To Root Store dotnet_diagnostic.CA5380.severity = warning # CA5381: Ensure Certificates Are Not Added To Root Store dotnet_diagnostic.CA5381.severity = warning # CA5382: Use Secure Cookies In ASP.NET Core dotnet_diagnostic.CA5382.severity = none # CA5383: Ensure Use Secure Cookies In ASP.NET Core dotnet_diagnostic.CA5383.severity = none # CA5384: Do Not Use Digital Signature Algorithm (DSA) dotnet_diagnostic.CA5384.severity = warning # CA5385: Use Rivest-Shamir-Adleman (RSA) Algorithm With Sufficient Key Size dotnet_diagnostic.CA5385.severity = warning # CA5386: Avoid hardcoding SecurityProtocolType value dotnet_diagnostic.CA5386.severity = none # CA5387: Do Not Use Weak Key Derivation Function With Insufficient Iteration Count dotnet_diagnostic.CA5387.severity = none # CA5388: Ensure Sufficient Iteration Count When Using Weak Key Derivation Function dotnet_diagnostic.CA5388.severity = none # CA5389: Do Not Add Archive Item's Path To The Target File System Path dotnet_diagnostic.CA5389.severity = none # CA5390: Do not hard-code encryption key dotnet_diagnostic.CA5390.severity = none # CA5391: Use antiforgery tokens in ASP.NET Core MVC controllers dotnet_diagnostic.CA5391.severity = none # CA5392: Use DefaultDllImportSearchPaths attribute for P/Invokes dotnet_diagnostic.CA5392.severity = none # CA5393: Do not use unsafe DllImportSearchPath value dotnet_diagnostic.CA5393.severity = none # CA5394: Do not use insecure randomness dotnet_diagnostic.CA5394.severity = none # CA5395: Miss HttpVerb attribute for action methods dotnet_diagnostic.CA5395.severity = none # CA5396: Set HttpOnly to true for HttpCookie dotnet_diagnostic.CA5396.severity = none # CA5397: Do not use deprecated SslProtocols values dotnet_diagnostic.CA5397.severity = none # CA5398: Avoid hardcoded SslProtocols values dotnet_diagnostic.CA5398.severity = none # CA5399: HttpClients should enable certificate revocation list checks dotnet_diagnostic.CA5399.severity = none # CA5400: Ensure HttpClient certificate revocation list check is not disabled dotnet_diagnostic.CA5400.severity = none # CA5401: Do not use CreateEncryptor with non-default IV dotnet_diagnostic.CA5401.severity = none # CA5402: Use CreateEncryptor with the default IV dotnet_diagnostic.CA5402.severity = none # CA5403: Do not hard-code certificate dotnet_diagnostic.CA5403.severity = none # CA5404: Do not disable token validation checks dotnet_diagnostic.CA5404.severity = none # CA5405: Do not always skip token validation in delegates dotnet_diagnostic.CA5405.severity = none # IL3000: Avoid using accessing Assembly file path when publishing as a single-file dotnet_diagnostic.IL3000.severity = warning # IL3001: Avoid using accessing Assembly file path when publishing as a single-file dotnet_diagnostic.IL3001.severity = warning # IL3002: Using member with RequiresAssemblyFilesAttribute can break functionality when embedded in a single-file app dotnet_diagnostic.IL3002.severity = warning # RS1001: Missing diagnostic analyzer attribute dotnet_diagnostic.RS1001.severity = warning # RS1002: Missing kind argument when registering an analyzer action dotnet_diagnostic.RS1002.severity = warning # RS1003: Unsupported SymbolKind argument when registering a symbol analyzer action dotnet_diagnostic.RS1003.severity = warning # RS1004: Recommend adding language support to diagnostic analyzer dotnet_diagnostic.RS1004.severity = warning # RS1005: ReportDiagnostic invoked with an unsupported DiagnosticDescriptor dotnet_diagnostic.RS1005.severity = warning # RS1006: Invalid type argument for DiagnosticAnalyzer's Register method dotnet_diagnostic.RS1006.severity = warning # RS1007: Provide localizable arguments to diagnostic descriptor constructor dotnet_diagnostic.RS1007.severity = none # RS1008: Avoid storing per-compilation data into the fields of a diagnostic analyzer dotnet_diagnostic.RS1008.severity = warning # RS1009: Only internal implementations of this interface are allowed dotnet_diagnostic.RS1009.severity = error # RS1010: Create code actions should have a unique EquivalenceKey for FixAll occurrences support dotnet_diagnostic.RS1010.severity = warning # RS1011: Use code actions that have a unique EquivalenceKey for FixAll occurrences support dotnet_diagnostic.RS1011.severity = warning # RS1012: Start action has no registered actions dotnet_diagnostic.RS1012.severity = warning # RS1013: Start action has no registered non-end actions dotnet_diagnostic.RS1013.severity = warning # RS1014: Do not ignore values returned by methods on immutable objects dotnet_diagnostic.RS1014.severity = warning # RS1015: Provide non-null 'helpLinkUri' value to diagnostic descriptor constructor dotnet_diagnostic.RS1015.severity = none # RS1016: Code fix providers should provide FixAll support dotnet_diagnostic.RS1016.severity = suggestion # RS1017: DiagnosticId for analyzers must be a non-null constant dotnet_diagnostic.RS1017.severity = warning # RS1018: DiagnosticId for analyzers must be in specified format dotnet_diagnostic.RS1018.severity = warning # RS1019: DiagnosticId must be unique across analyzers dotnet_diagnostic.RS1019.severity = warning # RS1020: Category for analyzers must be from the specified values dotnet_diagnostic.RS1020.severity = none # RS1021: Invalid entry in analyzer category and diagnostic ID range specification file dotnet_diagnostic.RS1021.severity = warning # RS1022: Do not use types from Workspaces assembly in an analyzer dotnet_diagnostic.RS1022.severity = warning # RS1023: Upgrade MSBuildWorkspace dotnet_diagnostic.RS1023.severity = warning # RS1024: Symbols should be compared for equality dotnet_diagnostic.RS1024.severity = warning # RS1025: Configure generated code analysis dotnet_diagnostic.RS1025.severity = warning # RS1026: Enable concurrent execution dotnet_diagnostic.RS1026.severity = warning # RS1027: Types marked with DiagnosticAnalyzerAttribute(s) should inherit from DiagnosticAnalyzer dotnet_diagnostic.RS1027.severity = warning # RS1028: Provide non-null 'customTags' value to diagnostic descriptor constructor dotnet_diagnostic.RS1028.severity = none # RS1029: Do not use reserved diagnostic IDs dotnet_diagnostic.RS1029.severity = warning # RS1030: Do not invoke Compilation.GetSemanticModel() method within a diagnostic analyzer dotnet_diagnostic.RS1030.severity = warning # RS1031: Define diagnostic title correctly dotnet_diagnostic.RS1031.severity = warning # RS1032: Define diagnostic message correctly dotnet_diagnostic.RS1032.severity = warning # RS1033: Define diagnostic description correctly dotnet_diagnostic.RS1033.severity = warning # RS1034: Prefer 'IsKind' for checking syntax kinds dotnet_diagnostic.RS1034.severity = warning # RS1035: Do not use APIs banned for analyzers dotnet_diagnostic.RS1035.severity = error # RS1036: Specify analyzer banned API enforcement setting dotnet_diagnostic.RS1036.severity = warning # RS1037: Add "CompilationEnd" custom tag to compilation end diagnostic descriptor dotnet_diagnostic.RS1037.severity = warning # RS1038: Compiler extensions should be implemented in assemblies with compiler-provided references dotnet_diagnostic.RS1038.severity = suggestion # RS2000: Add analyzer diagnostic IDs to analyzer release dotnet_diagnostic.RS2000.severity = warning # RS2001: Ensure up-to-date entry for analyzer diagnostic IDs are added to analyzer release dotnet_diagnostic.RS2001.severity = warning # RS2002: Do not add removed analyzer diagnostic IDs to unshipped analyzer release dotnet_diagnostic.RS2002.severity = warning # RS2003: Shipped diagnostic IDs that are no longer reported should have an entry in the 'Removed Rules' table in unshipped file dotnet_diagnostic.RS2003.severity = warning # RS2004: Diagnostic IDs marked as removed in analyzer release file should not be reported by analyzers dotnet_diagnostic.RS2004.severity = warning # RS2005: Remove duplicate entries for diagnostic ID in the same analyzer release dotnet_diagnostic.RS2005.severity = warning # RS2006: Remove duplicate entries for diagnostic ID between analyzer releases dotnet_diagnostic.RS2006.severity = warning # RS2007: Invalid entry in analyzer release file dotnet_diagnostic.RS2007.severity = warning # RS2008: Enable analyzer release tracking dotnet_diagnostic.RS2008.severity = warning # SA0001: XML comments dotnet_diagnostic.SA0001.severity = none # SA1000: Spacing around keywords dotnet_diagnostic.SA1000.severity = warning # SA1001: Commas should not be preceded by whitespace dotnet_diagnostic.SA1001.severity = warning # SA1002: Semicolons should not be preceded by a space dotnet_diagnostic.SA1002.severity = none # SA1003: Operator should not appear at the end of a line dotnet_diagnostic.SA1003.severity = none # SA1004: Documentation line should begin with a space dotnet_diagnostic.SA1004.severity = none # SA1005: Single line comment should begin with a space dotnet_diagnostic.SA1005.severity = none # SA1008: Opening parenthesis should not be preceded by a space dotnet_diagnostic.SA1008.severity = none # SA1009: Closing parenthesis should not be followed by a space dotnet_diagnostic.SA1009.severity = none # SA1010: Opening square brackets should not be preceded by a space dotnet_diagnostic.SA1010.severity = none # SA1011: Closing square bracket should be followed by a space dotnet_diagnostic.SA1011.severity = none # SA1012: Opening brace should be followed by a space dotnet_diagnostic.SA1012.severity = none # SA1013: Closing brace should be preceded by a space dotnet_diagnostic.SA1013.severity = none # SA1014: Opening generic brackets should not be preceded by a space dotnet_diagnostic.SA1014.severity = warning # SA1015: Closing generic bracket should not be followed by a space dotnet_diagnostic.SA1015.severity = none # SA1018: Nullable type symbol should not be preceded by a space dotnet_diagnostic.SA1018.severity = warning # SA1020: Increment symbol should not be preceded by a space dotnet_diagnostic.SA1020.severity = warning # SA1021: Negative sign should be preceded by a space dotnet_diagnostic.SA1021.severity = none # SA1023: Dereference symbol '*' should not be preceded by a space." dotnet_diagnostic.SA1023.severity = none # SA1024: Colon should be followed by a space dotnet_diagnostic.SA1024.severity = none # SA1025: Code should not contain multiple whitespace characters in a row dotnet_diagnostic.SA1025.severity = none # SA1026: Keyword followed by span or blank line dotnet_diagnostic.SA1026.severity = warning # SA1027: Tabs and spaces should be used correctly dotnet_diagnostic.SA1027.severity = warning # SA1028: Code should not contain trailing whitespace dotnet_diagnostic.SA1028.severity = warning # SA1100: Do not prefix calls with base unless local implementation exists dotnet_diagnostic.SA1100.severity = none # SA1101: Prefix local calls with this dotnet_diagnostic.SA1101.severity = none # SA1102: Query clause should follow previous clause dotnet_diagnostic.SA1102.severity = warning # SA1105: Query clauses spanning multiple lines should begin on own line dotnet_diagnostic.SA1105.severity = warning # SA1106: Code should not contain empty statements dotnet_diagnostic.SA1106.severity = none # SA1107: Code should not contain multiple statements on one line dotnet_diagnostic.SA1107.severity = none # SA1108: Block statements should not contain embedded comments dotnet_diagnostic.SA1108.severity = none # SA1110: Opening parenthesis or bracket should be on declaration line dotnet_diagnostic.SA1110.severity = none # SA1111: Closing parenthesis should be on line of last parameter dotnet_diagnostic.SA1111.severity = none # SA1113: Comma should be on the same line as previous parameter dotnet_diagnostic.SA1113.severity = warning # SA1114: Parameter list should follow declaration dotnet_diagnostic.SA1114.severity = none # SA1115: Parameter should begin on the line after the previous parameter dotnet_diagnostic.SA1115.severity = warning # SA1116: Split parameters should start on line after declaration dotnet_diagnostic.SA1116.severity = none # SA1117: Parameters should be on same line or separate lines dotnet_diagnostic.SA1117.severity = none # SA1118: Parameter should not span multiple lines dotnet_diagnostic.SA1118.severity = none # SA1119: Statement should not use unnecessary parenthesis dotnet_diagnostic.SA1119.severity = none # SA1120: Comments should contain text dotnet_diagnostic.SA1120.severity = none # SA1121: Use built-in type alias dotnet_diagnostic.SA1121.severity = warning # SA1122: Use string.Empty for empty strings dotnet_diagnostic.SA1122.severity = none # SA1123: Region should not be located within a code element dotnet_diagnostic.SA1123.severity = none # SA1124: Do not use regions dotnet_diagnostic.SA1124.severity = none # SA1125: Use shorthand for nullable types dotnet_diagnostic.SA1125.severity = none # SA1127: Generic type constraints should be on their own line dotnet_diagnostic.SA1127.severity = none # SA1128: Put constructor initializers on their own line dotnet_diagnostic.SA1128.severity = none # SA1129: Do not use default value type constructor dotnet_diagnostic.SA1129.severity = warning # SA1130: Use lambda syntax dotnet_diagnostic.SA1130.severity = none # SA1131: Constant values should appear on the right-hand side of comparisons dotnet_diagnostic.SA1131.severity = none # SA1132: Do not combine fields dotnet_diagnostic.SA1132.severity = none # SA1133: Do not combine attributes dotnet_diagnostic.SA1133.severity = none # SA1134: Each attribute should be placed on its own line of code dotnet_diagnostic.SA1134.severity = none # SA1135: Using directive should be qualified dotnet_diagnostic.SA1135.severity = none # SA1136: Enum values should be on separate lines dotnet_diagnostic.SA1136.severity = warning # SA1137: Elements should have the same indentation dotnet_diagnostic.SA1137.severity = none # SA1139: Use literal suffix notation instead of casting dotnet_diagnostic.SA1139.severity = none # SA1141: Use tuple syntax dotnet_diagnostic.SA1141.severity = warning # SA1142: Refer to tuple elements by name dotnet_diagnostic.SA1142.severity = warning # SA1200: Using directive should appear within a namespace declaration dotnet_diagnostic.SA1200.severity = none # SA1201: Elements should appear in the correct order dotnet_diagnostic.SA1201.severity = none # SA1202: Elements should be ordered by access dotnet_diagnostic.SA1202.severity = none # SA1203: Constants should appear before fields dotnet_diagnostic.SA1203.severity = none # SA1204: Static elements should appear before instance elements dotnet_diagnostic.SA1204.severity = none # SA1205: Partial elements should declare an access modifier dotnet_diagnostic.SA1205.severity = warning # SA1206: Keyword ordering dotnet_diagnostic.SA1206.severity = warning # SA1208: Using directive ordering dotnet_diagnostic.SA1208.severity = none # SA1209: Using alias directives should be placed after all using namespace directives dotnet_diagnostic.SA1209.severity = none # SA1210: Using directives should be ordered alphabetically by the namespaces dotnet_diagnostic.SA1210.severity = none # SA1211: Using alias directive ordering dotnet_diagnostic.SA1211.severity = none # SA1212: A get accessor appears after a set accessor within a property or indexer dotnet_diagnostic.SA1212.severity = warning # SA1214: Readonly fields should appear before non-readonly fields dotnet_diagnostic.SA1214.severity = none # SA1216: Using static directives should be placed at the correct location dotnet_diagnostic.SA1216.severity = none # SA1300: Element should begin with an uppercase letter dotnet_diagnostic.SA1300.severity = none # SA1302: Interface names should begin with I dotnet_diagnostic.SA1302.severity = warning # SA1303: Const field names should begin with upper-case letter dotnet_diagnostic.SA1303.severity = none # SA1304: Non-private readonly fields should begin with upper-case letter dotnet_diagnostic.SA1304.severity = none # SA1306: Field should begin with lower-case letter dotnet_diagnostic.SA1306.severity = none # SA1307: Field should begin with upper-case letter dotnet_diagnostic.SA1307.severity = none # SA1308: Field should not begin with the prefix 's_' dotnet_diagnostic.SA1308.severity = none # SA1309: Field names should not begin with underscore dotnet_diagnostic.SA1309.severity = none # SA1310: Field should not contain an underscore dotnet_diagnostic.SA1310.severity = none # SA1311: Static readonly fields should begin with upper-case letter dotnet_diagnostic.SA1311.severity = none # SA1312: Variable should begin with lower-case letter dotnet_diagnostic.SA1312.severity = none # SA1313: Parameter should begin with lower-case letter dotnet_diagnostic.SA1313.severity = none # SA1314: Type parameter names should begin with T dotnet_diagnostic.SA1314.severity = none # SA1316: Tuple element names should use correct casing dotnet_diagnostic.SA1316.severity = none # SA1400: Member should declare an access modifier dotnet_diagnostic.SA1400.severity = warning # SA1401: Fields should be private dotnet_diagnostic.SA1401.severity = none # SA1402: File may only contain a single type dotnet_diagnostic.SA1402.severity = none # SA1403: File may only contain a single namespace dotnet_diagnostic.SA1403.severity = none # SA1404: Code analysis suppression should have justification dotnet_diagnostic.SA1404.severity = warning # SA1405: Debug.Assert should provide message text dotnet_diagnostic.SA1405.severity = none # SA1407: Arithmetic expressions should declare precedence dotnet_diagnostic.SA1407.severity = none # SA1408: Conditional expressions should declare precedence dotnet_diagnostic.SA1408.severity = none # SA1410: Remove delegate parens when possible dotnet_diagnostic.SA1410.severity = warning # SA1411: Attribute constructor shouldn't use unnecessary parenthesis dotnet_diagnostic.SA1411.severity = warning # SA1413: Use trailing comma in multi-line initializers dotnet_diagnostic.SA1413.severity = none # SA1414: Tuple types in signatures should have element names dotnet_diagnostic.SA1414.severity = none # SA1500: Braces for multi-line statements should not share line dotnet_diagnostic.SA1500.severity = none # SA1501: Statement should not be on a single line dotnet_diagnostic.SA1501.severity = none # SA1502: Element should not be on a single line dotnet_diagnostic.SA1502.severity = none # SA1503: Braces should not be omitted dotnet_diagnostic.SA1503.severity = none # SA1504: All accessors should be single-line or multi-line dotnet_diagnostic.SA1504.severity = none # SA1505: An opening brace should not be followed by a blank line dotnet_diagnostic.SA1505.severity = none # SA1506: Element documentation headers should not be followed by blank line dotnet_diagnostic.SA1506.severity = none # SA1507: Code should not contain multiple blank lines in a row dotnet_diagnostic.SA1507.severity = none # SA1508: A closing brace should not be preceded by a blank line dotnet_diagnostic.SA1508.severity = none # SA1509: Opening braces should not be preceded by blank line dotnet_diagnostic.SA1509.severity = none # SA1510: 'else' statement should not be preceded by a blank line dotnet_diagnostic.SA1510.severity = none # SA1512: Single-line comments should not be followed by blank line dotnet_diagnostic.SA1512.severity = none # SA1513: Closing brace should be followed by blank line dotnet_diagnostic.SA1513.severity = none # SA1514: Element documentation header should be preceded by blank line dotnet_diagnostic.SA1514.severity = none # SA1515: Single-line comment should be preceded by blank line dotnet_diagnostic.SA1515.severity = none # SA1516: Elements should be separated by blank line dotnet_diagnostic.SA1516.severity = none # SA1517: Code should not contain blank lines at start of file dotnet_diagnostic.SA1517.severity = warning # SA1518: Code should not contain blank lines at the end of the file dotnet_diagnostic.SA1518.severity = warning # SA1519: Braces should not be omitted from multi-line child statement dotnet_diagnostic.SA1519.severity = none # SA1520: Use braces consistently dotnet_diagnostic.SA1520.severity = none # SA1600: Elements should be documented dotnet_diagnostic.SA1600.severity = none # SA1601: Partial elements should be documented dotnet_diagnostic.SA1601.severity = none # SA1602: Enumeration items should be documented dotnet_diagnostic.SA1602.severity = none # SA1604: Element documentation should have summary dotnet_diagnostic.SA1604.severity = none # SA1605: Partial element documentation should have summary dotnet_diagnostic.SA1605.severity = none # SA1606: Element documentation should have summary text dotnet_diagnostic.SA1606.severity = none # SA1608: Element documentation should not have default summary dotnet_diagnostic.SA1608.severity = none # SA1610: Property documentation should have value text dotnet_diagnostic.SA1610.severity = none # SA1611: The documentation for parameter 'message' is missing dotnet_diagnostic.SA1611.severity = none # SA1612: The parameter documentation is at incorrect position dotnet_diagnostic.SA1612.severity = none # SA1614: Element parameter documentation should have text dotnet_diagnostic.SA1614.severity = none # SA1615: Element return value should be documented dotnet_diagnostic.SA1615.severity = none # SA1616: Element return value documentation should have text dotnet_diagnostic.SA1616.severity = none # SA1618: The documentation for type parameter is missing dotnet_diagnostic.SA1618.severity = none # SA1619: The documentation for type parameter is missing dotnet_diagnostic.SA1619.severity = none # SA1622: Generic type parameter documentation should have text dotnet_diagnostic.SA1622.severity = none # SA1623: Property documentation text dotnet_diagnostic.SA1623.severity = none # SA1624: Because the property only contains a visible get accessor, the documentation summary text should begin with 'Gets' dotnet_diagnostic.SA1624.severity = none # SA1625: Element documentation should not be copied and pasted dotnet_diagnostic.SA1625.severity = none # SA1626: Single-line comments should not use documentation style slashes dotnet_diagnostic.SA1626.severity = none # SA1627: The documentation text within the \'exception\' tag should not be empty dotnet_diagnostic.SA1627.severity = none # SA1629: Documentation text should end with a period dotnet_diagnostic.SA1629.severity = none # SA1633: File should have header dotnet_diagnostic.SA1633.severity = none # SA1642: Constructor summary documentation should begin with standard text dotnet_diagnostic.SA1642.severity = none # SA1643: Destructor summary documentation should begin with standard text dotnet_diagnostic.SA1643.severity = none # SA1649: File name should match first type name dotnet_diagnostic.SA1649.severity = none # IDE0001: Simplify name dotnet_diagnostic.IDE0001.severity = suggestion # IDE0002: Simplify member access dotnet_diagnostic.IDE0002.severity = suggestion # IDE0003: Remove this or Me qualification dotnet_diagnostic.IDE0003.severity = suggestion # IDE0004: Remove Unnecessary Cast dotnet_diagnostic.IDE0004.severity = suggestion # IDE0005: Using directive is unnecessary. dotnet_diagnostic.IDE0005.severity = suggestion # IDE0007: Use implicit type dotnet_diagnostic.IDE0007.severity = silent # IDE0008: Use explicit type dotnet_diagnostic.IDE0008.severity = suggestion # IDE0009: Add this or Me qualification dotnet_diagnostic.IDE0009.severity = silent # IDE0010: Add missing cases dotnet_diagnostic.IDE0010.severity = silent # IDE0011: Add braces dotnet_diagnostic.IDE0011.severity = silent # IDE0016: Use 'throw' expression dotnet_diagnostic.IDE0016.severity = silent # IDE0017: Simplify object initialization dotnet_diagnostic.IDE0017.severity = suggestion # IDE0018: Inline variable declaration dotnet_diagnostic.IDE0018.severity = suggestion # IDE0019: Use pattern matching to avoid as followed by a null check dotnet_diagnostic.IDE0019.severity = suggestion # IDE0020: Use pattern matching to avoid is check followed by a cast (with variable) dotnet_diagnostic.IDE0020.severity = warning # IDE0021: Use expression body for constructors dotnet_diagnostic.IDE0021.severity = silent # IDE0022: Use expression body for methods dotnet_diagnostic.IDE0022.severity = silent # IDE0023: Use expression body for operators dotnet_diagnostic.IDE0023.severity = silent # IDE0024: Use expression body for operators dotnet_diagnostic.IDE0024.severity = silent # IDE0025: Use expression body for properties dotnet_diagnostic.IDE0025.severity = silent # IDE0026: Use expression body for indexers dotnet_diagnostic.IDE0026.severity = silent # IDE0027: Use expression body for accessors dotnet_diagnostic.IDE0027.severity = silent # IDE0028: Simplify collection initialization dotnet_diagnostic.IDE0028.severity = suggestion # IDE0029: Use coalesce expression dotnet_diagnostic.IDE0029.severity = warning # IDE0030: Use coalesce expression dotnet_diagnostic.IDE0030.severity = warning # IDE0031: Use null propagation dotnet_diagnostic.IDE0031.severity = warning # IDE0032: Use auto property dotnet_diagnostic.IDE0032.severity = silent # IDE0033: Use explicitly provided tuple name dotnet_diagnostic.IDE0033.severity = suggestion # IDE0034: Simplify 'default' expression dotnet_diagnostic.IDE0034.severity = suggestion # IDE0035: Remove unreachable code dotnet_diagnostic.IDE0035.severity = suggestion # IDE0036: Order modifiers dotnet_diagnostic.IDE0036.severity = warning # IDE0037: Use inferred member name dotnet_diagnostic.IDE0037.severity = silent # IDE0038: Use pattern matching to avoid is check followed by a cast (without variable) dotnet_diagnostic.IDE0038.severity = suggestion # IDE0039: Use local function dotnet_diagnostic.IDE0039.severity = suggestion # IDE0040: Add accessibility modifiers dotnet_diagnostic.IDE0040.severity = suggestion # IDE0041: Use 'is null' check dotnet_diagnostic.IDE0041.severity = warning # IDE0042: Deconstruct variable declaration dotnet_diagnostic.IDE0042.severity = silent # IDE0043: Invalid format string dotnet_diagnostic.IDE0043.severity = warning # IDE0044: Add readonly modifier dotnet_diagnostic.IDE0044.severity = suggestion # IDE0045: Use conditional expression for assignment dotnet_diagnostic.IDE0045.severity = suggestion # IDE0046: Use conditional expression for return dotnet_diagnostic.IDE0046.severity = suggestion # IDE0047: Remove unnecessary parentheses dotnet_diagnostic.IDE0047.severity = silent # IDE0048: Add parentheses for clarity dotnet_diagnostic.IDE0048.severity = silent # IDE0049: Use language keywords instead of framework type names for type references dotnet_diagnostic.IDE0049.severity = warning # IDE0051: Remove unused private members dotnet_diagnostic.IDE0051.severity = suggestion # IDE0052: Remove unread private members dotnet_diagnostic.IDE0052.severity = suggestion # IDE0053: Use expression body for lambdas dotnet_diagnostic.IDE0053.severity = silent # IDE0054: Use compound assignment dotnet_diagnostic.IDE0054.severity = warning # IDE0055: Fix formatting dotnet_diagnostic.IDE0055.severity = suggestion # IDE0056: Use index operator dotnet_diagnostic.IDE0056.severity = suggestion # IDE0057: Use range operator dotnet_diagnostic.IDE0057.severity = suggestion # IDE0058: Expression value is never used dotnet_diagnostic.IDE0058.severity = silent # IDE0059: Unnecessary assignment of a value dotnet_diagnostic.IDE0059.severity = warning # IDE0060: Remove unused parameter dotnet_diagnostic.IDE0060.severity = warning dotnet_code_quality_unused_parameters = non_public # IDE0061: Use expression body for local functions dotnet_diagnostic.IDE0061.severity = silent # IDE0062: Make local function 'static' dotnet_diagnostic.IDE0062.severity = warning # IDE0063: Use simple 'using' statement dotnet_diagnostic.IDE0063.severity = silent # IDE0064: Make readonly fields writable dotnet_diagnostic.IDE0064.severity = silent # IDE0065: Misplaced using directive dotnet_diagnostic.IDE0065.severity = warning # IDE0066: Convert switch statement to expression dotnet_diagnostic.IDE0066.severity = suggestion # IDE0070: Use 'System.HashCode' dotnet_diagnostic.IDE0070.severity = suggestion # IDE0071: Simplify interpolation dotnet_diagnostic.IDE0071.severity = warning # IDE0072: Add missing cases dotnet_diagnostic.IDE0072.severity = silent # IDE0073: The file header is missing or not located at the top of the file dotnet_diagnostic.IDE0073.severity = error # IDE0074: Use compound assignment dotnet_diagnostic.IDE0074.severity = warning # IDE0075: Simplify conditional expression dotnet_diagnostic.IDE0075.severity = silent # IDE0076: Invalid global 'SuppressMessageAttribute' dotnet_diagnostic.IDE0076.severity = warning # IDE0077: Avoid legacy format target in 'SuppressMessageAttribute' dotnet_diagnostic.IDE0077.severity = silent # IDE0078: Use pattern matching dotnet_diagnostic.IDE0078.severity = suggestion # IDE0079: Remove unnecessary suppression dotnet_diagnostic.IDE0079.severity = suggestion # IDE0080: Remove unnecessary suppression operator dotnet_diagnostic.IDE0080.severity = warning # IDE0081: Remove unnecessary suppression operator dotnet_diagnostic.IDE0081.severity = none # IDE0082: 'typeof' can be converted to 'nameof' dotnet_diagnostic.IDE0082.severity = warning # IDE0083: Use pattern matching dotnet_diagnostic.IDE0083.severity = silent # IDE0084: Use pattern matching (IsNot operator) dotnet_diagnostic.IDE0084.severity = none # IDE0090: Use 'new(...)' dotnet_diagnostic.IDE0090.severity = silent # IDE0100: Remove redundant equality dotnet_diagnostic.IDE0100.severity = warning # IDE0110: Remove unnecessary discard dotnet_diagnostic.IDE0110.severity = warning # IDE0120: Simplify LINQ expression dotnet_diagnostic.IDE0120.severity = none # IDE0130: Namespace does not match folder structure dotnet_diagnostic.IDE0130.severity = silent # IDE0140: Simplify object creation dotnet_diagnostic.IDE0140.severity = none # IDE0150: Prefer 'null' check over type check dotnet_diagnostic.IDE0150.severity = silent # IDE0160: Convert to block scoped namespace dotnet_diagnostic.IDE0160.severity = silent # IDE0161: Convert to file-scoped namespace dotnet_diagnostic.IDE0161.severity = silent # IDE0170: Simplify property pattern dotnet_diagnostic.IDE0170.severity = warning # IDE0180: Use tuple swap dotnet_diagnostic.IDE0180.severity = suggestion # IDE0200: Remove unnecessary lambda expression dotnet_diagnostic.IDE0200.severity = warning # IDE0210: Use top-level statements dotnet_diagnostic.IDE0210.severity = none # IDE0211: Convert to 'Program.Main' style program dotnet_diagnostic.IDE0211.severity = none # IDE0220: foreach cast dotnet_diagnostic.IDE0220.severity = silent # IDE0230: Use UTF8 string literal dotnet_diagnostic.IDE0230.severity = suggestion # IDE0240: Remove redundant nullable directive dotnet_diagnostic.IDE0240.severity = suggestion # IDE0241: Remove unnecessary nullable directive dotnet_diagnostic.IDE0241.severity = suggestion # IDE0250: Make struct readonly dotnet_diagnostic.IDE0250.severity = suggestion # IDE0251: Make member readonly dotnet_diagnostic.IDE0251.severity = suggestion # IDE0260: Use pattern matching dotnet_diagnostic.IDE0260.severity = suggestion # IDE0270: Use coalesce expression dotnet_diagnostic.IDE0270.severity = suggestion # IDE0280: Use 'nameof' dotnet_diagnostic.IDE0280.severity = warning # IDE0290: Use primary constructor dotnet_diagnostic.IDE0290.severity = suggestion # IDE0300: Use collection expression for array dotnet_diagnostic.IDE0300.severity = suggestion # IDE0301: Use collection expression for empty dotnet_diagnostic.IDE0301.severity = suggestion # IDE0302: Use collection expression for stackalloc dotnet_diagnostic.IDE0302.severity = suggestion # IDE0303: Use collection expression for Create() dotnet_diagnostic.IDE0303.severity = suggestion # IDE0304: Use collection expression for builder dotnet_diagnostic.IDE0304.severity = suggestion # IDE0305: Use collection expression for fluent dotnet_diagnostic.IDE0305.severity = suggestion # IDE1005: Delegate invocation can be simplified. dotnet_diagnostic.IDE1005.severity = warning # IDE1006: Naming styles dotnet_diagnostic.IDE1006.severity = silent # IDE2000: Allow multiple blank lines dotnet_diagnostic.IDE2000.severity = silent # IDE2001: Embedded statements must be on their own line dotnet_diagnostic.IDE2001.severity = silent # IDE2002: Consecutive braces must not have blank line between them dotnet_diagnostic.IDE2002.severity = silent # IDE2003: Allow statement immediately after block dotnet_diagnostic.IDE2003.severity = silent # IDE2004: Blank line not allowed after constructor initializer colon dotnet_diagnostic.IDE2004.severity = silent # IDE2005: Blank line not allowed after conditional expression token dotnet_diagnostic.IDE2005.severity = silent # IDE2006: Blank line not allowed after arrow expression clause token dotnet_diagnostic.IDE2006.severity = silent ================================================ FILE: eng/CodeAnalysis.test.globalconfig ================================================ is_global = true # AD0001: Analyzer threw an exception dotnet_diagnostic.AD0001.severity = none # BCL0001: Ensure minimum API surface is respected dotnet_diagnostic.BCL0001.severity = none # BCL0010: AppContext default value expected to be true dotnet_diagnostic.BCL0010.severity = none # BCL0011: AppContext default value defined in if statement with incorrect pattern dotnet_diagnostic.BCL0011.severity = none # BCL0012: AppContext default value defined in if statement at root of switch case dotnet_diagnostic.BCL0012.severity = none # BCL0015: Invalid P/Invoke call dotnet_diagnostic.BCL0015.severity = none # BCL0020: Invalid SR.Format call dotnet_diagnostic.BCL0020.severity = none # SYSLIB1045: Convert to 'GeneratedRegexAttribute'. dotnet_diagnostic.SYSLIB1045.severity = none # SYSLIB1054: Use 'LibraryImportAttribute' instead of 'DllImportAttribute' to generate P/Invoke marshalling code at compile time dotnet_diagnostic.SYSLIB1054.severity = none # SYSLIB1055: Invalid 'CustomMarshallerAttribute' usage dotnet_diagnostic.SYSLIB1055.severity = error # SYSLIB1056:Specified marshaller type is invalid dotnet_diagnostic.SYSLIB1056.severity = error # SYSLIB1057: Marshaller type does not have the required shape dotnet_diagnostic.SYSLIB1057.severity = error # SYSLIB1058: Invalid 'NativeMarshallingAttribute' usage dotnet_diagnostic.SYSLIB1058.severity = error # SYSLIB1060: Specified marshaller type is invalid dotnet_diagnostic.SYSLIB1060.severity = error # SYSLIB1061: Marshaller type has incompatible method signatures dotnet_diagnostic.SYSLIB1061.severity = error # CA1000: Do not declare static members on generic types dotnet_diagnostic.CA1000.severity = none # CA1001: Types that own disposable fields should be disposable dotnet_diagnostic.CA1001.severity = none # CA1002: Do not expose generic lists dotnet_diagnostic.CA1002.severity = none # CA1003: Use generic event handler instances dotnet_diagnostic.CA1003.severity = none # CA1005: Avoid excessive parameters on generic types dotnet_diagnostic.CA1005.severity = none # CA1008: Enums should have zero value dotnet_diagnostic.CA1008.severity = none # CA1010: Generic interface should also be implemented dotnet_diagnostic.CA1010.severity = none # CA1012: Abstract types should not have public constructors dotnet_diagnostic.CA1012.severity = none # CA1014: Mark assemblies with CLSCompliant dotnet_diagnostic.CA1014.severity = none # CA1016: Mark assemblies with assembly version dotnet_diagnostic.CA1016.severity = none # CA1017: Mark assemblies with ComVisible dotnet_diagnostic.CA1017.severity = none # CA1018: Mark attributes with AttributeUsageAttribute dotnet_diagnostic.CA1018.severity = none # CA1019: Define accessors for attribute arguments dotnet_diagnostic.CA1019.severity = none # CA1021: Avoid out parameters dotnet_diagnostic.CA1021.severity = none # CA1024: Use properties where appropriate dotnet_diagnostic.CA1024.severity = none # CA1027: Mark enums with FlagsAttribute dotnet_diagnostic.CA1027.severity = none # CA1028: Enum Storage should be Int32 dotnet_diagnostic.CA1028.severity = none # CA1030: Use events where appropriate dotnet_diagnostic.CA1030.severity = none # CA1031: Do not catch general exception types dotnet_diagnostic.CA1031.severity = none # CA1032: Implement standard exception constructors dotnet_diagnostic.CA1032.severity = none # CA1033: Interface methods should be callable by child types dotnet_diagnostic.CA1033.severity = none # CA1034: Nested types should not be visible dotnet_diagnostic.CA1034.severity = none # CA1036: Override methods on comparable types dotnet_diagnostic.CA1036.severity = none # CA1040: Avoid empty interfaces dotnet_diagnostic.CA1040.severity = none # CA1041: Provide ObsoleteAttribute message dotnet_diagnostic.CA1041.severity = none # CA1043: Use Integral Or String Argument For Indexers dotnet_diagnostic.CA1043.severity = none # CA1044: Properties should not be write only dotnet_diagnostic.CA1044.severity = none # CA1045: Do not pass types by reference dotnet_diagnostic.CA1045.severity = none # CA1046: Do not overload equality operator on reference types dotnet_diagnostic.CA1046.severity = none # CA1047: Do not declare protected member in sealed type dotnet_diagnostic.CA1047.severity = none # CA1050: Declare types in namespaces dotnet_diagnostic.CA1050.severity = none # CA1051: Do not declare visible instance fields dotnet_diagnostic.CA1051.severity = none # CA1052: Static holder types should be Static or NotInheritable dotnet_diagnostic.CA1052.severity = none # CA1054: URI-like parameters should not be strings dotnet_diagnostic.CA1054.severity = none # CA1055: URI-like return values should not be strings dotnet_diagnostic.CA1055.severity = none # CA1056: URI-like properties should not be strings dotnet_diagnostic.CA1056.severity = none # CA1058: Types should not extend certain base types dotnet_diagnostic.CA1058.severity = none # CA1060: Move pinvokes to native methods class dotnet_diagnostic.CA1060.severity = none # CA1061: Do not hide base class methods dotnet_diagnostic.CA1061.severity = none # CA1062: Validate arguments of public methods dotnet_diagnostic.CA1062.severity = none # CA1063: Implement IDisposable Correctly dotnet_diagnostic.CA1063.severity = none # CA1064: Exceptions should be public dotnet_diagnostic.CA1064.severity = none # CA1065: Do not raise exceptions in unexpected locations dotnet_diagnostic.CA1065.severity = none # CA1066: Implement IEquatable when overriding Object.Equals dotnet_diagnostic.CA1066.severity = none # CA1067: Override Object.Equals(object) when implementing IEquatable dotnet_diagnostic.CA1067.severity = none # CA1068: CancellationToken parameters must come last dotnet_diagnostic.CA1068.severity = none # CA1069: Enums values should not be duplicated dotnet_diagnostic.CA1069.severity = none # CA1070: Do not declare event fields as virtual dotnet_diagnostic.CA1070.severity = none # CA1200: Avoid using cref tags with a prefix dotnet_diagnostic.CA1200.severity = none # CA1303: Do not pass literals as localized parameters dotnet_diagnostic.CA1303.severity = none # CA1304: Specify CultureInfo dotnet_diagnostic.CA1304.severity = none # CA1305: Specify IFormatProvider dotnet_diagnostic.CA1305.severity = none # CA1307: Specify StringComparison for clarity dotnet_diagnostic.CA1307.severity = none # CA1308: Normalize strings to uppercase dotnet_diagnostic.CA1308.severity = none # CA1309: Use ordinal string comparison dotnet_diagnostic.CA1309.severity = none # CA1310: Specify StringComparison for correctness dotnet_diagnostic.CA1310.severity = none # CA1311: Specify a culture or use an invariant version dotnet_diagnostic.CA1311.severity = none # CA1401: P/Invokes should not be visible dotnet_diagnostic.CA1401.severity = none # CA1416: Validate platform compatibility dotnet_diagnostic.CA1416.severity = none # CA1417: Do not use 'OutAttribute' on string parameters for P/Invokes dotnet_diagnostic.CA1417.severity = none # CA1418: Use valid platform string dotnet_diagnostic.CA1418.severity = none # CA1419: Provide a parameterless constructor that is as visible as the containing type for concrete types derived from 'System.Runtime.InteropServices.SafeHandle' dotnet_diagnostic.CA1419.severity = none # CA1420: Property, type, or attribute requires runtime marshalling dotnet_diagnostic.CA1420.severity = none # CA1421: This method uses runtime marshalling even when the 'DisableRuntimeMarshallingAttribute' is applied dotnet_diagnostic.CA1421.severity = none # CA1422: Validate platform compatibility dotnet_diagnostic.CA1422.severity = none # CA1501: Avoid excessive inheritance dotnet_diagnostic.CA1501.severity = none # CA1502: Avoid excessive complexity dotnet_diagnostic.CA1502.severity = none # CA1505: Avoid unmaintainable code dotnet_diagnostic.CA1505.severity = none # CA1506: Avoid excessive class coupling dotnet_diagnostic.CA1506.severity = none # CA1507: Use nameof to express symbol names dotnet_diagnostic.CA1507.severity = none # CA1508: Avoid dead conditional code dotnet_diagnostic.CA1508.severity = none # CA1509: Invalid entry in code metrics rule specification file dotnet_diagnostic.CA1509.severity = none # CA1510: Use ArgumentNullException throw helper dotnet_diagnostic.CA1510.severity = none # CA1511: Use ArgumentException throw helper dotnet_diagnostic.CA1511.severity = none # CA1512: Use ArgumentOutOfRangeException throw helper dotnet_diagnostic.CA1512.severity = none # CA1513: Use ObjectDisposedException throw helper dotnet_diagnostic.CA1513.severity = none # CA1514: Avoid redundant length argument dotnet_diagnostic.CA1514.severity = none # CA1515: Consider making public types internal dotnet_diagnostic.CA1515.severity = none # CA1700: Do not name enum values 'Reserved' dotnet_diagnostic.CA1700.severity = none # CA1707: Identifiers should not contain underscores dotnet_diagnostic.CA1707.severity = none # CA1708: Identifiers should differ by more than case dotnet_diagnostic.CA1708.severity = none # CA1710: Identifiers should have correct suffix dotnet_diagnostic.CA1710.severity = none # CA1711: Identifiers should not have incorrect suffix dotnet_diagnostic.CA1711.severity = none # CA1712: Do not prefix enum values with type name dotnet_diagnostic.CA1712.severity = none # CA1713: Events should not have 'Before' or 'After' prefix dotnet_diagnostic.CA1713.severity = none # CA1715: Identifiers should have correct prefix dotnet_diagnostic.CA1715.severity = none # CA1716: Identifiers should not match keywords dotnet_diagnostic.CA1716.severity = none # CA1720: Identifier contains type name dotnet_diagnostic.CA1720.severity = none # CA1721: Property names should not match get methods dotnet_diagnostic.CA1721.severity = none # CA1724: Type names should not match namespaces dotnet_diagnostic.CA1724.severity = none # CA1725: Parameter names should match base declaration dotnet_diagnostic.CA1725.severity = none # CA1727: Use PascalCase for named placeholders dotnet_diagnostic.CA1727.severity = none # CA1802: Use literals where appropriate dotnet_diagnostic.CA1802.severity = none # CA1805: Do not initialize unnecessarily dotnet_diagnostic.CA1805.severity = none # CA1806: Do not ignore method results dotnet_diagnostic.CA1806.severity = none # CA1810: Initialize reference type static fields inline dotnet_diagnostic.CA1810.severity = none # CA1812: Avoid uninstantiated internal classes dotnet_diagnostic.CA1812.severity = none # CA1813: Avoid unsealed attributes dotnet_diagnostic.CA1813.severity = none # CA1814: Prefer jagged arrays over multidimensional dotnet_diagnostic.CA1814.severity = none # CA1815: Override equals and operator equals on value types dotnet_diagnostic.CA1815.severity = none # CA1816: Dispose methods should call SuppressFinalize dotnet_diagnostic.CA1816.severity = none # CA1819: Properties should not return arrays dotnet_diagnostic.CA1819.severity = none # CA1820: Test for empty strings using string length dotnet_diagnostic.CA1820.severity = none # CA1821: Remove empty Finalizers dotnet_diagnostic.CA1821.severity = none # CA1822: Mark members as static dotnet_diagnostic.CA1822.severity = none # CA1823: Avoid unused private fields dotnet_diagnostic.CA1823.severity = none # CA1824: Mark assemblies with NeutralResourcesLanguageAttribute dotnet_diagnostic.CA1824.severity = none # CA1825: Avoid zero-length array allocations. dotnet_diagnostic.CA1825.severity = none # CA1826: Do not use Enumerable methods on indexable collections dotnet_diagnostic.CA1826.severity = none # CA1827: Do not use Count() or LongCount() when Any() can be used dotnet_diagnostic.CA1827.severity = none # CA1828: Do not use CountAsync() or LongCountAsync() when AnyAsync() can be used dotnet_diagnostic.CA1828.severity = none # CA1829: Use Length/Count property instead of Count() when available dotnet_diagnostic.CA1829.severity = none # CA1830: Prefer strongly-typed Append and Insert method overloads on StringBuilder dotnet_diagnostic.CA1830.severity = none # CA1831: Use AsSpan or AsMemory instead of Range-based indexers when appropriate dotnet_diagnostic.CA1831.severity = none # CA1832: Use AsSpan or AsMemory instead of Range-based indexers when appropriate dotnet_diagnostic.CA1832.severity = none # CA1833: Use AsSpan or AsMemory instead of Range-based indexers when appropriate dotnet_diagnostic.CA1833.severity = none # CA1834: Consider using 'StringBuilder.Append(char)' when applicable dotnet_diagnostic.CA1834.severity = none # CA1835: Prefer the 'Memory'-based overloads for 'ReadAsync' and 'WriteAsync' dotnet_diagnostic.CA1835.severity = none # CA1836: Prefer IsEmpty over Count dotnet_diagnostic.CA1836.severity = none # CA1837: Use 'Environment.ProcessId' dotnet_diagnostic.CA1837.severity = none # CA1838: Avoid 'StringBuilder' parameters for P/Invokes dotnet_diagnostic.CA1838.severity = none # CA1839: Use 'Environment.ProcessPath' dotnet_diagnostic.CA1839.severity = none # CA1840: Use 'Environment.CurrentManagedThreadId' dotnet_diagnostic.CA1840.severity = none # CA1841: Prefer Dictionary.Contains methods dotnet_diagnostic.CA1841.severity = none # CA1842: Do not use 'WhenAll' with a single task dotnet_diagnostic.CA1842.severity = none # CA1843: Do not use 'WaitAll' with a single task dotnet_diagnostic.CA1843.severity = none # CA1844: Provide memory-based overrides of async methods when subclassing 'Stream' dotnet_diagnostic.CA1844.severity = none # CA1845: Use span-based 'string.Concat' dotnet_diagnostic.CA1845.severity = none # CA1846: Prefer 'AsSpan' over 'Substring' dotnet_diagnostic.CA1846.severity = none # CA1847: Use char literal for a single character lookup dotnet_diagnostic.CA1847.severity = none # CA1848: Use the LoggerMessage delegates dotnet_diagnostic.CA1848.severity = none # CA1849: Call async methods when in an async method dotnet_diagnostic.CA1849.severity = none # CA1850: Prefer static 'HashData' method over 'ComputeHash' dotnet_diagnostic.CA1850.severity = none # CA1851: Possible multiple enumerations of 'IEnumerable' collection dotnet_diagnostic.CA1851.severity = none # CA1852: Seal internal types dotnet_diagnostic.CA1852.severity = none # CA1853: Unnecessary call to 'Dictionary.ContainsKey(key)' dotnet_diagnostic.CA1853.severity = none # CA1854: Prefer the 'IDictionary.TryGetValue(TKey, out TValue)' method dotnet_diagnostic.CA1854.severity = none # CA1855: Prefer 'Clear' over 'Fill' dotnet_diagnostic.CA1855.severity = none # CA1856: Incorrect usage of ConstantExpected attribute dotnet_diagnostic.CA1856.severity = none # CA1857: A constant is expected for the parameter dotnet_diagnostic.CA1857.severity = none # CA1858: Use 'StartsWith' instead of 'IndexOf' dotnet_diagnostic.CA1858.severity = none # CA1859: Use concrete types when possible for improved performance dotnet_diagnostic.CA1859.severity = none # CA1860: Avoid using 'Enumerable.Any()' extension method dotnet_diagnostic.CA1860.severity = none # CA1861: Avoid constant arrays as arguments dotnet_diagnostic.CA1861.severity = none # CA1862: Prefer using 'StringComparer'/'StringComparison' to perform case-insensitive string comparisons dotnet_diagnostic.CA1862.severity = none # CA1863: Use 'CompositeFormat' dotnet_diagnostic.CA1863.severity = none # CA1864: Prefer the 'IDictionary.TryAdd(TKey, TValue)' method dotnet_diagnostic.CA1864.severity = none # CA1865: Use char overload dotnet_diagnostic.CA1865.severity = none # CA1866: Use char overload dotnet_diagnostic.CA1866.severity = none # CA1867: Use char overload dotnet_diagnostic.CA1867.severity = none # CA1868: Unnecessary call to 'Contains' for sets dotnet_diagnostic.CA1868.severity = none # CA1869: Cache and reuse 'JsonSerializerOptions' instances dotnet_diagnostic.CA1869.severity = none # CA1870: Use a cached 'SearchValues' instance dotnet_diagnostic.CA1870.severity = none # CA1871: Do not pass a nullable struct to 'ArgumentNullException.ThrowIfNull' dotnet_diagnostic.CA1871.severity = none # CA1872: Prefer 'Convert.ToHexString' and 'Convert.ToHexStringLower' over call chains based on 'BitConverter.ToString' dotnet_diagnostic.CA1872.severity = none # CA2000: Dispose objects before losing scope dotnet_diagnostic.CA2000.severity = none # CA2002: Do not lock on objects with weak identity dotnet_diagnostic.CA2002.severity = none # CA2007: Consider calling ConfigureAwait on the awaited task dotnet_diagnostic.CA2007.severity = none # CA2008: Do not create tasks without passing a TaskScheduler dotnet_diagnostic.CA2008.severity = none # CA2009: Do not call ToImmutableCollection on an ImmutableCollection value dotnet_diagnostic.CA2009.severity = none # CA2011: Avoid infinite recursion dotnet_diagnostic.CA2011.severity = none # CA2012: Use ValueTasks correctly dotnet_diagnostic.CA2012.severity = none # CA2013: Do not use ReferenceEquals with value types dotnet_diagnostic.CA2013.severity = none # CA2014: Do not use stackalloc in loops. dotnet_diagnostic.CA2014.severity = none # CA2015: Do not define finalizers for types derived from MemoryManager dotnet_diagnostic.CA2015.severity = none # CA2016: Forward the 'CancellationToken' parameter to methods dotnet_diagnostic.CA2016.severity = none # CA2017: Parameter count mismatch dotnet_diagnostic.CA2017.severity = none # CA2018: 'Buffer.BlockCopy' expects the number of bytes to be copied for the 'count' argument dotnet_diagnostic.CA2018.severity = none # CA2019: Improper 'ThreadStatic' field initialization dotnet_diagnostic.CA2019.severity = none # CA2020: Prevent behavioral change dotnet_diagnostic.CA2020.severity = none # CA2021: Do not call Enumerable.Cast or Enumerable.OfType with incompatible types dotnet_diagnostic.CA2021.severity = none # CA2022: Avoid inexact read with 'Stream.Read' dotnet_diagnostic.CA2022.severity = none # CA2100: Review SQL queries for security vulnerabilities dotnet_diagnostic.CA2100.severity = none # CA2101: Specify marshaling for P/Invoke string arguments dotnet_diagnostic.CA2101.severity = none # CA2119: Seal methods that satisfy private interfaces dotnet_diagnostic.CA2119.severity = none # CA2153: Do Not Catch Corrupted State Exceptions dotnet_diagnostic.CA2153.severity = none # CA2200: Rethrow to preserve stack details dotnet_diagnostic.CA2200.severity = none # CA2201: Do not raise reserved exception types dotnet_diagnostic.CA2201.severity = none # CA2207: Initialize value type static fields inline dotnet_diagnostic.CA2207.severity = none # CA2208: Instantiate argument exceptions correctly dotnet_diagnostic.CA2208.severity = none # CA2211: Non-constant fields should not be visible dotnet_diagnostic.CA2211.severity = none # CA2213: Disposable fields should be disposed dotnet_diagnostic.CA2213.severity = none # CA2214: Do not call overridable methods in constructors dotnet_diagnostic.CA2214.severity = none # CA2215: Dispose methods should call base class dispose dotnet_diagnostic.CA2215.severity = none # CA2216: Disposable types should declare finalizer dotnet_diagnostic.CA2216.severity = none # CA2217: Do not mark enums with FlagsAttribute dotnet_diagnostic.CA2217.severity = none # CA2218: Override GetHashCode on overriding Equals dotnet_diagnostic.CA2218.severity = none # CA2219: Do not raise exceptions in finally clauses dotnet_diagnostic.CA2219.severity = none # CA2224: Override Equals on overloading operator equals dotnet_diagnostic.CA2224.severity = none # CA2225: Operator overloads have named alternates dotnet_diagnostic.CA2225.severity = none # CA2226: Operators should have symmetrical overloads dotnet_diagnostic.CA2226.severity = none # CA2227: Collection properties should be read only dotnet_diagnostic.CA2227.severity = none # CA2229: Implement serialization constructors dotnet_diagnostic.CA2229.severity = none # CA2231: Overload operator equals on overriding value type Equals dotnet_diagnostic.CA2231.severity = none # CA2234: Pass system uri objects instead of strings dotnet_diagnostic.CA2234.severity = none # CA2235: Mark all non-serializable fields dotnet_diagnostic.CA2235.severity = none # CA2237: Mark ISerializable types with serializable dotnet_diagnostic.CA2237.severity = none # CA2241: Provide correct arguments to formatting methods dotnet_diagnostic.CA2241.severity = none # CA2242: Test for NaN correctly dotnet_diagnostic.CA2242.severity = none # CA2243: Attribute string literals should parse correctly dotnet_diagnostic.CA2243.severity = none # CA2244: Do not duplicate indexed element initializations dotnet_diagnostic.CA2244.severity = none # CA2245: Do not assign a property to itself dotnet_diagnostic.CA2245.severity = none # CA2246: Assigning symbol and its member in the same statement dotnet_diagnostic.CA2246.severity = none # CA2247: Argument passed to TaskCompletionSource constructor should be TaskCreationOptions enum instead of TaskContinuationOptions enum dotnet_diagnostic.CA2247.severity = none # CA2248: Provide correct 'enum' argument to 'Enum.HasFlag' dotnet_diagnostic.CA2248.severity = none # CA2249: Consider using 'string.Contains' instead of 'string.IndexOf' dotnet_diagnostic.CA2249.severity = none # CA2250: Use 'ThrowIfCancellationRequested' dotnet_diagnostic.CA2250.severity = none # CA2251: Use 'string.Equals' dotnet_diagnostic.CA2251.severity = none # CA2252: This API requires opting into preview features dotnet_diagnostic.CA2252.severity = error # CA2253: Named placeholders should not be numeric values dotnet_diagnostic.CA2253.severity = none # CA2254: Template should be a static expression dotnet_diagnostic.CA2254.severity = none # CA2255: The 'ModuleInitializer' attribute should not be used in libraries dotnet_diagnostic.CA2255.severity = none # CA2256: All members declared in parent interfaces must have an implementation in a DynamicInterfaceCastableImplementation-attributed interface dotnet_diagnostic.CA2256.severity = none # CA2257: Members defined on an interface with the 'DynamicInterfaceCastableImplementationAttribute' should be 'static' dotnet_diagnostic.CA2257.severity = none # CA2258: Providing a 'DynamicInterfaceCastableImplementation' interface in Visual Basic is unsupported dotnet_diagnostic.CA2258.severity = none # CA2259: 'ThreadStatic' only affects static fields dotnet_diagnostic.CA2259.severity = none # CA2260: Use correct type parameter dotnet_diagnostic.CA2260.severity = none # CA2261: Do not use ConfigureAwaitOptions.SuppressThrowing with Task dotnet_diagnostic.CA2261.severity = none # CA2262: Set 'MaxResponseHeadersLength' properly dotnet_diagnostic.CA2262.severity = none # CA2263: Prefer generic overload when type is known dotnet_diagnostic.CA2263.severity = none # CA2264: Do not pass a non-nullable value to 'ArgumentNullException.ThrowIfNull' dotnet_diagnostic.CA2264.severity = none # CA2265: Do not compare Span to 'null' or 'default' dotnet_diagnostic.CA2265.severity = none # CA2300: Do not use insecure deserializer BinaryFormatter dotnet_diagnostic.CA2300.severity = none # CA2301: Do not call BinaryFormatter.Deserialize without first setting BinaryFormatter.Binder dotnet_diagnostic.CA2301.severity = none # CA2302: Ensure BinaryFormatter.Binder is set before calling BinaryFormatter.Deserialize dotnet_diagnostic.CA2302.severity = none # CA2305: Do not use insecure deserializer LosFormatter dotnet_diagnostic.CA2305.severity = none # CA2310: Do not use insecure deserializer NetDataContractSerializer dotnet_diagnostic.CA2310.severity = none # CA2311: Do not deserialize without first setting NetDataContractSerializer.Binder dotnet_diagnostic.CA2311.severity = none # CA2312: Ensure NetDataContractSerializer.Binder is set before deserializing dotnet_diagnostic.CA2312.severity = none # CA2315: Do not use insecure deserializer ObjectStateFormatter dotnet_diagnostic.CA2315.severity = none # CA2321: Do not deserialize with JavaScriptSerializer using a SimpleTypeResolver dotnet_diagnostic.CA2321.severity = none # CA2322: Ensure JavaScriptSerializer is not initialized with SimpleTypeResolver before deserializing dotnet_diagnostic.CA2322.severity = none # CA2326: Do not use TypeNameHandling values other than None dotnet_diagnostic.CA2326.severity = none # CA2327: Do not use insecure JsonSerializerSettings dotnet_diagnostic.CA2327.severity = none # CA2328: Ensure that JsonSerializerSettings are secure dotnet_diagnostic.CA2328.severity = none # CA2329: Do not deserialize with JsonSerializer using an insecure configuration dotnet_diagnostic.CA2329.severity = none # CA2330: Ensure that JsonSerializer has a secure configuration when deserializing dotnet_diagnostic.CA2330.severity = none # CA2350: Do not use DataTable.ReadXml() with untrusted data dotnet_diagnostic.CA2350.severity = none # CA2351: Do not use DataSet.ReadXml() with untrusted data dotnet_diagnostic.CA2351.severity = none # CA2352: Unsafe DataSet or DataTable in serializable type can be vulnerable to remote code execution attacks dotnet_diagnostic.CA2352.severity = none # CA2353: Unsafe DataSet or DataTable in serializable type dotnet_diagnostic.CA2353.severity = none # CA2354: Unsafe DataSet or DataTable in deserialized object graph can be vulnerable to remote code execution attacks dotnet_diagnostic.CA2354.severity = none # CA2355: Unsafe DataSet or DataTable type found in deserializable object graph dotnet_diagnostic.CA2355.severity = none # CA2356: Unsafe DataSet or DataTable type in web deserializable object graph dotnet_diagnostic.CA2356.severity = none # CA2361: Ensure auto-generated class containing DataSet.ReadXml() is not used with untrusted data dotnet_diagnostic.CA2361.severity = none # CA2362: Unsafe DataSet or DataTable in auto-generated serializable type can be vulnerable to remote code execution attacks dotnet_diagnostic.CA2362.severity = none # CA3001: Review code for SQL injection vulnerabilities dotnet_diagnostic.CA3001.severity = none # CA3002: Review code for XSS vulnerabilities dotnet_diagnostic.CA3002.severity = none # CA3003: Review code for file path injection vulnerabilities dotnet_diagnostic.CA3003.severity = none # CA3004: Review code for information disclosure vulnerabilities dotnet_diagnostic.CA3004.severity = none # CA3005: Review code for LDAP injection vulnerabilities dotnet_diagnostic.CA3005.severity = none # CA3006: Review code for process command injection vulnerabilities dotnet_diagnostic.CA3006.severity = none # CA3007: Review code for open redirect vulnerabilities dotnet_diagnostic.CA3007.severity = none # CA3008: Review code for XPath injection vulnerabilities dotnet_diagnostic.CA3008.severity = none # CA3009: Review code for XML injection vulnerabilities dotnet_diagnostic.CA3009.severity = none # CA3010: Review code for XAML injection vulnerabilities dotnet_diagnostic.CA3010.severity = none # CA3011: Review code for DLL injection vulnerabilities dotnet_diagnostic.CA3011.severity = none # CA3012: Review code for regex injection vulnerabilities dotnet_diagnostic.CA3012.severity = none # CA3061: Do Not Add Schema By URL dotnet_diagnostic.CA3061.severity = none # CA3075: Insecure DTD processing in XML dotnet_diagnostic.CA3075.severity = none # CA3076: Insecure XSLT script processing dotnet_diagnostic.CA3076.severity = none # CA3077: Insecure Processing in API Design, XmlDocument and XmlTextReader dotnet_diagnostic.CA3077.severity = none # CA3147: Mark Verb Handlers With Validate Antiforgery Token dotnet_diagnostic.CA3147.severity = none # CA5350: Do Not Use Weak Cryptographic Algorithms dotnet_diagnostic.CA5350.severity = none # CA5351: Do Not Use Broken Cryptographic Algorithms dotnet_diagnostic.CA5351.severity = none # CA5358: Review cipher mode usage with cryptography experts dotnet_diagnostic.CA5358.severity = none # CA5359: Do Not Disable Certificate Validation dotnet_diagnostic.CA5359.severity = none # CA5360: Do Not Call Dangerous Methods In Deserialization dotnet_diagnostic.CA5360.severity = none # CA5361: Do Not Disable SChannel Use of Strong Crypto dotnet_diagnostic.CA5361.severity = none # CA5362: Potential reference cycle in deserialized object graph dotnet_diagnostic.CA5362.severity = none # CA5363: Do Not Disable Request Validation dotnet_diagnostic.CA5363.severity = none # CA5364: Do Not Use Deprecated Security Protocols dotnet_diagnostic.CA5364.severity = none # CA5365: Do Not Disable HTTP Header Checking dotnet_diagnostic.CA5365.severity = none # CA5366: Use XmlReader for 'DataSet.ReadXml()' dotnet_diagnostic.CA5366.severity = none # CA5367: Do Not Serialize Types With Pointer Fields dotnet_diagnostic.CA5367.severity = none # CA5368: Set ViewStateUserKey For Classes Derived From Page dotnet_diagnostic.CA5368.severity = none # CA5369: Use XmlReader for 'XmlSerializer.Deserialize()' dotnet_diagnostic.CA5369.severity = none # CA5370: Use XmlReader for XmlValidatingReader constructor dotnet_diagnostic.CA5370.severity = none # CA5371: Use XmlReader for 'XmlSchema.Read()' dotnet_diagnostic.CA5371.severity = none # CA5372: Use XmlReader for XPathDocument constructor dotnet_diagnostic.CA5372.severity = none # CA5373: Do not use obsolete key derivation function dotnet_diagnostic.CA5373.severity = none # CA5374: Do Not Use XslTransform dotnet_diagnostic.CA5374.severity = none # CA5375: Do Not Use Account Shared Access Signature dotnet_diagnostic.CA5375.severity = none # CA5376: Use SharedAccessProtocol HttpsOnly dotnet_diagnostic.CA5376.severity = none # CA5377: Use Container Level Access Policy dotnet_diagnostic.CA5377.severity = none # CA5378: Do not disable ServicePointManagerSecurityProtocols dotnet_diagnostic.CA5378.severity = none # CA5379: Ensure Key Derivation Function algorithm is sufficiently strong dotnet_diagnostic.CA5379.severity = none # CA5380: Do Not Add Certificates To Root Store dotnet_diagnostic.CA5380.severity = none # CA5381: Ensure Certificates Are Not Added To Root Store dotnet_diagnostic.CA5381.severity = none # CA5382: Use Secure Cookies In ASP.Net Core dotnet_diagnostic.CA5382.severity = none # CA5383: Ensure Use Secure Cookies In ASP.NET Core dotnet_diagnostic.CA5383.severity = none # CA5384: Do Not Use Digital Signature Algorithm (DSA) dotnet_diagnostic.CA5384.severity = none # CA5385: Use Rivest-Shamir-Adleman (RSA) Algorithm With Sufficient Key Size dotnet_diagnostic.CA5385.severity = none # CA5386: Avoid hardcoding SecurityProtocolType value dotnet_diagnostic.CA5386.severity = none # CA5387: Do Not Use Weak Key Derivation Function With Insufficient Iteration Count dotnet_diagnostic.CA5387.severity = none # CA5388: Ensure Sufficient Iteration Count When Using Weak Key Derivation Function dotnet_diagnostic.CA5388.severity = none # CA5389: Do Not Add Archive Item's Path To The Target File System Path dotnet_diagnostic.CA5389.severity = none # CA5390: Do not hard-code encryption key dotnet_diagnostic.CA5390.severity = none # CA5391: Use antiforgery tokens in ASP.NET Core MVC controllers dotnet_diagnostic.CA5391.severity = none # CA5392: Use DefaultDllImportSearchPaths attribute for P/Invokes dotnet_diagnostic.CA5392.severity = none # CA5393: Do not use unsafe DllImportSearchPath value dotnet_diagnostic.CA5393.severity = none # CA5394: Do not use insecure randomness dotnet_diagnostic.CA5394.severity = none # CA5395: Miss HttpVerb attribute for action methods dotnet_diagnostic.CA5395.severity = none # CA5396: Set HttpOnly to true for HttpCookie dotnet_diagnostic.CA5396.severity = none # CA5397: Do not use deprecated SslProtocols values dotnet_diagnostic.CA5397.severity = none # CA5398: Avoid hardcoded SslProtocols values dotnet_diagnostic.CA5398.severity = none # CA5399: HttpClients should enable certificate revocation list checks dotnet_diagnostic.CA5399.severity = none # CA5400: Ensure HttpClient certificate revocation list check is not disabled dotnet_diagnostic.CA5400.severity = none # CA5401: Do not use CreateEncryptor with non-default IV dotnet_diagnostic.CA5401.severity = none # CA5402: Use CreateEncryptor with the default IV dotnet_diagnostic.CA5402.severity = none # CA5403: Do not hard-code certificate dotnet_diagnostic.CA5403.severity = none # CA5404: Do not disable token validation checks dotnet_diagnostic.CA5404.severity = none # CA5405: Do not always skip token validation in delegates dotnet_diagnostic.CA5405.severity = none # IL3000: Avoid using accessing Assembly file path when publishing as a single-file dotnet_diagnostic.IL3000.severity = none # IL3001: Avoid using accessing Assembly file path when publishing as a single-file dotnet_diagnostic.IL3001.severity = none # IL3002: Using member with RequiresAssemblyFilesAttribute can break functionality when embedded in a single-file app dotnet_diagnostic.IL3002.severity = none # RS1001: Missing diagnostic analyzer attribute dotnet_diagnostic.RS1001.severity = none # RS1002: Missing kind argument when registering an analyzer action dotnet_diagnostic.RS1002.severity = none # RS1003: Unsupported SymbolKind argument when registering a symbol analyzer action dotnet_diagnostic.RS1003.severity = none # RS1004: Recommend adding language support to diagnostic analyzer dotnet_diagnostic.RS1004.severity = none # RS1005: ReportDiagnostic invoked with an unsupported DiagnosticDescriptor dotnet_diagnostic.RS1005.severity = none # RS1006: Invalid type argument for DiagnosticAnalyzer's Register method dotnet_diagnostic.RS1006.severity = none # RS1007: Provide localizable arguments to diagnostic descriptor constructor dotnet_diagnostic.RS1007.severity = none # RS1008: Avoid storing per-compilation data into the fields of a diagnostic analyzer dotnet_diagnostic.RS1008.severity = none # RS1009: Only internal implementations of this interface are allowed dotnet_diagnostic.RS1009.severity = none # RS1010: Create code actions should have a unique EquivalenceKey for FixAll occurrences support dotnet_diagnostic.RS1010.severity = none # RS1011: Use code actions that have a unique EquivalenceKey for FixAll occurrences support dotnet_diagnostic.RS1011.severity = none # RS1012: Start action has no registered actions dotnet_diagnostic.RS1012.severity = none # RS1013: Start action has no registered non-end actions dotnet_diagnostic.RS1013.severity = none # RS1014: Do not ignore values returned by methods on immutable objects dotnet_diagnostic.RS1014.severity = none # RS1015: Provide non-null 'helpLinkUri' value to diagnostic descriptor constructor dotnet_diagnostic.RS1015.severity = none # RS1016: Code fix providers should provide FixAll support dotnet_diagnostic.RS1016.severity = suggestion # RS1017: DiagnosticId for analyzers must be a non-null constant dotnet_diagnostic.RS1017.severity = none # RS1018: DiagnosticId for analyzers must be in specified format dotnet_diagnostic.RS1018.severity = none # RS1019: DiagnosticId must be unique across analyzers dotnet_diagnostic.RS1019.severity = none # RS1020: Category for analyzers must be from the specified values dotnet_diagnostic.RS1020.severity = none # RS1021: Invalid entry in analyzer category and diagnostic ID range specification file dotnet_diagnostic.RS1021.severity = none # RS1022: Do not use types from Workspaces assembly in an analyzer dotnet_diagnostic.RS1022.severity = none # RS1023: Upgrade MSBuildWorkspace dotnet_diagnostic.RS1023.severity = none # RS1024: Symbols should be compared for equality dotnet_diagnostic.RS1024.severity = none # RS1025: Configure generated code analysis dotnet_diagnostic.RS1025.severity = none # RS1026: Enable concurrent execution dotnet_diagnostic.RS1026.severity = none # RS1027: Types marked with DiagnosticAnalyzerAttribute(s) should inherit from DiagnosticAnalyzer dotnet_diagnostic.RS1027.severity = none # RS1028: Provide non-null 'customTags' value to diagnostic descriptor constructor dotnet_diagnostic.RS1028.severity = none # RS1029: Do not use reserved diagnostic IDs dotnet_diagnostic.RS1029.severity = none # RS1030: Do not invoke Compilation.GetSemanticModel() method within a diagnostic analyzer dotnet_diagnostic.RS1030.severity = none # RS1031: Define diagnostic title correctly dotnet_diagnostic.RS1031.severity = none # RS1032: Define diagnostic message correctly dotnet_diagnostic.RS1032.severity = none # RS1033: Define diagnostic description correctly dotnet_diagnostic.RS1033.severity = none # RS1034: Prefer 'IsKind' for checking syntax kinds dotnet_diagnostic.RS1034.severity = none # RS1035: Do not use APIs banned for analyzers dotnet_diagnostic.RS1035.severity = none # RS1036: Specify analyzer banned API enforcement setting dotnet_diagnostic.RS1036.severity = none # RS1037: Add "CompilationEnd" custom tag to compilation end diagnostic descriptor dotnet_diagnostic.RS1037.severity = none # RS1038: Compiler extensions should be implemented in assemblies with compiler-provided references dotnet_diagnostic.RS1038.severity = suggestion # RS2000: Add analyzer diagnostic IDs to analyzer release dotnet_diagnostic.RS2000.severity = none # RS2001: Ensure up-to-date entry for analyzer diagnostic IDs are added to analyzer release dotnet_diagnostic.RS2001.severity = none # RS2002: Do not add removed analyzer diagnostic IDs to unshipped analyzer release dotnet_diagnostic.RS2002.severity = none # RS2003: Shipped diagnostic IDs that are no longer reported should have an entry in the 'Removed Rules' table in unshipped file dotnet_diagnostic.RS2003.severity = none # RS2004: Diagnostic IDs marked as removed in analyzer release file should not be reported by analyzers dotnet_diagnostic.RS2004.severity = none # RS2005: Remove duplicate entries for diagnostic ID in the same analyzer release dotnet_diagnostic.RS2005.severity = none # RS2006: Remove duplicate entries for diagnostic ID between analyzer releases dotnet_diagnostic.RS2006.severity = none # RS2007: Invalid entry in analyzer release file dotnet_diagnostic.RS2007.severity = none # RS2008: Enable analyzer release tracking dotnet_diagnostic.RS2008.severity = none # SA0001: XML comments dotnet_diagnostic.SA0001.severity = none # SA1000: Spacing around keywords dotnet_diagnostic.SA1000.severity = none # SA1001: Commas should not be preceded by whitespace dotnet_diagnostic.SA1001.severity = none # SA1002: Semicolons should not be preceded by a space dotnet_diagnostic.SA1002.severity = none # SA1003: Operator should not appear at the end of a line dotnet_diagnostic.SA1003.severity = none # SA1004: Documentation line should begin with a space dotnet_diagnostic.SA1004.severity = none # SA1005: Single line comment should begin with a space dotnet_diagnostic.SA1005.severity = none # SA1008: Opening parenthesis should not be preceded by a space dotnet_diagnostic.SA1008.severity = none # SA1009: Closing parenthesis should not be followed by a space dotnet_diagnostic.SA1009.severity = none # SA1010: Opening square brackets should not be preceded by a space dotnet_diagnostic.SA1010.severity = none # SA1011: Closing square bracket should be followed by a space dotnet_diagnostic.SA1011.severity = none # SA1012: Opening brace should be followed by a space dotnet_diagnostic.SA1012.severity = none # SA1013: Closing brace should be preceded by a space dotnet_diagnostic.SA1013.severity = none # SA1014: Opening generic brackets should not be preceded by a space dotnet_diagnostic.SA1014.severity = none # SA1015: Closing generic bracket should not be followed by a space dotnet_diagnostic.SA1015.severity = none # SA1018: Nullable type symbol should not be preceded by a space dotnet_diagnostic.SA1018.severity = none # SA1020: Increment symbol should not be preceded by a space dotnet_diagnostic.SA1020.severity = none # SA1021: Negative sign should be preceded by a space dotnet_diagnostic.SA1021.severity = none # SA1023: Dereference symbol '*' should not be preceded by a space." dotnet_diagnostic.SA1023.severity = none # SA1024: Colon should be followed by a space dotnet_diagnostic.SA1024.severity = none # SA1025: Code should not contain multiple whitespace characters in a row dotnet_diagnostic.SA1025.severity = none # SA1026: Keyword followed by span or blank line dotnet_diagnostic.SA1026.severity = none # SA1027: Tabs and spaces should be used correctly dotnet_diagnostic.SA1027.severity = none # SA1028: Code should not contain trailing whitespace dotnet_diagnostic.SA1028.severity = none # SA1100: Do not prefix calls with base unless local implementation exists dotnet_diagnostic.SA1100.severity = none # SA1101: Prefix local calls with this dotnet_diagnostic.SA1101.severity = none # SA1102: Query clause should follow previous clause dotnet_diagnostic.SA1102.severity = none # SA1105: Query clauses spanning multiple lines should begin on own line dotnet_diagnostic.SA1105.severity = none # SA1106: Code should not contain empty statements dotnet_diagnostic.SA1106.severity = none # SA1107: Code should not contain multiple statements on one line dotnet_diagnostic.SA1107.severity = none # SA1108: Block statements should not contain embedded comments dotnet_diagnostic.SA1108.severity = none # SA1110: Opening parenthesis or bracket should be on declaration line dotnet_diagnostic.SA1110.severity = none # SA1111: Closing parenthesis should be on line of last parameter dotnet_diagnostic.SA1111.severity = none # SA1113: Comma should be on the same line as previous parameter dotnet_diagnostic.SA1113.severity = none # SA1114: Parameter list should follow declaration dotnet_diagnostic.SA1114.severity = none # SA1115: Parameter should begin on the line after the previous parameter dotnet_diagnostic.SA1115.severity = none # SA1116: Split parameters should start on line after declaration dotnet_diagnostic.SA1116.severity = none # SA1117: Parameters should be on same line or separate lines dotnet_diagnostic.SA1117.severity = none # SA1118: Parameter should not span multiple lines dotnet_diagnostic.SA1118.severity = none # SA1119: Statement should not use unnecessary parenthesis dotnet_diagnostic.SA1119.severity = none # SA1120: Comments should contain text dotnet_diagnostic.SA1120.severity = none # SA1121: Use built-in type alias dotnet_diagnostic.SA1121.severity = none # SA1122: Use string.Empty for empty strings dotnet_diagnostic.SA1122.severity = none # SA1123: Region should not be located within a code element dotnet_diagnostic.SA1123.severity = none # SA1124: Do not use regions dotnet_diagnostic.SA1124.severity = none # SA1125: Use shorthand for nullable types dotnet_diagnostic.SA1125.severity = none # SA1127: Generic type constraints should be on their own line dotnet_diagnostic.SA1127.severity = none # SA1128: Put constructor initializers on their own line dotnet_diagnostic.SA1128.severity = none # SA1129: Do not use default value type constructor dotnet_diagnostic.SA1129.severity = none # SA1130: Use lambda syntax dotnet_diagnostic.SA1130.severity = none # SA1131: Constant values should appear on the right-hand side of comparisons dotnet_diagnostic.SA1131.severity = none # SA1132: Do not combine fields dotnet_diagnostic.SA1132.severity = none # SA1133: Do not combine attributes dotnet_diagnostic.SA1133.severity = none # SA1134: Each attribute should be placed on its own line of code dotnet_diagnostic.SA1134.severity = none # SA1135: Using directive should be qualified dotnet_diagnostic.SA1135.severity = none # SA1136: Enum values should be on separate lines dotnet_diagnostic.SA1136.severity = none # SA1137: Elements should have the same indentation dotnet_diagnostic.SA1137.severity = none # SA1139: Use literal suffix notation instead of casting dotnet_diagnostic.SA1139.severity = none # SA1141: Use tuple syntax dotnet_diagnostic.SA1141.severity = none # SA1142: Refer to tuple elements by name dotnet_diagnostic.SA1142.severity = none # SA1200: Using directive should appear within a namespace declaration dotnet_diagnostic.SA1200.severity = none # SA1201: Elements should appear in the correct order dotnet_diagnostic.SA1201.severity = none # SA1202: Elements should be ordered by access dotnet_diagnostic.SA1202.severity = none # SA1203: Constants should appear before fields dotnet_diagnostic.SA1203.severity = none # SA1204: Static elements should appear before instance elements dotnet_diagnostic.SA1204.severity = none # SA1205: Partial elements should declare an access modifier dotnet_diagnostic.SA1205.severity = none # SA1206: Keyword ordering dotnet_diagnostic.SA1206.severity = none # SA1208: Using directive ordering dotnet_diagnostic.SA1208.severity = none # SA1209: Using alias directives should be placed after all using namespace directives dotnet_diagnostic.SA1209.severity = none # SA1210: Using directives should be ordered alphabetically by the namespaces dotnet_diagnostic.SA1210.severity = none # SA1211: Using alias directive ordering dotnet_diagnostic.SA1211.severity = none # SA1212: A get accessor appears after a set accessor within a property or indexer dotnet_diagnostic.SA1212.severity = none # SA1214: Readonly fields should appear before non-readonly fields dotnet_diagnostic.SA1214.severity = none # SA1216: Using static directives should be placed at the correct location dotnet_diagnostic.SA1216.severity = none # SA1300: Element should begin with an uppercase letter dotnet_diagnostic.SA1300.severity = none # SA1302: Interface names should begin with I dotnet_diagnostic.SA1302.severity = none # SA1303: Const field names should begin with upper-case letter dotnet_diagnostic.SA1303.severity = none # SA1304: Non-private readonly fields should begin with upper-case letter dotnet_diagnostic.SA1304.severity = none # SA1306: Field should begin with lower-case letter dotnet_diagnostic.SA1306.severity = none # SA1307: Field should begin with upper-case letter dotnet_diagnostic.SA1307.severity = none # SA1308: Field should not begin with the prefix 's_' dotnet_diagnostic.SA1308.severity = none # SA1309: Field names should not begin with underscore dotnet_diagnostic.SA1309.severity = none # SA1310: Field should not contain an underscore dotnet_diagnostic.SA1310.severity = none # SA1311: Static readonly fields should begin with upper-case letter dotnet_diagnostic.SA1311.severity = none # SA1312: Variable should begin with lower-case letter dotnet_diagnostic.SA1312.severity = none # SA1313: Parameter should begin with lower-case letter dotnet_diagnostic.SA1313.severity = none # SA1314: Type parameter names should begin with T dotnet_diagnostic.SA1314.severity = none # SA1316: Tuple element names should use correct casing dotnet_diagnostic.SA1316.severity = none # SA1400: Member should declare an access modifier dotnet_diagnostic.SA1400.severity = none # SA1401: Fields should be private dotnet_diagnostic.SA1401.severity = none # SA1402: File may only contain a single type dotnet_diagnostic.SA1402.severity = none # SA1403: File may only contain a single namespace dotnet_diagnostic.SA1403.severity = none # SA1404: Code analysis suppression should have justification dotnet_diagnostic.SA1404.severity = none # SA1405: Debug.Assert should provide message text dotnet_diagnostic.SA1405.severity = none # SA1407: Arithmetic expressions should declare precedence dotnet_diagnostic.SA1407.severity = none # SA1408: Conditional expressions should declare precedence dotnet_diagnostic.SA1408.severity = none # SA1410: Remove delegate parens when possible dotnet_diagnostic.SA1410.severity = none # SA1411: Attribute constructor shouldn't use unnecessary parenthesis dotnet_diagnostic.SA1411.severity = none # SA1413: Use trailing comma in multi-line initializers dotnet_diagnostic.SA1413.severity = none # SA1414: Tuple types in signatures should have element names dotnet_diagnostic.SA1414.severity = none # SA1500: Braces for multi-line statements should not share line dotnet_diagnostic.SA1500.severity = none # SA1501: Statement should not be on a single line dotnet_diagnostic.SA1501.severity = none # SA1502: Element should not be on a single line dotnet_diagnostic.SA1502.severity = none # SA1503: Braces should not be omitted dotnet_diagnostic.SA1503.severity = none # SA1504: All accessors should be single-line or multi-line dotnet_diagnostic.SA1504.severity = none # SA1505: An opening brace should not be followed by a blank line dotnet_diagnostic.SA1505.severity = none # SA1506: Element documentation headers should not be followed by blank line dotnet_diagnostic.SA1506.severity = none # SA1507: Code should not contain multiple blank lines in a row dotnet_diagnostic.SA1507.severity = none # SA1508: A closing brace should not be preceded by a blank line dotnet_diagnostic.SA1508.severity = none # SA1509: Opening braces should not be preceded by blank line dotnet_diagnostic.SA1509.severity = none # SA1510: 'else' statement should not be preceded by a blank line dotnet_diagnostic.SA1510.severity = none # SA1512: Single-line comments should not be followed by blank line dotnet_diagnostic.SA1512.severity = none # SA1513: Closing brace should be followed by blank line dotnet_diagnostic.SA1513.severity = none # SA1514: Element documentation header should be preceded by blank line dotnet_diagnostic.SA1514.severity = none # SA1515: Single-line comment should be preceded by blank line dotnet_diagnostic.SA1515.severity = none # SA1516: Elements should be separated by blank line dotnet_diagnostic.SA1516.severity = none # SA1517: Code should not contain blank lines at start of file dotnet_diagnostic.SA1517.severity = none # SA1518: Code should not contain blank lines at the end of the file dotnet_diagnostic.SA1518.severity = none # SA1519: Braces should not be omitted from multi-line child statement dotnet_diagnostic.SA1519.severity = none # SA1520: Use braces consistently dotnet_diagnostic.SA1520.severity = none # SA1600: Elements should be documented dotnet_diagnostic.SA1600.severity = none # SA1601: Partial elements should be documented dotnet_diagnostic.SA1601.severity = none # SA1602: Enumeration items should be documented dotnet_diagnostic.SA1602.severity = none # SA1604: Element documentation should have summary dotnet_diagnostic.SA1604.severity = none # SA1605: Partial element documentation should have summary dotnet_diagnostic.SA1605.severity = none # SA1606: Element documentation should have summary text dotnet_diagnostic.SA1606.severity = none # SA1608: Element documentation should not have default summary dotnet_diagnostic.SA1608.severity = none # SA1610: Property documentation should have value text dotnet_diagnostic.SA1610.severity = none # SA1611: The documentation for parameter 'message' is missing dotnet_diagnostic.SA1611.severity = none # SA1612: The parameter documentation is at incorrect position dotnet_diagnostic.SA1612.severity = none # SA1614: Element parameter documentation should have text dotnet_diagnostic.SA1614.severity = none # SA1615: Element return value should be documented dotnet_diagnostic.SA1615.severity = none # SA1616: Element return value documentation should have text dotnet_diagnostic.SA1616.severity = none # SA1618: The documentation for type parameter is missing dotnet_diagnostic.SA1618.severity = none # SA1619: The documentation for type parameter is missing dotnet_diagnostic.SA1619.severity = none # SA1622: Generic type parameter documentation should have text dotnet_diagnostic.SA1622.severity = none # SA1623: Property documentation text dotnet_diagnostic.SA1623.severity = none # SA1624: Because the property only contains a visible get accessor, the documentation summary text should begin with 'Gets' dotnet_diagnostic.SA1624.severity = none # SA1625: Element documentation should not be copied and pasted dotnet_diagnostic.SA1625.severity = none # SA1626: Single-line comments should not use documentation style slashes dotnet_diagnostic.SA1626.severity = none # SA1627: The documentation text within the \'exception\' tag should not be empty dotnet_diagnostic.SA1627.severity = none # SA1629: Documentation text should end with a period dotnet_diagnostic.SA1629.severity = none # SA1633: File should have header dotnet_diagnostic.SA1633.severity = none # SA1642: Constructor summary documentation should begin with standard text dotnet_diagnostic.SA1642.severity = none # SA1643: Destructor summary documentation should begin with standard text dotnet_diagnostic.SA1643.severity = none # SA1649: File name should match first type name dotnet_diagnostic.SA1649.severity = none # IDE0001: Simplify name dotnet_diagnostic.IDE0001.severity = silent # IDE0002: Simplify member access dotnet_diagnostic.IDE0002.severity = silent # IDE0003: Remove this or Me qualification dotnet_diagnostic.IDE0003.severity = silent # IDE0004: Remove Unnecessary Cast dotnet_diagnostic.IDE0004.severity = silent # IDE0005: Using directive is unnecessary. dotnet_diagnostic.IDE0005.severity = silent # IDE0007: Use implicit type dotnet_diagnostic.IDE0007.severity = silent # IDE0008: Use explicit type dotnet_diagnostic.IDE0008.severity = silent # IDE0009: Add this or Me qualification dotnet_diagnostic.IDE0009.severity = silent # IDE0010: Add missing cases dotnet_diagnostic.IDE0010.severity = silent # IDE0011: Add braces dotnet_diagnostic.IDE0011.severity = silent # IDE0016: Use 'throw' expression dotnet_diagnostic.IDE0016.severity = silent # IDE0017: Simplify object initialization dotnet_diagnostic.IDE0017.severity = silent # IDE0018: Inline variable declaration dotnet_diagnostic.IDE0018.severity = silent # IDE0019: Use pattern matching to avoid as followed by a null check dotnet_diagnostic.IDE0019.severity = silent # IDE0020: Use pattern matching to avoid is check followed by a cast (with variable) dotnet_diagnostic.IDE0020.severity = silent # IDE0021: Use expression body for constructors dotnet_diagnostic.IDE0021.severity = silent # IDE0022: Use expression body for methods dotnet_diagnostic.IDE0022.severity = silent # IDE0023: Use expression body for operators dotnet_diagnostic.IDE0023.severity = silent # IDE0024: Use expression body for operators dotnet_diagnostic.IDE0024.severity = silent # IDE0025: Use expression body for properties dotnet_diagnostic.IDE0025.severity = silent # IDE0026: Use expression body for indexers dotnet_diagnostic.IDE0026.severity = silent # IDE0027: Use expression body for accessors dotnet_diagnostic.IDE0027.severity = silent # IDE0028: Simplify collection initialization dotnet_diagnostic.IDE0028.severity = silent # IDE0029: Use coalesce expression dotnet_diagnostic.IDE0029.severity = silent # IDE0030: Use coalesce expression dotnet_diagnostic.IDE0030.severity = silent # IDE0031: Use null propagation dotnet_diagnostic.IDE0031.severity = silent # IDE0032: Use auto property dotnet_diagnostic.IDE0032.severity = silent # IDE0033: Use explicitly provided tuple name dotnet_diagnostic.IDE0033.severity = silent # IDE0034: Simplify 'default' expression dotnet_diagnostic.IDE0034.severity = silent # IDE0035: Remove unreachable code dotnet_diagnostic.IDE0035.severity = silent # IDE0036: Order modifiers dotnet_diagnostic.IDE0036.severity = silent # IDE0037: Use inferred member name dotnet_diagnostic.IDE0037.severity = silent # IDE0038: Use pattern matching to avoid is check followed by a cast (without variable) dotnet_diagnostic.IDE0038.severity = silent # IDE0039: Use local function dotnet_diagnostic.IDE0039.severity = silent # IDE0040: Add accessibility modifiers dotnet_diagnostic.IDE0040.severity = silent # IDE0041: Use 'is null' check dotnet_diagnostic.IDE0041.severity = silent # IDE0042: Deconstruct variable declaration dotnet_diagnostic.IDE0042.severity = silent # IDE0043: Invalid format string dotnet_diagnostic.IDE0043.severity = silent # IDE0044: Add readonly modifier dotnet_diagnostic.IDE0044.severity = silent # IDE0045: Use conditional expression for assignment dotnet_diagnostic.IDE0045.severity = silent # IDE0046: Use conditional expression for return dotnet_diagnostic.IDE0046.severity = silent # IDE0047: Remove unnecessary parentheses dotnet_diagnostic.IDE0047.severity = silent # IDE0048: Add parentheses for clarity dotnet_diagnostic.IDE0048.severity = silent # IDE0049: Use language keywords instead of framework type names for type references dotnet_diagnostic.IDE0049.severity = silent # IDE0051: Remove unused private members dotnet_diagnostic.IDE0051.severity = silent # IDE0052: Remove unread private members dotnet_diagnostic.IDE0052.severity = silent # IDE0053: Use expression body for lambdas dotnet_diagnostic.IDE0053.severity = silent # IDE0054: Use compound assignment dotnet_diagnostic.IDE0054.severity = silent # IDE0055: Fix formatting dotnet_diagnostic.IDE0055.severity = silent # IDE0056: Use index operator dotnet_diagnostic.IDE0056.severity = silent # IDE0057: Use range operator dotnet_diagnostic.IDE0057.severity = silent # IDE0058: Expression value is never used dotnet_diagnostic.IDE0058.severity = silent # IDE0059: Unnecessary assignment of a value dotnet_diagnostic.IDE0059.severity = silent # IDE0060: Remove unused parameter dotnet_diagnostic.IDE0060.severity = silent # IDE0061: Use expression body for local functions dotnet_diagnostic.IDE0061.severity = silent # IDE0062: Make local function 'static' dotnet_diagnostic.IDE0062.severity = silent # IDE0063: Use simple 'using' statement dotnet_diagnostic.IDE0063.severity = silent # IDE0064: Make readonly fields writable dotnet_diagnostic.IDE0064.severity = silent # IDE0065: Misplaced using directive dotnet_diagnostic.IDE0065.severity = silent # IDE0066: Convert switch statement to expression dotnet_diagnostic.IDE0066.severity = silent # IDE0070: Use 'System.HashCode' dotnet_diagnostic.IDE0070.severity = silent # IDE0071: Simplify interpolation dotnet_diagnostic.IDE0071.severity = silent # IDE0072: Add missing cases dotnet_diagnostic.IDE0072.severity = silent # IDE0073: The file header is missing or not located at the top of the file dotnet_diagnostic.IDE0073.severity = error # IDE0074: Use compound assignment dotnet_diagnostic.IDE0074.severity = silent # IDE0075: Simplify conditional expression dotnet_diagnostic.IDE0075.severity = silent # IDE0076: Invalid global 'SuppressMessageAttribute' dotnet_diagnostic.IDE0076.severity = silent # IDE0077: Avoid legacy format target in 'SuppressMessageAttribute' dotnet_diagnostic.IDE0077.severity = silent # IDE0078: Use pattern matching dotnet_diagnostic.IDE0078.severity = silent # IDE0079: RemoveUnnecessarySuppression dotnet_diagnostic.IDE0079.severity = silent # IDE0080: Remove unnecessary suppression operator dotnet_diagnostic.IDE0080.severity = silent # IDE0081: RemoveUnnecessaryByVal dotnet_diagnostic.IDE0081.severity = silent # IDE0082: 'typeof' can be converted to 'nameof' dotnet_diagnostic.IDE0082.severity = silent # IDE0083: Use pattern matching dotnet_diagnostic.IDE0083.severity = silent # IDE0084: Use pattern matching (IsNot operator) dotnet_diagnostic.IDE0084.severity = silent # IDE0090: Use 'new(...)' dotnet_diagnostic.IDE0090.severity = silent # IDE0100: Remove redundant equality dotnet_diagnostic.IDE0100.severity = silent # IDE0110: Remove unnecessary discard dotnet_diagnostic.IDE0110.severity = silent # IDE0120: Simplify LINQ expression dotnet_diagnostic.IDE0120.severity = silent # IDE0130: Namespace does not match folder structure dotnet_diagnostic.IDE0130.severity = silent # IDE0140: Simplify object creation dotnet_diagnostic.IDE0140.severity = silent # IDE0150: Prefer 'null' check over type check dotnet_diagnostic.IDE0150.severity = silent # IDE0160: Convert to block scoped namespace dotnet_diagnostic.IDE0160.severity = silent # IDE0161: Convert to file-scoped namespace dotnet_diagnostic.IDE0161.severity = silent # IDE0170: Simplify property pattern dotnet_diagnostic.IDE0170.severity = silent # IDE0180: Use tuple swap dotnet_diagnostic.IDE0180.severity = silent # IDE0200: Remove unnecessary lambda expression dotnet_diagnostic.IDE0200.severity = silent # IDE0210: Use top-level statements dotnet_diagnostic.IDE0210.severity = silent # IDE0211: Convert to 'Program.Main' style program dotnet_diagnostic.IDE0211.severity = silent # IDE0220: foreach cast dotnet_diagnostic.IDE0220.severity = silent # IDE0230: Use UTF8 string literal dotnet_diagnostic.IDE0230.severity = silent # IDE0240: Remove redundant nullable directive dotnet_diagnostic.IDE0240.severity = silent # IDE0241: Remove unnecessary nullable directive dotnet_diagnostic.IDE0241.severity = silent # IDE0250: Make struct readonly dotnet_diagnostic.IDE0250.severity = silent # IDE0251: Make member readonly dotnet_diagnostic.IDE0251.severity = silent # IDE0260: Use pattern matching dotnet_diagnostic.IDE0260.severity = silent # IDE0270: Use coalesce expression dotnet_diagnostic.IDE0270.severity = silent # IDE0280: Use 'nameof' dotnet_diagnostic.IDE0280.severity = silent # IDE0290: Use primary constructor dotnet_diagnostic.IDE0290.severity = silent # IDE0300: Use collection expression for array dotnet_diagnostic.IDE0300.severity = silent # IDE0301: Use collection expression for empty dotnet_diagnostic.IDE0301.severity = silent # IDE0302: Use collection expression for stackalloc dotnet_diagnostic.IDE0302.severity = silent # IDE0303: Use collection expression for Create() dotnet_diagnostic.IDE0303.severity = silent # IDE0304: Use collection expression for builder dotnet_diagnostic.IDE0304.severity = silent # IDE0305: Use collection expression for fluent dotnet_diagnostic.IDE0305.severity = silent # IDE1005: Delegate invocation can be simplified. dotnet_diagnostic.IDE1005.severity = silent # IDE1006: Naming Styles dotnet_diagnostic.IDE1006.severity = silent # IDE2000: C# dotnet_diagnostic.IDE2000.severity = silent # IDE2001: Embedded statements must be on their own line dotnet_diagnostic.IDE2001.severity = silent # IDE2002: Consecutive braces must not have blank line between them dotnet_diagnostic.IDE2002.severity = silent # IDE2003: C# dotnet_diagnostic.IDE2003.severity = silent # IDE2004: Blank line not allowed after constructor initializer colon dotnet_diagnostic.IDE2004.severity = silent # IDE2005: Blank line not allowed after conditional expression token dotnet_diagnostic.IDE2005.severity = silent # IDE2006: Blank line not allowed after arrow expression clause token dotnet_diagnostic.IDE2006.severity = silent # xUnit1000: Test classes must be public dotnet_diagnostic.xUnit1000.severity = warning # xUnit1001: Fact methods cannot have parameters dotnet_diagnostic.xUnit1001.severity = warning # xUnit1002: Test methods cannot have multiple Fact or Theory attributes dotnet_diagnostic.xUnit1002.severity = warning # xUnit1003: Theory methods must have test data dotnet_diagnostic.xUnit1003.severity = warning # xUnit1004: Test methods should not be skipped # dotnet_diagnostic.xUnit1004.severity = warning # xUnit1005: Fact methods should not have test data dotnet_diagnostic.xUnit1005.severity = warning # xUnit1006: Theory methods should have parameters dotnet_diagnostic.xUnit1006.severity = warning # xUnit1007: ClassData must point at a valid class dotnet_diagnostic.xUnit1007.severity = warning # xUnit1008: Test data attribute should only be used on a Theory dotnet_diagnostic.xUnit1008.severity = warning # xUnit1009: InlineData must match the number of method parameters dotnet_diagnostic.xUnit1009.severity = warning # xUnit1010: The value is not convertible to the method parameter type dotnet_diagnostic.xUnit1010.severity = warning # xUnit1011: There is no matching method parameter dotnet_diagnostic.xUnit1011.severity = warning # xUnit1012: Null should not be used for value type parameters dotnet_diagnostic.xUnit1012.severity = warning # xUnit1013: Public methods should be marked as test dotnet_diagnostic.xUnit1013.severity = warning # xUnit1014: MemberData should use nameof operator for member name dotnet_diagnostic.xUnit1014.severity = warning # xUnit1015: MemberData must reference an existing member dotnet_diagnostic.xUnit1015.severity = warning # xUnit1016: MemberData must reference a public member dotnet_diagnostic.xUnit1016.severity = warning # xUnit1017: MemberData must reference a static member dotnet_diagnostic.xUnit1017.severity = warning # xUnit1018: MemberData must reference a valid member kind dotnet_diagnostic.xUnit1018.severity = warning # xUnit1019: MemberData must reference a member providing a valid data type dotnet_diagnostic.xUnit1019.severity = warning # xUnit1020: MemberData must reference a property with a getter dotnet_diagnostic.xUnit1020.severity = warning # xUnit1021: MemberData should not have parameters if the referenced member is not a method dotnet_diagnostic.xUnit1021.severity = warning # xUnit1022: Theory methods cannot have a parameter array dotnet_diagnostic.xUnit1022.severity = warning # xUnit1023: Theory methods cannot have default parameter values dotnet_diagnostic.xUnit1023.severity = warning # xUnit1024: Test methods cannot have overloads dotnet_diagnostic.xUnit1024.severity = warning # xUnit1025: InlineData should be unique within the Theory it belongs to dotnet_diagnostic.xUnit1025.severity = warning # xUnit1026: Theory methods should use all of their parameters dotnet_diagnostic.xUnit1026.severity = warning # xUnit1030: Test methods should not call ConfigureAwait(), as it may bypass parallelization limits. dotnet_diagnostic.xUnit1030.severity = none # xUnit1031: Test methods must not use blocking task operations, as they can cause deadlocks. Use an async test method and await instead. dotnet_diagnostic.xUnit1031.severity = none # xUnit1051: Calls to methods which accept CancellationToken should use TestContext.Current.CancellationToken to allow test cancellation to be more responsive. dotnet_diagnostic.xUnit1051.severity = none # xUnit2000: Constants and literals should be the expected argument dotnet_diagnostic.xUnit2000.severity = warning # xUnit2001: Do not use invalid equality check dotnet_diagnostic.xUnit2001.severity = warning # xUnit2002: Do not use null check on value type dotnet_diagnostic.xUnit2002.severity = warning # xUnit2003: Do not use equality check to test for null value dotnet_diagnostic.xUnit2003.severity = warning # xUnit2004: Do not use equality check to test for boolean conditions dotnet_diagnostic.xUnit2004.severity = warning # xUnit2005: Do not use identity check on value type dotnet_diagnostic.xUnit2005.severity = warning # xUnit2006: Do not use invalid string equality check dotnet_diagnostic.xUnit2006.severity = warning # xUnit2007: Do not use typeof expression to check the type dotnet_diagnostic.xUnit2007.severity = warning # xUnit2008: Do not use boolean check to match on regular expressions dotnet_diagnostic.xUnit2008.severity = warning # xUnit2009: Do not use boolean check to check for substrings dotnet_diagnostic.xUnit2009.severity = warning # xUnit2010: Do not use boolean check to check for string equality dotnet_diagnostic.xUnit2010.severity = warning # xUnit2011: Do not use empty collection check dotnet_diagnostic.xUnit2011.severity = warning # xUnit2012: Do not use Enumerable.Any() to check if a value exists in a collection dotnet_diagnostic.xUnit2012.severity = warning # xUnit2013: Do not use equality check to check for collection size. dotnet_diagnostic.xUnit2013.severity = none # xUnit2014: Do not use throws check to check for asynchronously thrown exception dotnet_diagnostic.xUnit2014.severity = none # xUnit2015: Do not use typeof expression to check the exception type dotnet_diagnostic.xUnit2015.severity = warning # xUnit2016: Keep precision in the allowed range when asserting equality of doubles or decimals dotnet_diagnostic.xUnit2016.severity = warning # xUnit2017: Do not use Contains() to check if a value exists in a collection dotnet_diagnostic.xUnit2017.severity = none # xUnit2018: Do not compare an object's exact type to an abstract class or interface dotnet_diagnostic.xUnit2018.severity = warning # xUnit2019: Do not use obsolete throws check to check for asynchronously thrown exception dotnet_diagnostic.xUnit2019.severity = warning # xUnit3000: Test case classes must derive directly or indirectly from Xunit.LongLivedMarshalByRefObject dotnet_diagnostic.xUnit3000.severity = warning # xUnit3001: Classes that implement Xunit.Abstractions.IXunitSerializable must have a public parameterless constructor dotnet_diagnostic.xUnit3001.severity = warning ================================================ FILE: eng/PoliCheckExclusions.xml ================================================ SWAGGER.JSON ================================================ FILE: eng/Publishing.props ================================================ 3 <_UploadPathRoot>reverse-proxy $(PublishDependsOnTargets);_PublishBlobItems $([MSBuild]::NormalizeDirectory('$(ArtifactsDir)', 'YarpAppArtifacts', '$(Configuration)')) <_YarpAppFilesToPublish Include="$(YarpAppArtifactsOutputDir)\**\*.tar.gz" /> <_YarpAppFilesToPublish Include="$(YarpAppArtifactsOutputDir)\**\*.tar.gz.sha512" /> true true $(_UploadPathRoot)/$(_PackageVersion)/%(Filename)%(Extension) ================================================ FILE: eng/Signing.props ================================================ ================================================ FILE: eng/Version.Details.xml ================================================ https://github.com/dotnet/arcade 88c88084abfa1e379f54933af89e43fa774e323c https://github.com/dotnet/arcade 88c88084abfa1e379f54933af89e43fa774e323c https://github.com/dotnet/arcade 88c88084abfa1e379f54933af89e43fa774e323c ================================================ FILE: eng/Versions.props ================================================ 3.0.0 preview.1 release 8.0.0 6.0.36 0.2.0-alpha.24576.2 11.0.0-beta.26122.1 6.0.0 4.18.4 4.9.4 4.3.0 8.4.2 1.3.0 8.2.1 3.0.1 1.1.0 3.1.1 16.3.0 18.0.13 7.0.2 13.0.3 2.3.0 9.1.0 9.1.0 1.12.0 1.12.0 1.12.0 1.12.0 1.12.0 9.0.0 ================================================ FILE: eng/common/BuildConfiguration/build-configuration.json ================================================ { "RetryCountLimit": 1, "RetryByAnyError": false } ================================================ FILE: eng/common/CIBuild.cmd ================================================ @echo off powershell -ExecutionPolicy ByPass -NoProfile -command "& """%~dp0Build.ps1""" -restore -build -test -sign -pack -publish -ci %*" ================================================ FILE: eng/common/PSScriptAnalyzerSettings.psd1 ================================================ @{ IncludeRules=@('PSAvoidUsingCmdletAliases', 'PSAvoidUsingWMICmdlet', 'PSAvoidUsingPositionalParameters', 'PSAvoidUsingInvokeExpression', 'PSUseDeclaredVarsMoreThanAssignments', 'PSUseCmdletCorrectly', 'PSStandardDSCFunctionsInResource', 'PSUseIdenticalMandatoryParametersForDSC', 'PSUseIdenticalParametersForDSC') } ================================================ FILE: eng/common/README.md ================================================ # Don't touch this folder uuuuuuuuuuuuuuuuuuuu u" uuuuuuuuuuuuuuuuuu "u u" u$$$$$$$$$$$$$$$$$$$$u "u u" u$$$$$$$$$$$$$$$$$$$$$$$$u "u u" u$$$$$$$$$$$$$$$$$$$$$$$$$$$$u "u u" u$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$u "u u" u$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$u "u $ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ $ $ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ $ $ $$$" ... "$... ...$" ... "$$$ ... "$$$ $ $ $$$u `"$$$$$$$ $$$ $$$$$ $$ $$$ $$$ $ $ $$$$$$uu "$$$$ $$$ $$$$$ $$ """ u$$$ $ $ $$$""$$$ $$$$ $$$u "$$$" u$$ $$$$$$$$ $ $ $$$$....,$$$$$..$$$$$....,$$$$..$$$$$$$$ $ $ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ $ "u "$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$" u" "u "$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$" u" "u "$$$$$$$$$$$$$$$$$$$$$$$$$$$$" u" "u "$$$$$$$$$$$$$$$$$$$$$$$$" u" "u "$$$$$$$$$$$$$$$$$$$$" u" "u """""""""""""""""" u" """""""""""""""""""" !!! Changes made in this directory are subject to being overwritten by automation !!! The files in this directory are shared by all Arcade repos and managed by automation. If you need to make changes to these files, open an issue or submit a pull request to https://github.com/dotnet/arcade first. ================================================ FILE: eng/common/SetupNugetSources.ps1 ================================================ # This script adds internal feeds required to build commits that depend on internal package sources. For instance, # dotnet6-internal would be added automatically if dotnet6 was found in the nuget.config file. In addition also enables # disabled internal Maestro (darc-int*) feeds. # # Optionally, this script also adds a credential entry for each of the internal feeds if supplied. # # See example call for this script below. # # - task: PowerShell@2 # displayName: Setup internal Feeds Credentials # condition: eq(variables['Agent.OS'], 'Windows_NT') # inputs: # filePath: $(System.DefaultWorkingDirectory)/eng/common/SetupNugetSources.ps1 # arguments: -ConfigFile $(System.DefaultWorkingDirectory)/NuGet.config -Password $Env:Token # env: # Token: $(dn-bot-dnceng-artifact-feeds-rw) # # Note that the NuGetAuthenticate task should be called after SetupNugetSources. # This ensures that: # - Appropriate creds are set for the added internal feeds (if not supplied to the scrupt) # - The credential provider is installed. # # This logic is also abstracted into enable-internal-sources.yml. [CmdletBinding()] param ( [Parameter(Mandatory = $true)][string]$ConfigFile, $Password ) $ErrorActionPreference = "Stop" Set-StrictMode -Version 2.0 [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 . $PSScriptRoot\tools.ps1 # Adds or enables the package source with the given name function AddOrEnablePackageSource($sources, $disabledPackageSources, $SourceName, $SourceEndPoint, $creds, $Username, $pwd) { if ($disabledPackageSources -eq $null -or -not (EnableInternalPackageSource -DisabledPackageSources $disabledPackageSources -Creds $creds -PackageSourceName $SourceName)) { AddPackageSource -Sources $sources -SourceName $SourceName -SourceEndPoint $SourceEndPoint -Creds $creds -Username $userName -pwd $Password } } # Add source entry to PackageSources function AddPackageSource($sources, $SourceName, $SourceEndPoint, $creds, $Username, $pwd) { $packageSource = $sources.SelectSingleNode("add[@key='$SourceName']") if ($packageSource -eq $null) { Write-Host "Adding package source $SourceName" $packageSource = $doc.CreateElement("add") $packageSource.SetAttribute("key", $SourceName) $packageSource.SetAttribute("value", $SourceEndPoint) $sources.AppendChild($packageSource) | Out-Null } else { Write-Host "Package source $SourceName already present and enabled." } AddCredential -Creds $creds -Source $SourceName -Username $Username -pwd $pwd } # Add a credential node for the specified source function AddCredential($creds, $source, $username, $pwd) { # If no cred supplied, don't do anything. if (!$pwd) { return; } Write-Host "Inserting credential for feed: " $source # Looks for credential configuration for the given SourceName. Create it if none is found. $sourceElement = $creds.SelectSingleNode($Source) if ($sourceElement -eq $null) { $sourceElement = $doc.CreateElement($Source) $creds.AppendChild($sourceElement) | Out-Null } # Add the node to the credential if none is found. $usernameElement = $sourceElement.SelectSingleNode("add[@key='Username']") if ($usernameElement -eq $null) { $usernameElement = $doc.CreateElement("add") $usernameElement.SetAttribute("key", "Username") $sourceElement.AppendChild($usernameElement) | Out-Null } $usernameElement.SetAttribute("value", $Username) # Add the to the credential if none is found. # Add it as a clear text because there is no support for encrypted ones in non-windows .Net SDKs. # -> https://github.com/NuGet/Home/issues/5526 $passwordElement = $sourceElement.SelectSingleNode("add[@key='ClearTextPassword']") if ($passwordElement -eq $null) { $passwordElement = $doc.CreateElement("add") $passwordElement.SetAttribute("key", "ClearTextPassword") $sourceElement.AppendChild($passwordElement) | Out-Null } $passwordElement.SetAttribute("value", $pwd) } # Enable all darc-int package sources. function EnableMaestroInternalPackageSources($DisabledPackageSources, $Creds) { $maestroInternalSources = $DisabledPackageSources.SelectNodes("add[contains(@key,'darc-int')]") ForEach ($DisabledPackageSource in $maestroInternalSources) { EnableInternalPackageSource -DisabledPackageSources $DisabledPackageSources -Creds $Creds -PackageSourceName $DisabledPackageSource.key } } # Enables an internal package source by name, if found. Returns true if the package source was found and enabled, false otherwise. function EnableInternalPackageSource($DisabledPackageSources, $Creds, $PackageSourceName) { $DisabledPackageSource = $DisabledPackageSources.SelectSingleNode("add[@key='$PackageSourceName']") if ($DisabledPackageSource) { Write-Host "Enabling internal source '$($DisabledPackageSource.key)'." # Due to https://github.com/NuGet/Home/issues/10291, we must actually remove the disabled entries $DisabledPackageSources.RemoveChild($DisabledPackageSource) AddCredential -Creds $creds -Source $DisabledPackageSource.Key -Username $userName -pwd $Password return $true } return $false } if (!(Test-Path $ConfigFile -PathType Leaf)) { Write-PipelineTelemetryError -Category 'Build' -Message "Eng/common/SetupNugetSources.ps1 returned a non-zero exit code. Couldn't find the NuGet config file: $ConfigFile" ExitWithExitCode 1 } # Load NuGet.config $doc = New-Object System.Xml.XmlDocument $filename = (Get-Item $ConfigFile).FullName $doc.Load($filename) # Get reference to - fail if none exist $sources = $doc.DocumentElement.SelectSingleNode("packageSources") if ($sources -eq $null) { Write-PipelineTelemetryError -Category 'Build' -Message "Eng/common/SetupNugetSources.ps1 returned a non-zero exit code. NuGet config file must contain a packageSources section: $ConfigFile" ExitWithExitCode 1 } $creds = $null $feedSuffix = "v3/index.json" if ($Password) { $feedSuffix = "v2" # Looks for a node. Create it if none is found. $creds = $doc.DocumentElement.SelectSingleNode("packageSourceCredentials") if ($creds -eq $null) { $creds = $doc.CreateElement("packageSourceCredentials") $doc.DocumentElement.AppendChild($creds) | Out-Null } } $userName = "dn-bot" # Check for disabledPackageSources; we'll enable any darc-int ones we find there $disabledSources = $doc.DocumentElement.SelectSingleNode("disabledPackageSources") if ($disabledSources -ne $null) { Write-Host "Checking for any darc-int disabled package sources in the disabledPackageSources node" EnableMaestroInternalPackageSources -DisabledPackageSources $disabledSources -Creds $creds } $dotnetVersions = @('5','6','7','8','9','10') foreach ($dotnetVersion in $dotnetVersions) { $feedPrefix = "dotnet" + $dotnetVersion; $dotnetSource = $sources.SelectSingleNode("add[@key='$feedPrefix']") if ($dotnetSource -ne $null) { AddOrEnablePackageSource -Sources $sources -DisabledPackageSources $disabledSources -SourceName "$feedPrefix-internal" -SourceEndPoint "https://pkgs.dev.azure.com/dnceng/internal/_packaging/$feedPrefix-internal/nuget/$feedSuffix" -Creds $creds -Username $userName -pwd $Password AddOrEnablePackageSource -Sources $sources -DisabledPackageSources $disabledSources -SourceName "$feedPrefix-internal-transport" -SourceEndPoint "https://pkgs.dev.azure.com/dnceng/internal/_packaging/$feedPrefix-internal-transport/nuget/$feedSuffix" -Creds $creds -Username $userName -pwd $Password } } $doc.Save($filename) ================================================ FILE: eng/common/SetupNugetSources.sh ================================================ #!/usr/bin/env bash # This script adds internal feeds required to build commits that depend on internal package sources. For instance, # dotnet6-internal would be added automatically if dotnet6 was found in the nuget.config file. In addition also enables # disabled internal Maestro (darc-int*) feeds. # # Optionally, this script also adds a credential entry for each of the internal feeds if supplied. # # See example call for this script below. # # - task: Bash@3 # displayName: Setup Internal Feeds # inputs: # filePath: $(System.DefaultWorkingDirectory)/eng/common/SetupNugetSources.sh # arguments: $(System.DefaultWorkingDirectory)/NuGet.config # condition: ne(variables['Agent.OS'], 'Windows_NT') # - task: NuGetAuthenticate@1 # # Note that the NuGetAuthenticate task should be called after SetupNugetSources. # This ensures that: # - Appropriate creds are set for the added internal feeds (if not supplied to the scrupt) # - The credential provider is installed. # # This logic is also abstracted into enable-internal-sources.yml. ConfigFile=$1 CredToken=$2 NL='\n' TB=' ' source="${BASH_SOURCE[0]}" # resolve $source until the file is no longer a symlink while [[ -h "$source" ]]; do scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" source="$(readlink "$source")" # if $source was a relative symlink, we need to resolve it relative to the path where the # symlink file was located [[ $source != /* ]] && source="$scriptroot/$source" done scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" . "$scriptroot/tools.sh" if [ ! -f "$ConfigFile" ]; then Write-PipelineTelemetryError -Category 'Build' "Error: Eng/common/SetupNugetSources.sh returned a non-zero exit code. Couldn't find the NuGet config file: $ConfigFile" ExitWithExitCode 1 fi if [[ `uname -s` == "Darwin" ]]; then NL=$'\\\n' TB='' fi # Enables an internal package source by name, if found. Returns 0 if found and enabled, 1 if not found. EnableInternalPackageSource() { local PackageSourceName="$1" # Check if disabledPackageSources section exists grep -i "" "$ConfigFile" > /dev/null if [ "$?" != "0" ]; then return 1 # No disabled sources section fi # Check if this source name is disabled grep -i " /dev/null if [ "$?" == "0" ]; then echo "Enabling internal source '$PackageSourceName'." # Remove the disabled entry (including any surrounding comments or whitespace on the same line) sed -i.bak "//d" "$ConfigFile" # Add the source name to PackageSources for credential handling PackageSources+=("$PackageSourceName") return 0 # Found and enabled fi return 1 # Not found in disabled sources } # Add source entry to PackageSources AddPackageSource() { local SourceName="$1" local SourceEndPoint="$2" # Check if source already exists grep -i " /dev/null if [ "$?" == "0" ]; then echo "Package source $SourceName already present and enabled." PackageSources+=("$SourceName") return fi echo "Adding package source $SourceName" PackageSourcesNodeFooter="" PackageSourceTemplate="${TB}" sed -i.bak "s|$PackageSourcesNodeFooter|$PackageSourceTemplate${NL}$PackageSourcesNodeFooter|" "$ConfigFile" PackageSources+=("$SourceName") } # Adds or enables the package source with the given name AddOrEnablePackageSource() { local SourceName="$1" local SourceEndPoint="$2" # Try to enable if disabled, if not found then add new source EnableInternalPackageSource "$SourceName" if [ "$?" != "0" ]; then AddPackageSource "$SourceName" "$SourceEndPoint" fi } # Enable all darc-int package sources EnableMaestroInternalPackageSources() { # Check if disabledPackageSources section exists grep -i "" "$ConfigFile" > /dev/null if [ "$?" != "0" ]; then return # No disabled sources section fi # Find all darc-int disabled sources local DisabledDarcIntSources=() DisabledDarcIntSources+=$(grep -oh '"darc-int-[^"]*" value="true"' "$ConfigFile" | tr -d '"') for DisabledSourceName in ${DisabledDarcIntSources[@]} ; do if [[ $DisabledSourceName == darc-int* ]]; then EnableInternalPackageSource "$DisabledSourceName" fi done } # Ensure there is a ... section. grep -i "" $ConfigFile if [ "$?" != "0" ]; then Write-PipelineTelemetryError -Category 'Build' "Error: Eng/common/SetupNugetSources.sh returned a non-zero exit code. NuGet config file must contain a packageSources section: $ConfigFile" ExitWithExitCode 1 fi PackageSources=() # Set feed suffix based on whether credentials are provided FeedSuffix="v3/index.json" if [ -n "$CredToken" ]; then FeedSuffix="v2" # Ensure there is a ... section. grep -i "" $ConfigFile if [ "$?" != "0" ]; then echo "Adding ... section." PackageSourcesNodeFooter="" PackageSourceCredentialsTemplate="${TB}${NL}${TB}" sed -i.bak "s|$PackageSourcesNodeFooter|$PackageSourcesNodeFooter${NL}$PackageSourceCredentialsTemplate|" $ConfigFile fi fi # Check for disabledPackageSources; we'll enable any darc-int ones we find there grep -i "" $ConfigFile > /dev/null if [ "$?" == "0" ]; then echo "Checking for any darc-int disabled package sources in the disabledPackageSources node" EnableMaestroInternalPackageSources fi DotNetVersions=('5' '6' '7' '8' '9' '10') for DotNetVersion in ${DotNetVersions[@]} ; do FeedPrefix="dotnet${DotNetVersion}"; grep -i " /dev/null if [ "$?" == "0" ]; then AddOrEnablePackageSource "$FeedPrefix-internal" "https://pkgs.dev.azure.com/dnceng/internal/_packaging/$FeedPrefix-internal/nuget/$FeedSuffix" AddOrEnablePackageSource "$FeedPrefix-internal-transport" "https://pkgs.dev.azure.com/dnceng/internal/_packaging/$FeedPrefix-internal-transport/nuget/$FeedSuffix" fi done # I want things split line by line PrevIFS=$IFS IFS=$'\n' PackageSources+="$IFS" PackageSources+=$(grep -oh '"darc-int-[^"]*"' $ConfigFile | tr -d '"') IFS=$PrevIFS if [ "$CredToken" ]; then for FeedName in ${PackageSources[@]} ; do # Check if there is no existing credential for this FeedName grep -i "<$FeedName>" $ConfigFile if [ "$?" != "0" ]; then echo " Inserting credential for feed: $FeedName" PackageSourceCredentialsNodeFooter="" NewCredential="${TB}${TB}<$FeedName>${NL}${TB}${NL}${TB}${TB}${NL}${TB}${TB}" sed -i.bak "s|$PackageSourceCredentialsNodeFooter|$NewCredential${NL}$PackageSourceCredentialsNodeFooter|" $ConfigFile fi done fi ================================================ FILE: eng/common/build.cmd ================================================ @echo off powershell -ExecutionPolicy ByPass -NoProfile -command "& """%~dp0build.ps1""" %*" exit /b %ErrorLevel% ================================================ FILE: eng/common/build.ps1 ================================================ [CmdletBinding(PositionalBinding=$false)] Param( [string][Alias('c')]$configuration = "Debug", [string]$platform = $null, [string] $projects, [string][Alias('v')]$verbosity = "minimal", [string] $msbuildEngine = $null, [bool] $warnAsError = $true, [bool] $nodeReuse = $true, [switch] $buildCheck = $false, [switch][Alias('r')]$restore, [switch] $deployDeps, [switch][Alias('b')]$build, [switch] $rebuild, [switch] $deploy, [switch][Alias('t')]$test, [switch] $integrationTest, [switch] $performanceTest, [switch] $sign, [switch] $pack, [switch] $publish, [switch] $clean, [switch][Alias('pb')]$productBuild, [switch]$fromVMR, [switch][Alias('bl')]$binaryLog, [switch][Alias('nobl')]$excludeCIBinarylog, [switch] $ci, [switch] $prepareMachine, [string] $runtimeSourceFeed = '', [string] $runtimeSourceFeedKey = '', [switch] $excludePrereleaseVS, [switch] $nativeToolsOnMachine, [switch] $help, [Parameter(ValueFromRemainingArguments=$true)][String[]]$properties ) # Unset 'Platform' environment variable to avoid unwanted collision in InstallDotNetCore.targets file # some computer has this env var defined (e.g. Some HP) if($env:Platform) { $env:Platform="" } function Print-Usage() { Write-Host "Common settings:" Write-Host " -configuration Build configuration: 'Debug' or 'Release' (short: -c)" Write-Host " -platform Platform configuration: 'x86', 'x64' or any valid Platform value to pass to msbuild" Write-Host " -verbosity Msbuild verbosity: q[uiet], m[inimal], n[ormal], d[etailed], and diag[nostic] (short: -v)" Write-Host " -binaryLog Output binary log (short: -bl)" Write-Host " -help Print help and exit" Write-Host "" Write-Host "Actions:" Write-Host " -restore Restore dependencies (short: -r)" Write-Host " -build Build solution (short: -b)" Write-Host " -rebuild Rebuild solution" Write-Host " -deploy Deploy built VSIXes" Write-Host " -deployDeps Deploy dependencies (e.g. VSIXes for integration tests)" Write-Host " -test Run all unit tests in the solution (short: -t)" Write-Host " -integrationTest Run all integration tests in the solution" Write-Host " -performanceTest Run all performance tests in the solution" Write-Host " -pack Package build outputs into NuGet packages and Willow components" Write-Host " -sign Sign build outputs" Write-Host " -publish Publish artifacts (e.g. symbols)" Write-Host " -clean Clean the solution" Write-Host " -productBuild Build the solution in the way it will be built in the full .NET product (VMR) build (short: -pb)" Write-Host "" Write-Host "Advanced settings:" Write-Host " -projects Semi-colon delimited list of sln/proj's to build. Globbing is supported (*.sln)" Write-Host " -ci Set when running on CI server" Write-Host " -excludeCIBinarylog Don't output binary log (short: -nobl)" Write-Host " -prepareMachine Prepare machine for CI run, clean up processes after build" Write-Host " -warnAsError Sets warnaserror msbuild parameter ('true' or 'false')" Write-Host " -msbuildEngine Msbuild engine to use to run build ('dotnet', 'vs', or unspecified)." Write-Host " -excludePrereleaseVS Set to exclude build engines in prerelease versions of Visual Studio" Write-Host " -nativeToolsOnMachine Sets the native tools on machine environment variable (indicating that the script should use native tools on machine)" Write-Host " -nodeReuse Sets nodereuse msbuild parameter ('true' or 'false')" Write-Host " -buildCheck Sets /check msbuild parameter" Write-Host " -fromVMR Set when building from within the VMR" Write-Host "" Write-Host "Command line arguments not listed above are passed thru to msbuild." Write-Host "The above arguments can be shortened as much as to be unambiguous (e.g. -co for configuration, -t for test, etc.)." } . $PSScriptRoot\tools.ps1 function InitializeCustomToolset { if (-not $restore) { return } $script = Join-Path $EngRoot 'restore-toolset.ps1' if (Test-Path $script) { . $script } } function Build { $toolsetBuildProj = InitializeToolset InitializeCustomToolset $bl = if ($binaryLog) { '/bl:' + (Join-Path $LogDir 'Build.binlog') } else { '' } $platformArg = if ($platform) { "/p:Platform=$platform" } else { '' } $check = if ($buildCheck) { '/check' } else { '' } if ($projects) { # Re-assign properties to a new variable because PowerShell doesn't let us append properties directly for unclear reasons. # Explicitly set the type as string[] because otherwise PowerShell would make this char[] if $properties is empty. [string[]] $msbuildArgs = $properties # Resolve relative project paths into full paths $projects = ($projects.Split(';').ForEach({Resolve-Path $_}) -join ';') $msbuildArgs += "/p:Projects=$projects" $properties = $msbuildArgs } MSBuild $toolsetBuildProj ` $bl ` $platformArg ` $check ` /p:Configuration=$configuration ` /p:RepoRoot=$RepoRoot ` /p:Restore=$restore ` /p:DeployDeps=$deployDeps ` /p:Build=$build ` /p:Rebuild=$rebuild ` /p:Deploy=$deploy ` /p:Test=$test ` /p:Pack=$pack ` /p:DotNetBuild=$productBuild ` /p:DotNetBuildFromVMR=$fromVMR ` /p:IntegrationTest=$integrationTest ` /p:PerformanceTest=$performanceTest ` /p:Sign=$sign ` /p:Publish=$publish ` /p:RestoreStaticGraphEnableBinaryLogger=$binaryLog ` @properties } try { if ($clean) { if (Test-Path $ArtifactsDir) { Remove-Item -Recurse -Force $ArtifactsDir Write-Host 'Artifacts directory deleted.' } exit 0 } if ($help -or (($null -ne $properties) -and ($properties.Contains('/help') -or $properties.Contains('/?')))) { Print-Usage exit 0 } if ($ci) { if (-not $excludeCIBinarylog) { $binaryLog = $true } $nodeReuse = $false } if ($nativeToolsOnMachine) { $env:NativeToolsOnMachine = $true } if ($restore) { InitializeNativeTools } Build } catch { Write-Host $_.ScriptStackTrace Write-PipelineTelemetryError -Category 'InitializeToolset' -Message $_ ExitWithExitCode 1 } ExitWithExitCode 0 ================================================ FILE: eng/common/build.sh ================================================ #!/usr/bin/env bash # Stop script if unbound variable found (use ${var:-} if intentional) set -u # Stop script if command returns non-zero exit code. # Prevents hidden errors caused by missing error code propagation. set -e usage() { echo "Common settings:" echo " --configuration Build configuration: 'Debug' or 'Release' (short: -c)" echo " --verbosity Msbuild verbosity: q[uiet], m[inimal], n[ormal], d[etailed], and diag[nostic] (short: -v)" echo " --binaryLog Create MSBuild binary log (short: -bl)" echo " --help Print help and exit (short: -h)" echo "" echo "Actions:" echo " --restore Restore dependencies (short: -r)" echo " --build Build solution (short: -b)" echo " --sourceBuild Source-build the solution (short: -sb)" echo " Will additionally trigger the following actions: --restore, --build, --pack" echo " If --configuration is not set explicitly, will also set it to 'Release'" echo " --productBuild Build the solution in the way it will be built in the full .NET product (VMR) build (short: -pb)" echo " Will additionally trigger the following actions: --restore, --build, --pack" echo " If --configuration is not set explicitly, will also set it to 'Release'" echo " --rebuild Rebuild solution" echo " --test Run all unit tests in the solution (short: -t)" echo " --integrationTest Run all integration tests in the solution" echo " --performanceTest Run all performance tests in the solution" echo " --pack Package build outputs into NuGet packages and Willow components" echo " --sign Sign build outputs" echo " --publish Publish artifacts (e.g. symbols)" echo " --clean Clean the solution" echo "" echo "Advanced settings:" echo " --projects Project or solution file(s) to build" echo " --ci Set when running on CI server" echo " --excludeCIBinarylog Don't output binary log (short: -nobl)" echo " --prepareMachine Prepare machine for CI run, clean up processes after build" echo " --nodeReuse Sets nodereuse msbuild parameter ('true' or 'false')" echo " --warnAsError Sets warnaserror msbuild parameter ('true' or 'false')" echo " --buildCheck Sets /check msbuild parameter" echo " --fromVMR Set when building from within the VMR" echo "" echo "Command line arguments not listed above are passed thru to msbuild." echo "Arguments can also be passed in with a single hyphen." } source="${BASH_SOURCE[0]}" # resolve $source until the file is no longer a symlink while [[ -h "$source" ]]; do scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" source="$(readlink "$source")" # if $source was a relative symlink, we need to resolve it relative to the path where the # symlink file was located [[ $source != /* ]] && source="$scriptroot/$source" done scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" restore=false build=false source_build=false product_build=false from_vmr=false rebuild=false test=false integration_test=false performance_test=false pack=false publish=false sign=false public=false ci=false clean=false warn_as_error=true node_reuse=true build_check=false binary_log=false exclude_ci_binary_log=false pipelines_log=false projects='' configuration='' prepare_machine=false verbosity='minimal' runtime_source_feed='' runtime_source_feed_key='' properties=() while [[ $# -gt 0 ]]; do opt="$(echo "${1/#--/-}" | tr "[:upper:]" "[:lower:]")" case "$opt" in -help|-h) usage exit 0 ;; -clean) clean=true ;; -configuration|-c) configuration=$2 shift ;; -verbosity|-v) verbosity=$2 shift ;; -binarylog|-bl) binary_log=true ;; -excludecibinarylog|-nobl) exclude_ci_binary_log=true ;; -pipelineslog|-pl) pipelines_log=true ;; -restore|-r) restore=true ;; -build|-b) build=true ;; -rebuild) rebuild=true ;; -pack) pack=true ;; -sourcebuild|-source-build|-sb) build=true source_build=true product_build=true restore=true pack=true ;; -productbuild|-product-build|-pb) build=true product_build=true restore=true pack=true ;; -fromvmr|-from-vmr) from_vmr=true ;; -test|-t) test=true ;; -integrationtest) integration_test=true ;; -performancetest) performance_test=true ;; -sign) sign=true ;; -publish) publish=true ;; -preparemachine) prepare_machine=true ;; -projects) projects=$2 shift ;; -ci) ci=true ;; -warnaserror) warn_as_error=$2 shift ;; -nodereuse) node_reuse=$2 shift ;; -buildcheck) build_check=true ;; -runtimesourcefeed) runtime_source_feed=$2 shift ;; -runtimesourcefeedkey) runtime_source_feed_key=$2 shift ;; *) properties+=("$1") ;; esac shift done if [[ -z "$configuration" ]]; then if [[ "$source_build" = true ]]; then configuration="Release"; else configuration="Debug"; fi fi if [[ "$ci" == true ]]; then pipelines_log=true node_reuse=false if [[ "$exclude_ci_binary_log" == false ]]; then binary_log=true fi fi . "$scriptroot/tools.sh" function InitializeCustomToolset { local script="$eng_root/restore-toolset.sh" if [[ -a "$script" ]]; then . "$script" fi } function Build { InitializeToolset InitializeCustomToolset if [[ ! -z "$projects" ]]; then properties+=("/p:Projects=$projects") fi local bl="" if [[ "$binary_log" == true ]]; then bl="/bl:\"$log_dir/Build.binlog\"" fi local check="" if [[ "$build_check" == true ]]; then check="/check" fi MSBuild $_InitializeToolset \ $bl \ $check \ /p:Configuration=$configuration \ /p:RepoRoot="$repo_root" \ /p:Restore=$restore \ /p:Build=$build \ /p:DotNetBuild=$product_build \ /p:DotNetBuildSourceOnly=$source_build \ /p:DotNetBuildFromVMR=$from_vmr \ /p:Rebuild=$rebuild \ /p:Test=$test \ /p:Pack=$pack \ /p:IntegrationTest=$integration_test \ /p:PerformanceTest=$performance_test \ /p:Sign=$sign \ /p:Publish=$publish \ /p:RestoreStaticGraphEnableBinaryLogger=$binary_log \ ${properties[@]+"${properties[@]}"} ExitWithExitCode 0 } if [[ "$clean" == true ]]; then if [ -d "$artifacts_dir" ]; then rm -rf $artifacts_dir echo "Artifacts directory deleted." fi exit 0 fi if [[ "$restore" == true ]]; then InitializeNativeTools fi Build ================================================ FILE: eng/common/cibuild.sh ================================================ #!/usr/bin/env bash source="${BASH_SOURCE[0]}" # resolve $SOURCE until the file is no longer a symlink while [[ -h $source ]]; do scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" source="$(readlink "$source")" # if $source was a relative symlink, we need to resolve it relative to the path where # the symlink file was located [[ $source != /* ]] && source="$scriptroot/$source" done scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" . "$scriptroot/build.sh" --restore --build --test --pack --publish --ci $@ ================================================ FILE: eng/common/core-templates/job/job.yml ================================================ parameters: # Job schema parameters - https://docs.microsoft.com/en-us/azure/devops/pipelines/yaml-schema?view=vsts&tabs=schema#job cancelTimeoutInMinutes: '' condition: '' container: '' continueOnError: false dependsOn: '' displayName: '' pool: '' steps: [] strategy: '' timeoutInMinutes: '' variables: [] workspace: '' templateContext: {} # Job base template specific parameters # See schema documentation - https://github.com/dotnet/arcade/blob/master/Documentation/AzureDevOps/TemplateSchema.md # publishing defaults artifacts: '' enableMicrobuild: false enablePreviewMicrobuild: false microbuildPluginVersion: 'latest' enableMicrobuildForMacAndLinux: false microbuildUseESRP: true enablePublishBuildArtifacts: false enablePublishBuildAssets: false enablePublishTestResults: false enableBuildRetry: false mergeTestResults: false testRunTitle: '' testResultsFormat: '' name: '' componentGovernanceSteps: [] preSteps: [] artifactPublishSteps: [] runAsPublic: false # 1es specific parameters is1ESPipeline: '' jobs: - job: ${{ parameters.name }} ${{ if ne(parameters.cancelTimeoutInMinutes, '') }}: cancelTimeoutInMinutes: ${{ parameters.cancelTimeoutInMinutes }} ${{ if ne(parameters.condition, '') }}: condition: ${{ parameters.condition }} ${{ if ne(parameters.container, '') }}: container: ${{ parameters.container }} ${{ if ne(parameters.continueOnError, '') }}: continueOnError: ${{ parameters.continueOnError }} ${{ if ne(parameters.dependsOn, '') }}: dependsOn: ${{ parameters.dependsOn }} ${{ if ne(parameters.displayName, '') }}: displayName: ${{ parameters.displayName }} ${{ if ne(parameters.pool, '') }}: pool: ${{ parameters.pool }} ${{ if ne(parameters.strategy, '') }}: strategy: ${{ parameters.strategy }} ${{ if ne(parameters.timeoutInMinutes, '') }}: timeoutInMinutes: ${{ parameters.timeoutInMinutes }} ${{ if ne(parameters.templateContext, '') }}: templateContext: ${{ parameters.templateContext }} variables: - name: AllowPtrToDetectTestRunRetryFiles value: true - ${{ if ne(parameters.enableTelemetry, 'false') }}: - name: DOTNET_CLI_TELEMETRY_PROFILE value: '$(Build.Repository.Uri)' # Retry signature validation up to three times, waiting 2 seconds between attempts. # See https://learn.microsoft.com/en-us/nuget/reference/errors-and-warnings/nu3028#retry-untrusted-root-failures - name: NUGET_EXPERIMENTAL_CHAIN_BUILD_RETRY_POLICY value: 3,2000 - ${{ each variable in parameters.variables }}: # handle name-value variable syntax # example: # - name: [key] # value: [value] - ${{ if ne(variable.name, '') }}: - name: ${{ variable.name }} value: ${{ variable.value }} # handle variable groups - ${{ if ne(variable.group, '') }}: - group: ${{ variable.group }} # handle template variable syntax # example: # - template: path/to/template.yml # parameters: # [key]: [value] - ${{ if ne(variable.template, '') }}: - template: ${{ variable.template }} ${{ if ne(variable.parameters, '') }}: parameters: ${{ variable.parameters }} # handle key-value variable syntax. # example: # - [key]: [value] - ${{ if and(eq(variable.name, ''), eq(variable.group, ''), eq(variable.template, '')) }}: - ${{ each pair in variable }}: - name: ${{ pair.key }} value: ${{ pair.value }} # DotNet-HelixApi-Access provides 'HelixApiAccessToken' for internal builds - ${{ if and(eq(parameters.enableTelemetry, 'true'), eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: - group: DotNet-HelixApi-Access ${{ if ne(parameters.workspace, '') }}: workspace: ${{ parameters.workspace }} steps: - ${{ if eq(parameters.is1ESPipeline, '') }}: - 'Illegal entry point, is1ESPipeline is not defined. Repository yaml should not directly reference templates in core-templates folder.': error - ${{ if ne(parameters.preSteps, '') }}: - ${{ each preStep in parameters.preSteps }}: - ${{ preStep }} - ${{ if and(eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: - template: /eng/common/core-templates/steps/install-microbuild.yml parameters: enableMicrobuild: ${{ parameters.enableMicrobuild }} enablePreviewMicrobuild: ${{ parameters.enablePreviewMicrobuild }} microbuildPluginVersion: ${{ parameters.microbuildPluginVersion }} enableMicrobuildForMacAndLinux: ${{ parameters.enableMicrobuildForMacAndLinux }} microbuildUseESRP: ${{ parameters.microbuildUseESRP }} continueOnError: ${{ parameters.continueOnError }} - ${{ if and(eq(parameters.runAsPublic, 'false'), eq(variables['System.TeamProject'], 'internal')) }}: - task: NuGetAuthenticate@1 - ${{ if and(ne(parameters.artifacts.download, 'false'), ne(parameters.artifacts.download, '')) }}: - task: DownloadPipelineArtifact@2 inputs: buildType: current artifactName: ${{ coalesce(parameters.artifacts.download.name, 'Artifacts_$(Agent.OS)_$(_BuildConfig)') }} targetPath: ${{ coalesce(parameters.artifacts.download.path, 'artifacts') }} itemPattern: ${{ coalesce(parameters.artifacts.download.pattern, '**') }} - ${{ each step in parameters.steps }}: - ${{ step }} - ${{ each step in parameters.componentGovernanceSteps }}: - ${{ step }} - ${{ if and(eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: - template: /eng/common/core-templates/steps/cleanup-microbuild.yml parameters: enableMicrobuild: ${{ parameters.enableMicrobuild }} enablePreviewMicrobuild: ${{ parameters.enablePreviewMicrobuild }} microbuildPluginVersion: ${{ parameters.microbuildPluginVersion }} enableMicrobuildForMacAndLinux: ${{ parameters.enableMicrobuildForMacAndLinux }} continueOnError: ${{ parameters.continueOnError }} # Publish test results - ${{ if or(and(eq(parameters.enablePublishTestResults, 'true'), eq(parameters.testResultsFormat, '')), eq(parameters.testResultsFormat, 'xunit')) }}: - task: PublishTestResults@2 displayName: Publish XUnit Test Results inputs: testResultsFormat: 'xUnit' testResultsFiles: '*.xml' searchFolder: '$(System.DefaultWorkingDirectory)/artifacts/TestResults/$(_BuildConfig)' testRunTitle: ${{ coalesce(parameters.testRunTitle, parameters.name, '$(System.JobName)') }}-xunit mergeTestResults: ${{ parameters.mergeTestResults }} continueOnError: true condition: always() - ${{ if or(and(eq(parameters.enablePublishTestResults, 'true'), eq(parameters.testResultsFormat, '')), eq(parameters.testResultsFormat, 'vstest')) }}: - task: PublishTestResults@2 displayName: Publish TRX Test Results inputs: testResultsFormat: 'VSTest' testResultsFiles: '*.trx' searchFolder: '$(System.DefaultWorkingDirectory)/artifacts/TestResults/$(_BuildConfig)' testRunTitle: ${{ coalesce(parameters.testRunTitle, parameters.name, '$(System.JobName)') }}-trx mergeTestResults: ${{ parameters.mergeTestResults }} continueOnError: true condition: always() # gather artifacts - ${{ if ne(parameters.artifacts.publish, '') }}: - ${{ if and(ne(parameters.artifacts.publish.artifacts, 'false'), ne(parameters.artifacts.publish.artifacts, '')) }}: - task: CopyFiles@2 displayName: Gather binaries for publish to artifacts inputs: SourceFolder: 'artifacts/bin' Contents: '**' TargetFolder: '$(Build.ArtifactStagingDirectory)/artifacts/bin' - task: CopyFiles@2 displayName: Gather packages for publish to artifacts inputs: SourceFolder: 'artifacts/packages' Contents: '**' TargetFolder: '$(Build.ArtifactStagingDirectory)/artifacts/packages' - ${{ if and(ne(parameters.artifacts.publish.logs, 'false'), ne(parameters.artifacts.publish.logs, '')) }}: - task: CopyFiles@2 displayName: Gather logs for publish to artifacts inputs: SourceFolder: 'artifacts/log' Contents: '**' TargetFolder: '$(Build.ArtifactStagingDirectory)/artifacts/log' continueOnError: true condition: always() - ${{ if eq(parameters.enablePublishBuildArtifacts, 'true') }}: - task: CopyFiles@2 displayName: Gather logs for publish to artifacts inputs: SourceFolder: 'artifacts/log/$(_BuildConfig)' Contents: '**' TargetFolder: '$(Build.ArtifactStagingDirectory)/artifacts/log/$(_BuildConfig)' continueOnError: true condition: always() - ${{ if eq(parameters.enableBuildRetry, 'true') }}: - task: CopyFiles@2 displayName: Gather buildconfiguration for build retry inputs: SourceFolder: '$(System.DefaultWorkingDirectory)/eng/common/BuildConfiguration' Contents: '**' TargetFolder: '$(Build.ArtifactStagingDirectory)/eng/common/BuildConfiguration' continueOnError: true condition: always() - ${{ each step in parameters.artifactPublishSteps }}: - ${{ step }} ================================================ FILE: eng/common/core-templates/job/onelocbuild.yml ================================================ parameters: # Optional: dependencies of the job dependsOn: '' # Optional: A defined YAML pool - https://docs.microsoft.com/en-us/azure/devops/pipelines/yaml-schema?view=vsts&tabs=schema#pool pool: '' CeapexPat: $(dn-bot-ceapex-package-r) # PAT for the loc AzDO instance https://dev.azure.com/ceapex GithubPat: $(BotAccount-dotnet-bot-repo-PAT) SourcesDirectory: $(System.DefaultWorkingDirectory) CreatePr: true AutoCompletePr: false ReusePr: true UseLfLineEndings: true UseCheckedInLocProjectJson: false SkipLocProjectJsonGeneration: false LanguageSet: VS_Main_Languages LclSource: lclFilesInRepo LclPackageId: '' RepoType: gitHub GitHubOrg: dotnet MirrorRepo: '' MirrorBranch: main condition: '' JobNameSuffix: '' is1ESPipeline: '' jobs: - job: OneLocBuild${{ parameters.JobNameSuffix }} dependsOn: ${{ parameters.dependsOn }} displayName: OneLocBuild${{ parameters.JobNameSuffix }} variables: - group: OneLocBuildVariables # Contains the CeapexPat and GithubPat - name: _GenerateLocProjectArguments value: -SourcesDirectory ${{ parameters.SourcesDirectory }} -LanguageSet "${{ parameters.LanguageSet }}" -CreateNeutralXlfs - ${{ if eq(parameters.UseCheckedInLocProjectJson, 'true') }}: - name: _GenerateLocProjectArguments value: ${{ variables._GenerateLocProjectArguments }} -UseCheckedInLocProjectJson - template: /eng/common/core-templates/variables/pool-providers.yml parameters: is1ESPipeline: ${{ parameters.is1ESPipeline }} ${{ if ne(parameters.pool, '') }}: pool: ${{ parameters.pool }} ${{ if eq(parameters.pool, '') }}: pool: # We don't use the collection uri here because it might vary (.visualstudio.com vs. dev.azure.com) ${{ if eq(variables['System.TeamProject'], 'DevDiv') }}: name: AzurePipelines-EO image: 1ESPT-Windows2022 demands: Cmd os: windows # If it's not devdiv, it's dnceng ${{ if ne(variables['System.TeamProject'], 'DevDiv') }}: name: $(DncEngInternalBuildPool) image: 1es-windows-2022 os: windows steps: - ${{ if eq(parameters.is1ESPipeline, '') }}: - 'Illegal entry point, is1ESPipeline is not defined. Repository yaml should not directly reference templates in core-templates folder.': error - ${{ if ne(parameters.SkipLocProjectJsonGeneration, 'true') }}: - task: Powershell@2 inputs: filePath: $(System.DefaultWorkingDirectory)/eng/common/generate-locproject.ps1 arguments: $(_GenerateLocProjectArguments) displayName: Generate LocProject.json condition: ${{ parameters.condition }} - task: OneLocBuild@2 displayName: OneLocBuild env: SYSTEM_ACCESSTOKEN: $(System.AccessToken) inputs: locProj: eng/Localize/LocProject.json outDir: $(Build.ArtifactStagingDirectory) lclSource: ${{ parameters.LclSource }} lclPackageId: ${{ parameters.LclPackageId }} isCreatePrSelected: ${{ parameters.CreatePr }} isAutoCompletePrSelected: ${{ parameters.AutoCompletePr }} ${{ if eq(parameters.CreatePr, true) }}: isUseLfLineEndingsSelected: ${{ parameters.UseLfLineEndings }} isShouldReusePrSelected: ${{ parameters.ReusePr }} packageSourceAuth: patAuth patVariable: ${{ parameters.CeapexPat }} ${{ if eq(parameters.RepoType, 'gitHub') }}: repoType: ${{ parameters.RepoType }} gitHubPatVariable: "${{ parameters.GithubPat }}" ${{ if ne(parameters.MirrorRepo, '') }}: isMirrorRepoSelected: true gitHubOrganization: ${{ parameters.GitHubOrg }} mirrorRepo: ${{ parameters.MirrorRepo }} mirrorBranch: ${{ parameters.MirrorBranch }} condition: ${{ parameters.condition }} # Copy the locProject.json to the root of the Loc directory, then publish a pipeline artifact - task: CopyFiles@2 displayName: Copy LocProject.json inputs: SourceFolder: '$(System.DefaultWorkingDirectory)/eng/Localize/' Contents: 'LocProject.json' TargetFolder: '$(Build.ArtifactStagingDirectory)/loc' condition: ${{ parameters.condition }} - template: /eng/common/core-templates/steps/publish-pipeline-artifacts.yml parameters: is1ESPipeline: ${{ parameters.is1ESPipeline }} args: targetPath: '$(Build.ArtifactStagingDirectory)/loc' artifactName: 'Loc' displayName: 'Publish Localization Files' condition: ${{ parameters.condition }} ================================================ FILE: eng/common/core-templates/job/publish-build-assets.yml ================================================ parameters: configuration: 'Debug' # Optional: condition for the job to run condition: '' # Optional: 'true' if future jobs should run even if this job fails continueOnError: false # Optional: dependencies of the job dependsOn: '' # Optional: Include PublishBuildArtifacts task enablePublishBuildArtifacts: false # Optional: A defined YAML pool - https://docs.microsoft.com/en-us/azure/devops/pipelines/yaml-schema?view=vsts&tabs=schema#pool pool: {} # Optional: should run as a public build even in the internal project # if 'true', the build won't run any of the internal only steps, even if it is running in non-public projects. runAsPublic: false # Optional: whether the build's artifacts will be published using release pipelines or direct feed publishing publishAssetsImmediately: false artifactsPublishingAdditionalParameters: '' signingValidationAdditionalParameters: '' is1ESPipeline: '' # Optional: 🌤️ or not the build has assets it wants to publish to BAR isAssetlessBuild: false # Optional, publishing version publishingVersion: 3 # Optional: A minimatch pattern for the asset manifests to publish to BAR assetManifestsPattern: '*/manifests/**/*.xml' repositoryAlias: self officialBuildId: '' jobs: - job: Asset_Registry_Publish dependsOn: ${{ parameters.dependsOn }} timeoutInMinutes: 150 ${{ if eq(parameters.publishAssetsImmediately, 'true') }}: displayName: Publish Assets ${{ else }}: displayName: Publish to Build Asset Registry variables: - template: /eng/common/core-templates/variables/pool-providers.yml parameters: is1ESPipeline: ${{ parameters.is1ESPipeline }} - ${{ if and(eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: - group: Publish-Build-Assets - group: AzureDevOps-Artifact-Feeds-Pats - name: runCodesignValidationInjection value: false # unconditional - needed for logs publishing (redactor tool version) - template: /eng/common/core-templates/post-build/common-variables.yml - name: OfficialBuildId ${{ if ne(parameters.officialBuildId, '') }}: value: ${{ parameters.officialBuildId }} ${{ else }}: value: $(Build.BuildNumber) pool: # We don't use the collection uri here because it might vary (.visualstudio.com vs. dev.azure.com) ${{ if eq(variables['System.TeamProject'], 'DevDiv') }}: name: AzurePipelines-EO image: 1ESPT-Windows2022 demands: Cmd os: windows # If it's not devdiv, it's dnceng ${{ if ne(variables['System.TeamProject'], 'DevDiv') }}: name: NetCore1ESPool-Publishing-Internal image: windows.vs2022.amd64 os: windows steps: - ${{ if eq(parameters.is1ESPipeline, '') }}: - 'Illegal entry point, is1ESPipeline is not defined. Repository yaml should not directly reference templates in core-templates folder.': error - ${{ if and(eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: - checkout: ${{ parameters.repositoryAlias }} fetchDepth: 3 clean: true - ${{ if eq(parameters.isAssetlessBuild, 'false') }}: - ${{ if eq(parameters.publishingVersion, 3) }}: - task: DownloadPipelineArtifact@2 displayName: Download Asset Manifests inputs: artifactName: AssetManifests targetPath: '$(Build.StagingDirectory)/AssetManifests' condition: ${{ parameters.condition }} continueOnError: ${{ parameters.continueOnError }} - ${{ if eq(parameters.publishingVersion, 4) }}: - task: DownloadPipelineArtifact@2 displayName: Download V4 asset manifests inputs: itemPattern: '*/manifests/**/*.xml' targetPath: '$(Build.StagingDirectory)/AllAssetManifests' condition: ${{ parameters.condition }} continueOnError: ${{ parameters.continueOnError }} - task: CopyFiles@2 displayName: Copy V4 asset manifests to AssetManifests inputs: SourceFolder: '$(Build.StagingDirectory)/AllAssetManifests' Contents: ${{ parameters.assetManifestsPattern }} TargetFolder: '$(Build.StagingDirectory)/AssetManifests' flattenFolders: true condition: ${{ parameters.condition }} continueOnError: ${{ parameters.continueOnError }} - task: NuGetAuthenticate@1 # Populate internal runtime variables. - template: /eng/common/templates/steps/enable-internal-sources.yml ${{ if eq(variables['System.TeamProject'], 'DevDiv') }}: parameters: legacyCredential: $(dn-bot-dnceng-artifact-feeds-rw) - template: /eng/common/templates/steps/enable-internal-runtimes.yml - task: AzureCLI@2 displayName: Publish Build Assets inputs: azureSubscription: "Darc: Maestro Production" scriptType: ps scriptLocation: scriptPath scriptPath: $(System.DefaultWorkingDirectory)/eng/common/sdk-task.ps1 arguments: -task PublishBuildAssets -restore -msbuildEngine dotnet /p:ManifestsPath='$(Build.StagingDirectory)/AssetManifests' /p:IsAssetlessBuild=${{ parameters.isAssetlessBuild }} /p:MaestroApiEndpoint=https://maestro.dot.net /p:OfficialBuildId=$(OfficialBuildId) -runtimeSourceFeed https://ci.dot.net/internal -runtimeSourceFeedKey '$(dotnetbuilds-internal-container-read-token-base64)' condition: ${{ parameters.condition }} continueOnError: ${{ parameters.continueOnError }} - task: powershell@2 displayName: Create ReleaseConfigs Artifact inputs: targetType: inline script: | New-Item -Path "$(Build.StagingDirectory)/ReleaseConfigs" -ItemType Directory -Force $filePath = "$(Build.StagingDirectory)/ReleaseConfigs/ReleaseConfigs.txt" Add-Content -Path $filePath -Value $(BARBuildId) Add-Content -Path $filePath -Value "$(DefaultChannels)" Add-Content -Path $filePath -Value $(IsStableBuild) $symbolExclusionfile = "$(System.DefaultWorkingDirectory)/eng/SymbolPublishingExclusionsFile.txt" if (Test-Path -Path $symbolExclusionfile) { Write-Host "SymbolExclusionFile exists" Copy-Item -Path $symbolExclusionfile -Destination "$(Build.StagingDirectory)/ReleaseConfigs" } - ${{ if eq(parameters.publishingVersion, 4) }}: - template: /eng/common/core-templates/steps/publish-pipeline-artifacts.yml parameters: is1ESPipeline: ${{ parameters.is1ESPipeline }} args: targetPath: '$(Build.ArtifactStagingDirectory)/MergedManifest.xml' artifactName: AssetManifests displayName: 'Publish Merged Manifest' retryCountOnTaskFailure: 10 # for any logs being locked sbomEnabled: false # we don't need SBOM for logs - template: /eng/common/core-templates/steps/publish-build-artifacts.yml parameters: is1ESPipeline: ${{ parameters.is1ESPipeline }} args: displayName: Publish ReleaseConfigs Artifact pathToPublish: '$(Build.StagingDirectory)/ReleaseConfigs' publishLocation: Container artifactName: ReleaseConfigs - ${{ if or(eq(parameters.publishAssetsImmediately, 'true'), eq(parameters.isAssetlessBuild, 'true')) }}: - template: /eng/common/core-templates/post-build/setup-maestro-vars.yml parameters: BARBuildId: ${{ parameters.BARBuildId }} PromoteToChannelIds: ${{ parameters.PromoteToChannelIds }} is1ESPipeline: ${{ parameters.is1ESPipeline }} # Darc is targeting 8.0, so make sure it's installed - task: UseDotNet@2 inputs: version: 8.0.x - task: AzureCLI@2 displayName: Publish Using Darc inputs: azureSubscription: "Darc: Maestro Production" scriptType: ps scriptLocation: scriptPath scriptPath: $(System.DefaultWorkingDirectory)/eng/common/post-build/publish-using-darc.ps1 arguments: > -BuildId $(BARBuildId) -PublishingInfraVersion 3 -AzdoToken '$(System.AccessToken)' -WaitPublishingFinish true -ArtifactsPublishingAdditionalParameters '${{ parameters.artifactsPublishingAdditionalParameters }}' -SymbolPublishingAdditionalParameters '${{ parameters.symbolPublishingAdditionalParameters }}' -SkipAssetsPublishing '${{ parameters.isAssetlessBuild }}' -runtimeSourceFeed https://ci.dot.net/internal -runtimeSourceFeedKey '$(dotnetbuilds-internal-container-read-token-base64)' - ${{ if eq(parameters.enablePublishBuildArtifacts, 'true') }}: - template: /eng/common/core-templates/steps/publish-logs.yml parameters: is1ESPipeline: ${{ parameters.is1ESPipeline }} JobLabel: 'Publish_Artifacts_Logs' ================================================ FILE: eng/common/core-templates/job/source-build.yml ================================================ parameters: # This template adds arcade-powered source-build to CI. The template produces a server job with a # default ID 'Source_Build_Complete' to put in a dependency list if necessary. # Specifies the prefix for source-build jobs added to pipeline. Use this if disambiguation needed. jobNamePrefix: 'Source_Build' # Defines the platform on which to run the job. By default, a linux-x64 machine, suitable for # managed-only repositories. This is an object with these properties: # # name: '' # The name of the job. This is included in the job ID. # targetRID: '' # The name of the target RID to use, instead of the one auto-detected by Arcade. # portableBuild: false # Enables non-portable mode. This means a more specific RID (e.g. fedora.32-x64 rather than # linux-x64), and compiling against distro-provided packages rather than portable ones. The # default is portable mode. # skipPublishValidation: false # Disables publishing validation. By default, a check is performed to ensure no packages are # published by source-build. # container: '' # A container to use. Runs in docker. # pool: {} # A pool to use. Runs directly on an agent. # buildScript: '' # Specifies the build script to invoke to perform the build in the repo. The default # './build.sh' should work for typical Arcade repositories, but this is customizable for # difficult situations. # buildArguments: '' # Specifies additional build arguments to pass to the build script. # jobProperties: {} # A list of job properties to inject at the top level, for potential extensibility beyond # container and pool. platform: {} is1ESPipeline: '' # If set to true and running on a non-public project, # Internal nuget and blob storage locations will be enabled. # This is not enabled by default because many repositories do not need internal sources # and do not need to have the required service connections approved in the pipeline. enableInternalSources: false jobs: - job: ${{ parameters.jobNamePrefix }}_${{ parameters.platform.name }} displayName: Source-Build (${{ parameters.platform.name }}) ${{ each property in parameters.platform.jobProperties }}: ${{ property.key }}: ${{ property.value }} ${{ if ne(parameters.platform.container, '') }}: container: ${{ parameters.platform.container }} ${{ if eq(parameters.platform.pool, '') }}: # The default VM host AzDO pool. This should be capable of running Docker containers: almost all # source-build builds run in Docker, including the default managed platform. # /eng/common/core-templates/variables/pool-providers.yml can't be used here (some customers declare variables already), so duplicate its logic ${{ if eq(parameters.is1ESPipeline, 'true') }}: pool: ${{ if eq(variables['System.TeamProject'], 'public') }}: name: $[replace(replace(eq(contains(coalesce(variables['System.PullRequest.TargetBranch'], variables['Build.SourceBranch'], 'refs/heads/main'), 'release'), 'true'), True, 'NetCore-Svc-Public' ), False, 'NetCore-Public')] demands: ImageOverride -equals build.azurelinux.3.amd64.open ${{ if eq(variables['System.TeamProject'], 'internal') }}: name: $[replace(replace(eq(contains(coalesce(variables['System.PullRequest.TargetBranch'], variables['Build.SourceBranch'], 'refs/heads/main'), 'release'), 'true'), True, 'NetCore1ESPool-Svc-Internal'), False, 'NetCore1ESPool-Internal')] image: build.azurelinux.3.amd64 os: linux ${{ else }}: pool: ${{ if eq(variables['System.TeamProject'], 'public') }}: name: $[replace(replace(eq(contains(coalesce(variables['System.PullRequest.TargetBranch'], variables['Build.SourceBranch'], 'refs/heads/main'), 'release'), 'true'), True, 'NetCore-Svc-Public' ), False, 'NetCore-Public')] demands: ImageOverride -equals build.azurelinux.3.amd64.open ${{ if eq(variables['System.TeamProject'], 'internal') }}: name: $[replace(replace(eq(contains(coalesce(variables['System.PullRequest.TargetBranch'], variables['Build.SourceBranch'], 'refs/heads/main'), 'release'), 'true'), True, 'NetCore1ESPool-Svc-Internal'), False, 'NetCore1ESPool-Internal')] demands: ImageOverride -equals build.azurelinux.3.amd64 ${{ if ne(parameters.platform.pool, '') }}: pool: ${{ parameters.platform.pool }} workspace: clean: all steps: - ${{ if eq(parameters.is1ESPipeline, '') }}: - 'Illegal entry point, is1ESPipeline is not defined. Repository yaml should not directly reference templates in core-templates folder.': error - ${{ if eq(parameters.enableInternalSources, true) }}: - template: /eng/common/core-templates/steps/enable-internal-sources.yml parameters: is1ESPipeline: ${{ parameters.is1ESPipeline }} - template: /eng/common/core-templates/steps/enable-internal-runtimes.yml parameters: is1ESPipeline: ${{ parameters.is1ESPipeline }} - template: /eng/common/core-templates/steps/source-build.yml parameters: is1ESPipeline: ${{ parameters.is1ESPipeline }} platform: ${{ parameters.platform }} ================================================ FILE: eng/common/core-templates/job/source-index-stage1.yml ================================================ parameters: runAsPublic: false sourceIndexBuildCommand: powershell -NoLogo -NoProfile -ExecutionPolicy Bypass -Command "eng/common/build.ps1 -restore -build -binarylog -ci" preSteps: [] binlogPath: artifacts/log/Debug/Build.binlog condition: eq(variables['Build.SourceBranch'], 'refs/heads/main') dependsOn: '' pool: '' is1ESPipeline: '' jobs: - job: SourceIndexStage1 dependsOn: ${{ parameters.dependsOn }} condition: ${{ parameters.condition }} variables: - name: BinlogPath value: ${{ parameters.binlogPath }} - template: /eng/common/core-templates/variables/pool-providers.yml parameters: is1ESPipeline: ${{ parameters.is1ESPipeline }} ${{ if ne(parameters.pool, '') }}: pool: ${{ parameters.pool }} ${{ if eq(parameters.pool, '') }}: pool: ${{ if eq(variables['System.TeamProject'], 'public') }}: name: $(DncEngPublicBuildPool) image: windows.vs2026preview.scout.amd64.open ${{ if eq(variables['System.TeamProject'], 'internal') }}: name: $(DncEngInternalBuildPool) image: windows.vs2026preview.scout.amd64 steps: - ${{ if eq(parameters.is1ESPipeline, '') }}: - 'Illegal entry point, is1ESPipeline is not defined. Repository yaml should not directly reference templates in core-templates folder.': error - ${{ each preStep in parameters.preSteps }}: - ${{ preStep }} - script: ${{ parameters.sourceIndexBuildCommand }} displayName: Build Repository - template: /eng/common/core-templates/steps/source-index-stage1-publish.yml parameters: binLogPath: ${{ parameters.binLogPath }} ================================================ FILE: eng/common/core-templates/jobs/codeql-build.yml ================================================ parameters: # See schema documentation in /Documentation/AzureDevOps/TemplateSchema.md continueOnError: false # Required: A collection of jobs to run - https://docs.microsoft.com/en-us/azure/devops/pipelines/yaml-schema?view=vsts&tabs=schema#job jobs: [] # Optional: if specified, restore and use this version of Guardian instead of the default. overrideGuardianVersion: '' is1ESPipeline: '' jobs: - template: /eng/common/core-templates/jobs/jobs.yml parameters: is1ESPipeline: ${{ parameters.is1ESPipeline }} enableMicrobuild: false enablePublishBuildArtifacts: false enablePublishTestResults: false enablePublishBuildAssets: false enableTelemetry: true variables: - group: Publish-Build-Assets # The Guardian version specified in 'eng/common/sdl/packages.config'. This value must be kept in # sync with the packages.config file. - name: DefaultGuardianVersion value: 0.109.0 - name: GuardianPackagesConfigFile value: $(System.DefaultWorkingDirectory)\eng\common\sdl\packages.config - name: GuardianVersion value: ${{ coalesce(parameters.overrideGuardianVersion, '$(DefaultGuardianVersion)') }} jobs: ${{ parameters.jobs }} ================================================ FILE: eng/common/core-templates/jobs/jobs.yml ================================================ parameters: # See schema documentation in /Documentation/AzureDevOps/TemplateSchema.md continueOnError: false # Optional: Include PublishBuildArtifacts task enablePublishBuildArtifacts: false # Optional: Enable running the source-build jobs to build repo from source enableSourceBuild: false # Optional: Parameters for source-build template. # See /eng/common/core-templates/jobs/source-build.yml for options sourceBuildParameters: [] graphFileGeneration: # Optional: Enable generating the graph files at the end of the build enabled: false # Optional: Include toolset dependencies in the generated graph files includeToolset: false # Required: A collection of jobs to run - https://docs.microsoft.com/en-us/azure/devops/pipelines/yaml-schema?view=vsts&tabs=schema#job jobs: [] # Optional: Override automatically derived dependsOn value for "publish build assets" job publishBuildAssetsDependsOn: '' # Optional: Publish the assets as soon as the publish to BAR stage is complete, rather doing so in a separate stage. publishAssetsImmediately: false # Optional: 🌤️ or not the build has assets it wants to publish to BAR isAssetlessBuild: false # Optional: If using publishAssetsImmediately and additional parameters are needed, can be used to send along additional parameters (normally sent to post-build.yml) artifactsPublishingAdditionalParameters: '' signingValidationAdditionalParameters: '' # Optional: should run as a public build even in the internal project # if 'true', the build won't run any of the internal only steps, even if it is running in non-public projects. runAsPublic: false enableSourceIndex: false sourceIndexParams: {} artifacts: {} is1ESPipeline: '' repositoryAlias: self officialBuildId: '' # Internal resources (telemetry, microbuild) can only be accessed from non-public projects, # and some (Microbuild) should only be applied to non-PR cases for internal builds. jobs: - ${{ each job in parameters.jobs }}: - ${{ if eq(parameters.is1ESPipeline, 'true') }}: - template: /eng/common/templates-official/job/job.yml parameters: # pass along parameters ${{ each parameter in parameters }}: ${{ if ne(parameter.key, 'jobs') }}: ${{ parameter.key }}: ${{ parameter.value }} # pass along job properties ${{ each property in job }}: ${{ if ne(property.key, 'job') }}: ${{ property.key }}: ${{ property.value }} name: ${{ job.job }} - ${{ else }}: - template: /eng/common/templates/job/job.yml parameters: # pass along parameters ${{ each parameter in parameters }}: ${{ if ne(parameter.key, 'jobs') }}: ${{ parameter.key }}: ${{ parameter.value }} # pass along job properties ${{ each property in job }}: ${{ if ne(property.key, 'job') }}: ${{ property.key }}: ${{ property.value }} name: ${{ job.job }} - ${{ if eq(parameters.enableSourceBuild, true) }}: - template: /eng/common/core-templates/jobs/source-build.yml parameters: is1ESPipeline: ${{ parameters.is1ESPipeline }} ${{ each parameter in parameters.sourceBuildParameters }}: ${{ parameter.key }}: ${{ parameter.value }} - ${{ if eq(parameters.enableSourceIndex, 'true') }}: - template: ../job/source-index-stage1.yml parameters: is1ESPipeline: ${{ parameters.is1ESPipeline }} runAsPublic: ${{ parameters.runAsPublic }} ${{ each parameter in parameters.sourceIndexParams }}: ${{ parameter.key }}: ${{ parameter.value }} - ${{ if and(eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: - ${{ if or(eq(parameters.enablePublishBuildAssets, true), eq(parameters.artifacts.publish.manifests, 'true'), ne(parameters.artifacts.publish.manifests, ''), eq(parameters.isAssetlessBuild, true)) }}: - template: ../job/publish-build-assets.yml parameters: is1ESPipeline: ${{ parameters.is1ESPipeline }} continueOnError: ${{ parameters.continueOnError }} dependsOn: - ${{ if ne(parameters.publishBuildAssetsDependsOn, '') }}: - ${{ each job in parameters.publishBuildAssetsDependsOn }}: - ${{ job.job }} - ${{ if eq(parameters.publishBuildAssetsDependsOn, '') }}: - ${{ each job in parameters.jobs }}: - ${{ job.job }} runAsPublic: ${{ parameters.runAsPublic }} publishAssetsImmediately: ${{ or(parameters.publishAssetsImmediately, parameters.isAssetlessBuild) }} isAssetlessBuild: ${{ parameters.isAssetlessBuild }} enablePublishBuildArtifacts: ${{ parameters.enablePublishBuildArtifacts }} artifactsPublishingAdditionalParameters: ${{ parameters.artifactsPublishingAdditionalParameters }} signingValidationAdditionalParameters: ${{ parameters.signingValidationAdditionalParameters }} repositoryAlias: ${{ parameters.repositoryAlias }} officialBuildId: ${{ parameters.officialBuildId }} ================================================ FILE: eng/common/core-templates/jobs/source-build.yml ================================================ parameters: # This template adds arcade-powered source-build to CI. A job is created for each platform, as # well as an optional server job that completes when all platform jobs complete. # See /eng/common/core-templates/job/source-build.yml jobNamePrefix: 'Source_Build' # This is the default platform provided by Arcade, intended for use by a managed-only repo. defaultManagedPlatform: name: 'Managed' container: 'mcr.microsoft.com/dotnet-buildtools/prereqs:centos-stream-10-amd64' # Defines the platforms on which to run build jobs. One job is created for each platform, and the # object in this array is sent to the job template as 'platform'. If no platforms are specified, # one job runs on 'defaultManagedPlatform'. platforms: [] is1ESPipeline: '' # If set to true and running on a non-public project, # Internal nuget and blob storage locations will be enabled. # This is not enabled by default because many repositories do not need internal sources # and do not need to have the required service connections approved in the pipeline. enableInternalSources: false jobs: - ${{ each platform in parameters.platforms }}: - template: /eng/common/core-templates/job/source-build.yml parameters: is1ESPipeline: ${{ parameters.is1ESPipeline }} jobNamePrefix: ${{ parameters.jobNamePrefix }} platform: ${{ platform }} enableInternalSources: ${{ parameters.enableInternalSources }} - ${{ if eq(length(parameters.platforms), 0) }}: - template: /eng/common/core-templates/job/source-build.yml parameters: is1ESPipeline: ${{ parameters.is1ESPipeline }} jobNamePrefix: ${{ parameters.jobNamePrefix }} platform: ${{ parameters.defaultManagedPlatform }} enableInternalSources: ${{ parameters.enableInternalSources }} ================================================ FILE: eng/common/core-templates/post-build/common-variables.yml ================================================ variables: - group: Publish-Build-Assets # Whether the build is internal or not - name: IsInternalBuild value: ${{ and(ne(variables['System.TeamProject'], 'public'), contains(variables['Build.SourceBranch'], 'internal')) }} # Default Maestro++ API Endpoint and API Version - name: MaestroApiEndPoint value: "https://maestro.dot.net" - name: MaestroApiVersion value: "2020-02-20" - name: SourceLinkCLIVersion value: 3.0.0 - name: SymbolToolVersion value: 1.0.1 - name: BinlogToolVersion value: 1.0.11 - name: runCodesignValidationInjection value: false ================================================ FILE: eng/common/core-templates/post-build/post-build.yml ================================================ parameters: # Which publishing infra should be used. THIS SHOULD MATCH THE VERSION ON THE BUILD MANIFEST. # Publishing V1 is no longer supported # Publishing V2 is no longer supported # Publishing V3 is the default - name: publishingInfraVersion displayName: Which version of publishing should be used to promote the build definition? type: number default: 3 values: - 3 - name: BARBuildId displayName: BAR Build Id type: number default: 0 - name: PromoteToChannelIds displayName: Channel to promote BARBuildId to type: string default: '' - name: enableSourceLinkValidation displayName: Enable SourceLink validation type: boolean default: false - name: enableSigningValidation displayName: Enable signing validation type: boolean default: true - name: enableSymbolValidation displayName: Enable symbol validation type: boolean default: false - name: enableNugetValidation displayName: Enable NuGet validation type: boolean default: true - name: publishInstallersAndChecksums displayName: Publish installers and checksums type: boolean default: true - name: requireDefaultChannels displayName: Fail the build if there are no default channel(s) registrations for the current build type: boolean default: false - name: SDLValidationParameters type: object default: enable: false publishGdn: false continueOnError: false params: '' artifactNames: '' downloadArtifacts: true - name: isAssetlessBuild type: boolean displayName: Is Assetless Build default: false # These parameters let the user customize the call to sdk-task.ps1 for publishing # symbols & general artifacts as well as for signing validation - name: symbolPublishingAdditionalParameters displayName: Symbol publishing additional parameters type: string default: '' - name: artifactsPublishingAdditionalParameters displayName: Artifact publishing additional parameters type: string default: '' - name: signingValidationAdditionalParameters displayName: Signing validation additional parameters type: string default: '' # Which stages should finish execution before post-build stages start - name: validateDependsOn type: object default: - build - name: publishDependsOn type: object default: - Validate # Optional: Call asset publishing rather than running in a separate stage - name: publishAssetsImmediately type: boolean default: false - name: is1ESPipeline type: boolean default: false stages: - ${{ if or(eq( parameters.enableNugetValidation, 'true'), eq(parameters.enableSigningValidation, 'true'), eq(parameters.enableSourceLinkValidation, 'true'), eq(parameters.SDLValidationParameters.enable, 'true')) }}: - stage: Validate dependsOn: ${{ parameters.validateDependsOn }} displayName: Validate Build Assets variables: - template: /eng/common/core-templates/post-build/common-variables.yml - template: /eng/common/core-templates/variables/pool-providers.yml parameters: is1ESPipeline: ${{ parameters.is1ESPipeline }} jobs: - job: displayName: NuGet Validation condition: and(succeededOrFailed(), eq( ${{ parameters.enableNugetValidation }}, 'true')) pool: # We don't use the collection uri here because it might vary (.visualstudio.com vs. dev.azure.com) ${{ if eq(variables['System.TeamProject'], 'DevDiv') }}: name: AzurePipelines-EO image: 1ESPT-Windows2022 demands: Cmd os: windows # If it's not devdiv, it's dnceng ${{ else }}: ${{ if eq(parameters.is1ESPipeline, true) }}: name: $(DncEngInternalBuildPool) image: windows.vs2026preview.scout.amd64 os: windows ${{ else }}: name: $(DncEngInternalBuildPool) demands: ImageOverride -equals windows.vs2026preview.scout.amd64 steps: - template: /eng/common/core-templates/post-build/setup-maestro-vars.yml parameters: BARBuildId: ${{ parameters.BARBuildId }} PromoteToChannelIds: ${{ parameters.PromoteToChannelIds }} is1ESPipeline: ${{ parameters.is1ESPipeline }} - task: DownloadBuildArtifacts@0 displayName: Download Package Artifacts inputs: buildType: specific buildVersionToDownload: specific project: $(AzDOProjectName) pipeline: $(AzDOPipelineId) buildId: $(AzDOBuildId) artifactName: PackageArtifacts checkDownloadedFiles: true - task: PowerShell@2 displayName: Validate inputs: filePath: $(System.DefaultWorkingDirectory)/eng/common/post-build/nuget-validation.ps1 arguments: -PackagesPath $(Build.ArtifactStagingDirectory)/PackageArtifacts/ - job: displayName: Signing Validation condition: and( eq( ${{ parameters.enableSigningValidation }}, 'true'), ne( variables['PostBuildSign'], 'true')) pool: # We don't use the collection uri here because it might vary (.visualstudio.com vs. dev.azure.com) ${{ if eq(variables['System.TeamProject'], 'DevDiv') }}: name: AzurePipelines-EO image: 1ESPT-Windows2022 demands: Cmd os: windows # If it's not devdiv, it's dnceng ${{ else }}: ${{ if eq(parameters.is1ESPipeline, true) }}: name: $(DncEngInternalBuildPool) image: 1es-windows-2022 os: windows ${{ else }}: name: $(DncEngInternalBuildPool) demands: ImageOverride -equals windows.vs2026preview.scout.amd64 steps: - template: /eng/common/core-templates/post-build/setup-maestro-vars.yml parameters: BARBuildId: ${{ parameters.BARBuildId }} PromoteToChannelIds: ${{ parameters.PromoteToChannelIds }} is1ESPipeline: ${{ parameters.is1ESPipeline }} - task: DownloadBuildArtifacts@0 displayName: Download Package Artifacts inputs: buildType: specific buildVersionToDownload: specific project: $(AzDOProjectName) pipeline: $(AzDOPipelineId) buildId: $(AzDOBuildId) artifactName: PackageArtifacts checkDownloadedFiles: true # This is necessary whenever we want to publish/restore to an AzDO private feed # Since sdk-task.ps1 tries to restore packages we need to do this authentication here # otherwise it'll complain about accessing a private feed. - task: NuGetAuthenticate@1 displayName: 'Authenticate to AzDO Feeds' # Signing validation will optionally work with the buildmanifest file which is downloaded from # Azure DevOps above. - task: PowerShell@2 displayName: Validate inputs: filePath: eng\common\sdk-task.ps1 arguments: -task SigningValidation -restore -msbuildEngine vs /p:PackageBasePath='$(Build.ArtifactStagingDirectory)/PackageArtifacts' /p:SignCheckExclusionsFile='$(System.DefaultWorkingDirectory)/eng/SignCheckExclusionsFile.txt' ${{ parameters.signingValidationAdditionalParameters }} - template: /eng/common/core-templates/steps/publish-logs.yml parameters: is1ESPipeline: ${{ parameters.is1ESPipeline }} StageLabel: 'Validation' JobLabel: 'Signing' BinlogToolVersion: $(BinlogToolVersion) - job: displayName: SourceLink Validation condition: eq( ${{ parameters.enableSourceLinkValidation }}, 'true') pool: # We don't use the collection uri here because it might vary (.visualstudio.com vs. dev.azure.com) ${{ if eq(variables['System.TeamProject'], 'DevDiv') }}: name: AzurePipelines-EO image: 1ESPT-Windows2022 demands: Cmd os: windows # If it's not devdiv, it's dnceng ${{ else }}: ${{ if eq(parameters.is1ESPipeline, true) }}: name: $(DncEngInternalBuildPool) image: 1es-windows-2022 os: windows ${{ else }}: name: $(DncEngInternalBuildPool) demands: ImageOverride -equals windows.vs2026preview.scout.amd64 steps: - template: /eng/common/core-templates/post-build/setup-maestro-vars.yml parameters: BARBuildId: ${{ parameters.BARBuildId }} PromoteToChannelIds: ${{ parameters.PromoteToChannelIds }} is1ESPipeline: ${{ parameters.is1ESPipeline }} - task: DownloadBuildArtifacts@0 displayName: Download Blob Artifacts inputs: buildType: specific buildVersionToDownload: specific project: $(AzDOProjectName) pipeline: $(AzDOPipelineId) buildId: $(AzDOBuildId) artifactName: BlobArtifacts checkDownloadedFiles: true - task: PowerShell@2 displayName: Validate inputs: filePath: $(System.DefaultWorkingDirectory)/eng/common/post-build/sourcelink-validation.ps1 arguments: -InputPath $(Build.ArtifactStagingDirectory)/BlobArtifacts/ -ExtractPath $(Agent.BuildDirectory)/Extract/ -GHRepoName $(Build.Repository.Name) -GHCommit $(Build.SourceVersion) -SourcelinkCliVersion $(SourceLinkCLIVersion) continueOnError: true - ${{ if ne(parameters.publishAssetsImmediately, 'true') }}: - stage: publish_using_darc ${{ if or(eq(parameters.enableNugetValidation, 'true'), eq(parameters.enableSigningValidation, 'true'), eq(parameters.enableSourceLinkValidation, 'true'), eq(parameters.SDLValidationParameters.enable, 'true')) }}: dependsOn: ${{ parameters.publishDependsOn }} ${{ else }}: dependsOn: ${{ parameters.validateDependsOn }} displayName: Publish using Darc variables: - template: /eng/common/core-templates/post-build/common-variables.yml - template: /eng/common/core-templates/variables/pool-providers.yml parameters: is1ESPipeline: ${{ parameters.is1ESPipeline }} jobs: - job: displayName: Publish Using Darc timeoutInMinutes: 120 pool: # We don't use the collection uri here because it might vary (.visualstudio.com vs. dev.azure.com) ${{ if eq(variables['System.TeamProject'], 'DevDiv') }}: name: AzurePipelines-EO image: 1ESPT-Windows2022 demands: Cmd os: windows # If it's not devdiv, it's dnceng ${{ else }}: ${{ if eq(parameters.is1ESPipeline, true) }}: name: NetCore1ESPool-Publishing-Internal image: windows.vs2022.amd64 os: windows ${{ else }}: name: NetCore1ESPool-Publishing-Internal demands: ImageOverride -equals windows.vs2022.amd64 steps: - template: /eng/common/core-templates/post-build/setup-maestro-vars.yml parameters: BARBuildId: ${{ parameters.BARBuildId }} PromoteToChannelIds: ${{ parameters.PromoteToChannelIds }} is1ESPipeline: ${{ parameters.is1ESPipeline }} - task: NuGetAuthenticate@1 # Populate internal runtime variables. - template: /eng/common/templates/steps/enable-internal-sources.yml parameters: legacyCredential: $(dn-bot-dnceng-artifact-feeds-rw) - template: /eng/common/templates/steps/enable-internal-runtimes.yml - task: UseDotNet@2 inputs: version: 8.0.x - task: AzureCLI@2 displayName: Publish Using Darc inputs: azureSubscription: "Darc: Maestro Production" scriptType: ps scriptLocation: scriptPath scriptPath: $(System.DefaultWorkingDirectory)/eng/common/post-build/publish-using-darc.ps1 arguments: > -BuildId $(BARBuildId) -PublishingInfraVersion ${{ parameters.publishingInfraVersion }} -AzdoToken '$(System.AccessToken)' -WaitPublishingFinish true -RequireDefaultChannels ${{ parameters.requireDefaultChannels }} -ArtifactsPublishingAdditionalParameters '${{ parameters.artifactsPublishingAdditionalParameters }}' -SymbolPublishingAdditionalParameters '${{ parameters.symbolPublishingAdditionalParameters }}' -SkipAssetsPublishing '${{ parameters.isAssetlessBuild }}' -runtimeSourceFeed https://ci.dot.net/internal -runtimeSourceFeedKey '$(dotnetbuilds-internal-container-read-token-base64)' ================================================ FILE: eng/common/core-templates/post-build/setup-maestro-vars.yml ================================================ parameters: BARBuildId: '' PromoteToChannelIds: '' is1ESPipeline: '' steps: - ${{ if eq(parameters.is1ESPipeline, '') }}: - 'Illegal entry point, is1ESPipeline is not defined. Repository yaml should not directly reference templates in core-templates folder.': error - ${{ if eq(coalesce(parameters.PromoteToChannelIds, 0), 0) }}: - task: DownloadBuildArtifacts@0 displayName: Download Release Configs inputs: buildType: current artifactName: ReleaseConfigs checkDownloadedFiles: true - task: AzureCLI@2 name: setReleaseVars displayName: Set Release Configs Vars inputs: azureSubscription: "Darc: Maestro Production" scriptType: pscore scriptLocation: inlineScript inlineScript: | try { if (!$Env:PromoteToMaestroChannels -or $Env:PromoteToMaestroChannels.Trim() -eq '') { $Content = Get-Content $(Build.StagingDirectory)/ReleaseConfigs/ReleaseConfigs.txt $BarId = $Content | Select -Index 0 $Channels = $Content | Select -Index 1 $IsStableBuild = $Content | Select -Index 2 $AzureDevOpsProject = $Env:System_TeamProject $AzureDevOpsBuildDefinitionId = $Env:System_DefinitionId $AzureDevOpsBuildId = $Env:Build_BuildId } else { . $(System.DefaultWorkingDirectory)\eng\common\tools.ps1 $darc = Get-Darc $buildInfo = & $darc get-build ` --id ${{ parameters.BARBuildId }} ` --extended ` --output-format json ` --ci ` | convertFrom-Json $BarId = ${{ parameters.BARBuildId }} $Channels = $Env:PromoteToMaestroChannels -split "," $Channels = $Channels -join "][" $Channels = "[$Channels]" $IsStableBuild = $buildInfo.stable $AzureDevOpsProject = $buildInfo.azureDevOpsProject $AzureDevOpsBuildDefinitionId = $buildInfo.azureDevOpsBuildDefinitionId $AzureDevOpsBuildId = $buildInfo.azureDevOpsBuildId } Write-Host "##vso[task.setvariable variable=BARBuildId]$BarId" Write-Host "##vso[task.setvariable variable=TargetChannels]$Channels" Write-Host "##vso[task.setvariable variable=IsStableBuild]$IsStableBuild" Write-Host "##vso[task.setvariable variable=AzDOProjectName]$AzureDevOpsProject" Write-Host "##vso[task.setvariable variable=AzDOPipelineId]$AzureDevOpsBuildDefinitionId" Write-Host "##vso[task.setvariable variable=AzDOBuildId]$AzureDevOpsBuildId" } catch { Write-Host $_ Write-Host $_.Exception Write-Host $_.ScriptStackTrace exit 1 } env: PromoteToMaestroChannels: ${{ parameters.PromoteToChannelIds }} ================================================ FILE: eng/common/core-templates/steps/cleanup-microbuild.yml ================================================ parameters: # Enable cleanup tasks for MicroBuild enableMicrobuild: false # Enable cleanup tasks for MicroBuild on Mac and Linux # Will be ignored if 'enableMicrobuild' is false or 'Agent.Os' is 'Windows_NT' enableMicrobuildForMacAndLinux: false continueOnError: false steps: - ${{ if eq(parameters.enableMicrobuild, 'true') }}: - task: MicroBuildCleanup@1 displayName: Execute Microbuild cleanup tasks condition: and( always(), or( and( eq(variables['Agent.Os'], 'Windows_NT'), in(variables['_SignType'], 'real', 'test') ), and( ${{ eq(parameters.enableMicrobuildForMacAndLinux, true) }}, ne(variables['Agent.Os'], 'Windows_NT'), eq(variables['_SignType'], 'real') ) )) continueOnError: ${{ parameters.continueOnError }} env: TeamName: $(_TeamName) ================================================ FILE: eng/common/core-templates/steps/component-governance.yml ================================================ parameters: disableComponentGovernance: false componentGovernanceIgnoreDirectories: '' is1ESPipeline: false displayName: 'Component Detection' steps: - ${{ if eq(parameters.disableComponentGovernance, 'true') }}: - script: echo "##vso[task.setvariable variable=skipComponentGovernanceDetection]true" displayName: Set skipComponentGovernanceDetection variable - ${{ if ne(parameters.disableComponentGovernance, 'true') }}: - task: ComponentGovernanceComponentDetection@0 continueOnError: true displayName: ${{ parameters.displayName }} inputs: ignoreDirectories: ${{ parameters.componentGovernanceIgnoreDirectories }} ================================================ FILE: eng/common/core-templates/steps/enable-internal-runtimes.yml ================================================ # Obtains internal runtime download credentials and populates the 'dotnetbuilds-internal-container-read-token-base64' # variable with the base64-encoded SAS token, by default parameters: - name: federatedServiceConnection type: string default: 'dotnetbuilds-internal-read' - name: outputVariableName type: string default: 'dotnetbuilds-internal-container-read-token-base64' - name: expiryInHours type: number default: 1 - name: base64Encode type: boolean default: true - name: is1ESPipeline type: boolean default: false steps: - ${{ if ne(variables['System.TeamProject'], 'public') }}: - template: /eng/common/core-templates/steps/get-delegation-sas.yml parameters: federatedServiceConnection: ${{ parameters.federatedServiceConnection }} outputVariableName: ${{ parameters.outputVariableName }} expiryInHours: ${{ parameters.expiryInHours }} base64Encode: ${{ parameters.base64Encode }} storageAccount: dotnetbuilds container: internal permissions: rl is1ESPipeline: ${{ parameters.is1ESPipeline }} ================================================ FILE: eng/common/core-templates/steps/enable-internal-sources.yml ================================================ parameters: # This is the Azure federated service connection that we log into to get an access token. - name: nugetFederatedServiceConnection type: string default: 'dnceng-artifacts-feeds-read' - name: is1ESPipeline type: boolean default: false # Legacy parameters to allow for PAT usage - name: legacyCredential type: string default: '' steps: - ${{ if ne(variables['System.TeamProject'], 'public') }}: - ${{ if ne(parameters.legacyCredential, '') }}: - task: PowerShell@2 displayName: Setup Internal Feeds inputs: filePath: $(System.DefaultWorkingDirectory)/eng/common/SetupNugetSources.ps1 arguments: -ConfigFile $(System.DefaultWorkingDirectory)/NuGet.config -Password $Env:Token env: Token: ${{ parameters.legacyCredential }} # If running on dnceng (internal project), just use the default behavior for NuGetAuthenticate. # If running on DevDiv, NuGetAuthenticate is not really an option. It's scoped to a single feed, and we have many feeds that # may be added. Instead, we'll use the traditional approach (add cred to nuget.config), but use an account token. - ${{ else }}: - ${{ if eq(variables['System.TeamProject'], 'internal') }}: - task: PowerShell@2 displayName: Setup Internal Feeds inputs: filePath: $(System.DefaultWorkingDirectory)/eng/common/SetupNugetSources.ps1 arguments: -ConfigFile $(System.DefaultWorkingDirectory)/NuGet.config - ${{ else }}: - template: /eng/common/templates/steps/get-federated-access-token.yml parameters: federatedServiceConnection: ${{ parameters.nugetFederatedServiceConnection }} outputVariableName: 'dnceng-artifacts-feeds-read-access-token' - task: PowerShell@2 displayName: Setup Internal Feeds inputs: filePath: $(System.DefaultWorkingDirectory)/eng/common/SetupNugetSources.ps1 arguments: -ConfigFile $(System.DefaultWorkingDirectory)/NuGet.config -Password $(dnceng-artifacts-feeds-read-access-token) # This is required in certain scenarios to install the ADO credential provider. # It installed by default in some msbuild invocations (e.g. VS msbuild), but needs to be installed for others # (e.g. dotnet msbuild). - task: NuGetAuthenticate@1 ================================================ FILE: eng/common/core-templates/steps/generate-sbom.yml ================================================ # BuildDropPath - The root folder of the drop directory for which the manifest file will be generated. # PackageName - The name of the package this SBOM represents. # PackageVersion - The version of the package this SBOM represents. # ManifestDirPath - The path of the directory where the generated manifest files will be placed # IgnoreDirectories - Directories to ignore for SBOM generation. This will be passed through to the CG component detector. parameters: PackageVersion: 11.0.0 BuildDropPath: '$(System.DefaultWorkingDirectory)/artifacts' PackageName: '.NET' ManifestDirPath: $(Build.ArtifactStagingDirectory)/sbom IgnoreDirectories: '' sbomContinueOnError: true is1ESPipeline: false # disable publishArtifacts if some other step is publishing the artifacts (like job.yml). publishArtifacts: true steps: - task: PowerShell@2 displayName: Prep for SBOM generation in (Non-linux) condition: or(eq(variables['Agent.Os'], 'Windows_NT'), eq(variables['Agent.Os'], 'Darwin')) inputs: filePath: ./eng/common/generate-sbom-prep.ps1 arguments: ${{parameters.manifestDirPath}} # Chmodding is a workaround for https://github.com/dotnet/arcade/issues/8461 - script: | chmod +x ./eng/common/generate-sbom-prep.sh ./eng/common/generate-sbom-prep.sh ${{parameters.manifestDirPath}} displayName: Prep for SBOM generation in (Linux) condition: eq(variables['Agent.Os'], 'Linux') continueOnError: ${{ parameters.sbomContinueOnError }} - task: AzureArtifacts.manifest-generator-task.manifest-generator-task.ManifestGeneratorTask@0 displayName: 'Generate SBOM manifest' continueOnError: ${{ parameters.sbomContinueOnError }} inputs: PackageName: ${{ parameters.packageName }} BuildDropPath: ${{ parameters.buildDropPath }} PackageVersion: ${{ parameters.packageVersion }} ManifestDirPath: ${{ parameters.manifestDirPath }}/$(ARTIFACT_NAME) ${{ if ne(parameters.IgnoreDirectories, '') }}: AdditionalComponentDetectorArgs: '--IgnoreDirectories ${{ parameters.IgnoreDirectories }}' - ${{ if eq(parameters.publishArtifacts, 'true')}}: - template: /eng/common/core-templates/steps/publish-pipeline-artifacts.yml parameters: is1ESPipeline: ${{ parameters.is1ESPipeline }} args: displayName: Publish SBOM manifest continueOnError: ${{parameters.sbomContinueOnError}} targetPath: '${{ parameters.manifestDirPath }}' artifactName: $(ARTIFACT_NAME) ================================================ FILE: eng/common/core-templates/steps/get-delegation-sas.yml ================================================ parameters: - name: federatedServiceConnection type: string - name: outputVariableName type: string - name: expiryInHours type: number default: 1 - name: base64Encode type: boolean default: false - name: storageAccount type: string - name: container type: string - name: permissions type: string default: 'rl' - name: is1ESPipeline type: boolean default: false steps: - task: AzureCLI@2 displayName: 'Generate delegation SAS Token for ${{ parameters.storageAccount }}/${{ parameters.container }}' inputs: azureSubscription: ${{ parameters.federatedServiceConnection }} scriptType: 'pscore' scriptLocation: 'inlineScript' inlineScript: | # Calculate the expiration of the SAS token and convert to UTC $expiry = (Get-Date).AddHours(${{ parameters.expiryInHours }}).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ") $sas = az storage container generate-sas --account-name ${{ parameters.storageAccount }} --name ${{ parameters.container }} --permissions ${{ parameters.permissions }} --expiry $expiry --auth-mode login --as-user -o tsv if ($LASTEXITCODE -ne 0) { Write-Error "Failed to generate SAS token." exit 1 } if ('${{ parameters.base64Encode }}' -eq 'true') { $sas = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($sas)) } Write-Host "Setting '${{ parameters.outputVariableName }}' with the access token value" Write-Host "##vso[task.setvariable variable=${{ parameters.outputVariableName }};issecret=true]$sas" ================================================ FILE: eng/common/core-templates/steps/get-federated-access-token.yml ================================================ parameters: - name: federatedServiceConnection type: string - name: outputVariableName type: string - name: is1ESPipeline type: boolean - name: stepName type: string default: 'getFederatedAccessToken' - name: condition type: string default: '' # Resource to get a token for. Common values include: # - '499b84ac-1321-427f-aa17-267ca6975798' for Azure DevOps # - 'https://storage.azure.com/' for storage # Defaults to Azure DevOps - name: resource type: string default: '499b84ac-1321-427f-aa17-267ca6975798' - name: isStepOutputVariable type: boolean default: false steps: - task: AzureCLI@2 displayName: 'Getting federated access token for feeds' name: ${{ parameters.stepName }} ${{ if ne(parameters.condition, '') }}: condition: ${{ parameters.condition }} inputs: azureSubscription: ${{ parameters.federatedServiceConnection }} scriptType: 'pscore' scriptLocation: 'inlineScript' inlineScript: | $accessToken = az account get-access-token --query accessToken --resource ${{ parameters.resource }} --output tsv if ($LASTEXITCODE -ne 0) { Write-Error "Failed to get access token for resource '${{ parameters.resource }}'" exit 1 } Write-Host "Setting '${{ parameters.outputVariableName }}' with the access token value" Write-Host "##vso[task.setvariable variable=${{ parameters.outputVariableName }};issecret=true;isOutput=${{ parameters.isStepOutputVariable }}]$accessToken" ================================================ FILE: eng/common/core-templates/steps/install-microbuild-impl.yml ================================================ parameters: - name: microbuildTaskInputs type: object default: {} - name: microbuildEnv type: object default: {} - name: enablePreviewMicrobuild type: boolean default: false - name: condition type: string - name: continueOnError type: boolean steps: - ${{ if eq(parameters.enablePreviewMicrobuild, true) }}: - task: MicroBuildSigningPluginPreview@4 displayName: Install Preview MicroBuild plugin inputs: ${{ parameters.microbuildTaskInputs }} env: ${{ parameters.microbuildEnv }} continueOnError: ${{ parameters.continueOnError }} condition: ${{ parameters.condition }} - ${{ else }}: - task: MicroBuildSigningPlugin@4 displayName: Install MicroBuild plugin inputs: ${{ parameters.microbuildTaskInputs }} env: ${{ parameters.microbuildEnv }} continueOnError: ${{ parameters.continueOnError }} condition: ${{ parameters.condition }} ================================================ FILE: eng/common/core-templates/steps/install-microbuild.yml ================================================ parameters: # Enable install tasks for MicroBuild enableMicrobuild: false # Enable install tasks for MicroBuild on Mac and Linux # Will be ignored if 'enableMicrobuild' is false or 'Agent.Os' is 'Windows_NT' enableMicrobuildForMacAndLinux: false # Enable preview version of MB signing plugin enablePreviewMicrobuild: false # Determines whether the ESRP service connection information should be passed to the signing plugin. # This overlaps with _SignType to some degree. We only need the service connection for real signing. # It's important that the service connection not be passed to the MicroBuildSigningPlugin task in this place. # Doing so will cause the service connection to be authorized for the pipeline, which isn't allowed and won't work for non-prod. # Unfortunately, _SignType can't be used to exclude the use of the service connection in non-real sign scenarios. The # variable is not available in template expression. _SignType has a very large proliferation across .NET, so replacing it is tough. microbuildUseESRP: true # Microbuild installation directory microBuildOutputFolder: $(Agent.TempDirectory)/MicroBuild # Microbuild version microbuildPluginVersion: 'latest' continueOnError: false steps: - ${{ if eq(parameters.enableMicrobuild, 'true') }}: - ${{ if eq(parameters.enableMicrobuildForMacAndLinux, 'true') }}: # Needed to download the MicroBuild plugin nupkgs on Mac and Linux when nuget.exe is unavailable - task: UseDotNet@2 displayName: Install .NET 8.0 SDK for MicroBuild Plugin inputs: packageType: sdk version: 8.0.x installationPath: ${{ parameters.microBuildOutputFolder }}/.dotnet-microbuild condition: and(succeeded(), ne(variables['Agent.Os'], 'Windows_NT')) - script: | set -euo pipefail # UseDotNet@2 prepends the dotnet executable path to the PATH variable, so we can call dotnet directly version=$(dotnet --version) cat << 'EOF' > ${{ parameters.microBuildOutputFolder }}/global.json { "sdk": { "version": "$version", "paths": [ "${{ parameters.microBuildOutputFolder }}/.dotnet-microbuild" ], "errorMessage": "The .NET SDK version $version is required to install the MicroBuild signing plugin." } } EOF displayName: 'Add global.json to MicroBuild Installation path' workingDirectory: ${{ parameters.microBuildOutputFolder }} condition: and(succeeded(), ne(variables['Agent.Os'], 'Windows_NT')) - script: | REM Check if ESRP is disabled while SignType is real if /I "${{ parameters.microbuildUseESRP }}"=="false" if /I "$(_SignType)"=="real" ( echo Error: ESRP must be enabled when SignType is real. exit /b 1 ) displayName: 'Validate ESRP usage (Windows)' condition: and(succeeded(), eq(variables['Agent.Os'], 'Windows_NT')) - script: | # Check if ESRP is disabled while SignType is real if [ "${{ parameters.microbuildUseESRP }}" = "false" ] && [ "$(_SignType)" = "real" ]; then echo "Error: ESRP must be enabled when SignType is real." exit 1 fi displayName: 'Validate ESRP usage (Non-Windows)' condition: and(succeeded(), ne(variables['Agent.Os'], 'Windows_NT')) # Two different MB install steps. This is due to not being able to use the agent OS during # YAML expansion, and Windows vs. Linux/Mac uses different service connections. However, # we can avoid including the MB install step if not enabled at all. This avoids a bunch of # extra pipeline authorizations, since most pipelines do not sign on non-Windows. - template: /eng/common/core-templates/steps/install-microbuild-impl.yml@self parameters: enablePreviewMicrobuild: ${{ parameters.enablePreviewMicrobuild }} microbuildTaskInputs: signType: $(_SignType) zipSources: false feedSource: https://dnceng.pkgs.visualstudio.com/_packaging/MicroBuildToolset/nuget/v3/index.json version: ${{ parameters.microbuildPluginVersion }} ${{ if eq(parameters.microbuildUseESRP, true) }}: ConnectedServiceName: 'MicroBuild Signing Task (DevDiv)' ${{ if eq(variables['System.TeamProject'], 'DevDiv') }}: ConnectedPMEServiceName: 6cc74545-d7b9-4050-9dfa-ebefcc8961ea ${{ else }}: ConnectedPMEServiceName: 248d384a-b39b-46e3-8ad5-c2c210d5e7ca microbuildEnv: TeamName: $(_TeamName) MicroBuildOutputFolderOverride: ${{ parameters.microBuildOutputFolder }} SYSTEM_ACCESSTOKEN: $(System.AccessToken) continueOnError: ${{ parameters.continueOnError }} condition: and(succeeded(), eq(variables['Agent.Os'], 'Windows_NT'), in(variables['_SignType'], 'real', 'test')) - ${{ if eq(parameters.enableMicrobuildForMacAndLinux, true) }}: - template: /eng/common/core-templates/steps/install-microbuild-impl.yml@self parameters: enablePreviewMicrobuild: ${{ parameters.enablePreviewMicrobuild }} microbuildTaskInputs: signType: $(_SignType) zipSources: false feedSource: https://dnceng.pkgs.visualstudio.com/_packaging/MicroBuildToolset/nuget/v3/index.json version: ${{ parameters.microbuildPluginVersion }} workingDirectory: ${{ parameters.microBuildOutputFolder }} ${{ if eq(parameters.microbuildUseESRP, true) }}: ConnectedServiceName: 'MicroBuild Signing Task (DevDiv)' ${{ if eq(variables['System.TeamProject'], 'DevDiv') }}: ConnectedPMEServiceName: beb8cb23-b303-4c95-ab26-9e44bc958d39 ${{ else }}: ConnectedPMEServiceName: c24de2a5-cc7a-493d-95e4-8e5ff5cad2bc microbuildEnv: TeamName: $(_TeamName) MicroBuildOutputFolderOverride: ${{ parameters.microBuildOutputFolder }} SYSTEM_ACCESSTOKEN: $(System.AccessToken) continueOnError: ${{ parameters.continueOnError }} condition: and(succeeded(), ne(variables['Agent.Os'], 'Windows_NT'), eq(variables['_SignType'], 'real')) ================================================ FILE: eng/common/core-templates/steps/publish-build-artifacts.yml ================================================ parameters: - name: is1ESPipeline type: boolean default: false - name: args type: object default: {} steps: - ${{ if ne(parameters.is1ESPipeline, true) }}: - template: /eng/common/templates/steps/publish-build-artifacts.yml parameters: is1ESPipeline: ${{ parameters.is1ESPipeline }} ${{ each parameter in parameters.args }}: ${{ parameter.key }}: ${{ parameter.value }} - ${{ else }}: - template: /eng/common/templates-official/steps/publish-build-artifacts.yml parameters: is1ESPipeline: ${{ parameters.is1ESPipeline }} ${{ each parameter in parameters.args }}: ${{ parameter.key }}: ${{ parameter.value }} ================================================ FILE: eng/common/core-templates/steps/publish-logs.yml ================================================ parameters: StageLabel: '' JobLabel: '' CustomSensitiveDataList: '' # A default - in case value from eng/common/core-templates/post-build/common-variables.yml is not passed BinlogToolVersion: '1.0.11' is1ESPipeline: false steps: - task: Powershell@2 displayName: Prepare Binlogs to Upload inputs: targetType: inline script: | New-Item -ItemType Directory $(System.DefaultWorkingDirectory)/PostBuildLogs/${{parameters.StageLabel}}/${{parameters.JobLabel}}/ Move-Item -Path $(System.DefaultWorkingDirectory)/artifacts/log/Debug/* $(System.DefaultWorkingDirectory)/PostBuildLogs/${{parameters.StageLabel}}/${{parameters.JobLabel}}/ continueOnError: true condition: always() - task: PowerShell@2 displayName: Redact Logs inputs: filePath: $(System.DefaultWorkingDirectory)/eng/common/post-build/redact-logs.ps1 # For now this needs to have explicit list of all sensitive data. Taken from eng/publishing/v3/publish.yml # Sensitive data can as well be added to $(System.DefaultWorkingDirectory)/eng/BinlogSecretsRedactionFile.txt' # If the file exists - sensitive data for redaction will be sourced from it # (single entry per line, lines starting with '# ' are considered comments and skipped) arguments: -InputPath '$(System.DefaultWorkingDirectory)/PostBuildLogs' -BinlogToolVersion '${{parameters.BinlogToolVersion}}' -TokensFilePath '$(System.DefaultWorkingDirectory)/eng/BinlogSecretsRedactionFile.txt' -runtimeSourceFeed https://ci.dot.net/internal -runtimeSourceFeedKey '$(dotnetbuilds-internal-container-read-token-base64)' '$(publishing-dnceng-devdiv-code-r-build-re)' '$(MaestroAccessToken)' '$(dn-bot-all-orgs-artifact-feeds-rw)' '$(akams-client-id)' '$(microsoft-symbol-server-pat)' '$(symweb-symbol-server-pat)' '$(dnceng-symbol-server-pat)' '$(dn-bot-all-orgs-build-rw-code-rw)' '$(System.AccessToken)' ${{parameters.CustomSensitiveDataList}} continueOnError: true condition: always() - task: CopyFiles@2 displayName: Gather post build logs inputs: SourceFolder: '$(System.DefaultWorkingDirectory)/PostBuildLogs' Contents: '**' TargetFolder: '$(Build.ArtifactStagingDirectory)/PostBuildLogs' condition: always() - template: /eng/common/core-templates/steps/publish-build-artifacts.yml parameters: is1ESPipeline: ${{ parameters.is1ESPipeline }} args: displayName: Publish Logs pathToPublish: '$(Build.ArtifactStagingDirectory)/PostBuildLogs' publishLocation: Container artifactName: PostBuildLogs continueOnError: true condition: always() ================================================ FILE: eng/common/core-templates/steps/publish-pipeline-artifacts.yml ================================================ parameters: - name: is1ESPipeline type: boolean default: false - name: args type: object default: {} steps: - ${{ if ne(parameters.is1ESPipeline, true) }}: - template: /eng/common/templates/steps/publish-pipeline-artifacts.yml parameters: ${{ each parameter in parameters }}: ${{ parameter.key }}: ${{ parameter.value }} - ${{ else }}: - template: /eng/common/templates-official/steps/publish-pipeline-artifacts.yml parameters: ${{ each parameter in parameters }}: ${{ parameter.key }}: ${{ parameter.value }} ================================================ FILE: eng/common/core-templates/steps/retain-build.yml ================================================ parameters: # Optional azure devops PAT with build execute permissions for the build's organization, # only needed if the build that should be retained ran on a different organization than # the pipeline where this template is executing from Token: '' # Optional BuildId to retain, defaults to the current running build BuildId: '' # Azure devops Organization URI for the build in the https://dev.azure.com/ format. # Defaults to the organization the current pipeline is running on AzdoOrgUri: '$(System.CollectionUri)' # Azure devops project for the build. Defaults to the project the current pipeline is running on AzdoProject: '$(System.TeamProject)' steps: - task: powershell@2 inputs: targetType: 'filePath' filePath: eng/common/retain-build.ps1 pwsh: true arguments: > -AzdoOrgUri: ${{parameters.AzdoOrgUri}} -AzdoProject ${{parameters.AzdoProject}} -Token ${{coalesce(parameters.Token, '$env:SYSTEM_ACCESSTOKEN') }} -BuildId ${{coalesce(parameters.BuildId, '$env:BUILD_ID')}} displayName: Enable permanent build retention env: SYSTEM_ACCESSTOKEN: $(System.AccessToken) BUILD_ID: $(Build.BuildId) ================================================ FILE: eng/common/core-templates/steps/send-to-helix.yml ================================================ # Please remember to update the documentation if you make changes to these parameters! parameters: HelixSource: 'pr/default' # required -- sources must start with pr/, official/, prodcon/, or agent/ HelixType: 'tests/default/' # required -- Helix telemetry which identifies what type of data this is; should include "test" for clarity and must end in '/' HelixBuild: $(Build.BuildNumber) # required -- the build number Helix will use to identify this -- automatically set to the AzDO build number HelixTargetQueues: '' # required -- semicolon-delimited list of Helix queues to test on; see https://helix.dot.net/ for a list of queues HelixAccessToken: '' # required -- access token to make Helix API requests; should be provided by the appropriate variable group HelixProjectPath: 'eng/common/helixpublish.proj' # optional -- path to the project file to build relative to BUILD_SOURCESDIRECTORY HelixProjectArguments: '' # optional -- arguments passed to the build command HelixConfiguration: '' # optional -- additional property attached to a job HelixPreCommands: '' # optional -- commands to run before Helix work item execution HelixPostCommands: '' # optional -- commands to run after Helix work item execution WorkItemDirectory: '' # optional -- a payload directory to zip up and send to Helix; requires WorkItemCommand; incompatible with XUnitProjects WorkItemCommand: '' # optional -- a command to execute on the payload; requires WorkItemDirectory; incompatible with XUnitProjects WorkItemTimeout: '' # optional -- a timeout in TimeSpan.Parse-ready value (e.g. 00:02:00) for the work item command; requires WorkItemDirectory; incompatible with XUnitProjects CorrelationPayloadDirectory: '' # optional -- a directory to zip up and send to Helix as a correlation payload XUnitProjects: '' # optional -- semicolon-delimited list of XUnitProjects to parse and send to Helix; requires XUnitRuntimeTargetFramework, XUnitPublishTargetFramework, XUnitRunnerVersion, and IncludeDotNetCli=true XUnitWorkItemTimeout: '' # optional -- the workitem timeout in seconds for all workitems created from the xUnit projects specified by XUnitProjects XUnitPublishTargetFramework: '' # optional -- framework to use to publish your xUnit projects XUnitRuntimeTargetFramework: '' # optional -- framework to use for the xUnit console runner XUnitRunnerVersion: '' # optional -- version of the xUnit nuget package you wish to use on Helix; required for XUnitProjects IncludeDotNetCli: false # optional -- true will download a version of the .NET CLI onto the Helix machine as a correlation payload; requires DotNetCliPackageType and DotNetCliVersion DotNetCliPackageType: '' # optional -- either 'sdk', 'runtime' or 'aspnetcore-runtime'; determines whether the sdk or runtime will be sent to Helix; see https://raw.githubusercontent.com/dotnet/core/main/release-notes/releases-index.json DotNetCliVersion: '' # optional -- version of the CLI to send to Helix; based on this: https://raw.githubusercontent.com/dotnet/core/main/release-notes/releases-index.json WaitForWorkItemCompletion: true # optional -- true will make the task wait until work items have been completed and fail the build if work items fail. False is "fire and forget." IsExternal: false # [DEPRECATED] -- doesn't do anything, jobs are external if HelixAccessToken is empty and Creator is set HelixBaseUri: 'https://helix.dot.net/' # optional -- sets the Helix API base URI (allows targeting https://helix.int-dot.net ) Creator: '' # optional -- if the build is external, use this to specify who is sending the job DisplayNamePrefix: 'Run Tests' # optional -- rename the beginning of the displayName of the steps in AzDO condition: succeeded() # optional -- condition for step to execute; defaults to succeeded() continueOnError: false # optional -- determines whether to continue the build if the step errors; defaults to false steps: - powershell: 'powershell "$env:BUILD_SOURCESDIRECTORY\eng\common\msbuild.ps1 $env:BUILD_SOURCESDIRECTORY/${{ parameters.HelixProjectPath }} /restore /p:TreatWarningsAsErrors=false ${{ parameters.HelixProjectArguments }} /t:Test /bl:$env:BUILD_SOURCESDIRECTORY\artifacts\log\$env:BuildConfig\SendToHelix.binlog"' displayName: ${{ parameters.DisplayNamePrefix }} (Windows) env: BuildConfig: $(_BuildConfig) HelixSource: ${{ parameters.HelixSource }} HelixType: ${{ parameters.HelixType }} HelixBuild: ${{ parameters.HelixBuild }} HelixConfiguration: ${{ parameters.HelixConfiguration }} HelixTargetQueues: ${{ parameters.HelixTargetQueues }} HelixAccessToken: ${{ parameters.HelixAccessToken }} HelixPreCommands: ${{ parameters.HelixPreCommands }} HelixPostCommands: ${{ parameters.HelixPostCommands }} WorkItemDirectory: ${{ parameters.WorkItemDirectory }} WorkItemCommand: ${{ parameters.WorkItemCommand }} WorkItemTimeout: ${{ parameters.WorkItemTimeout }} CorrelationPayloadDirectory: ${{ parameters.CorrelationPayloadDirectory }} XUnitProjects: ${{ parameters.XUnitProjects }} XUnitWorkItemTimeout: ${{ parameters.XUnitWorkItemTimeout }} XUnitPublishTargetFramework: ${{ parameters.XUnitPublishTargetFramework }} XUnitRuntimeTargetFramework: ${{ parameters.XUnitRuntimeTargetFramework }} XUnitRunnerVersion: ${{ parameters.XUnitRunnerVersion }} IncludeDotNetCli: ${{ parameters.IncludeDotNetCli }} DotNetCliPackageType: ${{ parameters.DotNetCliPackageType }} DotNetCliVersion: ${{ parameters.DotNetCliVersion }} WaitForWorkItemCompletion: ${{ parameters.WaitForWorkItemCompletion }} HelixBaseUri: ${{ parameters.HelixBaseUri }} Creator: ${{ parameters.Creator }} SYSTEM_ACCESSTOKEN: $(System.AccessToken) condition: and(${{ parameters.condition }}, eq(variables['Agent.Os'], 'Windows_NT')) continueOnError: ${{ parameters.continueOnError }} - script: $BUILD_SOURCESDIRECTORY/eng/common/msbuild.sh $BUILD_SOURCESDIRECTORY/${{ parameters.HelixProjectPath }} /restore /p:TreatWarningsAsErrors=false ${{ parameters.HelixProjectArguments }} /t:Test /bl:$BUILD_SOURCESDIRECTORY/artifacts/log/$BuildConfig/SendToHelix.binlog displayName: ${{ parameters.DisplayNamePrefix }} (Unix) env: BuildConfig: $(_BuildConfig) HelixSource: ${{ parameters.HelixSource }} HelixType: ${{ parameters.HelixType }} HelixBuild: ${{ parameters.HelixBuild }} HelixConfiguration: ${{ parameters.HelixConfiguration }} HelixTargetQueues: ${{ parameters.HelixTargetQueues }} HelixAccessToken: ${{ parameters.HelixAccessToken }} HelixPreCommands: ${{ parameters.HelixPreCommands }} HelixPostCommands: ${{ parameters.HelixPostCommands }} WorkItemDirectory: ${{ parameters.WorkItemDirectory }} WorkItemCommand: ${{ parameters.WorkItemCommand }} WorkItemTimeout: ${{ parameters.WorkItemTimeout }} CorrelationPayloadDirectory: ${{ parameters.CorrelationPayloadDirectory }} XUnitProjects: ${{ parameters.XUnitProjects }} XUnitWorkItemTimeout: ${{ parameters.XUnitWorkItemTimeout }} XUnitPublishTargetFramework: ${{ parameters.XUnitPublishTargetFramework }} XUnitRuntimeTargetFramework: ${{ parameters.XUnitRuntimeTargetFramework }} XUnitRunnerVersion: ${{ parameters.XUnitRunnerVersion }} IncludeDotNetCli: ${{ parameters.IncludeDotNetCli }} DotNetCliPackageType: ${{ parameters.DotNetCliPackageType }} DotNetCliVersion: ${{ parameters.DotNetCliVersion }} WaitForWorkItemCompletion: ${{ parameters.WaitForWorkItemCompletion }} HelixBaseUri: ${{ parameters.HelixBaseUri }} Creator: ${{ parameters.Creator }} SYSTEM_ACCESSTOKEN: $(System.AccessToken) condition: and(${{ parameters.condition }}, ne(variables['Agent.Os'], 'Windows_NT')) continueOnError: ${{ parameters.continueOnError }} ================================================ FILE: eng/common/core-templates/steps/source-build.yml ================================================ parameters: # This template adds arcade-powered source-build to CI. # This is a 'steps' template, and is intended for advanced scenarios where the existing build # infra has a careful build methodology that must be followed. For example, a repo # (dotnet/runtime) might choose to clone the GitHub repo only once and store it as a pipeline # artifact for all subsequent jobs to use, to reduce dependence on a strong network connection to # GitHub. Using this steps template leaves room for that infra to be included. # Defines the platform on which to run the steps. See 'eng/common/core-templates/job/source-build.yml' # for details. The entire object is described in the 'job' template for simplicity, even though # the usage of the properties on this object is split between the 'job' and 'steps' templates. platform: {} is1ESPipeline: false steps: # Build. Keep it self-contained for simple reusability. (No source-build-specific job variables.) - script: | set -x df -h # If building on the internal project, the internal storage variable may be available (usually only if needed) # In that case, add variables to allow the download of internal runtimes if the specified versions are not found # in the default public locations. internalRuntimeDownloadArgs= if [ '$(dotnetbuilds-internal-container-read-token-base64)' != '$''(dotnetbuilds-internal-container-read-token-base64)' ]; then internalRuntimeDownloadArgs='/p:DotNetRuntimeSourceFeed=https://ci.dot.net/internal /p:DotNetRuntimeSourceFeedKey=$(dotnetbuilds-internal-container-read-token-base64) --runtimesourcefeed https://ci.dot.net/internal --runtimesourcefeedkey $(dotnetbuilds-internal-container-read-token-base64)' fi buildConfig=Release # Check if AzDO substitutes in a build config from a variable, and use it if so. if [ '$(_BuildConfig)' != '$''(_BuildConfig)' ]; then buildConfig='$(_BuildConfig)' fi targetRidArgs= if [ '${{ parameters.platform.targetRID }}' != '' ]; then targetRidArgs='/p:TargetRid=${{ parameters.platform.targetRID }}' fi portableBuildArgs= if [ '${{ parameters.platform.portableBuild }}' != '' ]; then portableBuildArgs='/p:PortableBuild=${{ parameters.platform.portableBuild }}' fi ${{ coalesce(parameters.platform.buildScript, './build.sh') }} --ci \ --configuration $buildConfig \ --restore --build --pack -bl \ --source-build \ ${{ parameters.platform.buildArguments }} \ $internalRuntimeDownloadArgs \ $targetRidArgs \ $portableBuildArgs \ displayName: Build - template: /eng/common/core-templates/steps/publish-pipeline-artifacts.yml parameters: is1ESPipeline: ${{ parameters.is1ESPipeline }} args: displayName: Publish BuildLogs targetPath: artifacts/log/${{ coalesce(variables._BuildConfig, 'Release') }} artifactName: BuildLogs_SourceBuild_${{ parameters.platform.name }}_Attempt$(System.JobAttempt) continueOnError: true condition: succeededOrFailed() sbomEnabled: false # we don't need SBOM for logs ================================================ FILE: eng/common/core-templates/steps/source-index-stage1-publish.yml ================================================ parameters: sourceIndexUploadPackageVersion: 2.0.0-20250906.1 sourceIndexProcessBinlogPackageVersion: 1.0.1-20250906.1 sourceIndexPackageSource: https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-tools/nuget/v3/index.json binlogPath: artifacts/log/Debug/Build.binlog steps: - task: UseDotNet@2 displayName: "Source Index: Use .NET 9 SDK" inputs: packageType: sdk version: 9.0.x installationPath: $(Agent.TempDirectory)/dotnet workingDirectory: $(Agent.TempDirectory) - script: | $(Agent.TempDirectory)/dotnet/dotnet tool install BinLogToSln --version ${{parameters.sourceIndexProcessBinlogPackageVersion}} --source ${{parameters.sourceIndexPackageSource}} --tool-path $(Agent.TempDirectory)/.source-index/tools $(Agent.TempDirectory)/dotnet/dotnet tool install UploadIndexStage1 --version ${{parameters.sourceIndexUploadPackageVersion}} --source ${{parameters.sourceIndexPackageSource}} --tool-path $(Agent.TempDirectory)/.source-index/tools displayName: "Source Index: Download netsourceindex Tools" # Set working directory to temp directory so 'dotnet' doesn't try to use global.json and use the repo's sdk. workingDirectory: $(Agent.TempDirectory) - script: $(Agent.TempDirectory)/.source-index/tools/BinLogToSln -i ${{parameters.BinlogPath}} -r $(System.DefaultWorkingDirectory) -n $(Build.Repository.Name) -o .source-index/stage1output displayName: "Source Index: Process Binlog into indexable sln" - ${{ if and(ne(parameters.runAsPublic, 'true'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: - task: AzureCLI@2 displayName: "Source Index: Upload Source Index stage1 artifacts to Azure" inputs: azureSubscription: 'SourceDotNet Stage1 Publish' addSpnToEnvironment: true scriptType: 'ps' scriptLocation: 'inlineScript' inlineScript: | $(Agent.TempDirectory)/.source-index/tools/UploadIndexStage1 -i .source-index/stage1output -n $(Build.Repository.Name) -s netsourceindexstage1 -b stage1 ================================================ FILE: eng/common/core-templates/variables/pool-providers.yml ================================================ parameters: is1ESPipeline: false variables: - ${{ if eq(parameters.is1ESPipeline, 'true') }}: - template: /eng/common/templates-official/variables/pool-providers.yml - ${{ else }}: - template: /eng/common/templates/variables/pool-providers.yml ================================================ FILE: eng/common/cross/armel/tizen/tizen.patch ================================================ diff -u -r a/usr/lib/libc.so b/usr/lib/libc.so --- a/usr/lib/libc.so 2016-12-30 23:00:08.284951863 +0900 +++ b/usr/lib/libc.so 2016-12-30 23:00:32.140951815 +0900 @@ -2,4 +2,4 @@ Use the shared library, but some functions are only in the static library, so try that secondarily. */ OUTPUT_FORMAT(elf32-littlearm) -GROUP ( /lib/libc.so.6 /usr/lib/libc_nonshared.a AS_NEEDED ( /lib/ld-linux.so.3 ) ) +GROUP ( libc.so.6 libc_nonshared.a AS_NEEDED ( ld-linux.so.3 ) ) ================================================ FILE: eng/common/cross/build-android-rootfs.sh ================================================ #!/usr/bin/env bash set -e __NDK_Version=r21 usage() { echo "Creates a toolchain and sysroot used for cross-compiling for Android." echo echo "Usage: $0 [BuildArch] [ApiLevel] [--ndk NDKVersion]" echo echo "BuildArch is the target architecture of Android. Currently only arm64 is supported." echo "ApiLevel is the target Android API level. API levels usually match to Android releases. See https://source.android.com/source/build-numbers.html" echo "NDKVersion is the version of Android NDK. The default is r21. See https://developer.android.com/ndk/downloads/revision_history" echo echo "By default, the toolchain and sysroot will be generated in cross/android-rootfs/toolchain/[BuildArch]. You can change this behavior" echo "by setting the TOOLCHAIN_DIR environment variable" echo echo "By default, the NDK will be downloaded into the cross/android-rootfs/android-ndk-$__NDK_Version directory. If you already have an NDK installation," echo "you can set the NDK_DIR environment variable to have this script use that installation of the NDK." echo "By default, this script will generate a file, android_platform, in the root of the ROOTFS_DIR directory that contains the RID for the supported and tested Android build: android.28-arm64. This file is to replace '/etc/os-release', which is not available for Android." exit 1 } __ApiLevel=28 # The minimum platform for arm64 is API level 21 but the minimum version that support glob(3) is 28. See $ANDROID_NDK/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/include/glob.h __BuildArch=arm64 __AndroidArch=aarch64 __AndroidToolchain=aarch64-linux-android while :; do if [[ "$#" -le 0 ]]; then break fi i=$1 lowerI="$(echo $i | tr "[:upper:]" "[:lower:]")" case $lowerI in -?|-h|--help) usage exit 1 ;; arm64) __BuildArch=arm64 __AndroidArch=aarch64 __AndroidToolchain=aarch64-linux-android ;; arm) __BuildArch=arm __AndroidArch=arm __AndroidToolchain=arm-linux-androideabi ;; --ndk) shift __NDK_Version=$1 ;; *[0-9]) __ApiLevel=$i ;; *) __UnprocessedBuildArgs="$__UnprocessedBuildArgs $i" ;; esac shift done if [[ "$__NDK_Version" == "r21" ]] || [[ "$__NDK_Version" == "r22" ]]; then __NDK_File_Arch_Spec=-x86_64 __SysRoot=sysroot else __NDK_File_Arch_Spec= __SysRoot=toolchains/llvm/prebuilt/linux-x86_64/sysroot fi # Obtain the location of the bash script to figure out where the root of the repo is. __ScriptBaseDir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" __CrossDir="$__ScriptBaseDir/../../../.tools/android-rootfs" if [[ ! -f "$__CrossDir" ]]; then mkdir -p "$__CrossDir" fi # Resolve absolute path to avoid `../` in build logs __CrossDir="$( cd "$__CrossDir" && pwd )" __NDK_Dir="$__CrossDir/android-ndk-$__NDK_Version" __lldb_Dir="$__CrossDir/lldb" __ToolchainDir="$__CrossDir/android-ndk-$__NDK_Version" if [[ -n "$TOOLCHAIN_DIR" ]]; then __ToolchainDir=$TOOLCHAIN_DIR fi if [[ -n "$NDK_DIR" ]]; then __NDK_Dir=$NDK_DIR fi echo "Target API level: $__ApiLevel" echo "Target architecture: $__BuildArch" echo "NDK version: $__NDK_Version" echo "NDK location: $__NDK_Dir" echo "Target Toolchain location: $__ToolchainDir" # Download the NDK if required if [ ! -d $__NDK_Dir ]; then echo Downloading the NDK into $__NDK_Dir mkdir -p $__NDK_Dir wget -q --progress=bar:force:noscroll --show-progress https://dl.google.com/android/repository/android-ndk-$__NDK_Version-linux$__NDK_File_Arch_Spec.zip -O $__CrossDir/android-ndk-$__NDK_Version-linux.zip unzip -q $__CrossDir/android-ndk-$__NDK_Version-linux.zip -d $__CrossDir fi if [ ! -d $__lldb_Dir ]; then mkdir -p $__lldb_Dir echo Downloading LLDB into $__lldb_Dir wget -q --progress=bar:force:noscroll --show-progress https://dl.google.com/android/repository/lldb-2.3.3614996-linux-x86_64.zip -O $__CrossDir/lldb-2.3.3614996-linux-x86_64.zip unzip -q $__CrossDir/lldb-2.3.3614996-linux-x86_64.zip -d $__lldb_Dir fi echo "Download dependencies..." __TmpDir=$__CrossDir/tmp/$__BuildArch/ mkdir -p "$__TmpDir" # combined dependencies for coreclr, installer and libraries __AndroidPackages="libicu" __AndroidPackages+=" libandroid-glob" __AndroidPackages+=" liblzma" __AndroidPackages+=" krb5" __AndroidPackages+=" openssl" for path in $(wget -qO- https://packages.termux.dev/termux-main-21/dists/stable/main/binary-$__AndroidArch/Packages |\ grep -A15 "Package: \(${__AndroidPackages// /\\|}\)" | grep -v "static\|tool" | grep Filename); do if [[ "$path" != "Filename:" ]]; then echo "Working on: $path" wget -qO- https://packages.termux.dev/termux-main-21/$path | dpkg -x - "$__TmpDir" fi done cp -R "$__TmpDir/data/data/com.termux/files/usr/"* "$__ToolchainDir/$__SysRoot/usr/" # Generate platform file for build.sh script to assign to __DistroRid echo "Generating platform file..." echo "RID=android.${__ApiLevel}-${__BuildArch}" > $__ToolchainDir/$__SysRoot/android_platform echo "Now to build coreclr, libraries and host; run:" echo ROOTFS_DIR=$(realpath $__ToolchainDir/$__SysRoot) ./build.sh clr+libs+host --cross --arch $__BuildArch ================================================ FILE: eng/common/cross/build-rootfs.sh ================================================ #!/usr/bin/env bash set -e usage() { echo "Usage: $0 [BuildArch] [CodeName] [lldbx.y] [llvmx[.y]] [--skipunmount] --rootfsdir ]" echo "BuildArch can be: arm(default), arm64, armel, armv6, loongarch64, ppc64le, riscv64, s390x, x64, x86" echo "CodeName - optional, Code name for Linux, can be: xenial(default), zesty, bionic, alpine" echo " for alpine can be specified with version: alpineX.YY or alpineedge" echo " for FreeBSD can be: freebsd13, freebsd14" echo " for illumos can be: illumos" echo " for Haiku can be: haiku." echo "lldbx.y - optional, LLDB version, can be: lldb3.9(default), lldb4.0, lldb5.0, lldb6.0 no-lldb. Ignored for alpine and FreeBSD" echo "llvmx[.y] - optional, LLVM version for LLVM related packages." echo "--skipunmount - optional, will skip the unmount of rootfs folder." echo "--skipsigcheck - optional, will skip package signature checks (allowing untrusted packages)." echo "--skipemulation - optional, will skip qemu and debootstrap requirement when building environment for debian based systems." echo "--use-mirror - optional, use mirror URL to fetch resources, when available." echo "--jobs N - optional, restrict to N jobs." exit 1 } __CodeName=xenial __CrossDir=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) __BuildArch=arm __AlpineArch=armv7 __FreeBSDArch=arm __FreeBSDMachineArch=armv7 __IllumosArch=arm7 __HaikuArch=arm __QEMUArch=arm __UbuntuArch=armhf __UbuntuRepo= __UbuntuSuites="updates security backports" __LLDB_Package="liblldb-3.9-dev" __SkipUnmount=0 # base development support __UbuntuPackages="build-essential" __AlpinePackages="alpine-base" __AlpinePackages+=" build-base" __AlpinePackages+=" linux-headers" __AlpinePackages+=" lldb-dev" __AlpinePackages+=" python3" __AlpinePackages+=" libedit" # symlinks fixer __UbuntuPackages+=" symlinks" # runtime dependencies __UbuntuPackages+=" libicu-dev" __UbuntuPackages+=" liblttng-ust-dev" __UbuntuPackages+=" libunwind8-dev" __AlpinePackages+=" gettext-dev" __AlpinePackages+=" icu-dev" __AlpinePackages+=" libunwind-dev" __AlpinePackages+=" lttng-ust-dev" __AlpinePackages+=" compiler-rt" # runtime libraries' dependencies __UbuntuPackages+=" libcurl4-openssl-dev" __UbuntuPackages+=" libkrb5-dev" __UbuntuPackages+=" libssl-dev" __UbuntuPackages+=" zlib1g-dev" __UbuntuPackages+=" libbrotli-dev" __AlpinePackages+=" curl-dev" __AlpinePackages+=" krb5-dev" __AlpinePackages+=" openssl-dev" __AlpinePackages+=" zlib-dev" __FreeBSDBase="13.5-RELEASE" __FreeBSDPkg="1.21.3" __FreeBSDABI="13" __FreeBSDPackages="libunwind" __FreeBSDPackages+=" icu" __FreeBSDPackages+=" libinotify" __FreeBSDPackages+=" openssl" __FreeBSDPackages+=" krb5" __FreeBSDPackages+=" terminfo-db" __IllumosPackages="icu" __IllumosPackages+=" mit-krb5" __IllumosPackages+=" openssl" __IllumosPackages+=" zlib" __HaikuPackages="gcc_syslibs" __HaikuPackages+=" gcc_syslibs_devel" __HaikuPackages+=" gmp" __HaikuPackages+=" gmp_devel" __HaikuPackages+=" icu[0-9]+" __HaikuPackages+=" icu[0-9]*_devel" __HaikuPackages+=" krb5" __HaikuPackages+=" krb5_devel" __HaikuPackages+=" libiconv" __HaikuPackages+=" libiconv_devel" __HaikuPackages+=" llvm[0-9]*_libunwind" __HaikuPackages+=" llvm[0-9]*_libunwind_devel" __HaikuPackages+=" mpfr" __HaikuPackages+=" mpfr_devel" __HaikuPackages+=" openssl3" __HaikuPackages+=" openssl3_devel" __HaikuPackages+=" zlib" __HaikuPackages+=" zlib_devel" # ML.NET dependencies __UbuntuPackages+=" libomp5" __UbuntuPackages+=" libomp-dev" # Taken from https://github.com/alpinelinux/alpine-chroot-install/blob/6d08f12a8a70dd9b9dc7d997c88aa7789cc03c42/alpine-chroot-install#L85-L133 __AlpineKeys=' 4a6a0840:MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1yHJxQgsHQREclQu4Ohe\nqxTxd1tHcNnvnQTu/UrTky8wWvgXT+jpveroeWWnzmsYlDI93eLI2ORakxb3gA2O\nQ0Ry4ws8vhaxLQGC74uQR5+/yYrLuTKydFzuPaS1dK19qJPXB8GMdmFOijnXX4SA\njixuHLe1WW7kZVtjL7nufvpXkWBGjsfrvskdNA/5MfxAeBbqPgaq0QMEfxMAn6/R\nL5kNepi/Vr4S39Xvf2DzWkTLEK8pcnjNkt9/aafhWqFVW7m3HCAII6h/qlQNQKSo\nGuH34Q8GsFG30izUENV9avY7hSLq7nggsvknlNBZtFUcmGoQrtx3FmyYsIC8/R+B\nywIDAQAB 5243ef4b:MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvNijDxJ8kloskKQpJdx+\nmTMVFFUGDoDCbulnhZMJoKNkSuZOzBoFC94omYPtxnIcBdWBGnrm6ncbKRlR+6oy\nDO0W7c44uHKCFGFqBhDasdI4RCYP+fcIX/lyMh6MLbOxqS22TwSLhCVjTyJeeH7K\naA7vqk+QSsF4TGbYzQDDpg7+6aAcNzg6InNePaywA6hbT0JXbxnDWsB+2/LLSF2G\nmnhJlJrWB1WGjkz23ONIWk85W4S0XB/ewDefd4Ly/zyIciastA7Zqnh7p3Ody6Q0\nsS2MJzo7p3os1smGjUF158s6m/JbVh4DN6YIsxwl2OjDOz9R0OycfJSDaBVIGZzg\ncQIDAQAB 524d27bb:MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr8s1q88XpuJWLCZALdKj\nlN8wg2ePB2T9aIcaxryYE/Jkmtu+ZQ5zKq6BT3y/udt5jAsMrhHTwroOjIsF9DeG\ne8Y3vjz+Hh4L8a7hZDaw8jy3CPag47L7nsZFwQOIo2Cl1SnzUc6/owoyjRU7ab0p\niWG5HK8IfiybRbZxnEbNAfT4R53hyI6z5FhyXGS2Ld8zCoU/R4E1P0CUuXKEN4p0\n64dyeUoOLXEWHjgKiU1mElIQj3k/IF02W89gDj285YgwqA49deLUM7QOd53QLnx+\nxrIrPv3A+eyXMFgexNwCKQU9ZdmWa00MjjHlegSGK8Y2NPnRoXhzqSP9T9i2HiXL\nVQIDAQAB 5261cecb:MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwlzMkl7b5PBdfMzGdCT0\ncGloRr5xGgVmsdq5EtJvFkFAiN8Ac9MCFy/vAFmS8/7ZaGOXoCDWbYVLTLOO2qtX\nyHRl+7fJVh2N6qrDDFPmdgCi8NaE+3rITWXGrrQ1spJ0B6HIzTDNEjRKnD4xyg4j\ng01FMcJTU6E+V2JBY45CKN9dWr1JDM/nei/Pf0byBJlMp/mSSfjodykmz4Oe13xB\nCa1WTwgFykKYthoLGYrmo+LKIGpMoeEbY1kuUe04UiDe47l6Oggwnl+8XD1MeRWY\nsWgj8sF4dTcSfCMavK4zHRFFQbGp/YFJ/Ww6U9lA3Vq0wyEI6MCMQnoSMFwrbgZw\nwwIDAQAB 58199dcc:MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3v8/ye/V/t5xf4JiXLXa\nhWFRozsnmn3hobON20GdmkrzKzO/eUqPOKTpg2GtvBhK30fu5oY5uN2ORiv2Y2ht\neLiZ9HVz3XP8Fm9frha60B7KNu66FO5P2o3i+E+DWTPqqPcCG6t4Znk2BypILcit\nwiPKTsgbBQR2qo/cO01eLLdt6oOzAaF94NH0656kvRewdo6HG4urbO46tCAizvCR\nCA7KGFMyad8WdKkTjxh8YLDLoOCtoZmXmQAiwfRe9pKXRH/XXGop8SYptLqyVVQ+\ntegOD9wRs2tOlgcLx4F/uMzHN7uoho6okBPiifRX+Pf38Vx+ozXh056tjmdZkCaV\naQIDAQAB 58cbb476:MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoSPnuAGKtRIS5fEgYPXD\n8pSGvKAmIv3A08LBViDUe+YwhilSHbYXUEAcSH1KZvOo1WT1x2FNEPBEFEFU1Eyc\n+qGzbA03UFgBNvArurHQ5Z/GngGqE7IarSQFSoqewYRtFSfp+TL9CUNBvM0rT7vz\n2eMu3/wWG+CBmb92lkmyWwC1WSWFKO3x8w+Br2IFWvAZqHRt8oiG5QtYvcZL6jym\nY8T6sgdDlj+Y+wWaLHs9Fc+7vBuyK9C4O1ORdMPW15qVSl4Lc2Wu1QVwRiKnmA+c\nDsH/m7kDNRHM7TjWnuj+nrBOKAHzYquiu5iB3Qmx+0gwnrSVf27Arc3ozUmmJbLj\nzQIDAQAB 58e4f17d:MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvBxJN9ErBgdRcPr5g4hV\nqyUSGZEKuvQliq2Z9SRHLh2J43+EdB6A+yzVvLnzcHVpBJ+BZ9RV30EM9guck9sh\nr+bryZcRHyjG2wiIEoduxF2a8KeWeQH7QlpwGhuobo1+gA8L0AGImiA6UP3LOirl\nI0G2+iaKZowME8/tydww4jx5vG132JCOScMjTalRsYZYJcjFbebQQolpqRaGB4iG\nWqhytWQGWuKiB1A22wjmIYf3t96l1Mp+FmM2URPxD1gk/BIBnX7ew+2gWppXOK9j\n1BJpo0/HaX5XoZ/uMqISAAtgHZAqq+g3IUPouxTphgYQRTRYpz2COw3NF43VYQrR\nbQIDAQAB 60ac2099:MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwR4uJVtJOnOFGchnMW5Y\nj5/waBdG1u5BTMlH+iQMcV5+VgWhmpZHJCBz3ocD+0IGk2I68S5TDOHec/GSC0lv\n6R9o6F7h429GmgPgVKQsc8mPTPtbjJMuLLs4xKc+viCplXc0Nc0ZoHmCH4da6fCV\ntdpHQjVe6F9zjdquZ4RjV6R6JTiN9v924dGMAkbW/xXmamtz51FzondKC52Gh8Mo\n/oA0/T0KsCMCi7tb4QNQUYrf+Xcha9uus4ww1kWNZyfXJB87a2kORLiWMfs2IBBJ\nTmZ2Fnk0JnHDb8Oknxd9PvJPT0mvyT8DA+KIAPqNvOjUXP4bnjEHJcoCP9S5HkGC\nIQIDAQAB 6165ee59:MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAutQkua2CAig4VFSJ7v54\nALyu/J1WB3oni7qwCZD3veURw7HxpNAj9hR+S5N/pNeZgubQvJWyaPuQDm7PTs1+\ntFGiYNfAsiibX6Rv0wci3M+z2XEVAeR9Vzg6v4qoofDyoTbovn2LztaNEjTkB+oK\ntlvpNhg1zhou0jDVYFniEXvzjckxswHVb8cT0OMTKHALyLPrPOJzVtM9C1ew2Nnc\n3848xLiApMu3NBk0JqfcS3Bo5Y2b1FRVBvdt+2gFoKZix1MnZdAEZ8xQzL/a0YS5\nHd0wj5+EEKHfOd3A75uPa/WQmA+o0cBFfrzm69QDcSJSwGpzWrD1ScH3AK8nWvoj\nv7e9gukK/9yl1b4fQQ00vttwJPSgm9EnfPHLAtgXkRloI27H6/PuLoNvSAMQwuCD\nhQRlyGLPBETKkHeodfLoULjhDi1K2gKJTMhtbnUcAA7nEphkMhPWkBpgFdrH+5z4\nLxy+3ek0cqcI7K68EtrffU8jtUj9LFTUC8dERaIBs7NgQ/LfDbDfGh9g6qVj1hZl\nk9aaIPTm/xsi8v3u+0qaq7KzIBc9s59JOoA8TlpOaYdVgSQhHHLBaahOuAigH+VI\nisbC9vmqsThF2QdDtQt37keuqoda2E6sL7PUvIyVXDRfwX7uMDjlzTxHTymvq2Ck\nhtBqojBnThmjJQFgZXocHG8CAwEAAQ== 61666e3f:MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAlEyxkHggKCXC2Wf5Mzx4\nnZLFZvU2bgcA3exfNPO/g1YunKfQY+Jg4fr6tJUUTZ3XZUrhmLNWvpvSwDS19ZmC\nIXOu0+V94aNgnhMsk9rr59I8qcbsQGIBoHzuAl8NzZCgdbEXkiY90w1skUw8J57z\nqCsMBydAueMXuWqF5nGtYbi5vHwK42PffpiZ7G5Kjwn8nYMW5IZdL6ZnMEVJUWC9\nI4waeKg0yskczYDmZUEAtrn3laX9677ToCpiKrvmZYjlGl0BaGp3cxggP2xaDbUq\nqfFxWNgvUAb3pXD09JM6Mt6HSIJaFc9vQbrKB9KT515y763j5CC2KUsilszKi3mB\nHYe5PoebdjS7D1Oh+tRqfegU2IImzSwW3iwA7PJvefFuc/kNIijfS/gH/cAqAK6z\nbhdOtE/zc7TtqW2Wn5Y03jIZdtm12CxSxwgtCF1NPyEWyIxAQUX9ACb3M0FAZ61n\nfpPrvwTaIIxxZ01L3IzPLpbc44x/DhJIEU+iDt6IMTrHOphD9MCG4631eIdB0H1b\n6zbNX1CXTsafqHRFV9XmYYIeOMggmd90s3xIbEujA6HKNP/gwzO6CDJ+nHFDEqoF\nSkxRdTkEqjTjVKieURW7Swv7zpfu5PrsrrkyGnsRrBJJzXlm2FOOxnbI2iSL1B5F\nrO5kbUxFeZUIDq+7Yv4kLWcCAwEAAQ== 616a9724:MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAnC+bR4bHf/L6QdU4puhQ\ngl1MHePszRC38bzvVFDUJsmCaMCL2suCs2A2yxAgGb9pu9AJYLAmxQC4mM3jNqhg\n/E7yuaBbek3O02zN/ctvflJ250wZCy+z0ZGIp1ak6pu1j14IwHokl9j36zNfGtfv\nADVOcdpWITFFlPqwq1qt/H3UsKVmtiF3BNWWTeUEQwKvlU8ymxgS99yn0+4OPyNT\nL3EUeS+NQJtDS01unau0t7LnjUXn+XIneWny8bIYOQCuVR6s/gpIGuhBaUqwaJOw\n7jkJZYF2Ij7uPb4b5/R3vX2FfxxqEHqssFSg8FFUNTZz3qNZs0CRVyfA972g9WkJ\nhPfn31pQYil4QGRibCMIeU27YAEjXoqfJKEPh4UWMQsQLrEfdGfb8VgwrPbniGfU\nL3jKJR3VAafL9330iawzVQDlIlwGl6u77gEXMl9K0pfazunYhAp+BMP+9ot5ckK+\nosmrqj11qMESsAj083GeFdfV3pXEIwUytaB0AKEht9DbqUfiE/oeZ/LAXgySMtVC\nsbC4ESmgVeY2xSBIJdDyUap7FR49GGrw0W49NUv9gRgQtGGaNVQQO9oGL2PBC41P\niWF9GLoX30HIz1P8PF/cZvicSSPkQf2Z6TV+t0ebdGNS5DjapdnCrq8m9Z0pyKsQ\nuxAL2a7zX8l5i1CZh1ycUGsCAwEAAQ== 616abc23:MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA0MfCDrhODRCIxR9Dep1s\neXafh5CE5BrF4WbCgCsevyPIdvTeyIaW4vmO3bbG4VzhogDZju+R3IQYFuhoXP5v\nY+zYJGnwrgz3r5wYAvPnLEs1+dtDKYOgJXQj+wLJBW1mzRDL8FoRXOe5iRmn1EFS\nwZ1DoUvyu7/J5r0itKicZp3QKED6YoilXed+1vnS4Sk0mzN4smuMR9eO1mMCqNp9\n9KTfRDHTbakIHwasECCXCp50uXdoW6ig/xUAFanpm9LtK6jctNDbXDhQmgvAaLXZ\nLvFqoaYJ/CvWkyYCgL6qxvMvVmPoRv7OPcyni4xR/WgWa0MSaEWjgPx3+yj9fiMA\n1S02pFWFDOr5OUF/O4YhFJvUCOtVsUPPfA/Lj6faL0h5QI9mQhy5Zb9TTaS9jB6p\nLw7u0dJlrjFedk8KTJdFCcaGYHP6kNPnOxMylcB/5WcztXZVQD5WpCicGNBxCGMm\nW64SgrV7M07gQfL/32QLsdqPUf0i8hoVD8wfQ3EpbQzv6Fk1Cn90bZqZafg8XWGY\nwddhkXk7egrr23Djv37V2okjzdqoyLBYBxMz63qQzFoAVv5VoY2NDTbXYUYytOvG\nGJ1afYDRVWrExCech1mX5ZVUB1br6WM+psFLJFoBFl6mDmiYt0vMYBddKISsvwLl\nIJQkzDwtXzT2cSjoj3T5QekCAwEAAQ== 616ac3bc:MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvaaoSLab+IluixwKV5Od\n0gib2YurjPatGIbn5Ov2DLUFYiebj2oJINXJSwUOO+4WcuHFEqiL/1rya+k5hLZt\nhnPL1tn6QD4rESznvGSasRCQNT2vS/oyZbTYJRyAtFkEYLlq0t3S3xBxxHWuvIf0\nqVxVNYpQWyM3N9RIeYBR/euXKJXileSHk/uq1I5wTC0XBIHWcthczGN0m9wBEiWS\n0m3cnPk4q0Ea8mUJ91Rqob19qETz6VbSPYYpZk3qOycjKosuwcuzoMpwU8KRiMFd\n5LHtX0Hx85ghGsWDVtS0c0+aJa4lOMGvJCAOvDfqvODv7gKlCXUpgumGpLdTmaZ8\n1RwqspAe3IqBcdKTqRD4m2mSg23nVx2FAY3cjFvZQtfooT7q1ItRV5RgH6FhQSl7\n+6YIMJ1Bf8AAlLdRLpg+doOUGcEn+pkDiHFgI8ylH1LKyFKw+eXaAml/7DaWZk1d\ndqggwhXOhc/UUZFQuQQ8A8zpA13PcbC05XxN2hyP93tCEtyynMLVPtrRwDnHxFKa\nqKzs3rMDXPSXRn3ZZTdKH3069ApkEjQdpcwUh+EmJ1Ve/5cdtzT6kKWCjKBFZP/s\n91MlRrX2BTRdHaU5QJkUheUtakwxuHrdah2F94lRmsnQlpPr2YseJu6sIE+Dnx4M\nCfhdVbQL2w54R645nlnohu8CAwEAAQ== 616adfeb:MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAq0BFD1D4lIxQcsqEpQzU\npNCYM3aP1V/fxxVdT4DWvSI53JHTwHQamKdMWtEXetWVbP5zSROniYKFXd/xrD9X\n0jiGHey3lEtylXRIPxe5s+wXoCmNLcJVnvTcDtwx/ne2NLHxp76lyc25At+6RgE6\nADjLVuoD7M4IFDkAsd8UQ8zM0Dww9SylIk/wgV3ZkifecvgUQRagrNUdUjR56EBZ\nraQrev4hhzOgwelT0kXCu3snbUuNY/lU53CoTzfBJ5UfEJ5pMw1ij6X0r5S9IVsy\nKLWH1hiO0NzU2c8ViUYCly4Fe9xMTFc6u2dy/dxf6FwERfGzETQxqZvSfrRX+GLj\n/QZAXiPg5178hT/m0Y3z5IGenIC/80Z9NCi+byF1WuJlzKjDcF/TU72zk0+PNM/H\nKuppf3JT4DyjiVzNC5YoWJT2QRMS9KLP5iKCSThwVceEEg5HfhQBRT9M6KIcFLSs\nmFjx9kNEEmc1E8hl5IR3+3Ry8G5/bTIIruz14jgeY9u5jhL8Vyyvo41jgt9sLHR1\n/J1TxKfkgksYev7PoX6/ZzJ1ksWKZY5NFoDXTNYUgzFUTOoEaOg3BAQKadb3Qbbq\nXIrxmPBdgrn9QI7NCgfnAY3Tb4EEjs3ON/BNyEhUENcXOH6I1NbcuBQ7g9P73kE4\nVORdoc8MdJ5eoKBpO8Ww8HECAwEAAQ== 616ae350:MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAyduVzi1mWm+lYo2Tqt/0\nXkCIWrDNP1QBMVPrE0/ZlU2bCGSoo2Z9FHQKz/mTyMRlhNqTfhJ5qU3U9XlyGOPJ\npiM+b91g26pnpXJ2Q2kOypSgOMOPA4cQ42PkHBEqhuzssfj9t7x47ppS94bboh46\nxLSDRff/NAbtwTpvhStV3URYkxFG++cKGGa5MPXBrxIp+iZf9GnuxVdST5PGiVGP\nODL/b69sPJQNbJHVquqUTOh5Ry8uuD2WZuXfKf7/C0jC/ie9m2+0CttNu9tMciGM\nEyKG1/Xhk5iIWO43m4SrrT2WkFlcZ1z2JSf9Pjm4C2+HovYpihwwdM/OdP8Xmsnr\nDzVB4YvQiW+IHBjStHVuyiZWc+JsgEPJzisNY0Wyc/kNyNtqVKpX6dRhMLanLmy+\nf53cCSI05KPQAcGj6tdL+D60uKDkt+FsDa0BTAobZ31OsFVid0vCXtsbplNhW1IF\nHwsGXBTVcfXg44RLyL8Lk/2dQxDHNHzAUslJXzPxaHBLmt++2COa2EI1iWlvtznk\nOk9WP8SOAIj+xdqoiHcC4j72BOVVgiITIJNHrbppZCq6qPR+fgXmXa+sDcGh30m6\n9Wpbr28kLMSHiENCWTdsFij+NQTd5S47H7XTROHnalYDuF1RpS+DpQidT5tUimaT\nJZDr++FjKrnnijbyNF8b98UCAwEAAQ== 616db30d:MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAnpUpyWDWjlUk3smlWeA0\nlIMW+oJ38t92CRLHH3IqRhyECBRW0d0aRGtq7TY8PmxjjvBZrxTNDpJT6KUk4LRm\na6A6IuAI7QnNK8SJqM0DLzlpygd7GJf8ZL9SoHSH+gFsYF67Cpooz/YDqWrlN7Vw\ntO00s0B+eXy+PCXYU7VSfuWFGK8TGEv6HfGMALLjhqMManyvfp8hz3ubN1rK3c8C\nUS/ilRh1qckdbtPvoDPhSbTDmfU1g/EfRSIEXBrIMLg9ka/XB9PvWRrekrppnQzP\nhP9YE3x/wbFc5QqQWiRCYyQl/rgIMOXvIxhkfe8H5n1Et4VAorkpEAXdsfN8KSVv\nLSMazVlLp9GYq5SUpqYX3KnxdWBgN7BJoZ4sltsTpHQ/34SXWfu3UmyUveWj7wp0\nx9hwsPirVI00EEea9AbP7NM2rAyu6ukcm4m6ATd2DZJIViq2es6m60AE6SMCmrQF\nwmk4H/kdQgeAELVfGOm2VyJ3z69fQuywz7xu27S6zTKi05Qlnohxol4wVb6OB7qG\nLPRtK9ObgzRo/OPumyXqlzAi/Yvyd1ZQk8labZps3e16bQp8+pVPiumWioMFJDWV\nGZjCmyMSU8V6MB6njbgLHoyg2LCukCAeSjbPGGGYhnKLm1AKSoJh3IpZuqcKCk5C\n8CM1S15HxV78s9dFntEqIokCAwEAAQ== 66ba20fe:MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtfB12w4ZgqsXWZDfUAV/\n6Y4aHUKIu3q4SXrNZ7CXF9nXoAVYrS7NAxJdAodsY3vPCN0g5O8DFXR+390LdOuQ\n+HsGKCc1k5tX5ZXld37EZNTNSbR0k+NKhd9h6X3u6wqPOx7SIKxwAQR8qeeFq4pP\nrt9GAGlxtuYgzIIcKJPwE0dZlcBCg+GnptCUZXp/38BP1eYC+xTXSL6Muq1etYfg\nodXdb7Yl+2h1IHuOwo5rjgY5kpY7GcAs8AjGk3lDD/av60OTYccknH0NCVSmPoXK\nvrxDBOn0LQRNBLcAfnTKgHrzy0Q5h4TNkkyTgxkoQw5ObDk9nnabTxql732yy9BY\ns+hM9+dSFO1HKeVXreYSA2n1ndF18YAvAumzgyqzB7I4pMHXq1kC/8bONMJxwSkS\nYm6CoXKyavp7RqGMyeVpRC7tV+blkrrUml0BwNkxE+XnwDRB3xDV6hqgWe0XrifD\nYTfvd9ScZQP83ip0r4IKlq4GMv/R5shcCRJSkSZ6QSGshH40JYSoiwJf5FHbj9ND\n7do0UAqebWo4yNx63j/wb2ULorW3AClv0BCFSdPsIrCStiGdpgJDBR2P2NZOCob3\nG9uMj+wJD6JJg2nWqNJxkANXX37Qf8plgzssrhrgOvB0fjjS7GYhfkfmZTJ0wPOw\nA8+KzFseBh4UFGgue78KwgkCAwEAAQ== ' __Keyring= __KeyringFile="/usr/share/keyrings/ubuntu-archive-keyring.gpg" __SkipSigCheck=0 __SkipEmulation=0 __UseMirror=0 __UnprocessedBuildArgs= while :; do if [[ "$#" -le 0 ]]; then break fi lowerI="$(echo "$1" | tr "[:upper:]" "[:lower:]")" case $lowerI in -\?|-h|--help) usage ;; arm) __BuildArch=arm __UbuntuArch=armhf __AlpineArch=armv7 __QEMUArch=arm ;; arm64) __BuildArch=arm64 __UbuntuArch=arm64 __AlpineArch=aarch64 __QEMUArch=aarch64 __FreeBSDArch=arm64 __FreeBSDMachineArch=aarch64 ;; armel) __BuildArch=armel __UbuntuArch=armel __UbuntuRepo="http://archive.debian.org/debian/" __CodeName=buster __KeyringFile="/usr/share/keyrings/debian-archive-keyring.gpg" __LLDB_Package="liblldb-6.0-dev" __UbuntuPackages="${__UbuntuPackages// libomp-dev/}" __UbuntuPackages="${__UbuntuPackages// libomp5/}" __UbuntuSuites= ;; armv6) __BuildArch=armv6 __UbuntuArch=armhf __QEMUArch=arm __UbuntuRepo="http://raspbian.raspberrypi.org/raspbian/" __CodeName=buster __KeyringFile="/usr/share/keyrings/raspbian-archive-keyring.gpg" __LLDB_Package="liblldb-6.0-dev" __UbuntuSuites= if [[ -e "$__KeyringFile" ]]; then __Keyring="--keyring $__KeyringFile" fi ;; loongarch64) __BuildArch=loongarch64 __AlpineArch=loongarch64 __QEMUArch=loongarch64 __UbuntuArch=loong64 __UbuntuSuites=unreleased __LLDB_Package="liblldb-19-dev" if [[ "$__CodeName" == "sid" ]]; then __UbuntuRepo="http://ftp.ports.debian.org/debian-ports/" fi ;; riscv64) __BuildArch=riscv64 __AlpineArch=riscv64 __AlpinePackages="${__AlpinePackages// lldb-dev/}" __QEMUArch=riscv64 __UbuntuArch=riscv64 __UbuntuPackages="${__UbuntuPackages// libunwind8-dev/}" unset __LLDB_Package ;; ppc64le) __BuildArch=ppc64le __AlpineArch=ppc64le __QEMUArch=ppc64le __UbuntuArch=ppc64el __UbuntuRepo="http://ports.ubuntu.com/ubuntu-ports/" __UbuntuPackages="${__UbuntuPackages// libunwind8-dev/}" __UbuntuPackages="${__UbuntuPackages// libomp-dev/}" __UbuntuPackages="${__UbuntuPackages// libomp5/}" unset __LLDB_Package ;; s390x) __BuildArch=s390x __AlpineArch=s390x __QEMUArch=s390x __UbuntuArch=s390x __UbuntuRepo="http://ports.ubuntu.com/ubuntu-ports/" __UbuntuPackages="${__UbuntuPackages// libunwind8-dev/}" __UbuntuPackages="${__UbuntuPackages// libomp-dev/}" __UbuntuPackages="${__UbuntuPackages// libomp5/}" unset __LLDB_Package ;; x64) __BuildArch=x64 __AlpineArch=x86_64 __UbuntuArch=amd64 __FreeBSDArch=amd64 __FreeBSDMachineArch=amd64 __illumosArch=x86_64 __HaikuArch=x86_64 __UbuntuRepo="http://archive.ubuntu.com/ubuntu/" ;; x86) __BuildArch=x86 __UbuntuArch=i386 __AlpineArch=x86 __UbuntuRepo="http://archive.ubuntu.com/ubuntu/" ;; lldb*) version="$(echo "$lowerI" | tr -d '[:alpha:]-=')" majorVersion="${version%%.*}" [ -z "${version##*.*}" ] && minorVersion="${version#*.}" if [ -z "$minorVersion" ]; then minorVersion=0 fi # for versions > 6.0, lldb has dropped the minor version if [ "$majorVersion" -le 6 ]; then version="$majorVersion.$minorVersion" else version="$majorVersion" fi __LLDB_Package="liblldb-${version}-dev" ;; no-lldb) unset __LLDB_Package ;; llvm*) version="$(echo "$lowerI" | tr -d '[:alpha:]-=')" __LLVM_MajorVersion="${version%%.*}" [ -z "${version##*.*}" ] && __LLVM_MinorVersion="${version#*.}" if [ -z "$__LLVM_MinorVersion" ]; then __LLVM_MinorVersion=0 fi # for versions > 6.0, lldb has dropped the minor version if [ "$__LLVM_MajorVersion" -gt 6 ]; then __LLVM_MinorVersion= fi ;; xenial) # Ubuntu 16.04 __CodeName=xenial ;; bionic) # Ubuntu 18.04 __CodeName=bionic ;; focal) # Ubuntu 20.04 __CodeName=focal ;; jammy) # Ubuntu 22.04 __CodeName=jammy ;; noble) # Ubuntu 24.04 __CodeName=noble __LLDB_Package="liblldb-19-dev" ;; stretch) # Debian 9 __CodeName=stretch __LLDB_Package="liblldb-6.0-dev" __KeyringFile="/usr/share/keyrings/debian-archive-keyring.gpg" if [[ -z "$__UbuntuRepo" ]]; then __UbuntuRepo="http://ftp.debian.org/debian/" fi ;; buster) # Debian 10 __CodeName=buster __LLDB_Package="liblldb-6.0-dev" __KeyringFile="/usr/share/keyrings/debian-archive-keyring.gpg" if [[ -z "$__UbuntuRepo" ]]; then __UbuntuRepo="http://archive.debian.org/debian/" fi ;; bullseye) # Debian 11 __CodeName=bullseye __KeyringFile="/usr/share/keyrings/debian-archive-keyring.gpg" if [[ -z "$__UbuntuRepo" ]]; then __UbuntuRepo="http://ftp.debian.org/debian/" fi ;; bookworm) # Debian 12 __CodeName=bookworm __KeyringFile="/usr/share/keyrings/debian-archive-keyring.gpg" if [[ -z "$__UbuntuRepo" ]]; then __UbuntuRepo="http://ftp.debian.org/debian/" fi ;; sid) # Debian sid __CodeName=sid __UbuntuSuites= # Debian-Ports architectures need different values case "$__UbuntuArch" in amd64|arm64|armel|armhf|i386|mips64el|ppc64el|riscv64|s390x) __KeyringFile="/usr/share/keyrings/debian-archive-keyring.gpg" if [[ -z "$__UbuntuRepo" ]]; then __UbuntuRepo="http://ftp.debian.org/debian/" fi ;; *) __KeyringFile="/usr/share/keyrings/debian-ports-archive-keyring.gpg" if [[ -z "$__UbuntuRepo" ]]; then __UbuntuRepo="http://ftp.ports.debian.org/debian-ports/" fi ;; esac if [[ -e "$__KeyringFile" ]]; then __Keyring="--keyring $__KeyringFile" fi ;; tizen) __CodeName= __UbuntuRepo= __Tizen=tizen ;; alpine*) __CodeName=alpine __UbuntuRepo= if [[ "$lowerI" == "alpineedge" ]]; then __AlpineVersion=edge else version="$(echo "$lowerI" | tr -d '[:alpha:]-=')" __AlpineMajorVersion="${version%%.*}" __AlpineMinorVersion="${version#*.}" __AlpineVersion="$__AlpineMajorVersion.$__AlpineMinorVersion" fi ;; freebsd13) __CodeName=freebsd __SkipUnmount=1 ;; freebsd14) __CodeName=freebsd __FreeBSDBase="14.3-RELEASE" __FreeBSDABI="14" __SkipUnmount=1 ;; illumos) __CodeName=illumos __SkipUnmount=1 ;; haiku) __CodeName=haiku __SkipUnmount=1 ;; --skipunmount) __SkipUnmount=1 ;; --skipsigcheck) __SkipSigCheck=1 ;; --skipemulation) __SkipEmulation=1 ;; --rootfsdir|-rootfsdir) shift __RootfsDir="$1" ;; --use-mirror) __UseMirror=1 ;; --use-jobs) shift MAXJOBS=$1 ;; *) __UnprocessedBuildArgs="$__UnprocessedBuildArgs $1" ;; esac shift done case "$__AlpineVersion" in 3.14) __AlpinePackages+=" llvm11-libs" ;; 3.15) __AlpinePackages+=" llvm12-libs" ;; 3.16) __AlpinePackages+=" llvm13-libs" ;; 3.17) __AlpinePackages+=" llvm15-libs" ;; edge) __AlpineLlvmLibsLookup=1 ;; *) if [[ "$__AlpineArch" =~ s390x|ppc64le ]]; then __AlpineVersion=3.15 # minimum version that supports lldb-dev __AlpinePackages+=" llvm12-libs" elif [[ "$__AlpineArch" == "x86" ]]; then __AlpineVersion=3.17 # minimum version that supports lldb-dev __AlpinePackages+=" llvm15-libs" elif [[ "$__AlpineArch" == "riscv64" || "$__AlpineArch" == "loongarch64" ]]; then __AlpineVersion=3.21 # minimum version that supports lldb-dev __AlpinePackages+=" llvm19-libs" elif [[ -n "$__AlpineMajorVersion" ]]; then # use whichever alpine version is provided and select the latest toolchain libs __AlpineLlvmLibsLookup=1 else __AlpineVersion=3.13 # 3.13 to maximize compatibility __AlpinePackages+=" llvm10-libs" fi esac if [[ "$__AlpineVersion" =~ 3\.1[345] ]]; then # compiler-rt--static was merged in compiler-rt package in alpine 3.16 # for older versions, we need compiler-rt--static, so replace the name __AlpinePackages="${__AlpinePackages/compiler-rt/compiler-rt-static}" fi __UbuntuPackages+=" ${__LLDB_Package:-}" if [[ -z "$__UbuntuRepo" ]]; then __UbuntuRepo="http://ports.ubuntu.com/" fi if [[ -n "$__LLVM_MajorVersion" ]]; then __UbuntuPackages+=" libclang-common-${__LLVM_MajorVersion}${__LLVM_MinorVersion:+.$__LLVM_MinorVersion}-dev" fi if [[ -z "$__RootfsDir" && -n "$ROOTFS_DIR" ]]; then __RootfsDir="$ROOTFS_DIR" fi if [[ -z "$__RootfsDir" ]]; then __RootfsDir="$__CrossDir/../../../.tools/rootfs/$__BuildArch" fi if [[ -d "$__RootfsDir" ]]; then if [[ "$__SkipUnmount" == "0" ]]; then umount "$__RootfsDir"/* || true fi rm -rf "$__RootfsDir" fi mkdir -p "$__RootfsDir" __RootfsDir="$( cd "$__RootfsDir" && pwd )" __hasWget= ensureDownloadTool() { if command -v wget &> /dev/null; then __hasWget=1 elif command -v curl &> /dev/null; then __hasWget=0 else >&2 echo "ERROR: either wget or curl is required by this script." exit 1 fi } if [[ "$__CodeName" == "alpine" ]]; then __ApkToolsVersion=2.12.11 __ApkToolsDir="$(mktemp -d)" __ApkKeysDir="$(mktemp -d)" arch="$(uname -m)" ensureDownloadTool if [[ "$__hasWget" == 1 ]]; then wget -P "$__ApkToolsDir" "https://gitlab.alpinelinux.org/api/v4/projects/5/packages/generic/v$__ApkToolsVersion/$arch/apk.static" else curl -SLO --create-dirs --output-dir "$__ApkToolsDir" "https://gitlab.alpinelinux.org/api/v4/projects/5/packages/generic/v$__ApkToolsVersion/$arch/apk.static" fi if [[ "$arch" == "x86_64" ]]; then __ApkToolsSHA512SUM="53e57b49230da07ef44ee0765b9592580308c407a8d4da7125550957bb72cb59638e04f8892a18b584451c8d841d1c7cb0f0ab680cc323a3015776affaa3be33" elif [[ "$arch" == "aarch64" ]]; then __ApkToolsSHA512SUM="9e2b37ecb2b56c05dad23d379be84fd494c14bd730b620d0d576bda760588e1f2f59a7fcb2f2080577e0085f23a0ca8eadd993b4e61c2ab29549fdb71969afd0" else echo "WARNING: add missing hash for your host architecture. To find the value, use: 'find /tmp -name apk.static -exec sha512sum {} \;'" fi echo "$__ApkToolsSHA512SUM $__ApkToolsDir/apk.static" | sha512sum -c chmod +x "$__ApkToolsDir/apk.static" if [[ "$__AlpineVersion" == "edge" ]]; then version=edge else version="v$__AlpineVersion" fi for line in $__AlpineKeys; do id="${line%%:*}" content="${line#*:}" echo -e "-----BEGIN PUBLIC KEY-----\n$content\n-----END PUBLIC KEY-----" > "$__ApkKeysDir/alpine-devel@lists.alpinelinux.org-$id.rsa.pub" done if [[ "$__SkipSigCheck" == "1" ]]; then __ApkSignatureArg="--allow-untrusted" else __ApkSignatureArg="--keys-dir $__ApkKeysDir" fi if [[ "$__SkipEmulation" == "1" ]]; then __NoEmulationArg="--no-scripts" fi # initialize DB # shellcheck disable=SC2086 "$__ApkToolsDir/apk.static" \ -X "http://dl-cdn.alpinelinux.org/alpine/$version/main" \ -X "http://dl-cdn.alpinelinux.org/alpine/$version/community" \ -U $__ApkSignatureArg --root "$__RootfsDir" --arch "$__AlpineArch" --initdb add if [[ "$__AlpineLlvmLibsLookup" == 1 ]]; then # shellcheck disable=SC2086 __AlpinePackages+=" $("$__ApkToolsDir/apk.static" \ -X "http://dl-cdn.alpinelinux.org/alpine/$version/main" \ -X "http://dl-cdn.alpinelinux.org/alpine/$version/community" \ -U $__ApkSignatureArg --root "$__RootfsDir" --arch "$__AlpineArch" \ search 'llvm*-libs' | grep -E '^llvm' | sort | tail -1 | sed 's/-[^-]*//2g')" fi # install all packages in one go # shellcheck disable=SC2086 "$__ApkToolsDir/apk.static" \ -X "http://dl-cdn.alpinelinux.org/alpine/$version/main" \ -X "http://dl-cdn.alpinelinux.org/alpine/$version/community" \ -U $__ApkSignatureArg --root "$__RootfsDir" --arch "$__AlpineArch" $__NoEmulationArg \ add $__AlpinePackages rm -r "$__ApkToolsDir" elif [[ "$__CodeName" == "freebsd" ]]; then mkdir -p "$__RootfsDir"/usr/local/etc JOBS=${MAXJOBS:="$(getconf _NPROCESSORS_ONLN)"} ensureDownloadTool if [[ "$__hasWget" == 1 ]]; then wget -O- "https://download.freebsd.org/ftp/releases/${__FreeBSDArch}/${__FreeBSDMachineArch}/${__FreeBSDBase}/base.txz" | tar -C "$__RootfsDir" -Jxf - ./lib ./usr/lib ./usr/libdata ./usr/include ./usr/share/keys ./etc ./bin/freebsd-version else curl -SL "https://download.freebsd.org/ftp/releases/${__FreeBSDArch}/${__FreeBSDMachineArch}/${__FreeBSDBase}/base.txz" | tar -C "$__RootfsDir" -Jxf - ./lib ./usr/lib ./usr/libdata ./usr/include ./usr/share/keys ./etc ./bin/freebsd-version fi echo "ABI = \"FreeBSD:${__FreeBSDABI}:${__FreeBSDMachineArch}\"; FINGERPRINTS = \"${__RootfsDir}/usr/share/keys\"; REPOS_DIR = [\"${__RootfsDir}/etc/pkg\"]; REPO_AUTOUPDATE = NO; RUN_SCRIPTS = NO;" > "${__RootfsDir}"/usr/local/etc/pkg.conf echo "FreeBSD: { url: \"pkg+http://pkg.FreeBSD.org/\${ABI}/quarterly\", mirror_type: \"srv\", signature_type: \"fingerprints\", fingerprints: \"/usr/share/keys/pkg\", enabled: yes }" > "${__RootfsDir}"/etc/pkg/FreeBSD.conf mkdir -p "$__RootfsDir"/tmp # get and build package manager if [[ "$__hasWget" == 1 ]]; then wget -O- "https://github.com/freebsd/pkg/archive/${__FreeBSDPkg}.tar.gz" | tar -C "$__RootfsDir"/tmp -zxf - else curl -SL "https://github.com/freebsd/pkg/archive/${__FreeBSDPkg}.tar.gz" | tar -C "$__RootfsDir"/tmp -zxf - fi cd "$__RootfsDir/tmp/pkg-${__FreeBSDPkg}" # needed for install to succeed mkdir -p "$__RootfsDir"/host/etc ./autogen.sh && ./configure --prefix="$__RootfsDir"/host && make -j "$JOBS" && make install rm -rf "$__RootfsDir/tmp/pkg-${__FreeBSDPkg}" # install packages we need. INSTALL_AS_USER=$(whoami) "$__RootfsDir"/host/sbin/pkg -r "$__RootfsDir" -C "$__RootfsDir"/usr/local/etc/pkg.conf update # shellcheck disable=SC2086 INSTALL_AS_USER=$(whoami) "$__RootfsDir"/host/sbin/pkg -r "$__RootfsDir" -C "$__RootfsDir"/usr/local/etc/pkg.conf install --yes $__FreeBSDPackages elif [[ "$__CodeName" == "illumos" ]]; then mkdir "$__RootfsDir/tmp" pushd "$__RootfsDir/tmp" JOBS=${MAXJOBS:="$(getconf _NPROCESSORS_ONLN)"} ensureDownloadTool echo "Downloading sysroot." if [[ "$__hasWget" == 1 ]]; then wget -O- https://github.com/illumos/sysroot/releases/download/20181213-de6af22ae73b-v1/illumos-sysroot-i386-20181213-de6af22ae73b-v1.tar.gz | tar -C "$__RootfsDir" -xzf - else curl -SL https://github.com/illumos/sysroot/releases/download/20181213-de6af22ae73b-v1/illumos-sysroot-i386-20181213-de6af22ae73b-v1.tar.gz | tar -C "$__RootfsDir" -xzf - fi echo "Building binutils. Please wait.." if [[ "$__hasWget" == 1 ]]; then wget -O- https://ftp.gnu.org/gnu/binutils/binutils-2.42.tar.xz | tar -xJf - else curl -SL https://ftp.gnu.org/gnu/binutils/binutils-2.42.tar.xz | tar -xJf - fi mkdir build-binutils && cd build-binutils ../binutils-2.42/configure --prefix="$__RootfsDir" --target="${__illumosArch}-sun-solaris2.11" --program-prefix="${__illumosArch}-illumos-" --with-sysroot="$__RootfsDir" make -j "$JOBS" && make install && cd .. echo "Building gcc. Please wait.." if [[ "$__hasWget" == 1 ]]; then wget -O- https://ftp.gnu.org/gnu/gcc/gcc-13.3.0/gcc-13.3.0.tar.xz | tar -xJf - else curl -SL https://ftp.gnu.org/gnu/gcc/gcc-13.3.0/gcc-13.3.0.tar.xz | tar -xJf - fi CFLAGS="-fPIC" CXXFLAGS="-fPIC" CXXFLAGS_FOR_TARGET="-fPIC" CFLAGS_FOR_TARGET="-fPIC" export CFLAGS CXXFLAGS CXXFLAGS_FOR_TARGET CFLAGS_FOR_TARGET mkdir build-gcc && cd build-gcc ../gcc-13.3.0/configure --prefix="$__RootfsDir" --target="${__illumosArch}-sun-solaris2.11" --program-prefix="${__illumosArch}-illumos-" --with-sysroot="$__RootfsDir" --with-gnu-as \ --with-gnu-ld --disable-nls --disable-libgomp --disable-libquadmath --disable-libssp --disable-libvtv --disable-libcilkrts --disable-libada --disable-libsanitizer \ --disable-libquadmath-support --disable-shared --enable-tls make -j "$JOBS" && make install && cd .. BaseUrl=https://pkgsrc.smartos.org if [[ "$__UseMirror" == 1 ]]; then BaseUrl=https://pkgsrc.smartos.skylime.net fi BaseUrl="$BaseUrl/packages/SmartOS/2019Q4/${__illumosArch}/All" echo "Downloading manifest" if [[ "$__hasWget" == 1 ]]; then wget "$BaseUrl" else curl -SLO "$BaseUrl" fi echo "Downloading dependencies." read -ra array <<<"$__IllumosPackages" for package in "${array[@]}"; do echo "Installing '$package'" # find last occurrence of package in listing and extract its name package="$(sed -En '/.*href="('"$package"'-[0-9].*).tgz".*/h;$!d;g;s//\1/p' All)" echo "Resolved name '$package'" if [[ "$__hasWget" == 1 ]]; then wget "$BaseUrl"/"$package".tgz else curl -SLO "$BaseUrl"/"$package".tgz fi ar -x "$package".tgz tar --skip-old-files -xzf "$package".tmp.tg* -C "$__RootfsDir" 2>/dev/null done echo "Cleaning up temporary files." popd rm -rf "$__RootfsDir"/{tmp,+*} mkdir -p "$__RootfsDir"/usr/include/net mkdir -p "$__RootfsDir"/usr/include/netpacket if [[ "$__hasWget" == 1 ]]; then wget -P "$__RootfsDir"/usr/include/net https://raw.githubusercontent.com/illumos/illumos-gate/master/usr/src/uts/common/io/bpf/net/bpf.h wget -P "$__RootfsDir"/usr/include/net https://raw.githubusercontent.com/illumos/illumos-gate/master/usr/src/uts/common/io/bpf/net/dlt.h wget -P "$__RootfsDir"/usr/include/netpacket https://raw.githubusercontent.com/illumos/illumos-gate/master/usr/src/uts/common/inet/sockmods/netpacket/packet.h wget -P "$__RootfsDir"/usr/include/sys https://raw.githubusercontent.com/illumos/illumos-gate/master/usr/src/uts/common/sys/sdt.h else curl -SLO --create-dirs --output-dir "$__RootfsDir"/usr/include/net https://raw.githubusercontent.com/illumos/illumos-gate/master/usr/src/uts/common/io/bpf/net/bpf.h curl -SLO --create-dirs --output-dir "$__RootfsDir"/usr/include/net https://raw.githubusercontent.com/illumos/illumos-gate/master/usr/src/uts/common/io/bpf/net/dlt.h curl -SLO --create-dirs --output-dir "$__RootfsDir"/usr/include/netpacket https://raw.githubusercontent.com/illumos/illumos-gate/master/usr/src/uts/common/inet/sockmods/netpacket/packet.h curl -SLO --create-dirs --output-dir "$__RootfsDir"/usr/include/sys https://raw.githubusercontent.com/illumos/illumos-gate/master/usr/src/uts/common/sys/sdt.h fi elif [[ "$__CodeName" == "haiku" ]]; then JOBS=${MAXJOBS:="$(getconf _NPROCESSORS_ONLN)"} echo "Building Haiku sysroot for $__HaikuArch" mkdir -p "$__RootfsDir/tmp" pushd "$__RootfsDir/tmp" mkdir "$__RootfsDir/tmp/download" ensureDownloadTool echo "Downloading Haiku package tools" git clone https://github.com/haiku/haiku-toolchains-ubuntu --depth 1 "$__RootfsDir/tmp/script" if [[ "$__hasWget" == 1 ]]; then wget -O "$__RootfsDir/tmp/download/hosttools.zip" "$("$__RootfsDir/tmp/script/fetch.sh" --hosttools)" else curl -SLo "$__RootfsDir/tmp/download/hosttools.zip" "$("$__RootfsDir/tmp/script/fetch.sh" --hosttools)" fi unzip -o "$__RootfsDir/tmp/download/hosttools.zip" -d "$__RootfsDir/tmp/bin" HaikuBaseUrl="https://eu.hpkg.haiku-os.org/haiku/master/$__HaikuArch/current" HaikuPortsBaseUrl="https://eu.hpkg.haiku-os.org/haikuports/master/$__HaikuArch/current" echo "Downloading HaikuPorts package repository index..." if [[ "$__hasWget" == 1 ]]; then wget -P "$__RootfsDir/tmp/download" "$HaikuPortsBaseUrl/repo" else curl -SLO --create-dirs --output-dir "$__RootfsDir/tmp/download" "$HaikuPortsBaseUrl/repo" fi echo "Downloading Haiku packages" read -ra array <<<"$__HaikuPackages" for package in "${array[@]}"; do echo "Downloading $package..." hpkgFilename="$(LD_LIBRARY_PATH="$__RootfsDir/tmp/bin" "$__RootfsDir/tmp/bin/package_repo" list -f "$__RootfsDir/tmp/download/repo" | grep -E "${package}-" | sort -V | tail -n 1 | xargs)" if [ -z "$hpkgFilename" ]; then >&2 echo "ERROR: package $package missing." exit 1 fi echo "Resolved filename: $hpkgFilename..." hpkgDownloadUrl="$HaikuPortsBaseUrl/packages/$hpkgFilename" if [[ "$__hasWget" == 1 ]]; then wget -P "$__RootfsDir/tmp/download" "$hpkgDownloadUrl" else curl -SLO --create-dirs --output-dir "$__RootfsDir/tmp/download" "$hpkgDownloadUrl" fi done for package in haiku haiku_devel; do echo "Downloading $package..." if [[ "$__hasWget" == 1 ]]; then hpkgVersion="$(wget -qO- "$HaikuBaseUrl" | sed -n 's/^.*version: "\([^"]*\)".*$/\1/p')" wget -P "$__RootfsDir/tmp/download" "$HaikuBaseUrl/packages/$package-$hpkgVersion-1-$__HaikuArch.hpkg" else hpkgVersion="$(curl -sSL "$HaikuBaseUrl" | sed -n 's/^.*version: "\([^"]*\)".*$/\1/p')" curl -SLO --create-dirs --output-dir "$__RootfsDir/tmp/download" "$HaikuBaseUrl/packages/$package-$hpkgVersion-1-$__HaikuArch.hpkg" fi done # Set up the sysroot echo "Setting up sysroot and extracting required packages" mkdir -p "$__RootfsDir/boot/system" for file in "$__RootfsDir/tmp/download/"*.hpkg; do echo "Extracting $file..." LD_LIBRARY_PATH="$__RootfsDir/tmp/bin" "$__RootfsDir/tmp/bin/package" extract -C "$__RootfsDir/boot/system" "$file" done # Download buildtools echo "Downloading Haiku buildtools" if [[ "$__hasWget" == 1 ]]; then wget -O "$__RootfsDir/tmp/download/buildtools.zip" "$("$__RootfsDir/tmp/script/fetch.sh" --buildtools --arch=$__HaikuArch)" else curl -SLo "$__RootfsDir/tmp/download/buildtools.zip" "$("$__RootfsDir/tmp/script/fetch.sh" --buildtools --arch=$__HaikuArch)" fi unzip -o "$__RootfsDir/tmp/download/buildtools.zip" -d "$__RootfsDir" # Cleaning up temporary files echo "Cleaning up temporary files" popd rm -rf "$__RootfsDir/tmp" elif [[ -n "$__CodeName" ]]; then __Suites="$__CodeName $(for suite in $__UbuntuSuites; do echo -n "$__CodeName-$suite "; done)" if [[ "$__SkipEmulation" == "1" ]]; then if [[ -z "$AR" ]]; then if command -v ar &>/dev/null; then AR="$(command -v ar)" elif command -v llvm-ar &>/dev/null; then AR="$(command -v llvm-ar)" else echo "Unable to find ar or llvm-ar on PATH, add them to PATH or set AR environment variable pointing to the available AR tool" exit 1 fi fi PYTHON=${PYTHON_EXECUTABLE:-python3} # shellcheck disable=SC2086,SC2046 echo running "$PYTHON" "$__CrossDir/install-debs.py" --arch "$__UbuntuArch" --mirror "$__UbuntuRepo" --rootfsdir "$__RootfsDir" --artool "$AR" \ $(for suite in $__Suites; do echo -n "--suite $suite "; done) \ $__UbuntuPackages # shellcheck disable=SC2086,SC2046 "$PYTHON" "$__CrossDir/install-debs.py" --arch "$__UbuntuArch" --mirror "$__UbuntuRepo" --rootfsdir "$__RootfsDir" --artool "$AR" \ $(for suite in $__Suites; do echo -n "--suite $suite "; done) \ $__UbuntuPackages exit 0 fi __UpdateOptions= if [[ "$__SkipSigCheck" == "0" ]]; then __Keyring="$__Keyring --force-check-gpg" else __Keyring= __UpdateOptions="--allow-unauthenticated --allow-insecure-repositories" fi # shellcheck disable=SC2086 echo running debootstrap "--variant=minbase" $__Keyring --arch "$__UbuntuArch" "$__CodeName" "$__RootfsDir" "$__UbuntuRepo" # shellcheck disable=SC2086 if ! debootstrap "--variant=minbase" $__Keyring --arch "$__UbuntuArch" "$__CodeName" "$__RootfsDir" "$__UbuntuRepo"; then echo "debootstrap failed! dumping debootstrap.log" cat "$__RootfsDir/debootstrap/debootstrap.log" exit 1 fi rm -rf "$__RootfsDir"/etc/apt/*.{sources,list} "$__RootfsDir"/etc/apt/sources.list.d mkdir -p "$__RootfsDir/etc/apt/sources.list.d/" # shellcheck disable=SC2086 cat > "$__RootfsDir/etc/apt/sources.list.d/$__CodeName.sources" < token2) - (token1 < token2) else: return -1 if isinstance(token1, str) else 1 return len(tokens1) - len(tokens2) def compare_debian_versions(version1, version2): """Compare two Debian package versions.""" epoch1, upstream1, revision1 = parse_debian_version(version1) epoch2, upstream2, revision2 = parse_debian_version(version2) if epoch1 != epoch2: return epoch1 - epoch2 result = compare_upstream_version(upstream1, upstream2) if result != 0: return result return compare_upstream_version(revision1, revision2) def resolve_dependencies(packages, aliases, desired_packages): """Recursively resolves dependencies for the desired packages.""" resolved = [] to_process = deque(desired_packages) while to_process: current = to_process.popleft() resolved_package = current if current in packages else aliases.get(current, [None])[0] if not resolved_package: print(f"Error: Package '{current}' was not found in the available packages.") sys.exit(1) if resolved_package not in resolved: resolved.append(resolved_package) deps = packages.get(resolved_package, {}).get("Depends", "") if deps: deps = [dep.split(' ')[0] for dep in deps.split(', ') if dep] for dep in deps: if dep not in resolved and dep not in to_process and dep in packages: to_process.append(dep) return resolved def parse_package_index(content): """Parses the Packages.gz file and returns package information.""" packages = {} aliases = {} entries = re.split(r'\n\n+', content) for entry in entries: fields = dict(re.findall(r'^(\S+): (.+)$', entry, re.MULTILINE)) if "Package" in fields: package_name = fields["Package"] version = fields.get("Version") filename = fields.get("Filename") depends = fields.get("Depends") provides = fields.get("Provides", None) # Only update if package_name is not in packages or if the new version is higher if package_name not in packages or compare_debian_versions(version, packages[package_name]["Version"]) > 0: packages[package_name] = { "Version": version, "Filename": filename, "Depends": depends } # Update aliases if package provides any alternatives if provides: provides_list = [x.strip() for x in provides.split(",")] for alias in provides_list: # Strip version specifiers alias_name = re.sub(r'\s*\(=.*\)', '', alias) if alias_name not in aliases: aliases[alias_name] = [] if package_name not in aliases[alias_name]: aliases[alias_name].append(package_name) return packages, aliases def install_packages(mirror, packages_info, aliases, tmp_dir, extract_dir, ar_tool, desired_packages): """Downloads .deb files and extracts them.""" resolved_packages = resolve_dependencies(packages_info, aliases, desired_packages) print(f"Resolved packages (including dependencies): {resolved_packages}") packages_to_download = {} for pkg in resolved_packages: if pkg in packages_info: packages_to_download[pkg] = packages_info[pkg] if pkg in aliases: for alias in aliases[pkg]: if alias in packages_info: packages_to_download[alias] = packages_info[alias] asyncio.run(download_deb_files_parallel(mirror, packages_to_download, tmp_dir)) package_to_deb_file_map = {} for pkg in resolved_packages: pkg_info = packages_info.get(pkg) if pkg_info: deb_filename = pkg_info.get("Filename") if deb_filename: deb_file_path = os.path.join(tmp_dir, os.path.basename(deb_filename)) package_to_deb_file_map[pkg] = deb_file_path for pkg in reversed(resolved_packages): deb_file = package_to_deb_file_map.get(pkg) if deb_file and os.path.exists(deb_file): extract_deb_file(deb_file, tmp_dir, extract_dir, ar_tool) print("All done!") def extract_deb_file(deb_file, tmp_dir, extract_dir, ar_tool): """Extract .deb file contents""" os.makedirs(extract_dir, exist_ok=True) with tempfile.TemporaryDirectory(dir=tmp_dir) as tmp_subdir: result = subprocess.run(f"{ar_tool} t {os.path.abspath(deb_file)}", cwd=tmp_subdir, check=True, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) tar_filename = None for line in result.stdout.decode().splitlines(): if line.startswith("data.tar"): tar_filename = line.strip() break if not tar_filename: raise FileNotFoundError(f"Could not find 'data.tar.*' in {deb_file}.") tar_file_path = os.path.join(tmp_subdir, tar_filename) print(f"Extracting {tar_filename} from {deb_file}..") subprocess.run(f"{ar_tool} p {os.path.abspath(deb_file)} {tar_filename} > {tar_file_path}", check=True, shell=True) file_extension = os.path.splitext(tar_file_path)[1].lower() if file_extension == ".xz": mode = "r:xz" elif file_extension == ".gz": mode = "r:gz" elif file_extension == ".zst": # zstd is not supported by standard library yet decompressed_tar_path = tar_file_path.replace(".zst", "") with open(tar_file_path, "rb") as zst_file, open(decompressed_tar_path, "wb") as decompressed_file: dctx = zstandard.ZstdDecompressor() dctx.copy_stream(zst_file, decompressed_file) tar_file_path = decompressed_tar_path mode = "r" else: raise ValueError(f"Unsupported compression format: {file_extension}") with tarfile.open(tar_file_path, mode) as tar: tar.extractall(path=extract_dir, filter='fully_trusted') def finalize_setup(rootfsdir): lib_dir = os.path.join(rootfsdir, 'lib') usr_lib_dir = os.path.join(rootfsdir, 'usr', 'lib') if os.path.exists(lib_dir): if os.path.islink(lib_dir): os.remove(lib_dir) else: os.makedirs(usr_lib_dir, exist_ok=True) for item in os.listdir(lib_dir): src = os.path.join(lib_dir, item) dest = os.path.join(usr_lib_dir, item) if os.path.isdir(src): shutil.copytree(src, dest, dirs_exist_ok=True) else: shutil.copy2(src, dest) shutil.rmtree(lib_dir) os.symlink(usr_lib_dir, lib_dir) if __name__ == "__main__": parser = argparse.ArgumentParser(description="Generate rootfs for .NET runtime on Debian-like OS") parser.add_argument("--distro", required=False, help="Distro name (e.g., debian, ubuntu, etc.)") parser.add_argument("--arch", required=True, help="Architecture (e.g., amd64, loong64, etc.)") parser.add_argument("--rootfsdir", required=True, help="Destination directory.") parser.add_argument('--suite', required=True, action='append', help='Specify one or more repository suites to collect index data.') parser.add_argument("--mirror", required=False, help="Mirror (e.g., http://ftp.debian.org/debian-ports etc.)") parser.add_argument("--artool", required=False, default="ar", help="ar tool to extract debs (e.g., ar, llvm-ar etc.)") parser.add_argument("packages", nargs="+", help="List of package names to be installed.") args = parser.parse_args() if args.mirror is None: if args.distro == "ubuntu": args.mirror = "http://archive.ubuntu.com/ubuntu" if args.arch in ["amd64", "i386"] else "http://ports.ubuntu.com/ubuntu-ports" elif args.distro == "debian": args.mirror = "http://ftp.debian.org/debian-ports" else: raise Exception("Unsupported distro") DESIRED_PACKAGES = args.packages + [ # base packages "dpkg", "busybox", "libc-bin", "base-files", "base-passwd", "debianutils" ] print(f"Creating rootfs. rootfsdir: {args.rootfsdir}, distro: {args.distro}, arch: {args.arch}, suites: {args.suite}, mirror: {args.mirror}") package_index_content = asyncio.run(download_package_index_parallel(args.mirror, args.arch, args.suite)) packages_info, aliases = parse_package_index(package_index_content) with tempfile.TemporaryDirectory() as tmp_dir: install_packages(args.mirror, packages_info, aliases, tmp_dir, args.rootfsdir, args.artool, DESIRED_PACKAGES) finalize_setup(args.rootfsdir) ================================================ FILE: eng/common/cross/riscv64/tizen/tizen.patch ================================================ diff -u -r a/usr/lib/libc.so b/usr/lib/libc.so --- a/usr/lib64/libc.so 2016-12-30 23:00:08.284951863 +0900 +++ b/usr/lib64/libc.so 2016-12-30 23:00:32.140951815 +0900 @@ -2,4 +2,4 @@ Use the shared library, but some functions are only in the static library, so try that secondarily. */ OUTPUT_FORMAT(elf64-littleriscv) -GROUP ( /lib64/libc.so.6 /usr/lib64/libc_nonshared.a AS_NEEDED ( /lib64/ld-linux-riscv64-lp64d.so.1 ) ) +GROUP ( libc.so.6 libc_nonshared.a AS_NEEDED ( ld-linux-riscv64-lp64d.so.1 ) ) ================================================ FILE: eng/common/cross/tizen-build-rootfs.sh ================================================ #!/usr/bin/env bash set -e ARCH=$1 LINK_ARCH=$ARCH case "$ARCH" in arm) TIZEN_ARCH="armv7hl" ;; armel) TIZEN_ARCH="armv7l" LINK_ARCH="arm" ;; arm64) TIZEN_ARCH="aarch64" ;; x86) TIZEN_ARCH="i686" ;; x64) TIZEN_ARCH="x86_64" LINK_ARCH="x86" ;; riscv64) TIZEN_ARCH="riscv64" LINK_ARCH="riscv" ;; *) echo "Unsupported architecture for tizen: $ARCH" exit 1 esac __CrossDir=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) __TIZEN_CROSSDIR="$__CrossDir/${ARCH}/tizen" if [[ -z "$ROOTFS_DIR" ]]; then echo "ROOTFS_DIR is not defined." exit 1; fi TIZEN_TMP_DIR=$ROOTFS_DIR/tizen_tmp mkdir -p $TIZEN_TMP_DIR # Download files echo ">>Start downloading files" VERBOSE=1 $__CrossDir/tizen-fetch.sh $TIZEN_TMP_DIR $TIZEN_ARCH echo "<>Start constructing Tizen rootfs" TIZEN_RPM_FILES=`ls $TIZEN_TMP_DIR/*.rpm` cd $ROOTFS_DIR for f in $TIZEN_RPM_FILES; do rpm2cpio $f | cpio -idm --quiet done echo "<>Start configuring Tizen rootfs" ln -sfn asm-${LINK_ARCH} ./usr/include/asm patch -p1 < $__TIZEN_CROSSDIR/tizen.patch if [[ "$TIZEN_ARCH" == "riscv64" ]]; then echo "Fixing broken symlinks in $PWD" rm ./usr/lib64/libresolv.so ln -s ../../lib64/libresolv.so.2 ./usr/lib64/libresolv.so rm ./usr/lib64/libpthread.so ln -s ../../lib64/libpthread.so.0 ./usr/lib64/libpthread.so rm ./usr/lib64/libdl.so ln -s ../../lib64/libdl.so.2 ./usr/lib64/libdl.so rm ./usr/lib64/libutil.so ln -s ../../lib64/libutil.so.1 ./usr/lib64/libutil.so rm ./usr/lib64/libm.so ln -s ../../lib64/libm.so.6 ./usr/lib64/libm.so rm ./usr/lib64/librt.so ln -s ../../lib64/librt.so.1 ./usr/lib64/librt.so rm ./lib/ld-linux-riscv64-lp64d.so.1 ln -s ../lib64/ld-linux-riscv64-lp64d.so.1 ./lib/ld-linux-riscv64-lp64d.so.1 fi echo "</dev/null; then VERBOSE=0 fi Log() { if [ $VERBOSE -ge 1 ]; then echo ${@:2} fi } Inform() { Log 1 -e "\x1B[0;34m$@\x1B[m" } Debug() { Log 2 -e "\x1B[0;32m$@\x1B[m" } Error() { >&2 Log 0 -e "\x1B[0;31m$@\x1B[m" } Fetch() { URL=$1 FILE=$2 PROGRESS=$3 if [ $VERBOSE -ge 1 ] && [ $PROGRESS ]; then CURL_OPT="--progress-bar" else CURL_OPT="--silent" fi curl $CURL_OPT $URL > $FILE } hash curl 2> /dev/null || { Error "Require 'curl' Aborting."; exit 1; } hash xmllint 2> /dev/null || { Error "Require 'xmllint' Aborting."; exit 1; } hash sha256sum 2> /dev/null || { Error "Require 'sha256sum' Aborting."; exit 1; } TMPDIR=$1 if [ ! -d $TMPDIR ]; then TMPDIR=./tizen_tmp Debug "Create temporary directory : $TMPDIR" mkdir -p $TMPDIR fi TIZEN_ARCH=$2 TIZEN_URL=http://download.tizen.org/snapshots/TIZEN/Tizen BUILD_XML=build.xml REPOMD_XML=repomd.xml PRIMARY_XML=primary.xml TARGET_URL="http://__not_initialized" Xpath_get() { XPATH_RESULT='' XPATH=$1 XML_FILE=$2 RESULT=$(xmllint --xpath $XPATH $XML_FILE) if [[ -z ${RESULT// } ]]; then Error "Can not find target from $XML_FILE" Debug "Xpath = $XPATH" exit 1 fi XPATH_RESULT=$RESULT } fetch_tizen_pkgs_init() { TARGET=$1 PROFILE=$2 Debug "Initialize TARGET=$TARGET, PROFILE=$PROFILE" TMP_PKG_DIR=$TMPDIR/tizen_${PROFILE}_pkgs if [ -d $TMP_PKG_DIR ]; then rm -rf $TMP_PKG_DIR; fi mkdir -p $TMP_PKG_DIR PKG_URL=$TIZEN_URL/$PROFILE/latest BUILD_XML_URL=$PKG_URL/$BUILD_XML TMP_BUILD=$TMP_PKG_DIR/$BUILD_XML TMP_REPOMD=$TMP_PKG_DIR/$REPOMD_XML TMP_PRIMARY=$TMP_PKG_DIR/$PRIMARY_XML TMP_PRIMARYGZ=${TMP_PRIMARY}.gz Fetch $BUILD_XML_URL $TMP_BUILD Debug "fetch $BUILD_XML_URL to $TMP_BUILD" TARGET_XPATH="//build/buildtargets/buildtarget[@name=\"$TARGET\"]/repo[@type=\"binary\"]/text()" Xpath_get $TARGET_XPATH $TMP_BUILD TARGET_PATH=$XPATH_RESULT TARGET_URL=$PKG_URL/$TARGET_PATH REPOMD_URL=$TARGET_URL/repodata/repomd.xml PRIMARY_XPATH='string(//*[local-name()="data"][@type="primary"]/*[local-name()="location"]/@href)' Fetch $REPOMD_URL $TMP_REPOMD Debug "fetch $REPOMD_URL to $TMP_REPOMD" Xpath_get $PRIMARY_XPATH $TMP_REPOMD PRIMARY_XML_PATH=$XPATH_RESULT PRIMARY_URL=$TARGET_URL/$PRIMARY_XML_PATH Fetch $PRIMARY_URL $TMP_PRIMARYGZ Debug "fetch $PRIMARY_URL to $TMP_PRIMARYGZ" gunzip $TMP_PRIMARYGZ Debug "unzip $TMP_PRIMARYGZ to $TMP_PRIMARY" } fetch_tizen_pkgs() { ARCH=$1 PACKAGE_XPATH_TPL='string(//*[local-name()="metadata"]/*[local-name()="package"][*[local-name()="name"][text()="_PKG_"]][*[local-name()="arch"][text()="_ARCH_"]]/*[local-name()="location"]/@href)' PACKAGE_CHECKSUM_XPATH_TPL='string(//*[local-name()="metadata"]/*[local-name()="package"][*[local-name()="name"][text()="_PKG_"]][*[local-name()="arch"][text()="_ARCH_"]]/*[local-name()="checksum"]/text())' for pkg in ${@:2} do Inform "Fetching... $pkg" XPATH=${PACKAGE_XPATH_TPL/_PKG_/$pkg} XPATH=${XPATH/_ARCH_/$ARCH} Xpath_get $XPATH $TMP_PRIMARY PKG_PATH=$XPATH_RESULT XPATH=${PACKAGE_CHECKSUM_XPATH_TPL/_PKG_/$pkg} XPATH=${XPATH/_ARCH_/$ARCH} Xpath_get $XPATH $TMP_PRIMARY CHECKSUM=$XPATH_RESULT PKG_URL=$TARGET_URL/$PKG_PATH PKG_FILE=$(basename $PKG_PATH) PKG_PATH=$TMPDIR/$PKG_FILE Debug "Download $PKG_URL to $PKG_PATH" Fetch $PKG_URL $PKG_PATH true echo "$CHECKSUM $PKG_PATH" | sha256sum -c - > /dev/null if [ $? -ne 0 ]; then Error "Fail to fetch $PKG_URL to $PKG_PATH" Debug "Checksum = $CHECKSUM" exit 1 fi done } BASE="Tizen-Base" UNIFIED="Tizen-Unified" Inform "Initialize ${TIZEN_ARCH} base" fetch_tizen_pkgs_init standard $BASE Inform "fetch common packages" fetch_tizen_pkgs ${TIZEN_ARCH} gcc gcc-devel-static glibc glibc-devel libicu libicu-devel libatomic linux-glibc-devel keyutils keyutils-devel libkeyutils Inform "fetch coreclr packages" fetch_tizen_pkgs ${TIZEN_ARCH} libgcc libstdc++ libstdc++-devel libunwind libunwind-devel lttng-ust-devel lttng-ust userspace-rcu-devel userspace-rcu if [ "$TIZEN_ARCH" != "riscv64" ]; then fetch_tizen_pkgs ${TIZEN_ARCH} lldb lldb-devel fi Inform "fetch corefx packages" fetch_tizen_pkgs ${TIZEN_ARCH} libcom_err libcom_err-devel zlib zlib-devel libopenssl11 libopenssl1.1-devel krb5 krb5-devel Inform "Initialize standard unified" fetch_tizen_pkgs_init standard $UNIFIED Inform "fetch corefx packages" fetch_tizen_pkgs ${TIZEN_ARCH} gssdp gssdp-devel tizen-release ================================================ FILE: eng/common/cross/toolchain.cmake ================================================ set(CROSS_ROOTFS $ENV{ROOTFS_DIR}) # reset platform variables (e.g. cmake 3.25 sets LINUX=1) unset(LINUX) unset(FREEBSD) unset(ILLUMOS) unset(ANDROID) unset(TIZEN) unset(HAIKU) set(TARGET_ARCH_NAME $ENV{TARGET_BUILD_ARCH}) if(EXISTS ${CROSS_ROOTFS}/bin/freebsd-version) set(CMAKE_SYSTEM_NAME FreeBSD) set(FREEBSD 1) elseif(EXISTS ${CROSS_ROOTFS}/usr/platform/i86pc) set(CMAKE_SYSTEM_NAME SunOS) set(ILLUMOS 1) elseif(EXISTS ${CROSS_ROOTFS}/boot/system/develop/headers/config/HaikuConfig.h) set(CMAKE_SYSTEM_NAME Haiku) set(HAIKU 1) else() set(CMAKE_SYSTEM_NAME Linux) set(LINUX 1) endif() set(CMAKE_SYSTEM_VERSION 1) if(EXISTS ${CROSS_ROOTFS}/etc/tizen-release) set(TIZEN 1) elseif(EXISTS ${CROSS_ROOTFS}/android_platform) set(ANDROID 1) endif() if(TARGET_ARCH_NAME STREQUAL "arm") set(CMAKE_SYSTEM_PROCESSOR armv7l) if(EXISTS ${CROSS_ROOTFS}/usr/lib/gcc/armv7-alpine-linux-musleabihf) set(TOOLCHAIN "armv7-alpine-linux-musleabihf") elseif(EXISTS ${CROSS_ROOTFS}/usr/lib/gcc/armv6-alpine-linux-musleabihf) set(TOOLCHAIN "armv6-alpine-linux-musleabihf") else() set(TOOLCHAIN "arm-linux-gnueabihf") endif() if(TIZEN) set(TIZEN_TOOLCHAIN "armv7hl-tizen-linux-gnueabihf") endif() elseif(TARGET_ARCH_NAME STREQUAL "arm64") set(CMAKE_SYSTEM_PROCESSOR aarch64) if(EXISTS ${CROSS_ROOTFS}/usr/lib/gcc/aarch64-alpine-linux-musl) set(TOOLCHAIN "aarch64-alpine-linux-musl") elseif(LINUX) set(TOOLCHAIN "aarch64-linux-gnu") if(TIZEN) set(TIZEN_TOOLCHAIN "aarch64-tizen-linux-gnu") endif() elseif(FREEBSD) set(triple "aarch64-unknown-freebsd12") endif() elseif(TARGET_ARCH_NAME STREQUAL "armel") set(CMAKE_SYSTEM_PROCESSOR armv7l) set(TOOLCHAIN "arm-linux-gnueabi") if(TIZEN) set(TIZEN_TOOLCHAIN "armv7l-tizen-linux-gnueabi") endif() elseif(TARGET_ARCH_NAME STREQUAL "armv6") set(CMAKE_SYSTEM_PROCESSOR armv6l) if(EXISTS ${CROSS_ROOTFS}/usr/lib/gcc/armv6-alpine-linux-musleabihf) set(TOOLCHAIN "armv6-alpine-linux-musleabihf") else() set(TOOLCHAIN "arm-linux-gnueabihf") endif() elseif(TARGET_ARCH_NAME STREQUAL "loongarch64") set(CMAKE_SYSTEM_PROCESSOR "loongarch64") if(EXISTS ${CROSS_ROOTFS}/usr/lib/gcc/loongarch64-alpine-linux-musl) set(TOOLCHAIN "loongarch64-alpine-linux-musl") else() set(TOOLCHAIN "loongarch64-linux-gnu") endif() elseif(TARGET_ARCH_NAME STREQUAL "ppc64le") set(CMAKE_SYSTEM_PROCESSOR ppc64le) if(EXISTS ${CROSS_ROOTFS}/usr/lib/gcc/powerpc64le-alpine-linux-musl) set(TOOLCHAIN "powerpc64le-alpine-linux-musl") else() set(TOOLCHAIN "powerpc64le-linux-gnu") endif() elseif(TARGET_ARCH_NAME STREQUAL "riscv64") set(CMAKE_SYSTEM_PROCESSOR riscv64) if(EXISTS ${CROSS_ROOTFS}/usr/lib/gcc/riscv64-alpine-linux-musl) set(TOOLCHAIN "riscv64-alpine-linux-musl") else() set(TOOLCHAIN "riscv64-linux-gnu") if(TIZEN) set(TIZEN_TOOLCHAIN "riscv64-tizen-linux-gnu") endif() endif() elseif(TARGET_ARCH_NAME STREQUAL "s390x") set(CMAKE_SYSTEM_PROCESSOR s390x) if(EXISTS ${CROSS_ROOTFS}/usr/lib/gcc/s390x-alpine-linux-musl) set(TOOLCHAIN "s390x-alpine-linux-musl") else() set(TOOLCHAIN "s390x-linux-gnu") endif() elseif(TARGET_ARCH_NAME STREQUAL "x64") set(CMAKE_SYSTEM_PROCESSOR x86_64) if(EXISTS ${CROSS_ROOTFS}/usr/lib/gcc/x86_64-alpine-linux-musl) set(TOOLCHAIN "x86_64-alpine-linux-musl") elseif(LINUX) set(TOOLCHAIN "x86_64-linux-gnu") if(TIZEN) set(TIZEN_TOOLCHAIN "x86_64-tizen-linux-gnu") endif() elseif(FREEBSD) set(triple "x86_64-unknown-freebsd12") elseif(ILLUMOS) set(TOOLCHAIN "x86_64-illumos") elseif(HAIKU) set(TOOLCHAIN "x86_64-unknown-haiku") endif() elseif(TARGET_ARCH_NAME STREQUAL "x86") set(CMAKE_SYSTEM_PROCESSOR i686) if(EXISTS ${CROSS_ROOTFS}/usr/lib/gcc/i586-alpine-linux-musl) set(TOOLCHAIN "i586-alpine-linux-musl") else() set(TOOLCHAIN "i686-linux-gnu") endif() if(TIZEN) set(TIZEN_TOOLCHAIN "i586-tizen-linux-gnu") endif() else() message(FATAL_ERROR "Arch is ${TARGET_ARCH_NAME}. Only arm, arm64, armel, armv6, loongarch64, ppc64le, riscv64, s390x, x64 and x86 are supported!") endif() if(DEFINED ENV{TOOLCHAIN}) set(TOOLCHAIN $ENV{TOOLCHAIN}) endif() # Specify include paths if(TIZEN) function(find_toolchain_dir prefix) # Dynamically find the version subdirectory file(GLOB DIRECTORIES "${prefix}/*") list(GET DIRECTORIES 0 FIRST_MATCH) get_filename_component(TOOLCHAIN_VERSION ${FIRST_MATCH} NAME) set(TIZEN_TOOLCHAIN_PATH "${prefix}/${TOOLCHAIN_VERSION}" PARENT_SCOPE) endfunction() if(TARGET_ARCH_NAME MATCHES "^(arm|armel|x86)$") find_toolchain_dir("${CROSS_ROOTFS}/usr/lib/gcc/${TIZEN_TOOLCHAIN}") else() find_toolchain_dir("${CROSS_ROOTFS}/usr/lib64/gcc/${TIZEN_TOOLCHAIN}") endif() message(STATUS "TIZEN_TOOLCHAIN_PATH set to: ${TIZEN_TOOLCHAIN_PATH}") include_directories(SYSTEM ${TIZEN_TOOLCHAIN_PATH}/include/c++) include_directories(SYSTEM ${TIZEN_TOOLCHAIN_PATH}/include/c++/${TIZEN_TOOLCHAIN}) endif() function(locate_toolchain_exec exec var) set(TOOLSET_PREFIX ${TOOLCHAIN}-) string(TOUPPER ${exec} EXEC_UPPERCASE) if(NOT "$ENV{CLR_${EXEC_UPPERCASE}}" STREQUAL "") set(${var} "$ENV{CLR_${EXEC_UPPERCASE}}" PARENT_SCOPE) return() endif() find_program(EXEC_LOCATION_${exec} NAMES "${TOOLSET_PREFIX}${exec}${CLR_CMAKE_COMPILER_FILE_NAME_VERSION}" "${TOOLSET_PREFIX}${exec}") if (EXEC_LOCATION_${exec} STREQUAL "EXEC_LOCATION_${exec}-NOTFOUND") message(FATAL_ERROR "Unable to find toolchain executable. Name: ${exec}, Prefix: ${TOOLSET_PREFIX}.") endif() set(${var} ${EXEC_LOCATION_${exec}} PARENT_SCOPE) endfunction() if(ANDROID) if(TARGET_ARCH_NAME STREQUAL "arm") set(ANDROID_ABI armeabi-v7a) elseif(TARGET_ARCH_NAME STREQUAL "arm64") set(ANDROID_ABI arm64-v8a) endif() # extract platform number required by the NDK's toolchain file(READ "${CROSS_ROOTFS}/android_platform" RID_FILE_CONTENTS) string(REPLACE "RID=" "" ANDROID_RID "${RID_FILE_CONTENTS}") string(REGEX REPLACE ".*\\.([0-9]+)-.*" "\\1" ANDROID_PLATFORM "${ANDROID_RID}") set(ANDROID_TOOLCHAIN clang) set(FEATURE_EVENT_TRACE 0) # disable event trace as there is no lttng-ust package in termux repository set(CMAKE_SYSTEM_LIBRARY_PATH "${CROSS_ROOTFS}/usr/lib") set(CMAKE_SYSTEM_INCLUDE_PATH "${CROSS_ROOTFS}/usr/include") # include official NDK toolchain script include(${CROSS_ROOTFS}/../build/cmake/android.toolchain.cmake) elseif(FREEBSD) # we cross-compile by instructing clang set(CMAKE_C_COMPILER_TARGET ${triple}) set(CMAKE_CXX_COMPILER_TARGET ${triple}) set(CMAKE_ASM_COMPILER_TARGET ${triple}) set(CMAKE_SYSROOT "${CROSS_ROOTFS}") set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fuse-ld=lld") set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -fuse-ld=lld") set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} -fuse-ld=lld") elseif(ILLUMOS) set(CMAKE_SYSROOT "${CROSS_ROOTFS}") set(CMAKE_SYSTEM_PREFIX_PATH "${CROSS_ROOTFS}") set(CMAKE_C_STANDARD_LIBRARIES "${CMAKE_C_STANDARD_LIBRARIES} -lssp") set(CMAKE_CXX_STANDARD_LIBRARIES "${CMAKE_CXX_STANDARD_LIBRARIES} -lssp") include_directories(SYSTEM ${CROSS_ROOTFS}/include) locate_toolchain_exec(gcc CMAKE_C_COMPILER) locate_toolchain_exec(g++ CMAKE_CXX_COMPILER) elseif(HAIKU) set(CMAKE_SYSROOT "${CROSS_ROOTFS}") set(CMAKE_PROGRAM_PATH "${CMAKE_PROGRAM_PATH};${CROSS_ROOTFS}/cross-tools-x86_64/bin") set(CMAKE_SYSTEM_PREFIX_PATH "${CROSS_ROOTFS}") set(CMAKE_C_STANDARD_LIBRARIES "${CMAKE_C_STANDARD_LIBRARIES} -lssp") set(CMAKE_CXX_STANDARD_LIBRARIES "${CMAKE_CXX_STANDARD_LIBRARIES} -lssp") locate_toolchain_exec(gcc CMAKE_C_COMPILER) locate_toolchain_exec(g++ CMAKE_CXX_COMPILER) # let CMake set up the correct search paths include(Platform/Haiku) else() set(CMAKE_SYSROOT "${CROSS_ROOTFS}") set(CMAKE_C_COMPILER_EXTERNAL_TOOLCHAIN "${CROSS_ROOTFS}/usr") set(CMAKE_CXX_COMPILER_EXTERNAL_TOOLCHAIN "${CROSS_ROOTFS}/usr") set(CMAKE_ASM_COMPILER_EXTERNAL_TOOLCHAIN "${CROSS_ROOTFS}/usr") endif() # Specify link flags function(add_toolchain_linker_flag Flag) set(Config "${ARGV1}") set(CONFIG_SUFFIX "") if (NOT Config STREQUAL "") set(CONFIG_SUFFIX "_${Config}") endif() set("CMAKE_EXE_LINKER_FLAGS${CONFIG_SUFFIX}_INIT" "${CMAKE_EXE_LINKER_FLAGS${CONFIG_SUFFIX}_INIT} ${Flag}" PARENT_SCOPE) set("CMAKE_SHARED_LINKER_FLAGS${CONFIG_SUFFIX}_INIT" "${CMAKE_SHARED_LINKER_FLAGS${CONFIG_SUFFIX}_INIT} ${Flag}" PARENT_SCOPE) endfunction() if(LINUX) add_toolchain_linker_flag("-Wl,--rpath-link=${CROSS_ROOTFS}/lib/${TOOLCHAIN}") add_toolchain_linker_flag("-Wl,--rpath-link=${CROSS_ROOTFS}/usr/lib/${TOOLCHAIN}") endif() if(TARGET_ARCH_NAME MATCHES "^(arm|armel)$") if(TIZEN) add_toolchain_linker_flag("-B${TIZEN_TOOLCHAIN_PATH}") add_toolchain_linker_flag("-L${CROSS_ROOTFS}/lib") add_toolchain_linker_flag("-L${CROSS_ROOTFS}/usr/lib") add_toolchain_linker_flag("-L${TIZEN_TOOLCHAIN_PATH}") endif() elseif(TARGET_ARCH_NAME MATCHES "^(arm64|x64|riscv64)$") if(TIZEN) add_toolchain_linker_flag("-B${TIZEN_TOOLCHAIN_PATH}") add_toolchain_linker_flag("-L${CROSS_ROOTFS}/lib64") add_toolchain_linker_flag("-L${CROSS_ROOTFS}/usr/lib64") add_toolchain_linker_flag("-L${TIZEN_TOOLCHAIN_PATH}") add_toolchain_linker_flag("-Wl,--rpath-link=${CROSS_ROOTFS}/lib64") add_toolchain_linker_flag("-Wl,--rpath-link=${CROSS_ROOTFS}/usr/lib64") add_toolchain_linker_flag("-Wl,--rpath-link=${TIZEN_TOOLCHAIN_PATH}") endif() elseif(TARGET_ARCH_NAME STREQUAL "s390x") add_toolchain_linker_flag("--target=${TOOLCHAIN}") elseif(TARGET_ARCH_NAME STREQUAL "x86") if(EXISTS ${CROSS_ROOTFS}/usr/lib/gcc/i586-alpine-linux-musl) add_toolchain_linker_flag("--target=${TOOLCHAIN}") add_toolchain_linker_flag("-Wl,--rpath-link=${CROSS_ROOTFS}/usr/lib/gcc/${TOOLCHAIN}") endif() add_toolchain_linker_flag(-m32) if(TIZEN) add_toolchain_linker_flag("-B${TIZEN_TOOLCHAIN_PATH}") add_toolchain_linker_flag("-L${CROSS_ROOTFS}/lib") add_toolchain_linker_flag("-L${CROSS_ROOTFS}/usr/lib") add_toolchain_linker_flag("-L${TIZEN_TOOLCHAIN_PATH}") endif() elseif(ILLUMOS) add_toolchain_linker_flag("-L${CROSS_ROOTFS}/lib/amd64") add_toolchain_linker_flag("-L${CROSS_ROOTFS}/usr/amd64/lib") elseif(HAIKU) add_toolchain_linker_flag("-lnetwork") add_toolchain_linker_flag("-lroot") endif() # Specify compile options if((TARGET_ARCH_NAME MATCHES "^(arm|arm64|armel|armv6|loongarch64|ppc64le|riscv64|s390x|x64|x86)$" AND NOT ANDROID AND NOT FREEBSD) OR ILLUMOS OR HAIKU) set(CMAKE_C_COMPILER_TARGET ${TOOLCHAIN}) set(CMAKE_CXX_COMPILER_TARGET ${TOOLCHAIN}) set(CMAKE_ASM_COMPILER_TARGET ${TOOLCHAIN}) endif() if(TARGET_ARCH_NAME MATCHES "^(arm|armel)$") add_compile_options(-mthumb) if (NOT DEFINED CLR_ARM_FPU_TYPE) set (CLR_ARM_FPU_TYPE vfpv3) endif (NOT DEFINED CLR_ARM_FPU_TYPE) add_compile_options (-mfpu=${CLR_ARM_FPU_TYPE}) if (NOT DEFINED CLR_ARM_FPU_CAPABILITY) set (CLR_ARM_FPU_CAPABILITY 0x7) endif (NOT DEFINED CLR_ARM_FPU_CAPABILITY) add_definitions (-DCLR_ARM_FPU_CAPABILITY=${CLR_ARM_FPU_CAPABILITY}) # persist variables across multiple try_compile passes list(APPEND CMAKE_TRY_COMPILE_PLATFORM_VARIABLES CLR_ARM_FPU_TYPE CLR_ARM_FPU_CAPABILITY) if(TARGET_ARCH_NAME STREQUAL "armel") add_compile_options(-mfloat-abi=softfp) endif() elseif(TARGET_ARCH_NAME STREQUAL "s390x") add_compile_options("--target=${TOOLCHAIN}") elseif(TARGET_ARCH_NAME STREQUAL "x86") if(EXISTS ${CROSS_ROOTFS}/usr/lib/gcc/i586-alpine-linux-musl) add_compile_options(--target=${TOOLCHAIN}) endif() add_compile_options(-m32) add_compile_options(-Wno-error=unused-command-line-argument) endif() if(TIZEN) if(TARGET_ARCH_NAME MATCHES "^(arm|armel|arm64|x86)$") add_compile_options(-Wno-deprecated-declarations) # compile-time option add_compile_options(-D__extern_always_inline=inline) # compile-time option endif() endif() # Set LLDB include and library paths for builds that need lldb. if(TARGET_ARCH_NAME MATCHES "^(arm|armel|x86)$") if(TARGET_ARCH_NAME STREQUAL "x86") set(LLVM_CROSS_DIR "$ENV{LLVM_CROSS_HOME}") else() # arm/armel case set(LLVM_CROSS_DIR "$ENV{LLVM_ARM_HOME}") endif() if(LLVM_CROSS_DIR) set(WITH_LLDB_LIBS "${LLVM_CROSS_DIR}/lib/" CACHE STRING "") set(WITH_LLDB_INCLUDES "${LLVM_CROSS_DIR}/include" CACHE STRING "") set(LLDB_H "${WITH_LLDB_INCLUDES}" CACHE STRING "") set(LLDB "${LLVM_CROSS_DIR}/lib/liblldb.so" CACHE STRING "") else() if(TARGET_ARCH_NAME STREQUAL "x86") set(WITH_LLDB_LIBS "${CROSS_ROOTFS}/usr/lib/i386-linux-gnu" CACHE STRING "") set(CHECK_LLVM_DIR "${CROSS_ROOTFS}/usr/lib/llvm-3.8/include") if(EXISTS "${CHECK_LLVM_DIR}" AND IS_DIRECTORY "${CHECK_LLVM_DIR}") set(WITH_LLDB_INCLUDES "${CHECK_LLVM_DIR}") else() set(WITH_LLDB_INCLUDES "${CROSS_ROOTFS}/usr/lib/llvm-3.6/include") endif() else() # arm/armel case set(WITH_LLDB_LIBS "${CROSS_ROOTFS}/usr/lib/${TOOLCHAIN}" CACHE STRING "") set(WITH_LLDB_INCLUDES "${CROSS_ROOTFS}/usr/lib/llvm-3.6/include" CACHE STRING "") endif() endif() endif() # Set C++ standard library options if specified set(CLR_CMAKE_CXX_STANDARD_LIBRARY "" CACHE STRING "Standard library flavor to link against. Only supported with the Clang compiler.") if (CLR_CMAKE_CXX_STANDARD_LIBRARY) add_compile_options($<$:--stdlib=${CLR_CMAKE_CXX_STANDARD_LIBRARY}>) add_link_options($<$:--stdlib=${CLR_CMAKE_CXX_STANDARD_LIBRARY}>) endif() option(CLR_CMAKE_CXX_STANDARD_LIBRARY_STATIC "Statically link against the C++ standard library" OFF) if(CLR_CMAKE_CXX_STANDARD_LIBRARY_STATIC) add_link_options($<$:-static-libstdc++>) endif() set(CLR_CMAKE_CXX_ABI_LIBRARY "" CACHE STRING "C++ ABI implementation library to link against. Only supported with the Clang compiler.") if (CLR_CMAKE_CXX_ABI_LIBRARY) # The user may specify the ABI library with the 'lib' prefix, like 'libstdc++'. Strip the prefix here so the linker finds the right library. string(REGEX REPLACE "^lib(.+)" "\\1" CLR_CMAKE_CXX_ABI_LIBRARY ${CLR_CMAKE_CXX_ABI_LIBRARY}) # We need to specify this as a linker-backend option as Clang will filter this option out when linking to libc++. add_link_options("LINKER:-l${CLR_CMAKE_CXX_ABI_LIBRARY}") endif() set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) ================================================ FILE: eng/common/darc-init.ps1 ================================================ param ( $darcVersion = $null, $versionEndpoint = 'https://maestro.dot.net/api/assets/darc-version?api-version=2020-02-20', $verbosity = 'minimal', $toolpath = $null ) . $PSScriptRoot\tools.ps1 function InstallDarcCli ($darcVersion, $toolpath) { $darcCliPackageName = 'microsoft.dotnet.darc' $dotnetRoot = InitializeDotNetCli -install:$true $dotnet = "$dotnetRoot\dotnet.exe" $toolList = & "$dotnet" tool list -g if ($toolList -like "*$darcCliPackageName*") { & "$dotnet" tool uninstall $darcCliPackageName -g } # If the user didn't explicitly specify the darc version, # query the Maestro API for the correct version of darc to install. if (-not $darcVersion) { $darcVersion = $(Invoke-WebRequest -Uri $versionEndpoint -UseBasicParsing).Content } $arcadeServicesSource = 'https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-eng/nuget/v3/index.json' Write-Host "Installing Darc CLI version $darcVersion..." Write-Host 'You may need to restart your command window if this is the first dotnet tool you have installed.' if (-not $toolpath) { Write-Host "'$dotnet' tool install $darcCliPackageName --version $darcVersion --add-source '$arcadeServicesSource' -v $verbosity -g" & "$dotnet" tool install $darcCliPackageName --version $darcVersion --add-source "$arcadeServicesSource" -v $verbosity -g }else { Write-Host "'$dotnet' tool install $darcCliPackageName --version $darcVersion --add-source '$arcadeServicesSource' -v $verbosity --tool-path '$toolpath'" & "$dotnet" tool install $darcCliPackageName --version $darcVersion --add-source "$arcadeServicesSource" -v $verbosity --tool-path "$toolpath" } } try { InstallDarcCli $darcVersion $toolpath } catch { Write-Host $_.ScriptStackTrace Write-PipelineTelemetryError -Category 'Darc' -Message $_ ExitWithExitCode 1 } ================================================ FILE: eng/common/darc-init.sh ================================================ #!/usr/bin/env bash source="${BASH_SOURCE[0]}" darcVersion='' versionEndpoint='https://maestro.dot.net/api/assets/darc-version?api-version=2020-02-20' verbosity='minimal' while [[ $# -gt 0 ]]; do opt="$(echo "$1" | tr "[:upper:]" "[:lower:]")" case "$opt" in --darcversion) darcVersion=$2 shift ;; --versionendpoint) versionEndpoint=$2 shift ;; --verbosity) verbosity=$2 shift ;; --toolpath) toolpath=$2 shift ;; *) echo "Invalid argument: $1" usage exit 1 ;; esac shift done # resolve $source until the file is no longer a symlink while [[ -h "$source" ]]; do scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" source="$(readlink "$source")" # if $source was a relative symlink, we need to resolve it relative to the path where the # symlink file was located [[ $source != /* ]] && source="$scriptroot/$source" done scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" . "$scriptroot/tools.sh" if [ -z "$darcVersion" ]; then darcVersion=$(curl -X GET "$versionEndpoint" -H "accept: text/plain") fi function InstallDarcCli { local darc_cli_package_name="microsoft.dotnet.darc" InitializeDotNetCli true local dotnet_root=$_InitializeDotNetCli if [ -z "$toolpath" ]; then local tool_list=$($dotnet_root/dotnet tool list -g) if [[ $tool_list = *$darc_cli_package_name* ]]; then echo $($dotnet_root/dotnet tool uninstall $darc_cli_package_name -g) fi else local tool_list=$($dotnet_root/dotnet tool list --tool-path "$toolpath") if [[ $tool_list = *$darc_cli_package_name* ]]; then echo $($dotnet_root/dotnet tool uninstall $darc_cli_package_name --tool-path "$toolpath") fi fi local arcadeServicesSource="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-eng/nuget/v3/index.json" echo "Installing Darc CLI version $darcVersion..." echo "You may need to restart your command shell if this is the first dotnet tool you have installed." if [ -z "$toolpath" ]; then echo $($dotnet_root/dotnet tool install $darc_cli_package_name --version $darcVersion --add-source "$arcadeServicesSource" -v $verbosity -g) else echo $($dotnet_root/dotnet tool install $darc_cli_package_name --version $darcVersion --add-source "$arcadeServicesSource" -v $verbosity --tool-path "$toolpath") fi } InstallDarcCli ================================================ FILE: eng/common/dotnet-install.cmd ================================================ @echo off powershell -ExecutionPolicy ByPass -NoProfile -command "& """%~dp0dotnet-install.ps1""" %*" ================================================ FILE: eng/common/dotnet-install.ps1 ================================================ [CmdletBinding(PositionalBinding=$false)] Param( [string] $verbosity = 'minimal', [string] $architecture = '', [string] $version = 'Latest', [string] $runtime = 'dotnet', [string] $RuntimeSourceFeed = '', [string] $RuntimeSourceFeedKey = '' ) . $PSScriptRoot\tools.ps1 $dotnetRoot = Join-Path $RepoRoot '.dotnet' $installdir = $dotnetRoot try { if ($architecture -and $architecture.Trim() -eq 'x86') { $installdir = Join-Path $installdir 'x86' } InstallDotNet $installdir $version $architecture $runtime $true -RuntimeSourceFeed $RuntimeSourceFeed -RuntimeSourceFeedKey $RuntimeSourceFeedKey } catch { Write-Host $_.ScriptStackTrace Write-PipelineTelemetryError -Category 'InitializeToolset' -Message $_ ExitWithExitCode 1 } ExitWithExitCode 0 ================================================ FILE: eng/common/dotnet-install.sh ================================================ #!/usr/bin/env bash source="${BASH_SOURCE[0]}" # resolve $source until the file is no longer a symlink while [[ -h "$source" ]]; do scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" source="$(readlink "$source")" # if $source was a relative symlink, we need to resolve it relative to the path where the # symlink file was located [[ $source != /* ]] && source="$scriptroot/$source" done scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" . "$scriptroot/tools.sh" version='Latest' architecture='' runtime='dotnet' runtimeSourceFeed='' runtimeSourceFeedKey='' while [[ $# -gt 0 ]]; do opt="$(echo "$1" | tr "[:upper:]" "[:lower:]")" case "$opt" in -version|-v) shift version="$1" ;; -architecture|-a) shift architecture="$1" ;; -runtime|-r) shift runtime="$1" ;; -runtimesourcefeed) shift runtimeSourceFeed="$1" ;; -runtimesourcefeedkey) shift runtimeSourceFeedKey="$1" ;; *) Write-PipelineTelemetryError -Category 'Build' -Message "Invalid argument: $1" exit 1 ;; esac shift done # Use uname to determine what the CPU is, see https://en.wikipedia.org/wiki/Uname#Examples cpuname=$(uname -m) case $cpuname in arm64|aarch64) buildarch=arm64 if [ "$(getconf LONG_BIT)" -lt 64 ]; then # This is 32-bit OS running on 64-bit CPU (for example Raspberry Pi OS) buildarch=arm fi ;; loongarch64) buildarch=loongarch64 ;; amd64|x86_64) buildarch=x64 ;; armv*l) buildarch=arm ;; i[3-6]86) buildarch=x86 ;; riscv64) buildarch=riscv64 ;; *) echo "Unknown CPU $cpuname detected, treating it as x64" buildarch=x64 ;; esac dotnetRoot="${repo_root}.dotnet" if [[ $architecture != "" ]] && [[ $architecture != $buildarch ]]; then dotnetRoot="$dotnetRoot/$architecture" fi InstallDotNet "$dotnetRoot" $version "$architecture" $runtime true $runtimeSourceFeed $runtimeSourceFeedKey || { local exit_code=$? Write-PipelineTelemetryError -Category 'InitializeToolset' -Message "dotnet-install.sh failed (exit code '$exit_code')." >&2 ExitWithExitCode $exit_code } ExitWithExitCode 0 ================================================ FILE: eng/common/dotnet.cmd ================================================ @echo off :: This script is used to install the .NET SDK. :: It will also invoke the SDK with any provided arguments. powershell -ExecutionPolicy ByPass -NoProfile -command "& """%~dp0dotnet.ps1""" %*" exit /b %ErrorLevel% ================================================ FILE: eng/common/dotnet.ps1 ================================================ # This script is used to install the .NET SDK. # It will also invoke the SDK with any provided arguments. . $PSScriptRoot\tools.ps1 $dotnetRoot = InitializeDotNetCli -install:$true # Invoke acquired SDK with args if they are provided if ($args.count -gt 0) { $env:DOTNET_NOLOGO=1 & "$dotnetRoot\dotnet.exe" $args } ================================================ FILE: eng/common/dotnet.sh ================================================ #!/usr/bin/env bash # This script is used to install the .NET SDK. # It will also invoke the SDK with any provided arguments. source="${BASH_SOURCE[0]}" # resolve $SOURCE until the file is no longer a symlink while [[ -h $source ]]; do scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" source="$(readlink "$source")" # if $source was a relative symlink, we need to resolve it relative to the path where the # symlink file was located [[ $source != /* ]] && source="$scriptroot/$source" done scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" source $scriptroot/tools.sh InitializeDotNetCli true # install # Invoke acquired SDK with args if they are provided if [[ $# -gt 0 ]]; then __dotnetDir=${_InitializeDotNetCli} dotnetPath=${__dotnetDir}/dotnet ${dotnetPath} "$@" fi ================================================ FILE: eng/common/enable-cross-org-publishing.ps1 ================================================ param( [string] $token ) . $PSScriptRoot\pipeline-logging-functions.ps1 # Write-PipelineSetVariable will no-op if a variable named $ci is not defined # Since this script is only ever called in AzDO builds, just universally set it $ci = $true Write-PipelineSetVariable -Name 'VSS_NUGET_ACCESSTOKEN' -Value $token -IsMultiJobVariable $false Write-PipelineSetVariable -Name 'VSS_NUGET_URI_PREFIXES' -Value 'https://dnceng.pkgs.visualstudio.com/;https://pkgs.dev.azure.com/dnceng/;https://devdiv.pkgs.visualstudio.com/;https://pkgs.dev.azure.com/devdiv/' -IsMultiJobVariable $false ================================================ FILE: eng/common/generate-locproject.ps1 ================================================ Param( [Parameter(Mandatory=$true)][string] $SourcesDirectory, # Directory where source files live; if using a Localize directory it should live in here [string] $LanguageSet = 'VS_Main_Languages', # Language set to be used in the LocProject.json [switch] $UseCheckedInLocProjectJson, # When set, generates a LocProject.json and compares it to one that already exists in the repo; otherwise just generates one [switch] $CreateNeutralXlfs # Creates neutral xlf files. Only set to false when running locally ) # Generates LocProject.json files for the OneLocBuild task. OneLocBuildTask is described here: # https://ceapex.visualstudio.com/CEINTL/_wiki/wikis/CEINTL.wiki/107/Localization-with-OneLocBuild-Task Set-StrictMode -Version 2.0 $ErrorActionPreference = "Stop" . $PSScriptRoot\pipeline-logging-functions.ps1 $exclusionsFilePath = "$SourcesDirectory\eng\Localize\LocExclusions.json" $exclusions = @{ Exclusions = @() } if (Test-Path -Path $exclusionsFilePath) { $exclusions = Get-Content "$exclusionsFilePath" | ConvertFrom-Json } Push-Location "$SourcesDirectory" # push location for Resolve-Path -Relative to work # Template files $jsonFiles = @() $jsonTemplateFiles = Get-ChildItem -Recurse -Path "$SourcesDirectory" | Where-Object { $_.FullName -Match "\.template\.config\\localize\\.+\.en\.json" } # .NET templating pattern $jsonTemplateFiles | ForEach-Object { $null = $_.Name -Match "(.+)\.[\w-]+\.json" # matches '[filename].[langcode].json $destinationFile = "$($_.Directory.FullName)\$($Matches.1).json" $jsonFiles += Copy-Item "$($_.FullName)" -Destination $destinationFile -PassThru } $jsonWinformsTemplateFiles = Get-ChildItem -Recurse -Path "$SourcesDirectory" | Where-Object { $_.FullName -Match "en\\strings\.json" } # current winforms pattern $wxlFilesV3 = @() $wxlFilesV5 = @() $wxlFiles = Get-ChildItem -Recurse -Path "$SourcesDirectory" | Where-Object { $_.FullName -Match "\\.+\.wxl" -And -Not( $_.Directory.Name -Match "\d{4}" ) } # localized files live in four digit lang ID directories; this excludes them if (-not $wxlFiles) { $wxlEnFiles = Get-ChildItem -Recurse -Path "$SourcesDirectory" | Where-Object { $_.FullName -Match "\\1033\\.+\.wxl" } # pick up en files (1033 = en) specifically so we can copy them to use as the neutral xlf files if ($wxlEnFiles) { $wxlFiles = @() $wxlEnFiles | ForEach-Object { $destinationFile = "$($_.Directory.Parent.FullName)\$($_.Name)" $content = Get-Content $_.FullName -Raw # Split files on schema to select different parser settings in the generated project. if ($content -like "*http://wixtoolset.org/schemas/v4/wxl*") { $wxlFilesV5 += Copy-Item $_.FullName -Destination $destinationFile -PassThru } elseif ($content -like "*http://schemas.microsoft.com/wix/2006/localization*") { $wxlFilesV3 += Copy-Item $_.FullName -Destination $destinationFile -PassThru } } } } $macosHtmlEnFiles = Get-ChildItem -Recurse -Path "$SourcesDirectory" | Where-Object { $_.FullName -Match "en\.lproj\\.+\.html$" } # add installer HTML files $macosHtmlFiles = @() if ($macosHtmlEnFiles) { $macosHtmlEnFiles | ForEach-Object { $destinationFile = "$($_.Directory.Parent.FullName)\$($_.Name)" $macosHtmlFiles += Copy-Item "$($_.FullName)" -Destination $destinationFile -PassThru } } $xlfFiles = @() $allXlfFiles = Get-ChildItem -Recurse -Path "$SourcesDirectory\*\*.xlf" $langXlfFiles = @() if ($allXlfFiles) { $null = $allXlfFiles[0].FullName -Match "\.([\w-]+)\.xlf" # matches '[langcode].xlf' $firstLangCode = $Matches.1 $langXlfFiles = Get-ChildItem -Recurse -Path "$SourcesDirectory\*\*.$firstLangCode.xlf" } $langXlfFiles | ForEach-Object { $null = $_.Name -Match "(.+)\.[\w-]+\.xlf" # matches '[filename].[langcode].xlf $destinationFile = "$($_.Directory.FullName)\$($Matches.1).xlf" $xlfFiles += Copy-Item "$($_.FullName)" -Destination $destinationFile -PassThru } $locFiles = $jsonFiles + $jsonWinformsTemplateFiles + $xlfFiles $locJson = @{ Projects = @( @{ LanguageSet = $LanguageSet LocItems = @( $locFiles | ForEach-Object { $outputPath = "$(($_.DirectoryName | Resolve-Path -Relative) + "\")" $continue = $true foreach ($exclusion in $exclusions.Exclusions) { if ($_.FullName.Contains($exclusion)) { $continue = $false } } $sourceFile = ($_.FullName | Resolve-Path -Relative) if (!$CreateNeutralXlfs -and $_.Extension -eq '.xlf') { Remove-Item -Path $sourceFile } if ($continue) { if ($_.Directory.Name -eq 'en' -and $_.Extension -eq '.json') { return @{ SourceFile = $sourceFile CopyOption = "LangIDOnPath" OutputPath = "$($_.Directory.Parent.FullName | Resolve-Path -Relative)\" } } else { return @{ SourceFile = $sourceFile CopyOption = "LangIDOnName" OutputPath = $outputPath } } } } ) }, @{ LanguageSet = $LanguageSet CloneLanguageSet = "WiX_CloneLanguages" LssFiles = @( "wxl_loc.lss" ) LocItems = @( $wxlFilesV3 | ForEach-Object { $outputPath = "$($_.Directory.FullName | Resolve-Path -Relative)\" $continue = $true foreach ($exclusion in $exclusions.Exclusions) { if ($_.FullName.Contains($exclusion)) { $continue = $false } } $sourceFile = ($_.FullName | Resolve-Path -Relative) if ($continue) { return @{ SourceFile = $sourceFile CopyOption = "LangIDOnPath" OutputPath = $outputPath } } } ) }, @{ LanguageSet = $LanguageSet CloneLanguageSet = "WiX_CloneLanguages" LssFiles = @( "P210WxlSchemaV4.lss" ) LocItems = @( $wxlFilesV5 | ForEach-Object { $outputPath = "$($_.Directory.FullName | Resolve-Path -Relative)\" $continue = $true foreach ($exclusion in $exclusions.Exclusions) { if ($_.FullName.Contains($exclusion)) { $continue = $false } } $sourceFile = ($_.FullName | Resolve-Path -Relative) if ($continue) { return @{ SourceFile = $sourceFile CopyOption = "LangIDOnPath" OutputPath = $outputPath } } } ) }, @{ LanguageSet = $LanguageSet CloneLanguageSet = "VS_macOS_CloneLanguages" LssFiles = @( ".\eng\common\loc\P22DotNetHtmlLocalization.lss" ) LocItems = @( $macosHtmlFiles | ForEach-Object { $outputPath = "$($_.Directory.FullName | Resolve-Path -Relative)\" $continue = $true foreach ($exclusion in $exclusions.Exclusions) { if ($_.FullName.Contains($exclusion)) { $continue = $false } } $sourceFile = ($_.FullName | Resolve-Path -Relative) $lciFile = $sourceFile + ".lci" if ($continue) { $result = @{ SourceFile = $sourceFile CopyOption = "LangIDOnPath" OutputPath = $outputPath } if (Test-Path $lciFile -PathType Leaf) { $result["LciFile"] = $lciFile } return $result } } ) } ) } $json = ConvertTo-Json $locJson -Depth 5 Write-Host "LocProject.json generated:`n`n$json`n`n" Pop-Location if (!$UseCheckedInLocProjectJson) { New-Item "$SourcesDirectory\eng\Localize\LocProject.json" -Force # Need this to make sure the Localize directory is created Set-Content "$SourcesDirectory\eng\Localize\LocProject.json" $json } else { New-Item "$SourcesDirectory\eng\Localize\LocProject-generated.json" -Force # Need this to make sure the Localize directory is created Set-Content "$SourcesDirectory\eng\Localize\LocProject-generated.json" $json if ((Get-FileHash "$SourcesDirectory\eng\Localize\LocProject-generated.json").Hash -ne (Get-FileHash "$SourcesDirectory\eng\Localize\LocProject.json").Hash) { Write-PipelineTelemetryError -Category "OneLocBuild" -Message "Existing LocProject.json differs from generated LocProject.json. Download LocProject-generated.json and compare them." exit 1 } else { Write-Host "Generated LocProject.json and current LocProject.json are identical." } } ================================================ FILE: eng/common/generate-sbom-prep.ps1 ================================================ Param( [Parameter(Mandatory=$true)][string] $ManifestDirPath # Manifest directory where sbom will be placed ) . $PSScriptRoot\pipeline-logging-functions.ps1 # Normally - we'd listen to the manifest path given, but 1ES templates will overwrite if this level gets uploaded directly # with their own overwriting ours. So we create it as a sub directory of the requested manifest path. $ArtifactName = "${env:SYSTEM_STAGENAME}_${env:AGENT_JOBNAME}_SBOM" $SafeArtifactName = $ArtifactName -replace '["/:<>\\|?@*"() ]', '_' $SbomGenerationDir = Join-Path $ManifestDirPath $SafeArtifactName Write-Host "Artifact name before : $ArtifactName" Write-Host "Artifact name after : $SafeArtifactName" Write-Host "Creating dir $ManifestDirPath" # create directory for sbom manifest to be placed if (!(Test-Path -path $SbomGenerationDir)) { New-Item -ItemType Directory -path $SbomGenerationDir Write-Host "Successfully created directory $SbomGenerationDir" } else{ Write-PipelineTelemetryError -category 'Build' "Unable to create sbom folder." } Write-Host "Updating artifact name" Write-Host "##vso[task.setvariable variable=ARTIFACT_NAME]$SafeArtifactName" ================================================ FILE: eng/common/generate-sbom-prep.sh ================================================ #!/usr/bin/env bash source="${BASH_SOURCE[0]}" # resolve $SOURCE until the file is no longer a symlink while [[ -h $source ]]; do scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" source="$(readlink "$source")" # if $source was a relative symlink, we need to resolve it relative to the path where the # symlink file was located [[ $source != /* ]] && source="$scriptroot/$source" done scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" . $scriptroot/pipeline-logging-functions.sh # replace all special characters with _, some builds use special characters like : in Agent.Jobname, that is not a permissible name while uploading artifacts. artifact_name=$SYSTEM_STAGENAME"_"$AGENT_JOBNAME"_SBOM" safe_artifact_name="${artifact_name//["/:<>\\|?@*$" ]/_}" manifest_dir=$1 # Normally - we'd listen to the manifest path given, but 1ES templates will overwrite if this level gets uploaded directly # with their own overwriting ours. So we create it as a sub directory of the requested manifest path. sbom_generation_dir="$manifest_dir/$safe_artifact_name" if [ ! -d "$sbom_generation_dir" ] ; then mkdir -p "$sbom_generation_dir" echo "Sbom directory created." $sbom_generation_dir else Write-PipelineTelemetryError -category 'Build' "Unable to create sbom folder." fi echo "Artifact name before : "$artifact_name echo "Artifact name after : "$safe_artifact_name export ARTIFACT_NAME=$safe_artifact_name echo "##vso[task.setvariable variable=ARTIFACT_NAME]$safe_artifact_name" exit 0 ================================================ FILE: eng/common/helixpublish.proj ================================================ msbuild %(Identity) $(WorkItemDirectory) $(WorkItemCommand) $(WorkItemTimeout) ================================================ FILE: eng/common/init-tools-native.cmd ================================================ @echo off powershell -NoProfile -NoLogo -ExecutionPolicy ByPass -command "& """%~dp0init-tools-native.ps1""" %*" exit /b %ErrorLevel% ================================================ FILE: eng/common/init-tools-native.ps1 ================================================ <# .SYNOPSIS Entry point script for installing native tools .DESCRIPTION Reads $RepoRoot\global.json file to determine native assets to install and executes installers for those tools .PARAMETER BaseUri Base file directory or Url from which to acquire tool archives .PARAMETER InstallDirectory Directory to install native toolset. This is a command-line override for the default Install directory precedence order: - InstallDirectory command-line override - NETCOREENG_INSTALL_DIRECTORY environment variable - (default) %USERPROFILE%/.netcoreeng/native .PARAMETER Clean Switch specifying to not install anything, but cleanup native asset folders .PARAMETER Force Clean and then install tools .PARAMETER DownloadRetries Total number of retry attempts .PARAMETER RetryWaitTimeInSeconds Wait time between retry attempts in seconds .PARAMETER GlobalJsonFile File path to global.json file .PARAMETER PathPromotion Optional switch to enable either promote native tools specified in the global.json to the path (in Azure Pipelines) or break the build if a native tool is not found on the path (on a local dev machine) .NOTES #> [CmdletBinding(PositionalBinding=$false)] Param ( [string] $BaseUri = 'https://netcorenativeassets.blob.core.windows.net/resource-packages/external', [string] $InstallDirectory, [switch] $Clean = $False, [switch] $Force = $False, [int] $DownloadRetries = 5, [int] $RetryWaitTimeInSeconds = 30, [string] $GlobalJsonFile, [switch] $PathPromotion ) if (!$GlobalJsonFile) { $GlobalJsonFile = Join-Path (Get-Item $PSScriptRoot).Parent.Parent.FullName 'global.json' } Set-StrictMode -version 2.0 $ErrorActionPreference='Stop' . $PSScriptRoot\pipeline-logging-functions.ps1 Import-Module -Name (Join-Path $PSScriptRoot 'native\CommonLibrary.psm1') try { # Define verbose switch if undefined $Verbose = $VerbosePreference -Eq 'Continue' $EngCommonBaseDir = Join-Path $PSScriptRoot 'native\' $NativeBaseDir = $InstallDirectory if (!$NativeBaseDir) { $NativeBaseDir = CommonLibrary\Get-NativeInstallDirectory } $Env:CommonLibrary_NativeInstallDir = $NativeBaseDir $InstallBin = Join-Path $NativeBaseDir 'bin' $InstallerPath = Join-Path $EngCommonBaseDir 'install-tool.ps1' # Process tools list Write-Host "Processing $GlobalJsonFile" If (-Not (Test-Path $GlobalJsonFile)) { Write-Host "Unable to find '$GlobalJsonFile'" exit 0 } $NativeTools = Get-Content($GlobalJsonFile) -Raw | ConvertFrom-Json | Select-Object -Expand 'native-tools' -ErrorAction SilentlyContinue if ($NativeTools) { if ($PathPromotion -eq $True) { $ArcadeToolsDirectory = "$env:SYSTEMDRIVE\arcade-tools" if (Test-Path $ArcadeToolsDirectory) { # if this directory exists, we should use native tools on machine $NativeTools.PSObject.Properties | ForEach-Object { $ToolName = $_.Name $ToolVersion = $_.Value $InstalledTools = @{} if ((Get-Command "$ToolName" -ErrorAction SilentlyContinue) -eq $null) { if ($ToolVersion -eq "latest") { $ToolVersion = "" } $ToolDirectories = (Get-ChildItem -Path "$ArcadeToolsDirectory" -Filter "$ToolName-$ToolVersion*" | Sort-Object -Descending) if ($ToolDirectories -eq $null) { Write-Error "Unable to find directory for $ToolName $ToolVersion; please make sure the tool is installed on this image." exit 1 } $ToolDirectory = $ToolDirectories[0] $BinPathFile = "$($ToolDirectory.FullName)\binpath.txt" if (-not (Test-Path -Path "$BinPathFile")) { Write-Error "Unable to find binpath.txt in '$($ToolDirectory.FullName)' ($ToolName $ToolVersion); artifact is either installed incorrectly or is not a bootstrappable tool." exit 1 } $BinPath = Get-Content "$BinPathFile" $ToolPath = Convert-Path -Path $BinPath Write-Host "Adding $ToolName to the path ($ToolPath)..." Write-Host "##vso[task.prependpath]$ToolPath" $env:PATH = "$ToolPath;$env:PATH" $InstalledTools += @{ $ToolName = $ToolDirectory.FullName } } } return $InstalledTools } else { $NativeTools.PSObject.Properties | ForEach-Object { $ToolName = $_.Name $ToolVersion = $_.Value if ((Get-Command "$ToolName" -ErrorAction SilentlyContinue) -eq $null) { Write-PipelineTelemetryError -Category 'NativeToolsBootstrap' -Message "$ToolName not found on path. Please install $ToolName $ToolVersion before proceeding." Write-PipelineTelemetryError -Category 'NativeToolsBootstrap' -Message "If this is running on a build machine, the arcade-tools directory was not found, which means there's an error with the image." } } exit 0 } } else { $NativeTools.PSObject.Properties | ForEach-Object { $ToolName = $_.Name $ToolVersion = $_.Value $LocalInstallerArguments = @{ ToolName = "$ToolName" } $LocalInstallerArguments += @{ InstallPath = "$InstallBin" } $LocalInstallerArguments += @{ BaseUri = "$BaseUri" } $LocalInstallerArguments += @{ CommonLibraryDirectory = "$EngCommonBaseDir" } $LocalInstallerArguments += @{ Version = "$ToolVersion" } if ($Verbose) { $LocalInstallerArguments += @{ Verbose = $True } } if (Get-Variable 'Force' -ErrorAction 'SilentlyContinue') { if($Force) { $LocalInstallerArguments += @{ Force = $True } } } if ($Clean) { $LocalInstallerArguments += @{ Clean = $True } } Write-Verbose "Installing $ToolName version $ToolVersion" Write-Verbose "Executing '$InstallerPath $($LocalInstallerArguments.Keys.ForEach({"-$_ '$($LocalInstallerArguments.$_)'"}) -join ' ')'" & $InstallerPath @LocalInstallerArguments if ($LASTEXITCODE -Ne "0") { $errMsg = "$ToolName installation failed" if ((Get-Variable 'DoNotAbortNativeToolsInstallationOnFailure' -ErrorAction 'SilentlyContinue') -and $DoNotAbortNativeToolsInstallationOnFailure) { $showNativeToolsWarning = $true if ((Get-Variable 'DoNotDisplayNativeToolsInstallationWarnings' -ErrorAction 'SilentlyContinue') -and $DoNotDisplayNativeToolsInstallationWarnings) { $showNativeToolsWarning = $false } if ($showNativeToolsWarning) { Write-Warning $errMsg } $toolInstallationFailure = $true } else { # We cannot change this to Write-PipelineTelemetryError because of https://github.com/dotnet/arcade/issues/4482 Write-Host $errMsg exit 1 } } } if ((Get-Variable 'toolInstallationFailure' -ErrorAction 'SilentlyContinue') -and $toolInstallationFailure) { # We cannot change this to Write-PipelineTelemetryError because of https://github.com/dotnet/arcade/issues/4482 Write-Host 'Native tools bootstrap failed' exit 1 } } } else { Write-Host 'No native tools defined in global.json' exit 0 } if ($Clean) { exit 0 } if (Test-Path $InstallBin) { Write-Host 'Native tools are available from ' (Convert-Path -Path $InstallBin) Write-Host "##vso[task.prependpath]$(Convert-Path -Path $InstallBin)" return $InstallBin } elseif (-not ($PathPromotion)) { Write-PipelineTelemetryError -Category 'NativeToolsBootstrap' -Message 'Native tools install directory does not exist, installation failed' exit 1 } exit 0 } catch { Write-Host $_.ScriptStackTrace Write-PipelineTelemetryError -Category 'NativeToolsBootstrap' -Message $_ ExitWithExitCode 1 } ================================================ FILE: eng/common/init-tools-native.sh ================================================ #!/usr/bin/env bash source="${BASH_SOURCE[0]}" scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" base_uri='https://netcorenativeassets.blob.core.windows.net/resource-packages/external' install_directory='' clean=false force=false download_retries=5 retry_wait_time_seconds=30 global_json_file="$(dirname "$(dirname "${scriptroot}")")/global.json" declare -a native_assets . $scriptroot/pipeline-logging-functions.sh . $scriptroot/native/common-library.sh while (($# > 0)); do lowerI="$(echo $1 | tr "[:upper:]" "[:lower:]")" case $lowerI in --baseuri) base_uri=$2 shift 2 ;; --installdirectory) install_directory=$2 shift 2 ;; --clean) clean=true shift 1 ;; --force) force=true shift 1 ;; --donotabortonfailure) donotabortonfailure=true shift 1 ;; --donotdisplaywarnings) donotdisplaywarnings=true shift 1 ;; --downloadretries) download_retries=$2 shift 2 ;; --retrywaittimeseconds) retry_wait_time_seconds=$2 shift 2 ;; --help) echo "Common settings:" echo " --installdirectory Directory to install native toolset." echo " This is a command-line override for the default" echo " Install directory precedence order:" echo " - InstallDirectory command-line override" echo " - NETCOREENG_INSTALL_DIRECTORY environment variable" echo " - (default) %USERPROFILE%/.netcoreeng/native" echo "" echo " --clean Switch specifying not to install anything, but cleanup native asset folders" echo " --donotabortonfailure Switch specifiying whether to abort native tools installation on failure" echo " --donotdisplaywarnings Switch specifiying whether to display warnings during native tools installation on failure" echo " --force Clean and then install tools" echo " --help Print help and exit" echo "" echo "Advanced settings:" echo " --baseuri Base URI for where to download native tools from" echo " --downloadretries Number of times a download should be attempted" echo " --retrywaittimeseconds Wait time between download attempts" echo "" exit 0 ;; esac done function ReadGlobalJsonNativeTools { # happy path: we have a proper JSON parsing tool `jq(1)` in PATH! if command -v jq &> /dev/null; then # jq: read each key/value pair under "native-tools" entry and emit: # KEY="" VALUE="" # followed by a null byte. # # bash: read line with null byte delimeter and push to array (for later `eval`uation). while IFS= read -rd '' line; do native_assets+=("$line") done < <(jq -r '. | select(has("native-tools")) | ."native-tools" | keys[] as $k | @sh "KEY=\($k) VALUE=\(.[$k])\u0000"' "$global_json_file") return fi # Warning: falling back to manually parsing JSON, which is not recommended. # Following routine matches the output and escaping logic of jq(1)'s @sh formatter used above. # It has been tested with several weird strings with escaped characters in entries (key and value) # and results were compared with the output of jq(1) in binary representation using xxd(1); # just before the assignment to 'native_assets' array (above and below). # try to capture the section under "native-tools". if [[ ! "$(cat "$global_json_file")" =~ \"native-tools\"[[:space:]\:\{]*([^\}]+) ]]; then return fi section="${BASH_REMATCH[1]}" parseStarted=0 possibleEnd=0 escaping=0 escaped=0 isKey=1 for (( i=0; i<${#section}; i++ )); do char="${section:$i:1}" if ! ((parseStarted)) && [[ "$char" =~ [[:space:],:] ]]; then continue; fi if ! ((escaping)) && [[ "$char" == "\\" ]]; then escaping=1 elif ((escaping)) && ! ((escaped)); then escaped=1 fi if ! ((parseStarted)) && [[ "$char" == "\"" ]]; then parseStarted=1 possibleEnd=0 elif [[ "$char" == "'" ]]; then token="$token'\\\''" possibleEnd=0 elif ((escaping)) || [[ "$char" != "\"" ]]; then token="$token$char" possibleEnd=1 fi if ((possibleEnd)) && ! ((escaping)) && [[ "$char" == "\"" ]]; then # Use printf to unescape token to match jq(1)'s @sh formatting rules. # do not use 'token="$(printf "$token")"' syntax, as $() eats the trailing linefeed. printf -v token "'$token'" if ((isKey)); then KEY="$token" isKey=0 else line="KEY=$KEY VALUE=$token" native_assets+=("$line") isKey=1 fi # reset for next token parseStarted=0 token= elif ((escaping)) && ((escaped)); then escaping=0 escaped=0 fi done } native_base_dir=$install_directory if [[ -z $install_directory ]]; then native_base_dir=$(GetNativeInstallDirectory) fi install_bin="${native_base_dir}/bin" installed_any=false ReadGlobalJsonNativeTools if [[ ${#native_assets[@]} -eq 0 ]]; then echo "No native tools defined in global.json" exit 0; else native_installer_dir="$scriptroot/native" for index in "${!native_assets[@]}"; do eval "${native_assets["$index"]}" installer_path="$native_installer_dir/install-$KEY.sh" installer_command="$installer_path" installer_command+=" --baseuri $base_uri" installer_command+=" --installpath $install_bin" installer_command+=" --version $VALUE" echo $installer_command if [[ $force = true ]]; then installer_command+=" --force" fi if [[ $clean = true ]]; then installer_command+=" --clean" fi if [[ -a $installer_path ]]; then $installer_command if [[ $? != 0 ]]; then if [[ $donotabortonfailure = true ]]; then if [[ $donotdisplaywarnings != true ]]; then Write-PipelineTelemetryError -category 'NativeToolsBootstrap' "Execution Failed" fi else Write-PipelineTelemetryError -category 'NativeToolsBootstrap' "Execution Failed" exit 1 fi else $installed_any = true fi else if [[ $donotabortonfailure == true ]]; then if [[ $donotdisplaywarnings != true ]]; then Write-PipelineTelemetryError -category 'NativeToolsBootstrap' "Execution Failed: no install script" fi else Write-PipelineTelemetryError -category 'NativeToolsBootstrap' "Execution Failed: no install script" exit 1 fi fi done fi if [[ $clean = true ]]; then exit 0 fi if [[ -d $install_bin ]]; then echo "Native tools are available from $install_bin" echo "##vso[task.prependpath]$install_bin" else if [[ $installed_any = true ]]; then Write-PipelineTelemetryError -category 'NativeToolsBootstrap' "Native tools install directory does not exist, installation failed" exit 1 fi fi exit 0 ================================================ FILE: eng/common/internal/Directory.Build.props ================================================ false false ================================================ FILE: eng/common/internal/NuGet.config ================================================ ================================================ FILE: eng/common/internal/Tools.csproj ================================================ net472 false false ================================================ FILE: eng/common/internal-feed-operations.ps1 ================================================ param( [Parameter(Mandatory=$true)][string] $Operation, [string] $AuthToken, [string] $CommitSha, [string] $RepoName, [switch] $IsFeedPrivate ) $ErrorActionPreference = 'Stop' Set-StrictMode -Version 2.0 . $PSScriptRoot\tools.ps1 # Sets VSS_NUGET_EXTERNAL_FEED_ENDPOINTS based on the "darc-int-*" feeds defined in NuGet.config. This is needed # in build agents by CredProvider to authenticate the restore requests to internal feeds as specified in # https://github.com/microsoft/artifacts-credprovider/blob/0f53327cd12fd893d8627d7b08a2171bf5852a41/README.md#environment-variables. This should ONLY be called from identified # internal builds function SetupCredProvider { param( [string] $AuthToken ) # Install the Cred Provider NuGet plugin Write-Host 'Setting up Cred Provider NuGet plugin in the agent...' Write-Host "Getting 'installcredprovider.ps1' from 'https://github.com/microsoft/artifacts-credprovider'..." $url = 'https://raw.githubusercontent.com/microsoft/artifacts-credprovider/master/helpers/installcredprovider.ps1' Write-Host "Writing the contents of 'installcredprovider.ps1' locally..." Invoke-WebRequest $url -UseBasicParsing -OutFile installcredprovider.ps1 Write-Host 'Installing plugin...' .\installcredprovider.ps1 -Force Write-Host "Deleting local copy of 'installcredprovider.ps1'..." Remove-Item .\installcredprovider.ps1 if (-Not("$env:USERPROFILE\.nuget\plugins\netcore")) { Write-PipelineTelemetryError -Category 'Arcade' -Message 'CredProvider plugin was not installed correctly!' ExitWithExitCode 1 } else { Write-Host 'CredProvider plugin was installed correctly!' } # Then, we set the 'VSS_NUGET_EXTERNAL_FEED_ENDPOINTS' environment variable to restore from the stable # feeds successfully $nugetConfigPath = Join-Path $RepoRoot "NuGet.config" if (-Not (Test-Path -Path $nugetConfigPath)) { Write-PipelineTelemetryError -Category 'Build' -Message 'NuGet.config file not found in repo root!' ExitWithExitCode 1 } $endpoints = New-Object System.Collections.ArrayList $nugetConfigPackageSources = Select-Xml -Path $nugetConfigPath -XPath "//packageSources/add[contains(@key, 'darc-int-')]/@value" | foreach{$_.Node.Value} if (($nugetConfigPackageSources | Measure-Object).Count -gt 0 ) { foreach ($stableRestoreResource in $nugetConfigPackageSources) { $trimmedResource = ([string]$stableRestoreResource).Trim() [void]$endpoints.Add(@{endpoint="$trimmedResource"; password="$AuthToken"}) } } if (($endpoints | Measure-Object).Count -gt 0) { $endpointCredentials = @{endpointCredentials=$endpoints} | ConvertTo-Json -Compress # Create the environment variables the AzDo way Write-LoggingCommand -Area 'task' -Event 'setvariable' -Data $endpointCredentials -Properties @{ 'variable' = 'VSS_NUGET_EXTERNAL_FEED_ENDPOINTS' 'issecret' = 'false' } # We don't want sessions cached since we will be updating the endpoints quite frequently Write-LoggingCommand -Area 'task' -Event 'setvariable' -Data 'False' -Properties @{ 'variable' = 'NUGET_CREDENTIALPROVIDER_SESSIONTOKENCACHE_ENABLED' 'issecret' = 'false' } } else { Write-Host 'No internal endpoints found in NuGet.config' } } #Workaround for https://github.com/microsoft/msbuild/issues/4430 function InstallDotNetSdkAndRestoreArcade { $dotnetTempDir = Join-Path $RepoRoot "dotnet" $dotnetSdkVersion="2.1.507" # After experimentation we know this version works when restoring the SDK (compared to 3.0.*) $dotnet = "$dotnetTempDir\dotnet.exe" $restoreProjPath = "$PSScriptRoot\restore.proj" Write-Host "Installing dotnet SDK version $dotnetSdkVersion to restore Arcade SDK..." InstallDotNetSdk "$dotnetTempDir" "$dotnetSdkVersion" '' | Out-File "$restoreProjPath" & $dotnet restore $restoreProjPath Write-Host 'Arcade SDK restored!' if (Test-Path -Path $restoreProjPath) { Remove-Item $restoreProjPath } if (Test-Path -Path $dotnetTempDir) { Remove-Item $dotnetTempDir -Recurse } } try { Push-Location $PSScriptRoot if ($Operation -like 'setup') { SetupCredProvider $AuthToken } elseif ($Operation -like 'install-restore') { InstallDotNetSdkAndRestoreArcade } else { Write-PipelineTelemetryError -Category 'Arcade' -Message "Unknown operation '$Operation'!" ExitWithExitCode 1 } } catch { Write-Host $_.ScriptStackTrace Write-PipelineTelemetryError -Category 'Arcade' -Message $_ ExitWithExitCode 1 } finally { Pop-Location } ================================================ FILE: eng/common/internal-feed-operations.sh ================================================ #!/usr/bin/env bash set -e # Sets VSS_NUGET_EXTERNAL_FEED_ENDPOINTS based on the "darc-int-*" feeds defined in NuGet.config. This is needed # in build agents by CredProvider to authenticate the restore requests to internal feeds as specified in # https://github.com/microsoft/artifacts-credprovider/blob/0f53327cd12fd893d8627d7b08a2171bf5852a41/README.md#environment-variables. # This should ONLY be called from identified internal builds function SetupCredProvider { local authToken=$1 # Install the Cred Provider NuGet plugin echo "Setting up Cred Provider NuGet plugin in the agent..."... echo "Getting 'installcredprovider.ps1' from 'https://github.com/microsoft/artifacts-credprovider'..." local url="https://raw.githubusercontent.com/microsoft/artifacts-credprovider/master/helpers/installcredprovider.sh" echo "Writing the contents of 'installcredprovider.ps1' locally..." local installcredproviderPath="installcredprovider.sh" if command -v curl > /dev/null; then curl $url > "$installcredproviderPath" else wget -q -O "$installcredproviderPath" "$url" fi echo "Installing plugin..." . "$installcredproviderPath" echo "Deleting local copy of 'installcredprovider.sh'..." rm installcredprovider.sh if [ ! -d "$HOME/.nuget/plugins" ]; then Write-PipelineTelemetryError -category 'Build' 'CredProvider plugin was not installed correctly!' ExitWithExitCode 1 else echo "CredProvider plugin was installed correctly!" fi # Then, we set the 'VSS_NUGET_EXTERNAL_FEED_ENDPOINTS' environment variable to restore from the stable # feeds successfully local nugetConfigPath="{$repo_root}NuGet.config" if [ ! "$nugetConfigPath" ]; then Write-PipelineTelemetryError -category 'Build' "NuGet.config file not found in repo's root!" ExitWithExitCode 1 fi local endpoints='[' local nugetConfigPackageValues=`cat "$nugetConfigPath" | grep "key=\"darc-int-"` local pattern="value=\"(.*)\"" for value in $nugetConfigPackageValues do if [[ $value =~ $pattern ]]; then local endpoint="${BASH_REMATCH[1]}" endpoints+="{\"endpoint\": \"$endpoint\", \"password\": \"$authToken\"}," fi done endpoints=${endpoints%?} endpoints+=']' if [ ${#endpoints} -gt 2 ]; then local endpointCredentials="{\"endpointCredentials\": "$endpoints"}" echo "##vso[task.setvariable variable=VSS_NUGET_EXTERNAL_FEED_ENDPOINTS]$endpointCredentials" echo "##vso[task.setvariable variable=NUGET_CREDENTIALPROVIDER_SESSIONTOKENCACHE_ENABLED]False" else echo "No internal endpoints found in NuGet.config" fi } # Workaround for https://github.com/microsoft/msbuild/issues/4430 function InstallDotNetSdkAndRestoreArcade { local dotnetTempDir="$repo_root/dotnet" local dotnetSdkVersion="2.1.507" # After experimentation we know this version works when restoring the SDK (compared to 3.0.*) local restoreProjPath="$repo_root/eng/common/restore.proj" echo "Installing dotnet SDK version $dotnetSdkVersion to restore Arcade SDK..." echo "" > "$restoreProjPath" InstallDotNetSdk "$dotnetTempDir" "$dotnetSdkVersion" local res=`$dotnetTempDir/dotnet restore $restoreProjPath` echo "Arcade SDK restored!" # Cleanup if [ "$restoreProjPath" ]; then rm "$restoreProjPath" fi if [ "$dotnetTempDir" ]; then rm -r $dotnetTempDir fi } source="${BASH_SOURCE[0]}" operation='' authToken='' repoName='' while [[ $# -gt 0 ]]; do opt="$(echo "$1" | tr "[:upper:]" "[:lower:]")" case "$opt" in --operation) operation=$2 shift ;; --authtoken) authToken=$2 shift ;; *) echo "Invalid argument: $1" usage exit 1 ;; esac shift done while [[ -h "$source" ]]; do scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" source="$(readlink "$source")" # if $source was a relative symlink, we need to resolve it relative to the path where the # symlink file was located [[ $source != /* ]] && source="$scriptroot/$source" done scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" . "$scriptroot/tools.sh" if [ "$operation" = "setup" ]; then SetupCredProvider $authToken elif [ "$operation" = "install-restore" ]; then InstallDotNetSdkAndRestoreArcade else echo "Unknown operation '$operation'!" fi ================================================ FILE: eng/common/loc/P22DotNetHtmlLocalization.lss ================================================ ================================================ FILE: eng/common/msbuild.ps1 ================================================ [CmdletBinding(PositionalBinding=$false)] Param( [string] $verbosity = 'minimal', [bool] $warnAsError = $true, [bool] $nodeReuse = $true, [switch] $ci, [switch] $prepareMachine, [switch] $excludePrereleaseVS, [string] $msbuildEngine = $null, [Parameter(ValueFromRemainingArguments=$true)][String[]]$extraArgs ) . $PSScriptRoot\tools.ps1 try { if ($ci) { $nodeReuse = $false } MSBuild @extraArgs } catch { Write-Host $_.ScriptStackTrace Write-PipelineTelemetryError -Category 'Build' -Message $_ ExitWithExitCode 1 } ExitWithExitCode 0 ================================================ FILE: eng/common/msbuild.sh ================================================ #!/usr/bin/env bash source="${BASH_SOURCE[0]}" # resolve $source until the file is no longer a symlink while [[ -h "$source" ]]; do scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" source="$(readlink "$source")" # if $source was a relative symlink, we need to resolve it relative to the path where the # symlink file was located [[ $source != /* ]] && source="$scriptroot/$source" done scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" verbosity='minimal' warn_as_error=true node_reuse=true prepare_machine=false extra_args='' while (($# > 0)); do lowerI="$(echo $1 | tr "[:upper:]" "[:lower:]")" case $lowerI in --verbosity) verbosity=$2 shift 2 ;; --warnaserror) warn_as_error=$2 shift 2 ;; --nodereuse) node_reuse=$2 shift 2 ;; --ci) ci=true shift 1 ;; --preparemachine) prepare_machine=true shift 1 ;; *) extra_args="$extra_args $1" shift 1 ;; esac done . "$scriptroot/tools.sh" if [[ "$ci" == true ]]; then node_reuse=false fi MSBuild $extra_args ExitWithExitCode 0 ================================================ FILE: eng/common/native/CommonLibrary.psm1 ================================================ <# .SYNOPSIS Helper module to install an archive to a directory .DESCRIPTION Helper module to download and extract an archive to a specified directory .PARAMETER Uri Uri of artifact to download .PARAMETER InstallDirectory Directory to extract artifact contents to .PARAMETER Force Force download / extraction if file or contents already exist. Default = False .PARAMETER DownloadRetries Total number of retry attempts. Default = 5 .PARAMETER RetryWaitTimeInSeconds Wait time between retry attempts in seconds. Default = 30 .NOTES Returns False if download or extraction fail, True otherwise #> function DownloadAndExtract { [CmdletBinding(PositionalBinding=$false)] Param ( [Parameter(Mandatory=$True)] [string] $Uri, [Parameter(Mandatory=$True)] [string] $InstallDirectory, [switch] $Force = $False, [int] $DownloadRetries = 5, [int] $RetryWaitTimeInSeconds = 30 ) # Define verbose switch if undefined $Verbose = $VerbosePreference -Eq "Continue" $TempToolPath = CommonLibrary\Get-TempPathFilename -Path $Uri # Download native tool $DownloadStatus = CommonLibrary\Get-File -Uri $Uri ` -Path $TempToolPath ` -DownloadRetries $DownloadRetries ` -RetryWaitTimeInSeconds $RetryWaitTimeInSeconds ` -Force:$Force ` -Verbose:$Verbose if ($DownloadStatus -Eq $False) { Write-Error "Download failed from $Uri" return $False } # Extract native tool $UnzipStatus = CommonLibrary\Expand-Zip -ZipPath $TempToolPath ` -OutputDirectory $InstallDirectory ` -Force:$Force ` -Verbose:$Verbose if ($UnzipStatus -Eq $False) { # Retry Download one more time with Force=true $DownloadRetryStatus = CommonLibrary\Get-File -Uri $Uri ` -Path $TempToolPath ` -DownloadRetries 1 ` -RetryWaitTimeInSeconds $RetryWaitTimeInSeconds ` -Force:$True ` -Verbose:$Verbose if ($DownloadRetryStatus -Eq $False) { Write-Error "Last attempt of download failed as well" return $False } # Retry unzip again one more time with Force=true $UnzipRetryStatus = CommonLibrary\Expand-Zip -ZipPath $TempToolPath ` -OutputDirectory $InstallDirectory ` -Force:$True ` -Verbose:$Verbose if ($UnzipRetryStatus -Eq $False) { Write-Error "Last attempt of unzip failed as well" # Clean up partial zips and extracts if (Test-Path $TempToolPath) { Remove-Item $TempToolPath -Force } if (Test-Path $InstallDirectory) { Remove-Item $InstallDirectory -Force -Recurse } return $False } } return $True } <# .SYNOPSIS Download a file, retry on failure .DESCRIPTION Download specified file and retry if attempt fails .PARAMETER Uri Uri of file to download. If Uri is a local path, the file will be copied instead of downloaded .PARAMETER Path Path to download or copy uri file to .PARAMETER Force Overwrite existing file if present. Default = False .PARAMETER DownloadRetries Total number of retry attempts. Default = 5 .PARAMETER RetryWaitTimeInSeconds Wait time between retry attempts in seconds Default = 30 #> function Get-File { [CmdletBinding(PositionalBinding=$false)] Param ( [Parameter(Mandatory=$True)] [string] $Uri, [Parameter(Mandatory=$True)] [string] $Path, [int] $DownloadRetries = 5, [int] $RetryWaitTimeInSeconds = 30, [switch] $Force = $False ) $Attempt = 0 if ($Force) { if (Test-Path $Path) { Remove-Item $Path -Force } } if (Test-Path $Path) { Write-Host "File '$Path' already exists, skipping download" return $True } $DownloadDirectory = Split-Path -ErrorAction Ignore -Path "$Path" -Parent if (-Not (Test-Path $DownloadDirectory)) { New-Item -path $DownloadDirectory -force -itemType "Directory" | Out-Null } $TempPath = "$Path.tmp" if (Test-Path -IsValid -Path $Uri) { Write-Verbose "'$Uri' is a file path, copying temporarily to '$TempPath'" Copy-Item -Path $Uri -Destination $TempPath Write-Verbose "Moving temporary file to '$Path'" Move-Item -Path $TempPath -Destination $Path return $? } else { Write-Verbose "Downloading $Uri" # Don't display the console progress UI - it's a huge perf hit $ProgressPreference = 'SilentlyContinue' while($Attempt -Lt $DownloadRetries) { try { Invoke-WebRequest -UseBasicParsing -Uri $Uri -OutFile $TempPath Write-Verbose "Downloaded to temporary location '$TempPath'" Move-Item -Path $TempPath -Destination $Path Write-Verbose "Moved temporary file to '$Path'" return $True } catch { $Attempt++ if ($Attempt -Lt $DownloadRetries) { $AttemptsLeft = $DownloadRetries - $Attempt Write-Warning "Download failed, $AttemptsLeft attempts remaining, will retry in $RetryWaitTimeInSeconds seconds" Start-Sleep -Seconds $RetryWaitTimeInSeconds } else { Write-Error $_ Write-Error $_.Exception } } } } return $False } <# .SYNOPSIS Generate a shim for a native tool .DESCRIPTION Creates a wrapper script (shim) that passes arguments forward to native tool assembly .PARAMETER ShimName The name of the shim .PARAMETER ShimDirectory The directory where shims are stored .PARAMETER ToolFilePath Path to file that shim forwards to .PARAMETER Force Replace shim if already present. Default = False .NOTES Returns $True if generating shim succeeds, $False otherwise #> function New-ScriptShim { [CmdletBinding(PositionalBinding=$false)] Param ( [Parameter(Mandatory=$True)] [string] $ShimName, [Parameter(Mandatory=$True)] [string] $ShimDirectory, [Parameter(Mandatory=$True)] [string] $ToolFilePath, [Parameter(Mandatory=$True)] [string] $BaseUri, [switch] $Force ) try { Write-Verbose "Generating '$ShimName' shim" if (-Not (Test-Path $ToolFilePath)){ Write-Error "Specified tool file path '$ToolFilePath' does not exist" return $False } # WinShimmer is a small .NET Framework program that creates .exe shims to bootstrapped programs # Many of the checks for installed programs expect a .exe extension for Windows tools, rather # than a .bat or .cmd file. # Source: https://github.com/dotnet/arcade/tree/master/src/WinShimmer if (-Not (Test-Path "$ShimDirectory\WinShimmer\winshimmer.exe")) { $InstallStatus = DownloadAndExtract -Uri "$BaseUri/windows/winshimmer/WinShimmer.zip" ` -InstallDirectory $ShimDirectory\WinShimmer ` -Force:$Force ` -DownloadRetries 2 ` -RetryWaitTimeInSeconds 5 ` -Verbose:$Verbose } if ((Test-Path (Join-Path $ShimDirectory "$ShimName.exe"))) { Write-Host "$ShimName.exe already exists; replacing..." Remove-Item (Join-Path $ShimDirectory "$ShimName.exe") } & "$ShimDirectory\WinShimmer\winshimmer.exe" $ShimName $ToolFilePath $ShimDirectory return $True } catch { Write-Host $_ Write-Host $_.Exception return $False } } <# .SYNOPSIS Returns the machine architecture of the host machine .NOTES Returns 'x64' on 64 bit machines Returns 'x86' on 32 bit machines #> function Get-MachineArchitecture { $ProcessorArchitecture = $Env:PROCESSOR_ARCHITECTURE $ProcessorArchitectureW6432 = $Env:PROCESSOR_ARCHITEW6432 if($ProcessorArchitecture -Eq "X86") { if(($ProcessorArchitectureW6432 -Eq "") -Or ($ProcessorArchitectureW6432 -Eq "X86")) { return "x86" } $ProcessorArchitecture = $ProcessorArchitectureW6432 } if (($ProcessorArchitecture -Eq "AMD64") -Or ($ProcessorArchitecture -Eq "IA64") -Or ($ProcessorArchitecture -Eq "ARM64") -Or ($ProcessorArchitecture -Eq "LOONGARCH64") -Or ($ProcessorArchitecture -Eq "RISCV64")) { return "x64" } return "x86" } <# .SYNOPSIS Get the name of a temporary folder under the native install directory #> function Get-TempDirectory { return Join-Path (Get-NativeInstallDirectory) "temp/" } function Get-TempPathFilename { [CmdletBinding(PositionalBinding=$false)] Param ( [Parameter(Mandatory=$True)] [string] $Path ) $TempDir = CommonLibrary\Get-TempDirectory $TempFilename = Split-Path $Path -leaf $TempPath = Join-Path $TempDir $TempFilename return $TempPath } <# .SYNOPSIS Returns the base directory to use for native tool installation .NOTES Returns the value of the NETCOREENG_INSTALL_DIRECTORY if that environment variable is set, or otherwise returns an install directory under the %USERPROFILE% #> function Get-NativeInstallDirectory { $InstallDir = $Env:NETCOREENG_INSTALL_DIRECTORY if (!$InstallDir) { $InstallDir = Join-Path $Env:USERPROFILE ".netcoreeng/native/" } return $InstallDir } <# .SYNOPSIS Unzip an archive .DESCRIPTION Powershell module to unzip an archive to a specified directory .PARAMETER ZipPath (Required) Path to archive to unzip .PARAMETER OutputDirectory (Required) Output directory for archive contents .PARAMETER Force Overwrite output directory contents if they already exist .NOTES - Returns True and does not perform an extraction if output directory already exists but Overwrite is not True. - Returns True if unzip operation is successful - Returns False if Overwrite is True and it is unable to remove contents of OutputDirectory - Returns False if unable to extract zip archive #> function Expand-Zip { [CmdletBinding(PositionalBinding=$false)] Param ( [Parameter(Mandatory=$True)] [string] $ZipPath, [Parameter(Mandatory=$True)] [string] $OutputDirectory, [switch] $Force ) Write-Verbose "Extracting '$ZipPath' to '$OutputDirectory'" try { if ((Test-Path $OutputDirectory) -And (-Not $Force)) { Write-Host "Directory '$OutputDirectory' already exists, skipping extract" return $True } if (Test-Path $OutputDirectory) { Write-Verbose "'Force' is 'True', but '$OutputDirectory' exists, removing directory" Remove-Item $OutputDirectory -Force -Recurse if ($? -Eq $False) { Write-Error "Unable to remove '$OutputDirectory'" return $False } } $TempOutputDirectory = Join-Path "$(Split-Path -Parent $OutputDirectory)" "$(Split-Path -Leaf $OutputDirectory).tmp" if (Test-Path $TempOutputDirectory) { Remove-Item $TempOutputDirectory -Force -Recurse } New-Item -Path $TempOutputDirectory -Force -ItemType "Directory" | Out-Null Add-Type -assembly "system.io.compression.filesystem" [io.compression.zipfile]::ExtractToDirectory("$ZipPath", "$TempOutputDirectory") if ($? -Eq $False) { Write-Error "Unable to extract '$ZipPath'" return $False } Move-Item -Path $TempOutputDirectory -Destination $OutputDirectory } catch { Write-Host $_ Write-Host $_.Exception return $False } return $True } export-modulemember -function DownloadAndExtract export-modulemember -function Expand-Zip export-modulemember -function Get-File export-modulemember -function Get-MachineArchitecture export-modulemember -function Get-NativeInstallDirectory export-modulemember -function Get-TempDirectory export-modulemember -function Get-TempPathFilename export-modulemember -function New-ScriptShim ================================================ FILE: eng/common/native/common-library.sh ================================================ #!/usr/bin/env bash function GetNativeInstallDirectory { local install_dir if [[ -z $NETCOREENG_INSTALL_DIRECTORY ]]; then install_dir=$HOME/.netcoreeng/native/ else install_dir=$NETCOREENG_INSTALL_DIRECTORY fi echo $install_dir return 0 } function GetTempDirectory { echo $(GetNativeInstallDirectory)temp/ return 0 } function ExpandZip { local zip_path=$1 local output_directory=$2 local force=${3:-false} echo "Extracting $zip_path to $output_directory" if [[ -d $output_directory ]] && [[ $force = false ]]; then echo "Directory '$output_directory' already exists, skipping extract" return 0 fi if [[ -d $output_directory ]]; then echo "'Force flag enabled, but '$output_directory' exists. Removing directory" rm -rf $output_directory if [[ $? != 0 ]]; then Write-PipelineTelemetryError -category 'NativeToolsBootstrap' "Unable to remove '$output_directory'" return 1 fi fi echo "Creating directory: '$output_directory'" mkdir -p $output_directory echo "Extracting archive" tar -xf $zip_path -C $output_directory if [[ $? != 0 ]]; then Write-PipelineTelemetryError -category 'NativeToolsBootstrap' "Unable to extract '$zip_path'" return 1 fi return 0 } function GetCurrentOS { local unameOut="$(uname -s)" case $unameOut in Linux*) echo "Linux";; Darwin*) echo "MacOS";; esac return 0 } function GetFile { local uri=$1 local path=$2 local force=${3:-false} local download_retries=${4:-5} local retry_wait_time_seconds=${5:-30} if [[ -f $path ]]; then if [[ $force = false ]]; then echo "File '$path' already exists. Skipping download" return 0 else rm -rf $path fi fi if [[ -f $uri ]]; then echo "'$uri' is a file path, copying file to '$path'" cp $uri $path return $? fi echo "Downloading $uri" # Use curl if available, otherwise use wget if command -v curl > /dev/null; then curl "$uri" -sSL --retry $download_retries --retry-delay $retry_wait_time_seconds --create-dirs -o "$path" --fail else wget -q -O "$path" "$uri" --tries="$download_retries" fi return $? } function GetTempPathFileName { local path=$1 local temp_dir=$(GetTempDirectory) local temp_file_name=$(basename $path) echo $temp_dir$temp_file_name return 0 } function DownloadAndExtract { local uri=$1 local installDir=$2 local force=${3:-false} local download_retries=${4:-5} local retry_wait_time_seconds=${5:-30} local temp_tool_path=$(GetTempPathFileName $uri) echo "downloading to: $temp_tool_path" # Download file GetFile "$uri" "$temp_tool_path" $force $download_retries $retry_wait_time_seconds if [[ $? != 0 ]]; then Write-PipelineTelemetryError -category 'NativeToolsBootstrap' "Failed to download '$uri' to '$temp_tool_path'." return 1 fi # Extract File echo "extracting from $temp_tool_path to $installDir" ExpandZip "$temp_tool_path" "$installDir" $force $download_retries $retry_wait_time_seconds if [[ $? != 0 ]]; then Write-PipelineTelemetryError -category 'NativeToolsBootstrap' "Failed to extract '$temp_tool_path' to '$installDir'." return 1 fi return 0 } function NewScriptShim { local shimpath=$1 local tool_file_path=$2 local force=${3:-false} echo "Generating '$shimpath' shim" if [[ -f $shimpath ]]; then if [[ $force = false ]]; then echo "File '$shimpath' already exists." >&2 return 1 else rm -rf $shimpath fi fi if [[ ! -f $tool_file_path ]]; then # try to see if the path is lower cased tool_file_path="$(echo $tool_file_path | tr "[:upper:]" "[:lower:]")" if [[ ! -f $tool_file_path ]]; then Write-PipelineTelemetryError -category 'NativeToolsBootstrap' "Specified tool file path:'$tool_file_path' does not exist" return 1 fi fi local shim_contents=$'#!/usr/bin/env bash\n' shim_contents+="SHIMARGS="$'$1\n' shim_contents+="$tool_file_path"$' $SHIMARGS\n' # Write shim file echo "$shim_contents" > $shimpath chmod +x $shimpath echo "Finished generating shim '$shimpath'" return $? } ================================================ FILE: eng/common/native/init-compiler.sh ================================================ #!/bin/sh # # This file detects the C/C++ compiler and exports it to the CC/CXX environment variables # # NOTE: some scripts source this file and rely on stdout being empty, make sure # to not output *anything* here, unless it is an error message that fails the # build. if [ -z "$build_arch" ] || [ -z "$compiler" ]; then echo "Usage..." echo "build_arch= compiler= init-compiler.sh" echo "Specify the target architecture." echo "Specify the name of compiler (clang or gcc)." exit 1 fi case "$compiler" in clang*|-clang*|--clang*) # clangx.y or clang-x.y version="$(echo "$compiler" | tr -d '[:alpha:]-=')" majorVersion="${version%%.*}" # LLVM based on v18 released in early 2024, with two releases per year maxVersion="$((18 + ((($(date +%Y) - 2024) * 12 + $(date +%-m) - 3) / 6)))" compiler=clang ;; gcc*|-gcc*|--gcc*) # gccx.y or gcc-x.y version="$(echo "$compiler" | tr -d '[:alpha:]-=')" majorVersion="${version%%.*}" # GCC based on v14 released in early 2024, with one release per year maxVersion="$((14 + ((($(date +%Y) - 2024) * 12 + $(date +%-m) - 3) / 12)))" compiler=gcc ;; esac cxxCompiler="$compiler++" # clear the existing CC and CXX from environment CC= CXX= LDFLAGS= if [ "$compiler" = "gcc" ]; then cxxCompiler="g++"; fi check_version_exists() { desired_version=-1 # Set up the environment to be used for building with the desired compiler. if command -v "$compiler-$1" > /dev/null; then desired_version="-$1" elif command -v "$compiler$1" > /dev/null; then desired_version="$1" fi echo "$desired_version" } __baseOS="$(uname)" set_compiler_version_from_CC() { if [ "$__baseOS" = "Darwin" ]; then # On Darwin, the versions from -version/-dumpversion refer to Xcode # versions, not llvm versions, so we can't rely on them. return fi version="$("$CC" -dumpversion)" if [ -z "$version" ]; then echo "Error: $CC -dumpversion didn't provide a version" exit 1 fi # gcc and clang often display 3 part versions. However, gcc can show only 1 part in some environments. IFS=. read -r majorVersion _ < /dev/null; then echo "Error: No compatible version of $compiler was found within the range of $minVersion to $maxVersion. Please upgrade your toolchain or specify the compiler explicitly using CLR_CC and CLR_CXX environment variables." exit 1 fi CC="$(command -v "$compiler" 2> /dev/null)" CXX="$(command -v "$cxxCompiler" 2> /dev/null)" set_compiler_version_from_CC fi else desired_version="$(check_version_exists "$majorVersion")" if [ "$desired_version" = "-1" ]; then echo "Error: Could not find specific version of $compiler: $majorVersion." exit 1 fi fi if [ -z "$CC" ]; then CC="$(command -v "$compiler$desired_version" 2> /dev/null)" CXX="$(command -v "$cxxCompiler$desired_version" 2> /dev/null)" if [ -z "$CXX" ]; then CXX="$(command -v "$cxxCompiler" 2> /dev/null)"; fi set_compiler_version_from_CC fi else if [ ! -f "$CLR_CC" ]; then echo "Error: CLR_CC is set but path '$CLR_CC' does not exist" exit 1 fi CC="$CLR_CC" CXX="$CLR_CXX" set_compiler_version_from_CC fi if [ -z "$CC" ]; then echo "Error: Unable to find $compiler." exit 1 fi if [ "$__baseOS" != "Darwin" ]; then # On Darwin, we always want to use the Apple linker. # Only lld version >= 9 can be considered stable. lld supports s390x starting from 18.0. if [ "$compiler" = "clang" ] && [ -n "$majorVersion" ] && [ "$majorVersion" -ge 9 ] && { [ "$build_arch" != "s390x" ] || [ "$majorVersion" -ge 18 ]; }; then if "$CC" -fuse-ld=lld -Wl,--version >/dev/null 2>&1; then LDFLAGS="-fuse-ld=lld" fi fi fi SCAN_BUILD_COMMAND="$(command -v "scan-build$desired_version" 2> /dev/null)" export CC CXX LDFLAGS SCAN_BUILD_COMMAND ================================================ FILE: eng/common/native/init-distro-rid.sh ================================================ #!/bin/sh # getNonPortableDistroRid # # Input: # targetOs: (str) # targetArch: (str) # rootfsDir: (str) # # Return: # non-portable rid getNonPortableDistroRid() { targetOs="$1" targetArch="$2" rootfsDir="$3" nonPortableRid="" if [ "$targetOs" = "linux" ]; then # shellcheck disable=SC1091 if [ -e "${rootfsDir}/etc/os-release" ]; then . "${rootfsDir}/etc/os-release" if echo "${VERSION_ID:-}" | grep -qE '^([[:digit:]]|\.)+$'; then nonPortableRid="${ID}.${VERSION_ID}-${targetArch}" else # Rolling release distros either do not set VERSION_ID, set it as blank or # set it to non-version looking string (such as TEMPLATE_VERSION_ID on ArchLinux); # so omit it here to be consistent with everything else. nonPortableRid="${ID}-${targetArch}" fi elif [ -e "${rootfsDir}/android_platform" ]; then # shellcheck disable=SC1091 . "${rootfsDir}/android_platform" nonPortableRid="$RID" fi fi if [ "$targetOs" = "freebsd" ]; then # $rootfsDir can be empty. freebsd-version is a shell script and should always work. __freebsd_major_version=$("$rootfsDir"/bin/freebsd-version | cut -d'.' -f1) nonPortableRid="freebsd.$__freebsd_major_version-${targetArch}" elif command -v getprop >/dev/null && getprop ro.product.system.model | grep -qi android; then __android_sdk_version=$(getprop ro.build.version.sdk) nonPortableRid="android.$__android_sdk_version-${targetArch}" elif [ "$targetOs" = "illumos" ]; then __uname_version=$(uname -v) nonPortableRid="illumos-${targetArch}" elif [ "$targetOs" = "solaris" ]; then __uname_version=$(uname -v) __solaris_major_version=$(echo "$__uname_version" | cut -d'.' -f1) nonPortableRid="solaris.$__solaris_major_version-${targetArch}" elif [ "$targetOs" = "haiku" ]; then __uname_release="$(uname -r)" nonPortableRid=haiku.r"$__uname_release"-"$targetArch" fi echo "$nonPortableRid" | tr '[:upper:]' '[:lower:]' } # initDistroRidGlobal # # Input: # os: (str) # arch: (str) # rootfsDir?: (nullable:string) # # Return: # None # # Notes: # It is important to note that the function does not return anything, but it # exports the following variables on success: # __DistroRid : Non-portable rid of the target platform. # __PortableTargetOS : OS-part of the portable rid that corresponds to the target platform. initDistroRidGlobal() { targetOs="$1" targetArch="$2" rootfsDir="" if [ $# -ge 3 ]; then rootfsDir="$3" fi if [ -n "${rootfsDir}" ]; then # We may have a cross build. Check for the existence of the rootfsDir if [ ! -e "${rootfsDir}" ]; then echo "Error: rootfsDir has been passed, but the location is not valid." exit 1 fi fi __DistroRid=$(getNonPortableDistroRid "${targetOs}" "${targetArch}" "${rootfsDir}") if [ -z "${__PortableTargetOS:-}" ]; then __PortableTargetOS="$targetOs" STRINGS="$(command -v strings || true)" if [ -z "$STRINGS" ]; then STRINGS="$(command -v llvm-strings || true)" fi # Check for musl-based distros (e.g. Alpine Linux, Void Linux). if "${rootfsDir}/usr/bin/ldd" --version 2>&1 | grep -q musl || ( [ -n "$STRINGS" ] && "$STRINGS" "${rootfsDir}/usr/bin/ldd" 2>&1 | grep -q musl ); then __PortableTargetOS="linux-musl" fi fi export __DistroRid __PortableTargetOS } ================================================ FILE: eng/common/native/init-os-and-arch.sh ================================================ #!/bin/sh # Use uname to determine what the OS is. OSName=$(uname -s | tr '[:upper:]' '[:lower:]') if command -v getprop && getprop ro.product.system.model 2>&1 | grep -qi android; then OSName="android" fi case "$OSName" in freebsd|linux|netbsd|openbsd|sunos|android|haiku) os="$OSName" ;; darwin) os=osx ;; *) echo "Unsupported OS $OSName detected!" exit 1 ;; esac # On Solaris, `uname -m` is discouraged, see https://docs.oracle.com/cd/E36784_01/html/E36870/uname-1.html # and `uname -p` returns processor type (e.g. i386 on amd64). # The appropriate tool to determine CPU is isainfo(1) https://docs.oracle.com/cd/E36784_01/html/E36870/isainfo-1.html. if [ "$os" = "sunos" ]; then if uname -o 2>&1 | grep -q illumos; then os="illumos" else os="solaris" fi CPUName=$(isainfo -n) else # For the rest of the operating systems, use uname(1) to determine what the CPU is. CPUName=$(uname -m) fi case "$CPUName" in arm64|aarch64) arch=arm64 if [ "$(getconf LONG_BIT)" -lt 64 ]; then # This is 32-bit OS running on 64-bit CPU (for example Raspberry Pi OS) arch=arm fi ;; loongarch64) arch=loongarch64 ;; riscv64) arch=riscv64 ;; amd64|x86_64) arch=x64 ;; armv7l|armv8l) # shellcheck disable=SC1091 if (NAME=""; . /etc/os-release; test "$NAME" = "Tizen"); then arch=armel else arch=arm fi ;; armv6l) arch=armv6 ;; i[3-6]86) echo "Unsupported CPU $CPUName detected, build might not succeed!" arch=x86 ;; s390x) arch=s390x ;; ppc64le) arch=ppc64le ;; *) echo "Unknown CPU $CPUName detected!" exit 1 ;; esac ================================================ FILE: eng/common/native/install-cmake-test.sh ================================================ #!/usr/bin/env bash source="${BASH_SOURCE[0]}" scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" . $scriptroot/common-library.sh base_uri= install_path= version= clean=false force=false download_retries=5 retry_wait_time_seconds=30 while (($# > 0)); do lowerI="$(echo $1 | tr "[:upper:]" "[:lower:]")" case $lowerI in --baseuri) base_uri=$2 shift 2 ;; --installpath) install_path=$2 shift 2 ;; --version) version=$2 shift 2 ;; --clean) clean=true shift 1 ;; --force) force=true shift 1 ;; --downloadretries) download_retries=$2 shift 2 ;; --retrywaittimeseconds) retry_wait_time_seconds=$2 shift 2 ;; --help) echo "Common settings:" echo " --baseuri Base file directory or Url wrom which to acquire tool archives" echo " --installpath Base directory to install native tool to" echo " --clean Don't install the tool, just clean up the current install of the tool" echo " --force Force install of tools even if they previously exist" echo " --help Print help and exit" echo "" echo "Advanced settings:" echo " --downloadretries Total number of retry attempts" echo " --retrywaittimeseconds Wait time between retry attempts in seconds" echo "" exit 0 ;; esac done tool_name="cmake-test" tool_os=$(GetCurrentOS) tool_folder="$(echo $tool_os | tr "[:upper:]" "[:lower:]")" tool_arch="x86_64" tool_name_moniker="$tool_name-$version-$tool_os-$tool_arch" tool_install_directory="$install_path/$tool_name/$version" tool_file_path="$tool_install_directory/$tool_name_moniker/bin/$tool_name" shim_path="$install_path/$tool_name.sh" uri="${base_uri}/$tool_folder/$tool_name/$tool_name_moniker.tar.gz" # Clean up tool and installers if [[ $clean = true ]]; then echo "Cleaning $tool_install_directory" if [[ -d $tool_install_directory ]]; then rm -rf $tool_install_directory fi echo "Cleaning $shim_path" if [[ -f $shim_path ]]; then rm -rf $shim_path fi tool_temp_path=$(GetTempPathFileName $uri) echo "Cleaning $tool_temp_path" if [[ -f $tool_temp_path ]]; then rm -rf $tool_temp_path fi exit 0 fi # Install tool if [[ -f $tool_file_path ]] && [[ $force = false ]]; then echo "$tool_name ($version) already exists, skipping install" exit 0 fi DownloadAndExtract $uri $tool_install_directory $force $download_retries $retry_wait_time_seconds if [[ $? != 0 ]]; then Write-PipelineTelemetryError -category 'NativeToolsBootstrap' 'Installation failed' exit 1 fi # Generate Shim # Always rewrite shims so that we are referencing the expected version NewScriptShim $shim_path $tool_file_path true if [[ $? != 0 ]]; then Write-PipelineTelemetryError -category 'NativeToolsBootstrap' 'Shim generation failed' exit 1 fi exit 0 ================================================ FILE: eng/common/native/install-cmake.sh ================================================ #!/usr/bin/env bash source="${BASH_SOURCE[0]}" scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" . $scriptroot/common-library.sh base_uri= install_path= version= clean=false force=false download_retries=5 retry_wait_time_seconds=30 while (($# > 0)); do lowerI="$(echo $1 | tr "[:upper:]" "[:lower:]")" case $lowerI in --baseuri) base_uri=$2 shift 2 ;; --installpath) install_path=$2 shift 2 ;; --version) version=$2 shift 2 ;; --clean) clean=true shift 1 ;; --force) force=true shift 1 ;; --downloadretries) download_retries=$2 shift 2 ;; --retrywaittimeseconds) retry_wait_time_seconds=$2 shift 2 ;; --help) echo "Common settings:" echo " --baseuri Base file directory or Url wrom which to acquire tool archives" echo " --installpath Base directory to install native tool to" echo " --clean Don't install the tool, just clean up the current install of the tool" echo " --force Force install of tools even if they previously exist" echo " --help Print help and exit" echo "" echo "Advanced settings:" echo " --downloadretries Total number of retry attempts" echo " --retrywaittimeseconds Wait time between retry attempts in seconds" echo "" exit 0 ;; esac done tool_name="cmake" tool_os=$(GetCurrentOS) tool_folder="$(echo $tool_os | tr "[:upper:]" "[:lower:]")" tool_arch="x86_64" tool_name_moniker="$tool_name-$version-$tool_os-$tool_arch" tool_install_directory="$install_path/$tool_name/$version" tool_file_path="$tool_install_directory/$tool_name_moniker/bin/$tool_name" shim_path="$install_path/$tool_name.sh" uri="${base_uri}/$tool_folder/$tool_name/$tool_name_moniker.tar.gz" # Clean up tool and installers if [[ $clean = true ]]; then echo "Cleaning $tool_install_directory" if [[ -d $tool_install_directory ]]; then rm -rf $tool_install_directory fi echo "Cleaning $shim_path" if [[ -f $shim_path ]]; then rm -rf $shim_path fi tool_temp_path=$(GetTempPathFileName $uri) echo "Cleaning $tool_temp_path" if [[ -f $tool_temp_path ]]; then rm -rf $tool_temp_path fi exit 0 fi # Install tool if [[ -f $tool_file_path ]] && [[ $force = false ]]; then echo "$tool_name ($version) already exists, skipping install" exit 0 fi DownloadAndExtract $uri $tool_install_directory $force $download_retries $retry_wait_time_seconds if [[ $? != 0 ]]; then Write-PipelineTelemetryError -category 'NativeToolsBootstrap' 'Installation failed' exit 1 fi # Generate Shim # Always rewrite shims so that we are referencing the expected version NewScriptShim $shim_path $tool_file_path true if [[ $? != 0 ]]; then Write-PipelineTelemetryError -category 'NativeToolsBootstrap' 'Shim generation failed' exit 1 fi exit 0 ================================================ FILE: eng/common/native/install-dependencies.sh ================================================ #!/bin/sh set -e # This is a simple script primarily used for CI to install necessary dependencies # # Usage: # # ./install-dependencies.sh os="$(echo "$1" | tr "[:upper:]" "[:lower:]")" if [ -z "$os" ]; then . "$(dirname "$0")"/init-os-and-arch.sh fi case "$os" in linux) if [ -e /etc/os-release ]; then . /etc/os-release fi if [ "$ID" = "debian" ] || [ "$ID_LIKE" = "debian" ]; then apt update apt install -y build-essential gettext locales cmake llvm clang lld lldb liblldb-dev libunwind8-dev libicu-dev liblttng-ust-dev \ libssl-dev libkrb5-dev pigz cpio ninja-build localedef -i en_US -c -f UTF-8 -A /usr/share/locale/locale.alias en_US.UTF-8 elif [ "$ID" = "fedora" ] || [ "$ID" = "rhel" ] || [ "$ID" = "azurelinux" ] || [ "$ID" = "centos" ]; then pkg_mgr="$(command -v tdnf 2>/dev/null || command -v dnf)" $pkg_mgr install -y cmake llvm lld lldb clang python curl libicu-devel openssl-devel krb5-devel lttng-ust-devel pigz cpio ninja-build elif [ "$ID" = "amzn" ]; then dnf install -y cmake llvm lld lldb clang python libicu-devel openssl-devel krb5-devel lttng-ust-devel pigz cpio ninja-build elif [ "$ID" = "alpine" ]; then apk add build-base cmake bash curl clang llvm-dev lld lldb krb5-dev lttng-ust-dev icu-dev openssl-dev pigz cpio ninja else echo "Unsupported distro. distro: $ID" exit 1 fi ;; osx|maccatalyst|ios|iossimulator|tvos|tvossimulator) echo "Installed xcode version: $(xcode-select -p)" export HOMEBREW_NO_INSTALL_CLEANUP=1 export HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=1 # Skip brew update for now, see https://github.com/actions/setup-python/issues/577 # brew update --preinstall brew bundle --no-upgrade --file=- < [CmdletBinding(PositionalBinding=$false)] Param ( [Parameter(Mandatory=$True)] [string] $ToolName, [Parameter(Mandatory=$True)] [string] $InstallPath, [Parameter(Mandatory=$True)] [string] $BaseUri, [Parameter(Mandatory=$True)] [string] $Version, [string] $CommonLibraryDirectory = $PSScriptRoot, [switch] $Force = $False, [switch] $Clean = $False, [int] $DownloadRetries = 5, [int] $RetryWaitTimeInSeconds = 30 ) . $PSScriptRoot\..\pipeline-logging-functions.ps1 # Import common library modules Import-Module -Name (Join-Path $CommonLibraryDirectory "CommonLibrary.psm1") try { # Define verbose switch if undefined $Verbose = $VerbosePreference -Eq "Continue" $Arch = CommonLibrary\Get-MachineArchitecture $ToolOs = "win64" if($Arch -Eq "x32") { $ToolOs = "win32" } $ToolNameMoniker = "$ToolName-$Version-$ToolOs-$Arch" $ToolInstallDirectory = Join-Path $InstallPath "$ToolName\$Version\" $Uri = "$BaseUri/windows/$ToolName/$ToolNameMoniker.zip" $ShimPath = Join-Path $InstallPath "$ToolName.exe" if ($Clean) { Write-Host "Cleaning $ToolInstallDirectory" if (Test-Path $ToolInstallDirectory) { Remove-Item $ToolInstallDirectory -Force -Recurse } Write-Host "Cleaning $ShimPath" if (Test-Path $ShimPath) { Remove-Item $ShimPath -Force } $ToolTempPath = CommonLibrary\Get-TempPathFilename -Path $Uri Write-Host "Cleaning $ToolTempPath" if (Test-Path $ToolTempPath) { Remove-Item $ToolTempPath -Force } exit 0 } # Install tool if ((Test-Path $ToolInstallDirectory) -And (-Not $Force)) { Write-Verbose "$ToolName ($Version) already exists, skipping install" } else { $InstallStatus = CommonLibrary\DownloadAndExtract -Uri $Uri ` -InstallDirectory $ToolInstallDirectory ` -Force:$Force ` -DownloadRetries $DownloadRetries ` -RetryWaitTimeInSeconds $RetryWaitTimeInSeconds ` -Verbose:$Verbose if ($InstallStatus -Eq $False) { Write-PipelineTelemetryError "Installation failed" -Category "NativeToolsetBootstrapping" exit 1 } } $ToolFilePath = Get-ChildItem $ToolInstallDirectory -Recurse -Filter "$ToolName.exe" | % { $_.FullName } if (@($ToolFilePath).Length -Gt 1) { Write-Error "There are multiple copies of $ToolName in $($ToolInstallDirectory): `n$(@($ToolFilePath | out-string))" exit 1 } elseif (@($ToolFilePath).Length -Lt 1) { Write-Host "$ToolName was not found in $ToolInstallDirectory." exit 1 } # Generate shim # Always rewrite shims so that we are referencing the expected version $GenerateShimStatus = CommonLibrary\New-ScriptShim -ShimName $ToolName ` -ShimDirectory $InstallPath ` -ToolFilePath "$ToolFilePath" ` -BaseUri $BaseUri ` -Force:$Force ` -Verbose:$Verbose if ($GenerateShimStatus -Eq $False) { Write-PipelineTelemetryError "Generate shim failed" -Category "NativeToolsetBootstrapping" return 1 } exit 0 } catch { Write-Host $_.ScriptStackTrace Write-PipelineTelemetryError -Category "NativeToolsetBootstrapping" -Message $_ exit 1 } ================================================ FILE: eng/common/pipeline-logging-functions.ps1 ================================================ # Source for this file was taken from https://github.com/microsoft/azure-pipelines-task-lib/blob/11c9439d4af17e6475d9fe058e6b2e03914d17e6/powershell/VstsTaskSdk/LoggingCommandFunctions.ps1 and modified. # NOTE: You should not be calling these method directly as they are likely to change. Instead you should be calling the Write-Pipeline* functions defined in tools.ps1 $script:loggingCommandPrefix = '##vso[' $script:loggingCommandEscapeMappings = @( # TODO: WHAT ABOUT "="? WHAT ABOUT "%"? New-Object psobject -Property @{ Token = ';' ; Replacement = '%3B' } New-Object psobject -Property @{ Token = "`r" ; Replacement = '%0D' } New-Object psobject -Property @{ Token = "`n" ; Replacement = '%0A' } New-Object psobject -Property @{ Token = "]" ; Replacement = '%5D' } ) # TODO: BUG: Escape % ??? # TODO: Add test to verify don't need to escape "=". # Specify "-Force" to force pipeline formatted output even if "$ci" is false or not set function Write-PipelineTelemetryError { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$Category, [Parameter(Mandatory = $true)] [string]$Message, [Parameter(Mandatory = $false)] [string]$Type = 'error', [string]$ErrCode, [string]$SourcePath, [string]$LineNumber, [string]$ColumnNumber, [switch]$AsOutput, [switch]$Force) $PSBoundParameters.Remove('Category') | Out-Null if ($Force -Or ((Test-Path variable:ci) -And $ci)) { $Message = "(NETCORE_ENGINEERING_TELEMETRY=$Category) $Message" } $PSBoundParameters.Remove('Message') | Out-Null $PSBoundParameters.Add('Message', $Message) Write-PipelineTaskError @PSBoundParameters } # Specify "-Force" to force pipeline formatted output even if "$ci" is false or not set function Write-PipelineTaskError { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$Message, [Parameter(Mandatory = $false)] [string]$Type = 'error', [string]$ErrCode, [string]$SourcePath, [string]$LineNumber, [string]$ColumnNumber, [switch]$AsOutput, [switch]$Force ) if (!$Force -And (-Not (Test-Path variable:ci) -Or !$ci)) { if ($Type -eq 'error') { Write-Host $Message -ForegroundColor Red return } elseif ($Type -eq 'warning') { Write-Host $Message -ForegroundColor Yellow return } } if (($Type -ne 'error') -and ($Type -ne 'warning')) { Write-Host $Message return } $PSBoundParameters.Remove('Force') | Out-Null if (-not $PSBoundParameters.ContainsKey('Type')) { $PSBoundParameters.Add('Type', 'error') } Write-LogIssue @PSBoundParameters } function Write-PipelineSetVariable { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$Name, [string]$Value, [switch]$Secret, [switch]$AsOutput, [bool]$IsMultiJobVariable = $true) if ((Test-Path variable:ci) -And $ci) { Write-LoggingCommand -Area 'task' -Event 'setvariable' -Data $Value -Properties @{ 'variable' = $Name 'isSecret' = $Secret 'isOutput' = $IsMultiJobVariable } -AsOutput:$AsOutput } } function Write-PipelinePrependPath { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$Path, [switch]$AsOutput) if ((Test-Path variable:ci) -And $ci) { Write-LoggingCommand -Area 'task' -Event 'prependpath' -Data $Path -AsOutput:$AsOutput } } function Write-PipelineSetResult { [CmdletBinding()] param( [ValidateSet("Succeeded", "SucceededWithIssues", "Failed", "Cancelled", "Skipped")] [Parameter(Mandatory = $true)] [string]$Result, [string]$Message) if ((Test-Path variable:ci) -And $ci) { Write-LoggingCommand -Area 'task' -Event 'complete' -Data $Message -Properties @{ 'result' = $Result } } } <######################################## # Private functions. ########################################> function Format-LoggingCommandData { [CmdletBinding()] param([string]$Value, [switch]$Reverse) if (!$Value) { return '' } if (!$Reverse) { foreach ($mapping in $script:loggingCommandEscapeMappings) { $Value = $Value.Replace($mapping.Token, $mapping.Replacement) } } else { for ($i = $script:loggingCommandEscapeMappings.Length - 1 ; $i -ge 0 ; $i--) { $mapping = $script:loggingCommandEscapeMappings[$i] $Value = $Value.Replace($mapping.Replacement, $mapping.Token) } } return $Value } function Format-LoggingCommand { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$Area, [Parameter(Mandatory = $true)] [string]$Event, [string]$Data, [hashtable]$Properties) # Append the preamble. [System.Text.StringBuilder]$sb = New-Object -TypeName System.Text.StringBuilder $null = $sb.Append($script:loggingCommandPrefix).Append($Area).Append('.').Append($Event) # Append the properties. if ($Properties) { $first = $true foreach ($key in $Properties.Keys) { [string]$value = Format-LoggingCommandData $Properties[$key] if ($value) { if ($first) { $null = $sb.Append(' ') $first = $false } else { $null = $sb.Append(';') } $null = $sb.Append("$key=$value") } } } # Append the tail and output the value. $Data = Format-LoggingCommandData $Data $sb.Append(']').Append($Data).ToString() } function Write-LoggingCommand { [CmdletBinding(DefaultParameterSetName = 'Parameters')] param( [Parameter(Mandatory = $true, ParameterSetName = 'Parameters')] [string]$Area, [Parameter(Mandatory = $true, ParameterSetName = 'Parameters')] [string]$Event, [Parameter(ParameterSetName = 'Parameters')] [string]$Data, [Parameter(ParameterSetName = 'Parameters')] [hashtable]$Properties, [Parameter(Mandatory = $true, ParameterSetName = 'Object')] $Command, [switch]$AsOutput) if ($PSCmdlet.ParameterSetName -eq 'Object') { Write-LoggingCommand -Area $Command.Area -Event $Command.Event -Data $Command.Data -Properties $Command.Properties -AsOutput:$AsOutput return } $command = Format-LoggingCommand -Area $Area -Event $Event -Data $Data -Properties $Properties if ($AsOutput) { $command } else { Write-Host $command } } function Write-LogIssue { [CmdletBinding()] param( [ValidateSet('warning', 'error')] [Parameter(Mandatory = $true)] [string]$Type, [string]$Message, [string]$ErrCode, [string]$SourcePath, [string]$LineNumber, [string]$ColumnNumber, [switch]$AsOutput) $command = Format-LoggingCommand -Area 'task' -Event 'logissue' -Data $Message -Properties @{ 'type' = $Type 'code' = $ErrCode 'sourcepath' = $SourcePath 'linenumber' = $LineNumber 'columnnumber' = $ColumnNumber } if ($AsOutput) { return $command } if ($Type -eq 'error') { $foregroundColor = $host.PrivateData.ErrorForegroundColor $backgroundColor = $host.PrivateData.ErrorBackgroundColor if ($foregroundColor -isnot [System.ConsoleColor] -or $backgroundColor -isnot [System.ConsoleColor]) { $foregroundColor = [System.ConsoleColor]::Red $backgroundColor = [System.ConsoleColor]::Black } } else { $foregroundColor = $host.PrivateData.WarningForegroundColor $backgroundColor = $host.PrivateData.WarningBackgroundColor if ($foregroundColor -isnot [System.ConsoleColor] -or $backgroundColor -isnot [System.ConsoleColor]) { $foregroundColor = [System.ConsoleColor]::Yellow $backgroundColor = [System.ConsoleColor]::Black } } Write-Host $command -ForegroundColor $foregroundColor -BackgroundColor $backgroundColor } ================================================ FILE: eng/common/pipeline-logging-functions.sh ================================================ #!/usr/bin/env bash function Write-PipelineTelemetryError { local telemetry_category='' local force=false local function_args=() local message='' while [[ $# -gt 0 ]]; do opt="$(echo "${1/#--/-}" | tr "[:upper:]" "[:lower:]")" case "$opt" in -category|-c) telemetry_category=$2 shift ;; -force|-f) force=true ;; -*) function_args+=("$1 $2") shift ;; *) message=$* ;; esac shift done if [[ $force != true ]] && [[ "$ci" != true ]]; then echo "$message" >&2 return fi if [[ $force == true ]]; then function_args+=("-force") fi message="(NETCORE_ENGINEERING_TELEMETRY=$telemetry_category) $message" function_args+=("$message") Write-PipelineTaskError ${function_args[@]} } function Write-PipelineTaskError { local message_type="error" local sourcepath='' local linenumber='' local columnnumber='' local error_code='' local force=false while [[ $# -gt 0 ]]; do opt="$(echo "${1/#--/-}" | tr "[:upper:]" "[:lower:]")" case "$opt" in -type|-t) message_type=$2 shift ;; -sourcepath|-s) sourcepath=$2 shift ;; -linenumber|-ln) linenumber=$2 shift ;; -columnnumber|-cn) columnnumber=$2 shift ;; -errcode|-e) error_code=$2 shift ;; -force|-f) force=true ;; *) break ;; esac shift done if [[ $force != true ]] && [[ "$ci" != true ]]; then echo "$@" >&2 return fi local message="##vso[task.logissue" message="$message type=$message_type" if [ -n "$sourcepath" ]; then message="$message;sourcepath=$sourcepath" fi if [ -n "$linenumber" ]; then message="$message;linenumber=$linenumber" fi if [ -n "$columnnumber" ]; then message="$message;columnnumber=$columnnumber" fi if [ -n "$error_code" ]; then message="$message;code=$error_code" fi message="$message]$*" echo "$message" } function Write-PipelineSetVariable { if [[ "$ci" != true ]]; then return fi local name='' local value='' local secret=false local as_output=false local is_multi_job_variable=true while [[ $# -gt 0 ]]; do opt="$(echo "${1/#--/-}" | tr "[:upper:]" "[:lower:]")" case "$opt" in -name|-n) name=$2 shift ;; -value|-v) value=$2 shift ;; -secret|-s) secret=true ;; -as_output|-a) as_output=true ;; -is_multi_job_variable|-i) is_multi_job_variable=$2 shift ;; esac shift done value=${value/;/%3B} value=${value/\\r/%0D} value=${value/\\n/%0A} value=${value/]/%5D} local message="##vso[task.setvariable variable=$name;isSecret=$secret;isOutput=$is_multi_job_variable]$value" if [[ "$as_output" == true ]]; then $message else echo "$message" fi } function Write-PipelinePrependPath { local prepend_path='' while [[ $# -gt 0 ]]; do opt="$(echo "${1/#--/-}" | tr "[:upper:]" "[:lower:]")" case "$opt" in -path|-p) prepend_path=$2 shift ;; esac shift done export PATH="$prepend_path:$PATH" if [[ "$ci" == true ]]; then echo "##vso[task.prependpath]$prepend_path" fi } function Write-PipelineSetResult { local result='' local message='' while [[ $# -gt 0 ]]; do opt="$(echo "${1/#--/-}" | tr "[:upper:]" "[:lower:]")" case "$opt" in -result|-r) result=$2 shift ;; -message|-m) message=$2 shift ;; esac shift done if [[ "$ci" == true ]]; then echo "##vso[task.complete result=$result;]$message" fi } ================================================ FILE: eng/common/post-build/check-channel-consistency.ps1 ================================================ param( [Parameter(Mandatory=$true)][string] $PromoteToChannels, # List of channels that the build should be promoted to [Parameter(Mandatory=$true)][array] $AvailableChannelIds # List of channel IDs available in the YAML implementation ) try { $ErrorActionPreference = 'Stop' Set-StrictMode -Version 2.0 # `tools.ps1` checks $ci to perform some actions. Since the post-build # scripts don't necessarily execute in the same agent that run the # build.ps1/sh script this variable isn't automatically set. $ci = $true $disableConfigureToolsetImport = $true . $PSScriptRoot\..\tools.ps1 if ($PromoteToChannels -eq "") { Write-PipelineTaskError -Type 'warning' -Message "This build won't publish assets as it's not configured to any Maestro channel. If that wasn't intended use Darc to configure a default channel using add-default-channel for this branch or to promote it to a channel using add-build-to-channel. See https://github.com/dotnet/arcade/blob/main/Documentation/Darc.md#assigning-an-individual-build-to-a-channel for more info." ExitWithExitCode 0 } # Check that every channel that Maestro told to promote the build to # is available in YAML $PromoteToChannelsIds = $PromoteToChannels -split "\D" | Where-Object { $_ } $hasErrors = $false foreach ($id in $PromoteToChannelsIds) { if (($id -ne 0) -and ($id -notin $AvailableChannelIds)) { Write-PipelineTaskError -Message "Channel $id is not present in the post-build YAML configuration! This is an error scenario. Please contact @dnceng." $hasErrors = $true } } # The `Write-PipelineTaskError` doesn't error the script and we might report several errors # in the previous lines. The check below makes sure that we return an error state from the # script if we reported any validation error if ($hasErrors) { ExitWithExitCode 1 } Write-Host 'done.' } catch { Write-Host $_ Write-PipelineTelemetryError -Category 'CheckChannelConsistency' -Message "There was an error while trying to check consistency of Maestro default channels for the build and post-build YAML configuration." ExitWithExitCode 1 } ================================================ FILE: eng/common/post-build/nuget-validation.ps1 ================================================ # This script validates NuGet package metadata information using this # tool: https://github.com/NuGet/NuGetGallery/tree/jver-verify/src/VerifyMicrosoftPackage param( [Parameter(Mandatory=$true)][string] $PackagesPath # Path to where the packages to be validated are ) # `tools.ps1` checks $ci to perform some actions. Since the post-build # scripts don't necessarily execute in the same agent that run the # build.ps1/sh script this variable isn't automatically set. $ci = $true $disableConfigureToolsetImport = $true . $PSScriptRoot\..\tools.ps1 try { & $PSScriptRoot\nuget-verification.ps1 ${PackagesPath}\*.nupkg } catch { Write-Host $_.ScriptStackTrace Write-PipelineTelemetryError -Category 'NuGetValidation' -Message $_ ExitWithExitCode 1 } ================================================ FILE: eng/common/post-build/nuget-verification.ps1 ================================================ <# .SYNOPSIS Verifies that Microsoft NuGet packages have proper metadata. .DESCRIPTION Downloads a verification tool and runs metadata validation on the provided NuGet packages. This script writes an error if any of the provided packages fail validation. All arguments provided to this PowerShell script that do not match PowerShell parameters are passed on to the verification tool downloaded during the execution of this script. .PARAMETER NuGetExePath The path to the nuget.exe binary to use. If not provided, nuget.exe will be downloaded into the -DownloadPath directory. .PARAMETER PackageSource The package source to use to download the verification tool. If not provided, nuget.org will be used. .PARAMETER DownloadPath The directory path to download the verification tool and nuget.exe to. If not provided, %TEMP%\NuGet.VerifyNuGetPackage will be used. .PARAMETER args Arguments that will be passed to the verification tool. .EXAMPLE PS> .\verify.ps1 *.nupkg Verifies the metadata of all .nupkg files in the currect working directory. .EXAMPLE PS> .\verify.ps1 --help Displays the help text of the downloaded verifiction tool. .LINK https://github.com/NuGet/NuGetGallery/blob/master/src/VerifyMicrosoftPackage/README.md #> # This script was copied from https://github.com/NuGet/NuGetGallery/blob/3e25ad135146676bcab0050a516939d9958bfa5d/src/VerifyMicrosoftPackage/verify.ps1 [CmdletBinding(PositionalBinding = $false)] param( [string]$NuGetExePath, [string]$PackageSource = "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public/nuget/v3/index.json", [string]$DownloadPath, [Parameter(ValueFromRemainingArguments = $true)] [string[]]$args ) # The URL to download nuget.exe. $nugetExeUrl = "https://dist.nuget.org/win-x86-commandline/v4.9.4/nuget.exe" # The package ID of the verification tool. $packageId = "NuGet.VerifyMicrosoftPackage" # The location that nuget.exe and the verification tool will be downloaded to. if (!$DownloadPath) { $DownloadPath = (Join-Path $env:TEMP "NuGet.VerifyMicrosoftPackage") } $fence = New-Object -TypeName string -ArgumentList '=', 80 # Create the download directory, if it doesn't already exist. if (!(Test-Path $DownloadPath)) { New-Item -ItemType Directory $DownloadPath | Out-Null } Write-Host "Using download path: $DownloadPath" if ($NuGetExePath) { $nuget = $NuGetExePath } else { $downloadedNuGetExe = Join-Path $DownloadPath "nuget.exe" # Download nuget.exe, if it doesn't already exist. if (!(Test-Path $downloadedNuGetExe)) { Write-Host "Downloading nuget.exe from $nugetExeUrl..." $ProgressPreference = 'SilentlyContinue' try { Invoke-WebRequest $nugetExeUrl -UseBasicParsing -OutFile $downloadedNuGetExe $ProgressPreference = 'Continue' } catch { $ProgressPreference = 'Continue' Write-Error $_ Write-Error "nuget.exe failed to download." exit } } $nuget = $downloadedNuGetExe } Write-Host "Using nuget.exe path: $nuget" Write-Host " " # Download the latest version of the verification tool. Write-Host "Downloading the latest version of $packageId from $packageSource..." Write-Host $fence & $nuget install $packageId ` -Prerelease ` -OutputDirectory $DownloadPath ` -Source $PackageSource Write-Host $fence Write-Host " " if ($LASTEXITCODE -ne 0) { Write-Error "nuget.exe failed to fetch the verify tool." exit } # Find the most recently downloaded tool Write-Host "Finding the most recently downloaded verification tool." $verifyProbePath = Join-Path $DownloadPath "$packageId.*" $verifyPath = Get-ChildItem -Path $verifyProbePath -Directory ` | Sort-Object -Property LastWriteTime -Descending ` | Select-Object -First 1 $verify = Join-Path $verifyPath "tools\NuGet.VerifyMicrosoftPackage.exe" Write-Host "Using verification tool: $verify" Write-Host " " # Execute the verification tool. Write-Host "Executing the verify tool..." Write-Host $fence & $verify $args Write-Host $fence Write-Host " " # Respond to the exit code. if ($LASTEXITCODE -ne 0) { Write-Error "The verify tool found some problems." } else { Write-Output "The verify tool succeeded." } ================================================ FILE: eng/common/post-build/publish-using-darc.ps1 ================================================ param( [Parameter(Mandatory=$true)][int] $BuildId, [Parameter(Mandatory=$true)][int] $PublishingInfraVersion, [Parameter(Mandatory=$true)][string] $AzdoToken, [Parameter(Mandatory=$false)][string] $MaestroApiEndPoint = 'https://maestro.dot.net', [Parameter(Mandatory=$true)][string] $WaitPublishingFinish, [Parameter(Mandatory=$false)][string] $ArtifactsPublishingAdditionalParameters, [Parameter(Mandatory=$false)][string] $SymbolPublishingAdditionalParameters, [Parameter(Mandatory=$false)][string] $RequireDefaultChannels, [Parameter(Mandatory=$false)][string] $SkipAssetsPublishing, [Parameter(Mandatory=$false)][string] $runtimeSourceFeed, [Parameter(Mandatory=$false)][string] $runtimeSourceFeedKey ) try { # `tools.ps1` checks $ci to perform some actions. Since the post-build # scripts don't necessarily execute in the same agent that run the # build.ps1/sh script this variable isn't automatically set. $ci = $true $disableConfigureToolsetImport = $true . $PSScriptRoot\..\tools.ps1 $darc = Get-Darc $optionalParams = [System.Collections.ArrayList]::new() if ("" -ne $ArtifactsPublishingAdditionalParameters) { $optionalParams.Add("--artifact-publishing-parameters") | Out-Null $optionalParams.Add($ArtifactsPublishingAdditionalParameters) | Out-Null } if ("" -ne $SymbolPublishingAdditionalParameters) { $optionalParams.Add("--symbol-publishing-parameters") | Out-Null $optionalParams.Add($SymbolPublishingAdditionalParameters) | Out-Null } if ("false" -eq $WaitPublishingFinish) { $optionalParams.Add("--no-wait") | Out-Null } if ("true" -eq $RequireDefaultChannels) { $optionalParams.Add("--default-channels-required") | Out-Null } if ("true" -eq $SkipAssetsPublishing) { $optionalParams.Add("--skip-assets-publishing") | Out-Null } & $darc add-build-to-channel ` --id $buildId ` --publishing-infra-version $PublishingInfraVersion ` --default-channels ` --source-branch main ` --azdev-pat "$AzdoToken" ` --bar-uri "$MaestroApiEndPoint" ` --ci ` --verbose ` @optionalParams if ($LastExitCode -ne 0) { Write-Host "Problems using Darc to promote build ${buildId} to default channels. Stopping execution..." exit 1 } Write-Host 'done.' } catch { Write-Host $_ Write-PipelineTelemetryError -Category 'PromoteBuild' -Message "There was an error while trying to publish build '$BuildId' to default channels." ExitWithExitCode 1 } ================================================ FILE: eng/common/post-build/redact-logs.ps1 ================================================ [CmdletBinding(PositionalBinding=$False)] param( [Parameter(Mandatory=$true, Position=0)][string] $InputPath, [Parameter(Mandatory=$true)][string] $BinlogToolVersion, [Parameter(Mandatory=$false)][string] $DotnetPath, [Parameter(Mandatory=$false)][string] $PackageFeed = 'https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public/nuget/v3/index.json', # File with strings to redact - separated by newlines. # For comments start the line with '# ' - such lines are ignored [Parameter(Mandatory=$false)][string] $TokensFilePath, [Parameter(ValueFromRemainingArguments=$true)][String[]]$TokensToRedact, [Parameter(Mandatory=$false)][string] $runtimeSourceFeed, [Parameter(Mandatory=$false)][string] $runtimeSourceFeedKey ) try { $ErrorActionPreference = 'Stop' Set-StrictMode -Version 2.0 # `tools.ps1` checks $ci to perform some actions. Since the post-build # scripts don't necessarily execute in the same agent that run the # build.ps1/sh script this variable isn't automatically set. $ci = $true $disableConfigureToolsetImport = $true . $PSScriptRoot\..\tools.ps1 $packageName = 'binlogtool' $dotnet = $DotnetPath if (!$dotnet) { $dotnetRoot = InitializeDotNetCli -install:$true $dotnet = "$dotnetRoot\dotnet.exe" } $toolList = & "$dotnet" tool list -g if ($toolList -like "*$packageName*") { & "$dotnet" tool uninstall $packageName -g } $toolPath = "$PSScriptRoot\..\..\..\.tools" $verbosity = 'minimal' New-Item -ItemType Directory -Force -Path $toolPath Push-Location -Path $toolPath try { Write-Host "Installing Binlog redactor CLI..." Write-Host "'$dotnet' new tool-manifest" & "$dotnet" new tool-manifest Write-Host "'$dotnet' tool install $packageName --local --add-source '$PackageFeed' -v $verbosity --version $BinlogToolVersion" & "$dotnet" tool install $packageName --local --add-source "$PackageFeed" -v $verbosity --version $BinlogToolVersion if (Test-Path $TokensFilePath) { Write-Host "Adding additional sensitive data for redaction from file: " $TokensFilePath $TokensToRedact += Get-Content -Path $TokensFilePath | Foreach {$_.Trim()} | Where { $_ -notmatch "^# " } } $optionalParams = [System.Collections.ArrayList]::new() Foreach ($p in $TokensToRedact) { if($p -match '^\$\(.*\)$') { Write-Host ("Ignoring token {0} as it is probably unexpanded AzDO variable" -f $p) } elseif($p) { $optionalParams.Add("-p:" + $p) | Out-Null } } & $dotnet binlogtool redact --input:$InputPath --recurse --in-place ` @optionalParams if ($LastExitCode -ne 0) { Write-PipelineTelemetryError -Category 'Redactor' -Type 'warning' -Message "Problems using Redactor tool (exit code: $LastExitCode). But ignoring them now." } } finally { Pop-Location } Write-Host 'done.' } catch { Write-Host $_ Write-PipelineTelemetryError -Category 'Redactor' -Message "There was an error while trying to redact logs. Error: $_" ExitWithExitCode 1 } ================================================ FILE: eng/common/post-build/sourcelink-validation.ps1 ================================================ param( [Parameter(Mandatory=$true)][string] $InputPath, # Full path to directory where Symbols.NuGet packages to be checked are stored [Parameter(Mandatory=$true)][string] $ExtractPath, # Full path to directory where the packages will be extracted during validation [Parameter(Mandatory=$false)][string] $GHRepoName, # GitHub name of the repo including the Org. E.g., dotnet/arcade [Parameter(Mandatory=$false)][string] $GHCommit, # GitHub commit SHA used to build the packages [Parameter(Mandatory=$true)][string] $SourcelinkCliVersion # Version of SourceLink CLI to use ) $ErrorActionPreference = 'Stop' Set-StrictMode -Version 2.0 # `tools.ps1` checks $ci to perform some actions. Since the post-build # scripts don't necessarily execute in the same agent that run the # build.ps1/sh script this variable isn't automatically set. $ci = $true $disableConfigureToolsetImport = $true . $PSScriptRoot\..\tools.ps1 # Cache/HashMap (File -> Exist flag) used to consult whether a file exist # in the repository at a specific commit point. This is populated by inserting # all files present in the repo at a specific commit point. $global:RepoFiles = @{} # Maximum number of jobs to run in parallel $MaxParallelJobs = 16 $MaxRetries = 5 $RetryWaitTimeInSeconds = 30 # Wait time between check for system load $SecondsBetweenLoadChecks = 10 if (!$InputPath -or !(Test-Path $InputPath)){ Write-Host "No files to validate." ExitWithExitCode 0 } $ValidatePackage = { param( [string] $PackagePath # Full path to a Symbols.NuGet package ) . $using:PSScriptRoot\..\tools.ps1 # Ensure input file exist if (!(Test-Path $PackagePath)) { Write-Host "Input file does not exist: $PackagePath" return [pscustomobject]@{ result = 1 packagePath = $PackagePath } } # Extensions for which we'll look for SourceLink information # For now we'll only care about Portable & Embedded PDBs $RelevantExtensions = @('.dll', '.exe', '.pdb') Write-Host -NoNewLine 'Validating ' ([System.IO.Path]::GetFileName($PackagePath)) '...' $PackageId = [System.IO.Path]::GetFileNameWithoutExtension($PackagePath) $ExtractPath = Join-Path -Path $using:ExtractPath -ChildPath $PackageId $FailedFiles = 0 Add-Type -AssemblyName System.IO.Compression.FileSystem [System.IO.Directory]::CreateDirectory($ExtractPath) | Out-Null try { $zip = [System.IO.Compression.ZipFile]::OpenRead($PackagePath) $zip.Entries | Where-Object {$RelevantExtensions -contains [System.IO.Path]::GetExtension($_.Name)} | ForEach-Object { $FileName = $_.FullName $Extension = [System.IO.Path]::GetExtension($_.Name) $FakeName = -Join((New-Guid), $Extension) $TargetFile = Join-Path -Path $ExtractPath -ChildPath $FakeName # We ignore resource DLLs if ($FileName.EndsWith('.resources.dll')) { return [pscustomobject]@{ result = 0 packagePath = $PackagePath } } [System.IO.Compression.ZipFileExtensions]::ExtractToFile($_, $TargetFile, $true) $ValidateFile = { param( [string] $FullPath, # Full path to the module that has to be checked [string] $RealPath, [ref] $FailedFiles ) $sourcelinkExe = "$env:USERPROFILE\.dotnet\tools" $sourcelinkExe = Resolve-Path "$sourcelinkExe\sourcelink.exe" $SourceLinkInfos = & $sourcelinkExe print-urls $FullPath | Out-String if ($LASTEXITCODE -eq 0 -and -not ([string]::IsNullOrEmpty($SourceLinkInfos))) { $NumFailedLinks = 0 # We only care about Http addresses $Matches = (Select-String '(http[s]?)(:\/\/)([^\s,]+)' -Input $SourceLinkInfos -AllMatches).Matches if ($Matches.Count -ne 0) { $Matches.Value | ForEach-Object { $Link = $_ $CommitUrl = "https://raw.githubusercontent.com/${using:GHRepoName}/${using:GHCommit}/" $FilePath = $Link.Replace($CommitUrl, "") $Status = 200 $Cache = $using:RepoFiles $attempts = 0 while ($attempts -lt $using:MaxRetries) { if ( !($Cache.ContainsKey($FilePath)) ) { try { $Uri = $Link -as [System.URI] if ($Link -match "submodules") { # Skip submodule links until sourcelink properly handles submodules $Status = 200 } elseif ($Uri.AbsoluteURI -ne $null -and ($Uri.Host -match 'github' -or $Uri.Host -match 'githubusercontent')) { # Only GitHub links are valid $Status = (Invoke-WebRequest -Uri $Link -UseBasicParsing -Method HEAD -TimeoutSec 5).StatusCode } else { # If it's not a github link, we want to break out of the loop and not retry. $Status = 0 $attempts = $using:MaxRetries } } catch { Write-Host $_ $Status = 0 } } if ($Status -ne 200) { $attempts++ if ($attempts -lt $using:MaxRetries) { $attemptsLeft = $using:MaxRetries - $attempts Write-Warning "Download failed, $attemptsLeft attempts remaining, will retry in $using:RetryWaitTimeInSeconds seconds" Start-Sleep -Seconds $using:RetryWaitTimeInSeconds } else { if ($NumFailedLinks -eq 0) { if ($FailedFiles.Value -eq 0) { Write-Host } Write-Host "`tFile $RealPath has broken links:" } Write-Host "`t`tFailed to retrieve $Link" $NumFailedLinks++ } } else { break } } } } if ($NumFailedLinks -ne 0) { $FailedFiles.value++ $global:LASTEXITCODE = 1 } } } &$ValidateFile $TargetFile $FileName ([ref]$FailedFiles) } } catch { Write-Host $_ } finally { $zip.Dispose() } if ($FailedFiles -eq 0) { Write-Host 'Passed.' return [pscustomobject]@{ result = 0 packagePath = $PackagePath } } else { Write-PipelineTelemetryError -Category 'SourceLink' -Message "$PackagePath has broken SourceLink links." return [pscustomobject]@{ result = 1 packagePath = $PackagePath } } } function CheckJobResult( $result, $packagePath, [ref]$ValidationFailures, [switch]$logErrors) { if ($result -ne '0') { if ($logErrors) { Write-PipelineTelemetryError -Category 'SourceLink' -Message "$packagePath has broken SourceLink links." } $ValidationFailures.Value++ } } function ValidateSourceLinkLinks { if ($GHRepoName -ne '' -and !($GHRepoName -Match '^[^\s\/]+/[^\s\/]+$')) { if (!($GHRepoName -Match '^[^\s-]+-[^\s]+$')) { Write-PipelineTelemetryError -Category 'SourceLink' -Message "GHRepoName should be in the format / or -. '$GHRepoName'" ExitWithExitCode 1 } else { $GHRepoName = $GHRepoName -replace '^([^\s-]+)-([^\s]+)$', '$1/$2'; } } if ($GHCommit -ne '' -and !($GHCommit -Match '^[0-9a-fA-F]{40}$')) { Write-PipelineTelemetryError -Category 'SourceLink' -Message "GHCommit should be a 40 chars hexadecimal string. '$GHCommit'" ExitWithExitCode 1 } if ($GHRepoName -ne '' -and $GHCommit -ne '') { $RepoTreeURL = -Join('http://api.github.com/repos/', $GHRepoName, '/git/trees/', $GHCommit, '?recursive=1') $CodeExtensions = @('.cs', '.vb', '.fs', '.fsi', '.fsx', '.fsscript') try { # Retrieve the list of files in the repo at that particular commit point and store them in the RepoFiles hash $Data = Invoke-WebRequest $RepoTreeURL -UseBasicParsing | ConvertFrom-Json | Select-Object -ExpandProperty tree foreach ($file in $Data) { $Extension = [System.IO.Path]::GetExtension($file.path) if ($CodeExtensions.Contains($Extension)) { $RepoFiles[$file.path] = 1 } } } catch { Write-Host "Problems downloading the list of files from the repo. Url used: $RepoTreeURL . Execution will proceed without caching." } } elseif ($GHRepoName -ne '' -or $GHCommit -ne '') { Write-Host 'For using the http caching mechanism both GHRepoName and GHCommit should be informed.' } if (Test-Path $ExtractPath) { Remove-Item $ExtractPath -Force -Recurse -ErrorAction SilentlyContinue } $ValidationFailures = 0 # Process each NuGet package in parallel Get-ChildItem "$InputPath\*.symbols.nupkg" | ForEach-Object { Write-Host "Starting $($_.FullName)" Start-Job -ScriptBlock $ValidatePackage -ArgumentList $_.FullName | Out-Null $NumJobs = @(Get-Job -State 'Running').Count while ($NumJobs -ge $MaxParallelJobs) { Write-Host "There are $NumJobs validation jobs running right now. Waiting $SecondsBetweenLoadChecks seconds to check again." sleep $SecondsBetweenLoadChecks $NumJobs = @(Get-Job -State 'Running').Count } foreach ($Job in @(Get-Job -State 'Completed')) { $jobResult = Wait-Job -Id $Job.Id | Receive-Job CheckJobResult $jobResult.result $jobResult.packagePath ([ref]$ValidationFailures) -LogErrors Remove-Job -Id $Job.Id } } foreach ($Job in @(Get-Job)) { $jobResult = Wait-Job -Id $Job.Id | Receive-Job CheckJobResult $jobResult.result $jobResult.packagePath ([ref]$ValidationFailures) Remove-Job -Id $Job.Id } if ($ValidationFailures -gt 0) { Write-PipelineTelemetryError -Category 'SourceLink' -Message "$ValidationFailures package(s) failed validation." ExitWithExitCode 1 } } function InstallSourcelinkCli { $sourcelinkCliPackageName = 'sourcelink' $dotnetRoot = InitializeDotNetCli -install:$true $dotnet = "$dotnetRoot\dotnet.exe" $toolList = & "$dotnet" tool list --global if (($toolList -like "*$sourcelinkCliPackageName*") -and ($toolList -like "*$sourcelinkCliVersion*")) { Write-Host "SourceLink CLI version $sourcelinkCliVersion is already installed." } else { Write-Host "Installing SourceLink CLI version $sourcelinkCliVersion..." Write-Host 'You may need to restart your command window if this is the first dotnet tool you have installed.' & "$dotnet" tool install $sourcelinkCliPackageName --version $sourcelinkCliVersion --verbosity "minimal" --global } } try { InstallSourcelinkCli foreach ($Job in @(Get-Job)) { Remove-Job -Id $Job.Id } ValidateSourceLinkLinks } catch { Write-Host $_.Exception Write-Host $_.ScriptStackTrace Write-PipelineTelemetryError -Category 'SourceLink' -Message $_ ExitWithExitCode 1 } ================================================ FILE: eng/common/post-build/symbols-validation.ps1 ================================================ param( [Parameter(Mandatory = $true)][string] $InputPath, # Full path to directory where NuGet packages to be checked are stored [Parameter(Mandatory = $true)][string] $ExtractPath, # Full path to directory where the packages will be extracted during validation [Parameter(Mandatory = $true)][string] $DotnetSymbolVersion, # Version of dotnet symbol to use [Parameter(Mandatory = $false)][switch] $CheckForWindowsPdbs, # If we should check for the existence of windows pdbs in addition to portable PDBs [Parameter(Mandatory = $false)][switch] $ContinueOnError, # If we should keep checking symbols after an error [Parameter(Mandatory = $false)][switch] $Clean, # Clean extracted symbols directory after checking symbols [Parameter(Mandatory = $false)][string] $SymbolExclusionFile # Exclude the symbols in the file from publishing to symbol server ) . $PSScriptRoot\..\tools.ps1 # Maximum number of jobs to run in parallel $MaxParallelJobs = 16 # Max number of retries $MaxRetry = 5 # Wait time between check for system load $SecondsBetweenLoadChecks = 10 # Set error codes Set-Variable -Name "ERROR_BADEXTRACT" -Option Constant -Value -1 Set-Variable -Name "ERROR_FILEDOESNOTEXIST" -Option Constant -Value -2 $WindowsPdbVerificationParam = "" if ($CheckForWindowsPdbs) { $WindowsPdbVerificationParam = "--windows-pdbs" } $ExclusionSet = New-Object System.Collections.Generic.HashSet[string]; if (!$InputPath -or !(Test-Path $InputPath)){ Write-Host "No symbols to validate." ExitWithExitCode 0 } #Check if the path exists if ($SymbolExclusionFile -and (Test-Path $SymbolExclusionFile)){ [string[]]$Exclusions = Get-Content "$SymbolExclusionFile" $Exclusions | foreach { if($_ -and $_.Trim()){$ExclusionSet.Add($_)} } } else{ Write-Host "Symbol Exclusion file does not exists. No symbols to exclude." } $CountMissingSymbols = { param( [string] $PackagePath, # Path to a NuGet package [string] $WindowsPdbVerificationParam # If we should check for the existence of windows pdbs in addition to portable PDBs ) Add-Type -AssemblyName System.IO.Compression.FileSystem Write-Host "Validating $PackagePath " # Ensure input file exist if (!(Test-Path $PackagePath)) { Write-PipelineTaskError "Input file does not exist: $PackagePath" return [pscustomobject]@{ result = $using:ERROR_FILEDOESNOTEXIST packagePath = $PackagePath } } # Extensions for which we'll look for symbols $RelevantExtensions = @('.dll', '.exe', '.so', '.dylib') # How many files are missing symbol information $MissingSymbols = 0 $PackageId = [System.IO.Path]::GetFileNameWithoutExtension($PackagePath) $PackageGuid = New-Guid $ExtractPath = Join-Path -Path $using:ExtractPath -ChildPath $PackageGuid $SymbolsPath = Join-Path -Path $ExtractPath -ChildPath 'Symbols' try { [System.IO.Compression.ZipFile]::ExtractToDirectory($PackagePath, $ExtractPath) } catch { Write-Host "Something went wrong extracting $PackagePath" Write-Host $_ return [pscustomobject]@{ result = $using:ERROR_BADEXTRACT packagePath = $PackagePath } } Get-ChildItem -Recurse $ExtractPath | Where-Object { $RelevantExtensions -contains $_.Extension } | ForEach-Object { $FileName = $_.FullName if ($FileName -Match '\\ref\\') { Write-Host "`t Ignoring reference assembly file " $FileName return } $FirstMatchingSymbolDescriptionOrDefault = { param( [string] $FullPath, # Full path to the module that has to be checked [string] $TargetServerParam, # Parameter to pass to `Symbol Tool` indicating the server to lookup for symbols [string] $WindowsPdbVerificationParam, # Parameter to pass to potential check for windows-pdbs. [string] $SymbolsPath ) $FileName = [System.IO.Path]::GetFileName($FullPath) $Extension = [System.IO.Path]::GetExtension($FullPath) # Those below are potential symbol files that the `dotnet symbol` might # return. Which one will be returned depend on the type of file we are # checking and which type of file was uploaded. # The file itself is returned $SymbolPath = $SymbolsPath + '\' + $FileName # PDB file for the module $PdbPath = $SymbolPath.Replace($Extension, '.pdb') # PDB file for R2R module (created by crossgen) $NGenPdb = $SymbolPath.Replace($Extension, '.ni.pdb') # DBG file for a .so library $SODbg = $SymbolPath.Replace($Extension, '.so.dbg') # DWARF file for a .dylib $DylibDwarf = $SymbolPath.Replace($Extension, '.dylib.dwarf') $dotnetSymbolExe = "$env:USERPROFILE\.dotnet\tools" $dotnetSymbolExe = Resolve-Path "$dotnetSymbolExe\dotnet-symbol.exe" $totalRetries = 0 while ($totalRetries -lt $using:MaxRetry) { # Save the output and get diagnostic output $output = & $dotnetSymbolExe --symbols --modules $WindowsPdbVerificationParam $TargetServerParam $FullPath -o $SymbolsPath --diagnostics | Out-String if ((Test-Path $PdbPath) -and (Test-path $SymbolPath)) { return 'Module and PDB for Module' } elseif ((Test-Path $NGenPdb) -and (Test-Path $PdbPath) -and (Test-Path $SymbolPath)) { return 'Dll, PDB and NGen PDB' } elseif ((Test-Path $SODbg) -and (Test-Path $SymbolPath)) { return 'So and DBG for SO' } elseif ((Test-Path $DylibDwarf) -and (Test-Path $SymbolPath)) { return 'Dylib and Dwarf for Dylib' } elseif (Test-Path $SymbolPath) { return 'Module' } else { $totalRetries++ } } return $null } $FileRelativePath = $FileName.Replace("$ExtractPath\", "") if (($($using:ExclusionSet) -ne $null) -and ($($using:ExclusionSet).Contains($FileRelativePath) -or ($($using:ExclusionSet).Contains($FileRelativePath.Replace("\", "/"))))){ Write-Host "Skipping $FileName from symbol validation" } else { $FileGuid = New-Guid $ExpandedSymbolsPath = Join-Path -Path $SymbolsPath -ChildPath $FileGuid $SymbolsOnMSDL = & $FirstMatchingSymbolDescriptionOrDefault ` -FullPath $FileName ` -TargetServerParam '--microsoft-symbol-server' ` -SymbolsPath "$ExpandedSymbolsPath-msdl" ` -WindowsPdbVerificationParam $WindowsPdbVerificationParam $SymbolsOnSymWeb = & $FirstMatchingSymbolDescriptionOrDefault ` -FullPath $FileName ` -TargetServerParam '--internal-server' ` -SymbolsPath "$ExpandedSymbolsPath-symweb" ` -WindowsPdbVerificationParam $WindowsPdbVerificationParam Write-Host -NoNewLine "`t Checking file " $FileName "... " if ($SymbolsOnMSDL -ne $null -and $SymbolsOnSymWeb -ne $null) { Write-Host "Symbols found on MSDL ($SymbolsOnMSDL) and SymWeb ($SymbolsOnSymWeb)" } else { $MissingSymbols++ if ($SymbolsOnMSDL -eq $null -and $SymbolsOnSymWeb -eq $null) { Write-Host 'No symbols found on MSDL or SymWeb!' } else { if ($SymbolsOnMSDL -eq $null) { Write-Host 'No symbols found on MSDL!' } else { Write-Host 'No symbols found on SymWeb!' } } } } } if ($using:Clean) { Remove-Item $ExtractPath -Recurse -Force } Pop-Location return [pscustomobject]@{ result = $MissingSymbols packagePath = $PackagePath } } function CheckJobResult( $result, $packagePath, [ref]$DupedSymbols, [ref]$TotalFailures) { if ($result -eq $ERROR_BADEXTRACT) { Write-PipelineTelemetryError -Category 'CheckSymbols' -Message "$packagePath has duplicated symbol files" $DupedSymbols.Value++ } elseif ($result -eq $ERROR_FILEDOESNOTEXIST) { Write-PipelineTelemetryError -Category 'CheckSymbols' -Message "$packagePath does not exist" $TotalFailures.Value++ } elseif ($result -gt '0') { Write-PipelineTelemetryError -Category 'CheckSymbols' -Message "Missing symbols for $result modules in the package $packagePath" $TotalFailures.Value++ } else { Write-Host "All symbols verified for package $packagePath" } } function CheckSymbolsAvailable { if (Test-Path $ExtractPath) { Remove-Item $ExtractPath -Force -Recurse -ErrorAction SilentlyContinue } $TotalPackages = 0 $TotalFailures = 0 $DupedSymbols = 0 Get-ChildItem "$InputPath\*.nupkg" | ForEach-Object { $FileName = $_.Name $FullName = $_.FullName # These packages from Arcade-Services include some native libraries that # our current symbol uploader can't handle. Below is a workaround until # we get issue: https://github.com/dotnet/arcade/issues/2457 sorted. if ($FileName -Match 'Microsoft\.DotNet\.Darc\.') { Write-Host "Ignoring Arcade-services file: $FileName" Write-Host return } elseif ($FileName -Match 'Microsoft\.DotNet\.Maestro\.Tasks\.') { Write-Host "Ignoring Arcade-services file: $FileName" Write-Host return } $TotalPackages++ Start-Job -ScriptBlock $CountMissingSymbols -ArgumentList @($FullName,$WindowsPdbVerificationParam) | Out-Null $NumJobs = @(Get-Job -State 'Running').Count while ($NumJobs -ge $MaxParallelJobs) { Write-Host "There are $NumJobs validation jobs running right now. Waiting $SecondsBetweenLoadChecks seconds to check again." sleep $SecondsBetweenLoadChecks $NumJobs = @(Get-Job -State 'Running').Count } foreach ($Job in @(Get-Job -State 'Completed')) { $jobResult = Wait-Job -Id $Job.Id | Receive-Job CheckJobResult $jobResult.result $jobResult.packagePath ([ref]$DupedSymbols) ([ref]$TotalFailures) Remove-Job -Id $Job.Id } Write-Host } foreach ($Job in @(Get-Job)) { $jobResult = Wait-Job -Id $Job.Id | Receive-Job CheckJobResult $jobResult.result $jobResult.packagePath ([ref]$DupedSymbols) ([ref]$TotalFailures) } if ($TotalFailures -gt 0 -or $DupedSymbols -gt 0) { if ($TotalFailures -gt 0) { Write-PipelineTelemetryError -Category 'CheckSymbols' -Message "Symbols missing for $TotalFailures/$TotalPackages packages" } if ($DupedSymbols -gt 0) { Write-PipelineTelemetryError -Category 'CheckSymbols' -Message "$DupedSymbols/$TotalPackages packages had duplicated symbol files and could not be extracted" } ExitWithExitCode 1 } else { Write-Host "All symbols validated!" } } function InstallDotnetSymbol { $dotnetSymbolPackageName = 'dotnet-symbol' $dotnetRoot = InitializeDotNetCli -install:$true $dotnet = "$dotnetRoot\dotnet.exe" $toolList = & "$dotnet" tool list --global if (($toolList -like "*$dotnetSymbolPackageName*") -and ($toolList -like "*$dotnetSymbolVersion*")) { Write-Host "dotnet-symbol version $dotnetSymbolVersion is already installed." } else { Write-Host "Installing dotnet-symbol version $dotnetSymbolVersion..." Write-Host 'You may need to restart your command window if this is the first dotnet tool you have installed.' & "$dotnet" tool install $dotnetSymbolPackageName --version $dotnetSymbolVersion --verbosity "minimal" --global } } try { InstallDotnetSymbol foreach ($Job in @(Get-Job)) { Remove-Job -Id $Job.Id } CheckSymbolsAvailable } catch { Write-Host $_.ScriptStackTrace Write-PipelineTelemetryError -Category 'CheckSymbols' -Message $_ ExitWithExitCode 1 } ================================================ FILE: eng/common/retain-build.ps1 ================================================ Param( [Parameter(Mandatory=$true)][int] $buildId, [Parameter(Mandatory=$true)][string] $azdoOrgUri, [Parameter(Mandatory=$true)][string] $azdoProject, [Parameter(Mandatory=$true)][string] $token ) $ErrorActionPreference = 'Stop' Set-StrictMode -Version 2.0 function Get-AzDOHeaders( [string] $token) { $base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(":${token}")) $headers = @{"Authorization"="Basic $base64AuthInfo"} return $headers } function Update-BuildRetention( [string] $azdoOrgUri, [string] $azdoProject, [int] $buildId, [string] $token) { $headers = Get-AzDOHeaders -token $token $requestBody = "{ `"keepForever`": `"true`" }" $requestUri = "${azdoOrgUri}/${azdoProject}/_apis/build/builds/${buildId}?api-version=6.0" write-Host "Attempting to retain build using the following URI: ${requestUri} ..." try { Invoke-RestMethod -Uri $requestUri -Method Patch -Body $requestBody -Header $headers -contentType "application/json" Write-Host "Updated retention settings for build ${buildId}." } catch { Write-Error "Failed to update retention settings for build: $_.Exception.Response.StatusDescription" exit 1 } } Update-BuildRetention -azdoOrgUri $azdoOrgUri -azdoProject $azdoProject -buildId $buildId -token $token exit 0 ================================================ FILE: eng/common/sdk-task.ps1 ================================================ [CmdletBinding(PositionalBinding=$false)] Param( [string] $configuration = 'Debug', [string] $task, [string] $verbosity = 'minimal', [string] $msbuildEngine = $null, [switch] $restore, [switch] $prepareMachine, [switch][Alias('nobl')]$excludeCIBinaryLog, [switch]$noWarnAsError, [switch] $help, [string] $runtimeSourceFeed = '', [string] $runtimeSourceFeedKey = '', [Parameter(ValueFromRemainingArguments=$true)][String[]]$properties ) $ci = $true $binaryLog = if ($excludeCIBinaryLog) { $false } else { $true } $warnAsError = if ($noWarnAsError) { $false } else { $true } . $PSScriptRoot\tools.ps1 function Print-Usage() { Write-Host "Common settings:" Write-Host " -task Name of Arcade task (name of a project in SdkTasks directory of the Arcade SDK package)" Write-Host " -restore Restore dependencies" Write-Host " -verbosity Msbuild verbosity: q[uiet], m[inimal], n[ormal], d[etailed], and diag[nostic]" Write-Host " -help Print help and exit" Write-Host "" Write-Host "Advanced settings:" Write-Host " -prepareMachine Prepare machine for CI run" Write-Host " -msbuildEngine Msbuild engine to use to run build ('dotnet', 'vs', or unspecified)." Write-Host " -excludeCIBinaryLog When running on CI, allow no binary log (short: -nobl)" Write-Host "" Write-Host "Command line arguments not listed above are passed thru to msbuild." } function Build([string]$target) { $logSuffix = if ($target -eq 'Execute') { '' } else { ".$target" } $log = Join-Path $LogDir "$task$logSuffix.binlog" $binaryLogArg = if ($binaryLog) { "/bl:$log" } else { "" } $outputPath = Join-Path $ToolsetDir "$task\" MSBuild $taskProject ` $binaryLogArg ` /t:$target ` /p:Configuration=$configuration ` /p:RepoRoot=$RepoRoot ` /p:BaseIntermediateOutputPath=$outputPath ` /v:$verbosity ` @properties } try { if ($help -or (($null -ne $properties) -and ($properties.Contains('/help') -or $properties.Contains('/?')))) { Print-Usage exit 0 } if ($task -eq "") { Write-PipelineTelemetryError -Category 'Build' -Message "Missing required parameter '-task '" Print-Usage ExitWithExitCode 1 } if( $msbuildEngine -eq "vs") { # Ensure desktop MSBuild is available for sdk tasks. if( -not ($GlobalJson.tools.PSObject.Properties.Name -contains "vs" )) { $GlobalJson.tools | Add-Member -Name "vs" -Value (ConvertFrom-Json "{ `"version`": `"16.5`" }") -MemberType NoteProperty } if( -not ($GlobalJson.tools.PSObject.Properties.Name -match "xcopy-msbuild" )) { $GlobalJson.tools | Add-Member -Name "xcopy-msbuild" -Value "18.0.0" -MemberType NoteProperty } if ($GlobalJson.tools."xcopy-msbuild".Trim() -ine "none") { $xcopyMSBuildToolsFolder = InitializeXCopyMSBuild $GlobalJson.tools."xcopy-msbuild" -install $true } if ($xcopyMSBuildToolsFolder -eq $null) { throw 'Unable to get xcopy downloadable version of msbuild' } $global:_MSBuildExe = "$($xcopyMSBuildToolsFolder)\MSBuild\Current\Bin\MSBuild.exe" } $taskProject = GetSdkTaskProject $task if (!(Test-Path $taskProject)) { Write-PipelineTelemetryError -Category 'Build' -Message "Unknown task: $task" ExitWithExitCode 1 } if ($restore) { Build 'Restore' } Build 'Execute' } catch { Write-Host $_.ScriptStackTrace Write-PipelineTelemetryError -Category 'Build' -Message $_ ExitWithExitCode 1 } ExitWithExitCode 0 ================================================ FILE: eng/common/sdk-task.sh ================================================ #!/usr/bin/env bash show_usage() { echo "Common settings:" echo " --task Name of Arcade task (name of a project in SdkTasks directory of the Arcade SDK package)" echo " --restore Restore dependencies" echo " --verbosity Msbuild verbosity: q[uiet], m[inimal], n[ormal], d[etailed], and diag[nostic]" echo " --help Print help and exit" echo "" echo "Advanced settings:" echo " --excludeCIBinarylog Don't output binary log (short: -nobl)" echo " --noWarnAsError Do not warn as error" echo "" echo "Command line arguments not listed above are passed thru to msbuild." } source="${BASH_SOURCE[0]}" # resolve $source until the file is no longer a symlink while [[ -h "$source" ]]; do scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" source="$(readlink "$source")" # if $source was a relative symlink, we need to resolve it relative to the path where the # symlink file was located [[ $source != /* ]] && source="$scriptroot/$source" done scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" Build() { local target=$1 local log_suffix="" [[ "$target" != "Execute" ]] && log_suffix=".$target" local log="$log_dir/$task$log_suffix.binlog" local binaryLogArg="" [[ $binary_log == true ]] && binaryLogArg="/bl:$log" local output_path="$toolset_dir/$task/" MSBuild "$taskProject" \ $binaryLogArg \ /t:"$target" \ /p:Configuration="$configuration" \ /p:RepoRoot="$repo_root" \ /p:BaseIntermediateOutputPath="$output_path" \ /v:"$verbosity" \ $properties } binary_log=true configuration="Debug" verbosity="minimal" exclude_ci_binary_log=false restore=false help=false properties='' warnAsError=true while (($# > 0)); do lowerI="$(echo $1 | tr "[:upper:]" "[:lower:]")" case $lowerI in --task) task=$2 shift 2 ;; --restore) restore=true shift 1 ;; --verbosity) verbosity=$2 shift 2 ;; --excludecibinarylog|--nobl) binary_log=false exclude_ci_binary_log=true shift 1 ;; --noWarnAsError) warnAsError=false shift 1 ;; --help) help=true shift 1 ;; *) properties="$properties $1" shift 1 ;; esac done ci=true if $help; then show_usage exit 0 fi . "$scriptroot/tools.sh" InitializeToolset if [[ -z "$task" ]]; then Write-PipelineTelemetryError -Category 'Task' -Name 'MissingTask' -Message "Missing required parameter '-task '" ExitWithExitCode 1 fi taskProject=$(GetSdkTaskProject "$task") if [[ ! -e "$taskProject" ]]; then Write-PipelineTelemetryError -Category 'Task' -Name 'UnknownTask' -Message "Unknown task: $task" ExitWithExitCode 1 fi if $restore; then Build "Restore" fi Build "Execute" ExitWithExitCode 0 ================================================ FILE: eng/common/sdl/NuGet.config ================================================  ================================================ FILE: eng/common/sdl/configure-sdl-tool.ps1 ================================================ Param( [string] $GuardianCliLocation, [string] $WorkingDirectory, [string] $TargetDirectory, [string] $GdnFolder, # The list of Guardian tools to configure. For each object in the array: # - If the item is a [hashtable], it must contain these entries: # - Name = The tool name as Guardian knows it. # - Scenario = (Optional) Scenario-specific name for this configuration entry. It must be unique # among all tool entries with the same Name. # - Args = (Optional) Array of Guardian tool configuration args, like '@("Target > C:\temp")' # - If the item is a [string] $v, it is treated as '@{ Name="$v" }' [object[]] $ToolsList, [string] $GuardianLoggerLevel='Standard', # Optional: Additional params to add to any tool using CredScan. [string[]] $CrScanAdditionalRunConfigParams, # Optional: Additional params to add to any tool using PoliCheck. [string[]] $PoliCheckAdditionalRunConfigParams, # Optional: Additional params to add to any tool using CodeQL/Semmle. [string[]] $CodeQLAdditionalRunConfigParams, # Optional: Additional params to add to any tool using Binskim. [string[]] $BinskimAdditionalRunConfigParams ) $ErrorActionPreference = 'Stop' Set-StrictMode -Version 2.0 $disableConfigureToolsetImport = $true $global:LASTEXITCODE = 0 try { # `tools.ps1` checks $ci to perform some actions. Since the SDL # scripts don't necessarily execute in the same agent that run the # build.ps1/sh script this variable isn't automatically set. $ci = $true . $PSScriptRoot\..\tools.ps1 # Normalize tools list: all in [hashtable] form with defined values for each key. $ToolsList = $ToolsList | ForEach-Object { if ($_ -is [string]) { $_ = @{ Name = $_ } } if (-not ($_['Scenario'])) { $_.Scenario = "" } if (-not ($_['Args'])) { $_.Args = @() } $_ } Write-Host "List of tools to configure:" $ToolsList | ForEach-Object { $_ | Out-String | Write-Host } # We store config files in the r directory of .gdn $gdnConfigPath = Join-Path $GdnFolder 'r' $ValidPath = Test-Path $GuardianCliLocation if ($ValidPath -eq $False) { Write-PipelineTelemetryError -Force -Category 'Sdl' -Message "Invalid Guardian CLI Location." ExitWithExitCode 1 } foreach ($tool in $ToolsList) { # Put together the name and scenario to make a unique key. $toolConfigName = $tool.Name if ($tool.Scenario) { $toolConfigName += "_" + $tool.Scenario } Write-Host "=== Configuring $toolConfigName..." $gdnConfigFile = Join-Path $gdnConfigPath "$toolConfigName-configure.gdnconfig" # For some tools, add default and automatic args. switch -Exact ($tool.Name) { 'credscan' { if ($targetDirectory) { $tool.Args += "`"TargetDirectory < $TargetDirectory`"" } $tool.Args += "`"OutputType < pre`"" $tool.Args += $CrScanAdditionalRunConfigParams } 'policheck' { if ($targetDirectory) { $tool.Args += "`"Target < $TargetDirectory`"" } $tool.Args += $PoliCheckAdditionalRunConfigParams } {$_ -in 'semmle', 'codeql'} { if ($targetDirectory) { $tool.Args += "`"SourceCodeDirectory < $TargetDirectory`"" } $tool.Args += $CodeQLAdditionalRunConfigParams } 'binskim' { if ($targetDirectory) { # Binskim crashes due to specific PDBs. GitHub issue: https://github.com/microsoft/binskim/issues/924. # We are excluding all `_.pdb` files from the scan. $tool.Args += "`"Target < $TargetDirectory\**;-:file|$TargetDirectory\**\_.pdb`"" } $tool.Args += $BinskimAdditionalRunConfigParams } } # Create variable pointing to the args array directly so we can use splat syntax later. $toolArgs = $tool.Args # Configure the tool. If args array is provided or the current tool has some default arguments # defined, add "--args" and splat each element on the end. Arg format is "{Arg id} < {Value}", # one per parameter. Doc page for "guardian configure": # https://dev.azure.com/securitytools/SecurityIntegration/_wiki/wikis/Guardian/1395/configure Exec-BlockVerbosely { & $GuardianCliLocation configure ` --working-directory $WorkingDirectory ` --tool $tool.Name ` --output-path $gdnConfigFile ` --logger-level $GuardianLoggerLevel ` --noninteractive ` --force ` $(if ($toolArgs) { "--args" }) @toolArgs Exit-IfNZEC "Sdl" } Write-Host "Created '$toolConfigName' configuration file: $gdnConfigFile" } } catch { Write-Host $_.ScriptStackTrace Write-PipelineTelemetryError -Force -Category 'Sdl' -Message $_ ExitWithExitCode 1 } ================================================ FILE: eng/common/sdl/execute-all-sdl-tools.ps1 ================================================ Param( [string] $GuardianPackageName, # Required: the name of guardian CLI package (not needed if GuardianCliLocation is specified) [string] $NugetPackageDirectory, # Required: directory where NuGet packages are installed (not needed if GuardianCliLocation is specified) [string] $GuardianCliLocation, # Optional: Direct location of Guardian CLI executable if GuardianPackageName & NugetPackageDirectory are not specified [string] $Repository=$env:BUILD_REPOSITORY_NAME, # Required: the name of the repository (e.g. dotnet/arcade) [string] $BranchName=$env:BUILD_SOURCEBRANCH, # Optional: name of branch or version of gdn settings; defaults to master [string] $SourceDirectory=$env:BUILD_SOURCESDIRECTORY, # Required: the directory where source files are located [string] $ArtifactsDirectory = (Join-Path $env:BUILD_ARTIFACTSTAGINGDIRECTORY ('artifacts')), # Required: the directory where build artifacts are located [string] $AzureDevOpsAccessToken, # Required: access token for dnceng; should be provided via KeyVault # Optional: list of SDL tools to run on source code. See 'configure-sdl-tool.ps1' for tools list # format. [object[]] $SourceToolsList, # Optional: list of SDL tools to run on built artifacts. See 'configure-sdl-tool.ps1' for tools # list format. [object[]] $ArtifactToolsList, # Optional: list of SDL tools to run without automatically specifying a target directory. See # 'configure-sdl-tool.ps1' for tools list format. [object[]] $CustomToolsList, [bool] $TsaPublish=$False, # Optional: true will publish results to TSA; only set to true after onboarding to TSA; TSA is the automated framework used to upload test results as bugs. [string] $TsaBranchName=$env:BUILD_SOURCEBRANCH, # Optional: required for TSA publish; defaults to $(Build.SourceBranchName); TSA is the automated framework used to upload test results as bugs. [string] $TsaRepositoryName=$env:BUILD_REPOSITORY_NAME, # Optional: TSA repository name; will be generated automatically if not submitted; TSA is the automated framework used to upload test results as bugs. [string] $BuildNumber=$env:BUILD_BUILDNUMBER, # Optional: required for TSA publish; defaults to $(Build.BuildNumber) [bool] $UpdateBaseline=$False, # Optional: if true, will update the baseline in the repository; should only be run after fixing any issues which need to be fixed [bool] $TsaOnboard=$False, # Optional: if true, will onboard the repository to TSA; should only be run once; TSA is the automated framework used to upload test results as bugs. [string] $TsaInstanceUrl, # Optional: only needed if TsaOnboard or TsaPublish is true; the instance-url registered with TSA; TSA is the automated framework used to upload test results as bugs. [string] $TsaCodebaseName, # Optional: only needed if TsaOnboard or TsaPublish is true; the name of the codebase registered with TSA; TSA is the automated framework used to upload test results as bugs. [string] $TsaProjectName, # Optional: only needed if TsaOnboard or TsaPublish is true; the name of the project registered with TSA; TSA is the automated framework used to upload test results as bugs. [string] $TsaNotificationEmail, # Optional: only needed if TsaOnboard is true; the email(s) which will receive notifications of TSA bug filings (e.g. alias@microsoft.com); TSA is the automated framework used to upload test results as bugs. [string] $TsaCodebaseAdmin, # Optional: only needed if TsaOnboard is true; the aliases which are admins of the TSA codebase (e.g. DOMAIN\alias); TSA is the automated framework used to upload test results as bugs. [string] $TsaBugAreaPath, # Optional: only needed if TsaOnboard is true; the area path where TSA will file bugs in AzDO; TSA is the automated framework used to upload test results as bugs. [string] $TsaIterationPath, # Optional: only needed if TsaOnboard is true; the iteration path where TSA will file bugs in AzDO; TSA is the automated framework used to upload test results as bugs. [string] $GuardianLoggerLevel='Standard', # Optional: the logger level for the Guardian CLI; options are Trace, Verbose, Standard, Warning, and Error [string[]] $CrScanAdditionalRunConfigParams, # Optional: Additional Params to custom build a CredScan run config in the format @("xyz:abc","sdf:1") [string[]] $PoliCheckAdditionalRunConfigParams, # Optional: Additional Params to custom build a Policheck run config in the format @("xyz:abc","sdf:1") [string[]] $CodeQLAdditionalRunConfigParams, # Optional: Additional Params to custom build a Semmle/CodeQL run config in the format @("xyz < abc","sdf < 1") [string[]] $BinskimAdditionalRunConfigParams, # Optional: Additional Params to custom build a Binskim run config in the format @("xyz < abc","sdf < 1") [bool] $BreakOnFailure=$False # Optional: Fail the build if there were errors during the run ) try { $ErrorActionPreference = 'Stop' Set-StrictMode -Version 2.0 $disableConfigureToolsetImport = $true $global:LASTEXITCODE = 0 # `tools.ps1` checks $ci to perform some actions. Since the SDL # scripts don't necessarily execute in the same agent that run the # build.ps1/sh script this variable isn't automatically set. $ci = $true . $PSScriptRoot\..\tools.ps1 #Replace repo names to the format of org/repo if (!($Repository.contains('/'))) { $RepoName = $Repository -replace '(.*?)-(.*)', '$1/$2'; } else{ $RepoName = $Repository; } if ($GuardianPackageName) { $guardianCliLocation = Join-Path $NugetPackageDirectory (Join-Path $GuardianPackageName (Join-Path 'tools' 'guardian.cmd')) } else { $guardianCliLocation = $GuardianCliLocation } $workingDirectory = (Split-Path $SourceDirectory -Parent) $ValidPath = Test-Path $guardianCliLocation if ($ValidPath -eq $False) { Write-PipelineTelemetryError -Force -Category 'Sdl' -Message 'Invalid Guardian CLI Location.' ExitWithExitCode 1 } Exec-BlockVerbosely { & $(Join-Path $PSScriptRoot 'init-sdl.ps1') -GuardianCliLocation $guardianCliLocation -Repository $RepoName -BranchName $BranchName -WorkingDirectory $workingDirectory -AzureDevOpsAccessToken $AzureDevOpsAccessToken -GuardianLoggerLevel $GuardianLoggerLevel } $gdnFolder = Join-Path $workingDirectory '.gdn' if ($TsaOnboard) { if ($TsaCodebaseName -and $TsaNotificationEmail -and $TsaCodebaseAdmin -and $TsaBugAreaPath) { Exec-BlockVerbosely { & $guardianCliLocation tsa-onboard --codebase-name "$TsaCodebaseName" --notification-alias "$TsaNotificationEmail" --codebase-admin "$TsaCodebaseAdmin" --instance-url "$TsaInstanceUrl" --project-name "$TsaProjectName" --area-path "$TsaBugAreaPath" --iteration-path "$TsaIterationPath" --working-directory $workingDirectory --logger-level $GuardianLoggerLevel } if ($LASTEXITCODE -ne 0) { Write-PipelineTelemetryError -Force -Category 'Sdl' -Message "Guardian tsa-onboard failed with exit code $LASTEXITCODE." ExitWithExitCode $LASTEXITCODE } } else { Write-PipelineTelemetryError -Force -Category 'Sdl' -Message 'Could not onboard to TSA -- not all required values ($TsaCodebaseName, $TsaNotificationEmail, $TsaCodebaseAdmin, $TsaBugAreaPath) were specified.' ExitWithExitCode 1 } } # Configure a list of tools with a default target directory. Populates the ".gdn/r" directory. function Configure-ToolsList([object[]] $tools, [string] $targetDirectory) { if ($tools -and $tools.Count -gt 0) { Exec-BlockVerbosely { & $(Join-Path $PSScriptRoot 'configure-sdl-tool.ps1') ` -GuardianCliLocation $guardianCliLocation ` -WorkingDirectory $workingDirectory ` -TargetDirectory $targetDirectory ` -GdnFolder $gdnFolder ` -ToolsList $tools ` -AzureDevOpsAccessToken $AzureDevOpsAccessToken ` -GuardianLoggerLevel $GuardianLoggerLevel ` -CrScanAdditionalRunConfigParams $CrScanAdditionalRunConfigParams ` -PoliCheckAdditionalRunConfigParams $PoliCheckAdditionalRunConfigParams ` -CodeQLAdditionalRunConfigParams $CodeQLAdditionalRunConfigParams ` -BinskimAdditionalRunConfigParams $BinskimAdditionalRunConfigParams if ($BreakOnFailure) { Exit-IfNZEC "Sdl" } } } } # Configure Artifact and Source tools with default Target directories. Configure-ToolsList $ArtifactToolsList $ArtifactsDirectory Configure-ToolsList $SourceToolsList $SourceDirectory # Configure custom tools with no default Target directory. Configure-ToolsList $CustomToolsList $null # At this point, all tools are configured in the ".gdn" directory. Run them all in a single call. # (If we used "run" multiple times, each run would overwrite data from earlier runs.) Exec-BlockVerbosely { & $(Join-Path $PSScriptRoot 'run-sdl.ps1') ` -GuardianCliLocation $guardianCliLocation ` -WorkingDirectory $SourceDirectory ` -UpdateBaseline $UpdateBaseline ` -GdnFolder $gdnFolder } if ($TsaPublish) { if ($TsaBranchName -and $BuildNumber) { if (-not $TsaRepositoryName) { $TsaRepositoryName = "$($Repository)-$($BranchName)" } Exec-BlockVerbosely { & $guardianCliLocation tsa-publish --all-tools --repository-name "$TsaRepositoryName" --branch-name "$TsaBranchName" --build-number "$BuildNumber" --onboard $True --codebase-name "$TsaCodebaseName" --notification-alias "$TsaNotificationEmail" --codebase-admin "$TsaCodebaseAdmin" --instance-url "$TsaInstanceUrl" --project-name "$TsaProjectName" --area-path "$TsaBugAreaPath" --iteration-path "$TsaIterationPath" --working-directory $workingDirectory --logger-level $GuardianLoggerLevel } if ($LASTEXITCODE -ne 0) { Write-PipelineTelemetryError -Force -Category 'Sdl' -Message "Guardian tsa-publish failed with exit code $LASTEXITCODE." ExitWithExitCode $LASTEXITCODE } } else { Write-PipelineTelemetryError -Force -Category 'Sdl' -Message 'Could not publish to TSA -- not all required values ($TsaBranchName, $BuildNumber) were specified.' ExitWithExitCode 1 } } if ($BreakOnFailure) { Write-Host "Failing the build in case of breaking results..." Exec-BlockVerbosely { & $guardianCliLocation break --working-directory $workingDirectory --logger-level $GuardianLoggerLevel } } else { Write-Host "Letting the build pass even if there were breaking results..." } } catch { Write-Host $_.ScriptStackTrace Write-PipelineTelemetryError -Force -Category 'Sdl' -Message $_ exit 1 } ================================================ FILE: eng/common/sdl/extract-artifact-archives.ps1 ================================================ # This script looks for each archive file in a directory and extracts it into the target directory. # For example, the file "$InputPath/bin.tar.gz" extracts to "$ExtractPath/bin.tar.gz.extracted/**". # Uses the "tar" utility added to Windows 10 / Windows 2019 that supports tar.gz and zip. param( # Full path to directory where archives are stored. [Parameter(Mandatory=$true)][string] $InputPath, # Full path to directory to extract archives into. May be the same as $InputPath. [Parameter(Mandatory=$true)][string] $ExtractPath ) $ErrorActionPreference = 'Stop' Set-StrictMode -Version 2.0 $disableConfigureToolsetImport = $true try { # `tools.ps1` checks $ci to perform some actions. Since the SDL # scripts don't necessarily execute in the same agent that run the # build.ps1/sh script this variable isn't automatically set. $ci = $true . $PSScriptRoot\..\tools.ps1 Measure-Command { $jobs = @() # Find archive files for non-Windows and Windows builds. $archiveFiles = @( Get-ChildItem (Join-Path $InputPath "*.tar.gz") Get-ChildItem (Join-Path $InputPath "*.zip") ) foreach ($targzFile in $archiveFiles) { $jobs += Start-Job -ScriptBlock { $file = $using:targzFile $fileName = [System.IO.Path]::GetFileName($file) $extractDir = Join-Path $using:ExtractPath "$fileName.extracted" New-Item $extractDir -ItemType Directory -Force | Out-Null Write-Host "Extracting '$file' to '$extractDir'..." # Pipe errors to stdout to prevent PowerShell detecting them and quitting the job early. # This type of quit skips the catch, so we wouldn't be able to tell which file triggered the # error. Save output so it can be stored in the exception string along with context. $output = tar -xf $file -C $extractDir 2>&1 # Handle NZEC manually rather than using Exit-IfNZEC: we are in a background job, so we # don't have access to the outer scope. if ($LASTEXITCODE -ne 0) { throw "Error extracting '$file': non-zero exit code ($LASTEXITCODE). Output: '$output'" } Write-Host "Extracted to $extractDir" } } Receive-Job $jobs -Wait } } catch { Write-Host $_ Write-PipelineTelemetryError -Force -Category 'Sdl' -Message $_ ExitWithExitCode 1 } ================================================ FILE: eng/common/sdl/extract-artifact-packages.ps1 ================================================ param( [Parameter(Mandatory=$true)][string] $InputPath, # Full path to directory where artifact packages are stored [Parameter(Mandatory=$true)][string] $ExtractPath # Full path to directory where the packages will be extracted ) $ErrorActionPreference = 'Stop' Set-StrictMode -Version 2.0 $disableConfigureToolsetImport = $true function ExtractArtifacts { if (!(Test-Path $InputPath)) { Write-Host "Input Path does not exist: $InputPath" ExitWithExitCode 0 } $Jobs = @() Get-ChildItem "$InputPath\*.nupkg" | ForEach-Object { $Jobs += Start-Job -ScriptBlock $ExtractPackage -ArgumentList $_.FullName } foreach ($Job in $Jobs) { Wait-Job -Id $Job.Id | Receive-Job } } try { # `tools.ps1` checks $ci to perform some actions. Since the SDL # scripts don't necessarily execute in the same agent that run the # build.ps1/sh script this variable isn't automatically set. $ci = $true . $PSScriptRoot\..\tools.ps1 $ExtractPackage = { param( [string] $PackagePath # Full path to a NuGet package ) if (!(Test-Path $PackagePath)) { Write-PipelineTelemetryError -Category 'Build' -Message "Input file does not exist: $PackagePath" ExitWithExitCode 1 } $RelevantExtensions = @('.dll', '.exe', '.pdb') Write-Host -NoNewLine 'Extracting ' ([System.IO.Path]::GetFileName($PackagePath)) '...' $PackageId = [System.IO.Path]::GetFileNameWithoutExtension($PackagePath) $ExtractPath = Join-Path -Path $using:ExtractPath -ChildPath $PackageId Add-Type -AssemblyName System.IO.Compression.FileSystem [System.IO.Directory]::CreateDirectory($ExtractPath); try { $zip = [System.IO.Compression.ZipFile]::OpenRead($PackagePath) $zip.Entries | Where-Object {$RelevantExtensions -contains [System.IO.Path]::GetExtension($_.Name)} | ForEach-Object { $TargetPath = Join-Path -Path $ExtractPath -ChildPath (Split-Path -Path $_.FullName) [System.IO.Directory]::CreateDirectory($TargetPath); $TargetFile = Join-Path -Path $ExtractPath -ChildPath $_.FullName [System.IO.Compression.ZipFileExtensions]::ExtractToFile($_, $TargetFile) } } catch { Write-Host $_ Write-PipelineTelemetryError -Force -Category 'Sdl' -Message $_ ExitWithExitCode 1 } finally { $zip.Dispose() } } Measure-Command { ExtractArtifacts } } catch { Write-Host $_ Write-PipelineTelemetryError -Force -Category 'Sdl' -Message $_ ExitWithExitCode 1 } ================================================ FILE: eng/common/sdl/init-sdl.ps1 ================================================ Param( [string] $GuardianCliLocation, [string] $Repository, [string] $BranchName='master', [string] $WorkingDirectory, [string] $AzureDevOpsAccessToken, [string] $GuardianLoggerLevel='Standard' ) $ErrorActionPreference = 'Stop' Set-StrictMode -Version 2.0 $disableConfigureToolsetImport = $true $global:LASTEXITCODE = 0 # `tools.ps1` checks $ci to perform some actions. Since the SDL # scripts don't necessarily execute in the same agent that run the # build.ps1/sh script this variable isn't automatically set. $ci = $true . $PSScriptRoot\..\tools.ps1 # Don't display the console progress UI - it's a huge perf hit $ProgressPreference = 'SilentlyContinue' # Construct basic auth from AzDO access token; construct URI to the repository's gdn folder stored in that repository; construct location of zip file $encodedPat = [Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes(":$AzureDevOpsAccessToken")) $escapedRepository = [Uri]::EscapeDataString("/$Repository/$BranchName/.gdn") $uri = "https://dev.azure.com/dnceng/internal/_apis/git/repositories/sdl-tool-cfg/Items?path=$escapedRepository&versionDescriptor[versionOptions]=0&`$format=zip&api-version=5.0" $zipFile = "$WorkingDirectory/gdn.zip" Add-Type -AssemblyName System.IO.Compression.FileSystem $gdnFolder = (Join-Path $WorkingDirectory '.gdn') try { # if the folder does not exist, we'll do a guardian init and push it to the remote repository Write-Host 'Initializing Guardian...' Write-Host "$GuardianCliLocation init --working-directory $WorkingDirectory --logger-level $GuardianLoggerLevel" & $GuardianCliLocation init --working-directory $WorkingDirectory --logger-level $GuardianLoggerLevel if ($LASTEXITCODE -ne 0) { Write-PipelineTelemetryError -Force -Category 'Build' -Message "Guardian init failed with exit code $LASTEXITCODE." ExitWithExitCode $LASTEXITCODE } # We create the mainbaseline so it can be edited later Write-Host "$GuardianCliLocation baseline --working-directory $WorkingDirectory --name mainbaseline" & $GuardianCliLocation baseline --working-directory $WorkingDirectory --name mainbaseline if ($LASTEXITCODE -ne 0) { Write-PipelineTelemetryError -Force -Category 'Build' -Message "Guardian baseline failed with exit code $LASTEXITCODE." ExitWithExitCode $LASTEXITCODE } ExitWithExitCode 0 } catch { Write-Host $_.ScriptStackTrace Write-PipelineTelemetryError -Force -Category 'Sdl' -Message $_ ExitWithExitCode 1 } ================================================ FILE: eng/common/sdl/packages.config ================================================ ================================================ FILE: eng/common/sdl/run-sdl.ps1 ================================================ Param( [string] $GuardianCliLocation, [string] $WorkingDirectory, [string] $GdnFolder, [string] $UpdateBaseline, [string] $GuardianLoggerLevel='Standard' ) $ErrorActionPreference = 'Stop' Set-StrictMode -Version 2.0 $disableConfigureToolsetImport = $true $global:LASTEXITCODE = 0 try { # `tools.ps1` checks $ci to perform some actions. Since the SDL # scripts don't necessarily execute in the same agent that run the # build.ps1/sh script this variable isn't automatically set. $ci = $true . $PSScriptRoot\..\tools.ps1 # We store config files in the r directory of .gdn $gdnConfigPath = Join-Path $GdnFolder 'r' $ValidPath = Test-Path $GuardianCliLocation if ($ValidPath -eq $False) { Write-PipelineTelemetryError -Force -Category 'Sdl' -Message "Invalid Guardian CLI Location." ExitWithExitCode 1 } $gdnConfigFiles = Get-ChildItem $gdnConfigPath -Recurse -Include '*.gdnconfig' Write-Host "Discovered Guardian config files:" $gdnConfigFiles | Out-String | Write-Host Exec-BlockVerbosely { & $GuardianCliLocation run ` --working-directory $WorkingDirectory ` --baseline mainbaseline ` --update-baseline $UpdateBaseline ` --logger-level $GuardianLoggerLevel ` --config @gdnConfigFiles Exit-IfNZEC "Sdl" } } catch { Write-Host $_.ScriptStackTrace Write-PipelineTelemetryError -Force -Category 'Sdl' -Message $_ ExitWithExitCode 1 } ================================================ FILE: eng/common/sdl/sdl.ps1 ================================================ function Install-Gdn { param( [Parameter(Mandatory=$true)] [string]$Path, # If omitted, install the latest version of Guardian, otherwise install that specific version. [string]$Version ) $ErrorActionPreference = 'Stop' Set-StrictMode -Version 2.0 $disableConfigureToolsetImport = $true $global:LASTEXITCODE = 0 # `tools.ps1` checks $ci to perform some actions. Since the SDL # scripts don't necessarily execute in the same agent that run the # build.ps1/sh script this variable isn't automatically set. $ci = $true . $PSScriptRoot\..\tools.ps1 $argumentList = @("install", "Microsoft.Guardian.Cli", "-Source https://securitytools.pkgs.visualstudio.com/_packaging/Guardian/nuget/v3/index.json", "-OutputDirectory $Path", "-NonInteractive", "-NoCache") if ($Version) { $argumentList += "-Version $Version" } Start-Process nuget -Verbose -ArgumentList $argumentList -NoNewWindow -Wait $gdnCliPath = Get-ChildItem -Filter guardian.cmd -Recurse -Path $Path if (!$gdnCliPath) { Write-PipelineTelemetryError -Category 'Sdl' -Message 'Failure installing Guardian' } return $gdnCliPath.FullName } ================================================ FILE: eng/common/sdl/trim-assets-version.ps1 ================================================ <# .SYNOPSIS Install and run the 'Microsoft.DotNet.VersionTools.Cli' tool with the 'trim-artifacts-version' command to trim the version from the NuGet assets file name. .PARAMETER InputPath Full path to directory where artifact packages are stored .PARAMETER Recursive Search for NuGet packages recursively #> Param( [string] $InputPath, [bool] $Recursive = $true ) $CliToolName = "Microsoft.DotNet.VersionTools.Cli" function Install-VersionTools-Cli { param( [Parameter(Mandatory=$true)][string]$Version ) Write-Host "Installing the package '$CliToolName' with a version of '$version' ..." $feed = "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-eng/nuget/v3/index.json" $argumentList = @("tool", "install", "--local", "$CliToolName", "--add-source $feed", "--no-cache", "--version $Version", "--create-manifest-if-needed") Start-Process "$dotnet" -Verbose -ArgumentList $argumentList -NoNewWindow -Wait } # ------------------------------------------------------------------- if (!(Test-Path $InputPath)) { Write-Host "Input Path '$InputPath' does not exist" ExitWithExitCode 1 } $ErrorActionPreference = 'Stop' Set-StrictMode -Version 2.0 $disableConfigureToolsetImport = $true $global:LASTEXITCODE = 0 # `tools.ps1` checks $ci to perform some actions. Since the SDL # scripts don't necessarily execute in the same agent that run the # build.ps1/sh script this variable isn't automatically set. $ci = $true . $PSScriptRoot\..\tools.ps1 try { $dotnetRoot = InitializeDotNetCli -install:$true $dotnet = "$dotnetRoot\dotnet.exe" $toolsetVersion = Read-ArcadeSdkVersion Install-VersionTools-Cli -Version $toolsetVersion $cliToolFound = (& "$dotnet" tool list --local | Where-Object {$_.Split(' ')[0] -eq $CliToolName}) if ($null -eq $cliToolFound) { Write-PipelineTelemetryError -Force -Category 'Sdl' -Message "The '$CliToolName' tool is not installed." ExitWithExitCode 1 } Exec-BlockVerbosely { & "$dotnet" $CliToolName trim-assets-version ` --assets-path $InputPath ` --recursive $Recursive Exit-IfNZEC "Sdl" } } catch { Write-Host $_ Write-PipelineTelemetryError -Force -Category 'Sdl' -Message $_ ExitWithExitCode 1 } ================================================ FILE: eng/common/template-guidance.md ================================================ # Overview Arcade provides templates for public (`/templates`) and 1ES pipeline templates (`/templates-official`) scenarios. Pipelines which are required to be managed by 1ES pipeline templates should reference `/templates-offical`, all other pipelines may reference `/templates`. ## How to use Basic guidance is: - 1ES Pipeline Template or 1ES Microbuild template runs should reference `eng/common/templates-official`. Any internal production-graded pipeline should use these templates. - All other runs should reference `eng/common/templates`. See [azure-pipelines.yml](../../azure-pipelines.yml) (templates-official example) or [azure-pipelines-pr.yml](../../azure-pipelines-pr.yml) (templates example) for examples. #### The `templateIs1ESManaged` parameter The `templateIs1ESManaged` is available on most templates and affects which of the variants is used for nested templates. See [Development Notes](#development-notes) below for more information on the `templateIs1ESManaged1 parameter. - For templates under `job/`, `jobs/`, `steps`, or `post-build/`, this parameter must be explicitly set. ## Multiple outputs 1ES pipeline templates impose a policy where every publish artifact execution results in additional security scans being injected into your pipeline. When using `templates-official/jobs/jobs.yml`, Arcade reduces the number of additional security injections by gathering all publishing outputs into the [Build.ArtifactStagingDirectory](https://learn.microsoft.com/en-us/azure/devops/pipelines/build/variables?view=azure-devops&tabs=yaml#build-variables-devops-services), and utilizing the [outputParentDirectory](https://eng.ms/docs/cloud-ai-platform/devdiv/one-engineering-system-1es/1es-docs/1es-pipeline-templates/features/outputs#multiple-outputs) feature of 1ES pipeline templates. When implementing your pipeline, if you ensure publish artifacts are located in the `$(Build.ArtifactStagingDirectory)`, and utilize the 1ES provided template context, then you can reduce the number of security scans for your pipeline. Example: ``` yaml # azure-pipelines.yml extends: template: azure-pipelines/MicroBuild.1ES.Official.yml@MicroBuildTemplate parameters: stages: - stage: build jobs: - template: /eng/common/templates-official/jobs/jobs.yml@self parameters: # 1ES makes use of outputs to reduce security task injection overhead templateContext: outputs: - output: pipelineArtifact displayName: 'Publish logs from source' continueOnError: true condition: always() targetPath: $(Build.ArtifactStagingDirectory)/artifacts/log artifactName: Logs jobs: - job: Windows steps: - script: echo "friendly neighborhood" > artifacts/marvel/spiderman.txt # copy build outputs to artifact staging directory for publishing - task: CopyFiles@2 displayName: Gather build output inputs: SourceFolder: '$(System.DefaultWorkingDirectory)/artifacts/marvel' Contents: '**' TargetFolder: '$(Build.ArtifactStagingDirectory)/artifacts/marvel' ``` Note: Multiple outputs are ONLY applicable to 1ES PT publishing (only usable when referencing `templates-official`). ## Development notes **Folder / file structure** ``` text eng\common\ [templates || templates-official]\ job\ job.yml (shim + artifact publishing logic) onelocbuild.yml (shim) publish-build-assets.yml (shim) source-build.yml (shim) source-index-stage1.yml (shim) jobs\ codeql-build.yml (shim) jobs.yml (shim) source-build.yml (shim) post-build\ post-build.yml (shim) common-variabls.yml (shim) setup-maestro-vars.yml (shim) steps\ publish-build-artifacts.yml (logic) publish-pipeline-artifacts.yml (logic) component-governance.yml (shim) generate-sbom.yml (shim) publish-logs.yml (shim) retain-build.yml (shim) send-to-helix.yml (shim) source-build.yml (shim) variables\ pool-providers.yml (logic + redirect) # templates/variables/pool-providers.yml will redirect to templates-official/variables/pool-providers.yml if you are running in the internal project sdl-variables.yml (logic) core-templates\ job\ job.yml (logic) onelocbuild.yml (logic) publish-build-assets.yml (logic) source-build.yml (logic) source-index-stage1.yml (logic) jobs\ codeql-build.yml (logic) jobs.yml (logic) source-build.yml (logic) post-build\ common-variabls.yml (logic) post-build.yml (logic) setup-maestro-vars.yml (logic) steps\ component-governance.yml (logic) generate-sbom.yml (logic) publish-build-artifacts.yml (redirect) publish-logs.yml (logic) publish-pipeline-artifacts.yml (redirect) retain-build.yml (logic) send-to-helix.yml (logic) source-build.yml (logic) variables\ pool-providers.yml (redirect) ``` In the table above, a file is designated as "shim", "logic", or "redirect". - shim - represents a yaml file which is an intermediate step between pipeline logic and .Net Core Engineering's templates (`core-templates`) and defines the `is1ESPipeline` parameter value. - logic - represents actual base template logic. - redirect- represents a file in `core-templates` which redirects to the "logic" file in either `templates` or `templates-official`. Logic for Arcade's templates live **primarily** in the `core-templates` folder. The exceptions to the location of the logic files are around artifact publishing, which is handled differently between 1es pipeline templates and standard templates. `templates` and `templates-official` provide shim entry points which redirect to `core-templates` while also defining the `is1ESPipeline` parameter. If a shim is referenced in `templates`, then `is1ESPipeline` is set to `false`. If a shim is referenced in `templates-official`, then `is1ESPipeline` is set to `true`. Within `templates` and `templates-official`, the templates at the "stages", and "jobs" / "job" level have been replaced with shims. Templates at the "steps" and "variables" level are typically too granular to be replaced with shims and instead persist logic which is directly applicable to either scenario. Within `core-templates`, there are a handful of places where logic is dependent on which shim entry point was used. In those places, we redirect back to the respective logic file in `templates` or `templates-official`. ================================================ FILE: eng/common/templates/job/job.yml ================================================ parameters: enablePublishBuildArtifacts: false disableComponentGovernance: '' componentGovernanceIgnoreDirectories: '' # Sbom related params enableSbom: true runAsPublic: false PackageVersion: 9.0.0 BuildDropPath: '$(System.DefaultWorkingDirectory)/artifacts' jobs: - template: /eng/common/core-templates/job/job.yml parameters: is1ESPipeline: false ${{ each parameter in parameters }}: ${{ if and(ne(parameter.key, 'steps'), ne(parameter.key, 'is1ESPipeline')) }}: ${{ parameter.key }}: ${{ parameter.value }} steps: - ${{ each step in parameters.steps }}: - ${{ step }} componentGovernanceSteps: - template: /eng/common/templates/steps/component-governance.yml parameters: ${{ if eq(parameters.disableComponentGovernance, '') }}: ${{ if and(ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest'), eq(parameters.runAsPublic, 'false'), or(startsWith(variables['Build.SourceBranch'], 'refs/heads/release/'), startsWith(variables['Build.SourceBranch'], 'refs/heads/dotnet/'), startsWith(variables['Build.SourceBranch'], 'refs/heads/microsoft/'), eq(variables['Build.SourceBranch'], 'refs/heads/main'))) }}: disableComponentGovernance: false ${{ else }}: disableComponentGovernance: true ${{ else }}: disableComponentGovernance: ${{ parameters.disableComponentGovernance }} componentGovernanceIgnoreDirectories: ${{ parameters.componentGovernanceIgnoreDirectories }} artifactPublishSteps: - ${{ if ne(parameters.artifacts.publish, '') }}: - ${{ if and(ne(parameters.artifacts.publish.artifacts, 'false'), ne(parameters.artifacts.publish.artifacts, '')) }}: - template: /eng/common/core-templates/steps/publish-build-artifacts.yml parameters: is1ESPipeline: false args: displayName: Publish pipeline artifacts pathToPublish: '$(Build.ArtifactStagingDirectory)/artifacts' publishLocation: Container artifactName: ${{ coalesce(parameters.artifacts.publish.artifacts.name , 'Artifacts_$(Agent.Os)_$(_BuildConfig)') }} continueOnError: true condition: always() retryCountOnTaskFailure: 10 # for any logs being locked - ${{ if and(ne(parameters.artifacts.publish.logs, 'false'), ne(parameters.artifacts.publish.logs, '')) }}: - template: /eng/common/core-templates/steps/publish-pipeline-artifacts.yml parameters: is1ESPipeline: false args: targetPath: '$(Build.ArtifactStagingDirectory)/artifacts/log' artifactName: ${{ coalesce(parameters.artifacts.publish.logs.name, 'Logs_Build_$(Agent.Os)_$(_BuildConfig)') }} displayName: 'Publish logs' continueOnError: true condition: always() retryCountOnTaskFailure: 10 # for any logs being locked sbomEnabled: false # we don't need SBOM for logs - ${{ if ne(parameters.enablePublishBuildArtifacts, 'false') }}: - template: /eng/common/core-templates/steps/publish-build-artifacts.yml parameters: is1ESPipeline: false args: displayName: Publish Logs pathToPublish: '$(Build.ArtifactStagingDirectory)/artifacts/log/$(_BuildConfig)' publishLocation: Container artifactName: ${{ coalesce(parameters.enablePublishBuildArtifacts.artifactName, '$(Agent.Os)_$(Agent.JobName)_Attempt$(System.JobAttempt)' ) }} continueOnError: true condition: always() - ${{ if eq(parameters.enableBuildRetry, 'true') }}: - template: /eng/common/core-templates/steps/publish-pipeline-artifacts.yml parameters: is1ESPipeline: false args: targetPath: '$(System.DefaultWorkingDirectory)\eng\common\BuildConfiguration' artifactName: 'BuildConfiguration' displayName: 'Publish build retry configuration' continueOnError: true sbomEnabled: false # we don't need SBOM for BuildConfiguration ================================================ FILE: eng/common/templates/job/onelocbuild.yml ================================================ jobs: - template: /eng/common/core-templates/job/onelocbuild.yml parameters: is1ESPipeline: false ${{ each parameter in parameters }}: ${{ parameter.key }}: ${{ parameter.value }} ================================================ FILE: eng/common/templates/job/publish-build-assets.yml ================================================ jobs: - template: /eng/common/core-templates/job/publish-build-assets.yml parameters: is1ESPipeline: false ${{ each parameter in parameters }}: ${{ parameter.key }}: ${{ parameter.value }} ================================================ FILE: eng/common/templates/job/source-build.yml ================================================ jobs: - template: /eng/common/core-templates/job/source-build.yml parameters: is1ESPipeline: false ${{ each parameter in parameters }}: ${{ parameter.key }}: ${{ parameter.value }} ================================================ FILE: eng/common/templates/job/source-index-stage1.yml ================================================ jobs: - template: /eng/common/core-templates/job/source-index-stage1.yml parameters: is1ESPipeline: false ${{ each parameter in parameters }}: ${{ parameter.key }}: ${{ parameter.value }} ================================================ FILE: eng/common/templates/jobs/codeql-build.yml ================================================ jobs: - template: /eng/common/core-templates/jobs/codeql-build.yml parameters: is1ESPipeline: false ${{ each parameter in parameters }}: ${{ parameter.key }}: ${{ parameter.value }} ================================================ FILE: eng/common/templates/jobs/jobs.yml ================================================ jobs: - template: /eng/common/core-templates/jobs/jobs.yml parameters: is1ESPipeline: false ${{ each parameter in parameters }}: ${{ parameter.key }}: ${{ parameter.value }} ================================================ FILE: eng/common/templates/jobs/source-build.yml ================================================ jobs: - template: /eng/common/core-templates/jobs/source-build.yml parameters: is1ESPipeline: false ${{ each parameter in parameters }}: ${{ parameter.key }}: ${{ parameter.value }} ================================================ FILE: eng/common/templates/post-build/common-variables.yml ================================================ variables: - template: /eng/common/core-templates/post-build/common-variables.yml parameters: # Specifies whether to use 1ES is1ESPipeline: false ${{ each parameter in parameters }}: ${{ parameter.key }}: ${{ parameter.value }} ================================================ FILE: eng/common/templates/post-build/post-build.yml ================================================ stages: - template: /eng/common/core-templates/post-build/post-build.yml parameters: # Specifies whether to use 1ES is1ESPipeline: false ${{ each parameter in parameters }}: ${{ parameter.key }}: ${{ parameter.value }} ================================================ FILE: eng/common/templates/post-build/setup-maestro-vars.yml ================================================ steps: - template: /eng/common/core-templates/post-build/setup-maestro-vars.yml parameters: # Specifies whether to use 1ES is1ESPipeline: false ${{ each parameter in parameters }}: ${{ parameter.key }}: ${{ parameter.value }} ================================================ FILE: eng/common/templates/steps/component-governance.yml ================================================ steps: - template: /eng/common/core-templates/steps/component-governance.yml parameters: is1ESPipeline: false ${{ each parameter in parameters }}: ${{ parameter.key }}: ${{ parameter.value }} ================================================ FILE: eng/common/templates/steps/enable-internal-runtimes.yml ================================================ # Obtains internal runtime download credentials and populates the 'dotnetbuilds-internal-container-read-token-base64' # variable with the base64-encoded SAS token, by default steps: - template: /eng/common/core-templates/steps/enable-internal-runtimes.yml parameters: is1ESPipeline: false ${{ each parameter in parameters }}: ${{ parameter.key }}: ${{ parameter.value }} ================================================ FILE: eng/common/templates/steps/enable-internal-sources.yml ================================================ steps: - template: /eng/common/core-templates/steps/enable-internal-sources.yml parameters: is1ESPipeline: false ${{ each parameter in parameters }}: ${{ parameter.key }}: ${{ parameter.value }} ================================================ FILE: eng/common/templates/steps/generate-sbom.yml ================================================ steps: - template: /eng/common/core-templates/steps/generate-sbom.yml parameters: is1ESPipeline: false ${{ each parameter in parameters }}: ${{ parameter.key }}: ${{ parameter.value }} ================================================ FILE: eng/common/templates/steps/get-delegation-sas.yml ================================================ steps: - template: /eng/common/core-templates/steps/get-delegation-sas.yml parameters: is1ESPipeline: false ${{ each parameter in parameters }}: ${{ parameter.key }}: ${{ parameter.value }} ================================================ FILE: eng/common/templates/steps/get-federated-access-token.yml ================================================ steps: - template: /eng/common/core-templates/steps/get-federated-access-token.yml parameters: is1ESPipeline: false ${{ each parameter in parameters }}: ${{ parameter.key }}: ${{ parameter.value }} ================================================ FILE: eng/common/templates/steps/publish-build-artifacts.yml ================================================ parameters: - name: is1ESPipeline type: boolean default: false - name: displayName type: string default: 'Publish to Build Artifact' - name: condition type: string default: succeeded() - name: artifactName type: string - name: pathToPublish type: string - name: continueOnError type: boolean default: false - name: publishLocation type: string default: 'Container' - name: retryCountOnTaskFailure type: string default: 10 steps: - ${{ if eq(parameters.is1ESPipeline, true) }}: - 'eng/common/templates cannot be referenced from a 1ES managed template': error - task: PublishBuildArtifacts@1 displayName: ${{ parameters.displayName }} condition: ${{ parameters.condition }} ${{ if parameters.continueOnError }}: continueOnError: ${{ parameters.continueOnError }} inputs: PublishLocation: ${{ parameters.publishLocation }} PathtoPublish: ${{ parameters.pathToPublish }} ${{ if parameters.artifactName }}: ArtifactName: ${{ parameters.artifactName }} ${{ if parameters.retryCountOnTaskFailure }}: retryCountOnTaskFailure: ${{ parameters.retryCountOnTaskFailure }} ================================================ FILE: eng/common/templates/steps/publish-logs.yml ================================================ steps: - template: /eng/common/core-templates/steps/publish-logs.yml parameters: is1ESPipeline: false ${{ each parameter in parameters }}: ${{ parameter.key }}: ${{ parameter.value }} ================================================ FILE: eng/common/templates/steps/publish-pipeline-artifacts.yml ================================================ parameters: - name: is1ESPipeline type: boolean default: false - name: args type: object default: {} steps: - ${{ if eq(parameters.is1ESPipeline, true) }}: - 'eng/common/templates cannot be referenced from a 1ES managed template': error - task: PublishPipelineArtifact@1 displayName: ${{ coalesce(parameters.args.displayName, 'Publish to Build Artifact') }} ${{ if parameters.args.condition }}: condition: ${{ parameters.args.condition }} ${{ else }}: condition: succeeded() ${{ if parameters.args.continueOnError }}: continueOnError: ${{ parameters.args.continueOnError }} inputs: targetPath: ${{ parameters.args.targetPath }} ${{ if parameters.args.artifactName }}: artifactName: ${{ parameters.args.artifactName }} ${{ if parameters.args.publishLocation }}: publishLocation: ${{ parameters.args.publishLocation }} ${{ if parameters.args.fileSharePath }}: fileSharePath: ${{ parameters.args.fileSharePath }} ${{ if parameters.args.Parallel }}: parallel: ${{ parameters.args.Parallel }} ${{ if parameters.args.parallelCount }}: parallelCount: ${{ parameters.args.parallelCount }} ${{ if parameters.args.properties }}: properties: ${{ parameters.args.properties }} ================================================ FILE: eng/common/templates/steps/retain-build.yml ================================================ steps: - template: /eng/common/core-templates/steps/retain-build.yml parameters: is1ESPipeline: false ${{ each parameter in parameters }}: ${{ parameter.key }}: ${{ parameter.value }} ================================================ FILE: eng/common/templates/steps/send-to-helix.yml ================================================ steps: - template: /eng/common/core-templates/steps/send-to-helix.yml parameters: is1ESPipeline: false ${{ each parameter in parameters }}: ${{ parameter.key }}: ${{ parameter.value }} ================================================ FILE: eng/common/templates/steps/source-build.yml ================================================ steps: - template: /eng/common/core-templates/steps/source-build.yml parameters: is1ESPipeline: false ${{ each parameter in parameters }}: ${{ parameter.key }}: ${{ parameter.value }} ================================================ FILE: eng/common/templates/steps/source-index-stage1-publish.yml ================================================ steps: - template: /eng/common/core-templates/steps/source-index-stage1-publish.yml parameters: is1ESPipeline: false ${{ each parameter in parameters }}: ${{ parameter.key }}: ${{ parameter.value }} ================================================ FILE: eng/common/templates/steps/vmr-sync.yml ================================================ ### These steps synchronize new code from product repositories into the VMR (https://github.com/dotnet/dotnet). ### They initialize the darc CLI and pull the new updates. ### Changes are applied locally onto the already cloned VMR (located in $vmrPath). parameters: - name: targetRef displayName: Target revision in dotnet/ to synchronize type: string default: $(Build.SourceVersion) - name: vmrPath displayName: Path where the dotnet/dotnet is checked out to type: string default: $(Agent.BuildDirectory)/vmr - name: additionalSyncs displayName: Optional list of package names whose repo's source will also be synchronized in the local VMR, e.g. NuGet.Protocol type: object default: [] steps: - checkout: vmr displayName: Clone dotnet/dotnet path: vmr clean: true - checkout: self displayName: Clone $(Build.Repository.Name) path: repo fetchDepth: 0 # This step is needed so that when we get a detached HEAD / shallow clone, # we still pull the commit into the temporary repo clone to use it during the sync. # Also unshallow the clone so that forwardflow command would work. - script: | git branch repo-head git rev-parse HEAD displayName: Label PR commit workingDirectory: $(Agent.BuildDirectory)/repo - script: | git config --global user.name "dotnet-maestro[bot]" git config --global user.email "dotnet-maestro[bot]@users.noreply.github.com" displayName: Set git author to dotnet-maestro[bot] workingDirectory: ${{ parameters.vmrPath }} - script: | ./eng/common/vmr-sync.sh \ --vmr ${{ parameters.vmrPath }} \ --tmp $(Agent.TempDirectory) \ --azdev-pat '$(dn-bot-all-orgs-code-r)' \ --ci \ --debug if [ "$?" -ne 0 ]; then echo "##vso[task.logissue type=error]Failed to synchronize the VMR" exit 1 fi displayName: Sync repo into VMR (Unix) condition: ne(variables['Agent.OS'], 'Windows_NT') workingDirectory: $(Agent.BuildDirectory)/repo - script: | git config --global diff.astextplain.textconv echo git config --system core.longpaths true displayName: Configure Windows git (longpaths, astextplain) condition: eq(variables['Agent.OS'], 'Windows_NT') - powershell: | ./eng/common/vmr-sync.ps1 ` -vmr ${{ parameters.vmrPath }} ` -tmp $(Agent.TempDirectory) ` -azdevPat '$(dn-bot-all-orgs-code-r)' ` -ci ` -debugOutput if ($LASTEXITCODE -ne 0) { echo "##vso[task.logissue type=error]Failed to synchronize the VMR" exit 1 } displayName: Sync repo into VMR (Windows) condition: eq(variables['Agent.OS'], 'Windows_NT') workingDirectory: $(Agent.BuildDirectory)/repo - ${{ if eq(variables['Build.Reason'], 'PullRequest') }}: - task: CopyFiles@2 displayName: Collect failed patches condition: failed() inputs: SourceFolder: '$(Agent.TempDirectory)' Contents: '*.patch' TargetFolder: '$(Build.ArtifactStagingDirectory)/FailedPatches' - publish: '$(Build.ArtifactStagingDirectory)/FailedPatches' artifact: $(System.JobDisplayName)_FailedPatches displayName: Upload failed patches condition: failed() - ${{ each assetName in parameters.additionalSyncs }}: # The vmr-sync script ends up staging files in the local VMR so we have to commit those - script: git commit --allow-empty -am "Forward-flow $(Build.Repository.Name)" displayName: Commit local VMR changes workingDirectory: ${{ parameters.vmrPath }} - script: | set -ex echo "Searching for details of asset ${{ assetName }}..." # Use darc to get dependencies information dependencies=$(./.dotnet/dotnet darc get-dependencies --name '${{ assetName }}' --ci) # Extract repository URL and commit hash repository=$(echo "$dependencies" | grep 'Repo:' | sed 's/Repo:[[:space:]]*//' | head -1) if [ -z "$repository" ]; then echo "##vso[task.logissue type=error]Asset ${{ assetName }} not found in the dependency list" exit 1 fi commit=$(echo "$dependencies" | grep 'Commit:' | sed 's/Commit:[[:space:]]*//' | head -1) echo "Updating the VMR from $repository / $commit..." cd .. git clone $repository ${{ assetName }} cd ${{ assetName }} git checkout $commit git branch "sync/$commit" ./eng/common/vmr-sync.sh \ --vmr ${{ parameters.vmrPath }} \ --tmp $(Agent.TempDirectory) \ --azdev-pat '$(dn-bot-all-orgs-code-r)' \ --ci \ --debug if [ "$?" -ne 0 ]; then echo "##vso[task.logissue type=error]Failed to synchronize the VMR" exit 1 fi displayName: Sync ${{ assetName }} into (Unix) condition: ne(variables['Agent.OS'], 'Windows_NT') workingDirectory: $(Agent.BuildDirectory)/repo - powershell: | $ErrorActionPreference = 'Stop' Write-Host "Searching for details of asset ${{ assetName }}..." $dependencies = .\.dotnet\dotnet darc get-dependencies --name '${{ assetName }}' --ci $repository = $dependencies | Select-String -Pattern 'Repo:\s+([^\s]+)' | Select-Object -First 1 $repository -match 'Repo:\s+([^\s]+)' | Out-Null $repository = $matches[1] if ($repository -eq $null) { Write-Error "Asset ${{ assetName }} not found in the dependency list" exit 1 } $commit = $dependencies | Select-String -Pattern 'Commit:\s+([^\s]+)' | Select-Object -First 1 $commit -match 'Commit:\s+([^\s]+)' | Out-Null $commit = $matches[1] Write-Host "Updating the VMR from $repository / $commit..." cd .. git clone $repository ${{ assetName }} cd ${{ assetName }} git checkout $commit git branch "sync/$commit" .\eng\common\vmr-sync.ps1 ` -vmr ${{ parameters.vmrPath }} ` -tmp $(Agent.TempDirectory) ` -azdevPat '$(dn-bot-all-orgs-code-r)' ` -ci ` -debugOutput if ($LASTEXITCODE -ne 0) { echo "##vso[task.logissue type=error]Failed to synchronize the VMR" exit 1 } displayName: Sync ${{ assetName }} into (Windows) condition: ne(variables['Agent.OS'], 'Windows_NT') workingDirectory: $(Agent.BuildDirectory)/repo ================================================ FILE: eng/common/templates/variables/pool-providers.yml ================================================ # Select a pool provider based off branch name. Anything with branch name containing 'release' must go into an -Svc pool, # otherwise it should go into the "normal" pools. This separates out the queueing and billing of released branches. # Motivation: # Once a given branch of a repository's output has been officially "shipped" once, it is then considered to be COGS # (Cost of goods sold) and should be moved to a servicing pool provider. This allows both separation of queueing # (allowing release builds and main PR builds to not intefere with each other) and billing (required for COGS. # Additionally, the pool provider name itself may be subject to change when the .NET Core Engineering Services # team needs to move resources around and create new and potentially differently-named pools. Using this template # file from an Arcade-ified repo helps guard against both having to update one's release/* branches and renaming. # How to use: # This yaml assumes your shipped product branches use the naming convention "release/..." (which many do). # If we find alternate naming conventions in broad usage it can be added to the condition below. # # First, import the template in an arcade-ified repo to pick up the variables, e.g.: # # variables: # - template: /eng/common/templates/variables/pool-providers.yml # # ... then anywhere specifying the pool provider use the runtime variables, # $(DncEngInternalBuildPool) and $ (DncEngPublicBuildPool), e.g.: # # pool: # name: $(DncEngInternalBuildPool) # demands: ImageOverride -equals windows.vs2022.amd64 variables: - ${{ if eq(variables['System.TeamProject'], 'internal') }}: - template: /eng/common/templates-official/variables/pool-providers.yml - ${{ else }}: # Coalesce the target and source branches so we know when a PR targets a release branch # If these variables are somehow missing, fall back to main (tends to have more capacity) # Any new -Svc alternative pools should have variables added here to allow for splitting work - name: DncEngPublicBuildPool value: $[ replace( replace( eq(contains(coalesce(variables['System.PullRequest.TargetBranch'], variables['Build.SourceBranch'], 'refs/heads/main'), 'release'), 'true'), True, 'NetCore-Svc-Public' ), False, 'NetCore-Public' ) ] - name: DncEngInternalBuildPool value: $[ replace( replace( eq(contains(coalesce(variables['System.PullRequest.TargetBranch'], variables['Build.SourceBranch'], 'refs/heads/main'), 'release'), 'true'), True, 'NetCore1ESPool-Svc-Internal' ), False, 'NetCore1ESPool-Internal' ) ] ================================================ FILE: eng/common/templates/vmr-build-pr.yml ================================================ # This pipeline is used for running the VMR verification of the PR changes in repo-level PRs. # # It will run a full set of verification jobs defined in: # https://github.com/dotnet/dotnet/blob/10060d128e3f470e77265f8490f5e4f72dae738e/eng/pipelines/templates/stages/vmr-build.yml#L27-L38 # # For repos that do not need to run the full set, you would do the following: # # 1. Copy this YML file to a repo-specific location, i.e. outside of eng/common. # # 2. Add `verifications` parameter to VMR template reference # # Examples: # - For source-build stage 1 verification, add the following: # verifications: [ "source-build-stage1" ] # # - For Windows only verifications, add the following: # verifications: [ "unified-build-windows-x64", "unified-build-windows-x86" ] trigger: none pr: none variables: - template: /eng/common/templates/variables/pool-providers.yml@self - name: skipComponentGovernanceDetection # we run CG on internal builds only value: true - name: Codeql.Enabled # we run CodeQL on internal builds only value: false resources: repositories: - repository: vmr type: github name: dotnet/dotnet endpoint: dotnet ref: refs/heads/main # Set to whatever VMR branch the PR build should insert into stages: - template: /eng/pipelines/templates/stages/vmr-build.yml@vmr parameters: isBuiltFromVmr: false scope: lite ================================================ FILE: eng/common/templates-official/job/job.yml ================================================ parameters: # Sbom related params enableSbom: true runAsPublic: false PackageVersion: 9.0.0 BuildDropPath: '$(System.DefaultWorkingDirectory)/artifacts' jobs: - template: /eng/common/core-templates/job/job.yml parameters: is1ESPipeline: true componentGovernanceSteps: - ${{ if and(eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest'), eq(parameters.enableSbom, 'true')) }}: - template: /eng/common/templates/steps/generate-sbom.yml parameters: PackageVersion: ${{ parameters.packageVersion }} BuildDropPath: ${{ parameters.buildDropPath }} ManifestDirPath: $(Build.ArtifactStagingDirectory)/sbom publishArtifacts: false # publish artifacts # for 1ES managed templates, use the templateContext.output to handle multiple outputs. templateContext: outputParentDirectory: $(Build.ArtifactStagingDirectory) outputs: - ${{ if ne(parameters.artifacts.publish, '') }}: - ${{ if and(ne(parameters.artifacts.publish.artifacts, 'false'), ne(parameters.artifacts.publish.artifacts, '')) }}: - output: buildArtifacts displayName: Publish pipeline artifacts PathtoPublish: '$(Build.ArtifactStagingDirectory)/artifacts' ArtifactName: ${{ coalesce(parameters.artifacts.publish.artifacts.name , 'Artifacts_$(Agent.Os)_$(_BuildConfig)') }} condition: always() retryCountOnTaskFailure: 10 # for any logs being locked continueOnError: true - ${{ if and(ne(parameters.artifacts.publish.logs, 'false'), ne(parameters.artifacts.publish.logs, '')) }}: - output: pipelineArtifact targetPath: '$(Build.ArtifactStagingDirectory)/artifacts/log' artifactName: ${{ coalesce(parameters.artifacts.publish.logs.name, 'Logs_Build_$(Agent.Os)_$(_BuildConfig)_Attempt$(System.JobAttempt)') }} displayName: 'Publish logs' continueOnError: true condition: always() retryCountOnTaskFailure: 10 # for any logs being locked sbomEnabled: false # we don't need SBOM for logs - ${{ if eq(parameters.enablePublishBuildArtifacts, true) }}: - output: buildArtifacts displayName: Publish Logs PathtoPublish: '$(Build.ArtifactStagingDirectory)/artifacts/log/$(_BuildConfig)' publishLocation: Container ArtifactName: ${{ coalesce(parameters.enablePublishBuildArtifacts.artifactName, '$(Agent.Os)_$(Agent.JobName)_Attempt$(System.JobAttempt)' ) }} continueOnError: true condition: always() sbomEnabled: false # we don't need SBOM for logs - ${{ if eq(parameters.enableBuildRetry, 'true') }}: - output: pipelineArtifact targetPath: '$(Build.ArtifactStagingDirectory)/artifacts/eng/common/BuildConfiguration' artifactName: 'BuildConfiguration' displayName: 'Publish build retry configuration' continueOnError: true sbomEnabled: false # we don't need SBOM for BuildConfiguration - ${{ if and(eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest'), eq(parameters.enableSbom, 'true')) }}: - output: pipelineArtifact displayName: Publish SBOM manifest continueOnError: true targetPath: $(Build.ArtifactStagingDirectory)/sbom artifactName: $(ARTIFACT_NAME) # add any outputs provided via root yaml - ${{ if ne(parameters.templateContext.outputs, '') }}: - ${{ each output in parameters.templateContext.outputs }}: - ${{ output }} # add any remaining templateContext properties ${{ each context in parameters.templateContext }}: ${{ if and(ne(context.key, 'outputParentDirectory'), ne(context.key, 'outputs')) }}: ${{ context.key }}: ${{ context.value }} ${{ each parameter in parameters }}: ${{ if and(ne(parameter.key, 'templateContext'), ne(parameter.key, 'is1ESPipeline')) }}: ${{ parameter.key }}: ${{ parameter.value }} ================================================ FILE: eng/common/templates-official/job/onelocbuild.yml ================================================ jobs: - template: /eng/common/core-templates/job/onelocbuild.yml parameters: is1ESPipeline: true ${{ each parameter in parameters }}: ${{ parameter.key }}: ${{ parameter.value }} ================================================ FILE: eng/common/templates-official/job/publish-build-assets.yml ================================================ jobs: - template: /eng/common/core-templates/job/publish-build-assets.yml parameters: is1ESPipeline: true ${{ each parameter in parameters }}: ${{ parameter.key }}: ${{ parameter.value }} ================================================ FILE: eng/common/templates-official/job/source-build.yml ================================================ jobs: - template: /eng/common/core-templates/job/source-build.yml parameters: is1ESPipeline: true ${{ each parameter in parameters }}: ${{ parameter.key }}: ${{ parameter.value }} ================================================ FILE: eng/common/templates-official/job/source-index-stage1.yml ================================================ jobs: - template: /eng/common/core-templates/job/source-index-stage1.yml parameters: is1ESPipeline: true ${{ each parameter in parameters }}: ${{ parameter.key }}: ${{ parameter.value }} ================================================ FILE: eng/common/templates-official/jobs/codeql-build.yml ================================================ jobs: - template: /eng/common/core-templates/jobs/codeql-build.yml parameters: is1ESPipeline: true ${{ each parameter in parameters }}: ${{ parameter.key }}: ${{ parameter.value }} ================================================ FILE: eng/common/templates-official/jobs/jobs.yml ================================================ jobs: - template: /eng/common/core-templates/jobs/jobs.yml parameters: is1ESPipeline: true ${{ each parameter in parameters }}: ${{ parameter.key }}: ${{ parameter.value }} ================================================ FILE: eng/common/templates-official/jobs/source-build.yml ================================================ jobs: - template: /eng/common/core-templates/jobs/source-build.yml parameters: is1ESPipeline: true ${{ each parameter in parameters }}: ${{ parameter.key }}: ${{ parameter.value }} ================================================ FILE: eng/common/templates-official/post-build/common-variables.yml ================================================ variables: - template: /eng/common/core-templates/post-build/common-variables.yml parameters: # Specifies whether to use 1ES is1ESPipeline: true ${{ each parameter in parameters }}: ${{ parameter.key }}: ${{ parameter.value }} ================================================ FILE: eng/common/templates-official/post-build/post-build.yml ================================================ stages: - template: /eng/common/core-templates/post-build/post-build.yml parameters: # Specifies whether to use 1ES is1ESPipeline: true ${{ each parameter in parameters }}: ${{ parameter.key }}: ${{ parameter.value }} ================================================ FILE: eng/common/templates-official/post-build/setup-maestro-vars.yml ================================================ steps: - template: /eng/common/core-templates/post-build/setup-maestro-vars.yml parameters: # Specifies whether to use 1ES is1ESPipeline: true ${{ each parameter in parameters }}: ${{ parameter.key }}: ${{ parameter.value }} ================================================ FILE: eng/common/templates-official/steps/component-governance.yml ================================================ steps: - template: /eng/common/core-templates/steps/component-governance.yml parameters: is1ESPipeline: true ${{ each parameter in parameters }}: ${{ parameter.key }}: ${{ parameter.value }} ================================================ FILE: eng/common/templates-official/steps/enable-internal-runtimes.yml ================================================ # Obtains internal runtime download credentials and populates the 'dotnetbuilds-internal-container-read-token-base64' # variable with the base64-encoded SAS token, by default steps: - template: /eng/common/core-templates/steps/enable-internal-runtimes.yml parameters: is1ESPipeline: true ${{ each parameter in parameters }}: ${{ parameter.key }}: ${{ parameter.value }} ================================================ FILE: eng/common/templates-official/steps/enable-internal-sources.yml ================================================ steps: - template: /eng/common/core-templates/steps/enable-internal-sources.yml parameters: is1ESPipeline: true ${{ each parameter in parameters }}: ${{ parameter.key }}: ${{ parameter.value }} ================================================ FILE: eng/common/templates-official/steps/generate-sbom.yml ================================================ steps: - template: /eng/common/core-templates/steps/generate-sbom.yml parameters: is1ESPipeline: true ${{ each parameter in parameters }}: ${{ parameter.key }}: ${{ parameter.value }} ================================================ FILE: eng/common/templates-official/steps/get-delegation-sas.yml ================================================ steps: - template: /eng/common/core-templates/steps/get-delegation-sas.yml parameters: is1ESPipeline: true ${{ each parameter in parameters }}: ${{ parameter.key }}: ${{ parameter.value }} ================================================ FILE: eng/common/templates-official/steps/get-federated-access-token.yml ================================================ steps: - template: /eng/common/core-templates/steps/get-federated-access-token.yml parameters: is1ESPipeline: true ${{ each parameter in parameters }}: ${{ parameter.key }}: ${{ parameter.value }} ================================================ FILE: eng/common/templates-official/steps/publish-build-artifacts.yml ================================================ parameters: - name: displayName type: string default: 'Publish to Build Artifact' - name: condition type: string default: succeeded() - name: artifactName type: string - name: pathToPublish type: string - name: continueOnError type: boolean default: false - name: publishLocation type: string default: 'Container' - name: is1ESPipeline type: boolean default: true - name: retryCountOnTaskFailure type: string default: 10 steps: - ${{ if ne(parameters.is1ESPipeline, true) }}: - 'eng/common/templates-official cannot be referenced from a non-1ES managed template': error - task: 1ES.PublishBuildArtifacts@1 displayName: ${{ parameters.displayName }} condition: ${{ parameters.condition }} ${{ if parameters.continueOnError }}: continueOnError: ${{ parameters.continueOnError }} inputs: PublishLocation: ${{ parameters.publishLocation }} PathtoPublish: ${{ parameters.pathToPublish }} ${{ if parameters.artifactName }}: ArtifactName: ${{ parameters.artifactName }} ${{ if parameters.retryCountOnTaskFailure }}: retryCountOnTaskFailure: ${{ parameters.retryCountOnTaskFailure }} ================================================ FILE: eng/common/templates-official/steps/publish-logs.yml ================================================ steps: - template: /eng/common/core-templates/steps/publish-logs.yml parameters: is1ESPipeline: true ${{ each parameter in parameters }}: ${{ parameter.key }}: ${{ parameter.value }} ================================================ FILE: eng/common/templates-official/steps/publish-pipeline-artifacts.yml ================================================ parameters: - name: is1ESPipeline type: boolean default: true - name: args type: object default: {} steps: - ${{ if ne(parameters.is1ESPipeline, true) }}: - 'eng/common/templates-official cannot be referenced from a non-1ES managed template': error - task: 1ES.PublishPipelineArtifact@1 displayName: ${{ coalesce(parameters.args.displayName, 'Publish to Build Artifact') }} ${{ if parameters.args.condition }}: condition: ${{ parameters.args.condition }} ${{ else }}: condition: succeeded() ${{ if parameters.args.continueOnError }}: continueOnError: ${{ parameters.args.continueOnError }} inputs: targetPath: ${{ parameters.args.targetPath }} ${{ if parameters.args.artifactName }}: artifactName: ${{ parameters.args.artifactName }} ${{ if parameters.args.properties }}: properties: ${{ parameters.args.properties }} ${{ if parameters.args.sbomEnabled }}: sbomEnabled: ${{ parameters.args.sbomEnabled }} ================================================ FILE: eng/common/templates-official/steps/retain-build.yml ================================================ steps: - template: /eng/common/core-templates/steps/retain-build.yml parameters: is1ESPipeline: true ${{ each parameter in parameters }}: ${{ parameter.key }}: ${{ parameter.value }} ================================================ FILE: eng/common/templates-official/steps/send-to-helix.yml ================================================ steps: - template: /eng/common/core-templates/steps/send-to-helix.yml parameters: is1ESPipeline: true ${{ each parameter in parameters }}: ${{ parameter.key }}: ${{ parameter.value }} ================================================ FILE: eng/common/templates-official/steps/source-build.yml ================================================ steps: - template: /eng/common/core-templates/steps/source-build.yml parameters: is1ESPipeline: true ${{ each parameter in parameters }}: ${{ parameter.key }}: ${{ parameter.value }} ================================================ FILE: eng/common/templates-official/steps/source-index-stage1-publish.yml ================================================ steps: - template: /eng/common/core-templates/steps/source-index-stage1-publish.yml parameters: is1ESPipeline: true ${{ each parameter in parameters }}: ${{ parameter.key }}: ${{ parameter.value }} ================================================ FILE: eng/common/templates-official/variables/pool-providers.yml ================================================ # Select a pool provider based off branch name. Anything with branch name containing 'release' must go into an -Svc pool, # otherwise it should go into the "normal" pools. This separates out the queueing and billing of released branches. # Motivation: # Once a given branch of a repository's output has been officially "shipped" once, it is then considered to be COGS # (Cost of goods sold) and should be moved to a servicing pool provider. This allows both separation of queueing # (allowing release builds and main PR builds to not intefere with each other) and billing (required for COGS. # Additionally, the pool provider name itself may be subject to change when the .NET Core Engineering Services # team needs to move resources around and create new and potentially differently-named pools. Using this template # file from an Arcade-ified repo helps guard against both having to update one's release/* branches and renaming. # How to use: # This yaml assumes your shipped product branches use the naming convention "release/..." (which many do). # If we find alternate naming conventions in broad usage it can be added to the condition below. # # First, import the template in an arcade-ified repo to pick up the variables, e.g.: # # variables: # - template: /eng/common/templates-official/variables/pool-providers.yml # # ... then anywhere specifying the pool provider use the runtime variables, # $(DncEngInternalBuildPool) # # pool: # name: $(DncEngInternalBuildPool) # image: 1es-windows-2022 variables: # Coalesce the target and source branches so we know when a PR targets a release branch # If these variables are somehow missing, fall back to main (tends to have more capacity) # Any new -Svc alternative pools should have variables added here to allow for splitting work - name: DncEngInternalBuildPool value: $[ replace( replace( eq(contains(coalesce(variables['System.PullRequest.TargetBranch'], variables['Build.SourceBranch'], 'refs/heads/main'), 'release'), 'true'), True, 'NetCore1ESPool-Svc-Internal' ), False, 'NetCore1ESPool-Internal' ) ] ================================================ FILE: eng/common/templates-official/variables/sdl-variables.yml ================================================ variables: # The Guardian version specified in 'eng/common/sdl/packages.config'. This value must be kept in # sync with the packages.config file. - name: DefaultGuardianVersion value: 0.109.0 - name: GuardianPackagesConfigFile value: $(System.DefaultWorkingDirectory)\eng\common\sdl\packages.config ================================================ FILE: eng/common/tools.ps1 ================================================ # Initialize variables if they aren't already defined. # These may be defined as parameters of the importing script, or set after importing this script. # CI mode - set to true on CI server for PR validation build or official build. [bool]$ci = if (Test-Path variable:ci) { $ci } else { $false } # Build configuration. Common values include 'Debug' and 'Release', but the repository may use other names. [string]$configuration = if (Test-Path variable:configuration) { $configuration } else { 'Debug' } # Set to true to opt out of outputting binary log while running in CI [bool]$excludeCIBinarylog = if (Test-Path variable:excludeCIBinarylog) { $excludeCIBinarylog } else { $false } # Set to true to output binary log from msbuild. Note that emitting binary log slows down the build. [bool]$binaryLog = if (Test-Path variable:binaryLog) { $binaryLog } else { $ci -and !$excludeCIBinarylog } # Set to true to use the pipelines logger which will enable Azure logging output. # https://github.com/Microsoft/azure-pipelines-tasks/blob/master/docs/authoring/commands.md # This flag is meant as a temporary opt-opt for the feature while validate it across # our consumers. It will be deleted in the future. [bool]$pipelinesLog = if (Test-Path variable:pipelinesLog) { $pipelinesLog } else { $ci } # Turns on machine preparation/clean up code that changes the machine state (e.g. kills build processes). [bool]$prepareMachine = if (Test-Path variable:prepareMachine) { $prepareMachine } else { $false } # True to restore toolsets and dependencies. [bool]$restore = if (Test-Path variable:restore) { $restore } else { $true } # Adjusts msbuild verbosity level. [string]$verbosity = if (Test-Path variable:verbosity) { $verbosity } else { 'minimal' } # Set to true to reuse msbuild nodes. Recommended to not reuse on CI. [bool]$nodeReuse = if (Test-Path variable:nodeReuse) { $nodeReuse } else { !$ci } # Configures warning treatment in msbuild. [bool]$warnAsError = if (Test-Path variable:warnAsError) { $warnAsError } else { $true } # Specifies which msbuild engine to use for build: 'vs', 'dotnet' or unspecified (determined based on presence of tools.vs in global.json). [string]$msbuildEngine = if (Test-Path variable:msbuildEngine) { $msbuildEngine } else { $null } # True to attempt using .NET Core already that meets requirements specified in global.json # installed on the machine instead of downloading one. [bool]$useInstalledDotNetCli = if (Test-Path variable:useInstalledDotNetCli) { $useInstalledDotNetCli } else { $true } # Enable repos to use a particular version of the on-line dotnet-install scripts. # default URL: https://builds.dotnet.microsoft.com/dotnet/scripts/v1/dotnet-install.ps1 [string]$dotnetInstallScriptVersion = if (Test-Path variable:dotnetInstallScriptVersion) { $dotnetInstallScriptVersion } else { 'v1' } # True to use global NuGet cache instead of restoring packages to repository-local directory. [bool]$useGlobalNuGetCache = if (Test-Path variable:useGlobalNuGetCache) { $useGlobalNuGetCache } else { !$ci } # True to exclude prerelease versions Visual Studio during build [bool]$excludePrereleaseVS = if (Test-Path variable:excludePrereleaseVS) { $excludePrereleaseVS } else { $false } # An array of names of processes to stop on script exit if prepareMachine is true. $processesToStopOnExit = if (Test-Path variable:processesToStopOnExit) { $processesToStopOnExit } else { @('msbuild', 'dotnet', 'vbcscompiler') } $disableConfigureToolsetImport = if (Test-Path variable:disableConfigureToolsetImport) { $disableConfigureToolsetImport } else { $null } set-strictmode -version 2.0 $ErrorActionPreference = 'Stop' [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 # If specifies, provides an alternate path for getting .NET Core SDKs and Runtimes. This script will still try public sources first. [string]$runtimeSourceFeed = if (Test-Path variable:runtimeSourceFeed) { $runtimeSourceFeed } else { $null } # Base-64 encoded SAS token that has permission to storage container described by $runtimeSourceFeed [string]$runtimeSourceFeedKey = if (Test-Path variable:runtimeSourceFeedKey) { $runtimeSourceFeedKey } else { $null } # True when the build is running within the VMR. [bool]$fromVMR = if (Test-Path variable:fromVMR) { $fromVMR } else { $false } function Create-Directory ([string[]] $path) { New-Item -Path $path -Force -ItemType 'Directory' | Out-Null } function Unzip([string]$zipfile, [string]$outpath) { Add-Type -AssemblyName System.IO.Compression.FileSystem [System.IO.Compression.ZipFile]::ExtractToDirectory($zipfile, $outpath) } # This will exec a process using the console and return it's exit code. # This will not throw when the process fails. # Returns process exit code. function Exec-Process([string]$command, [string]$commandArgs) { $startInfo = New-Object System.Diagnostics.ProcessStartInfo $startInfo.FileName = $command $startInfo.Arguments = $commandArgs $startInfo.UseShellExecute = $false $startInfo.WorkingDirectory = Get-Location $process = New-Object System.Diagnostics.Process $process.StartInfo = $startInfo $process.Start() | Out-Null $finished = $false try { while (-not $process.WaitForExit(100)) { # Non-blocking loop done to allow ctr-c interrupts } $finished = $true return $global:LASTEXITCODE = $process.ExitCode } finally { # If we didn't finish then an error occurred or the user hit ctrl-c. Either # way kill the process if (-not $finished) { $process.Kill() } } } # Take the given block, print it, print what the block probably references from the current set of # variables using low-effort string matching, then run the block. # # This is intended to replace the pattern of manually copy-pasting a command, wrapping it in quotes, # and printing it using "Write-Host". The copy-paste method is more readable in build logs, but less # maintainable and less reliable. It is easy to make a mistake and modify the command without # properly updating the "Write-Host" line, resulting in misleading build logs. The probability of # this mistake makes the pattern hard to trust when it shows up in build logs. Finding the bug in # existing source code can also be difficult, because the strings are not aligned to each other and # the line may be 300+ columns long. # # By removing the need to maintain two copies of the command, Exec-BlockVerbosely avoids the issues. # # In Bash (or any posix-like shell), "set -x" prints usable verbose output automatically. # "Set-PSDebug" appears to be similar at first glance, but unfortunately, it isn't very useful: it # doesn't print any info about the variables being used by the command, which is normally the # interesting part to diagnose. function Exec-BlockVerbosely([scriptblock] $block) { Write-Host "--- Running script block:" $blockString = $block.ToString().Trim() Write-Host $blockString Write-Host "--- List of variables that might be used:" # For each variable x in the environment, check the block for a reference to x via simple "$x" or # "@x" syntax. This doesn't detect other ways to reference variables ("${x}" nor "$variable:x", # among others). It only catches what this function was originally written for: simple # command-line commands. $variableTable = Get-Variable | Where-Object { $blockString.Contains("`$$($_.Name)") -or $blockString.Contains("@$($_.Name)") } | Format-Table -AutoSize -HideTableHeaders -Wrap | Out-String Write-Host $variableTable.Trim() Write-Host "--- Executing:" & $block Write-Host "--- Done running script block!" } # createSdkLocationFile parameter enables a file being generated under the toolset directory # which writes the sdk's location into. This is only necessary for cmd --> powershell invocations # as dot sourcing isn't possible. function InitializeDotNetCli([bool]$install, [bool]$createSdkLocationFile) { if (Test-Path variable:global:_DotNetInstallDir) { return $global:_DotNetInstallDir } # Disable first run since we do not need all ASP.NET packages restored. $env:DOTNET_NOLOGO=1 # Disable telemetry on CI. if ($ci) { $env:DOTNET_CLI_TELEMETRY_OPTOUT=1 } # Find the first path on %PATH% that contains the dotnet.exe if ($useInstalledDotNetCli -and (-not $globalJsonHasRuntimes) -and ($env:DOTNET_INSTALL_DIR -eq $null)) { $dotnetExecutable = GetExecutableFileName 'dotnet' $dotnetCmd = Get-Command $dotnetExecutable -ErrorAction SilentlyContinue if ($dotnetCmd -ne $null) { $env:DOTNET_INSTALL_DIR = Split-Path $dotnetCmd.Path -Parent } } $dotnetSdkVersion = $GlobalJson.tools.dotnet # Use dotnet installation specified in DOTNET_INSTALL_DIR if it contains the required SDK version, # otherwise install the dotnet CLI and SDK to repo local .dotnet directory to avoid potential permission issues. if ((-not $globalJsonHasRuntimes) -and (-not [string]::IsNullOrEmpty($env:DOTNET_INSTALL_DIR)) -and (Test-Path(Join-Path $env:DOTNET_INSTALL_DIR "sdk\$dotnetSdkVersion"))) { $dotnetRoot = $env:DOTNET_INSTALL_DIR } else { $dotnetRoot = Join-Path $RepoRoot '.dotnet' if (-not (Test-Path(Join-Path $dotnetRoot "sdk\$dotnetSdkVersion"))) { if ($install) { InstallDotNetSdk $dotnetRoot $dotnetSdkVersion } else { Write-PipelineTelemetryError -Category 'InitializeToolset' -Message "Unable to find dotnet with SDK version '$dotnetSdkVersion'" ExitWithExitCode 1 } } $env:DOTNET_INSTALL_DIR = $dotnetRoot } # Creates a temporary file under the toolset dir. # The following code block is protecting against concurrent access so that this function can # be called in parallel. if ($createSdkLocationFile) { do { $sdkCacheFileTemp = Join-Path $ToolsetDir $([System.IO.Path]::GetRandomFileName()) } until (!(Test-Path $sdkCacheFileTemp)) Set-Content -Path $sdkCacheFileTemp -Value $dotnetRoot try { Move-Item -Force $sdkCacheFileTemp (Join-Path $ToolsetDir 'sdk.txt') } catch { # Somebody beat us Remove-Item -Path $sdkCacheFileTemp } } # Add dotnet to PATH. This prevents any bare invocation of dotnet in custom # build steps from using anything other than what we've downloaded. # It also ensures that VS msbuild will use the downloaded sdk targets. $env:PATH = "$dotnetRoot;$env:PATH" # Make Sure that our bootstrapped dotnet cli is available in future steps of the Azure Pipelines build Write-PipelinePrependPath -Path $dotnetRoot Write-PipelineSetVariable -Name 'DOTNET_NOLOGO' -Value '1' return $global:_DotNetInstallDir = $dotnetRoot } function Retry($downloadBlock, $maxRetries = 5) { $retries = 1 while($true) { try { & $downloadBlock break } catch { Write-PipelineTelemetryError -Category 'InitializeToolset' -Message $_ } if (++$retries -le $maxRetries) { $delayInSeconds = [math]::Pow(2, $retries) - 1 # Exponential backoff Write-Host "Retrying. Waiting for $delayInSeconds seconds before next attempt ($retries of $maxRetries)." Start-Sleep -Seconds $delayInSeconds } else { Write-PipelineTelemetryError -Category 'InitializeToolset' -Message "Unable to download file in $maxRetries attempts." break } } } function GetDotNetInstallScript([string] $dotnetRoot) { $installScript = Join-Path $dotnetRoot 'dotnet-install.ps1' $shouldDownload = $false if (!(Test-Path $installScript)) { $shouldDownload = $true } else { # Check if the script is older than 30 days $fileAge = (Get-Date) - (Get-Item $installScript).LastWriteTime if ($fileAge.Days -gt 30) { Write-Host "Existing install script is too old, re-downloading..." $shouldDownload = $true } } if ($shouldDownload) { Create-Directory $dotnetRoot $ProgressPreference = 'SilentlyContinue' # Don't display the console progress UI - it's a huge perf hit $uri = "https://builds.dotnet.microsoft.com/dotnet/scripts/$dotnetInstallScriptVersion/dotnet-install.ps1" Retry({ Write-Host "GET $uri" Invoke-WebRequest $uri -UseBasicParsing -OutFile $installScript }) } return $installScript } function InstallDotNetSdk([string] $dotnetRoot, [string] $version, [string] $architecture = '', [switch] $noPath) { InstallDotNet $dotnetRoot $version $architecture '' $false $runtimeSourceFeed $runtimeSourceFeedKey -noPath:$noPath } function InstallDotNet([string] $dotnetRoot, [string] $version, [string] $architecture = '', [string] $runtime = '', [bool] $skipNonVersionedFiles = $false, [string] $runtimeSourceFeed = '', [string] $runtimeSourceFeedKey = '', [switch] $noPath) { $dotnetVersionLabel = "'sdk v$version'" if ($runtime -ne '' -and $runtime -ne 'sdk') { $runtimePath = $dotnetRoot $runtimePath = $runtimePath + "\shared" if ($runtime -eq "dotnet") { $runtimePath = $runtimePath + "\Microsoft.NETCore.App" } if ($runtime -eq "aspnetcore") { $runtimePath = $runtimePath + "\Microsoft.AspNetCore.App" } if ($runtime -eq "windowsdesktop") { $runtimePath = $runtimePath + "\Microsoft.WindowsDesktop.App" } $runtimePath = $runtimePath + "\" + $version $dotnetVersionLabel = "runtime toolset '$runtime/$architecture v$version'" if (Test-Path $runtimePath) { Write-Host " Runtime toolset '$runtime/$architecture v$version' already installed." $installSuccess = $true Exit } } $installScript = GetDotNetInstallScript $dotnetRoot $installParameters = @{ Version = $version InstallDir = $dotnetRoot } if ($architecture) { $installParameters.Architecture = $architecture } if ($runtime) { $installParameters.Runtime = $runtime } if ($skipNonVersionedFiles) { $installParameters.SkipNonVersionedFiles = $skipNonVersionedFiles } if ($noPath) { $installParameters.NoPath = $True } $variations = @() $variations += @($installParameters) $dotnetBuilds = $installParameters.Clone() $dotnetbuilds.AzureFeed = "https://ci.dot.net/public" $variations += @($dotnetBuilds) if ($runtimeSourceFeed) { $runtimeSource = $installParameters.Clone() $runtimeSource.AzureFeed = $runtimeSourceFeed if ($runtimeSourceFeedKey) { $decodedBytes = [System.Convert]::FromBase64String($runtimeSourceFeedKey) $decodedString = [System.Text.Encoding]::UTF8.GetString($decodedBytes) $runtimeSource.FeedCredential = $decodedString } $variations += @($runtimeSource) } $installSuccess = $false foreach ($variation in $variations) { if ($variation | Get-Member AzureFeed) { $location = $variation.AzureFeed } else { $location = "public location"; } Write-Host " Attempting to install $dotnetVersionLabel from $location." try { & $installScript @variation $installSuccess = $true break } catch { Write-Host " Failed to install $dotnetVersionLabel from $location." } } if (-not $installSuccess) { Write-PipelineTelemetryError -Category 'InitializeToolset' -Message "Failed to install $dotnetVersionLabel from any of the specified locations." ExitWithExitCode 1 } } # # Locates Visual Studio MSBuild installation. # The preference order for MSBuild to use is as follows: # # 1. MSBuild from an active VS command prompt # 2. MSBuild from a compatible VS installation # 3. MSBuild from the xcopy tool package # # Returns full path to msbuild.exe. # Throws on failure. # function InitializeVisualStudioMSBuild([bool]$install, [object]$vsRequirements = $null) { if (-not (IsWindowsPlatform)) { throw "Cannot initialize Visual Studio on non-Windows" } if (Test-Path variable:global:_MSBuildExe) { return $global:_MSBuildExe } # Minimum VS version to require. $vsMinVersionReqdStr = '17.7' $vsMinVersionReqd = [Version]::new($vsMinVersionReqdStr) # If the version of msbuild is going to be xcopied, # use this version. Version matches a package here: # https://dev.azure.com/dnceng/public/_artifacts/feed/dotnet-eng/NuGet/Microsoft.DotNet.Arcade.MSBuild.Xcopy/versions/18.0.0 $defaultXCopyMSBuildVersion = '18.0.0' if (!$vsRequirements) { if (Get-Member -InputObject $GlobalJson.tools -Name 'vs') { $vsRequirements = $GlobalJson.tools.vs } else { $vsRequirements = New-Object PSObject -Property @{ version = $vsMinVersionReqdStr } } } $vsMinVersionStr = if ($vsRequirements.version) { $vsRequirements.version } else { $vsMinVersionReqdStr } $vsMinVersion = [Version]::new($vsMinVersionStr) # Try msbuild command available in the environment. if ($env:VSINSTALLDIR -ne $null) { $msbuildCmd = Get-Command 'msbuild.exe' -ErrorAction SilentlyContinue if ($msbuildCmd -ne $null) { # Workaround for https://github.com/dotnet/roslyn/issues/35793 # Due to this issue $msbuildCmd.Version returns 0.0.0.0 for msbuild.exe 16.2+ $msbuildVersion = [Version]::new((Get-Item $msbuildCmd.Path).VersionInfo.ProductVersion.Split([char[]]@('-', '+'))[0]) if ($msbuildVersion -ge $vsMinVersion) { return $global:_MSBuildExe = $msbuildCmd.Path } # Report error - the developer environment is initialized with incompatible VS version. throw "Developer Command Prompt for VS $($env:VisualStudioVersion) is not recent enough. Please upgrade to $vsMinVersionStr or build from a plain CMD window" } } # Locate Visual Studio installation or download x-copy msbuild. $vsInfo = LocateVisualStudio $vsRequirements if ($vsInfo -ne $null -and $env:ForceUseXCopyMSBuild -eq $null) { # Ensure vsInstallDir has a trailing slash $vsInstallDir = Join-Path $vsInfo.installationPath "\" $vsMajorVersion = $vsInfo.installationVersion.Split('.')[0] InitializeVisualStudioEnvironmentVariables $vsInstallDir $vsMajorVersion } else { if (Get-Member -InputObject $GlobalJson.tools -Name 'xcopy-msbuild') { $xcopyMSBuildVersion = $GlobalJson.tools.'xcopy-msbuild' $vsMajorVersion = $xcopyMSBuildVersion.Split('.')[0] } else { #if vs version provided in global.json is incompatible (too low) then use the default version for xcopy msbuild download if($vsMinVersion -lt $vsMinVersionReqd){ Write-Host "Using xcopy-msbuild version of $defaultXCopyMSBuildVersion since VS version $vsMinVersionStr provided in global.json is not compatible" $xcopyMSBuildVersion = $defaultXCopyMSBuildVersion $vsMajorVersion = $xcopyMSBuildVersion.Split('.')[0] } else{ # If the VS version IS compatible, look for an xcopy msbuild package # with a version matching VS. # Note: If this version does not exist, then an explicit version of xcopy msbuild # can be specified in global.json. This will be required for pre-release versions of msbuild. $vsMajorVersion = $vsMinVersion.Major $vsMinorVersion = $vsMinVersion.Minor $xcopyMSBuildVersion = "$vsMajorVersion.$vsMinorVersion.0" } } $vsInstallDir = $null if ($xcopyMSBuildVersion.Trim() -ine "none") { $vsInstallDir = InitializeXCopyMSBuild $xcopyMSBuildVersion $install if ($vsInstallDir -eq $null) { throw "Could not xcopy msbuild. Please check that package 'Microsoft.DotNet.Arcade.MSBuild.Xcopy @ $xcopyMSBuildVersion' exists on feed 'dotnet-eng'." } } if ($vsInstallDir -eq $null) { throw 'Unable to find Visual Studio that has required version and components installed' } } $msbuildVersionDir = if ([int]$vsMajorVersion -lt 16) { "$vsMajorVersion.0" } else { "Current" } $local:BinFolder = Join-Path $vsInstallDir "MSBuild\$msbuildVersionDir\Bin" $local:Prefer64bit = if (Get-Member -InputObject $vsRequirements -Name 'Prefer64bit') { $vsRequirements.Prefer64bit } else { $false } if ($local:Prefer64bit -and (Test-Path(Join-Path $local:BinFolder "amd64"))) { $global:_MSBuildExe = Join-Path $local:BinFolder "amd64\msbuild.exe" } else { $global:_MSBuildExe = Join-Path $local:BinFolder "msbuild.exe" } return $global:_MSBuildExe } function InitializeVisualStudioEnvironmentVariables([string] $vsInstallDir, [string] $vsMajorVersion) { $env:VSINSTALLDIR = $vsInstallDir Set-Item "env:VS$($vsMajorVersion)0COMNTOOLS" (Join-Path $vsInstallDir "Common7\Tools\") $vsSdkInstallDir = Join-Path $vsInstallDir "VSSDK\" if (Test-Path $vsSdkInstallDir) { Set-Item "env:VSSDK$($vsMajorVersion)0Install" $vsSdkInstallDir $env:VSSDKInstall = $vsSdkInstallDir } } function InstallXCopyMSBuild([string]$packageVersion) { return InitializeXCopyMSBuild $packageVersion -install $true } function InitializeXCopyMSBuild([string]$packageVersion, [bool]$install) { $packageName = 'Microsoft.DotNet.Arcade.MSBuild.Xcopy' $packageDir = Join-Path $ToolsDir "msbuild\$packageVersion" $packagePath = Join-Path $packageDir "$packageName.$packageVersion.nupkg" if (!(Test-Path $packageDir)) { if (!$install) { return $null } Create-Directory $packageDir Write-Host "Downloading $packageName $packageVersion" $ProgressPreference = 'SilentlyContinue' # Don't display the console progress UI - it's a huge perf hit Retry({ Invoke-WebRequest "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-eng/nuget/v3/flat2/$packageName/$packageVersion/$packageName.$packageVersion.nupkg" -UseBasicParsing -OutFile $packagePath }) if (!(Test-Path $packagePath)) { Write-PipelineTelemetryError -Category 'InitializeToolset' -Message "See https://dev.azure.com/dnceng/internal/_wiki/wikis/DNCEng%20Services%20Wiki/1074/Updating-Microsoft.DotNet.Arcade.MSBuild.Xcopy-WAS-RoslynTools.MSBuild-(xcopy-msbuild)-generation?anchor=troubleshooting for help troubleshooting issues with XCopy MSBuild" throw } Unzip $packagePath $packageDir } return Join-Path $packageDir 'tools' } # # Locates Visual Studio instance that meets the minimal requirements specified by tools.vs object in global.json. # # The following properties of tools.vs are recognized: # "version": "{major}.{minor}" # Two part minimal VS version, e.g. "15.9", "16.0", etc. # "components": ["componentId1", "componentId2", ...] # Array of ids of workload components that must be available in the VS instance. # See e.g. https://docs.microsoft.com/en-us/visualstudio/install/workload-component-id-vs-enterprise?view=vs-2017 # # Returns JSON describing the located VS instance (same format as returned by vswhere), # or $null if no instance meeting the requirements is found on the machine. # function LocateVisualStudio([object]$vsRequirements = $null){ if (-not (IsWindowsPlatform)) { throw "Cannot run vswhere on non-Windows platforms." } if (Get-Member -InputObject $GlobalJson.tools -Name 'vswhere') { $vswhereVersion = $GlobalJson.tools.vswhere } else { # keep this in sync with the VSWhereVersion in DefaultVersions.props $vswhereVersion = '3.1.7' } $vsWhereDir = Join-Path $ToolsDir "vswhere\$vswhereVersion" $vsWhereExe = Join-Path $vsWhereDir 'vswhere.exe' if (!(Test-Path $vsWhereExe)) { Create-Directory $vsWhereDir Write-Host "Downloading vswhere $vswhereVersion" $ProgressPreference = 'SilentlyContinue' # Don't display the console progress UI - it's a huge perf hit Retry({ Invoke-WebRequest "https://netcorenativeassets.blob.core.windows.net/resource-packages/external/windows/vswhere/$vswhereVersion/vswhere.exe" -UseBasicParsing -OutFile $vswhereExe }) } if (!$vsRequirements) { if (Get-Member -InputObject $GlobalJson.tools -Name 'vs' -ErrorAction SilentlyContinue) { $vsRequirements = $GlobalJson.tools.vs } else { $vsRequirements = $null } } $args = @('-latest', '-format', 'json', '-requires', 'Microsoft.Component.MSBuild', '-products', '*') if (!$excludePrereleaseVS) { $args += '-prerelease' } if ($vsRequirements -and (Get-Member -InputObject $vsRequirements -Name 'version' -ErrorAction SilentlyContinue)) { $args += '-version' $args += $vsRequirements.version } if ($vsRequirements -and (Get-Member -InputObject $vsRequirements -Name 'components' -ErrorAction SilentlyContinue)) { foreach ($component in $vsRequirements.components) { $args += '-requires' $args += $component } } $vsInfo =& $vsWhereExe $args | ConvertFrom-Json if ($lastExitCode -ne 0) { return $null } if ($null -eq $vsInfo -or $vsInfo.Count -eq 0) { throw "No instance of Visual Studio meeting the requirements specified was found. Requirements: $($args -join ' ')" return $null } # use first matching instance return $vsInfo[0] } function InitializeBuildTool() { if (Test-Path variable:global:_BuildTool) { # If the requested msbuild parameters do not match, clear the cached variables. if($global:_BuildTool.Contains('ExcludePrereleaseVS') -and $global:_BuildTool.ExcludePrereleaseVS -ne $excludePrereleaseVS) { Remove-Item variable:global:_BuildTool Remove-Item variable:global:_MSBuildExe } else { return $global:_BuildTool } } if (-not $msbuildEngine) { $msbuildEngine = GetDefaultMSBuildEngine } # Initialize dotnet cli if listed in 'tools' $dotnetRoot = $null if (Get-Member -InputObject $GlobalJson.tools -Name 'dotnet') { $dotnetRoot = InitializeDotNetCli -install:$restore } if ($msbuildEngine -eq 'dotnet') { if (!$dotnetRoot) { Write-PipelineTelemetryError -Category 'InitializeToolset' -Message "/global.json must specify 'tools.dotnet'." ExitWithExitCode 1 } $dotnetPath = Join-Path $dotnetRoot (GetExecutableFileName 'dotnet') $buildTool = @{ Path = $dotnetPath; Command = 'msbuild'; Tool = 'dotnet'; Framework = 'net' } } elseif ($msbuildEngine -eq "vs") { try { $msbuildPath = InitializeVisualStudioMSBuild -install:$restore } catch { Write-PipelineTelemetryError -Category 'InitializeToolset' -Message $_ ExitWithExitCode 1 } $buildTool = @{ Path = $msbuildPath; Command = ""; Tool = "vs"; Framework = "netframework"; ExcludePrereleaseVS = $excludePrereleaseVS } } else { Write-PipelineTelemetryError -Category 'InitializeToolset' -Message "Unexpected value of -msbuildEngine: '$msbuildEngine'." ExitWithExitCode 1 } return $global:_BuildTool = $buildTool } function GetDefaultMSBuildEngine() { # Presence of tools.vs indicates the repo needs to build using VS msbuild on Windows. if (Get-Member -InputObject $GlobalJson.tools -Name 'vs') { return 'vs' } if (Get-Member -InputObject $GlobalJson.tools -Name 'dotnet') { return 'dotnet' } Write-PipelineTelemetryError -Category 'InitializeToolset' -Message "-msbuildEngine must be specified, or /global.json must specify 'tools.dotnet' or 'tools.vs'." ExitWithExitCode 1 } function GetNuGetPackageCachePath() { if ($env:NUGET_PACKAGES -eq $null) { # Use local cache on CI to ensure deterministic build. # Avoid using the http cache as workaround for https://github.com/NuGet/Home/issues/3116 # use global cache in dev builds to avoid cost of downloading packages. # For directory normalization, see also: https://github.com/NuGet/Home/issues/7968 if ($useGlobalNuGetCache) { $env:NUGET_PACKAGES = Join-Path $env:UserProfile '.nuget\packages\' } else { $env:NUGET_PACKAGES = Join-Path $RepoRoot '.packages\' } } return $env:NUGET_PACKAGES } # Returns a full path to an Arcade SDK task project file. function GetSdkTaskProject([string]$taskName) { return Join-Path (Split-Path (InitializeToolset) -Parent) "SdkTasks\$taskName.proj" } function InitializeNativeTools() { if (-Not (Test-Path variable:DisableNativeToolsetInstalls) -And (Get-Member -InputObject $GlobalJson -Name "native-tools")) { $nativeArgs= @{} if ($ci) { $nativeArgs = @{ InstallDirectory = "$ToolsDir" } } if ($env:NativeToolsOnMachine) { Write-Host "Variable NativeToolsOnMachine detected, enabling native tool path promotion..." $nativeArgs += @{ PathPromotion = $true } } & "$PSScriptRoot/init-tools-native.ps1" @nativeArgs } } function Read-ArcadeSdkVersion() { return $GlobalJson.'msbuild-sdks'.'Microsoft.DotNet.Arcade.Sdk' } function InitializeToolset() { # For Unified Build/Source-build support, check whether the environment variable is # set. If it is, then use this as the toolset build project. if ($env:_InitializeToolset -ne $null) { return $global:_InitializeToolset = $env:_InitializeToolset } if (Test-Path variable:global:_InitializeToolset) { return $global:_InitializeToolset } $nugetCache = GetNuGetPackageCachePath $toolsetVersion = Read-ArcadeSdkVersion $toolsetLocationFile = Join-Path $ToolsetDir "$toolsetVersion.txt" if (Test-Path $toolsetLocationFile) { $path = Get-Content $toolsetLocationFile -TotalCount 1 if (Test-Path $path) { return $global:_InitializeToolset = $path } } if (-not $restore) { Write-PipelineTelemetryError -Category 'InitializeToolset' -Message "Toolset version $toolsetVersion has not been restored." ExitWithExitCode 1 } $buildTool = InitializeBuildTool $proj = Join-Path $ToolsetDir 'restore.proj' $bl = if ($binaryLog) { '/bl:' + (Join-Path $LogDir 'ToolsetRestore.binlog') } else { '' } '' | Set-Content $proj MSBuild-Core $proj $bl /t:__WriteToolsetLocation /clp:ErrorsOnly`;NoSummary /p:__ToolsetLocationOutputFile=$toolsetLocationFile $path = Get-Content $toolsetLocationFile -Encoding UTF8 -TotalCount 1 if (!(Test-Path $path)) { throw "Invalid toolset path: $path" } return $global:_InitializeToolset = $path } function ExitWithExitCode([int] $exitCode) { if ($ci -and $prepareMachine) { Stop-Processes } exit $exitCode } # Check if $LASTEXITCODE is a nonzero exit code (NZEC). If so, print a Azure Pipeline error for # diagnostics, then exit the script with the $LASTEXITCODE. function Exit-IfNZEC([string] $category = "General") { Write-Host "Exit code $LASTEXITCODE" if ($LASTEXITCODE -ne 0) { $message = "Last command failed with exit code $LASTEXITCODE." Write-PipelineTelemetryError -Force -Category $category -Message $message ExitWithExitCode $LASTEXITCODE } } function Stop-Processes() { Write-Host 'Killing running build processes...' foreach ($processName in $processesToStopOnExit) { Get-Process -Name $processName -ErrorAction SilentlyContinue | Stop-Process } } # # Executes msbuild (or 'dotnet msbuild') with arguments passed to the function. # The arguments are automatically quoted. # Terminates the script if the build fails. # function MSBuild() { if ($pipelinesLog) { $buildTool = InitializeBuildTool if ($ci -and $buildTool.Tool -eq 'dotnet') { $env:NUGET_PLUGIN_HANDSHAKE_TIMEOUT_IN_SECONDS = 20 $env:NUGET_PLUGIN_REQUEST_TIMEOUT_IN_SECONDS = 20 Write-PipelineSetVariable -Name 'NUGET_PLUGIN_HANDSHAKE_TIMEOUT_IN_SECONDS' -Value '20' Write-PipelineSetVariable -Name 'NUGET_PLUGIN_REQUEST_TIMEOUT_IN_SECONDS' -Value '20' } Enable-Nuget-EnhancedRetry $toolsetBuildProject = InitializeToolset $basePath = Split-Path -parent $toolsetBuildProject $selectedPath = Join-Path $basePath (Join-Path $buildTool.Framework 'Microsoft.DotNet.ArcadeLogging.dll') if (-not $selectedPath) { Write-PipelineTelemetryError -Category 'Build' -Message "Unable to find arcade sdk logger assembly: $selectedPath" ExitWithExitCode 1 } $args += "/logger:$selectedPath" } MSBuild-Core @args } # # Executes msbuild (or 'dotnet msbuild') with arguments passed to the function. # The arguments are automatically quoted. # Terminates the script if the build fails. # function MSBuild-Core() { if ($ci) { if (!$binaryLog -and !$excludeCIBinarylog) { Write-PipelineTelemetryError -Category 'Build' -Message 'Binary log must be enabled in CI build, or explicitly opted-out from with the -excludeCIBinarylog switch.' ExitWithExitCode 1 } if ($nodeReuse) { Write-PipelineTelemetryError -Category 'Build' -Message 'Node reuse must be disabled in CI build.' ExitWithExitCode 1 } } Enable-Nuget-EnhancedRetry $buildTool = InitializeBuildTool $cmdArgs = "$($buildTool.Command) /m /nologo /clp:Summary /v:$verbosity /nr:$nodeReuse /p:ContinuousIntegrationBuild=$ci" # Add -mt flag for MSBuild multithreaded mode if enabled via environment variable if ($env:MSBUILD_MT_ENABLED -eq "1") { $cmdArgs += ' -mt' } if ($warnAsError) { $cmdArgs += ' /warnaserror /p:TreatWarningsAsErrors=true' } else { $cmdArgs += ' /p:TreatWarningsAsErrors=false' } foreach ($arg in $args) { if ($null -ne $arg -and $arg.Trim() -ne "") { if ($arg.EndsWith('\')) { $arg = $arg + "\" } $cmdArgs += " `"$arg`"" } } # Be sure quote the path in case there are spaces in the dotnet installation location. $env:ARCADE_BUILD_TOOL_COMMAND = "`"$($buildTool.Path)`" $cmdArgs" $exitCode = Exec-Process $buildTool.Path $cmdArgs if ($exitCode -ne 0) { # We should not Write-PipelineTaskError here because that message shows up in the build summary # The build already logged an error, that's the reason it failed. Producing an error here only adds noise. Write-Host "Build failed with exit code $exitCode. Check errors above." -ForegroundColor Red $buildLog = GetMSBuildBinaryLogCommandLineArgument $args if ($null -ne $buildLog) { Write-Host "See log: $buildLog" -ForegroundColor DarkGray } # When running on Azure Pipelines, override the returned exit code to avoid double logging. # Skip this when the build is a child of the VMR build. if ($ci -and $env:SYSTEM_TEAMPROJECT -ne $null -and !$fromVMR) { Write-PipelineSetResult -Result "Failed" -Message "msbuild execution failed." # Exiting with an exit code causes the azure pipelines task to log yet another "noise" error # The above Write-PipelineSetResult will cause the task to be marked as failure without adding yet another error ExitWithExitCode 0 } else { ExitWithExitCode $exitCode } } } function GetMSBuildBinaryLogCommandLineArgument($arguments) { foreach ($argument in $arguments) { if ($argument -ne $null) { $arg = $argument.Trim() if ($arg.StartsWith('/bl:', "OrdinalIgnoreCase")) { return $arg.Substring('/bl:'.Length) } if ($arg.StartsWith('/binaryLogger:', 'OrdinalIgnoreCase')) { return $arg.Substring('/binaryLogger:'.Length) } } } return $null } function GetExecutableFileName($baseName) { if (IsWindowsPlatform) { return "$baseName.exe" } else { return $baseName } } function IsWindowsPlatform() { return [environment]::OSVersion.Platform -eq [PlatformID]::Win32NT } function Get-Darc($version) { $darcPath = "$TempDir\darc\$([guid]::NewGuid())" if ($version -ne $null) { & $PSScriptRoot\darc-init.ps1 -toolpath $darcPath -darcVersion $version | Out-Host } else { & $PSScriptRoot\darc-init.ps1 -toolpath $darcPath | Out-Host } return "$darcPath\darc.exe" } . $PSScriptRoot\pipeline-logging-functions.ps1 $RepoRoot = Resolve-Path (Join-Path $PSScriptRoot '..\..\') $EngRoot = Resolve-Path (Join-Path $PSScriptRoot '..') $ArtifactsDir = Join-Path $RepoRoot 'artifacts' $ToolsetDir = Join-Path $ArtifactsDir 'toolset' $ToolsDir = Join-Path $RepoRoot '.tools' $LogDir = Join-Path (Join-Path $ArtifactsDir 'log') $configuration $TempDir = Join-Path (Join-Path $ArtifactsDir 'tmp') $configuration $GlobalJson = Get-Content -Raw -Path (Join-Path $RepoRoot 'global.json') | ConvertFrom-Json # true if global.json contains a "runtimes" section $globalJsonHasRuntimes = if ($GlobalJson.tools.PSObject.Properties.Name -Match 'runtimes') { $true } else { $false } Create-Directory $ToolsetDir Create-Directory $TempDir Create-Directory $LogDir Write-PipelineSetVariable -Name 'Artifacts' -Value $ArtifactsDir Write-PipelineSetVariable -Name 'Artifacts.Toolset' -Value $ToolsetDir Write-PipelineSetVariable -Name 'Artifacts.Log' -Value $LogDir Write-PipelineSetVariable -Name 'TEMP' -Value $TempDir Write-PipelineSetVariable -Name 'TMP' -Value $TempDir # Import custom tools configuration, if present in the repo. # Note: Import in global scope so that the script set top-level variables without qualification. if (!$disableConfigureToolsetImport) { $configureToolsetScript = Join-Path $EngRoot 'configure-toolset.ps1' if (Test-Path $configureToolsetScript) { . $configureToolsetScript if ((Test-Path variable:failOnConfigureToolsetError) -And $failOnConfigureToolsetError) { if ((Test-Path variable:LastExitCode) -And ($LastExitCode -ne 0)) { Write-PipelineTelemetryError -Category 'Build' -Message 'configure-toolset.ps1 returned a non-zero exit code' ExitWithExitCode $LastExitCode } } } } # # If $ci flag is set, turn on (and log that we did) special environment variables for improved Nuget client retry logic. # function Enable-Nuget-EnhancedRetry() { if ($ci) { Write-Host "Setting NUGET enhanced retry environment variables" $env:NUGET_ENABLE_ENHANCED_HTTP_RETRY = 'true' $env:NUGET_ENHANCED_MAX_NETWORK_TRY_COUNT = 6 $env:NUGET_ENHANCED_NETWORK_RETRY_DELAY_MILLISECONDS = 1000 $env:NUGET_RETRY_HTTP_429 = 'true' Write-PipelineSetVariable -Name 'NUGET_ENABLE_ENHANCED_HTTP_RETRY' -Value 'true' Write-PipelineSetVariable -Name 'NUGET_ENHANCED_MAX_NETWORK_TRY_COUNT' -Value '6' Write-PipelineSetVariable -Name 'NUGET_ENHANCED_NETWORK_RETRY_DELAY_MILLISECONDS' -Value '1000' Write-PipelineSetVariable -Name 'NUGET_RETRY_HTTP_429' -Value 'true' } } ================================================ FILE: eng/common/tools.sh ================================================ #!/usr/bin/env bash # Initialize variables if they aren't already defined. # CI mode - set to true on CI server for PR validation build or official build. ci=${ci:-false} # Build mode source_build=${source_build:-false} # Set to true to use the pipelines logger which will enable Azure logging output. # https://github.com/Microsoft/azure-pipelines-tasks/blob/master/docs/authoring/commands.md # This flag is meant as a temporary opt-opt for the feature while validate it across # our consumers. It will be deleted in the future. if [[ "$ci" == true ]]; then pipelines_log=${pipelines_log:-true} else pipelines_log=${pipelines_log:-false} fi # Build configuration. Common values include 'Debug' and 'Release', but the repository may use other names. configuration=${configuration:-'Debug'} # Set to true to opt out of outputting binary log while running in CI exclude_ci_binary_log=${exclude_ci_binary_log:-false} if [[ "$ci" == true && "$exclude_ci_binary_log" == false ]]; then binary_log_default=true else binary_log_default=false fi # Set to true to output binary log from msbuild. Note that emitting binary log slows down the build. binary_log=${binary_log:-$binary_log_default} # Turns on machine preparation/clean up code that changes the machine state (e.g. kills build processes). prepare_machine=${prepare_machine:-false} # True to restore toolsets and dependencies. restore=${restore:-true} # Adjusts msbuild verbosity level. verbosity=${verbosity:-'minimal'} # Set to true to reuse msbuild nodes. Recommended to not reuse on CI. if [[ "$ci" == true ]]; then node_reuse=${node_reuse:-false} else node_reuse=${node_reuse:-true} fi # Configures warning treatment in msbuild. warn_as_error=${warn_as_error:-true} # True to attempt using .NET Core already that meets requirements specified in global.json # installed on the machine instead of downloading one. use_installed_dotnet_cli=${use_installed_dotnet_cli:-true} # Enable repos to use a particular version of the on-line dotnet-install scripts. # default URL: https://builds.dotnet.microsoft.com/dotnet/scripts/v1/dotnet-install.sh dotnetInstallScriptVersion=${dotnetInstallScriptVersion:-'v1'} # True to use global NuGet cache instead of restoring packages to repository-local directory. # Keep in sync with NuGetPackageroot in Arcade SDK's RepositoryLayout.props. if [[ "$ci" == true || "$source_build" == true ]]; then use_global_nuget_cache=${use_global_nuget_cache:-false} else use_global_nuget_cache=${use_global_nuget_cache:-true} fi # Used when restoring .NET SDK from alternative feeds runtime_source_feed=${runtime_source_feed:-''} runtime_source_feed_key=${runtime_source_feed_key:-''} # True when the build is running within the VMR. from_vmr=${from_vmr:-false} # Resolve any symlinks in the given path. function ResolvePath { local path=$1 while [[ -h $path ]]; do local dir="$( cd -P "$( dirname "$path" )" && pwd )" path="$(readlink "$path")" # if $path was a relative symlink, we need to resolve it relative to the path where the # symlink file was located [[ $path != /* ]] && path="$dir/$path" done # return value _ResolvePath="$path" } # ReadVersionFromJson [json key] function ReadGlobalVersion { local key=$1 if command -v jq &> /dev/null; then _ReadGlobalVersion="$(jq -r ".[] | select(has(\"$key\")) | .\"$key\"" "$global_json_file")" elif [[ "$(cat "$global_json_file")" =~ \"$key\"[[:space:]\:]*\"([^\"]+) ]]; then _ReadGlobalVersion=${BASH_REMATCH[1]} fi if [[ -z "$_ReadGlobalVersion" ]]; then Write-PipelineTelemetryError -category 'Build' "Error: Cannot find \"$key\" in $global_json_file" ExitWithExitCode 1 fi } function InitializeDotNetCli { if [[ -n "${_InitializeDotNetCli:-}" ]]; then return fi local install=$1 # Disable first run since we want to control all package sources export DOTNET_NOLOGO=1 # Disable telemetry on CI if [[ $ci == true ]]; then export DOTNET_CLI_TELEMETRY_OPTOUT=1 fi # LTTNG is the logging infrastructure used by Core CLR. Need this variable set # so it doesn't output warnings to the console. export LTTNG_HOME="$HOME" # Find the first path on $PATH that contains the dotnet.exe if [[ "$use_installed_dotnet_cli" == true && $global_json_has_runtimes == false && -z "${DOTNET_INSTALL_DIR:-}" ]]; then local dotnet_path=`command -v dotnet` if [[ -n "$dotnet_path" ]]; then ResolvePath "$dotnet_path" export DOTNET_INSTALL_DIR=`dirname "$_ResolvePath"` fi fi ReadGlobalVersion "dotnet" local dotnet_sdk_version=$_ReadGlobalVersion local dotnet_root="" # Use dotnet installation specified in DOTNET_INSTALL_DIR if it contains the required SDK version, # otherwise install the dotnet CLI and SDK to repo local .dotnet directory to avoid potential permission issues. if [[ $global_json_has_runtimes == false && -n "${DOTNET_INSTALL_DIR:-}" && -d "$DOTNET_INSTALL_DIR/sdk/$dotnet_sdk_version" ]]; then dotnet_root="$DOTNET_INSTALL_DIR" else dotnet_root="${repo_root}.dotnet" export DOTNET_INSTALL_DIR="$dotnet_root" if [[ ! -d "$DOTNET_INSTALL_DIR/sdk/$dotnet_sdk_version" ]]; then if [[ "$install" == true ]]; then InstallDotNetSdk "$dotnet_root" "$dotnet_sdk_version" else Write-PipelineTelemetryError -category 'InitializeToolset' "Unable to find dotnet with SDK version '$dotnet_sdk_version'" ExitWithExitCode 1 fi fi fi # Add dotnet to PATH. This prevents any bare invocation of dotnet in custom # build steps from using anything other than what we've downloaded. Write-PipelinePrependPath -path "$dotnet_root" Write-PipelineSetVariable -name "DOTNET_NOLOGO" -value "1" # return value _InitializeDotNetCli="$dotnet_root" } function InstallDotNetSdk { local root=$1 local version=$2 local architecture="unset" if [[ $# -ge 3 ]]; then architecture=$3 fi InstallDotNet "$root" "$version" $architecture 'sdk' 'true' $runtime_source_feed $runtime_source_feed_key } function InstallDotNet { local root=$1 local version=$2 local runtime=$4 local dotnetVersionLabel="'$runtime v$version'" if [[ -n "${4:-}" ]] && [ "$4" != 'sdk' ]; then runtimePath="$root" runtimePath="$runtimePath/shared" case "$runtime" in dotnet) runtimePath="$runtimePath/Microsoft.NETCore.App" ;; aspnetcore) runtimePath="$runtimePath/Microsoft.AspNetCore.App" ;; windowsdesktop) runtimePath="$runtimePath/Microsoft.WindowsDesktop.App" ;; *) ;; esac runtimePath="$runtimePath/$version" dotnetVersionLabel="runtime toolset '$runtime/$architecture v$version'" if [ -d "$runtimePath" ]; then echo " Runtime toolset '$runtime/$architecture v$version' already installed." local installSuccess=1 return fi fi GetDotNetInstallScript "$root" local install_script=$_GetDotNetInstallScript local installParameters=(--version $version --install-dir "$root") if [[ -n "${3:-}" ]] && [ "$3" != 'unset' ]; then installParameters+=(--architecture $3) fi if [[ -n "${4:-}" ]] && [ "$4" != 'sdk' ]; then installParameters+=(--runtime $4) fi if [[ "$#" -ge "5" ]] && [[ "$5" != 'false' ]]; then installParameters+=(--skip-non-versioned-files) fi local variations=() # list of variable names with parameter arrays in them local public_location=("${installParameters[@]}") variations+=(public_location) local dotnetbuilds=("${installParameters[@]}" --azure-feed "https://ci.dot.net/public") variations+=(dotnetbuilds) if [[ -n "${6:-}" ]]; then variations+=(private_feed) local private_feed=("${installParameters[@]}" --azure-feed $6) if [[ -n "${7:-}" ]]; then # The 'base64' binary on alpine uses '-d' and doesn't support '--decode' # '-d'. To work around this, do a simple detection and switch the parameter # accordingly. decodeArg="--decode" if base64 --help 2>&1 | grep -q "BusyBox"; then decodeArg="-d" fi decodedFeedKey=`echo $7 | base64 $decodeArg` private_feed+=(--feed-credential $decodedFeedKey) fi fi local installSuccess=0 for variationName in "${variations[@]}"; do local name="$variationName[@]" local variation=("${!name}") echo " Attempting to install $dotnetVersionLabel from $variationName." bash "$install_script" "${variation[@]}" && installSuccess=1 if [[ "$installSuccess" -eq 1 ]]; then break fi echo " Failed to install $dotnetVersionLabel from $variationName." done if [[ "$installSuccess" -eq 0 ]]; then Write-PipelineTelemetryError -category 'InitializeToolset' "Failed to install $dotnetVersionLabel from any of the specified locations." ExitWithExitCode 1 fi } function with_retries { local maxRetries=5 local retries=1 echo "Trying to run '$@' for maximum of $maxRetries attempts." while [[ $((retries++)) -le $maxRetries ]]; do "$@" if [[ $? == 0 ]]; then echo "Ran '$@' successfully." return 0 fi timeout=$((3**$retries-1)) echo "Failed to execute '$@'. Waiting $timeout seconds before next attempt ($retries out of $maxRetries)." 1>&2 sleep $timeout done echo "Failed to execute '$@' for $maxRetries times." 1>&2 return 1 } function GetDotNetInstallScript { local root=$1 local install_script="$root/dotnet-install.sh" local install_script_url="https://builds.dotnet.microsoft.com/dotnet/scripts/$dotnetInstallScriptVersion/dotnet-install.sh" local timestamp_file="$root/.dotnet-install.timestamp" local should_download=false if [[ ! -a "$install_script" ]]; then should_download=true elif [[ -f "$timestamp_file" ]]; then # Check if the script is older than 30 days using timestamp file local download_time=$(cat "$timestamp_file" 2>/dev/null || echo "0") local current_time=$(date +%s) local age_seconds=$((current_time - download_time)) # 30 days = 30 * 24 * 60 * 60 = 2592000 seconds if [[ $age_seconds -gt 2592000 ]]; then echo "Existing install script is too old, re-downloading..." should_download=true fi else # No timestamp file exists, assume script is old and re-download echo "No timestamp found for existing install script, re-downloading..." should_download=true fi if [[ "$should_download" == true ]]; then mkdir -p "$root" echo "Downloading '$install_script_url'" # Use curl if available, otherwise use wget if command -v curl > /dev/null; then # first, try directly, if this fails we will retry with verbose logging curl "$install_script_url" -sSL --retry 10 --create-dirs -o "$install_script" || { if command -v openssl &> /dev/null; then echo "Curl failed; dumping some information about dotnet.microsoft.com for later investigation" echo | openssl s_client -showcerts -servername dotnet.microsoft.com -connect dotnet.microsoft.com:443 || true fi echo "Will now retry the same URL with verbose logging." with_retries curl "$install_script_url" -sSL --verbose --retry 10 --create-dirs -o "$install_script" || { local exit_code=$? Write-PipelineTelemetryError -category 'InitializeToolset' "Failed to acquire dotnet install script (exit code '$exit_code')." ExitWithExitCode $exit_code } } else with_retries wget -v -O "$install_script" "$install_script_url" || { local exit_code=$? Write-PipelineTelemetryError -category 'InitializeToolset' "Failed to acquire dotnet install script (exit code '$exit_code')." ExitWithExitCode $exit_code } fi # Create timestamp file to track download time in seconds from epoch date +%s > "$timestamp_file" fi # return value _GetDotNetInstallScript="$install_script" } function InitializeBuildTool { if [[ -n "${_InitializeBuildTool:-}" ]]; then return fi InitializeDotNetCli $restore # return values _InitializeBuildTool="$_InitializeDotNetCli/dotnet" _InitializeBuildToolCommand="msbuild" } function GetNuGetPackageCachePath { if [[ -z ${NUGET_PACKAGES:-} ]]; then if [[ "$use_global_nuget_cache" == true ]]; then export NUGET_PACKAGES="$HOME/.nuget/packages/" else export NUGET_PACKAGES="$repo_root/.packages/" fi fi # return value _GetNuGetPackageCachePath=$NUGET_PACKAGES } function InitializeNativeTools() { if [[ -n "${DisableNativeToolsetInstalls:-}" ]]; then return fi if grep -Fq "native-tools" $global_json_file then local nativeArgs="" if [[ "$ci" == true ]]; then nativeArgs="--installDirectory $tools_dir" fi "$_script_dir/init-tools-native.sh" $nativeArgs fi } function InitializeToolset { if [[ -n "${_InitializeToolset:-}" ]]; then return fi GetNuGetPackageCachePath ReadGlobalVersion "Microsoft.DotNet.Arcade.Sdk" local toolset_version=$_ReadGlobalVersion local toolset_location_file="$toolset_dir/$toolset_version.txt" if [[ -a "$toolset_location_file" ]]; then local path=`cat "$toolset_location_file"` if [[ -a "$path" ]]; then # return value _InitializeToolset="$path" return fi fi if [[ "$restore" != true ]]; then Write-PipelineTelemetryError -category 'InitializeToolset' "Toolset version $toolset_version has not been restored." ExitWithExitCode 2 fi local proj="$toolset_dir/restore.proj" local bl="" if [[ "$binary_log" == true ]]; then bl="/bl:$log_dir/ToolsetRestore.binlog" fi echo '' > "$proj" MSBuild-Core "$proj" $bl /t:__WriteToolsetLocation /clp:ErrorsOnly\;NoSummary /p:__ToolsetLocationOutputFile="$toolset_location_file" local toolset_build_proj=`cat "$toolset_location_file"` if [[ ! -a "$toolset_build_proj" ]]; then Write-PipelineTelemetryError -category 'Build' "Invalid toolset path: $toolset_build_proj" ExitWithExitCode 3 fi # return value _InitializeToolset="$toolset_build_proj" } function ExitWithExitCode { if [[ "$ci" == true && "$prepare_machine" == true ]]; then StopProcesses fi exit $1 } function StopProcesses { echo "Killing running build processes..." pkill -9 "dotnet" || true pkill -9 "vbcscompiler" || true return 0 } function MSBuild { local args=( "$@" ) if [[ "$pipelines_log" == true ]]; then InitializeBuildTool InitializeToolset if [[ "$ci" == true ]]; then export NUGET_PLUGIN_HANDSHAKE_TIMEOUT_IN_SECONDS=20 export NUGET_PLUGIN_REQUEST_TIMEOUT_IN_SECONDS=20 Write-PipelineSetVariable -name "NUGET_PLUGIN_HANDSHAKE_TIMEOUT_IN_SECONDS" -value "20" Write-PipelineSetVariable -name "NUGET_PLUGIN_REQUEST_TIMEOUT_IN_SECONDS" -value "20" fi local toolset_dir="${_InitializeToolset%/*}" local selectedPath="$toolset_dir/net/Microsoft.DotNet.ArcadeLogging.dll" if [[ -z "$selectedPath" ]]; then Write-PipelineTelemetryError -category 'Build' "Unable to find arcade sdk logger assembly: $selectedPath" ExitWithExitCode 1 fi args+=( "-logger:$selectedPath" ) fi MSBuild-Core "${args[@]}" } function MSBuild-Core { if [[ "$ci" == true ]]; then if [[ "$binary_log" != true && "$exclude_ci_binary_log" != true ]]; then Write-PipelineTelemetryError -category 'Build' "Binary log must be enabled in CI build, or explicitly opted-out from with the -noBinaryLog switch." ExitWithExitCode 1 fi if [[ "$node_reuse" == true ]]; then Write-PipelineTelemetryError -category 'Build' "Node reuse must be disabled in CI build." ExitWithExitCode 1 fi fi InitializeBuildTool local warnaserror_switch="" if [[ $warn_as_error == true ]]; then warnaserror_switch="/warnaserror" fi function RunBuildTool { export ARCADE_BUILD_TOOL_COMMAND="$_InitializeBuildTool $@" "$_InitializeBuildTool" "$@" || { local exit_code=$? # We should not Write-PipelineTaskError here because that message shows up in the build summary # The build already logged an error, that's the reason it failed. Producing an error here only adds noise. echo "Build failed with exit code $exit_code. Check errors above." # When running on Azure Pipelines, override the returned exit code to avoid double logging. # Skip this when the build is a child of the VMR build. if [[ "$ci" == true && -n ${SYSTEM_TEAMPROJECT:-} && "$from_vmr" != true ]]; then Write-PipelineSetResult -result "Failed" -message "msbuild execution failed." # Exiting with an exit code causes the azure pipelines task to log yet another "noise" error # The above Write-PipelineSetResult will cause the task to be marked as failure without adding yet another error ExitWithExitCode 0 else ExitWithExitCode $exit_code fi } } # Add -mt flag for MSBuild multithreaded mode if enabled via environment variable local mt_switch="" if [[ "${MSBUILD_MT_ENABLED:-}" == "1" ]]; then mt_switch="-mt" fi RunBuildTool "$_InitializeBuildToolCommand" /m /nologo /clp:Summary /v:$verbosity /nr:$node_reuse $warnaserror_switch $mt_switch /p:TreatWarningsAsErrors=$warn_as_error /p:ContinuousIntegrationBuild=$ci "$@" } function GetDarc { darc_path="$temp_dir/darc" version="$1" if [[ -n "$version" ]]; then version="--darcversion $version" fi "$eng_root/common/darc-init.sh" --toolpath "$darc_path" $version darc_tool="$darc_path/darc" } # Returns a full path to an Arcade SDK task project file. function GetSdkTaskProject { taskName=$1 echo "$(dirname $_InitializeToolset)/SdkTasks/$taskName.proj" } ResolvePath "${BASH_SOURCE[0]}" _script_dir=`dirname "$_ResolvePath"` . "$_script_dir/pipeline-logging-functions.sh" eng_root=`cd -P "$_script_dir/.." && pwd` repo_root=`cd -P "$_script_dir/../.." && pwd` repo_root="${repo_root}/" artifacts_dir="${repo_root}artifacts" toolset_dir="$artifacts_dir/toolset" tools_dir="${repo_root}.tools" log_dir="$artifacts_dir/log/$configuration" temp_dir="$artifacts_dir/tmp/$configuration" global_json_file="${repo_root}global.json" # determine if global.json contains a "runtimes" entry global_json_has_runtimes=false if command -v jq &> /dev/null; then if jq -e '.tools | has("runtimes")' "$global_json_file" &> /dev/null; then global_json_has_runtimes=true fi elif [[ "$(cat "$global_json_file")" =~ \"runtimes\"[[:space:]\:]*\{ ]]; then global_json_has_runtimes=true fi # HOME may not be defined in some scenarios, but it is required by NuGet if [[ -z $HOME ]]; then export HOME="${repo_root}artifacts/.home/" mkdir -p "$HOME" fi mkdir -p "$toolset_dir" mkdir -p "$temp_dir" mkdir -p "$log_dir" Write-PipelineSetVariable -name "Artifacts" -value "$artifacts_dir" Write-PipelineSetVariable -name "Artifacts.Toolset" -value "$toolset_dir" Write-PipelineSetVariable -name "Artifacts.Log" -value "$log_dir" Write-PipelineSetVariable -name "Temp" -value "$temp_dir" Write-PipelineSetVariable -name "TMP" -value "$temp_dir" # Import custom tools configuration, if present in the repo. if [ -z "${disable_configure_toolset_import:-}" ]; then configure_toolset_script="$eng_root/configure-toolset.sh" if [[ -a "$configure_toolset_script" ]]; then . "$configure_toolset_script" fi fi # TODO: https://github.com/dotnet/arcade/issues/1468 # Temporary workaround to avoid breaking change. # Remove once repos are updated. if [[ -n "${useInstalledDotNetCli:-}" ]]; then use_installed_dotnet_cli="$useInstalledDotNetCli" fi ================================================ FILE: eng/common/vmr-sync.ps1 ================================================ <# .SYNOPSIS This script is used for synchronizing the current repository into a local VMR. It pulls the current repository's code into the specified VMR directory for local testing or Source-Build validation. .DESCRIPTION The tooling used for synchronization will clone the VMR repository into a temporary folder if it does not already exist. These clones can be reused in future synchronizations, so it is recommended to dedicate a folder for this to speed up re-runs. .EXAMPLE Synchronize current repository into a local VMR: ./vmr-sync.ps1 -vmrDir "$HOME/repos/dotnet" -tmpDir "$HOME/repos/tmp" .PARAMETER tmpDir Required. Path to the temporary folder where repositories will be cloned .PARAMETER vmrBranch Optional. Branch of the 'dotnet/dotnet' repo to synchronize. The VMR will be checked out to this branch .PARAMETER azdevPat Optional. Azure DevOps PAT to use for cloning private repositories. .PARAMETER vmrDir Optional. Path to the dotnet/dotnet repository. When null, gets cloned to the temporary folder .PARAMETER debugOutput Optional. Enables debug logging in the darc vmr command. .PARAMETER ci Optional. Denotes that the script is running in a CI environment. #> param ( [Parameter(Mandatory=$true, HelpMessage="Path to the temporary folder where repositories will be cloned")] [string][Alias('t', 'tmp')]$tmpDir, [string][Alias('b', 'branch')]$vmrBranch, [string]$remote, [string]$azdevPat, [string][Alias('v', 'vmr')]$vmrDir, [switch]$ci, [switch]$debugOutput ) function Fail { Write-Host "> $($args[0])" -ForegroundColor 'Red' } function Highlight { Write-Host "> $($args[0])" -ForegroundColor 'Cyan' } $verbosity = 'verbose' if ($debugOutput) { $verbosity = 'debug' } # Validation if (-not $tmpDir) { Fail "Missing -tmpDir argument. Please specify the path to the temporary folder where the repositories will be cloned" exit 1 } # Sanitize the input if (-not $vmrDir) { $vmrDir = Join-Path $tmpDir 'dotnet' } if (-not (Test-Path -Path $tmpDir -PathType Container)) { New-Item -ItemType Directory -Path $tmpDir | Out-Null } # Prepare the VMR if (-not (Test-Path -Path $vmrDir -PathType Container)) { Highlight "Cloning 'dotnet/dotnet' into $vmrDir.." git clone https://github.com/dotnet/dotnet $vmrDir if ($vmrBranch) { git -C $vmrDir switch -c $vmrBranch } } else { if ((git -C $vmrDir diff --quiet) -eq $false) { Fail "There are changes in the working tree of $vmrDir. Please commit or stash your changes" exit 1 } if ($vmrBranch) { Highlight "Preparing $vmrDir" git -C $vmrDir checkout $vmrBranch git -C $vmrDir pull } } Set-StrictMode -Version Latest # Prepare darc Highlight 'Installing .NET, preparing the tooling..' . .\eng\common\tools.ps1 $dotnetRoot = InitializeDotNetCli -install:$true $env:DOTNET_ROOT = $dotnetRoot $darc = Get-Darc Highlight "Starting the synchronization of VMR.." # Synchronize the VMR $versionDetailsPath = Resolve-Path (Join-Path $PSScriptRoot '..\Version.Details.xml') | Select-Object -ExpandProperty Path [xml]$versionDetails = Get-Content -Path $versionDetailsPath $repoName = $versionDetails.SelectSingleNode('//Source').Mapping if (-not $repoName) { Fail "Failed to resolve repo mapping from $versionDetailsPath" exit 1 } $darcArgs = ( "vmr", "forwardflow", "--tmp", $tmpDir, "--$verbosity", $vmrDir ) if ($ci) { $darcArgs += ("--ci") } if ($azdevPat) { $darcArgs += ("--azdev-pat", $azdevPat) } & "$darc" $darcArgs if ($LASTEXITCODE -eq 0) { Highlight "Synchronization succeeded" } else { Highlight "Failed to flow code into the local VMR. Falling back to resetting the VMR to match repo contents..." git -C $vmrDir reset --hard $resetArgs = ( "vmr", "reset", "${repoName}:HEAD", "--vmr", $vmrDir, "--tmp", $tmpDir, "--additional-remotes", "${repoName}:${repoRoot}" ) & "$darc" $resetArgs if ($LASTEXITCODE -eq 0) { Highlight "Successfully reset the VMR using 'darc vmr reset'" } else { Fail "Synchronization of repo to VMR failed!" Fail "'$vmrDir' is left in its last state (re-run of this script will reset it)." Fail "Please inspect the logs which contain path to the failing patch file (use -debugOutput to get all the details)." Fail "Once you make changes to the conflicting VMR patch, commit it locally and re-run this script." exit 1 } } ================================================ FILE: eng/common/vmr-sync.sh ================================================ #!/bin/bash ### This script is used for synchronizing the current repository into a local VMR. ### It pulls the current repository's code into the specified VMR directory for local testing or ### Source-Build validation. ### ### The tooling used for synchronization will clone the VMR repository into a temporary folder if ### it does not already exist. These clones can be reused in future synchronizations, so it is ### recommended to dedicate a folder for this to speed up re-runs. ### ### USAGE: ### Synchronize current repository into a local VMR: ### ./vmr-sync.sh --tmp "$HOME/repos/tmp" "$HOME/repos/dotnet" ### ### Options: ### -t, --tmp, --tmp-dir PATH ### Required. Path to the temporary folder where repositories will be cloned ### ### -b, --branch, --vmr-branch BRANCH_NAME ### Optional. Branch of the 'dotnet/dotnet' repo to synchronize. The VMR will be checked out to this branch ### ### --debug ### Optional. Turns on the most verbose logging for the VMR tooling ### ### --remote name:URI ### Optional. Additional remote to use during the synchronization ### This can be used to synchronize to a commit from a fork of the repository ### Example: 'runtime:https://github.com/yourfork/runtime' ### ### --azdev-pat ### Optional. Azure DevOps PAT to use for cloning private repositories. ### ### -v, --vmr, --vmr-dir PATH ### Optional. Path to the dotnet/dotnet repository. When null, gets cloned to the temporary folder source="${BASH_SOURCE[0]}" # resolve $source until the file is no longer a symlink while [[ -h "$source" ]]; do scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" source="$(readlink "$source")" # if $source was a relative symlink, we need to resolve it relative to the path where the # symlink file was located [[ $source != /* ]] && source="$scriptroot/$source" done scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" function print_help () { sed -n '/^### /,/^$/p' "$source" | cut -b 5- } COLOR_RED=$(tput setaf 1 2>/dev/null || true) COLOR_CYAN=$(tput setaf 6 2>/dev/null || true) COLOR_CLEAR=$(tput sgr0 2>/dev/null || true) COLOR_RESET=uniquesearchablestring FAILURE_PREFIX='> ' function fail () { echo "${COLOR_RED}$FAILURE_PREFIX${1//${COLOR_RESET}/${COLOR_RED}}${COLOR_CLEAR}" >&2 } function highlight () { echo "${COLOR_CYAN}$FAILURE_PREFIX${1//${COLOR_RESET}/${COLOR_CYAN}}${COLOR_CLEAR}" } tmp_dir='' vmr_dir='' vmr_branch='' additional_remotes='' verbosity=verbose azdev_pat='' ci=false while [[ $# -gt 0 ]]; do opt="$(echo "$1" | tr "[:upper:]" "[:lower:]")" case "$opt" in -t|--tmp|--tmp-dir) tmp_dir=$2 shift ;; -v|--vmr|--vmr-dir) vmr_dir=$2 shift ;; -b|--branch|--vmr-branch) vmr_branch=$2 shift ;; --remote) additional_remotes="$additional_remotes $2" shift ;; --azdev-pat) azdev_pat=$2 shift ;; --ci) ci=true ;; -d|--debug) verbosity=debug ;; -h|--help) print_help exit 0 ;; *) fail "Invalid argument: $1" print_help exit 1 ;; esac shift done # Validation if [[ -z "$tmp_dir" ]]; then fail "Missing --tmp-dir argument. Please specify the path to the temporary folder where the repositories will be cloned" exit 1 fi # Sanitize the input if [[ -z "$vmr_dir" ]]; then vmr_dir="$tmp_dir/dotnet" fi if [[ ! -d "$tmp_dir" ]]; then mkdir -p "$tmp_dir" fi if [[ "$verbosity" == "debug" ]]; then set -x fi # Prepare the VMR if [[ ! -d "$vmr_dir" ]]; then highlight "Cloning 'dotnet/dotnet' into $vmr_dir.." git clone https://github.com/dotnet/dotnet "$vmr_dir" if [[ -n "$vmr_branch" ]]; then git -C "$vmr_dir" switch -c "$vmr_branch" fi else if ! git -C "$vmr_dir" diff --quiet; then fail "There are changes in the working tree of $vmr_dir. Please commit or stash your changes" exit 1 fi if [[ -n "$vmr_branch" ]]; then highlight "Preparing $vmr_dir" git -C "$vmr_dir" checkout "$vmr_branch" git -C "$vmr_dir" pull fi fi set -e # Prepare darc highlight 'Installing .NET, preparing the tooling..' source "./eng/common/tools.sh" InitializeDotNetCli true GetDarc dotnetDir=$( cd ./.dotnet/; pwd -P ) dotnet=$dotnetDir/dotnet highlight "Starting the synchronization of VMR.." set +e if [[ -n "$additional_remotes" ]]; then additional_remotes="--additional-remotes $additional_remotes" fi if [[ -n "$azdev_pat" ]]; then azdev_pat="--azdev-pat $azdev_pat" fi ci_arg='' if [[ "$ci" == "true" ]]; then ci_arg="--ci" fi # Synchronize the VMR version_details_path=$(cd "$scriptroot/.."; pwd -P)/Version.Details.xml repo_name=$(grep -m 1 ' net9.0 true true true false $(ArtifactsShippingPackagesDir) $([MSBuild]::NormalizeDirectory('$(ArtifactsDir)', 'YarpAppArtifacts', '$(Configuration)')) Yarp.Application.$(YarpAppRuntime) Reverse proxy $([MSBuild]::NormalizeDirectory('$(YarpAppArtifactsOutputDir)', '$(YarpAppRuntime)')) $(YarpArchiveDirectory)reverse-proxy-$(YarpAppRuntime) $(YarpArchiveBaseName).tar.gz $(YarpArchiveFileName).sha512 $([MSBuild]::NormalizeDirectory('$(ArtifactsBinDir)', 'Yarp.Application', '$(Configuration)', '$(TargetFramework)', '$(YarpAppRuntime)/', 'publish')) <_PublishItems Include="$(ArtifactsBinDir)/Yarp.Application/$(Configuration)/$(TargetFramework)/$(YarpAppRuntime)/publish/**/*" /> $(YarpArchiveSha512Name) ================================================ FILE: eng/yarpapppack/yarpapppack-linux-arm64.csproj ================================================ linux-arm64 Unix ================================================ FILE: eng/yarpapppack/yarpapppack-linux-x64.csproj ================================================ linux-x64 Unix ================================================ FILE: es-metadata.yml ================================================ schemaVersion: 0.0.1 isProduction: true accountableOwners: service: b317f6f4-0741-4d30-9f8c-9f0e61554b81 routing: defaultAreaPath: org: devdiv path: DevDiv\NET Libraries\Networking ================================================ FILE: global.json ================================================ { "sdk": { "version": "11.0.100-preview.1.26104.118" }, "tools": { "dotnet": "11.0.100-preview.1.26104.118", "runtimes": { "dotnet": [ "8.0.13", "9.0.2" ], "aspnetcore": [ "8.0.13", "9.0.2" ] } }, "test": { "runner": "Microsoft.Testing.Platform" }, "msbuild-sdks": { "Microsoft.DotNet.Arcade.Sdk": "11.0.0-beta.26122.1", "Microsoft.DotNet.Helix.Sdk": "11.0.0-beta.26122.1" } } ================================================ FILE: pack.cmd ================================================ @echo off powershell -ExecutionPolicy ByPass -NoProfile -command "& """%~dp0eng\common\Build.ps1""" -restore -build -pack %*" ================================================ FILE: pack.sh ================================================ #!/usr/bin/env bash source="${BASH_SOURCE[0]}" # resolve $SOURCE until the file is no longer a symlink while [[ -h $source ]]; do scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" source="$(readlink "$source")" # if $source was a relative symlink, we need to resolve it relative to the path where the # symlink file was located [[ $source != /* ]] && source="$scriptroot/$source" done scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" "$scriptroot/eng/common/build.sh" --build --restore --pack $@ ================================================ FILE: restore.cmd ================================================ @echo off powershell -ExecutionPolicy ByPass -NoProfile -command "& """%~dp0eng\common\Build.ps1""" -restore %*" ================================================ FILE: restore.sh ================================================ #!/usr/bin/env bash source="${BASH_SOURCE[0]}" # resolve $SOURCE until the file is no longer a symlink while [[ -h $source ]]; do scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" source="$(readlink "$source")" # if $source was a relative symlink, we need to resolve it relative to the path where the # symlink file was located [[ $source != /* ]] && source="$scriptroot/$source" done scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" "$scriptroot/eng/common/build.sh" --restore $@ ================================================ FILE: samples/BasicYarpSample/BasicYarpSample.csproj ================================================ $(ReleaseTFMs) latest ================================================ FILE: samples/BasicYarpSample/Program.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); // Add the reverse proxy capability to the server builder.Services.AddReverseProxy() // Initialize the reverse proxy from the "ReverseProxy" section of configuration .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy")); var app = builder.Build(); // Register the reverse proxy routes app.MapReverseProxy(); app.Run(); ================================================ FILE: samples/BasicYarpSample/Properties/launchSettings.json ================================================ { "$schema": "http://json.schemastore.org/launchsettings.json", "profiles": { "YARP Proxy Sample": { "commandName": "Project", "launchBrowser": false, "launchUrl": "", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } } } } ================================================ FILE: samples/BasicYarpSample/README.md ================================================ # Basic YARP Sample This sample shows how to consume the YARP Library to produce a simple reverse proxy server. The proxy server is implemented as 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 changeable 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 8 runtime, and to reference the proxy library from [nuget](https://www.nuget.org/packages/Yarp.ReverseProxy/) (.NET's package manager). - [Program.cs](Program.cs) - Provides the main entrypoint for .NET which uses a WebApplication to initialize the server which listens for http requests. This is also 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 (alternatively 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 --project ../SampleServer --Urls "http://localhost:10000;https://localhost:10010"``` ================================================ FILE: samples/BasicYarpSample/appsettings.json ================================================ { // Base URLs the server listens on, must be configured independently of the routes below. // Can also be configured via Kestrel/Endpoints, see https://docs.microsoft.com/aspnet/core/fundamentals/servers/kestrel/endpoints "Urls": "http://localhost:5000;https://localhost:5001", //Sets the Logging level for ASP.NET "Logging": { "LogLevel": { "Default": "Information", // Uncomment to hide diagnostic messages from runtime and proxy // "Microsoft": "Warning", // "Yarp" : "Warning", "Microsoft.Hosting.Lifetime": "Information" } }, "ReverseProxy": { // Routes tell the proxy which requests to forward "Routes": { "minimumroute": { // Matches anything and routes it to www.example.com "ClusterId": "minimumcluster", "Match": { "Path": "{**catch-all}" } }, "route2": { // matches /something/* and routes to 2 external addresses "ClusterId": "cluster2", "Match": { "Path": "/something/{*any}" } } }, // Clusters tell the proxy where and how to forward requests "Clusters": { "minimumcluster": { "Destinations": { "example.com": { "Address": "http://www.example.com/" } } }, "cluster2": { "Destinations": { "first_destination": { "Address": "https://contoso.com" }, "another_destination": { "Address": "https://bing.com" } }, "LoadBalancingPolicy": "PowerOfTwoChoices" } } } } ================================================ FILE: samples/Directory.Build.props ================================================ true false false ================================================ FILE: samples/KubernetesIngress.Sample/Combined/Dockerfile ================================================ #See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base WORKDIR /app EXPOSE 80 EXPOSE 443 FROM mcr.microsoft.com/dotnet/sdk:8.0 AS publish WORKDIR /src # We need to install the SDK manually because we might target an unreleased SDK COPY ["global.json", ""] RUN curl -sSL https://dot.net/v1/dotnet-install.sh | bash /dev/stdin --jsonfile global.json ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT 1 # Copy csproj files and other files needed for restoring (to build a nuget cache layer to speed up rebuilds) COPY ["samples/KubernetesIngress.Sample/Combined/Yarp.Kubernetes.IngressController.csproj", "samples/KubernetesIngress.Sample/Combined/"] COPY ["src/ReverseProxy/Yarp.ReverseProxy.csproj", "src/ReverseProxy/"] COPY ["src/Kubernetes.Controller/Yarp.Kubernetes.Controller.csproj", "src/Kubernetes.Controller/"] COPY ["src/Directory.Build.props", "src/"] COPY ["Directory.Build.*", "./"] COPY ["TFMs.props", ""] COPY ["NuGet.config", ""] COPY ["eng/Versions.props", "eng/"] # Build a cache layer with all of the nuget packages RUN /root/.dotnet/dotnet restore samples/KubernetesIngress.Sample/Combined/Yarp.Kubernetes.IngressController.csproj # Copy the remaining source files WORKDIR /src COPY . . WORKDIR /src/samples/KubernetesIngress.Sample/Combined/ RUN /root/.dotnet/dotnet publish -c Release --no-restore -o /app/publish -f net8.0 FROM base AS final WORKDIR /app COPY --from=publish /app/publish . ENTRYPOINT ["dotnet", "Yarp.Kubernetes.IngressController.dll"] ================================================ FILE: samples/KubernetesIngress.Sample/Combined/Program.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Serilog; using Serilog.Sinks.SystemConsole.Themes; var builder = WebApplication.CreateBuilder(args); using var serilog = new LoggerConfiguration() .MinimumLevel.Debug() .Enrich.FromLogContext() .WriteTo.Console(theme: AnsiConsoleTheme.Code) .CreateLogger(); builder.Logging.ClearProviders(); builder.Logging.AddSerilog(serilog, dispose: false); builder.Configuration.AddJsonFile("/app/config/yarp.json", optional: true); builder.WebHost.UseKubernetesReverseProxyCertificateSelector(); builder.Services.AddKubernetesReverseProxy(builder.Configuration); var app = builder.Build(); app.MapReverseProxy(); app.Run(); ================================================ FILE: samples/KubernetesIngress.Sample/Combined/Properties/launchSettings.json ================================================ { "profiles": { "Ingress": { "commandName": "Project", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, "applicationUrl": "https://localhost:5021;http://localhost:5020" }, "Docker": { "commandName": "Docker", "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}", "publishAllPorts": true, "useSSL": true } } } ================================================ FILE: samples/KubernetesIngress.Sample/Combined/README.md ================================================ # Yarp Ingress Controller This directory contains a sample ingress as well as the definition for the Kubernetes manifests for the ingress controller. The sample ingress controller is a single deployable. ## Building the Docker Image From the base directory for this repo (where the .slnx file is), run the command: ```bash docker build -t yarp-combined:latest -f ./samples/KubernetesIngress.Sample/Combined/Dockerfile . ``` ## Deploying the Sample Ingress Controller 1. Open the [ingress-controller.yaml](./ingress-controller.yaml) file 2. Modify the container image to match the name used when building the image, e.g. change `/yarp-combined:` to `yarp-combined:latest` 3. From the root of this repo. run the command `kubectl apply -f ./samples/KubernetesIngress.Sample/Combined/ingress-controller.yaml` To undeploy the ingress controller, run the command `kubectl delete -f ./samples/KubernetesIngress.Sample/Combined/ingress-controller.yaml` ================================================ FILE: samples/KubernetesIngress.Sample/Combined/Yarp.Kubernetes.IngressController.csproj ================================================ $(ReleaseTFMs) 78d1f3b4-abce-4c5a-b914-3321fab1f8d0 Linux $('System.TeamProject') != 'internal' ================================================ FILE: samples/KubernetesIngress.Sample/Combined/appsettings.Development.json ================================================ { "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } } } ================================================ FILE: samples/KubernetesIngress.Sample/Combined/appsettings.json ================================================ { "Logging": { "LogLevel": { "Default": "Debug" } }, "AllowedHosts": "*", "Yarp": { "ControllerClass": "microsoft.com/ingress-yarp", "ServerCertificates": false, "DefaultSslCertificate": "yarp/yarp-ingress-tls" } } ================================================ FILE: samples/KubernetesIngress.Sample/Combined/ingress-controller.yaml ================================================ kind: Namespace apiVersion: v1 metadata: name: yarp --- apiVersion: v1 kind: ConfigMap metadata: name: yarp-config namespace: yarp data: yarp.json: | { "Yarp": { "ControllerClass": "microsoft.com/ingress-yarp", "ServerCertificates": false, "DefaultSslCertificate": "yarp/yarp-ingress-tls", "ControllerServiceName": "ingress-yarp-controller", "ControllerServiceNamespace": "yarp" } } --- apiVersion: v1 kind: ServiceAccount metadata: name: yarp-serviceaccount namespace: yarp --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: yarp-ingress-clusterrole namespace: yarp rules: - apiGroups: - "" resources: - endpoints - nodes - pods - secrets - namespaces verbs: - list - watch - apiGroups: - "" resources: - nodes verbs: - get - apiGroups: - "" resources: - services verbs: - get - list - watch - apiGroups: - "" resources: - services/status verbs: - get - apiGroups: - networking.k8s.io - extensions - networking.internal.knative.dev resources: - ingresses - ingressclasses verbs: - get - list - watch - apiGroups: - networking.k8s.io resources: - events verbs: - create - patch - apiGroups: - networking.k8s.io - extensions - networking.internal.knative.dev resources: - ingresses/status verbs: - get - update --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: yarp-ingress-clusterrole-nisa-binding roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: yarp-ingress-clusterrole subjects: - kind: ServiceAccount name: yarp-serviceaccount namespace: yarp --- apiVersion: networking.k8s.io/v1 kind: IngressClass metadata: name: yarp annotations: #ingressclass.kubernetes.io/is-default-class: "true" spec: controller: microsoft.com/ingress-yarp --- apiVersion: v1 kind: Service metadata: name: ingress-yarp-controller namespace: yarp spec: ports: - name: proxy port: 80 protocol: TCP targetPort: 8000 - name: proxy-ssl port: 443 protocol: TCP targetPort: 8443 selector: app: ingress-yarp-controller type: LoadBalancer --- apiVersion: apps/v1 kind: Deployment metadata: labels: app: ingress-yarp-controller name: ingress-yarp namespace: yarp spec: replicas: 1 selector: matchLabels: app: ingress-yarp-controller template: metadata: labels: app: ingress-yarp-controller spec: containers: - name: yarp-controller imagePullPolicy: IfNotPresent image: /yarp-combined: ports: - containerPort: 8000 name: proxy protocol: TCP - containerPort: 8443 name: proxy-ssl protocol: TCP env: - name: ASPNETCORE_URLS value: http://*:8000;https://*:8443 volumeMounts: - name: config readOnly: true mountPath: /app/config volumes: - name: config configMap: name: yarp-config serviceAccountName: yarp-serviceaccount ================================================ FILE: samples/KubernetesIngress.Sample/Ingress/Dockerfile ================================================ #See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base WORKDIR /app EXPOSE 80 EXPOSE 443 FROM mcr.microsoft.com/dotnet/sdk:8.0 AS publish WORKDIR /src # We need to install the SDK manually because we might target an unreleased SDK COPY ["global.json", ""] RUN curl -sSL https://dot.net/v1/dotnet-install.sh | bash /dev/stdin --jsonfile global.json ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT 1 # Copy csproj files and other files needed for restoring (to build a nuget cache layer to speed up rebuilds) COPY ["samples/KubernetesIngress.Sample/Ingress/Yarp.Kubernetes.Ingress.csproj", "samples/KubernetesIngress.Sample/Ingress/"] COPY ["src/ReverseProxy/Yarp.ReverseProxy.csproj", "src/ReverseProxy/"] COPY ["src/Kubernetes.Controller/Yarp.Kubernetes.Controller.csproj", "src/Kubernetes.Controller/"] COPY ["src/Directory.Build.props", "src/"] COPY ["Directory.Build.*", "./"] COPY ["TFMs.props", ""] COPY ["NuGet.config", ""] COPY ["eng/Versions.props", "eng/"] # Build a cache layer with all of the nuget packages RUN /root/.dotnet/dotnet restore samples/KubernetesIngress.Sample/Ingress/Yarp.Kubernetes.Ingress.csproj # Copy the remaining source files WORKDIR /src COPY . . WORKDIR /src/samples/KubernetesIngress.Sample/Ingress/ RUN /root/.dotnet/dotnet publish -c Release --no-restore -o /app/publish -f net8.0 FROM base AS final WORKDIR /app COPY --from=publish /app/publish . ENTRYPOINT ["dotnet", "Yarp.Kubernetes.Ingress.dll"] ================================================ FILE: samples/KubernetesIngress.Sample/Ingress/Program.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Serilog; using Serilog.Sinks.SystemConsole.Themes; using Yarp.Kubernetes.Protocol; var builder = WebApplication.CreateBuilder(args); using var serilog = new LoggerConfiguration() .MinimumLevel.Debug() .Enrich.FromLogContext() .WriteTo.Console(theme: AnsiConsoleTheme.Code) .CreateLogger(); builder.Logging.ClearProviders(); builder.Logging.AddSerilog(serilog, dispose: false); var services = builder.Services; services.Configure(builder.Configuration.Bind); services.AddHostedService(); services.AddReverseProxy() .LoadFromMessages(); var app = builder.Build(); app.MapReverseProxy(); app.Run(); ================================================ FILE: samples/KubernetesIngress.Sample/Ingress/Properties/launchSettings.json ================================================ { "profiles": { "Ingress": { "commandName": "Project", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, "applicationUrl": "https://localhost:5021;http://localhost:5020" }, "Docker": { "commandName": "Docker", "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}", "publishAllPorts": true, "useSSL": true } } } ================================================ FILE: samples/KubernetesIngress.Sample/Ingress/README.md ================================================ # Yarp Ingress Controller This directory contains a sample ingress as well as the definition for the Kubernetes manifests for the ingress controller. This sample requires two applications to be deployed: * An Ingress (this application) * A Kubernetes Ingress Monitor (a process listening for changes in k8s and dispatching the Yarp configuration to ingress instances) NOTE: Yarp Kubernetes can also be configured as a combined (single) deployable. See the combined [README.md](../Combined/README.md) for more information. ## Building the Docker Images From the base directory for this repo (where the .slnx file is), run the commands: ```bash docker build -t yarp-monitor:latest -f ./samples/KubernetesIngress.Sample/Monitor/Dockerfile . docker build -t yarp-ingress:latest -f ./samples/KubernetesIngress.Sample/Ingress/Dockerfile . ``` ## Deploying the Sample Ingress Controller 1. Open the [ingress-monitor.yaml](../Monitor/ingress-monitor.yaml) file 1. Modify the container image to match the name used when building the image, e.g. change `/yarp-monitor:` to `yarp-monitor:latest` 1. Run the command `kubectl apply -f ./samples/KubernetesIngress.Sample/Monitor/ingress-monitor.yaml` 1. Open the [ingress.yaml](./ingress.yaml) file 1. Modify the container image to match the name used when building the image, e.g. change `/yarp-ingress:` to `yarp-ingress:latest` 1. Run the command `kubectl apply -f ./samples/KubernetesIngress.Sample/Ingress/ingress.yaml` To undeploy the ingress, run the commands ```bash kubectl delete -f ./samples/KubernetesIngress.Sample/Ingress/ingress.yaml kubectl delete -f ./samples/KubernetesIngress.Sample/Monitor/ingress-monitor.yaml ``` ================================================ FILE: samples/KubernetesIngress.Sample/Ingress/Yarp.Kubernetes.Ingress.csproj ================================================ $(ReleaseTFMs) b2dc6cd7-acbb-4d65-ad19-74771ff3c80f Linux $('System.TeamProject') != 'internal' ================================================ FILE: samples/KubernetesIngress.Sample/Ingress/appsettings.Development.json ================================================ { "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } } } ================================================ FILE: samples/KubernetesIngress.Sample/Ingress/appsettings.json ================================================ { "Logging": { "LogLevel": { "Default": "Debug" } }, "AllowedHosts": "*", "ControllerUrl": "http://yarp-controller.yarp.svc.cluster.local:8000/api/dispatch" } ================================================ FILE: samples/KubernetesIngress.Sample/Ingress/ingress.yaml ================================================ apiVersion: v1 kind: Service metadata: name: yarp-proxy namespace: yarp spec: ports: - name: proxy port: 80 protocol: TCP targetPort: 8000 - name: proxy-ssl port: 443 protocol: TCP targetPort: 8443 selector: app: ingress-yarp type: LoadBalancer --- apiVersion: apps/v1 kind: Deployment metadata: labels: app: ingress-yarp name: yarp-proxy namespace: yarp spec: replicas: 1 selector: matchLabels: app: ingress-yarp template: metadata: labels: app: ingress-yarp spec: containers: - name: yarp-proxy imagePullPolicy: IfNotPresent image: /yarp-ingress: ports: - containerPort: 8000 name: proxy protocol: TCP - containerPort: 8443 name: proxy-ssl protocol: TCP env: - name: ASPNETCORE_URLS value: http://*:8000 --- ================================================ FILE: samples/KubernetesIngress.Sample/Monitor/Dockerfile ================================================ #See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base WORKDIR /app EXPOSE 80 EXPOSE 443 FROM mcr.microsoft.com/dotnet/sdk:8.0 AS publish WORKDIR /src # We need to install the SDK manually because we might target an unreleased SDK COPY ["global.json", ""] RUN curl -sSL https://dot.net/v1/dotnet-install.sh | bash /dev/stdin --jsonfile global.json ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT 1 # Copy csproj files and other files needed for restoring (to build a nuget cache layer to speed up rebuilds) COPY ["samples/KubernetesIngress.Sample/Monitor/Yarp.Kubernetes.Monitor.csproj", "samples/KubernetesIngress.Sample/Monitor/"] COPY ["src/ReverseProxy/Yarp.ReverseProxy.csproj", "src/ReverseProxy/"] COPY ["src/Kubernetes.Controller/Yarp.Kubernetes.Controller.csproj", "src/Kubernetes.Controller/"] COPY ["src/Directory.Build.props", "src/"] COPY ["Directory.Build.*", "./"] COPY ["TFMs.props", ""] COPY ["NuGet.config", ""] COPY ["eng/Versions.props", "eng/"] # Build a cache layer with all of the nuget packages RUN /root/.dotnet/dotnet restore samples/KubernetesIngress.Sample/Monitor/Yarp.Kubernetes.Monitor.csproj # Copy the remaining source files WORKDIR /src COPY . . WORKDIR /src/samples/KubernetesIngress.Sample/Monitor/ RUN /root/.dotnet/dotnet publish -c Release --no-restore -o /app/publish -f net8.0 FROM base AS final WORKDIR /app COPY --from=publish /app/publish . ENTRYPOINT ["dotnet", "Yarp.Kubernetes.Monitor.dll"] ================================================ FILE: samples/KubernetesIngress.Sample/Monitor/Program.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Serilog; using Serilog.Sinks.SystemConsole.Themes; var builder = WebApplication.CreateBuilder(args); using var serilog = new LoggerConfiguration() .MinimumLevel.Debug() .Enrich.FromLogContext() .WriteTo.Console(theme: AnsiConsoleTheme.Code) .CreateLogger(); builder.Logging.ClearProviders(); builder.Logging.AddSerilog(serilog, dispose: false); builder.Configuration.AddJsonFile("/app/config/yarp.json", optional: true); builder.Services.AddKubernetesIngressMonitor(builder.Configuration); // Add ASP.NET Core controller support builder.Services.AddControllers() .AddKubernetesDispatchController(); var app = builder.Build(); app.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); app.Run(); ================================================ FILE: samples/KubernetesIngress.Sample/Monitor/Properties/launchSettings.json ================================================ { "profiles": { "Ingress": { "commandName": "Project", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, "applicationUrl": "https://localhost:5021;http://localhost:5020" }, "Docker": { "commandName": "Docker", "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}", "publishAllPorts": true, "useSSL": true } } } ================================================ FILE: samples/KubernetesIngress.Sample/Monitor/README.md ================================================ # Yarp Ingress Monitor This directory contains a sample ingress monitor as well as the definition for the Kubernetes manifests for the ingress controller. This monitor works in conjunction with the Yarp Ingress. See [README.md](../Ingress/README.md) file for build and deployment instructions. ================================================ FILE: samples/KubernetesIngress.Sample/Monitor/Yarp.Kubernetes.Monitor.csproj ================================================ $(ReleaseTFMs) 42f98116-26c4-4115-b6af-c5dec1f88c84 Linux $('System.TeamProject') != 'internal' ================================================ FILE: samples/KubernetesIngress.Sample/Monitor/appsettings.Development.json ================================================ { "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } } } ================================================ FILE: samples/KubernetesIngress.Sample/Monitor/appsettings.json ================================================ { "Logging": { "LogLevel": { "Default": "Debug" } }, "AllowedHosts": "*", "Yarp": { "ControllerClass": "microsoft.com/ingress-yarp" } } ================================================ FILE: samples/KubernetesIngress.Sample/Monitor/ingress-monitor.yaml ================================================ kind: Namespace apiVersion: v1 metadata: name: yarp --- apiVersion: v1 kind: ConfigMap metadata: name: yarp-config namespace: yarp data: yarp.json: | { "Yarp": { "ControllerClass": "microsoft.com/ingress-yarp", "ControllerServiceName": "yarp-controller", "ControllerServiceNamespace": "yarp" } } --- apiVersion: v1 kind: ServiceAccount metadata: name: yarp-serviceaccount namespace: yarp --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: yarp-ingress-clusterrole namespace: yarp rules: - apiGroups: - "" resources: - endpoints - nodes - pods - secrets - namespaces verbs: - list - watch - apiGroups: - "" resources: - nodes verbs: - get - apiGroups: - "" resources: - services verbs: - get - list - watch - apiGroups: - "" resources: - services/status verbs: - get - apiGroups: - networking.k8s.io - extensions - networking.internal.knative.dev resources: - ingresses - ingressclasses verbs: - get - list - watch - apiGroups: - networking.k8s.io resources: - events verbs: - create - patch - apiGroups: - networking.k8s.io - extensions - networking.internal.knative.dev resources: - ingresses/status verbs: - get - update --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: yarp-ingress-clusterrole-nisa-binding roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: yarp-ingress-clusterrole subjects: - kind: ServiceAccount name: yarp-serviceaccount namespace: yarp --- apiVersion: networking.k8s.io/v1 kind: IngressClass metadata: name: yarp annotations: #ingressclass.kubernetes.io/is-default-class: true spec: controller: microsoft.com/ingress-yarp --- apiVersion: v1 kind: Service metadata: name: yarp-controller namespace: yarp spec: ports: - name: api port: 8000 protocol: TCP targetPort: 8000 selector: app: ingress-yarp-controller type: ClusterIP --- apiVersion: apps/v1 kind: Deployment metadata: labels: app: ingress-yarp-controller name: yarp-controller namespace: yarp spec: replicas: 1 selector: matchLabels: app: ingress-yarp-controller template: metadata: labels: app: ingress-yarp-controller spec: containers: - name: yarp-controller imagePullPolicy: IfNotPresent image: /yarp-monitor: ports: - containerPort: 8000 name: api protocol: TCP env: - name: ASPNETCORE_URLS value: http://*:8000 volumeMounts: - name: config readOnly: true mountPath: /app/config volumes: - name: config configMap: name: yarp-config serviceAccountName: yarp-serviceaccount ================================================ FILE: samples/KubernetesIngress.Sample/README.md ================================================ # Kubernetes Ingress Samples These samples show how to deploy the YARP Kubernetes Ingress Controller into a Kubernetes cluster. An [Ingress Controller](https://kubernetes.io/docs/concepts/services-networking/ingress-controllers/) monitors for [Ingress resources](https://kubernetes.io/docs/concepts/services-networking/ingress/) and routes traffic to services. There are three parts to these samples: - [Backend](./backend/README.md) - [Combined Ingress Controller](./Combined/README.md) - [Separate Ingress Controller and Monitor](./Ingress/README.md) The "Backend" is a Dockerized ASP.NET Core application that returns dummy information in web requests. This project contains Kubernetes manifest files for deploying the application and an Ingress resource into a cluster. The Ingress Controller can be deployed either as: - a single deployable (see the Combined sample), or - as two separate deployables where one (the "monitor") watches the Ingress resources and the other (the "ingress") retrieves the YARP configuration from the "monitor" and handles the routing Both of these controllers utilize the `Yarp.Kubernetes.Controller` project. ## Ingress Resource The `Yarp.Kubernetes.Controller` project currently supports the following Ingress features: - [Ingress rules](https://kubernetes.io/docs/concepts/services-networking/ingress/#ingress-rules) for host name and path-based routing to backend services - [Ingress class](https://kubernetes.io/docs/concepts/services-networking/ingress/#ingress-class) for multiple, independent instances of the controller (cluster scope only) - [Default ingress class](https://kubernetes.io/docs/concepts/services-networking/ingress/#default-ingress-class) for simplifying Ingress resource configuration The `Yarp.Kubernetes.Controller` project does not support: - The [TLS specification](https://kubernetes.io/docs/concepts/services-networking/ingress/#tls) for Ingress resources (coming soon), though you could combined with the LetsEncrypt.Sample. - The [deprecated annotation](https://kubernetes.io/docs/concepts/services-networking/ingress/#deprecated-annotation) for ingress resources. ### Annotations The `Yarp.Kubernetes.Controller` project supports a number of **optional** annotations on Ingress resources for functionality provided by YARP. These annotations would be specified like this: ```yaml apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: minimal-ingress namespace: default annotations: yarp.ingress.kubernetes.io/authorization-policy: authzpolicy yarp.ingress.kubernetes.io/rate-limiter-policy: ratelimiterpolicy yarp.ingress.kubernetes.io/output-cache-policy: outputcachepolicy yarp.ingress.kubernetes.io/transforms: | - PathRemovePrefix: "/apis" yarp.ingress.kubernetes.io/route-headers: | - Name: the-header-key Values: - the-header-value Mode: Contains IsCaseSensitive: false - Name: another-header-key Values: - another-header-value Mode: Contains IsCaseSensitive: false yarp.ingress.kubernetes.io/route-queryparameters: | - Name: the-queryparameters-key Values: - the-queryparameters-value Mode: Contains IsCaseSensitive: false - Name: another-queryparameters-key Values: - another-queryparameters-value Mode: Contains IsCaseSensitive: false yarp.ingress.kubernetes.io/route-methods: | - GET - POST spec: rules: - http: paths: - path: /foo pathType: Prefix backend: service: name: frontend port: number: 80 ``` The table below lists the available annotations. |Annotation|Data Type| |---|---| |yarp.ingress.kubernetes.io/authorization-policy|string| |yarp.ingress.kubernetes.io/rate-limiter-policy|string| |yarp.ingress.kubernetes.io/output-cache-policy|string| |yarp.ingress.kubernetes.io/backend-protocol|string| |yarp.ingress.kubernetes.io/cors-policy|string| |yarp.ingress.kubernetes.io/health-check|[ActivateHealthCheckConfig](https://learn.microsoft.com/dotnet/api/yarp.reverseproxy.configuration.activehealthcheckconfig)| |yarp.ingress.kubernetes.io/http-client|[HttpClientConfig](https://learn.microsoft.com/dotnet/api/yarp.reverseproxy.configuration.httpclientconfig)| |yarp.ingress.kubernetes.io/http-request|[ForwarderRequestConfig](https://learn.microsoft.com/en-us/dotnet/api/yarp.reverseproxy.forwarder.forwarderrequestconfig)| |yarp.ingress.kubernetes.io/load-balancing|string| |yarp.ingress.kubernetes.io/route-metadata|Dictionary<string, string>| |yarp.ingress.kubernetes.io/session-affinity|[SessionAffinityConfig](https://learn.microsoft.com/dotnet/api/yarp.reverseproxy.configuration.sessionaffinityconfig)| |yarp.ingress.kubernetes.io/transforms|List<Dictionary<string, string>>| |yarp.ingress.kubernetes.io/route-headers|List<[RouteHeader](https://learn.microsoft.com/dotnet/api/yarp.reverseproxy.configuration.routeheader)>| |yarp.ingress.kubernetes.io/route-queryparameters|List<[RouteQueryParameter](https://learn.microsoft.com/dotnet/api/yarp.reverseproxy.configuration.routequeryparameter)>| |yarp.ingress.kubernetes.io/route-order|int| |yarp.ingress.kubernetes.io/route-methods|List<string>| #### Authorization Policy See https://learn.microsoft.com/aspnet/core/fundamentals/servers/yarp/authn-authz for a list of available policies, or how to add your own custom policies. `yarp.ingress.kubernetes.io/authorization-policy: anonymous` #### RateLimiter Policy See https://learn.microsoft.com/aspnet/core/fundamentals/servers/yarp/rate-limiting for a list of available policies, or how to add your own custom policies. `yarp.ingress.kubernetes.io/rate-limiter-policy: mypolicy` #### Output Cache Policy `yarp.ingress.kubernetes.io/output-cache-policy: mycachepolicy` #### Backend Protocol Specifies the protocol of the backend service. Defaults to http. `yarp.ingress.kubernetes.io/backend-protocol: "https"` #### CORS Policy See https://learn.microsoft.com/aspnet/core/fundamentals/servers/yarp/cors for the list of available policies, or how to add your own custom policies. `yarp.ingress.kubernetes.io/cors-policy: mypolicy` #### Health Check Proactively monitors destination health by sending periodic probing requests to designated health endpoints and analyzing responses. See https://learn.microsoft.com/aspnet/core/fundamentals/servers/yarp/dests-health-checks. ```yaml yarp.ingress.kubernetes.io/health-check | Active: Enabled: true Interval: '00:00:10' Timeout: '00:00:10' Policy: ConsecutiveFailures Path: "/api/health" ``` #### HTTP Client Configures the HTTP client that will be used for the destination service. See https://learn.microsoft.com/aspnet/core/fundamentals/servers/yarp/http-client-config. ```yaml yarp.ingress.kubernetes.io/http-client: | SslProtocols: Ssl3 MaxConnectionsPerServer: 2 DangerousAcceptAnyServerCertificate: true ``` #### HTTP Request Configures the HTTP request that will be sent to the destination service. See https://learn.microsoft.com/dotnet/api/yarp.reverseproxy.forwarder.forwarderrequestconfig. ```yaml yarp.ingress.kubernetes.io/http-request: | ActivityTimeout: '00:01:00' Version: '2.0' VersionPolicy: 'RequestVersionExact' AllowResponseBuffering: false ``` #### Load Balancing See https://learn.microsoft.com/aspnet/core/fundamentals/servers/yarp/load-balancing for a list of the available options. `yarp.ingress.kubernetes.io/load-balancing: Random` #### Route Metadata See https://learn.microsoft.com/dotnet/api/yarp.reverseproxy.configuration.routeconfig.metadata#yarp-reverseproxy-configuration-routeconfig-metadata. ```yaml yarp.ingress.kubernetes.io/route-metadata: | Custom: "orange" Tenant: "12345" ``` #### Session Affinity See https://learn.microsoft.com/aspnet/core/fundamentals/servers/yarp/session-affinity. ```yaml yarp.ingress.kubernetes.io/session-affinity: | Enabled: true Policy: Cookie FailurePolicy: Redistribute AffinityKeyName: Key1 Cookie: Domain: localhost Expiration: HttpOnly: true IsEssential: true MaxAge: Path: mypath SameSite: Strict SecurePolicy: Always ``` #### Transforms Transforms use the YAML key-value pairs as per the YARP [Request Transforms](https://learn.microsoft.com/aspnet/core/fundamentals/servers/yarp/transforms#request-transforms) ```yaml yarp.ingress.kubernetes.io/transforms: | - PathPrefix: "/apis" - RequestHeader: header1 Append: bar ``` #### Route Headers `route-headers` are the YAML representation of YARP [Header Based Routing](https://learn.microsoft.com/aspnet/core/fundamentals/servers/yarp/header-routing). See https://learn.microsoft.com/dotnet/api/yarp.reverseproxy.configuration.routeheader. ```yaml yarp.ingress.kubernetes.io/route-headers: | - Name: the-header-key Values: - the-header-value Mode: Contains IsCaseSensitive: false - Name: another-header-key Values: - another-header-value Mode: Contains IsCaseSensitive: false ``` #### Route QueryParameters `route-queryparameters` are the YAML representation of YARP [Parameter Based Routing](https://learn.microsoft.com/aspnet/core/fundamentals/servers/yarp/queryparameter-routing). See https://learn.microsoft.com/dotnet/api/yarp.reverseproxy.configuration.routequeryparameter. ```yaml yarp.ingress.kubernetes.io/route-queryparameters: | - Name: the-queryparameter-name Values: - the-queryparameter-value Mode: Contains IsCaseSensitive: false - Name: another-queryparameter-name Values: - another-queryparameter-value Mode: Contains IsCaseSensitive: false ``` #### Route Order See https://learn.microsoft.com/dotnet/api/yarp.reverseproxy.configuration.routeconfig.order#yarp-reverseproxy-configuration-routeconfig-order. ```yaml yarp.ingress.kubernetes.io/route-order: '10' ``` #### Route Methods See https://learn.microsoft.com/dotnet/api/yarp.reverseproxy.configuration.routematch.methods#yarp-reverseproxy-configuration-routematch-methods. ```yaml yarp.ingress.kubernetes.io/route-methods: | - GET - POST ``` ================================================ FILE: samples/KubernetesIngress.Sample/backend/Dockerfile ================================================ #See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base WORKDIR /app EXPOSE 80 EXPOSE 443 FROM mcr.microsoft.com/dotnet/sdk:8.0 AS publish WORKDIR /src # We need to install the SDK manually because we might target an unreleased SDK COPY ["global.json", ""] COPY ["TFMs.props", "Directory.Build.props"] RUN curl -sSL https://dot.net/v1/dotnet-install.sh | bash /dev/stdin --jsonfile global.json ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT 1 COPY ["samples/KubernetesIngress.Sample/backend", "backend"] WORKDIR /src/backend RUN /root/.dotnet/dotnet publish -c Release -o /app/publish -f net8.0 FROM base AS final WORKDIR /app COPY --from=publish /app/publish . ENTRYPOINT ["dotnet", "backend.dll"] ================================================ FILE: samples/KubernetesIngress.Sample/backend/Program.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; using System.Net; using System.Text.Json; using Microsoft.AspNetCore.Builder; Activity.DefaultIdFormat = ActivityIdFormat.W3C; var builder = WebApplication.CreateBuilder(args); var app = builder.Build(); app.MapGet("/", async context => { var backendInfo = new BackendInfo() { IP = context.Connection.LocalIpAddress.ToString(), Hostname = Dns.GetHostName(), }; context.Response.ContentType = "application/json; charset=utf-8"; await JsonSerializer.SerializeAsync(context.Response.Body, backendInfo); }); app.Run(); internal sealed class BackendInfo { public string IP { get; set; } = default!; public string Hostname { get; set; } = default!; } ================================================ FILE: samples/KubernetesIngress.Sample/backend/Properties/launchSettings.json ================================================ { "iisSettings": { "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { "applicationUrl": "http://localhost:21017", "sslPort": 44378 } }, "profiles": { "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, "backend": { "commandName": "Project", "launchBrowser": true, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, "applicationUrl": "https://localhost:5002;http://localhost:5003" }, "Docker": { "commandName": "Docker", "launchBrowser": true, "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}", "publishAllPorts": true, "useSSL": true } } } ================================================ FILE: samples/KubernetesIngress.Sample/backend/README.md ================================================ # Sample backend This directory contains a sample ASP.NET Core application that acts as a "backend service" within a cluster as well as the definition for the Kubernetes manifests for the ingress controller. ## Building the Docker Images From the base directory for this repo (where the .slnx file is), run the commands: ```bash docker build -t backend:latest -f ./samples/KubernetesIngress.Sample/backend/Dockerfile . ``` ## Deploying the Backend 1. Open the [backend.yaml](./backend.yaml) file 1. Modify the container image to match the name used when building the image, e.g. change `/backend:` to `backend:latest` 1. Run the command `kubectl apply -f ./samples/KubernetesIngress.Sample/backend/backend.yaml` 1. Run the command `kubectl apply -f ./samples/KubernetesIngress.Sample/backend/ingress-sample.yaml` To undeploy the backend, run the commands ```bash kubectl delete -f ./samples/KubernetesIngress.Sample/backend/ingress-sample.yaml kubectl delete -f ./samples/KubernetesIngress.Sample/backend/backend.yaml ``` ================================================ FILE: samples/KubernetesIngress.Sample/backend/appsettings.Development.json ================================================ { "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } } } ================================================ FILE: samples/KubernetesIngress.Sample/backend/appsettings.json ================================================ { "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } }, "AllowedHosts": "*" } ================================================ FILE: samples/KubernetesIngress.Sample/backend/backend.csproj ================================================ $(ReleaseTFMs) Backend aaa98da6-d0d4-4ad6-9821-f66057413c3a Linux ..\..\..\.. ================================================ FILE: samples/KubernetesIngress.Sample/backend/backend.yaml ================================================ apiVersion: apps/v1 kind: Deployment metadata: name: backend spec: replicas: 1 selector: matchLabels: app: backend template: metadata: labels: app: backend spec: containers: - name: backend image: /backend: imagePullPolicy: IfNotPresent ports: - containerPort: 80 --- apiVersion: v1 kind: Service metadata: name: backend spec: selector: app: backend ports: - port: 80 targetPort: 80 type: ClusterIP ================================================ FILE: samples/KubernetesIngress.Sample/backend/ingress-sample.yaml ================================================ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: minimal-ingress spec: ingressClassName: yarp rules: - http: paths: - path: / pathType: Prefix backend: service: name: backend port: number: 80 ================================================ FILE: samples/Prometheus/HttpLoadApp/HttpLoadApp.csproj ================================================ $(ReleaseTFMs) Exe ================================================ FILE: samples/Prometheus/HttpLoadApp/Program.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Net.Http; using System.Threading.Tasks; var client = new HttpClient(); string[] planets = { "Mercury", "Venus", "Earth", "Mars", "Jupiter" }; foreach (var planet in planets) { _ = Task.Run(() => { CreateLoad("http://localhost:5000/" + planet); }); } var c = Console.ReadKey(); async void CreateLoad(string UrlPrefix) { var i = 0; while (true) { var url = UrlPrefix + "/" + i; var resp = await client.GetAsync(url); var result = await resp.Content.ReadAsStringAsync(); Console.WriteLine($"Requested: {url}, Result: {resp.StatusCode}, Length: {result.Length}"); i++; } } ================================================ FILE: samples/Prometheus/README.md ================================================ # YARP Prometheus sample This sample demonstrates how to use the ReverseProxy.Telemetry.Consumption library to listen to telemetry data from YARP and then to publish them as a endpoint for Prometheus to consume. This sample uses the [prometheus-net](https://github.com/prometheus-net/prometheus-net) library to expose Counters, Gauges & Histograms to Prometheus, which makes exposing the telemetry much easier. Internally YARP uses EventCounters to collect telemetry events and metrics from a number of subsystems that are used toprocess the requests. The YARP telemetry library provides wrapper classes that collect these metrics and make them available for Consumption. To listen for the metrics you register classes with DI that implement an interface for each subsystem. Event/metric listeners will only be created for the subsystems that you register for, as each registration has performance implications. The subsystems are: - **Proxy** which represents the overall proxy operation, and success or failure. Metrics include: - Number of requests started - Number of request in flight - Number of requests that have failed - **Kestrel** which is the web server that handles incoming requests. Metrics include: - Connection Rate - how many connections are opened a second - Total number of connections - Number of TLS handshakes - Incoming queue lengths - **Http** which is the HttpClient which makes outgoing requests to the destination servers. Metrics include: - Number of outgoing requests started - Number of Requests failed - Number of active requests - Number of outbound connections - **Sockets** which collects metrics about the amount of data send and received - **NameResolution** which collects metrics for DNS lookup of destinations ## Sample Contents - **ReverseProxy.Metrics.Prometheus.Sample** - AppSettings.json - provides the configuration of routes, clusters and destinations. In this case it has 5 routes that map to a series of clusters which share 10 destinations between them. - Startup.cs - follows the same pattern as other samples but also calls - services.AddAllPrometheusMetrics() - to register handlers for all the metric collection - proxyPipeline.UsePerRequestMetricCollection() - to add a middleware step to the proxy pipeline that can monitor the requests and has access to contextual data such as the route, cluster and destination so it can create metrics that add those as dimensions. - endpoints.MapMetrics() - this adds the /metrics endpoint for prometheus-net that is polled by Prometheus. - Metrics consumer classes - these all follow the same pattern - they implement the respective metrics consumption interface, handle the event with metrics and then write those using Counters, Gauges & Histograms from prometheus-net. This includes: - PrometheusDNSMetrics.cs - PrometheusKestrelMetrics.cs - PrometheusOutboundHttpMetrics.cs - PrometheusProxyMetrics.cs - PrometheusSocketMetrics.cs - PrometheusServiceExtensions.cs - Includes helper extension methods to perform the service registration for the above classes. - **HttpLoadApp** - A simple app that uses HttpClient to create load against the 5 default routes defined by the proxy sample - **run10destinations** - Scripts for Windows & Linux that will start the sample server listening to endpoints http://localhost:10000 to http://localhost:10009 - **prometheus.yml** - A sample config file for Prometheus that includes polling from http://localhost:5000/metrics for the results from the server. ## Running the sample ### ReverseProxy.Metrics.Prometheus.Sample The sample can be started with dotnet run: ```shell dotnet run --project ReverseProxy.Metrics.Prometheus.Sample --framework net5.0 ``` ### Destinations The proxy configuration assumes that there are 10 destination endpoints running on ports 10000 to 10009. This can be done using the SampleServer included in the samples, and specifying the endpoints using the "Urls" command argument. This is encapsulated in [run10destinations.cmd](run10destinations.cmd) and [run10destinations.sh](run10destinations.sh) ### HttpLoadApp To cause the proxy to generate some metrics and with dimensions, a quick and dirty app is included that will create requests against each route. It can be started with dotnet run: ```shell dotnet run --project HttpLoadApp ``` ### Prometheus To run the sample you will need Prometheus running to collect the metrics and present them. It can be downloaded from https://prometheus.io/. Prometheus requires a configuration file to tell it which endpoints to poll for metrics. A sample configuration file, [prometheus.yml](prometheus.yml) is included that assumes the proxy is exposing http://localhost:5000/metrics. To start prometheus, use ```shell prometheus --config.file prometheus.yml ``` ### Viewing Results Assuming that you started each of the above in order, you should now see that the proxy and destination server are responding to requests, and so metrics should be being generated. Open http://localhost:5000/metrics with your browser or curl. That is the endpoint produced by prometheus.net that exposes the metrics in the right format for prometheus to consume. You should note that the name of each metric exposed starts with "yarp_". Open http://localhost:9090/graph in your browser. This is the UI for querying prometheus. In the search expression you can type "yarp_" to get completion of all the metrics that have been created. ![Prometheus Screenshot](graph_screenshot.png) ================================================ FILE: samples/Prometheus/ReverseProxy.Metrics-Prometheus.Sample/Program.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using Prometheus; using Yarp.Sample; var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); builder.Services.AddReverseProxy() .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy")); // Enable metric collection for all the underlying event counters used by YARP builder.Services.AddAllPrometheusMetrics(); var app = builder.Build(); // Add the reverse proxy endpoints based on routes app.MapReverseProxy(); // Add the /Metrics endpoint for prometheus to query on app.MapMetrics(); app.Run(); ================================================ FILE: samples/Prometheus/ReverseProxy.Metrics-Prometheus.Sample/PrometheusDnsMetrics.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Yarp.Telemetry.Consumption; using Prometheus; namespace Yarp.Sample { public sealed class PrometheusDnsMetrics : IMetricsConsumer { private static readonly Counter _dnsLookupsRequested = Metrics.CreateCounter( "yarp_dns_lookups_requested", "Number of DNS lookups requested" ); private static readonly Gauge _averageLookupDuration = Metrics.CreateGauge( "yarp_dns_average_lookup_duration", "Average DNS lookup duration" ); public void OnMetrics(NameResolutionMetrics previous, NameResolutionMetrics current) { _dnsLookupsRequested.IncTo(current.DnsLookupsRequested); _averageLookupDuration.Set(current.AverageLookupDuration.TotalMilliseconds); } } } ================================================ FILE: samples/Prometheus/ReverseProxy.Metrics-Prometheus.Sample/PrometheusForwarderMetrics.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Yarp.Telemetry.Consumption; using Prometheus; namespace Yarp.Sample { public sealed class PrometheusForwarderMetrics : IMetricsConsumer { private static readonly Counter _requestsStarted = Metrics.CreateCounter( "yarp_proxy_requests_started", "Number of requests initiated through the proxy" ); private static readonly Counter _requestsFailed = Metrics.CreateCounter( "yarp_proxy_requests_failed", "Number of proxy requests that have failed" ); private static readonly Gauge _CurrentRequests = Metrics.CreateGauge( "yarp_proxy_current_requests", "Number of active proxy requests that have started but not yet completed or failed" ); public void OnMetrics(ForwarderMetrics previous, ForwarderMetrics current) { _requestsStarted.IncTo(current.RequestsStarted); _requestsFailed.IncTo(current.RequestsFailed); _CurrentRequests.Set(current.CurrentRequests); } } } ================================================ FILE: samples/Prometheus/ReverseProxy.Metrics-Prometheus.Sample/PrometheusKestrelMetrics.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Yarp.Telemetry.Consumption; using Prometheus; namespace Yarp.Sample { public sealed class PrometheusKestrelMetrics : IMetricsConsumer { private static readonly Counter _totalConnections = Metrics.CreateCounter( "yarp_kestrel_total_connections", "Number of incoming connections opened" ); private static readonly Counter _totalTlsHandshakes = Metrics.CreateCounter( "yarp_kestrel_total_tls_Handshakes", "Number of TLS handshakes started" ); private static readonly Gauge _currentTlsHandshakes = Metrics.CreateGauge( "yarp_kestrel_current_tls_handshakes", "Number of active TLS handshakes that have started but not yet completed or failed" ); private static readonly Counter _failedTlsHandshakes = Metrics.CreateCounter( "yarp_kestrel_failed_tls_handshakes", "Number of TLS handshakes that failed" ); private static readonly Gauge _currentConnections = Metrics.CreateGauge( "yarp_kestrel_current_connections", "Number of currently open incoming connections" ); private static readonly Gauge _connectionQueueLength = Metrics.CreateGauge( "yarp_kestrel_connection_queue_length", "Number of connections on the queue." ); private static readonly Gauge _requestQueueLength = Metrics.CreateGauge( "yarp_kestrel_request_queue_length", "Number of requests on the queue" ); public void OnMetrics(KestrelMetrics previous, KestrelMetrics current) { _totalConnections.IncTo(current.TotalConnections); _totalTlsHandshakes.IncTo(current.TotalTlsHandshakes); _currentTlsHandshakes.Set(current.CurrentTlsHandshakes); _failedTlsHandshakes.IncTo(current.FailedTlsHandshakes); _currentConnections.Set(current.CurrentConnections); _connectionQueueLength.Set(current.ConnectionQueueLength); _requestQueueLength.Set(current.RequestQueueLength); } } } ================================================ FILE: samples/Prometheus/ReverseProxy.Metrics-Prometheus.Sample/PrometheusOutboundHttpMetrics.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using Yarp.Telemetry.Consumption; using Prometheus; namespace Yarp.Sample { /// /// Collects outbound http metrics and exposes them using prometheus-net /// public sealed class PrometheusOutboundHttpMetrics : IMetricsConsumer { private static readonly double CUBE_ROOT_10 = Math.Pow(10, (1.0 / 3)); private static readonly Counter _outboundRequestsStarted = Metrics.CreateCounter( "yarp_outbound_http_requests_started", "Number of outbound requests initiated by the proxy" ); private static readonly Counter _outboundRequestsFailed = Metrics.CreateCounter( "yarp_outbound_http_requests_failed", "Number of outbound requests failed" ); private static readonly Gauge _outboundCurrentRequests = Metrics.CreateGauge( "yarp_outbound_http_current_requests", "Number of active outbound requests that have started but not yet completed or failed" ); private static readonly Gauge _outboundCurrentHttp11Connections = Metrics.CreateGauge( "yarp_outbound_http11_connections", "Number of currently open HTTP 1.1 connections" ); private static readonly Gauge _outboundCurrentHttp20Connections = Metrics.CreateGauge( "yarp_outbound_http20_connections", "Number of active proxy requests that have started but not yet completed or failed" ); private static readonly Histogram _outboundHttp11RequestQueueDuration = Metrics.CreateHistogram( "yarp_outbound_http11_request_queue_duration", "Average time spent on queue for HTTP 1.1 requests that hit the MaxConnectionsPerServer limit in the last metrics interval", new HistogramConfiguration { Buckets = Histogram.ExponentialBuckets(10, CUBE_ROOT_10, 10) }); private static readonly Histogram _outboundHttp20RequestQueueDuration = Metrics.CreateHistogram( "yarp_outbound_http20_request_queue_duration", "Average time spent on queue for HTTP 2.0 requests that hit the MAX_CONCURRENT_STREAMS limit on the connection in the last metrics interval", new HistogramConfiguration { Buckets = Histogram.ExponentialBuckets(10, CUBE_ROOT_10, 10) }); public void OnMetrics(HttpMetrics previous, HttpMetrics current) { _outboundRequestsStarted.IncTo(current.RequestsStarted); _outboundRequestsFailed.IncTo(current.RequestsFailed); _outboundCurrentRequests.Set(current.CurrentRequests); _outboundCurrentHttp11Connections.Set(current.CurrentHttp11Connections); _outboundCurrentHttp20Connections.Set(current.CurrentHttp20Connections); _outboundHttp11RequestQueueDuration.Observe(current.Http11RequestsQueueDuration.TotalMilliseconds); _outboundHttp20RequestQueueDuration.Observe(current.Http20RequestsQueueDuration.TotalMilliseconds); } } } ================================================ FILE: samples/Prometheus/ReverseProxy.Metrics-Prometheus.Sample/PrometheusServiceExtensions.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Extensions.DependencyInjection; using Yarp.Telemetry.Consumption; namespace Yarp.Sample { public static class PrometheusServiceExtensions { public static IServiceCollection AddPrometheusForwarderMetrics(this IServiceCollection services) { services.AddTelemetryListeners(); services.AddSingleton, PrometheusForwarderMetrics>(); return services; } public static IServiceCollection AddPrometheusDnsMetrics(this IServiceCollection services) { services.AddTelemetryListeners(); services.AddSingleton, PrometheusDnsMetrics>(); return services; } public static IServiceCollection AddPrometheusKestrelMetrics(this IServiceCollection services) { services.AddTelemetryListeners(); services.AddSingleton, PrometheusKestrelMetrics>(); return services; } public static IServiceCollection AddPrometheusOutboundHttpMetrics(this IServiceCollection services) { services.AddTelemetryListeners(); services.AddSingleton, PrometheusOutboundHttpMetrics>(); return services; } public static IServiceCollection AddPrometheusSocketsMetrics(this IServiceCollection services) { services.AddTelemetryListeners(); services.AddSingleton, PrometheusSocketMetrics>(); return services; } public static IServiceCollection AddAllPrometheusMetrics(this IServiceCollection services) { services.AddPrometheusForwarderMetrics(); services.AddPrometheusDnsMetrics(); services.AddPrometheusKestrelMetrics(); services.AddPrometheusOutboundHttpMetrics(); services.AddPrometheusSocketsMetrics(); return services; } } } ================================================ FILE: samples/Prometheus/ReverseProxy.Metrics-Prometheus.Sample/PrometheusSocketMetrics.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Yarp.Telemetry.Consumption; using Prometheus; namespace Yarp.Sample { public sealed class PrometheusSocketMetrics : IMetricsConsumer { private static readonly Counter _outgoingConnectionsEstablished = Metrics.CreateCounter( "yarp_sockets_outgoing_connections_established", "Number of outgoing (Connect) Socket connections established" ); private static readonly Counter _incomingConnectionsEstablished = Metrics.CreateCounter( "yarp_sockets_incoming_connections_established", "Number of incoming (Accept) Socket connections established" ); private static readonly Counter _bytesReceived = Metrics.CreateCounter( "yarp_sockets_bytes_received", "Number of bytes received" ); private static readonly Counter _bytesSent = Metrics.CreateCounter( "yarp_sockets_bytes_sent", "Number of bytes sent" ); private static readonly Counter _datagramsReceived = Metrics.CreateCounter( "yarp_sockets_datagrams_received", "Number of datagrams received" ); private static readonly Counter _datagramsSent = Metrics.CreateCounter( "yarp_sockets_datagrams_sent", "Number of datagrams Sent" ); public void OnMetrics(SocketsMetrics previous, SocketsMetrics current) { _outgoingConnectionsEstablished.IncTo(current.OutgoingConnectionsEstablished); _incomingConnectionsEstablished.IncTo(current.IncomingConnectionsEstablished); _bytesReceived.IncTo(current.BytesReceived); _bytesSent.IncTo(current.BytesSent); _datagramsReceived.IncTo(current.DatagramsReceived); _datagramsSent.IncTo(current.DatagramsSent); } } } ================================================ FILE: samples/Prometheus/ReverseProxy.Metrics-Prometheus.Sample/Properties/launchSettings.json ================================================ { "iisSettings": { "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { "applicationUrl": "https://localhost:44356/", "sslPort": 44356 } }, "profiles": { "ReverseProxy.Metrics.Prometheus.Sample": { "commandName": "Project", "launchBrowser": false, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } } } } ================================================ FILE: samples/Prometheus/ReverseProxy.Metrics-Prometheus.Sample/ReverseProxy.Metrics.Prometheus.Sample.csproj ================================================ $(ReleaseTFMs) Exe Yarp.Sample latest ================================================ FILE: samples/Prometheus/ReverseProxy.Metrics-Prometheus.Sample/appsettings.Development.json ================================================ { "Logging": { "LogLevel": { "Default": "Debug", "Microsoft": "Debug", "Microsoft.Hosting.Lifetime": "Information" } } } ================================================ FILE: samples/Prometheus/ReverseProxy.Metrics-Prometheus.Sample/appsettings.json ================================================ { "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": { "Mercury": { "ClusterId": "gamma", "Match": { "Path": "/Mercury/{*all}" } }, "Venus": { "ClusterId": "gamma", "Match": { "Path": "/Venus/{*all}" } }, "Earth": { "ClusterId": "delta", "Match": { "Path": "/Earth/{*all}" } }, "Mars": { "ClusterId": "delta", "Match": { "Path": "/Mars/{*all}" } }, "Jupiter": { "ClusterId": "epsilon", "Match": { "Path": "/Jupiter/{*all}" } } }, "Clusters": { "gamma": { "Destinations": { "d0": { "Address": "http://localhost:10000" }, "d2": { "Address": "http://localhost:10002" }, "d4": { "Address": "http://localhost:10004" }, "d6": { "Address": "http://localhost:10006" }, "d8": { "Address": "http://localhost:10008" } } }, "delta": { "Destinations": { "d1": { "Address": "http://localhost:10001" }, "d3": { "Address": "http://localhost:10003" }, "d5": { "Address": "http://localhost:10005" }, "d7": { "Address": "http://localhost:10007" }, "d9": { "Address": "http://localhost:10009" } } }, "epsilon": { "Destinations": { "d0": { "Address": "http://localhost:10000" }, "d1": { "Address": "http://localhost:10001" }, "d2": { "Address": "http://localhost:10002" }, "d3": { "Address": "http://localhost:10003" }, "d4": { "Address": "http://localhost:10004" } } } } } } ================================================ FILE: samples/Prometheus/prometheus.yml ================================================ # my global config global: scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute. evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute. # scrape_timeout is set to the global default (10s). # Alertmanager configuration alerting: alertmanagers: - static_configs: - targets: # - alertmanager:9093 # Load rules once and periodically evaluate them according to the global 'evaluation_interval'. rule_files: # - "first_rules.yml" # - "second_rules.yml" # A scrape configuration containing exactly one endpoint to scrape: # Here it's Prometheus itself. scrape_configs: # The job name is added as a label `job=` to any timeseries scraped from this config. - job_name: 'prometheus' # metrics_path defaults to '/metrics' # scheme defaults to 'http'. static_configs: - targets: ['localhost:9090'] - job_name: 'YARP' static_configs: - targets: ['localhost:5000'] ================================================ FILE: samples/Prometheus/run10destinations.cmd ================================================ dotnet run --project %~dp0/../SampleServer/SampleServer.csproj --Urls "http://localhost:10000;http://localhost:10001;http://localhost:10002;http://localhost:10003;http://localhost:10004;http://localhost:10005;http://localhost:10006;http://localhost:10007;http://localhost:10008;http://localhost:10009" ================================================ FILE: samples/Prometheus/run10destinations.sh ================================================ #!/bin/bash full_path=$(realpath $0) dir_path=$(dirname $full_path) samples=$(dirname $dir_path ) dotnet run --project $samples/SampleServer/SampleServer.csproj --Urls "http://localhost:10000;http://localhost:10001;http://localhost:10002;http://localhost:10003;http://localhost:10004;http://localhost:10005;http://localhost:10006;http://localhost:10007;http://localhost:10008;http://localhost:10009" ================================================ FILE: samples/README.md ================================================ # Samples ## Warning: Breaking Changes The samples in this folder are in sync with the main branch for YARP. If there have been breaking changes to the API or configuration, they may not match what is published on Nuget. To avoid disappointment, if using the samples with the YARP library published to Nuget, please change to the branch to match the latest release or preview, either using the branch dropdown or these links: **[Samples folder in latest release/preview](https://github.com/dotnet/yarp/tree/release/latest/samples)** **[Source zip for latest release, including samples](https://github.com/dotnet/yarp/releases/latest)** ---- The following samples are provided: | Name | Description | | ------- | ----- | | [Basic Yarp Sample](BasicYarpSample) | A simple sample that shows how to add YARP to the empty ASP.NET sample to create a fully functioning reverse proxy. | | [Configuration](ReverseProxy.Config.Sample) | Shows all the options that are available in the YARP config file | | [Minimal](ReverseProxy.Minimal.Sample) | Shows a minimal config-based YARP application using .NET 6's [Minimal Hosting for ASP.NET Core](https://devblogs.microsoft.com/aspnet/asp-net-core-updates-in-net-6-preview-4/#introducing-minimal-apis) | | [Http.sys Delegation](ReverseProxy.HttpSysDelegation.Sample) | Shows an example of using YARP to do Http.sys queue delegation in addition to proxying. | | [Transforms](ReverseProxy.Transforms.Sample) | Shows how to transform headers as part of the proxy operation | | [Code extensibility](ReverseProxy.Code.Sample) | Shows how you can extend YARP using a custom configuration provider, and a middleware component as part of the YARP pipeline | | [Authentication & Authorization](ReverseProxy.Auth.Sample) | Shows how to add authentication and authorization for routes to the proxy | | [Configuration Filter](ReverseProxy.ConfigFilter.Sample) | Shows how to use extensibility to modify configuration as its loaded from the configuration file. This sample implements an indirection to enable config values to be pulled from environment variables which can be useful in a cloud environment. | | [Metrics](ReverseProxy.Metrics.Sample) | Shows how to consume YARP telemetry. This sample collects detailed timings for the sub-operations involved in the proxy process. | | [Using IHttpProxy Directly](ReverseProxy.Direct.Sample) | Shows how to use IHttpProxy, which performs the proxy operation, directly without using YARP's configuration, pipeline etc. | | [Lets Encrypt](ReverseProxy.LetsEncrypt.Sample) | Shows how to use a certificate authority such as Lets Encrypt to set up TLS termination in YARP. | | [Kubernetes Ingress](KubernetesIngress.Sample) | Shows how to use YARP as a Kubernetes ingress controller | | [Prometheus](Prometheus) | Shows how to consume the YARP telemetry library and export metrics to external telemetry such as Prometheus | ================================================ FILE: samples/ReverseProxy.Auth.Sample/Controllers/AccountController.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Security.Claims; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace Yarp.Sample.Controllers { [AllowAnonymous] [Route("[controller]/[action]")] public class AccountController : Controller { [HttpGet] public IActionResult Login(string returnUrl) { ViewData["returnUrl"] = returnUrl; return View(); } // Processes input from the Login.cshtml page to authenticate the user [HttpPost] public IActionResult Login(string name, string myClaimValue, string returnUrl) { // Create a new identity with 2 claims based on the fields in the form var identity = new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, name), new Claim("myCustomClaim", myClaimValue) }, CookieAuthenticationDefaults.AuthenticationScheme); var principal = new ClaimsPrincipal(identity); return SignIn(principal, new AuthenticationProperties() { RedirectUri = returnUrl // SignIn is the only one that requires a scheme: https://github.com/dotnet/aspnetcore/issues/23325 }, CookieAuthenticationDefaults.AuthenticationScheme); } [HttpPost] public IActionResult Logout() { return SignOut(new AuthenticationProperties() { RedirectUri = "/Account/LoggedOut", }); } [HttpGet] public IActionResult LoggedOut() { return View(); } [HttpGet] public IActionResult AccessDenied() { return View(); } } } ================================================ FILE: samples/ReverseProxy.Auth.Sample/Program.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Net.Http.Headers; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using Yarp.ReverseProxy.Transforms; using Yarp.Sample; var builder = WebApplication.CreateBuilder(args); var services = builder.Services; // Required to supply the authentication UI in Views/* services.AddRazorPages(); services.AddSingleton(); services.AddReverseProxy() .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy")) .AddTransforms(transformBuilderContext => // Add transforms inline { // 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. // Only do this for routes that require auth. if (string.Equals("myPolicy", transformBuilderContext.Route.AuthorizationPolicy)) { transformBuilderContext.AddRequestTransform(async transformContext => { // AuthN and AuthZ will have already been completed after request routing. var ticket = await transformContext.HttpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme); var tokenService = transformContext.HttpContext.RequestServices.GetRequiredService(); var token = await tokenService.GetAuthTokenAsync(ticket.Principal); // Reject invalid requests if (string.IsNullOrEmpty(token)) { var response = transformContext.HttpContext.Response; response.StatusCode = 401; return; } transformContext.ProxyRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); }); } }); ; services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie(); services.AddAuthorization(options => { // Creates a policy called "myPolicy" that depends on having a claim "myCustomClaim" with the value "green". // See AccountController.Login method for where this claim is applied to the user identity // This policy can then be used by routes in the proxy, see "ClaimsAuthRoute" in appsettings.json options.AddPolicy("myPolicy", builder => builder .RequireClaim("myCustomClaim", "green") .RequireAuthenticatedUser()); // The default policy is to require authentication, but no additional claims // Uncommenting the following would have no effect // options.DefaultPolicy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build(); // FallbackPolicy is used for routes that do not specify a policy in config // Make all routes that do not specify a policy to be anonymous (this is the default). options.FallbackPolicy = null; // Or make all routes that do not specify a policy require some auth: // options.FallbackPolicy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build(); }); var app = builder.Build(); app.UseStaticFiles(); app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); app.MapReverseProxy(); app.Run(); ================================================ FILE: samples/ReverseProxy.Auth.Sample/Properties/launchSettings.json ================================================ { "iisSettings": { "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { "applicationUrl": "https://localhost:44356/", "sslPort": 44356 } }, "profiles": { "ReverseProxy.Auth.Sample": { "commandName": "Project", "launchBrowser": false, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } } } } ================================================ FILE: samples/ReverseProxy.Auth.Sample/README.md ================================================ # Authentication & Authorization sample This sample shows how the YARP proxy can be integrated with the ASP.NET [authentication](https://docs.microsoft.com/aspnet/core/security/authentication) and [authorization](https://docs.microsoft.com/aspnet/core/security/authorization/introduction) system to specify claims requirements on routes that will be enforced by the proxy before it will forward applicable requests. The sample includes the following parts: - **[Program.cs](Program.cs)** Sets up the ASP.NET server to have the proxy together with the other middleware for authentication, authorization and Razor pages. It sets up a custom authorization policy "myPolicy" with a custom claim. - **[AccountController.cs](Controllers/AccountController.cs)** Handles the login UI actions, and adds a value from a field in the login page to the "myCustomClaim" claim in the active identity. That claim is later required by the "myPolicy" authorization policy created in Startup.cs - **[appsettings.json](appsettings.json)** Defines the routes used by the reverse proxy including: - /default - requires authentication to access - /custom - uses the "myPolicy" authorization policy which requires authentication and a myCustomClaim value of "green" - /open - which uses "Anonymous" as the authorization policy so its always open regardless of the default - \* - which uses the built-in FallbackPolicy which is configured in Startup.cs to not require authentication - **Login UI** The Razor pages in [Views/Account](Views/Account) provide the pages to login, logout and be shown when access is denied. ## Usage Start the sample with ```dotnet run``` which by default will bind to http://localhost:5000 and https://localhost:5001. Try accessing the urls "/", "/default" and "/custom". When shown the login ui, pick a value for myCustomClaim. Using "green" will allow access to content under "/custom", using other values will deny access. ================================================ FILE: samples/ReverseProxy.Auth.Sample/ReverseProxy.Auth.Sample.csproj ================================================ $(ReleaseTFMs) Exe Yarp.Sample latest ================================================ FILE: samples/ReverseProxy.Auth.Sample/TokenService.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Security.Claims; using System.Threading.Tasks; namespace Yarp.Sample { internal sealed class TokenService { internal Task GetAuthTokenAsync(ClaimsPrincipal user) { // we only have tokens for bob if (string.Equals("Bob", user.Identity.Name)) { return Task.FromResult(Guid.NewGuid().ToString()); } return Task.FromResult(null); } } } ================================================ FILE: samples/ReverseProxy.Auth.Sample/Views/Account/AccessDenied.cshtml ================================================ @{ ViewData["Title"] = "Access Denied"; }

@ViewData["Title"]

You do not have access to this route.

================================================ FILE: samples/ReverseProxy.Auth.Sample/Views/Account/LoggedOut.cshtml ================================================ @{ ViewData["Title"] = "Logged Out"; } You have been logged out. ================================================ FILE: samples/ReverseProxy.Auth.Sample/Views/Account/Login.cshtml ================================================ @{ ViewData["Title"] = "Login"; }

Login




Note:The authorization policy will check for the value of "green", other values should pass authentication, but not authorize for specific routes
================================================ FILE: samples/ReverseProxy.Auth.Sample/appsettings.Development.json ================================================ { "Logging": { "LogLevel": { "Default": "Debug", "Microsoft": "Debug", "Microsoft.Hosting.Lifetime": "Information" } } } ================================================ FILE: samples/ReverseProxy.Auth.Sample/appsettings.json ================================================ { "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Information", "Microsoft.Hosting.Lifetime": "Information" } }, "AllowedHosts": "*", "Kestrel": { "Endpoints": { "https": { "Url": "https://localhost:5001" }, "http": { "Url": "http://localhost:5000" } } }, "ReverseProxy": { "Clusters": { "cluster1": { "Destinations": { "cluster1/destination1": { "Address": "https://example.com/" } } } }, "Routes": { "DefaultAuthRoute": { "ClusterId": "cluster1", // This route uses the built-in default authorization policy which is to require authenticated users "AuthorizationPolicy": "Default", "Match": { "Path": "/default" } }, "ClaimsAuthRoute": { "ClusterId": "cluster1", // This route requires the "myPolicy" authorization policy which is defined in Startup.cs "AuthorizationPolicy": "myPolicy", "Match": { "Path": "/custom/{*any}" } }, "AnonymousRoute": { "ClusterId": "cluster1", // This route uses the explicit name Anonymous to not require authentication "AuthorizationPolicy": "Anonymous", "Match": { "Path": "/open/{*any}" } }, "Other": { // As the following route does not define an authorization policy, it uses the fallback policy // which is set in Startup.cs to be null, and so not require authentication or claims. "ClusterId": "cluster1", "Match": { "Path": "{**catchall}" } } } } } ================================================ FILE: samples/ReverseProxy.Code.Sample/Program.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using Yarp.ReverseProxy.Configuration; using Microsoft.AspNetCore.Http; using Yarp.ReverseProxy.Model; const string DEBUG_HEADER = "Debug"; const string DEBUG_METADATA_KEY = "debug"; const string DEBUG_VALUE = "true"; var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); builder.Services.AddReverseProxy() .LoadFromMemory(GetRoutes(), GetClusters()); var app = builder.Build(); app.Map("/update", context => { context.RequestServices.GetRequiredService().Update(GetRoutes(), GetClusters()); return Task.CompletedTask; }); // We can customize the proxy pipeline and add/remove/replace steps app.MapReverseProxy(proxyPipeline => { // Use a custom proxy middleware, defined below proxyPipeline.Use(MyCustomProxyStep); // Don't forget to include these two middleware when you make a custom proxy pipeline (if you need them). proxyPipeline.UseSessionAffinity(); proxyPipeline.UseLoadBalancing(); }); app.Run(); RouteConfig[] GetRoutes() { return [ new RouteConfig() { RouteId = "route" + Random.Shared.Next(), // Forces a new route id each time GetRoutes is called. ClusterId = "cluster1", Match = new RouteMatch { // Path or Hosts are required for each route. This catch-all pattern matches all request paths. Path = "{**catch-all}" } } ]; } ClusterConfig[] GetClusters() { var debugMetadata = new Dictionary { { DEBUG_METADATA_KEY, DEBUG_VALUE } }; return [ new ClusterConfig() { ClusterId = "cluster1", SessionAffinity = new SessionAffinityConfig { Enabled = true, Policy = "Cookie", AffinityKeyName = ".Yarp.ReverseProxy.Affinity" }, Destinations = new Dictionary(StringComparer.OrdinalIgnoreCase) { { "destination1", new DestinationConfig() { Address = "https://example.com" } }, { "debugdestination1", new DestinationConfig() { Address = "https://bing.com", Metadata = debugMetadata } }, } } ]; } /// /// Custom proxy step that filters destinations based on a header in the inbound request /// Looks at each destination metadata, and filters in/out based on their debug flag and the inbound header /// Task MyCustomProxyStep(HttpContext context, Func next) { // Can read data from the request via the context var useDebugDestinations = context.Request.Headers.TryGetValue(DEBUG_HEADER, out var headerValues) && headerValues.Count == 1 && headerValues[0] == DEBUG_VALUE; // The context also stores a ReverseProxyFeature which holds proxy specific data such as the cluster, route and destinations var availableDestinationsFeature = context.Features.Get(); var filteredDestinations = new List(); // Filter destinations based on criteria foreach (var d in availableDestinationsFeature.AvailableDestinations) { //Todo: Replace with a lookup of metadata - but not currently exposed correctly here if (d.DestinationId.Contains("debug") == useDebugDestinations) { filteredDestinations.Add(d); } } availableDestinationsFeature.AvailableDestinations = filteredDestinations; // Important - required to move to the next step in the proxy pipeline return next(); } ================================================ FILE: samples/ReverseProxy.Code.Sample/Properties/launchSettings.json ================================================ { "iisSettings": { "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { "applicationUrl": "https://localhost:44356/", "sslPort": 44356 } }, "profiles": { "ReverseProxy.Code.Sample": { "commandName": "Project", "launchBrowser": false, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } } } } ================================================ FILE: samples/ReverseProxy.Code.Sample/README.md ================================================ # YARP Code Extensibility Sample This sample shows two common customizations via code of the YARP reverse proxy: - ## Dynamic configuration from code YARP supports pulling configuration from a config file, but in many scenarios the configuration of the routes to use, and which destinations the requests should be sent to need to be programmatically fetched from another source. The extensibility of YARP makes it easy for you to fetch that data from where you need to, and then pass to the proxy as lists of objects. This sample shows the routes and destinations being created in code, and then passed to an in-memory provider. The role of the in-memory provider is to give change notifications to YARP for when the config has been changed and needs to be updated. YARP uses a snapshot model for its configuration, so that changes are applied as an atomic action, that will apply to subsequent requests after the change is applied. Existing requests that are already being processed will be completed using the configuration snapshot from the time that they were received. The ```IProxyConfig``` interface implemented in InMemoryConfigProvider includes a change token which is used to signal when a batch of changes to the configuration is complete, and the proxy should take a snapshot and update its internal configuration. Part of the snapshot processing is to create an optimized route table in ASP.NET, which can be a CPU intensive operation, for that reason we don't recommend signaling for updates more than once per 15 seconds. - ## Custom pipeline step YARP uses a pipeline model for the stages involved in processing each request: - Mapping the request path to a route and cluster of destinations - Pre-assigning servers based on existing session affinity headers in the request - Filtering the destination list for servers that are not healthy - Load balancing between the remaining servers based on load etc - Storing session affinity if applicable - Transforming headers if required - Proxying the request/response to/from the destination server You can insert additional custom stages into the pipeline, or replace built-in steps with your own implementations. This sample adds an additional stage that will filter the destinations from a cluster based on a "debug" metadata attribute being included in the config data based. If a custom header "Debug:true" is present in the request, then destinations with the debug metadata will be retained, and others will be filtered out, or vice-versa. ## Key Files The following files are key to implementing the features described above: - ### [Program.cs](Program.cs) Provides the initialization routines for ASP.NET and the reverse proxy. It: - sets up the proxy passing in the InMemoryConfigProvider instance. The sample routes and clusters definitions are created as part of this initialization. The config provider instance is used for the lifetime of the proxy. - sets up the request pipeline. As an additional step is added, the proxy pipeline is configured here. - ```MyCustomProxyStep``` is the implementation of the additional step. It finds the proxy functionality via features added to the HttpContext, and then filters the destinations based on the presence of a "Debug" header in the request. ================================================ FILE: samples/ReverseProxy.Code.Sample/ReverseProxy.Code.Sample.csproj ================================================ $(ReleaseTFMs) Exe Yarp.Sample latest ================================================ FILE: samples/ReverseProxy.Code.Sample/appsettings.Development.json ================================================ { "Logging": { "LogLevel": { "Default": "Debug", "Microsoft": "Debug", "Microsoft.Hosting.Lifetime": "Information" } } } ================================================ FILE: samples/ReverseProxy.Code.Sample/appsettings.json ================================================ { "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } }, "AllowedHosts": "*" } ================================================ FILE: samples/ReverseProxy.Config.Sample/Program.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); builder.Services.AddReverseProxy() .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy")); var app = builder.Build(); app.UseCors(); app.MapReverseProxy(); app.Run(); ================================================ FILE: samples/ReverseProxy.Config.Sample/Properties/launchSettings.json ================================================ { "iisSettings": { "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { "applicationUrl": "https://localhost:44356/", "sslPort": 44356 } }, "profiles": { "ReverseProxy.Config.Sample": { "commandName": "Project", "launchBrowser": false, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } } } } ================================================ FILE: samples/ReverseProxy.Config.Sample/README.md ================================================ # Configuration Sample This sample shows off the properties that can be supplied to YARP via configuration. In this case its using appsettings.json, but the same configuration properties could be supplied through code instead. See [ReverseProxy.Code.Sample](../ReverseProxy.Code.Sample) for an example The [configuration file](appsettings.json) includes all the settings that are currently supported by YARP. The configuration shows two routes and two clusters: - minimalRoute which will map to any URL - Routes to cluster "minimalCluster" which has one destination "www.example.com" - allRouteProps route - Which includes further restrictions: - Path must be /download/* - Host must be localhost, www.aaaaa.com or www.bbbbb.com - Http Method must be GET or POST - Must have a header "MyCustomHeader" with a value of "value1", "value2" or "another value" - A "MyHeader" header will be added with the value "MyValue" - Must have a query parameter "MyQueryParameter" with a value of "value1", "value2" or "another value" - This will route to cluster "allClusterProps" which has 2 destinations - https://dotnet.microsoft.com and https://10.20.30.40 - Requests will be [load balanced](https://learn.microsoft.com/aspnet/core/fundamentals/servers/yarp/load-balancing) between destinations using a "PowerOfTwoChoices" algorithm, which picks two destinations at random, then uses the least loaded of the two. - It includes [session affinity](https://learn.microsoft.com/aspnet/core/fundamentals/servers/yarp/session-affinity) using a cookie which will ensure subsequent requests from the same client go to the same host. - It is configured to have both active and passive [health checks](https://learn.microsoft.com/aspnet/core/fundamentals/servers/yarp/dests-health-checks) - note the second destination will timeout for active checks (unless you have a host with that IP on your network) - It includes [HttpClient configuration](https://learn.microsoft.com/aspnet/core/fundamentals/servers/yarp/http-client-config) setting outbound connection properties - HttpRequest properties defaulting to HTTP/2 with a 2min timeout The other files in the sample are the same as the getting started instructions. To make a request that would be successful against the second route, you will need a client request similar to: ```bash curl -v -k -X GET -H "MyCustomHeader: value1" https://localhost:5001/download?MyQueryParameter=value1 ``` ================================================ FILE: samples/ReverseProxy.Config.Sample/ReverseProxy.Config.Sample.csproj ================================================ $(ReleaseTFMs) Exe Yarp.Sample latest ================================================ FILE: samples/ReverseProxy.Config.Sample/appsettings.Development.json ================================================ { "Logging": { "LogLevel": { "Default": "Debug", "Microsoft": "Debug", "Microsoft.Hosting.Lifetime": "Information" } } } ================================================ FILE: samples/ReverseProxy.Config.Sample/appsettings.json ================================================ { // Base URLs the server listens on, must be configured independently of the routes below. // Can also be configured via Kestrel/Endpoints, see https://docs.microsoft.com/aspnet/core/fundamentals/servers/kestrel/endpoints "Urls": "http://localhost:5000;https://localhost:5001", //Sets the Logging level for ASP.NET "Logging": { "LogLevel": { "Default": "Information", // Uncomment to hide diagnostic messages from runtime and proxy // "Microsoft": "Warning", // "Yarp" : "Warning", "Microsoft.Hosting.Lifetime": "Information" } }, "AllowedHosts": "*", // Configuration for YARP "ReverseProxy": { // Routes tell the proxy which requests to forward "Routes": { "minimalRoute": { // Matches anything and routes it to www.example.com "ClusterId": "minimalCluster", "Match": { "Path": "{**catch-all}" } }, "allRouteProps": { // matches /download/* and routes to "allClusterProps" "ClusterId": "allClusterProps", // Name of one of the clusters "Order": 0, // Lower numbers have higher precedence, default is 0 "AuthorizationPolicy": "Anonymous", // Name of the policy or "Default", "Anonymous" "CorsPolicy": "disable", // Name of the CorsPolicy to apply to this route or "default", "disable" "Match": { // Rules that have to be met for the route to match the request "Path": "/download/{**remainder}", // The path to match using ASP.NET syntax. "Hosts": [ "localhost", "www.aaaaa.com", "www.bbbbb.com" ], // The host names to match, unspecified is any "Methods": [ "GET", "PUT" ], // The HTTP methods that match, unspecified is all "Headers": [ // The headers to match, unspecified is any { "Name": "MyCustomHeader", // Name of the header "Values": [ "value1", "value2", "another value" ], // Matches are against any of these values "Mode": "ExactHeader", // or "HeaderPrefix", "Exists" , "Contains", "NotContains" "IsCaseSensitive": true } ], "QueryParameters": [ // The query parameters to match, unspecified is any { "Name": "MyQueryParameter", // Name of the query parameter "Values": [ "value1", "value2", "another value" ], // Matches are against any of these values "Mode": "Exact", // or "Prefix", "Exists" , "Contains", "NotContains" "IsCaseSensitive": true } ] }, "Metadata": { // List of key value pairs that can be used by custom extensions "MyName": "MyValue" }, "Transforms": [ // List of transforms. See ReverseProxy.Transforms.Sample for more details { "RequestHeader": "MyHeader", "Set": "MyValue" } ] } }, // Clusters tell the proxy where and how to forward requests "Clusters": { // Cluster with the minimum information "minimalCluster": { "Destinations": { // Specifies which back end servers requests should be routed to. "example.com": { // name is used for logging and via extensibility "Address": "http://www.example.com" // Should specify Protocol, Address/IP & Port, but not path } } }, "allClusterProps": { // Cluster with all properties "Destinations": { // Specifies which back end servers requests should be routed to. "first_destination": { // name is used for logging and via extensibility "Address": "https://dotnet.microsoft.com" // Should specify Protocol, Address/IP & Port, but not path }, "another_destination": { "Address": "https://10.20.30.40", "Health": "https://10.20.30.40:12345", // override for active health checks "Host": "contoso", "Metadata": { "SomeKey": "SomeValue" } } }, "LoadBalancingPolicy": "PowerOfTwoChoices", // Alternatively "First", "Random", "RoundRobin", "LeastRequests" "SessionAffinity": { // Ensures subsequent requests from a client go to the same destination server "Enabled": true, // Defaults to 'false' "Policy": "HashCookie", // Default, alternatively "Cookie" or "CustomHeader" "FailurePolicy": "Redistribute", // default, alternatively "Return503Error" "AffinityKeyName": "MySessionCookieName", // Required, no default "Cookie": { // Options for cookie based session affinity "Path": "/", "SameSite": "None", "HttpOnly": true, "Expiration": "00:30:00", "Domain": "example.com", "MaxAge": "08:00:00", "SecurePolicy": "Always", "IsEssential": true } }, "HealthCheck": { // Ways to determine which destinations should be filtered out due to unhealthy state "Active": { // Makes API calls to validate the health of each destination "Enabled": true, "Interval": "00:00:10", // How often to query for health data "Timeout": "00:00:10", // Timeout for the health check request/response "Policy": "ConsecutiveFailures", // Or other custom policy that has been registered "Path": "/favicon.ico", // API endpoint to query for health state. Looks for 2XX response codes to indicate healthy state // Typically something like "/api/health" but used favicon to enable sample to run "Query": "?healthCheck=true" // Query string to append to the health check request }, "Passive": { // Disables destinations based on HTTP response codes for proxy requests "Enabled": true, // Defaults to false "Policy": "TransportFailureRate", // Or other custom policy that has been registered "ReactivationPeriod": "00:00:10" // how long before the destination is re-enabled }, "AvailableDestinationsPolicy": "HealthyOrPanic" // Policy for which destinations can be used when sending requests }, "HttpClient": { // Configuration of HttpClient instance used to contact destinations "SslProtocols": [ "Tls13" ], "DangerousAcceptAnyServerCertificate": true, // Disables destination cert validation "MaxConnectionsPerServer": 1024, // Destination server can further limit this number "EnableMultipleHttp2Connections": true, "RequestHeaderEncoding": "Latin1", // How to interpret non ASCII characters in proxied request's header values "ResponseHeaderEncoding": "Latin1", // How to interpret non ASCII characters in proxied request's response header values "WebProxy": { // Optional proxy configuration for outgoing requests "Address": "http://127.0.0.1", "BypassOnLocal": true, "UseDefaultCredentials": false } }, "HttpRequest": { // Options for sending request to destination "ActivityTimeout": "00:02:00", // Activity timeout for the request "Version": "2", // Http Version that should be tried first "VersionPolicy": "RequestVersionOrLower", // Policy for which other versions can be be used "AllowResponseBuffering": false }, "Metadata": { // Custom Key/value pairs for extensibility "TransportFailureRateHealthPolicy.RateLimit": "0.5", // Used by Passive health policy "MyKey": "MyValue" } } } } } ================================================ FILE: samples/ReverseProxy.ConfigFilter.Sample/CustomConfigFilter.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Yarp.ReverseProxy.Configuration; namespace Yarp.Sample { public class CustomConfigFilter : IProxyConfigFilter { // Matches {{env_var_name}} private readonly Regex _exp = new("\\{\\{(\\w+)\\}\\}"); // Configuration filter for clusters, will be passed each cluster in turn, which it should either return as-is or // clone and create a new version of with updated changes // // This sample looks at the destination addresses and any of the form {{key}} will be modified, looking up the key // as an environment variable. This is useful when hosted in Azure etc, as it enables a simple way to replace // destination addresses via the management console public ValueTask ConfigureClusterAsync(ClusterConfig origCluster, CancellationToken cancel) { // Each cluster has a dictionary of destinations, which is read-only, so we'll create a new one with our updates var newDests = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var d in origCluster.Destinations) { var origAddress = d.Value.Address; if (_exp.IsMatch(origAddress)) { // Get the name of the env variable from the destination and lookup value var lookup = _exp.Matches(origAddress)[0].Groups[1].Value; var newAddress = System.Environment.GetEnvironmentVariable(lookup); if (string.IsNullOrWhiteSpace(newAddress)) { throw new System.ArgumentException($"Configuration Filter Error: Substitution for '{lookup}' in cluster '{d.Key}' not found as an environment variable."); } // using c# 9 "with" to clone and initialize a new record var modifiedDest = d.Value with { Address = newAddress }; newDests.Add(d.Key, modifiedDest); } else { newDests.Add(d.Key, d.Value); } } return new ValueTask(origCluster with { Destinations = newDests }); } public ValueTask ConfigureRouteAsync(RouteConfig route, ClusterConfig cluster, CancellationToken cancel) { // 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) { return new ValueTask(route with { Order = 1 }); } return new ValueTask(route); } } } ================================================ FILE: samples/ReverseProxy.ConfigFilter.Sample/Program.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using Yarp.Sample; var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); builder.Services.AddReverseProxy() .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy")) .AddConfigFilter(); var app = builder.Build(); app.MapReverseProxy(); app.Run(); ================================================ FILE: samples/ReverseProxy.ConfigFilter.Sample/Properties/launchSettings.json ================================================ { "iisSettings": { "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { "applicationUrl": "https://localhost:44356/", "sslPort": 44356 } }, "profiles": { "ReverseProxy.ConfigFilter.Sample": { "commandName": "Project", "environmentVariables": { "Key": "Value", "contoso": "https://contoso.com", "ASPNETCORE_ENVIRONMENT": "Development" } }, "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } } } } ================================================ FILE: samples/ReverseProxy.ConfigFilter.Sample/README.md ================================================ # Configuration Filter Sample This sample shows an example of a configuration filter. A configuration filter enables a callback as part of the configuration load where custom code can modify the configuration values for the proxy as they are loaded. This is valuable when the configuration file provides most of what you need, but you want to be able to tweak some values, but don't want to have to write a custom config provider. ## IProxyConfigFilter The bulk of the code is the CustomConfigFilter class which implements the IProxyConfigFilter interface. The interface has two methods which act as callbacks when Clusters and Routes are loaded from config. The methods will be called for each Route and Cluster, and as both are defined as Records, they are immutable so the method should return the same object as-is or a replacement. ## CustomConfigFilter Class ### ConfigureClusterAsync This looks at the value of each destination and sees whether it matches the pattern {{env_var_name}}, and if so it treats it as an indirection to an environment variable, and replaces the destination address with the value of the named variable (if it exists). **Note:** AppSettings.json includes a destination of {{contoso}} which will be matched. The Properties/launchSettings.json file includes a definition of the environment variable, which will be used by Visual Studio and other tools when debugging with "F5". ================================================ FILE: samples/ReverseProxy.ConfigFilter.Sample/ReverseProxy.ConfigFilter.Sample.csproj ================================================ $(ReleaseTFMs) Exe Yarp.Sample latest ================================================ FILE: samples/ReverseProxy.ConfigFilter.Sample/appsettings.Development.json ================================================ { "Logging": { "LogLevel": { "Default": "Debug", "Microsoft": "Debug", "Microsoft.Hosting.Lifetime": "Information" } } } ================================================ FILE: samples/ReverseProxy.ConfigFilter.Sample/appsettings.json ================================================ { "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": { "route1": { "ClusterId": "cluster1", "Match": { "Methods": [ "GET", "POST" ], "Hosts": [ "localhost" ], "Path": "/api/{**catch-all}" } }, "route2": { "ClusterId": "cluster2", "Match": { "Path": "{**catch-all}" } } }, "Clusters": { "cluster1": { "Destinations": { "cluster1/destination1": { // Following value will be found by regex and looked up as an environment variable "Address": "{{contoso}}" }, "cluster1/destination2": { "Address": "https://bing.com/" } } }, "cluster2": { "Destinations": { "cluster2/destination1": { "Address": "https://example.com/" } } } } } } ================================================ FILE: samples/ReverseProxy.Direct.Sample/Program.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Diagnostics; using System.Net; using System.Net.Http; using System.Threading.Tasks; using System.Threading; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Yarp.ReverseProxy.Forwarder; using Yarp.ReverseProxy.Transforms; var builder = WebApplication.CreateBuilder(args); builder.Services.AddHttpForwarder(); var app = builder.Build(); // Configure our own HttpMessageInvoker for outbound calls for proxy operations var httpClient = new HttpMessageInvoker(new SocketsHttpHandler { UseProxy = false, AllowAutoRedirect = false, AutomaticDecompression = DecompressionMethods.None, UseCookies = false, EnableMultipleHttp2Connections = true, ActivityHeadersPropagator = new ReverseProxyPropagator(DistributedContextPropagator.Current), ConnectTimeout = TimeSpan.FromSeconds(15), }); // Setup our own request transform class var transformer = new CustomTransformer(); // or HttpTransformer.Default; var requestOptions = new ForwarderRequestConfig { ActivityTimeout = TimeSpan.FromSeconds(100) }; app.UseRouting(); // When using IHttpForwarder for direct forwarding you are responsible for routing, destination discovery, load balancing, affinity, etc.. // For an alternate example that includes those features see BasicYarpSample. app.Map("/test/{**catch-all}", async (HttpContext httpContext, IHttpForwarder forwarder) => { var error = await forwarder.SendAsync(httpContext, "https://example.com", httpClient, requestOptions, static (context, proxyRequest) => { // Customize the query string: var queryContext = new QueryTransformContext(context.Request); queryContext.Collection.Remove("param1"); queryContext.Collection["area"] = "xx2"; // Assign the custom uri. Be careful about extra slashes when concatenating here. RequestUtilities.MakeDestinationAddress is a safe default. proxyRequest.RequestUri = RequestUtilities.MakeDestinationAddress("https://example.com", context.Request.Path, queryContext.QueryString); // Suppress the original request header, use the one from the destination Uri. proxyRequest.Headers.Host = null; return default; }); // Check if the proxy operation was successful if (error != ForwarderError.None) { var errorFeature = httpContext.Features.Get(); var exception = errorFeature.Exception; } }); app.MapForwarder("/sample/{id}", "https://httpbin.org", "/anything/{id}"); app.MapForwarder("/sample/anything/{id}", "https://httpbin.org", b => b.AddPathRemovePrefix("/sample")); // When using extension methods for registering IHttpForwarder providing configuration, transforms, and HttpMessageInvoker is optional (defaults will be used). app.MapForwarder("/{**catch-all}", "https://example.com", requestOptions, transformer, httpClient); app.Run(); /// /// Custom request transformation /// internal sealed class CustomTransformer : HttpTransformer { /// /// A callback that is invoked prior to sending the proxied request. All HttpRequestMessage /// fields are initialized except RequestUri, which will be initialized after the /// callback if no value is provided. The string parameter represents the destination /// URI prefix that should be used when constructing the RequestUri. The headers /// are copied by the base implementation, excluding some protocol headers like HTTP/2 /// pseudo headers (":authority"). /// /// The incoming request. /// The outgoing proxy request. /// The uri prefix for the selected destination server which can be used to create /// the RequestUri. public override async ValueTask TransformRequestAsync(HttpContext httpContext, HttpRequestMessage proxyRequest, string destinationPrefix, CancellationToken cancellationToken) { // Copy all request headers await base.TransformRequestAsync(httpContext, proxyRequest, destinationPrefix, cancellationToken); // 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. RequestUtilities.MakeDestinationAddress is a safe default. proxyRequest.RequestUri = RequestUtilities.MakeDestinationAddress("https://example.com", httpContext.Request.Path, queryContext.QueryString); // Suppress the original request header, use the one from the destination Uri. proxyRequest.Headers.Host = null; } } ================================================ FILE: samples/ReverseProxy.Direct.Sample/Properties/launchSettings.json ================================================ { "iisSettings": { "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { "applicationUrl": "https://localhost:44356/", "sslPort": 44356 } }, "profiles": { "ReverseProxy.Direct.Sample": { "commandName": "Project", "launchBrowser": false, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } } } } ================================================ FILE: samples/ReverseProxy.Direct.Sample/README.md ================================================ # YARP Direct Proxy Example Some customers who have an existing custom proxy for HTTP/1.1 are looking at YARP for a solution to handle more complex requests, such as HTTP/2, gRPC, WebSockets in future QUIC and HTTP/3. These applications have their own means of routing, load balancing, affinity, etc. and only need to forward a specific request to a specific destination. To make it easier to integrate YARP into these scenarios, the component that proxies requests is exposed via IHttpForwarder which can be called directly, and has few dependencies on the rest of YARP's infrastructure. This example shows how to use IHttpForwarder to proxy a request to/from a specified destination. The operation of the proxy can be thought of as: ```text +-------------------+ +-------------------+ +-------------------+ | Client | ──(a)──► | Proxy | ──(b)──► | Destination | | | ◄──(d)── | | ◄──(c)── | | +-------------------+ +-------------------+ +-------------------+ ``` (a) and (b) show the *request* path, going from the client to the destination. (c) and (d) show the *response* path, going from the destination back to the client. Normal proxying comprises the following steps: | \# | Step | Direction | | -- | ---- | --------- | | 1 | Disable ASP .NET Core limits for streaming requests | | | 2 | Create outgoing HttpRequestMessage | | | 3 | Setup copy of request body (background) | Client --► Proxy --► Destination | | 4 | Copy request headers | Client --► Proxy --► Destination | | 5 | Send the outgoing request using HttpMessageInvoker | Client --► Proxy --► Destination | | 6 | Copy response status line | Client ◄-- Proxy ◄-- Destination | | 7 | Copy response headers | Client ◄-- Proxy ◄-- Destination | | 8.1 | Check for a 101 upgrade response, this takes care of WebSockets as well as any other upgradeable protocol. | | | 8.1.1 | Upgrade client channel | Client ◄--- Proxy ◄--- Destination | | 8.1.2 | Copy duplex streams and return | Client ◄--► Proxy ◄--► Destination | | 8.2 | Copy (normal) response body | Client ◄-- Proxy ◄-- Destination | | 9 | Copy response trailer headers and finish response | Client ◄-- Proxy ◄-- Destination | | 10 | Wait for completion of step 2: copying request body | Client --► Proxy --► Destination | To enable control over mapping request and response fields and headers between the client and destination (steps 4 and 7 above), the HttpForwarder.ProxyAsync method takes a HttpTransformer. Your implementation can modify the request url, method, protocol version, response status code, or decide which headers are copied, modify them, or insert additional headers as required. **Note:** When using the HttpForwarder class directly there are some [header transforms](https://learn.microsoft.com/aspnet/core/fundamentals/servers/yarp/transforms) included by default, such as adding ```X-Forwarded-For``` and removing the original Host header. This is the same set of transforms included by default in the YARP pipeline model (see BasicYarpSample). ## Files The key functionality for this sample is all included in [Program.cs](Program.cs). ================================================ FILE: samples/ReverseProxy.Direct.Sample/ReverseProxy.Direct.Sample.csproj ================================================ $(ReleaseTFMs) Exe Yarp.Sample latest ================================================ FILE: samples/ReverseProxy.Direct.Sample/appsettings.Development.json ================================================ { "Logging": { "LogLevel": { "Default": "Debug", "Microsoft": "Debug", "Microsoft.Hosting.Lifetime": "Information" } } } ================================================ FILE: samples/ReverseProxy.Direct.Sample/appsettings.json ================================================ { "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } }, "AllowedHosts": "*", "Kestrel": { "Endpoints": { "https": { "Url": "https://localhost:5001" }, "http": { "Url": "http://localhost:5000" } } } } ================================================ FILE: samples/ReverseProxy.HttpSysDelegation.Sample/README.md ================================================ # Http.sys Delegation Sample This sample shows how to use YARP to delegate requests to other Http.sys request queues instead of or in addition to proxying requests. Using Http.sys delegation requires hosting YARP on [ASP.NET Core's Http.sys server](https://docs.microsoft.com/aspnet/core/fundamentals/servers/httpsys) and requests can only be delegated to other processes which use Http.sys for request processing (e.g. ASP.NET Core using Http.sys server or IIS). **Note:** delegation only works for ASP.NET Core 6+ running on new versions of Windows ## Sample Projects There are two projects as part of this sample. A sample Http.sys server where traffic will be delegated to and a YARP example which both proxies and delegates request depending on the route. Both projects use the minimal API style but this isn't a requirement. ### ReverseProxy Delegation There are four parts to enable YARP delegation support: - Use the ASP.NET Core Http.sys server ```c# builder.WebHost.UseHttpSys(); ``` - Add YARP services ```c# builder.Services.AddReverseProxy() .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy")); ``` - Add YARP to the request pipeline. You need to use the overload that allows you to define the middleware used in the YARP pipeline. ```c# app.MapReverseProxy(proxyPipeline => { // Add the three middleware YARP adds by default plus the Http.sys delegation middleware proxyPipeline.UseSessionAffinity(); // Has no affect on delegation destinations because the response doesn't go through YARP proxyPipeline.UseLoadBalancing(); proxyPipeline.UsePassiveHealthChecks(); proxyPipeline.UseHttpSysDelegation(); }); ``` - Add a ReverseProxy section to appsettings.json. Configuration is almost identical to how YARP in typically configured. The only difference is, for destinations which should use delegation, they have metadata which indicates the Http.sys queue name to delegate the request to. ```json "Destinations": { "SampleHttpSysServer": { "Address": "http://localhost:5600/", "Metadata": { "HttpSysDelegationQueue": "SampleHttpSysServerQueue" } } } ``` ## Usage To run the sample: 1. Start the SampleHttpSysServer project `dotnet run --project SampleHttpSysServer\SampleHttpSysServer.csproj` 2. Start the ReverseProxy.HttpSysDelegation.Sample project `dotnet run --project ReverseProxy\ReverseProxy.HttpSysDelegation.Sample.csproj` By default, the SampleHttpSysServer will listen to http://localhost:5600 and the ReverseProxy will listen to http://localhost:5500. The ReverseProxy will delegate any requests under the path http://localhost:5500/delegate to the SampleHttpSysServer. Any other path will be proxied to https://httpbin.org/. ================================================ FILE: samples/ReverseProxy.HttpSysDelegation.Sample/ReverseProxy/Program.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; var builder = WebApplication.CreateBuilder(args); Debug.Assert(OperatingSystem.IsWindows()); builder.WebHost.UseHttpSys(); builder.Services.AddReverseProxy() .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy")); var app = builder.Build(); app.MapReverseProxy(proxyPipeline => { proxyPipeline.UseSessionAffinity(); // Has no effect on delegation destinations because the response doesn't go through YARP proxyPipeline.UseLoadBalancing(); proxyPipeline.UsePassiveHealthChecks(); proxyPipeline.UseHttpSysDelegation(); }); app.Run(); ================================================ FILE: samples/ReverseProxy.HttpSysDelegation.Sample/ReverseProxy/Properties/launchSettings.json ================================================ { "profiles": { "ReverseProxy.HttpSysDelegation": { "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, "applicationUrl": "http://localhost:5500", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } } } } ================================================ FILE: samples/ReverseProxy.HttpSysDelegation.Sample/ReverseProxy/ReverseProxy.HttpSysDelegation.Sample.csproj ================================================ $(ReleaseTFMs) enable enable ================================================ FILE: samples/ReverseProxy.HttpSysDelegation.Sample/ReverseProxy/appsettings.Development.json ================================================ { "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } } } ================================================ FILE: samples/ReverseProxy.HttpSysDelegation.Sample/ReverseProxy/appsettings.json ================================================ { "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } }, "AllowedHosts": "*", "ReverseProxy": { "Routes": { "delegateroute": { "ClusterId": "delegatecluster", "Match": { "Path": "/delegate/{**catch-all}" } }, "proxyroute": { "ClusterId": "proxycluster", "Match": { "Path": "{**catch-all}" } } }, "Clusters": { "delegatecluster": { "Destinations": { "SampleHttpSysServer": { "Address": "http://localhost:5600/", "Metadata": { "HttpSysDelegationQueue": "SampleHttpSysServerQueue" } } } }, "proxycluster": { "Destinations": { "httpbin.org": { "Address": "https://httpbin.org/" } } } } } } ================================================ FILE: samples/ReverseProxy.HttpSysDelegation.Sample/SampleHttpSysServer/Program.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; using Microsoft.AspNetCore.Server.HttpSys; var builder = WebApplication.CreateBuilder(args); Debug.Assert(OperatingSystem.IsWindows()); builder.WebHost.UseHttpSys(options => { options.RequestQueueName = "SampleHttpSysServerQueue"; options.RequestQueueMode = RequestQueueMode.Create; }); var app = builder.Build(); app.Run(async context => { await context.Response.WriteAsync($"Hello World! (PID: {Environment.ProcessId})"); }); app.Run(); ================================================ FILE: samples/ReverseProxy.HttpSysDelegation.Sample/SampleHttpSysServer/Properties/launchSettings.json ================================================ { "profiles": { "SampleHttpSysServer": { "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, "applicationUrl": "http://localhost:5600", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } } } } ================================================ FILE: samples/ReverseProxy.HttpSysDelegation.Sample/SampleHttpSysServer/SampleHttpSysServer.csproj ================================================ $(ReleaseTFMs) enable enable ================================================ FILE: samples/ReverseProxy.HttpSysDelegation.Sample/SampleHttpSysServer/appsettings.Development.json ================================================ { "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } } } ================================================ FILE: samples/ReverseProxy.HttpSysDelegation.Sample/SampleHttpSysServer/appsettings.json ================================================ { "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*" } ================================================ FILE: samples/ReverseProxy.LetsEncrypt.Sample/Program.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; var builder = WebApplication.CreateBuilder(args); builder.Services.AddLettuceEncrypt(); builder.Services.AddControllers(); // Add the reverse proxy capability to the server builder.Services.AddReverseProxy() // Initialize the reverse proxy from the "ReverseProxy" section of configuration .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy")); var app = builder.Build(); // Register the reverse proxy routes app.MapReverseProxy(); app.Run(); ================================================ FILE: samples/ReverseProxy.LetsEncrypt.Sample/Properties/launchSettings.json ================================================ { "$schema": "http://json.schemastore.org/launchsettings.json", "profiles": { "ReverseProxy.LetsEncrypt.Sample": { "commandName": "Project", "launchBrowser": false, "launchUrl": "", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } } } } ================================================ FILE: samples/ReverseProxy.LetsEncrypt.Sample/README.md ================================================ # Lets Encrypt Sample [Lets Encrypt](https://letsencrypt.org/) is a certificate authority (CA) that provides HTTPS (SSL/TLS) certificates for free. This sample shows how to add Lets Encrypt for TLS termination in YARP by integrating with [LettuceEncrypt](https://github.com/natemcmaster/LettuceEncrypt). It allows to set up TLS between the client and YARP with minimal configuration. The sample includes the following parts: - **[Program.cs](Program.cs)** It calls `IServiceCollection.AddLettuceEncrypt` in the `ConfigureServices` method. - **[appsettings.json](appsettings.json)** Sets up the required options for LettuceEncrypt including: - "DomainNames" - at least one domain name is required - "EmailAddress" - email address must be specified to register with the certificate authority ================================================ FILE: samples/ReverseProxy.LetsEncrypt.Sample/ReverseProxy.LetsEncrypt.Sample.csproj ================================================ $(ReleaseTFMs) latest ================================================ FILE: samples/ReverseProxy.LetsEncrypt.Sample/appsettings.json ================================================ { "Kestrel": { "Endpoints": { "Http": { "Url": "http://*:80" }, "Https": { "Url": "https://*:443" } } }, "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Information", "Microsoft.Hosting.Lifetime": "Information" } }, "AllowedHosts": "*", "ReverseProxy": { "Routes": { "route1": { "ClusterId": "cluster1", "Match": { "Path": "{**catch-all}" } } }, "Clusters": { "cluster1": { "Destinations": { "destination1": { "Address": "https://example.com/" } } } } }, "LettuceEncrypt": { // Set this to automatically accept the terms of service of your certificate authority. // If you don't set this in config, you will need to press "y" whenever the application starts "AcceptTermsOfService": true, // You must specify at least one domain name "DomainNames": [ "example.com" ], // You must specify an email address to register with the certificate authority "EmailAddress": "it-admin@example.com" } } ================================================ FILE: samples/ReverseProxy.Metrics.Sample/ForwarderMetricsConsumer.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using Yarp.Telemetry.Consumption; namespace Yarp.Sample { public sealed class ForwarderMetricsConsumer : IMetricsConsumer { public void OnMetrics(ForwarderMetrics previous, ForwarderMetrics current) { var elapsed = current.Timestamp - previous.Timestamp; var newRequests = current.RequestsStarted - previous.RequestsStarted; Console.Title = $"Forwarded {current.RequestsStarted} requests ({newRequests} in the last {(int)elapsed.TotalMilliseconds} ms)"; } } } ================================================ FILE: samples/ReverseProxy.Metrics.Sample/ForwarderTelemetryConsumer.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using Yarp.ReverseProxy.Forwarder; using Yarp.Telemetry.Consumption; namespace Yarp.Sample { public sealed class ForwarderTelemetryConsumer : IForwarderTelemetryConsumer { public void OnForwarderStart(DateTime timestamp, string destinationPrefix) { var metrics = PerRequestMetrics.Current; metrics.ProxyStartOffset = metrics.CalcOffset(timestamp); } public void OnForwarderStop(DateTime timestamp, int statusCode) { var metrics = PerRequestMetrics.Current; metrics.ProxyStopOffset = metrics.CalcOffset(timestamp); } public void OnForwarderFailed(DateTime timestamp, ForwarderError error) { var metrics = PerRequestMetrics.Current; metrics.Error = error; } public void OnContentTransferred(DateTime timestamp, bool isRequest, long contentLength, long iops, TimeSpan readTime, TimeSpan writeTime, TimeSpan firstReadTime) { var metrics = PerRequestMetrics.Current; if (isRequest) { metrics.RequestBodyLength = contentLength; metrics.RequestContentIops = iops; } else { // We don't get a content stop from http as it is returning a stream that is up to the consumer to // read, but we know its ended here. metrics.HttpResponseContentStopOffset = metrics.CalcOffset(timestamp); metrics.ResponseBodyLength = contentLength; metrics.ResponseContentIops = iops; } } public void OnForwarderInvoke(DateTime timestamp, string clusterId, string routeId, string destinationId) { var metrics = PerRequestMetrics.Current; metrics.RouteInvokeOffset = metrics.CalcOffset(timestamp); metrics.RouteId = routeId; metrics.ClusterId = clusterId; metrics.DestinationId = destinationId; } } } ================================================ FILE: samples/ReverseProxy.Metrics.Sample/HttpClientTelemetryConsumer.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Net.Http; using Yarp.Telemetry.Consumption; namespace Yarp.Sample { public sealed class HttpClientTelemetryConsumer : IHttpTelemetryConsumer { public void OnRequestStart(DateTime timestamp, string scheme, string host, int port, string pathAndQuery, int versionMajor, int versionMinor, HttpVersionPolicy versionPolicy) { var metrics = PerRequestMetrics.Current; metrics.HttpRequestStartOffset = metrics.CalcOffset(timestamp); } public void OnRequestStop(DateTime timestamp) { var metrics = PerRequestMetrics.Current; metrics.HttpRequestStopOffset = metrics.CalcOffset(timestamp); } public void OnConnectionEstablished(DateTime timestamp, int versionMajor, int versionMinor) { var metrics = PerRequestMetrics.Current; metrics.HttpConnectionEstablishedOffset = metrics.CalcOffset(timestamp); } public void OnRequestLeftQueue(DateTime timestamp, TimeSpan timeOnQueue, int versionMajor, int versionMinor) { var metrics = PerRequestMetrics.Current; metrics.HttpRequestLeftQueueOffset = metrics.CalcOffset(timestamp); } public void OnRequestHeadersStart(DateTime timestamp) { var metrics = PerRequestMetrics.Current; metrics.HttpRequestHeadersStartOffset = metrics.CalcOffset(timestamp); } public void OnRequestHeadersStop(DateTime timestamp) { var metrics = PerRequestMetrics.Current; metrics.HttpRequestHeadersStopOffset = metrics.CalcOffset(timestamp); } public void OnRequestContentStart(DateTime timestamp) { var metrics = PerRequestMetrics.Current; metrics.HttpRequestContentStartOffset = metrics.CalcOffset(timestamp); } public void OnRequestContentStop(DateTime timestamp, long contentLength) { var metrics = PerRequestMetrics.Current; metrics.HttpRequestContentStopOffset = metrics.CalcOffset(timestamp); } public void OnResponseHeadersStart(DateTime timestamp) { var metrics = PerRequestMetrics.Current; metrics.HttpResponseHeadersStartOffset = metrics.CalcOffset(timestamp); } public void OnResponseHeadersStop(DateTime timestamp) { var metrics = PerRequestMetrics.Current; metrics.HttpResponseHeadersStopOffset = metrics.CalcOffset(timestamp); } } } ================================================ FILE: samples/ReverseProxy.Metrics.Sample/PerRequestMetrics.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Threading; using Yarp.ReverseProxy.Forwarder; using System.Text.Json; namespace Yarp.Sample { public class PerRequestMetrics { private static readonly AsyncLocal _local = new AsyncLocal(); private static readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions { WriteIndented = true }; // Ensure we are only fetched via the factory private PerRequestMetrics() { } /// /// Factory to instantiate or restore the metrics from AsyncLocal storage /// public static PerRequestMetrics Current => _local.Value ??= new PerRequestMetrics(); // Time the request was started via the pipeline public DateTime StartTime { get; set; } // Offset Tics for each part of the proxy operation public float RouteInvokeOffset { get; set; } public float ProxyStartOffset { get; set; } public float HttpRequestStartOffset { get; set; } public float HttpConnectionEstablishedOffset { get; set; } public float HttpRequestLeftQueueOffset { get; set; } public float HttpRequestHeadersStartOffset { get; set; } public float HttpRequestHeadersStopOffset { get; set; } public float HttpRequestContentStartOffset { get; set; } public float HttpRequestContentStopOffset { get; set; } public float HttpResponseHeadersStartOffset { get; set; } public float HttpResponseHeadersStopOffset { get; set; } public float HttpResponseContentStopOffset { get; set; } public float HttpRequestStopOffset { get; set; } public float ProxyStopOffset { get; set; } // Info about the request public ForwarderError Error { get; set; } public long RequestBodyLength { get; set; } public long ResponseBodyLength { get; set; } public long RequestContentIops { get; set; } public long ResponseContentIops { get; set; } public string DestinationId { get; set; } public string ClusterId { get; set; } public string RouteId { get; set; } public string ToJson() { return JsonSerializer.Serialize(this, _jsonOptions); } public float CalcOffset(DateTime timestamp) { return (float)(timestamp - StartTime).TotalMilliseconds; } } } ================================================ FILE: samples/ReverseProxy.Metrics.Sample/PerRequestYarpMetricCollectionMiddleware.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; namespace Yarp.Sample { /// /// Middleware that collects YARP metrics and logs them at the end of each request /// public class PerRequestYarpMetricCollectionMiddleware { // Required for middleware private readonly RequestDelegate _next; // Supplied via DI private readonly ILogger _logger; public PerRequestYarpMetricCollectionMiddleware(RequestDelegate next, ILogger logger) { _logger = logger; _next = next; } /// /// Entrypoint for being called as part of the request pipeline /// public async Task InvokeAsync(HttpContext context) { var metrics = PerRequestMetrics.Current; metrics.StartTime = DateTime.UtcNow; // Call the next steps in the middleware, including the proxy await _next(context); // Called after the other middleware steps have completed // Write the info to the console via ILogger. In a production scenario you probably want // to write the results to your telemetry systems directly. _logger.LogInformation("PerRequestMetrics: " + metrics.ToJson()); } } /// /// Helper to aid with registration of the middleware /// public static class YarpMetricCollectionMiddlewareHelper { public static IApplicationBuilder UsePerRequestMetricCollection( this IApplicationBuilder builder) { return builder.UseMiddleware(); } } } ================================================ FILE: samples/ReverseProxy.Metrics.Sample/Program.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using Yarp.Sample; var builder = WebApplication.CreateBuilder(args); var services = builder.Services; services.AddControllers(); services.AddReverseProxy() .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy")); services.AddHttpContextAccessor(); // Interface that collects general metrics about the proxy forwarder services.AddMetricsConsumer(); // Registration of a consumer to events for proxy forwarder telemetry services.AddTelemetryConsumer(); // Registration of a consumer to events for HttpClient telemetry services.AddTelemetryConsumer(); services.AddTelemetryConsumer(); var app = builder.Build(); // Custom middleware that collects and reports the proxy metrics // Placed at the beginning so that it is the first and last thing to run for each request app.UsePerRequestMetricCollection(); // Middleware used to intercept the WebSocket connection and collect telemetry exposed to WebSocketsTelemetryConsumer app.UseWebSocketsTelemetry(); app.MapReverseProxy(); app.Run(); ================================================ FILE: samples/ReverseProxy.Metrics.Sample/Properties/launchSettings.json ================================================ { "iisSettings": { "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { "applicationUrl": "https://localhost:44356/", "sslPort": 44356 } }, "profiles": { "ReverseProxy.Metrics.Sample": { "commandName": "Project", "launchBrowser": false, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } } } } ================================================ FILE: samples/ReverseProxy.Metrics.Sample/README.md ================================================ # ReverseProxy.Metrics.Sample This sample demonstrates how to use the ReverseProxy.Telemetry.Consumption library to listen to telemetry data from YARP. In this case it uses the events to create a per-request data structure with detailed timings for each operation that takes place as part of the proxy operation. Internally YARP uses EventSource to collect telemetry events and metrics from a number of subsystems that are used to process the requests. The YARP telemetry library provides wrapper classes that collect these events metrics and make them available for Consumption. To listen for the metrics you register classes with DI that implement an interface for each subsystem. The subsystems are: - **Proxy** which represents the overall proxy operation, and success or failure. Events include: - When proxy requests are started and stopped - When request/response bodies are processed Metrics include: - Number of requests started - Number of request in flight - Number of requests that have failed - **Kestrel** which is the web server that handles incoming requests. Events include: - When requests are started/stopped or fail Metrics include: - Connection Rate - how many connections are opened a second - Total number of connections - Number of TLS handshakes - Incoming queue length - **Http** which is the HttpClient which makes outgoing requests to the destination servers. Events include: - When connections are created - When requests are started/stopped or fail - When headers/contents are sent/received - When requests are dequeued as connections become available Metrics include: - Number of outgoing requests started - Number of requests failed - Number of active requests - Number of outbound connections - **Sockets** which includes events around connection attempts & metrics about the amount of data sent and received - **NameResolution** which includes events around name resolution attempts & metrics about DNS lookups of destinations - **NetSecurity** which includes events around SslStream handshakes & metrics about the number and latency of handshakes per protocol ## Key Files The following files are key to implementing the features described above: ### Program.cs Performs registrtion of the proxy, the listener classes and a custom ASP.NET middleware step that starts per-request telemetry and reports the results when complete ### ProxyTelemetryConsumer.cs Listens to events from the proxy telemetry and records timings and info about the high level processing involved in proxying a request. ### HttpTelemetryConsumer.cs Listens to events from the HttpClient telemetry and records timings and info about the outbound request and response from the destination server. ### PerRequestMetrics.cs Class to store the metrics on a per request basis. Instances are stored in AsyncLocal storage for the duration of the request. ### PerRequestYarpMetricCollectionMiddleware.cs ASP.NET Core middleware that is the first and last thing called as part of the ASP.NET handling of the request. It initializes the per-request metrics and logs the results at the end of the request. ================================================ FILE: samples/ReverseProxy.Metrics.Sample/ReverseProxy.Metrics.Sample.csproj ================================================ $(ReleaseTFMs) Exe Yarp.Sample latest ================================================ FILE: samples/ReverseProxy.Metrics.Sample/WebSocketsTelemetryConsumer.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using Microsoft.Extensions.Logging; using Yarp.Telemetry.Consumption; namespace Yarp.Sample { public sealed class WebSocketsTelemetryConsumer : IWebSocketsTelemetryConsumer { private readonly ILogger _logger; public WebSocketsTelemetryConsumer(ILogger logger) { ArgumentNullException.ThrowIfNull(logger); _logger = logger; } public void OnWebSocketClosed(DateTime timestamp, DateTime establishedTime, WebSocketCloseReason closeReason, long messagesRead, long messagesWritten) { _logger.LogInformation($"WebSocket connection closed ({closeReason}) after reading {messagesRead} and writing {messagesWritten} messages over {(timestamp - establishedTime).TotalSeconds:N2} seconds."); } } } ================================================ FILE: samples/ReverseProxy.Metrics.Sample/appsettings.Development.json ================================================ { "Logging": { "LogLevel": { "Default": "Debug", "Microsoft": "Debug", "Microsoft.Hosting.Lifetime": "Information" } } } ================================================ FILE: samples/ReverseProxy.Metrics.Sample/appsettings.json ================================================ { "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": { "route1": { "ClusterId": "cluster1", "Match": { "Path": "{**catch-all}" } } }, "Clusters": { "cluster1": { "Destinations": { "cluster1/destination1": { "Address": "https://example.com/" } } } } } } ================================================ FILE: samples/ReverseProxy.Transforms.Sample/MyTransformFactory.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Net.Http; using Yarp.ReverseProxy.Transforms; using Yarp.ReverseProxy.Transforms.Builder; namespace Yarp.Sample { internal sealed 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 => { transformContext.ProxyRequest.Options.Set(new HttpRequestOptionsKey("CustomTransform"), value); return default; }); return true; } return false; } } } ================================================ FILE: samples/ReverseProxy.Transforms.Sample/MyTransformProvider.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Net.Http; using Yarp.ReverseProxy.Transforms; using Yarp.ReverseProxy.Transforms.Builder; namespace Yarp.Sample { internal sealed class MyTransformProvider : ITransformProvider { public void ValidateRoute(TransformRouteValidationContext context) { // Check all routes for a custom property and validate the associated transform data. if (context.Route.Metadata?.TryGetValue("CustomMetadata", out var value) ?? false) { if (string.IsNullOrEmpty(value)) { context.Errors.Add(new ArgumentException("A non-empty CustomMetadata value is required")); } } } public void ValidateCluster(TransformClusterValidationContext context) { // Check all clusters for a custom property and validate the associated transform data. if (context.Cluster.Metadata?.TryGetValue("CustomMetadata", out var 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. if ((transformBuildContext.Route.Metadata?.TryGetValue("CustomMetadata", out var value) ?? false) || (transformBuildContext.Cluster?.Metadata?.TryGetValue("CustomMetadata", out value) ?? false)) { if (string.IsNullOrEmpty(value)) { throw new ArgumentException("A non-empty CustomMetadata value is required"); } transformBuildContext.AddRequestTransform(transformContext => { transformContext.ProxyRequest.Options.Set(new HttpRequestOptionsKey("CustomMetadata"), value); return default; }); } } } } ================================================ FILE: samples/ReverseProxy.Transforms.Sample/Program.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Net.Http.Headers; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using Yarp.ReverseProxy.Transforms; using Yarp.Sample; var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); builder.Services.AddReverseProxy() .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy")) .AddTransforms() // Adds custom transforms via code. .AddTransformFactory() // Adds custom transforms via config. .AddTransforms(transformBuilderContext => // Add transforms inline { // 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); }); } }); var app = builder.Build(); app.UseAuthentication(); app.UseAuthorization(); // Register the reverse proxy routes app.MapReverseProxy(); app.Run(); ================================================ FILE: samples/ReverseProxy.Transforms.Sample/Properties/launchSettings.json ================================================ { "iisSettings": { "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { "applicationUrl": "https://localhost:44356/", "sslPort": 44356 } }, "profiles": { "ReverseProxy.Transforms.Sample": { "commandName": "Project", "launchBrowser": false, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } } } } ================================================ FILE: samples/ReverseProxy.Transforms.Sample/ReverseProxy.Transforms.Sample.csproj ================================================ $(ReleaseTFMs) Exe Yarp.Sample latest ================================================ FILE: samples/ReverseProxy.Transforms.Sample/TokenService.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Security.Claims; using System.Threading.Tasks; namespace Yarp.Sample { internal sealed class TokenService { internal Task GetAuthTokenAsync(ClaimsPrincipal user) { return Task.FromResult(user.Identity.Name); } } } ================================================ FILE: samples/ReverseProxy.Transforms.Sample/appsettings.Development.json ================================================ { "Logging": { "LogLevel": { "Default": "Debug", "Microsoft": "Debug", "Microsoft.Hosting.Lifetime": "Information" } } } ================================================ FILE: samples/ReverseProxy.Transforms.Sample/appsettings.json ================================================ { "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": { "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" } } } } } } ================================================ FILE: samples/SampleServer/Controllers/HealthController.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Mvc; namespace SampleServer.Controllers { /// /// Controller for active health check probes. /// [ApiController] public class HealthController : ControllerBase { private static volatile int _count; /// /// Returns 200 if server is healthy. /// [HttpGet] [Route("/api/health")] public IActionResult CheckHealth() { _count++; // Simulate temporary health degradation. return _count % 10 < 4 ? Ok() : StatusCode(500); } } } ================================================ FILE: samples/SampleServer/Controllers/HttpController.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you 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[key] = value; } } } } ================================================ FILE: samples/SampleServer/Controllers/WebSocketsController.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you 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.ContentType = "text/html"; await HttpContext.Response.SendFileAsync("./wwwroot/index.html"); return; } 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); } } } } ================================================ FILE: samples/SampleServer/Program.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers() .AddJsonOptions(options => options.JsonSerializerOptions.WriteIndented = true); var app = builder.Build(); app.UseWebSockets(); app.MapControllers(); app.Run(); ================================================ FILE: samples/SampleServer/Properties/launchSettings.json ================================================ { "profiles": { "SampleServer": { "commandName": "Project", "launchBrowser": false, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } } } } ================================================ FILE: samples/SampleServer/README.md ================================================ # Sample Server This is a simple web server implementation that can be used to test YARP proxy, by using it as the destination. Functionality in this sample server includes: ## Echoing of Request Headers Provided that the request URI path doesn't match other endpoints described below, then the request headers will be reported back as text in the response body. This enables you to quickly see what headers were sent, for example to analyze header transforms made by the reverse proxy. ## Healthcheck status endpoint [HealthController](Controllers/HealthController.cs) implements an API endpoint for /api/health that will randomly return bad health status. ## WebSockets endpoint [WebSocketsController](Controllers/WebSocketsController.cs) implements an endpoint for testing web sockets at /api/websockets. ## Usage To run the sample server use: - ```dotnet run``` from the sample folder - ```dotnet run SampleServer/SampleServer.csproj``` passing in the path to the .csproj file - Build an executable using ```dotnet build SampleServer.csproj``` and then run the executable directly The server will listen to http://localhost:5000 and https://localhost:5001 by default. The ports and interface can be changed using the urls option on the cmd line. For example ```dotnet run SampleServer/SampleServer.csproj --urls "https://localhost:10000;http://localhost:10010"``` ================================================ FILE: samples/SampleServer/SampleServer.csproj ================================================ $(ReleaseTFMs) Exe SampleServer ================================================ FILE: samples/SampleServer/appsettings.Development.json ================================================ { "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Information", "Microsoft.Hosting.Lifetime": "Information" } } } ================================================ FILE: samples/SampleServer/appsettings.json ================================================ { "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } }, "AllowedHosts": "*" } ================================================ FILE: samples/SampleServer/wwwroot/index.html ================================================

WebSocket Test Page

Ready to connect...

Note: When connected to the default server (i.e. the server in the address bar ;)), the message "ServerClose" will cause the server to close the connection. Similarly, the message "ServerAbort" will cause the server to forcibly terminate the connection without a closing handshake

Communication Log

From To Data
================================================ FILE: src/Application/Extensions.cs ================================================ using System.Net; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using HealthChecks.ApplicationStatus.DependencyInjection; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Diagnostics.HealthChecks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using OpenTelemetry; using OpenTelemetry.Exporter; using OpenTelemetry.Logs; using OpenTelemetry.Metrics; using OpenTelemetry.Trace; using static System.Net.WebRequestMethods; namespace Microsoft.Extensions.Hosting; // Adds common .NET Aspire services: service discovery, resilience, health checks, and OpenTelemetry. // This project should be referenced by each service project in your solution. // To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults public static class Extensions { public static TBuilder AddServiceDefaults(this TBuilder builder) where TBuilder : IHostApplicationBuilder { builder.ConfigureOpenTelemetry(); builder.AddDefaultHealthChecks(); builder.Services.AddServiceDiscovery(); return builder; } public static TBuilder ConfigureOpenTelemetry(this TBuilder builder) where TBuilder : IHostApplicationBuilder { var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); if (useOtlpExporter) { builder.Logging.AddOpenTelemetry(logging => { logging.IncludeFormattedMessage = true; logging.IncludeScopes = true; }); var telemetryBuilder = builder.Services.AddOpenTelemetry(); telemetryBuilder .WithLogging(logging => logging.AddOtlpExporter()) .WithMetrics(metrics => { metrics.AddAspNetCoreInstrumentation() .AddHttpClientInstrumentation() .AddRuntimeInstrumentation() .SetExemplarFilter(ExemplarFilterType.TraceBased) .AddOtlpExporter(); }) .WithTracing(tracing => { tracing.AddAspNetCoreInstrumentation() .AddHttpClientInstrumentation() .AddOtlpExporter(); }); if (string.Equals(Environment.GetEnvironmentVariable("YARP_UNSAFE_OLTP_CERT_ACCEPT_ANY_SERVER_CERTIFICATE"), "true", StringComparison.InvariantCultureIgnoreCase)) { // We cannot use UseOtlpExporter() since it doesn't support configuration via OtlpExporterOptions // https://github.com/open-telemetry/opentelemetry-dotnet/issues/5802 builder.Services.Configure(ConfigureOtlpExporterOptions); } } static void ConfigureOtlpExporterOptions(OtlpExporterOptions options) { options.HttpClientFactory = () => { var handler = new HttpClientHandler(); handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator; var httpClient = new HttpClient(handler); return httpClient; }; } return builder; } public static TBuilder AddDefaultHealthChecks(this TBuilder builder) where TBuilder : IHostApplicationBuilder { builder.Services.AddHealthChecks() // Add a default liveness check on application status. .AddApplicationStatus(tags: ["live"]); return builder; } } ================================================ FILE: src/Application/Program.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; var builder = WebApplication.CreateBuilder(); // Load configuration from file if passed if (args.Length == 1) { var configFile = args[0]; var fileInfo = new FileInfo(configFile); if (!fileInfo.Exists) { Console.Error.WriteLine($"Could not find '{configFile}'."); return 2; } builder.Configuration.AddJsonFile(fileInfo.FullName, optional: false, reloadOnChange: true); builder.Configuration.AddEnvironmentVariables(); } // Configure YARP builder.AddServiceDefaults(); builder.Services.AddServiceDiscovery(); builder.Services.AddReverseProxy() .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy")) .AddServiceDiscoveryDestinationResolver(); var app = builder.Build(); var isEnabledStaticFiles = Environment.GetEnvironmentVariable("YARP_ENABLE_STATIC_FILES"); if (string.Equals(isEnabledStaticFiles, "true", StringComparison.OrdinalIgnoreCase)) { app.UseFileServer(); } app.UseRouting(); app.MapReverseProxy(); await app.RunAsync(); return 0; ================================================ FILE: src/Application/Yarp.Application.csproj ================================================  Exe win-x64;win-arm64;linux-x64;linux-arm64; net9.0 enable enable yarp false false ================================================ FILE: src/Common/Package.targets ================================================ ================================================ FILE: src/Directory.Build.props ================================================ $(MSBuildProjectDirectory)\ConfigurationSchema.json true $(TargetsForTfmSpecificContentInPackage);AddPackageTargetsInPackage true true true icon.png ================================================ FILE: src/Kubernetes.Controller/Caching/Endpoints.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using k8s.Models; using System; using System.Collections.Generic; namespace Yarp.Kubernetes.Controller.Caching; /// /// Holds data needed from a resource. /// public struct Endpoints { public Endpoints(V1Endpoints endpoints) { ArgumentNullException.ThrowIfNull(endpoints); Name = endpoints.Name(); Subsets = endpoints.Subsets; } public string Name { get; set; } public IList Subsets { get; } } ================================================ FILE: src/Kubernetes.Controller/Caching/ICache.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using k8s; using k8s.Models; using System.Collections.Generic; using System.Collections.Immutable; using Yarp.Kubernetes.Controller.Services; namespace Yarp.Kubernetes.Controller.Caching; /// /// ICache service interface holds onto the least amount of data necessary /// for to process work. /// public interface ICache { void Update(WatchEventType eventType, V1IngressClass ingressClass); bool Update(WatchEventType eventType, V1Ingress ingress); ImmutableList Update(WatchEventType eventType, V1Service service); ImmutableList Update(WatchEventType eventType, V1Endpoints endpoints); void Update(WatchEventType eventType, V1Secret secret); bool TryGetReconcileData(NamespacedName key, out ReconcileData data); void GetKeys(List keys); IEnumerable GetIngresses(); } ================================================ FILE: src/Kubernetes.Controller/Caching/IngressCache.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using k8s; using k8s.Models; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using Yarp.Kubernetes.Controller.Certificates; using Yarp.Kubernetes.Controller.Services; namespace Yarp.Kubernetes.Controller.Caching; /// /// ICache service interface holds onto the least amount of data necessary /// for to process work. /// public class IngressCache : ICache { private readonly object _sync = new object(); private readonly Dictionary _ingressClassData = new Dictionary(); private readonly Dictionary _namespaceCaches = new Dictionary(); private readonly YarpOptions _options; private readonly IServerCertificateSelector _certificateSelector; private readonly ICertificateHelper _certificateHelper; private readonly ILogger _logger; private bool _isDefaultController; public IngressCache(IOptions options, IServerCertificateSelector certificateSelector, ICertificateHelper certificateHelper, ILogger logger) { ArgumentNullException.ThrowIfNull(options?.Value); ArgumentNullException.ThrowIfNull(certificateSelector); ArgumentNullException.ThrowIfNull(certificateHelper); ArgumentNullException.ThrowIfNull(logger); _options = options.Value; _certificateSelector = certificateSelector; _certificateHelper = certificateHelper; _logger = logger; } public void Update(WatchEventType eventType, V1IngressClass ingressClass) { ArgumentNullException.ThrowIfNull(ingressClass); if (!string.Equals(_options.ControllerClass, ingressClass.Spec.Controller, StringComparison.OrdinalIgnoreCase)) { _logger.LogInformation( "Ignoring {IngressClassNamespace}/{IngressClassName} as the spec.controller is not the same as this ingress", ingressClass.Metadata.NamespaceProperty, ingressClass.Metadata.Name); return; } var ingressClassName = ingressClass.Name(); lock (_sync) { if (eventType == WatchEventType.Added || eventType == WatchEventType.Modified) { _ingressClassData[ingressClassName] = new IngressClassData(ingressClass); } else if (eventType == WatchEventType.Deleted) { _ingressClassData.Remove(ingressClassName); } _isDefaultController = _ingressClassData.Values.Any(ic => ic.IsDefault); } } public bool Update(WatchEventType eventType, V1Ingress ingress) { ArgumentNullException.ThrowIfNull(ingress); Namespace(ingress.Namespace()).Update(eventType, ingress); return true; } public ImmutableList Update(WatchEventType eventType, V1Service service) { ArgumentNullException.ThrowIfNull(service); return Namespace(service.Namespace()).Update(eventType, service); } public ImmutableList Update(WatchEventType eventType, V1Endpoints endpoints) { return Namespace(endpoints.Namespace()).Update(eventType, endpoints); } public void Update(WatchEventType eventType, V1Secret secret) { var namespacedName = NamespacedName.From(secret); _logger.LogDebug("Found secret '{NamespacedName}'. Checking against default {CertificateSecretName}", namespacedName, _options.DefaultSslCertificate); if (!string.Equals(namespacedName.ToString(), _options.DefaultSslCertificate, StringComparison.OrdinalIgnoreCase)) { return; } _logger.LogInformation("Found secret `{NamespacedName}` to use as default certificate for HTTPS traffic", namespacedName); var certificate = _certificateHelper.ConvertCertificate(namespacedName, secret); if (certificate is null) { return; } if (eventType == WatchEventType.Added || eventType == WatchEventType.Modified) { _certificateSelector.AddCertificate(namespacedName, certificate); } else if (eventType == WatchEventType.Deleted) { _certificateSelector.RemoveCertificate(namespacedName); } } public bool TryGetReconcileData(NamespacedName key, out ReconcileData data) { return Namespace(key.Namespace).TryLookup(key, out data); } public void GetKeys(List keys) { lock (_sync) { foreach (var (ns, cache) in _namespaceCaches) { cache.GetKeys(ns, keys); } } } public IEnumerable GetIngresses() { var ingresses = new List(); lock (_sync) { foreach (var ns in _namespaceCaches) { ingresses.AddRange(ns.Value.GetIngresses().Where(IsYarpIngress)); } } return ingresses; } private bool IsYarpIngress(IngressData ingress) { if (ingress.Spec.IngressClassName is null) { return _isDefaultController; } lock (_sync) { return _ingressClassData.ContainsKey(ingress.Spec.IngressClassName); } } private NamespaceCache Namespace(string key) { lock (_sync) { if (!_namespaceCaches.TryGetValue(key, out var value)) { value = new NamespaceCache(); _namespaceCaches.Add(key, value); } return value; } } } ================================================ FILE: src/Kubernetes.Controller/Caching/IngressClassData.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using k8s.Models; namespace Yarp.Kubernetes.Controller.Caching; /// /// Holds data needed from a resource. /// public struct IngressClassData { public IngressClassData(V1IngressClass ingressClass) { ArgumentNullException.ThrowIfNull(ingressClass); IngressClass = ingressClass; IsDefault = GetDefaultAnnotation(ingressClass); } public V1IngressClass IngressClass { get; } public bool IsDefault { get; } private static bool GetDefaultAnnotation(V1IngressClass ingressClass) { var annotation = ingressClass.GetAnnotation("ingressclass.kubernetes.io/is-default-class"); return string.Equals("true", annotation, StringComparison.OrdinalIgnoreCase); } } ================================================ FILE: src/Kubernetes.Controller/Caching/IngressData.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using k8s.Models; using System; namespace Yarp.Kubernetes.Controller.Caching; /// /// Holds data needed from a resource. /// public struct IngressData { public IngressData(V1Ingress ingress) { ArgumentNullException.ThrowIfNull(ingress); Spec = ingress.Spec; Metadata = ingress.Metadata; } public V1IngressSpec Spec { get; set; } public V1ObjectMeta Metadata { get; set; } } ================================================ FILE: src/Kubernetes.Controller/Caching/NamespaceCache.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using k8s; using k8s.Models; using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using Yarp.Kubernetes.Controller.Services; namespace Yarp.Kubernetes.Controller.Caching; /// /// Per-namespace cache data. Implicitly scopes name-based lookups to same namespace. Also /// intended to make updates faster because cross-reference dictionaries are not cluster-wide. /// public class NamespaceCache { private readonly object _sync = new object(); private readonly Dictionary> _ingressToServiceNames = new Dictionary>(); private readonly Dictionary> _serviceToIngressNames = new Dictionary>(); private readonly Dictionary _ingressData = new Dictionary(); private readonly Dictionary _serviceData = new Dictionary(); private readonly Dictionary _endpointsData = new Dictionary(); public void Update(WatchEventType eventType, V1Ingress ingress) { ArgumentNullException.ThrowIfNull(ingress); var serviceNames = ImmutableList.Empty; if (eventType == WatchEventType.Added || eventType == WatchEventType.Modified) { // If the ingress exists, list out the related services var spec = ingress.Spec; var defaultBackend = spec?.DefaultBackend; var defaultService = defaultBackend?.Service; if (!string.IsNullOrEmpty(defaultService?.Name)) { serviceNames = serviceNames.Add(defaultService.Name); } foreach (var rule in spec.Rules ?? Enumerable.Empty()) { var http = rule.Http; foreach (var path in http.Paths ?? Enumerable.Empty()) { var backend = path.Backend; var service = backend.Service; if (!serviceNames.Contains(service.Name)) { serviceNames = serviceNames.Add(service.Name); } } } } var ingressName = ingress.Name(); lock (_sync) { var serviceNamesPrevious = ImmutableList.Empty; if (eventType == WatchEventType.Added || eventType == WatchEventType.Modified) { // If the ingress exists then remember details _ingressData[ingressName] = new IngressData(ingress); if (_ingressToServiceNames.TryGetValue(ingressName, out serviceNamesPrevious)) { _ingressToServiceNames[ingressName] = serviceNames; } else { serviceNamesPrevious = ImmutableList.Empty; _ingressToServiceNames.Add(ingressName, serviceNames); } } else if (eventType == WatchEventType.Deleted) { // otherwise clear out details _ingressData.Remove(ingressName); if (_ingressToServiceNames.TryGetValue(ingressName, out serviceNamesPrevious)) { _ingressToServiceNames.Remove(ingressName); } } // update cross-reference for new ingress-to-services linkage not previously known foreach (var serviceName in serviceNames) { if (!serviceNamesPrevious.Contains(serviceName)) { if (_serviceToIngressNames.TryGetValue(serviceName, out var ingressNamesPrevious)) { _serviceToIngressNames[serviceName] = _serviceToIngressNames[serviceName].Add(ingressName); } else { _serviceToIngressNames.Add(serviceName, ImmutableList.Empty.Add(ingressName)); } } } // remove cross-reference for previous ingress-to-services linkage no longer present foreach (var serviceName in serviceNamesPrevious) { if (!serviceNames.Contains(serviceName)) { _serviceToIngressNames[serviceName] = _serviceToIngressNames[serviceName].Remove(ingressName); } } } } public ImmutableList Update(WatchEventType eventType, V1Service service) { ArgumentNullException.ThrowIfNull(service); var serviceName = service.Name(); lock (_sync) { if (eventType == WatchEventType.Added || eventType == WatchEventType.Modified) { _serviceData[serviceName] = new ServiceData(service); } else if (eventType == WatchEventType.Deleted) { _serviceData.Remove(serviceName); } if (_serviceToIngressNames.TryGetValue(serviceName, out var ingressNames)) { return ingressNames; } else { return ImmutableList.Empty; } } } public void GetKeys(string ns, List keys) { ArgumentNullException.ThrowIfNull(keys); lock (_sync) { foreach (var name in _ingressData.Keys) { keys.Add(new NamespacedName(ns, name)); } } } public ImmutableList Update(WatchEventType eventType, V1Endpoints endpoints) { ArgumentNullException.ThrowIfNull(endpoints); var serviceName = endpoints.Name(); lock (_sync) { if (eventType == WatchEventType.Added || eventType == WatchEventType.Modified) { _endpointsData[serviceName] = new Endpoints(endpoints); } else if (eventType == WatchEventType.Deleted) { _endpointsData.Remove(serviceName); } if (_serviceToIngressNames.TryGetValue(serviceName, out var ingressNames)) { return ingressNames; } else { return ImmutableList.Empty; } } } public IEnumerable GetIngresses() { return _ingressData.Values; } public bool IngressExists(V1Ingress ingress) { return _ingressData.ContainsKey(ingress.Name()); } public bool TryLookup(NamespacedName key, out ReconcileData data) { var endpointsList = new List(); var servicesList = new List(); lock (_sync) { if (!_ingressData.TryGetValue(key.Name, out var ingress)) { data = default; return false; } if (_ingressToServiceNames.TryGetValue(key.Name, out var serviceNames)) { foreach (var serviceName in serviceNames) { if (_serviceData.TryGetValue(serviceName, out var serviceData)) { servicesList.Add(serviceData); } if (_endpointsData.TryGetValue(serviceName, out var endpoints)) { endpointsList.Add(endpoints); } } } if (_serviceData.Count == 0) { data = default; return false; } data = new ReconcileData(ingress, servicesList, endpointsList); return true; } } } ================================================ FILE: src/Kubernetes.Controller/Caching/ServiceData.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using k8s.Models; using System; namespace Yarp.Kubernetes.Controller.Caching; /// /// Holds data needed from a resource. /// public struct ServiceData { public ServiceData(V1Service service) { ArgumentNullException.ThrowIfNull(service); Spec = service.Spec; Metadata = service.Metadata; } public V1ServiceSpec Spec { get; set; } public V1ObjectMeta Metadata { get; set; } } ================================================ FILE: src/Kubernetes.Controller/Certificates/CertificateHelper.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Runtime.InteropServices; using System.Security.Cryptography.X509Certificates; using System.Text; using k8s.Models; using Microsoft.Extensions.Logging; namespace Yarp.Kubernetes.Controller.Certificates; public class CertificateHelper : ICertificateHelper { private const string TlsCertKey = "tls.crt"; private const string TlsPrivateKeyKey = "tls.key"; private readonly ILogger _logger; public CertificateHelper(ILogger logger) { ArgumentNullException.ThrowIfNull(logger); _logger = logger; } public X509Certificate2 ConvertCertificate(NamespacedName namespacedName, V1Secret secret) { try { var cert = secret?.Data[TlsCertKey]; var privateKey = secret?.Data[TlsPrivateKeyKey]; if (cert == null || cert.Length == 0 || privateKey == null || privateKey.Length == 0) { _logger.LogWarning("TLS secret '{NamespacedName}' contains invalid data.", namespacedName); return null; } var certString = EnsurePemFormat(cert, "CERTIFICATE"); var privateString = EnsurePemFormat(privateKey, "PRIVATE KEY"); if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { // Cert needs converting. Read https://github.com/dotnet/runtime/issues/23749#issuecomment-388231655 using var convertedCertificate = X509Certificate2.CreateFromPem(certString, privateString); return new X509Certificate2(convertedCertificate.Export(X509ContentType.Pkcs12)); } return X509Certificate2.CreateFromPem(certString, privateString); } catch (Exception ex) { _logger.LogWarning(ex, "Failed to convert secret '{NamespacedName}'", namespacedName); } return null; } /// /// Kubernetes Secrets should be stored in base-64 encoded DER format (see https://kubernetes.io/docs/concepts/configuration/secret/#tls-secrets) /// but need can be imported into a object via PEM. Before this type of secret existed, an Opaque secret would be /// used containing the full PEM format, so it's possible that the incorrect format would be used. /// Doing it this way means we are more tolerant in handling certs in the wrong format. /// /// The raw data. /// The type for the PEM header. /// The certificate data in PEM format. private static string EnsurePemFormat(byte[] data, string pemType) { var der = Encoding.ASCII.GetString(data); if (!der.StartsWith("---", StringComparison.Ordinal)) { // Convert from encoded DER to PEM return $"-----BEGIN {pemType}-----\n{der}\n-----END {pemType}-----"; } return der; } } ================================================ FILE: src/Kubernetes.Controller/Certificates/ICertificateHelper.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Security.Cryptography.X509Certificates; using k8s.Models; namespace Yarp.Kubernetes.Controller.Certificates; public interface ICertificateHelper { X509Certificate2 ConvertCertificate(NamespacedName namespacedName, V1Secret secret); } ================================================ FILE: src/Kubernetes.Controller/Certificates/IServerCertificateSelector.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Security.Cryptography.X509Certificates; using Microsoft.AspNetCore.Connections; namespace Yarp.Kubernetes.Controller.Certificates; /// /// A mechanism for obtaining server certificates dynamically based on the SNI domain name. /// public interface IServerCertificateSelector { /// /// Retrieve a certificate using the provided domain name. /// /// The connection context. /// The domain name. /// Either returns the specific certificate for the domain name, a wildcard certificates, or no certificate. X509Certificate2 GetCertificate(ConnectionContext connectionContext, string domainName); /// /// Adds a certificate to the selector. /// /// An identifier for the certificate that can be used to remove it. /// The server certificate. void AddCertificate(NamespacedName certificateName, X509Certificate2 certificate); /// /// Removes a certificate from the selector. /// /// An identifier for the certificate that can be used to remove it. void RemoveCertificate(NamespacedName certificateName); } ================================================ FILE: src/Kubernetes.Controller/Certificates/ServerCertificateSelector.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Security.Cryptography.X509Certificates; using Microsoft.AspNetCore.Connections; namespace Yarp.Kubernetes.Controller.Certificates; internal class ServerCertificateSelector : IServerCertificateSelector { private X509Certificate2 _defaultCertificate; public void AddCertificate(NamespacedName certificateName, X509Certificate2 certificate) { _defaultCertificate = certificate; } public X509Certificate2 GetCertificate(ConnectionContext connectionContext, string domainName) { return _defaultCertificate; } public void RemoveCertificate(NamespacedName certificateName) { _defaultCertificate = null; } } ================================================ FILE: src/Kubernetes.Controller/Client/GroupApiVersionKind.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using k8s.Models; using System; using System.Reflection; namespace Yarp.Kubernetes.Controller.Client; public struct GroupApiVersionKind : IEquatable { public GroupApiVersionKind(string group, string apiVersion, string kind) { ApiVersion = apiVersion; GroupApiVersion = string.IsNullOrEmpty(group) ? apiVersion : $"{group}/{apiVersion}"; Kind = kind; } public string ApiVersion { get; } public string GroupApiVersion { get; } public string Kind { get; } public static GroupApiVersionKind From() => From(typeof(TResource)); public static GroupApiVersionKind From(Type resourceType) { var entity = resourceType.GetTypeInfo().GetCustomAttribute(); return new GroupApiVersionKind( group: entity.Group, apiVersion: entity.ApiVersion, kind: entity.Kind); } public override bool Equals(object obj) { return obj is GroupApiVersionKind kind && Equals(kind); } public bool Equals(GroupApiVersionKind other) { return GroupApiVersion == other.GroupApiVersion && Kind == other.Kind; } public override int GetHashCode() { return HashCode.Combine(GroupApiVersion, Kind); } public override string ToString() { return $"{Kind}.{GroupApiVersion}"; } public static bool operator ==(GroupApiVersionKind left, GroupApiVersionKind right) { return left.Equals(right); } public static bool operator !=(GroupApiVersionKind left, GroupApiVersionKind right) { return !(left == right); } } ================================================ FILE: src/Kubernetes.Controller/Client/IIngressResourceStatusUpdater.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Threading; using System.Threading.Tasks; namespace Yarp.Kubernetes.Controller.Client; public interface IIngressResourceStatusUpdater { /// /// Updates the status of cached ingresses. /// Task UpdateStatusAsync(CancellationToken cancellationToken); } ================================================ FILE: src/Kubernetes.Controller/Client/IResourceInformer.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using k8s; using k8s.Models; using Microsoft.Extensions.Hosting; using System; using System.Threading; using System.Threading.Tasks; namespace Yarp.Kubernetes.Controller.Client; /// /// Callback for resource event notifications. /// /// The type of being monitored. /// The type of change event which was received. /// The instance of the resource which was received. public delegate void ResourceInformerCallback(WatchEventType eventType, TResource resource) where TResource : class, IKubernetesObject; /// /// Interface IResourceInformer is a service which generates /// notifications for a specific type /// of Kubernetes object. The callback eventType informs if the notification /// is because it is new, modified, or has been deleted. /// Implements the . /// /// The type of the t resource. /// /// public interface IResourceInformer : IHostedService, IResourceInformer where TResource : class, IKubernetesObject, new() { /// /// Registers a callback for change notification. To ensure no events are missed the registration /// may be created in the constructor of a dependant . The returned /// registration should be disposed when the receiver is ending its work. /// /// The delegate that is invoked with each resource notification. /// A registration that should be disposed to end the notifications. IResourceInformerRegistration Register(ResourceInformerCallback callback); } public interface IResourceInformer { /// /// Instructs the resource informer to being watching resources. Allows the startup of informers to be synchronised. /// void StartWatching(); /// /// Returns a task that can be awaited to know when the initial listing of resources is complete. /// Once an await on this method is completed it is safe to assume that all the knowledge of this resource /// type has been made available. Any new changes will be a result of receiving new updates. /// /// The cancellation token that can be used by other objects or threads to receive notice of cancellation. /// Task. Task ReadyAsync(CancellationToken cancellationToken); /// /// Registers a callback for change notification. To ensure no events are missed the registration /// may be created in the constructor of a dependant . The returned /// registration should be disposed when the receiver is ending its work. /// /// The delegate that is invoked with each resource notification. /// A registration that should be disposed to end the notifications. IResourceInformerRegistration Register(ResourceInformerCallback> callback); } ================================================ FILE: src/Kubernetes.Controller/Client/IResourceInformerRegistration.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Threading; using System.Threading.Tasks; namespace Yarp.Kubernetes.Controller.Client; /// /// Returned by to control the lifetime of an event /// notification connection. Call when the lifetime of the notification receiver is ending. /// public interface IResourceInformerRegistration : IDisposable { /// /// Returns a task that can be awaited to know when the initial listing of resources is complete. /// Once an await on this method is completed it is safe to assume that all the knowledge of this resource /// type has been made available. Any new changes will be a result of receiving new updates. /// /// The cancellation token that can be used by other objects or threads to receive notice of cancellation. /// Task. Task ReadyAsync(CancellationToken cancellationToken); } ================================================ FILE: src/Kubernetes.Controller/Client/KubernetesClientOptions.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using k8s; namespace Yarp.Kubernetes.Controller.Client; /// /// Class KubernetesClientOptions. /// public class KubernetesClientOptions { /// /// Gets or sets the configuration. /// /// The configuration. public KubernetesClientConfiguration Configuration { get; set; } } ================================================ FILE: src/Kubernetes.Controller/Client/ResourceInformer.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using k8s; using k8s.Autorest; using k8s.Models; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Collections.Immutable; using System.IO; using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; using Yarp.Kubernetes.Controller.Hosting; using Yarp.Kubernetes.Controller.Rate; namespace Yarp.Kubernetes.Controller.Client; /// /// Class ResourceInformer. /// Implements the . /// Implements the . /// /// The type of the t resource. /// The type of the t resource used in lists. /// /// public abstract class ResourceInformer : BackgroundHostedService, IResourceInformer where TResource : class, IKubernetesObject, new() where TListResource : class, IKubernetesObject, IItems, new() { private readonly object _sync = new object(); private readonly GroupApiVersionKind _names; private readonly SemaphoreSlim _ready = new SemaphoreSlim(0); private readonly SemaphoreSlim _start = new SemaphoreSlim(0); private readonly ResourceSelector _selector; private ImmutableList _registrations = ImmutableList.Empty; private Dictionary> _cache = []; private string _lastResourceVersion; /// /// Initializes a new instance of the class. /// /// The client. /// A resource selector for (optionally) filtering the list of resources. /// The host application lifetime. /// The logger. public ResourceInformer( IKubernetes client, ResourceSelector selector, IHostApplicationLifetime hostApplicationLifetime, ILogger logger) : base(hostApplicationLifetime, logger) { ArgumentNullException.ThrowIfNull(client); ArgumentNullException.ThrowIfNull(selector); Client = client; _selector = selector; _names = GroupApiVersionKind.From(); } private enum EventType { SynchronizeStarted = 101, SynchronizeComplete = 102, WatchingResource = 103, ReceivedError = 104, WatchingComplete = 105, InformerWatchEvent = 106, DisposingToReconnect = 107, IgnoringError = 108, } protected IKubernetes Client { get; init; } /// protected override void Dispose(bool disposing) { if (disposing) { try { _start.Dispose(); _ready.Dispose(); } catch (ObjectDisposedException) { // ignore redundant exception to allow shutdown sequence to progress uninterrupted } } base.Dispose(disposing); } public void StartWatching() { _start.Release(); } public virtual IResourceInformerRegistration Register(ResourceInformerCallback callback) { return new Registration(this, callback); } public IResourceInformerRegistration Register(ResourceInformerCallback> callback) { return new Registration(this, (eventType, resource) => callback(eventType, resource)); } /// public virtual async Task ReadyAsync(CancellationToken cancellationToken) { await _ready.WaitAsync(cancellationToken).ConfigureAwait(false); // Release is called after each WaitAsync because // the semaphore is being used as a manual reset event _ready.Release(); } /// /// RunAsync starts processing when StartAsync is called, and is terminated when /// StopAsync is called. /// /// The cancellation token that can be used by other objects or threads to receive notice of cancellation. /// A representing the result of the asynchronous operation. public override async Task RunAsync(CancellationToken cancellationToken) { try { await _start.WaitAsync(cancellationToken).ConfigureAwait(false); var limiter = new Limiter(new Limit(0.2), 3); var shouldSync = true; var firstSync = true; while (true) { cancellationToken.ThrowIfCancellationRequested(); try { if (shouldSync) { await ListAsync(cancellationToken).ConfigureAwait(true); shouldSync = false; } if (firstSync) { _ready.Release(); firstSync = false; } await WatchAsync(cancellationToken).ConfigureAwait(true); } catch (IOException ex) when (ex.InnerException is SocketException) { Logger.LogDebug( EventId(EventType.ReceivedError), "Received error watching {ResourceType}: {ErrorMessage}", typeof(TResource).Name, ex.Message); } catch (KubernetesException ex) { Logger.LogDebug( EventId(EventType.ReceivedError), "Received error watching {ResourceType}: {ErrorMessage}", typeof(TResource).Name, ex.Message); // deal with this non-recoverable condition "too old resource version" // with a re-sync to listing everything again ensuring no subscribers miss updates if (ex is KubernetesException kubernetesError) { if (string.Equals(kubernetesError.Status.Reason, "Expired", StringComparison.Ordinal)) { shouldSync = true; } } } // rate limiting the reconnect loop await limiter.WaitAsync(cancellationToken).ConfigureAwait(true); } } catch (Exception error) { Logger.LogInformation( EventId(EventType.WatchingComplete), error, "No longer watching {ResourceType} resources from API server.", typeof(TResource).Name); throw; } } protected abstract Task> RetrieveResourceListAsync(string resourceVersion = null, ResourceSelector resourceSelector = null, CancellationToken cancellationToken = default); protected abstract Watcher WatchResourceListAsync(string resourceVersion = null, ResourceSelector resourceSelector = null, Action onEvent = null, Action onError = null, Action onClosed = null); private static EventId EventId(EventType eventType) => new EventId((int)eventType, eventType.ToString()); private async Task ListAsync(CancellationToken cancellationToken) { var previousCache = _cache; _cache = new Dictionary>(); if (_selector.FieldSelector is not null) { Logger.LogInformation( EventId(EventType.SynchronizeStarted), "Started synchronizing {ResourceType} resources from API server with field selector '{FieldSelector}'.", typeof(TResource).Name, _selector.FieldSelector); } else { Logger.LogInformation( EventId(EventType.SynchronizeStarted), "Started synchronizing {ResourceType} resources from API server.", typeof(TResource).Name); } string continueParameter = null; do { cancellationToken.ThrowIfCancellationRequested(); // request next page of items using var listWithHttpMessage = await RetrieveResourceListAsync(resourceVersion: _lastResourceVersion, resourceSelector: _selector, cancellationToken: cancellationToken); var list = listWithHttpMessage.Body; foreach (var item in list.Items) { // These properties are not already set on items while listing // assigned here for consistency item.ApiVersion = _names.GroupApiVersion; item.Kind = _names.Kind; var key = NamespacedName.From(item); _cache[key] = item?.Metadata?.OwnerReferences; var watchEventType = WatchEventType.Added; if (previousCache.Remove(key)) { // an already-known key is provided as a modification for re-sync purposes watchEventType = WatchEventType.Modified; } InvokeRegistrationCallbacks(watchEventType, item); } foreach (var (key, value) in previousCache) { // for anything which was previously known but not part of list // send a deleted notification to clear any observer caches var item = new TResource { ApiVersion = _names.GroupApiVersion, Kind = _names.Kind, Metadata = new V1ObjectMeta { Name = key.Name, NamespaceProperty = key.Namespace, OwnerReferences = value } }; InvokeRegistrationCallbacks(WatchEventType.Deleted, item); } // keep track of values needed for next page and to start watching _lastResourceVersion = list.ResourceVersion(); continueParameter = list.Continue(); } while (!string.IsNullOrEmpty(continueParameter)); Logger.LogInformation( EventId(EventType.SynchronizeComplete), "Completed synchronizing {ResourceType} resources from API server.", typeof(TResource).Name); } private async Task WatchAsync(CancellationToken cancellationToken) { Logger.LogInformation( EventId(EventType.WatchingResource), "Watching {ResourceType} starting from resource version {ResourceVersion}.", typeof(TResource).Name, _lastResourceVersion); // completion source helps turn OnClose callback into something awaitable var watcherCompletionSource = new TaskCompletionSource(); // begin watching where list left off var watcher = WatchResourceListAsync(resourceVersion: _lastResourceVersion, resourceSelector: _selector, (watchEventType, item) => { if (!watcherCompletionSource.Task.IsCompleted) { OnEvent(watchEventType, item); } }, error => { if (error is KubernetesException kubernetesError) { // deal with this non-recoverable condition "too old resource version" if (string.Equals(kubernetesError.Status.Reason, "Expired", StringComparison.Ordinal)) { // cause this error to surface watcherCompletionSource.TrySetException(error); throw error; } } Logger.LogDebug( EventId(EventType.IgnoringError), "Ignoring error {ErrorType}: {ErrorMessage}", error.GetType().Name, error.Message); }, () => { watcherCompletionSource.TrySetResult(0); } ); var lastEventUtc = DateTime.UtcNow; // reconnect if no events have arrived after a certain time using var checkLastEventUtcTimer = new Timer( _ => { var lastEvent = DateTime.UtcNow - lastEventUtc; if (lastEvent > TimeSpan.FromMinutes(9.5)) { lastEventUtc = DateTime.MaxValue; Logger.LogDebug( EventId(EventType.DisposingToReconnect), "Disposing watcher for {ResourceType} to cause reconnect.", typeof(TResource).Name); watcherCompletionSource.TrySetCanceled(); watcher.Dispose(); } }, state: null, dueTime: TimeSpan.FromSeconds(45), period: TimeSpan.FromSeconds(45)); using var registration = cancellationToken.Register(watcher.Dispose); try { await watcherCompletionSource.Task; } catch (TaskCanceledException) { } } private void OnEvent(WatchEventType watchEventType, TResource item) { if (watchEventType != WatchEventType.Modified || item.Kind != "ConfigMap") { Logger.LogDebug( EventId(EventType.InformerWatchEvent), "Informer {ResourceType} received {WatchEventType} notification for {ItemKind}/{ItemName}.{ItemNamespace} at resource version {ResourceVersion}", typeof(TResource).Name, watchEventType, item.Kind, item.Name(), item.Namespace(), item.ResourceVersion()); } if (watchEventType == WatchEventType.Added || watchEventType == WatchEventType.Modified) { // BUGBUG: log warning if cache was not in expected state _cache[NamespacedName.From(item)] = item.Metadata?.OwnerReferences; } if (watchEventType == WatchEventType.Deleted) { _cache.Remove(NamespacedName.From(item)); } if (watchEventType == WatchEventType.Added || watchEventType == WatchEventType.Modified || watchEventType == WatchEventType.Deleted || watchEventType == WatchEventType.Bookmark) { _lastResourceVersion = item.ResourceVersion(); } if (watchEventType == WatchEventType.Added || watchEventType == WatchEventType.Modified || watchEventType == WatchEventType.Deleted) { InvokeRegistrationCallbacks(watchEventType, item); } } private void InvokeRegistrationCallbacks(WatchEventType eventType, TResource resource) { List innerExceptions = default; foreach (var registration in _registrations) { try { registration.Callback.Invoke(eventType, resource); } catch (Exception innerException) { innerExceptions ??= new List(); innerExceptions.Add(innerException); } } if (innerExceptions is not null) { throw new AggregateException("One or more exceptions thrown by ResourceInformerCallback.", innerExceptions); } } internal class Registration : IResourceInformerRegistration { private bool _disposedValue; public Registration(ResourceInformer resourceInformer, ResourceInformerCallback callback) { ResourceInformer = resourceInformer; Callback = callback; lock (resourceInformer._sync) { resourceInformer._registrations = resourceInformer._registrations.Add(this); } } ~Registration() { // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method Dispose(disposing: false); } public ResourceInformer ResourceInformer { get; } public ResourceInformerCallback Callback { get; } public Task ReadyAsync(CancellationToken cancellationToken) => ResourceInformer.ReadyAsync(cancellationToken); protected virtual void Dispose(bool disposing) { if (!_disposedValue) { lock (ResourceInformer._sync) { ResourceInformer._registrations = ResourceInformer._registrations.Remove(this); } _disposedValue = true; } } public void Dispose() { // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method Dispose(disposing: true); GC.SuppressFinalize(this); } } } ================================================ FILE: src/Kubernetes.Controller/Client/ResourceSelector.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using k8s; using k8s.Models; namespace Yarp.Kubernetes.Controller.Client; /// /// Provides a mechanism for to constrain search based on fields in the resource. /// public class ResourceSelector where TResource : class, IKubernetesObject, new() { public ResourceSelector(string fieldSelector) { FieldSelector = fieldSelector; } public string FieldSelector { get; } } ================================================ FILE: src/Kubernetes.Controller/Client/V1EndpointsResourceInformer.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using k8s; using k8s.Autorest; using k8s.Models; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using System.Threading; using System.Threading.Tasks; namespace Yarp.Kubernetes.Controller.Client; internal class V1EndpointsResourceInformer : ResourceInformer { public V1EndpointsResourceInformer( IKubernetes client, ResourceSelector selector, IHostApplicationLifetime hostApplicationLifetime, ILogger logger) : base(client, selector, hostApplicationLifetime, logger) { } protected override Task> RetrieveResourceListAsync(string resourceVersion = null, ResourceSelector resourceSelector = null, CancellationToken cancellationToken = default) { return Client.CoreV1.ListEndpointsForAllNamespacesWithHttpMessagesAsync(resourceVersion: resourceVersion, fieldSelector: resourceSelector?.FieldSelector, cancellationToken: cancellationToken); } protected override Watcher WatchResourceListAsync(string resourceVersion = null, ResourceSelector resourceSelector = null, Action onEvent = null, Action onError = null, Action onClosed = null) { return Client.CoreV1.WatchListEndpointsForAllNamespaces(resourceVersion: resourceVersion, fieldSelector: resourceSelector?.FieldSelector, onEvent: onEvent, onError: onError, onClosed: onClosed); } } ================================================ FILE: src/Kubernetes.Controller/Client/V1IngressClassResourceInformer.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using k8s; using k8s.Autorest; using k8s.Models; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using System.Threading; using System.Threading.Tasks; namespace Yarp.Kubernetes.Controller.Client; internal class V1IngressClassResourceInformer : ResourceInformer { public V1IngressClassResourceInformer( IKubernetes client, ResourceSelector selector, IHostApplicationLifetime hostApplicationLifetime, ILogger logger) : base(client, selector, hostApplicationLifetime, logger) { } protected override Task> RetrieveResourceListAsync(string resourceVersion = null, ResourceSelector resourceSelector = null, CancellationToken cancellationToken = default) { return Client.NetworkingV1.ListIngressClassWithHttpMessagesAsync(resourceVersion: resourceVersion, fieldSelector: resourceSelector?.FieldSelector, cancellationToken: cancellationToken); } protected override Watcher WatchResourceListAsync(string resourceVersion = null, ResourceSelector resourceSelector = null, Action onEvent = null, Action onError = null, Action onClosed = null) { return Client.NetworkingV1.WatchListIngressClass(resourceVersion: resourceVersion, fieldSelector: resourceSelector?.FieldSelector, onEvent: onEvent, onError: onError, onClosed: onClosed); } } ================================================ FILE: src/Kubernetes.Controller/Client/V1IngressResourceInformer.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using k8s; using k8s.Autorest; using k8s.Models; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using System.Threading; using System.Threading.Tasks; namespace Yarp.Kubernetes.Controller.Client; internal class V1IngressResourceInformer : ResourceInformer { public V1IngressResourceInformer( IKubernetes client, ResourceSelector selector, IHostApplicationLifetime hostApplicationLifetime, ILogger logger) : base(client, selector, hostApplicationLifetime, logger) { } protected override Task> RetrieveResourceListAsync(string resourceVersion = null, ResourceSelector resourceSelector = null, CancellationToken cancellationToken = default) { return Client.NetworkingV1.ListIngressForAllNamespacesWithHttpMessagesAsync(resourceVersion: resourceVersion, fieldSelector: resourceSelector?.FieldSelector, cancellationToken: cancellationToken); } protected override Watcher WatchResourceListAsync(string resourceVersion = null, ResourceSelector resourceSelector = null, Action onEvent = null, Action onError = null, Action onClosed = null) { return Client.NetworkingV1.WatchListIngressForAllNamespaces(resourceVersion: resourceVersion, fieldSelector: resourceSelector?.FieldSelector, onEvent: onEvent, onError: onError, onClosed: onClosed); } } ================================================ FILE: src/Kubernetes.Controller/Client/V1IngressResourceStatusUpdater.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Linq; using k8s; using k8s.Models; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using System.Threading.Tasks; using Yarp.Kubernetes.Controller.Caching; using System.Threading; namespace Yarp.Kubernetes.Controller.Client; internal sealed class V1IngressResourceStatusUpdater : IIngressResourceStatusUpdater { private readonly IKubernetes _client; private readonly YarpOptions _options; private readonly ICache _cache; private readonly ILogger _logger; public V1IngressResourceStatusUpdater( IKubernetes client, IOptions options, ICache cache, ILogger logger) { ArgumentNullException.ThrowIfNull(options?.Value); _options = options.Value; _client = client; _cache = cache; _logger = logger; } public async Task UpdateStatusAsync(CancellationToken cancellationToken) { var service = await _client.CoreV1.ReadNamespacedServiceStatusAsync(_options.ControllerServiceName, _options.ControllerServiceNamespace, cancellationToken: cancellationToken); if (service.Status?.LoadBalancer?.Ingress is { } loadBalancerIngresses) { var status = new V1IngressStatus { LoadBalancer = new V1IngressLoadBalancerStatus { Ingress = loadBalancerIngresses?.Select(ingress => new V1IngressLoadBalancerIngress { Hostname = ingress.Hostname, Ip = ingress.Ip, Ports = ingress.Ports?.Select(port => new V1IngressPortStatus { Port = port.Port, Protocol = port.Protocol, Error = port.Error }).ToArray() }).ToArray() } }; var ingresses = _cache.GetIngresses().ToArray(); foreach (var ingress in ingresses) { _logger.LogInformation("Updating ingress {IngressClassNamespace}/{IngressClassName} status.", ingress.Metadata.NamespaceProperty, ingress.Metadata.Name); var ingressStatus = await _client.NetworkingV1.ReadNamespacedIngressStatusAsync(ingress.Metadata.Name, ingress.Metadata.NamespaceProperty, cancellationToken: cancellationToken); ingressStatus.Status = status; await _client.NetworkingV1.ReplaceNamespacedIngressStatusAsync(ingressStatus, ingress.Metadata.Name, ingress.Metadata.NamespaceProperty, cancellationToken: cancellationToken); _logger.LogInformation("Updated ingress {IngressClassNamespace}/{IngressClassName} status.", ingress.Metadata.NamespaceProperty, ingress.Metadata.Name); } } } } ================================================ FILE: src/Kubernetes.Controller/Client/V1SecretResourceInformer.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using k8s; using k8s.Autorest; using k8s.Models; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using System.Threading; using System.Threading.Tasks; namespace Yarp.Kubernetes.Controller.Client; internal class V1SecretResourceInformer : ResourceInformer { public V1SecretResourceInformer( IKubernetes client, ResourceSelector selector, IHostApplicationLifetime hostApplicationLifetime, ILogger logger) : base(client, selector, hostApplicationLifetime, logger) { } protected override Task> RetrieveResourceListAsync(string resourceVersion = null, ResourceSelector resourceSelector = null, CancellationToken cancellationToken = default) { return Client.CoreV1.ListSecretForAllNamespacesWithHttpMessagesAsync(resourceVersion: resourceVersion, fieldSelector: resourceSelector?.FieldSelector, cancellationToken: cancellationToken); } protected override Watcher WatchResourceListAsync(string resourceVersion = null, ResourceSelector resourceSelector = null, Action onEvent = null, Action onError = null, Action onClosed = null) { return Client.CoreV1.WatchListSecretForAllNamespaces(resourceVersion: resourceVersion, fieldSelector: resourceSelector?.FieldSelector, onEvent: onEvent, onError: onError, onClosed: onClosed); } } ================================================ FILE: src/Kubernetes.Controller/Client/V1ServiceResourceInformer.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using k8s; using k8s.Autorest; using k8s.Models; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using System.Threading; using System.Threading.Tasks; namespace Yarp.Kubernetes.Controller.Client; internal class V1ServiceResourceInformer : ResourceInformer { public V1ServiceResourceInformer( IKubernetes client, ResourceSelector selector, IHostApplicationLifetime hostApplicationLifetime, ILogger logger) : base(client, selector, hostApplicationLifetime, logger) { } protected override Task> RetrieveResourceListAsync(string resourceVersion = null, ResourceSelector resourceSelector = null, CancellationToken cancellationToken = default) { return Client.CoreV1.ListServiceForAllNamespacesWithHttpMessagesAsync(resourceVersion: resourceVersion, fieldSelector: resourceSelector?.FieldSelector, cancellationToken: cancellationToken); } protected override Watcher WatchResourceListAsync(string resourceVersion = null, ResourceSelector resourceSelector = null, Action onEvent = null, Action onError = null, Action onClosed = null) { return Client.CoreV1.WatchListServiceForAllNamespaces(resourceVersion: resourceVersion, fieldSelector: resourceSelector?.FieldSelector, onEvent: onEvent, onError: onError, onClosed: onClosed); } } ================================================ FILE: src/Kubernetes.Controller/ConfigProvider/IUpdateConfig.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Yarp.ReverseProxy.Configuration; namespace Yarp.Kubernetes.Controller.Configuration; public interface IUpdateConfig { Task UpdateAsync(IReadOnlyList routes, IReadOnlyList clusters, CancellationToken cancellationToken); } ================================================ FILE: src/Kubernetes.Controller/ConfigProvider/KubernetesConfigProvider.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Primitives; using Yarp.ReverseProxy.Configuration; namespace Yarp.Kubernetes.Controller.Configuration; internal class KubernetesConfigProvider : IProxyConfigProvider, IUpdateConfig { private volatile MessageConfig _config; public KubernetesConfigProvider() { _config = new MessageConfig(null, null); } public IProxyConfig GetConfig() => _config; public Task UpdateAsync(IReadOnlyList routes, IReadOnlyList clusters, CancellationToken cancellationToken) { var newConfig = new MessageConfig(routes, clusters); var oldConfig = Interlocked.Exchange(ref _config, newConfig); oldConfig.SignalChange(); return Task.CompletedTask; } private class MessageConfig : IProxyConfig { private readonly CancellationTokenSource _cts = new CancellationTokenSource(); public MessageConfig(IReadOnlyList routes, IReadOnlyList clusters) : this(routes, clusters, Guid.NewGuid().ToString()) { } public MessageConfig(IReadOnlyList routes, IReadOnlyList clusters, string revisionId) { ArgumentNullException.ThrowIfNull(revisionId); RevisionId = revisionId; Routes = routes; Clusters = clusters; ChangeToken = new CancellationChangeToken(_cts.Token); } public string RevisionId { get; } public IReadOnlyList Routes { get; } public IReadOnlyList Clusters { get; } public IChangeToken ChangeToken { get; } internal void SignalChange() { _cts.Cancel(); } } } ================================================ FILE: src/Kubernetes.Controller/Converters/ClusterTransfer.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; using Yarp.ReverseProxy.Configuration; using Yarp.ReverseProxy.Forwarder; namespace Yarp.Kubernetes.Controller.Converters; internal sealed class ClusterTransfer { public Dictionary Destinations { get; set; } = new Dictionary(); public string ClusterId { get; set; } public string LoadBalancingPolicy { get; set; } public SessionAffinityConfig SessionAffinity { get; set; } public HealthCheckConfig HealthCheck { get; set; } public HttpClientConfig HttpClientConfig { get; set; } public ForwarderRequestConfig HttpRequest { get; set; } } ================================================ FILE: src/Kubernetes.Controller/Converters/YarpConfigContext.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; using System.Linq; using Yarp.ReverseProxy.Configuration; namespace Yarp.Kubernetes.Controller.Converters; internal class YarpConfigContext { public Dictionary ClusterTransfers { get; set; } = new Dictionary(); public List Routes { get; set; } = new List(); public List BuildClusterConfig() { return ClusterTransfers.Values.Select(c => new ClusterConfig() { Destinations = c.Destinations, ClusterId = c.ClusterId, HealthCheck = c.HealthCheck, LoadBalancingPolicy = c.LoadBalancingPolicy, SessionAffinity = c.SessionAffinity, HttpClient = c.HttpClientConfig, HttpRequest = c.HttpRequest }).ToList(); } } ================================================ FILE: src/Kubernetes.Controller/Converters/YarpIngressContext.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; using Yarp.Kubernetes.Controller.Caching; namespace Yarp.Kubernetes.Controller.Converters; internal sealed class YarpIngressContext { public YarpIngressContext(IngressData ingress, List services, List endpoints) { Ingress = ingress; Services = services; Endpoints = endpoints; } public YarpIngressOptions Options { get; set; } = new YarpIngressOptions(); public IngressData Ingress { get; } public List Services { get; } public List Endpoints { get; } } ================================================ FILE: src/Kubernetes.Controller/Converters/YarpIngressOptions.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using Yarp.ReverseProxy.Configuration; using Yarp.ReverseProxy.Forwarder; namespace Yarp.Kubernetes.Controller.Converters; internal sealed class YarpIngressOptions { public bool Https { get; set; } public List> Transforms { get; set; } public string AuthorizationPolicy { get; set; } public string RateLimiterPolicy { get; set; } public string OutputCachePolicy { get; set; } public SessionAffinityConfig SessionAffinity { get; set; } public HttpClientConfig HttpClientConfig { get; set; } public ForwarderRequestConfig HttpRequest { get; set; } public string LoadBalancingPolicy { get; set; } public string CorsPolicy { get; set; } public string TimeoutPolicy { get; set; } public TimeSpan? Timeout { get; set; } public HealthCheckConfig HealthCheck { get; set; } public Dictionary RouteMetadata { get; set; } public List RouteHeaders { get; set; } public List RouteQueryParameters { get; set; } public int? RouteOrder { get; set; } public List RouteMethods { get; set; } } internal sealed class RouteHeaderWrapper { public string Name { get; init; } public List Values { get; init; } public HeaderMatchMode Mode { get; init; } public bool IsCaseSensitive { get; init; } public RouteHeader ToRouteHeader() { return new RouteHeader { Name = Name, Values = Values, Mode = Mode, IsCaseSensitive = IsCaseSensitive }; } } internal sealed class RouteQueryParameterWrapper { public string Name { get; init; } public List Values { get; init; } public QueryParameterMatchMode Mode { get; init; } public bool IsCaseSensitive { get; init; } public RouteQueryParameter ToRouteQueryParameter() { return new RouteQueryParameter { Name = Name, Values = Values, Mode = Mode, IsCaseSensitive = IsCaseSensitive }; } } ================================================ FILE: src/Kubernetes.Controller/Converters/YarpParser.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using k8s.Models; using YamlDotNet.Serialization; using Yarp.ReverseProxy.Configuration; using Yarp.Kubernetes.Controller.Caching; using System.Runtime.InteropServices; using Yarp.ReverseProxy.Forwarder; namespace Yarp.Kubernetes.Controller.Converters; internal static class YarpParser { private const string ExternalNameServiceType = "ExternalName"; private static readonly Deserializer YamlDeserializer = new(); internal static void ConvertFromKubernetesIngress(YarpIngressContext ingressContext, YarpConfigContext configContext) { var spec = ingressContext.Ingress.Spec; var defaultBackend = spec?.DefaultBackend; var defaultService = defaultBackend?.Service; IList defaultSubsets = default; if (!string.IsNullOrEmpty(defaultService?.Name)) { defaultSubsets = ingressContext.Endpoints.SingleOrDefault(x => x.Name == defaultService?.Name).Subsets; } // cluster can contain multiple replicas for each destination, need to know the lookup base don endpoints var options = HandleAnnotations(ingressContext, ingressContext.Ingress.Metadata); foreach (var rule in spec?.Rules ?? Enumerable.Empty()) { HandleIngressRule(ingressContext, ingressContext.Endpoints, defaultSubsets, rule, configContext); } } private static void HandleIngressRule(YarpIngressContext ingressContext, List endpoints, IList defaultSubsets, V1IngressRule rule, YarpConfigContext configContext) { var http = rule.Http; foreach (var path in http.Paths ?? Enumerable.Empty()) { var service = ingressContext.Services.SingleOrDefault(s => s.Metadata.Name == path.Backend.Service.Name); if (service.Spec != null) { if (string.Equals(service.Spec.Type, ExternalNameServiceType, StringComparison.OrdinalIgnoreCase)) { HandleExternalIngressRulePath(ingressContext, service.Spec.ExternalName, rule, path, configContext); } else { var servicePort = service.Spec.Ports.SingleOrDefault(p => MatchesPort(p, path.Backend.Service.Port)); if (servicePort != null) { HandleIngressRulePath(ingressContext, servicePort, endpoints, defaultSubsets, rule, path, configContext); } } } } } private static void HandleExternalIngressRulePath(YarpIngressContext ingressContext, string externalName, V1IngressRule rule, V1HTTPIngressPath path, YarpConfigContext configContext) { var backend = path.Backend; var ingressServiceBackend = backend.Service; var routes = configContext.Routes; var cluster = GetOrAddCluster(ingressContext, configContext, ingressServiceBackend); var pathMatch = FixupPathMatch(path); var host = rule.Host; routes.Add(CreateRoute(ingressContext, path, cluster, pathMatch, host)); AddDestination(cluster, ingressContext, externalName, ingressServiceBackend.Port.Number); } private static void HandleIngressRulePath(YarpIngressContext ingressContext, V1ServicePort servicePort, List endpoints, IList defaultSubsets, V1IngressRule rule, V1HTTPIngressPath path, YarpConfigContext configContext) { var backend = path.Backend; var ingressServiceBackend = backend.Service; var subsets = defaultSubsets; var routes = configContext.Routes; if (!string.IsNullOrEmpty(ingressServiceBackend?.Name)) { subsets = endpoints.SingleOrDefault(x => x.Name == ingressServiceBackend?.Name).Subsets; } var cluster = GetOrAddCluster(ingressContext, configContext, ingressServiceBackend); // make sure cluster is present foreach (var subset in subsets ?? Enumerable.Empty()) { var isRoutePresent = false; foreach (var port in subset.Ports ?? Enumerable.Empty()) { if (!MatchesPort(port, servicePort)) { continue; } if (!isRoutePresent) { var pathMatch = FixupPathMatch(path); var host = rule.Host; routes.Add(CreateRoute(ingressContext, path, cluster, pathMatch, host)); isRoutePresent = true; } // Add destination for every endpoint address foreach (var address in subset.Addresses ?? Enumerable.Empty()) { AddDestination(cluster, ingressContext, address.Ip, port.Port); } } } } private static void AddDestination(ClusterTransfer cluster, YarpIngressContext ingressContext, string host, int? port) { var isHttps = ingressContext.Options.Https || cluster.ClusterId.EndsWith(":443", StringComparison.Ordinal) || cluster.ClusterId.EndsWith(":https", StringComparison.OrdinalIgnoreCase); var protocol = isHttps ? "https" : "http"; var uri = $"{protocol}://{host}"; if (port.HasValue) { uri += $":{port}"; } cluster.Destinations[uri] = new DestinationConfig() { Address = uri }; } private static RouteConfig CreateRoute(YarpIngressContext ingressContext, V1HTTPIngressPath path, ClusterTransfer cluster, string pathMatch, string host) { return new RouteConfig() { Match = new RouteMatch() { Methods = ingressContext.Options.RouteMethods, Hosts = host is not null ? new[] { host } : Array.Empty(), Path = pathMatch, Headers = ingressContext.Options.RouteHeaders, QueryParameters = ingressContext.Options.RouteQueryParameters }, ClusterId = cluster.ClusterId, RouteId = $"{ingressContext.Ingress.Metadata.Name}.{ingressContext.Ingress.Metadata.NamespaceProperty}:{host}{path.Path}", Transforms = ingressContext.Options.Transforms, AuthorizationPolicy = ingressContext.Options.AuthorizationPolicy, RateLimiterPolicy = ingressContext.Options.RateLimiterPolicy, OutputCachePolicy = ingressContext.Options.OutputCachePolicy, Timeout = ingressContext.Options.Timeout, TimeoutPolicy = ingressContext.Options.TimeoutPolicy, CorsPolicy = ingressContext.Options.CorsPolicy, Metadata = ingressContext.Options.RouteMetadata, Order = ingressContext.Options.RouteOrder, }; } private static ClusterTransfer GetOrAddCluster(YarpIngressContext ingressContext, YarpConfigContext configContext, V1IngressServiceBackend ingressServiceBackend) { var clusters = configContext.ClusterTransfers; // Each ingress rule path can only be for one service var key = UpstreamName(ingressContext.Ingress.Metadata.NamespaceProperty, ingressServiceBackend); var cluster = CollectionsMarshal.GetValueRefOrAddDefault(clusters, key, out _) ??= new ClusterTransfer(); cluster.ClusterId = key; cluster.LoadBalancingPolicy = ingressContext.Options.LoadBalancingPolicy; cluster.SessionAffinity = ingressContext.Options.SessionAffinity; cluster.HealthCheck = ingressContext.Options.HealthCheck; cluster.HttpClientConfig = ingressContext.Options.HttpClientConfig; cluster.HttpRequest = ingressContext.Options.HttpRequest; return cluster; } private static string UpstreamName(string namespaceName, V1IngressServiceBackend ingressServiceBackend) { if (ingressServiceBackend is not null) { if (ingressServiceBackend.Port.Number.HasValue && ingressServiceBackend.Port.Number.Value > 0) { return $"{ingressServiceBackend.Name}.{namespaceName}:{ingressServiceBackend.Port.Number}"; } if (!string.IsNullOrWhiteSpace(ingressServiceBackend.Port.Name)) { return $"{ingressServiceBackend.Name}.{namespaceName}:{ingressServiceBackend.Port.Name}"; } } return $"{namespaceName}-INVALID"; } private static string FixupPathMatch(V1HTTPIngressPath path) { var pathMatch = path.Path; // Prefix match is the default for implementation specific. if (string.Equals(path.PathType, "Prefix", StringComparison.OrdinalIgnoreCase) || string.Equals(path.PathType, "ImplementationSpecific", StringComparison.OrdinalIgnoreCase)) { if (!pathMatch.EndsWith('/')) { pathMatch += "/"; } // remember for prefix matches, /foo/ works for either /foo or /foo/ pathMatch += "{**catch-all}"; } return pathMatch; } private static YarpIngressOptions HandleAnnotations(YarpIngressContext context, V1ObjectMeta metadata) { var options = context.Options; var annotations = metadata.Annotations; if (annotations is null) { return options; } if (annotations.TryGetValue("yarp.ingress.kubernetes.io/backend-protocol", out var http)) { options.Https = http.Equals("https", StringComparison.OrdinalIgnoreCase); } if (annotations.TryGetValue("yarp.ingress.kubernetes.io/transforms", out var transforms)) { options.Transforms = YamlDeserializer.Deserialize>>(transforms); } if (annotations.TryGetValue("yarp.ingress.kubernetes.io/authorization-policy", out var authorizationPolicy)) { options.AuthorizationPolicy = authorizationPolicy; } if (annotations.TryGetValue("yarp.ingress.kubernetes.io/rate-limiter-policy", out var rateLimiterPolicy)) { options.RateLimiterPolicy = rateLimiterPolicy; } if (annotations.TryGetValue("yarp.ingress.kubernetes.io/output-cache-policy", out var outputCachePolicy)) { options.OutputCachePolicy = outputCachePolicy; } if (annotations.TryGetValue("yarp.ingress.kubernetes.io/timeout", out var timeout)) { options.Timeout = TimeSpan.Parse(timeout, CultureInfo.InvariantCulture); } if (annotations.TryGetValue("yarp.ingress.kubernetes.io/timeout-policy", out var timeoutPolicy)) { options.TimeoutPolicy = timeoutPolicy; } if (annotations.TryGetValue("yarp.ingress.kubernetes.io/cors-policy", out var corsPolicy)) { options.CorsPolicy = corsPolicy; } if (annotations.TryGetValue("yarp.ingress.kubernetes.io/session-affinity", out var sessionAffinity)) { options.SessionAffinity = YamlDeserializer.Deserialize(sessionAffinity); } if (annotations.TryGetValue("yarp.ingress.kubernetes.io/load-balancing", out var loadBalancing)) { options.LoadBalancingPolicy = loadBalancing; } if (annotations.TryGetValue("yarp.ingress.kubernetes.io/http-client", out var httpClientConfig)) { options.HttpClientConfig = YamlDeserializer.Deserialize(httpClientConfig); } if (annotations.TryGetValue("yarp.ingress.kubernetes.io/http-request", out var httpRequest)) { options.HttpRequest = YamlDeserializer.Deserialize(httpRequest); } if (annotations.TryGetValue("yarp.ingress.kubernetes.io/health-check", out var healthCheck)) { options.HealthCheck = YamlDeserializer.Deserialize(healthCheck); } if (annotations.TryGetValue("yarp.ingress.kubernetes.io/route-metadata", out var routeMetadata)) { options.RouteMetadata = YamlDeserializer.Deserialize>(routeMetadata); } if (annotations.TryGetValue("yarp.ingress.kubernetes.io/route-headers", out var routeHeaders)) { // YamlDeserializer does not support IReadOnlyList in RouteHeader for now, so we use RouteHeaderWrapper to solve this problem. options.RouteHeaders = YamlDeserializer.Deserialize>(routeHeaders).Select(p => p.ToRouteHeader()).ToList(); } if (annotations.TryGetValue("yarp.ingress.kubernetes.io/route-queryparameters", out var routeQueryParameters)) { // YamlDeserializer does not support IReadOnlyList in RouteParameters for now, so we use RouterQueryParameterWrapper to solve this problem. options.RouteQueryParameters = YamlDeserializer.Deserialize>(routeQueryParameters).Select(p => p.ToRouteQueryParameter()).ToList(); } if (annotations.TryGetValue("yarp.ingress.kubernetes.io/route-order", out var routeOrder)) { options.RouteOrder = int.Parse(routeOrder, CultureInfo.InvariantCulture); } if (annotations.TryGetValue("yarp.ingress.kubernetes.io/route-methods", out var routeMethods)) { options.RouteMethods = YamlDeserializer.Deserialize>(routeMethods); } // metadata to support: // rewrite target // auth // http or https // default backend // CORS // GRPC // HTTP2 // Connection limits // rate limits // backend health checks. return options; } private static bool MatchesPort(Corev1EndpointPort port1, V1ServicePort port2) { if (port1 is null || port2?.TargetPort is null) { return false; } if (int.TryParse(port2.TargetPort, out var port2Number) && port2Number == port1.Port) { return true; } if (string.Equals(port2.Name, port1.Name, StringComparison.OrdinalIgnoreCase)) { return true; } return false; } private static bool MatchesPort(V1ServicePort port1, V1ServiceBackendPort port2) { if (port1 is null || port2 is null) { return false; } if (port2.Number is not null && port2.Number == port1.Port) { return true; } if (port2.Name is not null && string.Equals(port2.Name, port1.Name, StringComparison.Ordinal)) { return true; } return false; } } ================================================ FILE: src/Kubernetes.Controller/Hosting/BackgroundHostedService.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using System; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace Yarp.Kubernetes.Controller.Hosting; /// /// Class BackgroundHostedService. /// Implements the /// Implements the . /// /// /// public abstract class BackgroundHostedService : IHostedService, IDisposable { private readonly IHostApplicationLifetime _hostApplicationLifetime; private readonly CancellationTokenRegistration _hostApplicationStoppingRegistration; private readonly CancellationTokenSource _runCancellation = new CancellationTokenSource(); private readonly string _serviceTypeName; private bool _disposedValue; private Task _runTask; /// /// Initializes a new instance of the class. /// /// The host application lifetime. /// The logger. protected BackgroundHostedService( IHostApplicationLifetime hostApplicationLifetime, ILogger logger) { ArgumentNullException.ThrowIfNull(hostApplicationLifetime); ArgumentNullException.ThrowIfNull(logger); _hostApplicationLifetime = hostApplicationLifetime; Logger = logger; // register the stoppingToken to become cancelled as soon as the // shutdown sequence is initiated. _hostApplicationStoppingRegistration = _hostApplicationLifetime.ApplicationStopping.Register(_runCancellation.Cancel); var serviceType = GetType(); if (serviceType.IsGenericType) { _serviceTypeName = $"{serviceType.Name.Split('`').First()}<{string.Join(",", serviceType.GenericTypeArguments.Select(type => type.Name))}>"; } else { _serviceTypeName = serviceType.Name; } } /// /// Gets or sets the logger. /// /// The logger. protected ILogger Logger { get; set; } /// /// Triggered when the application host is ready to start the service. /// /// Indicates that the start process has been aborted. /// Task. public Task StartAsync(CancellationToken cancellationToken) { // fork off a new async causality line beginning with the call to RunAsync _runTask = Task.Run(CallRunAsync, CancellationToken.None); // the rest of the startup sequence should proceed without delay return Task.CompletedTask; // entry-point to run async background work separated from the startup sequence async Task CallRunAsync() { // don't bother running in case of abnormally early shutdown _runCancellation.Token.ThrowIfCancellationRequested(); try { Logger?.LogInformation( new EventId(1, "RunStarting"), "Calling RunAsync for {BackgroundHostedService}", _serviceTypeName); try { // call the overridden method await RunAsync(_runCancellation.Token).ConfigureAwait(true); } finally { Logger?.LogInformation( new EventId(2, "RunComplete"), "RunAsync completed for {BackgroundHostedService}", _serviceTypeName); } } catch { if (!_hostApplicationLifetime.ApplicationStopping.IsCancellationRequested) { // For any exception the application is instructed to tear down. // this would normally happen if IHostedService.StartAsync throws, so it // is a safe assumption the intent of an unhandled exception from background // RunAsync is the same. _hostApplicationLifetime.StopApplication(); Logger?.LogInformation( new EventId(3, "RequestedStopApplication"), "Called StopApplication for {BackgroundHostedService}", _serviceTypeName); } throw; } } } /// /// stop as an asynchronous operation. /// /// Indicates that the shutdown process should no longer be graceful. /// A representing the result of the asynchronous operation. public async Task StopAsync(CancellationToken cancellationToken) { try { // signal for the RunAsync call to be completed _runCancellation.Cancel(); // join the result of the RunAsync causality line back into the results of // this StopAsync call. this await statement will not complete until CallRunAsync // method has unwound and returned. if RunAsync completed by throwing an exception // it will be rethrown by this await. rethrown Exceptions will pass through // Hosting and may be caught by Program.Main. await _runTask.ConfigureAwait(false); } catch (OperationCanceledException) { // this exception is ignored - it's a natural result of cancellation token } finally { _runTask = null; } } /// /// Runs the asynchronous background work. /// /// The cancellation token that can be used by other objects or threads to receive notice of cancellation. /// A representing the result of the asynchronous operation. public abstract Task RunAsync(CancellationToken cancellationToken); protected virtual void Dispose(bool disposing) { if (!_disposedValue) { if (disposing) { try { _runCancellation.Dispose(); } catch (ObjectDisposedException) { // ignore redundant exception to allow shutdown sequence to progress uninterrupted } try { _hostApplicationStoppingRegistration.Dispose(); } catch (ObjectDisposedException) { // ignore redundant exception to allow shutdown sequence to progress uninterrupted } } _disposedValue = true; } } public void Dispose() { // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method Dispose(disposing: true); GC.SuppressFinalize(this); } } ================================================ FILE: src/Kubernetes.Controller/Hosting/HostedServiceAdapter.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Extensions.Hosting; using System.Threading; using System.Threading.Tasks; namespace Yarp.Kubernetes.Controller.Hosting; /// /// Delegates start and stop calls to service-specific interface. /// /// The service interface to delegate onto. public class HostedServiceAdapter : IHostedService where TService : IHostedService { private readonly TService _service; /// /// Initializes a new instance of the class. /// /// The service interface to delegate onto. public HostedServiceAdapter(TService service) => _service = service; public Task StartAsync(CancellationToken cancellationToken) => _service.StartAsync(cancellationToken); public Task StopAsync(CancellationToken cancellationToken) => _service.StopAsync(cancellationToken); } ================================================ FILE: src/Kubernetes.Controller/Hosting/ServiceCollectionHostedServiceAdapterExtensions.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Extensions.Hosting; using System.Linq; using Yarp.Kubernetes.Controller.Hosting; namespace Microsoft.Extensions.DependencyInjection; /// /// Class ServiceCollectionHostedServiceAdapterExtensions. /// public static class ServiceCollectionHostedServiceAdapterExtensions { /// /// Registers the hosted service. /// /// The type of the t service. /// The services. /// IServiceCollection. public static IServiceCollection RegisterHostedService(this IServiceCollection services) where TService : IHostedService { if (!services.Any(serviceDescriptor => serviceDescriptor.ServiceType == typeof(HostedServiceAdapter))) { services = services.AddHostedService>(); } return services; } } ================================================ FILE: src/Kubernetes.Controller/Management/KubernetesCoreExtensions.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using k8s; using Microsoft.Extensions.Options; using System.Linq; using Yarp.Kubernetes.Controller.Client; namespace Microsoft.Extensions.DependencyInjection; /// /// Class KubernetesCoreExtensions. /// public static class KubernetesCoreExtensions { /// /// Adds the kubernetes. /// /// The services. /// IServiceCollection. public static IServiceCollection AddKubernetesCore(this IServiceCollection services) { if (!services.Any(serviceDescriptor => serviceDescriptor.ServiceType == typeof(IKubernetes))) { services = services.Configure(options => { options.Configuration ??= KubernetesClientConfiguration.BuildDefaultConfig(); }); services = services.AddSingleton(sp => { var options = sp.GetRequiredService>().Value; return new Kubernetes(options.Configuration); }); } return services; } } ================================================ FILE: src/Kubernetes.Controller/Management/KubernetesReverseProxyServiceCollectionExtensions.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Diagnostics.CodeAnalysis; using k8s; using k8s.Models; using Microsoft.Extensions.Configuration; using Yarp.Kubernetes.Controller; using Yarp.Kubernetes.Controller.Caching; using Yarp.Kubernetes.Controller.Certificates; using Yarp.Kubernetes.Controller.Client; using Yarp.Kubernetes.Controller.Configuration; using Yarp.Kubernetes.Controller.Controllers; using Yarp.Kubernetes.Controller.Dispatching; using Yarp.Kubernetes.Controller.Protocol; using Yarp.Kubernetes.Controller.Services; using Yarp.ReverseProxy.Configuration; namespace Microsoft.Extensions.DependencyInjection; /// /// Extensions for /// used to register the Kubernetes-based ReverseProxy's components. /// public static class KubernetesReverseProxyServiceCollectionExtensions { /// /// Adds ReverseProxy's services to Dependency Injection. /// /// Dependency injection registration. /// Application configuration. /// The same for chaining. public static IReverseProxyBuilder AddKubernetesReverseProxy(this IServiceCollection services, IConfiguration config) { // Add components from the kubernetes controller framework services.AddKubernetesControllerRuntime(config); // Add the in-memory configuration cache. var provider = new KubernetesConfigProvider(); services.AddSingleton(provider); services.AddSingleton(provider); return services.AddReverseProxy(); } /// /// Adds an ingress controller that monitors for Ingress resource changes and notifies a Yarp "Ingress" application. /// /// Dependency injection registration. /// Application configuration. /// The same for chaining. public static IServiceCollection AddKubernetesIngressMonitor(this IServiceCollection services, IConfiguration config) { // Add components from the kubernetes controller framework services.AddKubernetesControllerRuntime(config); // Add the dispatcher for the Ingress application to connect to. services.AddSingleton(); services.AddSingleton(); return services; } /// /// Adds the dispatching controller that allows a Yarp "Ingress" application to monitor for changes. /// /// The MVC builder. /// The same for chaining. public static IMvcBuilder AddKubernetesDispatchController(this IMvcBuilder builder) { return builder.AddApplicationPart(typeof(DispatchController).Assembly); } public static IServiceCollection AddKubernetesControllerRuntime(this IServiceCollection services, IConfiguration config) { ArgumentNullException.ThrowIfNull(config, nameof(config)); // Add components implemented by this application services.AddHostedService(); services.AddSingleton(); services.AddTransient(); services.Configure(config.GetSection("Yarp")); // Register the necessary Kubernetes resource informers services.RegisterResourceInformer(); services.RegisterResourceInformer(); services.RegisterResourceInformer(); services.RegisterResourceInformer(); // We should only retrieve secrets we might be interested in (because Helm V3, for example, can generate lots of secrets) services.RegisterResourceInformer("type=kubernetes.io/tls"); // Add the Ingress/Secret to certificate management services.AddSingleton(); services.AddSingleton(); // ingress status updater services.AddSingleton(); return services.AddKubernetesCore(); } /// /// Registers the resource informer. /// /// The type of the t related resource. /// The implementation type of the resource informer. /// The services. /// IServiceCollection. public static IServiceCollection RegisterResourceInformer(this IServiceCollection services) where TResource : class, IKubernetesObject, new() where TService : IResourceInformer { return services.RegisterResourceInformer(null); } /// /// Registers the resource informer with a field selector. /// /// The type of the t related resource. /// The implementation type of the resource informer. /// The services. /// A field selector to constrain the resources the informer retrieves. /// IServiceCollection. public static IServiceCollection RegisterResourceInformer(this IServiceCollection services, string fieldSelector) where TResource : class, IKubernetesObject, new() where TService : IResourceInformer { services.AddSingleton(new ResourceSelector(fieldSelector)); services.AddSingleton(typeof(IResourceInformer), typeof(TService)); return services.RegisterHostedService>(); } } ================================================ FILE: src/Kubernetes.Controller/Management/KubernetesReverseProxyWebHostBuilderExtensions.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using Microsoft.Extensions.DependencyInjection; using Yarp.Kubernetes.Controller.Certificates; namespace Microsoft.AspNetCore.Hosting; /// /// Extensions for /// used to register the Kubernetes-based ReverseProxy's components. /// public static class KubernetesReverseProxyWebHostBuilderExtensions { /// /// Configures Kestrel for SNI-based certificate selection using Kubernetes Ingress TLS annotations and Kubernetes Secrets. /// /// The web host builder. /// The same for chaining. public static IWebHostBuilder UseKubernetesReverseProxyCertificateSelector(this IWebHostBuilder builder) { ArgumentNullException.ThrowIfNull(builder, nameof(builder)); builder.ConfigureKestrel(kestrelOptions => { kestrelOptions.ConfigureHttpsDefaults(httpsOptions => { var selector = kestrelOptions.ApplicationServices.GetService(); if (selector is null) { throw new InvalidOperationException("Missing required services. Did you call '.AddKubernetesReverseProxy()' when configuring services?"); } httpsOptions.ServerCertificateSelector = (connectionContext, domainName) => { return selector.GetCertificate(connectionContext, domainName); }; }); }); return builder; } } ================================================ FILE: src/Kubernetes.Controller/NamespacedName.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using k8s; using k8s.Models; using System; using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; namespace Yarp.Kubernetes.Controller; /// /// Struct NamespacedName is a value that acts as a dictionary key. It is a comparable /// combination of a metadata namespace and name. /// Implements the . /// /// public struct NamespacedName : IEquatable { /// /// Initializes a new instance of the struct. /// /// The namespace. /// The name. [JsonConstructor] public NamespacedName(string @namespace, string name) { Namespace = @namespace; Name = name; } /// /// Initializes a new instance of the struct. /// /// The name. public NamespacedName(string name) { Namespace = null; Name = name; } /// /// Gets the namespace. /// /// The namespace. public string Namespace { get; } /// /// Gets the name. /// /// The name. public string Name { get; } /// /// Implements the == operator. /// /// The left. /// The right. /// The result of the operator. public static bool operator ==(NamespacedName left, NamespacedName right) { return left.Equals(right); } /// /// Implements the != operator. /// /// The left. /// The right. /// The result of the operator. public static bool operator !=(NamespacedName left, NamespacedName right) { return !(left == right); } /// /// Gets key values from the specified resource. /// /// The resource. /// NamespacedName. public static NamespacedName From(IKubernetesObject resource) { ArgumentNullException.ThrowIfNull(resource); return new NamespacedName(resource.Namespace(), resource.Name()); } /// /// Gets key values from the specified metadata. /// /// The metadata. /// The owner reference. /// if set to true [cluster scoped]. /// NamespacedName. public static NamespacedName From(V1ObjectMeta metadata, [NotNull] V1OwnerReference ownerReference, bool? clusterScoped = null) { ArgumentNullException.ThrowIfNull(metadata); ArgumentNullException.ThrowIfNull(ownerReference); return new NamespacedName( clusterScoped ?? false ? null : metadata.NamespaceProperty, ownerReference.Name); } public override bool Equals(object obj) { return obj is NamespacedName name && Equals(name); } public bool Equals([AllowNull] NamespacedName other) { return Namespace == other.Namespace && Name == other.Name; } public override int GetHashCode() { return HashCode.Combine(Namespace, Name); } /// /// Returns a that represents this instance. /// /// A that represents this instance. public override string ToString() { return $"{Namespace}/{Name}"; } } ================================================ FILE: src/Kubernetes.Controller/Properties/AssemblyInfo.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("Yarp.Kubernetes.Tests, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")] ================================================ FILE: src/Kubernetes.Controller/Protocol/DispatchActionResult.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Yarp.Kubernetes.Protocol; namespace Yarp.Kubernetes.Controller.Dispatching; /// /// DispatchActionResult is an IActionResult which registers itself as /// an IDispatchTarget with the provided IDispatcher. As long as the client /// is connected this result will continue to write data to the response. /// public class DispatchActionResult : IActionResult, IDispatchTarget { private static readonly byte[] _newline = Encoding.UTF8.GetBytes(Environment.NewLine); private readonly IDispatcher _dispatcher; private Task _task = Task.CompletedTask; private readonly object _taskSync = new(); private HttpContext _httpContext; public DispatchActionResult(IDispatcher dispatcher) { _dispatcher = dispatcher; } public override string ToString() { return $"{_httpContext?.Connection.Id}:{_httpContext?.TraceIdentifier}"; } public async Task ExecuteResultAsync(ActionContext context) { ArgumentNullException.ThrowIfNull(context); var cancellationToken = context.HttpContext.RequestAborted; _httpContext = context.HttpContext; _httpContext.Response.ContentType = "text/plain"; _httpContext.Response.Headers["Connection"] = "close"; await _httpContext.Response.Body.FlushAsync(cancellationToken).ConfigureAwait(false); await _dispatcher.AttachAsync(this, cancellationToken).ConfigureAwait(false); try { var utf8Bytes = JsonSerializer.SerializeToUtf8Bytes(new Message { MessageType = MessageType.Heartbeat }); while (!cancellationToken.IsCancellationRequested) { await Task.Delay(TimeSpan.FromSeconds(35), cancellationToken).ConfigureAwait(false); await SendAsync(utf8Bytes, cancellationToken).ConfigureAwait(false); } } catch (TaskCanceledException) { // This is fine. } finally { _dispatcher.Detach(this); } } public async Task SendAsync(byte[] bytes, CancellationToken cancellationToken) { var result = Task.CompletedTask; lock (_taskSync) { cancellationToken.ThrowIfCancellationRequested(); if (_task.IsCanceled || _task.IsFaulted) { result = _task; } else { _task = DoSendAsync(_task, bytes); } async Task DoSendAsync(Task task, byte[] bytes) { await task.ConfigureAwait(false); await _httpContext.Response.BodyWriter.WriteAsync(bytes, cancellationToken); await _httpContext.Response.BodyWriter.WriteAsync(_newline, cancellationToken); await _httpContext.Response.BodyWriter.FlushAsync(cancellationToken); } } await result.ConfigureAwait(false); } } ================================================ FILE: src/Kubernetes.Controller/Protocol/DispatchConfigProvider.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Linq; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Yarp.Kubernetes.Controller.Configuration; using Yarp.Kubernetes.Controller.Dispatching; using Yarp.Kubernetes.Protocol; using Yarp.ReverseProxy.Configuration; namespace Yarp.Kubernetes.Controller.Protocol; public class DispatchConfigProvider : IUpdateConfig { private readonly IDispatcher _dispatcher; public DispatchConfigProvider(IDispatcher dispatcher) { ArgumentNullException.ThrowIfNull(dispatcher); _dispatcher = dispatcher; } public async Task UpdateAsync(IReadOnlyList routes, IReadOnlyList clusters, CancellationToken cancellationToken) { var message = new Message { MessageType = MessageType.Update, Key = string.Empty, Cluster = clusters.ToList(), Routes = routes.ToList(), }; var bytes = JsonSerializer.SerializeToUtf8Bytes(message); await _dispatcher.SendAsync(bytes, cancellationToken).ConfigureAwait(false); } } ================================================ FILE: src/Kubernetes.Controller/Protocol/DispatchController.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Yarp.Kubernetes.Controller.Dispatching; namespace Yarp.Kubernetes.Controller.Controllers; /// /// DispatchController provides API used by callers to begin streaming /// information being sent out through the muxer. /// [Route("api/dispatch")] [ApiController] public class DispatchController : ControllerBase { private readonly IDispatcher _dispatcher; public DispatchController(IDispatcher dispatcher) { _dispatcher = dispatcher; } [HttpGet("/api/dispatch")] public Task WatchAsync() { return Task.FromResult(new DispatchActionResult(_dispatcher)); } } ================================================ FILE: src/Kubernetes.Controller/Protocol/Dispatcher.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Extensions.Logging; using System.Collections.Immutable; using System.Threading; using System.Threading.Tasks; namespace Yarp.Kubernetes.Controller.Dispatching; /// /// IDispatcher is a service interface to bridge outgoing data to the /// current connections. /// public class Dispatcher : IDispatcher { private readonly ILogger _logger; private readonly object _targetsSync = new object(); private ImmutableList _targets = ImmutableList.Empty; private byte[] _lastMessage; public Dispatcher(ILogger logger) { _logger = logger; } public async Task AttachAsync(IDispatchTarget target, CancellationToken cancellationToken) { _logger.LogDebug("Attaching {DispatchTarget}", target?.ToString()); lock (_targetsSync) { _targets = _targets.Add(target); } if (_lastMessage is not null) { await target.SendAsync(_lastMessage, cancellationToken).ConfigureAwait(false); } } public void Detach(IDispatchTarget target) { _logger.LogDebug("Detaching {DispatchTarget}", target?.ToString()); lock (_targetsSync) { _targets = _targets.Remove(target); } } public async Task SendAsync(byte[] utf8Bytes, CancellationToken cancellationToken) { _lastMessage = utf8Bytes; foreach (var target in _targets) { await target.SendAsync(utf8Bytes, cancellationToken).ConfigureAwait(false); } } } ================================================ FILE: src/Kubernetes.Controller/Protocol/IDispatchTarget.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Threading; using System.Threading.Tasks; namespace Yarp.Kubernetes.Controller.Dispatching; /// /// IDispatchTarget is what an will use to /// dispatch information. /// public interface IDispatchTarget { public Task SendAsync(byte[] utf8Bytes, CancellationToken cancellationToken); } ================================================ FILE: src/Kubernetes.Controller/Protocol/IDispatcher.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Threading; using System.Threading.Tasks; namespace Yarp.Kubernetes.Controller.Dispatching; /// /// IDispatcher is a service interface to bridge outgoing data to the /// current connections. /// public interface IDispatcher { Task AttachAsync(IDispatchTarget target, CancellationToken cancellationToken); void Detach(IDispatchTarget target); Task SendAsync(byte[] utf8Bytes, CancellationToken cancellationToken); } ================================================ FILE: src/Kubernetes.Controller/Protocol/Message.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; using System.Text.Json.Serialization; using Yarp.ReverseProxy.Configuration; namespace Yarp.Kubernetes.Protocol; public enum MessageType { Heartbeat, Update, Remove, } public struct Message { [JsonConverter(typeof(JsonStringEnumConverter))] public MessageType MessageType { get; set; } public string Key { get; set; } public List Routes { get; set; } public List Cluster { get; set; } } ================================================ FILE: src/Kubernetes.Controller/Protocol/MessageConfigProviderExtensions.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using Microsoft.Extensions.DependencyInjection; using Yarp.Kubernetes.Controller.Configuration; using Yarp.ReverseProxy.Configuration; namespace Yarp.Kubernetes.Protocol; public static class MessageConfigProviderExtensions { public static IReverseProxyBuilder LoadFromMessages(this IReverseProxyBuilder builder) { ArgumentNullException.ThrowIfNull(builder); var provider = new KubernetesConfigProvider(); builder.Services.AddSingleton(provider); builder.Services.AddSingleton(provider); return builder; } } ================================================ FILE: src/Kubernetes.Controller/Protocol/Receiver.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.IO; using System.Net.Http; using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Yarp.Kubernetes.Controller.Configuration; using Yarp.Kubernetes.Controller.Hosting; using Yarp.Kubernetes.Controller.Rate; namespace Yarp.Kubernetes.Protocol; public class Receiver : BackgroundHostedService { private readonly ReceiverOptions _options; private readonly Limiter _limiter; private readonly IUpdateConfig _proxyConfigProvider; public Receiver( IOptions options, IHostApplicationLifetime hostApplicationLifetime, ILogger logger, IUpdateConfig proxyConfigProvider) : base(hostApplicationLifetime, logger) { ArgumentNullException.ThrowIfNull(options); _options = options.Value; _options.Client ??= new HttpMessageInvoker(new SocketsHttpHandler { ConnectTimeout = TimeSpan.FromSeconds(15), }); // two requests per second after third failure _limiter = new Limiter(new Limit(2), 3); _proxyConfigProvider = proxyConfigProvider; } public override async Task RunAsync(CancellationToken cancellationToken) { while (!cancellationToken.IsCancellationRequested) { await _limiter.WaitAsync(cancellationToken).ConfigureAwait(false); Logger.LogInformation("Connecting with {ControllerUrl}", _options.ControllerUrl.ToString()); try { var requestMessage = new HttpRequestMessage(HttpMethod.Get, _options.ControllerUrl); var responseMessage = await _options.Client.SendAsync(requestMessage, cancellationToken).ConfigureAwait(false); using var stream = await responseMessage.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); using var reader = new StreamReader(stream, Encoding.UTF8, leaveOpen: true); using var cancellation = cancellationToken.Register(stream.Close); while (true) { var json = await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false); if (string.IsNullOrEmpty(json)) { break; } var message = System.Text.Json.JsonSerializer.Deserialize(json); Logger.LogInformation("Received {MessageType} for {MessageKey}", message.MessageType, message.Key); Logger.LogInformation(json); Logger.LogInformation(message.MessageType.ToString()); if (message.MessageType == MessageType.Update) { await _proxyConfigProvider.UpdateAsync(message.Routes, message.Cluster, cancellation.Token).ConfigureAwait(false); } } } catch (Exception ex) { Logger.LogInformation(ex, "Stream ended"); } } } } ================================================ FILE: src/Kubernetes.Controller/Protocol/ReceiverOptions.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Net.Http; namespace Yarp.Kubernetes.Protocol; public class ReceiverOptions { public Uri ControllerUrl { get; set; } public HttpMessageInvoker Client { get; set; } } ================================================ FILE: src/Kubernetes.Controller/Queues/IWorkQueue.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Threading; using System.Threading.Tasks; namespace Yarp.Kubernetes.Controller.Queues; /// /// Interface IWorkQueue holds a series of work item objects. When objects are removed from the queue they are noted /// as well in a processing set. If new items arrive while processing they are not added to the queue until /// the processing of that item is . In this way processing the same item twice simultaneously due to /// incoming event notifications is not possible. /// Ported from https://github.com/kubernetes/client-go/blob/master/util/workqueue/queue.go. /// /// The type of the t item. public interface IWorkQueue : IDisposable { /// /// Adds the specified item. /// /// The item. void Add(TItem item); /// /// Returns number of items actively waiting in queue. /// /// System.Int32. int Len(); /// /// Gets the next item in the queue once it is available. /// /// The cancellation token that can be used by other objects or threads to receive notice of cancellation. /// Task<System.ValueTuple<TItem, System.Boolean>>. Task<(TItem item, bool shutdown)> GetAsync(CancellationToken cancellationToken); /// /// Called after to inform the queue that the item /// processing is complete. /// /// The item. void Done(TItem item); /// /// Shuts down. /// void ShutDown(); /// /// Shutting down. /// /// true if XXXX, false otherwise. bool ShuttingDown(); } ================================================ FILE: src/Kubernetes.Controller/Queues/ProcessingRateLimitedQueue.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Threading; using System.Threading.Tasks; using Yarp.Kubernetes.Controller.Rate; namespace Yarp.Kubernetes.Controller.Queues; public class ProcessingRateLimitedQueue : WorkQueue { private readonly Limiter _limiter; public ProcessingRateLimitedQueue(double perSecond, int burst) { _limiter = new Limiter(new Limit(perSecond), burst); } protected override async Task OnGetAsync(CancellationToken cancellationToken) { var delay = _limiter.Reserve().Delay(); await Task.Delay(delay, cancellationToken); } } ================================================ FILE: src/Kubernetes.Controller/Queues/WorkQueue.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; namespace Yarp.Kubernetes.Controller.Queues; /// /// Class WorkQueue is the default implementation of a work queue. /// Implements the . /// /// The type of the t item. /// public class WorkQueue : IWorkQueue { private readonly object _sync = new object(); private readonly Dictionary _dirty = new Dictionary(); private readonly Dictionary _processing = new Dictionary(); private readonly Queue _queue = new Queue(); private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(0); private readonly CancellationTokenSource _shuttingDown = new CancellationTokenSource(); private bool _disposedValue; // To detect redundant calls /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// public void Dispose() { // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method Dispose(disposing: true); GC.SuppressFinalize(this); } /// /// Adds the specified item. /// /// The item. public void Add(TItem item) { lock (_sync) { if (_shuttingDown.IsCancellationRequested) { return; } if (_dirty.ContainsKey(item)) { return; } _dirty.Add(item, null); if (_processing.ContainsKey(item)) { return; } _queue.Enqueue(item); _semaphore.Release(); } } /// /// Gets the specified cancellation token. /// /// The cancellation token that can be used by other objects or threads to receive notice of cancellation. /// Task<System.ValueTuple<TItem, System.Boolean>>. public async Task<(TItem item, bool shutdown)> GetAsync(CancellationToken cancellationToken) { using (var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _shuttingDown.Token)) { try { await _semaphore.WaitAsync(linkedTokenSource.Token); await OnGetAsync(linkedTokenSource.Token); } catch (OperationCanceledException) { if (_shuttingDown.IsCancellationRequested) { return (default, true); } throw; } } lock (_sync) { if (_queue.Count == 0 || _shuttingDown.IsCancellationRequested) { _semaphore.Release(); return (default, true); } var item = _queue.Dequeue(); _processing.Add(item, null); _dirty.Remove(item); return (item, false); } } /// /// Done the specified item. /// /// The item. public void Done(TItem item) { lock (_sync) { _processing.Remove(item); if (_dirty.ContainsKey(item)) { _queue.Enqueue(item); _semaphore.Release(); } } } /// /// Gets the queue length of this instance. /// public int Len() { lock (_sync) { return _queue.Count; } } /// /// Shuts down. /// public void ShutDown() { lock (_sync) { _shuttingDown.Cancel(); _semaphore.Release(); } } /// /// Shutting down. /// /// true if XXXX, false otherwise. public bool ShuttingDown() { return _shuttingDown.IsCancellationRequested; } /// /// Releases unmanaged and - optionally - managed resources. /// /// true to release both managed and unmanaged resources; false to release only unmanaged resources. protected virtual void Dispose(bool disposing) { if (!_disposedValue) { if (disposing) { _semaphore.Dispose(); } _disposedValue = true; } } /// /// Called in GetAsync BEFORE the items is dequeued to allow rate-limiting of processing. /// /// The cancellation token that can be used by other objects or threads to receive notice of cancellation /// A task. protected virtual Task OnGetAsync(CancellationToken cancellationToken) { return Task.CompletedTask; } } ================================================ FILE: src/Kubernetes.Controller/Rate/Limit.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Diagnostics.CodeAnalysis; namespace Yarp.Kubernetes.Controller.Rate; /// /// Struct Limit defines the maximum frequency of some events. /// Limit is represented as number of events per second. /// A zero Limit allows no events. /// https://github.com/golang/time/blob/master/rate/rate.go#L19 /// Implements the . /// /// public struct Limit : IEquatable { private readonly double _tokensPerSecond; /// /// Initializes a new instance of the struct. /// /// The per second. public Limit(double perSecond) { _tokensPerSecond = perSecond; } /// /// Gets a predefined maximum . /// /// The maximum. public static Limit Max { get; } = new Limit(double.MaxValue); /// /// Implements the == operator. /// /// The left. /// The right. /// The result of the operator. public static bool operator ==(Limit left, Limit right) { return left.Equals(right); } /// /// Implements the != operator. /// /// The left. /// The right. /// The result of the operator. public static bool operator !=(Limit left, Limit right) { return !(left == right); } /// /// TokensFromDuration is a unit conversion function from a time duration to the number of tokens /// which could be accumulated during that duration at a rate of limit tokens per second. /// https://github.com/golang/time/blob/master/rate/rate.go#L396. /// /// The duration. /// System.Double. public double TokensFromDuration(TimeSpan duration) { var sec = duration.Ticks / TimeSpan.TicksPerSecond * _tokensPerSecond; var nsec = duration.Ticks % TimeSpan.TicksPerSecond * _tokensPerSecond; return sec + nsec / TimeSpan.TicksPerSecond; } /// /// Durations from tokens is a unit conversion function from the number of tokens to the duration /// of time it takes to accumulate them at a rate of limit tokens per second. /// https://github.com/golang/time/blob/master/rate/rate.go#L389. /// /// The tokens. /// TimeSpan. public TimeSpan DurationFromTokens(double tokens) { return TimeSpan.FromSeconds(tokens / _tokensPerSecond); } /// /// Determines whether the specified is equal to this instance. /// /// The object to compare with the current instance. /// true if the specified is equal to this instance; otherwise, false. public override bool Equals(object obj) { return obj is Limit limit && Equals(limit); } /// /// Indicates whether the current object is equal to another object of the same type. /// /// An object to compare with this object. /// if the current object is equal to the parameter; otherwise, . public bool Equals([AllowNull] Limit other) { return _tokensPerSecond == other._tokensPerSecond; } /// /// Returns a hash code for this instance. /// /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. public override int GetHashCode() { return HashCode.Combine(_tokensPerSecond); } } ================================================ FILE: src/Kubernetes.Controller/Rate/Limiter.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Threading; using System.Threading.Tasks; namespace Yarp.Kubernetes.Controller.Rate; /// /// /// Class Limiter controls how frequently events are allowed to happen. /// It implements a "token bucket" of size b, initially full and refilled /// at rate r tokens per second. /// Informally, in any large enough time interval, the Limiter limits the /// rate to r tokens per second, with a maximum burst size of b events. /// As a special case, if r == Inf (the infinite rate), b is ignored. /// See https://en.wikipedia.org/wiki/Token_bucket for more about token buckets. /// /// /// The zero value is a valid Limiter, but it will reject all events. /// Use NewLimiter to create non-zero Limiters. /// /// /// Limiter has three main methods, Allow, Reserve, and Wait. /// Most callers should use Wait. /// /// /// Each of the three methods consumes a single token. /// They differ in their behavior when no token is available. /// If no token is available, Allow returns false. /// If no token is available, Reserve returns a reservation for a future token /// and the amount of time the caller must wait before using it. /// If no token is available, Wait blocks until one can be obtained /// or its associated context.Context is canceled. /// The methods AllowN, ReserveN, and WaitN consume n tokens. /// /// https://github.com/golang/time/blob/master/rate/rate.go#L55. /// public class Limiter { private readonly object _sync = new object(); private readonly Limit _limit; private readonly TimeProvider _timeProvider; private readonly int _burst; private double _tokens; /// /// The last time the limiter's tokens field was updated. /// private DateTimeOffset _last; /// /// the latest time of a rate-limited event (past or future). /// private DateTimeOffset _lastEvent; /// /// Initializes a new instance of the class. /// Allows events up to and permits bursts of /// at most tokens. /// /// The count per second which is allowed. /// The burst. /// Accessor for the current UTC time. public Limiter(Limit limit, int burst, TimeProvider timeProvider = default) { _limit = limit; _burst = burst; _timeProvider = timeProvider ?? TimeProvider.System; } /// /// Checks if a token is available effective immediately. If so, it is consumed. /// /// true if a token is available and used, false otherwise. public bool Allow() { return AllowN(_timeProvider.GetUtcNow(), 1); } /// /// Checks if a number of tokens are available by a given time. /// They are consumed if available. /// /// The now. /// The number. /// true if a number token is available and used, false otherwise. public bool AllowN(DateTimeOffset now, int number) { return ReserveImpl(now, number, TimeSpan.Zero).Ok; } /// /// Reserves this instance. /// /// Reservation. public Reservation Reserve() { return Reserve(_timeProvider.GetUtcNow(), 1); } /// /// ReserveN returns a Reservation that indicates how long the caller must wait before n events happen. /// The Limiter takes this Reservation into account when allowing future events. /// The returned Reservation’s OK() method returns false if n exceeds the Limiter's burst size. /// Usage example: /// r := lim.ReserveN(time.Now(), 1) /// if !r.OK() { /// return /// } /// time.Sleep(r.Delay()) /// Act() /// Use this method if you wish to wait and slow down in accordance with the rate limit without dropping events. /// If you need to respect a deadline or cancel the delay, use Wait instead. /// To drop or skip events exceeding rate limit, use Allow instead. /// /// The now. /// The number. /// Reservation. public Reservation Reserve(DateTimeOffset now, int count) { return ReserveImpl(now, count, TimeSpan.MaxValue); } /// /// Waits the asynchronous. /// /// The cancellation token that can be used by other objects or threads to receive notice of cancellation. /// Task. public Task WaitAsync(CancellationToken cancellationToken) { return WaitAsync(1, cancellationToken); } /// /// wait as an asynchronous operation. /// /// The count. /// The cancellation token that can be used by other objects or threads to receive notice of cancellation. /// rate: Wait(count={count}) exceeds limiter's burst {burst}. /// A representing the asynchronous operation. public async Task WaitAsync(int count, CancellationToken cancellationToken) { // https://github.com/golang/time/blob/master/rate/rate.go#L226 int burst = default; Limit limit = default; lock (_sync) { burst = _burst; limit = _limit; } if (count > burst && limit != Limit.Max) { throw new Exception($"rate: Wait(count={count}) exceeds limiter's burst {burst}"); } // Check if ctx is already cancelled cancellationToken.ThrowIfCancellationRequested(); // Determine wait limit var waitLimit = limit.DurationFromTokens(count); while (true) { var now = _timeProvider.GetUtcNow(); var r = ReserveImpl(now, count, waitLimit); if (r.Ok) { var delay = r.DelayFrom(now); if (delay > TimeSpan.Zero) { await Task.Delay(delay, cancellationToken).ConfigureAwait(false); } return; } await Task.Delay(waitLimit, cancellationToken).ConfigureAwait(false); } } /// /// reserveN is a helper method for AllowN, ReserveN, and WaitN. /// maxFutureReserve specifies the maximum reservation wait duration allowed. /// reserveN returns Reservation, not *Reservation, to avoid allocation in AllowN and WaitN. /// /// The now. /// The number. /// The maximum future reserve. /// Reservation. private Reservation ReserveImpl(DateTimeOffset now, int number, TimeSpan maxFutureReserve) { lock (_sync) { if (_limit == Limit.Max) { return new Reservation( timeProvider: _timeProvider, limiter: this, ok: true, tokens: number, timeToAct: now); } var (newNow, last, tokens) = Advance(now); now = newNow; // Calculate the remaining number of tokens resulting from the request. tokens -= number; // Calculate the wait duration TimeSpan waitDuration = default; if (tokens < 0) { waitDuration = _limit.DurationFromTokens(-tokens); } // Decide result var ok = number <= _burst && waitDuration <= maxFutureReserve; // Prepare reservation if (ok) { var reservation = new Reservation( timeProvider: _timeProvider, limiter: this, ok: true, tokens: number, limit: _limit, timeToAct: now.Add(waitDuration)); _last = newNow; _tokens = tokens; _lastEvent = reservation.TimeToAct; return reservation; } else { var reservation = new Reservation( timeProvider: _timeProvider, limiter: this, ok: false, limit: _limit); _last = last; return reservation; } } } /// /// advance calculates and returns an updated state for lim resulting from the passage of time. /// lim is not changed. /// advance requires that lim.mu is held. /// /// The now. private (DateTimeOffset newNow, DateTimeOffset newLast, double newTokens) Advance(DateTimeOffset now) { lock (_sync) { var last = _last; if (now < last) { last = now; } // Avoid making delta overflow below when last is very old. var maxElapsed = _limit.DurationFromTokens(_burst - _tokens); var elapsed = now - last; if (elapsed > maxElapsed) { elapsed = maxElapsed; } // Calculate the new number of tokens, due to time that passed. var delta = _limit.TokensFromDuration(elapsed); var tokens = _tokens + delta; if (tokens > _burst) { tokens = _burst; } return (now, last, tokens); } } } ================================================ FILE: src/Kubernetes.Controller/Rate/Reservation.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; #pragma warning disable CS0169 // The field 'Reservation.limit' is never used namespace Yarp.Kubernetes.Controller.Rate; /// /// Class Reservation holds information about events that are permitted by a Limiter to happen after a delay. /// A Reservation may be canceled, which may enable the Limiter to permit additional events. /// https://github.com/golang/time/blob/master/rate/rate.go#L106. /// public class Reservation { private readonly TimeProvider _timeProvider; private readonly Limiter _limiter; private readonly Limit _limit; private readonly double _tokens; /// /// Initializes a new instance of the class. /// /// Gets the system time. /// The limiter. /// if set to true [ok]. /// The tokens. /// The time to act. /// The limit. public Reservation( TimeProvider timeProvider, Limiter limiter, bool ok, double tokens = default, DateTimeOffset timeToAct = default, Limit limit = default) { _timeProvider = timeProvider; _limiter = limiter; Ok = ok; _tokens = tokens; TimeToAct = timeToAct; _limit = limit; } /// /// Gets a value indicating whether this is ok. /// /// true if ok; otherwise, false. public bool Ok { get; } /// /// Gets the time to act. /// /// The time to act. public DateTimeOffset TimeToAct { get; } /// /// Delays this instance. /// /// TimeSpanOffset. public TimeSpan Delay() { return DelayFrom(_timeProvider.GetUtcNow()); } /// /// Delays from. /// /// The now. /// TimeSpan. public TimeSpan DelayFrom(DateTimeOffset now) { // https://github.com/golang/time/blob/master/rate/rate.go#L134 if (!Ok) { return TimeSpan.MaxValue; } var delay = TimeToAct - now; if (delay < TimeSpan.Zero) { return TimeSpan.Zero; } return delay; } } ================================================ FILE: src/Kubernetes.Controller/Services/IReconciler.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Threading; using System.Threading.Tasks; namespace Yarp.Kubernetes.Controller.Services; /// /// IReconciler is a service interface called by the to process /// the work items as they are dequeued. /// public interface IReconciler { Task ProcessAsync(CancellationToken cancellationToken); } ================================================ FILE: src/Kubernetes.Controller/Services/IngressController.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using k8s; using k8s.Models; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Yarp.Kubernetes.Controller.Caching; using Yarp.Kubernetes.Controller.Client; using Yarp.Kubernetes.Controller.Hosting; using Yarp.Kubernetes.Controller.Queues; namespace Yarp.Kubernetes.Controller.Services; /// /// Controller receives notifications from informers. The data which is needed for processing is /// saved in an instance and resources which need to be reconciled are /// added to an . The background task dequeues /// items and passes them to an service for processing. /// public class IngressController : BackgroundHostedService { private readonly IReadOnlyList _registrations; private readonly ICache _cache; private readonly IReconciler _reconciler; private bool _registrationsReady; private readonly WorkQueue _queue; private readonly QueueItem _ingressChangeQueueItem; public IngressController( ICache cache, IReconciler reconciler, IResourceInformer ingressInformer, IResourceInformer serviceInformer, IResourceInformer endpointsInformer, IResourceInformer ingressClassInformer, IResourceInformer secretInformer, IHostApplicationLifetime hostApplicationLifetime, IOptions options, ILogger logger) : base(hostApplicationLifetime, logger) { ArgumentNullException.ThrowIfNull(ingressInformer, nameof(ingressInformer)); ArgumentNullException.ThrowIfNull(serviceInformer, nameof(serviceInformer)); ArgumentNullException.ThrowIfNull(endpointsInformer, nameof(endpointsInformer)); ArgumentNullException.ThrowIfNull(ingressClassInformer, nameof(ingressClassInformer)); ArgumentNullException.ThrowIfNull(secretInformer, nameof(secretInformer)); ArgumentNullException.ThrowIfNull(options, nameof(options)); var watchSecrets = options.Value.ServerCertificates; var registrations = new List() { serviceInformer.Register(Notification), endpointsInformer.Register(Notification), ingressClassInformer.Register(Notification), ingressInformer.Register(Notification) }; if (watchSecrets) { registrations.Add(secretInformer.Register(Notification)); } _registrations = registrations; _registrationsReady = false; serviceInformer.StartWatching(); endpointsInformer.StartWatching(); ingressClassInformer.StartWatching(); ingressInformer.StartWatching(); if (watchSecrets) { secretInformer.StartWatching(); } _queue = new ProcessingRateLimitedQueue(perSecond: 0.5, burst: 1); ArgumentNullException.ThrowIfNull(cache); ArgumentNullException.ThrowIfNull(reconciler); _cache = cache; _reconciler = reconciler; _ingressChangeQueueItem = new QueueItem("Ingress Change"); } /// /// Disconnects from resource informers, and cause queue to become shut down. /// /// protected override void Dispose(bool disposing) { if (disposing) { foreach (var registration in _registrations) { registration.Dispose(); } _queue.Dispose(); } base.Dispose(disposing); } /// /// Called by the informer with real-time resource updates. /// /// Indicates if the resource new, updated, or deleted. /// The information as provided by the Kubernetes API server. private void Notification(WatchEventType eventType, V1Ingress resource) { if (_cache.Update(eventType, resource)) { NotificationIngressChanged(); } } private void NotificationIngressChanged() { if (!_registrationsReady) { return; } _queue.Add(_ingressChangeQueueItem); } /// /// Called by the informer with real-time resource updates. /// /// Indicates if the resource new, updated, or deleted. /// The information as provided by the Kubernetes API server. private void Notification(WatchEventType eventType, V1Service resource) { var ingressNames = _cache.Update(eventType, resource); if (ingressNames.Count > 0) { NotificationIngressChanged(); } } /// /// Called by the informer with real-time resource updates. /// /// Indicates if the resource new, updated, or deleted. /// The information as provided by the Kubernetes API server. private void Notification(WatchEventType eventType, V1Endpoints resource) { var ingressNames = _cache.Update(eventType, resource); if (ingressNames.Count > 0) { NotificationIngressChanged(); } } /// /// Called by the informer with real-time resource updates. /// /// Indicates if the resource new, updated, or deleted. /// The information as provided by the Kubernetes API server. private void Notification(WatchEventType eventType, V1IngressClass resource) { _cache.Update(eventType, resource); } /// /// Called by the informer with real-time resource updates. /// /// Indicates if the resource new, updated, or deleted. /// The information as provided by the Kubernetes API server. private void Notification(WatchEventType eventType, V1Secret resource) { _cache.Update(eventType, resource); } /// /// Called once at startup by the hosting infrastructure. This function must remain running /// for the entire lifetime of an application. /// /// Indicates when the web application is shutting down. /// The Task representing the async function results. public override async Task RunAsync(CancellationToken cancellationToken) { // First wait for all informers to fully List resources before processing begins. foreach (var registration in _registrations) { await registration.ReadyAsync(cancellationToken).ConfigureAwait(false); } // At this point we know that all the Ingress and Endpoint caches are at least in sync // with cluster's state as of the start of this controller. _registrationsReady = true; NotificationIngressChanged(); // Now begin one loop to process work until an application shutdown is requested. while (!cancellationToken.IsCancellationRequested) { // Dequeue the next item to process var (item, shutdown) = await _queue.GetAsync(cancellationToken).ConfigureAwait(false); if (shutdown) { Logger.LogInformation("Work queue has been shutdown. Exiting reconciliation loop."); return; } try { await _reconciler.ProcessAsync(cancellationToken).ConfigureAwait(false); } catch { Logger.LogInformation("Rescheduling {Change}", item.Change); // Any failure to process this item results in being re-queued _queue.Add(item); } finally { _queue.Done(item); } } Logger.LogInformation("Reconciliation loop cancelled"); } } ================================================ FILE: src/Kubernetes.Controller/Services/QueueItem.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; namespace Yarp.Kubernetes.Controller.Services; /// /// QueueItem acts as the "Key" for the _queue to manage items. /// public struct QueueItem : IEquatable { public QueueItem(string change) { Change = change; } /// /// This identifies that a change has occurred and either configuration requires to be rebuilt, or needs to be dispatched. /// public string Change { get; } public override bool Equals(object obj) { return obj is QueueItem item && Equals(item); } public bool Equals(QueueItem other) { return Change.Equals(other.Change, StringComparison.Ordinal); } public override int GetHashCode() { return Change.GetHashCode(); } public static bool operator ==(QueueItem left, QueueItem right) { return left.Equals(right); } public static bool operator !=(QueueItem left, QueueItem right) { return !(left == right); } } ================================================ FILE: src/Kubernetes.Controller/Services/ReconcileData.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; using Yarp.Kubernetes.Controller.Caching; namespace Yarp.Kubernetes.Controller.Services; /// /// ReconcileData is the information returned from /// and needed by . /// public struct ReconcileData { public ReconcileData(IngressData ingress, List services, List endpoints) { Ingress = ingress; ServiceList = services; EndpointsList = endpoints; } public IngressData Ingress { get; } public List ServiceList { get; } public List EndpointsList { get; } } ================================================ FILE: src/Kubernetes.Controller/Services/Reconciler.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Linq; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Yarp.Kubernetes.Controller.Caching; using Yarp.Kubernetes.Controller.Client; using Yarp.Kubernetes.Controller.Configuration; using Yarp.Kubernetes.Controller.Converters; namespace Yarp.Kubernetes.Controller.Services; /// /// IReconciler is a service interface called by the to process /// the work items as they are dequeued. /// public partial class Reconciler : IReconciler { private readonly ICache _cache; private readonly IUpdateConfig _updateConfig; private readonly IIngressResourceStatusUpdater _ingressResourceStatusUpdater; private readonly ILogger _logger; public Reconciler(ICache cache, IUpdateConfig updateConfig, IIngressResourceStatusUpdater ingressResourceStatusUpdater, ILogger logger) { ArgumentNullException.ThrowIfNull(cache); ArgumentNullException.ThrowIfNull(updateConfig); ArgumentNullException.ThrowIfNull(ingressResourceStatusUpdater); _cache = cache; _updateConfig = updateConfig; _ingressResourceStatusUpdater = ingressResourceStatusUpdater; _logger = logger; } public async Task ProcessAsync(CancellationToken cancellationToken) { try { var ingresses = _cache.GetIngresses().ToArray(); var configContext = new YarpConfigContext(); foreach (var ingress in ingresses) { try { if (_cache.TryGetReconcileData(new NamespacedName(ingress.Metadata.NamespaceProperty, ingress.Metadata.Name), out var data)) { var ingressContext = new YarpIngressContext(ingress, data.ServiceList, data.EndpointsList); YarpParser.ConvertFromKubernetesIngress(ingressContext, configContext); } } catch (Exception ex) { _logger.LogWarning(ex, "Uncaught exception occurred while reconciling ingress {IngressNamespace}/{IngressName}", ingress.Metadata.NamespaceProperty, ingress.Metadata.Name); } } var clusters = configContext.BuildClusterConfig(); _logger.LogInformation(JsonSerializer.Serialize(configContext.Routes)); _logger.LogInformation(JsonSerializer.Serialize(clusters)); await _updateConfig.UpdateAsync(configContext.Routes, clusters, cancellationToken).ConfigureAwait(false); await _ingressResourceStatusUpdater.UpdateStatusAsync(cancellationToken); } catch (Exception ex) { _logger.LogWarning(ex, "Uncaught exception occurred while reconciling"); throw; } } } ================================================ FILE: src/Kubernetes.Controller/Yarp.Kubernetes.Controller.csproj ================================================ Toolkit for building a Kubernetes Ingress Controller in .NET using the infrastructure from ASP.NET and .NET $(ReleaseTFMs) Library $(NoWarn);CS8002 true false yarp;dotnet;reverse-proxy;aspnetcore;kubernetes ================================================ FILE: src/Kubernetes.Controller/YarpOptions.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. namespace Yarp.Kubernetes.Controller; public class YarpOptions { /// /// Defines a name of the ingress controller. IngressClass ".spec.controller" field should match this. /// This field is required. /// public string ControllerClass { get; set; } public bool ServerCertificates { get; set; } public string DefaultSslCertificate { get; set; } /// /// Name of the Kubernetes Service the ingress controller is running in. /// This field is required. /// public string ControllerServiceName { get; set; } /// /// Namespace of the Kubernetes Service the ingress controller is running in. /// This field is required. /// public string ControllerServiceNamespace { get; set; } } ================================================ FILE: src/ReverseProxy/Configuration/ActiveHealthCheckConfig.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; namespace Yarp.ReverseProxy.Configuration; /// /// Active health check config. /// public sealed record ActiveHealthCheckConfig { /// /// Whether active health checks are enabled. /// public bool? Enabled { get; init; } /// /// Health probe interval. /// public TimeSpan? Interval { get; init; } /// /// Health probe timeout, after which a destination is considered unhealthy. /// public TimeSpan? Timeout { get; init; } /// /// Active health check policy. /// public string? Policy { get; init; } /// /// HTTP health check endpoint path. /// public string? Path { get; init; } /// /// Query string to append to the probe, including the leading '?'. /// public string? Query { get; init; } public bool Equals(ActiveHealthCheckConfig? other) { if (other is null) { return false; } return Enabled == other.Enabled && Interval == other.Interval && Timeout == other.Timeout && string.Equals(Policy, other.Policy, StringComparison.OrdinalIgnoreCase) && string.Equals(Path, other.Path, StringComparison.Ordinal) && string.Equals(Query, other.Query, StringComparison.Ordinal); } public override int GetHashCode() { return HashCode.Combine(Enabled, Interval, Timeout, Policy?.GetHashCode(StringComparison.OrdinalIgnoreCase), Path?.GetHashCode(StringComparison.Ordinal), Query?.GetHashCode(StringComparison.Ordinal)); } } ================================================ FILE: src/ReverseProxy/Configuration/AuthorizationConstants.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. namespace Yarp.ReverseProxy.Configuration; internal static class AuthorizationConstants { internal const string Default = "Default"; internal const string Anonymous = "Anonymous"; } ================================================ FILE: src/ReverseProxy/Configuration/ClusterConfig.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using Yarp.ReverseProxy.Forwarder; using Yarp.ReverseProxy.Utilities; namespace Yarp.ReverseProxy.Configuration; /// /// A cluster is a group of equivalent endpoints and associated policies. /// public sealed record ClusterConfig { /// /// The Id for this cluster. This needs to be globally unique. /// This field is required. /// public string ClusterId { get; init; } = default!; /// /// Load balancing policy. /// public string? LoadBalancingPolicy { get; init; } /// /// Session affinity config. /// public SessionAffinityConfig? SessionAffinity { get; init; } /// /// Health checking config. /// public HealthCheckConfig? HealthCheck { get; init; } /// /// Config for the HTTP client that is used to call destinations in this cluster. /// public HttpClientConfig? HttpClient { get; init; } /// /// Config for outgoing HTTP requests. /// public ForwarderRequestConfig? HttpRequest { get; init; } /// /// The set of destinations associated with this cluster. /// public IReadOnlyDictionary? Destinations { get; init; } /// /// Arbitrary key-value pairs that further describe this cluster. /// public IReadOnlyDictionary? Metadata { get; init; } public bool Equals(ClusterConfig? other) { if (other is null) { return false; } return EqualsExcludingDestinations(other) && CollectionEqualityHelper.Equals(Destinations, other.Destinations); } internal bool EqualsExcludingDestinations(ClusterConfig other) { if (other is null) { return false; } return string.Equals(ClusterId, other.ClusterId, StringComparison.OrdinalIgnoreCase) && string.Equals(LoadBalancingPolicy, other.LoadBalancingPolicy, StringComparison.OrdinalIgnoreCase) // CS0252 warning only shows up in VS https://github.com/dotnet/roslyn/issues/49302 && SessionAffinity == other.SessionAffinity && HealthCheck == other.HealthCheck && HttpClient == other.HttpClient && HttpRequest == other.HttpRequest && CaseSensitiveEqualHelper.Equals(Metadata, other.Metadata); } public override int GetHashCode() { return HashCode.Combine( ClusterId?.GetHashCode(StringComparison.OrdinalIgnoreCase), LoadBalancingPolicy?.GetHashCode(StringComparison.OrdinalIgnoreCase), SessionAffinity, HealthCheck, HttpClient, HttpRequest, CollectionEqualityHelper.GetHashCode(Destinations), CaseSensitiveEqualHelper.GetHashCode(Metadata)); } } ================================================ FILE: src/ReverseProxy/Configuration/ClusterValidators/DestinationValidator.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Threading.Tasks; namespace Yarp.ReverseProxy.Configuration.ClusterValidators; internal sealed class DestinationValidator : IClusterValidator { public ValueTask ValidateAsync(ClusterConfig cluster, IList errors) { if (cluster.Destinations is null) { return ValueTask.CompletedTask; } foreach (var (name, destination) in cluster.Destinations) { if (string.IsNullOrEmpty(destination.Address)) { errors.Add(new ArgumentException($"No address found for destination '{name}' on cluster '{cluster.ClusterId}'.")); } } return ValueTask.CompletedTask; } } ================================================ FILE: src/ReverseProxy/Configuration/ClusterValidators/HealthCheckValidator.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Frozen; using System.Collections.Generic; using System.Threading.Tasks; using Yarp.ReverseProxy.Health; using Yarp.ReverseProxy.Utilities; namespace Yarp.ReverseProxy.Configuration.ClusterValidators; internal sealed class HealthCheckValidator : IClusterValidator { private readonly FrozenDictionary _availableDestinationsPolicies; private readonly FrozenDictionary _activeHealthCheckPolicies; private readonly FrozenDictionary _passiveHealthCheckPolicies; public HealthCheckValidator(IEnumerable availableDestinationsPolicies, IEnumerable activeHealthCheckPolicies, IEnumerable passiveHealthCheckPolicies) { ArgumentNullException.ThrowIfNull(availableDestinationsPolicies); ArgumentNullException.ThrowIfNull(activeHealthCheckPolicies); ArgumentNullException.ThrowIfNull(passiveHealthCheckPolicies); _availableDestinationsPolicies = availableDestinationsPolicies.ToDictionaryByUniqueId(p => p.Name); _activeHealthCheckPolicies = activeHealthCheckPolicies.ToDictionaryByUniqueId(p => p.Name); _passiveHealthCheckPolicies = passiveHealthCheckPolicies.ToDictionaryByUniqueId(p => p.Name); } public ValueTask ValidateAsync(ClusterConfig cluster, IList errors) { var availableDestinationsPolicy = cluster.HealthCheck?.AvailableDestinationsPolicy; if (string.IsNullOrEmpty(availableDestinationsPolicy)) { // The default. availableDestinationsPolicy = HealthCheckConstants.AvailableDestinations.HealthyOrPanic; } if (!_availableDestinationsPolicies.ContainsKey(availableDestinationsPolicy)) { errors.Add(new ArgumentException($"No matching {nameof(IAvailableDestinationsPolicy)} found for the available destinations policy '{availableDestinationsPolicy}' set on the cluster.'{cluster.ClusterId}'.")); } ValidateActiveHealthCheck(cluster, errors); ValidatePassiveHealthCheck(cluster, errors); return ValueTask.CompletedTask; } private void ValidateActiveHealthCheck(ClusterConfig cluster, IList errors) { if (!(cluster.HealthCheck?.Active?.Enabled ?? false)) { // Active health check is disabled return; } var activeOptions = cluster.HealthCheck.Active; var policy = activeOptions.Policy; if (string.IsNullOrEmpty(policy)) { // default policy policy = HealthCheckConstants.ActivePolicy.ConsecutiveFailures; } if (!_activeHealthCheckPolicies.ContainsKey(policy)) { errors.Add(new ArgumentException($"No matching {nameof(IActiveHealthCheckPolicy)} found for the active health check policy name '{policy}' set on the cluster '{cluster.ClusterId}'.")); } if (activeOptions.Interval is not null && activeOptions.Interval <= TimeSpan.Zero) { errors.Add(new ArgumentException($"Destination probing interval set on the cluster '{cluster.ClusterId}' must be positive.")); } if (activeOptions.Timeout is not null && activeOptions.Timeout <= TimeSpan.Zero) { errors.Add(new ArgumentException($"Destination probing timeout set on the cluster '{cluster.ClusterId}' must be positive.")); } } private void ValidatePassiveHealthCheck(ClusterConfig cluster, IList errors) { if (!(cluster.HealthCheck?.Passive?.Enabled ?? false)) { // Passive health check is disabled return; } var passiveOptions = cluster.HealthCheck.Passive; var policy = passiveOptions.Policy; if (string.IsNullOrEmpty(policy)) { // default policy policy = HealthCheckConstants.PassivePolicy.TransportFailureRate; } if (!_passiveHealthCheckPolicies.ContainsKey(policy)) { errors.Add(new ArgumentException($"No matching {nameof(IPassiveHealthCheckPolicy)} found for the passive health check policy name '{policy}' set on the cluster '{cluster.ClusterId}'.")); } if (passiveOptions.ReactivationPeriod is not null && passiveOptions.ReactivationPeriod <= TimeSpan.Zero) { errors.Add(new ArgumentException($"Unhealthy destination reactivation period set on the cluster '{cluster.ClusterId}' must be positive.")); } } } ================================================ FILE: src/ReverseProxy/Configuration/ClusterValidators/IClusterValidator.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Threading.Tasks; namespace Yarp.ReverseProxy.Configuration.ClusterValidators; /// /// Provides method to validate cluster configuration. /// public interface IClusterValidator { /// /// Perform validation on a cluster configuration by adding exceptions to the provided collection. /// /// Cluster configuration to validate /// Collection of all validation exceptions /// A ValueTask representing the asynchronous validation operation. public ValueTask ValidateAsync(ClusterConfig cluster, IList errors); } ================================================ FILE: src/ReverseProxy/Configuration/ClusterValidators/LoadBalancingValidator.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Frozen; using System.Collections.Generic; using System.Threading.Tasks; using Yarp.ReverseProxy.LoadBalancing; using Yarp.ReverseProxy.Utilities; namespace Yarp.ReverseProxy.Configuration.ClusterValidators; internal sealed class LoadBalancingValidator : IClusterValidator { private readonly FrozenDictionary _loadBalancingPolicies; public LoadBalancingValidator(IEnumerable loadBalancingPolicies) { ArgumentNullException.ThrowIfNull(loadBalancingPolicies); _loadBalancingPolicies = loadBalancingPolicies.ToDictionaryByUniqueId(p => p.Name); } public ValueTask ValidateAsync(ClusterConfig cluster, IList errors) { var loadBalancingPolicy = cluster.LoadBalancingPolicy; if (string.IsNullOrEmpty(loadBalancingPolicy)) { // The default. loadBalancingPolicy = LoadBalancingPolicies.PowerOfTwoChoices; } if (!_loadBalancingPolicies.ContainsKey(loadBalancingPolicy)) { errors.Add(new ArgumentException($"No matching {nameof(ILoadBalancingPolicy)} found for the load balancing policy '{loadBalancingPolicy}' set on the cluster '{cluster.ClusterId}'.")); } return ValueTask.CompletedTask; } } ================================================ FILE: src/ReverseProxy/Configuration/ClusterValidators/ProxyHttpClientValidator.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Text; using System.Threading.Tasks; namespace Yarp.ReverseProxy.Configuration.ClusterValidators; internal sealed class ProxyHttpClientValidator : IClusterValidator { public ValueTask ValidateAsync(ClusterConfig cluster, IList errors) { if (cluster.HttpClient is null) { // Proxy http client options are not set. return ValueTask.CompletedTask; } if (cluster.HttpClient.MaxConnectionsPerServer is not null && cluster.HttpClient.MaxConnectionsPerServer <= 0) { errors.Add(new ArgumentException($"Max connections per server limit set on the cluster '{cluster.ClusterId}' must be positive.")); } var requestHeaderEncoding = cluster.HttpClient.RequestHeaderEncoding; if (requestHeaderEncoding is not null) { try { Encoding.GetEncoding(requestHeaderEncoding); } catch (ArgumentException aex) { errors.Add(new ArgumentException($"Invalid request header encoding '{requestHeaderEncoding}'.", aex)); } } var responseHeaderEncoding = cluster.HttpClient.ResponseHeaderEncoding; if (responseHeaderEncoding is null) { return ValueTask.CompletedTask; } try { Encoding.GetEncoding(responseHeaderEncoding); } catch (ArgumentException aex) { errors.Add(new ArgumentException($"Invalid response header encoding '{responseHeaderEncoding}'.", aex)); } return ValueTask.CompletedTask; } } ================================================ FILE: src/ReverseProxy/Configuration/ClusterValidators/ProxyHttpRequestValidator.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Net; using System.Threading.Tasks; using Microsoft.Extensions.Logging; namespace Yarp.ReverseProxy.Configuration.ClusterValidators; internal sealed class ProxyHttpRequestValidator(ILogger logger) : IClusterValidator { public ValueTask ValidateAsync(ClusterConfig cluster, IList errors) { if (cluster.HttpRequest is null) { // Proxy http request options are not set. return ValueTask.CompletedTask; } if (cluster.HttpRequest.Version is not null && cluster.HttpRequest.Version != HttpVersion.Version10 && cluster.HttpRequest.Version != HttpVersion.Version11 && cluster.HttpRequest.Version != HttpVersion.Version20 && cluster.HttpRequest.Version != HttpVersion.Version30) { errors.Add(new ArgumentException($"Outgoing request version '{cluster.HttpRequest.Version}' is not any of supported HTTP versions (1.0, 1.1, 2 and 3).")); } if (cluster.HttpRequest.Version == HttpVersion.Version10) { Log.Http10Version(logger); } return ValueTask.CompletedTask; } private static class Log { private static readonly Action _http10RequestVersionDetected = LoggerMessage.Define( LogLevel.Warning, EventIds.Http10RequestVersionDetected, "The HttpRequest version is set to 1.0 which can result in poor performance and port exhaustion. Use 1.1, 2, or 3 instead."); public static void Http10Version(ILogger logger) { _http10RequestVersionDetected(logger, null); } } } ================================================ FILE: src/ReverseProxy/Configuration/ClusterValidators/SessionAffinityValidator.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Frozen; using System.Collections.Generic; using System.Threading.Tasks; using Yarp.ReverseProxy.SessionAffinity; using Yarp.ReverseProxy.Utilities; namespace Yarp.ReverseProxy.Configuration.ClusterValidators; internal sealed class SessionAffinityValidator : IClusterValidator { private readonly FrozenDictionary _affinityFailurePolicies; public SessionAffinityValidator(IEnumerable affinityFailurePolicies) { ArgumentNullException.ThrowIfNull(affinityFailurePolicies); _affinityFailurePolicies = affinityFailurePolicies.ToDictionaryByUniqueId(p => p.Name); } public ValueTask ValidateAsync(ClusterConfig cluster, IList errors) { if (!(cluster.SessionAffinity?.Enabled ?? false)) { // Session affinity is disabled return ValueTask.CompletedTask; } // Note some affinity validation takes place in AffinitizeTransformProvider.ValidateCluster. var affinityFailurePolicy = cluster.SessionAffinity.FailurePolicy; if (string.IsNullOrEmpty(affinityFailurePolicy)) { // The default. affinityFailurePolicy = SessionAffinityConstants.FailurePolicies.Redistribute; } if (!_affinityFailurePolicies.ContainsKey(affinityFailurePolicy)) { errors.Add(new ArgumentException($"No matching {nameof(IAffinityFailurePolicy)} found for the affinity failure policy name '{affinityFailurePolicy}' set on the cluster '{cluster.ClusterId}'.")); } if (string.IsNullOrEmpty(cluster.SessionAffinity.AffinityKeyName)) { errors.Add(new ArgumentException($"Affinity key name set on the cluster '{cluster.ClusterId}' must not be null.")); } var cookieConfig = cluster.SessionAffinity.Cookie; if (cookieConfig is null) { return ValueTask.CompletedTask; } if (cookieConfig.Expiration is not null && cookieConfig.Expiration <= TimeSpan.Zero) { errors.Add(new ArgumentException($"Session affinity cookie expiration must be positive or null.")); } if (cookieConfig.MaxAge is not null && cookieConfig.MaxAge <= TimeSpan.Zero) { errors.Add(new ArgumentException($"Session affinity cookie max-age must be positive or null.")); } return ValueTask.CompletedTask; } } ================================================ FILE: src/ReverseProxy/Configuration/ConfigProvider/ConfigurationConfigProvider.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Net.Http; using System.Security.Authentication; using System.Threading; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Primitives; using Yarp.ReverseProxy.Forwarder; namespace Yarp.ReverseProxy.Configuration.ConfigProvider; /// /// Reacts to configuration changes and applies configurations to the Reverse Proxy core. /// When configs are loaded from appsettings.json, this takes care of hot updates /// when appsettings.json is modified on disk. /// internal sealed class ConfigurationConfigProvider : IProxyConfigProvider, IDisposable { private readonly object _lockObject = new(); private readonly ILogger _logger; private readonly IConfiguration _configuration; private ConfigurationSnapshot? _snapshot; private CancellationTokenSource? _changeToken; private bool _disposed; private IDisposable? _subscription; public ConfigurationConfigProvider( ILogger logger, IConfiguration configuration) { ArgumentNullException.ThrowIfNull(logger); ArgumentNullException.ThrowIfNull(configuration); _logger = logger; _configuration = configuration; } public void Dispose() { if (!_disposed) { _subscription?.Dispose(); _changeToken?.Dispose(); _disposed = true; } } public IProxyConfig GetConfig() { // First time load if (_snapshot is null) { _subscription = ChangeToken.OnChange(_configuration.GetReloadToken, UpdateSnapshot); UpdateSnapshot(); } return _snapshot; } [MemberNotNull(nameof(_snapshot))] private void UpdateSnapshot() { // Prevent overlapping updates, especially on startup. lock (_lockObject) { Log.LoadData(_logger); ConfigurationSnapshot newSnapshot; try { newSnapshot = new ConfigurationSnapshot(); foreach (var section in _configuration.GetSection("Clusters").GetChildren()) { newSnapshot.Clusters.Add(CreateCluster(section)); } foreach (var section in _configuration.GetSection("Routes").GetChildren()) { newSnapshot.Routes.Add(CreateRoute(section)); } } catch (Exception ex) { Log.ConfigurationDataConversionFailed(_logger, ex); // Re-throw on the first time load to prevent app from starting. if (_snapshot is null) { throw; } return; } var oldToken = _changeToken; _changeToken = new CancellationTokenSource(); newSnapshot.ChangeToken = new CancellationChangeToken(_changeToken.Token); _snapshot = newSnapshot; try { oldToken?.Cancel(throwOnFirstException: false); } catch (Exception ex) { Log.ErrorSignalingChange(_logger, ex); } } } private static ClusterConfig CreateCluster(IConfigurationSection section) { var destinations = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var destination in section.GetSection(nameof(ClusterConfig.Destinations)).GetChildren()) { destinations.Add(destination.Key, CreateDestination(destination)); } return new ClusterConfig { ClusterId = section.Key, LoadBalancingPolicy = section[nameof(ClusterConfig.LoadBalancingPolicy)], SessionAffinity = CreateSessionAffinityConfig(section.GetSection(nameof(ClusterConfig.SessionAffinity))), HealthCheck = CreateHealthCheckConfig(section.GetSection(nameof(ClusterConfig.HealthCheck))), HttpClient = CreateHttpClientConfig(section.GetSection(nameof(ClusterConfig.HttpClient))), HttpRequest = CreateProxyRequestConfig(section.GetSection(nameof(ClusterConfig.HttpRequest))), Metadata = section.GetSection(nameof(ClusterConfig.Metadata)).ReadStringDictionary(), Destinations = destinations, }; } private static RouteConfig CreateRoute(IConfigurationSection section) { if (!string.IsNullOrEmpty(section["RouteId"])) { throw new Exception("The route config format has changed, routes are now objects instead of an array. The route id must be set as the object name, not with the 'RouteId' field."); } return new RouteConfig { RouteId = section.Key, Order = section.ReadInt32(nameof(RouteConfig.Order)), MaxRequestBodySize = section.ReadInt64(nameof(RouteConfig.MaxRequestBodySize)), ClusterId = section[nameof(RouteConfig.ClusterId)], AuthorizationPolicy = section[nameof(RouteConfig.AuthorizationPolicy)], RateLimiterPolicy = section[nameof(RouteConfig.RateLimiterPolicy)], OutputCachePolicy = section[nameof(RouteConfig.OutputCachePolicy)], TimeoutPolicy = section[nameof(RouteConfig.TimeoutPolicy)], Timeout = section.ReadTimeSpan(nameof(RouteConfig.Timeout)), CorsPolicy = section[nameof(RouteConfig.CorsPolicy)], Metadata = section.GetSection(nameof(RouteConfig.Metadata)).ReadStringDictionary(), Transforms = CreateTransforms(section.GetSection(nameof(RouteConfig.Transforms))), Match = CreateRouteMatch(section.GetSection(nameof(RouteConfig.Match))), }; } private static Dictionary[]? CreateTransforms(IConfigurationSection section) { if (section.GetChildren() is var children && !children.Any()) { return null; } return children .Select(subSection => subSection.GetChildren().ToDictionary(d => d.Key, d => d.Value!, StringComparer.OrdinalIgnoreCase)) .ToArray(); } private static RouteMatch CreateRouteMatch(IConfigurationSection section) { if (!section.Exists()) { return new RouteMatch(); } return new RouteMatch() { Methods = section.GetSection(nameof(RouteMatch.Methods)).ReadStringArray(), Hosts = section.GetSection(nameof(RouteMatch.Hosts)).ReadStringArray(), Path = section[nameof(RouteMatch.Path)], Headers = CreateRouteHeaders(section.GetSection(nameof(RouteMatch.Headers))), QueryParameters = CreateRouteQueryParameters(section.GetSection(nameof(RouteMatch.QueryParameters))) }; } private static RouteHeader[]? CreateRouteHeaders(IConfigurationSection section) { if (!section.Exists()) { return null; } return section.GetChildren().Select(CreateRouteHeader).ToArray(); } private static RouteHeader CreateRouteHeader(IConfigurationSection section) { return new RouteHeader() { Name = section[nameof(RouteHeader.Name)]!, Values = section.GetSection(nameof(RouteHeader.Values)).ReadStringArray(), Mode = section.ReadEnum(nameof(RouteHeader.Mode)) ?? HeaderMatchMode.ExactHeader, IsCaseSensitive = section.ReadBool(nameof(RouteHeader.IsCaseSensitive)) ?? false, }; } private static RouteQueryParameter[]? CreateRouteQueryParameters(IConfigurationSection section) { if (!section.Exists()) { return null; } return section.GetChildren().Select(CreateRouteQueryParameter).ToArray(); } private static RouteQueryParameter CreateRouteQueryParameter(IConfigurationSection section) { return new RouteQueryParameter() { Name = section[nameof(RouteQueryParameter.Name)]!, Values = section.GetSection(nameof(RouteQueryParameter.Values)).ReadStringArray(), Mode = section.ReadEnum(nameof(RouteQueryParameter.Mode)) ?? QueryParameterMatchMode.Exact, IsCaseSensitive = section.ReadBool(nameof(RouteQueryParameter.IsCaseSensitive)) ?? false, }; } private static SessionAffinityConfig? CreateSessionAffinityConfig(IConfigurationSection section) { if (!section.Exists()) { return null; } return new SessionAffinityConfig { Enabled = section.ReadBool(nameof(SessionAffinityConfig.Enabled)), Policy = section[nameof(SessionAffinityConfig.Policy)], FailurePolicy = section[nameof(SessionAffinityConfig.FailurePolicy)], AffinityKeyName = section[nameof(SessionAffinityConfig.AffinityKeyName)]!, Cookie = CreateSessionAffinityCookieConfig(section.GetSection(nameof(SessionAffinityConfig.Cookie))) }; } private static SessionAffinityCookieConfig? CreateSessionAffinityCookieConfig(IConfigurationSection section) { if (!section.Exists()) { return null; } return new SessionAffinityCookieConfig { Path = section[nameof(SessionAffinityCookieConfig.Path)], SameSite = section.ReadEnum(nameof(SessionAffinityCookieConfig.SameSite)), HttpOnly = section.ReadBool(nameof(SessionAffinityCookieConfig.HttpOnly)), MaxAge = section.ReadTimeSpan(nameof(SessionAffinityCookieConfig.MaxAge)), Domain = section[nameof(SessionAffinityCookieConfig.Domain)], IsEssential = section.ReadBool(nameof(SessionAffinityCookieConfig.IsEssential)), SecurePolicy = section.ReadEnum(nameof(SessionAffinityCookieConfig.SecurePolicy)), Expiration = section.ReadTimeSpan(nameof(SessionAffinityCookieConfig.Expiration)) }; } private static HealthCheckConfig? CreateHealthCheckConfig(IConfigurationSection section) { if (!section.Exists()) { return null; } return new HealthCheckConfig { Passive = CreatePassiveHealthCheckConfig(section.GetSection(nameof(HealthCheckConfig.Passive))), Active = CreateActiveHealthCheckConfig(section.GetSection(nameof(HealthCheckConfig.Active))), AvailableDestinationsPolicy = section[nameof(HealthCheckConfig.AvailableDestinationsPolicy)] }; } private static PassiveHealthCheckConfig? CreatePassiveHealthCheckConfig(IConfigurationSection section) { if (!section.Exists()) { return null; } return new PassiveHealthCheckConfig { Enabled = section.ReadBool(nameof(PassiveHealthCheckConfig.Enabled)), Policy = section[nameof(PassiveHealthCheckConfig.Policy)], ReactivationPeriod = section.ReadTimeSpan(nameof(PassiveHealthCheckConfig.ReactivationPeriod)) }; } private static ActiveHealthCheckConfig? CreateActiveHealthCheckConfig(IConfigurationSection section) { if (!section.Exists()) { return null; } return new ActiveHealthCheckConfig { Enabled = section.ReadBool(nameof(ActiveHealthCheckConfig.Enabled)), Interval = section.ReadTimeSpan(nameof(ActiveHealthCheckConfig.Interval)), Timeout = section.ReadTimeSpan(nameof(ActiveHealthCheckConfig.Timeout)), Policy = section[nameof(ActiveHealthCheckConfig.Policy)], Path = section[nameof(ActiveHealthCheckConfig.Path)], Query = section[nameof(ActiveHealthCheckConfig.Query)] }; } private static HttpClientConfig? CreateHttpClientConfig(IConfigurationSection section) { if (!section.Exists()) { return null; } SslProtocols? sslProtocols = null; if (section.GetSection(nameof(HttpClientConfig.SslProtocols)) is IConfigurationSection sslProtocolsSection) { foreach (var protocolConfig in sslProtocolsSection.GetChildren().Select(s => Enum.Parse(s.Value!, ignoreCase: true))) { sslProtocols = sslProtocols is null ? protocolConfig : sslProtocols | protocolConfig; } } WebProxyConfig? webProxy; var webProxySection = section.GetSection(nameof(HttpClientConfig.WebProxy)); if (webProxySection.Exists()) { webProxy = new WebProxyConfig() { Address = webProxySection.ReadUri(nameof(WebProxyConfig.Address)), BypassOnLocal = webProxySection.ReadBool(nameof(WebProxyConfig.BypassOnLocal)), UseDefaultCredentials = webProxySection.ReadBool(nameof(WebProxyConfig.UseDefaultCredentials)) }; } else { webProxy = null; } return new HttpClientConfig { SslProtocols = sslProtocols, DangerousAcceptAnyServerCertificate = section.ReadBool(nameof(HttpClientConfig.DangerousAcceptAnyServerCertificate)), MaxConnectionsPerServer = section.ReadInt32(nameof(HttpClientConfig.MaxConnectionsPerServer)), EnableMultipleHttp2Connections = section.ReadBool(nameof(HttpClientConfig.EnableMultipleHttp2Connections)), RequestHeaderEncoding = section[nameof(HttpClientConfig.RequestHeaderEncoding)], ResponseHeaderEncoding = section[nameof(HttpClientConfig.ResponseHeaderEncoding)], WebProxy = webProxy }; } private static ForwarderRequestConfig? CreateProxyRequestConfig(IConfigurationSection section) { if (!section.Exists()) { return null; } return new ForwarderRequestConfig { ActivityTimeout = section.ReadTimeSpan(nameof(ForwarderRequestConfig.ActivityTimeout)), Version = section.ReadVersion(nameof(ForwarderRequestConfig.Version)), VersionPolicy = section.ReadEnum(nameof(ForwarderRequestConfig.VersionPolicy)), AllowResponseBuffering = section.ReadBool(nameof(ForwarderRequestConfig.AllowResponseBuffering)) }; } private static DestinationConfig CreateDestination(IConfigurationSection section) { return new DestinationConfig { Address = section[nameof(DestinationConfig.Address)]!, Health = section[nameof(DestinationConfig.Health)], Metadata = section.GetSection(nameof(DestinationConfig.Metadata)).ReadStringDictionary(), Host = section[nameof(DestinationConfig.Host)] }; } private static class Log { private static readonly Action _errorSignalingChange = LoggerMessage.Define( LogLevel.Error, EventIds.ErrorSignalingChange, "An exception was thrown from the change notification."); private static readonly Action _loadData = LoggerMessage.Define( LogLevel.Information, EventIds.LoadData, "Loading proxy data from config."); private static readonly Action _configurationDataConversionFailed = LoggerMessage.Define( LogLevel.Error, EventIds.ConfigurationDataConversionFailed, "Configuration data conversion failed."); public static void ErrorSignalingChange(ILogger logger, Exception exception) { _errorSignalingChange(logger, exception); } public static void LoadData(ILogger logger) { _loadData(logger, null); } public static void ConfigurationDataConversionFailed(ILogger logger, Exception exception) { _configurationDataConversionFailed(logger, exception); } } } ================================================ FILE: src/ReverseProxy/Configuration/ConfigProvider/ConfigurationReadingExtensions.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Globalization; using System.Linq; namespace Microsoft.Extensions.Configuration; internal static class ConfigurationReadingExtensions { internal static int? ReadInt32(this IConfiguration configuration, string name) { return configuration[name] is string value ? int.Parse(value, NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture) : null; } internal static long? ReadInt64(this IConfiguration configuration, string name) { return configuration[name] is string value ? long.Parse(value, NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture) : null; } internal static double? ReadDouble(this IConfiguration configuration, string name) { return configuration[name] is string value ? double.Parse(value, CultureInfo.InvariantCulture) : null; } internal static TimeSpan? ReadTimeSpan(this IConfiguration configuration, string name) { // Format "c" => [-][d'.']hh':'mm':'ss['.'fffffff]. // You also can find more info at https://docs.microsoft.com/dotnet/standard/base-types/standard-timespan-format-strings#the-constant-c-format-specifier return configuration[name] is string value ? TimeSpan.ParseExact(value, "c", CultureInfo.InvariantCulture) : null; } internal static Uri? ReadUri(this IConfiguration configuration, string name) { return configuration[name] is string value ? new Uri(value) : null; } internal static TEnum? ReadEnum(this IConfiguration configuration, string name) where TEnum : struct { return configuration[name] is string value ? Enum.Parse(value, ignoreCase: true) : null; } internal static bool? ReadBool(this IConfiguration configuration, string name) { return configuration[name] is string value ? bool.Parse(value) : null; } internal static Version? ReadVersion(this IConfiguration configuration, string name) { return configuration[name] is string value && !string.IsNullOrEmpty(value) ? Version.Parse(value + (value.Contains('.') ? "" : ".0")) : null; } internal static IReadOnlyDictionary? ReadStringDictionary(this IConfigurationSection section) { if (section.GetChildren() is var children && !children.Any()) { return null; } return new ReadOnlyDictionary(children.ToDictionary(s => s.Key, s => s.Value!, StringComparer.OrdinalIgnoreCase)); } internal static string[]? ReadStringArray(this IConfigurationSection section) { if (section.GetChildren() is var children && !children.Any()) { return null; } return children.Select(s => s.Value!).ToArray(); } } ================================================ FILE: src/ReverseProxy/Configuration/ConfigProvider/ConfigurationSnapshot.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; using Microsoft.Extensions.Primitives; namespace Yarp.ReverseProxy.Configuration.ConfigProvider; internal sealed class ConfigurationSnapshot : IProxyConfig { public List Routes { get; internal set; } = new List(); public List Clusters { get; internal set; } = new List(); IReadOnlyList IProxyConfig.Routes => Routes; IReadOnlyList IProxyConfig.Clusters => Clusters; // This field is required. public IChangeToken ChangeToken { get; internal set; } = default!; } ================================================ FILE: src/ReverseProxy/Configuration/ConfigValidator.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Yarp.ReverseProxy.Configuration.ClusterValidators; using Yarp.ReverseProxy.Configuration.RouteValidators; using Yarp.ReverseProxy.Transforms.Builder; namespace Yarp.ReverseProxy.Configuration; internal sealed class ConfigValidator : IConfigValidator { private readonly ITransformBuilder _transformBuilder; private readonly IRouteValidator[] _routeValidators; private readonly IClusterValidator[] _clusterValidators; public ConfigValidator(ITransformBuilder transformBuilder, IEnumerable routeValidators, IEnumerable clusterValidators) { ArgumentNullException.ThrowIfNull(transformBuilder); ArgumentNullException.ThrowIfNull(routeValidators); ArgumentNullException.ThrowIfNull(clusterValidators); _transformBuilder = transformBuilder; _routeValidators = routeValidators.ToArray(); _clusterValidators = clusterValidators.ToArray(); } // Note this performs all validation steps without short-circuiting in order to report all possible errors. public async ValueTask> ValidateRouteAsync(RouteConfig route) { ArgumentNullException.ThrowIfNull(route); var errors = new List(); if (string.IsNullOrEmpty(route.RouteId)) { errors.Add(new ArgumentException("Missing Route Id.")); } errors.AddRange(_transformBuilder.ValidateRoute(route)); if (route.Match is null) { errors.Add(new ArgumentException($"Route '{route.RouteId}' did not set any match criteria, it requires Hosts or Path specified. Set the Path to '/{{**catchall}}' to match all requests.")); return errors; } if ((route.Match.Hosts is null || route.Match.Hosts.All(string.IsNullOrEmpty)) && string.IsNullOrEmpty(route.Match.Path)) { errors.Add(new ArgumentException($"Route '{route.RouteId}' requires Hosts or Path specified. Set the Path to '/{{**catchall}}' to match all requests.")); } foreach (var routeValidator in _routeValidators) { await routeValidator.ValidateAsync(route, errors); } return errors; } // Note this performs all validation steps without short-circuiting in order to report all possible errors. public async ValueTask> ValidateClusterAsync(ClusterConfig cluster) { ArgumentNullException.ThrowIfNull(cluster); var errors = new List(); if (string.IsNullOrEmpty(cluster.ClusterId)) { errors.Add(new ArgumentException("Missing Cluster Id.")); } errors.AddRange(_transformBuilder.ValidateCluster(cluster)); foreach (var clusterValidator in _clusterValidators) { await clusterValidator.ValidateAsync(cluster, errors); } return errors; } } ================================================ FILE: src/ReverseProxy/Configuration/CorsConstants.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. namespace Yarp.ReverseProxy.Configuration; internal static class CorsConstants { internal const string Default = "Default"; internal const string Disable = "Disable"; } ================================================ FILE: src/ReverseProxy/Configuration/DestinationConfig.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using Yarp.ReverseProxy.Utilities; namespace Yarp.ReverseProxy.Configuration; /// /// Describes a destination of a cluster. /// public sealed record DestinationConfig { /// /// Address of this destination. E.g. https://127.0.0.1:123/abcd1234/. /// This field is required. /// public string Address { get; init; } = default!; /// /// Endpoint accepting active health check probes. E.g. http://127.0.0.1:1234/. /// public string? Health { get; init; } /// /// Arbitrary key-value pairs that further describe this destination. /// public IReadOnlyDictionary? Metadata { get; init; } /// /// Host header value to pass to this destination. /// Used as a fallback if a host is not already specified by request transforms. /// public string? Host { get; init; } public bool Equals(DestinationConfig? other) { if (other is null) { return false; } return string.Equals(Address, other.Address, StringComparison.OrdinalIgnoreCase) && string.Equals(Health, other.Health, StringComparison.OrdinalIgnoreCase) && string.Equals(Host, other.Host, StringComparison.OrdinalIgnoreCase) && CaseSensitiveEqualHelper.Equals(Metadata, other.Metadata); } public override int GetHashCode() { return HashCode.Combine( Address?.GetHashCode(StringComparison.OrdinalIgnoreCase), Health?.GetHashCode(StringComparison.OrdinalIgnoreCase), Host?.GetHashCode(StringComparison.OrdinalIgnoreCase), CaseSensitiveEqualHelper.GetHashCode(Metadata)); } } ================================================ FILE: src/ReverseProxy/Configuration/HeaderMatchMode.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. namespace Yarp.ReverseProxy.Configuration; /// /// How to match header values. /// public enum HeaderMatchMode { /// /// Any of the headers with the given name must match in its entirety, subject to case sensitivity settings. /// If a header contains multiple values (separated by , or ;), they are split before matching. /// A single pair of quotes will also be stripped from the value before matching. /// ExactHeader, /// /// Any of the headers with the given name must match by prefix, subject to case sensitivity settings. /// If a header contains multiple values (separated by , or ;), they are split before matching. /// A single pair of quotes will also be stripped from the value before matching. /// HeaderPrefix, /// /// Any of the headers with the given name must contain any of the match values, subject to case sensitivity settings. /// Contains, /// /// The header must exist and the value must be non-empty. /// None of the headers with the given name may contain any of the match values, subject to case sensitivity settings. /// NotContains, /// /// The header must exist and contain any non-empty value. /// If there are multiple headers with the same name, the rule will also match. /// Exists, /// /// The header must not exist. /// NotExists, } ================================================ FILE: src/ReverseProxy/Configuration/HealthCheckConfig.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; namespace Yarp.ReverseProxy.Configuration; /// /// All health check config. /// public sealed record HealthCheckConfig { /// /// Passive health check config. /// public PassiveHealthCheckConfig? Passive { get; init; } /// /// Active health check config. /// public ActiveHealthCheckConfig? Active { get; init; } /// /// Available destinations policy. /// public string? AvailableDestinationsPolicy { get; init; } public bool Equals(HealthCheckConfig? other) { if (other is null) { return false; } return Passive == other.Passive && Active == other.Active && string.Equals(AvailableDestinationsPolicy, other.AvailableDestinationsPolicy, StringComparison.OrdinalIgnoreCase); } public override int GetHashCode() { return HashCode.Combine( Passive, Active, AvailableDestinationsPolicy?.GetHashCode(StringComparison.OrdinalIgnoreCase)); } } ================================================ FILE: src/ReverseProxy/Configuration/HttpClientConfig.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Net.Http; using System.Security.Authentication; using System.Text; using Microsoft.AspNetCore.Server.Kestrel.Core; using Yarp.ReverseProxy.Forwarder; namespace Yarp.ReverseProxy.Configuration; /// /// Options used for communicating with the destination servers. /// /// /// If you need a more granular approach, please use a custom implementation of . /// public sealed record HttpClientConfig { /// /// An empty options instance. /// public static readonly HttpClientConfig Empty = new(); /// /// What TLS protocols to use. /// public SslProtocols? SslProtocols { get; init; } /// /// Indicates if destination server https certificate errors should be ignored. /// This should only be done when using self-signed certificates. /// public bool? DangerousAcceptAnyServerCertificate { get; init; } /// /// Limits the number of connections used when communicating with the destination server. /// public int? MaxConnectionsPerServer { get; init; } /// /// Optional web proxy used when communicating with the destination server. /// public WebProxyConfig? WebProxy { get; init; } /// /// 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; } /// /// Allows overriding the default (ASCII) encoding for outgoing request headers. /// /// Setting this value will in turn set and use the selected encoding for all request headers. /// The value is then parsed by , so use values like: "utf-8", "iso-8859-1", etc. /// /// /// /// Note: If you're using an encoding other than UTF-8 here, then you may also need to configure your server to accept request headers with such an encoding via the corresponding options for the server. /// /// For example, when using Kestrel as the server, use to /// configure Kestrel to use the same encoding. /// /// public string? RequestHeaderEncoding { get; init; } /// /// Allows overriding the default (Latin1) encoding for incoming request headers. /// /// Setting this value will in turn set and use the selected encoding for all response headers. /// The value is then parsed by , so use values like: "utf-8", "iso-8859-1", etc. /// /// /// /// Note: If you're using an encoding other than ASCII here, then you may also need to configure your server to send response headers with such an encoding via the corresponding options for the server. /// /// For example, when using Kestrel as the server, use to /// configure Kestrel to use the same encoding. /// /// public string? ResponseHeaderEncoding { get; init; } public bool Equals(HttpClientConfig? other) { if (other is null) { return false; } return SslProtocols == other.SslProtocols && DangerousAcceptAnyServerCertificate == other.DangerousAcceptAnyServerCertificate && MaxConnectionsPerServer == other.MaxConnectionsPerServer && EnableMultipleHttp2Connections == other.EnableMultipleHttp2Connections // Comparing by reference is fine here since Encoding.GetEncoding returns the same instance for each encoding. && RequestHeaderEncoding == other.RequestHeaderEncoding && ResponseHeaderEncoding == other.ResponseHeaderEncoding && WebProxy == other.WebProxy; } public override int GetHashCode() { return HashCode.Combine(SslProtocols, DangerousAcceptAnyServerCertificate, MaxConnectionsPerServer, EnableMultipleHttp2Connections, RequestHeaderEncoding, ResponseHeaderEncoding, WebProxy); } } ================================================ FILE: src/ReverseProxy/Configuration/IConfigChangeListener.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; namespace Yarp.ReverseProxy.Configuration; /// /// Allows subscribing to events notifying you when the configuration is loaded and applied, or when those actions fail. /// public interface IConfigChangeListener { /// /// Invoked when an error occurs while loading the configuration. /// /// The instance of the configuration provider that failed to provide the configuration. /// The thrown exception. void ConfigurationLoadingFailed(IProxyConfigProvider configProvider, Exception exception); /// /// Invoked once the configuration have been successfully loaded. /// /// The list of instances that have been loaded. void ConfigurationLoaded(IReadOnlyList proxyConfigs); /// /// Invoked when an error occurs while applying the configuration. /// /// The list of instances that were being processed. /// The thrown exception. void ConfigurationApplyingFailed(IReadOnlyList proxyConfigs, Exception exception); /// /// Invoked once the configuration has been successfully applied. /// /// The list of instances that have been applied. void ConfigurationApplied(IReadOnlyList proxyConfigs); } ================================================ FILE: src/ReverseProxy/Configuration/IConfigValidator.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Threading.Tasks; namespace Yarp.ReverseProxy.Configuration; /// /// Provides methods to validate routes and clusters. /// public interface IConfigValidator { /// /// Validates a route and returns all errors /// ValueTask> ValidateRouteAsync(RouteConfig route); /// /// Validates a cluster and returns all errors. /// ValueTask> ValidateClusterAsync(ClusterConfig cluster); } ================================================ FILE: src/ReverseProxy/Configuration/IProxyConfig.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Runtime.CompilerServices; using Microsoft.Extensions.Primitives; namespace Yarp.ReverseProxy.Configuration; /// /// Represents a snapshot of proxy configuration data. These properties may be accessed multiple times and should not be modified. /// public interface IProxyConfig { private static readonly ConditionalWeakTable _revisionIdsTable = new(); /// /// A unique identifier for this revision of the configuration. /// string RevisionId => _revisionIdsTable.GetValue(this, static _ => Guid.NewGuid().ToString()); /// /// Routes matching requests to clusters. /// IReadOnlyList Routes { get; } /// /// Cluster information for where to proxy requests to. /// IReadOnlyList Clusters { get; } /// /// A notification that triggers when this snapshot expires. /// IChangeToken ChangeToken { get; } } ================================================ FILE: src/ReverseProxy/Configuration/IProxyConfigFilter.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Threading; using System.Threading.Tasks; namespace Yarp.ReverseProxy.Configuration; /// /// A configuration filter that will run each time the proxy configuration is loaded. /// public interface IProxyConfigFilter { /// /// Allows modification of a cluster configuration. /// /// The instance to configure. /// ValueTask ConfigureClusterAsync(ClusterConfig cluster, CancellationToken cancel); /// /// Allows modification of a route configuration. /// /// The instance to configure. /// The instance related to . /// ValueTask ConfigureRouteAsync(RouteConfig route, ClusterConfig? cluster, CancellationToken cancel); } ================================================ FILE: src/ReverseProxy/Configuration/IProxyConfigProvider.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. namespace Yarp.ReverseProxy.Configuration; /// /// A data source for proxy route and cluster information. /// public interface IProxyConfigProvider { /// /// Returns the current route and cluster data. /// /// IProxyConfig GetConfig(); } ================================================ FILE: src/ReverseProxy/Configuration/IYarpOutputCachePolicyProvider.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Threading.Tasks; using Microsoft.AspNetCore.OutputCaching; using Microsoft.Extensions.Options; namespace Yarp.ReverseProxy.Configuration; // TODO: update or remove this once AspNetCore provides a mechanism to validate the OutputCache policies https://github.com/dotnet/aspnetcore/issues/52419 internal interface IYarpOutputCachePolicyProvider { ValueTask GetPolicyAsync(string policyName); } internal sealed class YarpOutputCachePolicyProvider : IYarpOutputCachePolicyProvider { // Workaround for https://github.com/dotnet/yarp/issues/2598 to make YARP work with NativeAOT on .NET 8. This is not needed on .NET 9+. [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] private static readonly Type s_OutputCacheOptionsType = typeof(OutputCacheOptions); private readonly OutputCacheOptions _outputCacheOptions; private readonly IDictionary _policyMap; public YarpOutputCachePolicyProvider(IOptions outputCacheOptions) { ArgumentNullException.ThrowIfNull(outputCacheOptions?.Value); _outputCacheOptions = outputCacheOptions.Value; var property = s_OutputCacheOptionsType.GetProperty("NamedPolicies", BindingFlags.Instance | BindingFlags.NonPublic); if (property == null || !typeof(IDictionary).IsAssignableFrom(property.PropertyType)) { throw new NotSupportedException("This version of YARP is incompatible with the current version of ASP.NET Core."); } _policyMap = (property.GetValue(_outputCacheOptions, null) as IDictionary) ?? new Dictionary(); } public ValueTask GetPolicyAsync(string policyName) { return ValueTask.FromResult(_policyMap[policyName]); } } ================================================ FILE: src/ReverseProxy/Configuration/IYarpRateLimiterPolicyProvider.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections; using System.Reflection; using Microsoft.AspNetCore.RateLimiting; using Microsoft.Extensions.Options; using System.Threading.Tasks; namespace Yarp.ReverseProxy.Configuration; // TODO: update or remove this once AspNetCore provides a mechanism to validate the RateLimiter policies https://github.com/dotnet/aspnetcore/issues/45684 internal interface IYarpRateLimiterPolicyProvider { ValueTask GetPolicyAsync(string policyName); } internal sealed class YarpRateLimiterPolicyProvider : IYarpRateLimiterPolicyProvider { private readonly RateLimiterOptions _rateLimiterOptions; private readonly IDictionary _policyMap, _unactivatedPolicyMap; public YarpRateLimiterPolicyProvider(IOptions rateLimiterOptions) { ArgumentNullException.ThrowIfNull(rateLimiterOptions?.Value); _rateLimiterOptions = rateLimiterOptions.Value; var type = typeof(RateLimiterOptions); var flags = BindingFlags.Instance | BindingFlags.NonPublic; _policyMap = type.GetProperty("PolicyMap", flags)?.GetValue(_rateLimiterOptions, null) as IDictionary ?? throw new NotSupportedException("This version of YARP is incompatible with the current version of ASP.NET Core."); _unactivatedPolicyMap = type.GetProperty("UnactivatedPolicyMap", flags)?.GetValue(_rateLimiterOptions, null) as IDictionary ?? throw new NotSupportedException("This version of YARP is incompatible with the current version of ASP.NET Core."); } public ValueTask GetPolicyAsync(string policyName) { return ValueTask.FromResult(_policyMap[policyName] ?? _unactivatedPolicyMap[policyName]); } } ================================================ FILE: src/ReverseProxy/Configuration/InMemoryConfigProvider.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Threading; using Microsoft.Extensions.Primitives; namespace Yarp.ReverseProxy.Configuration; /// /// Provides an implementation of IProxyConfigProvider to support config being generated by code. /// public sealed class InMemoryConfigProvider : IProxyConfigProvider { // Marked as volatile so that updates are atomic private volatile InMemoryConfig _config; /// /// Creates a new instance. /// public InMemoryConfigProvider(IReadOnlyList routes, IReadOnlyList clusters) : this(routes, clusters, Guid.NewGuid().ToString()) { } /// /// Creates a new instance, specifying a revision id of the configuration. /// public InMemoryConfigProvider(IReadOnlyList routes, IReadOnlyList clusters, string revisionId) { _config = new InMemoryConfig(routes, clusters, revisionId); } /// /// Implementation of the IProxyConfigProvider.GetConfig method to supply the current snapshot of configuration /// /// An immutable snapshot of the current configuration state public IProxyConfig GetConfig() => _config; /// /// Swaps the config state with a new snapshot of the configuration, then signals that the old one is outdated. /// public void Update(IReadOnlyList routes, IReadOnlyList clusters) { var newConfig = new InMemoryConfig(routes, clusters); UpdateInternal(newConfig); } /// /// Swaps the config state with a new snapshot of the configuration, then signals that the old one is outdated. /// public void Update(IReadOnlyList routes, IReadOnlyList clusters, string revisionId) { var newConfig = new InMemoryConfig(routes, clusters, revisionId); UpdateInternal(newConfig); } private void UpdateInternal(InMemoryConfig newConfig) { var oldConfig = Interlocked.Exchange(ref _config, newConfig); oldConfig.SignalChange(); } /// /// Implementation of IProxyConfig which is a snapshot of the current config state. The data for this class should be immutable. /// private sealed class InMemoryConfig : IProxyConfig { // Used to implement the change token for the state private readonly CancellationTokenSource _cts = new CancellationTokenSource(); public InMemoryConfig(IReadOnlyList routes, IReadOnlyList clusters) : this(routes, clusters, Guid.NewGuid().ToString()) { } public InMemoryConfig(IReadOnlyList routes, IReadOnlyList clusters, string revisionId) { ArgumentNullException.ThrowIfNull(revisionId); RevisionId = revisionId; Routes = routes; Clusters = clusters; ChangeToken = new CancellationChangeToken(_cts.Token); } /// public string RevisionId { get; } /// /// A snapshot of the list of routes for the proxy /// public IReadOnlyList Routes { get; } /// /// A snapshot of the list of Clusters which are collections of interchangeable destination endpoints /// public IReadOnlyList Clusters { get; } /// /// Fired to indicate the proxy state has changed, and that this snapshot is now stale /// public IChangeToken ChangeToken { get; } internal void SignalChange() { _cts.Cancel(); } } } ================================================ FILE: src/ReverseProxy/Configuration/InMemoryConfigProviderExtensions.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; using Yarp.ReverseProxy.Configuration; namespace Microsoft.Extensions.DependencyInjection; public static class InMemoryConfigProviderExtensions { /// /// Adds an InMemoryConfigProvider /// public static IReverseProxyBuilder LoadFromMemory(this IReverseProxyBuilder builder, IReadOnlyList routes, IReadOnlyList clusters) { builder.Services.AddSingleton(new InMemoryConfigProvider(routes, clusters)); builder.Services.AddSingleton(s => s.GetRequiredService()); return builder; } } ================================================ FILE: src/ReverseProxy/Configuration/PassiveHealthCheckConfig.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using Yarp.ReverseProxy.Model; namespace Yarp.ReverseProxy.Configuration; /// /// Passive health check config. /// public sealed record PassiveHealthCheckConfig { /// /// Whether passive health checks are enabled. /// public bool? Enabled { get; init; } /// /// Passive health check policy. /// public string? Policy { get; init; } /// /// Destination reactivation period after which an unhealthy destination reverts back to . /// public TimeSpan? ReactivationPeriod { get; init; } public bool Equals(PassiveHealthCheckConfig? other) { if (other is null) { return false; } return Enabled == other.Enabled && string.Equals(Policy, other.Policy, StringComparison.OrdinalIgnoreCase) && ReactivationPeriod == other.ReactivationPeriod; } public override int GetHashCode() { return HashCode.Combine(Enabled, Policy?.GetHashCode(StringComparison.OrdinalIgnoreCase), ReactivationPeriod); } } ================================================ FILE: src/ReverseProxy/Configuration/QueryParameterMatchMode.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. namespace Yarp.ReverseProxy.Configuration; /// /// How to match Query Parameter values. /// public enum QueryParameterMatchMode { /// /// Query string must match in its entirety, /// Subject to case sensitivity settings. /// Only single query parameter name supported. If there are multiple query parameters with the same name then the match fails. /// Exact, /// /// Query string key must be present and substring must match for each of the respective query string values. /// Subject to case sensitivity settings. /// Only single query parameter name supported. If there are multiple query parameters with the same name then the match fails. /// Contains, /// /// Query string key must be present and value must not match for each of the respective query string values. /// Subject to case sensitivity settings. /// If there are multiple values then it needs to not contain ANY of the values /// Only single query parameter name supported. If there are multiple query parameters with the same name then the match fails. /// NotContains, /// /// Query string key must be present and prefix must match for each of the respective query string values. /// Subject to case sensitivity settings. /// Only single query parameter name supported. If there are multiple query parameters with the same name then the match fails. /// Prefix, /// /// Query string key must exist and contain any non-empty value. /// Exists } ================================================ FILE: src/ReverseProxy/Configuration/RateLimitingConstants.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. namespace Yarp.ReverseProxy.Configuration; internal static class RateLimitingConstants { internal const string Default = "Default"; internal const string Disable = "Disable"; } ================================================ FILE: src/ReverseProxy/Configuration/RouteConfig.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Routing; using Yarp.ReverseProxy.Utilities; namespace Yarp.ReverseProxy.Configuration; /// /// Describes a route that matches incoming requests based on the criteria /// and proxies matching requests to the cluster identified by its . /// public sealed record RouteConfig { /// /// Globally unique identifier of the route. /// This field is required. /// public string RouteId { get; init; } = default!; /// /// Parameters used to match requests. /// This field is required. /// public RouteMatch Match { get; init; } = default!; /// /// Optionally, an order value for this route. Routes with lower numbers take precedence over higher numbers. /// public int? Order { get; init; } /// /// Gets or sets the cluster that requests matching this route /// should be proxied to. /// public string? ClusterId { get; init; } /// /// The name of the AuthorizationPolicy to apply to this route. /// If not set then only the FallbackPolicy will apply. /// Set to "Default" to enable authorization with the applications default policy. /// Set to "Anonymous" to disable all authorization checks for this route. /// public string? AuthorizationPolicy { get; init; } /// /// The name of the RateLimiterPolicy to apply to this route. /// If not set then only the GlobalLimiter will apply. /// Set to "Disable" to disable rate limiting for this route. /// Set to "Default" or leave empty to use the global rate limits, if any. /// public string? RateLimiterPolicy { get; init; } /// /// The name of the OutputCachePolicy to apply to this route. /// If not set then only the BasePolicy will apply. /// public string? OutputCachePolicy { get; init; } /// /// The name of the TimeoutPolicy to apply to this route. /// Setting both Timeout and TimeoutPolicy is an error. /// If not set then only the system default will apply. /// Set to "Disable" to disable timeouts for this route. /// Set to "Default" or leave empty to use the system defaults, if any. /// public string? TimeoutPolicy { get; init; } /// /// The Timeout to apply to this route. This overrides any system defaults. /// Setting both Timeout and TimeoutPolicy is an error. /// Timeout granularity is limited to milliseconds. /// public TimeSpan? Timeout { get; init; } /// /// The name of the CorsPolicy to apply to this route. /// If not set then the route won't be automatically matched for cors preflight requests. /// Set to "Default" to enable cors with the default policy. /// Set to "Disable" to refuses cors requests for this route. /// public string? CorsPolicy { get; init; } /// /// An optional override for how large request bodies can be in bytes. If set, this overrides the server's default (30MB) per request. /// Set to '-1' to disable the limit for this route. /// Note that this limit applies only to the YARP forwarder middleware, it does not apply when reading the request body from a custom middleware registered via /// . /// public long? MaxRequestBodySize { get; init; } /// /// Arbitrary key-value pairs that further describe this route. /// public IReadOnlyDictionary? Metadata { get; init; } /// /// Parameters used to transform the request and response. See . /// public IReadOnlyList>? Transforms { get; init; } public bool Equals(RouteConfig? other) { if (other is null) { return false; } return Order == other.Order && string.Equals(RouteId, other.RouteId, StringComparison.OrdinalIgnoreCase) && string.Equals(ClusterId, other.ClusterId, StringComparison.OrdinalIgnoreCase) && string.Equals(AuthorizationPolicy, other.AuthorizationPolicy, StringComparison.OrdinalIgnoreCase) && string.Equals(RateLimiterPolicy, other.RateLimiterPolicy, StringComparison.OrdinalIgnoreCase) && string.Equals(OutputCachePolicy, other.OutputCachePolicy, StringComparison.OrdinalIgnoreCase) && string.Equals(TimeoutPolicy, other.TimeoutPolicy, StringComparison.OrdinalIgnoreCase) && Timeout == other.Timeout && string.Equals(CorsPolicy, other.CorsPolicy, StringComparison.OrdinalIgnoreCase) && Match == other.Match && CaseSensitiveEqualHelper.Equals(Metadata, other.Metadata) && CaseSensitiveEqualHelper.Equals(Transforms, other.Transforms); } public override int GetHashCode() { // HashCode.Combine(...) takes only 8 arguments var hash = new HashCode(); hash.Add(Order); hash.Add(RouteId?.GetHashCode(StringComparison.OrdinalIgnoreCase)); hash.Add(ClusterId?.GetHashCode(StringComparison.OrdinalIgnoreCase)); hash.Add(AuthorizationPolicy?.GetHashCode(StringComparison.OrdinalIgnoreCase)); hash.Add(RateLimiterPolicy?.GetHashCode(StringComparison.OrdinalIgnoreCase)); hash.Add(OutputCachePolicy?.GetHashCode(StringComparison.OrdinalIgnoreCase)); hash.Add(Timeout?.GetHashCode()); hash.Add(TimeoutPolicy?.GetHashCode(StringComparison.OrdinalIgnoreCase)); hash.Add(CorsPolicy?.GetHashCode(StringComparison.OrdinalIgnoreCase)); hash.Add(Match); hash.Add(CaseSensitiveEqualHelper.GetHashCode(Metadata)); hash.Add(CaseSensitiveEqualHelper.GetHashCode(Transforms)); return hash.ToHashCode(); } } ================================================ FILE: src/ReverseProxy/Configuration/RouteHeader.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using Yarp.ReverseProxy.Utilities; namespace Yarp.ReverseProxy.Configuration; /// /// Route criteria for a header that must be present on the incoming request. /// public sealed record RouteHeader { /// /// Name of the header to look for. /// This field is case insensitive and required. /// public string Name { get; init; } = default!; /// /// A collection of acceptable header values used during routing. Only one value must match. /// The list must not be empty unless using or . /// public IReadOnlyList? Values { get; init; } /// /// Specifies how header values should be compared (e.g. exact matches Vs. by prefix). /// Defaults to . /// public HeaderMatchMode Mode { get; init; } /// /// Specifies whether header value comparisons should ignore case. /// When true, is used. /// When false, is used. /// Defaults to false. /// public bool IsCaseSensitive { get; init; } public bool Equals(RouteHeader? other) { if (other is null) { return false; } return string.Equals(Name, other.Name, StringComparison.OrdinalIgnoreCase) && Mode == other.Mode && IsCaseSensitive == other.IsCaseSensitive && (IsCaseSensitive ? CaseSensitiveEqualHelper.Equals(Values, other.Values) : CaseInsensitiveEqualHelper.Equals(Values, other.Values)); } public override int GetHashCode() { return HashCode.Combine( Name?.GetHashCode(StringComparison.OrdinalIgnoreCase), Mode, IsCaseSensitive, IsCaseSensitive ? CaseSensitiveEqualHelper.GetHashCode(Values) : CaseInsensitiveEqualHelper.GetHashCode(Values)); } } ================================================ FILE: src/ReverseProxy/Configuration/RouteMatch.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using Yarp.ReverseProxy.Utilities; namespace Yarp.ReverseProxy.Configuration; /// /// Describes the matching criteria for a route. /// public sealed record RouteMatch { /// /// Only match requests that use these optional HTTP methods. E.g. GET, POST. /// public IReadOnlyList? Methods { get; init; } /// /// Only match requests with the given Host header. /// Supports wildcards and ports. For unicode host names, do not use punycode. /// public IReadOnlyList? Hosts { get; init; } /// /// Only match requests with the given Path pattern. /// public string? Path { get; init; } /// /// Only match requests that contain all of these query parameters. /// public IReadOnlyList? QueryParameters { get; init; } /// /// Only match requests that contain all of these headers. /// public IReadOnlyList? Headers { get; init; } public bool Equals(RouteMatch? other) { if (other is null) { return false; } return string.Equals(Path, other.Path, StringComparison.OrdinalIgnoreCase) && CaseInsensitiveEqualHelper.Equals(Hosts, other.Hosts) && CaseInsensitiveEqualHelper.Equals(Methods, other.Methods) && CollectionEqualityHelper.Equals(Headers, other.Headers) && CollectionEqualityHelper.Equals(QueryParameters, other.QueryParameters); } public override int GetHashCode() { return HashCode.Combine( Path?.GetHashCode(StringComparison.OrdinalIgnoreCase), CaseInsensitiveEqualHelper.GetHashCode(Hosts), CaseInsensitiveEqualHelper.GetHashCode(Methods), CollectionEqualityHelper.GetHashCode(Headers), CollectionEqualityHelper.GetHashCode(QueryParameters)); } } ================================================ FILE: src/ReverseProxy/Configuration/RouteQueryParameter.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using Yarp.ReverseProxy.Utilities; namespace Yarp.ReverseProxy.Configuration; /// /// Route criteria for a query parameter that must be present on the incoming request. /// public sealed record RouteQueryParameter { /// /// Name of the query parameter to look for. /// This field is case insensitive and required. /// public string Name { get; init; } = default!; /// /// A collection of acceptable query parameter values used during routing. /// public IReadOnlyList? Values { get; init; } /// /// Specifies how query parameter values should be compared (e.g. exact matches Vs. contains). /// Defaults to . /// public QueryParameterMatchMode Mode { get; init; } /// /// Specifies whether query parameter value comparisons should ignore case. /// When true, is used. /// When false, is used. /// Defaults to false. /// public bool IsCaseSensitive { get; init; } public bool Equals(RouteQueryParameter? other) { if (other is null) { return false; } return string.Equals(Name, other.Name, StringComparison.OrdinalIgnoreCase) && Mode == other.Mode && IsCaseSensitive == other.IsCaseSensitive && (IsCaseSensitive ? CaseSensitiveEqualHelper.Equals(Values, other.Values) : CaseInsensitiveEqualHelper.Equals(Values, other.Values)); } public override int GetHashCode() { return HashCode.Combine( Name?.GetHashCode(StringComparison.OrdinalIgnoreCase), Mode, IsCaseSensitive, IsCaseSensitive ? CaseSensitiveEqualHelper.GetHashCode(Values) : CaseInsensitiveEqualHelper.GetHashCode(Values)); } } ================================================ FILE: src/ReverseProxy/Configuration/RouteValidators/AuthorizationPolicyValidator.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; namespace Yarp.ReverseProxy.Configuration.RouteValidators; internal sealed class AuthorizationPolicyValidator (IAuthorizationPolicyProvider authorizationPolicyProvider) : IRouteValidator { public async ValueTask ValidateAsync(RouteConfig routeConfig, IList errors) { var authorizationPolicyName = routeConfig.AuthorizationPolicy; if (string.IsNullOrEmpty(authorizationPolicyName)) { return; } if (string.Equals(AuthorizationConstants.Default, authorizationPolicyName, StringComparison.OrdinalIgnoreCase)) { var policy = await authorizationPolicyProvider.GetPolicyAsync(authorizationPolicyName); if (policy is not null) { errors.Add(new ArgumentException($"The application has registered an authorization policy named '{authorizationPolicyName}' that conflicts with the reserved authorization policy name used on this route. The registered policy name needs to be changed for this route to function.")); } return; } if (string.Equals(AuthorizationConstants.Anonymous, authorizationPolicyName, StringComparison.OrdinalIgnoreCase)) { var policy = await authorizationPolicyProvider.GetPolicyAsync(authorizationPolicyName); if (policy is not null) { errors.Add(new ArgumentException( $"The application has registered an authorization policy named '{authorizationPolicyName}' that conflicts with the reserved authorization policy name used on this route. The registered policy name needs to be changed for this route to function.")); } return; } try { var policy = await authorizationPolicyProvider.GetPolicyAsync(authorizationPolicyName); if (policy is null) { errors.Add(new ArgumentException( $"Authorization policy '{authorizationPolicyName}' not found for route '{routeConfig.RouteId}'.")); } } catch (Exception ex) { errors.Add(new ArgumentException( $"Unable to retrieve the authorization policy '{authorizationPolicyName}' for route '{routeConfig.RouteId}'.", ex)); } } } ================================================ FILE: src/ReverseProxy/Configuration/RouteValidators/CorsPolicyValidator.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.AspNetCore.Cors.Infrastructure; using Microsoft.AspNetCore.Http; namespace Yarp.ReverseProxy.Configuration.RouteValidators; internal sealed class CorsPolicyValidator(ICorsPolicyProvider corsPolicyProvider) : IRouteValidator { public async ValueTask ValidateAsync(RouteConfig routeConfig, IList errors) { var corsPolicyName = routeConfig.CorsPolicy; if (string.IsNullOrEmpty(corsPolicyName)) { return; } if (string.Equals(CorsConstants.Default, corsPolicyName, StringComparison.OrdinalIgnoreCase)) { var dummyHttpContext = new DefaultHttpContext(); var policy = await corsPolicyProvider.GetPolicyAsync(dummyHttpContext, corsPolicyName); if (policy is not null) { errors.Add(new ArgumentException( $"The application has registered a CORS policy named '{corsPolicyName}' that conflicts with the reserved CORS policy name used on this route. The registered policy name needs to be changed for this route to function.")); } return; } if (string.Equals(CorsConstants.Disable, corsPolicyName, StringComparison.OrdinalIgnoreCase)) { var dummyHttpContext = new DefaultHttpContext(); var policy = await corsPolicyProvider.GetPolicyAsync(dummyHttpContext, corsPolicyName); if (policy is not null) { errors.Add(new ArgumentException( $"The application has registered a CORS policy named '{corsPolicyName}' that conflicts with the reserved CORS policy name used on this route. The registered policy name needs to be changed for this route to function.")); } return; } try { var dummyHttpContext = new DefaultHttpContext(); var policy = await corsPolicyProvider.GetPolicyAsync(dummyHttpContext, corsPolicyName); if (policy is null) { errors.Add(new ArgumentException( $"CORS policy '{corsPolicyName}' not found for route '{routeConfig.RouteId}'.")); } } catch (Exception ex) { errors.Add(new ArgumentException( $"Unable to retrieve the CORS policy '{corsPolicyName}' for route '{routeConfig.RouteId}'.", ex)); } } } ================================================ FILE: src/ReverseProxy/Configuration/RouteValidators/HeadersValidator.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Threading.Tasks; namespace Yarp.ReverseProxy.Configuration.RouteValidators; internal sealed class HeadersValidator : IRouteValidator { public ValueTask ValidateAsync(RouteConfig routeConfig, IList errors) { var route = routeConfig.Match; if (route.Headers is null) { // Headers are optional return ValueTask.CompletedTask; } foreach (var header in route.Headers) { if (header is null) { errors.Add(new ArgumentException($"A null route header has been set for route '{routeConfig.RouteId}'.")); continue; } if (string.IsNullOrEmpty(header.Name)) { errors.Add(new ArgumentException($"A null or empty route header name has been set for route '{routeConfig.RouteId}'.")); } if (header.Mode != HeaderMatchMode.Exists && header.Mode != HeaderMatchMode.NotExists && (header.Values is null || header.Values.Count == 0)) { errors.Add(new ArgumentException($"No header values were set on route header '{header.Name}' for route '{routeConfig.RouteId}'.")); } if ((header.Mode == HeaderMatchMode.Exists || header.Mode == HeaderMatchMode.NotExists) && header.Values?.Count > 0) { errors.Add(new ArgumentException($"Header values were set when using mode '{header.Mode}' on route header '{header.Name}' for route '{routeConfig.RouteId}'.")); } } return ValueTask.CompletedTask; } } ================================================ FILE: src/ReverseProxy/Configuration/RouteValidators/HostValidator.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Threading.Tasks; namespace Yarp.ReverseProxy.Configuration.RouteValidators; internal sealed class HostValidator : IRouteValidator { public ValueTask ValidateAsync(RouteConfig routeConfig, IList errors) { var route = routeConfig.Match; if (route.Hosts is null || route.Hosts.Count == 0) { // Host is optional when Path is specified return ValueTask.CompletedTask; } foreach (var host in route.Hosts) { if (string.IsNullOrEmpty(host)) { errors.Add(new ArgumentException($"Empty host name has been set for route '{routeConfig.RouteId}'.")); } else if (host.Contains("xn--", StringComparison.OrdinalIgnoreCase)) { errors.Add(new ArgumentException($"Punycode host name '{host}' has been set for route '{routeConfig.RouteId}'. Use the unicode host name instead.")); } } return ValueTask.CompletedTask; } } ================================================ FILE: src/ReverseProxy/Configuration/RouteValidators/IRouteValidator.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Threading.Tasks; namespace Yarp.ReverseProxy.Configuration.RouteValidators; /// /// Provides method to validate route configuration. /// public interface IRouteValidator { /// /// Perform validation on a route by adding exceptions to the provided collection. /// /// Route configuration to validate /// Collection of all validation exceptions /// A ValueTask representing the asynchronous validation operation. public ValueTask ValidateAsync(RouteConfig routeConfig, IList errors); } ================================================ FILE: src/ReverseProxy/Configuration/RouteValidators/MethodsValidator.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Threading.Tasks; namespace Yarp.ReverseProxy.Configuration.RouteValidators; internal sealed class MethodsValidator : IRouteValidator { private static readonly HashSet _validMethods = new(StringComparer.OrdinalIgnoreCase) { "HEAD", "OPTIONS", "GET", "PUT", "POST", "PATCH", "DELETE", "TRACE", }; public ValueTask ValidateAsync(RouteConfig routeConfig, IList errors) { var route = routeConfig.Match; if (route.Methods is null) { // Methods are optional return ValueTask.CompletedTask; } var seenMethods = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (var method in route.Methods) { if (!seenMethods.Add(method)) { errors.Add(new ArgumentException($"Duplicate HTTP method '{method}' for route '{routeConfig.RouteId}'.")); continue; } if (!_validMethods.Contains(method)) { errors.Add(new ArgumentException($"Unsupported HTTP method '{method}' has been set for route '{routeConfig.RouteId}'.")); } } return ValueTask.CompletedTask; } } ================================================ FILE: src/ReverseProxy/Configuration/RouteValidators/OutputCachePolicyValidator.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Threading.Tasks; namespace Yarp.ReverseProxy.Configuration.RouteValidators; internal sealed class OutputCachePolicyValidator : IRouteValidator { private readonly IYarpOutputCachePolicyProvider _outputCachePolicyProvider; public OutputCachePolicyValidator(IYarpOutputCachePolicyProvider outputCachePolicyProvider) { _outputCachePolicyProvider = outputCachePolicyProvider; } public async ValueTask ValidateAsync(RouteConfig routeConfig, IList errors) { var outputCachePolicyName = routeConfig.OutputCachePolicy; if (string.IsNullOrEmpty(outputCachePolicyName)) { return; } try { var policy = await _outputCachePolicyProvider.GetPolicyAsync(outputCachePolicyName); if (policy is null) { errors.Add(new ArgumentException( $"OutputCache policy '{outputCachePolicyName}' not found for route '{routeConfig.RouteId}'.")); } } catch (Exception ex) { errors.Add(new ArgumentException( $"Unable to retrieve the OutputCache policy '{outputCachePolicyName}' for route '{routeConfig.RouteId}'.", ex)); } } } ================================================ FILE: src/ReverseProxy/Configuration/RouteValidators/PathValidator.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.AspNetCore.Routing.Patterns; namespace Yarp.ReverseProxy.Configuration.RouteValidators; internal sealed class PathValidator : IRouteValidator { public ValueTask ValidateAsync(RouteConfig routeConfig, IList errors) { var route = routeConfig.Match; if (string.IsNullOrEmpty(route.Path)) { // Path is optional when Host is specified return ValueTask.CompletedTask; } try { RoutePatternFactory.Parse(route.Path); } catch (RoutePatternException ex) { errors.Add(new ArgumentException($"Invalid path '{route.Path}' for route '{routeConfig.RouteId}'.", ex)); } return ValueTask.CompletedTask; } } ================================================ FILE: src/ReverseProxy/Configuration/RouteValidators/QueryParametersValidator.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Threading.Tasks; namespace Yarp.ReverseProxy.Configuration.RouteValidators; internal sealed class QueryParametersValidator : IRouteValidator { public ValueTask ValidateAsync(RouteConfig routeConfig, IList errors) { var route = routeConfig.Match; if (route.QueryParameters is null) { // Query Parameters are optional return ValueTask.CompletedTask; } foreach (var queryParameter in route.QueryParameters) { if (queryParameter is null) { errors.Add(new ArgumentException($"A null route query parameter has been set for route '{routeConfig.RouteId}'.")); continue; } if (string.IsNullOrEmpty(queryParameter.Name)) { errors.Add(new ArgumentException($"A null or empty route query parameter name has been set for route '{routeConfig.RouteId}'.")); } if (queryParameter.Mode != QueryParameterMatchMode.Exists && (queryParameter.Values is null || queryParameter.Values.Count == 0)) { errors.Add(new ArgumentException($"No query parameter values were set on route query parameter '{queryParameter.Name}' for route '{routeConfig.RouteId}'.")); } if (queryParameter.Mode == QueryParameterMatchMode.Exists && queryParameter.Values?.Count > 0) { errors.Add(new ArgumentException($"Query parameter values where set when using mode '{nameof(QueryParameterMatchMode.Exists)}' on route query parameter '{queryParameter.Name}' for route '{routeConfig.RouteId}'.")); } } return ValueTask.CompletedTask; } } ================================================ FILE: src/ReverseProxy/Configuration/RouteValidators/RateLimitPolicyValidator.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Threading.Tasks; namespace Yarp.ReverseProxy.Configuration.RouteValidators; internal sealed class RateLimitPolicyValidator : IRouteValidator { private readonly IYarpRateLimiterPolicyProvider _rateLimiterPolicyProvider; public RateLimitPolicyValidator(IYarpRateLimiterPolicyProvider rateLimiterPolicyProvider) { _rateLimiterPolicyProvider = rateLimiterPolicyProvider; } public async ValueTask ValidateAsync(RouteConfig routeConfig, IList errors) { var rateLimiterPolicyName = routeConfig.RateLimiterPolicy; if (string.IsNullOrEmpty(rateLimiterPolicyName)) { return; } if (string.Equals(RateLimitingConstants.Default, rateLimiterPolicyName, StringComparison.OrdinalIgnoreCase) || string.Equals(RateLimitingConstants.Disable, rateLimiterPolicyName, StringComparison.OrdinalIgnoreCase)) { var policy = await _rateLimiterPolicyProvider.GetPolicyAsync(rateLimiterPolicyName); if (policy is not null) { // We weren't expecting to find a policy with these names. errors.Add(new ArgumentException( $"The application has registered a RateLimiter policy named '{rateLimiterPolicyName}' that conflicts with the reserved RateLimiter policy name used on this route. The registered policy name needs to be changed for this route to function.")); } return; } try { var policy = await _rateLimiterPolicyProvider.GetPolicyAsync(rateLimiterPolicyName); if (policy is null) { errors.Add(new ArgumentException( $"RateLimiter policy '{rateLimiterPolicyName}' not found for route '{routeConfig.RouteId}'.")); } } catch (Exception ex) { errors.Add(new ArgumentException( $"Unable to retrieve the RateLimiter policy '{rateLimiterPolicyName}' for route '{routeConfig.RouteId}'.", ex)); } } } ================================================ FILE: src/ReverseProxy/Configuration/RouteValidators/TimeoutPolicyValidator.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.AspNetCore.Http.Timeouts; using Microsoft.Extensions.Options; namespace Yarp.ReverseProxy.Configuration.RouteValidators; internal sealed class TimeoutPolicyValidator : IRouteValidator { private readonly IOptionsMonitor _timeoutOptions; public TimeoutPolicyValidator(IOptionsMonitor timeoutOptions) { _timeoutOptions = timeoutOptions; } public ValueTask ValidateAsync(RouteConfig routeConfig, IList errors) { var timeoutPolicyName = routeConfig.TimeoutPolicy; var timeout = routeConfig.Timeout; if (!string.IsNullOrEmpty(timeoutPolicyName)) { var policies = _timeoutOptions.CurrentValue.Policies; if (string.Equals(TimeoutPolicyConstants.Disable, timeoutPolicyName, StringComparison.OrdinalIgnoreCase)) { if (policies.TryGetValue(timeoutPolicyName, out var _)) { errors.Add(new ArgumentException($"The application has registered a timeout policy named '{timeoutPolicyName}' that conflicts with the reserved timeout policy name used on this route. The registered policy name needs to be changed for this route to function.")); } } else if (!policies.TryGetValue(timeoutPolicyName, out var _)) { errors.Add(new ArgumentException($"Timeout policy '{timeoutPolicyName}' not found for route '{routeConfig.RouteId}'.")); } if (timeout.HasValue) { errors.Add(new ArgumentException($"Route '{routeConfig.RouteId}' has both a Timeout '{timeout}' and TimeoutPolicy '{timeoutPolicyName}'.")); } } if (timeout.HasValue && timeout.Value.TotalMilliseconds <= 0) { errors.Add(new ArgumentException($"The Timeout value '{timeout.Value}' is invalid for route '{routeConfig.RouteId}'. The Timeout must be greater than zero milliseconds.")); } return ValueTask.CompletedTask; } } ================================================ FILE: src/ReverseProxy/Configuration/SessionAffinityConfig.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; namespace Yarp.ReverseProxy.Configuration; /// /// Session affinity options. /// public sealed record SessionAffinityConfig { /// /// Indicates whether session affinity is enabled. /// public bool? Enabled { get; init; } /// /// The session affinity policy to use. /// public string? Policy { get; init; } /// /// Strategy for handling a missing destination for an affinitized request. /// public string? FailurePolicy { get; init; } /// /// Identifies the name of the field where the affinity value is stored. /// For the cookie affinity policy this will be the cookie name. /// For the header affinity policy this will be the header name. /// This value should be unique across clusters to avoid affinity conflicts. /// https://github.com/dotnet/yarp/issues/976 /// This field is required. /// public string AffinityKeyName { get; init; } = default!; /// /// Configuration of a cookie storing the session affinity key in case /// the is set to 'Cookie'. /// public SessionAffinityCookieConfig? Cookie { get; init; } public bool Equals(SessionAffinityConfig? other) { if (other is null) { return false; } return Enabled == other.Enabled && string.Equals(Policy, other.Policy, StringComparison.OrdinalIgnoreCase) && string.Equals(FailurePolicy, other.FailurePolicy, StringComparison.OrdinalIgnoreCase) && string.Equals(AffinityKeyName, other.AffinityKeyName, StringComparison.Ordinal) && Cookie == other.Cookie; } public override int GetHashCode() { return HashCode.Combine(Enabled, Policy?.GetHashCode(StringComparison.OrdinalIgnoreCase), FailurePolicy?.GetHashCode(StringComparison.OrdinalIgnoreCase), AffinityKeyName?.GetHashCode(StringComparison.Ordinal), Cookie); } } ================================================ FILE: src/ReverseProxy/Configuration/SessionAffinityCookieConfig.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using Microsoft.AspNetCore.Http; namespace Yarp.ReverseProxy.Configuration; // Mirrors CookieBuilder and CookieOptions // https://github.com/dotnet/aspnetcore/blob/main/src/Http/Http.Abstractions/src/CookieBuilder.cs /// /// Config for session affinity cookies. /// public sealed record SessionAffinityCookieConfig { /// /// The cookie path. /// public string? Path { get; init; } /// /// The domain to associate the cookie with. /// public string? Domain { get; init; } /// /// Indicates whether a cookie is accessible by client-side script. /// /// Defaults to "true". public bool? HttpOnly { get; init; } /// /// The policy that will be used to determine . /// /// Defaults to . public CookieSecurePolicy? SecurePolicy { get; init; } /// /// The SameSite attribute of the cookie. /// /// Defaults to . public SameSiteMode? SameSite { get; init; } /// /// Gets or sets the lifespan of a cookie. /// public TimeSpan? Expiration { get; init; } /// /// Gets or sets the max-age for the cookie. /// public TimeSpan? MaxAge { get; init; } /// /// Indicates if this cookie is essential for the application to function correctly. If true then /// consent policy checks may be bypassed. /// /// Defaults to "false". public bool? IsEssential { get; init; } public bool Equals(SessionAffinityCookieConfig? other) { if (other is null) { return false; } return string.Equals(Path, other.Path, StringComparison.Ordinal) && string.Equals(Domain, other.Domain, StringComparison.OrdinalIgnoreCase) && HttpOnly == other.HttpOnly && SecurePolicy == other.SecurePolicy && SameSite == other.SameSite && Expiration == other.Expiration && MaxAge == other.MaxAge && IsEssential == other.IsEssential; } public override int GetHashCode() { return HashCode.Combine(Path?.GetHashCode(StringComparison.Ordinal), Domain?.GetHashCode(StringComparison.OrdinalIgnoreCase), HttpOnly, SecurePolicy, SameSite, Expiration, MaxAge, IsEssential); } } ================================================ FILE: src/ReverseProxy/Configuration/TimeoutPolicyConstants.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. namespace Yarp.ReverseProxy.Configuration; internal static class TimeoutPolicyConstants { internal const string Disable = "Disable"; } ================================================ FILE: src/ReverseProxy/Configuration/WebProxyConfig.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; namespace Yarp.ReverseProxy.Configuration; /// /// Config used to construct instance. /// public sealed record WebProxyConfig : IEquatable { /// /// The URI of the proxy server. /// public Uri? Address { get; init; } /// /// true to bypass the proxy for local addresses; otherwise, false. /// If null, default value will be used: false /// public bool? BypassOnLocal { get; init; } /// /// Controls whether the are sent with requests. /// If null, default value will be used: false /// public bool? UseDefaultCredentials { get; init; } public bool Equals(WebProxyConfig? other) { if (other is null) { return false; } return Address == other.Address && BypassOnLocal == other.BypassOnLocal && UseDefaultCredentials == other.UseDefaultCredentials; } public override int GetHashCode() { return HashCode.Combine( Address, BypassOnLocal, UseDefaultCredentials ); } } ================================================ FILE: src/ReverseProxy/ConfigurationSchema.json ================================================ { "type": "object", "properties": { "ReverseProxy": { "type": "object", "description": "Reverse proxy configuration for routes and clusters.", "properties": { "Routes": { "type": "object", "description": "Named routes that direct incoming requests to clusters.", "patternProperties": { ".": { "type": "object", "properties": { "ClusterId": { "type": "string", "description": "Name of the cluster this route points to." }, "Order": { "type": [ "number", "null" ], "description": "Order value for this route. Routes with lower numbers take precedence over higher numbers." }, "MaxRequestBodySize": { "type": [ "number", "null" ], "description": "An optional override for how large request bodies can be in bytes." }, "AuthorizationPolicy": { "type": [ "string", "null" ], "description": "Specifies which authorization policy applies, e.g. 'Default' or 'Anonymous'." }, "RateLimiterPolicy": { "type": [ "string", "null" ], "description": "The name of the RateLimiterPolicy to apply to this route." }, "OutputCachePolicy": { "type": [ "string", "null" ], "description": "The name of the OutputCachePolicy to apply to this route." }, "Timeout": { "type": [ "string", "null" ], "pattern": "^\\d*?\\.?\\d\\d?:\\d\\d?:\\d\\d?\\.?\\d{0,7}$", "description": "The Timeout to apply to this route. This overrides any system defaults. Setting both Timeout and TimeoutPolicy is an error. Format: 'hh:mm:ss'." }, "TimeoutPolicy": { "type": [ "string", "null" ], "description": "Specifies which timeout policy applies, e.g. 'Default' or 'Disable'. Setting both Timeout and TimeoutPolicy is an error." }, "CorsPolicy": { "type": [ "string", "null" ], "description": "Specifies which CORS policy applies, e.g. 'Default' or 'Disable'." }, "Match": { "type": "object", "properties": { "Path": { "type": [ "string", "null" ], "description": "Path pattern using ASP.NET route template syntax, e.g. '/something/{**remainder}'." }, "Hosts": { "type": [ "array", "null" ], "description": "Only match requests with the given Host header. Supports wildcards and ports. For unicode host names, do not use punycode.", "items": { "type": "string" } }, "Methods": { "description": "Allowed HTTP methods.", "items": { "anyOf": [ { "type": "string", "enum": [ "DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT", "TRACE" ] }, { "type": [ "array", "null" ] } ] } }, "Headers": { "type": [ "array", "null" ], "description": "List of header match conditions.", "items": { "type": "object", "properties": { "Name": { "type": "string", "description": "Header name." }, "Values": { "type": "array", "description": "Matches against any of these values.", "items": { "type": "string" } }, "Mode": { "type": "string", "description": "How the header values should be matched.", "enum": [ "ExactHeader", "HeaderPrefix", "Contains", "NotContains", "Exists", "NotExists" ] }, "IsCaseSensitive": { "anyOf": [ { "type": "boolean" }, { "type": "string", "enum": [ "true", "false", "True", "False" ] } ] } }, "required": [ "Name" ], "additionalProperties": false } }, "QueryParameters": { "type": [ "array", "null" ], "description": "List of query string match conditions.", "items": { "type": "object", "properties": { "Name": { "type": "string", "description": "Name of the query parameter." }, "Values": { "type": "array", "description": "Matches against any of these values.", "items": { "type": "string" } }, "Mode": { "type": "string", "description": "How the query parameter values should be matched.", "enum": [ "Exact", "Contains", "NotContains", "Prefix", "Exists" ] }, "IsCaseSensitive": { "anyOf": [ { "type": "boolean" }, { "type": "string", "enum": [ "true", "false", "True", "False" ] } ] } }, "required": [ "Name" ], "additionalProperties": false } } }, "additionalProperties": false, "anyOf": [ { "required": [ "Path" ] }, { "required": [ "Hosts" ] } ] }, "Metadata": { "type": [ "object", "null" ], "description": "Arbitrary key-value pairs for custom route logic.", "additionalProperties": { "type": [ "null", "number", "string", "boolean" ] } }, "Transforms": { "type": [ "array", "null" ], "description": "List of transform objects for request customization.", "items": { "description": "A single transform definition.", "anyOf": [ { "type": [ "object", "null" ], "$comment": "Fallback that matches any custom user-defined transforms.", "properties": { "RequestHeadersCopy": { "not": {} }, "RequestHeaderOriginalHost": { "not": {} }, "RequestHeader": { "not": {} }, "PathRemovePrefix": { "not": {} }, "PathSet": { "not": {} }, "PathPrefix": { "not": {} }, "QueryRouteParameter": { "not": {} }, "PathPattern": { "not": {} }, "QueryValueParameter": { "not": {} }, "QueryRemoveParameter": { "not": {} }, "HttpMethodChange": { "not": {} }, "RequestHeaderRouteValue": { "not": {} }, "RequestHeaderRemove": { "not": {} }, "RequestHeadersAllowed": { "not": {} }, "X-Forwarded": { "not": {} }, "Forwarded": { "not": {} }, "ClientCert": { "not": {} }, "ResponseHeadersCopy": { "not": {} }, "ResponseHeader": { "not": {} }, "ResponseHeaderRemove": { "not": {} }, "ResponseHeadersAllowed": { "not": {} }, "ResponseTrailersCopy": { "not": {} }, "ResponseTrailer": { "not": {} }, "ResponseTrailerRemove": { "not": {} }, "ResponseTrailersAllowed": { "not": {} } } }, { "type": "object", "description": "Sets whether incoming request headers are copied to the outbound request.", "properties": { "RequestHeadersCopy": { "description": "If true, copies all request headers to outbound request.", "anyOf": [ { "type": "boolean" }, { "type": "string", "enum": [ "true", "false", "True", "False" ] } ] } }, "additionalProperties": false, "required": [ "RequestHeadersCopy" ] }, { "type": "object", "description": "Specifies if the incoming request Host header should be copied to the proxy request.", "properties": { "RequestHeaderOriginalHost": { "description": "If true, preserve the original Host header; otherwise the destination's host is used.", "anyOf": [ { "type": "boolean" }, { "type": "string", "enum": [ "true", "false", "True", "False" ] } ] } }, "additionalProperties": false, "required": [ "RequestHeaderOriginalHost" ] }, { "type": "object", "description": "Transform for setting, appending, or removing a request header.", "properties": { "RequestHeader": { "type": "string", "description": "The header name to operate on." }, "Set": { "type": "string", "description": "Value to set the given request header to." }, "Append": { "type": "string", "description": "Value to append to the given request header." }, "Remove": { "description": "Removes the header if true.", "anyOf": [ { "type": "boolean" }, { "type": "string", "enum": [ "true", "false", "True", "False" ] } ] } }, "additionalProperties": false, "anyOf": [ { "required": [ "RequestHeader", "Set" ] }, { "required": [ "RequestHeader", "Append" ] }, { "required": [ "RequestHeader", "Remove" ] } ] }, { "type": "object", "description": "Transform that removes a specified prefix from the request path.", "properties": { "PathRemovePrefix": { "type": "string", "description": "Prefix to remove from the existing request path." } }, "additionalProperties": false, "required": [ "PathRemovePrefix" ] }, { "type": "object", "description": "Transform that replaces the entire request path with the provided value.", "properties": { "PathSet": { "type": "string", "description": "Sets the request path to this value." } }, "additionalProperties": false, "required": [ "PathSet" ] }, { "type": "object", "description": "Transform that adds the specified prefix to the request path.", "properties": { "PathPrefix": { "type": "string", "description": "Path prefix to add." } }, "additionalProperties": false, "required": [ "PathPrefix" ] }, { "type": "object", "description": "Transform that adds or replaces a query string parameter with a value from the route configuration.", "properties": { "QueryRouteParameter": { "type": "string", "description": "Specifies the query parameter name to add or replace." }, "Set": { "type": "string", "description": "Name of the route parameter to set the query parameter to." }, "Append": { "type": "string", "description": "Name of the route parameter to append to the query parameter." } }, "additionalProperties": false, "anyOf": [ { "required": [ "QueryRouteParameter", "Set" ] }, { "required": [ "QueryRouteParameter", "Append" ] } ] }, { "type": "object", "description": "Transform that replaces the entire request path using a pattern template, replacing {} segments with the route value.", "properties": { "PathPattern": { "type": "string", "description": "A path template starting with a '/', e.g. '/my/{plugin}/api/{**remainder}'." } }, "additionalProperties": false, "required": [ "PathPattern" ] }, { "type": "object", "description": "Adds or replaces parameters in the request query string.", "properties": { "QueryValueParameter": { "type": "string", "description": "Name of a query string parameter." }, "Set": { "type": "string", "description": "Value to set the given query parameter to." }, "Append": { "type": "string", "description": "Value to append to the given query parameter." } }, "additionalProperties": false, "anyOf": [ { "required": [ "QueryValueParameter", "Set" ] }, { "required": [ "QueryValueParameter", "Append" ] } ] }, { "type": "object", "description": "Removes the specified parameter from the request query string.", "properties": { "QueryRemoveParameter": { "type": "string", "description": "Name of a query string parameter." } }, "additionalProperties": false, "required": [ "QueryRemoveParameter" ] }, { "type": "object", "description": "Changes the http method used in the request.", "properties": { "HttpMethodChange": { "description": "The HTTP method to replace.", "anyOf": [ { "type": "string", "enum": [ "DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT", "TRACE" ] }, { "type": "string" } ] }, "Set": { "type": "string", "description": "The new HTTP method." } }, "additionalProperties": false, "required": [ "HttpMethodChange", "Set" ] }, { "type": "object", "description": "Adds or replaces a header with a value from the route configuration.", "properties": { "RequestHeaderRouteValue": { "type": "string", "description": "The header name to operate on." }, "Set": { "type": "string", "description": "Route value to set the given header to." }, "Append": { "type": "string", "description": "Route value to append to the given header." } }, "additionalProperties": false, "anyOf": [ { "required": [ "RequestHeaderRouteValue", "Set" ] }, { "required": [ "RequestHeaderRouteValue", "Append" ] } ] }, { "type": "object", "description": "Removes the specified header from the request.", "properties": { "RequestHeaderRemove": { "type": "string", "description": "The header name." } }, "additionalProperties": false, "required": [ "RequestHeaderRemove" ] }, { "type": "object", "description": "YARP copies most request headers to the proxy request by default, this transform disables RequestHeadersCopy and only copies the given headers.", "properties": { "RequestHeadersAllowed": { "type": "string", "pattern": "^[a-zA-Z0-9!#$%&'*+-.^_`|~;]+$", "description": "A semicolon separated list of allowed header names." } }, "additionalProperties": false, "required": [ "RequestHeadersAllowed" ] }, { "type": "object", "description": "Adds headers with information about the original client request.", "properties": { "X-Forwarded": { "type": "string", "description": "Default action to apply to all X-Forwarded-* headers.", "enum": [ "Set", "Append", "Remove", "Off" ] }, "For": { "type": "string", "description": "Action to apply to the 'For' header.", "enum": [ "Set", "Append", "Remove", "Off" ] }, "Proto": { "type": "string", "description": "Action to apply to the 'Proto' header.", "enum": [ "Set", "Append", "Remove", "Off" ] }, "Host": { "type": "string", "description": "Action to apply to the 'Host' header.", "enum": [ "Set", "Append", "Remove", "Off" ] }, "Prefix": { "type": "string", "description": "Action to apply to the 'Prefix' header.", "enum": [ "Set", "Append", "Remove", "Off" ] }, "HeaderPrefix": { "type": "string", "description": "The header name prefix.", "default": "X-Forwarded-" } }, "additionalProperties": false, "required": [ "X-Forwarded" ] }, { "type": "object", "description": "Adds a header with information about the original client request.", "properties": { "Forwarded": { "type": "string", "pattern": "^(?:(?:for|by|proto|host),?)+$", "description": "A comma separated list containing any of these values: 'for,by,proto,host'." }, "ForFormat": { "type": "string", "description": "Format to apply to the 'For' header.", "enum": [ "Random", "RandomAndPort", "RandomAndRandomPort", "Unknown", "UnknownAndPort", "UnknownAndRandomPort", "Ip", "IpAndPort", "IpAndRandomPort" ] }, "ByFormat": { "type": "string", "description": "Format to apply to the 'For' header.", "enum": [ "Random", "RandomAndPort", "RandomAndRandomPort", "Unknown", "UnknownAndPort", "UnknownAndRandomPort", "Ip", "IpAndPort", "IpAndRandomPort" ] }, "Action": { "type": "string", "description": "Action to apply to the 'Forwarded' header.", "enum": [ "Set", "Append", "Remove", "Off" ] } }, "additionalProperties": false, "required": [ "Forwarded" ] }, { "type": "object", "description": "Forwards the client cert used on the inbound connection as a header to the destination.", "properties": { "ClientCert": { "type": "string", "description": "The header name to use for the forwarded client cert." } }, "additionalProperties": false, "required": [ "ClientCert" ] }, { "type": "object", "description": "Transform controlling whether response headers are copied from the original response.", "properties": { "ResponseHeadersCopy": { "description": "If true, copies all response headers from the destination back to the client.", "anyOf": [ { "type": "boolean" }, { "type": "string", "enum": [ "true", "false", "True", "False" ] } ] } }, "additionalProperties": false, "required": [ "ResponseHeadersCopy" ] }, { "type": "object", "description": "Transform for setting, appending, or removing a response header.", "properties": { "ResponseHeader": { "type": "string", "description": "The header name to operate on." }, "Set": { "type": "string", "description": "Value to set the given response header to." }, "Append": { "type": "string", "description": "Value to append to the given response header." }, "When": { "type": "string", "enum": [ "Success", "Always", "Failure" ], "description": "Specifies if the response header should be included for all, successful, or failure responses. Any response with a status code less than 400 is considered a success." } }, "additionalProperties": false, "anyOf": [ { "required": [ "ResponseHeader", "Set" ] }, { "required": [ "ResponseHeader", "Append" ] } ] }, { "type": "object", "description": "Removes the specified header from the response.", "properties": { "ResponseHeaderRemove": { "type": "string", "description": "The header name." }, "When": { "type": "string", "enum": [ "Success", "Always", "Failure" ], "description": "Specifies if the response header should be included for all, successful, or failure responses. Any response with a status code less than 400 is considered a success." } }, "additionalProperties": false, "required": [ "ResponseHeaderRemove" ] }, { "type": "object", "description": "YARP copies most response headers to the proxy response by default, this transform disables ResponseHeadersCopy and only copies the given headers.", "properties": { "ResponseHeadersAllowed": { "type": "string", "pattern": "^[a-zA-Z0-9!#$%&'*+-.^_`|~;]+$", "description": "A semicolon separated list of allowed header names." } }, "additionalProperties": false, "required": [ "ResponseHeadersAllowed" ] }, { "type": "object", "description": "Transform controlling whether trailing response headers are copied from the original response.", "properties": { "ResponseTrailersCopy": { "description": "If true, copies all trailing response headers from the destination back to the client.", "anyOf": [ { "type": "boolean" }, { "type": "string", "enum": [ "true", "false", "True", "False" ] } ] } }, "additionalProperties": false, "required": [ "ResponseTrailersCopy" ] }, { "type": "object", "description": "Transform for setting, appending, or removing a response trailer.", "properties": { "ResponseTrailer": { "type": "string", "description": "The trailer name to operate on." }, "Set": { "type": "string", "description": "Value to set the given response trailer to." }, "Append": { "type": "string", "description": "Value to append to the given response trailer." }, "When": { "type": "string", "enum": [ "Success", "Always", "Failure" ], "description": "Specifies if the response trailer should be included for all, successful, or failure responses. Any response with a status code less than 400 is considered a success." } }, "additionalProperties": false, "anyOf": [ { "required": [ "ResponseTrailer", "Set" ] }, { "required": [ "ResponseTrailer", "Append" ] } ] }, { "type": "object", "description": "Removes the specified trailer from the response.", "properties": { "ResponseTrailerRemove": { "type": "string", "description": "The trailer name." }, "When": { "type": "string", "enum": [ "Success", "Always", "Failure" ], "description": "Specifies if the response trailer should be removed for all, successful, or failure responses. Any response with a status code less than 400 is considered a success." } }, "additionalProperties": false, "required": [ "ResponseTrailerRemove" ] }, { "type": "object", "description": "YARP copies most response trailers to the proxy response by default, this transform disables ResponseTrailersCopy and only copies the given headers.", "properties": { "ResponseTrailersAllowed": { "type": "string", "pattern": "^[a-zA-Z0-9!#$%&'*+-.^_`|~;]+$", "description": "A semicolon separated list of allowed trailer names." } }, "additionalProperties": false, "required": [ "ResponseTrailersAllowed" ] } ] } } }, "required": [ "ClusterId", "Match" ], "additionalProperties": false } } }, "Clusters": { "type": "object", "description": "Named clusters describing destinations.", "patternProperties": { ".": { "type": "object", "properties": { "Destinations": { "type": "object", "description": "Named destinations where traffic is forwarded.", "patternProperties": { ".": { "type": "object", "properties": { "Address": { "type": "string", "description": "Destination address (must include scheme)." }, "Health": { "type": [ "string", "null" ], "description": "Optional override URL accepting active health check probes." }, "Host": { "type": [ "string", "null" ], "description": "Optional fallback host header value used if a host is not already specified by request transforms." }, "Metadata": { "type": [ "object", "null" ], "description": "Arbitrary key-value pairs for custom destination logic.", "additionalProperties": { "type": [ "null", "number", "string", "boolean" ] } } }, "additionalProperties": false, "required": [ "Address" ] } } }, "LoadBalancingPolicy": { "anyOf": [ { "type": "string", "enum": [ "PowerOfTwoChoices", "FirstAlphabetical", "Random", "RoundRobin", "LeastRequests" ] }, { "type": [ "string", "null" ] } ], "description": "Determines traffic distribution among destinations." }, "SessionAffinity": { "type": [ "object", "null" ], "description": "Session affinity is a mechanism to bind (affinitize) a causally related request sequence to the destination that handled the first request when the load is balanced among several destinations.", "properties": { "Enabled": { "anyOf": [ { "type": [ "boolean", "null" ] }, { "type": "string", "enum": [ "true", "false", "True", "False" ] } ] }, "Policy": { "anyOf": [ { "type": "string", "enum": [ "HashCookie", "ArrCookie", "Cookie", "CustomHeader" ] }, { "type": [ "string", "null" ] } ], "description": "Determines how the session will be stored and retrieved." }, "FailurePolicy": { "anyOf": [ { "type": "string", "enum": [ "Redistribute", "Return503Error" ] }, { "type": [ "string", "null" ] } ], "description": "Strategy for handling a missing destination for an affinitized request." }, "AffinityKeyName": { "type": "string", "description": "Identifies the name of the field where the affinity value is stored (cookie or header name)." }, "Cookie": { "type": [ "object", "null" ], "properties": { "Domain": { "type": [ "string", "null" ], "description": "Specifies the domain of the cookie." }, "Path": { "type": [ "string", "null" ], "description": "Specifies the path of the cookie." }, "Expiration": { "type": [ "string", "null" ], "pattern": "^\\d*?\\.?\\d\\d?:\\d\\d?:\\d\\d?\\.?\\d{0,7}$", "description": "Specifies the expiration of the cookie. Format: 'hh:mm:ss'." }, "MaxAge": { "type": [ "string", "null" ], "pattern": "^\\d*?\\.?\\d\\d?:\\d\\d?:\\d\\d?\\.?\\d{0,7}$", "description": "Specifies the maximum age of the cookie. Format: 'hh:mm:ss'." }, "SecurePolicy": { "type": [ "string", "null" ], "enum": [ "Always", "None", "SameAsRequest" ], "description": "Specifies the Secure attribute of the cookie." }, "HttpOnly": { "anyOf": [ { "type": [ "boolean", "null" ] }, { "type": "string", "enum": [ "true", "false", "True", "False" ] } ], "description": "Specifies whether a cookie is accessible by client-side script." }, "SameSite": { "type": [ "string", "null" ], "enum": [ "Lax", "None", "Strict", "Unspecified" ], "description": "Specifies the SameSite attribute of the cookie." }, "IsEssential": { "anyOf": [ { "type": [ "boolean", "null" ] }, { "type": "string", "enum": [ "true", "false", "True", "False" ] } ], "description": "Specifies whether a cookie is essential for the application to function correctly. If true then consent policy checks may be bypassed." } }, "additionalProperties": false } }, "additionalProperties": false, "required": [ "AffinityKeyName" ] }, "HealthCheck": { "type": [ "object", "null" ], "description": "Health check configuration for destinations.", "properties": { "Active": { "type": [ "object", "null" ], "description": "Active health checks are based on sending health probing requests.", "properties": { "Enabled": { "anyOf": [ { "type": [ "boolean", "null" ] }, { "type": "string", "enum": [ "true", "false", "True", "False" ] } ], "description": "Determines if active health checks are enabled.", "default": false }, "Interval": { "type": [ "string", "null" ], "pattern": "^\\d*?\\.?\\d\\d?:\\d\\d?:\\d\\d?\\.?\\d{0,7}$", "description": "Period of sending health probing requests. Format: 'hh:mm:ss'.", "default": "00:00:15" }, "Timeout": { "type": [ "string", "null" ], "pattern": "^\\d*?\\.?\\d\\d?:\\d\\d?:\\d\\d?\\.?\\d{0,7}$", "description": "Period of waiting for a health check response. Format: 'hh:mm:ss'.", "default": "00:00:10" }, "Policy": { "anyOf": [ { "type": "string", "enum": [ "ConsecutiveFailures" ] }, { "type": [ "string", "null" ] } ], "description": "Determines the health check policy." }, "Path": { "type": [ "string", "null" ], "description": "HTTP health check endpoint path.", "default": "/" }, "Query": { "type": [ "string", "null" ], "description": "Query string to append to the probe, including the leading '?'." } }, "additionalProperties": false }, "Passive": { "type": [ "object", "null" ], "description": "Passive health checks are based on observing the health of the responses from the destination.", "properties": { "Enabled": { "anyOf": [ { "type": [ "boolean", "null" ] }, { "type": "string", "enum": [ "true", "false", "True", "False" ] } ], "description": "Determines if passive health checks are enabled.", "default": false }, "Policy": { "anyOf": [ { "type": "string", "enum": [ "TransportFailureRate" ] }, { "type": [ "string", "null" ] } ], "description": "Determines the health check policy." }, "ReactivationPeriod": { "type": [ "string", "null" ], "pattern": "^\\d*?\\.?\\d\\d?:\\d\\d?:\\d\\d?\\.?\\d{0,7}$", "description": "Period after which an unhealthy destination reverts back to an Unknown health state. Format: 'hh:mm:ss'." } }, "additionalProperties": false }, "AvailableDestinationsPolicy": { "anyOf": [ { "type": "string", "enum": [ "HealthyAndUnknown", "HealthyOrPanic" ] }, { "type": [ "string", "null" ] } ] } }, "additionalProperties": false }, "HttpClient": { "type": [ "object", "null" ], "description": "Configuration for outbound HTTP connections.", "properties": { "SslProtocols": { "type": "array", "description": "Specifies the SSL protocols to use.", "items": { "type": "string" } }, "DangerousAcceptAnyServerCertificate": { "anyOf": [ { "type": [ "boolean", "null" ] }, { "type": "string", "enum": [ "true", "false", "True", "False" ] } ], "description": "Determines whether the server's SSL certificate validity is checked by the client. Setting it to true completely disables validation.", "default": false }, "MaxConnectionsPerServer": { "type": [ "number", "null" ], "description": "Specifies the maximum number of connections per server." }, "EnableMultipleHttp2Connections": { "anyOf": [ { "type": [ "boolean", "null" ] }, { "type": "string", "enum": [ "true", "false", "True", "False" ] } ], "description": "Determines if multiple HTTP/2 connections are enabled.", "default": true }, "RequestHeaderEncoding": { "type": [ "string", "null" ], "description": "Specifies the encoding of request headers, e.g. 'utf-8'." }, "ResponseHeaderEncoding": { "type": [ "string", "null" ], "description": "Specifies the encoding of response headers, e.g. 'utf-8'." }, "WebProxy": { "type": [ "object", "null" ], "description": "Config used to construct a System.Net.WebProxy instance used for outgoing requests.", "properties": { "Address": { "type": [ "string", "null" ], "description": "The URI of the proxy server." }, "BypassOnLocal": { "anyOf": [ { "type": [ "boolean", "null" ] }, { "type": "string", "enum": [ "true", "false", "True", "False" ] } ], "description": "If true, bypasses the proxy for local addresses.", "default": false }, "UseDefaultCredentials": { "anyOf": [ { "type": [ "boolean", "null" ] }, { "type": "string", "enum": [ "true", "false", "True", "False" ] } ], "description": "If true, sends CredentialCache.DefaultCredentials with requests.", "default": false } }, "additionalProperties": false } }, "additionalProperties": false }, "HttpRequest": { "type": [ "object", "null" ], "description": "Options controlling requests sent to destinations.", "properties": { "ActivityTimeout": { "type": [ "string", "null" ], "pattern": "^\\d*?\\.?\\d\\d?:\\d\\d?:\\d\\d?\\.?\\d{0,7}$", "description": "Specifies how long a request is allowed to remain idle between any operation completing, after which it will be canceled. Format: 'hh:mm:ss'.", "default": "00:01:40" }, "Version": { "type": [ "string", "null" ], "description": "Preferred version of the outgoing request.", "default": "2.0" }, "VersionPolicy": { "type": [ "string", "null" ], "description": "The policy applied to version selection, e.g. whether to prefer downgrades, upgrades or request an exact version.", "default": "RequestVersionOrLower", "enum": [ "RequestVersionExact", "RequestVersionOrLower", "RequestVersionOrHigher" ] }, "AllowResponseBuffering": { "anyOf": [ { "type": [ "boolean", "null" ] }, { "type": "string", "enum": [ "true", "false", "True", "False" ] } ], "description": "Determines if response buffering is allowed." } }, "additionalProperties": false }, "Metadata": { "type": [ "object", "null" ], "description": "Arbitrary key-value pairs for custom cluster logic.", "additionalProperties": { "type": [ "null", "number", "string", "boolean" ] } } }, "additionalProperties": false } } } } } } } ================================================ FILE: src/ReverseProxy/Delegation/AppBuilderDelegationExtensions.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Server.HttpSys; using Microsoft.Extensions.DependencyInjection; using Yarp.ReverseProxy.Delegation; namespace Microsoft.AspNetCore.Builder; /// /// Extensions for adding delegation middleware to the pipeline. /// public static class AppBuilderDelegationExtensions { /// /// Adds middleware to check if the selected destination should use Http.sys delegation. /// If so, the request is delegated to the destination queue instead of being proxied over HTTP. /// This should be placed after load balancing and passive health checks. /// /// /// This middleware only works with the ASP.NET Core Http.sys server implementation. /// public static IReverseProxyApplicationBuilder UseHttpSysDelegation(this IReverseProxyApplicationBuilder builder) { // IServerDelegationFeature isn't added to DI https://github.com/dotnet/aspnetcore/issues/40043 _ = builder.ApplicationServices.GetRequiredService().Features?.Get() ?? throw new NotSupportedException($"{typeof(IHttpSysRequestDelegationFeature).FullName} is not available. Http.sys delegation is only supported when using the Http.sys server"); builder.UseMiddleware(); return builder; } } ================================================ FILE: src/ReverseProxy/Delegation/DelegationExtensions.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Yarp.ReverseProxy.Model; namespace Yarp.ReverseProxy.Delegation; internal static class DelegationExtensions { public const string HttpSysDelegationQueueMetadataKey = "HttpSysDelegationQueue"; public static string? GetHttpSysDelegationQueue(this DestinationState? destination) { return destination?.Model?.Config?.Metadata?.TryGetValue(HttpSysDelegationQueueMetadataKey, out var name) ?? false ? name : null; } public static bool ShouldUseHttpSysDelegation(this DestinationState destination) { return destination.GetHttpSysDelegationQueue() is not null; } } ================================================ FILE: src/ReverseProxy/Delegation/DummyHttpSysDelegator.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. namespace Yarp.ReverseProxy.Delegation; // Only used as part of a workaround for https://github.com/dotnet/aspnetcore/issues/59166. internal sealed class DummyHttpSysDelegator : IHttpSysDelegator { public void ResetQueue(string queueName, string urlPrefix) { } } ================================================ FILE: src/ReverseProxy/Delegation/HttpSysDelegator.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Concurrent; using System.Data; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.CompilerServices; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Server.HttpSys; using Microsoft.Extensions.Logging; using Yarp.ReverseProxy.Forwarder; using Yarp.ReverseProxy.Model; namespace Yarp.ReverseProxy.Delegation; internal sealed class HttpSysDelegator : IHttpSysDelegator, IClusterChangeListener { private const int ERROR_OBJECT_NO_LONGER_EXISTS = 0x1A97; private readonly IServerDelegationFeature? _serverDelegationFeature; private readonly ILogger _logger; private readonly ConcurrentDictionary> _queues; private readonly ConditionalWeakTable _queuesPerDestination; public HttpSysDelegator( IServer server, ILogger logger) { ArgumentNullException.ThrowIfNull(logger); _logger = logger; // IServerDelegationFeature isn't added to DI https://github.com/dotnet/aspnetcore/issues/40043 // IServerDelegationFeature may not be set if not http.sys server or the OS doesn't support delegation _serverDelegationFeature = server.Features?.Get(); _queues = new ConcurrentDictionary>(); _queuesPerDestination = new ConditionalWeakTable(); } public void ResetQueue(string queueName, string urlPrefix) { if (_serverDelegationFeature is not null) { var key = new QueueKey(queueName, urlPrefix); if (_queues.TryGetValue(key, out var queueWeakRef) && queueWeakRef.TryGetTarget(out var queue)) { var detachedQueueState = queue.Detach(); Log.QueueReset(_logger, queueName, urlPrefix, detachedQueueState); } } } public void DelegateRequest(HttpContext context, DestinationState destination) { ArgumentNullException.ThrowIfNull(context); ArgumentNullException.ThrowIfNull(destination); var requestDelegationFeature = context.Features.Get() ?? throw new InvalidOperationException($"{typeof(IHttpSysRequestDelegationFeature).FullName} is missing."); if (!requestDelegationFeature.CanDelegate) { throw new InvalidOperationException( "Current request can't be delegated. Either the request body has started to be read or the response has started to be sent."); } if (_serverDelegationFeature is null || !_queuesPerDestination.TryGetValue(destination, out var queue)) { Log.QueueNotFound(_logger, destination); context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable; context.Features.Set(new ForwarderErrorFeature(ForwarderError.NoAvailableDestinations, ex: null)); return; } Delegate(context, destination, _serverDelegationFeature, requestDelegationFeature, queue, _logger, shouldRetry: true); static void Delegate( HttpContext context, DestinationState destination, IServerDelegationFeature serverDelegationFeature, IHttpSysRequestDelegationFeature requestDelegationFeature, DelegationQueue queue, ILogger logger, bool shouldRetry) { // Opportunistically retry initialization if it failed previously. // This helps when the target queue wasn't yet created because // the target process hadn't yet started up. var queueState = queue.Initialize(serverDelegationFeature); if (!queueState.IsInitialized) { Log.QueueNotInitialized(logger, destination, queueState, queueState.InitializationException); context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable; context.Features.Set(new ForwarderErrorFeature(ForwarderError.NoAvailableDestinations, queueState.InitializationException)); return; } try { Log.DelegatingRequest(logger, destination, queueState); requestDelegationFeature.DelegateRequest(queueState.Rule); } catch (ObjectDisposedException) when (shouldRetry) { Log.QueueDisposed(logger, destination.GetHttpSysDelegationQueue(), destination.Model?.Config?.Address); // Another thread detached/disposed the queue // Attempt to delegate one more time which will to try re-initialize the queue Delegate(context, destination, serverDelegationFeature, requestDelegationFeature, queue, logger, shouldRetry: false); } catch (HttpSysException ex) when (shouldRetry && ex.ErrorCode == ERROR_OBJECT_NO_LONGER_EXISTS) { Log.QueueNoLongerExists(logger, destination.GetHttpSysDelegationQueue(), destination.Model?.Config?.Address, queueState, ex); // The target queue is gone. Detach from it so that we can try to re-attach. queue.Detach(queueState); // Attempt to delegate one more time which will try re-initialize the queue Delegate(context, destination, serverDelegationFeature, requestDelegationFeature, queue, logger, shouldRetry: false); } catch (Exception ex) { Log.DelegationFailed(logger, destination, queueState, ex); context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable; context.Features.Set(new ForwarderErrorFeature(ForwarderError.Request, ex)); } } } void IClusterChangeListener.OnClusterAdded(ClusterState cluster) { AddOrUpdateRules(cluster); } void IClusterChangeListener.OnClusterChanged(ClusterState cluster) { AddOrUpdateRules(cluster); RemoveDeadQueueReferences(); } void IClusterChangeListener.OnClusterRemoved(ClusterState cluster) { RemoveDeadQueueReferences(); } private void AddOrUpdateRules(ClusterState cluster) { if (_serverDelegationFeature is null) { return; } // We support multiple destinations referencing the same queue but http.sys only // allows us to create one handle to the queue, so we keep track of queues two ways. // 1. We map destination => queue using a ConditionalWeakTable // This allows us to find the queue to delegate to when processing a request // 2. We map (queue name + url prefix) => queue using a WeakReference // This allows us to find the already created queue if more than one destination points to the same queue. // It also allows us to find the queue by name/url prefix to support reset. // // Using weak references means we ensure the queue exist as long as the referencing destinations exist. // Once all the destinations are gone, GC will eventually finalize the underlying SafeHandle to the http.sys // queue, which will clean up references in http.sys, allowing us to re-create it again later if needed. foreach (var destination in cluster.Destinations.Select(kvp => kvp.Value)) { var queueName = destination.GetHttpSysDelegationQueue(); var urlPrefix = destination.Model?.Config?.Address; if (queueName is not null && urlPrefix is not null) { var queueKey = new QueueKey(queueName, urlPrefix); if (!_queuesPerDestination.TryGetValue(destination, out var queue) || !queue.Equals(queueKey)) { var queueWeakRef = _queues.GetOrAdd(queueKey, key => new WeakReference(new DelegationQueue(key.QueueName, key.UrlPrefix))); if (!queueWeakRef.TryGetTarget(out queue)) { // The queue was GC'd since it was originally created lock (queueWeakRef) { if (!queueWeakRef.TryGetTarget(out queue)) { queue = new DelegationQueue(queueName, urlPrefix); queueWeakRef.SetTarget(queue); } } } var queueState = queue.Initialize(_serverDelegationFeature); if (!queueState.IsInitialized) { Log.QueueInitFailed( _logger, destination.DestinationId, queueName, urlPrefix, queueState.InitializationException); } _queuesPerDestination.AddOrUpdate(destination, queue); } } else { // Handles the case a destination switches from delegation to proxy _queuesPerDestination.Remove(destination); } } } private void RemoveDeadQueueReferences() { foreach (var kvp in _queues) { if (!kvp.Value.TryGetTarget(out _)) { _queues.TryRemove(kvp); } } } private sealed class DelegationQueue { public const uint ERROR_FILE_NOT_FOUND = 2; private readonly QueueKey _queueKey; private readonly object _syncRoot; private DelegationQueueState _currentState; public DelegationQueue(string queueName, string urlPrefix) { _queueKey = new QueueKey(queueName, urlPrefix); _syncRoot = new object(); _currentState = new DelegationQueueState(); } public DelegationQueueState Initialize(IServerDelegationFeature delegationFeature) { var state = _currentState; if (!state.IsInitialized && ShouldRetryInitialization(state.InitializationException)) { lock (_syncRoot) { state = _currentState; if (!state.IsInitialized && ShouldRetryInitialization(state.InitializationException)) { try { state = new DelegationQueueState(delegationFeature.CreateDelegationRule(_queueKey.QueueName, _queueKey.UrlPrefix)); } catch (Exception ex) { state = new DelegationQueueState(ex); } _currentState = state; } } } return state; } public DelegationQueueState? Detach(DelegationQueueState? state = null) { if (state == null || state == _currentState) { lock (_syncRoot) { if (state == null || state == _currentState) { _currentState.Rule?.Dispose(); var oldState = _currentState; _currentState = new DelegationQueueState(); return oldState; } } } return null; } public bool Equals(QueueKey queueKey) { return _queueKey.Equals(queueKey); } private static bool ShouldRetryInitialization(Exception? exception) { return exception switch { null => true, HttpSysException httpSysEx when httpSysEx.ErrorCode == ERROR_FILE_NOT_FOUND => true, _ => false, }; } } private sealed class DelegationQueueState { public DelegationQueueState() { IsInitialized = false; } public DelegationQueueState(DelegationRule rule) { IsInitialized = true; Rule = rule; } public DelegationQueueState(Exception ex) { IsInitialized = false; InitializationException = ex; } [MemberNotNullWhen(true, nameof(Rule))] public bool IsInitialized { get; } public DelegationRule? Rule { get; } public Exception? InitializationException { get; } public string Id { get; } = Activity.Current switch { { IdFormat: ActivityIdFormat.W3C } => Activity.Current.SpanId.ToHexString(), { Id: not null } => Activity.Current.Id, _ => ActivitySpanId.CreateRandom().ToHexString(), }; } private readonly struct QueueKey : IEquatable { private readonly int _hashCode; public QueueKey(string queueName, string urlPrefix) { QueueName = queueName; UrlPrefix = urlPrefix; _hashCode = HashCode.Combine( StringComparer.OrdinalIgnoreCase.GetHashCode(queueName), StringComparer.OrdinalIgnoreCase.GetHashCode(urlPrefix)); } public string QueueName { get; } public string UrlPrefix { get; } public bool Equals(QueueKey other) { return _hashCode == other._hashCode && string.Equals(QueueName, other.QueueName, StringComparison.OrdinalIgnoreCase) && string.Equals(UrlPrefix, other.UrlPrefix, StringComparison.OrdinalIgnoreCase); } public override bool Equals([NotNullWhen(true)] object? obj) { return obj is QueueKey other && Equals(other); } public override int GetHashCode() => _hashCode; } private static class Log { private static readonly Action _queueInitFailed = LoggerMessage.Define( LogLevel.Warning, EventIds.DelegationQueueInitializationFailed, "Failed to initialize queue for destination '{destinationId}' with queue name '{queueName}' and url prefix '{urlPrefix}'."); private static readonly Action _queueNotFound = LoggerMessage.Define( LogLevel.Warning, EventIds.DelegationQueueNotFound, "Failed to get delegation queue for destination '{destinationId}' with queue name '{queueName}' and url prefix '{urlPrefix}'"); private static readonly Action _queueNotInitialized = LoggerMessage.Define( LogLevel.Information, EventIds.DelegationQueueNotInitialized, "Delegation queue not initialized for destination '{destinationId}' with queue '{queueName}' and url prefix '{urlPrefix}'. Current state id '{stateId}'"); private static readonly Action _queueReset = LoggerMessage.Define( LogLevel.Information, EventIds.DelegationQueueReset, "Detached from queue with name '{queueName}' and url prefix '{urlPrefix}'. Detached queue state id '{stateId}'"); private static readonly Action _queueNoLongerExists = LoggerMessage.Define( LogLevel.Debug, EventIds.DelegationQueueNoLongerExists, "Destination queue with name '{queueName}' and url prefix '{urlPrefix}' no longer exists. Detaching and attempting to re-initialize. Current state id '{stateId}'"); private static readonly Action _queueDisposed = LoggerMessage.Define( LogLevel.Debug, EventIds.DelegationQueueDisposed, "Destination queue with name '{queueName}' and url prefix '{urlPrefix}' was disposed. Attempting to re-initialize."); private static readonly Action _delegatingRequest = LoggerMessage.Define( LogLevel.Information, EventIds.DelegatingRequest, "Delegating to destination '{destinationId}' with queue '{queueName}' and url prefix '{urlPrefix}'. Current state id '{stateId}'"); private static readonly Action _delegationFailed = LoggerMessage.Define( LogLevel.Error, EventIds.DelegationFailed, "Failed to delegate request for destination '{destinationId}' with queue name '{queueName}' and url prefix '{urlPrefix}'. Current state id '{stateId}'"); public static void QueueInitFailed(ILogger logger, string destinationId, string queueName, string urlPrefix, Exception? ex) { _queueInitFailed(logger, destinationId, queueName, urlPrefix, ex); } public static void QueueNotFound(ILogger logger, DestinationState destination) { _queueNotFound(logger, destination.DestinationId, destination.GetHttpSysDelegationQueue(), destination.Model?.Config?.Address, null); } public static void QueueNotInitialized(ILogger logger, DestinationState destination, DelegationQueueState queueState, Exception? ex) { _queueNotInitialized(logger, destination.DestinationId, destination.GetHttpSysDelegationQueue(), destination.Model?.Config?.Address, queueState.Id, ex); } public static void QueueReset(ILogger logger, string queueName, string urlPrefix, DelegationQueueState? detachedQueueState) { _queueReset(logger, queueName, urlPrefix, detachedQueueState?.Id, null); } public static void QueueNoLongerExists(ILogger logger, string? queueName, string? urlPrefix, DelegationQueueState queueState, Exception? ex) { _queueNoLongerExists(logger, queueName, urlPrefix, queueState.Id, ex); } public static void QueueDisposed(ILogger logger, string? queueName, string? urlPrefix) { _queueDisposed(logger, queueName, urlPrefix, null); } public static void DelegatingRequest(ILogger logger, DestinationState destination, DelegationQueueState queueState) { _delegatingRequest(logger, destination.DestinationId, destination.GetHttpSysDelegationQueue(), destination.Model?.Config?.Address, queueState.Id, null); } public static void DelegationFailed(ILogger logger, DestinationState destination, DelegationQueueState queueState, Exception ex) { _delegationFailed(logger, destination.DestinationId, destination.GetHttpSysDelegationQueue(), destination.Model?.Config?.Address, queueState.Id, ex); } } } ================================================ FILE: src/ReverseProxy/Delegation/HttpSysDelegatorMiddleware.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Yarp.ReverseProxy.Model; using Yarp.ReverseProxy.Utilities; namespace Yarp.ReverseProxy.Delegation; internal sealed class HttpSysDelegatorMiddleware { private readonly RequestDelegate _next; private readonly ILogger _logger; private readonly HttpSysDelegator _delegator; private readonly IRandomFactory _randomFactory; public HttpSysDelegatorMiddleware( RequestDelegate next, ILogger logger, HttpSysDelegator delegator, IRandomFactory randomFactory) { ArgumentNullException.ThrowIfNull(next); ArgumentNullException.ThrowIfNull(logger); ArgumentNullException.ThrowIfNull(delegator); ArgumentNullException.ThrowIfNull(randomFactory); _next = next; _logger = logger; _delegator = delegator; _randomFactory = randomFactory; } public Task Invoke(HttpContext context) { ArgumentNullException.ThrowIfNull(context); var reverseProxyFeature = context.GetReverseProxyFeature(); var destinations = reverseProxyFeature.AvailableDestinations ?? throw new InvalidOperationException($"The {nameof(IReverseProxyFeature)} Destinations collection was not set."); var cluster = reverseProxyFeature.Cluster ?? throw new InvalidOperationException($"The {nameof(IReverseProxyFeature)} Cluster was not set."); if (destinations.Any()) { // This logic mimics behavior in ForwarderMiddleware, except we save the chosen destination back // to the proxy feature to ensure a delegation destination doesn't slip past this middleware. var destination = destinations[0]; if (destinations.Count > 1) { var random = _randomFactory.CreateRandomInstance(); Log.MultipleDestinationsAvailable(_logger, reverseProxyFeature.Cluster.Config.ClusterId); destination = destinations[random.Next(destinations.Count)]; reverseProxyFeature.AvailableDestinations = destination; } if (destination.ShouldUseHttpSysDelegation()) { reverseProxyFeature.ProxiedDestination = destination; _delegator.DelegateRequest(context, destination); return Task.CompletedTask; } } return _next(context); } private static class Log { private static readonly Action _multipleDestinationsAvailable = LoggerMessage.Define( LogLevel.Warning, EventIds.MultipleDestinationsAvailable, "More than one destination available for cluster '{clusterId}', load balancing may not be configured correctly. Choosing randomly."); public static void MultipleDestinationsAvailable(ILogger logger, string clusterId) { _multipleDestinationsAvailable(logger, clusterId, null); } } } ================================================ FILE: src/ReverseProxy/Delegation/IHttpSysDelegator.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. namespace Yarp.ReverseProxy.Delegation; public interface IHttpSysDelegator { /// /// Disposes the handle to the given queue if it exists. /// /// /// If any destinations still reference the queue, the handle will be /// re-created the next time a request is routed to one of the destinations. /// /// The name of the queue to reset. /// The url prefix of the queue to reset. void ResetQueue(string queueName, string urlPrefix); } ================================================ FILE: src/ReverseProxy/Forwarder/CallbackHttpClientFactory.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Net.Http; using Microsoft.Extensions.Logging; namespace Yarp.ReverseProxy.Forwarder; internal sealed class CallbackHttpClientFactory : ForwarderHttpClientFactory { private readonly Action _configureClient; internal CallbackHttpClientFactory(ILogger logger, Action configureClient) : base(logger) { ArgumentNullException.ThrowIfNull(configureClient); _configureClient = configureClient; } protected override void ConfigureHandler(ForwarderHttpClientContext context, SocketsHttpHandler handler) { base.ConfigureHandler(context, handler); _configureClient(context, handler); } } ================================================ FILE: src/ReverseProxy/Forwarder/DirectForwardingHttpClientProvider.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Net.Http; using Yarp.ReverseProxy.Configuration; namespace Yarp.ReverseProxy.Forwarder; internal sealed class DirectForwardingHttpClientProvider { public HttpMessageInvoker HttpClient { get; } public DirectForwardingHttpClientProvider() : this(new ForwarderHttpClientFactory()) { } public DirectForwardingHttpClientProvider(IForwarderHttpClientFactory factory) { HttpClient = factory.CreateClient(new ForwarderHttpClientContext { NewConfig = HttpClientConfig.Empty }); } } ================================================ FILE: src/ReverseProxy/Forwarder/EmptyHttpContent.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.IO; using System.Net; using System.Net.Http; using System.Threading; using System.Threading.Tasks; namespace Yarp.ReverseProxy.Forwarder; internal sealed class EmptyHttpContent : HttpContent { protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context) => Task.CompletedTask; protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context, CancellationToken cancellationToken) => Task.CompletedTask; protected override bool TryComputeLength(out long length) { length = 0; return true; } } ================================================ FILE: src/ReverseProxy/Forwarder/ForwarderError.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. namespace Yarp.ReverseProxy.Forwarder; /// /// Errors reported when forwarding a request to the destination. /// public enum ForwarderError : int { /// /// No error. /// None, /// /// Failed to connect, send the request headers, or receive the response headers. /// Request, /// /// Timed out when trying to connect, send the request headers, or receive the response headers. /// RequestTimedOut, /// /// Canceled when trying to connect, send the request headers, or receive the response headers. /// RequestCanceled, /// /// Canceled while copying the request body. /// RequestBodyCanceled, /// /// Failed reading the request body from the client. /// RequestBodyClient, /// /// Failed writing the request body to the destination. /// RequestBodyDestination, /// /// Failed to copy response headers. /// ResponseHeaders, /// /// Canceled while copying the response body. /// ResponseBodyCanceled, /// /// Failed when writing response body to the client. /// ResponseBodyClient, /// /// Failed when reading response body from the destination. /// ResponseBodyDestination, /// /// Canceled while copying the upgraded response body. /// UpgradeRequestCanceled, /// /// Failed reading the upgraded request body from the client. /// UpgradeRequestClient, /// /// Failed writing the upgraded request body to the destination. /// UpgradeRequestDestination, /// /// Canceled while copying the upgraded response body. /// UpgradeResponseCanceled, /// /// Failed when writing the upgraded response body to the client. /// UpgradeResponseClient, /// /// Failed when reading the upgraded response body from the destination. /// UpgradeResponseDestination, /// /// Indicates there were no destinations remaining to proxy the request to. /// The configured destinations may have been excluded due to heath or other considerations. /// NoAvailableDestinations, /// /// Failed while creating the request message. /// RequestCreation, /// /// An upgraded request was idle and canceled due to the activity timeout. /// UpgradeActivityTimeout, } ================================================ FILE: src/ReverseProxy/Forwarder/ForwarderErrorFeature.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; namespace Yarp.ReverseProxy.Forwarder; internal sealed class ForwarderErrorFeature : IForwarderErrorFeature { internal ForwarderErrorFeature(ForwarderError error, Exception? ex) { Error = error; Exception = ex; } /// /// The specified ForwarderError. /// public ForwarderError Error { get; } /// /// The error, if any. /// public Exception? Exception { get; } } ================================================ FILE: src/ReverseProxy/Forwarder/ForwarderHttpClientContext.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; using System.Net.Http; using Yarp.ReverseProxy.Configuration; using Yarp.ReverseProxy.Model; namespace Yarp.ReverseProxy.Forwarder; /// /// Contains the old and the new HTTP client configurations. /// public class ForwarderHttpClientContext { /// /// Id of a HTTP client belongs to. /// public string ClusterId { get; init; } = default!; /// /// Old instance /// from which the was created. /// Can be empty if a client is getting constructed for the first time. /// public HttpClientConfig OldConfig { get; init; } = default!; /// /// Old metadata instance from which the was created, if any. /// public IReadOnlyDictionary? OldMetadata { get; init; } /// /// Old instance. /// Can be null if a client is getting constructed for the first time. /// public HttpMessageInvoker? OldClient { get; init; } /// /// New instance /// specifying the settings for a new client. /// public HttpClientConfig NewConfig { get; init; } = default!; /// /// New metadata instance used for a new client construction, if any. /// public IReadOnlyDictionary? NewMetadata { get; init; } } ================================================ FILE: src/ReverseProxy/Forwarder/ForwarderHttpClientFactory.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Diagnostics; using System.Net; using System.Net.Http; using System.Text; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Yarp.ReverseProxy.Configuration; namespace Yarp.ReverseProxy.Forwarder; /// /// Default implementation of . /// public class ForwarderHttpClientFactory : IForwarderHttpClientFactory { private readonly ILogger _logger; /// /// Initializes a new instance of the class. /// public ForwarderHttpClientFactory() : this(NullLogger.Instance) { } /// /// Initializes a new instance of the class. /// public ForwarderHttpClientFactory(ILogger logger) { ArgumentNullException.ThrowIfNull(logger); _logger = logger; } /// public HttpMessageInvoker CreateClient(ForwarderHttpClientContext context) { if (CanReuseOldClient(context)) { Log.ClientReused(_logger, context.ClusterId); return context.OldClient!; } var handler = new SocketsHttpHandler { UseProxy = false, AllowAutoRedirect = false, AutomaticDecompression = DecompressionMethods.None, UseCookies = false, EnableMultipleHttp2Connections = true, ActivityHeadersPropagator = new ReverseProxyPropagator(DistributedContextPropagator.Current), ConnectTimeout = TimeSpan.FromSeconds(15), // NOTE: MaxResponseHeadersLength = 64, which means up to 64 KB of headers are allowed by default as of .NET Core 3.1. }; ConfigureHandler(context, handler); var middleware = WrapHandler(context, handler); Log.ClientCreated(_logger, context.ClusterId); return new HttpMessageInvoker(middleware, disposeHandler: true); } /// /// Checks if the options have changed since the old client was created. If not then the /// old client will be re-used. Re-use can avoid the latency of creating new connections. /// protected virtual bool CanReuseOldClient(ForwarderHttpClientContext context) { return context.OldClient is not null && context.NewConfig == context.OldConfig; } /// /// Allows configuring the instance. The base implementation /// applies settings from . /// , , /// , and /// are disabled prior to this call. /// protected virtual void ConfigureHandler(ForwarderHttpClientContext context, SocketsHttpHandler handler) { var newConfig = context.NewConfig; if (newConfig.SslProtocols.HasValue) { handler.SslOptions.EnabledSslProtocols = newConfig.SslProtocols.Value; } if (newConfig.MaxConnectionsPerServer is not null) { handler.MaxConnectionsPerServer = newConfig.MaxConnectionsPerServer.Value; } if (newConfig.DangerousAcceptAnyServerCertificate ?? false) { #pragma warning disable CA5359 // Do Not Disable Certificate Validation -- this setting is explicitly opt-in by the user. handler.SslOptions.RemoteCertificateValidationCallback = delegate { return true; }; #pragma warning restore CA5359 } handler.EnableMultipleHttp2Connections = newConfig.EnableMultipleHttp2Connections.GetValueOrDefault(true); if (newConfig.RequestHeaderEncoding is not null) { var encoding = Encoding.GetEncoding(newConfig.RequestHeaderEncoding); handler.RequestHeaderEncodingSelector = (_, _) => encoding; } if (newConfig.ResponseHeaderEncoding is not null) { var encoding = Encoding.GetEncoding(newConfig.ResponseHeaderEncoding); handler.ResponseHeaderEncodingSelector = (_, _) => encoding; } var webProxy = TryCreateWebProxy(newConfig.WebProxy); if (webProxy is not null) { handler.Proxy = webProxy; handler.UseProxy = true; } } private static WebProxy? TryCreateWebProxy(WebProxyConfig? webProxyConfig) { if (webProxyConfig is null || webProxyConfig.Address is null) { return null; } var webProxy = new WebProxy(webProxyConfig.Address); webProxy.UseDefaultCredentials = webProxyConfig.UseDefaultCredentials.GetValueOrDefault(false); webProxy.BypassProxyOnLocal = webProxyConfig.BypassOnLocal.GetValueOrDefault(false); return webProxy; } /// /// Adds any wrapping middleware around the . /// protected virtual HttpMessageHandler WrapHandler(ForwarderHttpClientContext context, HttpMessageHandler handler) { return handler; } private static class Log { private static readonly Action _clientCreated = LoggerMessage.Define( LogLevel.Debug, EventIds.ClientCreated, "New client created for cluster '{clusterId}'."); private static readonly Action _clientReused = LoggerMessage.Define( LogLevel.Debug, EventIds.ClientReused, "Existing client reused for cluster '{clusterId}'."); public static void ClientCreated(ILogger logger, string clusterId) { _clientCreated(logger, clusterId, null); } public static void ClientReused(ILogger logger, string clusterId) { _clientReused(logger, clusterId, null); } } } ================================================ FILE: src/ReverseProxy/Forwarder/ForwarderMiddleware.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Diagnostics; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Yarp.ReverseProxy.Model; using Yarp.ReverseProxy.Utilities; namespace Yarp.ReverseProxy.Forwarder; /// /// Invokes the proxy at the end of the request processing pipeline. /// internal sealed class ForwarderMiddleware { private readonly IRandomFactory _randomFactory; private readonly RequestDelegate _next; // Unused, this middleware is always terminal private readonly ILogger _logger; private readonly IHttpForwarder _forwarder; public ForwarderMiddleware(RequestDelegate next, ILogger logger, IHttpForwarder forwarder, IRandomFactory randomFactory) { ArgumentNullException.ThrowIfNull(next); ArgumentNullException.ThrowIfNull(logger); ArgumentNullException.ThrowIfNull(forwarder); ArgumentNullException.ThrowIfNull(randomFactory); _next = next; _logger = logger; _forwarder = forwarder; _randomFactory = randomFactory; } /// public async Task Invoke(HttpContext context) { ArgumentNullException.ThrowIfNull(context); var reverseProxyFeature = context.GetReverseProxyFeature(); var destinations = reverseProxyFeature.AvailableDestinations ?? throw new InvalidOperationException($"The {nameof(IReverseProxyFeature)} Destinations collection was not set."); var route = context.GetRouteModel(); var cluster = route.Cluster!; var activity = context.GetYarpActivity(); activity?.AddTag("proxy.route_id", route.Config.RouteId); activity?.AddTag("proxy.cluster_id", cluster.ClusterId); if (destinations.Count == 0) { Log.NoAvailableDestinations(_logger, cluster.ClusterId); context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable; context.Features.Set(new ForwarderErrorFeature(ForwarderError.NoAvailableDestinations, ex: null)); activity?.SetStatus(ActivityStatusCode.Error); activity?.AddError("Proxy forwarding failed", "No available destinations to forward to"); return; } var destination = destinations[0]; if (destinations.Count > 1) { var random = _randomFactory.CreateRandomInstance(); Log.MultipleDestinationsAvailable(_logger, cluster.ClusterId); destination = destinations[random.Next(destinations.Count)]; } reverseProxyFeature.ProxiedDestination = destination; activity?.AddTag("proxy.destination_id", destination.DestinationId); var destinationModel = destination.Model; if (destinationModel is null) { throw new InvalidOperationException($"Chosen destination has no model set: '{destination.DestinationId}'"); } try { cluster.ConcurrencyCounter.Increment(); destination.ConcurrencyCounter.Increment(); ForwarderTelemetry.Log.ForwarderInvoke(cluster.ClusterId, route.Config.RouteId, destination.DestinationId); var clusterConfig = reverseProxyFeature.Cluster; var result = await _forwarder.SendAsync(context, destinationModel.Config.Address, clusterConfig.HttpClient, clusterConfig.Config.HttpRequest ?? ForwarderRequestConfig.Empty, route.Transformer); activity?.SetStatus((result == ForwarderError.None) ? ActivityStatusCode.Ok : ActivityStatusCode.Error); } finally { destination.ConcurrencyCounter.Decrement(); cluster.ConcurrencyCounter.Decrement(); } } private static class Log { private static readonly Action _noAvailableDestinations = LoggerMessage.Define( LogLevel.Warning, EventIds.NoAvailableDestinations, "No available destinations after load balancing for cluster '{clusterId}'."); private static readonly Action _multipleDestinationsAvailable = LoggerMessage.Define( LogLevel.Warning, EventIds.MultipleDestinationsAvailable, "More than one destination available for cluster '{clusterId}', load balancing may not be configured correctly. Choosing randomly."); public static void NoAvailableDestinations(ILogger logger, string clusterId) { _noAvailableDestinations(logger, clusterId, null); } public static void MultipleDestinationsAvailable(ILogger logger, string clusterId) { _multipleDestinationsAvailable(logger, clusterId, null); } } } ================================================ FILE: src/ReverseProxy/Forwarder/ForwarderRequestConfig.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Net.Http; using System.Threading; using Microsoft.AspNetCore.Http; namespace Yarp.ReverseProxy.Forwarder; /// /// Config for /// public sealed record ForwarderRequestConfig { /// /// An empty instance of this type. /// public static ForwarderRequestConfig Empty { get; } = new(); /// /// How long a request is allowed to remain idle between any operation completing, after which it will be canceled. /// The default is 100 seconds. The timeout will reset when response headers are received or after successfully reading or /// writing any request, response, or streaming data like gRPC or WebSockets. TCP keep-alive packets and HTTP/2 protocol pings will /// not reset the timeout, but WebSocket pings will. /// public TimeSpan? ActivityTimeout { get; init; } /// /// Preferred version of the outgoing request. /// The default is HTTP/2. /// public Version? Version { get; init; } /// /// The policy applied to version selection, e.g. whether to prefer downgrades, upgrades or /// request an exact version. The default is `RequestVersionOrLower`. /// public HttpVersionPolicy? VersionPolicy { get; init; } /// /// Allows to use write buffering when sending a response back to the client, /// if the server hosting YARP (e.g. IIS) supports it. /// NOTE: enabling it can break SSE (server side event) scenarios. /// public bool? AllowResponseBuffering { get; init; } public bool Equals(ForwarderRequestConfig? other) { if (other is null) { return false; } return ActivityTimeout == other.ActivityTimeout && VersionPolicy == other.VersionPolicy && Version == other.Version && AllowResponseBuffering == other.AllowResponseBuffering; } public override int GetHashCode() { return HashCode.Combine(ActivityTimeout, VersionPolicy, Version, AllowResponseBuffering); } } ================================================ FILE: src/ReverseProxy/Forwarder/ForwarderStage.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. namespace Yarp.ReverseProxy.Forwarder; internal enum ForwarderStage : int { SendAsyncStart = 1, SendAsyncStop, RequestContentTransferStart, ResponseContentTransferStart, ResponseUpgrade, } ================================================ FILE: src/ReverseProxy/Forwarder/ForwarderTelemetry.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Tracing; using System.Threading; namespace Yarp.ReverseProxy.Forwarder; [EventSource(Name = "Yarp.ReverseProxy")] internal sealed class ForwarderTelemetry : EventSource { public static readonly ForwarderTelemetry Log = new(); private IncrementingPollingCounter? _startedRequestsPerSecondCounter; private PollingCounter? _startedRequestsCounter; private PollingCounter? _currentRequestsCounter; private PollingCounter? _failedRequestsCounter; private long _startedRequests; private long _stoppedRequests; private long _failedRequests; [Event(1, Level = EventLevel.Informational)] public void ForwarderStart(string destinationPrefix) { Interlocked.Increment(ref _startedRequests); if (IsEnabled(EventLevel.Informational, EventKeywords.All)) { WriteEvent(eventId: 1, destinationPrefix); } } [Event(2, Level = EventLevel.Informational)] public void ForwarderStop(int statusCode) { Interlocked.Increment(ref _stoppedRequests); if (IsEnabled(EventLevel.Informational, EventKeywords.All)) { WriteEvent(eventId: 2, statusCode); } } [Event(3, Level = EventLevel.Informational)] public void ForwarderFailed(ForwarderError error) { Interlocked.Increment(ref _failedRequests); if (IsEnabled(EventLevel.Informational, EventKeywords.All)) { Debug.Assert(sizeof(ForwarderError) == sizeof(int), "Backing type of ForwarderError MUST NOT be changed"); WriteEvent(eventId: 3, (int)error); } } [Event(4, Level = EventLevel.Informational)] public void ForwarderStage(ForwarderStage stage) { if (IsEnabled(EventLevel.Informational, EventKeywords.All)) { Debug.Assert(sizeof(ForwarderStage) == sizeof(int), "Backing type of ForwarderStage MUST NOT be changed"); WriteEvent(eventId: 4, (int)stage); } } [Event(5, Level = EventLevel.Informational)] public void ContentTransferring(bool isRequest, long contentLength, long iops, long readTime, long writeTime) { if (IsEnabled(EventLevel.Informational, EventKeywords.All)) { WriteEvent(eventId: 5, isRequest, contentLength, iops, readTime, writeTime); } } [Event(6, Level = EventLevel.Informational)] public void ContentTransferred(bool isRequest, long contentLength, long iops, long readTime, long writeTime, long firstReadTime) { if (IsEnabled(EventLevel.Informational, EventKeywords.All)) { WriteEvent(eventId: 6, isRequest, contentLength, iops, readTime, writeTime, firstReadTime); } } [Event(7, Level = EventLevel.Informational)] public void ForwarderInvoke(string clusterId, string routeId, string destinationId) { if (IsEnabled(EventLevel.Informational, EventKeywords.All)) { WriteEvent(eventId: 7, clusterId, routeId, destinationId); } } protected override void OnEventCommand(EventCommandEventArgs command) { if (command.Command == EventCommand.Enable) { _startedRequestsCounter ??= new PollingCounter("requests-started", this, () => Volatile.Read(ref _startedRequests)) { DisplayName = "Requests Started", }; _startedRequestsPerSecondCounter ??= new IncrementingPollingCounter("requests-started-rate", this, () => Volatile.Read(ref _startedRequests)) { DisplayName = "Requests Started Rate", DisplayRateTimeScale = TimeSpan.FromSeconds(1) }; _failedRequestsCounter ??= new PollingCounter("requests-failed", this, () => Volatile.Read(ref _failedRequests)) { DisplayName = "Requests Failed" }; _currentRequestsCounter ??= new PollingCounter("current-requests", this, () => -Volatile.Read(ref _stoppedRequests) + Volatile.Read(ref _startedRequests)) { DisplayName = "Current Requests" }; } } [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "Parameters to this method are primitive and are trimmer safe.")] [NonEvent] private unsafe void WriteEvent(int eventId, bool arg1, long arg2, long arg3, long arg4, long arg5) { const int NumEventDatas = 5; var descrs = stackalloc EventData[NumEventDatas]; descrs[0] = new EventData { DataPointer = (IntPtr)(&arg1), Size = sizeof(int) // EventSource defines bool as a 32-bit type }; descrs[1] = new EventData { DataPointer = (IntPtr)(&arg2), Size = sizeof(long) }; descrs[2] = new EventData { DataPointer = (IntPtr)(&arg3), Size = sizeof(long) }; descrs[3] = new EventData { DataPointer = (IntPtr)(&arg4), Size = sizeof(long) }; descrs[4] = new EventData { DataPointer = (IntPtr)(&arg5), Size = sizeof(long) }; WriteEventCore(eventId, NumEventDatas, descrs); } [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "Parameters to this method are primitive and are trimmer safe.")] [NonEvent] private unsafe void WriteEvent(int eventId, bool arg1, long arg2, long arg3, long arg4, long arg5, long arg6) { const int NumEventDatas = 6; var descrs = stackalloc EventData[NumEventDatas]; descrs[0] = new EventData { DataPointer = (IntPtr)(&arg1), Size = sizeof(int) // EventSource defines bool as a 32-bit type }; descrs[1] = new EventData { DataPointer = (IntPtr)(&arg2), Size = sizeof(long) }; descrs[2] = new EventData { DataPointer = (IntPtr)(&arg3), Size = sizeof(long) }; descrs[3] = new EventData { DataPointer = (IntPtr)(&arg4), Size = sizeof(long) }; descrs[4] = new EventData { DataPointer = (IntPtr)(&arg5), Size = sizeof(long) }; descrs[5] = new EventData { DataPointer = (IntPtr)(&arg6), Size = sizeof(long) }; WriteEventCore(eventId, NumEventDatas, descrs); } } ================================================ FILE: src/ReverseProxy/Forwarder/HttpForwarder.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Net; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Timeouts; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; using Yarp.ReverseProxy.Utilities; namespace Yarp.ReverseProxy.Forwarder; /// /// Default implementation of . /// internal sealed class HttpForwarder : IHttpForwarder { private const string WebSocketName = "websocket"; private static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(100); private static readonly Version DefaultVersion = HttpVersion.Version20; private const HttpVersionPolicy DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrLower; private readonly ILogger _logger; private readonly TimeProvider _timeProvider; public HttpForwarder(ILogger logger, TimeProvider timeProvider) { ArgumentNullException.ThrowIfNull(logger); ArgumentNullException.ThrowIfNull(timeProvider); _logger = logger; _timeProvider = timeProvider; } /// /// Proxies the incoming request to the destination server, and the response back to the client. /// /// /// In what follows, as well as throughout in Reverse Proxy, we consider /// the following picture as illustrative of the Proxy. /// /// +-------------------+ /// | Destination + /// +-------------------+ /// ▲ | /// (b) | | (c) /// | ▼ /// +-------------------+ /// | Proxy + /// +-------------------+ /// ▲ | /// (a) | | (d) /// | ▼ /// +-------------------+ /// | Client + /// +-------------------+ /// /// /// (a) and (b) show the *request* path, going from the client to the target. /// (c) and (d) show the *response* path, going from the destination back to the client. /// /// Normal proxying comprises the following steps: /// (0) Disable ASP .NET Core limits for streaming requests /// (1) Create outgoing HttpRequestMessage /// (2) Setup copy of request body (background) Client --► Proxy --► Destination /// (3) Copy request headers Client --► Proxy --► Destination /// (4) Send the outgoing request using HttpMessageInvoker Client --► Proxy --► Destination /// (5) Copy response status line Client ◄-- Proxy ◄-- Destination /// (6) Copy response headers Client ◄-- Proxy ◄-- Destination /// (7-A) Check for a 101 upgrade response, this takes care of WebSockets as well as any other upgradeable protocol. /// (7-A-1) Upgrade client channel Client ◄--- Proxy ◄--- Destination /// (7-A-2) Copy duplex streams and return Client ◄--► Proxy ◄--► Destination /// (7-B) Copy (normal) response body Client ◄-- Proxy ◄-- Destination /// (8) Copy response trailer headers and finish response Client ◄-- Proxy ◄-- Destination /// (9) Wait for completion of step 2: copying request body Client --► Proxy --► Destination /// /// ASP .NET Core (Kestrel) will finally send response trailers (if any) /// after we complete the steps above and relinquish control. /// public ValueTask SendAsync( HttpContext context, string destinationPrefix, HttpMessageInvoker httpClient, ForwarderRequestConfig requestConfig, HttpTransformer transformer) => SendAsync(context, destinationPrefix, httpClient, requestConfig, transformer, CancellationToken.None); public async ValueTask SendAsync( HttpContext context, string destinationPrefix, HttpMessageInvoker httpClient, ForwarderRequestConfig requestConfig, HttpTransformer transformer, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(context); ArgumentNullException.ThrowIfNull(destinationPrefix); ArgumentNullException.ThrowIfNull(httpClient); ArgumentNullException.ThrowIfNull(requestConfig); ArgumentNullException.ThrowIfNull(transformer); if (RequestUtilities.IsResponseSet(context.Response)) { throw new InvalidOperationException("The request cannot be forwarded, the response has already started"); } // HttpClient overload for SendAsync changes response behavior to fully buffered which impacts performance // See discussion in https://github.com/dotnet/yarp/issues/458 if (httpClient is HttpClient) { throw new ArgumentException($"The http client must be of type HttpMessageInvoker, not HttpClient", nameof(httpClient)); } // "http://a".Length = 8 if (destinationPrefix is null || destinationPrefix.Length < 8) { throw new ArgumentException("Invalid destination prefix.", nameof(destinationPrefix)); } ForwarderTelemetry.Log.ForwarderStart(destinationPrefix); var activityCancellationSource = ActivityCancellationTokenSource.Rent(requestConfig?.ActivityTimeout ?? DefaultTimeout, context.RequestAborted, cancellationToken); try { var isClientHttp2OrGreater = ProtocolHelper.IsHttp2OrGreater(context.Request.Protocol); // NOTE: We heuristically assume gRPC-looking requests may require streaming semantics. // See https://github.com/dotnet/yarp/issues/118 for design discussion. var isStreamingRequest = isClientHttp2OrGreater && ProtocolHelper.IsGrpcContentType(context.Request.ContentType); HttpRequestMessage? destinationRequest = null; StreamCopyHttpContent? requestContent = null; HttpResponseMessage destinationResponse; try { // :: Step 1-3: Create outgoing HttpRequestMessage bool tryDowngradingH2WsOnFailure; (destinationRequest, requestContent, tryDowngradingH2WsOnFailure) = await CreateRequestMessageAsync( context, destinationPrefix, transformer, requestConfig, isStreamingRequest, activityCancellationSource); // Transforms generated a response, do not proxy. if (RequestUtilities.IsResponseSet(context.Response)) { Log.NotProxying(_logger, context.Response.StatusCode); return ForwarderError.None; } Log.Proxying(_logger, destinationRequest, isStreamingRequest); // :: Step 4: Send the outgoing request using HttpClient ForwarderTelemetry.Log.ForwarderStage(ForwarderStage.SendAsyncStart); try { // CodeQL [SM03781] SSRF - Request endpoint is controlled by YARP configuration. Sensitive headers are not copied by default. destinationResponse = await httpClient.SendAsync(destinationRequest, activityCancellationSource.Token); } catch (HttpRequestException hre) when (tryDowngradingH2WsOnFailure) { Debug.Assert(requestContent is null); // This is how SocketsHttpHandler communicates to us that we tried a HTTP/2 extension that wasn't // enabled by the server. We should retry on HTTP/1.1. if (hre.Data.Contains("SETTINGS_ENABLE_CONNECT_PROTOCOL")) { Debug.Assert(false == (bool?)hre.Data["SETTINGS_ENABLE_CONNECT_PROTOCOL"]); Log.RetryingWebSocketDowngradeNoConnect(_logger); } // This is how SocketsHttpHandler communicates to us that we tried HTTP/2, but the server only supports // HTTP/1.x (as determined by ALPN). We'll only get this when using TLS/https. Retry on HTTP/1.1. // We don't let SocketsHttpHandler downgrade automatically for us because we need to send different headers. else if (hre.Data.Contains("HTTP2_ENABLED")) { Debug.Assert(false == (bool?)hre.Data["HTTP2_ENABLED"]); Log.RetryingWebSocketDowngradeNoHttp2(_logger); } else { throw; } // Trying again activityCancellationSource.ResetTimeout(); Debug.Assert(requestConfig?.VersionPolicy is null or HttpVersionPolicy.RequestVersionOrLower || requestConfig.Version?.Major is null or 1, "HTTP/1.X was disallowed by policy, we shouldn't be retrying."); var config = requestConfig! with { Version = HttpVersion.Version11, VersionPolicy = HttpVersionPolicy.RequestVersionExact }; // Set the request back to null while we call into CreateRequestMessageAsync so that // potential exceptions are correctly treated as 'RequestCreation'. destinationRequest = null; (destinationRequest, requestContent, _) = await CreateRequestMessageAsync( context, destinationPrefix, transformer, config, isStreamingRequest, activityCancellationSource); // Transforms generated a response, do not proxy. if (RequestUtilities.IsResponseSet(context.Response)) { Log.NotProxying(_logger, context.Response.StatusCode); return ForwarderError.None; } // CodeQL [SM03781] SSRF - Request endpoint is controlled by YARP configuration. Sensitive headers are not copied by default. destinationResponse = await httpClient.SendAsync(destinationRequest, activityCancellationSource.Token); } } catch (Exception requestException) { return await HandleRequestFailureAsync(context, requestContent, requestException, transformer, activityCancellationSource, failedDuringRequestCreation: destinationRequest is null); } ForwarderTelemetry.Log.ForwarderStage(ForwarderStage.SendAsyncStop); // Reset the timeout since we received the response headers. activityCancellationSource.ResetTimeout(); Log.ResponseReceived(_logger, destinationResponse); try { // :: Step 5: Copy response status line Client ◄-- Proxy ◄-- Destination // :: Step 6: Copy response headers Client ◄-- Proxy ◄-- Destination var copyBody = await CopyResponseStatusAndHeadersAsync(destinationResponse, context, transformer, activityCancellationSource.Token); if (!copyBody) { // The transforms callback decided that the response body should be discarded. destinationResponse.Dispose(); if (requestContent is not null && requestContent.InProgress) { activityCancellationSource.Cancel(); await requestContent.ConsumptionTask; } return ForwarderError.None; } } catch (Exception ex) { destinationResponse.Dispose(); if (requestContent is not null && requestContent.InProgress) { activityCancellationSource.Cancel(); await requestContent.ConsumptionTask; } ReportProxyError(context, ForwarderError.ResponseHeaders, ex); // Clear the response since status code, reason and some headers might have already been copied and we want clean 502 response. context.Response.Clear(); context.Response.StatusCode = StatusCodes.Status502BadGateway; return ForwarderError.ResponseHeaders; } // :: Step 7-A: Check for a 101 upgrade response, this takes care of WebSockets as well as any other upgradeable protocol. // Also check for HTTP/2 CONNECT 200 responses, they function similarly. if (destinationResponse.StatusCode == HttpStatusCode.SwitchingProtocols || (destinationResponse.StatusCode == HttpStatusCode.OK && destinationResponse.Version == HttpVersion.Version20 && destinationRequest.Headers.Protocol is not null && destinationRequest.Method.Equals(HttpMethod.Connect)) ) { Debug.Assert(requestContent?.Started != true); return await HandleUpgradedResponse(context, destinationResponse, activityCancellationSource); } // NOTE: it may *seem* wise to call `context.Response.StartAsync()` at this point // since it looks like we are ready to send back response headers // (and this might help reduce extra delays while we wait to receive the body from the destination). // HOWEVER, this would produce the wrong result if it turns out that there is no content // from the destination -- instead of sending headers and terminating the stream at once, // we would send headers thinking a body may be coming, and there is none. // This is problematic on gRPC connections when the destination server encounters an error, // in which case it immediately returns the response headers and trailing headers, but no content, // and clients misbehave if the initial headers response does not indicate stream end. // :: Step 7-B: Copy response body Client ◄-- Proxy ◄-- Destination StreamCopyResult responseBodyCopyResult; Exception? responseBodyException; using (var destinationResponseStream = await destinationResponse.Content.ReadAsStreamAsync(activityCancellationSource.Token)) { // The response content-length is enforced by the server. (responseBodyCopyResult, responseBodyException) = await StreamCopier.CopyAsync(isRequest: false, destinationResponseStream, context.Response.Body, StreamCopier.UnknownLength, _timeProvider, activityCancellationSource, activityCancellationSource.Token); } if (responseBodyCopyResult != StreamCopyResult.Success) { return await HandleResponseBodyErrorAsync(context, requestContent, responseBodyCopyResult, responseBodyException!, activityCancellationSource); } // :: Step 8: Copy response trailer headers and finish response Client ◄-- Proxy ◄-- Destination await CopyResponseTrailingHeadersAsync(destinationResponse, context, transformer, activityCancellationSource.Token); if (isStreamingRequest) { // NOTE: We must call `CompleteAsync` so that Kestrel will flush all bytes to the client. // In the case where there was no response body, // this is also when headers and trailing headers are sent to the client. // Without this, the client might wait forever waiting for response bytes, // while we might wait forever waiting for request bytes, // leading to a stuck connection and no way to make progress. await context.Response.CompleteAsync(); } // :: Step 9: Wait for completion of step 2: copying request body Client --► Proxy --► Destination // NOTE: It is possible for the request body to NOT be copied even when there was an incoming request body, // e.g. when the request includes header `Expect: 100-continue` and the destination produced a non-1xx response. // We must only wait for the request body to complete if it actually started, // otherwise we run the risk of waiting indefinitely for a task that will never complete. if (requestContent is not null && requestContent.Started) { var (requestBodyCopyResult, requestBodyException) = await requestContent.ConsumptionTask; if (requestBodyCopyResult != StreamCopyResult.Success) { // The response succeeded. If there was a request body error then it was probably because the client or destination decided // to cancel it. Report as low severity. var error = requestBodyCopyResult switch { StreamCopyResult.InputError => ForwarderError.RequestBodyClient, StreamCopyResult.OutputError => ForwarderError.RequestBodyDestination, StreamCopyResult.Canceled => ForwarderError.RequestBodyCanceled, _ => throw new NotImplementedException(requestBodyCopyResult.ToString()) }; ReportProxyError(context, error, requestBodyException!); return error; } } } finally { activityCancellationSource.Return(); ForwarderTelemetry.Log.ForwarderStop(context.Response.StatusCode); } return ForwarderError.None; } private async ValueTask<(HttpRequestMessage, StreamCopyHttpContent?, bool)> CreateRequestMessageAsync(HttpContext context, string destinationPrefix, HttpTransformer transformer, ForwarderRequestConfig? requestConfig, bool isStreamingRequest, ActivityCancellationTokenSource activityToken) { var destinationRequest = new HttpRequestMessage(); var upgradeFeature = context.Features.Get(); var upgradeHeader = context.Request.Headers[HeaderNames.Upgrade].ToString(); var isSpdyRequest = (upgradeFeature?.IsUpgradableRequest ?? false) && upgradeHeader.StartsWith("SPDY/", StringComparison.OrdinalIgnoreCase); var isH1WsRequest = (upgradeFeature?.IsUpgradableRequest ?? false) && string.Equals(WebSocketName, upgradeHeader, StringComparison.OrdinalIgnoreCase); var connectFeature = context.Features.Get(); var connectProtocol = connectFeature?.Protocol; var isH2WsRequest = (connectFeature?.IsExtendedConnect ?? false) && string.Equals(WebSocketName, connectProtocol, StringComparison.OrdinalIgnoreCase); var outgoingHttps = destinationPrefix.StartsWith("https://", StringComparison.OrdinalIgnoreCase); var outgoingVersion = requestConfig?.Version ?? DefaultVersion; var outgoingPolicy = requestConfig?.VersionPolicy ?? DefaultVersionPolicy; var outgoingUpgrade = false; var outgoingConnect = false; var tryDowngradingH2WsOnFailure = false; if (isSpdyRequest) { // Can only be done on HTTP/1.1. outgoingUpgrade = true; } else if (isH1WsRequest || isH2WsRequest) { switch (outgoingVersion.Major, outgoingPolicy, outgoingHttps) { case (2, HttpVersionPolicy.RequestVersionExact, _): case (2, HttpVersionPolicy.RequestVersionOrHigher, _): outgoingConnect = true; break; case (1, HttpVersionPolicy.RequestVersionOrHigher, true): case (2, HttpVersionPolicy.RequestVersionOrLower, true): case (3, HttpVersionPolicy.RequestVersionOrLower, true): // Try H2WS, downgrade if needed. outgoingConnect = true; tryDowngradingH2WsOnFailure = true; break; default: // Override to use HTTP/1.1, nothing else is supported. outgoingUpgrade = true; break; } } bool http1IsAllowed = outgoingPolicy == HttpVersionPolicy.RequestVersionOrLower || outgoingVersion.Major == 1; if (outgoingUpgrade) { // Can only be done on HTTP/1.1, throw if disallowed by options. if (!http1IsAllowed) { throw new HttpRequestException(isSpdyRequest ? "SPDY requests require HTTP/1.1 support, but outbound HTTP/1.1 was disallowed by HttpVersionPolicy." : "An outgoing HTTP/1.1 Upgrade request is required to proxy this request, but is disallowed by HttpVersionPolicy."); } destinationRequest.Version = HttpVersion.Version11; destinationRequest.VersionPolicy = HttpVersionPolicy.RequestVersionOrLower; destinationRequest.Method = HttpMethod.Get; } else if (outgoingConnect) { // HTTP/2 only (for now). destinationRequest.Version = HttpVersion.Version20; destinationRequest.VersionPolicy = HttpVersionPolicy.RequestVersionExact; destinationRequest.Method = HttpMethod.Connect; destinationRequest.Headers.Protocol = connectProtocol ?? WebSocketName; tryDowngradingH2WsOnFailure &= http1IsAllowed; } else { Debug.Assert(http1IsAllowed || outgoingVersion.Major != 1); destinationRequest.Method = RequestUtilities.GetHttpMethod(context.Request.Method); destinationRequest.Version = outgoingVersion; destinationRequest.VersionPolicy = outgoingPolicy; } // :: Step 2: Setup copy of request body (background) Client --► Proxy --► Destination // Note that we must do this before step (3) because step (3) may also add headers to the HttpContent that we set up here. var requestContent = SetupRequestBodyCopy(context, isStreamingRequest, activityToken); destinationRequest.Content = requestContent; // :: Step 3: Copy request headers Client --► Proxy --► Destination await transformer.TransformRequestAsync(context, destinationRequest, destinationPrefix, activityToken.Token); if (!ReferenceEquals(requestContent, destinationRequest.Content) && destinationRequest.Content is not EmptyHttpContent) { throw new InvalidOperationException("Replacing the YARP outgoing request HttpContent is not supported. You should configure the HttpContext.Request instead."); } // The transformer generated a response, do not forward. if (RequestUtilities.IsResponseSet(context.Response)) { return (destinationRequest, requestContent, false); } // Transforms may have taken a while, especially if they buffered the body, they count as forward progress. activityToken.ResetTimeout(); FixupUpgradeRequestHeaders(context, destinationRequest, outgoingUpgrade, outgoingConnect); // Allow someone to custom build the request uri, otherwise provide a default for them. var request = context.Request; destinationRequest.RequestUri ??= RequestUtilities.MakeDestinationAddress(destinationPrefix, request.Path, request.QueryString); if (requestConfig?.AllowResponseBuffering != true) { context.Features.Get()?.DisableBuffering(); } return (destinationRequest, requestContent, tryDowngradingH2WsOnFailure); } // Connection and Upgrade headers were not copied with the rest of the headers. private void FixupUpgradeRequestHeaders(HttpContext context, HttpRequestMessage request, bool outgoingUpgrade, bool outgoingConnect) { if (outgoingUpgrade) { // H2->H1, add Connection, Upgrade, Sec-WebSocket-Key if (HttpProtocol.IsHttp2(context.Request.Protocol)) { request.Headers.TryAddWithoutValidation(HeaderNames.Connection, HeaderNames.Upgrade); request.Headers.TryAddWithoutValidation(HeaderNames.Upgrade, WebSocketName); // The client shouldn't be sending a Sec-WebSocket-Key header with H2 WebSockets, but if it did, let's use it. if (RequestUtilities.TryGetValues(request.Headers, HeaderNames.SecWebSocketKey, out var clientKey)) { if (!ProtocolHelper.CheckSecWebSocketKey(clientKey)) { Log.InvalidSecWebSocketKeyHeader(_logger, clientKey); // The request will not be forwarded if we change the status code. context.Response.StatusCode = StatusCodes.Status400BadRequest; } } else { var key = ProtocolHelper.CreateSecWebSocketKey(); request.Headers.TryAddWithoutValidation(HeaderNames.SecWebSocketKey, key); } } // H1->H1, re-add the original Connection, Upgrade headers. else { var connectionValues = context.Request.Headers.GetCommaSeparatedValues(HeaderNames.Connection); string? connectionUpgradeValue = null; foreach (var headerValue in connectionValues) { if (headerValue.Equals(HeaderNames.Upgrade, StringComparison.OrdinalIgnoreCase)) { // Preserve original value, case connectionUpgradeValue = headerValue; break; } } if (connectionUpgradeValue is not null && context.Request.Headers.TryGetValue(HeaderNames.Upgrade, out var upgradeValue)) { request.Headers.TryAddWithoutValidation(HeaderNames.Connection, connectionUpgradeValue); request.Headers.TryAddWithoutValidation(HeaderNames.Upgrade, (IEnumerable)upgradeValue); } } } // H1->H2, remove Sec-WebSocket-Key else if (outgoingConnect && !HttpProtocol.IsHttp2(context.Request.Protocol)) { var key = context.Request.Headers[HeaderNames.SecWebSocketKey]; if (!ProtocolHelper.CheckSecWebSocketKey(key)) { Log.InvalidSecWebSocketKeyHeader(_logger, key); // The request will not be forwarded if we change the status code. context.Response.StatusCode = StatusCodes.Status400BadRequest; } request.Headers.Remove(HeaderNames.SecWebSocketKey); } // else not an upgrade, or H2->H2, no changes needed } private StreamCopyHttpContent? SetupRequestBodyCopy(HttpContext context, bool isStreamingRequest, ActivityCancellationTokenSource activityToken) { // If we generate an HttpContent without a Content-Length then for HTTP/1.1 HttpClient will add a Transfer-Encoding: chunked header // even if it's a GET request. Some servers reject requests containing a Transfer-Encoding header if they're not expecting a body. // Try to be as specific as possible about the client's intent to send a body. The one thing we don't want to do is to start // reading the body early because that has side effects like 100-continue. var request = context.Request; var hasBody = true; var contentLength = request.Headers.ContentLength; var method = request.Method; var canHaveBodyFeature = request.HttpContext.Features.Get(); if (canHaveBodyFeature is not null) { // 5.0 servers provide a definitive answer for us. hasBody = canHaveBodyFeature.CanHaveBody; } // https://tools.ietf.org/html/rfc7230#section-3.3.3 // All HTTP/1.1 requests should have Transfer-Encoding or Content-Length. // Http.Sys/IIS will even add a Transfer-Encoding header to HTTP/2 requests with bodies for back-compat. // HTTP/1.0 Connection: close bodies are only allowed on responses, not requests. // https://tools.ietf.org/html/rfc1945#section-7.2.2 // // Transfer-Encoding overrides Content-Length per spec else if (request.Headers.TryGetValue(HeaderNames.TransferEncoding, out var transferEncoding) && transferEncoding.Count == 1 && string.Equals("chunked", transferEncoding.ToString(), StringComparison.OrdinalIgnoreCase)) { hasBody = true; } else if (contentLength.HasValue) { hasBody = contentLength > 0; } // Kestrel HTTP/2: There are no required headers that indicate if there is a request body, so we need to sniff other fields. else if (!ProtocolHelper.IsHttp2OrGreater(request.Protocol)) { hasBody = false; } // https://tools.ietf.org/html/rfc7231#section-4.3.1 // A payload within a GET/HEAD/DELETE/CONNECT request message has no defined semantics; sending a payload body on a // GET/HEAD/DELETE/CONNECT request might cause some existing implementations to reject the request. // https://tools.ietf.org/html/rfc7231#section-4.3.8 // A client MUST NOT send a message body in a TRACE request. else if (HttpMethods.IsGet(method) || HttpMethods.IsHead(method) || HttpMethods.IsDelete(method) || HttpMethods.IsConnect(method) || HttpMethods.IsTrace(method)) { hasBody = false; } // else hasBody defaults to true if (hasBody) { return new StreamCopyHttpContent(context, isStreamingRequest, _timeProvider, _logger, activityToken); } return null; } private ForwarderError HandleRequestBodyFailure(HttpContext context, StreamCopyResult requestBodyCopyResult, Exception requestBodyException, Exception additionalException, bool timedOut) { ForwarderError requestBodyError; int statusCode; switch (requestBodyCopyResult) { // Failed while trying to copy the request body from the client. It's ambiguous if the request or response failed first. case StreamCopyResult.InputError: requestBodyError = ForwarderError.RequestBodyClient; statusCode = timedOut ? StatusCodes.Status408RequestTimeout // Attempt to capture the status code from the request exception if available. : requestBodyException is BadHttpRequestException badHttpRequestException ? badHttpRequestException.StatusCode : StatusCodes.Status400BadRequest; break; // Failed while trying to copy the request body to the destination. It's ambiguous if the request or response failed first. case StreamCopyResult.OutputError: requestBodyError = ForwarderError.RequestBodyDestination; statusCode = timedOut ? StatusCodes.Status504GatewayTimeout : StatusCodes.Status502BadGateway; break; default: throw new NotImplementedException(requestBodyCopyResult.ToString()); } ReportProxyError(context, requestBodyError, new AggregateException(requestBodyException, additionalException)); // We don't know if the client is still around to see this error, but set it for diagnostics to see. if (!context.Response.HasStarted) { // Nothing has been sent to the client yet, we can still send a good error response. context.Response.Clear(); context.Response.StatusCode = statusCode; return requestBodyError; } ResetOrAbort(context, isCancelled: requestBodyCopyResult == StreamCopyResult.Canceled); return requestBodyError; } private async ValueTask HandleRequestFailureAsync(HttpContext context, StreamCopyHttpContent? requestContent, Exception requestException, HttpTransformer transformer, ActivityCancellationTokenSource requestCancellationSource, bool failedDuringRequestCreation) { var triedRequestBody = requestContent?.ConsumptionTask.IsCompleted == true; if (requestCancellationSource.CancelledByLinkedToken) { var requestBodyCanceled = false; if (triedRequestBody) { var (requestBodyCopyResult, requestBodyException) = requestContent!.ConsumptionTask.Result; requestBodyCanceled = requestBodyCopyResult == StreamCopyResult.Canceled; if (requestBodyCanceled) { requestException = new AggregateException(requestException, requestBodyException!); } } // Either the client went away (HttpContext.RequestAborted) or the CancellationToken provided to SendAsync was signaled. return await ReportErrorAsync(requestBodyCanceled ? ForwarderError.RequestBodyCanceled : ForwarderError.RequestCanceled, context.RequestAborted.IsCancellationRequested ? StatusCodes.Status400BadRequest : StatusCodes.Status502BadGateway); } // Check for request body errors, these may have triggered the response error. if (triedRequestBody) { var (requestBodyCopyResult, requestBodyException) = requestContent!.ConsumptionTask.Result; if (requestBodyCopyResult is StreamCopyResult.InputError or StreamCopyResult.OutputError) { var error = HandleRequestBodyFailure(context, requestBodyCopyResult, requestBodyException!, requestException, timedOut: requestCancellationSource.IsCancellationRequested); try { await transformer.TransformResponseAsync(context, proxyResponse: null, requestCancellationSource.Token); } catch (OperationCanceledException) { // We're about to report a more specific error, so ignore OCEs that occur here. } return error; } } if (requestException is OperationCanceledException) { Debug.Assert(requestCancellationSource.IsCancellationRequested || requestException.ToString().Contains("ConnectTimeout"), requestException.ToString()); return await ReportErrorAsync(ForwarderError.RequestTimedOut, StatusCodes.Status504GatewayTimeout); } // We couldn't communicate with the destination. return await ReportErrorAsync(failedDuringRequestCreation ? ForwarderError.RequestCreation : ForwarderError.Request, StatusCodes.Status502BadGateway); async ValueTask ReportErrorAsync(ForwarderError error, int statusCode) { ReportProxyError(context, error, requestException); context.Response.StatusCode = statusCode; if (requestContent is not null && requestContent.InProgress) { requestCancellationSource.Cancel(); await requestContent.ConsumptionTask; } try { await transformer.TransformResponseAsync(context, proxyResponse: null, requestCancellationSource.Token); } catch (OperationCanceledException) { // We may have manually cancelled the request CTS as part of error handling. // We're about to report a more specific error, so ignore OCEs that occur here. } return error; } } private static ValueTask CopyResponseStatusAndHeadersAsync(HttpResponseMessage source, HttpContext context, HttpTransformer transformer, CancellationToken cancellationToken) { context.Response.StatusCode = (int)source.StatusCode; 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, cancellationToken); } private async ValueTask HandleUpgradedResponse(HttpContext context, HttpResponseMessage destinationResponse, ActivityCancellationTokenSource activityCancellationSource) { ForwarderTelemetry.Log.ForwarderStage(ForwarderStage.ResponseUpgrade); var isHttp2Request = HttpProtocol.IsHttp2(context.Request.Protocol); var headerError = FixupUpgradeResponseHeaders(context, destinationResponse, isHttp2Request); if (headerError != ForwarderError.None) { destinationResponse.Dispose(); return headerError; } // :: Step 7-A-1: Upgrade the client channel. This will also send response headers. Stream upgradeResult; try { if (isHttp2Request) { var connectFeature = context.Features.Get(); Debug.Assert(connectFeature != null); upgradeResult = await connectFeature.AcceptAsync(); } else { var upgradeFeature = context.Features.Get(); Debug.Assert(upgradeFeature != null); upgradeResult = await upgradeFeature.UpgradeAsync(); } // Disable request timeout, if there is one, after the upgrade has been accepted context.Features.Get()?.DisableTimeout(); } catch (Exception ex) { destinationResponse.Dispose(); ReportProxyError(context, ForwarderError.UpgradeResponseClient, ex); return ForwarderError.UpgradeResponseClient; } using var clientStream = upgradeResult; // :: Step 7-A-2: Copy duplex streams using var destinationStream = await destinationResponse.Content.ReadAsStreamAsync(activityCancellationSource.Token); var requestTask = StreamCopier.CopyAsync(isRequest: true, clientStream, destinationStream, StreamCopier.UnknownLength, _timeProvider, activityCancellationSource, // HTTP/2 HttpClient request streams buffer by default. autoFlush: destinationResponse.Version == HttpVersion.Version20, activityCancellationSource.Token).AsTask(); var responseTask = StreamCopier.CopyAsync(isRequest: false, destinationStream, clientStream, StreamCopier.UnknownLength, _timeProvider, activityCancellationSource, activityCancellationSource.Token).AsTask(); // Make sure we report the first failure. var firstTask = await Task.WhenAny(requestTask, responseTask); var requestFinishedFirst = firstTask == requestTask; var secondTask = requestFinishedFirst ? responseTask : requestTask; ForwarderError error; var (firstResult, firstException) = firstTask.Result; if (firstResult != StreamCopyResult.Success) { error = ReportResult(context, requestFinishedFirst, firstResult, firstException!, activityCancellationSource); // Cancel the other direction activityCancellationSource.Cancel(); // Wait for this to finish before exiting so the resources get cleaned up properly. await secondTask; } else { var cancelReads = !requestFinishedFirst && !secondTask.IsCompleted; if (cancelReads) { // The response is finished, unblock the incoming reads activityCancellationSource.Cancel(); } var (secondResult, secondException) = await secondTask; if (!cancelReads && secondResult != StreamCopyResult.Success) { error = ReportResult(context, !requestFinishedFirst, secondResult, secondException!, activityCancellationSource); } else { error = ForwarderError.None; } } return error; ForwarderError ReportResult(HttpContext context, bool request, StreamCopyResult result, Exception exception, ActivityCancellationTokenSource activityCancellationSource) { var error = result switch { StreamCopyResult.InputError => request ? ForwarderError.UpgradeRequestClient : ForwarderError.UpgradeResponseDestination, StreamCopyResult.OutputError => request ? ForwarderError.UpgradeRequestDestination : ForwarderError.UpgradeResponseClient, StreamCopyResult.Canceled => request ? ForwarderError.UpgradeRequestCanceled : ForwarderError.UpgradeResponseCanceled, _ => throw new NotImplementedException(result.ToString()), }; if (activityCancellationSource.IsCancellationRequested && !activityCancellationSource.CancelledByLinkedToken) { // We haven't manually called `Cancel` on the ActivityCancellationTokenSource, and the linked tokens haven't been signaled, // so the only remaining option is that this failure was caused by the ActivityTimeout firing. error = ForwarderError.UpgradeActivityTimeout; } ReportProxyError(context, error, exception); return error; } } // The Connection and Upgrade headers were not copied by default private ForwarderError FixupUpgradeResponseHeaders(HttpContext context, HttpResponseMessage response, bool isHttp2Request) { if (isHttp2Request) { // H2 <- H1 Validate & remove the Sec-WebSocket-Accept header. if (response.Version != HttpVersion.Version20) { var success = RequestUtilities.TryGetValues(response.RequestMessage!.Headers, HeaderNames.SecWebSocketKey, out var key); Debug.Assert(success); var accept = context.Response.Headers[HeaderNames.SecWebSocketAccept]; var expectedAccept = ProtocolHelper.CreateSecWebSocketAccept(key.ToString()); if (!string.Equals(expectedAccept, accept, StringComparison.Ordinal)) // Base64 is case-sensitive { context.Response.Clear(); context.Response.StatusCode = StatusCodes.Status502BadGateway; ReportProxyError(context, ForwarderError.ResponseHeaders, new InvalidOperationException("The Sec-WebSocket-Accept header does not match the expected value.")); return ForwarderError.ResponseHeaders; } context.Response.Headers.Remove(HeaderNames.SecWebSocketAccept); context.Response.StatusCode = StatusCodes.Status200OK; } // else H2 <- H2, no changes needed return ForwarderError.None; } // H1 <- H2 if (response.Version == HttpVersion.Version20) { // Generate and add the Sec-WebSocket-Accept header, and the Connection and Upgrade headers var key = context.Request.Headers[HeaderNames.SecWebSocketKey]; var accept = ProtocolHelper.CreateSecWebSocketAccept(key); context.Response.Headers.TryAdd(HeaderNames.SecWebSocketAccept, accept); context.Response.Headers.TryAdd(HeaderNames.Connection, HeaderNames.Upgrade); context.Response.Headers.TryAdd(HeaderNames.Upgrade, WebSocketName); return ForwarderError.None; } // H1 <- H1 // Restore the Connection and Upgrade headers // We don't use NonValidated for the Connection header as we do want value validation. // HttpHeaders.TryGetValues will handle the parsing and split the values for us. if (RequestUtilities.TryGetValues(response.Headers, HeaderNames.Upgrade, out var upgradeValues) && response.Headers.TryGetValues(HeaderNames.Connection, out var connectionValues)) { foreach (var value in connectionValues) { if (value.Equals(HeaderNames.Upgrade, StringComparison.OrdinalIgnoreCase)) { context.Response.Headers.TryAdd(HeaderNames.Connection, value); context.Response.Headers.TryAdd(HeaderNames.Upgrade, upgradeValues); break; } } } return ForwarderError.None; } private async ValueTask HandleResponseBodyErrorAsync(HttpContext context, StreamCopyHttpContent? requestContent, StreamCopyResult responseBodyCopyResult, Exception responseBodyException, ActivityCancellationTokenSource requestCancellationSource) { if (requestContent is not null && requestContent.Started) { var alreadyFinished = requestContent.ConsumptionTask.IsCompleted; if (!alreadyFinished) { requestCancellationSource.Cancel(); } var (requestBodyCopyResult, requestBodyError) = await requestContent.ConsumptionTask; // Check for request body errors, these may have triggered the response error. if (alreadyFinished && requestBodyCopyResult is StreamCopyResult.InputError or StreamCopyResult.OutputError) { return HandleRequestBodyFailure(context, requestBodyCopyResult, requestBodyError!, responseBodyException, timedOut: requestCancellationSource.IsCancellationRequested && !requestCancellationSource.CancelledByLinkedToken); } } var error = responseBodyCopyResult switch { StreamCopyResult.InputError => ForwarderError.ResponseBodyDestination, StreamCopyResult.OutputError => ForwarderError.ResponseBodyClient, StreamCopyResult.Canceled => ForwarderError.ResponseBodyCanceled, _ => throw new NotImplementedException(responseBodyCopyResult.ToString()), }; ReportProxyError(context, error, responseBodyException); if (!context.Response.HasStarted) { // Nothing has been sent to the client yet, we can still send a good error response. context.Response.Clear(); context.Response.StatusCode = StatusCodes.Status502BadGateway; return error; } // The response has already started, we must forcefully terminate it so the client doesn't get // the mistaken impression that the truncated response is complete. ResetOrAbort(context, isCancelled: responseBodyCopyResult == StreamCopyResult.Canceled); return error; } private static ValueTask CopyResponseTrailingHeadersAsync(HttpResponseMessage source, HttpContext context, HttpTransformer transformer, CancellationToken cancellationToken) { // Copies trailers return transformer.TransformResponseTrailersAsync(context, source, cancellationToken); } private void ReportProxyError(HttpContext context, ForwarderError error, Exception ex) { context.Features.Set(new ForwarderErrorFeature(error, ex)); Log.ErrorProxying(_logger, error, ex); ForwarderTelemetry.Log.ForwarderFailed(error); } private static void ResetOrAbort(HttpContext context, bool isCancelled) { var resetFeature = context.Features.Get(); if (resetFeature is not null) { // https://tools.ietf.org/html/rfc7540#section-7 const int Cancelled = 2; const int InternalError = 8; resetFeature.Reset(isCancelled ? Cancelled : InternalError); return; } context.Abort(); } private static class Log { private static readonly Action _responseReceived = LoggerMessage.Define( LogLevel.Information, EventIds.ResponseReceived, "Received HTTP/{version} response {statusCode}."); private static readonly Action _proxying = LoggerMessage.Define( LogLevel.Information, EventIds.Forwarding, "Proxying to {targetUrl} {version} {versionPolicy} {isStreaming}"); private static readonly Action _proxyError = LoggerMessage.Define( LogLevel.Warning, EventIds.ForwardingError, "{error}: {message}"); private static readonly Action _proxyRequestCancelled = LoggerMessage.Define( LogLevel.Debug, EventIds.ForwardingRequestCancelled, "{error}: {message}"); private static readonly Action _notProxying = LoggerMessage.Define( LogLevel.Information, EventIds.NotForwarding, "Not Proxying, a {statusCode} response was set by the transforms."); private static readonly Action _retryingWebSocketDowngradeNoConnect = LoggerMessage.Define( LogLevel.Information, EventIds.RetryingWebSocketDowngradeNoConnect, "Unable to proxy the WebSocket using HTTP/2, the server does not support RFC 8441, retrying with HTTP/1.1."); private static readonly Action _retryingWebSocketDowngradeNoHttp2 = LoggerMessage.Define( LogLevel.Information, EventIds.RetryingWebSocketDowngradeNoHttp2, "Unable to proxy the WebSocket using HTTP/2, server does not support HTTP/2. Retrying with HTTP/1.1. Disable HTTP/2 negotiation for improved performance."); private static readonly Action _invalidKeyHeader = LoggerMessage.Define( LogLevel.Warning, EventIds.InvalidSecWebSocketKeyHeader, "Invalid Sec-WebSocket-Key header: '{key}'."); public static void ResponseReceived(ILogger logger, HttpResponseMessage msg) { _responseReceived(logger, msg.Version, (int)msg.StatusCode, null); } public static void Proxying(ILogger logger, HttpRequestMessage msg, bool isStreamingRequest) { // Avoid computing the AbsoluteUri unless logging is enabled if (logger.IsEnabled(LogLevel.Information)) { var streaming = isStreamingRequest ? "streaming" : string.Empty; var version = HttpProtocol.GetHttpProtocol(msg.Version); var versionPolicy = ProtocolHelper.GetVersionPolicy(msg.VersionPolicy); _proxying(logger, msg.RequestUri!.AbsoluteUri, version, versionPolicy, streaming, null); } } public static void NotProxying(ILogger logger, int statusCode) { _notProxying(logger, statusCode, null); } public static void InvalidSecWebSocketKeyHeader(ILogger logger, string? key) { _invalidKeyHeader(logger, key, null); } public static void ErrorProxying(ILogger logger, ForwarderError error, Exception ex) { var message = GetMessage(error); if (error is ForwarderError.RequestCanceled or ForwarderError.RequestBodyCanceled or ForwarderError.ResponseBodyCanceled or ForwarderError.UpgradeRequestCanceled or ForwarderError.UpgradeResponseCanceled) { // These error conditions are triggered by the client and are not generally indicative of a problem with the proxy. // It's unlikely that they will be useful in most cases, so we log them at Debug level to reduce noise. _proxyRequestCancelled(logger, error, message, ex); } else { _proxyError(logger, error, message, ex); } } public static void RetryingWebSocketDowngradeNoConnect(ILogger logger) { _retryingWebSocketDowngradeNoConnect(logger, null); } public static void RetryingWebSocketDowngradeNoHttp2(ILogger logger) { _retryingWebSocketDowngradeNoHttp2(logger, null); } private static string GetMessage(ForwarderError error) { return error switch { ForwarderError.None => throw new NotSupportedException("A more specific error must be used"), ForwarderError.Request => "An error was encountered before receiving a response.", ForwarderError.RequestCreation => "An error was encountered while creating the request message.", ForwarderError.RequestTimedOut => "The request timed out before receiving a response.", ForwarderError.RequestCanceled => "The request was canceled before receiving a response.", ForwarderError.RequestBodyCanceled => "Copying the request body was canceled.", ForwarderError.RequestBodyClient => "The client reported an error when copying the request body.", ForwarderError.RequestBodyDestination => "The destination reported an error when copying the request body.", ForwarderError.ResponseBodyCanceled => "Copying the response body was canceled.", ForwarderError.ResponseBodyClient => "The client reported an error when copying the response body.", ForwarderError.ResponseBodyDestination => "The destination reported an error when copying the response body.", ForwarderError.ResponseHeaders => "The destination returned a response that cannot be proxied back to the client.", ForwarderError.UpgradeRequestCanceled => "Copying the upgraded request body was canceled.", ForwarderError.UpgradeRequestClient => "The client reported an error when copying the upgraded request body.", ForwarderError.UpgradeRequestDestination => "The destination reported an error when copying the upgraded request body.", ForwarderError.UpgradeResponseCanceled => "Copying the upgraded response body was canceled.", ForwarderError.UpgradeResponseClient => "The client reported an error when copying the upgraded response body.", ForwarderError.UpgradeResponseDestination => "The destination reported an error when copying the upgraded response body.", ForwarderError.UpgradeActivityTimeout => "The WebSocket connection was closed after being idle longer than the Activity Timeout.", ForwarderError.NoAvailableDestinations => throw new NotImplementedException(), // Not used in this class _ => throw new NotImplementedException(error.ToString()), }; } } } ================================================ FILE: src/ReverseProxy/Forwarder/HttpTransformer.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Diagnostics; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; using Yarp.ReverseProxy.Transforms.Builder; namespace Yarp.ReverseProxy.Forwarder; public class HttpTransformer { /// /// A default set of transforms that adds X-Forwarded-* headers, removes the original Host value and /// copies all other request and response fields and headers, except for some protocol specific values. /// public static readonly HttpTransformer Default = TransformBuilder.CreateTransformer(new TransformBuilderContext()); /// /// An empty transformer that copies all request and response fields and headers, except for some /// protocol specific values. /// public static readonly HttpTransformer Empty = new HttpTransformer(); /// /// Used to create derived instances. /// protected HttpTransformer() { } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool IsBodylessStatusCode(HttpStatusCode statusCode) => statusCode switch { // A 1xx response is terminated by the end of the header section; it cannot contain content // or trailers. // See https://www.rfc-editor.org/rfc/rfc9110.html#section-15.2-2 >= HttpStatusCode.Continue and < HttpStatusCode.OK => true, // A 204 response is terminated by the end of the header section; it cannot contain content // or trailers. // See https://www.rfc-editor.org/rfc/rfc9110.html#section-15.3.5-5 HttpStatusCode.NoContent => true, // Since the 205 status code implies that no additional content will be provided, a server // MUST NOT generate content in a 205 response. // See https://www.rfc-editor.org/rfc/rfc9110.html#section-15.3.6-3 HttpStatusCode.ResetContent => true, _ => false }; /// /// A callback that is invoked prior to sending the proxied request. All HttpRequestMessage fields are /// initialized except RequestUri, which will be initialized after the callback if no value is provided. /// See for constructing a custom request Uri. /// The string parameter represents the destination URI prefix that should be used when constructing the RequestUri. /// The headers are copied by the base implementation, excluding some protocol headers like HTTP/2 pseudo headers (":authority"). /// This method may be overridden to conditionally produce a response, such as for error conditions, and prevent the request from /// being proxied. This is indicated by setting the `HttpResponse.StatusCode` to a value other than 200, or calling `HttpResponse.StartAsync()`, /// or writing to the `HttpResponse.Body` or `BodyWriter`. /// /// The incoming request. /// The outgoing proxy request. /// The uri prefix for the selected destination server which can be used to create the RequestUri. /// Indicates that the request is being canceled. public virtual ValueTask TransformRequestAsync(HttpContext httpContext, HttpRequestMessage proxyRequest, string destinationPrefix, CancellationToken cancellationToken) #pragma warning disable CS0618 // We're calling the overload without the CancellationToken for backwards compatibility. => TransformRequestAsync(httpContext, proxyRequest, destinationPrefix); #pragma warning restore CS0618 /// /// A callback that is invoked prior to sending the proxied request. All HttpRequestMessage fields are /// initialized except RequestUri, which will be initialized after the callback if no value is provided. /// See for constructing a custom request Uri. /// The string parameter represents the destination URI prefix that should be used when constructing the RequestUri. /// The headers are copied by the base implementation, excluding some protocol headers like HTTP/2 pseudo headers (":authority"). /// This method may be overridden to conditionally produce a response, such as for error conditions, and prevent the request from /// being proxied. This is indicated by setting the `HttpResponse.StatusCode` to a value other than 200, or calling `HttpResponse.StartAsync()`, /// or writing to the `HttpResponse.Body` or `BodyWriter`. /// /// The incoming request. /// The outgoing proxy request. /// The uri prefix for the selected destination server which can be used to create the RequestUri. [Obsolete("This overload of TransformRequestAsync is obsolete. Override and use the overload accepting a CancellationToken instead.")] public virtual ValueTask TransformRequestAsync(HttpContext httpContext, HttpRequestMessage proxyRequest, string destinationPrefix) { foreach (var header in httpContext.Request.Headers) { var headerName = header.Key; var headerValue = header.Value; if (RequestUtilities.ShouldSkipRequestHeader(headerName)) { continue; } RequestUtilities.AddHeader(proxyRequest, headerName, headerValue); } // https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.3 // If a message is received with both a Transfer-Encoding and a // Content-Length header field, the Transfer-Encoding overrides the // Content-Length. Such a message might indicate an attempt to // perform request smuggling (Section 9.5) or response splitting // (Section 9.4) and ought to be handled as an error. A sender MUST // remove the received Content-Length field prior to forwarding such // a message downstream. if (httpContext.Request.Headers.ContainsKey(HeaderNames.TransferEncoding) && httpContext.Request.Headers.ContainsKey(HeaderNames.ContentLength)) { proxyRequest.Content?.Headers.Remove(HeaderNames.ContentLength); } // https://datatracker.ietf.org/doc/html/rfc7540#section-8.1.2.2 // The only exception to this is the TE header field, which MAY be // present in an HTTP/2 request; when it is, it MUST NOT contain any // value other than "trailers". if (ProtocolHelper.IsHttp2OrGreater(httpContext.Request.Protocol)) { var te = httpContext.Request.Headers.GetCommaSeparatedValues(HeaderNames.TE); if (te is not null) { for (var i = 0; i < te.Length; i++) { if (string.Equals(te[i], "trailers", StringComparison.OrdinalIgnoreCase)) { var added = proxyRequest.Headers.TryAddWithoutValidation(HeaderNames.TE, te[i]); Debug.Assert(added); break; } } } } return default; } /// /// A callback that is invoked when the proxied response is received. The status code and reason phrase will be copied /// to the HttpContext.Response before the callback is invoked, but may still be modified there. The headers will be /// copied to HttpContext.Response.Headers by the base implementation, excludes certain protocol headers like /// `Transfer-Encoding: chunked`. /// /// The incoming request. /// The response from the destination. This can be null if the destination did not respond. /// Indicates that the request is being canceled. /// A bool indicating if the response should be proxied to the client or not. A derived implementation /// that returns false may send an alternate response inline or return control to the caller for it to retry, respond, /// etc. public virtual ValueTask TransformResponseAsync(HttpContext httpContext, HttpResponseMessage? proxyResponse, CancellationToken cancellationToken) #pragma warning disable CS0618 // We're calling the overload without the CancellationToken for backwards compatibility. => TransformResponseAsync(httpContext, proxyResponse); #pragma warning restore CS0618 /// /// A callback that is invoked when the proxied response is received. The status code and reason phrase will be copied /// to the HttpContext.Response before the callback is invoked, but may still be modified there. The headers will be /// copied to HttpContext.Response.Headers by the base implementation, excludes certain protocol headers like /// `Transfer-Encoding: chunked`. /// /// The incoming request. /// The response from the destination. This can be null if the destination did not respond. /// A bool indicating if the response should be proxied to the client or not. A derived implementation /// that returns false may send an alternate response inline or return control to the caller for it to retry, respond, /// etc. [Obsolete("This overload of TransformResponseAsync is obsolete. Override and use the overload accepting a CancellationToken instead.")] public virtual ValueTask TransformResponseAsync(HttpContext httpContext, HttpResponseMessage? proxyResponse) { if (proxyResponse is null) { return new ValueTask(false); } var responseHeaders = httpContext.Response.Headers; CopyResponseHeaders(proxyResponse.Headers, responseHeaders); if (proxyResponse.Content is not null) { CopyResponseHeaders(proxyResponse.Content.Headers, responseHeaders); } // https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.3 // If a message is received with both a Transfer-Encoding and a // Content-Length header field, the Transfer-Encoding overrides the // Content-Length. Such a message might indicate an attempt to // perform request smuggling (Section 9.5) or response splitting // (Section 9.4) and ought to be handled as an error. A sender MUST // remove the received Content-Length field prior to forwarding such // a message downstream. if (proxyResponse.Content is not null && proxyResponse.Headers.NonValidated.Contains(HeaderNames.TransferEncoding) && proxyResponse.Content.Headers.NonValidated.Contains(HeaderNames.ContentLength)) { httpContext.Response.Headers.Remove(HeaderNames.ContentLength); } // For responses with status codes that shouldn't include a body, // we remove the 'Content-Length: 0' header if one is present. if (proxyResponse.Content is not null && IsBodylessStatusCode(proxyResponse.StatusCode) && proxyResponse.Content.Headers.NonValidated.TryGetValues(HeaderNames.ContentLength, out var contentLengthValue) && contentLengthValue.ToString() == "0") { httpContext.Response.Headers.Remove(HeaderNames.ContentLength); } return new ValueTask(true); } /// /// A callback that is invoked after the response body to modify trailers, if supported. The trailers will be /// copied to the HttpContext.Response by the base implementation. /// /// The incoming request. /// The response from the destination. /// Indicates that the request is being canceled. public virtual ValueTask TransformResponseTrailersAsync(HttpContext httpContext, HttpResponseMessage proxyResponse, CancellationToken cancellationToken) #pragma warning disable CS0618 // We're calling the overload without the CancellationToken for backwards compatibility. => TransformResponseTrailersAsync(httpContext, proxyResponse); #pragma warning restore CS0618 /// /// A callback that is invoked after the response body to modify trailers, if supported. The trailers will be /// copied to the HttpContext.Response by the base implementation. /// /// The incoming request. /// The response from the destination. [Obsolete("This overload of TransformResponseTrailersAsync is obsolete. Override and use the overload accepting a CancellationToken instead.")] public virtual ValueTask TransformResponseTrailersAsync(HttpContext httpContext, HttpResponseMessage proxyResponse) { // NOTE: Deliberately not using `context.Response.SupportsTrailers()`, `context.Response.AppendTrailer(...)` // because they lookup `IHttpResponseTrailersFeature` for every call. Here we do it just once instead. var responseTrailersFeature = httpContext.Features.Get(); var outgoingTrailers = responseTrailersFeature?.Trailers; if (outgoingTrailers is not null && !outgoingTrailers.IsReadOnly) { // Note that trailers, if any, should already have been declared in Proxy's response // by virtue of us having proxied all response headers in step 6. CopyResponseHeaders(proxyResponse.TrailingHeaders, outgoingTrailers); } return default; } private static void CopyResponseHeaders(HttpHeaders source, IHeaderDictionary destination) { // We want to append to any prior values, if any. // Not using Append here because it skips empty headers. foreach (var header in source.NonValidated) { var headerName = header.Key; if (RequestUtilities.ShouldSkipResponseHeader(headerName)) { continue; } var currentValue = destination[headerName]; // https://github.com/dotnet/yarp/issues/2269 // The Strict-Transport-Security may be added by the proxy before forwarding. Only copy the header // if it's not already present. if (!StringValues.IsNullOrEmpty(currentValue) && string.Equals(headerName, HeaderNames.StrictTransportSecurity, StringComparison.OrdinalIgnoreCase)) { continue; } destination[headerName] = RequestUtilities.Concat(currentValue, header.Value); } } } ================================================ FILE: src/ReverseProxy/Forwarder/IForwarderErrorFeature.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; namespace Yarp.ReverseProxy.Forwarder; /// /// Stores errors and exceptions that occurred when forwarding the request to the destination. /// public interface IForwarderErrorFeature { /// /// The specified ProxyError. /// ForwarderError Error { get; } /// /// An Exception that occurred when forwarding the request to the destination, if any. /// Exception? Exception { get; } } ================================================ FILE: src/ReverseProxy/Forwarder/IForwarderHttpClientFactory.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Net.Http; namespace Yarp.ReverseProxy.Forwarder; /// /// Provides a method to create instances of /// for forwarding requests to an upstream server. /// /// /// This is somewhat similarly to `System.Net.Http.IHttpClientFactory`, /// except that this factory class is meant for direct use, /// which the forwarder requires in order to keep separate pools for each cluster. /// public interface IForwarderHttpClientFactory { /// /// Creates and configures an instance /// that can be used for forwarding requests to an upstream server. /// /// An carrying old and new cluster configurations. /// /// /// A call to can return either /// a new instance or an old one if the configuration has not changed. /// If the old configuration is null, a new is always created. /// The returned instance MUST NOT be disposed /// because it can be used concurrently by several in-flight requests. /// /// HttpMessageInvoker CreateClient(ForwarderHttpClientContext context); } ================================================ FILE: src/ReverseProxy/Forwarder/IHttpForwarder.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; namespace Yarp.ReverseProxy.Forwarder; /// /// Forward an HTTP request to a chosen destination. /// public interface IHttpForwarder { /// /// Forwards the incoming request to the destination server, and the response back to the client. /// /// The HttpContext to forward. /// The url prefix for where to forward the request to. /// The HTTP client used to forward the request. /// Config for the outgoing request. /// Request and response transforms. Use if /// custom transformations are not needed. /// The result of forwarding the request and response. ValueTask SendAsync(HttpContext context, string destinationPrefix, HttpMessageInvoker httpClient, ForwarderRequestConfig requestConfig, HttpTransformer transformer); /// /// Forwards the incoming request to the destination server, and the response back to the client. /// /// The HttpContext to forward. /// The url prefix for where to forward the request to. /// The HTTP client used to forward the request. /// Config for the outgoing request. /// Request and response transforms. Use if /// custom transformations are not needed. /// A cancellation token that can be used to abort the request. /// The result of forwarding the request and response. ValueTask SendAsync(HttpContext context, string destinationPrefix, HttpMessageInvoker httpClient, ForwarderRequestConfig requestConfig, HttpTransformer transformer, CancellationToken cancellationToken) => SendAsync(context, destinationPrefix, httpClient, requestConfig, transformer); } ================================================ FILE: src/ReverseProxy/Forwarder/IHttpForwarderExtensions.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Net.Http; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; namespace Yarp.ReverseProxy.Forwarder; /// /// Extensions methods for . /// public static class IHttpForwarderExtensions { /// /// Forwards the incoming request to the destination server, and the response back to the client. /// /// The forwarder instance. /// The HttpContext to forward. /// The url prefix for where to forward the request to. /// Config for the outgoing request. /// Transform function to apply to the forwarded request. /// The status of a forwarding operation. public static ValueTask SendAsync(this IHttpForwarder forwarder, HttpContext context, string destinationPrefix, ForwarderRequestConfig? requestConfig = null, Func? requestTransform = null) { ArgumentNullException.ThrowIfNull(forwarder); ArgumentNullException.ThrowIfNull(context); requestConfig ??= ForwarderRequestConfig.Empty; var transformer = requestTransform is null ? HttpTransformer.Default : new RequestTransformer(requestTransform); var httpClientProvider = context.RequestServices.GetRequiredService(); return forwarder.SendAsync(context, destinationPrefix, httpClientProvider.HttpClient, requestConfig, transformer); } /// /// Forwards the incoming request to the destination server, and the response back to the client. /// /// The forwarder instance. /// The HttpContext to forward. /// The url prefix for where to forward the request to. /// The HTTP client used to forward the request. /// The status of a forwarding operation. public static ValueTask SendAsync(this IHttpForwarder forwarder, HttpContext context, string destinationPrefix, HttpMessageInvoker httpClient) { ArgumentNullException.ThrowIfNull(forwarder); return forwarder.SendAsync(context, destinationPrefix, httpClient, ForwarderRequestConfig.Empty, HttpTransformer.Default); } /// /// Forwards the incoming request to the destination server, and the response back to the client. /// /// The forwarder instance. /// The HttpContext to forward. /// The url prefix for where to forward the request to. /// The HTTP client used to forward the request. /// Config for the outgoing request. /// The status of a forwarding operation. public static ValueTask SendAsync(this IHttpForwarder forwarder, HttpContext context, string destinationPrefix, HttpMessageInvoker httpClient, ForwarderRequestConfig requestConfig) { ArgumentNullException.ThrowIfNull(forwarder); return forwarder.SendAsync(context, destinationPrefix, httpClient, requestConfig, HttpTransformer.Default); } /// /// Forwards the incoming request to the destination server, and the response back to the client. /// /// The forwarder instance. /// The HttpContext to forward. /// The url prefix for where to forward the request to. /// The HTTP client used to forward the request. /// Transform function to apply to the forwarded request. /// The status of a forwarding operation. public static ValueTask SendAsync(this IHttpForwarder forwarder, HttpContext context, string destinationPrefix, HttpMessageInvoker httpClient, Func requestTransform) { return forwarder.SendAsync(context, destinationPrefix, httpClient, ForwarderRequestConfig.Empty, requestTransform); } /// /// Forwards the incoming request to the destination server, and the response back to the client. /// /// The forwarder instance. /// The HttpContext to forward. /// The url prefix for where to forward the request to. /// The HTTP client used to forward the request. /// Config for the outgoing request. /// Transform function to apply to the forwarded request. /// The status of a forwarding operation. public static ValueTask SendAsync(this IHttpForwarder forwarder, HttpContext context, string destinationPrefix, HttpMessageInvoker httpClient, ForwarderRequestConfig requestConfig, Func requestTransform) { return forwarder.SendAsync(context, destinationPrefix, httpClient, requestConfig, new RequestTransformer(requestTransform)); } } ================================================ FILE: src/ReverseProxy/Forwarder/ProtocolHelper.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Diagnostics; using System.Net; using System.Net.Http; using System.Security.Cryptography; using System.Text; using Microsoft.AspNetCore.Http; using Microsoft.Net.Http.Headers; namespace Yarp.ReverseProxy.Forwarder; internal static class ProtocolHelper { internal static readonly Version Http2Version = HttpVersion.Version20; internal static readonly Version Http11Version = HttpVersion.Version11; internal const string GrpcContentType = "application/grpc"; public static bool IsHttp2OrGreater(string protocol) => HttpProtocol.IsHttp2(protocol) || HttpProtocol.IsHttp3(protocol); public static string GetVersionPolicy(HttpVersionPolicy policy) { return policy switch { HttpVersionPolicy.RequestVersionOrLower => "RequestVersionOrLower", HttpVersionPolicy.RequestVersionOrHigher => "RequestVersionOrHigher", HttpVersionPolicy.RequestVersionExact => "RequestVersionExact", _ => throw new NotImplementedException(policy.ToString()), }; } /// /// Checks whether the provided content type header value represents a gRPC request. /// public static bool IsGrpcContentType(string? contentType) => contentType is not null && contentType.StartsWith(GrpcContentType, StringComparison.OrdinalIgnoreCase) && MediaTypeHeaderValue.TryParse(contentType, out var mediaType) && mediaType.MatchesMediaType(GrpcContentType); /// /// Creates a security key for sending in the Sec-WebSocket-Key header. /// internal static string CreateSecWebSocketKey() { // The value of this header field MUST be a nonce consisting of a randomly selected 16-byte // value that has been base64-encoded Span bytes = stackalloc byte[16]; // Base64-encode a new Guid's bytes to get the security key var success = Guid.NewGuid().TryWriteBytes(bytes); Debug.Assert(success); var secKey = Convert.ToBase64String(bytes); return secKey; } internal static bool CheckSecWebSocketKey(string? key) { // The value of this header field MUST be a nonce consisting of a randomly selected 16-byte // value that has been base64-encoded return !string.IsNullOrEmpty(key) && key.Length == 24; } /// /// Creates the Accept response to a given security key for sending in or verifying the Sec-WebSocket-Accept header value. /// internal static string CreateSecWebSocketAccept(string? key) { if (!CheckSecWebSocketKey(key)) { // This could happen if a custom message handler modified headers incorrectly. Debug.Fail("This should have already been validated elsewhere"); throw new InvalidOperationException("Unexpected Sec-WebSocket-Key header format."); } // GUID appended by the server as part of the security key response. Defined in the RFC. var wsServerGuidBytes = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"u8; Span bytes = stackalloc byte[24 /* Base64 guid length */ + wsServerGuidBytes.Length]; // Get the corresponding ASCII bytes for seckey+wsServerGuidBytes var encodedSecKeyLength = Encoding.ASCII.GetBytes(key, bytes); Debug.Assert(encodedSecKeyLength == 24); wsServerGuidBytes.CopyTo(bytes.Slice(encodedSecKeyLength)); // Hash the seckey+wsServerGuidBytes bytes #pragma warning disable CA5350 // Do Not Use Weak Cryptographic Algorithms -- the spec demands SHA1 in this case. SHA1.TryHashData(bytes, bytes, out var bytesWritten); #pragma warning restore CA5350 Debug.Assert(bytesWritten == 20 /* SHA1 hash length */); var accept = Convert.ToBase64String(bytes[..bytesWritten]); // Return the security key + accept value return accept; } } ================================================ FILE: src/ReverseProxy/Forwarder/RequestTransformer.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; namespace Yarp.ReverseProxy.Forwarder; internal sealed class RequestTransformer : HttpTransformer { private readonly Func _requestTransform; public RequestTransformer(Func requestTransform) { _requestTransform = requestTransform; } public override async ValueTask TransformRequestAsync(HttpContext httpContext, HttpRequestMessage proxyRequest, string destinationPrefix, CancellationToken cancellationToken) { await base.TransformRequestAsync(httpContext, proxyRequest, destinationPrefix, cancellationToken); await _requestTransform(httpContext, proxyRequest); } } ================================================ FILE: src/ReverseProxy/Forwarder/RequestUtilities.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Buffers; using System.Collections.Frozen; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; using System.Runtime.CompilerServices; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; using Yarp.ReverseProxy.Utilities; namespace Yarp.ReverseProxy.Forwarder; /// /// APIs that can be used when transforming requests. /// public static class RequestUtilities { // https://datatracker.ietf.org/doc/html/rfc3986/#appendix-A // pchar = unreserved / pct-encoded / sub-delims / ":" / "@" // pct-encoded = "%" HEXDIG HEXDIG // unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" // reserved = gen-delims / sub-delims // gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@" // sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "=" private static readonly SearchValues s_validPathChars = SearchValues.Create("!$&'()*+,-./0123456789:;=@ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz~"); /// /// Converts the given HTTP method (usually obtained from ) /// into the corresponding static instance. /// internal static HttpMethod GetHttpMethod(string method) => method switch { string mth when HttpMethods.IsGet(mth) => HttpMethod.Get, string mth when HttpMethods.IsPost(mth) => HttpMethod.Post, string mth when HttpMethods.IsPut(mth) => HttpMethod.Put, string mth when HttpMethods.IsDelete(mth) => HttpMethod.Delete, string mth when HttpMethods.IsOptions(mth) => HttpMethod.Options, string mth when HttpMethods.IsHead(mth) => HttpMethod.Head, string mth when HttpMethods.IsPatch(mth) => HttpMethod.Patch, string mth when HttpMethods.IsTrace(mth) => HttpMethod.Trace, // NOTE: Proxying "CONNECT" is not supported (by design!) string mth when HttpMethods.IsConnect(mth) => throw new NotSupportedException($"Unsupported request method '{method}'."), _ => new HttpMethod(method) }; internal static bool ShouldSkipRequestHeader(string headerName) { if (_headersToExclude.Contains(headerName)) { return true; } // Filter out HTTP/2 pseudo headers like ":method" and ":path", those go into other fields. if (headerName.StartsWith(':')) { return true; } return false; } internal static bool ShouldSkipResponseHeader(string headerName) { return _headersToExclude.Contains(headerName); } private static readonly FrozenSet _headersToExclude = new HashSet(17, StringComparer.OrdinalIgnoreCase) { HeaderNames.Connection, HeaderNames.TransferEncoding, HeaderNames.KeepAlive, HeaderNames.Upgrade, HeaderNames.ProxyConnection, HeaderNames.ProxyAuthenticate, "Proxy-Authentication-Info", HeaderNames.ProxyAuthorization, "Proxy-Features", "Proxy-Instruction", "Security-Scheme", "ALPN", "Close", "HTTP2-Settings", HeaderNames.UpgradeInsecureRequests, HeaderNames.TE, HeaderNames.AltSvc, }.ToFrozenSet(StringComparer.OrdinalIgnoreCase); // Headers marked as HttpHeaderType.Content in // https://github.com/dotnet/runtime/blob/main/src/libraries/System.Net.Http/src/System/Net/Http/Headers/KnownHeaders.cs private static readonly FrozenSet _contentHeaders = new HashSet(11, StringComparer.OrdinalIgnoreCase) { HeaderNames.Allow, HeaderNames.ContentDisposition, HeaderNames.ContentEncoding, HeaderNames.ContentLanguage, HeaderNames.ContentLength, HeaderNames.ContentLocation, HeaderNames.ContentMD5, HeaderNames.ContentRange, HeaderNames.ContentType, HeaderNames.Expires, HeaderNames.LastModified }.ToFrozenSet(StringComparer.OrdinalIgnoreCase); /// /// Appends the given path and query to the destination prefix while avoiding duplicate '/'. /// /// The scheme, host, port, and optional path base for the destination server. /// e.g. "http://example.com:80/path/prefix" /// The path to append. /// The query to append public static Uri MakeDestinationAddress(string destinationPrefix, PathString path, QueryString query) { ReadOnlySpan prefixSpan = destinationPrefix; if (path.HasValue && destinationPrefix.EndsWith('/')) { // When PathString has a value it always starts with a '/'. Avoid double slashes when concatenating. prefixSpan = prefixSpan[0..^1]; } var targetAddress = string.Concat(prefixSpan, EncodePath(path), query.ToUriComponent()); return new Uri(targetAddress, UriKind.Absolute); } // This isn't using PathString.ToUriComponent() because it doesn't round trip some escape sequences the way we want. internal static string EncodePath(PathString path) { var value = path.Value; if (string.IsNullOrEmpty(value)) { return string.Empty; } // Check if any escaping is required. var indexOfInvalidChar = value.AsSpan().IndexOfAnyExcept(s_validPathChars); return indexOfInvalidChar < 0 ? value : EncodePath(value, indexOfInvalidChar); } private static string EncodePath(string value, int i) { var builder = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]); var start = 0; var count = i; var requiresEscaping = false; while (i < value.Length) { if (s_validPathChars.Contains(value[i])) { if (requiresEscaping) { // the current segment requires escape builder.Append(Uri.EscapeDataString(value.Substring(start, count))); requiresEscaping = false; start = i; count = 0; } count++; i++; } else { if (!requiresEscaping) { // the current segment doesn't require escape builder.Append(value.AsSpan(start, count)); requiresEscaping = true; start = i; count = 0; } count++; i++; } } Debug.Assert(count > 0); if (requiresEscaping) { builder.Append(Uri.EscapeDataString(value.Substring(start, count))); } else { builder.Append(value.AsSpan(start, count)); } return builder.ToString(); } // Note: HttpClient.SendAsync will end up sending the union of // HttpRequestMessage.Headers and HttpRequestMessage.Content.Headers. // We don't really care where the proxied headers appear among those 2, // as long as they appear in one (and only one, otherwise they would be duplicated). // Some headers may only appear on HttpContentHeaders, in which case we inject // an EmptyHttpContent - dummy 0-length container only used for headers. internal static void AddHeader(HttpRequestMessage request, string headerName, StringValues value) { if (value.Count == 1) { string headerValue = value!; if (ContainsNewLines(headerValue)) { // TODO: Log return; } if (!request.Headers.TryAddWithoutValidation(headerName, headerValue)) { if (request.Content is null && _contentHeaders.Contains(headerName)) { request.Content = new EmptyHttpContent(); } var added = request.Content?.Headers.TryAddWithoutValidation(headerName, headerValue); // TODO: Log Debug.Assert(added.GetValueOrDefault(), $"A header was dropped; {headerName}: {headerValue}"); } } else { string[] headerValues = value!; foreach (var headerValue in headerValues) { if (ContainsNewLines(headerValue)) { // TODO: Log return; } } if (!request.Headers.TryAddWithoutValidation(headerName, headerValues)) { if (request.Content is null && _contentHeaders.Contains(headerName)) { request.Content = new EmptyHttpContent(); } var added = request.Content?.Headers.TryAddWithoutValidation(headerName, headerValues); // TODO: Log Debug.Assert(added.GetValueOrDefault(), $"A header was dropped; {headerName}: {string.Join(", ", headerValues)}"); } } #if DEBUG if (request.Content is EmptyHttpContent content && content.Headers.TryGetValues(HeaderNames.ContentLength, out var contentLength)) { Debug.Assert(contentLength.Single() == "0", "An actual content should have been set"); } #endif [MethodImpl(MethodImplOptions.AggressiveInlining)] static bool ContainsNewLines(string value) => value.AsSpan().IndexOfAny('\r', '\n') >= 0; } internal static void RemoveHeader(HttpRequestMessage request, string headerName) { if (_contentHeaders.Contains(headerName)) { request.Content?.Headers.Remove(headerName); } else { request.Headers.Remove(headerName); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static StringValues Concat(in StringValues existing, in HeaderStringValues values) { if (values.Count <= 1) { return StringValues.Concat(existing, values.ToString()); } else { return ConcatSlow(existing, values); } static StringValues ConcatSlow(in StringValues existing, in HeaderStringValues values) { Debug.Assert(values.Count > 1); var count = existing.Count; var newArray = new string[count + values.Count]; if (count == 1) { newArray[0] = existing.ToString(); } else { existing.ToArray().CopyTo(newArray, 0); } foreach (var value in values) { newArray[count++] = value; } Debug.Assert(count == newArray.Length); return newArray; } } [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static bool TryGetValues(HttpHeaders headers, string headerName, out StringValues values) { if (headers.NonValidated.TryGetValues(headerName, out var headerStringValues)) { if (headerStringValues.Count <= 1) { values = headerStringValues.ToString(); } else { values = ToArray(headerStringValues); } return true; } static StringValues ToArray(in HeaderStringValues values) { var array = new string[values.Count]; var i = 0; foreach (var value in values) { array[i++] = value; } Debug.Assert(i == array.Length); return array; } values = default; return false; } internal static bool IsResponseSet(HttpResponse response) { return response.StatusCode != StatusCodes.Status200OK || response.HasStarted; } } ================================================ FILE: src/ReverseProxy/Forwarder/ReverseProxyPropagator.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Net.Http; namespace Yarp.ReverseProxy.Forwarder; /// /// Removes existing headers and then delegates to the inner propagator. /// public sealed class ReverseProxyPropagator : DistributedContextPropagator { private readonly DistributedContextPropagator _innerPropagator; private readonly string[] _headersToRemove; /// /// ReverseProxyPropagator removes headers pointed out in innerPropagator. /// public ReverseProxyPropagator(DistributedContextPropagator innerPropagator) { ArgumentNullException.ThrowIfNull(innerPropagator); _innerPropagator = innerPropagator; _headersToRemove = _innerPropagator.Fields.ToArray(); } public override void Inject(Activity? activity, object? carrier, PropagatorSetterCallback? setter) { if (carrier is HttpRequestMessage message) { var headers = message.Headers; foreach (var header in _headersToRemove) { headers.Remove(header); } } _innerPropagator.Inject(activity, carrier, setter); } public override void ExtractTraceIdAndState(object? carrier, PropagatorGetterCallback? getter, out string? traceId, out string? traceState) => _innerPropagator.ExtractTraceIdAndState(carrier, getter, out traceId, out traceState); public override IEnumerable>? ExtractBaggage(object? carrier, PropagatorGetterCallback? getter) => _innerPropagator.ExtractBaggage(carrier, getter); public override IReadOnlyCollection Fields => _innerPropagator.Fields; } ================================================ FILE: src/ReverseProxy/Forwarder/StreamCopier.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Buffers; using System.Diagnostics; using System.Diagnostics.Tracing; using System.IO; using System.Threading; using System.Threading.Tasks; using Yarp.ReverseProxy.Utilities; namespace Yarp.ReverseProxy.Forwarder; /// /// A stream copier that captures errors. /// internal static class StreamCopier { // Based on performance investigations, see https://github.com/dotnet/yarp/pull/330#issuecomment-758851852. private const int DefaultBufferSize = 65536; public const long UnknownLength = -1; public static ValueTask<(StreamCopyResult, Exception?)> CopyAsync(bool isRequest, Stream input, Stream output, long promisedContentLength, TimeProvider timeProvider, ActivityCancellationTokenSource activityToken, CancellationToken cancellation) => CopyAsync(isRequest, input, output, promisedContentLength, timeProvider, activityToken, autoFlush: false, cancellation); public static ValueTask<(StreamCopyResult, Exception?)> CopyAsync(bool isRequest, Stream input, Stream output, long promisedContentLength, TimeProvider timeProvider, ActivityCancellationTokenSource activityToken, bool autoFlush, CancellationToken cancellation) { Debug.Assert(input is not null); Debug.Assert(output is not null); Debug.Assert(timeProvider is not null); Debug.Assert(activityToken is not null); // Avoid capturing 'isRequest' and 'timeProvider' in the state machine when telemetry is disabled var telemetry = ForwarderTelemetry.Log.IsEnabled(EventLevel.Informational, EventKeywords.All) ? new StreamCopierTelemetry(isRequest, timeProvider) : null; return CopyAsync(input, output, promisedContentLength, telemetry, activityToken, autoFlush, cancellation); } private static async ValueTask<(StreamCopyResult, Exception?)> CopyAsync(Stream input, Stream output, long promisedContentLength, StreamCopierTelemetry? telemetry, ActivityCancellationTokenSource activityToken, bool autoFlush, CancellationToken cancellation) { var buffer = ArrayPool.Shared.Rent(DefaultBufferSize); var read = 0; long contentLength = 0; try { while (true) { read = 0; // Issue a zero-byte read to the input stream to defer buffer allocation until data is available. // Note that if the underlying stream does not supporting blocking on zero byte reads, then this will // complete immediately and won't save any memory, but will still function correctly. var zeroByteReadTask = input.ReadAsync(Memory.Empty, cancellation); if (zeroByteReadTask.IsCompletedSuccessfully) { // Consume the ValueTask's result in case it is backed by an IValueTaskSource _ = zeroByteReadTask.Result; } else { // Take care not to return the same buffer to the pool twice in case zeroByteReadTask throws var bufferToReturn = buffer; buffer = null; ArrayPool.Shared.Return(bufferToReturn); await zeroByteReadTask; buffer = ArrayPool.Shared.Rent(DefaultBufferSize); } read = await input.ReadAsync(buffer.AsMemory(), cancellation); contentLength += read; // Normally this is enforced by the server, but it could get out of sync if something in the proxy modified the body. if (promisedContentLength != UnknownLength && contentLength > promisedContentLength) { return (StreamCopyResult.InputError, new InvalidOperationException("More bytes received than the specified Content-Length.")); } telemetry?.AfterRead(contentLength); // Success, reset the activity monitor. activityToken.ResetTimeout(); // End of the source stream. if (read == 0) { if (promisedContentLength == UnknownLength || contentLength == promisedContentLength) { return (StreamCopyResult.Success, null); } else { // This can happen if something in the proxy consumes or modifies part or all of the request body before proxying. return (StreamCopyResult.InputError, new InvalidOperationException($"Sent {contentLength} request content bytes, but Content-Length promised {promisedContentLength}.")); } } await output.WriteAsync(buffer.AsMemory(0, read), cancellation); if (autoFlush) { // HttpClient doesn't always flush outgoing data unless the buffer is full or the caller asks. // This is a problem for streaming protocols like WebSockets and gRPC. await output.FlushAsync(cancellation); } telemetry?.AfterWrite(); // Success, reset the activity monitor. activityToken.ResetTimeout(); } } catch (Exception ex) { if (read == 0) { telemetry?.AfterRead(contentLength); } else { telemetry?.AfterWrite(); } if (activityToken.CancelledByLinkedToken) { return (StreamCopyResult.Canceled, ex); } // If the activity timeout triggered while reading or writing, blame the sender or receiver. var result = read == 0 ? StreamCopyResult.InputError : StreamCopyResult.OutputError; return (result, ex); } finally { if (buffer is not null) { ArrayPool.Shared.Return(buffer); } telemetry?.Stop(); } } private sealed class StreamCopierTelemetry { private readonly bool _isRequest; private readonly TimeProvider _timeProvider; private long _contentLength; private long _iops; private long _readTime; private long _writeTime; private long _firstReadTime; private long _lastTime; private long _nextTransferringEvent; public StreamCopierTelemetry(bool isRequest, TimeProvider timeProvider) { ArgumentNullException.ThrowIfNull(timeProvider); _isRequest = isRequest; _timeProvider = timeProvider; _firstReadTime = -1; ForwarderTelemetry.Log.ForwarderStage(isRequest ? ForwarderStage.RequestContentTransferStart : ForwarderStage.ResponseContentTransferStart); _lastTime = timeProvider.GetTimestamp(); _nextTransferringEvent = _lastTime + _timeProvider.TimestampFrequency; } public void AfterRead(long contentLength) { _contentLength = contentLength; _iops++; var readStop = _timeProvider.GetTimestamp(); var currentReadTime = readStop - _lastTime; _lastTime = readStop; _readTime += currentReadTime; if (_firstReadTime < 0) { _firstReadTime = currentReadTime; } } public void AfterWrite() { var writeStop = _timeProvider.GetTimestamp(); _writeTime += writeStop - _lastTime; _lastTime = writeStop; if (writeStop >= _nextTransferringEvent) { ForwarderTelemetry.Log.ContentTransferring( _isRequest, _contentLength, _iops, _timeProvider.GetElapsedTime(0, _readTime).Ticks, _timeProvider.GetElapsedTime(0, _writeTime).Ticks); // Avoid attributing the time taken by logging ContentTransferring to the next read call _lastTime = _timeProvider.GetTimestamp(); _nextTransferringEvent = _lastTime + _timeProvider.TimestampFrequency; } } public void Stop() { ForwarderTelemetry.Log.ContentTransferred( _isRequest, _contentLength, _iops, _timeProvider.GetElapsedTime(0, _readTime).Ticks, _timeProvider.GetElapsedTime(0, _writeTime).Ticks, _timeProvider.GetElapsedTime(0, Math.Max(0, _firstReadTime)).Ticks); } } } ================================================ FILE: src/ReverseProxy/Forwarder/StreamCopyHttpContent.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Diagnostics; using System.IO; using System.Net; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Core.Features; using Microsoft.Extensions.Logging; using Yarp.ReverseProxy.Utilities; namespace Yarp.ReverseProxy.Forwarder; /// /// Custom /// used to proxy the incoming request body to the upstream server. /// /// /// /// By implementing a custom , we are able to execute /// our custom code for all HTTP protocol versions. /// See the remarks section of /// for more details. /// /// /// declares an internal property `AllowDuplex` /// which, when set to true, causes and friends /// to NOT tie up the request body stream operations to the same cancellation token /// that is passed to . /// /// /// When proxying duplex channels such as HTTP/2, gRPC, /// we need `HttpContent.AllowDuplex` to be true. /// It so happens to be by default on .NET Core 3.1. Should that ever change, /// this class will need to be updated. /// /// internal sealed class StreamCopyHttpContent : HttpContent { private readonly HttpContext _context; // HttpClient's machinery keeps an internal buffer that doesn't get flushed to the socket on every write. // Some protocols (e.g. gRPC) may rely on specific bytes being sent, and HttpClient's buffering would prevent it. private bool _isStreamingRequest; private readonly TimeProvider _timeProvider; private readonly ILogger _logger; private readonly ActivityCancellationTokenSource _activityToken; private readonly TaskCompletionSource<(StreamCopyResult, Exception?)> _tcs = new(TaskCreationOptions.RunContinuationsAsynchronously); private int _started; public StreamCopyHttpContent(HttpContext context, bool isStreamingRequest, TimeProvider timeProvider, ILogger logger, ActivityCancellationTokenSource activityToken) { ArgumentNullException.ThrowIfNull(context); ArgumentNullException.ThrowIfNull(timeProvider); _context = context; _isStreamingRequest = isStreamingRequest; _timeProvider = timeProvider; _logger = logger; _activityToken = activityToken; } /// /// Gets a that completes in successful or failed state /// mimicking the result of SerializeToStreamAsync. /// public Task<(StreamCopyResult, Exception?)> ConsumptionTask => _tcs.Task; /// /// Gets a value indicating whether consumption of this content has begun. /// Property can be used to track the asynchronous outcome of the operation. /// /// /// When used as an outgoing request content with , /// this should always be true by the time the task returned by /// /// completes, even when using . /// public bool Started => Volatile.Read(ref _started) == 1; public bool InProgress => Started && !ConsumptionTask.IsCompleted; /// /// Copies bytes from the stream provided in our constructor into the target . /// /// /// This is used internally by HttpClient.SendAsync to send the request body. /// Here's the sequence of events as of commit 17300169760c61a90cab8d913636c1058a30a8c1 (https://github.com/dotnet/corefx -- tag v3.1.1). /// /// /// HttpClient.SendAsync --> /// HttpMessageInvoker.SendAsync --> /// HttpClientHandler.SendAsync --> /// SocketsHttpHandler.SendAsync --> /// HttpConnectionHandler.SendAsync --> /// HttpConnectionPoolManager.SendAsync --> /// HttpConnectionPool.SendAsync --> ... --> /// { /// HTTP/1.1: HttpConnection.SendAsync --> /// HttpConnection.SendAsyncCore --> /// HttpConnection.SendRequestContentAsync --> /// HttpContent.CopyToAsync /// /// HTTP/2: Http2Connection.SendAsync --> /// Http2Stream.SendRequestBodyAsync --> /// HttpContent.CopyToAsync /// /// /* Only in .NET 5: /// HTTP/3: Http3Connection.SendAsync --> /// Http3Connection.SendWithoutWaitingAsync --> /// Http3RequestStream.SendAsync --> /// Http3RequestStream.SendContentAsync --> /// HttpContent.CopyToAsync /// */ /// } /// /// HttpContent.CopyToAsync --> /// HttpContent.SerializeToStreamAsync (bingo!) /// /// /// Conclusion: by overriding HttpContent.SerializeToStreamAsync, /// we have full control over pumping bytes to the target stream for all protocols /// (except Web Sockets, which is handled separately). /// protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context) { return SerializeToStreamAsync(stream, context, CancellationToken.None); } protected override async Task SerializeToStreamAsync(Stream stream, TransportContext? context, CancellationToken cancellationToken) { if (Interlocked.Exchange(ref _started, 1) == 1) { throw new InvalidOperationException("Stream was already consumed."); } // The cancellationToken that is passed to this method is: // On HTTP/1.1: Linked HttpContext.RequestAborted + Request Timeout // On HTTP/2.0: SocketsHttpHandler error / the server wants us to stop sending content / H2 connection closed // _cancellation will be the same as cancellationToken for HTTP/1.1, so we can avoid the overhead of linking them CancellationTokenSource? linkedCts = null; if (_activityToken.Token == cancellationToken) { // We're talking to the destination via HTTP/1.1, so this can't be a streaming gRPC request. _isStreamingRequest = false; // TODO: Log if _isStreamingRequest is true? Something went wrong with protocol selection. } else { Debug.Assert(cancellationToken.CanBeCanceled); linkedCts = CancellationTokenSource.CreateLinkedTokenSource(_activityToken.Token, cancellationToken); cancellationToken = linkedCts.Token; if (_isStreamingRequest) { DisableMinRequestBodyDataRateAndMaxRequestBodySize(_context); } } try { // Immediately flush request stream to send headers // https://github.com/dotnet/corefx/issues/39586#issuecomment-516210081 try { await stream.FlushAsync(cancellationToken); } catch (Exception ex) { var canceled = ex is OperationCanceledException || _activityToken.CancelledByLinkedToken; _tcs.TrySetResult((canceled ? StreamCopyResult.Canceled : StreamCopyResult.OutputError, ex)); return; } // Check that the content-length matches the request body size. This can be removed in .NET 7 now that SocketsHttpHandler // enforces this: https://github.com/dotnet/runtime/issues/62258. // // Note on `_isStreamingRequest`: // The.NET Core HttpClient stack keeps its own buffers on top of the underlying outgoing connection socket. // We flush those buffers down to the socket on every write when this is set, // but it does NOT result in calls to flush on the underlying socket. // This is necessary because we proxy http2 transparently, // and we are deliberately unaware of packet structure used e.g. in gRPC duplex channels. // Because the sockets aren't flushed, the perf impact of this choice is expected to be small. // Future: It may be wise to set this to true for *all* http2 incoming requests, // but for now, out of an abundance of caution, we only do it for requests that look like gRPC. var (result, error) = await StreamCopier.CopyAsync(isRequest: true, _context.Request.Body, stream, Headers.ContentLength ?? StreamCopier.UnknownLength, _timeProvider, _activityToken, _isStreamingRequest, cancellationToken); _tcs.TrySetResult((result, error)); // Check for errors that weren't the result of the destination failing. // We have to throw something here so the transport knows the body is incomplete. // We can't re-throw the original exception since that would cause concurrency issues. // We need to wrap it. if (result == StreamCopyResult.InputError) { throw new IOException("An error occurred when reading the request body from the client.", error); } if (result == StreamCopyResult.Canceled) { throw new OperationCanceledException("The request body copy was canceled.", error); } } finally { linkedCts?.Dispose(); } } // this is used internally by HttpContent.ReadAsStreamAsync(...) protected override Task CreateContentReadStreamAsync() { // Nobody should be calling this... throw new NotImplementedException(); } protected override bool TryComputeLength(out long length) { // We can't know the length of the content being pushed to the output stream. length = -1; return false; } /// /// Disable some ASP .NET Core server limits so that we can handle long-running gRPC requests unconstrained. /// Note that the gRPC server implementation on ASP .NET Core does the same for client-streaming and duplex methods. /// Since in Gateway we have no way to determine if the current request requires client-streaming or duplex comm, /// we do this for *all* incoming requests that look like they might be gRPC. /// /// /// Inspired on /// . /// private void DisableMinRequestBodyDataRateAndMaxRequestBodySize(HttpContext httpContext) { var minRequestBodyDataRateFeature = httpContext.Features.Get(); if (minRequestBodyDataRateFeature is not null) { minRequestBodyDataRateFeature.MinDataRate = null; } var maxRequestBodySizeFeature = httpContext.Features.Get(); if (maxRequestBodySizeFeature is not null) { if (!maxRequestBodySizeFeature.IsReadOnly) { maxRequestBodySizeFeature.MaxRequestBodySize = null; } else { // IsReadOnly could be true if middleware has already started reading the request body // In that case we can't disable the max request body size for the request stream _logger.LogWarning("Unable to disable max request body size."); } } } } ================================================ FILE: src/ReverseProxy/Forwarder/StreamCopyResult.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. namespace Yarp.ReverseProxy.Forwarder; internal enum StreamCopyResult { Success, InputError, OutputError, Canceled } ================================================ FILE: src/ReverseProxy/Health/ActiveHealthCheckMonitor.Log.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using Microsoft.Extensions.Logging; namespace Yarp.ReverseProxy.Health; internal partial class ActiveHealthCheckMonitor { private static class Log { private static readonly Action _explicitActiveCheckOfAllClustersHealthFailed = LoggerMessage.Define( LogLevel.Error, EventIds.ExplicitActiveCheckOfAllClustersHealthFailed, "An explicitly started active check of all clusters health failed."); private static readonly Action _activeHealthProbingFailedOnCluster = LoggerMessage.Define( LogLevel.Error, EventIds.ActiveHealthProbingFailedOnCluster, "Active health probing failed on cluster `{clusterId}`."); private static readonly Action _errorOccurredDuringActiveHealthProbingShutdownOnCluster = LoggerMessage.Define( LogLevel.Error, EventIds.ErrorOccurredDuringActiveHealthProbingShutdownOnCluster, "An error occurred during shutdown of an active health probing on cluster `{clusterId}`."); private static readonly Action _activeHealthProbeConstructionFailedOnCluster = LoggerMessage.Define( LogLevel.Error, EventIds.ActiveHealthProbeConstructionFailedOnCluster, "Construction of an active health probe for destination `{destinationId}` on cluster `{clusterId}` failed."); private static readonly Action _activeHealthProbeCancelledOnDestination = LoggerMessage.Define( LogLevel.Debug, EventIds.ActiveHealthProbeCancelledOnDestination, "Active health probing for destination `{destinationId}` on cluster `{clusterId}` was cancelled."); private static readonly Action _startingActiveHealthProbingOnCluster = LoggerMessage.Define( LogLevel.Debug, EventIds.StartingActiveHealthProbingOnCluster, "Starting active health check probing on cluster `{clusterId}`."); private static readonly Action _stoppedActiveHealthProbingOnCluster = LoggerMessage.Define( LogLevel.Debug, EventIds.StoppedActiveHealthProbingOnCluster, "Active health check probing on cluster `{clusterId}` has stopped."); private static readonly Action _destinationProbingCompleted = LoggerMessage.Define( LogLevel.Information, EventIds.DestinationProbingCompleted, "Probing destination `{destinationId}` on cluster `{clusterId}` completed with the response code `{responseCode}`."); private static readonly Action _destinationProbingFailed = LoggerMessage.Define( LogLevel.Warning, EventIds.DestinationProbingFailed, "Probing destination `{destinationId}` on cluster `{clusterId}` failed."); private static readonly Action _sendingHealthProbeToEndpointOfDestination = LoggerMessage.Define( LogLevel.Debug, EventIds.SendingHealthProbeToEndpointOfDestination, "Sending a health probe to endpoint `{endpointUri}` of destination `{destinationId}` on cluster `{clusterId}`."); public static void ExplicitActiveCheckOfAllClustersHealthFailed(ILogger logger, Exception ex) { _explicitActiveCheckOfAllClustersHealthFailed(logger, ex); } public static void ActiveHealthProbingFailedOnCluster(ILogger logger, string clusterId, Exception ex) { _activeHealthProbingFailedOnCluster(logger, clusterId, ex); } public static void ErrorOccurredDuringActiveHealthProbingShutdownOnCluster(ILogger logger, string clusterId, Exception ex) { _errorOccurredDuringActiveHealthProbingShutdownOnCluster(logger, clusterId, ex); } public static void ActiveHealthProbeConstructionFailedOnCluster(ILogger logger, string destinationId, string clusterId, Exception ex) { _activeHealthProbeConstructionFailedOnCluster(logger, destinationId, clusterId, ex); } public static void ActiveHealthProbeCancelledOnDestination(ILogger logger, string destinationId, string clusterId) { _activeHealthProbeCancelledOnDestination(logger, destinationId, clusterId, null); } public static void StartingActiveHealthProbingOnCluster(ILogger logger, string clusterId) { _startingActiveHealthProbingOnCluster(logger, clusterId, null); } public static void StoppedActiveHealthProbingOnCluster(ILogger logger, string clusterId) { _stoppedActiveHealthProbingOnCluster(logger, clusterId, null); } public static void DestinationProbingCompleted(ILogger logger, string destinationId, string clusterId, int responseCode) { _destinationProbingCompleted(logger, destinationId, clusterId, responseCode, null); } public static void DestinationProbingFailed(ILogger logger, string destinationId, string clusterId, Exception ex) { _destinationProbingFailed(logger, destinationId, clusterId, ex); } public static void SendingHealthProbeToEndpointOfDestination(ILogger logger, Uri? endpointUri, string destinationId, string clusterId) { _sendingHealthProbeToEndpointOfDestination(logger, endpointUri, destinationId, clusterId, null); } } } ================================================ FILE: src/ReverseProxy/Health/ActiveHealthCheckMonitor.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Frozen; using System.Collections.Generic; using System.Diagnostics; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Yarp.ReverseProxy.Model; using Yarp.ReverseProxy.Utilities; namespace Yarp.ReverseProxy.Health; internal partial class ActiveHealthCheckMonitor : IActiveHealthCheckMonitor, IClusterChangeListener, IDisposable { private readonly ActiveHealthCheckMonitorOptions _monitorOptions; private readonly FrozenDictionary _policies; private readonly IProbingRequestFactory _probingRequestFactory; private readonly ILogger _logger; public ActiveHealthCheckMonitor( IOptions monitorOptions, IEnumerable policies, IProbingRequestFactory probingRequestFactory, TimeProvider timeProvider, ILogger logger) { ArgumentNullException.ThrowIfNull(monitorOptions?.Value); ArgumentNullException.ThrowIfNull(policies); ArgumentNullException.ThrowIfNull(probingRequestFactory); ArgumentNullException.ThrowIfNull(logger); _monitorOptions = monitorOptions.Value; _policies = policies.ToDictionaryByUniqueId(p => p.Name); _probingRequestFactory = probingRequestFactory; _logger = logger; Scheduler = new EntityActionScheduler(cluster => ProbeCluster(cluster), autoStart: false, runOnce: false, timeProvider); } public bool InitialProbeCompleted { get; private set; } internal EntityActionScheduler Scheduler { get; } public Task CheckHealthAsync(IEnumerable clusters) { return Task.Run(async () => { try { var probeClusterTasks = new List(); foreach (var cluster in clusters) { if ((cluster.Model.Config.HealthCheck?.Active?.Enabled).GetValueOrDefault()) { probeClusterTasks.Add(ProbeCluster(cluster)); } } await Task.WhenAll(probeClusterTasks); } catch (Exception ex) { Log.ExplicitActiveCheckOfAllClustersHealthFailed(_logger, ex); } finally { InitialProbeCompleted = true; } Scheduler.Start(); }); } public void OnClusterAdded(ClusterState cluster) { var config = cluster.Model.Config.HealthCheck?.Active; if (config is not null && config.Enabled.GetValueOrDefault()) { Scheduler.ScheduleEntity(cluster, config.Interval ?? _monitorOptions.DefaultInterval); } } public void OnClusterChanged(ClusterState cluster) { var config = cluster.Model.Config.HealthCheck?.Active; if (config is not null && config.Enabled.GetValueOrDefault()) { Scheduler.ChangePeriod(cluster, config.Interval ?? _monitorOptions.DefaultInterval); } else { Scheduler.UnscheduleEntity(cluster); } } public void OnClusterRemoved(ClusterState cluster) { Scheduler.UnscheduleEntity(cluster); } public void Dispose() { Scheduler.Dispose(); } private async Task ProbeCluster(ClusterState cluster) { var config = cluster.Model.Config.HealthCheck?.Active; if (config is null || !config.Enabled.GetValueOrDefault()) { return; } // Creates an Activity to trace the active health checks using var activity = Observability.YarpActivitySource.StartActivity("proxy.cluster_health_checks", ActivityKind.Consumer); activity?.AddTag("proxy.cluster_id", cluster.ClusterId); Log.StartingActiveHealthProbingOnCluster(_logger, cluster.ClusterId); var allDestinations = cluster.DestinationsState.AllDestinations; var probeTasks = new Task[allDestinations.Count]; var probeResults = new DestinationProbingResult[probeTasks.Length]; var timeout = config.Timeout ?? _monitorOptions.DefaultTimeout; for (var i = 0; i < probeTasks.Length; i++) { probeTasks[i] = ProbeDestinationAsync(cluster, allDestinations[i], timeout); } for (var i = 0; i < probeResults.Length; i++) { probeResults[i] = await probeTasks[i]; } try { var policy = _policies.GetRequiredServiceById(config.Policy, HealthCheckConstants.ActivePolicy.ConsecutiveFailures); policy.ProbingCompleted(cluster, probeResults); activity?.SetStatus(ActivityStatusCode.Ok); } catch (Exception ex) { Log.ActiveHealthProbingFailedOnCluster(_logger, cluster.ClusterId, ex); activity?.SetStatus(ActivityStatusCode.Error); } finally { try { foreach (var probeResult in probeResults) { probeResult.Response?.Dispose(); } } catch (Exception ex) { Log.ErrorOccurredDuringActiveHealthProbingShutdownOnCluster(_logger, cluster.ClusterId, ex); } Log.StoppedActiveHealthProbingOnCluster(_logger, cluster.ClusterId); } } private async Task ProbeDestinationAsync(ClusterState cluster, DestinationState destination, TimeSpan timeout) { using var probeActivity = Observability.YarpActivitySource.StartActivity("proxy.destination_health_check", ActivityKind.Client); probeActivity?.AddTag("proxy.cluster_id", cluster.ClusterId); probeActivity?.AddTag("proxy.destination_id", destination.DestinationId); using var cts = new CancellationTokenSource(timeout); HttpRequestMessage request; try { request = await _probingRequestFactory.CreateRequestAsync(cluster, destination, cts.Token); } catch (OperationCanceledException oce) when (!cts.IsCancellationRequested) { Log.ActiveHealthProbeCancelledOnDestination(_logger, destination.DestinationId, cluster.ClusterId); return new DestinationProbingResult(destination, null, oce); } catch (Exception ex) { Log.ActiveHealthProbeConstructionFailedOnCluster(_logger, destination.DestinationId, cluster.ClusterId, ex); probeActivity?.SetStatus(ActivityStatusCode.Error); return new DestinationProbingResult(destination, null, ex); } try { Log.SendingHealthProbeToEndpointOfDestination(_logger, request.RequestUri, destination.DestinationId, cluster.ClusterId); var response = await cluster.Model.HttpClient.SendAsync(request, cts.Token); Log.DestinationProbingCompleted(_logger, destination.DestinationId, cluster.ClusterId, (int)response.StatusCode); probeActivity?.SetStatus(ActivityStatusCode.Ok); return new DestinationProbingResult(destination, response, null); } catch (OperationCanceledException oce) when (!cts.IsCancellationRequested) { Log.ActiveHealthProbeCancelledOnDestination(_logger, destination.DestinationId, cluster.ClusterId); return new DestinationProbingResult(destination, null, oce); } catch (Exception ex) { Log.DestinationProbingFailed(_logger, destination.DestinationId, cluster.ClusterId, ex); probeActivity?.SetStatus(ActivityStatusCode.Error); return new DestinationProbingResult(destination, null, ex); } } } ================================================ FILE: src/ReverseProxy/Health/ActiveHealthCheckMonitorOptions.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; namespace Yarp.ReverseProxy.Health; /// /// Defines options for the active health check monitor. /// public class ActiveHealthCheckMonitorOptions { /// /// Default probing interval. /// public TimeSpan DefaultInterval { get; set; } = TimeSpan.FromSeconds(15); /// /// Default probes timeout. /// public TimeSpan DefaultTimeout { get; set; } = TimeSpan.FromSeconds(10); } ================================================ FILE: src/ReverseProxy/Health/AppBuilderHealthExtensions.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Yarp.ReverseProxy.Health; namespace Microsoft.AspNetCore.Builder; /// /// Extensions for adding proxy middleware to the pipeline. /// public static class AppBuilderHealthExtensions { /// /// Passively checks destinations health by watching for successes and failures in client request proxying. /// public static IReverseProxyApplicationBuilder UsePassiveHealthChecks(this IReverseProxyApplicationBuilder builder) { builder.UseMiddleware(); return builder; } } ================================================ FILE: src/ReverseProxy/Health/ClusterDestinationsUpdater.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Frozen; using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Threading; using Yarp.ReverseProxy.Model; using Yarp.ReverseProxy.Utilities; namespace Yarp.ReverseProxy.Health; internal sealed class ClusterDestinationsUpdater : IClusterDestinationsUpdater { private readonly ConditionalWeakTable _clusterLocks = new ConditionalWeakTable(); private readonly FrozenDictionary _destinationPolicies; public ClusterDestinationsUpdater(IEnumerable destinationPolicies) { ArgumentNullException.ThrowIfNull(destinationPolicies); _destinationPolicies = destinationPolicies.ToDictionaryByUniqueId(p => p.Name); } public void UpdateAvailableDestinations(ClusterState cluster) { var allDestinations = cluster.DestinationsState?.AllDestinations; if (allDestinations is null) { throw new InvalidOperationException($"{nameof(UpdateAllDestinations)} must be called first."); } UpdateInternal(cluster, allDestinations, force: false); } public void UpdateAllDestinations(ClusterState cluster) { // Values already makes a copy of the collection, downcast to avoid making a second copy. // https://github.com/dotnet/runtime/blob/e164551f1c96138521b4e58f14f8ac1e4369005d/src/libraries/System.Collections.Concurrent/src/System/Collections/Concurrent/ConcurrentDictionary.cs#L2145-L2168 var allDestinations = (IReadOnlyList)cluster.Destinations.Values; UpdateInternal(cluster, allDestinations, force: true); } private void UpdateInternal(ClusterState cluster, IReadOnlyList allDestinations, bool force) { // Prevent overlapping updates and debounce extra concurrent calls. // If there are multiple concurrent calls to rebuild the dynamic state, we want to ensure that // updates don't conflict with each other. Additionally, we debounce extra concurrent calls if // they arrive in a quick succession to avoid spending too much CPU on frequent state rebuilds. // Specifically, only up to two threads are allowed to wait here and actually execute a rebuild, // all others will be debounced and the call will return without updating the ClusterState.DestinationsState. // However, changes made by those debounced threads (e.g. destination health updates) will be // taken into account by one of blocked threads after they get unblocked to run a rebuild. var updateLock = _clusterLocks.GetValue(cluster, _ => new SemaphoreSlim(2)); var lockTaken = false; if (force) { lockTaken = true; updateLock.Wait(); } else { lockTaken = updateLock.Wait(0); } if (!lockTaken) { return; } lock (updateLock) { try { var config = cluster.Model.Config; var destinationPolicy = _destinationPolicies.GetRequiredServiceById( config.HealthCheck?.AvailableDestinationsPolicy, HealthCheckConstants.AvailableDestinations.HealthyOrPanic); var availableDestinations = destinationPolicy.GetAvailableDestinations(config, allDestinations); cluster.DestinationsState = new ClusterDestinationsState(allDestinations, availableDestinations); } finally { // Semaphore is released while still holding the lock to AVOID the following case. // The first thread (T1) finished a rebuild and left the lock while still holding the semaphore. The second thread (T2) // waiting on the lock gets awaken, proceeds under the lock and begins the next rebuild. If at this exact moment // the third thread (T3) enters this method and tries to acquire the semaphore, it will be debounced because // the semaphore's count is still 0. However, T2 could have already made some progress and didn't observe updates made // by T3. // By releasing the semaphore under the lock, we make sure that in the above situation T3 will proceed till the lock and // its updates will be observed anyway. updateLock.Release(); } } } } ================================================ FILE: src/ReverseProxy/Health/ConsecutiveFailuresHealthPolicy.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Globalization; using System.Net.Http; using System.Runtime.CompilerServices; using Microsoft.Extensions.Options; using Yarp.ReverseProxy.Model; using Yarp.ReverseProxy.Utilities; namespace Yarp.ReverseProxy.Health; internal sealed class ConsecutiveFailuresHealthPolicy : IActiveHealthCheckPolicy { private readonly ConsecutiveFailuresHealthPolicyOptions _options; private readonly ConditionalWeakTable> _clusterThresholds = new ConditionalWeakTable>(); private readonly ConditionalWeakTable _failureCounters = new ConditionalWeakTable(); private readonly IDestinationHealthUpdater _healthUpdater; public string Name => HealthCheckConstants.ActivePolicy.ConsecutiveFailures; public ConsecutiveFailuresHealthPolicy(IOptions options, IDestinationHealthUpdater healthUpdater) { ArgumentNullException.ThrowIfNull(options?.Value); ArgumentNullException.ThrowIfNull(healthUpdater); _options = options.Value; _healthUpdater = healthUpdater; } public void ProbingCompleted(ClusterState cluster, IReadOnlyList probingResults) { if (probingResults.Count == 0) { return; } var threshold = GetFailureThreshold(cluster); var newHealthStates = new NewActiveDestinationHealth[probingResults.Count]; for (var i = 0; i < probingResults.Count; i++) { var destination = probingResults[i].Destination; var previousState = destination.Health.Active; var count = _failureCounters.GetOrCreateValue(destination); var newHealth = EvaluateHealthState(threshold, probingResults[i].Response, count, previousState); newHealthStates[i] = new NewActiveDestinationHealth(destination, newHealth); } _healthUpdater.SetActive(cluster, newHealthStates); } private double GetFailureThreshold(ClusterState cluster) { var thresholdEntry = _clusterThresholds.GetValue(cluster, c => new ParsedMetadataEntry(TryParse, c, ConsecutiveFailuresHealthPolicyOptions.ThresholdMetadataName)); return thresholdEntry.GetParsedOrDefault(_options.DefaultThreshold); } private static DestinationHealth EvaluateHealthState(double threshold, HttpResponseMessage? response, AtomicCounter count, DestinationHealth previousState) { DestinationHealth newHealth; if (response is not null && response.IsSuccessStatusCode) { // Success count.Reset(); newHealth = DestinationHealth.Healthy; } else { // Failure var currentFailureCount = count.Increment(); newHealth = currentFailureCount < threshold ? previousState : DestinationHealth.Unhealthy; } return newHealth; } private static bool TryParse(string stringValue, out double parsedValue) { return double.TryParse(stringValue, NumberStyles.Float, CultureInfo.InvariantCulture, out parsedValue); } } ================================================ FILE: src/ReverseProxy/Health/ConsecutiveFailuresHealthPolicyOptions.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. namespace Yarp.ReverseProxy.Health; /// /// Defines options for the consecutive failures active health check policy. /// public class ConsecutiveFailuresHealthPolicyOptions { /// /// Name of the consecutive failure threshold metadata parameter. /// It's the number of consecutive failure that needs to happen in order to mark a destination as unhealthy. /// public static readonly string ThresholdMetadataName = "ConsecutiveFailuresHealthPolicy.Threshold"; /// /// Default consecutive failures threshold that is applied if it's not set on a cluster's metadata. /// public long DefaultThreshold { get; set; } = 2; } ================================================ FILE: src/ReverseProxy/Health/DefaultProbingRequestFactory.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Net; using System.Net.Http; using System.Reflection; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.Net.Http.Headers; using Yarp.ReverseProxy.Model; namespace Yarp.ReverseProxy.Health; internal sealed class DefaultProbingRequestFactory : IProbingRequestFactory { private static readonly string? _version = Assembly.GetExecutingAssembly().GetCustomAttribute()?.InformationalVersion; private static readonly string _defaultUserAgent = $"YARP{(string.IsNullOrEmpty(_version) ? "" : $"/{_version.Split('+')[0]}")} (Active Health Check Monitor)"; public HttpRequestMessage CreateRequest(ClusterModel cluster, DestinationModel destination) { var probeAddress = !string.IsNullOrEmpty(destination.Config.Health) ? destination.Config.Health : destination.Config.Address; var probePath = cluster.Config.HealthCheck?.Active?.Path; UriHelper.FromAbsolute(probeAddress, out var destinationScheme, out var destinationHost, out var destinationPathBase, out _, out _); var query = QueryString.FromUriComponent(cluster.Config.HealthCheck?.Active?.Query ?? ""); var probeUri = UriHelper.BuildAbsolute(destinationScheme, destinationHost, destinationPathBase, probePath, query); var request = new HttpRequestMessage(HttpMethod.Get, probeUri) { Version = cluster.Config.HttpRequest?.Version ?? HttpVersion.Version20, VersionPolicy = cluster.Config.HttpRequest?.VersionPolicy ?? HttpVersionPolicy.RequestVersionOrLower, }; if (!string.IsNullOrEmpty(destination.Config.Host)) { request.Headers.Add(HeaderNames.Host, destination.Config.Host); } request.Headers.Add(HeaderNames.UserAgent, _defaultUserAgent); return request; } public ValueTask CreateRequestAsync(ClusterState cluster, DestinationState destination, CancellationToken cancellationToken = default) => ValueTask.FromResult(CreateRequest(cluster.Model, destination.Model)); } ================================================ FILE: src/ReverseProxy/Health/DestinationHealthUpdater.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Yarp.ReverseProxy.Model; namespace Yarp.ReverseProxy.Health; internal sealed class DestinationHealthUpdater : IDestinationHealthUpdater, IDisposable { private readonly EntityActionScheduler<(ClusterState Cluster, DestinationState Destination)> _scheduler; private readonly IClusterDestinationsUpdater _clusterUpdater; private readonly ILogger _logger; public DestinationHealthUpdater( TimeProvider timeProvider, IClusterDestinationsUpdater clusterDestinationsUpdater, ILogger logger) { ArgumentNullException.ThrowIfNull(clusterDestinationsUpdater); ArgumentNullException.ThrowIfNull(logger); _scheduler = new EntityActionScheduler<(ClusterState Cluster, DestinationState Destination)>(d => Reactivate(d.Cluster, d.Destination), autoStart: true, runOnce: true, timeProvider); _clusterUpdater = clusterDestinationsUpdater; _logger = logger; } public void SetActive(ClusterState cluster, IEnumerable newHealthPairs) { var changed = false; foreach (var newHealthPair in newHealthPairs) { var destination = newHealthPair.Destination; var newHealth = newHealthPair.NewActiveHealth; var healthState = destination.Health; if (newHealth != healthState.Active) { healthState.Active = newHealth; changed = true; if (newHealth == DestinationHealth.Unhealthy) { Log.ActiveDestinationHealthStateIsSetToUnhealthy(_logger, destination.DestinationId, cluster.ClusterId); } else { Log.ActiveDestinationHealthStateIsSet(_logger, destination.DestinationId, cluster.ClusterId, newHealth); } } } if (changed) { _clusterUpdater.UpdateAvailableDestinations(cluster); } } public void SetPassive(ClusterState cluster, DestinationState destination, DestinationHealth newHealth, TimeSpan reactivationPeriod) { _ = SetPassiveAsync(cluster, destination, newHealth, reactivationPeriod); } internal Task SetPassiveAsync(ClusterState cluster, DestinationState destination, DestinationHealth newHealth, TimeSpan reactivationPeriod) { var healthState = destination.Health; if (newHealth != healthState.Passive) { healthState.Passive = newHealth; ScheduleReactivation(cluster, destination, newHealth, reactivationPeriod); return Task.Factory.StartNew(c => UpdateDestinations(c!), cluster, CancellationToken.None, TaskCreationOptions.RunContinuationsAsynchronously, TaskScheduler.Default); } return Task.CompletedTask; } private void UpdateDestinations(object cluster) { _clusterUpdater.UpdateAvailableDestinations((ClusterState)cluster); } private void ScheduleReactivation(ClusterState cluster, DestinationState destination, DestinationHealth newHealth, TimeSpan reactivationPeriod) { if (newHealth == DestinationHealth.Unhealthy) { _scheduler.ScheduleEntity((cluster, destination), reactivationPeriod); Log.UnhealthyDestinationIsScheduledForReactivation(_logger, destination.DestinationId, reactivationPeriod); } } public void Dispose() { _scheduler.Dispose(); } private Task Reactivate(ClusterState cluster, DestinationState destination) { var healthState = destination.Health; if (healthState.Passive == DestinationHealth.Unhealthy) { healthState.Passive = DestinationHealth.Unknown; Log.PassiveDestinationHealthResetToUnknownState(_logger, destination.DestinationId); _clusterUpdater.UpdateAvailableDestinations(cluster); } return Task.CompletedTask; } private static class Log { private static readonly Action _unhealthyDestinationIsScheduledForReactivation = LoggerMessage.Define( LogLevel.Warning, EventIds.UnhealthyDestinationIsScheduledForReactivation, "Destination `{destinationId}` marked as 'Unhealthy` by the passive health check is scheduled for a reactivation in `{reactivationPeriod}`."); private static readonly Action _passiveDestinationHealthResetToUnknownState = LoggerMessage.Define( LogLevel.Information, EventIds.PassiveDestinationHealthResetToUnknownState, "Passive health state of the destination `{destinationId}` is reset to 'Unknown`."); private static readonly Action _activeDestinationHealthStateIsSetToUnhealthy = LoggerMessage.Define( LogLevel.Warning, EventIds.ActiveDestinationHealthStateIsSetToUnhealthy, "Active health state of destination `{destinationId}` on cluster `{clusterId}` is set to 'Unhealthy'."); private static readonly Action _activeDestinationHealthStateIsSet = LoggerMessage.Define( LogLevel.Information, EventIds.ActiveDestinationHealthStateIsSet, "Active health state of destination `{destinationId}` on cluster `{clusterId}` is set to '{newHealthState}'."); public static void ActiveDestinationHealthStateIsSetToUnhealthy(ILogger logger, string destinationId, string clusterId) { _activeDestinationHealthStateIsSetToUnhealthy(logger, destinationId, clusterId, null); } public static void ActiveDestinationHealthStateIsSet(ILogger logger, string destinationId, string clusterId, DestinationHealth newHealthState) { _activeDestinationHealthStateIsSet(logger, destinationId, clusterId, newHealthState, null); } public static void UnhealthyDestinationIsScheduledForReactivation(ILogger logger, string destinationId, TimeSpan reactivationPeriod) { _unhealthyDestinationIsScheduledForReactivation(logger, destinationId, reactivationPeriod, null); } public static void PassiveDestinationHealthResetToUnknownState(ILogger logger, string destinationId) { _passiveDestinationHealthResetToUnknownState(logger, destinationId, null); } } } ================================================ FILE: src/ReverseProxy/Health/DestinationProbingResult.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Net.Http; using Yarp.ReverseProxy.Model; namespace Yarp.ReverseProxy.Health; /// /// Result of a destination's active health probing. /// public readonly struct DestinationProbingResult { public DestinationProbingResult(DestinationState destination, HttpResponseMessage? response, Exception? exception) { ArgumentNullException.ThrowIfNull(destination); Destination = destination; Response = response; Exception = exception; } /// /// Probed destination. /// public DestinationState Destination { get; } /// /// Response received. /// It can be null in case of a failure. /// public HttpResponseMessage? Response { get; } /// /// Exception thrown during probing. /// It is null in case of a success. /// public Exception? Exception { get; } } ================================================ FILE: src/ReverseProxy/Health/EntityActionScheduler.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using Yarp.ReverseProxy.Utilities; namespace Yarp.ReverseProxy.Health; /// /// Periodically invokes specified actions on registered entities. /// /// /// It creates a separate for each registration which is considered /// reasonably efficient because .NET already maintains a process-wide managed timer queue. /// There are 2 scheduling modes supported: run once and infinite run. In "run once" mode, /// an entity gets unscheduled after the respective timer fired for the first time whereas /// in "infinite run" entities get repeatedly rescheduled until either they are explicitly removed /// or the instance is disposed. /// internal sealed class EntityActionScheduler : IDisposable where T : notnull { private readonly ConcurrentDictionary _entries = new(); private readonly WeakReference> _weakThisRef; private readonly Func _action; private readonly bool _runOnce; private readonly TimeProvider _timeProvider; private const int NotStarted = 0; private const int Started = 1; private const int Disposed = 2; private int _status; public EntityActionScheduler(Func action, bool autoStart, bool runOnce, TimeProvider timeProvider) { ArgumentNullException.ThrowIfNull(action); ArgumentNullException.ThrowIfNull(timeProvider); _action = action; _runOnce = runOnce; _timeProvider = timeProvider; _status = autoStart ? Started : NotStarted; _weakThisRef = new WeakReference>(this); } public void Dispose() { Volatile.Write(ref _status, Disposed); foreach (var entry in _entries.Values) { entry.Dispose(); } } public void Start() { if (Interlocked.CompareExchange(ref _status, Started, NotStarted) != NotStarted) { return; } foreach (var entry in _entries.Values) { entry.EnsureStarted(); } } public void ScheduleEntity(T entity, TimeSpan period) { // Ensure the Timer has a weak reference to this scheduler; otherwise, // EntityActionScheduler can be rooted by the Timer implementation. var entry = new SchedulerEntry(_weakThisRef, entity, period, _timeProvider); if (_entries.TryAdd(entity, entry)) { // Scheduler could have been started while we were adding the new entry. // Start timer here to ensure it's not forgotten. if (Volatile.Read(ref _status) == Started) { entry.EnsureStarted(); } } else { entry.Dispose(); } } public void ChangePeriod(T entity, TimeSpan newPeriod) { Debug.Assert(!_runOnce, "Calling ChangePeriod on a RunOnce scheduler may cause the callback to fire twice"); if (_entries.TryGetValue(entity, out var entry)) { entry.ChangePeriod(newPeriod); } else { ScheduleEntity(entity, newPeriod); } } public void UnscheduleEntity(T entity) { if (_entries.TryRemove(entity, out var entry)) { entry.Dispose(); } } public bool IsScheduled(T entity) { return _entries.ContainsKey(entity); } private sealed class SchedulerEntry : IDisposable { private readonly WeakReference> _scheduler; private readonly T _entity; private readonly ITimer _timer; private TimeSpan _period; private bool _timerStarted; private bool _runningCallback; public SchedulerEntry(WeakReference> scheduler, T entity, TimeSpan period, TimeProvider timeProvider) { _scheduler = scheduler; _entity = entity; _period = period; // Don't capture the current ExecutionContext and its AsyncLocals onto the timer causing them to live forever var restoreFlow = false; try { if (!ExecutionContext.IsFlowSuppressed()) { ExecutionContext.SuppressFlow(); restoreFlow = true; } _timer = timeProvider.CreateTimer(static s => _ = TimerCallback(s), this, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan); } finally { if (restoreFlow) { ExecutionContext.RestoreFlow(); } } } public void ChangePeriod(TimeSpan newPeriod) { lock (this) { _period = newPeriod; if (_timerStarted && !_runningCallback) { SetTimer(); } } } public void EnsureStarted() { lock (this) { if (!_timerStarted) { SetTimer(); } } } private void SetTimer() { Debug.Assert(Monitor.IsEntered(this)); Debug.Assert(!_runningCallback); _timerStarted = true; try { _timer.Change(_period, Timeout.InfiniteTimeSpan); } catch (ObjectDisposedException) { // It can be thrown if the timer has been already disposed. // Just suppress it. } } public void Dispose() { _timer.Dispose(); } // Timer.Change is racy as the callback could already be scheduled while we are starting the timer again. // Avoid running the callback multiple times concurrently by using the _runningCallback flag. private static async Task TimerCallback(object? state) { var entry = (SchedulerEntry)state!; lock (entry) { if (entry._runningCallback) { return; } entry._runningCallback = true; } if (!entry._scheduler.TryGetTarget(out var scheduler)) { return; } var pair = new KeyValuePair(entry._entity, entry); if (scheduler._runOnce && scheduler._entries.TryRemove(pair)) { entry.Dispose(); } try { await scheduler._action(entry._entity); if (!scheduler._runOnce && scheduler._entries.Contains(pair)) { // This entry has not been unscheduled - set the timer again lock (entry) { entry._runningCallback = false; entry.SetTimer(); } } } catch (Exception ex) { // We are running on the ThreadPool, don't propagate exceptions Debug.Fail(ex.ToString()); // TODO: Log if (scheduler._entries.TryRemove(pair)) { entry.Dispose(); } return; } } } } ================================================ FILE: src/ReverseProxy/Health/HealthCheckConstants.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. namespace Yarp.ReverseProxy.Health; public static class HealthCheckConstants { public static class PassivePolicy { public static readonly string TransportFailureRate = nameof(TransportFailureRate); } public static class ActivePolicy { public static readonly string ConsecutiveFailures = nameof(ConsecutiveFailures); } public static class AvailableDestinations { /// /// Marks destination as available for proxying requests to if its health state /// is either 'Healthy' or 'Unknown'. If no destinations are available then /// requests will get a 503 error. /// /// It applies only if active or passive health checks are enabled. public static readonly string HealthyAndUnknown = nameof(HealthyAndUnknown); /// /// Calls policy at first to determine /// destinations' availability. If no available destinations are returned /// from this call, it marks all cluster's destination as available. /// public static readonly string HealthyOrPanic = nameof(HealthyOrPanic); } } ================================================ FILE: src/ReverseProxy/Health/HealthyAndUnknownDestinationsPolicy.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; using System.Linq; using Yarp.ReverseProxy.Configuration; using Yarp.ReverseProxy.Model; namespace Yarp.ReverseProxy.Health; // Policy marking destinations as available only if their active and passive health states /// are either 'Healthy' or 'Unknown'/>. internal class HealthyAndUnknownDestinationsPolicy : IAvailableDestinationsPolicy { public virtual string Name => HealthCheckConstants.AvailableDestinations.HealthyAndUnknown; public virtual IReadOnlyList GetAvailableDestinations(ClusterConfig config, IReadOnlyList allDestinations) { var availableDestinations = allDestinations; var activeEnabled = (config.HealthCheck?.Active?.Enabled).GetValueOrDefault(); var passiveEnabled = (config.HealthCheck?.Passive?.Enabled).GetValueOrDefault(); if (activeEnabled || passiveEnabled) { availableDestinations = allDestinations.Where(destination => { // Only consider the current state if those checks are enabled. var healthState = destination.Health; var active = activeEnabled ? healthState.Active : DestinationHealth.Unknown; var passive = passiveEnabled ? healthState.Passive : DestinationHealth.Unknown; return active != DestinationHealth.Unhealthy && passive != DestinationHealth.Unhealthy; }).ToList(); } return availableDestinations; } } ================================================ FILE: src/ReverseProxy/Health/HealthyOrPanicDestinationsPolicy.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; using Yarp.ReverseProxy.Configuration; using Yarp.ReverseProxy.Model; namespace Yarp.ReverseProxy.Health; internal sealed class HealthyOrPanicDestinationsPolicy : HealthyAndUnknownDestinationsPolicy { public override string Name => HealthCheckConstants.AvailableDestinations.HealthyOrPanic; public override IReadOnlyList GetAvailableDestinations(ClusterConfig config, IReadOnlyList allDestinations) { var availableDestinations = base.GetAvailableDestinations(config, allDestinations); return availableDestinations.Count > 0 ? availableDestinations : allDestinations; } } ================================================ FILE: src/ReverseProxy/Health/IActiveHealthCheckMonitor.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; using System.Threading.Tasks; using Yarp.ReverseProxy.Model; namespace Yarp.ReverseProxy.Health; /// /// Actively monitors destinations health. /// public interface IActiveHealthCheckMonitor { /// /// Gets a value that determines whether the initial round of active health checks have run, regardless of the results. /// /// /// false until the initial round of health check requests has been processed. /// true when all the initially configured destinations have been queried, regardless their availability or returned status code. /// The property stays true for the rest of the proxy process lifetime. /// bool InitialProbeCompleted { get; } /// /// Checks health of all clusters' destinations. /// /// Clusters to check the health of their destinations. /// representing the health check process. Task CheckHealthAsync(IEnumerable clusters); } ================================================ FILE: src/ReverseProxy/Health/IActiveHealthCheckPolicy.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; using Yarp.ReverseProxy.Model; namespace Yarp.ReverseProxy.Health; /// /// Active health check evaluation policy. /// public interface IActiveHealthCheckPolicy { /// /// Policy's name. /// string Name { get; } /// /// Analyzes results of active health probes sent to destinations and calculates their new health states. /// /// Cluster. /// Destination probing results. void ProbingCompleted(ClusterState cluster, IReadOnlyList probingResults); } ================================================ FILE: src/ReverseProxy/Health/IAvailableDestinationsPolicy.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; using Yarp.ReverseProxy.Configuration; using Yarp.ReverseProxy.Model; namespace Yarp.ReverseProxy.Health; /// /// Policy evaluating which destinations should be available for proxying requests to. /// public interface IAvailableDestinationsPolicy { /// /// Policy name. /// string Name { get; } /// /// Reviews all given destinations and returns the ones available for proxying requests to. /// /// Target cluster. /// All destinations configured for the target cluster. /// IReadOnlyList GetAvailableDestinations(ClusterConfig config, IReadOnlyList allDestinations); } ================================================ FILE: src/ReverseProxy/Health/IClusterDestinationsUpdater.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Yarp.ReverseProxy.Model; namespace Yarp.ReverseProxy.Health; /// /// Updates the cluster's destination collections. /// public interface IClusterDestinationsUpdater { /// /// Updates the cluster's collection of destination available for proxying requests to. /// Call this if health state has changed for any destinations. /// /// The owing the destinations. void UpdateAvailableDestinations(ClusterState cluster); /// /// Updates the cluster's collection of all configured destinations. /// Call this after destinations have been added, removed, or their configuration changed. /// This does not need to be called for state updates like health, use UpdateAvailableDestinations for state updates. /// /// The owing the destinations. void UpdateAllDestinations(ClusterState cluster); } ================================================ FILE: src/ReverseProxy/Health/IDestinationHealthUpdater.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using Yarp.ReverseProxy.Model; namespace Yarp.ReverseProxy.Health; /// /// Updates destinations' health states when it's requested by a health check policy /// while taking into account not only the new evaluated value but also the overall current cluster's health state. /// public interface IDestinationHealthUpdater { /// /// Sets the passive health on the given . /// /// Cluster. /// Destination. /// New passive health value. /// If is , /// this parameter specifies a reactivation period after which the destination's value /// will be reset to . Otherwise, it's not used. void SetPassive(ClusterState cluster, DestinationState destination, DestinationHealth newHealth, TimeSpan reactivationPeriod); /// /// Sets the active health values on the given destinations. /// /// Cluster. /// New active health states. void SetActive(ClusterState cluster, IEnumerable newHealthStates); } ================================================ FILE: src/ReverseProxy/Health/IPassiveHealthCheckPolicy.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Http; using Yarp.ReverseProxy.Model; namespace Yarp.ReverseProxy.Health; /// /// Passive health check evaluation policy. /// public interface IPassiveHealthCheckPolicy { /// /// Policy's name. /// string Name { get; } /// /// Registers a successful or failed request and evaluates a new value. /// /// Context. /// Request's cluster. /// Request's destination. void RequestProxied(HttpContext context, ClusterState cluster, DestinationState destination); } ================================================ FILE: src/ReverseProxy/Health/IProbingRequestFactory.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Yarp.ReverseProxy.Model; namespace Yarp.ReverseProxy.Health; /// /// A factory for creating s for active health probes to be sent to destinations. /// public interface IProbingRequestFactory { /// /// Creates a probing request. /// /// The cluster being probed. /// The destination being probed. /// Probing . HttpRequestMessage CreateRequest(ClusterModel cluster, DestinationModel destination); /// /// Creates a probing request. /// /// The cluster being probed. /// The destination being probed. /// A token to cancel the operation. /// Probing . ValueTask CreateRequestAsync(ClusterState cluster, DestinationState destination, CancellationToken cancellationToken = default) => ValueTask.FromResult(CreateRequest(cluster.Model, destination.Model)); } ================================================ FILE: src/ReverseProxy/Health/NewActiveDestinationHealth.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Yarp.ReverseProxy.Model; namespace Yarp.ReverseProxy.Health; /// /// Stores a new active health state for the given destination. /// public readonly struct NewActiveDestinationHealth { public NewActiveDestinationHealth(DestinationState destination, DestinationHealth newActiveHealth) { Destination = destination; NewActiveHealth = newActiveHealth; } public DestinationState Destination { get; } public DestinationHealth NewActiveHealth { get; } } ================================================ FILE: src/ReverseProxy/Health/PassiveHealthCheckMiddleware.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Frozen; using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Yarp.ReverseProxy.Utilities; namespace Yarp.ReverseProxy.Health; public class PassiveHealthCheckMiddleware { private readonly RequestDelegate _next; private readonly FrozenDictionary _policies; public PassiveHealthCheckMiddleware(RequestDelegate next, IEnumerable policies) { ArgumentNullException.ThrowIfNull(next); ArgumentNullException.ThrowIfNull(policies); _next = next; _policies = policies.ToDictionaryByUniqueId(p => p.Name); } public async Task Invoke(HttpContext context) { await _next(context); var proxyFeature = context.GetReverseProxyFeature(); var options = proxyFeature.Cluster.Config.HealthCheck?.Passive; // Do nothing if no target destination has been chosen for the request. if (options is null || !options.Enabled.GetValueOrDefault() || proxyFeature.ProxiedDestination is null) { return; } var policy = _policies.GetRequiredServiceById(options.Policy, HealthCheckConstants.PassivePolicy.TransportFailureRate); var cluster = context.GetRouteModel().Cluster!; policy.RequestProxied(context, cluster, proxyFeature.ProxiedDestination); } } ================================================ FILE: src/ReverseProxy/Health/TransportFailureRateHealthPolicy.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Globalization; using System.Runtime.CompilerServices; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Options; using Yarp.ReverseProxy.Model; using Yarp.ReverseProxy.Forwarder; using Yarp.ReverseProxy.Utilities; namespace Yarp.ReverseProxy.Health; /// /// Calculates the proxied requests failure rate for each destination and marks it as unhealthy if the specified limit is exceeded. /// /// /// Rate is calculated as a percentage of failed requests to the total number of request proxied to a destination in the given period of time. Failed and total counters are tracked /// in a sliding time window which means that only the recent readings fitting in the window are taken into account. The window is implemented as a linked-list of timestamped records /// where each record contains the difference from the previous one in the number of failed and total requests. Additionally, there are 2 destination-wide counters storing aggregated values /// to enable a fast calculation of the current failure rate. When a new proxied request is reported, its status firstly affects those 2 aggregated counters and then also gets put /// in the record history. Once some record moves out of the detection time window, the failed and total counter deltas stored on it get subtracted from the respective aggregated counters. /// internal sealed class TransportFailureRateHealthPolicy : IPassiveHealthCheckPolicy { private static readonly TimeSpan _defaultReactivationPeriod = TimeSpan.FromSeconds(60); private readonly IDestinationHealthUpdater _healthUpdater; private readonly TransportFailureRateHealthPolicyOptions _policyOptions; private readonly TimeProvider _timeProvider; private readonly ConditionalWeakTable> _clusterFailureRateLimits = new ConditionalWeakTable>(); private readonly ConditionalWeakTable _requestHistories = new ConditionalWeakTable(); public string Name => HealthCheckConstants.PassivePolicy.TransportFailureRate; public TransportFailureRateHealthPolicy( IOptions policyOptions, TimeProvider timeProvider, IDestinationHealthUpdater healthUpdater) { ArgumentNullException.ThrowIfNull(timeProvider); ArgumentNullException.ThrowIfNull(policyOptions?.Value); ArgumentNullException.ThrowIfNull(healthUpdater); _timeProvider = timeProvider; _policyOptions = policyOptions.Value; _healthUpdater = healthUpdater; } public void RequestProxied(HttpContext context, ClusterState cluster, DestinationState destination) { var newHealth = EvaluateProxiedRequest(cluster, destination, DetermineIfDestinationFailed(context)); var clusterReactivationPeriod = cluster.Model.Config.HealthCheck?.Passive?.ReactivationPeriod ?? _defaultReactivationPeriod; // Avoid reactivating until the history has expired so that it does not affect future health assessments. var reactivationPeriod = clusterReactivationPeriod >= _policyOptions.DetectionWindowSize ? clusterReactivationPeriod : _policyOptions.DetectionWindowSize; _healthUpdater.SetPassive(cluster, destination, newHealth, reactivationPeriod); } private DestinationHealth EvaluateProxiedRequest(ClusterState cluster, DestinationState destination, bool failed) { var history = _requestHistories.GetOrCreateValue(destination); var rateLimitEntry = _clusterFailureRateLimits.GetValue(cluster, c => new ParsedMetadataEntry(TryParse, c, TransportFailureRateHealthPolicyOptions.FailureRateLimitMetadataName)); var rateLimit = rateLimitEntry.GetParsedOrDefault(_policyOptions.DefaultFailureRateLimit); lock (history) { var failureRate = history.AddNew( _timeProvider, _policyOptions.DetectionWindowSize, _policyOptions.MinimalTotalCountThreshold, failed); return failureRate < rateLimit ? DestinationHealth.Healthy : DestinationHealth.Unhealthy; } } private static bool TryParse(string stringValue, out double parsedValue) { return double.TryParse(stringValue, NumberStyles.Float, CultureInfo.InvariantCulture, out parsedValue); } private static bool DetermineIfDestinationFailed(HttpContext context) { var errorFeature = context.Features.Get(); if (errorFeature is null) { return false; } if (context.RequestAborted.IsCancellationRequested) { // The client disconnected/canceled the request - the failure may not be the destination's fault return false; } var error = errorFeature.Error; return error == ForwarderError.Request || error == ForwarderError.RequestTimedOut || error == ForwarderError.RequestBodyDestination || error == ForwarderError.ResponseBodyDestination || error == ForwarderError.UpgradeRequestDestination || error == ForwarderError.UpgradeResponseDestination; } private sealed class ProxiedRequestHistory { private long _nextRecordCreatedAt; private long _nextRecordTotalCount; private long _nextRecordFailedCount; private long _failedCount; private double _totalCount; private readonly Queue _records = new Queue(); public double AddNew(TimeProvider timeProvider, TimeSpan detectionWindowSize, int totalCountThreshold, bool failed) { var eventTime = timeProvider.GetTimestamp(); var detectionWindowSizeLong = detectionWindowSize.TotalSeconds * timeProvider.TimestampFrequency; if (_nextRecordCreatedAt == 0) { // Initialization. _nextRecordCreatedAt = eventTime + timeProvider.TimestampFrequency; } // Don't create a new record on each event because it can negatively affect performance. // Instead, accumulate failed and total request counts reported during some period // and then add only one record storing them. if (eventTime >= _nextRecordCreatedAt) { _records.Enqueue(new HistoryRecord(_nextRecordCreatedAt, _nextRecordTotalCount, _nextRecordFailedCount)); _nextRecordCreatedAt = eventTime + timeProvider.TimestampFrequency; _nextRecordTotalCount = 0; _nextRecordFailedCount = 0; } _nextRecordTotalCount++; _totalCount++; if (failed) { _failedCount++; _nextRecordFailedCount++; } while (_records.Count > 0 && (eventTime - _records.Peek().RecordedAt > detectionWindowSizeLong)) { var removed = _records.Dequeue(); _failedCount -= removed.FailedCount; _totalCount -= removed.TotalCount; } return _totalCount < totalCountThreshold || _totalCount == 0 ? 0.0 : _failedCount / _totalCount; } private readonly struct HistoryRecord { public HistoryRecord(long recordedAt, long totalCount, long failedCount) { RecordedAt = recordedAt; TotalCount = totalCount; FailedCount = failedCount; } public long RecordedAt { get; } public long TotalCount { get; } public long FailedCount { get; } } } } ================================================ FILE: src/ReverseProxy/Health/TransportFailureRateHealthPolicyOptions.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; namespace Yarp.ReverseProxy.Health; /// /// Defines options for the transport failure rate passive health policy. /// public class TransportFailureRateHealthPolicyOptions { /// /// Name of failure rate limit metadata parameter. Destination marked as unhealthy once this limit is reached. /// public static readonly string FailureRateLimitMetadataName = "TransportFailureRateHealthPolicy.RateLimit"; /// /// Period of time while detected failures are kept and taken into account in the rate calculation. /// The default is 60 seconds. /// public TimeSpan DetectionWindowSize { get; set; } = TimeSpan.FromSeconds(60); /// /// Minimal total number of requests which must be proxied to a destination within the detection window /// before this policy starts evaluating the destination's health and enforcing the failure rate limit. /// The default is 10. /// public int MinimalTotalCountThreshold { get; set; } = 10; /// /// Default failure rate limit for a destination to be marked as unhealthy that is applied if it's not set on a cluster's metadata. /// It's calculated as a percentage of failed requests out of all requests proxied to the same destination in the period. /// The value is in range (0,1). The default is 0.3 (30%). /// public double DefaultFailureRateLimit { get; set; } = 0.3; } ================================================ FILE: src/ReverseProxy/Limits/LimitsMiddleware.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.Logging; namespace Yarp.ReverseProxy.Limits; /// /// Updates request limits based on route config. This is implemented as middleware at the end of the proxy /// pipeline so that apps can call ReassignProxyRequest to move the request to a different route before limits are applied. While similar to a proposed aspnetcore feature (https://github.com/dotnet/aspnetcore/issues/40452), /// the possibility of reassigning routes means we need to apply this limit very late. Trying to apply it twice could /// result in unexpected behavior like being unable to set it back to the server default. /// internal sealed class LimitsMiddleware { private readonly RequestDelegate _next; private readonly ILogger _logger; public LimitsMiddleware(RequestDelegate next, ILogger logger) { ArgumentNullException.ThrowIfNull(next); ArgumentNullException.ThrowIfNull(logger); _next = next; _logger = logger; } /// public Task Invoke(HttpContext context) { ArgumentNullException.ThrowIfNull(context); var config = context.GetRouteModel().Config; if (config.MaxRequestBodySize.HasValue) { var sizeFeature = context.Features.Get(); if (sizeFeature != null && !sizeFeature.IsReadOnly) { // -1 for disabled var limit = config.MaxRequestBodySize.Value; long? newValue = limit == -1 ? null : limit; sizeFeature.MaxRequestBodySize = newValue; Log.MaxRequestBodySizeSet(_logger, limit); } } return _next(context); } private static class Log { private static readonly Action _maxRequestBodySizeSet = LoggerMessage.Define( LogLevel.Debug, EventIds.MaxRequestBodySizeSet, "The MaxRequestBodySize has been set to '{limit}'."); public static void MaxRequestBodySizeSet(ILogger logger, long? limit) { _maxRequestBodySizeSet(logger, limit, null); } } } ================================================ FILE: src/ReverseProxy/LoadBalancing/AppBuilderLoadBalancingExtensions.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Yarp.ReverseProxy.LoadBalancing; namespace Microsoft.AspNetCore.Builder; /// /// Extensions for adding proxy middleware to the pipeline. /// public static class AppBuilderLoadBalancingExtensions { /// /// Load balances across the available endpoints. /// public static IReverseProxyApplicationBuilder UseLoadBalancing(this IReverseProxyApplicationBuilder builder) { builder.UseMiddleware(); return builder; } } ================================================ FILE: src/ReverseProxy/LoadBalancing/FirstLoadBalancingPolicy.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using Microsoft.AspNetCore.Http; using Yarp.ReverseProxy.Model; namespace Yarp.ReverseProxy.LoadBalancing; /// /// Select the alphabetically first available destination without considering load. This is useful for dual destination fail-over systems. /// internal sealed class FirstLoadBalancingPolicy : ILoadBalancingPolicy { public string Name => LoadBalancingPolicies.FirstAlphabetical; public DestinationState? PickDestination(HttpContext context, ClusterState cluster, IReadOnlyList availableDestinations) { if (availableDestinations.Count == 0) { return null; } var selectedDestination = availableDestinations[0]; for (var i = 1; i < availableDestinations.Count; i++) { var destination = availableDestinations[i]; if (string.Compare(selectedDestination.DestinationId, destination.DestinationId, StringComparison.OrdinalIgnoreCase) > 0) { selectedDestination = destination; } } return selectedDestination; } } ================================================ FILE: src/ReverseProxy/LoadBalancing/ILoadBalancingPolicy.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; using Microsoft.AspNetCore.Http; using Yarp.ReverseProxy.Model; namespace Yarp.ReverseProxy.LoadBalancing; /// /// Provides a method that applies a load balancing policy /// to select a destination. /// public interface ILoadBalancingPolicy { /// /// A unique identifier for this load balancing policy. This will be referenced from config. /// string Name { get; } /// /// Picks a destination to send traffic to. /// DestinationState? PickDestination(HttpContext context, ClusterState cluster, IReadOnlyList availableDestinations); } ================================================ FILE: src/ReverseProxy/LoadBalancing/LeastRequestsLoadBalancingPolicy.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; using Microsoft.AspNetCore.Http; using Yarp.ReverseProxy.Model; namespace Yarp.ReverseProxy.LoadBalancing; internal sealed class LeastRequestsLoadBalancingPolicy : ILoadBalancingPolicy { public string Name => LoadBalancingPolicies.LeastRequests; public DestinationState? PickDestination(HttpContext context, ClusterState cluster, IReadOnlyList availableDestinations) { if (availableDestinations.Count == 0) { return null; } var destinationCount = availableDestinations.Count; var leastRequestsDestination = availableDestinations[0]; var leastRequestsCount = leastRequestsDestination.ConcurrentRequestCount; for (var i = 1; i < destinationCount; i++) { var destination = availableDestinations[i]; var endpointRequestCount = destination.ConcurrentRequestCount; if (endpointRequestCount < leastRequestsCount) { leastRequestsDestination = destination; leastRequestsCount = endpointRequestCount; } } return leastRequestsDestination; } } ================================================ FILE: src/ReverseProxy/LoadBalancing/LoadBalancingMiddleware.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Frozen; using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Yarp.ReverseProxy.Model; using Yarp.ReverseProxy.Utilities; namespace Yarp.ReverseProxy.LoadBalancing; /// /// Load balances across the available destinations. /// internal sealed class LoadBalancingMiddleware { private readonly ILogger _logger; private readonly FrozenDictionary _loadBalancingPolicies; private readonly RequestDelegate _next; public LoadBalancingMiddleware( RequestDelegate next, ILogger logger, IEnumerable loadBalancingPolicies) { ArgumentNullException.ThrowIfNull(next); ArgumentNullException.ThrowIfNull(logger); ArgumentNullException.ThrowIfNull(loadBalancingPolicies); _next = next; _logger = logger; _loadBalancingPolicies = loadBalancingPolicies.ToDictionaryByUniqueId(p => p.Name); } public Task Invoke(HttpContext context) { var proxyFeature = context.GetReverseProxyFeature(); var destinations = proxyFeature.AvailableDestinations; var destinationCount = destinations.Count; DestinationState? destination; if (destinationCount == 0) { destination = null; } else if (destinationCount == 1) { destination = destinations[0]; } else { var currentPolicy = _loadBalancingPolicies.GetRequiredServiceById(proxyFeature.Cluster.Config.LoadBalancingPolicy, LoadBalancingPolicies.PowerOfTwoChoices); destination = currentPolicy.PickDestination(context, proxyFeature.Route.Cluster!, destinations); } if (destination is null) { // We intentionally do not short circuit here, we allow for later middleware to decide how to handle this case. Log.NoAvailableDestinations(_logger, proxyFeature.Cluster.Config.ClusterId); proxyFeature.AvailableDestinations = Array.Empty(); } else { proxyFeature.AvailableDestinations = destination; } return _next(context); } private static class Log { private static readonly Action _noAvailableDestinations = LoggerMessage.Define( LogLevel.Warning, EventIds.NoAvailableDestinations, "No available destinations after load balancing for cluster '{clusterId}'."); public static void NoAvailableDestinations(ILogger logger, string clusterId) { _noAvailableDestinations(logger, clusterId, null); } } } ================================================ FILE: src/ReverseProxy/LoadBalancing/LoadBalancingPolicies.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. namespace Yarp.ReverseProxy.LoadBalancing; /// /// Names of built-in load balancing policies. /// public static class LoadBalancingPolicies { /// /// Select the alphabetically first available destination without considering load. This is useful for dual destination fail-over systems. /// public static string FirstAlphabetical => nameof(FirstAlphabetical); /// /// Select a destination randomly. /// public static string Random => nameof(Random); /// /// Select a destination by cycling through them in order. /// public static string RoundRobin => nameof(RoundRobin); /// /// Select the destination with the least assigned requests. This requires examining all destinations. /// public static string LeastRequests => nameof(LeastRequests); /// /// Select two random destinations and then select the one with the least assigned requests. /// This avoids the overhead of LeastRequests and the worst case for Random where it selects a busy destination. /// public static string PowerOfTwoChoices => nameof(PowerOfTwoChoices); } ================================================ FILE: src/ReverseProxy/LoadBalancing/PowerOfTwoChoicesLoadBalancingPolicy.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; using Microsoft.AspNetCore.Http; using Yarp.ReverseProxy.Model; using Yarp.ReverseProxy.Utilities; namespace Yarp.ReverseProxy.LoadBalancing; internal sealed class PowerOfTwoChoicesLoadBalancingPolicy : ILoadBalancingPolicy { private readonly IRandomFactory _randomFactory; public PowerOfTwoChoicesLoadBalancingPolicy(IRandomFactory randomFactory) { _randomFactory = randomFactory; } public string Name => LoadBalancingPolicies.PowerOfTwoChoices; public DestinationState? PickDestination(HttpContext context, ClusterState cluster, IReadOnlyList availableDestinations) { var destinationCount = availableDestinations.Count; if (destinationCount == 0) { return null; } if (destinationCount == 1) { return availableDestinations[0]; } // Pick two, and then return the least busy. This avoids the effort of searching the whole list, but // still avoids overloading a single destination. var random = _randomFactory.CreateRandomInstance(); var firstIndex = random.Next(destinationCount); int secondIndex; do { secondIndex = random.Next(destinationCount); } while (firstIndex == secondIndex); var first = availableDestinations[firstIndex]; var second = availableDestinations[secondIndex]; return (first.ConcurrentRequestCount <= second.ConcurrentRequestCount) ? first : second; } } ================================================ FILE: src/ReverseProxy/LoadBalancing/RandomLoadBalancingPolicy.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; using Microsoft.AspNetCore.Http; using Yarp.ReverseProxy.Model; using Yarp.ReverseProxy.Utilities; namespace Yarp.ReverseProxy.LoadBalancing; internal sealed class RandomLoadBalancingPolicy : ILoadBalancingPolicy { private readonly IRandomFactory _randomFactory; public RandomLoadBalancingPolicy(IRandomFactory randomFactory) { _randomFactory = randomFactory; } public string Name => LoadBalancingPolicies.Random; public DestinationState? PickDestination(HttpContext context, ClusterState cluster, IReadOnlyList availableDestinations) { if (availableDestinations.Count == 0) { return null; } var random = _randomFactory.CreateRandomInstance(); return availableDestinations[random.Next(availableDestinations.Count)]; } } ================================================ FILE: src/ReverseProxy/LoadBalancing/RoundRobinLoadBalancingPolicy.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; using System.Runtime.CompilerServices; using Microsoft.AspNetCore.Http; using Yarp.ReverseProxy.Model; using Yarp.ReverseProxy.Utilities; namespace Yarp.ReverseProxy.LoadBalancing; internal sealed class RoundRobinLoadBalancingPolicy : ILoadBalancingPolicy { private readonly ConditionalWeakTable _counters = new(); public string Name => LoadBalancingPolicies.RoundRobin; public DestinationState? PickDestination(HttpContext context, ClusterState cluster, IReadOnlyList availableDestinations) { if (availableDestinations.Count == 0) { return null; } var counter = _counters.GetOrCreateValue(cluster); // Increment returns the new value and we want the first return value to be 0. var offset = counter.Increment() - 1; // Preventing negative indices from being computed by masking off sign. // Ordering of index selection is consistent across all offsets. // There may be a discontinuity when the sign of offset changes. return availableDestinations[(offset & 0x7FFFFFFF) % availableDestinations.Count]; } } ================================================ FILE: src/ReverseProxy/Management/IProxyStateLookup.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using Yarp.ReverseProxy.Model; namespace Yarp.ReverseProxy; /// /// Allows access to the proxy's current set of routes and clusters. /// public interface IProxyStateLookup { /// /// Retrieves a specific route by id, if present. /// bool TryGetRoute(string id, [NotNullWhen(true)] out RouteModel? route); /// /// Enumerates all current routes. This is thread safe but the collection may change mid-enumeration if the configuration is reloaded. /// IEnumerable GetRoutes(); /// /// Retrieves a specific cluster by id, if present. /// bool TryGetCluster(string id, [NotNullWhen(true)] out ClusterState? cluster); /// /// Enumerates all current clusters. This is thread safe but the collection may change mid-enumeration if the configuration is reloaded. /// IEnumerable GetClusters(); } ================================================ FILE: src/ReverseProxy/Management/IReverseProxyBuilder.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. namespace Microsoft.Extensions.DependencyInjection; /// /// Reverse Proxy builder interface. /// public interface IReverseProxyBuilder { /// /// Gets the services. /// IServiceCollection Services { get; } } ================================================ FILE: src/ReverseProxy/Management/IReverseProxyBuilderExtensions.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Linq; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Yarp.ReverseProxy.Configuration; using Yarp.ReverseProxy.Configuration.ClusterValidators; using Yarp.ReverseProxy.Configuration.RouteValidators; using Yarp.ReverseProxy.Delegation; using Yarp.ReverseProxy.Forwarder; using Yarp.ReverseProxy.Health; using Yarp.ReverseProxy.LoadBalancing; using Yarp.ReverseProxy.Model; using Yarp.ReverseProxy.Routing; using Yarp.ReverseProxy.ServiceDiscovery; using Yarp.ReverseProxy.SessionAffinity; using Yarp.ReverseProxy.Transforms; using Yarp.ReverseProxy.Utilities; namespace Yarp.ReverseProxy.Management; internal static class IReverseProxyBuilderExtensions { public static IReverseProxyBuilder AddConfigBuilder(this IReverseProxyBuilder builder) { builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); builder.Services.TryAddSingleton(); builder.AddTransformFactory(); builder.AddTransformFactory(); builder.AddTransformFactory(); builder.AddTransformFactory(); builder.AddTransformFactory(); builder.AddTransformFactory(); builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); return builder; } public static IReverseProxyBuilder AddRuntimeStateManagers(this IReverseProxyBuilder builder) { builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddEnumerable(new[] { ServiceDescriptor.Singleton(), ServiceDescriptor.Singleton() }); return builder; } public static IReverseProxyBuilder AddConfigManager(this IReverseProxyBuilder builder) { builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(sp => sp.GetRequiredService()); return builder; } public static IReverseProxyBuilder AddProxy(this IReverseProxyBuilder builder) { builder.Services.TryAddSingleton(); builder.Services.AddHttpForwarder(); return builder; } public static IReverseProxyBuilder AddLoadBalancingPolicies(this IReverseProxyBuilder builder) { builder.Services.TryAddSingleton(); builder.Services.TryAddEnumerable(new[] { ServiceDescriptor.Singleton(), ServiceDescriptor.Singleton(), ServiceDescriptor.Singleton(), ServiceDescriptor.Singleton(), ServiceDescriptor.Singleton() }); return builder; } public static IReverseProxyBuilder AddSessionAffinityPolicies(this IReverseProxyBuilder builder) { builder.Services.TryAddEnumerable(new[] { ServiceDescriptor.Singleton(), ServiceDescriptor.Singleton() }); builder.Services.TryAddEnumerable(new[] { ServiceDescriptor.Singleton(), ServiceDescriptor.Singleton(), ServiceDescriptor.Singleton(), ServiceDescriptor.Singleton() }); builder.AddTransforms(); return builder; } public static IReverseProxyBuilder AddActiveHealthChecks(this IReverseProxyBuilder builder) { builder.Services.TryAddSingleton(); // Avoid registering several IActiveHealthCheckMonitor implementations. if (!builder.Services.Any(d => d.ServiceType == typeof(IActiveHealthCheckMonitor))) { builder.Services.AddSingleton(); builder.Services.AddSingleton(p => p.GetRequiredService()); builder.Services.AddSingleton(p => p.GetRequiredService()); } builder.Services.AddSingleton(); return builder; } public static IReverseProxyBuilder AddPassiveHealthCheck(this IReverseProxyBuilder builder) { builder.Services.AddSingleton(); return builder; } public static IReverseProxyBuilder AddHttpSysDelegation(this IReverseProxyBuilder builder) { builder.Services.AddSingleton(); builder.Services.TryAddSingleton(p => p.GetRequiredService()); builder.Services.AddSingleton(p => p.GetRequiredService()); return builder; } public static IReverseProxyBuilder AddDestinationResolver(this IReverseProxyBuilder builder) { builder.Services.TryAddSingleton(); return builder; } } ================================================ FILE: src/ReverseProxy/Management/ProxyConfigManager.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Primitives; using Yarp.ReverseProxy.Configuration; using Yarp.ReverseProxy.Forwarder; using Yarp.ReverseProxy.Health; using Yarp.ReverseProxy.Model; using Yarp.ReverseProxy.Routing; using Yarp.ReverseProxy.ServiceDiscovery; using Yarp.ReverseProxy.Transforms.Builder; namespace Yarp.ReverseProxy.Management; /// /// Provides a method to apply Proxy configuration changes. /// Also an Implementation of that supports being dynamically updated /// in a thread-safe manner while avoiding locks on the hot path. /// // https://github.com/dotnet/aspnetcore/blob/cbe16474ce9db7ff588aed89596ff4df5c3f62e1/src/Mvc/Mvc.Core/src/Routing/ActionEndpointDataSourceBase.cs internal sealed class ProxyConfigManager : EndpointDataSource, IProxyStateLookup, IDisposable { private static readonly IReadOnlyDictionary _emptyClusterDictionary = new ReadOnlyDictionary(new Dictionary()); private readonly object _syncRoot = new(); private readonly ILogger _logger; private readonly IProxyConfigProvider[] _providers; private readonly ConfigState[] _configs; private readonly IClusterChangeListener[] _clusterChangeListeners; private readonly ConcurrentDictionary _clusters = new(StringComparer.OrdinalIgnoreCase); private readonly ConcurrentDictionary _routes = new(StringComparer.OrdinalIgnoreCase); private readonly IProxyConfigFilter[] _filters; private readonly IConfigValidator _configValidator; private readonly IForwarderHttpClientFactory _httpClientFactory; private readonly ProxyEndpointFactory _proxyEndpointFactory; private readonly ITransformBuilder _transformBuilder; private readonly List> _conventions; private readonly IActiveHealthCheckMonitor _activeHealthCheckMonitor; private readonly IClusterDestinationsUpdater _clusterDestinationsUpdater; private readonly IDestinationResolver _destinationResolver; private readonly IConfigChangeListener[] _configChangeListeners; private List? _endpoints; private CancellationTokenSource _endpointsChangeSource = new(); private IChangeToken _endpointsChangeToken; private CancellationTokenSource _configChangeSource = new(); public ProxyConfigManager( ILogger logger, IEnumerable providers, IEnumerable clusterChangeListeners, IEnumerable filters, IConfigValidator configValidator, ProxyEndpointFactory proxyEndpointFactory, ITransformBuilder transformBuilder, IForwarderHttpClientFactory httpClientFactory, IActiveHealthCheckMonitor activeHealthCheckMonitor, IClusterDestinationsUpdater clusterDestinationsUpdater, IEnumerable configChangeListeners, IDestinationResolver destinationResolver) { ArgumentNullException.ThrowIfNull(logger); ArgumentNullException.ThrowIfNull(providers); ArgumentNullException.ThrowIfNull(clusterChangeListeners); ArgumentNullException.ThrowIfNull(filters); ArgumentNullException.ThrowIfNull(configValidator); ArgumentNullException.ThrowIfNull(proxyEndpointFactory); ArgumentNullException.ThrowIfNull(transformBuilder); ArgumentNullException.ThrowIfNull(httpClientFactory); ArgumentNullException.ThrowIfNull(activeHealthCheckMonitor); ArgumentNullException.ThrowIfNull(clusterDestinationsUpdater); ArgumentNullException.ThrowIfNull(configChangeListeners); ArgumentNullException.ThrowIfNull(destinationResolver); _logger = logger; _providers = providers.ToArray(); _clusterChangeListeners = clusterChangeListeners.ToArray(); _filters = filters.ToArray(); _configValidator = configValidator; _proxyEndpointFactory = proxyEndpointFactory; _transformBuilder = transformBuilder; _httpClientFactory = httpClientFactory; _activeHealthCheckMonitor = activeHealthCheckMonitor; _clusterDestinationsUpdater = clusterDestinationsUpdater; _destinationResolver = destinationResolver; _configChangeListeners = configChangeListeners.ToArray(); if (_providers.Length == 0) { throw new ArgumentException($"At least one {nameof(IProxyConfigProvider)} is required.", nameof(providers)); } _configs = new ConfigState[_providers.Length]; _conventions = new List>(); DefaultBuilder = new ReverseProxyConventionBuilder(_conventions); _endpointsChangeToken = new CancellationChangeToken(_endpointsChangeSource.Token); } public ReverseProxyConventionBuilder DefaultBuilder { get; } // EndpointDataSource /// public override IReadOnlyList Endpoints { get { // The Endpoints needs to be lazy the first time to give a chance to ReverseProxyConventionBuilder to add its conventions. // Endpoints are accessed by routing on the first request. if (_endpoints is null) { lock (_syncRoot) { if (_endpoints is null) { CreateEndpoints(); } } } return _endpoints; } } [MemberNotNull(nameof(_endpoints))] private void CreateEndpoints() { var endpoints = new List(); // Directly enumerate the ConcurrentDictionary to limit locking and copying. foreach (var existingRoute in _routes) { // Only rebuild the endpoint for modified routes or clusters. var endpoint = existingRoute.Value.CachedEndpoint; if (endpoint is null) { endpoint = _proxyEndpointFactory.CreateEndpoint(existingRoute.Value.Model, _conventions); existingRoute.Value.CachedEndpoint = endpoint; } endpoints.Add(endpoint); } UpdateEndpoints(endpoints); } /// public override IChangeToken GetChangeToken() => Volatile.Read(ref _endpointsChangeToken); private static IProxyConfig[] ExtractListOfProxyConfigs(IEnumerable configStates) { return configStates.Select(state => state.LatestConfig).ToArray(); } internal async Task InitialLoadAsync() { // Trigger the first load immediately and throw if it fails. // We intend this to crash the app, so we don't try listening for further changes. try { var routes = new List(); var clusters = new List(); // Begin resolving config providers concurrently. var resolvedConfigs = new List<(int Index, IProxyConfigProvider Provider, ValueTask Config)>(_providers.Length); for (var i = 0; i < _providers.Length; i++) { var provider = _providers[i]; var configLoadTask = LoadConfigAsync(provider, cancellationToken: default); resolvedConfigs.Add((i, provider, configLoadTask)); } // Wait for all configs to be resolved. foreach (var (i, provider, configLoadTask) in resolvedConfigs) { var config = await configLoadTask; _configs[i] = new ConfigState(provider, config); routes.AddRange(config.Routes ?? Array.Empty()); clusters.AddRange(config.Clusters ?? Array.Empty()); } var proxyConfigs = ExtractListOfProxyConfigs(_configs); foreach (var configChangeListener in _configChangeListeners) { configChangeListener.ConfigurationLoaded(proxyConfigs); } await ApplyConfigAsync(routes, clusters); foreach (var configChangeListener in _configChangeListeners) { configChangeListener.ConfigurationApplied(proxyConfigs); } ListenForConfigChanges(); } catch (Exception ex) { throw new InvalidOperationException("Unable to load or apply the proxy configuration.", ex); } // Initial active health check is run in the background. // Directly enumerate the ConcurrentDictionary to limit locking and copying. _ = _activeHealthCheckMonitor.CheckHealthAsync(_clusters.Select(pair => pair.Value)); return this; } private async Task ReloadConfigAsync() { _configChangeSource.Dispose(); var sourcesChanged = false; var routes = new List(); var clusters = new List(); var reloadedConfigs = new List<(ConfigState Config, ValueTask ResolveTask)>(); // Start reloading changed configurations. foreach (var instance in _configs) { if (instance.LatestConfig.ChangeToken.HasChanged) { try { var reloadTask = LoadConfigAsync(instance.Provider, cancellationToken: default); reloadedConfigs.Add((instance, reloadTask)); } catch (Exception ex) { OnConfigLoadError(instance, ex); } } } // Wait for all changed config providers to be reloaded. foreach (var (instance, loadTask) in reloadedConfigs) { try { instance.LatestConfig = await loadTask.ConfigureAwait(false); instance.LoadFailed = false; sourcesChanged = true; } catch (Exception ex) { OnConfigLoadError(instance, ex); } } // Extract the routes and clusters from the configs, regardless of whether they were reloaded. foreach (var instance in _configs) { if (instance.LatestConfig.Routes is { Count: > 0 } updatedRoutes) { routes.AddRange(updatedRoutes); } if (instance.LatestConfig.Clusters is { Count: > 0 } updatedClusters) { clusters.AddRange(updatedClusters); } } var proxyConfigs = ExtractListOfProxyConfigs(_configs); foreach (var configChangeListener in _configChangeListeners) { configChangeListener.ConfigurationLoaded(proxyConfigs); } try { // Only reload if at least one provider changed. if (sourcesChanged) { var hasChanged = await ApplyConfigAsync(routes, clusters); lock (_syncRoot) { // Skip if changes are signaled before the endpoints are initialized for the first time. // The endpoint conventions might not be ready yet. if (hasChanged && _endpoints is not null) { CreateEndpoints(); } } } foreach (var configChangeListener in _configChangeListeners) { configChangeListener.ConfigurationApplied(proxyConfigs); } } catch (Exception ex) { Log.ErrorApplyingConfig(_logger, ex); foreach (var configChangeListener in _configChangeListeners) { configChangeListener.ConfigurationApplyingFailed(proxyConfigs, ex); } } ListenForConfigChanges(); void OnConfigLoadError(ConfigState instance, Exception ex) { instance.LoadFailed = true; Log.ErrorReloadingConfig(_logger, ex); foreach (var configChangeListener in _configChangeListeners) { configChangeListener.ConfigurationLoadingFailed(instance.Provider, ex); } } } private static void ValidateConfigProperties(IProxyConfig config) { if (config is null) { throw new InvalidOperationException($"{nameof(IProxyConfigProvider.GetConfig)} returned a null value."); } if (config.ChangeToken is null) { throw new InvalidOperationException($"{nameof(IProxyConfig.ChangeToken)} has a null value."); } } private ValueTask LoadConfigAsync(IProxyConfigProvider provider, CancellationToken cancellationToken) { var config = provider.GetConfig(); ValidateConfigProperties(config); if (_destinationResolver.GetType() == typeof(NoOpDestinationResolver)) { return new(config); } return LoadConfigAsyncCore(config, cancellationToken); } private async ValueTask LoadConfigAsyncCore(IProxyConfig config, CancellationToken cancellationToken) { List<(int Index, ValueTask Task)> resolverTasks = new(); List clusters = new(config.Clusters); List? changeTokens = null; for (var i = 0; i < clusters.Count; i++) { var cluster = clusters[i]; if (cluster.Destinations is { Count: > 0 } destinations) { // Resolve destinations if there are any. var task = _destinationResolver.ResolveDestinationsAsync(destinations, cancellationToken); resolverTasks.Add((i, task)); } } if (resolverTasks.Count > 0) { foreach (var (i, task) in resolverTasks) { ResolvedDestinationCollection resolvedDestinations; try { resolvedDestinations = await task; } catch (Exception exception) { var cluster = clusters[i]; throw new InvalidOperationException($"Error resolving destinations for cluster {cluster.ClusterId}", exception); } clusters[i] = clusters[i] with { Destinations = resolvedDestinations.Destinations }; if (resolvedDestinations.ChangeToken is { } token) { changeTokens ??= new(); changeTokens.Add(token); } } IChangeToken changeToken; if (changeTokens is not null) { // Combine change tokens from the resolver with the configuration's existing change token. changeTokens.Add(config.ChangeToken); changeToken = new CompositeChangeToken(changeTokens); } else { changeToken = config.ChangeToken; } // Return updated config return new ResolvedProxyConfig(config, clusters, changeToken); } return config; } private sealed class ResolvedProxyConfig : IProxyConfig { private readonly IProxyConfig _innerConfig; public ResolvedProxyConfig(IProxyConfig innerConfig, IReadOnlyList clusters, IChangeToken changeToken) { _innerConfig = innerConfig; Clusters = clusters; ChangeToken = changeToken; } public IReadOnlyList Routes => _innerConfig.Routes; public IReadOnlyList Clusters { get; } public IChangeToken ChangeToken { get; } } private void ListenForConfigChanges() { // Use a central change token to avoid overlap between different sources. var source = new CancellationTokenSource(); _configChangeSource = source; var poll = false; foreach (var configState in _configs) { if (configState.LoadFailed) { // We can't register for change notifications if the last load failed. poll = true; continue; } configState.CallbackCleanup?.Dispose(); var token = configState.LatestConfig.ChangeToken; if (token.ActiveChangeCallbacks) { configState.CallbackCleanup = token.RegisterChangeCallback(SignalChange, source); } else { poll = true; } } if (poll) { source.CancelAfter(TimeSpan.FromMinutes(5)); } // Don't register until we're done hooking everything up to avoid cancellation races. source.Token.Register(ReloadConfig, this); static void SignalChange(object? obj) { var token = (CancellationTokenSource)obj!; try { token.Cancel(); } // Don't throw if the source was already disposed. catch (ObjectDisposedException) { } } static void ReloadConfig(object? state) { var manager = (ProxyConfigManager)state!; _ = manager.ReloadConfigAsync(); } } // Throws for validation failures private async Task ApplyConfigAsync(IReadOnlyList routes, IReadOnlyList clusters) { var (configuredClusters, clusterErrors) = await VerifyClustersAsync(clusters, cancellation: default); var (configuredRoutes, routeErrors) = await VerifyRoutesAsync(routes, configuredClusters, cancellation: default); if (routeErrors.Count > 0 || clusterErrors.Count > 0) { throw new AggregateException("The proxy config is invalid.", routeErrors.Concat(clusterErrors)); } // Update clusters first because routes need to reference them. UpdateRuntimeClusters(configuredClusters); var routesChanged = UpdateRuntimeRoutes(configuredRoutes); return routesChanged; } private async Task<(IList, IList)> VerifyRoutesAsync(IReadOnlyList routes, IReadOnlyDictionary clusters, CancellationToken cancellation) { if (routes is null) { return (Array.Empty(), Array.Empty()); } var seenRouteIds = new HashSet(routes.Count, StringComparer.OrdinalIgnoreCase); var configuredRoutes = new List(routes.Count); var errors = new List(); foreach (var r in routes) { if (seenRouteIds.Contains(r.RouteId)) { errors.Add(new ArgumentException($"Duplicate route '{r.RouteId}'")); continue; } var route = r; try { if (_filters.Length != 0) { ClusterConfig? cluster = null; if (route.ClusterId is not null) { clusters.TryGetValue(route.ClusterId, out cluster); } foreach (var filter in _filters) { route = await filter.ConfigureRouteAsync(route, cluster, cancellation); } } } catch (Exception ex) { errors.Add(new Exception($"An exception was thrown from the configuration callbacks for route '{r.RouteId}'.", ex)); continue; } var routeErrors = await _configValidator.ValidateRouteAsync(route); if (routeErrors.Count > 0) { errors.AddRange(routeErrors); continue; } seenRouteIds.Add(route.RouteId); configuredRoutes.Add(route); } if (errors.Count > 0) { return (Array.Empty(), errors); } return (configuredRoutes, errors); } private async Task<(IReadOnlyDictionary, IList)> VerifyClustersAsync(IReadOnlyList clusters, CancellationToken cancellation) { if (clusters is null) { return (_emptyClusterDictionary, Array.Empty()); } var configuredClusters = new Dictionary(clusters.Count, StringComparer.OrdinalIgnoreCase); var errors = new List(); // The IProxyConfigProvider provides a fresh snapshot that we need to reconfigure each time. foreach (var c in clusters) { try { if (configuredClusters.ContainsKey(c.ClusterId)) { errors.Add(new ArgumentException($"Duplicate cluster '{c.ClusterId}'.")); continue; } // Don't modify the original var cluster = c; foreach (var filter in _filters) { cluster = await filter.ConfigureClusterAsync(cluster, cancellation); } var clusterErrors = await _configValidator.ValidateClusterAsync(cluster); if (clusterErrors.Count > 0) { errors.AddRange(clusterErrors); continue; } configuredClusters.Add(cluster.ClusterId, cluster); } catch (Exception ex) { errors.Add(new ArgumentException($"An exception was thrown from the configuration callbacks for cluster '{c.ClusterId}'.", ex)); } } if (errors.Count > 0) { return (_emptyClusterDictionary, errors); } return (configuredClusters, errors); } private void UpdateRuntimeClusters(IReadOnlyDictionary incomingClusters) { var desiredClusters = new HashSet(incomingClusters.Count, StringComparer.OrdinalIgnoreCase); foreach (var incomingCluster in incomingClusters.Values) { var added = desiredClusters.Add(incomingCluster.ClusterId); Debug.Assert(added); if (_clusters.TryGetValue(incomingCluster.ClusterId, out var currentCluster)) { var destinationsChanged = UpdateRuntimeDestinations(incomingCluster.Destinations, currentCluster.Destinations); var currentClusterModel = currentCluster.Model; var httpClient = _httpClientFactory.CreateClient(new ForwarderHttpClientContext { ClusterId = currentCluster.ClusterId, OldConfig = currentClusterModel.Config.HttpClient ?? HttpClientConfig.Empty, OldMetadata = currentClusterModel.Config.Metadata, OldClient = currentClusterModel.HttpClient, NewConfig = incomingCluster.HttpClient ?? HttpClientConfig.Empty, NewMetadata = incomingCluster.Metadata }); var newClusterModel = new ClusterModel(incomingCluster, httpClient); // Excludes destination changes, they're tracked separately. var configChanged = currentClusterModel.HasConfigChanged(newClusterModel); if (configChanged) { currentCluster.Revision++; Log.ClusterChanged(_logger, incomingCluster.ClusterId); } if (destinationsChanged || configChanged) { // Config changed, so update runtime cluster currentCluster.Model = newClusterModel; _clusterDestinationsUpdater.UpdateAllDestinations(currentCluster); foreach (var listener in _clusterChangeListeners) { listener.OnClusterChanged(currentCluster); } } } else { var newClusterState = new ClusterState(incomingCluster.ClusterId); UpdateRuntimeDestinations(incomingCluster.Destinations, newClusterState.Destinations); var httpClient = _httpClientFactory.CreateClient(new ForwarderHttpClientContext { ClusterId = newClusterState.ClusterId, NewConfig = incomingCluster.HttpClient ?? HttpClientConfig.Empty, NewMetadata = incomingCluster.Metadata }); newClusterState.Model = new ClusterModel(incomingCluster, httpClient); newClusterState.Revision++; Log.ClusterAdded(_logger, incomingCluster.ClusterId); _clusterDestinationsUpdater.UpdateAllDestinations(newClusterState); added = _clusters.TryAdd(newClusterState.ClusterId, newClusterState); Debug.Assert(added); foreach (var listener in _clusterChangeListeners) { listener.OnClusterAdded(newClusterState); } } } // Directly enumerate the ConcurrentDictionary to limit locking and copying. foreach (var existingClusterPair in _clusters) { var existingCluster = existingClusterPair.Value; if (!desiredClusters.Contains(existingCluster.ClusterId)) { // NOTE 1: Remove is safe to do within the `foreach` loop on ConcurrentDictionary // // NOTE 2: Removing the cluster from _clusters is safe and existing // ASP .NET Core endpoints will continue to work with their existing behavior (until those endpoints are updated) // and the Garbage Collector won't destroy this cluster object while it's referenced elsewhere. Log.ClusterRemoved(_logger, existingCluster.ClusterId); var removed = _clusters.TryRemove(existingCluster.ClusterId, out var _); Debug.Assert(removed); foreach (var listener in _clusterChangeListeners) { listener.OnClusterRemoved(existingCluster); } } } } private bool UpdateRuntimeDestinations(IReadOnlyDictionary? incomingDestinations, ConcurrentDictionary currentDestinations) { var desiredDestinations = new HashSet(incomingDestinations?.Count ?? 0, StringComparer.OrdinalIgnoreCase); var changed = false; if (incomingDestinations is not null) { foreach (var incomingDestination in incomingDestinations) { var added = desiredDestinations.Add(incomingDestination.Key); Debug.Assert(added); if (currentDestinations.TryGetValue(incomingDestination.Key, out var currentDestination)) { if (currentDestination.Model.HasChanged(incomingDestination.Value)) { Log.DestinationChanged(_logger, incomingDestination.Key); currentDestination.Model = new DestinationModel(incomingDestination.Value); changed = true; } } else { Log.DestinationAdded(_logger, incomingDestination.Key); var newDestination = new DestinationState(incomingDestination.Key) { Model = new DestinationModel(incomingDestination.Value), }; added = currentDestinations.TryAdd(newDestination.DestinationId, newDestination); Debug.Assert(added); changed = true; } } } // Directly enumerate the ConcurrentDictionary to limit locking and copying. foreach (var existingDestinationPair in currentDestinations) { var id = existingDestinationPair.Value.DestinationId; if (!desiredDestinations.Contains(id)) { // NOTE 1: Remove is safe to do within the `foreach` loop on ConcurrentDictionary // // NOTE 2: Removing the endpoint from `IEndpointManager` is safe and existing // clusters will continue to work with their existing behavior (until those clusters are updated) // and the Garbage Collector won't destroy this cluster object while it's referenced elsewhere. Log.DestinationRemoved(_logger, id); var removed = currentDestinations.TryRemove(id, out var _); Debug.Assert(removed); changed = true; } } return changed; } private bool UpdateRuntimeRoutes(IList incomingRoutes) { var desiredRoutes = new HashSet(incomingRoutes.Count, StringComparer.OrdinalIgnoreCase); var changed = false; foreach (var incomingRoute in incomingRoutes) { var added = desiredRoutes.Add(incomingRoute.RouteId); Debug.Assert(added); // Note that this can be null, and that is fine. The resulting route may match // but would then fail to route, which is exactly what we were instructed to do in this case // since no valid cluster was specified. _clusters.TryGetValue(incomingRoute.ClusterId ?? string.Empty, out var cluster); if (_routes.TryGetValue(incomingRoute.RouteId, out var currentRoute)) { if (currentRoute.Model.HasConfigChanged(incomingRoute, cluster, currentRoute.ClusterRevision)) { currentRoute.CachedEndpoint = null; // Recreate endpoint var newModel = BuildRouteModel(incomingRoute, cluster); currentRoute.Model = newModel; currentRoute.ClusterRevision = cluster?.Revision; changed = true; Log.RouteChanged(_logger, currentRoute.RouteId); } } else { var newModel = BuildRouteModel(incomingRoute, cluster); var newState = new RouteState(incomingRoute.RouteId) { Model = newModel, ClusterRevision = cluster?.Revision, }; added = _routes.TryAdd(newState.RouteId, newState); Debug.Assert(added); changed = true; Log.RouteAdded(_logger, newState.RouteId); } } // Directly enumerate the ConcurrentDictionary to limit locking and copying. foreach (var existingRoutePair in _routes) { var routeId = existingRoutePair.Value.RouteId; if (!desiredRoutes.Contains(routeId)) { // NOTE 1: Remove is safe to do within the `foreach` loop on ConcurrentDictionary // // NOTE 2: Removing the route from _routes is safe and existing // ASP.NET Core endpoints will continue to work with their existing behavior since // their copy of `RouteModel` is immutable and remains operational in whichever state is was in. Log.RouteRemoved(_logger, routeId); var removed = _routes.TryRemove(routeId, out var _); Debug.Assert(removed); changed = true; } } return changed; } /// /// Applies a new set of ASP .NET Core endpoints. Changes take effect immediately. /// /// New endpoints to apply. [MemberNotNull(nameof(_endpoints))] private void UpdateEndpoints(List endpoints) { ArgumentNullException.ThrowIfNull(endpoints); lock (_syncRoot) { // These steps are done in a specific order to ensure callers always see a consistent state. // Step 1 - capture old token var oldCancellationTokenSource = _endpointsChangeSource; // Step 2 - update endpoints Volatile.Write(ref _endpoints, endpoints); // Step 3 - create new change token _endpointsChangeSource = new CancellationTokenSource(); Volatile.Write(ref _endpointsChangeToken, new CancellationChangeToken(_endpointsChangeSource.Token)); // Step 4 - trigger old token oldCancellationTokenSource?.Cancel(); } } private RouteModel BuildRouteModel(RouteConfig source, ClusterState? cluster) { var transforms = _transformBuilder.Build(source, cluster?.Model?.Config); return new RouteModel(source, cluster, transforms); } public bool TryGetRoute(string id, [NotNullWhen(true)] out RouteModel? route) { if (_routes.TryGetValue(id, out var routeState)) { route = routeState.Model; return true; } route = null; return false; } public IEnumerable GetRoutes() { foreach (var (_, route) in _routes) { yield return route.Model; } } public bool TryGetCluster(string id, [NotNullWhen(true)] out ClusterState? cluster) { return _clusters.TryGetValue(id, out cluster!); } public IEnumerable GetClusters() { foreach (var (_, cluster) in _clusters) { yield return cluster; } } public void Dispose() { _configChangeSource.Dispose(); foreach (var instance in _configs) { instance?.CallbackCleanup?.Dispose(); } } private sealed class ConfigState { public ConfigState(IProxyConfigProvider provider, IProxyConfig config) { Provider = provider; LatestConfig = config; } public IProxyConfigProvider Provider { get; } public IProxyConfig LatestConfig { get; set; } public bool LoadFailed { get; set; } public IDisposable? CallbackCleanup { get; set; } } private static class Log { private static readonly Action _clusterAdded = LoggerMessage.Define( LogLevel.Debug, EventIds.ClusterAdded, "Cluster '{clusterId}' has been added."); private static readonly Action _clusterChanged = LoggerMessage.Define( LogLevel.Debug, EventIds.ClusterChanged, "Cluster '{clusterId}' has changed."); private static readonly Action _clusterRemoved = LoggerMessage.Define( LogLevel.Debug, EventIds.ClusterRemoved, "Cluster '{clusterId}' has been removed."); private static readonly Action _destinationAdded = LoggerMessage.Define( LogLevel.Debug, EventIds.DestinationAdded, "Destination '{destinationId}' has been added."); private static readonly Action _destinationChanged = LoggerMessage.Define( LogLevel.Debug, EventIds.DestinationChanged, "Destination '{destinationId}' has changed."); private static readonly Action _destinationRemoved = LoggerMessage.Define( LogLevel.Debug, EventIds.DestinationRemoved, "Destination '{destinationId}' has been removed."); private static readonly Action _routeAdded = LoggerMessage.Define( LogLevel.Debug, EventIds.RouteAdded, "Route '{routeId}' has been added."); private static readonly Action _routeChanged = LoggerMessage.Define( LogLevel.Debug, EventIds.RouteChanged, "Route '{routeId}' has changed."); private static readonly Action _routeRemoved = LoggerMessage.Define( LogLevel.Debug, EventIds.RouteRemoved, "Route '{routeId}' has been removed."); private static readonly Action _errorReloadingConfig = LoggerMessage.Define( LogLevel.Error, EventIds.ErrorReloadingConfig, "Failed to reload config. Unable to register for change notifications, polling for changes until successful."); private static readonly Action _errorApplyingConfig = LoggerMessage.Define( LogLevel.Error, EventIds.ErrorApplyingConfig, "Failed to apply the new config."); public static void ClusterAdded(ILogger logger, string clusterId) { _clusterAdded(logger, clusterId, null); } public static void ClusterChanged(ILogger logger, string clusterId) { _clusterChanged(logger, clusterId, null); } public static void ClusterRemoved(ILogger logger, string clusterId) { _clusterRemoved(logger, clusterId, null); } public static void DestinationAdded(ILogger logger, string destinationId) { _destinationAdded(logger, destinationId, null); } public static void DestinationChanged(ILogger logger, string destinationId) { _destinationChanged(logger, destinationId, null); } public static void DestinationRemoved(ILogger logger, string destinationId) { _destinationRemoved(logger, destinationId, null); } public static void RouteAdded(ILogger logger, string routeId) { _routeAdded(logger, routeId, null); } public static void RouteChanged(ILogger logger, string routeId) { _routeChanged(logger, routeId, null); } public static void RouteRemoved(ILogger logger, string routeId) { _routeRemoved(logger, routeId, null); } public static void ErrorReloadingConfig(ILogger logger, Exception ex) { _errorReloadingConfig(logger, ex); } public static void ErrorApplyingConfig(ILogger logger, Exception ex) { _errorApplyingConfig(logger, ex); } } } ================================================ FILE: src/ReverseProxy/Management/ReverseProxyBuilder.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using Microsoft.Extensions.DependencyInjection; namespace Yarp.ReverseProxy.Management; /// /// Reverse Proxy builder for DI configuration. /// internal sealed class ReverseProxyBuilder : IReverseProxyBuilder { /// /// Initializes a new instance of the class. /// /// Services collection. public ReverseProxyBuilder(IServiceCollection services) { ArgumentNullException.ThrowIfNull(services); Services = services; } /// /// Gets the services collection. /// public IServiceCollection Services { get; } } ================================================ FILE: src/ReverseProxy/Management/ReverseProxyServiceCollectionExtensions.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Net.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; using Yarp.ReverseProxy.Configuration; using Yarp.ReverseProxy.Configuration.ConfigProvider; using Yarp.ReverseProxy.Delegation; using Yarp.ReverseProxy.Forwarder; using Yarp.ReverseProxy.Management; using Yarp.ReverseProxy.Routing; using Yarp.ReverseProxy.ServiceDiscovery; using Yarp.ReverseProxy.Transforms.Builder; namespace Microsoft.Extensions.DependencyInjection; /// /// Extensions for /// used to register the ReverseProxy's components. /// public static class ReverseProxyServiceCollectionExtensions { /// /// Registers the service for direct forwarding scenarios. /// public static IServiceCollection AddHttpForwarder(this IServiceCollection services) { services.TryAddSingleton(TimeProvider.System); services.TryAddSingleton(); services.TryAddSingleton(); services.AddSingleton(); return services; } /// /// Adds ReverseProxy's services to Dependency Injection. /// public static IReverseProxyBuilder AddReverseProxy(this IServiceCollection services) { var builder = new ReverseProxyBuilder(services); builder .AddConfigBuilder() .AddRuntimeStateManagers() .AddConfigManager() .AddSessionAffinityPolicies() .AddActiveHealthChecks() .AddPassiveHealthCheck() .AddLoadBalancingPolicies() .AddDestinationResolver() .AddProxy(); if (OperatingSystem.IsWindows()) { // Workaround for https://github.com/dotnet/aspnetcore/issues/59166. // .NET 9.0 packages for Ubuntu ship a broken Microsoft.AspNetCore.Server.HttpSys assembly. // Avoid loading types from that assembly on Linux unless the user explicitly tries to do so. builder.AddHttpSysDelegation(); } else { // Add a no-op delegator in case someone is injecting the interface in their cross-plat logic. builder.Services.TryAddSingleton(); } services.TryAddSingleton(); services.AddDataProtection(); services.AddAuthorization(); services.AddCors(); services.AddRouting(); return builder; } /// /// Loads routes and endpoints from config. /// public static IReverseProxyBuilder LoadFromConfig(this IReverseProxyBuilder builder, IConfiguration config) { ArgumentNullException.ThrowIfNull(config); builder.Services.AddSingleton(sp => { // This is required because we're capturing the configuration via a closure return new ConfigurationConfigProvider(sp.GetRequiredService>(), config); }); return builder; } /// /// Registers a singleton IProxyConfigFilter service. Multiple filters are allowed, and they will be run in registration order. /// /// A class that implements IProxyConfigFilter. public static IReverseProxyBuilder AddConfigFilter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TService>(this IReverseProxyBuilder builder) where TService : class, IProxyConfigFilter { ArgumentNullException.ThrowIfNull(builder); builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); return builder; } /// /// Provides a callback that will be run for each route to conditionally add transforms. /// can be called multiple times to /// provide multiple callbacks. /// public static IReverseProxyBuilder AddTransforms(this IReverseProxyBuilder builder, Action action) { ArgumentNullException.ThrowIfNull(action); builder.Services.AddSingleton(new ActionTransformProvider(action)); return builder; } /// /// Provides a implementation that will be run for each route to conditionally add transforms. /// can be called multiple times to provide multiple distinct types. /// public static IReverseProxyBuilder AddTransforms<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] T>(this IReverseProxyBuilder builder) where T : class, ITransformProvider { builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); return builder; } /// /// Adds a implementation that will be used to read route transform config and generate /// the associated transform actions. can be called multiple /// times to provide multiple distinct types. /// public static IReverseProxyBuilder AddTransformFactory<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] T>(this IReverseProxyBuilder builder) where T : class, ITransformFactory { builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); return builder; } /// /// Provides a callback to customize settings used for proxying requests. /// This will be called each time a cluster is added or changed. Cluster settings are applied to the handler before /// the callback. Custom data can be provided in the cluster metadata. /// public static IReverseProxyBuilder ConfigureHttpClient(this IReverseProxyBuilder builder, Action configure) { ArgumentNullException.ThrowIfNull(configure); // Avoid overriding any other custom factories. This does not handle the case where a IForwarderHttpClientFactory // is registered after this call. var service = builder.Services.FirstOrDefault(service => service.ServiceType == typeof(IForwarderHttpClientFactory)); if (service is not null) { if (service.ImplementationType != typeof(ForwarderHttpClientFactory)) { throw new InvalidOperationException($"ConfigureHttpClient will override the custom IForwarderHttpClientFactory type."); } } builder.Services.AddSingleton(services => { var logger = services.GetRequiredService>(); return new CallbackHttpClientFactory(logger, configure); }); return builder; } /// /// Provides a implementation which uses to resolve destinations. /// public static IReverseProxyBuilder AddDnsDestinationResolver(this IReverseProxyBuilder builder, Action? configureOptions = null) { builder.Services.AddSingleton(); if (configureOptions is not null) { builder.Services.Configure(configureOptions); } return builder; } } ================================================ FILE: src/ReverseProxy/Model/ClusterDestinationsState.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; namespace Yarp.ReverseProxy.Model; public sealed class ClusterDestinationsState { public ClusterDestinationsState( IReadOnlyList allDestinations, IReadOnlyList availableDestinations) { ArgumentNullException.ThrowIfNull(allDestinations); ArgumentNullException.ThrowIfNull(availableDestinations); AllDestinations = allDestinations; AvailableDestinations = availableDestinations; } public IReadOnlyList AllDestinations { get; } public IReadOnlyList AvailableDestinations { get; } } ================================================ FILE: src/ReverseProxy/Model/ClusterModel.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Net.Http; using Yarp.ReverseProxy.Configuration; namespace Yarp.ReverseProxy.Model; /// /// Immutable representation of the portions of a cluster /// that only change in reaction to configuration changes /// (e.g. http client options). /// /// /// All members must remain immutable to avoid thread safety issues. /// Instead, instances of are replaced /// in their entirety when values need to change. /// public sealed class ClusterModel { /// /// Creates a new Instance. /// public ClusterModel( ClusterConfig config, HttpMessageInvoker httpClient) { ArgumentNullException.ThrowIfNull(config); ArgumentNullException.ThrowIfNull(httpClient); Config = config; HttpClient = httpClient; } /// /// The config for this cluster. /// public ClusterConfig Config { get; } /// /// An that used for proxying requests to an upstream server. /// public HttpMessageInvoker HttpClient { get; } // We intentionally do not consider destination changes when updating the cluster Revision. // Revision is used to rebuild routing endpoints which should be unrelated to destinations, // and destinations are the most likely to change. internal bool HasConfigChanged(ClusterModel newModel) { return !Config.EqualsExcludingDestinations(newModel.Config) || newModel.HttpClient != HttpClient; } } ================================================ FILE: src/ReverseProxy/Model/ClusterState.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Concurrent; using Microsoft.AspNetCore.Http; using Yarp.ReverseProxy.Utilities; namespace Yarp.ReverseProxy.Model; /// /// Representation of a cluster for use at runtime. /// public sealed class ClusterState { private volatile ClusterDestinationsState _destinationsState = new ClusterDestinationsState(Array.Empty(), Array.Empty()); private volatile ClusterModel _model = default!; // Initialized right after construction. /// /// Creates a new instance. This constructor is for tests and infrastructure, this type is normally constructed by the configuration /// loading infrastructure. /// public ClusterState(string clusterId) { ArgumentNullException.ThrowIfNull(clusterId); ClusterId = clusterId; } /// /// Constructor overload to additionally initialize the for tests and infrastructure, /// such as updating the via /// /// is . public ClusterState(string clusterId, ClusterModel model) : this(clusterId) { ArgumentNullException.ThrowIfNull(model); Model = model; } /// /// The cluster's unique id. /// public string ClusterId { get; } /// /// Encapsulates parts of a cluster that can change atomically in reaction to config changes. /// public ClusterModel Model { get => _model; internal set => _model = value ?? throw new ArgumentNullException(nameof(value)); } /// /// All the destinations associated with this cluster. This collection is populated by the configuration system /// and should only be directly modified in a test environment. /// Call after modifying this collection. /// public ConcurrentDictionary Destinations { get; } = new(StringComparer.OrdinalIgnoreCase); /// /// Stores the state of cluster's destinations that can change atomically /// in reaction to runtime state changes (e.g. changes of destinations' health). /// public ClusterDestinationsState DestinationsState { get => _destinationsState; set => _destinationsState = value ?? throw new ArgumentNullException(nameof(value)); } /// /// Keeps track of the total number of concurrent requests on this cluster. /// internal AtomicCounter ConcurrencyCounter { get; } = new AtomicCounter(); /// /// Tracks changes to the cluster configuration for use with rebuilding dependent endpoints. Destination changes do not affect this property. /// internal int Revision { get; set; } } ================================================ FILE: src/ReverseProxy/Model/DestinationHealth.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. namespace Yarp.ReverseProxy.Model; public enum DestinationHealth { Unknown, Healthy, Unhealthy, } ================================================ FILE: src/ReverseProxy/Model/DestinationHealthState.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. namespace Yarp.ReverseProxy.Model; /// /// Tracks destination passive and active health states. /// public class DestinationHealthState { private volatile DestinationHealth _active; private volatile DestinationHealth _passive; /// /// Passive health state. /// public DestinationHealth Passive { get => _passive; set => _passive = value; } /// /// Active health state. /// public DestinationHealth Active { get => _active; set => _active = value; } } ================================================ FILE: src/ReverseProxy/Model/DestinationModel.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using Yarp.ReverseProxy.Configuration; namespace Yarp.ReverseProxy.Model; /// /// Immutable representation of the portions of a destination /// that only change in reaction to configuration changes /// (e.g. address). /// /// /// All members must remain immutable to avoid thread safety issues. /// Instead, instances of are replaced /// in their entirety when values need to change. /// public sealed class DestinationModel { /// /// Creates a new instance. This constructor is for tests and infrastructure, this type is normally constructed by /// the configuration loading infrastructure. /// public DestinationModel(DestinationConfig destination) { ArgumentNullException.ThrowIfNull(destination); Config = destination; } /// /// This destination's configuration. /// public DestinationConfig Config { get; } internal bool HasChanged(DestinationConfig destination) { return Config != destination; } } ================================================ FILE: src/ReverseProxy/Model/DestinationState.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections; using System.Collections.Generic; using Microsoft.AspNetCore.Http; using Yarp.ReverseProxy.Utilities; namespace Yarp.ReverseProxy.Model; /// /// Representation of a cluster's destination for use at runtime. /// public sealed class DestinationState : IReadOnlyList { private volatile DestinationModel _model = default!; /// /// Creates a new instance. This constructor is for tests and infrastructure, this type is normally constructed by /// the configuration loading infrastructure. /// public DestinationState(string destinationId) { ArgumentException.ThrowIfNullOrEmpty(destinationId); DestinationId = destinationId; } /// /// Constructor overload to additionally initialize the for tests and infrastructure, /// such as updating the via /// /// is . public DestinationState(string destinationId, DestinationModel model) : this(destinationId) { ArgumentNullException.ThrowIfNull(model); Model = model; } /// /// The destination's unique id. /// public string DestinationId { get; } /// /// A snapshot of the current configuration /// public DestinationModel Model { get => _model; internal set => _model = value ?? throw new ArgumentNullException(nameof(value)); } /// /// Mutable health state for this destination. /// public DestinationHealthState Health { get; } = new DestinationHealthState(); /// /// Keeps track of the total number of concurrent requests on this endpoint. /// The setter should only be used for testing purposes. /// public int ConcurrentRequestCount { get => ConcurrencyCounter.Value; set => ConcurrencyCounter.Value = value; } internal AtomicCounter ConcurrencyCounter { get; } = new AtomicCounter(); DestinationState IReadOnlyList.this[int index] => index == 0 ? this : throw new IndexOutOfRangeException(); int IReadOnlyCollection.Count => 1; private Enumerator GetEnumerator() { return new Enumerator(this); } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } private struct Enumerator : IEnumerator { private bool _read; internal Enumerator(DestinationState instance) { Current = instance; _read = false; } public DestinationState Current { get; } object IEnumerator.Current => Current; public bool MoveNext() { if (!_read) { _read = true; return true; } return false; } public void Dispose() { } void IEnumerator.Reset() { throw new NotSupportedException(); } } } ================================================ FILE: src/ReverseProxy/Model/HttpContextFeaturesExtensions.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using Yarp.ReverseProxy.Model; using Yarp.ReverseProxy.Forwarder; namespace Microsoft.AspNetCore.Http; /// /// Extension methods for fetching proxy configuration from the current HttpContext. /// public static class HttpContextFeaturesExtensions { /// /// Retrieves the instance associated with the current request. /// public static RouteModel GetRouteModel(this HttpContext context) { var proxyFeature = context.GetReverseProxyFeature(); var route = proxyFeature.Route ?? throw new InvalidOperationException($"The {typeof(IReverseProxyFeature).FullName} is missing the {typeof(RouteModel).FullName}."); return route; } /// /// Retrieves the instance associated with the current request. /// public static IReverseProxyFeature GetReverseProxyFeature(this HttpContext context) { return context.Features.Get() ?? throw new InvalidOperationException($"{typeof(IReverseProxyFeature).FullName} is missing."); } /// /// Retrieves the instance associated with the current request, if any. /// public static IForwarderErrorFeature? GetForwarderErrorFeature(this HttpContext context) { return context.Features.Get(); } // Compare to ProxyPipelineInitializerMiddleware /// /// Replaces the assigned cluster and destinations in with the new , /// causing the request to be sent to the new cluster instead. /// public static void ReassignProxyRequest(this HttpContext context, ClusterState cluster) { var oldFeature = context.GetReverseProxyFeature(); var destinations = cluster.DestinationsState; var newFeature = new ReverseProxyFeature() { Route = oldFeature.Route, Cluster = cluster.Model, AllDestinations = destinations.AllDestinations, AvailableDestinations = destinations.AvailableDestinations, ProxiedDestination = oldFeature.ProxiedDestination, }; context.Features.Set(newFeature); } // ReassignProxyRequest overload to also replace the route when updating IReverseProxyFeature /// /// Replaces the assigned route, cluster, and destinations in with the new /// and new , causing the request to be sent using the new route to the new cluster. /// public static void ReassignProxyRequest(this HttpContext context, RouteModel route, ClusterState cluster) { var oldFeature = context.GetReverseProxyFeature(); var destinations = cluster.DestinationsState; var newFeature = new ReverseProxyFeature() { Route = route, Cluster = cluster.Model, AllDestinations = destinations.AllDestinations, AvailableDestinations = destinations.AvailableDestinations, ProxiedDestination = oldFeature.ProxiedDestination, }; context.Features.Set(newFeature); } } ================================================ FILE: src/ReverseProxy/Model/IClusterChangeListener.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. namespace Yarp.ReverseProxy.Model; /// /// Listener for changes in the clusters. /// public interface IClusterChangeListener { /// /// Gets called after a new has been added. /// /// Added instance. void OnClusterAdded(ClusterState cluster); /// /// Gets called after an existing has been changed. /// /// Changed instance. void OnClusterChanged(ClusterState cluster); /// /// Gets called after an existing has been removed. /// /// Removed instance. void OnClusterRemoved(ClusterState cluster); } ================================================ FILE: src/ReverseProxy/Model/IReverseProxyApplicationBuilder.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. namespace Microsoft.AspNetCore.Builder; /// /// An for building the `MapReverseProxy` pipeline. /// public interface IReverseProxyApplicationBuilder : IApplicationBuilder { } ================================================ FILE: src/ReverseProxy/Model/IReverseProxyFeature.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; namespace Yarp.ReverseProxy.Model; /// /// Stores the current proxy configuration used when processing the request. /// public interface IReverseProxyFeature { /// /// The route model for the current request. /// RouteModel Route { get; } /// /// The cluster model for the current request. /// ClusterModel Cluster { get; } /// /// All destinations for the current cluster. /// IReadOnlyList AllDestinations { get; } /// /// Cluster destinations that can handle the current request. This will initially include all destinations except those /// currently marked as unhealthy if health checks are enabled. /// IReadOnlyList AvailableDestinations { get; set; } /// /// The actual destination that the request was proxied to. /// DestinationState? ProxiedDestination { get; set; } } ================================================ FILE: src/ReverseProxy/Model/ProxyPipelineInitializerMiddleware.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Diagnostics; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using System.Threading; using Microsoft.AspNetCore.Http.Timeouts; using Microsoft.Extensions.Options; using Microsoft.Extensions.Logging; using Yarp.ReverseProxy.Utilities; namespace Yarp.ReverseProxy.Model; /// /// Initializes the proxy processing pipeline with the available healthy destinations. /// internal sealed class ProxyPipelineInitializerMiddleware { private readonly ILogger _logger; private readonly RequestDelegate _next; private readonly IOptionsMonitor _timeoutOptions; public ProxyPipelineInitializerMiddleware(RequestDelegate next, ILogger logger, IOptionsMonitor timeoutOptions) { ArgumentNullException.ThrowIfNull(logger); ArgumentNullException.ThrowIfNull(next); ArgumentNullException.ThrowIfNull(timeoutOptions); _logger = logger; _next = next; _timeoutOptions = timeoutOptions; } public Task Invoke(HttpContext context) { var endpoint = context.GetEndpoint() ?? throw new InvalidOperationException($"Routing Endpoint wasn't set for the current request."); var route = endpoint.Metadata.GetMetadata() ?? throw new InvalidOperationException($"Routing Endpoint is missing {typeof(RouteModel).FullName} metadata."); var cluster = route.Cluster; // TODO: Validate on load https://github.com/dotnet/yarp/issues/797 if (cluster is null) { Log.NoClusterFound(_logger, route.Config.RouteId); context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable; return Task.CompletedTask; } EnsureRequestTimeoutPolicyIsAppliedCorrectly(context, endpoint, route); var destinationsState = cluster.DestinationsState; context.Features.Set(new ReverseProxyFeature { Route = route, Cluster = cluster.Model, AllDestinations = destinationsState.AllDestinations, AvailableDestinations = destinationsState.AvailableDestinations, }); var activity = Observability.YarpActivitySource.CreateActivity("proxy.forwarder", ActivityKind.Server); return activity is null ? _next(context) : AwaitWithActivity(context, activity); } private async Task AwaitWithActivity(HttpContext context, Activity activity) { context.SetYarpActivity(activity); activity.Start(); try { await _next(context); } finally { activity.Dispose(); } } private void EnsureRequestTimeoutPolicyIsAppliedCorrectly(HttpContext context, Endpoint endpoint, RouteModel route) { // There's no way to detect the presence of the timeout middleware before this, only the options. if (endpoint.Metadata.GetMetadata() is { } requestTimeout && context.Features.Get() is null && // The feature is skipped if the request is already canceled. We'll handle canceled requests later for consistency. !context.RequestAborted.IsCancellationRequested && // The policy may set the timeout to null / infinite. TimeoutPolicyRequestedATimeoutBeSet(requestTimeout)) { // A timeout should have been set. // Out of an abundance of caution, refuse the request rather than allowing it to proceed without the configured timeout. ThrowIfDebuggerNotAttached(route); } void ThrowIfDebuggerNotAttached(RouteModel route) { // The feature is skipped if the debugger is attached. if (!Debugger.IsAttached) { Log.TimeoutNotApplied(_logger, route.Config.RouteId); throw new InvalidOperationException( $"The timeout was not applied for route '{route.Config.RouteId}', " + "ensure `IApplicationBuilder.UseRequestTimeouts()` is called between " + "`IApplicationBuilder.UseRouting()` and `IApplicationBuilder.UseEndpoints()`."); } } } private bool TimeoutPolicyRequestedATimeoutBeSet(RequestTimeoutAttribute requestTimeout) { if (requestTimeout.Timeout is not TimeSpan timeout) { if (requestTimeout.PolicyName is not string policyName) { Debug.Fail("Either Timeout or PolicyName should have been set."); return false; } if (!_timeoutOptions.CurrentValue.Policies.TryGetValue(policyName, out var policy)) { // This should only happen if the policy existed at some point, but the options were updated to remove it. return false; } if (policy.Timeout is null) { // The policy requested no timeout. return false; } timeout = policy.Timeout.Value; } return timeout != Timeout.InfiniteTimeSpan; } private static class Log { private static readonly Action _noClusterFound = LoggerMessage.Define( LogLevel.Information, EventIds.NoClusterFound, "Route '{routeId}' has no cluster information."); private static readonly Action _timeoutNotApplied = LoggerMessage.Define( LogLevel.Error, EventIds.TimeoutNotApplied, "The timeout was not applied for route '{routeId}', ensure `IApplicationBuilder.UseRequestTimeouts()` is called between `IApplicationBuilder.UseRouting()` and `IApplicationBuilder.UseEndpoints()`."); public static void NoClusterFound(ILogger logger, string routeId) { _noClusterFound(logger, routeId, null); } public static void TimeoutNotApplied(ILogger logger, string routeId) { _timeoutNotApplied(logger, routeId, null); } } } ================================================ FILE: src/ReverseProxy/Model/README.md ================================================ # ReverseProxy.RuntimeModel namespace Classes in this folder define the internal representation of ReverseProxy's runtime state used in perf-critical code paths. All classes should be immutable, and all members and members of members MUST be either: A) immutable B) `AtomicHolder` wrapping an immutable type `T`. C) Thread-safe (e.g. `AtomicCounter`) This ensures we can easily handle hot-swappable configurations without explicit synchronization overhead across threads, and each thread can operate safely with up-to-date yet consistent information (always the latest and consistent snapshot available when processing of a request starts). ## Class naming conventions * Classes named `*Info` (`RouteInfo`, `ClusterInfo`, `EndpointInfo`) represent the 3 primary abstractions in Reverse Proxy (Routes, Clusters and Destinations); * Classes named `*Config` (`RouteConfig`, `ClusterConfig`, `EndpointConfig`) represent portions of the 3 abstractions that only change in reaction to Reverse Proxy config changes. For example, when the health check interval for a cluster is updated, a new instance of `ClusterConfig` is created with the new values, and the corresponding `AtomicHolder` in `ClusterInfo` is updated to point at the new instance; * Classes named `*DynamicState` (`ClusterDynamicState`, `EndpointDynamicState`) represent portions of the 3 abstractions that change in reaction to Reverse Proxy's runtime state. For example, when new destinations are discovered for a cluster, a new instance of `ClusterDynamicState` is created with the new values, and the corresponding `AtomicHolder` in `ClusterInfo` is updated to point at the new instance; ================================================ FILE: src/ReverseProxy/Model/ReverseProxyApplicationBuilder.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; namespace Yarp.ReverseProxy.Model; public class ReverseProxyApplicationBuilder : IReverseProxyApplicationBuilder { private readonly IApplicationBuilder _applicationBuilder; public ReverseProxyApplicationBuilder(IApplicationBuilder applicationBuilder) { ArgumentNullException.ThrowIfNull(applicationBuilder); _applicationBuilder = applicationBuilder; } public IServiceProvider ApplicationServices { get => _applicationBuilder.ApplicationServices; set => _applicationBuilder.ApplicationServices = value; } public IFeatureCollection ServerFeatures => _applicationBuilder.ServerFeatures; public IDictionary Properties => _applicationBuilder.Properties; public RequestDelegate Build() => _applicationBuilder.Build(); public IApplicationBuilder New() => _applicationBuilder.New(); public IApplicationBuilder Use(Func middleware) => _applicationBuilder.Use(middleware); } ================================================ FILE: src/ReverseProxy/Model/ReverseProxyFeature.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; namespace Yarp.ReverseProxy.Model; /// /// Stores the current proxy configuration used when processing the request. /// public class ReverseProxyFeature : IReverseProxyFeature { private IReadOnlyList _availableDestinations = default!; /// public RouteModel Route { get; init; } = default!; /// public ClusterModel Cluster { get; set; } = default!; /// public IReadOnlyList AllDestinations { get; init; } = default!; /// public IReadOnlyList AvailableDestinations { get => _availableDestinations; set => _availableDestinations = value ?? throw new ArgumentNullException(nameof(value)); } /// public DestinationState? ProxiedDestination { get; set; } } ================================================ FILE: src/ReverseProxy/Model/RouteModel.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using Yarp.ReverseProxy.Configuration; using Yarp.ReverseProxy.Forwarder; namespace Yarp.ReverseProxy.Model; /// /// Immutable representation of the portions of a route /// that only change in reaction to configuration changes. /// /// /// All members must remain immutable to avoid thread safety issues. /// Instead, instances of are replaced /// in their entirety when values need to change. /// public sealed class RouteModel { /// /// Creates a new instance. /// public RouteModel( RouteConfig config, ClusterState? cluster, HttpTransformer transformer) { ArgumentNullException.ThrowIfNull(config); ArgumentNullException.ThrowIfNull(transformer); Config = config; Cluster = cluster; Transformer = transformer; } // May not be populated if the cluster config is missing. https://github.com/dotnet/yarp/issues/797 /// /// The instance associated with this route. /// public ClusterState? Cluster { get; } /// /// Transforms to apply for this route. /// public HttpTransformer Transformer { get; } /// /// The configuration data used to build this route. /// public RouteConfig Config { get; } internal bool HasConfigChanged(RouteConfig newConfig, ClusterState? cluster, int? routeRevision) { return Cluster != cluster || routeRevision != cluster?.Revision || !Config.Equals(newConfig); } } ================================================ FILE: src/ReverseProxy/Model/RouteState.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using Microsoft.AspNetCore.Http; namespace Yarp.ReverseProxy.Model; /// /// Representation of a route for use at runtime. /// internal sealed class RouteState { private volatile RouteModel _model = default!; public RouteState(string routeId) { ArgumentException.ThrowIfNullOrEmpty(routeId); RouteId = routeId; } public string RouteId { get; } /// /// Encapsulates parts of a route that can change atomically /// in reaction to config changes. /// internal RouteModel Model { get => _model; set { ArgumentNullException.ThrowIfNull(value); _model = value; } } /// /// Tracks changes to the cluster configuration for use with rebuilding the route endpoint. /// internal int? ClusterRevision { get; set; } /// /// A cached Endpoint that will be cleared and rebuilt if the RouteConfig or cluster config change. /// internal Endpoint? CachedEndpoint { get; set; } } ================================================ FILE: src/ReverseProxy/README.md ================================================ YARP (Yet Another Reverse Proxy) is a highly customizable reverse proxy built using .NET. The biggest differentiator between YARP and other reverse proxies is how it is built and packaged – YARP is supplied as a library and samples showing how to create a proxy that is customized to the needs of your specific scenarios. To learn more see the docs at https://learn.microsoft.com/aspnet/core/fundamentals/servers/yarp/getting-started, the GitHub repo at https://github.com/dotnet/yarp, and the 1.0 Announcement Blog post at https://devblogs.microsoft.com/dotnet/announcing-yarp-1-0-release/. ================================================ FILE: src/ReverseProxy/Routing/DirectForwardingIEndpointRouteBuilderExtensions.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Diagnostics.CodeAnalysis; using System.Net.Http; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; using Yarp.ReverseProxy.Forwarder; using Yarp.ReverseProxy.Transforms; using Yarp.ReverseProxy.Transforms.Builder; namespace Microsoft.AspNetCore.Builder; /// /// Extension methods for used to add direct forwarding to the ASP.NET Core request pipeline. /// public static class DirectForwardingIEndpointRouteBuilderExtensions { /// /// Adds direct forwarding of HTTP requests that match the specified pattern to a specific destination using default configuration for the outgoing request, default transforms, and default HTTP client. /// public static IEndpointConventionBuilder MapForwarder(this IEndpointRouteBuilder endpoints, [StringSyntax("Route")] string pattern, string destinationPrefix) { return endpoints.MapForwarder(pattern, destinationPrefix, ForwarderRequestConfig.Empty); } /// /// Adds direct forwarding of HTTP requests that match the specified pattern to a specific destination and target path applying route values from the pattern using default configuration for the outgoing request, and default HTTP client. /// public static IEndpointConventionBuilder MapForwarder(this IEndpointRouteBuilder endpoints, [StringSyntax("Route")] string pattern, string destinationPrefix, [StringSyntax("Route")] string targetPath) { return endpoints.MapForwarder(pattern, destinationPrefix, ForwarderRequestConfig.Empty, targetPath); } /// /// Adds direct forwarding of HTTP requests that match the specified pattern to a specific destination and target path applying route values from the pattern using customized configuration for the outgoing request, and default HTTP client. /// public static IEndpointConventionBuilder MapForwarder(this IEndpointRouteBuilder endpoints, [StringSyntax("Route")] string pattern, string destinationPrefix, ForwarderRequestConfig requestConfig, [StringSyntax("Route")] string targetPath) { return endpoints.MapForwarder(pattern, destinationPrefix, requestConfig, b => b.AddPathRouteValues(targetPath)); } /// /// Adds direct forwarding of HTTP requests that match the specified pattern to a specific destination using default configuration for the outgoing request, customized transforms, and default HTTP client. /// public static IEndpointConventionBuilder MapForwarder(this IEndpointRouteBuilder endpoints, [StringSyntax("Route")] string pattern, string destinationPrefix, Action configureTransform) { return endpoints.MapForwarder(pattern, destinationPrefix, ForwarderRequestConfig.Empty, configureTransform); } /// /// Adds direct forwarding of HTTP requests that match the specified pattern to a specific destination using customized configuration for the outgoing request, customized transforms, and default HTTP client. /// public static IEndpointConventionBuilder MapForwarder(this IEndpointRouteBuilder endpoints, [StringSyntax("Route")] string pattern, string destinationPrefix, ForwarderRequestConfig requestConfig, Action configureTransform) { var transformBuilder = endpoints.ServiceProvider.GetRequiredService(); var transformer = transformBuilder.Create(configureTransform); return endpoints.MapForwarder(pattern, destinationPrefix, requestConfig, transformer); } /// /// Adds direct forwarding of HTTP requests that match the specified pattern to a specific destination using customized configuration for the outgoing request, default transforms, and default HTTP client. /// public static IEndpointConventionBuilder MapForwarder(this IEndpointRouteBuilder endpoints, [StringSyntax("Route")] string pattern, string destinationPrefix, ForwarderRequestConfig requestConfig) { return endpoints.MapForwarder(pattern, destinationPrefix, requestConfig, HttpTransformer.Default); } /// /// Adds direct forwarding of HTTP requests that match the specified pattern to a specific destination using customized configuration for the outgoing request, customized transforms, and default HTTP client. /// public static IEndpointConventionBuilder MapForwarder(this IEndpointRouteBuilder endpoints, [StringSyntax("Route")] string pattern, string destinationPrefix, ForwarderRequestConfig requestConfig, HttpTransformer transformer) { var httpClientProvider = endpoints.ServiceProvider.GetRequiredService(); return endpoints.MapForwarder(pattern, destinationPrefix, requestConfig, transformer, httpClientProvider.HttpClient); } /// /// Adds direct forwarding of HTTP requests that match the specified pattern to a specific destination using customized configuration for the outgoing request, customized transforms, and customized HTTP client. /// public static IEndpointConventionBuilder MapForwarder(this IEndpointRouteBuilder endpoints, [StringSyntax("Route")] string pattern, string destinationPrefix, ForwarderRequestConfig requestConfig, HttpTransformer transformer, HttpMessageInvoker httpClient) { ArgumentNullException.ThrowIfNull(endpoints); ArgumentNullException.ThrowIfNull(destinationPrefix); ArgumentNullException.ThrowIfNull(httpClient); ArgumentNullException.ThrowIfNull(requestConfig); ArgumentNullException.ThrowIfNull(transformer); var forwarder = endpoints.ServiceProvider.GetRequiredService(); return endpoints.Map(pattern, async httpContext => { await forwarder.SendAsync(httpContext, destinationPrefix, httpClient, requestConfig, transformer); }); } } ================================================ FILE: src/ReverseProxy/Routing/HeaderMatcher.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Linq; using Microsoft.Net.Http.Headers; using Yarp.ReverseProxy.Configuration; namespace Yarp.ReverseProxy.Routing; /// /// A request header matcher used during routing. /// internal sealed class HeaderMatcher { /// /// Creates a new instance. /// public HeaderMatcher(string name, IReadOnlyList? values, HeaderMatchMode mode, bool isCaseSensitive) { if (string.IsNullOrEmpty(name)) { throw new ArgumentException("A header name is required.", nameof(name)); } if ((mode != HeaderMatchMode.Exists && mode != HeaderMatchMode.NotExists) && (values is null || values.Count == 0)) { throw new ArgumentException("Header values must have at least one value.", nameof(values)); } if ((mode == HeaderMatchMode.Exists || mode == HeaderMatchMode.NotExists) && values?.Count > 0) { throw new ArgumentException($"Header values must not be specified when using '{mode}'.", nameof(values)); } if (values is not null && values.Any(string.IsNullOrEmpty)) { throw new ArgumentNullException(nameof(values), "Header values must be not be empty."); } Name = name; Values = values?.ToArray() ?? Array.Empty(); Mode = mode; Comparison = isCaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase; Separator = name.Equals(HeaderNames.Cookie, StringComparison.OrdinalIgnoreCase) ? ';' : ','; } /// /// Name of the header to look for. /// public string Name { get; } /// /// Returns a read-only collection of acceptable header values used during routing. /// At least one value is required unless is set to /// or . /// public string[] Values { get; } /// /// Specifies how header values should be compared (e.g. exact matches Vs. by prefix). /// Defaults to . /// public HeaderMatchMode Mode { get; } public StringComparison Comparison { get; } public char Separator { get; } } ================================================ FILE: src/ReverseProxy/Routing/HeaderMatcherPolicy.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Matching; using Microsoft.Extensions.Primitives; using Yarp.ReverseProxy.Configuration; namespace Yarp.ReverseProxy.Routing; internal sealed class HeaderMatcherPolicy : MatcherPolicy, IEndpointComparerPolicy, IEndpointSelectorPolicy { /// // Run after HttpMethodMatcherPolicy (-1000) and HostMatcherPolicy (-100), but before default (0) public override int Order => -50; /// public IComparer Comparer => new HeaderMetadataEndpointComparer(); /// bool IEndpointSelectorPolicy.AppliesToEndpoints(IReadOnlyList endpoints) { ArgumentNullException.ThrowIfNull(endpoints); // When the node contains dynamic endpoints we can't make any assumptions. if (ContainsDynamicEndpoints(endpoints)) { return true; } return AppliesToEndpointsCore(endpoints); } private static bool AppliesToEndpointsCore(IReadOnlyList endpoints) { return endpoints.Any(e => { var metadata = e.Metadata.GetMetadata(); return metadata?.Matchers?.Length > 0; }); } /// public Task ApplyAsync(HttpContext httpContext, CandidateSet candidates) { ArgumentNullException.ThrowIfNull(httpContext); ArgumentNullException.ThrowIfNull(candidates); var headers = httpContext.Request.Headers; for (var i = 0; i < candidates.Count; i++) { if (!candidates.IsValidCandidate(i)) { continue; } var matchers = candidates[i].Endpoint.Metadata.GetMetadata()?.Matchers; if (matchers is null) { continue; } foreach (var matcher in matchers) { headers.TryGetValue(matcher.Name, out var requestHeaderValues); var valueIsEmpty = StringValues.IsNullOrEmpty(requestHeaderValues); var matched = matcher.Mode switch { HeaderMatchMode.Exists => !valueIsEmpty, HeaderMatchMode.NotExists => valueIsEmpty, HeaderMatchMode.ExactHeader => !valueIsEmpty && TryMatchExactOrPrefix(matcher, requestHeaderValues), HeaderMatchMode.HeaderPrefix => !valueIsEmpty && TryMatchExactOrPrefix(matcher, requestHeaderValues), HeaderMatchMode.Contains => !valueIsEmpty && TryMatchContainsOrNotContains(matcher, requestHeaderValues), HeaderMatchMode.NotContains => valueIsEmpty || TryMatchContainsOrNotContains(matcher, requestHeaderValues), _ => false }; if (!matched) { candidates.SetValidity(i, false); break; } } } return Task.CompletedTask; } private static bool TryMatchExactOrPrefix(HeaderMatcher matcher, StringValues requestHeaderValues) { var requestHeaderCount = requestHeaderValues.Count; for (var i = 0; i < requestHeaderCount; i++) { var requestValue = requestHeaderValues[i].AsSpan(); while (!requestValue.IsEmpty) { requestValue = requestValue.TrimStart(' '); // Find the end of the next value. // Separators inside a quote pair must be ignored as they are a part of the value. var separatorOrQuoteIndex = requestValue.IndexOfAny('"', matcher.Separator); while (separatorOrQuoteIndex != -1 && requestValue[separatorOrQuoteIndex] == '"') { var closingQuoteIndex = requestValue.Slice(separatorOrQuoteIndex + 1).IndexOf('"'); if (closingQuoteIndex == -1) { separatorOrQuoteIndex = -1; } else { var offset = separatorOrQuoteIndex + closingQuoteIndex + 2; separatorOrQuoteIndex = requestValue.Slice(offset).IndexOfAny('"', matcher.Separator); if (separatorOrQuoteIndex != -1) { separatorOrQuoteIndex += offset; } } } ReadOnlySpan value; if (separatorOrQuoteIndex == -1) { value = requestValue; requestValue = default; } else { value = requestValue.Slice(0, separatorOrQuoteIndex); requestValue = requestValue.Slice(separatorOrQuoteIndex + 1); } if (value.Length > 1 && value[0] == '"' && value[^1] == '"') { value = value.Slice(1, value.Length - 2); } foreach (var expectedValue in matcher.Values) { if (matcher.Mode == HeaderMatchMode.ExactHeader ? value.Equals(expectedValue, matcher.Comparison) : value.StartsWith(expectedValue, matcher.Comparison)) { return true; } } } } return false; } private static bool TryMatchContainsOrNotContains(HeaderMatcher matcher, StringValues requestHeaderValues) { Debug.Assert(matcher.Mode is HeaderMatchMode.Contains or HeaderMatchMode.NotContains, $"{matcher.Mode}"); var requestHeaderCount = requestHeaderValues.Count; for (var i = 0; i < requestHeaderCount; i++) { var requestValue = requestHeaderValues[i]; if (requestValue is null) { continue; } foreach (var expectedValue in matcher.Values) { if (requestValue.Contains(expectedValue, matcher.Comparison)) { return matcher.Mode != HeaderMatchMode.NotContains; } } } return matcher.Mode == HeaderMatchMode.NotContains; } private sealed class HeaderMetadataEndpointComparer : EndpointMetadataComparer { protected override int CompareMetadata(IHeaderMetadata? x, IHeaderMetadata? y) { return (y?.Matchers?.Length ?? 0).CompareTo(x?.Matchers?.Length ?? 0); } } } ================================================ FILE: src/ReverseProxy/Routing/HeaderMetadata.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Linq; namespace Yarp.ReverseProxy.Routing; /// /// Represents request header metadata used during routing. /// internal sealed class HeaderMetadata : IHeaderMetadata { public HeaderMetadata(IReadOnlyList matchers) { ArgumentNullException.ThrowIfNull(matchers); Matchers = matchers.ToArray(); } /// public HeaderMatcher[] Matchers { get; } } ================================================ FILE: src/ReverseProxy/Routing/IHeaderMetadata.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. namespace Yarp.ReverseProxy.Routing; /// /// Represents request header metadata used during routing. /// internal interface IHeaderMetadata { /// /// One or more matchers to apply to the request headers. /// HeaderMatcher[] Matchers { get; } } ================================================ FILE: src/ReverseProxy/Routing/IQueryParameterMetadata.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. namespace Yarp.ReverseProxy.Routing; /// /// Represents request query parameter metadata used during routing. /// internal interface IQueryParameterMetadata { /// /// One or more matchers to apply to the request query parameters. /// QueryParameterMatcher[] Matchers { get; } } ================================================ FILE: src/ReverseProxy/Routing/ProxyEndpointFactory.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Linq; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Cors.Infrastructure; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Timeouts; using Microsoft.AspNetCore.RateLimiting; using Microsoft.AspNetCore.OutputCaching; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Patterns; using Yarp.ReverseProxy.Model; using CorsConstants = Yarp.ReverseProxy.Configuration.CorsConstants; using AuthorizationConstants = Yarp.ReverseProxy.Configuration.AuthorizationConstants; using RateLimitingConstants = Yarp.ReverseProxy.Configuration.RateLimitingConstants; using TimeoutPolicyConstants = Yarp.ReverseProxy.Configuration.TimeoutPolicyConstants; namespace Yarp.ReverseProxy.Routing; internal sealed class ProxyEndpointFactory { private static readonly IAuthorizeData _defaultAuthorization = new AuthorizeAttribute(); private static readonly DisableRateLimitingAttribute _disableRateLimit = new(); private static readonly DisableRequestTimeoutAttribute _disableRequestTimeout = new(); private static readonly IEnableCorsAttribute _defaultCors = new EnableCorsAttribute(); private static readonly IDisableCorsAttribute _disableCors = new DisableCorsAttribute(); private static readonly IAllowAnonymous _allowAnonymous = new AllowAnonymousAttribute(); private RequestDelegate? _pipeline; public Endpoint CreateEndpoint(RouteModel route, IReadOnlyList> conventions) { var config = route.Config; var match = config.Match; // Catch-all pattern when no path was specified var pathPattern = string.IsNullOrEmpty(match.Path) ? "/{**catchall}" : match.Path; var endpointBuilder = new RouteEndpointBuilder( requestDelegate: _pipeline ?? throw new InvalidOperationException("The pipeline hasn't been provided yet."), routePattern: RoutePatternFactory.Parse(pathPattern), order: config.Order.GetValueOrDefault()) { DisplayName = config.RouteId }; endpointBuilder.Metadata.Add(route); if (match.Hosts is not null && match.Hosts.Count != 0) { endpointBuilder.Metadata.Add(new HostAttribute(match.Hosts.ToArray())); } if (match.Headers is not null && match.Headers.Count > 0) { var matchers = new List(match.Headers.Count); foreach (var header in match.Headers) { matchers.Add(new HeaderMatcher(header.Name, header.Values, header.Mode, header.IsCaseSensitive)); } endpointBuilder.Metadata.Add(new HeaderMetadata(matchers)); } if (match.QueryParameters is not null && match.QueryParameters.Count > 0) { var matchers = new List(match.QueryParameters.Count); foreach (var queryparam in match.QueryParameters) { matchers.Add(new QueryParameterMatcher(queryparam.Name, queryparam.Values, queryparam.Mode, queryparam.IsCaseSensitive)); } endpointBuilder.Metadata.Add(new QueryParameterMetadata(matchers)); } bool acceptCorsPreflight; if (string.Equals(CorsConstants.Default, config.CorsPolicy, StringComparison.OrdinalIgnoreCase)) { endpointBuilder.Metadata.Add(_defaultCors); acceptCorsPreflight = true; } else if (string.Equals(CorsConstants.Disable, config.CorsPolicy, StringComparison.OrdinalIgnoreCase)) { endpointBuilder.Metadata.Add(_disableCors); acceptCorsPreflight = true; } else if (!string.IsNullOrEmpty(config.CorsPolicy)) { endpointBuilder.Metadata.Add(new EnableCorsAttribute(config.CorsPolicy)); acceptCorsPreflight = true; } else { acceptCorsPreflight = false; } if (match.Methods is not null && match.Methods.Count > 0) { endpointBuilder.Metadata.Add(new HttpMethodMetadata(match.Methods, acceptCorsPreflight)); } if (string.Equals(AuthorizationConstants.Default, config.AuthorizationPolicy, StringComparison.OrdinalIgnoreCase)) { endpointBuilder.Metadata.Add(_defaultAuthorization); } else if (string.Equals(AuthorizationConstants.Anonymous, config.AuthorizationPolicy, StringComparison.OrdinalIgnoreCase)) { endpointBuilder.Metadata.Add(_allowAnonymous); } else if (!string.IsNullOrEmpty(config.AuthorizationPolicy)) { endpointBuilder.Metadata.Add(new AuthorizeAttribute(config.AuthorizationPolicy)); } if (string.Equals(RateLimitingConstants.Default, config.RateLimiterPolicy, StringComparison.OrdinalIgnoreCase)) { // No-op (middleware applies the default) } else if (string.Equals(RateLimitingConstants.Disable, config.RateLimiterPolicy, StringComparison.OrdinalIgnoreCase)) { endpointBuilder.Metadata.Add(_disableRateLimit); } else if (!string.IsNullOrEmpty(config.RateLimiterPolicy)) { endpointBuilder.Metadata.Add(new EnableRateLimitingAttribute(config.RateLimiterPolicy)); } if (!string.IsNullOrEmpty(config.OutputCachePolicy)) { endpointBuilder.Metadata.Add(new OutputCacheAttribute { PolicyName = config.OutputCachePolicy }); } if (string.Equals(TimeoutPolicyConstants.Disable, config.TimeoutPolicy, StringComparison.OrdinalIgnoreCase)) { endpointBuilder.Metadata.Add(_disableRequestTimeout); } // The config validator shouldn't allow both TimeoutPolicy and Timeout, so we don't have to consider priority. else if (!string.IsNullOrEmpty(config.TimeoutPolicy)) { endpointBuilder.Metadata.Add(new RequestTimeoutAttribute(config.TimeoutPolicy)); } else if (config.Timeout.HasValue) { endpointBuilder.Metadata.Add(new RequestTimeoutAttribute((int)config.Timeout.Value.TotalMilliseconds)); } for (var i = 0; i < conventions.Count; i++) { conventions[i](endpointBuilder); } return endpointBuilder.Build(); } public void SetProxyPipeline(RequestDelegate pipeline) { ArgumentNullException.ThrowIfNull(pipeline); _pipeline = pipeline; } } ================================================ FILE: src/ReverseProxy/Routing/QueryParameterMatcher.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Linq; using Yarp.ReverseProxy.Configuration; namespace Yarp.ReverseProxy.Routing; /// /// A request query parameter matcher used during routing. /// internal sealed class QueryParameterMatcher { /// /// Creates a new instance. /// public QueryParameterMatcher(string name, IReadOnlyList? values, QueryParameterMatchMode mode, bool isCaseSensitive) { ArgumentException.ThrowIfNullOrEmpty(name); if (mode != QueryParameterMatchMode.Exists && (values is null || values.Count == 0)) { throw new ArgumentException("Query parameter values must have at least one value.", nameof(values)); } if (mode == QueryParameterMatchMode.Exists && values?.Count > 0) { throw new ArgumentException($"Query parameter values must not be specified when using '{nameof(QueryParameterMatchMode.Exists)}'.", nameof(values)); } if (values is not null && values.Any(string.IsNullOrEmpty)) { throw new ArgumentNullException(nameof(values), "Query parameter values must not be empty."); } Name = name; Values = values?.ToArray() ?? Array.Empty(); Mode = mode; Comparison = isCaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase; } /// /// Name of the query parameter to look for. /// public string Name { get; } /// /// Returns a read-only collection of acceptable query parameter values used during routing. /// At least one value is required unless is set to . /// public string[] Values { get; } /// /// Specifies how query parameter values should be compared (e.g. exact matches Vs. contains). /// Defaults to . /// public QueryParameterMatchMode Mode { get; } public StringComparison Comparison { get; } } ================================================ FILE: src/ReverseProxy/Routing/QueryParameterMatcherPolicy.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Matching; using Microsoft.Extensions.Primitives; using Yarp.ReverseProxy.Configuration; namespace Yarp.ReverseProxy.Routing; internal sealed class QueryParameterMatcherPolicy : MatcherPolicy, IEndpointComparerPolicy, IEndpointSelectorPolicy { /// // Run after HttpMethodMatcherPolicy (-1000) and HostMatcherPolicy (-100), and HeaderMatcherPolicy (-50), but before default (0) public override int Order => -25; /// public IComparer Comparer => new QueryParameterMetadataEndpointComparer(); /// bool IEndpointSelectorPolicy.AppliesToEndpoints(IReadOnlyList endpoints) { ArgumentNullException.ThrowIfNull(endpoints); // When the node contains dynamic endpoints we can't make any assumptions. if (ContainsDynamicEndpoints(endpoints)) { return true; } return AppliesToEndpointsCore(endpoints); } private static bool AppliesToEndpointsCore(IReadOnlyList endpoints) { return endpoints.Any(e => { var metadata = e.Metadata.GetMetadata(); return metadata?.Matchers?.Length > 0; }); } /// public Task ApplyAsync(HttpContext httpContext, CandidateSet candidates) { ArgumentNullException.ThrowIfNull(httpContext); ArgumentNullException.ThrowIfNull(candidates); var query = httpContext.Request.Query; for (var i = 0; i < candidates.Count; i++) { if (!candidates.IsValidCandidate(i)) { continue; } var matchers = candidates[i].Endpoint.Metadata.GetMetadata()?.Matchers; if (matchers is null) { continue; } foreach (var matcher in matchers) { query.TryGetValue(matcher.Name, out var requestQueryParameterValues); var valueIsEmpty = StringValues.IsNullOrEmpty(requestQueryParameterValues); var matched = matcher.Mode switch { QueryParameterMatchMode.Exists => !valueIsEmpty, QueryParameterMatchMode.Exact => !valueIsEmpty && TryMatch(matcher, requestQueryParameterValues), QueryParameterMatchMode.Prefix => !valueIsEmpty && TryMatch(matcher, requestQueryParameterValues), QueryParameterMatchMode.Contains => !valueIsEmpty && TryMatch(matcher, requestQueryParameterValues), QueryParameterMatchMode.NotContains => valueIsEmpty || TryMatch(matcher, requestQueryParameterValues), _ => false }; if (!matched) { candidates.SetValidity(i, false); break; } } } return Task.CompletedTask; } private static bool TryMatch(QueryParameterMatcher matcher, StringValues requestHeaderValues) { var requestHeaderCount = requestHeaderValues.Count; for (var i = 0; i < requestHeaderCount; i++) { var requestValue = requestHeaderValues[i]; if (requestValue is null) { continue; } foreach (var expectedValue in matcher.Values) { if (TryMatch(matcher, requestValue, expectedValue)) { return matcher.Mode != QueryParameterMatchMode.NotContains; } } } return matcher.Mode == QueryParameterMatchMode.NotContains; static bool TryMatch(QueryParameterMatcher matcher, string queryValue, string expectedValue) { return matcher.Mode switch { QueryParameterMatchMode.Exact => queryValue.Equals(expectedValue, matcher.Comparison), QueryParameterMatchMode.Prefix => queryValue.StartsWith(expectedValue, matcher.Comparison), _ => queryValue.Contains(expectedValue, matcher.Comparison) }; } } private sealed class QueryParameterMetadataEndpointComparer : EndpointMetadataComparer { protected override int CompareMetadata(IQueryParameterMetadata? x, IQueryParameterMetadata? y) { return (y?.Matchers?.Length ?? 0).CompareTo(x?.Matchers?.Length ?? 0); } } } ================================================ FILE: src/ReverseProxy/Routing/QueryParameterMetadata.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Linq; namespace Yarp.ReverseProxy.Routing; /// /// Represents request query parameter metadata used during routing. /// internal sealed class QueryParameterMetadata : IQueryParameterMetadata { public QueryParameterMetadata(IReadOnlyList matchers) { ArgumentNullException.ThrowIfNull(matchers); Matchers = matchers.ToArray(); } /// public QueryParameterMatcher[] Matchers { get; } } ================================================ FILE: src/ReverseProxy/Routing/ReverseProxyConventionBuilder.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Linq; using Yarp.ReverseProxy.Configuration; using Yarp.ReverseProxy.Model; namespace Microsoft.AspNetCore.Builder; public class ReverseProxyConventionBuilder : IEndpointConventionBuilder { private readonly List> _conventions; internal ReverseProxyConventionBuilder(List> conventions) { ArgumentNullException.ThrowIfNull(conventions); _conventions = conventions; } /// /// Adds the specified convention to the builder. Conventions are used to customize instances. /// /// The convention to add to the builder. public void Add(Action convention) { ArgumentNullException.ThrowIfNull(convention); _conventions.Add(convention); } /// /// Configures the endpoints for all routes /// /// The convention to add to the builder. /// public ReverseProxyConventionBuilder ConfigureEndpoints(Action convention) { ArgumentNullException.ThrowIfNull(convention); void Action(EndpointBuilder endpointBuilder) { var conventionBuilder = new EndpointBuilderConventionBuilder(endpointBuilder); convention(conventionBuilder); } Add(Action); return this; } /// /// Configures the endpoints for all routes /// /// The convention to add to the builder. /// public ReverseProxyConventionBuilder ConfigureEndpoints(Action convention) { ArgumentNullException.ThrowIfNull(convention); void Action(EndpointBuilder endpointBuilder) { var route = endpointBuilder.Metadata.OfType().Single(); var conventionBuilder = new EndpointBuilderConventionBuilder(endpointBuilder); convention(conventionBuilder, route.Config); } Add(Action); return this; } /// /// Configures the endpoints for all routes /// /// The convention to add to the builder. /// public ReverseProxyConventionBuilder ConfigureEndpoints(Action convention) { ArgumentNullException.ThrowIfNull(convention); void Action(EndpointBuilder endpointBuilder) { var routeModel = endpointBuilder.Metadata.OfType().Single(); var clusterConfig = routeModel.Cluster?.Model.Config; var routeConfig = routeModel.Config; var conventionBuilder = new EndpointBuilderConventionBuilder(endpointBuilder); convention(conventionBuilder, routeConfig, clusterConfig); } Add(Action); return this; } private sealed class EndpointBuilderConventionBuilder : IEndpointConventionBuilder { private readonly EndpointBuilder _endpointBuilder; public EndpointBuilderConventionBuilder(EndpointBuilder endpointBuilder) { _endpointBuilder = endpointBuilder; } public void Add(Action convention) { convention(_endpointBuilder); } } } ================================================ FILE: src/ReverseProxy/Routing/ReverseProxyIEndpointRouteBuilderExtensions.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Linq; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; using Yarp.ReverseProxy.Forwarder; using Yarp.ReverseProxy.Limits; using Yarp.ReverseProxy.Management; using Yarp.ReverseProxy.Model; using Yarp.ReverseProxy.Routing; namespace Microsoft.AspNetCore.Builder; /// /// Extension methods for /// used to add Reverse Proxy to the ASP .NET Core request pipeline. /// public static class ReverseProxyIEndpointRouteBuilderExtensions { /// /// Adds Reverse Proxy routes to the route table using the default processing pipeline. /// public static ReverseProxyConventionBuilder MapReverseProxy(this IEndpointRouteBuilder endpoints) { return endpoints.MapReverseProxy(app => { app.UseSessionAffinity(); app.UseLoadBalancing(); app.UsePassiveHealthChecks(); }); } /// /// Adds Reverse Proxy routes to the route table with the customized processing pipeline. The pipeline includes /// by default the initialization step and the final proxy step, but not LoadBalancingMiddleware or other intermediate components. /// public static ReverseProxyConventionBuilder MapReverseProxy(this IEndpointRouteBuilder endpoints, Action configureApp) { ArgumentNullException.ThrowIfNull(endpoints); ArgumentNullException.ThrowIfNull(configureApp); var proxyAppBuilder = new ReverseProxyApplicationBuilder(endpoints.CreateApplicationBuilder()); proxyAppBuilder.UseMiddleware(); configureApp(proxyAppBuilder); proxyAppBuilder.UseMiddleware(); proxyAppBuilder.UseMiddleware(); var app = proxyAppBuilder.Build(); var proxyEndpointFactory = endpoints.ServiceProvider.GetRequiredService(); proxyEndpointFactory.SetProxyPipeline(app); return GetOrCreateDataSource(endpoints).DefaultBuilder; } private static ProxyConfigManager GetOrCreateDataSource(IEndpointRouteBuilder endpoints) { var dataSource = endpoints.DataSources.OfType().FirstOrDefault(); if (dataSource is null) { dataSource = endpoints.ServiceProvider.GetRequiredService(); endpoints.DataSources.Add(dataSource); // Config validation is async but startup is sync. We want this to block so that A) any validation errors can prevent // the app from starting, and B) so that all the config is ready before the server starts accepting requests. // Reloads will be async. dataSource.InitialLoadAsync().GetAwaiter().GetResult(); } return dataSource; } } ================================================ FILE: src/ReverseProxy/ServiceDiscovery/DnsDestinationResolver.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Net; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; using Yarp.ReverseProxy.Configuration; namespace Yarp.ReverseProxy.ServiceDiscovery; /// /// Implementation of which resolves host names to IP addresses using DNS. /// internal sealed class DnsDestinationResolver : IDestinationResolver { private readonly IOptionsMonitor _options; /// /// Initializes a new instance. /// /// The options. public DnsDestinationResolver(IOptionsMonitor options) { _options = options; } /// public async ValueTask ResolveDestinationsAsync(IReadOnlyDictionary destinations, CancellationToken cancellationToken) { var options = _options.CurrentValue; Dictionary results = new(); var tasks = new List>>(destinations.Count); foreach (var (destinationId, destinationConfig) in destinations) { tasks.Add(ResolveHostAsync(options, destinationId, destinationConfig, cancellationToken)); } await Task.WhenAll(tasks); foreach (var task in tasks) { var configs = await task; foreach (var (name, config) in configs) { results[name] = config; } } var changeToken = options.RefreshPeriod switch { { } refreshPeriod when refreshPeriod > TimeSpan.Zero => new CancellationChangeToken(new CancellationTokenSource(refreshPeriod).Token), _ => null, }; return new ResolvedDestinationCollection(results, changeToken); } private static async Task> ResolveHostAsync( DnsDestinationResolverOptions options, string originalName, DestinationConfig originalConfig, CancellationToken cancellationToken) { var originalUri = new Uri(originalConfig.Address); var originalHost = originalConfig.Host is { Length: > 0 } host ? host : originalUri.Authority; var hostName = originalUri.DnsSafeHost; IPAddress[] addresses; try { addresses = options.AddressFamily switch { { } addressFamily => await Dns.GetHostAddressesAsync(hostName, addressFamily, cancellationToken).ConfigureAwait(false), null => await Dns.GetHostAddressesAsync(hostName, cancellationToken).ConfigureAwait(false) }; } catch (Exception exception) { throw new InvalidOperationException($"Failed to resolve host '{hostName}'. See {nameof(Exception.InnerException)} for details.", exception); } var results = new List<(string Name, DestinationConfig Config)>(addresses.Length); var uriBuilder = new UriBuilder(originalUri); var healthUri = originalConfig.Health is { Length: > 0 } health ? new Uri(health) : null; var healthUriBuilder = healthUri is { } ? new UriBuilder(healthUri) : null; foreach (var address in addresses) { var addressString = address.ToString(); uriBuilder.Host = addressString; var resolvedAddress = uriBuilder.Uri.ToString(); var healthAddress = originalConfig.Health; if (healthUriBuilder is not null) { healthUriBuilder.Host = addressString; healthAddress = healthUriBuilder.Uri.ToString(); } var name = $"{originalName}[{addressString}]"; var config = originalConfig with { Host = originalHost, Address = resolvedAddress, Health = healthAddress }; results.Add((name, config)); } return results; } } ================================================ FILE: src/ReverseProxy/ServiceDiscovery/DnsDestinationResolverOptions.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Net.Sockets; namespace Yarp.ReverseProxy.ServiceDiscovery; /// /// Options for . /// public class DnsDestinationResolverOptions { /// /// The period between requesting a refresh of a resolved name. /// /// /// Defaults to 5 minutes. /// public TimeSpan? RefreshPeriod { get; set; } = TimeSpan.FromMinutes(5); /// /// The optional address family to query for. /// Use for IPv4 addresses and for IPv6 addresses. /// /// /// Defaults to (any address). /// public AddressFamily? AddressFamily { get; set; } } ================================================ FILE: src/ReverseProxy/ServiceDiscovery/IDestinationResolver.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Yarp.ReverseProxy.Configuration; namespace Yarp.ReverseProxy.ServiceDiscovery; /// /// Resolves destination addresses. /// public interface IDestinationResolver { /// /// Resolves the provided destinations and returns resolved destinations. /// /// The destinations to resolve. /// The cancellation token. /// /// The resolved destinations and a change token used to indicate when resolution should be performed again. /// ValueTask ResolveDestinationsAsync( IReadOnlyDictionary destinations, CancellationToken cancellationToken); } ================================================ FILE: src/ReverseProxy/ServiceDiscovery/NoOpDestinationResolver.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Yarp.ReverseProxy.Configuration; namespace Yarp.ReverseProxy.ServiceDiscovery; /// /// An which performs no action. /// internal sealed class NoOpDestinationResolver : IDestinationResolver { public ValueTask ResolveDestinationsAsync(IReadOnlyDictionary destinations, CancellationToken cancellationToken) => new(new ResolvedDestinationCollection(destinations, changeToken: null)); } ================================================ FILE: src/ReverseProxy/ServiceDiscovery/ResolvedDestinationCollection.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; using Microsoft.Extensions.Primitives; using Yarp.ReverseProxy.Configuration; namespace Yarp.ReverseProxy.ServiceDiscovery { /// /// Represents a collection of resolved destinations. /// public sealed class ResolvedDestinationCollection { public ResolvedDestinationCollection(IReadOnlyDictionary destinations, IChangeToken? changeToken) { Destinations = destinations; ChangeToken = changeToken; } /// /// Gets the map of destination names to destination configurations. /// public IReadOnlyDictionary Destinations { get; init; } /// /// Gets the optional change token used to signal when this collection should be refreshed. /// public IChangeToken? ChangeToken { get; init; } } } ================================================ FILE: src/ReverseProxy/SessionAffinity/AffinitizeTransform.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Diagnostics; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Yarp.ReverseProxy.Model; using Yarp.ReverseProxy.Transforms; namespace Yarp.ReverseProxy.SessionAffinity; /// /// Affinitizes the request to a chosen . /// internal sealed class AffinitizeTransform : ResponseTransform { private readonly ISessionAffinityPolicy _sessionAffinityPolicy; public AffinitizeTransform(ISessionAffinityPolicy sessionAffinityPolicy) { ArgumentNullException.ThrowIfNull(sessionAffinityPolicy); _sessionAffinityPolicy = sessionAffinityPolicy; } public override ValueTask ApplyAsync(ResponseTransformContext context) { var proxyFeature = context.HttpContext.GetReverseProxyFeature(); var options = proxyFeature.Cluster.Config.SessionAffinity; // The transform should only be added to routes that have affinity enabled. // However, the cluster can be re-assigned dynamically. if (options is null || !options.Enabled.GetValueOrDefault()) { return default; } Debug.Assert(proxyFeature.Route.Cluster is not null); Debug.Assert(proxyFeature.ProxiedDestination is not null); return _sessionAffinityPolicy.AffinitizeResponseAsync( context.HttpContext, proxyFeature.Route.Cluster, options, proxyFeature.ProxiedDestination, context.CancellationToken); } } ================================================ FILE: src/ReverseProxy/SessionAffinity/AffinitizeTransformProvider.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Frozen; using System.Collections.Generic; using Yarp.ReverseProxy.Transforms.Builder; using Yarp.ReverseProxy.Utilities; namespace Yarp.ReverseProxy.SessionAffinity; internal sealed class AffinitizeTransformProvider : ITransformProvider { private readonly FrozenDictionary _sessionAffinityPolicies; public AffinitizeTransformProvider(IEnumerable sessionAffinityPolicies) { ArgumentNullException.ThrowIfNull(sessionAffinityPolicies); _sessionAffinityPolicies = sessionAffinityPolicies.ToDictionaryByUniqueId(p => p.Name); } 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 policy = context.Cluster.SessionAffinity.Policy; if (string.IsNullOrEmpty(policy)) { // The default. policy = SessionAffinityConstants.Policies.HashCookie; } if (!_sessionAffinityPolicies.ContainsKey(policy)) { context.Errors.Add(new ArgumentException($"No matching {nameof(ISessionAffinityPolicy)} found for the session affinity policy '{policy}' set on the cluster '{context.Cluster.ClusterId}'.")); } } public void Apply(TransformBuilderContext context) { var options = context.Cluster?.SessionAffinity; if (options is not null && options.Enabled.GetValueOrDefault()) { var policy = _sessionAffinityPolicies.GetRequiredServiceById(options.Policy, SessionAffinityConstants.Policies.HashCookie); context.ResponseTransforms.Add(new AffinitizeTransform(policy)); } } } ================================================ FILE: src/ReverseProxy/SessionAffinity/AffinityHelpers.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using Microsoft.AspNetCore.Http; using Yarp.ReverseProxy.Configuration; namespace Yarp.ReverseProxy.SessionAffinity; internal static class AffinityHelpers { internal static CookieOptions CreateCookieOptions(SessionAffinityCookieConfig? config, bool isHttps, TimeProvider timeProvider) { return new CookieOptions { Path = config?.Path ?? "/", SameSite = config?.SameSite ?? SameSiteMode.Unspecified, HttpOnly = config?.HttpOnly ?? true, MaxAge = config?.MaxAge, Domain = config?.Domain, IsEssential = config?.IsEssential ?? false, Secure = config?.SecurePolicy == CookieSecurePolicy.Always || (config?.SecurePolicy == CookieSecurePolicy.SameAsRequest && isHttps), Expires = config?.Expiration is not null ? timeProvider.GetUtcNow().Add(config.Expiration.Value) : default(DateTimeOffset?), }; } } ================================================ FILE: src/ReverseProxy/SessionAffinity/AffinityResult.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; using Yarp.ReverseProxy.Model; namespace Yarp.ReverseProxy.SessionAffinity; /// /// Affinity resolution result. /// public readonly struct AffinityResult { public IReadOnlyList? Destinations { get; } public AffinityStatus Status { get; } public AffinityResult(IReadOnlyList? destinations, AffinityStatus status) { Destinations = destinations; Status = status; } } ================================================ FILE: src/ReverseProxy/SessionAffinity/AffinityStatus.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. namespace Yarp.ReverseProxy.SessionAffinity; /// /// Affinity resolution status. /// public enum AffinityStatus { OK, AffinityKeyNotSet, AffinityKeyExtractionFailed, DestinationNotFound } ================================================ FILE: src/ReverseProxy/SessionAffinity/AppBuilderSessionAffinityExtensions.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Yarp.ReverseProxy.SessionAffinity; namespace Microsoft.AspNetCore.Builder; /// /// Extensions for adding proxy middleware to the pipeline. /// public static class AppBuilderSessionAffinityExtensions { /// /// 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. /// public static IReverseProxyApplicationBuilder UseSessionAffinity(this IReverseProxyApplicationBuilder builder) { builder.UseMiddleware(); return builder; } } ================================================ FILE: src/ReverseProxy/SessionAffinity/ArrCookieSessionAffinityPolicy.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Runtime.CompilerServices; using System.Security.Cryptography; using System.Text; using Microsoft.Extensions.Logging; using Yarp.ReverseProxy.Model; namespace Yarp.ReverseProxy.SessionAffinity; internal sealed class ArrCookieSessionAffinityPolicy : BaseHashCookieSessionAffinityPolicy { private readonly ConditionalWeakTable _hashes = new(); public ArrCookieSessionAffinityPolicy( TimeProvider timeProvider, ILogger logger) : base(timeProvider, logger) { } public override string Name => SessionAffinityConstants.Policies.ArrCookie; protected override string GetDestinationHash(DestinationState d) { return _hashes.GetValue(d, static d => { // Matches the format used by ARR var destinationIdBytes = Encoding.Unicode.GetBytes(d.DestinationId.ToLowerInvariant()); var hashBytes = SHA256.HashData(destinationIdBytes); return Convert.ToHexString(hashBytes); }); } } ================================================ FILE: src/ReverseProxy/SessionAffinity/BaseEncryptedSessionAffinityPolicy.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Text; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Yarp.ReverseProxy.Configuration; using Yarp.ReverseProxy.Model; namespace Yarp.ReverseProxy.SessionAffinity; internal abstract class BaseEncryptedSessionAffinityPolicy : ISessionAffinityPolicy { private readonly IDataProtector _dataProtector; protected static readonly object AffinityKeyId = new object(); protected readonly ILogger Logger; protected BaseEncryptedSessionAffinityPolicy(IDataProtectionProvider dataProtectionProvider, ILogger logger) { ArgumentNullException.ThrowIfNull(logger); _dataProtector = dataProtectionProvider?.CreateProtector(GetType().FullName!) ?? throw new ArgumentNullException(nameof(dataProtectionProvider)); Logger = logger; } public abstract string Name { get; } public void AffinitizeResponse(HttpContext context, ClusterState cluster, SessionAffinityConfig config, DestinationState destination) { if (!config.Enabled.GetValueOrDefault()) { throw new InvalidOperationException($"Session affinity is disabled for cluster."); } if (context.RequestAborted.IsCancellationRequested) { // Avoid wasting time if the client is already gone. return; } // Affinity key is set on the response only if it's a new affinity. if (!context.Items.ContainsKey(AffinityKeyId)) { var affinityKey = GetDestinationAffinityKey(destination); SetAffinityKey(context, cluster, config, affinityKey); } } public virtual AffinityResult FindAffinitizedDestinations(HttpContext context, ClusterState cluster, SessionAffinityConfig config, IReadOnlyList destinations) { if (!config.Enabled.GetValueOrDefault()) { throw new InvalidOperationException($"Session affinity is disabled for cluster {cluster.ClusterId}."); } var requestAffinityKey = GetRequestAffinityKey(context, cluster, config); if (requestAffinityKey.Key is null) { return new AffinityResult(null, requestAffinityKey.ExtractedSuccessfully ? AffinityStatus.AffinityKeyNotSet : AffinityStatus.AffinityKeyExtractionFailed); } IReadOnlyList? matchingDestinations = null; if (destinations.Count > 0) { for (var i = 0; i < destinations.Count; i++) { // TODO: Add fast destination lookup by ID if (requestAffinityKey.Key.Equals(GetDestinationAffinityKey(destinations[i]))) { // It's allowed to affinitize a request to a pool of destinations to enable load-balancing among them. // However, we currently stop after the first match found to avoid performance degradation. matchingDestinations = destinations[i]; break; } } if (matchingDestinations is null) { Log.DestinationMatchingToAffinityKeyNotFound(Logger, cluster.ClusterId); } } else { Log.AffinityCannotBeEstablishedBecauseNoDestinationsFound(Logger, cluster.ClusterId); } // Empty destination list passed to this method is handled the same way as if no matching destinations are found. if (matchingDestinations is null) { return new AffinityResult(null, AffinityStatus.DestinationNotFound); } context.Items[AffinityKeyId] = requestAffinityKey; return new AffinityResult(matchingDestinations, AffinityStatus.OK); } protected abstract T GetDestinationAffinityKey(DestinationState destination); protected abstract (T? Key, bool ExtractedSuccessfully) GetRequestAffinityKey(HttpContext context, ClusterState cluster, SessionAffinityConfig config); protected abstract void SetAffinityKey(HttpContext context, ClusterState cluster, SessionAffinityConfig config, T unencryptedKey); protected string Protect(string unencryptedKey) { if (string.IsNullOrEmpty(unencryptedKey)) { return unencryptedKey; } var userData = Encoding.UTF8.GetBytes(unencryptedKey); var protectedData = _dataProtector.Protect(userData); return Convert.ToBase64String(protectedData).TrimEnd('='); } protected (string? Key, bool ExtractedSuccessfully) Unprotect(string? encryptedRequestKey) { if (string.IsNullOrEmpty(encryptedRequestKey)) { return (Key: null, ExtractedSuccessfully: true); } try { var keyBytes = Convert.FromBase64String(Pad(encryptedRequestKey)); var decryptedKeyBytes = _dataProtector.Unprotect(keyBytes); if (decryptedKeyBytes is null) { Log.RequestAffinityKeyDecryptionFailed(Logger, null); return (Key: null, ExtractedSuccessfully: false); } return (Key: Encoding.UTF8.GetString(decryptedKeyBytes), ExtractedSuccessfully: true); } catch (Exception ex) { Log.RequestAffinityKeyDecryptionFailed(Logger, ex); return (Key: null, ExtractedSuccessfully: false); } } private static string Pad(string text) { var padding = 3 - ((text.Length + 3) % 4); if (padding == 0) { return text; } return text + new string('=', padding); } } ================================================ FILE: src/ReverseProxy/SessionAffinity/BaseHashCookieSessionAffinityPolicy.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Yarp.ReverseProxy.Configuration; using Yarp.ReverseProxy.Model; namespace Yarp.ReverseProxy.SessionAffinity; internal abstract class BaseHashCookieSessionAffinityPolicy : ISessionAffinityPolicy { private static readonly object AffinityKeyId = new(); private readonly ILogger _logger; private readonly TimeProvider _timeProvider; public BaseHashCookieSessionAffinityPolicy(TimeProvider timeProvider, ILogger logger) { ArgumentNullException.ThrowIfNull(timeProvider); ArgumentNullException.ThrowIfNull(logger); _timeProvider = timeProvider; _logger = logger; } public abstract string Name { get; } public void AffinitizeResponse(HttpContext context, ClusterState cluster, SessionAffinityConfig config, DestinationState destination) { if (!config.Enabled.GetValueOrDefault()) { throw new InvalidOperationException("Session affinity is disabled for cluster."); } if (context.RequestAborted.IsCancellationRequested) { // Avoid wasting time if the client is already gone. return; } // Affinity key is set on the response only if it's a new affinity. if (!context.Items.ContainsKey(AffinityKeyId)) { var affinityKey = GetDestinationHash(destination); var affinityCookieOptions = AffinityHelpers.CreateCookieOptions(config.Cookie, context.Request.IsHttps, _timeProvider); // CodeQL [SM02373] - Whether CookieOptions.Secure is used depends on YARP configuration, and session affinity may be used in non-HTTPS setups. Hash-based affinity policies do not intend to provide privacy protection. See https://learn.microsoft.com/aspnet/core/fundamentals/servers/yarp/session-affinity#key-protection. context.Response.Cookies.Append(config.AffinityKeyName, affinityKey, affinityCookieOptions); } } public AffinityResult FindAffinitizedDestinations(HttpContext context, ClusterState cluster, SessionAffinityConfig config, IReadOnlyList destinations) { if (!config.Enabled.GetValueOrDefault()) { throw new InvalidOperationException($"Session affinity is disabled for cluster {cluster.ClusterId}."); } var affinityHash = context.Request.Cookies[config.AffinityKeyName]; if (affinityHash is null) { return new(null, AffinityStatus.AffinityKeyNotSet); } foreach (var d in destinations) { var hashValue = GetDestinationHash(d); if (affinityHash == hashValue) { context.Items[AffinityKeyId] = affinityHash; return new(d, AffinityStatus.OK); } } if (destinations.Count == 0) { Log.AffinityCannotBeEstablishedBecauseNoDestinationsFound(_logger, cluster.ClusterId); } else { Log.DestinationMatchingToAffinityKeyNotFound(_logger, cluster.ClusterId); } return new(null, AffinityStatus.DestinationNotFound); } protected abstract string GetDestinationHash(DestinationState d); } ================================================ FILE: src/ReverseProxy/SessionAffinity/CookieSessionAffinityPolicy.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Yarp.ReverseProxy.Configuration; using Yarp.ReverseProxy.Model; namespace Yarp.ReverseProxy.SessionAffinity; internal sealed class CookieSessionAffinityPolicy : BaseEncryptedSessionAffinityPolicy { private readonly TimeProvider _timeProvider; public CookieSessionAffinityPolicy( IDataProtectionProvider dataProtectionProvider, TimeProvider timeProvider, ILogger logger) : base(dataProtectionProvider, logger) { ArgumentNullException.ThrowIfNull(timeProvider); _timeProvider = timeProvider; } public override string Name => SessionAffinityConstants.Policies.Cookie; protected override string GetDestinationAffinityKey(DestinationState destination) { return destination.DestinationId; } protected override (string? Key, bool ExtractedSuccessfully) GetRequestAffinityKey(HttpContext context, ClusterState cluster, SessionAffinityConfig config) { var encryptedRequestKey = context.Request.Cookies.TryGetValue(config.AffinityKeyName, out var keyInCookie) ? keyInCookie : null; return Unprotect(encryptedRequestKey); } protected override void SetAffinityKey(HttpContext context, ClusterState cluster, SessionAffinityConfig config, string unencryptedKey) { var affinityCookieOptions = AffinityHelpers.CreateCookieOptions(config.Cookie, context.Request.IsHttps, _timeProvider); // CodeQL [SM02373] - Whether CookieOptions.Secure is used depends on YARP configuration, and session affinity may be used in non-HTTPS setups. Cookie values are encrypted using ASP.NET DataProtection. See https://learn.microsoft.com/aspnet/core/fundamentals/servers/yarp/session-affinity#key-protection. context.Response.Cookies.Append(config.AffinityKeyName, Protect(unencryptedKey), affinityCookieOptions); } } ================================================ FILE: src/ReverseProxy/SessionAffinity/CustomHeaderSessionAffinityPolicy.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Primitives; using Yarp.ReverseProxy.Configuration; using Yarp.ReverseProxy.Model; namespace Yarp.ReverseProxy.SessionAffinity; internal sealed class CustomHeaderSessionAffinityPolicy : BaseEncryptedSessionAffinityPolicy { public CustomHeaderSessionAffinityPolicy( IDataProtectionProvider dataProtectionProvider, ILogger logger) : base(dataProtectionProvider, logger) { } public override string Name => SessionAffinityConstants.Policies.CustomHeader; protected override string GetDestinationAffinityKey(DestinationState destination) { return destination.DestinationId; } protected override (string? Key, bool ExtractedSuccessfully) GetRequestAffinityKey(HttpContext context, ClusterState cluster, SessionAffinityConfig config) { var customHeaderName = config.AffinityKeyName; var keyHeaderValues = context.Request.Headers[customHeaderName]; if (StringValues.IsNullOrEmpty(keyHeaderValues)) { // It means affinity key is not defined that is a successful case return (Key: null, ExtractedSuccessfully: true); } if (keyHeaderValues.Count > 1) { // Multiple values is an ambiguous case which is considered a key extraction failure Log.RequestAffinityHeaderHasMultipleValues(Logger, customHeaderName, keyHeaderValues.Count); return (Key: null, ExtractedSuccessfully: false); } return Unprotect(keyHeaderValues[0]); } protected override void SetAffinityKey(HttpContext context, ClusterState cluster, SessionAffinityConfig config, string unencryptedKey) { context.Response.Headers.Append(config.AffinityKeyName, Protect(unencryptedKey)); } private static class Log { private static readonly Action _requestAffinityHeaderHasMultipleValues = LoggerMessage.Define( LogLevel.Error, EventIds.RequestAffinityHeaderHasMultipleValues, "The request affinity header `{headerName}` has `{valueCount}` values."); public static void RequestAffinityHeaderHasMultipleValues(ILogger logger, string headerName, int valueCount) { _requestAffinityHeaderHasMultipleValues(logger, headerName, valueCount, null); } } } ================================================ FILE: src/ReverseProxy/SessionAffinity/HashCookieSessionAffinityPolicy.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.IO.Hashing; using System.Runtime.CompilerServices; using System.Text; using Microsoft.Extensions.Logging; using Yarp.ReverseProxy.Model; namespace Yarp.ReverseProxy.SessionAffinity; internal sealed class HashCookieSessionAffinityPolicy : BaseHashCookieSessionAffinityPolicy { private readonly ConditionalWeakTable _hashes = new(); public HashCookieSessionAffinityPolicy( TimeProvider timeProvider, ILogger logger) : base(timeProvider, logger) { } public override string Name => SessionAffinityConstants.Policies.HashCookie; protected override string GetDestinationHash(DestinationState d) { return _hashes.GetValue(d, static d => { // Stable format across instances var destinationIdBytes = Encoding.Unicode.GetBytes(d.DestinationId.ToUpperInvariant()); var hashBytes = XxHash64.Hash(destinationIdBytes); return Convert.ToHexString(hashBytes).ToLowerInvariant(); }); } } ================================================ FILE: src/ReverseProxy/SessionAffinity/IAffinityFailurePolicy.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Yarp.ReverseProxy.Model; namespace Yarp.ReverseProxy.SessionAffinity; /// /// Affinity failures handling policy. /// public interface IAffinityFailurePolicy { /// /// A unique identifier for this failure policy. This will be referenced from config. /// string Name { get; } /// /// Handles affinity failures. This method assumes the full control on /// and can change it in any way. /// /// Current request's context. /// The associated cluster for the request. /// Affinity resolution status. /// /// '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. /// Task Handle(HttpContext context, ClusterState cluster, AffinityStatus affinityStatus); } ================================================ FILE: src/ReverseProxy/SessionAffinity/ISessionAffinityPolicy.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Yarp.ReverseProxy.Configuration; using Yarp.ReverseProxy.Model; namespace Yarp.ReverseProxy.SessionAffinity; /// /// Provides session affinity for load-balanced clusters. /// public interface ISessionAffinityPolicy { /// /// A unique identifier for this session affinity implementation. This will be referenced from config. /// string Name { get; } /// /// Finds to which the current request is affinitized by the affinity key. /// /// Current request's context. /// Current request's cluster. /// Affinity config. /// s available for the request. /// carrying the found affinitized destinations if any and the . AffinityResult FindAffinitizedDestinations(HttpContext context, ClusterState cluster, SessionAffinityConfig config, IReadOnlyList destinations); /// /// Finds to which the current request is affinitized by the affinity key. /// /// Current request's context. /// Current request's cluster. /// Affinity config. /// s available for the request. /// The token to monitor for cancellation requests. /// carrying the found affinitized destinations if any and the . ValueTask FindAffinitizedDestinationsAsync(HttpContext context, ClusterState cluster, SessionAffinityConfig config, IReadOnlyList destinations, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); return new ValueTask(FindAffinitizedDestinations(context, cluster, config, destinations)); } /// /// Affinitize the current response to the given by setting the affinity key extracted from . /// /// Current request's context. /// Current request's cluster. /// Affinity config. /// to which request is to be affinitized. void AffinitizeResponse(HttpContext context, ClusterState cluster, SessionAffinityConfig config, DestinationState destination); /// /// Affinitize the current response to the given by setting the affinity key extracted from . /// /// Current request's context. /// Current request's cluster. /// Affinity config. /// to which request is to be affinitized. /// The token to monitor for cancellation requests. ValueTask AffinitizeResponseAsync(HttpContext context, ClusterState cluster, SessionAffinityConfig config, DestinationState destination, CancellationToken cancellationToken) { AffinitizeResponse(context, cluster, config, destination); return default; } } ================================================ FILE: src/ReverseProxy/SessionAffinity/Log.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using Microsoft.Extensions.Logging; namespace Yarp.ReverseProxy.SessionAffinity; internal static class Log { private static readonly Action _affinityCannotBeEstablishedBecauseNoDestinationsFound = LoggerMessage.Define( LogLevel.Warning, EventIds.AffinityCannotBeEstablishedBecauseNoDestinationsFoundOnCluster, "The request affinity cannot be established because no destinations are found on cluster `{clusterId}`."); private static readonly Action _requestAffinityKeyDecryptionFailed = LoggerMessage.Define( LogLevel.Error, EventIds.RequestAffinityKeyDecryptionFailed, "The request affinity key decryption failed."); private static readonly Action _destinationMatchingToAffinityKeyNotFound = LoggerMessage.Define( LogLevel.Warning, EventIds.DestinationMatchingToAffinityKeyNotFound, "Destination matching to the request affinity key is not found on cluster `{clusterId}`. Configured failure policy will be applied."); public static void AffinityCannotBeEstablishedBecauseNoDestinationsFound(ILogger logger, string clusterId) { _affinityCannotBeEstablishedBecauseNoDestinationsFound(logger, clusterId, null); } public static void RequestAffinityKeyDecryptionFailed(ILogger logger, Exception? ex) { _requestAffinityKeyDecryptionFailed(logger, ex); } public static void DestinationMatchingToAffinityKeyNotFound(ILogger logger, string clusterId) { _destinationMatchingToAffinityKeyNotFound(logger, clusterId, null); } } ================================================ FILE: src/ReverseProxy/SessionAffinity/RedistributeAffinityFailurePolicy.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Yarp.ReverseProxy.Model; using Yarp.ReverseProxy.Utilities; namespace Yarp.ReverseProxy.SessionAffinity; internal sealed class RedistributeAffinityFailurePolicy : IAffinityFailurePolicy { public string Name => SessionAffinityConstants.FailurePolicies.Redistribute; public Task Handle(HttpContext context, ClusterState cluster, AffinityStatus affinityStatus) { if (affinityStatus == AffinityStatus.OK || affinityStatus == AffinityStatus.AffinityKeyNotSet) { throw new InvalidOperationException($"{nameof(RedistributeAffinityFailurePolicy)} is called to handle a successful request's affinity status {affinityStatus}."); } // Available destinations list have not been changed in the context, // so simply allow processing to proceed to load balancing. return TaskUtilities.TrueTask; } } ================================================ FILE: src/ReverseProxy/SessionAffinity/Return503ErrorAffinityFailurePolicy.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Yarp.ReverseProxy.Model; using Yarp.ReverseProxy.Utilities; namespace Yarp.ReverseProxy.SessionAffinity; internal sealed class Return503ErrorAffinityFailurePolicy : IAffinityFailurePolicy { public string Name => SessionAffinityConstants.FailurePolicies.Return503Error; public Task Handle(HttpContext context, ClusterState cluster, AffinityStatus affinityStatus) { if (affinityStatus == AffinityStatus.OK || affinityStatus == AffinityStatus.AffinityKeyNotSet) { throw new InvalidOperationException($"{nameof(Return503ErrorAffinityFailurePolicy)} is called to handle a successful request's affinity status {affinityStatus}."); } context.Response.StatusCode = 503; return TaskUtilities.FalseTask; } } ================================================ FILE: src/ReverseProxy/SessionAffinity/SessionAffinityConstants.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. namespace Yarp.ReverseProxy.SessionAffinity; /// /// Names of built-in session affinity services. /// public static class SessionAffinityConstants { public static class Policies { public static string Cookie => nameof(Cookie); public static string HashCookie => nameof(HashCookie); public static string ArrCookie => nameof(ArrCookie); public static string CustomHeader => nameof(CustomHeader); } public static class FailurePolicies { public static string Redistribute => nameof(Redistribute); public static string Return503Error => nameof(Return503Error); } } ================================================ FILE: src/ReverseProxy/SessionAffinity/SessionAffinityMiddleware.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Frozen; using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Yarp.ReverseProxy.Configuration; using Yarp.ReverseProxy.Model; using Yarp.ReverseProxy.Utilities; namespace Yarp.ReverseProxy.SessionAffinity; /// /// Looks up an affinitized matching the request's affinity key if any is set /// internal sealed class SessionAffinityMiddleware { private readonly RequestDelegate _next; private readonly FrozenDictionary _sessionAffinityPolicies; private readonly FrozenDictionary _affinityFailurePolicies; private readonly ILogger _logger; public SessionAffinityMiddleware( RequestDelegate next, IEnumerable sessionAffinityPolicies, IEnumerable affinityFailurePolicies, ILogger logger) { ArgumentNullException.ThrowIfNull(next); ArgumentNullException.ThrowIfNull(logger); ArgumentNullException.ThrowIfNull(sessionAffinityPolicies); ArgumentNullException.ThrowIfNull(affinityFailurePolicies); _next = next; _logger = logger; _sessionAffinityPolicies = sessionAffinityPolicies.ToDictionaryByUniqueId(p => p.Name); _affinityFailurePolicies = affinityFailurePolicies.ToDictionaryByUniqueId(p => p.Name); } public Task Invoke(HttpContext context) { var proxyFeature = context.GetReverseProxyFeature(); var config = proxyFeature.Cluster.Config.SessionAffinity; if (config is null || !config.Enabled.GetValueOrDefault()) { return _next(context); } return InvokeInternal(context, proxyFeature, config); } private async Task InvokeInternal(HttpContext context, IReverseProxyFeature proxyFeature, SessionAffinityConfig config) { var destinations = proxyFeature.AvailableDestinations; var cluster = proxyFeature.Route.Cluster!; var policy = _sessionAffinityPolicies.GetRequiredServiceById(config.Policy, SessionAffinityConstants.Policies.HashCookie); var affinityResult = await policy.FindAffinitizedDestinationsAsync(context, cluster, config, destinations, context.RequestAborted); // Used for Distributed Tracing as part of Open Telemetry, null if there are no listeners var activity = context.GetYarpActivity(); activity?.SetTag("proxy.session_affinity.policy", policy.Name); switch (affinityResult.Status) { case AffinityStatus.OK: proxyFeature.AvailableDestinations = affinityResult.Destinations!; activity?.SetTag("proxy.session_affinity.status", "success"); break; case AffinityStatus.AffinityKeyNotSet: // Nothing to do so just continue processing break; case AffinityStatus.AffinityKeyExtractionFailed: case AffinityStatus.DestinationNotFound: var failurePolicy = _affinityFailurePolicies.GetRequiredServiceById(config.FailurePolicy, SessionAffinityConstants.FailurePolicies.Redistribute); var keepProcessing = await failurePolicy.Handle(context, proxyFeature.Route.Cluster!, affinityResult.Status); if (!keepProcessing) { // Policy reported the failure is unrecoverable and took the full responsibility for its handling, // so we simply stop processing. Log.AffinityResolutionFailedForCluster(_logger, cluster.ClusterId); activity?.SetTag("proxy.session_affinity.status", "failed"); return; } Log.AffinityResolutionFailureWasHandledProcessingWillBeContinued(_logger, cluster.ClusterId, failurePolicy.Name); activity?.SetTag("proxy.session_affinity.status", "recovered"); break; default: throw new NotSupportedException($"Affinity status '{affinityResult.Status}' is not supported."); } await _next(context); } private static class Log { private static readonly Action _affinityResolutionFailedForCluster = LoggerMessage.Define( LogLevel.Warning, EventIds.AffinityResolutionFailedForCluster, "Affinity resolution failed for cluster '{clusterId}'."); private static readonly Action _affinityResolutionFailureWasHandledProcessingWillBeContinued = LoggerMessage.Define( LogLevel.Debug, EventIds.AffinityResolutionFailureWasHandledProcessingWillBeContinued, "Affinity resolution failure for cluster '{clusterId}' was handled successfully by the policy '{policyName}'. Request processing will be continued."); public static void AffinityResolutionFailedForCluster(ILogger logger, string clusterId) { _affinityResolutionFailedForCluster(logger, clusterId, null); } public static void AffinityResolutionFailureWasHandledProcessingWillBeContinued(ILogger logger, string clusterId, string policyName) { _affinityResolutionFailureWasHandledProcessingWillBeContinued(logger, clusterId, policyName, null); } } } ================================================ FILE: src/ReverseProxy/Transforms/Builder/ActionTransformProvider.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; namespace Yarp.ReverseProxy.Transforms.Builder; internal sealed class ActionTransformProvider : ITransformProvider { private readonly Action _action; public ActionTransformProvider(Action action) { ArgumentNullException.ThrowIfNull(action); _action = action; } public void Apply(TransformBuilderContext transformBuildContext) { _action(transformBuildContext); } public void ValidateRoute(TransformRouteValidationContext context) { } public void ValidateCluster(TransformClusterValidationContext context) { } } ================================================ FILE: src/ReverseProxy/Transforms/Builder/ITransformBuilder.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using Yarp.ReverseProxy.Configuration; using Yarp.ReverseProxy.Forwarder; namespace Yarp.ReverseProxy.Transforms.Builder; /// /// Validates and builds request and response transforms for a given route. /// 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 ValidateRoute(RouteConfig route); /// /// Validates that any cluster data needed for transforms is valid. /// IReadOnlyList ValidateCluster(ClusterConfig cluster); /// /// Builds the transforms for the given route into executable rules. /// HttpTransformer Build(RouteConfig route, ClusterConfig? cluster); HttpTransformer Create(Action action); } ================================================ FILE: src/ReverseProxy/Transforms/Builder/ITransformFactory.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; namespace Yarp.ReverseProxy.Transforms.Builder; /// /// Validates and builds transforms from the given parameters /// public interface ITransformFactory { /// /// Checks if the given transform values match a known transform, and if those values have any errors. /// /// 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(TransformRouteValidationContext context, IReadOnlyDictionary transformValues); /// /// Checks if the given transform values match a known transform, and if so, generates a transform and /// adds it to the context. This can throw if the transform values are invalid. /// /// The context to add any generated transforms to. /// The transform values to use as input. /// True if this factory matches the given transform, otherwise false. bool Build(TransformBuilderContext context, IReadOnlyDictionary transformValues); } ================================================ FILE: src/ReverseProxy/Transforms/Builder/ITransformProvider.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. namespace Yarp.ReverseProxy.Transforms.Builder; /// /// Enables the implementor to inspect each route and conditionally add transforms. /// public interface ITransformProvider { /// /// Validates any route data needed for transforms. /// /// The context to add any generated errors to. 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. /// This is called for every route, each time that route is built. /// /// The context to add any generated transforms to. void Apply(TransformBuilderContext context); } ================================================ FILE: src/ReverseProxy/Transforms/Builder/StructuredTransformer.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Yarp.ReverseProxy.Forwarder; namespace Yarp.ReverseProxy.Transforms.Builder; /// /// Transforms for a given route. /// internal sealed class StructuredTransformer : HttpTransformer { /// /// Creates a new instance. /// internal StructuredTransformer(bool? copyRequestHeaders, bool? copyResponseHeaders, bool? copyResponseTrailers, IList requestTransforms, IList responseTransforms, IList responseTrailerTransforms) { ArgumentNullException.ThrowIfNull(requestTransforms); ArgumentNullException.ThrowIfNull(responseTransforms); ArgumentNullException.ThrowIfNull(responseTrailerTransforms); ShouldCopyRequestHeaders = copyRequestHeaders; ShouldCopyResponseHeaders = copyResponseHeaders; ShouldCopyResponseTrailers = copyResponseTrailers; RequestTransforms = requestTransforms.ToArray(); ResponseTransforms = responseTransforms.ToArray(); ResponseTrailerTransforms = responseTrailerTransforms.ToArray(); } /// /// Indicates if all request headers should be copied to the proxy request before applying transforms. /// internal bool? ShouldCopyRequestHeaders { get; } /// /// Indicates if all response headers should be copied to the client response before applying transforms. /// internal bool? ShouldCopyResponseHeaders { get; } /// /// Indicates if all response trailers should be copied to the client response before applying transforms. /// internal bool? ShouldCopyResponseTrailers { get; } /// /// Request transforms. /// internal RequestTransform[] RequestTransforms { get; } /// /// Response header transforms. /// internal ResponseTransform[] ResponseTransforms { get; } /// /// Response trailer transforms. /// internal ResponseTrailersTransform[] ResponseTrailerTransforms { get; } #pragma warning disable CS0672 // We're overriding the obsolete overloads to preserve backwards compatibility. public override ValueTask TransformRequestAsync(HttpContext httpContext, HttpRequestMessage proxyRequest, string destinationPrefix) => TransformRequestAsync(httpContext, proxyRequest, destinationPrefix, CancellationToken.None); public override ValueTask TransformResponseAsync(HttpContext httpContext, HttpResponseMessage? proxyResponse) => TransformResponseAsync(httpContext, proxyResponse, CancellationToken.None); public override ValueTask TransformResponseTrailersAsync(HttpContext httpContext, HttpResponseMessage proxyResponse) => TransformResponseTrailersAsync(httpContext, proxyResponse, CancellationToken.None); #pragma warning restore public override async ValueTask TransformRequestAsync(HttpContext httpContext, HttpRequestMessage proxyRequest, string destinationPrefix, CancellationToken cancellationToken) { if (ShouldCopyRequestHeaders.GetValueOrDefault(true)) { // We own the base implementation and know it doesn't make use of the cancellation token. // We're intentionally calling the overload without it to avoid it calling back into this derived implementation, causing a stack overflow. #pragma warning disable CA2016 // Forward the 'CancellationToken' parameter to methods #pragma warning disable CS0618 // Type or member is obsolete await base.TransformRequestAsync(httpContext, proxyRequest, destinationPrefix); #pragma warning restore CS0618 #pragma warning restore CA2016 } if (RequestTransforms.Length == 0) { return; } var transformContext = new RequestTransformContext() { DestinationPrefix = destinationPrefix, HttpContext = httpContext, ProxyRequest = proxyRequest, Path = httpContext.Request.Path, HeadersCopied = ShouldCopyRequestHeaders.GetValueOrDefault(true), CancellationToken = cancellationToken, }; foreach (var requestTransform in RequestTransforms) { await requestTransform.ApplyAsync(transformContext); // The transform generated a response, do not apply further transforms and do not forward. if (RequestUtilities.IsResponseSet(httpContext.Response)) { return; } } // Allow a transform to directly set a custom RequestUri. if (proxyRequest.RequestUri is null) { var queryString = transformContext.MaybeQuery?.QueryString ?? httpContext.Request.QueryString; proxyRequest.RequestUri = RequestUtilities.MakeDestinationAddress( transformContext.DestinationPrefix, transformContext.Path, queryString); } } public override async ValueTask TransformResponseAsync(HttpContext httpContext, HttpResponseMessage? proxyResponse, CancellationToken cancellationToken) { if (ShouldCopyResponseHeaders.GetValueOrDefault(true)) { // We own the base implementation and know it doesn't make use of the cancellation token. // We're intentionally calling the overload without it to avoid it calling back into this derived implementation, causing a stack overflow. #pragma warning disable CA2016 // Forward the 'CancellationToken' parameter to methods #pragma warning disable CS0618 // Type or member is obsolete await base.TransformResponseAsync(httpContext, proxyResponse); #pragma warning restore CS0618 #pragma warning restore CA2016 } if (ResponseTransforms.Length == 0) { return true; } var transformContext = new ResponseTransformContext() { HttpContext = httpContext, ProxyResponse = proxyResponse, HeadersCopied = ShouldCopyResponseHeaders.GetValueOrDefault(true), CancellationToken = cancellationToken, }; foreach (var responseTransform in ResponseTransforms) { await responseTransform.ApplyAsync(transformContext); } return !transformContext.SuppressResponseBody; } public override async ValueTask TransformResponseTrailersAsync(HttpContext httpContext, HttpResponseMessage proxyResponse, CancellationToken cancellationToken) { if (ShouldCopyResponseTrailers.GetValueOrDefault(true)) { // We own the base implementation and know it doesn't make use of the cancellation token. // We're intentionally calling the overload without it to avoid it calling back into this derived implementation, causing a stack overflow. #pragma warning disable CA2016 // Forward the 'CancellationToken' parameter to methods #pragma warning disable CS0618 // Type or member is obsolete await base.TransformResponseTrailersAsync(httpContext, proxyResponse); #pragma warning restore CS0618 #pragma warning restore CA2016 } if (ResponseTrailerTransforms.Length == 0) { return; } // Only run the transforms if trailers are actually supported by the client response. var responseTrailersFeature = httpContext.Features.Get(); var outgoingTrailers = responseTrailersFeature?.Trailers; if (outgoingTrailers is not null && !outgoingTrailers.IsReadOnly) { var transformContext = new ResponseTrailersTransformContext() { HttpContext = httpContext, ProxyResponse = proxyResponse, HeadersCopied = ShouldCopyResponseTrailers.GetValueOrDefault(true), CancellationToken = cancellationToken, }; foreach (var responseTrailerTransform in ResponseTrailerTransforms) { await responseTrailerTransform.ApplyAsync(transformContext); } } } } ================================================ FILE: src/ReverseProxy/Transforms/Builder/TransformBuilder.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Linq; using Yarp.ReverseProxy.Configuration; using Yarp.ReverseProxy.Forwarder; namespace Yarp.ReverseProxy.Transforms.Builder; /// /// Validates and builds request and response transforms for a given route. /// internal sealed class TransformBuilder : ITransformBuilder { private readonly IServiceProvider _services; private readonly List _factories; private readonly List _providers; /// /// Creates a new /// public TransformBuilder(IServiceProvider services, IEnumerable factories, IEnumerable providers) { ArgumentNullException.ThrowIfNull(services); ArgumentNullException.ThrowIfNull(factories); ArgumentNullException.ThrowIfNull(providers); _services = services; _factories = factories.ToList(); _providers = providers.ToList(); } /// public IReadOnlyList ValidateRoute(RouteConfig route) { var context = new TransformRouteValidationContext() { Services = _services, Route = route, }; var rawTransforms = route?.Transforms; if (rawTransforms?.Count > 0) { foreach (var rawTransform in rawTransforms) { var handled = false; foreach (var factory in _factories) { if (factory.Validate(context, rawTransform)) { handled = true; break; } } if (!handled) { context.Errors.Add(new ArgumentException($"Unknown transform: {string.Join(';', rawTransform.Keys)}")); } } } // Let the app add any more validation it wants. foreach (var transformProvider in _providers) { transformProvider.ValidateRoute(context); } // We promise not to modify the list after we return it. return (IReadOnlyList)context.Errors; } /// public IReadOnlyList ValidateCluster(ClusterConfig cluster) { 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(RouteConfig route, ClusterConfig? cluster) { return BuildInternal(route, cluster); } // This is separate from Build for testing purposes. internal StructuredTransformer BuildInternal(RouteConfig route, ClusterConfig? cluster) { var rawTransforms = route.Transforms; var context = new TransformBuilderContext { Services = _services, Route = route, Cluster = cluster, }; if (rawTransforms?.Count > 0) { foreach (var rawTransform in rawTransforms) { var handled = false; foreach (var factory in _factories) { if (factory.Build(context, rawTransform)) { handled = true; break; } } if (!handled) { throw new ArgumentException($"Unknown transform: {string.Join(';', rawTransform.Keys)}"); } } } // Let the app add any more transforms it wants. foreach (var transformProvider in _providers) { transformProvider.Apply(context); } return CreateTransformer(context); } public HttpTransformer Create(Action action) { return CreateInternal(action); } internal StructuredTransformer CreateInternal(Action action) { var context = new TransformBuilderContext { Services = _services, }; action(context); return CreateTransformer(context); } internal static StructuredTransformer CreateTransformer(TransformBuilderContext context) { // RequestHeaderOriginalHostKey defaults to false, and CopyRequestHeaders defaults to true. // If RequestHeaderOriginalHostKey was not specified then we need to make sure the transform gets // added anyway to remove the original host and to observe hosts specified in DestinationConfig. if (!context.RequestTransforms.Any(item => item is RequestHeaderOriginalHostTransform)) { context.AddOriginalHost(false); } // Add default forwarders only if they haven't already been added or disabled. if (context.UseDefaultForwarders.GetValueOrDefault(true)) { context.AddXForwarded(); } return new StructuredTransformer( context.CopyRequestHeaders, context.CopyResponseHeaders, context.CopyResponseTrailers, context.RequestTransforms, context.ResponseTransforms, context.ResponseTrailersTransforms); } } ================================================ FILE: src/ReverseProxy/Transforms/Builder/TransformBuilderContext.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using Yarp.ReverseProxy.Configuration; namespace Yarp.ReverseProxy.Transforms.Builder; /// /// State used when building transforms for the given route. /// public class TransformBuilderContext { /// /// Application services that can be used to construct transforms. /// public IServiceProvider Services { get; init; } = default!; /// /// The route these transforms will be associated with. /// public RouteConfig Route { get; init; } = default!; /// /// The cluster config used by the route. /// This may be null if the route is not currently paired with a cluster. /// public ClusterConfig? Cluster { get; init; } /// /// Indicates if request headers should all be copied to the proxy request before transforms are applied. /// public bool? CopyRequestHeaders { get; set; } /// /// Indicates if response headers should all be copied to the client response before transforms are applied. /// public bool? CopyResponseHeaders { get; set; } /// /// Indicates if response trailers should all be copied to the client response before transforms are applied. /// public bool? CopyResponseTrailers { get; set; } /// /// Indicates if default x-forwarded-* transforms should be added to this route. Disable this if you do not want /// x-forwarded-* headers or have configured your own. /// public bool? UseDefaultForwarders { get; set; } /// /// Add request transforms here for the given route. /// public IList RequestTransforms { get; } = new List(); /// /// Add response transforms here for the given route. /// public IList ResponseTransforms { get; } = new List(); /// /// Add response trailers transforms here for the given route. /// public IList ResponseTrailersTransforms { get; } = new List(); } ================================================ FILE: src/ReverseProxy/Transforms/Builder/TransformClusterValidationContext.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using Yarp.ReverseProxy.Configuration; namespace Yarp.ReverseProxy.Transforms.Builder; /// /// 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; } = default!; /// /// The cluster configuration that may be used when creating transforms. /// public ClusterConfig Cluster { get; init; } = default!; /// /// The accumulated list of validation errors for this cluster. /// Add validation errors here. /// public IList Errors { get; } = new List(); } ================================================ FILE: src/ReverseProxy/Transforms/Builder/TransformHelpers.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; namespace Yarp.ReverseProxy.Transforms.Builder; public static class TransformHelpers { public static void TryCheckTooManyParameters(TransformRouteValidationContext context, IReadOnlyDictionary rawTransform, int expected) { if (rawTransform.Count > expected) { context.Errors.Add(new InvalidOperationException("The transform contains more parameters than expected: " + string.Join(';', rawTransform.Keys))); } } public static void CheckTooManyParameters(IReadOnlyDictionary rawTransform, int expected) { if (rawTransform.Count > expected) { throw new InvalidOperationException("The transform contains more parameters than expected: " + string.Join(';', rawTransform.Keys)); } } internal static void RemoveAllXForwardedHeaders(TransformBuilderContext context, string prefix) { context.AddXForwardedFor(prefix + ForwardedTransformFactory.ForKey, ForwardedTransformActions.Remove); context.AddXForwardedPrefix(prefix + ForwardedTransformFactory.PrefixKey, ForwardedTransformActions.Remove); context.AddXForwardedHost(prefix + ForwardedTransformFactory.HostKey, ForwardedTransformActions.Remove); context.AddXForwardedProto(prefix + ForwardedTransformFactory.ProtoKey, ForwardedTransformActions.Remove); } internal static void RemoveForwardedHeader(TransformBuilderContext context) { context.RequestTransforms.Add(RequestHeaderForwardedTransform.RemoveTransform); } } ================================================ FILE: src/ReverseProxy/Transforms/Builder/TransformRouteValidationContext.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using Yarp.ReverseProxy.Configuration; namespace Yarp.ReverseProxy.Transforms.Builder; /// /// State used when validating transforms for the given route. /// public class TransformRouteValidationContext { /// /// Application services that can be used to validate transforms. /// public IServiceProvider Services { get; init; } = default!; /// /// The route these transforms are associated with. /// public RouteConfig Route { get; init; } = default!; /// /// The accumulated list of validation errors for this route. /// Add transform validation errors here. /// public IList Errors { get; } = new List(); } ================================================ FILE: src/ReverseProxy/Transforms/ForwardedTransformActions.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. namespace Yarp.ReverseProxy.Transforms; public enum ForwardedTransformActions { Off = 0, Set, Append, Remove } ================================================ FILE: src/ReverseProxy/Transforms/ForwardedTransformExtensions.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; using Microsoft.Extensions.DependencyInjection; using Yarp.ReverseProxy.Configuration; using Yarp.ReverseProxy.Transforms.Builder; using Yarp.ReverseProxy.Utilities; namespace Yarp.ReverseProxy.Transforms; /// /// Extensions for adding forwarded header transforms. /// public static class ForwardedTransformExtensions { /// /// Clones the route and adds the transform which will add X-Forwarded-* headers. /// public static RouteConfig WithTransformXForwarded( this RouteConfig route, string headerPrefix = "X-Forwarded-", ForwardedTransformActions xDefault = ForwardedTransformActions.Set, ForwardedTransformActions? xFor = null, ForwardedTransformActions? xHost = null, ForwardedTransformActions? xProto = null, ForwardedTransformActions? xPrefix = null) { return route.WithTransform(transform => { transform[ForwardedTransformFactory.XForwardedKey] = xDefault.ToString(); if (xFor is not null) { transform[ForwardedTransformFactory.ForKey] = xFor.Value.ToString(); } if (xPrefix is not null) { transform[ForwardedTransformFactory.PrefixKey] = xPrefix.Value.ToString(); } if (xHost is not null) { transform[ForwardedTransformFactory.HostKey] = xHost.Value.ToString(); } if (xProto is not null) { transform[ForwardedTransformFactory.ProtoKey] = xProto.Value.ToString(); } transform[ForwardedTransformFactory.HeaderPrefixKey] = headerPrefix; }); } /// /// Adds the transform which will add X-Forwarded-For request header. /// public static TransformBuilderContext AddXForwardedFor(this TransformBuilderContext context, string headerName = "X-Forwarded-For", ForwardedTransformActions action = ForwardedTransformActions.Set) { context.UseDefaultForwarders = false; if (action == ForwardedTransformActions.Off) { return context; } context.RequestTransforms.Add(new RequestHeaderXForwardedForTransform(headerName, action)); return context; } /// /// Adds the transform which will add X-Forwarded-Host request header. /// public static TransformBuilderContext AddXForwardedHost(this TransformBuilderContext context, string headerName = "X-Forwarded-Host", ForwardedTransformActions action = ForwardedTransformActions.Set) { context.UseDefaultForwarders = false; if (action == ForwardedTransformActions.Off) { return context; } context.RequestTransforms.Add(new RequestHeaderXForwardedHostTransform(headerName, action)); return context; } /// /// Adds the transform which will add X-Forwarded-Proto request header. /// public static TransformBuilderContext AddXForwardedProto(this TransformBuilderContext context, string headerName = "X-Forwarded-Proto", ForwardedTransformActions action = ForwardedTransformActions.Set) { context.UseDefaultForwarders = false; if (action == ForwardedTransformActions.Off) { return context; } context.RequestTransforms.Add(new RequestHeaderXForwardedProtoTransform(headerName, action)); return context; } /// /// Adds the transform which will add X-Forwarded-Prefix request header. /// public static TransformBuilderContext AddXForwardedPrefix(this TransformBuilderContext context, string headerName = "X-Forwarded-Prefix", ForwardedTransformActions action = ForwardedTransformActions.Set) { context.UseDefaultForwarders = false; if (action == ForwardedTransformActions.Off) { return context; } context.RequestTransforms.Add(new RequestHeaderXForwardedPrefixTransform(headerName, action)); return context; } /// /// Adds the transform which will add X-Forwarded-* request headers. /// /// /// Also optionally removes the Forwarded header when enabled. /// public static TransformBuilderContext AddXForwarded(this TransformBuilderContext context, ForwardedTransformActions action = ForwardedTransformActions.Set, bool removeForwardedHeader = true) { context.AddXForwardedFor(action: action); context.AddXForwardedPrefix(action: action); context.AddXForwardedHost(action: action); context.AddXForwardedProto(action: action); if (removeForwardedHeader) { // Remove the Forwarded header when an X-Forwarded transform is enabled TransformHelpers.RemoveForwardedHeader(context); } return context; } /// /// Clones the route and adds the transform which will add the Forwarded header as defined by [RFC 7239](https://tools.ietf.org/html/rfc7239). /// public static RouteConfig WithTransformForwarded(this RouteConfig route, bool useHost = true, bool useProto = true, NodeFormat forFormat = NodeFormat.Random, NodeFormat byFormat = NodeFormat.Random, ForwardedTransformActions action = ForwardedTransformActions.Set) { var headers = new List(); if (forFormat != NodeFormat.None) { headers.Add(ForwardedTransformFactory.ForKey); } if (byFormat != NodeFormat.None) { headers.Add(ForwardedTransformFactory.ByKey); } if (useHost) { headers.Add(ForwardedTransformFactory.HostKey); } if (useProto) { headers.Add(ForwardedTransformFactory.ProtoKey); } return route.WithTransform(transform => { transform[ForwardedTransformFactory.ForwardedKey] = string.Join(',', headers); transform[ForwardedTransformFactory.ActionKey] = action.ToString(); if (forFormat != NodeFormat.None) { transform.Add(ForwardedTransformFactory.ForFormatKey, forFormat.ToString()); } if (byFormat != NodeFormat.None) { transform.Add(ForwardedTransformFactory.ByFormatKey, byFormat.ToString()); } }); } /// /// Adds the transform which will add the Forwarded header as defined by [RFC 7239](https://tools.ietf.org/html/rfc7239). /// /// /// Also optionally removes the X-Forwarded headers when enabled. /// public static TransformBuilderContext AddForwarded(this TransformBuilderContext context, bool useHost = true, bool useProto = true, NodeFormat forFormat = NodeFormat.Random, NodeFormat byFormat = NodeFormat.Random, ForwardedTransformActions action = ForwardedTransformActions.Set, bool removeAllXForwardedHeaders = true) { context.UseDefaultForwarders = false; if (action == ForwardedTransformActions.Off) { return context; } if (byFormat != NodeFormat.None || forFormat != NodeFormat.None || useHost || useProto) { var random = context.Services.GetRequiredService(); context.RequestTransforms.Add(new RequestHeaderForwardedTransform(random, forFormat, byFormat, useHost, useProto, action)); if (removeAllXForwardedHeaders) { // Remove the X-Forwarded headers when a Forwarded transform is enabled TransformHelpers.RemoveAllXForwardedHeaders(context, ForwardedTransformFactory.DefaultXForwardedPrefix); } } return context; } /// /// Clones the route and adds the transform which will set the given header with the Base64 encoded client certificate. /// public static RouteConfig WithTransformClientCertHeader(this RouteConfig route, string headerName) { return route.WithTransform(transform => { transform[ForwardedTransformFactory.ClientCertKey] = headerName; }); } /// /// Adds the transform which will set the given header with the Base64 encoded client certificate. /// public static TransformBuilderContext AddClientCertHeader(this TransformBuilderContext context, string headerName) { context.RequestTransforms.Add(new RequestHeaderClientCertTransform(headerName)); return context; } } ================================================ FILE: src/ReverseProxy/Transforms/ForwardedTransformFactory.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using Yarp.ReverseProxy.Transforms.Builder; using Yarp.ReverseProxy.Utilities; namespace Yarp.ReverseProxy.Transforms; internal sealed class ForwardedTransformFactory : ITransformFactory { internal const string XForwardedKey = "X-Forwarded"; internal const string DefaultXForwardedPrefix = "X-Forwarded-"; internal const string ForwardedKey = "Forwarded"; internal const string ActionKey = "Action"; internal const string HeaderPrefixKey = "HeaderPrefix"; internal const string ForKey = "For"; internal const string ByKey = "By"; internal const string HostKey = "Host"; internal const string ProtoKey = "Proto"; internal const string PrefixKey = "Prefix"; internal const string ForFormatKey = "ForFormat"; internal const string ByFormatKey = "ByFormat"; internal const string ClientCertKey = "ClientCert"; private readonly IRandomFactory _randomFactory; public ForwardedTransformFactory(IRandomFactory randomFactory) { ArgumentNullException.ThrowIfNull(randomFactory); _randomFactory = randomFactory; } public bool Validate(TransformRouteValidationContext context, IReadOnlyDictionary transformValues) { if (transformValues.TryGetValue(XForwardedKey, out var headerValue)) { var xExpected = 1; ValidateAction(context, XForwardedKey, headerValue); if (transformValues.TryGetValue(HeaderPrefixKey, out _)) { xExpected++; } if (transformValues.TryGetValue(ForKey, out headerValue)) { xExpected++; ValidateAction(context, ForKey, headerValue); } if (transformValues.TryGetValue(PrefixKey, out headerValue)) { xExpected++; ValidateAction(context, PrefixKey, headerValue); } if (transformValues.TryGetValue(HostKey, out headerValue)) { xExpected++; ValidateAction(context, HostKey, headerValue); } if (transformValues.TryGetValue(ProtoKey, out headerValue)) { xExpected++; ValidateAction(context, ProtoKey, headerValue); } TransformHelpers.TryCheckTooManyParameters(context, transformValues, xExpected); } else if (transformValues.TryGetValue(ForwardedKey, out var forwardedHeader)) { var expected = 1; if (transformValues.TryGetValue(ActionKey, out headerValue)) { expected++; ValidateAction(context, ForwardedKey + ":" + ActionKey, headerValue); } var enumValues = "Random,RandomAndPort,Unknown,UnknownAndPort,Ip,IpAndPort"; if (transformValues.TryGetValue(ForFormatKey, out var forFormat)) { expected++; if (!Enum.TryParse(forFormat, ignoreCase: true, out var _)) { context.Errors.Add(new ArgumentException($"Unexpected value for Forwarded:ForFormat: {forFormat}. Expected: {enumValues}")); } } if (transformValues.TryGetValue(ByFormatKey, out var byFormat)) { expected++; if (!Enum.TryParse(byFormat, ignoreCase: true, out var _)) { context.Errors.Add(new ArgumentException($"Unexpected value for Forwarded:ByFormat: {byFormat}. Expected: {enumValues}")); } } TransformHelpers.TryCheckTooManyParameters(context, transformValues, expected); // for, host, proto, by var tokens = forwardedHeader.Split([',', ' '], StringSplitOptions.RemoveEmptyEntries); foreach (var token in tokens) { if (!string.Equals(token, ByKey, StringComparison.OrdinalIgnoreCase) && !string.Equals(token, HostKey, StringComparison.OrdinalIgnoreCase) && !string.Equals(token, ProtoKey, StringComparison.OrdinalIgnoreCase) && !string.Equals(token, ForKey, StringComparison.OrdinalIgnoreCase)) { context.Errors.Add(new ArgumentException($"Unexpected value for X-Forwarded: {token}. Expected 'for', 'host', 'proto', or 'by'")); } } } else if (transformValues.TryGetValue(ClientCertKey, out _)) { TransformHelpers.TryCheckTooManyParameters(context, transformValues, expected: 1); } else { return false; } return true; } public bool Build(TransformBuilderContext context, IReadOnlyDictionary transformValues) { if (transformValues.TryGetValue(XForwardedKey, out var headerValue)) { var xExpected = 1; var defaultXAction = Enum.Parse(headerValue); var prefix = DefaultXForwardedPrefix; if (transformValues.TryGetValue(HeaderPrefixKey, out var prefixValue)) { xExpected++; prefix = prefixValue; } var xForAction = defaultXAction; if (transformValues.TryGetValue(ForKey, out headerValue)) { xExpected++; xForAction = Enum.Parse(headerValue); } var xPrefixAction = defaultXAction; if (transformValues.TryGetValue(PrefixKey, out headerValue)) { xExpected++; xPrefixAction = Enum.Parse(headerValue); } var xHostAction = defaultXAction; if (transformValues.TryGetValue(HostKey, out headerValue)) { xExpected++; xHostAction = Enum.Parse(headerValue); } var xProtoAction = defaultXAction; if (transformValues.TryGetValue(ProtoKey, out headerValue)) { xExpected++; xProtoAction = Enum.Parse(headerValue); } TransformHelpers.CheckTooManyParameters(transformValues, xExpected); context.AddXForwardedFor(prefix + ForKey, xForAction); context.AddXForwardedPrefix(prefix + PrefixKey, xPrefixAction); context.AddXForwardedHost(prefix + HostKey, xHostAction); context.AddXForwardedProto(prefix + ProtoKey, xProtoAction); if (xForAction != ForwardedTransformActions.Off || xPrefixAction != ForwardedTransformActions.Off || xHostAction != ForwardedTransformActions.Off || xProtoAction != ForwardedTransformActions.Off) { // Remove the Forwarded header when an X-Forwarded transform is enabled TransformHelpers.RemoveForwardedHeader(context); } } else if (transformValues.TryGetValue(ForwardedKey, out var forwardedHeader)) { var useHost = false; var useProto = false; var useFor = false; var useBy = false; var forFormat = NodeFormat.None; var byFormat = NodeFormat.None; // for, host, proto, Prefix var tokens = forwardedHeader.Split([',', ' '], StringSplitOptions.RemoveEmptyEntries); foreach (var token in tokens) { if (string.Equals(token, ForKey, StringComparison.OrdinalIgnoreCase)) { useFor = true; forFormat = NodeFormat.Random; // RFC Default } else if (string.Equals(token, ByKey, StringComparison.OrdinalIgnoreCase)) { useBy = true; byFormat = NodeFormat.Random; // RFC Default } else if (string.Equals(token, HostKey, StringComparison.OrdinalIgnoreCase)) { useHost = true; } else if (string.Equals(token, ProtoKey, StringComparison.OrdinalIgnoreCase)) { useProto = true; } else { throw new ArgumentException($"Unexpected value for Forwarded: {token}. Expected 'for', 'host', 'proto', or 'by'"); } } var expected = 1; var headerAction = ForwardedTransformActions.Set; if (transformValues.TryGetValue(ActionKey, out headerValue)) { expected++; headerAction = Enum.Parse(headerValue); } if (useFor && transformValues.TryGetValue(ForFormatKey, out var forFormatString)) { expected++; forFormat = Enum.Parse(forFormatString, ignoreCase: true); } if (useBy && transformValues.TryGetValue(ByFormatKey, out var byFormatString)) { expected++; byFormat = Enum.Parse(byFormatString, ignoreCase: true); } TransformHelpers.CheckTooManyParameters(transformValues, expected); context.UseDefaultForwarders = false; if (headerAction != ForwardedTransformActions.Off && (useBy || useFor || useHost || useProto)) { // Not using the extension to avoid resolving the random factory each time. context.RequestTransforms.Add(new RequestHeaderForwardedTransform(_randomFactory, forFormat, byFormat, useHost, useProto, headerAction)); // Remove the X-Forwarded headers when a Forwarded transform is enabled TransformHelpers.RemoveAllXForwardedHeaders(context, DefaultXForwardedPrefix); } } else if (transformValues.TryGetValue(ClientCertKey, out var clientCertHeader)) { TransformHelpers.CheckTooManyParameters(transformValues, expected: 1); context.AddClientCertHeader(clientCertHeader); } else { return false; } return true; } private static void ValidateAction(TransformRouteValidationContext context, string key, string? headerValue) { if (!Enum.TryParse(headerValue, out var _)) { context.Errors.Add(new ArgumentException($"Unexpected value for {key}: {headerValue}. Expected one of {nameof(ForwardedTransformActions)}")); } } } ================================================ FILE: src/ReverseProxy/Transforms/HttpMethodChangeTransform.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Net.Http; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; namespace Yarp.ReverseProxy.Transforms; /// /// Replaces the HTTP method if it matches. /// public class HttpMethodChangeTransform : RequestTransform { /// /// Creates a new transform. /// /// The method to match. /// The method to it change to. public HttpMethodChangeTransform(string fromMethod, string toMethod) { if (string.IsNullOrEmpty(fromMethod)) { throw new ArgumentException($"'{nameof(fromMethod)}' cannot be null or empty.", nameof(fromMethod)); } if (string.IsNullOrEmpty(toMethod)) { throw new ArgumentException($"'{nameof(toMethod)}' cannot be null or empty.", nameof(toMethod)); } FromMethod = GetCanonicalizedValue(fromMethod); ToMethod = GetCanonicalizedValue(toMethod); } internal HttpMethod FromMethod { get; } internal HttpMethod ToMethod { get; } /// public override ValueTask ApplyAsync(RequestTransformContext context) { if (FromMethod.Equals(context.ProxyRequest.Method)) { context.ProxyRequest.Method = ToMethod; } return default; } private static HttpMethod GetCanonicalizedValue(string method) { return method switch { string _ when HttpMethods.IsGet(method) => HttpMethod.Get, string _ when HttpMethods.IsPost(method) => HttpMethod.Post, string _ when HttpMethods.IsPut(method) => HttpMethod.Put, string _ when HttpMethods.IsDelete(method) => HttpMethod.Delete, string _ when HttpMethods.IsOptions(method) => HttpMethod.Options, string _ when HttpMethods.IsHead(method) => HttpMethod.Head, string _ when HttpMethods.IsPatch(method) => HttpMethod.Patch, string _ when HttpMethods.IsTrace(method) => HttpMethod.Trace, string _ => new HttpMethod(method), }; } } ================================================ FILE: src/ReverseProxy/Transforms/HttpMethodTransformExtensions.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Yarp.ReverseProxy.Configuration; using Yarp.ReverseProxy.Transforms.Builder; namespace Yarp.ReverseProxy.Transforms; /// /// Extensions for modifying the request method. /// public static class HttpMethodTransformExtensions { /// /// Clones the route and adds the transform that will replace the HTTP method if it matches. /// public static RouteConfig WithTransformHttpMethodChange(this RouteConfig route, string fromHttpMethod, string toHttpMethod) { return route.WithTransform(transform => { transform[HttpMethodTransformFactory.HttpMethodChangeKey] = fromHttpMethod; transform[HttpMethodTransformFactory.SetKey] = toHttpMethod; }); } /// /// Adds the transform that will replace the HTTP method if it matches. /// public static TransformBuilderContext AddHttpMethodChange(this TransformBuilderContext context, string fromHttpMethod, string toHttpMethod) { context.RequestTransforms.Add(new HttpMethodChangeTransform(fromHttpMethod, toHttpMethod)); return context; } } ================================================ FILE: src/ReverseProxy/Transforms/HttpMethodTransformFactory.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using Yarp.ReverseProxy.Transforms.Builder; namespace Yarp.ReverseProxy.Transforms; internal sealed class HttpMethodTransformFactory : ITransformFactory { internal const string HttpMethodChangeKey = "HttpMethodChange"; internal const string SetKey = "Set"; public bool Validate(TransformRouteValidationContext context, IReadOnlyDictionary transformValues) { if (transformValues.TryGetValue(HttpMethodChangeKey, out var _)) { TransformHelpers.CheckTooManyParameters(transformValues, expected: 2); if (!transformValues.TryGetValue(SetKey, out var _)) { context.Errors.Add(new ArgumentException($"Unexpected parameters for HttpMethod: {string.Join(';', transformValues.Keys)}. Expected 'Set'")); } } else { return false; } return true; } public bool Build(TransformBuilderContext context, IReadOnlyDictionary transformValues) { if (transformValues.TryGetValue(HttpMethodChangeKey, out var fromHttpMethod)) { TransformHelpers.CheckTooManyParameters(transformValues, expected: 2); if (transformValues.TryGetValue(SetKey, out var toHttpMethod)) { context.AddHttpMethodChange(fromHttpMethod, toHttpMethod); } } else { return false; } return true; } } ================================================ FILE: src/ReverseProxy/Transforms/NodeFormat.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. namespace Yarp.ReverseProxy.Transforms; /// /// For use with . /// public enum NodeFormat { None, Random, RandomAndPort, RandomAndRandomPort, Unknown, UnknownAndPort, UnknownAndRandomPort, Ip, IpAndPort, IpAndRandomPort, } ================================================ FILE: src/ReverseProxy/Transforms/PathRouteValuesTransform.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.AspNetCore.Routing.Template; namespace Yarp.ReverseProxy.Transforms; /// /// Generates a new request path by plugging matched route parameters into the given pattern. /// public class PathRouteValuesTransform : RequestTransform { private readonly TemplateBinderFactory _binderFactory; /// /// Creates a new transform. /// /// The pattern used to create the new request path. /// The factory used to bind route parameters to the given path pattern. public PathRouteValuesTransform( [StringSyntax("Route")] string pattern, TemplateBinderFactory binderFactory) { ArgumentNullException.ThrowIfNull(pattern); ArgumentNullException.ThrowIfNull(binderFactory); _binderFactory = binderFactory; Pattern = RoutePatternFactory.Parse(pattern); } internal RoutePattern Pattern { get; } /// public override ValueTask ApplyAsync(RequestTransformContext context) { ArgumentNullException.ThrowIfNull(context); // TemplateBinder.BindValues will modify the RouteValueDictionary // We make a copy so that the original request is not modified by the transform var routeValues = context.HttpContext.Request.RouteValues; var routeValuesCopy = new RouteValueDictionary(); // Only copy route values used in the pattern, otherwise they'll be added as query parameters. foreach (var pattern in Pattern.Parameters) { if (routeValues.TryGetValue(pattern.Name, out var value)) { routeValuesCopy[pattern.Name] = value; } } var binder = _binderFactory.Create(Pattern); context.Path = binder.BindValues(acceptedValues: routeValuesCopy); return default; } } ================================================ FILE: src/ReverseProxy/Transforms/PathStringTransform.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; namespace Yarp.ReverseProxy.Transforms; /// /// Modifies the proxy request Path with the given value. /// public class PathStringTransform : RequestTransform { /// /// Creates a new transform. /// /// A indicating how the given value should update the existing path. /// The path value used to update the existing value. public PathStringTransform(PathTransformMode mode, PathString value) { ArgumentNullException.ThrowIfNull(value.Value); Mode = mode; Value = value; } internal PathString Value { get; } internal PathTransformMode Mode { get; } /// public override ValueTask ApplyAsync(RequestTransformContext context) { ArgumentNullException.ThrowIfNull(context); switch (Mode) { case PathTransformMode.Set: context.Path = Value; break; case PathTransformMode.Prefix: context.Path = Value + context.Path; break; case PathTransformMode.RemovePrefix: context.Path = context.Path.StartsWithSegments(Value, out var remainder) ? remainder : context.Path; break; default: throw new NotImplementedException(Mode.ToString()); } return default; } public enum PathTransformMode { Set, Prefix, RemovePrefix, } } ================================================ FILE: src/ReverseProxy/Transforms/PathTransformExtensions.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Diagnostics.CodeAnalysis; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing.Template; using Microsoft.Extensions.DependencyInjection; using Yarp.ReverseProxy.Configuration; using Yarp.ReverseProxy.Transforms.Builder; namespace Yarp.ReverseProxy.Transforms; /// /// Extensions for adding path transforms. /// public static class PathTransformExtensions { /// /// Clones the route and adds the transform which sets the request path with the given value. /// public static RouteConfig WithTransformPathSet(this RouteConfig route, PathString path) { ArgumentNullException.ThrowIfNull(path.Value); return route.WithTransform(transform => { transform[PathTransformFactory.PathSetKey] = path.Value; }); } /// /// Adds the transform which sets the request path with the given value. /// public static TransformBuilderContext AddPathSet(this TransformBuilderContext context, PathString path) { context.RequestTransforms.Add(new PathStringTransform(PathStringTransform.PathTransformMode.Set, path)); return context; } /// /// Clones the route and adds the transform which will prefix the request path with the given value. /// public static RouteConfig WithTransformPathPrefix(this RouteConfig route, PathString prefix) { ArgumentNullException.ThrowIfNull(prefix.Value); return route.WithTransform(transform => { transform[PathTransformFactory.PathPrefixKey] = prefix.Value; }); } /// /// Adds the transform which will prefix the request path with the given value. /// public static TransformBuilderContext AddPathPrefix(this TransformBuilderContext context, PathString prefix) { context.RequestTransforms.Add(new PathStringTransform(PathStringTransform.PathTransformMode.Prefix, prefix)); return context; } /// /// Clones the route and adds the transform which will remove the matching prefix from the request path. /// public static RouteConfig WithTransformPathRemovePrefix(this RouteConfig route, PathString prefix) { ArgumentNullException.ThrowIfNull(prefix.Value); return route.WithTransform(transform => { transform[PathTransformFactory.PathRemovePrefixKey] = prefix.Value; }); } /// /// Adds the transform which will remove the matching prefix from the request path. /// public static TransformBuilderContext AddPathRemovePrefix(this TransformBuilderContext context, PathString prefix) { context.RequestTransforms.Add(new PathStringTransform(PathStringTransform.PathTransformMode.RemovePrefix, prefix)); return context; } /// /// Clones the route and adds the transform which will set the request path with the given value. /// public static RouteConfig WithTransformPathRouteValues(this RouteConfig route, [StringSyntax("Route")] PathString pattern) { ArgumentNullException.ThrowIfNull(pattern.Value); return route.WithTransform(transform => { transform[PathTransformFactory.PathPatternKey] = pattern.Value; }); } /// /// Clones the route and adds the transform which will set the request path with the given value. /// public static TransformBuilderContext AddPathRouteValues(this TransformBuilderContext context, [StringSyntax("Route")] PathString pattern) { ArgumentNullException.ThrowIfNull(pattern.Value); var binder = context.Services.GetRequiredService(); context.RequestTransforms.Add(new PathRouteValuesTransform(pattern.Value, binder)); return context; } } ================================================ FILE: src/ReverseProxy/Transforms/PathTransformFactory.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing.Template; using Yarp.ReverseProxy.Transforms.Builder; namespace Yarp.ReverseProxy.Transforms; internal sealed class PathTransformFactory : ITransformFactory { internal const string PathSetKey = "PathSet"; internal const string PathPrefixKey = "PathPrefix"; internal const string PathRemovePrefixKey = "PathRemovePrefix"; internal const string PathPatternKey = "PathPattern"; private readonly TemplateBinderFactory _binderFactory; public PathTransformFactory(TemplateBinderFactory binderFactory) { ArgumentNullException.ThrowIfNull(binderFactory); _binderFactory = binderFactory; } public bool Validate(TransformRouteValidationContext context, IReadOnlyDictionary transformValues) { if (transformValues.TryGetValue(PathSetKey, out var pathSet)) { TransformHelpers.TryCheckTooManyParameters(context, transformValues, expected: 1); CheckPathNotNull(context, PathSetKey, pathSet); } else if (transformValues.TryGetValue(PathPrefixKey, out var pathPrefix)) { TransformHelpers.TryCheckTooManyParameters(context, transformValues, expected: 1); CheckPathNotNull(context, PathPrefixKey, pathPrefix); } else if (transformValues.TryGetValue(PathRemovePrefixKey, out var pathRemovePrefix)) { TransformHelpers.TryCheckTooManyParameters(context, transformValues, expected: 1); CheckPathNotNull(context, PathRemovePrefixKey, pathRemovePrefix); } else if (transformValues.TryGetValue(PathPatternKey, out var pathPattern)) { TransformHelpers.TryCheckTooManyParameters(context, transformValues, expected: 1); CheckPathNotNull(context, PathPatternKey, pathPattern); // TODO: Validate the pattern format. Does it build? } else { return false; } return true; } private static void CheckPathNotNull(TransformRouteValidationContext context, string fieldName, string? path) { if (path is null) { context.Errors.Add(new ArgumentNullException(fieldName)); } } public bool Build(TransformBuilderContext context, IReadOnlyDictionary transformValues) { if (transformValues.TryGetValue(PathSetKey, out var pathSet)) { TransformHelpers.CheckTooManyParameters(transformValues, expected: 1); var path = MakePathString(pathSet); context.AddPathSet(path); } else if (transformValues.TryGetValue(PathPrefixKey, out var pathPrefix)) { TransformHelpers.CheckTooManyParameters(transformValues, expected: 1); var path = MakePathString(pathPrefix); context.AddPathPrefix(path); } else if (transformValues.TryGetValue(PathRemovePrefixKey, out var pathRemovePrefix)) { TransformHelpers.CheckTooManyParameters(transformValues, expected: 1); var path = MakePathString(pathRemovePrefix); context.AddPathRemovePrefix(path); } else if (transformValues.TryGetValue(PathPatternKey, out var pathPattern)) { TransformHelpers.CheckTooManyParameters(transformValues, expected: 1); var path = MakePathString(pathPattern); // We don't use the extension here because we want to avoid doing a DI lookup for the binder every time. context.RequestTransforms.Add(new PathRouteValuesTransform(path.Value!, _binderFactory)); } else { return false; } return true; } private static PathString MakePathString(string path) { ArgumentNullException.ThrowIfNull(path); if (!path.StartsWith('/')) { path = "/" + path; } return new PathString(path); } } ================================================ FILE: src/ReverseProxy/Transforms/QueryParameterFromRouteTransform.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; namespace Yarp.ReverseProxy.Transforms; public class QueryParameterRouteTransform : QueryParameterTransform { public QueryParameterRouteTransform(QueryStringTransformMode mode, string key, string routeValueKey) : base(mode, key) { if (string.IsNullOrEmpty(key)) { throw new ArgumentException($"'{nameof(key)}' cannot be null or empty.", nameof(key)); } if (string.IsNullOrEmpty(routeValueKey)) { throw new ArgumentException($"'{nameof(routeValueKey)}' cannot be null or empty.", nameof(routeValueKey)); } RouteValueKey = routeValueKey; } internal string RouteValueKey { get; } /// protected override string? GetValue(RequestTransformContext context) { var routeValues = context.HttpContext.Request.RouteValues; if (!routeValues.TryGetValue(RouteValueKey, out var value)) { return null; } return value?.ToString(); } } ================================================ FILE: src/ReverseProxy/Transforms/QueryParameterFromStaticTransform.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; namespace Yarp.ReverseProxy.Transforms; public class QueryParameterFromStaticTransform : QueryParameterTransform { public QueryParameterFromStaticTransform(QueryStringTransformMode mode, string key, string value) : base(mode, key) { if (string.IsNullOrEmpty(key)) { throw new ArgumentException($"'{nameof(key)}' cannot be null or empty.", nameof(key)); } ArgumentNullException.ThrowIfNull(value); Value = value; } internal string Value { get; } /// protected override string GetValue(RequestTransformContext context) { return Value; } } ================================================ FILE: src/ReverseProxy/Transforms/QueryParameterRemoveTransform.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Threading.Tasks; namespace Yarp.ReverseProxy.Transforms; /// /// A request transform that removes the given query parameter. /// public class QueryParameterRemoveTransform : RequestTransform { public QueryParameterRemoveTransform(string key) { if (string.IsNullOrEmpty(key)) { throw new ArgumentException($"'{nameof(key)}' cannot be null or empty.", nameof(key)); } Key = key; } internal string Key { get; } /// public override ValueTask ApplyAsync(RequestTransformContext context) { ArgumentNullException.ThrowIfNull(context); context.Query.Collection.Remove(Key); return default; } } ================================================ FILE: src/ReverseProxy/Transforms/QueryParameterTransform.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Threading.Tasks; using Microsoft.Extensions.Primitives; namespace Yarp.ReverseProxy.Transforms; public abstract class QueryParameterTransform : RequestTransform { public QueryParameterTransform(QueryStringTransformMode mode, string key) { if (string.IsNullOrEmpty(key)) { throw new ArgumentException($"'{nameof(key)}' cannot be null or empty.", nameof(key)); } Mode = mode; Key = key; } internal QueryStringTransformMode Mode { get; } internal string Key { get; } /// public override ValueTask ApplyAsync(RequestTransformContext context) { ArgumentNullException.ThrowIfNull(context); var value = GetValue(context); if (value is not null) { switch (Mode) { case QueryStringTransformMode.Append: StringValues newValue = value; if (context.Query.Collection.TryGetValue(Key, out var currentValue)) { newValue = StringValues.Concat(currentValue, value); } context.Query.Collection[Key] = newValue; break; case QueryStringTransformMode.Set: context.Query.Collection[Key] = value; break; default: throw new NotImplementedException(Mode.ToString()); } } return default; } protected abstract string? GetValue(RequestTransformContext context); } public enum QueryStringTransformMode { Append, Set } ================================================ FILE: src/ReverseProxy/Transforms/QueryTransformContext.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.Extensions.Primitives; namespace Yarp.ReverseProxy.Transforms; /// /// Transform state for use with /// public class QueryTransformContext { private readonly HttpRequest _request; private readonly QueryString _originalQueryString; private Dictionary? _modifiedQueryParameters; public QueryTransformContext(HttpRequest request) { ArgumentNullException.ThrowIfNull(request); _request = request; _originalQueryString = request.QueryString; _modifiedQueryParameters = null; } public QueryString QueryString { get { if (_modifiedQueryParameters is null) { return _originalQueryString; } return new QueryBuilder(_modifiedQueryParameters).ToQueryString(); } } public IDictionary Collection { get { if (_modifiedQueryParameters is null) { _modifiedQueryParameters = new Dictionary(_request.Query, StringComparer.OrdinalIgnoreCase); } return _modifiedQueryParameters; } } } ================================================ FILE: src/ReverseProxy/Transforms/QueryTransformExtensions.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Yarp.ReverseProxy.Configuration; using Yarp.ReverseProxy.Transforms.Builder; namespace Yarp.ReverseProxy.Transforms; /// /// Extensions for adding query transforms. /// public static class QueryTransformExtensions { /// /// Clones the route and adds the transform that will append or set the query parameter from the given value. /// public static RouteConfig WithTransformQueryValue(this RouteConfig route, string queryKey, string value, bool append = true) { var type = append ? QueryTransformFactory.AppendKey : QueryTransformFactory.SetKey; return route.WithTransform(transform => { transform[QueryTransformFactory.QueryValueParameterKey] = queryKey; transform[type] = value; }); } /// /// Adds the transform that will append or set the query parameter from the given value. /// public static TransformBuilderContext AddQueryValue(this TransformBuilderContext context, string queryKey, string value, bool append = true) { context.RequestTransforms.Add(new QueryParameterFromStaticTransform( append ? QueryStringTransformMode.Append : QueryStringTransformMode.Set, queryKey, value)); return context; } /// /// Clones the route and adds the transform that will append or set the query parameter from a route value. /// public static RouteConfig WithTransformQueryRouteValue(this RouteConfig route, string queryKey, string routeValueKey, bool append = true) { var type = append ? QueryTransformFactory.AppendKey : QueryTransformFactory.SetKey; return route.WithTransform(transform => { transform[QueryTransformFactory.QueryRouteParameterKey] = queryKey; transform[type] = routeValueKey; }); } /// /// Adds the transform that will append or set the query parameter from a route value. /// public static TransformBuilderContext AddQueryRouteValue(this TransformBuilderContext context, string queryKey, string routeValueKey, bool append = true) { context.RequestTransforms.Add(new QueryParameterRouteTransform( append ? QueryStringTransformMode.Append : QueryStringTransformMode.Set, queryKey, routeValueKey)); return context; } /// /// Clones the route and adds the transform that will remove the given query key. /// public static RouteConfig WithTransformQueryRemoveKey(this RouteConfig route, string queryKey) { return route.WithTransform(transform => { transform[QueryTransformFactory.QueryRemoveParameterKey] = queryKey; }); } /// /// Adds the transform that will remove the given query key. /// public static TransformBuilderContext AddQueryRemoveKey(this TransformBuilderContext context, string queryKey) { context.RequestTransforms.Add(new QueryParameterRemoveTransform(queryKey)); return context; } } ================================================ FILE: src/ReverseProxy/Transforms/QueryTransformFactory.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using Yarp.ReverseProxy.Transforms.Builder; namespace Yarp.ReverseProxy.Transforms; internal sealed class QueryTransformFactory : ITransformFactory { internal const string QueryValueParameterKey = "QueryValueParameter"; internal const string QueryRouteParameterKey = "QueryRouteParameter"; internal const string QueryRemoveParameterKey = "QueryRemoveParameter"; internal const string AppendKey = "Append"; internal const string SetKey = "Set"; public bool Validate(TransformRouteValidationContext context, IReadOnlyDictionary transformValues) { if (transformValues.TryGetValue(QueryValueParameterKey, out var queryValueParameter)) { TransformHelpers.TryCheckTooManyParameters(context, transformValues, expected: 2); if (!transformValues.TryGetValue(AppendKey, out var _) && !transformValues.TryGetValue(SetKey, out var _)) { context.Errors.Add(new ArgumentException($"Unexpected parameters for QueryValueParameter: {string.Join(';', transformValues.Keys)}. Expected 'Append' or 'Set'.")); } } else if (transformValues.TryGetValue(QueryRouteParameterKey, out var queryRouteParameter)) { TransformHelpers.TryCheckTooManyParameters(context, transformValues, expected: 2); if (!transformValues.TryGetValue(AppendKey, out var _) && !transformValues.TryGetValue(SetKey, out var _)) { context.Errors.Add(new ArgumentException($"Unexpected parameters for QueryRouteParameter: {string.Join(';', transformValues.Keys)}. Expected 'Append' or 'Set'.")); } } else if (transformValues.TryGetValue(QueryRemoveParameterKey, out var removeQueryParameter)) { TransformHelpers.TryCheckTooManyParameters(context, transformValues, expected: 1); } else { return false; } return true; } public bool Build(TransformBuilderContext context, IReadOnlyDictionary transformValues) { if (transformValues.TryGetValue(QueryValueParameterKey, out var queryValueParameter)) { TransformHelpers.CheckTooManyParameters(transformValues, expected: 2); if (transformValues.TryGetValue(AppendKey, out var appendValue)) { context.AddQueryValue(queryValueParameter, appendValue, append: true); } else if (transformValues.TryGetValue(SetKey, out var setValue)) { context.AddQueryValue(queryValueParameter, setValue, append: false); } else { throw new NotSupportedException(string.Join(";", transformValues.Keys)); } } else if (transformValues.TryGetValue(QueryRouteParameterKey, out var queryRouteParameter)) { TransformHelpers.CheckTooManyParameters(transformValues, expected: 2); if (transformValues.TryGetValue(AppendKey, out var routeValueKeyAppend)) { context.AddQueryRouteValue(queryRouteParameter, routeValueKeyAppend, append: true); } else if (transformValues.TryGetValue(SetKey, out var routeValueKeySet)) { context.AddQueryRouteValue(queryRouteParameter, routeValueKeySet, append: false); } else { throw new NotSupportedException(string.Join(";", transformValues.Keys)); } } else if (transformValues.TryGetValue(QueryRemoveParameterKey, out var removeQueryParameter)) { TransformHelpers.CheckTooManyParameters(transformValues, expected: 1); context.AddQueryRemoveKey(removeQueryParameter); } else { return false; } return true; } } ================================================ FILE: src/ReverseProxy/Transforms/RequestFuncTransform.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Threading.Tasks; namespace Yarp.ReverseProxy.Transforms; /// /// A request transform that runs the given Func. /// public class RequestFuncTransform : RequestTransform { private readonly Func _func; public RequestFuncTransform(Func func) { ArgumentNullException.ThrowIfNull(func); _func = func; } /// public override ValueTask ApplyAsync(RequestTransformContext context) { return _func(context); } } ================================================ FILE: src/ReverseProxy/Transforms/RequestHeaderClientCertTransform.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Threading.Tasks; namespace Yarp.ReverseProxy.Transforms; /// /// Base64 encodes the client certificate (if any) and sets it as the header value. /// public class RequestHeaderClientCertTransform : RequestTransform { public RequestHeaderClientCertTransform(string headerName) { if (string.IsNullOrEmpty(headerName)) { throw new ArgumentException($"'{nameof(headerName)}' cannot be null or empty.", nameof(headerName)); } HeaderName = headerName; } internal string HeaderName { get; } /// public override ValueTask ApplyAsync(RequestTransformContext context) { ArgumentNullException.ThrowIfNull(context); RemoveHeader(context, HeaderName); var clientCert = context.HttpContext.Connection.ClientCertificate; if (clientCert is not null) { var encoded = Convert.ToBase64String(clientCert.RawData); AddHeader(context, HeaderName, encoded); } return default; } } ================================================ FILE: src/ReverseProxy/Transforms/RequestHeaderForwardedTransform.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Diagnostics; using System.Net; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; using Yarp.ReverseProxy.Utilities; namespace Yarp.ReverseProxy.Transforms; /// /// An implementation of the Forwarded header as defined in https://tools.ietf.org/html/rfc7239. /// public class RequestHeaderForwardedTransform : RequestTransform { internal static readonly RequestHeaderForwardedTransform RemoveTransform = new RequestHeaderForwardedTransform(new NullRandomFactory(), NodeFormat.Random, NodeFormat.Random, false, false, ForwardedTransformActions.Remove); private const string ForwardedHeaderName = "Forwarded"; // obfnode = "_" 1*( ALPHA / DIGIT / "." / "_" / "-") private const string ObfChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789._-"; private readonly IRandomFactory _randomFactory; public RequestHeaderForwardedTransform(IRandomFactory randomFactory, NodeFormat forFormat, NodeFormat byFormat, bool host, bool proto, ForwardedTransformActions action) { ArgumentNullException.ThrowIfNull(randomFactory); _randomFactory = randomFactory; ForFormat = forFormat; ByFormat = byFormat; HostEnabled = host; ProtoEnabled = proto; Debug.Assert(action != ForwardedTransformActions.Off); TransformAction = action; } internal NodeFormat ForFormat { get; } internal NodeFormat ByFormat { get; } internal bool HostEnabled { get; } internal bool ProtoEnabled { get; } internal ForwardedTransformActions TransformAction { get; } /// public override ValueTask ApplyAsync(RequestTransformContext context) { ArgumentNullException.ThrowIfNull(context); var httpContext = context.HttpContext; switch (TransformAction) { case ForwardedTransformActions.Set: RemoveHeader(context, ForwardedHeaderName); AddHeader(context, ForwardedHeaderName, GetHeaderValue(httpContext)); break; case ForwardedTransformActions.Append: var existingValues = TakeHeader(context, ForwardedHeaderName); var values = StringValues.Concat(existingValues, GetHeaderValue(httpContext)); AddHeader(context, ForwardedHeaderName, values); break; case ForwardedTransformActions.Remove: RemoveHeader(context, ForwardedHeaderName); break; default: throw new NotImplementedException(TransformAction.ToString()); } return default; } private string GetHeaderValue(HttpContext httpContext) { var builder = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]); AppendProto(httpContext, ref builder); AppendHost(httpContext, ref builder); AppendFor(httpContext, ref builder); AppendBy(httpContext, ref builder); return builder.ToString(); } private void AppendProto(HttpContext context, ref ValueStringBuilder builder) { if (ProtoEnabled) { // Always first doesn't need to check for ';' builder.Append("proto="); builder.Append(context.Request.Scheme); } } private void AppendHost(HttpContext context, ref ValueStringBuilder builder) { if (HostEnabled) { if (builder.Length > 0) { builder.Append(';'); } // Quoted because of the ':' when there's a port. builder.Append("host=\""); builder.Append(context.Request.Host.ToUriComponent()); builder.Append('"'); } } private void AppendFor(HttpContext context, ref ValueStringBuilder builder) { if (ForFormat > NodeFormat.None) { if (builder.Length > 0) { builder.Append(';'); } builder.Append("for="); AppendNode(context.Connection.RemoteIpAddress, context.Connection.RemotePort, ForFormat, ref builder); } } private void AppendBy(HttpContext context, ref ValueStringBuilder builder) { if (ByFormat > NodeFormat.None) { if (builder.Length > 0) { builder.Append(';'); } builder.Append("by="); AppendNode(context.Connection.LocalIpAddress, context.Connection.LocalPort, ByFormat, ref builder); } } // https://tools.ietf.org/html/rfc7239#section-6 private void AppendNode(IPAddress? ipAddress, int port, NodeFormat format, ref ValueStringBuilder builder) { // Prefer IPv4 formatting if (ipAddress is { IsIPv4MappedToIPv6: true }) { ipAddress = ipAddress.MapToIPv4(); } // "It is important to note that an IPv6 address and any nodename with // 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 addRandomPort = (format == NodeFormat.IpAndRandomPort || format == NodeFormat.UnknownAndRandomPort || format == NodeFormat.RandomAndRandomPort); var ipv6 = (format == NodeFormat.Ip || format == NodeFormat.IpAndPort || format == NodeFormat.IpAndRandomPort) && ipAddress is not null && ipAddress.AddressFamily == System.Net.Sockets.AddressFamily.InterNetworkV6; var quote = addPort || addRandomPort || ipv6; if (quote) { builder.Append('"'); } switch (format) { case NodeFormat.Ip: case NodeFormat.IpAndPort: case NodeFormat.IpAndRandomPort: if (ipAddress is not null) { if (ipv6) { builder.Append('['); } builder.Append(ipAddress.ToString()); if (ipv6) { builder.Append(']'); } break; } // This primarily happens in test environments that don't use real connections. 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(ref builder); break; default: throw new NotImplementedException(format.ToString()); } if (addPort) { builder.Append(':'); builder.Append(port); } else if (addRandomPort) { builder.Append(':'); AppendRandom(ref builder); } if (quote) { builder.Append('"'); } } // https://tools.ietf.org/html/rfc7239#section-6.3 private void AppendRandom(ref ValueStringBuilder builder) { var random = _randomFactory.CreateRandomInstance(); builder.Append('_'); // This length is arbitrary. for (var i = 0; i < 9; i++) { builder.Append(ObfChars[random.Next(ObfChars.Length)]); } } } ================================================ FILE: src/ReverseProxy/Transforms/RequestHeaderOriginalHostTransform.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Threading.Tasks; using Microsoft.Net.Http.Headers; using Yarp.ReverseProxy.Forwarder; using Yarp.ReverseProxy.Model; namespace Yarp.ReverseProxy.Transforms; /// /// A transform used to include or suppress the original request host header. /// public class RequestHeaderOriginalHostTransform : RequestTransform { public static readonly RequestHeaderOriginalHostTransform OriginalHost = new(true); public static readonly RequestHeaderOriginalHostTransform SuppressHost = new(false); /// /// Creates a new . /// /// True if the original request host header should be used, /// false otherwise. private RequestHeaderOriginalHostTransform(bool useOriginalHost) { UseOriginalHost = useOriginalHost; } internal bool UseOriginalHost { get; } public override ValueTask ApplyAsync(RequestTransformContext context) { var destinationConfigHost = context.HttpContext.Features.Get()?.ProxiedDestination?.Model.Config?.Host; var originalHost = context.HttpContext.Request.Host.Value is { Length: > 0 } host ? host : null; var existingHost = RequestUtilities.TryGetValues(context.ProxyRequest.Headers, HeaderNames.Host, out var currentHost) ? currentHost.ToString() : null; if (UseOriginalHost) { if (!context.HeadersCopied && existingHost is null) { // Propagate the host if the transform pipeline didn't already override it. // If there was no original host specified, allow the destination config host to flow through. context.ProxyRequest.Headers.TryAddWithoutValidation(HeaderNames.Host, originalHost ?? destinationConfigHost); } } else if (existingHost is null || string.Equals(originalHost, existingHost, StringComparison.Ordinal)) { // Use the host from destination configuration (which may be null) if either: // * there is no host header set, or // * the original host header is being suppressed and has not been modified by the transform pipeline context.ProxyRequest.Headers.Host = destinationConfigHost; } return default; } } ================================================ FILE: src/ReverseProxy/Transforms/RequestHeaderRemoveTransform.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Threading.Tasks; namespace Yarp.ReverseProxy.Transforms; /// /// Removes a request header. /// public class RequestHeaderRemoveTransform : RequestTransform { public RequestHeaderRemoveTransform(string headerName) { if (string.IsNullOrEmpty(headerName)) { throw new ArgumentException($"'{nameof(headerName)}' cannot be null or empty.", nameof(headerName)); } HeaderName = headerName; } internal string HeaderName { get; } /// public override ValueTask ApplyAsync(RequestTransformContext context) { ArgumentNullException.ThrowIfNull(context); RemoveHeader(context, HeaderName); return default; } } ================================================ FILE: src/ReverseProxy/Transforms/RequestHeaderRouteValueTransform.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; namespace Yarp.ReverseProxy.Transforms; public class RequestHeaderRouteValueTransform : RequestHeaderTransform { public RequestHeaderRouteValueTransform(string headerName, string routeValueKey, bool append) : base(headerName, append) { if (string.IsNullOrEmpty(headerName)) { throw new ArgumentException($"'{nameof(headerName)}' cannot be null or empty.", nameof(headerName)); } if (string.IsNullOrEmpty(routeValueKey)) { throw new ArgumentException($"'{nameof(routeValueKey)}' cannot be null or empty.", nameof(routeValueKey)); } RouteValueKey = routeValueKey; } internal string RouteValueKey { get; } protected override string? GetValue(RequestTransformContext context) { var routeValues = context.HttpContext.Request.RouteValues; if (!routeValues.TryGetValue(RouteValueKey, out var value)) { return null; } return value?.ToString(); } } ================================================ FILE: src/ReverseProxy/Transforms/RequestHeaderTransform.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Threading.Tasks; using Microsoft.Extensions.Primitives; namespace Yarp.ReverseProxy.Transforms; public abstract class RequestHeaderTransform : RequestTransform { protected RequestHeaderTransform(string headerName, bool append) { if (string.IsNullOrEmpty(headerName)) { throw new ArgumentException($"'{nameof(headerName)}' cannot be null or empty.", nameof(headerName)); } Append = append; HeaderName = headerName; } internal bool Append { get; } internal string HeaderName { get; } public override ValueTask ApplyAsync(RequestTransformContext context) { ArgumentNullException.ThrowIfNull(context); var value = GetValue(context); if (value is null) { return default; } if (Append) { var existingValues = TakeHeader(context, HeaderName); var newValue = StringValues.Concat(existingValues, value); AddHeader(context, HeaderName, newValue); } else { RemoveHeader(context, HeaderName); AddHeader(context, HeaderName, value); } return default; } protected abstract string? GetValue(RequestTransformContext context); } ================================================ FILE: src/ReverseProxy/Transforms/RequestHeaderValueTransform.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Threading.Tasks; namespace Yarp.ReverseProxy.Transforms; /// /// Sets or appends simple request header values. /// public class RequestHeaderValueTransform : RequestHeaderTransform { public RequestHeaderValueTransform(string headerName, string value, bool append) : base(headerName, append) { ArgumentException.ThrowIfNullOrEmpty(headerName); ArgumentNullException.ThrowIfNull(value); Value = value; } internal string Value { get; } /// public override ValueTask ApplyAsync(RequestTransformContext context) { return base.ApplyAsync(context); } /// protected override string GetValue(RequestTransformContext context) { return Value; } } ================================================ FILE: src/ReverseProxy/Transforms/RequestHeaderXForwardedForTransform.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Diagnostics; using System.Threading.Tasks; using Microsoft.Extensions.Primitives; namespace Yarp.ReverseProxy.Transforms; /// /// Sets or appends the X-Forwarded-For header with the previous client's IP address. /// public class RequestHeaderXForwardedForTransform : RequestTransform { /// /// Creates a new transform. /// /// The header name. /// Action to applied to the header. public RequestHeaderXForwardedForTransform(string headerName, ForwardedTransformActions action) { if (string.IsNullOrEmpty(headerName)) { throw new ArgumentException($"'{nameof(headerName)}' cannot be null or empty.", nameof(headerName)); } HeaderName = headerName; Debug.Assert(action != ForwardedTransformActions.Off); TransformAction = action; } internal string HeaderName { get; } internal ForwardedTransformActions TransformAction { get; } /// public override ValueTask ApplyAsync(RequestTransformContext context) { ArgumentNullException.ThrowIfNull(context); string? remoteIp = null; var remoteIpAddress = context.HttpContext.Connection.RemoteIpAddress; if (remoteIpAddress is not null) { remoteIp = remoteIpAddress.IsIPv4MappedToIPv6 ? remoteIpAddress.MapToIPv4().ToString() : remoteIpAddress.ToString(); } switch (TransformAction) { case ForwardedTransformActions.Set: RemoveHeader(context, HeaderName); if (remoteIp is not null) { AddHeader(context, HeaderName, remoteIp); } break; case ForwardedTransformActions.Append: Append(context, remoteIp); break; case ForwardedTransformActions.Remove: RemoveHeader(context, HeaderName); break; default: throw new NotImplementedException(TransformAction.ToString()); } return default; } private void Append(RequestTransformContext context, string? remoteIp) { var existingValues = TakeHeader(context, HeaderName); if (remoteIp is null) { if (!string.IsNullOrEmpty(existingValues)) { AddHeader(context, HeaderName, existingValues); } } else { var values = StringValues.Concat(existingValues, remoteIp); AddHeader(context, HeaderName, values); } } } ================================================ FILE: src/ReverseProxy/Transforms/RequestHeaderXForwardedHostTransform.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Diagnostics; using System.Threading.Tasks; using Microsoft.Extensions.Primitives; namespace Yarp.ReverseProxy.Transforms; /// /// Sets or appends the X-Forwarded-Host header with the request's original Host header. /// public class RequestHeaderXForwardedHostTransform : RequestTransform { /// /// Creates a new transform. /// /// The header name. /// Action to applied to the header. public RequestHeaderXForwardedHostTransform(string headerName, ForwardedTransformActions action) { ArgumentException.ThrowIfNullOrEmpty(headerName); HeaderName = headerName; Debug.Assert(action != ForwardedTransformActions.Off); TransformAction = action; } internal string HeaderName { get; } internal ForwardedTransformActions TransformAction { get; } /// public override ValueTask ApplyAsync(RequestTransformContext context) { ArgumentNullException.ThrowIfNull(context); var host = context.HttpContext.Request.Host; switch (TransformAction) { case ForwardedTransformActions.Set: RemoveHeader(context, HeaderName); if (host.HasValue) { AddHeader(context, HeaderName, host.ToUriComponent()); } break; case ForwardedTransformActions.Append: Append(context, host); break; case ForwardedTransformActions.Remove: RemoveHeader(context, HeaderName); break; default: throw new NotImplementedException(TransformAction.ToString()); } return default; } private void Append(RequestTransformContext context, Microsoft.AspNetCore.Http.HostString host) { var existingValues = TakeHeader(context, HeaderName); if (!host.HasValue) { if (!string.IsNullOrEmpty(existingValues)) { AddHeader(context, HeaderName, existingValues); } } else { var values = StringValues.Concat(existingValues, host.ToUriComponent()); AddHeader(context, HeaderName, values); } } } ================================================ FILE: src/ReverseProxy/Transforms/RequestHeaderXForwardedPrefixTransform.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Diagnostics; using System.Threading.Tasks; using Microsoft.Extensions.Primitives; namespace Yarp.ReverseProxy.Transforms; /// /// Sets or appends the X-Forwarded-Prefix header with the request's original PathBase. /// public class RequestHeaderXForwardedPrefixTransform : RequestTransform { public RequestHeaderXForwardedPrefixTransform(string headerName, ForwardedTransformActions action) { if (string.IsNullOrEmpty(headerName)) { throw new ArgumentException($"'{nameof(headerName)}' cannot be null or empty.", nameof(headerName)); } HeaderName = headerName; Debug.Assert(action != ForwardedTransformActions.Off); TransformAction = action; } internal string HeaderName { get; } internal ForwardedTransformActions TransformAction { get; } /// public override ValueTask ApplyAsync(RequestTransformContext context) { ArgumentNullException.ThrowIfNull(context); var pathBase = context.HttpContext.Request.PathBase; switch (TransformAction) { case ForwardedTransformActions.Set: RemoveHeader(context, HeaderName); if (pathBase.HasValue) { AddHeader(context, HeaderName, pathBase.ToUriComponent()); } break; case ForwardedTransformActions.Append: Append(context, pathBase); break; case ForwardedTransformActions.Remove: RemoveHeader(context, HeaderName); break; default: throw new NotImplementedException(TransformAction.ToString()); } return default; } private void Append(RequestTransformContext context, Microsoft.AspNetCore.Http.PathString pathBase) { var existingValues = TakeHeader(context, HeaderName); if (!pathBase.HasValue) { if (!string.IsNullOrEmpty(existingValues)) { AddHeader(context, HeaderName, existingValues); } } else { var values = StringValues.Concat(existingValues, pathBase.ToUriComponent()); AddHeader(context, HeaderName, values); } } } ================================================ FILE: src/ReverseProxy/Transforms/RequestHeaderXForwardedProtoTransform.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Diagnostics; using System.Threading.Tasks; using Microsoft.Extensions.Primitives; namespace Yarp.ReverseProxy.Transforms; /// /// Sets or appends the X-Forwarded-Proto header with the request's original url scheme. /// public class RequestHeaderXForwardedProtoTransform : RequestTransform { /// /// Creates a new transform. /// /// The header name. /// Action to applied to the header. public RequestHeaderXForwardedProtoTransform(string headerName, ForwardedTransformActions action) { if (string.IsNullOrEmpty(headerName)) { throw new ArgumentException($"'{nameof(headerName)}' cannot be null or empty.", nameof(headerName)); } HeaderName = headerName; Debug.Assert(action != ForwardedTransformActions.Off); TransformAction = action; } internal string HeaderName { get; } internal ForwardedTransformActions TransformAction { get; } /// public override ValueTask ApplyAsync(RequestTransformContext context) { ArgumentNullException.ThrowIfNull(context); var scheme = context.HttpContext.Request.Scheme; switch (TransformAction) { case ForwardedTransformActions.Set: RemoveHeader(context, HeaderName); AddHeader(context, HeaderName, scheme); break; case ForwardedTransformActions.Append: var existingValues = TakeHeader(context, HeaderName); var values = StringValues.Concat(existingValues, scheme); AddHeader(context, HeaderName, values); break; case ForwardedTransformActions.Remove: RemoveHeader(context, HeaderName); break; default: throw new NotImplementedException(TransformAction.ToString()); } return default; } } ================================================ FILE: src/ReverseProxy/Transforms/RequestHeadersAllowedTransform.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Frozen; using System.Collections.Generic; using System.Diagnostics; using System.Threading.Tasks; using Microsoft.Extensions.Primitives; namespace Yarp.ReverseProxy.Transforms; /// /// Copies only allowed request headers. /// public class RequestHeadersAllowedTransform : RequestTransform { public RequestHeadersAllowedTransform(string[] allowedHeaders) { ArgumentNullException.ThrowIfNull(allowedHeaders); AllowedHeaders = allowedHeaders; AllowedHeadersSet = new HashSet(allowedHeaders, StringComparer.OrdinalIgnoreCase).ToFrozenSet(StringComparer.OrdinalIgnoreCase); } internal string[] AllowedHeaders { get; } private FrozenSet AllowedHeadersSet { get; } /// public override ValueTask ApplyAsync(RequestTransformContext context) { ArgumentNullException.ThrowIfNull(context); Debug.Assert(!context.HeadersCopied); foreach (var header in context.HttpContext.Request.Headers) { var headerName = header.Key; var headerValue = header.Value; if (!StringValues.IsNullOrEmpty(headerValue) && AllowedHeadersSet.Contains(headerName)) { AddHeader(context, headerName, headerValue); } } context.HeadersCopied = true; return default; } } ================================================ FILE: src/ReverseProxy/Transforms/RequestHeadersTransformExtensions.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Yarp.ReverseProxy.Configuration; using Yarp.ReverseProxy.Transforms.Builder; namespace Yarp.ReverseProxy.Transforms; /// /// Extensions for adding request header transforms. /// public static class RequestHeadersTransformExtensions { /// /// Clones the route and adds the transform which will enable or suppress copying request headers to the proxy request. /// public static RouteConfig WithTransformCopyRequestHeaders(this RouteConfig route, bool copy = true) { return route.WithTransform(transform => { transform[RequestHeadersTransformFactory.RequestHeadersCopyKey] = copy ? bool.TrueString : bool.FalseString; }); } /// /// Clones the route and adds the transform which will copy the incoming request Host header to the proxy request. /// public static RouteConfig WithTransformUseOriginalHostHeader(this RouteConfig route, bool useOriginal = true) { return route.WithTransform(transform => { transform[RequestHeadersTransformFactory.RequestHeaderOriginalHostKey] = useOriginal ? bool.TrueString : bool.FalseString; }); } /// /// Clones the route and adds the transform which will append or set the request header. /// public static RouteConfig WithTransformRequestHeader(this RouteConfig route, string headerName, string value, bool append = true) { var type = append ? RequestHeadersTransformFactory.AppendKey : RequestHeadersTransformFactory.SetKey; return route.WithTransform(transform => { transform[RequestHeadersTransformFactory.RequestHeaderKey] = headerName; transform[type] = value; }); } /// /// Clones the route and adds the transform which will append or set the request header from a route value. /// public static RouteConfig WithTransformRequestHeaderRouteValue(this RouteConfig route, string headerName, string routeValueKey, bool append = true) { var type = append ? RequestHeadersTransformFactory.AppendKey : RequestHeadersTransformFactory.SetKey; return route.WithTransform(transform => { transform[RequestHeadersTransformFactory.RequestHeaderRouteValueKey] = headerName; transform[type] = routeValueKey; }); } /// /// Clones the route and adds the transform which will remove the request header. /// public static RouteConfig WithTransformRequestHeaderRemove(this RouteConfig route, string headerName) { return route.WithTransform(transform => { transform[RequestHeadersTransformFactory.RequestHeaderRemoveKey] = headerName; }); } /// /// Clones the route and adds the transform which will only copy the allowed request headers. Other transforms /// that modify or append to existing headers may be affected if not included in the allow list. /// public static RouteConfig WithTransformRequestHeadersAllowed(this RouteConfig route, params string[] allowedHeaders) { return route.WithTransform(transform => { transform[RequestHeadersTransformFactory.RequestHeadersAllowedKey] = string.Join(';', allowedHeaders); }); } /// /// Adds the transform which will append or set the request header. /// public static TransformBuilderContext AddRequestHeader(this TransformBuilderContext context, string headerName, string value, bool append = true) { context.RequestTransforms.Add(new RequestHeaderValueTransform(headerName, value, append)); return context; } /// /// Adds the transform which will append or set the request header from a route value. /// public static TransformBuilderContext AddRequestHeaderRouteValue(this TransformBuilderContext context, string headerName, string routeValueKey, bool append = true) { context.RequestTransforms.Add(new RequestHeaderRouteValueTransform(headerName, routeValueKey, append)); return context; } /// /// Adds the transform which will remove the request header. /// public static TransformBuilderContext AddRequestHeaderRemove(this TransformBuilderContext context, string headerName) { context.RequestTransforms.Add(new RequestHeaderRemoveTransform(headerName)); return context; } /// /// Adds the transform which will only copy the allowed request headers. Other transforms /// that modify or append to existing headers may be affected if not included in the allow list. /// public static TransformBuilderContext AddRequestHeadersAllowed(this TransformBuilderContext context, params string[] allowedHeaders) { context.CopyRequestHeaders = false; context.RequestTransforms.Add(new RequestHeadersAllowedTransform(allowedHeaders)); return context; } /// /// Adds the transform which will copy or remove the original host header. /// public static TransformBuilderContext AddOriginalHost(this TransformBuilderContext context, bool useOriginal = true) { if (useOriginal) { context.RequestTransforms.Add(RequestHeaderOriginalHostTransform.OriginalHost); } else { context.RequestTransforms.Add(RequestHeaderOriginalHostTransform.SuppressHost); } return context; } } ================================================ FILE: src/ReverseProxy/Transforms/RequestHeadersTransformFactory.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using Yarp.ReverseProxy.Transforms.Builder; namespace Yarp.ReverseProxy.Transforms; internal sealed class RequestHeadersTransformFactory : ITransformFactory { internal const string RequestHeadersCopyKey = "RequestHeadersCopy"; internal const string RequestHeaderOriginalHostKey = "RequestHeaderOriginalHost"; internal const string RequestHeaderKey = "RequestHeader"; internal const string RequestHeaderRouteValueKey = "RequestHeaderRouteValue"; internal const string RequestHeaderRemoveKey = "RequestHeaderRemove"; internal const string RequestHeadersAllowedKey = "RequestHeadersAllowed"; internal const string AppendKey = "Append"; internal const string SetKey = "Set"; public bool Validate(TransformRouteValidationContext context, IReadOnlyDictionary transformValues) { if (transformValues.TryGetValue(RequestHeadersCopyKey, out var copyHeaders)) { TransformHelpers.TryCheckTooManyParameters(context, transformValues, expected: 1); if (!bool.TryParse(copyHeaders, out var _)) { context.Errors.Add(new ArgumentException($"Unexpected value for RequestHeaderCopy: {copyHeaders}. Expected 'true' or 'false'")); } } else if (transformValues.TryGetValue(RequestHeaderOriginalHostKey, out var originalHost)) { TransformHelpers.TryCheckTooManyParameters(context, transformValues, expected: 1); if (!bool.TryParse(originalHost, out var _)) { context.Errors.Add(new ArgumentException($"Unexpected value for RequestHeaderOriginalHost: {originalHost}. Expected 'true' or 'false'")); } } else if (transformValues.TryGetValue(RequestHeaderKey, out var _)) { TransformHelpers.TryCheckTooManyParameters(context, transformValues, expected: 2); if (!transformValues.TryGetValue(SetKey, out var _) && !transformValues.TryGetValue(AppendKey, out var _)) { context.Errors.Add(new ArgumentException($"Unexpected parameters for RequestHeader: {string.Join(';', transformValues.Keys)}. Expected 'Set' or 'Append'")); } } else if (transformValues.TryGetValue(RequestHeaderRouteValueKey, out var _)) { TransformHelpers.TryCheckTooManyParameters(context, transformValues, expected: 2); if (!transformValues.TryGetValue(AppendKey, out _) && !transformValues.TryGetValue(SetKey, out _)) { context.Errors.Add(new ArgumentException($"Unexpected parameters for RequestHeaderFromRoute: {string.Join(';', transformValues.Keys)}. Expected 'Set' or 'Append'.")); } } else if (transformValues.TryGetValue(RequestHeaderRemoveKey, out var _)) { TransformHelpers.TryCheckTooManyParameters(context, transformValues, expected: 1); } else if (transformValues.TryGetValue(RequestHeadersAllowedKey, out var _)) { TransformHelpers.TryCheckTooManyParameters(context, transformValues, expected: 1); } else { return false; } return true; } public bool Build(TransformBuilderContext context, IReadOnlyDictionary transformValues) { if (transformValues.TryGetValue(RequestHeadersCopyKey, out var copyHeaders)) { TransformHelpers.CheckTooManyParameters(transformValues, expected: 1); context.CopyRequestHeaders = bool.Parse(copyHeaders); } else if (transformValues.TryGetValue(RequestHeaderOriginalHostKey, out var originalHost)) { TransformHelpers.CheckTooManyParameters(transformValues, expected: 1); context.AddOriginalHost(bool.Parse(originalHost)); } else if (transformValues.TryGetValue(RequestHeaderKey, out var headerName)) { TransformHelpers.CheckTooManyParameters(transformValues, expected: 2); if (transformValues.TryGetValue(SetKey, out var setValue)) { context.AddRequestHeader(headerName, setValue, append: false); } else if (transformValues.TryGetValue(AppendKey, out var appendValue)) { context.AddRequestHeader(headerName, appendValue, append: true); } else { throw new ArgumentException($"Unexpected parameters for RequestHeader: {string.Join(';', transformValues.Keys)}. Expected 'Set' or 'Append'"); } } else if (transformValues.TryGetValue(RequestHeaderRouteValueKey, out var headerNameFromRoute)) { TransformHelpers.CheckTooManyParameters(transformValues, expected: 2); if (transformValues.TryGetValue(AppendKey, out var routeValueKeyAppend)) { context.AddRequestHeaderRouteValue(headerNameFromRoute, routeValueKeyAppend, append: true); } else if (transformValues.TryGetValue(SetKey, out var routeValueKeySet)) { context.AddRequestHeaderRouteValue(headerNameFromRoute, routeValueKeySet, append: false); } else { throw new NotSupportedException(string.Join(";", transformValues.Keys)); } } else if (transformValues.TryGetValue(RequestHeaderRemoveKey, out var removeHeaderName)) { TransformHelpers.CheckTooManyParameters(transformValues, expected: 1); context.AddRequestHeaderRemove(removeHeaderName); } else if (transformValues.TryGetValue(RequestHeadersAllowedKey, out var allowedHeaders)) { TransformHelpers.CheckTooManyParameters(transformValues, expected: 1); var headersList = allowedHeaders.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); context.AddRequestHeadersAllowed(headersList); } else { return false; } return true; } } ================================================ FILE: src/ReverseProxy/Transforms/RequestTransform.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Threading.Tasks; using Microsoft.Extensions.Primitives; using Yarp.ReverseProxy.Forwarder; namespace Yarp.ReverseProxy.Transforms; /// /// The base class for request transforms. /// public abstract class RequestTransform { /// /// Transforms any of the available fields before building the outgoing request. /// public abstract ValueTask ApplyAsync(RequestTransformContext context); /// /// Removes and returns the current header value by first checking the HttpRequestMessage, /// then the HttpContent, and falling back to the HttpContext only if /// is not set. /// This ordering allows multiple transforms to mutate the same header. /// /// The transform context. /// The name of the header to take. /// The requested header value, or StringValues.Empty if none. public static StringValues TakeHeader(RequestTransformContext context, string headerName) { if (string.IsNullOrEmpty(headerName)) { throw new ArgumentException($"'{nameof(headerName)}' cannot be null or empty.", nameof(headerName)); } var proxyRequest = context.ProxyRequest; if (RequestUtilities.TryGetValues(proxyRequest.Headers, headerName, out var existingValues)) { proxyRequest.Headers.Remove(headerName); } else if (proxyRequest.Content is { } content && RequestUtilities.TryGetValues(content.Headers, headerName, out existingValues)) { content.Headers.Remove(headerName); } else if (!context.HeadersCopied) { existingValues = context.HttpContext.Request.Headers[headerName]; } return existingValues; } /// /// Adds the given header to the HttpRequestMessage or HttpContent where applicable. /// public static void AddHeader(RequestTransformContext context, string headerName, StringValues values) { ArgumentNullException.ThrowIfNull(context); ArgumentException.ThrowIfNullOrEmpty(headerName); RequestUtilities.AddHeader(context.ProxyRequest, headerName, values); } /// /// Removes the given header from the HttpRequestMessage or HttpContent where applicable. /// public static void RemoveHeader(RequestTransformContext context, string headerName) { ArgumentNullException.ThrowIfNull(context); ArgumentException.ThrowIfNullOrEmpty(headerName); RequestUtilities.RemoveHeader(context.ProxyRequest, headerName); } } ================================================ FILE: src/ReverseProxy/Transforms/RequestTransformContext.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Net.Http; using System.Threading; using Microsoft.AspNetCore.Http; namespace Yarp.ReverseProxy.Transforms; /// /// Transform state for use with /// public class RequestTransformContext { /// /// The current request context. /// public HttpContext HttpContext { get; init; } = default!; /// /// The outgoing proxy request. All field are initialized except for the 'RequestUri' and optionally headers. /// If no value is provided then the 'RequestUri' will be initialized using the updated 'DestinationPrefix', /// 'Path', and 'Query' properties after the transforms have run. The headers will be copied later when /// applying header transforms. /// public HttpRequestMessage ProxyRequest { get; init; } = default!; /// /// Gets or sets if the request headers have been copied from the HttpRequest to the HttpRequestMessage and HttpContent. /// Transforms use this when searching for the current value of a header they should operate on. /// public bool HeadersCopied { get; set; } /// /// The path to use for the proxy request. /// /// /// This will be prefixed by any PathBase specified for the destination server. /// public PathString Path { get; set; } internal QueryTransformContext? MaybeQuery { get; private set; } /// /// The query used for the proxy request. /// public QueryTransformContext Query { get => MaybeQuery ??= new QueryTransformContext(HttpContext.Request); set => MaybeQuery = value; } /// /// The URI prefix for the proxy request. This includes the scheme and host and can optionally include a /// port and path base. The 'Path' and 'Query' properties will be appended to this after the transforms have run. /// Changing this value can have side effects on load balancing and health checks. /// public string DestinationPrefix { get; set; } = default!; /// /// A indicating that the request is being aborted. /// public CancellationToken CancellationToken { get; set; } } ================================================ FILE: src/ReverseProxy/Transforms/ResponseCondition.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. namespace Yarp.ReverseProxy.Transforms; /// /// Specifies the conditions under which a response transform will run. /// public enum ResponseCondition { /// /// The transform runs for all conditions. /// Always, /// /// The transform only runs if there is a successful response with a status code less than 400. /// Success, /// /// The transform only runs if there is no response or a response with a 400+ status code. /// Failure } ================================================ FILE: src/ReverseProxy/Transforms/ResponseFuncTransform.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Threading.Tasks; namespace Yarp.ReverseProxy.Transforms; /// /// A response transform that runs the given Func. /// public class ResponseFuncTransform : ResponseTransform { private readonly Func _func; public ResponseFuncTransform(Func func) { ArgumentNullException.ThrowIfNull(func); _func = func; } /// public override ValueTask ApplyAsync(ResponseTransformContext context) { return _func(context); } } ================================================ FILE: src/ReverseProxy/Transforms/ResponseHeaderRemoveTransform.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Threading.Tasks; namespace Yarp.ReverseProxy.Transforms; /// /// Removes a response header. /// public class ResponseHeaderRemoveTransform : ResponseTransform { public ResponseHeaderRemoveTransform(string headerName, ResponseCondition condition) { if (string.IsNullOrEmpty(headerName)) { throw new ArgumentException($"'{nameof(headerName)}' cannot be null or empty.", nameof(headerName)); } HeaderName = headerName; Condition = condition; } internal string HeaderName { get; } internal ResponseCondition Condition { get; } // Assumes the response status code has been set on the HttpContext already. /// public override ValueTask ApplyAsync(ResponseTransformContext context) { ArgumentNullException.ThrowIfNull(context); if (Condition == ResponseCondition.Always || Success(context) == (Condition == ResponseCondition.Success)) { context.HttpContext.Response.Headers.Remove(HeaderName); } return default; } } ================================================ FILE: src/ReverseProxy/Transforms/ResponseHeaderValueTransform.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Threading.Tasks; using Microsoft.Extensions.Primitives; namespace Yarp.ReverseProxy.Transforms; /// /// Sets or appends simple response header values. /// public class ResponseHeaderValueTransform : ResponseTransform { public ResponseHeaderValueTransform(string headerName, string value, bool append, ResponseCondition condition) { if (string.IsNullOrEmpty(headerName)) { throw new ArgumentException($"'{nameof(headerName)}' cannot be null or empty.", nameof(headerName)); } HeaderName = headerName; ArgumentNullException.ThrowIfNull(value); Value = value; Append = append; Condition = condition; } internal ResponseCondition Condition { get; } internal bool Append { get; } internal string HeaderName { get; } internal string Value { get; } // Assumes the response status code has been set on the HttpContext already. /// public override ValueTask ApplyAsync(ResponseTransformContext context) { ArgumentNullException.ThrowIfNull(context); if (Condition == ResponseCondition.Always || Success(context) == (Condition == ResponseCondition.Success)) { if (Append) { var existingHeader = TakeHeader(context, HeaderName); var value = StringValues.Concat(existingHeader, Value); SetHeader(context, HeaderName, value); } else { SetHeader(context, HeaderName, Value); } } return default; } } ================================================ FILE: src/ReverseProxy/Transforms/ResponseHeadersAllowedTransform.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Frozen; using System.Collections.Generic; using System.Diagnostics; using System.Net.Http.Headers; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Yarp.ReverseProxy.Forwarder; namespace Yarp.ReverseProxy.Transforms; /// /// Copies only allowed response headers. /// public class ResponseHeadersAllowedTransform : ResponseTransform { public ResponseHeadersAllowedTransform(string[] allowedHeaders) { ArgumentNullException.ThrowIfNull(allowedHeaders); AllowedHeaders = allowedHeaders; AllowedHeadersSet = new HashSet(allowedHeaders, StringComparer.OrdinalIgnoreCase).ToFrozenSet(StringComparer.OrdinalIgnoreCase); } internal string[] AllowedHeaders { get; } private FrozenSet AllowedHeadersSet { get; } /// public override ValueTask ApplyAsync(ResponseTransformContext context) { ArgumentNullException.ThrowIfNull(context); if (context.ProxyResponse is null) { return default; } Debug.Assert(!context.HeadersCopied); // See https://github.com/dotnet/yarp/blob/51d797986b1fea03500a1ad173d13a1176fb5552/src/ReverseProxy/Forwarder/HttpTransformer.cs#L67-L77 var responseHeaders = context.HttpContext.Response.Headers; CopyResponseHeaders(context.ProxyResponse.Headers, responseHeaders); if (context.ProxyResponse.Content is not null) { CopyResponseHeaders(context.ProxyResponse.Content.Headers, responseHeaders); } context.HeadersCopied = true; return default; } // See https://github.com/dotnet/yarp/blob/main/src/ReverseProxy/Forwarder/HttpTransformer.cs#:~:text=void-,CopyResponseHeaders private void CopyResponseHeaders(HttpHeaders source, IHeaderDictionary destination) { foreach (var header in source.NonValidated) { var headerName = header.Key; if (!AllowedHeadersSet.Contains(headerName)) { continue; } destination[headerName] = RequestUtilities.Concat(destination[headerName], header.Value); } } } ================================================ FILE: src/ReverseProxy/Transforms/ResponseTrailerRemoveTransform.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Diagnostics; using System.Threading.Tasks; using Microsoft.AspNetCore.Http.Features; namespace Yarp.ReverseProxy.Transforms; /// /// Removes a response trailer. /// public class ResponseTrailerRemoveTransform : ResponseTrailersTransform { public ResponseTrailerRemoveTransform(string headerName, ResponseCondition condition) { if (string.IsNullOrEmpty(headerName)) { throw new ArgumentException($"'{nameof(headerName)}' cannot be null or empty.", nameof(headerName)); } HeaderName = headerName; Condition = condition; } internal string HeaderName { get; } internal ResponseCondition Condition { get; } // Assumes the response status code has been set on the HttpContext already. /// public override ValueTask ApplyAsync(ResponseTrailersTransformContext context) { ArgumentNullException.ThrowIfNull(context); Debug.Assert(context.ProxyResponse is not null); if (Condition == ResponseCondition.Always || Success(context) == (Condition == ResponseCondition.Success)) { var responseTrailersFeature = context.HttpContext.Features.Get(); var responseTrailers = responseTrailersFeature?.Trailers; // Support should have already been checked by the caller. Debug.Assert(responseTrailers is not null); Debug.Assert(!responseTrailers.IsReadOnly); responseTrailers.Remove(HeaderName); } return default; } } ================================================ FILE: src/ReverseProxy/Transforms/ResponseTrailerValueTransform.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Threading.Tasks; using Microsoft.Extensions.Primitives; namespace Yarp.ReverseProxy.Transforms; /// /// Sets or appends simple response trailer values. /// public class ResponseTrailerValueTransform : ResponseTrailersTransform { public ResponseTrailerValueTransform(string headerName, string value, bool append, ResponseCondition condition) { if (string.IsNullOrEmpty(headerName)) { throw new ArgumentException($"'{nameof(headerName)}' cannot be null or empty.", nameof(headerName)); } HeaderName = headerName; ArgumentNullException.ThrowIfNull(value); Value = value; Append = append; Condition = condition; } internal ResponseCondition Condition { get; } internal bool Append { get; } internal string HeaderName { get; } internal string Value { get; } // Assumes the response status code has been set on the HttpContext already. /// public override ValueTask ApplyAsync(ResponseTrailersTransformContext context) { ArgumentNullException.ThrowIfNull(context); if (Condition == ResponseCondition.Always || Success(context) == (Condition == ResponseCondition.Success)) { if (Append) { var existingHeader = TakeHeader(context, HeaderName); var value = StringValues.Concat(existingHeader, Value); SetHeader(context, HeaderName, value); } else { SetHeader(context, HeaderName, Value); } } return default; } } ================================================ FILE: src/ReverseProxy/Transforms/ResponseTrailersAllowedTransform.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Frozen; using System.Collections.Generic; using System.Diagnostics; using System.Net.Http.Headers; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Yarp.ReverseProxy.Forwarder; namespace Yarp.ReverseProxy.Transforms; /// /// Copies only allowed response trailers. /// public class ResponseTrailersAllowedTransform : ResponseTrailersTransform { public ResponseTrailersAllowedTransform(string[] allowedHeaders) { ArgumentNullException.ThrowIfNull(allowedHeaders); AllowedHeaders = allowedHeaders; AllowedHeadersSet = new HashSet(allowedHeaders, StringComparer.OrdinalIgnoreCase).ToFrozenSet(StringComparer.OrdinalIgnoreCase); } internal string[] AllowedHeaders { get; } private FrozenSet AllowedHeadersSet { get; } /// public override ValueTask ApplyAsync(ResponseTrailersTransformContext context) { ArgumentNullException.ThrowIfNull(context); Debug.Assert(context.ProxyResponse is not null); Debug.Assert(!context.HeadersCopied); // See https://github.com/dotnet/yarp/blob/51d797986b1fea03500a1ad173d13a1176fb5552/src/ReverseProxy/Forwarder/HttpTransformer.cs#L85-L99 // NOTE: Deliberately not using `context.Response.SupportsTrailers()`, `context.Response.AppendTrailer(...)` // because they lookup `IHttpResponseTrailersFeature` for every call. Here we do it just once instead. var responseTrailersFeature = context.HttpContext.Features.Get(); var outgoingTrailers = responseTrailersFeature?.Trailers; if (outgoingTrailers is not null && !outgoingTrailers.IsReadOnly) { // Note that trailers, if any, should already have been declared in Proxy's response CopyResponseHeaders(context.ProxyResponse.TrailingHeaders, outgoingTrailers); } context.HeadersCopied = true; return default; } // See https://github.com/dotnet/yarp/blob/main/src/ReverseProxy/Forwarder/HttpTransformer.cs#:~:text=void-,CopyResponseHeaders private void CopyResponseHeaders(HttpHeaders source, IHeaderDictionary destination) { foreach (var header in source.NonValidated) { var headerName = header.Key; if (!AllowedHeadersSet.Contains(headerName)) { continue; } destination[headerName] = RequestUtilities.Concat(destination[headerName], header.Value); } } } ================================================ FILE: src/ReverseProxy/Transforms/ResponseTrailersFuncTransform.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Threading.Tasks; namespace Yarp.ReverseProxy.Transforms; /// /// A response trailers transform that runs the given Func. /// public class ResponseTrailersFuncTransform : ResponseTrailersTransform { private readonly Func _func; public ResponseTrailersFuncTransform(Func func) { ArgumentNullException.ThrowIfNull(func); _func = func; } /// public override ValueTask ApplyAsync(ResponseTrailersTransformContext context) { return _func(context); } } ================================================ FILE: src/ReverseProxy/Transforms/ResponseTrailersTransform.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Diagnostics; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.Primitives; using Yarp.ReverseProxy.Forwarder; namespace Yarp.ReverseProxy.Transforms; /// /// Transforms for response trailers. /// 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 ValueTask ApplyAsync(ResponseTrailersTransformContext context); /// /// Removes and returns the current trailer value by first checking the HttpResponse /// and falling back to the value from HttpResponseMessage only if /// is not set. /// This ordering allows multiple transforms to mutate the same header. /// /// The transform context. /// The name of the header to take. /// The response header value, or StringValues.Empty if none. public static StringValues TakeHeader(ResponseTrailersTransformContext context, string headerName) { ArgumentNullException.ThrowIfNull(context); ArgumentException.ThrowIfNullOrEmpty(headerName); Debug.Assert(context.ProxyResponse is not null); var responseTrailersFeature = context.HttpContext.Features.Get(); var responseTrailers = responseTrailersFeature?.Trailers; // Support should have already been checked by the caller. Debug.Assert(responseTrailers is not null); Debug.Assert(!responseTrailers.IsReadOnly); if (responseTrailers.TryGetValue(headerName, out var existingValues)) { responseTrailers.Remove(headerName); } else if (!context.HeadersCopied) { RequestUtilities.TryGetValues(context.ProxyResponse.TrailingHeaders, headerName, out existingValues); } return existingValues; } /// /// Sets the given trailer on the HttpResponse. /// public static void SetHeader(ResponseTrailersTransformContext context, string headerName, StringValues values) { var responseTrailersFeature = context.HttpContext.Features.Get(); var responseTrailers = responseTrailersFeature?.Trailers; // Support should have already been checked by the caller. Debug.Assert(responseTrailers is not null); Debug.Assert(!responseTrailers.IsReadOnly); responseTrailers[headerName] = values; } internal static bool Success(ResponseTrailersTransformContext context) { // TODO: How complex should this get? Compare with http://nginx.org/en/docs/http/ngx_http_headers_module.html#add_header return context.HttpContext.Response.StatusCode < 400; } } ================================================ FILE: src/ReverseProxy/Transforms/ResponseTrailersTransformContext.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Net.Http; using System.Threading; using Microsoft.AspNetCore.Http; namespace Yarp.ReverseProxy.Transforms; /// /// Transform state for use with /// public class ResponseTrailersTransformContext { /// /// The current request context. /// public HttpContext HttpContext { get; init; } = default!; /// /// The incoming proxy response. /// public HttpResponseMessage ProxyResponse { get; init; } = default!; /// /// Gets or sets if the response trailers have been copied from the HttpResponseMessage /// to the HttpResponse. Transforms use this when searching for the current value of a header they /// should operate on. /// public bool HeadersCopied { get; set; } /// /// A indicating that the request is being aborted. /// public CancellationToken CancellationToken { get; set; } } ================================================ FILE: src/ReverseProxy/Transforms/ResponseTransform.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; using Yarp.ReverseProxy.Forwarder; namespace Yarp.ReverseProxy.Transforms; /// /// Transforms for responses. /// 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 ValueTask ApplyAsync(ResponseTransformContext context); /// /// Removes and returns the current header value by first checking the HttpResponse /// and falling back to the value from HttpResponseMessage or HttpContent only if /// is not set. /// This ordering allows multiple transforms to mutate the same header. /// /// The transform context. /// The name of the header to take. /// The response header value, or StringValues.Empty if none. public static StringValues TakeHeader(ResponseTransformContext context, string headerName) { ArgumentNullException.ThrowIfNull(context); if (string.IsNullOrEmpty(headerName)) { throw new ArgumentException($"'{nameof(headerName)}' cannot be null or empty.", nameof(headerName)); } if (context.HttpContext.Response.Headers.TryGetValue(headerName, out var existingValues)) { context.HttpContext.Response.Headers.Remove(headerName); } else if (context.ProxyResponse is { } proxyResponse && !context.HeadersCopied) { if (!RequestUtilities.TryGetValues(proxyResponse.Headers, headerName, out existingValues)) { RequestUtilities.TryGetValues(proxyResponse.Content.Headers, headerName, out existingValues); } } return existingValues; } /// /// Sets the given header on the HttpResponse. /// public static void SetHeader(ResponseTransformContext context, string headerName, StringValues values) { context.HttpContext.Response.Headers[headerName] = values; } internal static bool Success(ResponseTransformContext context) { // TODO: How complex should this get? Compare with http://nginx.org/en/docs/http/ngx_http_headers_module.html#add_header return context.HttpContext.Response.StatusCode < 400; } } ================================================ FILE: src/ReverseProxy/Transforms/ResponseTransformContext.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Net.Http; using System.Threading; using Microsoft.AspNetCore.Http; using Yarp.ReverseProxy.Forwarder; namespace Yarp.ReverseProxy.Transforms; /// /// Transform state for use with /// public class ResponseTransformContext { /// /// The current request context. /// public HttpContext HttpContext { get; init; } = default!; /// /// The proxy response. This can be null if the destination did not respond. /// When null, check Get<IForwarderErrorFeature>() method /// or method /// for details about the error via the . /// public HttpResponseMessage? ProxyResponse { get; init; } /// /// Gets or sets if the response headers have been copied from the HttpResponseMessage and HttpContent /// to the HttpResponse. Transforms use this when searching for the current value of a header they /// should operate on. /// public bool HeadersCopied { get; set; } /// /// Set to true if the proxy should exclude the body and trailing headers when proxying this response. /// Defaults to false. /// public bool SuppressResponseBody { get; set; } /// /// A indicating that the request is being aborted. /// public CancellationToken CancellationToken { get; set; } } ================================================ FILE: src/ReverseProxy/Transforms/ResponseTransformExtensions.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Yarp.ReverseProxy.Configuration; using Yarp.ReverseProxy.Transforms.Builder; namespace Yarp.ReverseProxy.Transforms; /// /// Extensions for adding response header and trailer transforms. /// public static class ResponseTransformExtensions { /// /// Clones the route and adds the transform which will enable or suppress copying response headers to the client response. /// public static RouteConfig WithTransformCopyResponseHeaders(this RouteConfig route, bool copy = true) { return route.WithTransform(transform => { transform[ResponseTransformFactory.ResponseHeadersCopyKey] = copy ? bool.TrueString : bool.FalseString; }); } /// /// Clones the route and adds the transform which will enable or suppress copying response trailers to the client response. /// public static RouteConfig WithTransformCopyResponseTrailers(this RouteConfig route, bool copy = true) { return route.WithTransform(transform => { transform[ResponseTransformFactory.ResponseTrailersCopyKey] = copy ? bool.TrueString : bool.FalseString; }); } /// /// Clones the route and adds the transform which will append or set the response header. /// public static RouteConfig WithTransformResponseHeader(this RouteConfig route, string headerName, string value, bool append = true, ResponseCondition condition = ResponseCondition.Success) { var type = append ? ResponseTransformFactory.AppendKey : ResponseTransformFactory.SetKey; return route.WithTransform(transform => { transform[ResponseTransformFactory.ResponseHeaderKey] = headerName; transform[type] = value; transform[ResponseTransformFactory.WhenKey] = condition.ToString(); }); } /// /// Clones the route and adds the transform which will remove the response header. /// public static RouteConfig WithTransformResponseHeaderRemove(this RouteConfig route, string headerName, ResponseCondition condition = ResponseCondition.Success) { return route.WithTransform(transform => { transform[ResponseTransformFactory.ResponseHeaderRemoveKey] = headerName; transform[ResponseTransformFactory.WhenKey] = condition.ToString(); }); } /// /// Clones the route and adds the transform which will only copy the allowed response headers. Other transforms /// that modify or append to existing headers may be affected if not included in the allow list. /// public static RouteConfig WithTransformResponseHeadersAllowed(this RouteConfig route, params string[] allowedHeaders) { return route.WithTransform(transform => { transform[ResponseTransformFactory.ResponseHeadersAllowedKey] = string.Join(';', allowedHeaders); }); } /// /// Adds the transform which will append or set the response header. /// public static TransformBuilderContext AddResponseHeader(this TransformBuilderContext context, string headerName, string value, bool append = true, ResponseCondition condition = ResponseCondition.Success) { context.ResponseTransforms.Add(new ResponseHeaderValueTransform(headerName, value, append, condition)); return context; } /// /// Adds the transform which will remove the response header. /// public static TransformBuilderContext AddResponseHeaderRemove(this TransformBuilderContext context, string headerName, ResponseCondition condition = ResponseCondition.Success) { context.ResponseTransforms.Add(new ResponseHeaderRemoveTransform(headerName, condition)); return context; } /// /// Adds the transform which will only copy the allowed response headers. Other transforms /// that modify or append to existing headers may be affected if not included in the allow list. /// public static TransformBuilderContext AddResponseHeadersAllowed(this TransformBuilderContext context, params string[] allowedHeaders) { context.CopyResponseHeaders = false; context.ResponseTransforms.Add(new ResponseHeadersAllowedTransform(allowedHeaders)); return context; } /// /// Clones the route and adds the transform which will append or set the response trailer. /// public static RouteConfig WithTransformResponseTrailer(this RouteConfig route, string headerName, string value, bool append = true, ResponseCondition condition = ResponseCondition.Success) { var type = append ? ResponseTransformFactory.AppendKey : ResponseTransformFactory.SetKey; return route.WithTransform(transform => { transform[ResponseTransformFactory.ResponseTrailerKey] = headerName; transform[type] = value; transform[ResponseTransformFactory.WhenKey] = condition.ToString(); }); } /// /// Adds the transform which will append or set the response trailer. /// public static TransformBuilderContext AddResponseTrailer(this TransformBuilderContext context, string headerName, string value, bool append = true, ResponseCondition condition = ResponseCondition.Success) { context.ResponseTrailersTransforms.Add(new ResponseTrailerValueTransform(headerName, value, append, condition)); return context; } /// /// Adds the transform which will remove the response trailer. /// public static TransformBuilderContext AddResponseTrailerRemove(this TransformBuilderContext context, string headerName, ResponseCondition condition = ResponseCondition.Success) { context.ResponseTrailersTransforms.Add(new ResponseTrailerRemoveTransform(headerName, condition)); return context; } /// /// Clones the route and adds the transform which will remove the response trailer. /// public static RouteConfig WithTransformResponseTrailerRemove(this RouteConfig route, string headerName, ResponseCondition condition = ResponseCondition.Success) { return route.WithTransform(transform => { transform[ResponseTransformFactory.ResponseTrailerRemoveKey] = headerName; transform[ResponseTransformFactory.WhenKey] = condition.ToString(); }); } /// /// Clones the route and adds the transform which will only copy the allowed response trailers. Other transforms /// that modify or append to existing trailers may be affected if not included in the allow list. /// public static RouteConfig WithTransformResponseTrailersAllowed(this RouteConfig route, params string[] allowedHeaders) { return route.WithTransform(transform => { transform[ResponseTransformFactory.ResponseTrailersAllowedKey] = string.Join(';', allowedHeaders); }); } /// /// Adds the transform which will only copy the allowed response trailers. Other transforms /// that modify or append to existing trailers may be affected if not included in the allow list. /// public static TransformBuilderContext AddResponseTrailersAllowed(this TransformBuilderContext context, params string[] allowedHeaders) { context.CopyResponseTrailers = false; context.ResponseTrailersTransforms.Add(new ResponseTrailersAllowedTransform(allowedHeaders)); return context; } } ================================================ FILE: src/ReverseProxy/Transforms/ResponseTransformFactory.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using Yarp.ReverseProxy.Transforms.Builder; namespace Yarp.ReverseProxy.Transforms; internal sealed class ResponseTransformFactory : ITransformFactory { internal const string ResponseHeadersCopyKey = "ResponseHeadersCopy"; internal const string ResponseTrailersCopyKey = "ResponseTrailersCopy"; internal const string ResponseHeaderKey = "ResponseHeader"; internal const string ResponseTrailerKey = "ResponseTrailer"; internal const string ResponseHeaderRemoveKey = "ResponseHeaderRemove"; internal const string ResponseTrailerRemoveKey = "ResponseTrailerRemove"; internal const string ResponseHeadersAllowedKey = "ResponseHeadersAllowed"; internal const string ResponseTrailersAllowedKey = "ResponseTrailersAllowed"; internal const string WhenKey = "When"; internal const string AppendKey = "Append"; internal const string SetKey = "Set"; public bool Validate(TransformRouteValidationContext context, IReadOnlyDictionary transformValues) { if (transformValues.TryGetValue(ResponseHeadersCopyKey, out var copyHeaders)) { TransformHelpers.TryCheckTooManyParameters(context, transformValues, expected: 1); if (!bool.TryParse(copyHeaders, out var _)) { context.Errors.Add(new ArgumentException($"Unexpected value for ResponseHeadersCopy: {copyHeaders}. Expected 'true' or 'false'")); } } else if (transformValues.TryGetValue(ResponseTrailersCopyKey, out copyHeaders)) { TransformHelpers.TryCheckTooManyParameters(context, transformValues, expected: 1); if (!bool.TryParse(copyHeaders, out var _)) { context.Errors.Add(new ArgumentException($"Unexpected value for ResponseTrailersCopy: {copyHeaders}. Expected 'true' or 'false'")); } } else if (transformValues.TryGetValue(ResponseHeaderKey, out var _)) { if (transformValues.TryGetValue(WhenKey, out var whenValue)) { TransformHelpers.TryCheckTooManyParameters(context, transformValues, expected: 3); if (!Enum.TryParse(whenValue, ignoreCase: true, out var _)) { context.Errors.Add(new ArgumentException($"Unexpected value for ResponseHeader:When: {whenValue}. Expected 'Always', 'Success', or 'Failure'")); } } else { TransformHelpers.TryCheckTooManyParameters(context, transformValues, expected: 2); } if (!transformValues.TryGetValue(SetKey, out var _) && !transformValues.TryGetValue(AppendKey, out var _)) { context.Errors.Add(new ArgumentException($"Unexpected parameters for ResponseHeader: {string.Join(';', transformValues.Keys)}. Expected 'Set' or 'Append'")); } } else if (transformValues.TryGetValue(ResponseTrailerKey, out var _)) { if (transformValues.TryGetValue(WhenKey, out var whenValue)) { TransformHelpers.TryCheckTooManyParameters(context, transformValues, expected: 3); if (!Enum.TryParse(whenValue, ignoreCase: true, out var _)) { context.Errors.Add(new ArgumentException($"Unexpected value for ResponseTrailer:When: {whenValue}. Expected 'Always', 'Success', or 'Failure'")); } } else { TransformHelpers.TryCheckTooManyParameters(context, transformValues, expected: 2); } if (!transformValues.TryGetValue(SetKey, out var _) && !transformValues.TryGetValue(AppendKey, out var _)) { context.Errors.Add(new ArgumentException($"Unexpected parameters for ResponseTrailer: {string.Join(';', transformValues.Keys)}. Expected 'Set' or 'Append'")); } } else if (transformValues.TryGetValue(ResponseHeaderRemoveKey, out var _)) { if (transformValues.TryGetValue(WhenKey, out var whenValue)) { TransformHelpers.TryCheckTooManyParameters(context, transformValues, expected: 2); if (!Enum.TryParse(whenValue, ignoreCase: true, out var _)) { context.Errors.Add(new ArgumentException($"Unexpected value for ResponseHeaderRemove:When: {whenValue}. Expected 'Always', 'Success', or 'Failure'")); } } else { TransformHelpers.TryCheckTooManyParameters(context, transformValues, expected: 1); } } else if (transformValues.TryGetValue(ResponseTrailerRemoveKey, out var _)) { if (transformValues.TryGetValue(WhenKey, out var whenValue)) { TransformHelpers.TryCheckTooManyParameters(context, transformValues, expected: 2); if (!Enum.TryParse(whenValue, ignoreCase: true, out var _)) { context.Errors.Add(new ArgumentException($"Unexpected value for ResponseTrailerRemove:When: {whenValue}. Expected 'Always', 'Success', or 'Failure'")); } } else { TransformHelpers.TryCheckTooManyParameters(context, transformValues, expected: 1); } } else if (transformValues.TryGetValue(ResponseHeadersAllowedKey, out var _)) { TransformHelpers.TryCheckTooManyParameters(context, transformValues, expected: 1); } else if (transformValues.TryGetValue(ResponseTrailersAllowedKey, out var _)) { TransformHelpers.TryCheckTooManyParameters(context, transformValues, expected: 1); } else { return false; } return true; } public bool Build(TransformBuilderContext context, IReadOnlyDictionary transformValues) { if (transformValues.TryGetValue(ResponseHeadersCopyKey, out var copyHeaders)) { TransformHelpers.CheckTooManyParameters(transformValues, expected: 1); context.CopyResponseHeaders = bool.Parse(copyHeaders); } else if (transformValues.TryGetValue(ResponseTrailersCopyKey, out copyHeaders)) { TransformHelpers.CheckTooManyParameters(transformValues, expected: 1); context.CopyResponseTrailers = bool.Parse(copyHeaders); } else if (transformValues.TryGetValue(ResponseHeaderKey, out var responseHeaderName)) { var condition = ResponseCondition.Success; if (transformValues.TryGetValue(WhenKey, out var whenValue)) { TransformHelpers.CheckTooManyParameters(transformValues, expected: 3); condition = Enum.Parse(whenValue, ignoreCase: true); } else { TransformHelpers.CheckTooManyParameters(transformValues, expected: 2); } if (transformValues.TryGetValue(SetKey, out var setValue)) { context.AddResponseHeader(responseHeaderName, setValue, append: false, condition); } else if (transformValues.TryGetValue(AppendKey, out var appendValue)) { context.AddResponseHeader(responseHeaderName, appendValue, append: true, condition); } else { throw new ArgumentException($"Unexpected parameters for ResponseHeader: {string.Join(';', transformValues.Keys)}. Expected 'Set' or 'Append'"); } } else if (transformValues.TryGetValue(ResponseTrailerKey, out var responseTrailerName)) { var condition = ResponseCondition.Success; if (transformValues.TryGetValue(WhenKey, out var whenValue)) { TransformHelpers.CheckTooManyParameters(transformValues, expected: 3); condition = Enum.Parse(whenValue, ignoreCase: true); } else { TransformHelpers.CheckTooManyParameters(transformValues, expected: 2); } if (transformValues.TryGetValue(SetKey, out var setValue)) { context.AddResponseTrailer(responseTrailerName, setValue, append: false, condition); } else if (transformValues.TryGetValue(AppendKey, out var appendValue)) { context.AddResponseTrailer(responseTrailerName, appendValue, append: true, condition); } else { throw new ArgumentException($"Unexpected parameters for ResponseTrailer: {string.Join(';', transformValues.Keys)}. Expected 'Set' or 'Append'"); } } else if (transformValues.TryGetValue(ResponseHeaderRemoveKey, out var removeResponseHeaderName)) { var condition = ResponseCondition.Success; if (transformValues.TryGetValue(WhenKey, out var whenValue)) { TransformHelpers.CheckTooManyParameters(transformValues, expected: 2); condition = Enum.Parse(whenValue, ignoreCase: true); } else { TransformHelpers.CheckTooManyParameters(transformValues, expected: 1); } context.AddResponseHeaderRemove(removeResponseHeaderName, condition); } else if (transformValues.TryGetValue(ResponseTrailerRemoveKey, out var removeResponseTrailerName)) { var condition = ResponseCondition.Success; if (transformValues.TryGetValue(WhenKey, out var whenValue)) { TransformHelpers.CheckTooManyParameters(transformValues, expected: 2); condition = Enum.Parse(whenValue, ignoreCase: true); } else { TransformHelpers.CheckTooManyParameters(transformValues, expected: 1); } context.AddResponseTrailerRemove(removeResponseTrailerName, condition); } else if (transformValues.TryGetValue(ResponseHeadersAllowedKey, out var allowedHeaders)) { TransformHelpers.CheckTooManyParameters(transformValues, expected: 1); var headersList = allowedHeaders.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); context.AddResponseHeadersAllowed(headersList); } else if (transformValues.TryGetValue(ResponseTrailersAllowedKey, out var allowedTrailers)) { TransformHelpers.CheckTooManyParameters(transformValues, expected: 1); var headersList = allowedTrailers.Split(';', options: StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); context.AddResponseTrailersAllowed(headersList); } else { return false; } return true; } } ================================================ FILE: src/ReverseProxy/Transforms/RouteConfigTransformExtensions.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using Yarp.ReverseProxy.Configuration; namespace Yarp.ReverseProxy.Transforms; /// /// Extensions for adding transforms to . /// public static class RouteConfigTransformExtensions { /// /// Clones the and adds the transform. /// /// The cloned route with the new transform. public static RouteConfig WithTransform(this RouteConfig route, Action> createTransform) { ArgumentNullException.ThrowIfNull(createTransform); List> transforms; if (route.Transforms is null) { transforms = new List>(); } else { transforms = new List>(route.Transforms.Count + 1); transforms.AddRange(route.Transforms); } var transform = new Dictionary(StringComparer.OrdinalIgnoreCase); createTransform(transform); transforms.Add(transform); return route with { Transforms = transforms }; } } ================================================ FILE: src/ReverseProxy/Transforms/TransformBuilderContextFuncExtensions.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Threading.Tasks; using Yarp.ReverseProxy.Transforms.Builder; namespace Yarp.ReverseProxy.Transforms; /// /// Extension methods for . /// 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) { ArgumentNullException.ThrowIfNull(context); ArgumentNullException.ThrowIfNull(func); context.RequestTransforms.Add(new RequestFuncTransform(func)); return context; } /// /// Adds a transform Func that runs on each response for the given route. /// public static TransformBuilderContext AddResponseTransform(this TransformBuilderContext context, Func func) { ArgumentNullException.ThrowIfNull(context); ArgumentNullException.ThrowIfNull(func); context.ResponseTransforms.Add(new ResponseFuncTransform(func)); return context; } /// /// Adds a transform Func that runs on each response for the given route. /// public static TransformBuilderContext AddResponseTrailersTransform(this TransformBuilderContext context, Func func) { ArgumentNullException.ThrowIfNull(context); ArgumentNullException.ThrowIfNull(func); context.ResponseTrailersTransforms.Add(new ResponseTrailersFuncTransform(func)); return context; } } ================================================ FILE: src/ReverseProxy/Utilities/ActivityCancellationTokenSource.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Concurrent; using System.Diagnostics; using System.Threading; namespace Yarp.ReverseProxy.Utilities; internal sealed class ActivityCancellationTokenSource : CancellationTokenSource { // Avoid paying the cost of updating the timeout timer if doing so won't meaningfully affect // the overall timeout duration (default is 100s). This is a trade-off between precision and performance. // The exact value is somewhat arbitrary, but should be large enough to avoid most timer updates. private const int TimeoutResolutionMs = 20; private const int MaxQueueSize = 1024; private static readonly ConcurrentQueue _sharedSources = new(); private static int _count; private static readonly Action _linkedTokenCancelDelegate = static s => { var cts = (ActivityCancellationTokenSource)s!; // If a cancellation was triggered by a timeout or manual call to Cancel, it's possible that this will // cascade into other tokens firing. Avoid incorrectly marking CancelledByLinkedToken in such cases. if (!cts.IsCancellationRequested) { cts.CancelledByLinkedToken = true; cts.Cancel(throwOnFirstException: false); } }; private int _activityTimeoutMs; private uint _lastTimeoutTicks; private CancellationTokenRegistration _linkedRegistration1; private CancellationTokenRegistration _linkedRegistration2; private ActivityCancellationTokenSource() { } public bool CancelledByLinkedToken { get; private set; } private void StartTimeout() { _lastTimeoutTicks = (uint)Environment.TickCount; CancelAfter(_activityTimeoutMs); } public void ResetTimeout() { var currentMs = (uint)Environment.TickCount; var elapsedMs = currentMs - _lastTimeoutTicks; if (elapsedMs > TimeoutResolutionMs) { _lastTimeoutTicks = currentMs; CancelAfter(_activityTimeoutMs); } } public static ActivityCancellationTokenSource Rent(TimeSpan activityTimeout, CancellationToken linkedToken1 = default, CancellationToken linkedToken2 = default) { if (_sharedSources.TryDequeue(out var cts)) { Interlocked.Decrement(ref _count); } else { cts = new ActivityCancellationTokenSource(); } cts._activityTimeoutMs = (int)activityTimeout.TotalMilliseconds; cts._linkedRegistration1 = linkedToken1.UnsafeRegister(_linkedTokenCancelDelegate, cts); cts._linkedRegistration2 = linkedToken2.UnsafeRegister(_linkedTokenCancelDelegate, cts); cts.StartTimeout(); return cts; } public void Return() { _linkedRegistration1.Dispose(); _linkedRegistration1 = default; _linkedRegistration2.Dispose(); _linkedRegistration2 = default; if (TryReset()) { Debug.Assert(!CancelledByLinkedToken); if (Interlocked.Increment(ref _count) <= MaxQueueSize) { _sharedSources.Enqueue(this); return; } Interlocked.Decrement(ref _count); } Dispose(); } } ================================================ FILE: src/ReverseProxy/Utilities/AtomicCounter.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Threading; namespace Yarp.ReverseProxy.Utilities; internal sealed class AtomicCounter { private int _value; /// /// Gets the current value of the counter. /// public int Value { get => Volatile.Read(ref _value); set => Volatile.Write(ref _value, value); } /// /// Atomically increments the counter value by 1. /// public int Increment() { return Interlocked.Increment(ref _value); } /// /// Atomically decrements the counter value by 1. /// public int Decrement() { return Interlocked.Decrement(ref _value); } /// /// Atomically resets the counter value to 0. /// public void Reset() { Interlocked.Exchange(ref _value, 0); } } ================================================ FILE: src/ReverseProxy/Utilities/CaseInsensitiveEqualHelper.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; namespace Yarp.ReverseProxy.Utilities; internal static class CaseInsensitiveEqualHelper { internal static bool Equals(IReadOnlyList? list1, IReadOnlyList? list2) { return CollectionEqualityHelper.Equals(list1, list2, StringComparer.OrdinalIgnoreCase); } internal static int GetHashCode(IReadOnlyList? values) { return CollectionEqualityHelper.GetHashCode(values, StringComparer.OrdinalIgnoreCase); } } ================================================ FILE: src/ReverseProxy/Utilities/CaseSensitiveEqualHelper.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; namespace Yarp.ReverseProxy.Utilities; internal static class CaseSensitiveEqualHelper { internal static bool Equals(IReadOnlyList? list1, IReadOnlyList? list2) { return CollectionEqualityHelper.Equals(list1, list2, StringComparer.Ordinal); } internal static bool Equals(IReadOnlyDictionary? dictionary1, IReadOnlyDictionary? dictionary2) { return CollectionEqualityHelper.Equals(dictionary1, dictionary2, StringComparer.Ordinal); } internal static bool Equals(IReadOnlyList>? dictionaryList1, IReadOnlyList>? dictionaryList2) { return CollectionEqualityHelper.Equals(dictionaryList1, dictionaryList2, StringComparer.Ordinal); } internal static int GetHashCode(IReadOnlyList? values) { return CollectionEqualityHelper.GetHashCode(values, StringComparer.Ordinal); } internal static int GetHashCode(IReadOnlyDictionary? dictionary) { return CollectionEqualityHelper.GetHashCode(dictionary, StringComparer.Ordinal); } internal static int GetHashCode(IReadOnlyList>? dictionaryList) { return CollectionEqualityHelper.GetHashCode(dictionaryList); } } ================================================ FILE: src/ReverseProxy/Utilities/CollectionEqualityHelper.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Linq; namespace Yarp.ReverseProxy.Utilities; internal static class CollectionEqualityHelper { public static bool Equals(IReadOnlyList? list1, IReadOnlyList? list2, IEqualityComparer? valueComparer = null) { if (ReferenceEquals(list1, list2)) { return true; } if (list1 is null || list2 is null) { return false; } if (list1.Count != list2.Count) { return false; } valueComparer ??= EqualityComparer.Default; for (var i = 0; i < list1.Count; i++) { if (!valueComparer.Equals(list1[i], list2[i])) { return false; } } return true; } public static bool Equals(IReadOnlyDictionary? dictionary1, IReadOnlyDictionary? dictionary2, IEqualityComparer? valueComparer = null) { if (ReferenceEquals(dictionary1, dictionary2)) { return true; } if (dictionary1 is null || dictionary2 is null) { return false; } if (dictionary1.Count != dictionary2.Count) { return false; } if (dictionary1.Count == 0) { return true; } valueComparer ??= EqualityComparer.Default; foreach (var (key, value1) in dictionary1) { if (dictionary2.TryGetValue(key, out var value2)) { if (!valueComparer.Equals(value1, value2)) { return false; } } else { return false; } } return true; } public static bool Equals(IReadOnlyList>? dictionaryList1, IReadOnlyList>? dictionaryList2, IEqualityComparer? valueComparer = null) { if (ReferenceEquals(dictionaryList1, dictionaryList2)) { return true; } if (dictionaryList1 is null || dictionaryList2 is null) { return false; } if (dictionaryList1.Count != dictionaryList2.Count) { return false; } for (var i = 0; i < dictionaryList1.Count; i++) { if (!Equals(dictionaryList1[i], dictionaryList2[i], valueComparer)) { return false; } } return true; } public static int GetHashCode(IReadOnlyList? values, IEqualityComparer? valueComparer = null) { if (values is null) { return 0; } valueComparer ??= EqualityComparer.Default; var hashCode = new HashCode(); foreach (var value in values) { hashCode.Add(value, valueComparer); } return hashCode.ToHashCode(); } public static int GetHashCode(IReadOnlyDictionary? dictionary, IEqualityComparer? valueComparer = null) { if (dictionary is null) { return 0; } if (dictionary.Count == 0) { return 42; } // We don't know what comparer the dictionary was created with, so we assume it's Ordinal/OrdinalIgnoreCase // If a culture-sensitive comparer was used, this may result in GetHashCode returning different values for "equal" strings // If that comes up as a realistic scenario, we can consider ignoring keys in the future var keyComparer = StringComparer.OrdinalIgnoreCase; valueComparer ??= EqualityComparer.Default; // Dictionaries are unordered collections and HashCode uses an order-sensitive algorithm (xxHash), so we have to sort the elements var keys = dictionary.Keys.ToArray(); Array.Sort(keys, keyComparer); var hashCode = new HashCode(); foreach (var key in keys) { hashCode.Add(key, keyComparer); hashCode.Add(dictionary[key], valueComparer); } return hashCode.ToHashCode(); } public static int GetHashCode(IReadOnlyList>? dictionaryList, IEqualityComparer? valueComparer = null) { if (dictionaryList is null) { return 0; } var hashCode = new HashCode(); foreach (var dictionary in dictionaryList) { hashCode.Add(GetHashCode(dictionary, valueComparer)); } return hashCode.ToHashCode(); } } ================================================ FILE: src/ReverseProxy/Utilities/ConcurrentDictionaryExtensions.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Concurrent; using System.Collections.Generic; namespace Yarp.ReverseProxy.Utilities; internal static class ConcurrentDictionaryExtensions { public static bool Contains(this ConcurrentDictionary dictionary, KeyValuePair item) where TKey : notnull { return ((ICollection>)dictionary).Contains(item); } } ================================================ FILE: src/ReverseProxy/Utilities/DelegatingStream.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Diagnostics; using System.IO; using System.Threading; using System.Threading.Tasks; namespace Yarp.ReverseProxy.Utilities; // Taken from https://github.com/dotnet/runtime/blob/00f37bc13b4edbba1afca9e98d74432a94f5192f/src/libraries/Common/src/System/IO/DelegatingStream.cs // Forwards all calls to an inner stream except where overridden in a derived class. internal abstract class DelegatingStream : Stream { private readonly Stream _innerStream; #region Properties public override bool CanRead { get { return _innerStream.CanRead; } } public override bool CanSeek { get { return _innerStream.CanSeek; } } public override bool CanWrite { get { return _innerStream.CanWrite; } } public override long Length { get { return _innerStream.Length; } } public override long Position { get { return _innerStream.Position; } set { _innerStream.Position = value; } } public override int ReadTimeout { get { return _innerStream.ReadTimeout; } set { _innerStream.ReadTimeout = value; } } public override bool CanTimeout { get { return _innerStream.CanTimeout; } } public override int WriteTimeout { get { return _innerStream.WriteTimeout; } set { _innerStream.WriteTimeout = value; } } #endregion Properties protected DelegatingStream(Stream innerStream) { Debug.Assert(innerStream is not null); _innerStream = innerStream; } protected override void Dispose(bool disposing) { if (disposing) { _innerStream.Dispose(); } base.Dispose(disposing); } public override ValueTask DisposeAsync() { return _innerStream.DisposeAsync(); } #region Read public override long Seek(long offset, SeekOrigin origin) { return _innerStream.Seek(offset, origin); } public override int Read(byte[] buffer, int offset, int count) { return _innerStream.Read(buffer, offset, count); } public override int Read(Span buffer) { return _innerStream.Read(buffer); } public override int ReadByte() { return _innerStream.ReadByte(); } public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { return _innerStream.ReadAsync(buffer, offset, count, cancellationToken); } public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) { return _innerStream.ReadAsync(buffer, cancellationToken); } public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) { return _innerStream.BeginRead(buffer, offset, count, callback!, state); } public override int EndRead(IAsyncResult asyncResult) { return _innerStream.EndRead(asyncResult); } #endregion Read #region Write public override void Flush() { _innerStream.Flush(); } public override Task FlushAsync(CancellationToken cancellationToken) { return _innerStream.FlushAsync(cancellationToken); } public override void SetLength(long value) { _innerStream.SetLength(value); } public override void Write(byte[] buffer, int offset, int count) { _innerStream.Write(buffer, offset, count); } public override void Write(ReadOnlySpan buffer) { _innerStream.Write(buffer); } public override void WriteByte(byte value) { _innerStream.WriteByte(value); } public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { return _innerStream.WriteAsync(buffer, offset, count, cancellationToken); } public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) { return _innerStream.WriteAsync(buffer, cancellationToken); } public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) { return _innerStream.BeginWrite(buffer, offset, count, callback!, state); } public override void EndWrite(IAsyncResult asyncResult) { _innerStream.EndWrite(asyncResult); } public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) { return _innerStream.CopyToAsync(destination, bufferSize, cancellationToken); } #endregion Write } ================================================ FILE: src/ReverseProxy/Utilities/EventIds.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Extensions.Logging; namespace Yarp.ReverseProxy; internal static class EventIds { public static readonly EventId LoadData = new EventId(1, "ApplyProxyConfig"); public static readonly EventId ErrorSignalingChange = new EventId(2, "ApplyProxyConfigFailed"); public static readonly EventId NoClusterFound = new EventId(4, "NoClusterFound"); public static readonly EventId NoAvailableDestinations = new EventId(7, "NoAvailableDestinations"); public static readonly EventId MultipleDestinationsAvailable = new EventId(8, "MultipleDestinationsAvailable"); public static readonly EventId Forwarding = new EventId(9, "Forwarding"); public static readonly EventId ExplicitActiveCheckOfAllClustersHealthFailed = new EventId(10, "ExplicitActiveCheckOfAllClustersHealthFailed"); public static readonly EventId ActiveHealthProbingFailedOnCluster = new EventId(11, "ActiveHealthProbingFailedOnCluster"); public static readonly EventId ErrorOccurredDuringActiveHealthProbingShutdownOnCluster = new EventId(12, "ErrorOccurredDuringActiveHealthProbingShutdownOnCluster"); public static readonly EventId ActiveHealthProbeConstructionFailedOnCluster = new EventId(13, "ActiveHealthProbeConstructionFailedOnCluster"); public static readonly EventId StartingActiveHealthProbingOnCluster = new EventId(14, "StartingActiveHealthProbingOnCluster"); public static readonly EventId StoppedActiveHealthProbingOnCluster = new EventId(15, "StoppedActiveHealthProbingOnCluster"); public static readonly EventId DestinationProbingCompleted = new EventId(16, "DestinationActiveProbingCompleted"); public static readonly EventId DestinationProbingFailed = new EventId(17, "DestinationActiveProbingFailed"); public static readonly EventId SendingHealthProbeToEndpointOfDestination = new EventId(18, "SendingHealthProbeToEndpointOfDestination"); public static readonly EventId UnhealthyDestinationIsScheduledForReactivation = new EventId(19, "UnhealthyDestinationIsScheduledForReactivation"); public static readonly EventId PassiveDestinationHealthResetToUnknownState = new EventId(20, "PassiveDestinationHealthResetToUnknownState"); public static readonly EventId ClusterAdded = new EventId(21, "ClusterAdded"); public static readonly EventId ClusterChanged = new EventId(22, "ClusterChanged"); public static readonly EventId ClusterRemoved = new EventId(23, "ClusterRemoved"); public static readonly EventId DestinationAdded = new EventId(24, "EndpointAdded"); public static readonly EventId DestinationChanged = new EventId(25, "EndpointChanged"); public static readonly EventId DestinationRemoved = new EventId(26, "EndpointRemoved"); public static readonly EventId RouteAdded = new EventId(27, "RouteAdded"); public static readonly EventId RouteChanged = new EventId(28, "RouteChanged"); public static readonly EventId RouteRemoved = new EventId(29, "RouteRemoved"); public static readonly EventId HttpDowngradeDetected = new EventId(30, "HttpDowngradeDetected"); public static readonly EventId OperationStarted = new EventId(31, "OperationStarted"); public static readonly EventId OperationEnded = new EventId(32, "OperationEnded"); public static readonly EventId OperationFailed = new EventId(33, "OperationFailed"); public static readonly EventId AffinityResolutionFailedForCluster = new EventId(34, "AffinityResolutionFailedForCluster"); public static readonly EventId MultipleDestinationsOnClusterToEstablishRequestAffinity = new EventId(35, "MultipleDestinationsOnClusterToEstablishRequestAffinity"); public static readonly EventId AffinityCannotBeEstablishedBecauseNoDestinationsFoundOnCluster = new EventId(36, "AffinityCannotBeEstablishedBecauseNoDestinationsFoundOnCluster"); public static readonly EventId NoDestinationOnClusterToEstablishRequestAffinity = new EventId(37, "NoDestinationOnClusterToEstablishRequestAffinity"); public static readonly EventId RequestAffinityKeyDecryptionFailed = new EventId(38, "RequestAffinityKeyDecryptionFailed"); public static readonly EventId DestinationMatchingToAffinityKeyNotFound = new EventId(39, "DestinationMatchingToAffinityKeyNotFound"); public static readonly EventId RequestAffinityHeaderHasMultipleValues = new EventId(40, "RequestAffinityHeaderHasMultipleValues"); public static readonly EventId AffinityResolutionFailureWasHandledProcessingWillBeContinued = new EventId(41, "AffinityResolutionFailureWasHandledProcessingWillBeContinued"); public static readonly EventId ClusterConfigException = new EventId(42, "ClusterConfigException"); public static readonly EventId ErrorReloadingConfig = new EventId(43, "ErrorReloadingConfig"); public static readonly EventId ErrorApplyingConfig = new EventId(44, "ErrorApplyingConfig"); public static readonly EventId ClientCreated = new EventId(45, "ClientCreated"); public static readonly EventId ClientReused = new EventId(46, "ClientReused"); public static readonly EventId ConfigurationDataConversionFailed = new EventId(47, "ConfigurationDataConversionFailed"); public static readonly EventId ForwardingError = new EventId(48, "ForwardingError"); public static readonly EventId ActiveDestinationHealthStateIsSetToUnhealthy = new EventId(49, "ActiveDestinationHealthStateIsSetToUnhealthy"); public static readonly EventId ActiveDestinationHealthStateIsSet = new EventId(50, "ActiveDestinationHealthStateIsSet"); public static readonly EventId DelegationQueueInitializationFailed = new EventId(51, "DelegationQueueInitializationFailed"); public static readonly EventId DelegationQueueNotFound = new EventId(52, "DelegationQueueNotFound"); public static readonly EventId DelegationQueueNotInitialized = new EventId(53, "DelegationQueueNotInitialized"); public static readonly EventId DelegatingRequest = new EventId(54, "DelegatingRequest"); public static readonly EventId DelegationFailed = new EventId(55, "DelegationFailed"); public static readonly EventId ResponseReceived = new EventId(56, "ResponseReceived"); public static readonly EventId DelegationQueueReset = new EventId(57, "DelegationQueueReset"); public static readonly EventId Http10RequestVersionDetected = new EventId(58, "Http10RequestVersionDetected"); public static readonly EventId NotForwarding = new EventId(59, "NotForwarding"); public static readonly EventId MaxRequestBodySizeSet = new EventId(60, "MaxRequestBodySizeSet"); public static readonly EventId RetryingWebSocketDowngradeNoConnect = new EventId(61, "RetryingWebSocketDowngradeNoConnect"); public static readonly EventId RetryingWebSocketDowngradeNoHttp2 = new EventId(62, "RetryingWebSocketDowngradeNoHttp2"); public static readonly EventId InvalidSecWebSocketKeyHeader = new EventId(63, "InvalidSecWebSocketKeyHeader"); public static readonly EventId TimeoutNotApplied = new(64, nameof(TimeoutNotApplied)); public static readonly EventId DelegationQueueNoLongerExists = new(65, nameof(DelegationQueueNoLongerExists)); public static readonly EventId ForwardingRequestCancelled = new(66, nameof(ForwardingRequestCancelled)); public static readonly EventId DelegationQueueDisposed = new(67, nameof(DelegationQueueDisposed)); public static readonly EventId ActiveHealthProbeCancelledOnDestination = new(68, nameof(ActiveHealthProbeCancelledOnDestination)); } ================================================ FILE: src/ReverseProxy/Utilities/IClock.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Threading; using System.Threading.Tasks; namespace Yarp.ReverseProxy.Utilities; /// /// Abstraction over monotonic time providers /// (Environment.TickCount64, Stopwatch.GetTimestamp, as opposed to DateTime). /// [Obsolete("For testing only. Use TimeProvider instead.")] public interface IClock { /// /// Gets the current time in UTC as a . /// /// DateTimeOffset GetUtcNow(); /// /// Gets a value that indicates the current tick count measured as milliseconds from an arbitrary reference time. /// The default implementation leverages . /// This is generally more efficient than , but provides less precision. /// long TickCount { get; } /// /// Gets a precise time measurement using as the time source. /// /// The time measurement. TimeSpan GetStopwatchTime(); /// /// Creates a cancellable task that completes after a specified time interval. /// This is equivalent to , /// and facilitates unit tests that use virtual time. /// /// The time span to wait before completing the returned task, or TimeSpan.FromMilliseconds(-1) to wait indefinitely. /// A cancellation token to observe while waiting for the task to complete. /// A task that represents the time delay. Task Delay(TimeSpan delay, CancellationToken cancellationToken); /// /// Creates a cancellable task that completes after a specified time interval. /// This is equivalent to , /// and facilitates unit tests that use virtual time. /// /// The number of milliseconds to wait before completing the returned task, or -1 to wait indefinitely. /// A cancellation token to observe while waiting for the task to complete. /// A task that represents the time delay. Task Delay(int millisecondsDelay, CancellationToken cancellationToken); } ================================================ FILE: src/ReverseProxy/Utilities/IRandomFactory.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; namespace Yarp.ReverseProxy.Utilities; /// /// Factory for creating random class. This factory let us able to inject random class into other class. /// So that we can mock the random class for unit test. /// public interface IRandomFactory { /// /// Create a instance of random class. /// Random CreateRandomInstance(); } ================================================ FILE: src/ReverseProxy/Utilities/NullRandomFactory.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; namespace Yarp.ReverseProxy.Utilities; internal sealed class NullRandomFactory : IRandomFactory { public Random CreateRandomInstance() { throw new NotImplementedException(); } } ================================================ FILE: src/ReverseProxy/Utilities/Observability.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; using Microsoft.AspNetCore.Http; namespace Yarp.ReverseProxy.Utilities; internal static class Observability { public static readonly ActivitySource YarpActivitySource = new ActivitySource("Yarp.ReverseProxy"); public static Activity? GetYarpActivity(this HttpContext context) { return context.Features[typeof(YarpActivity)] as Activity; } public static void SetYarpActivity(this HttpContext context, Activity? activity) { if (activity is not null) { context.Features[typeof(YarpActivity)] = activity; } } public static void AddError(this Activity activity, string message, string description) { if (activity is not null) { var tagsCollection = new ActivityTagsCollection { { "error", message }, { "description", description } }; activity.AddEvent(new ActivityEvent("Error", default, tagsCollection)); } } private sealed class YarpActivity { } } ================================================ FILE: src/ReverseProxy/Utilities/ParsedMetadataEntry.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using Yarp.ReverseProxy.Model; namespace Yarp.ReverseProxy.Utilities; internal sealed class ParsedMetadataEntry { private readonly Parser _parser; private readonly string _metadataName; private readonly ClusterState _cluster; // Use a volatile field of a reference Tuple type to ensure atomicity during concurrent access. private volatile Tuple? _value; public delegate bool Parser(string stringValue, out T parsedValue); public ParsedMetadataEntry(Parser parser, ClusterState cluster, string metadataName) { ArgumentNullException.ThrowIfNull(parser); ArgumentNullException.ThrowIfNull(cluster); ArgumentNullException.ThrowIfNull(metadataName); _parser = parser; _cluster = cluster; _metadataName = metadataName; } public T GetParsedOrDefault(T defaultValue) { var currentValue = _value; if (_cluster.Model.Config.Metadata is not null && _cluster.Model.Config.Metadata.TryGetValue(_metadataName, out var stringValue)) { if (currentValue is null || currentValue.Item1 != stringValue) { _value = Tuple.Create(stringValue, _parser(stringValue, out var parsedValue) ? parsedValue : defaultValue); } } else if (currentValue is null || currentValue.Item1 is not null) { _value = Tuple.Create(null, defaultValue); } return _value!.Item2; } } ================================================ FILE: src/ReverseProxy/Utilities/RandomFactory.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; namespace Yarp.ReverseProxy.Utilities; /// internal sealed class RandomFactory : IRandomFactory { /// public Random CreateRandomInstance() { return Random.Shared; } } ================================================ FILE: src/ReverseProxy/Utilities/ServiceLookupHelper.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Frozen; using System.Collections.Generic; namespace Yarp.ReverseProxy.Utilities; internal static class ServiceLookupHelper { public static FrozenDictionary ToDictionaryByUniqueId(this IEnumerable services, Func idSelector) { ArgumentNullException.ThrowIfNull(services); var result = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var service in services) { if (!result.TryAdd(idSelector(service), service)) { throw new ArgumentException($"More than one {typeof(T)} found with the same identifier.", nameof(services)); } } return result.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase); } public static T GetRequiredServiceById(this FrozenDictionary services, string? id, string defaultId) { var lookup = id; if (string.IsNullOrEmpty(lookup)) { lookup = defaultId; } if (!services.TryGetValue(lookup, out var result)) { throw new ArgumentException($"No {typeof(T)} was found for the id '{lookup}'.", nameof(id)); } return result; } } ================================================ FILE: src/ReverseProxy/Utilities/SkipLocalsInit.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // Used to indicate to the compiler that the .locals init flag should not be set in method headers. [module: System.Runtime.CompilerServices.SkipLocalsInit] ================================================ FILE: src/ReverseProxy/Utilities/TaskUtilities.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Threading.Tasks; namespace Yarp.ReverseProxy.Utilities; internal static class TaskUtilities { internal static readonly Task TrueTask = Task.FromResult(true); internal static readonly Task FalseTask = Task.FromResult(false); } ================================================ FILE: src/ReverseProxy/Utilities/TlsFrameHelper.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. using System; using System.Diagnostics; using System.Buffers.Binary; using System.Globalization; using System.Net.Security; using System.Security.Authentication; using System.Text; namespace Yarp.ReverseProxy.Utilities.Tls; // SSL3/TLS protocol frames definitions. public enum TlsContentType : byte { ChangeCipherSpec = 20, Alert = 21, Handshake = 22, AppData = 23 } public enum TlsHandshakeType : byte { HelloRequest = 0, ClientHello = 1, ServerHello = 2, NewSessionTicket = 4, EndOfEarlyData = 5, EncryptedExtensions = 8, Certificate = 11, ServerKeyExchange = 12, CertificateRequest = 13, ServerHelloDone = 14, CertificateVerify = 15, ClientKeyExchange = 16, Finished = 20, KeyUpdate = 24, MessageHash = 254 } public enum TlsAlertLevel : byte { Warning = 1, Fatal = 2, } public enum TlsAlertDescription : byte { CloseNotify = 0, // warning UnexpectedMessage = 10, // error BadRecordMac = 20, // error DecryptionFailed = 21, // reserved RecordOverflow = 22, // error DecompressionFail = 30, // error HandshakeFailure = 40, // error BadCertificate = 42, // warning or error UnsupportedCert = 43, // warning or error CertificateRevoked = 44, // warning or error CertificateExpired = 45, // warning or error CertificateUnknown = 46, // warning or error IllegalParameter = 47, // error UnknownCA = 48, // error AccessDenied = 49, // error DecodeError = 50, // error DecryptError = 51, // error ExportRestriction = 60, // reserved ProtocolVersion = 70, // error InsufficientSecurity = 71, // error InternalError = 80, // error UserCanceled = 90, // warning or error NoRenegotiation = 100, // warning UnsupportedExt = 110, // error } public enum ExtensionType : ushort { ServerName = 0, MaximumFragmentLength = 1, ClientCertificateUrl = 2, TrustedCaKeys = 3, TruncatedHmac = 4, CertificateStatusRequest = 5, ApplicationProtocols = 16, SupportedVersions = 43 } public struct TlsFrameHeader { public TlsContentType Type; public SslProtocols Version; public int Length; public override string ToString() => $"{Version}:{Type}[{Length}]"; } public static class TlsFrameHelper { public const int HeaderSize = 5; [Flags] public enum ProcessingOptions { ServerName = 0x1, ApplicationProtocol = 0x2, Versions = 0x4, CipherSuites = 0x8, All = 0x7FFFFFFF, } [Flags] public enum ApplicationProtocolInfo { None = 0, Http11 = 1, Http2 = 2, Other = 128 } public enum ParsingStatus { Ok = 0, IncompleteFrame = 1, InvalidFrame = 2, UnsupportedFrame = 3, } public struct TlsFrameInfo { internal TlsCipherSuite[]? _ciphers; public TlsFrameHeader Header; public TlsHandshakeType HandshakeType; public SslProtocols SupportedVersions; public string TargetName; public ApplicationProtocolInfo ApplicationProtocols; public TlsAlertDescription AlertDescription; public ParsingStatus ParsingStatus; public ReadOnlyMemory TlsCipherSuites { get { return _ciphers is null ? ReadOnlyMemory.Empty : new ReadOnlyMemory(_ciphers); } } public override string ToString() { if (Header.Type == TlsContentType.Handshake) { if (HandshakeType == TlsHandshakeType.ClientHello) { return $"{Header.Version}:{HandshakeType}[{Header.Length}] TargetName='{TargetName}' SupportedVersion='{SupportedVersions}' ApplicationProtocols='{ApplicationProtocols}'"; } else if (HandshakeType == TlsHandshakeType.ServerHello) { return $"{Header.Version}:{HandshakeType}[{Header.Length}] SupportedVersion='{SupportedVersions}' ApplicationProtocols='{ApplicationProtocols}'"; } else { return $"{Header.Version}:{HandshakeType}[{Header.Length}] SupportedVersion='{SupportedVersions}'"; } } else { return $"{Header.Version}:{Header.Type}[{Header.Length}]"; } } } public delegate bool HelloExtensionCallback(ref TlsFrameInfo info, ExtensionType type, ReadOnlySpan extensionsData); private static readonly byte[] s_protocolMismatch13 = new byte[] { (byte)TlsContentType.Alert, 3, 4, 0, 2, 2, 70 }; private static readonly byte[] s_protocolMismatch12 = new byte[] { (byte)TlsContentType.Alert, 3, 3, 0, 2, 2, 70 }; private static readonly byte[] s_protocolMismatch11 = new byte[] { (byte)TlsContentType.Alert, 3, 2, 0, 2, 2, 70 }; private static readonly byte[] s_protocolMismatch10 = new byte[] { (byte)TlsContentType.Alert, 3, 1, 0, 2, 2, 70 }; private static readonly byte[] s_protocolMismatch30 = new byte[] { (byte)TlsContentType.Alert, 3, 0, 0, 2, 2, 40 }; private const int UInt24Size = 3; private const int RandomSize = 32; private const int OpaqueType1LengthSize = sizeof(byte); private const int OpaqueType2LengthSize = sizeof(ushort); private const int ProtocolVersionMajorOffset = 0; private const int ProtocolVersionMinorOffset = 1; private const int ProtocolVersionSize = 2; private const int ProtocolVersionTlsMajorValue = 3; // Per spec "AllowUnassigned flag MUST be set". See comment above DecodeString() for more details. private static readonly IdnMapping s_idnMapping = new IdnMapping() { AllowUnassigned = true }; private static readonly Encoding s_encoding = Encoding.GetEncoding("utf-8", new EncoderExceptionFallback(), new DecoderExceptionFallback()); public static bool TryGetFrameHeader(ReadOnlySpan frame, ref TlsFrameHeader header) { var result = frame.Length > 4; if (frame.Length >= 1) { header.Type = (TlsContentType)frame[0]; if (frame.Length > 4) { // SSLv3, TLS or later if (frame[1] == 3) { header.Length = ((frame[3] << 8) | frame[4]); header.Version = TlsMinorVersionToProtocol(frame[2]); return true; } // May be SSL3/TLS frame wrapped in unified header. else if (frame[2] == (byte)TlsHandshakeType.ClientHello && frame[3] == 3) // SSL3 or above { int length; if ((frame[0] & 0x80) != 0) { // Two bytes length = (((frame[0] & 0x7f) << 8) | frame[1]) + 2; } else { // Three bytes length = (((frame[0] & 0x3f) << 8) | frame[1]) + 3; } // max frame for SSLv2 is 32767. // However, we expect something reasonable for initial HELLO // We don't have enough logic to verify full validity, // the limits below are guesses. if (length > 20 && length < 1000) { #pragma warning disable CS0618 // Ssl2 and Ssl3 are obsolete header.Version = SslProtocols.Ssl2; #pragma warning restore CS0618 header.Length = length; header.Type = TlsContentType.Handshake; return true; } } } } header.Length = -1; header.Version = SslProtocols.None; return result; } // Returns frame size e.g. header + content public static int GetFrameSize(ReadOnlySpan frame) { if (frame.Length < 5 || frame[1] < 3) { return -1; } return ((frame[3] << 8) | frame[4]) + HeaderSize; } // This function will try to parse TLS hello frame and fill details in provided info structure. // If frame was fully processed without any error, function returns true. // Otherwise, it returns false and info may have partial data. // It is OK to call it again if more data becomes available. // It is also possible to limit what information is processed. // If callback delegate is provided, it will be called on ALL extensions. public static bool TryGetFrameInfo(ReadOnlySpan frame, ref TlsFrameInfo info, ProcessingOptions options = ProcessingOptions.All, HelloExtensionCallback? callback = null) { const int HandshakeTypeOffset = 5; if (frame.Length < HeaderSize) { info.ParsingStatus = ParsingStatus.IncompleteFrame; return false; } // This will not fail since we have enough data. var gotHeader = TryGetFrameHeader(frame, ref info.Header); Debug.Assert(gotHeader); info.SupportedVersions = info.Header.Version; #pragma warning disable CS0618 // Ssl2 and Ssl3 are obsolete if (info.Header.Version == SslProtocols.Ssl2) { // This is safe. We would not get here if the length is too small. info.SupportedVersions |= TlsMinorVersionToProtocol(frame[4]); // We only recognize Unified ClientHello at the moment. // This is needed to trigger certificate selection callback in SslStream. info.HandshakeType = TlsHandshakeType.ClientHello; // There is no more parsing for old protocols. return true; } #pragma warning restore CS0618 if (info.Header.Type == TlsContentType.Alert) { TlsAlertLevel level = default; TlsAlertDescription description = default; if (TryGetAlertInfo(frame, ref level, ref description)) { info.AlertDescription = description; info.ParsingStatus = ParsingStatus.Ok; return true; } info.ParsingStatus = ParsingStatus.IncompleteFrame; return false; } if (info.Header.Type != TlsContentType.Handshake) { info.ParsingStatus = ParsingStatus.UnsupportedFrame; return false; } if (frame.Length <= HandshakeTypeOffset) { info.ParsingStatus = ParsingStatus.IncompleteFrame; return false; } info.HandshakeType = (TlsHandshakeType)frame[HandshakeTypeOffset]; // Check if we have full frame. var isComplete = frame.Length >= HeaderSize + info.Header.Length; info.ParsingStatus = isComplete ? ParsingStatus.Ok : ParsingStatus.IncompleteFrame; #pragma warning disable SYSLIB0039 // TLS 1.0 and 1.1 are obsolete if (((int)info.Header.Version >= (int)SslProtocols.Tls) && #pragma warning restore SYSLIB0039 (info.HandshakeType == TlsHandshakeType.ClientHello || info.HandshakeType == TlsHandshakeType.ServerHello)) { if (!TryParseHelloFrame(frame.Slice(HeaderSize, Math.Min(info.Header.Length, frame.Length - HeaderSize)), ref info, options, callback)) { isComplete = false; } } return isComplete; } // This is similar to TryGetFrameInfo, but it will only process SNI. // It returns TargetName as string or NULL if SNI is missing or parsing error happened. public static string? GetServerName(ReadOnlySpan frame) { TlsFrameInfo info = default; if (!TryGetFrameInfo(frame, ref info, ProcessingOptions.ServerName)) { return null; } return info.TargetName; } // This function will parse the TLS Alert message, and return the alert level and description. public static bool TryGetAlertInfo(ReadOnlySpan frame, ref TlsAlertLevel level, ref TlsAlertDescription description) { if (frame.Length < 7 || frame[0] != (byte)TlsContentType.Alert) { return false; } level = (TlsAlertLevel)frame[5]; description = (TlsAlertDescription)frame[6]; return true; } private static byte[] CreateProtocolVersionAlert(SslProtocols version) => version switch { SslProtocols.Tls13 => s_protocolMismatch13, SslProtocols.Tls12 => s_protocolMismatch12, #pragma warning disable SYSLIB0039 // TLS 1.0 and 1.1 are obsolete SslProtocols.Tls11 => s_protocolMismatch11, SslProtocols.Tls => s_protocolMismatch10, #pragma warning restore SYSLIB0039 #pragma warning disable 0618 SslProtocols.Ssl3 => s_protocolMismatch30, #pragma warning restore 0618 _ => Array.Empty(), }; public static byte[] CreateAlertFrame(SslProtocols version, TlsAlertDescription reason) { if (reason == TlsAlertDescription.ProtocolVersion) { return CreateProtocolVersionAlert(version); } #pragma warning disable SYSLIB0039 // TLS 1.0 and 1.1 are obsolete else if ((int)version > (int)SslProtocols.Tls) { // Create TLS1.2 alert var buffer = new byte[] { (byte)TlsContentType.Alert, 3, 3, 0, 2, 2, (byte)reason }; switch (version) { case SslProtocols.Tls13: buffer[2] = 4; break; case SslProtocols.Tls11: buffer[2] = 2; break; case SslProtocols.Tls: buffer[2] = 1; break; } #pragma warning restore SYSLIB0039 return buffer; } return Array.Empty(); } private static bool TryParseHelloFrame(ReadOnlySpan sslHandshake, ref TlsFrameInfo info, ProcessingOptions options, HelloExtensionCallback? callback) { // https://tools.ietf.org/html/rfc6101#section-5.6 // struct { // HandshakeType msg_type; /* handshake type */ // uint24 length; /* bytes in message */ // select (HandshakeType) { // ... // case client_hello: ClientHello; // case server_hello: ServerHello; // ... // } body; // } Handshake; const int HandshakeTypeOffset = 0; const int HelloLengthOffset = HandshakeTypeOffset + sizeof(TlsHandshakeType); const int HelloOffset = HelloLengthOffset + UInt24Size; const int HandshakeHeaderLength = 4; // Type and Handshake length const int MinimalHandshakeLength = 44; // Version, Random, SessionID and Cipher length with at least one cipher if (info.Header.Length - HandshakeHeaderLength < MinimalHandshakeLength) { info.ParsingStatus = ParsingStatus.InvalidFrame; return false; } if (sslHandshake.Length < HelloOffset + 3) { info.ParsingStatus = ParsingStatus.IncompleteFrame; return false; } if ((TlsHandshakeType)sslHandshake[HandshakeTypeOffset] != TlsHandshakeType.ClientHello && (TlsHandshakeType)sslHandshake[HandshakeTypeOffset] != TlsHandshakeType.ServerHello) { info.ParsingStatus = ParsingStatus.UnsupportedFrame; return false; } var helloLength = ReadUInt24BigEndian(sslHandshake.Slice(HelloLengthOffset)); if (helloLength < MinimalHandshakeLength || helloLength > info.Header.Length - HandshakeHeaderLength) { info.ParsingStatus = ParsingStatus.InvalidFrame; return false; } var helloData = sslHandshake.Slice(HelloOffset); if (helloData.Length < helloLength) { info.ParsingStatus = ParsingStatus.IncompleteFrame; return false; } // ProtocolVersion may be different from frame header. if (helloData[ProtocolVersionMajorOffset] == ProtocolVersionTlsMajorValue) { info.SupportedVersions |= TlsMinorVersionToProtocol(helloData[ProtocolVersionMinorOffset]); } return (TlsHandshakeType)sslHandshake[HandshakeTypeOffset] == TlsHandshakeType.ClientHello ? TryParseClientHello(helloData.Slice(0, helloLength), ref info, options, callback) : TryParseServerHello(helloData.Slice(0, helloLength), ref info, options, callback); } private static bool TryParseClientHello(ReadOnlySpan clientHello, ref TlsFrameInfo info, ProcessingOptions options, HelloExtensionCallback? callback) { // Basic structure: https://tools.ietf.org/html/rfc6101#section-5.6.1.2 // Extended structure: https://tools.ietf.org/html/rfc3546#section-2.1 // struct { // ProtocolVersion client_version; // 2x uint8 // Random random; // 32 bytes // SessionID session_id; // opaque type // CipherSuite cipher_suites<2..2^16-1>; // opaque type // CompressionMethod compression_methods<1..2^8-1>; // opaque type // Extension client_hello_extension_list<0..2^16-1>; // } ClientHello; var p = SkipBytes(clientHello, ProtocolVersionSize + RandomSize); // Skip SessionID (max size 32 => size fits in 1 byte) p = SkipOpaqueType1(p); if (options.HasFlag(ProcessingOptions.CipherSuites)) { TryGetCipherSuites(p, ref info); } // Skip cipher suites (max size 2^16-1 => size fits in 2 bytes) p = SkipOpaqueType2(p); // Skip compression methods (max size 2^8-1 => size fits in 1 byte) p = SkipOpaqueType1(p); // no extension if (p.IsEmpty) { return true; } // client_hello_extension_list (max size 2^16-1 => size fits in 2 bytes) int extensionListLength = BinaryPrimitives.ReadUInt16BigEndian(p); p = SkipBytes(p, sizeof(ushort)); if (extensionListLength != p.Length) { return false; } return TryParseHelloExtensions(p, ref info, options, callback); } private static bool TryParseServerHello(ReadOnlySpan serverHello, ref TlsFrameInfo info, ProcessingOptions options, HelloExtensionCallback? callback) { // Basic structure: https://tools.ietf.org/html/rfc6101#section-5.6.1.3 // Extended structure: https://tools.ietf.org/html/rfc3546#section-2.2 // struct { // ProtocolVersion server_version; // Random random; // SessionID session_id; // CipherSuite cipher_suite; // CompressionMethod compression_method; // Extension server_hello_extension_list<0..2^16-1>; // } // ServerHello; const int CipherSuiteLength = 2; const int CompressionMethodLength = 1; var p = SkipBytes(serverHello, ProtocolVersionSize + RandomSize); // Skip SessionID (max size 32 => size fits in 1 byte) p = SkipOpaqueType1(p); p = SkipBytes(p, CipherSuiteLength + CompressionMethodLength); // is invalid structure or no extensions? if (p.IsEmpty) { return false; } // client_hello_extension_list (max size 2^16-1 => size fits in 2 bytes) int extensionListLength = BinaryPrimitives.ReadUInt16BigEndian(p); p = SkipBytes(p, sizeof(ushort)); if (extensionListLength != p.Length) { return false; } return TryParseHelloExtensions(p, ref info, options, callback); } // This is common for ClientHello and ServerHello. private static bool TryParseHelloExtensions(ReadOnlySpan extensions, ref TlsFrameInfo info, ProcessingOptions options, HelloExtensionCallback? callback) { const int ExtensionHeader = 4; var isComplete = true; while (extensions.Length >= ExtensionHeader) { var extensionType = (ExtensionType)BinaryPrimitives.ReadUInt16BigEndian(extensions); extensions = SkipBytes(extensions, sizeof(ushort)); var extensionLength = BinaryPrimitives.ReadUInt16BigEndian(extensions); extensions = SkipBytes(extensions, sizeof(ushort)); if (extensions.Length < extensionLength) { isComplete = false; break; } var extensionData = extensions.Slice(0, extensionLength); if (extensionType == ExtensionType.ServerName && options.HasFlag(ProcessingOptions.ServerName)) { if (!TryGetSniFromServerNameList(extensionData, out var sni)) { return false; } info.TargetName = sni!; } else if (extensionType == ExtensionType.SupportedVersions && options.HasFlag(ProcessingOptions.Versions)) { if (!TryGetSupportedVersionsFromExtension(extensionData, out var versions)) { return false; } info.SupportedVersions |= versions; } else if (extensionType == ExtensionType.ApplicationProtocols && options.HasFlag(ProcessingOptions.ApplicationProtocol)) { if (!TryGetApplicationProtocolsFromExtension(extensionData, out var alpn)) { return false; } info.ApplicationProtocols |= alpn; } callback?.Invoke(ref info, extensionType, extensionData); extensions = extensions.Slice(extensionLength); } return isComplete; } private static bool TryGetSniFromServerNameList(ReadOnlySpan serverNameListExtension, out string? sni) { // https://tools.ietf.org/html/rfc3546#section-3.1 // struct { // ServerName server_name_list<1..2^16-1> // } ServerNameList; // ServerNameList is an opaque type (length of sufficient size for max data length is prepended) const int ServerNameListOffset = sizeof(ushort); sni = null; if (serverNameListExtension.Length < ServerNameListOffset) { return false; } int serverNameListLength = BinaryPrimitives.ReadUInt16BigEndian(serverNameListExtension); var serverNameList = serverNameListExtension.Slice(ServerNameListOffset); if (serverNameListLength != serverNameList.Length) { return false; } var serverName = serverNameList.Slice(0, serverNameListLength); sni = GetSniFromServerName(serverName, out var invalid); return invalid == false; } private static string? GetSniFromServerName(ReadOnlySpan serverName, out bool invalid) { // https://tools.ietf.org/html/rfc3546#section-3.1 // struct { // NameType name_type; // select (name_type) { // case host_name: HostName; // } name; // } ServerName; // ServerName is an opaque type (length of sufficient size for max data length is prepended) const int NameTypeOffset = 0; const int HostNameStructOffset = NameTypeOffset + sizeof(NameType); if (serverName.Length < HostNameStructOffset) { invalid = true; return null; } // Following can underflow but it is ok due to equality check below var nameType = (NameType)serverName[NameTypeOffset]; var hostNameStruct = serverName.Slice(HostNameStructOffset); if (nameType != NameType.HostName) { invalid = true; return null; } return GetSniFromHostNameStruct(hostNameStruct, out invalid); } private static string? GetSniFromHostNameStruct(ReadOnlySpan hostNameStruct, out bool invalid) { // https://tools.ietf.org/html/rfc3546#section-3.1 // HostName is an opaque type (length of sufficient size for max data length is prepended) const int HostNameLengthOffset = 0; const int HostNameOffset = HostNameLengthOffset + sizeof(ushort); int hostNameLength = BinaryPrimitives.ReadUInt16BigEndian(hostNameStruct); var hostName = hostNameStruct.Slice(HostNameOffset); if (hostNameLength != hostName.Length) { invalid = true; return null; } invalid = false; return DecodeString(hostName); } private static bool TryGetSupportedVersionsFromExtension(ReadOnlySpan extensionData, out SslProtocols protocols) { // https://tools.ietf.org/html/rfc8446#section-4.2.1 // struct { // select(Handshake.msg_type) { // case client_hello: // ProtocolVersion versions<2..254 >; // // case server_hello: /* and HelloRetryRequest */ // ProtocolVersion selected_version; // }; const int VersionListLengthOffset = 0; const int VersionListNameOffset = VersionListLengthOffset + sizeof(byte); const int VersionLength = 2; protocols = SslProtocols.None; var supportedVersionLength = extensionData[VersionListLengthOffset]; extensionData = extensionData.Slice(VersionListNameOffset); if (extensionData.Length != supportedVersionLength) { return false; } // Get list of protocols we support. Ignore the rest. while (extensionData.Length >= VersionLength) { if (extensionData[ProtocolVersionMajorOffset] == ProtocolVersionTlsMajorValue) { protocols |= TlsMinorVersionToProtocol(extensionData[ProtocolVersionMinorOffset]); } extensionData = extensionData.Slice(VersionLength); } return true; } private static bool TryGetApplicationProtocolsFromExtension(ReadOnlySpan extensionData, out ApplicationProtocolInfo alpn) { // https://tools.ietf.org/html/rfc7301#section-3.1 // opaque ProtocolName<1..2 ^ 8 - 1 >; // // struct { // ProtocolName protocol_name_list<2..2^16-1> // } // ProtocolNameList; const int AlpnListLengthOffset = 0; const int AlpnListOffset = AlpnListLengthOffset + sizeof(short); alpn = ApplicationProtocolInfo.None; if (extensionData.Length < AlpnListOffset) { return false; } int AlpnListLength = BinaryPrimitives.ReadUInt16BigEndian(extensionData); var alpnList = extensionData.Slice(AlpnListOffset); if (AlpnListLength != alpnList.Length) { return false; } while (!alpnList.IsEmpty) { var protocolLength = alpnList[0]; if (alpnList.Length < protocolLength + 1) { return false; } var protocol = alpnList.Slice(1, protocolLength); if (protocolLength == 2) { if (protocol.SequenceEqual(SslApplicationProtocol.Http2.Protocol.Span)) { alpn |= ApplicationProtocolInfo.Http2; } else { alpn |= ApplicationProtocolInfo.Other; } } else if (protocolLength == SslApplicationProtocol.Http11.Protocol.Length && protocol.SequenceEqual(SslApplicationProtocol.Http11.Protocol.Span)) { alpn |= ApplicationProtocolInfo.Http11; } else { alpn |= ApplicationProtocolInfo.Other; } alpnList = alpnList.Slice(protocolLength + 1); } return true; } private static bool TryGetCipherSuites(ReadOnlySpan bytes, ref TlsFrameInfo info) { if (bytes.Length < OpaqueType2LengthSize) { return false; } var length = BinaryPrimitives.ReadUInt16BigEndian(bytes); if (bytes.Length < OpaqueType2LengthSize + length) { return false; } bytes = bytes.Slice(OpaqueType2LengthSize, length); var count = length / 2; info._ciphers = new TlsCipherSuite[count]; for (var i = 0; i < count; i++) { info._ciphers[i] = (TlsCipherSuite)BinaryPrimitives.ReadUInt16BigEndian(bytes.Slice(i * 2, 2)); } return true; } private static SslProtocols TlsMinorVersionToProtocol(byte value) { return value switch { 4 => SslProtocols.Tls13, 3 => SslProtocols.Tls12, #pragma warning disable SYSLIB0039 // TLS 1.0 and 1.1 are obsolete 2 => SslProtocols.Tls11, 1 => SslProtocols.Tls, #pragma warning restore SYSLIB0039 #pragma warning disable 0618 0 => SslProtocols.Ssl3, #pragma warning restore 0618 _ => SslProtocols.None, }; } private static string? DecodeString(ReadOnlySpan bytes) { // https://tools.ietf.org/html/rfc3546#section-3.1 // Per spec: // If the hostname labels contain only US-ASCII characters, then the // client MUST ensure that labels are separated only by the byte 0x2E, // representing the dot character U+002E (requirement 1 in section 3.1 // of [IDNA] notwithstanding). If the server needs to match the HostName // against names that contain non-US-ASCII characters, it MUST perform // the conversion operation described in section 4 of [IDNA], treating // the HostName as a "query string" (i.e. the AllowUnassigned flag MUST // be set). Note that IDNA allows labels to be separated by any of the // Unicode characters U+002E, U+3002, U+FF0E, and U+FF61, therefore // servers MUST accept any of these characters as a label separator. If // the server only needs to match the HostName against names containing // exclusively ASCII characters, it MUST compare ASCII names case- // insensitively. string idnEncodedString; try { idnEncodedString = s_encoding.GetString(bytes); } catch (DecoderFallbackException) { return null; } try { return s_idnMapping.GetUnicode(idnEncodedString); } catch (ArgumentException) { // client has not done IDN mapping return idnEncodedString; } } private static int ReadUInt24BigEndian(ReadOnlySpan bytes) { return (bytes[0] << 16) | (bytes[1] << 8) | bytes[2]; } private static ReadOnlySpan SkipBytes(ReadOnlySpan bytes, int numberOfBytesToSkip) { return (numberOfBytesToSkip < bytes.Length) ? bytes.Slice(numberOfBytesToSkip) : ReadOnlySpan.Empty; } // Opaque type is of structure: // - length (minimum number of bytes to hold the max value) // - data (length bytes) // We will only use opaque types which are of max size: 255 (length = 1) or 2^16-1 (length = 2). // We will call them SkipOpaqueType`length` private static ReadOnlySpan SkipOpaqueType1(ReadOnlySpan bytes) { if (bytes.Length < OpaqueType1LengthSize) { return ReadOnlySpan.Empty; } var length = bytes[0]; var totalBytes = OpaqueType1LengthSize + length; return SkipBytes(bytes, totalBytes); } private static ReadOnlySpan SkipOpaqueType2(ReadOnlySpan bytes) { if (bytes.Length < OpaqueType2LengthSize) { return ReadOnlySpan.Empty; } var length = BinaryPrimitives.ReadUInt16BigEndian(bytes); var totalBytes = OpaqueType2LengthSize + length; return SkipBytes(bytes, totalBytes); } private enum NameType : byte { HostName = 0x00 } } ================================================ FILE: src/ReverseProxy/Utilities/ValueStopwatch.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Diagnostics; namespace Yarp.ReverseProxy.Utilities; /// /// Value-type replacement for which avoids allocations. /// /// /// Inspired on . /// internal struct ValueStopwatch { private static readonly double _timestampToTicks = TimeSpan.TicksPerSecond / (double)Stopwatch.Frequency; private readonly long _startTimestamp; private ValueStopwatch(long startTimestamp) { _startTimestamp = startTimestamp; } /// /// Gets the time elapsed since the stopwatch was created with . /// public TimeSpan Elapsed { get { // Start timestamp can't be zero in an initialized ValueStopwatch. It would have to be literally the first thing executed when the machine boots to be 0. // So it being 0 is a clear indication of default(ValueStopwatch) if (_startTimestamp == 0) { throw new InvalidOperationException("An uninitialized, or 'default', ValueStopwatch cannot be used to get elapsed time."); } var end = Stopwatch.GetTimestamp(); var timestampDelta = end - _startTimestamp; var ticks = (long)(_timestampToTicks * timestampDelta); return new TimeSpan(ticks); } } /// /// Creates a new that is ready to be used. /// public static ValueStopwatch StartNew() => new ValueStopwatch(Stopwatch.GetTimestamp()); } ================================================ FILE: src/ReverseProxy/Utilities/ValueStringBuilder.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Buffers; using System.Diagnostics; using System.Globalization; using System.Runtime.CompilerServices; namespace Yarp.ReverseProxy.Utilities; // Adapted from https://github.com/dotnet/runtime/blob/82fee2692b3954ba8903fa4764f1f4e36a26341a/src/libraries/Common/src/System/Text/ValueStringBuilder.cs internal ref partial struct ValueStringBuilder { public const int StackallocThreshold = 512; private char[]? _arrayToReturnToPool; private Span _chars; private int _pos; public ValueStringBuilder(Span initialBuffer) { _arrayToReturnToPool = null; _chars = initialBuffer; _pos = 0; } public int Length { get => _pos; set { Debug.Assert(value >= 0); Debug.Assert(value <= _chars.Length); _pos = value; } } public override string ToString() { var s = _chars.Slice(0, _pos).ToString(); Dispose(); return s; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Append(char c) { var pos = _pos; var chars = _chars; if ((uint)pos < (uint)chars.Length) { chars[pos] = c; _pos = pos + 1; } else { GrowAndAppend(c); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Append(string s) { if (s is null) { return; } var pos = _pos; if (pos > _chars.Length - s.Length) { Grow(s.Length); } s.CopyTo(_chars.Slice(pos)); _pos += s.Length; } public void Append(ReadOnlySpan value) { var pos = _pos; if (pos > _chars.Length - value.Length) { Grow(value.Length); } value.CopyTo(_chars.Slice(_pos)); _pos += value.Length; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Append(int i) { var pos = _pos; if (i.TryFormat(_chars.Slice(pos), out var charsWritten, default, null)) { _pos = pos + charsWritten; } else { Append(i.ToString(CultureInfo.InvariantCulture)); } } [MethodImpl(MethodImplOptions.NoInlining)] private void GrowAndAppend(char c) { Grow(1); Append(c); } /// /// Resize the internal buffer either by doubling current buffer size or /// by adding to /// whichever is greater. /// /// /// Number of chars requested beyond current position. /// [MethodImpl(MethodImplOptions.NoInlining)] private void Grow(int additionalCapacityBeyondPos) { Debug.Assert(additionalCapacityBeyondPos > 0); Debug.Assert(_pos > _chars.Length - additionalCapacityBeyondPos, "Grow called incorrectly, no resize is needed."); const uint ArrayMaxLength = 0x7FFFFFC7; // same as Array.MaxLength // Increase to at least the required size (_pos + additionalCapacityBeyondPos), but try // to double the size if possible, bounding the doubling to not go beyond the max array length. var newCapacity = (int)Math.Max( (uint)(_pos + additionalCapacityBeyondPos), Math.Min((uint)_chars.Length * 2, ArrayMaxLength)); // Make sure to let Rent throw an exception if the caller has a bug and the desired capacity is negative. // This could also go negative if the actual required length wraps around. var poolArray = ArrayPool.Shared.Rent(newCapacity); _chars.Slice(0, _pos).CopyTo(poolArray); var toReturn = _arrayToReturnToPool; _chars = _arrayToReturnToPool = poolArray; if (toReturn is not null) { ArrayPool.Shared.Return(toReturn); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Dispose() { var toReturn = _arrayToReturnToPool; this = default; // for safety, to avoid using pooled array if this instance is erroneously appended to again if (toReturn is not null) { ArrayPool.Shared.Return(toReturn); } } } ================================================ FILE: src/ReverseProxy/WebSocketsTelemetry/HttpConnectFeatureWrapper.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Diagnostics; using System.IO; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; namespace Yarp.ReverseProxy.WebSocketsTelemetry; internal sealed class HttpConnectFeatureWrapper : IHttpExtendedConnectFeature { private readonly TimeProvider _timeProvider; public HttpContext HttpContext { get; private set; } public IHttpExtendedConnectFeature InnerConnectFeature { get; private set; } public WebSocketsTelemetryStream? TelemetryStream { get; private set; } public bool IsExtendedConnect => InnerConnectFeature.IsExtendedConnect; public string? Protocol => InnerConnectFeature.Protocol; public HttpConnectFeatureWrapper(TimeProvider timeProvider, HttpContext httpContext, IHttpExtendedConnectFeature connectFeature) { ArgumentNullException.ThrowIfNull(timeProvider); ArgumentNullException.ThrowIfNull(httpContext); ArgumentNullException.ThrowIfNull(connectFeature); _timeProvider = timeProvider; HttpContext = httpContext; InnerConnectFeature = connectFeature; } public async ValueTask AcceptAsync() { Debug.Assert(TelemetryStream is null); var opaqueTransport = await InnerConnectFeature.AcceptAsync(); TelemetryStream = new WebSocketsTelemetryStream(_timeProvider, opaqueTransport); return TelemetryStream; } } ================================================ FILE: src/ReverseProxy/WebSocketsTelemetry/HttpUpgradeFeatureWrapper.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Diagnostics; using System.IO; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.Net.Http.Headers; namespace Yarp.ReverseProxy.WebSocketsTelemetry; internal sealed class HttpUpgradeFeatureWrapper : IHttpUpgradeFeature { private readonly TimeProvider _timeProvider; public HttpContext HttpContext { get; private set; } public IHttpUpgradeFeature InnerUpgradeFeature { get; private set; } public WebSocketsTelemetryStream? TelemetryStream { get; private set; } public bool IsUpgradableRequest => InnerUpgradeFeature.IsUpgradableRequest; public HttpUpgradeFeatureWrapper(TimeProvider timeProvider, HttpContext httpContext, IHttpUpgradeFeature upgradeFeature) { ArgumentNullException.ThrowIfNull(timeProvider); ArgumentNullException.ThrowIfNull(httpContext); ArgumentNullException.ThrowIfNull(upgradeFeature); _timeProvider = timeProvider; HttpContext = httpContext; InnerUpgradeFeature = upgradeFeature; } public async Task UpgradeAsync() { Debug.Assert(TelemetryStream is null); var opaqueTransport = await InnerUpgradeFeature.UpgradeAsync(); if (HttpContext.Response.Headers.TryGetValue(HeaderNames.Upgrade, out var upgradeValues) && upgradeValues.Count == 1 && string.Equals("WebSocket", upgradeValues.ToString(), StringComparison.OrdinalIgnoreCase)) { TelemetryStream = new WebSocketsTelemetryStream(_timeProvider, opaqueTransport); } return TelemetryStream ?? opaqueTransport; } } ================================================ FILE: src/ReverseProxy/WebSocketsTelemetry/WebSocketCloseReason.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. namespace Yarp.ReverseProxy.WebSocketsTelemetry; internal enum WebSocketCloseReason : int { Unknown, ClientGracefulClose, ServerGracefulClose, ClientDisconnect, ServerDisconnect, ActivityTimeout, } ================================================ FILE: src/ReverseProxy/WebSocketsTelemetry/WebSocketsParser.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Diagnostics; namespace Yarp.ReverseProxy.WebSocketsTelemetry; internal unsafe struct WebSocketsParser { private const int MaskLength = 4; private const int MinHeaderSize = 2; private const int MaxHeaderSize = MinHeaderSize + MaskLength + sizeof(ulong); private fixed byte _leftoverBuffer[MaxHeaderSize - 1]; private readonly byte _minHeaderSize; private byte _leftover; private ulong _bytesToSkip; private long _closeTime; private readonly TimeProvider _timeProvider; public long MessageCount { get; private set; } public DateTime? CloseTime => _closeTime == 0 ? null : new DateTime(_closeTime, DateTimeKind.Utc); public WebSocketsParser(TimeProvider timeProvider, bool isServer) { _minHeaderSize = (byte)(MinHeaderSize + (isServer ? MaskLength : 0)); _leftover = 0; _bytesToSkip = 0; _closeTime = 0; _timeProvider = timeProvider; MessageCount = 0; } // The WebSocket Protocol: https://datatracker.ietf.org/doc/html/rfc6455#section-5.2 // 0 1 2 3 // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 // +-+-+-+-+-------+-+-------------+-------------------------------+ // |F|R|R|R| opcode|M| Payload len | Extended payload length | // |I|S|S|S| (4) |A| (7) | (16/64) | // |N|V|V|V| |S| | (if payload len==126/127) | // | |1|2|3| |K| | | // +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + // | Extended payload length continued, if payload len == 127 | // + - - - - - - - - - - - - - - - +-------------------------------+ // | |Masking-key, if MASK set to 1 | // +-------------------------------+-------------------------------+ // | Masking-key (continued) | Payload Data | // +-------------------------------- - - - - - - - - - - - - - - - + // : Payload Data continued ... : // +---------------------------------------------------------------+ // // The header can be 2-10 bytes long, followed by a 4 byte mask if the message was sent by the client. // We have to read the first 2 bytes to know how long the frame header will be. // Since the buffer may not contain the full frame, we make use of a leftoverBuffer // where we store leftover bytes that don't represent a complete frame header. // On the next call to Consume, we interpret the leftover bytes as the beginning of the frame. // As we are not interested in the actual payload data, we skip over (payload length + mask length) bytes after each header. public void Consume(ReadOnlySpan buffer) { int leftover = _leftover; var bytesToSkip = _bytesToSkip; while (true) { var toSkip = Math.Min(bytesToSkip, (ulong)buffer.Length); buffer = buffer.Slice((int)toSkip); bytesToSkip -= toSkip; var available = leftover + buffer.Length; int headerSize = _minHeaderSize; if (available < headerSize) { break; } var length = (leftover > 1 ? _leftoverBuffer[1] : buffer[1 - leftover]) & 0x7FUL; if (length > 125) { // The actual length will be encoded in 2 or 8 bytes, based on whether the length was 126 or 127 var lengthBytes = 2 << (((int)length & 1) << 1); headerSize += lengthBytes; Debug.Assert(leftover < headerSize); if (available < headerSize) { break; } lengthBytes += MinHeaderSize; length = 0; for (var i = MinHeaderSize; i < lengthBytes; i++) { length <<= 8; length |= i < leftover ? _leftoverBuffer[i] : buffer[i - leftover]; } } Debug.Assert(leftover < headerSize); bytesToSkip = length; const int NonReservedBitsMask = 0b_1000_1111; var header = (leftover > 0 ? _leftoverBuffer[0] : buffer[0]) & NonReservedBitsMask; // Don't count control frames under MessageCount if ((uint)(header - 0x80) <= 0x02) { // Has FIN (0x80) and is a Continuation (0x00) / Text (0x01) / Binary (0x02) opcode MessageCount++; } else if ((header & 0xF) == 0x8) // CLOSE { if (_closeTime == 0) { _closeTime = _timeProvider.GetUtcNow().Ticks; } } // Advance the buffer by the number of bytes read for the header, // accounting for any bytes we may have read from the leftoverBuffer buffer = buffer.Slice(headerSize - leftover); leftover = 0; } Debug.Assert(bytesToSkip == 0 || buffer.Length == 0); _bytesToSkip = bytesToSkip; Debug.Assert(leftover + buffer.Length < MaxHeaderSize); for (var i = 0; i < buffer.Length; i++, leftover++) { _leftoverBuffer[leftover] = buffer[i]; } _leftover = (byte)leftover; } } ================================================ FILE: src/ReverseProxy/WebSocketsTelemetry/WebSocketsTelemetry.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Tracing; namespace Yarp.ReverseProxy.WebSocketsTelemetry; [EventSource(Name = "Yarp.ReverseProxy.WebSockets")] internal sealed class WebSocketsTelemetry : EventSource { public static readonly WebSocketsTelemetry Log = new(); [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "Parameters to this method are primitive and are trimmer safe.")] [Event(1, Level = EventLevel.Informational)] public void WebSocketClosed(long establishedTime, WebSocketCloseReason closeReason, long messagesRead, long messagesWritten) { if (IsEnabled(EventLevel.Informational, EventKeywords.All)) { WriteEvent(eventId: 1, establishedTime, closeReason, messagesRead, messagesWritten); } } } ================================================ FILE: src/ReverseProxy/WebSocketsTelemetry/WebSocketsTelemetryExtensions.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using Microsoft.Extensions.DependencyInjection; using Yarp.ReverseProxy.WebSocketsTelemetry; namespace Microsoft.AspNetCore.Builder; /// /// extension methods to add the . /// public static class WebSocketsTelemetryExtensions { /// /// Adds a to the request pipeline. /// Must be added before . /// public static IApplicationBuilder UseWebSocketsTelemetry(this IApplicationBuilder app) { return app.Use(next => { // UseWebSocketsTelemetry may be used independently of the rest of YARP. // Avoid exposing another extension method (AddWebSocketsTelemetry) just because of TimeProvider. var timeProvider = app.ApplicationServices.GetService() ?? TimeProvider.System; return new WebSocketsTelemetryMiddleware(next, timeProvider).InvokeAsync; }); } } ================================================ FILE: src/ReverseProxy/WebSocketsTelemetry/WebSocketsTelemetryMiddleware.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; namespace Yarp.ReverseProxy.WebSocketsTelemetry; internal sealed class WebSocketsTelemetryMiddleware { private readonly RequestDelegate _next; private readonly TimeProvider _timeProvider; public WebSocketsTelemetryMiddleware(RequestDelegate next, TimeProvider timeProvider) { ArgumentNullException.ThrowIfNull(next); ArgumentNullException.ThrowIfNull(timeProvider); _next = next; _timeProvider = timeProvider; } public Task InvokeAsync(HttpContext context) { if (WebSocketsTelemetry.Log.IsEnabled()) { if (context.Features.Get() is { IsUpgradableRequest: true } upgradeFeature) { var upgradeWrapper = new HttpUpgradeFeatureWrapper(_timeProvider, context, upgradeFeature); return InvokeAsyncCore(upgradeWrapper, _next); } else if (context.Features.Get() is { IsExtendedConnect: true } connectFeature && string.Equals("websocket", connectFeature.Protocol, StringComparison.OrdinalIgnoreCase)) { var connectWrapper = new HttpConnectFeatureWrapper(_timeProvider, context, connectFeature); return InvokeAsyncCore(connectWrapper, _next); } } return _next(context); } private static async Task InvokeAsyncCore(HttpUpgradeFeatureWrapper upgradeWrapper, RequestDelegate next) { upgradeWrapper.HttpContext.Features.Set(upgradeWrapper); try { await next(upgradeWrapper.HttpContext); } finally { if (upgradeWrapper.TelemetryStream is { } telemetryStream) { WebSocketsTelemetry.Log.WebSocketClosed( telemetryStream.EstablishedTime.Ticks, telemetryStream.GetCloseReason(upgradeWrapper.HttpContext), telemetryStream.MessagesRead, telemetryStream.MessagesWritten); } upgradeWrapper.HttpContext.Features.Set(upgradeWrapper.InnerUpgradeFeature); } } private static async Task InvokeAsyncCore(HttpConnectFeatureWrapper connectWrapper, RequestDelegate next) { connectWrapper.HttpContext.Features.Set(connectWrapper); try { await next(connectWrapper.HttpContext); } finally { if (connectWrapper.TelemetryStream is { } telemetryStream) { WebSocketsTelemetry.Log.WebSocketClosed( telemetryStream.EstablishedTime.Ticks, telemetryStream.GetCloseReason(connectWrapper.HttpContext), telemetryStream.MessagesRead, telemetryStream.MessagesWritten); } connectWrapper.HttpContext.Features.Set(connectWrapper.InnerConnectFeature); } } } ================================================ FILE: src/ReverseProxy/WebSocketsTelemetry/WebSocketsTelemetryStream.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.IO; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Yarp.ReverseProxy.Forwarder; using Yarp.ReverseProxy.Utilities; namespace Yarp.ReverseProxy.WebSocketsTelemetry; internal sealed class WebSocketsTelemetryStream : DelegatingStream { private WebSocketsParser _readParser, _writeParser; public DateTime EstablishedTime { get; } public long MessagesRead => _readParser.MessageCount; public long MessagesWritten => _writeParser.MessageCount; public WebSocketsTelemetryStream(TimeProvider timeProvider, Stream innerStream) : base(innerStream) { EstablishedTime = timeProvider.GetUtcNow().UtcDateTime; _readParser = new WebSocketsParser(timeProvider, isServer: true); _writeParser = new WebSocketsParser(timeProvider, isServer: false); } public WebSocketCloseReason GetCloseReason(HttpContext context) { var clientCloseTime = _readParser.CloseTime; var serverCloseTime = _writeParser.CloseTime; // Mutual, graceful WebSocket close. We report whichever one we saw first. if (clientCloseTime.HasValue && serverCloseTime.HasValue) { return clientCloseTime.Value < serverCloseTime.Value ? WebSocketCloseReason.ClientGracefulClose : WebSocketCloseReason.ServerGracefulClose; } // One side sent a WebSocket close, but we never saw a response from the other side // It is possible an error occurred, but we saw a graceful close first, so that is the initiator if (clientCloseTime.HasValue) { return WebSocketCloseReason.ClientGracefulClose; } if (serverCloseTime.HasValue) { return WebSocketCloseReason.ServerGracefulClose; } return context.Features.Get()?.Error switch { // Either side disconnected without sending a WebSocket close ForwarderError.UpgradeRequestClient => WebSocketCloseReason.ClientDisconnect, ForwarderError.UpgradeRequestCanceled => WebSocketCloseReason.ClientDisconnect, ForwarderError.UpgradeResponseClient => WebSocketCloseReason.ClientDisconnect, ForwarderError.UpgradeResponseCanceled => WebSocketCloseReason.ClientDisconnect, ForwarderError.UpgradeRequestDestination => WebSocketCloseReason.ServerDisconnect, ForwarderError.UpgradeResponseDestination => WebSocketCloseReason.ServerDisconnect, // Activity Timeout ForwarderError.UpgradeActivityTimeout => WebSocketCloseReason.ActivityTimeout, // Both sides gracefully closed the underlying connection without sending a WebSocket close, // or the server closed the connection and we canceled the client and suppressed the errors. null => WebSocketCloseReason.ServerDisconnect, // We are not expecting any other error from HttpForwarder after a successful connection upgrade // Technically, a user could overwrite the IForwarderErrorFeature, in which case we don't know what's going on _ => WebSocketCloseReason.Unknown }; } public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) { var readTask = base.ReadAsync(buffer, cancellationToken); if (buffer.Length == 0) { return readTask; } if (readTask.IsCompletedSuccessfully) { var read = readTask.GetAwaiter().GetResult(); _readParser.Consume(buffer.Span.Slice(0, read)); return new ValueTask(read); } return Core(buffer, readTask); async ValueTask Core(Memory buffer, ValueTask readTask) { var read = await readTask; _readParser.Consume(buffer.Span.Slice(0, read)); return read; } } public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) { _writeParser.Consume(buffer.Span); return base.WriteAsync(buffer, cancellationToken); } } ================================================ FILE: src/ReverseProxy/Yarp.ReverseProxy.csproj ================================================ Reverse proxy toolkit for building fast proxy servers in .NET using the infrastructure from ASP.NET and .NET $(ReleaseTFMs) Library Yarp.ReverseProxy true enable true README.md yarp;dotnet;reverse-proxy;aspnetcore ================================================ FILE: src/TelemetryConsumption/EventListenerService.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Diagnostics.Tracing; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace Yarp.Telemetry.Consumption; internal abstract class EventListenerService : EventListener, IHostedService where TMetrics : class, new() { protected abstract string EventSourceName { get; } protected abstract int NumberOfMetrics { get; } protected abstract void OnEvent(TTelemetryConsumer[] consumers, EventWrittenEventArgs eventData); protected abstract bool TrySaveMetric(TMetrics metrics, string name, double value); private readonly ILogger _logger; private readonly TTelemetryConsumer[]? _telemetryConsumers; private readonly IMetricsConsumer[]? _metricsConsumers; private int _metricsCount; private TMetrics? _previousMetrics; private TMetrics? _currentMetrics; private EventSource? _eventSource; private readonly object _syncObject = new(); private readonly bool _initialized; public EventListenerService( ILogger logger, IEnumerable telemetryConsumers, IEnumerable> metricsConsumers) { ArgumentNullException.ThrowIfNull(logger); ArgumentNullException.ThrowIfNull(telemetryConsumers); ArgumentNullException.ThrowIfNull(metricsConsumers); _logger = logger; _telemetryConsumers = telemetryConsumers.ToArray(); _metricsConsumers = metricsConsumers.ToArray(); if (_telemetryConsumers.Any(s => s is null) || metricsConsumers.Any(c => c is null)) { throw new ArgumentException("A consumer may not be null", _telemetryConsumers.Any(s => s is null) ? nameof(telemetryConsumers) : nameof(metricsConsumers)); } if (_telemetryConsumers.Length == 0) { _telemetryConsumers = null; } if (_metricsConsumers.Length == 0) { _metricsConsumers = null; } lock (_syncObject) { if (_eventSource is EventSource eventSource) { EnableEventSource(eventSource); } _initialized = true; } } protected override void OnEventSourceCreated(EventSource eventSource) { if (eventSource.Name == EventSourceName) { lock (_syncObject) { _eventSource = eventSource; if (_initialized) { // Ctor already finished - enable the EventSource here EnableEventSource(eventSource); } } } } private void EnableEventSource(EventSource eventSource) { var enableEvents = _telemetryConsumers is not null; var enableMetrics = _metricsConsumers is not null; if (!enableEvents && !enableMetrics) { return; } var eventLevel = enableEvents ? EventLevel.Informational : EventLevel.Critical; var arguments = enableMetrics ? new Dictionary { { "EventCounterIntervalSec", MetricsOptions.Interval.TotalSeconds.ToString() } } : null; EnableEvents(eventSource, eventLevel, EventKeywords.None, arguments); } protected sealed override void OnEventWritten(EventWrittenEventArgs eventData) { if (eventData.EventId <= 0) { OnNonUserEvent(eventData); } else if (_telemetryConsumers is TTelemetryConsumer[] consumers) { OnEvent(consumers, eventData); } } private void OnNonUserEvent(EventWrittenEventArgs eventData) { if (eventData.EventId == -1) { if (!ReferenceEquals(eventData.EventSource, _eventSource)) { // Workaround for https://github.com/dotnet/runtime/issues/31927 // EventCounters are published to all EventListeners, regardless of // which EventSource providers a listener is enabled for. return; } // Throwing an exception here would crash the process if (eventData.EventName != "EventCounters" || eventData.Payload?.Count != 1 || eventData.Payload[0] is not IDictionary counters || !counters.TryGetValue("Name", out var nameObject) || nameObject is not string name || !(counters.TryGetValue("Mean", out var valueObj) || counters.TryGetValue("Increment", out valueObj)) || valueObj is not double value) { _logger.LogDebug("Failed to parse EventCounters event from {EventSourceName}", EventSourceName); return; } var metrics = _currentMetrics ??= new(); if (!TrySaveMetric(metrics, name, value)) { return; } if (++_metricsCount == NumberOfMetrics) { _metricsCount = 0; var previous = _previousMetrics; _previousMetrics = metrics; _currentMetrics = null; if (previous is null) { return; } if (_metricsConsumers is IMetricsConsumer[] consumers) { foreach (var consumer in consumers) { try { consumer.OnMetrics(previous, metrics); } catch (Exception ex) { _logger.LogError(ex, "Uncaught exception occurred while processing metrics for EventSource {EventSourceName}", EventSourceName); } } } } } else if (eventData.EventId == 0) { _logger.LogError("Received an error message from EventSource {EventSourceName}: {Message}", EventSourceName, eventData.Message); } else { _logger.LogDebug("Received an unknown event from EventSource {EventSourceName}: {EventId}", EventSourceName, eventData.EventId); } } public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask; public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; } ================================================ FILE: src/TelemetryConsumption/Forwarder/ForwarderEventListenerService.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.Diagnostics.Tracing; using Microsoft.Extensions.Logging; using Yarp.ReverseProxy.Forwarder; namespace Yarp.Telemetry.Consumption; internal sealed class ForwarderEventListenerService : EventListenerService { protected override string EventSourceName => "Yarp.ReverseProxy"; protected override int NumberOfMetrics => 4; public ForwarderEventListenerService(ILogger logger, IEnumerable telemetryConsumers, IEnumerable> metricsConsumers) : base(logger, telemetryConsumers, metricsConsumers) { } protected override void OnEvent(IForwarderTelemetryConsumer[] consumers, EventWrittenEventArgs eventData) { #pragma warning disable IDE0007 // Use implicit type // Explicit type here to drop the object? signature of payload elements ReadOnlyCollection payload = eventData.Payload!; #pragma warning restore IDE0007 // Use implicit type switch (eventData.EventId) { case 1: Debug.Assert(eventData.EventName == "ForwarderStart" && payload.Count == 1); { var destinationPrefix = (string)payload[0]; foreach (var consumer in consumers) { consumer.OnForwarderStart(eventData.TimeStamp, destinationPrefix); } } break; case 2: Debug.Assert(eventData.EventName == "ForwarderStop" && payload.Count == 1); { var statusCode = (int)payload[0]; foreach (var consumer in consumers) { consumer.OnForwarderStop(eventData.TimeStamp, statusCode); } } break; case 3: Debug.Assert(eventData.EventName == "ForwarderFailed" && payload.Count == 1); { var error = (ForwarderError)payload[0]; foreach (var consumer in consumers) { consumer.OnForwarderFailed(eventData.TimeStamp, error); } } break; case 4: Debug.Assert(eventData.EventName == "ForwarderStage" && payload.Count == 1); { var proxyStage = (ForwarderStage)payload[0]; foreach (var consumer in consumers) { consumer.OnForwarderStage(eventData.TimeStamp, proxyStage); } } break; case 5: Debug.Assert(eventData.EventName == "ContentTransferring" && payload.Count == 5); { var isRequest = (bool)payload[0]; var contentLength = (long)payload[1]; var iops = (long)payload[2]; var readTime = new TimeSpan((long)payload[3]); var writeTime = new TimeSpan((long)payload[4]); foreach (var consumer in consumers) { consumer.OnContentTransferring(eventData.TimeStamp, isRequest, contentLength, iops, readTime, writeTime); } } break; case 6: Debug.Assert(eventData.EventName == "ContentTransferred" && payload.Count == 6); { var isRequest = (bool)payload[0]; var contentLength = (long)payload[1]; var iops = (long)payload[2]; var readTime = new TimeSpan((long)payload[3]); var writeTime = new TimeSpan((long)payload[4]); var firstReadTime = new TimeSpan((long)payload[5]); foreach (var consumer in consumers) { consumer.OnContentTransferred(eventData.TimeStamp, isRequest, contentLength, iops, readTime, writeTime, firstReadTime); } } break; case 7: Debug.Assert(eventData.EventName == "ForwarderInvoke" && payload.Count == 3); { var clusterId = (string)payload[0]; var routeId = (string)payload[1]; var destinationId = (string)payload[2]; foreach (var consumer in consumers) { consumer.OnForwarderInvoke(eventData.TimeStamp, clusterId, routeId, destinationId); } } break; } } protected override bool TrySaveMetric(ForwarderMetrics metrics, string name, double value) { var longValue = (long)value; switch (name) { case "requests-started": metrics.RequestsStarted = longValue; break; case "requests-started-rate": metrics.RequestsStartedRate = longValue; break; case "requests-failed": metrics.RequestsFailed = longValue; break; case "current-requests": metrics.CurrentRequests = longValue; break; default: return false; } return true; } } ================================================ FILE: src/TelemetryConsumption/Forwarder/ForwarderMetrics.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; namespace Yarp.Telemetry.Consumption; /// /// Represents metrics reported by the Yarp.ReverseProxy event counters. /// public sealed class ForwarderMetrics { public ForwarderMetrics() => Timestamp = DateTime.UtcNow; /// /// Timestamp of when this instance was created. /// public DateTime Timestamp { get; internal set; } /// /// Number of proxy requests started since telemetry was enabled. /// public long RequestsStarted { get; internal set; } /// /// Number of proxy requests started in the last metrics interval. /// public long RequestsStartedRate { get; internal set; } /// /// Number of proxy requests that failed since telemetry was enabled. /// public long RequestsFailed { get; internal set; } /// /// Number of active proxy requests that have started but not yet completed or failed. /// public long CurrentRequests { get; internal set; } } ================================================ FILE: src/TelemetryConsumption/Forwarder/ForwarderStage.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. namespace Yarp.Telemetry.Consumption; /// /// Stages of forwarding a request. /// public enum ForwarderStage : int { SendAsyncStart = 1, SendAsyncStop, RequestContentTransferStart, ResponseContentTransferStart, ResponseUpgrade, } ================================================ FILE: src/TelemetryConsumption/Forwarder/IForwarderTelemetryConsumer.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using Yarp.ReverseProxy.Forwarder; namespace Yarp.Telemetry.Consumption; /// /// A consumer of Yarp.ReverseProxy EventSource events. /// public interface IForwarderTelemetryConsumer { /// /// Called before forwarding a request. /// /// Timestamp when the event was fired. /// void OnForwarderStart(DateTime timestamp, string destinationPrefix) { } /// /// Called after forwarding a request. /// /// Timestamp when the event was fired. /// The status code returned in the response. void OnForwarderStop(DateTime timestamp, int statusCode) { } /// /// Called before if forwarding the request failed. /// /// Timestamp when the event was fired. /// information for the forwarding failure. void OnForwarderFailed(DateTime timestamp, ForwarderError error) { } /// /// Called when reaching a given stage of forwarding a request. /// /// Timestamp when the event was fired. /// Stage of the forwarding operation. void OnForwarderStage(DateTime timestamp, ForwarderStage stage) { } /// /// Called periodically while a content transfer is active. /// /// Timestamp when the event was fired. /// Indicates whether we are transferring the content from the client to the backend or vice-versa. /// Number of bytes transferred. /// Number of read/write pairs performed. /// Time spent reading from the source. /// Time spent writing to the destination. void OnContentTransferring(DateTime timestamp, bool isRequest, long contentLength, long iops, TimeSpan readTime, TimeSpan writeTime) { } /// /// Called after transferring the request or response content. /// /// Timestamp when the event was fired. /// Indicates whether we transferred the content from the client to the backend or vice-versa. /// Number of bytes transferred. /// Number of read/write pairs performed. /// Time spent reading from the source. /// Time spent writing to the destination. /// Time spent on the first read of the source. void OnContentTransferred(DateTime timestamp, bool isRequest, long contentLength, long iops, TimeSpan readTime, TimeSpan writeTime, TimeSpan firstReadTime) { } /// /// Called before forwarding a request. /// /// Timestamp when the event was fired. /// Cluster ID /// Route ID /// Destination ID void OnForwarderInvoke(DateTime timestamp, string clusterId, string routeId, string destinationId) { } } ================================================ FILE: src/TelemetryConsumption/Http/HttpEventListenerService.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.Diagnostics.Tracing; using System.Net.Http; using Microsoft.Extensions.Logging; namespace Yarp.Telemetry.Consumption; internal sealed class HttpEventListenerService : EventListenerService { protected override string EventSourceName => "System.Net.Http"; protected override int NumberOfMetrics => 11; public HttpEventListenerService(ILogger logger, IEnumerable telemetryConsumers, IEnumerable> metricsConsumers) : base(logger, telemetryConsumers, metricsConsumers) { } protected override void OnEvent(IHttpTelemetryConsumer[] consumers, EventWrittenEventArgs eventData) { #pragma warning disable IDE0007 // Use implicit type // Explicit type here to drop the object? signature of payload elements ReadOnlyCollection payload = eventData.Payload!; #pragma warning restore IDE0007 // Use implicit type switch (eventData.EventId) { case 1: Debug.Assert(eventData.EventName == "RequestStart" && payload.Count == 7); { var scheme = (string)payload[0]; var host = (string)payload[1]; var port = (int)payload[2]; var pathAndQuery = (string)payload[3]; var versionMajor = (int)(byte)payload[4]; var versionMinor = (int)(byte)payload[5]; var versionPolicy = (HttpVersionPolicy)payload[6]; foreach (var consumer in consumers) { consumer.OnRequestStart(eventData.TimeStamp, scheme, host, port, pathAndQuery, versionMajor, versionMinor, versionPolicy); } } break; case 2: Debug.Assert(eventData.EventName == "RequestStop" && payload.Count == (eventData.Version == 0 ? 0 : 1)); { var statusCode = (int)payload[0]; foreach (var consumer in consumers) { consumer.OnRequestStop(eventData.TimeStamp, statusCode); } } break; case 3: Debug.Assert(eventData.EventName == "RequestFailed" && payload.Count == (eventData.Version == 0 ? 0 : 1)); { var exceptionMessage = (string)payload[0]; foreach (var consumer in consumers) { consumer.OnRequestFailed(eventData.TimeStamp, exceptionMessage); } } break; case 4: Debug.Assert(eventData.EventName == "ConnectionEstablished" && payload.Count == (eventData.Version == 0 ? 2 : 7)); { var versionMajor = (int)(byte)payload[0]; var versionMinor = (int)(byte)payload[1]; var connectionId = (long)payload[2]; var scheme = (string)payload[3]; var host = (string)payload[4]; var port = (int)payload[5]; var remoteAddress = (string?)payload[6]; foreach (var consumer in consumers) { consumer.OnConnectionEstablished(eventData.TimeStamp, versionMajor, versionMinor, connectionId, scheme, host, port, remoteAddress); } } break; case 5: Debug.Assert(eventData.EventName == "ConnectionClosed" && payload.Count == (eventData.Version == 0 ? 2 : 3)); { var versionMajor = (int)(byte)payload[0]; var versionMinor = (int)(byte)payload[1]; var connectionId = (long)payload[2]; foreach (var consumer in consumers) { consumer.OnConnectionClosed(eventData.TimeStamp, versionMajor, versionMinor, connectionId); } } break; case 6: Debug.Assert(eventData.EventName == "RequestLeftQueue" && payload.Count == 3); { var timeOnQueue = TimeSpan.FromMilliseconds((double)payload[0]); var versionMajor = (int)(byte)payload[1]; var versionMinor = (int)(byte)payload[2]; foreach (var consumer in consumers) { consumer.OnRequestLeftQueue(eventData.TimeStamp, timeOnQueue, versionMajor, versionMinor); } } break; case 7: Debug.Assert(eventData.EventName == "RequestHeadersStart" && payload.Count == (eventData.Version == 0 ? 0 : 1)); { var connectionId = (long)payload[0]; foreach (var consumer in consumers) { consumer.OnRequestHeadersStart(eventData.TimeStamp, connectionId); } } break; case 8: Debug.Assert(eventData.EventName == "RequestHeadersStop" && payload.Count == 0); { foreach (var consumer in consumers) { consumer.OnRequestHeadersStop(eventData.TimeStamp); } } break; case 9: Debug.Assert(eventData.EventName == "RequestContentStart" && payload.Count == 0); { foreach (var consumer in consumers) { consumer.OnRequestContentStart(eventData.TimeStamp); } } break; case 10: Debug.Assert(eventData.EventName == "RequestContentStop" && payload.Count == 1); { var contentLength = (long)payload[0]; foreach (var consumer in consumers) { consumer.OnRequestContentStop(eventData.TimeStamp, contentLength); } } break; case 11: Debug.Assert(eventData.EventName == "ResponseHeadersStart" && payload.Count == 0); { foreach (var consumer in consumers) { consumer.OnResponseHeadersStart(eventData.TimeStamp); } } break; case 12: Debug.Assert(eventData.EventName == "ResponseHeadersStop" && payload.Count == (eventData.Version == 0 ? 0 : 1)); { var statusCode = (int)payload[0]; foreach (var consumer in consumers) { consumer.OnResponseHeadersStop(eventData.TimeStamp, statusCode); } } break; case 13: Debug.Assert(eventData.EventName == "ResponseContentStart" && payload.Count == 0); { foreach (var consumer in consumers) { consumer.OnResponseContentStart(eventData.TimeStamp); } } break; case 14: Debug.Assert(eventData.EventName == "ResponseContentStop" && payload.Count == 0); { foreach (var consumer in consumers) { consumer.OnResponseContentStop(eventData.TimeStamp); } } break; case 15: Debug.Assert(eventData.EventName == "RequestFailedDetailed" && payload.Count == 1); // This event is more expensive to collect and requires an opt-in keyword. // We should only see it if a different EventListener opted in (potentially from a different process). break; case 16: Debug.Assert(eventData.EventName == "Redirect" && payload.Count == 1); { var redirectUri = (string)payload[0]; foreach (var consumer in consumers) { consumer.OnRedirect(eventData.TimeStamp, redirectUri); } } break; } } protected override bool TrySaveMetric(HttpMetrics metrics, string name, double value) { switch (name) { case "requests-started": metrics.RequestsStarted = (long)value; break; case "requests-started-rate": metrics.RequestsStartedRate = (long)value; break; case "requests-failed": metrics.RequestsFailed = (long)value; break; case "requests-failed-rate": metrics.RequestsFailedRate = (long)value; break; case "current-requests": metrics.CurrentRequests = (long)value; break; case "http11-connections-current-total": metrics.CurrentHttp11Connections = (long)value; break; case "http20-connections-current-total": metrics.CurrentHttp20Connections = (long)value; break; case "http11-requests-queue-duration": metrics.Http11RequestsQueueDuration = TimeSpan.FromMilliseconds(value); break; case "http20-requests-queue-duration": metrics.Http20RequestsQueueDuration = TimeSpan.FromMilliseconds(value); break; case "http30-connections-current-total": metrics.CurrentHttp30Connections = (long)value; break; case "http30-requests-queue-duration": metrics.Http30RequestsQueueDuration = TimeSpan.FromMilliseconds(value); break; default: return false; } return true; } } ================================================ FILE: src/TelemetryConsumption/Http/HttpMetrics.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; namespace Yarp.Telemetry.Consumption; /// /// Represents metrics reported by the System.Net.Http event counters. /// public sealed class HttpMetrics { public HttpMetrics() => Timestamp = DateTime.UtcNow; /// /// Timestamp of when this instance was created. /// public DateTime Timestamp { get; internal set; } /// /// Number of HTTP requests started since telemetry was enabled. /// public long RequestsStarted { get; internal set; } /// /// Number of HTTP requests started in the last metrics interval. /// public long RequestsStartedRate { get; internal set; } /// /// Number of HTTP requests that failed since telemetry was enabled. /// public long RequestsFailed { get; internal set; } /// /// Number of HTTP requests that failed in the last metrics interval. /// public long RequestsFailedRate { get; internal set; } /// /// Number of active HTTP requests that have started but not yet completed or failed. /// public long CurrentRequests { get; internal set; } /// /// Number of currently open HTTP 1.1 connections. /// public long CurrentHttp11Connections { get; internal set; } /// /// Number of currently open HTTP 2.0 connections. /// public long CurrentHttp20Connections { get; internal set; } /// /// Average time spent on queue for HTTP 1.1 requests that hit the MaxConnectionsPerServer limit in the last metrics interval. /// public TimeSpan Http11RequestsQueueDuration { get; internal set; } /// /// Average time spent on queue for HTTP 2.0 requests that hit the MAX_CONCURRENT_STREAMS limit on the connection in the last metrics interval. /// public TimeSpan Http20RequestsQueueDuration { get; internal set; } /// /// Number of currently open HTTP 3.0 connections. /// public long CurrentHttp30Connections { get; internal set; } /// /// Average time spent on queue for HTTP 3.0 requests in the last metrics interval. /// public TimeSpan Http30RequestsQueueDuration { get; internal set; } } ================================================ FILE: src/TelemetryConsumption/Http/IHttpTelemetryConsumer.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Net.Http; namespace Yarp.Telemetry.Consumption; /// /// A consumer of System.Net.Http EventSource events. /// public interface IHttpTelemetryConsumer { /// /// Called before an HTTP request. /// /// Timestamp when the event was fired. /// Scheme of the request Uri. /// Host of the request Uri. /// Port of the request Uri. /// Path and query of the request Uri. /// Major component of the request's HTTP version. /// Minor component of the request's HTTP version. /// of the request. void OnRequestStart(DateTime timestamp, string scheme, string host, int port, string pathAndQuery, int versionMajor, int versionMinor, HttpVersionPolicy versionPolicy) { } /// /// Called after an HTTP request. /// /// Timestamp when the event was fired. void OnRequestStop(DateTime timestamp) { } /// /// Called before if the request failed. /// /// Timestamp when the event was fired. void OnRequestFailed(DateTime timestamp) { } /// /// Called when a new HTTP connection is established. /// /// Timestamp when the event was fired. /// Major component of the connection's HTTP version. /// Minor component of the connection's HTTP version. void OnConnectionEstablished(DateTime timestamp, int versionMajor, int versionMinor) { } /// /// Called when a new HTTP connection is closed. /// /// Timestamp when the event was fired. /// Major component of the connection's HTTP version. /// Minor component of the connection's HTTP version. void OnConnectionClosed(DateTime timestamp, int versionMajor, int versionMinor) { } /// /// Called when a request that hit the MaxConnectionsPerServer or MAX_CONCURRENT_STREAMS limit leaves the queue. /// /// Timestamp when the event was fired. /// Time spent on queue. /// Major component of the request's HTTP version. /// Minor component of the request's HTTP version. void OnRequestLeftQueue(DateTime timestamp, TimeSpan timeOnQueue, int versionMajor, int versionMinor) { } /// /// Called before sending the request headers. /// /// Timestamp when the event was fired. void OnRequestHeadersStart(DateTime timestamp) { } /// /// Called after sending the request headers. /// /// Timestamp when the event was fired. void OnRequestHeadersStop(DateTime timestamp) { } /// /// Called before sending the request content. /// /// Timestamp when the event was fired. void OnRequestContentStart(DateTime timestamp) { } /// /// Called after sending the request content. /// /// Timestamp when the event was fired. /// void OnRequestContentStop(DateTime timestamp, long contentLength) { } /// /// Called before reading the response headers. /// /// Timestamp when the event was fired. void OnResponseHeadersStart(DateTime timestamp) { } /// /// Called after reading all response headers. /// /// Timestamp when the event was fired. void OnResponseHeadersStop(DateTime timestamp) { } /// /// Called when starts buffering the response content. /// This event WILL NOT be called for requests made by YARP, as they are not buffered. /// /// Timestamp when the event was fired. void OnResponseContentStart(DateTime timestamp) { } /// /// Called when stops buffering the response content. /// This event WILL NOT be called for requests made by YARP, as they are not buffered. /// /// Timestamp when the event was fired. void OnResponseContentStop(DateTime timestamp) { } /// /// Called after an HTTP request. /// /// Timestamp when the event was fired. /// The status code returned by the server. -1 if no response was received. void OnRequestStop(DateTime timestamp, int statusCode) => OnRequestStop(timestamp); /// /// Called before if the request failed. /// /// Timestamp when the event was fired. /// A message that describes the exception associated with this request failure. void OnRequestFailed(DateTime timestamp, string exceptionMessage) => OnRequestFailed(timestamp); /// /// Called when a new HTTP connection is established. /// /// Timestamp when the event was fired. /// Major component of the connection's HTTP version. /// Minor component of the connection's HTTP version. /// ID of the connection that was established, unique for this process. /// Scheme the connection was established with. /// Host the connection was established to. /// Port the connection was established to. /// The remote address this connection was established to, if available. void OnConnectionEstablished(DateTime timestamp, int versionMajor, int versionMinor, long connectionId, string scheme, string host, int port, string? remoteAddress) => OnConnectionEstablished(timestamp, versionMajor, versionMinor); /// /// Called when a new HTTP connection is closed. /// /// Timestamp when the event was fired. /// Major component of the connection's HTTP version. /// Minor component of the connection's HTTP version. /// ID of the connection that was closed. void OnConnectionClosed(DateTime timestamp, int versionMajor, int versionMinor, long connectionId) => OnConnectionClosed(timestamp, versionMajor, versionMinor); /// /// Called before sending the request headers. /// /// Timestamp when the event was fired. /// ID of the connection we are sending this request on. void OnRequestHeadersStart(DateTime timestamp, long connectionId) => OnRequestHeadersStart(timestamp); /// /// Called after reading all response headers. /// /// Timestamp when the event was fired. /// The status code returned by the server. void OnResponseHeadersStop(DateTime timestamp, int statusCode) => OnResponseHeadersStop(timestamp); /// /// Called before a request is redirected if is enabled. /// /// Timestamp when the event was fired. /// The uri the request is being redirected to. void OnRedirect(DateTime timestamp, string redirectUri) { } } ================================================ FILE: src/TelemetryConsumption/IMetricsConsumer.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. namespace Yarp.Telemetry.Consumption; /// /// A consumer of . /// public interface IMetricsConsumer { /// /// Processes from the last event counter interval. /// /// collected in the previous interval. /// collected in the last interval. void OnMetrics(TMetrics previous, TMetrics current); } ================================================ FILE: src/TelemetryConsumption/Kestrel/IKestrelTelemetryConsumer.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; namespace Yarp.Telemetry.Consumption; /// /// A consumer of Microsoft-AspNetCore-Server-Kestrel EventSource events. /// public interface IKestrelTelemetryConsumer { /// /// Called at the start of a connection. /// /// Timestamp when the event was fired. /// ID of the connection. /// Local endpoint for the connection. /// Remote endpoint for the connection. void OnConnectionStart(DateTime timestamp, string connectionId, string? localEndPoint, string? remoteEndPoint) { } /// /// Called at the end of a connection. /// /// Timestamp when the event was fired. /// ID of the connection. void OnConnectionStop(DateTime timestamp, string connectionId) { } /// /// Called at the start of a request. /// /// Timestamp when the event was fired. /// ID of the connection. /// ID of the request. /// HTTP version of the request. /// Path of the request. /// HTTP method of the request. void OnRequestStart(DateTime timestamp, string connectionId, string requestId, string httpVersion, string path, string method) { } /// /// Called at the end of a request. /// /// Timestamp when the event was fired. /// ID of the connection. /// ID of the request. /// HTTP version of the request. /// Path of the request. /// HTTP method of the request. void OnRequestStop(DateTime timestamp, string connectionId, string requestId, string httpVersion, string path, string method) { } } ================================================ FILE: src/TelemetryConsumption/Kestrel/KestrelEventListenerService.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.Diagnostics.Tracing; using Microsoft.Extensions.Logging; namespace Yarp.Telemetry.Consumption; internal sealed class KestrelEventListenerService : EventListenerService { protected override string EventSourceName => "Microsoft-AspNetCore-Server-Kestrel"; protected override int NumberOfMetrics => 10; public KestrelEventListenerService(ILogger logger, IEnumerable telemetryConsumers, IEnumerable> metricsConsumers) : base(logger, telemetryConsumers, metricsConsumers) { } protected override void OnEvent(IKestrelTelemetryConsumer[] consumers, EventWrittenEventArgs eventData) { #pragma warning disable IDE0007 // Use implicit type // Explicit type here to drop the object? signature of payload elements ReadOnlyCollection payload = eventData.Payload!; #pragma warning restore IDE0007 // Use implicit type switch (eventData.EventId) { case 1: Debug.Assert(eventData.EventName == "ConnectionStart" && payload.Count == 3); { var connectionId = (string)payload[0]; var localEndPoint = (string?)payload[1]; var remoteEndPoint = (string?)payload[2]; foreach (var consumer in consumers) { consumer.OnConnectionStart(eventData.TimeStamp, connectionId, localEndPoint, remoteEndPoint); } } break; case 2: Debug.Assert(eventData.EventName == "ConnectionStop" && payload.Count == 1); { var connectionId = (string)payload[0]; foreach (var consumer in consumers) { consumer.OnConnectionStop(eventData.TimeStamp, connectionId); } } break; case 3: Debug.Assert(eventData.EventName == "RequestStart" && payload.Count == 5); { var connectionId = (string)payload[0]; var requestId = (string)payload[1]; var httpVersion = (string)payload[2]; var path = (string)payload[3]; var method = (string)payload[4]; foreach (var consumer in consumers) { consumer.OnRequestStart(eventData.TimeStamp, connectionId, requestId, httpVersion, path, method); } } break; case 4: Debug.Assert(eventData.EventName == "RequestStop" && payload.Count == 5); { var connectionId = (string)payload[0]; var requestId = (string)payload[1]; var httpVersion = (string)payload[2]; var path = (string)payload[3]; var method = (string)payload[4]; foreach (var consumer in consumers) { consumer.OnRequestStop(eventData.TimeStamp, connectionId, requestId, httpVersion, path, method); } } break; } } protected override bool TrySaveMetric(KestrelMetrics metrics, string name, double value) { var longValue = (long)value; switch (name) { case "connections-per-second": metrics.ConnectionRate = longValue; break; case "total-connections": metrics.TotalConnections = longValue; break; case "tls-handshakes-per-second": metrics.TlsHandshakeRate = longValue; break; case "total-tls-handshakes": metrics.TotalTlsHandshakes = longValue; break; case "current-tls-handshakes": metrics.CurrentTlsHandshakes = longValue; break; case "failed-tls-handshakes": metrics.FailedTlsHandshakes = longValue; break; case "current-connections": metrics.CurrentConnections = longValue; break; case "connection-queue-length": metrics.ConnectionQueueLength = longValue; break; case "request-queue-length": metrics.RequestQueueLength = longValue; break; case "current-upgraded-requests": metrics.CurrentUpgradedRequests = longValue; break; default: return false; } return true; } } ================================================ FILE: src/TelemetryConsumption/Kestrel/KestrelMetrics.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; namespace Yarp.Telemetry.Consumption; /// /// Represents metrics reported by the Microsoft-AspNetCore-Server-Kestrel event counters. /// public sealed class KestrelMetrics { public KestrelMetrics() => Timestamp = DateTime.UtcNow; /// /// Timestamp of when this instance was created. /// public DateTime Timestamp { get; internal set; } /// /// Number of connections opened in the last metrics interval. /// public long ConnectionRate { get; internal set; } /// /// Number of connections opened since telemetry was enabled. /// public long TotalConnections { get; internal set; } /// /// Number of TLS handshakes started in the last metrics interval. /// public long TlsHandshakeRate { get; internal set; } /// /// Number of TLS handshakes started since telemetry was enabled. /// public long TotalTlsHandshakes { get; internal set; } /// /// Number of active TLS handshakes that have started but not yet completed or failed. /// public long CurrentTlsHandshakes { get; internal set; } /// /// Number of TLS handshakes that failed since telemetry was enabled. /// public long FailedTlsHandshakes { get; internal set; } /// /// Number of currently open connections. /// public long CurrentConnections { get; internal set; } /// /// Number of connections on the queue. /// public long ConnectionQueueLength { get; internal set; } /// /// Number of requests on the queue. /// public long RequestQueueLength { get; internal set; } /// /// Number of currently upgraded requests (number of webSocket connections). /// public long CurrentUpgradedRequests { get; internal set; } } ================================================ FILE: src/TelemetryConsumption/MetricsOptions.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; namespace Yarp.Telemetry.Consumption; internal static class MetricsOptions { // TODO: Should this be publicly configurable? It's currently only visible to tests to reduce execution time public static TimeSpan Interval { get; set; } = TimeSpan.FromSeconds(1); } ================================================ FILE: src/TelemetryConsumption/NameResolution/INameResolutionTelemetryConsumer.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; namespace Yarp.Telemetry.Consumption; /// /// A consumer of System.Net.NameResolution EventSource events. /// public interface INameResolutionTelemetryConsumer { /// /// Called before a name resolution. /// /// Timestamp when the event was fired. /// Host name or address we are resolving. void OnResolutionStart(DateTime timestamp, string hostNameOrAddress) { } /// /// Called after a name resolution. /// /// Timestamp when the event was fired. void OnResolutionStop(DateTime timestamp) { } /// /// Called before if the name resolution failed. /// /// Timestamp when the event was fired. void OnResolutionFailed(DateTime timestamp) { } } ================================================ FILE: src/TelemetryConsumption/NameResolution/NameResolutionEventListenerService.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.Diagnostics.Tracing; using Microsoft.Extensions.Logging; namespace Yarp.Telemetry.Consumption; internal sealed class NameResolutionEventListenerService : EventListenerService { protected override string EventSourceName => "System.Net.NameResolution"; protected override int NumberOfMetrics => 3; public NameResolutionEventListenerService(ILogger logger, IEnumerable telemetryConsumers, IEnumerable> metricsConsumers) : base(logger, telemetryConsumers, metricsConsumers) { } protected override void OnEvent(INameResolutionTelemetryConsumer[] consumers, EventWrittenEventArgs eventData) { #pragma warning disable IDE0007 // Use implicit type // Explicit type here to drop the object? signature of payload elements ReadOnlyCollection payload = eventData.Payload!; #pragma warning restore IDE0007 // Use implicit type switch (eventData.EventId) { case 1: Debug.Assert(eventData.EventName == "ResolutionStart" && payload.Count == 1); { var hostNameOrAddress = (string)payload[0]; foreach (var consumer in consumers) { consumer.OnResolutionStart(eventData.TimeStamp, hostNameOrAddress); } } break; case 2: Debug.Assert(eventData.EventName == "ResolutionStop" && payload.Count == 0); { foreach (var consumer in consumers) { consumer.OnResolutionStop(eventData.TimeStamp); } } break; case 3: Debug.Assert(eventData.EventName == "ResolutionFailed" && payload.Count == 0); { foreach (var consumer in consumers) { consumer.OnResolutionFailed(eventData.TimeStamp); } } break; } } protected override bool TrySaveMetric(NameResolutionMetrics metrics, string name, double value) { switch (name) { case "dns-lookups-requested": metrics.DnsLookupsRequested = (long)value; break; case "dns-lookups-duration": metrics.AverageLookupDuration = TimeSpan.FromMilliseconds(value); break; case "current-dns-lookups": metrics.CurrentDnsLookups = (long)value; break; default: return false; } return true; } } ================================================ FILE: src/TelemetryConsumption/NameResolution/NameResolutionMetrics.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; namespace Yarp.Telemetry.Consumption; /// /// Represents metrics reported by the System.Net.NameResolution event counters. /// public sealed class NameResolutionMetrics { public NameResolutionMetrics() => Timestamp = DateTime.UtcNow; /// /// Timestamp of when this instance was created. /// public DateTime Timestamp { get; internal set; } /// /// Number of DNS lookups requested since telemetry was enabled. /// public long DnsLookupsRequested { get; internal set; } /// /// Average DNS lookup duration in the last metrics interval. /// public TimeSpan AverageLookupDuration { get; internal set; } /// /// Number of DNS lookups that have started but not yet completed or failed. /// public long CurrentDnsLookups { get; internal set; } } ================================================ FILE: src/TelemetryConsumption/NetSecurity/INetSecurityTelemetryConsumer.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Security.Authentication; namespace Yarp.Telemetry.Consumption; /// /// A consumer of System.Net.Security EventSource events. /// public interface INetSecurityTelemetryConsumer { /// /// Called before a handshake. /// /// Timestamp when the event was fired. /// Indicates whether we are authenticating as the server. /// Name of the host we are authenticating with. void OnHandshakeStart(DateTime timestamp, bool isServer, string targetHost) { } /// /// Called after a handshake. /// /// Timestamp when the event was fired. /// The protocol established by the handshake. void OnHandshakeStop(DateTime timestamp, SslProtocols protocol) { } /// /// Called before if the handshake failed. /// /// Timestamp when the event was fired. /// Indicates whether we were authenticating as the server. /// Time elapsed since the start of the handshake. /// Exception information for the handshake failure. void OnHandshakeFailed(DateTime timestamp, bool isServer, TimeSpan elapsed, string exceptionMessage) { } } ================================================ FILE: src/TelemetryConsumption/NetSecurity/NetSecurityEventListenerService.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.Diagnostics.Tracing; using System.Security.Authentication; using Microsoft.Extensions.Logging; namespace Yarp.Telemetry.Consumption; internal sealed class NetSecurityEventListenerService : EventListenerService { protected override string EventSourceName => "System.Net.Security"; protected override int NumberOfMetrics => 14; public NetSecurityEventListenerService(ILogger logger, IEnumerable telemetryConsumers, IEnumerable> metricsConsumers) : base(logger, telemetryConsumers, metricsConsumers) { } protected override void OnEvent(INetSecurityTelemetryConsumer[] consumers, EventWrittenEventArgs eventData) { #pragma warning disable IDE0007 // Use implicit type // Explicit type here to drop the object? signature of payload elements ReadOnlyCollection payload = eventData.Payload!; #pragma warning restore IDE0007 // Use implicit type switch (eventData.EventId) { case 1: Debug.Assert(eventData.EventName == "HandshakeStart" && payload.Count == 2); { var isServer = (bool)payload[0]; var targetHost = (string)payload[1]; foreach (var consumer in consumers) { consumer.OnHandshakeStart(eventData.TimeStamp, isServer, targetHost); } } break; case 2: Debug.Assert(eventData.EventName == "HandshakeStop" && payload.Count == 1); { var protocol = (SslProtocols)payload[0]; foreach (var consumer in consumers) { consumer.OnHandshakeStop(eventData.TimeStamp, protocol); } } break; case 3: Debug.Assert(eventData.EventName == "HandshakeFailed" && payload.Count == 3); { var isServer = (bool)payload[0]; var elapsed = TimeSpan.FromMilliseconds((double)payload[1]); var exceptionMessage = (string)payload[2]; foreach (var consumer in consumers) { consumer.OnHandshakeFailed(eventData.TimeStamp, isServer, elapsed, exceptionMessage); } } break; } } protected override bool TrySaveMetric(NetSecurityMetrics metrics, string name, double value) { switch (name) { case "tls-handshake-rate": metrics.TlsHandshakeRate = (long)value; break; case "total-tls-handshakes": metrics.TotalTlsHandshakes = (long)value; break; case "current-tls-handshakes": metrics.CurrentTlsHandshakes = (long)value; break; case "failed-tls-handshakes": metrics.FailedTlsHandshakes = (long)value; break; case "all-tls-sessions-open": metrics.TlsSessionsOpen = (long)value; break; case "tls10-sessions-open": metrics.Tls10SessionsOpen = (long)value; break; case "tls11-sessions-open": metrics.Tls11SessionsOpen = (long)value; break; case "tls12-sessions-open": metrics.Tls12SessionsOpen = (long)value; break; case "tls13-sessions-open": metrics.Tls13SessionsOpen = (long)value; break; case "all-tls-handshake-duration": metrics.TlsHandshakeDuration = TimeSpan.FromMilliseconds(value); break; case "tls10-handshake-duration": metrics.Tls10HandshakeDuration = TimeSpan.FromMilliseconds(value); break; case "tls11-handshake-duration": metrics.Tls11HandshakeDuration = TimeSpan.FromMilliseconds(value); break; case "tls12-handshake-duration": metrics.Tls12HandshakeDuration = TimeSpan.FromMilliseconds(value); break; case "tls13-handshake-duration": metrics.Tls13HandshakeDuration = TimeSpan.FromMilliseconds(value); break; default: return false; } return true; } } ================================================ FILE: src/TelemetryConsumption/NetSecurity/NetSecurityMetrics.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; namespace Yarp.Telemetry.Consumption; /// /// Represents metrics reported by the System.Net.Security event counters. /// public sealed class NetSecurityMetrics { public NetSecurityMetrics() => Timestamp = DateTime.UtcNow; /// /// Timestamp of when this instance was created. /// public DateTime Timestamp { get; internal set; } /// /// Number of TLS handshakes completed in the last metrics interval. /// public long TlsHandshakeRate { get; internal set; } /// /// Number of TLS handshakes completed since telemetry was enabled. /// public long TotalTlsHandshakes { get; internal set; } /// /// Number of active TLS handshakes that have started but not yet completed or failed. /// public long CurrentTlsHandshakes { get; internal set; } /// /// Number of TLS handshakes that failed since telemetry was enabled. /// public long FailedTlsHandshakes { get; internal set; } /// /// Number of currently open TLS sessions. /// public long TlsSessionsOpen { get; internal set; } /// /// Number of currently open TLS 1.0 sessions. /// public long Tls10SessionsOpen { get; internal set; } /// /// Number of currently open TLS 1.1 sessions. /// public long Tls11SessionsOpen { get; internal set; } /// /// Number of currently open TLS 1.2 sessions. /// public long Tls12SessionsOpen { get; internal set; } /// /// Number of currently open TLS 1.3 sessions. /// public long Tls13SessionsOpen { get; internal set; } /// /// Average duration of all TLS handshakes completed in the last metrics interval. /// public TimeSpan TlsHandshakeDuration { get; internal set; } /// /// Average duration of all TLS 1.0 handshakes completed in the last metrics interval. /// public TimeSpan Tls10HandshakeDuration { get; internal set; } /// /// Average duration of all TLS 1.1 handshakes completed in the last metrics interval. /// public TimeSpan Tls11HandshakeDuration { get; internal set; } /// /// Average duration of all TLS 1.2 handshakes completed in the last metrics interval. /// public TimeSpan Tls12HandshakeDuration { get; internal set; } /// /// Average duration of all TLS 1.3 handshakes completed in the last metrics interval. /// public TimeSpan Tls13HandshakeDuration { get; internal set; } } ================================================ FILE: src/TelemetryConsumption/README.md ================================================ YARP (Yet Another Reverse Proxy) is a highly customizable reverse proxy built using .NET. This package extends the base Yarp.ReverseProxy implementation to enable consuming AspNetCore, HttpClient, and YARP telemetry in process, allowing you to live monitor the performance and export the data as needed. To learn more see the docs at https://learn.microsoft.com/aspnet/core/fundamentals/servers/yarp/diagnosing-yarp-issues#using-telemetry-events and the GitHub repo at https://github.com/dotnet/yarp. ================================================ FILE: src/TelemetryConsumption/Sockets/ISocketsTelemetryConsumer.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Net.Sockets; namespace Yarp.Telemetry.Consumption; /// /// A consumer of System.Net.Sockets EventSource events. /// public interface ISocketsTelemetryConsumer { /// /// Called before a Socket connect. /// /// Timestamp when the event was fired. /// Socket address we are connecting to. void OnConnectStart(DateTime timestamp, string address) { } /// /// Called after a Socket connect. /// /// Timestamp when the event was fired. void OnConnectStop(DateTime timestamp) { } /// /// Called before if the connect failed. /// /// Timestamp when the event was fired. /// information for the connect failure. /// Exception information for the connect failure. void OnConnectFailed(DateTime timestamp, SocketError error, string exceptionMessage) { } } ================================================ FILE: src/TelemetryConsumption/Sockets/SocketsEventListenerService.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.Diagnostics.Tracing; using System.Net.Sockets; using Microsoft.Extensions.Logging; namespace Yarp.Telemetry.Consumption; internal sealed class SocketsEventListenerService : EventListenerService { protected override string EventSourceName => "System.Net.Sockets"; protected override int NumberOfMetrics => 7; public SocketsEventListenerService(ILogger logger, IEnumerable telemetryConsumers, IEnumerable> metricsConsumers) : base(logger, telemetryConsumers, metricsConsumers) { } protected override void OnEvent(ISocketsTelemetryConsumer[] consumers, EventWrittenEventArgs eventData) { #pragma warning disable IDE0007 // Use implicit type // Explicit type here to drop the object? signature of payload elements ReadOnlyCollection payload = eventData.Payload!; #pragma warning restore IDE0007 // Use implicit type switch (eventData.EventId) { case 1: Debug.Assert(eventData.EventName == "ConnectStart" && payload.Count == 1); { var address = (string)payload[0]; foreach (var consumer in consumers) { consumer.OnConnectStart(eventData.TimeStamp, address); } } break; case 2: Debug.Assert(eventData.EventName == "ConnectStop" && payload.Count == 0); { foreach (var consumer in consumers) { consumer.OnConnectStop(eventData.TimeStamp); } } break; case 3: Debug.Assert(eventData.EventName == "ConnectFailed" && payload.Count == 2); { var error = (SocketError)payload[0]; var exceptionMessage = (string)payload[1]; foreach (var consumer in consumers) { consumer.OnConnectFailed(eventData.TimeStamp, error, exceptionMessage); } } break; } } protected override bool TrySaveMetric(SocketsMetrics metrics, string name, double value) { var longValue = (long)value; switch (name) { case "outgoing-connections-established": metrics.OutgoingConnectionsEstablished = longValue; break; case "incoming-connections-established": metrics.IncomingConnectionsEstablished = longValue; break; case "bytes-received": metrics.BytesReceived = longValue; break; case "bytes-sent": metrics.BytesSent = longValue; break; case "datagrams-received": metrics.DatagramsReceived = longValue; break; case "datagrams-sent": metrics.DatagramsSent = longValue; break; case "current-outgoing-connect-attempts": metrics.CurrentOutgoingConnectAttempts = longValue; break; default: return false; } return true; } } ================================================ FILE: src/TelemetryConsumption/Sockets/SocketsMetrics.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; namespace Yarp.Telemetry.Consumption; /// /// Represents metrics reported by the System.Net.Sockets event counters. /// public sealed class SocketsMetrics { public SocketsMetrics() => Timestamp = DateTime.UtcNow; /// /// Timestamp of when this instance was created. /// public DateTime Timestamp { get; internal set; } /// /// Number of outgoing (Connect) Socket connections established since telemetry was enabled. /// public long OutgoingConnectionsEstablished { get; internal set; } /// /// Number of incoming (Accept) Socket connections established since telemetry was enabled. /// public long IncomingConnectionsEstablished { get; internal set; } /// /// Number of bytes received since telemetry was enabled. /// public long BytesReceived { get; internal set; } /// /// Number of bytes sent since telemetry was enabled. /// public long BytesSent { get; internal set; } /// /// Number of datagrams received since telemetry was enabled. /// public long DatagramsReceived { get; internal set; } /// /// Number of datagrams sent since telemetry was enabled. /// public long DatagramsSent { get; internal set; } /// /// Number of outgoing (Connect) Socket connection attempts that are currently in progress. /// public long CurrentOutgoingConnectAttempts { get; internal set; } } ================================================ FILE: src/TelemetryConsumption/TelemetryConsumptionExtensions.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.DependencyInjection.Extensions; using Yarp.Telemetry.Consumption; namespace Microsoft.Extensions.DependencyInjection; public static class TelemetryConsumptionExtensions { /// /// Registers all telemetry listeners (Forwarder, Kestrel, Http, NameResolution, NetSecurity, Sockets and WebSockets). /// public static IServiceCollection AddTelemetryListeners(this IServiceCollection services) { services.AddHostedService(); services.AddHostedService(); services.AddHostedService(); services.AddHostedService(); services.AddHostedService(); services.AddHostedService(); services.AddHostedService(); return services; } /// /// Registers a consumer singleton for every I*TelemetryConsumer interface it implements. /// public static IServiceCollection AddTelemetryConsumer(this IServiceCollection services, object consumer) { var implementsAny = false; if (consumer is IWebSocketsTelemetryConsumer webSocketsTelemetryConsumer) { services.TryAddEnumerable(ServiceDescriptor.Singleton(webSocketsTelemetryConsumer)); implementsAny = true; } if (consumer is IForwarderTelemetryConsumer forwarderTelemetryConsumer) { services.TryAddEnumerable(ServiceDescriptor.Singleton(forwarderTelemetryConsumer)); implementsAny = true; } if (consumer is IKestrelTelemetryConsumer kestrelTelemetryConsumer) { services.TryAddEnumerable(ServiceDescriptor.Singleton(kestrelTelemetryConsumer)); implementsAny = true; } if (consumer is IHttpTelemetryConsumer httpTelemetryConsumer) { services.TryAddEnumerable(ServiceDescriptor.Singleton(httpTelemetryConsumer)); implementsAny = true; } if (consumer is INameResolutionTelemetryConsumer nameResolutionTelemetryConsumer) { services.TryAddEnumerable(ServiceDescriptor.Singleton(nameResolutionTelemetryConsumer)); implementsAny = true; } if (consumer is INetSecurityTelemetryConsumer netSecurityTelemetryConsumer) { services.TryAddEnumerable(ServiceDescriptor.Singleton(netSecurityTelemetryConsumer)); implementsAny = true; } if (consumer is ISocketsTelemetryConsumer socketsTelemetryConsumer) { services.TryAddEnumerable(ServiceDescriptor.Singleton(socketsTelemetryConsumer)); implementsAny = true; } if (!implementsAny) { throw new ArgumentException("The consumer must implement at least one I*TelemetryConsumer interface.", nameof(consumer)); } services.AddTelemetryListeners(); return services; } /// /// Registers a singleton for every I*TelemetryConsumer interface it implements. /// public static IServiceCollection AddTelemetryConsumer<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TConsumer>(this IServiceCollection services) where TConsumer : class { var implementsAny = false; if (typeof(IWebSocketsTelemetryConsumer).IsAssignableFrom(typeof(TConsumer))) { services.AddSingleton(services => (IWebSocketsTelemetryConsumer)services.GetRequiredService()); implementsAny = true; } if (typeof(IForwarderTelemetryConsumer).IsAssignableFrom(typeof(TConsumer))) { services.AddSingleton(services => (IForwarderTelemetryConsumer)services.GetRequiredService()); implementsAny = true; } if (typeof(IKestrelTelemetryConsumer).IsAssignableFrom(typeof(TConsumer))) { services.AddSingleton(services => (IKestrelTelemetryConsumer)services.GetRequiredService()); implementsAny = true; } if (typeof(IHttpTelemetryConsumer).IsAssignableFrom(typeof(TConsumer))) { services.AddSingleton(services => (IHttpTelemetryConsumer)services.GetRequiredService()); implementsAny = true; } if (typeof(INameResolutionTelemetryConsumer).IsAssignableFrom(typeof(TConsumer))) { services.AddSingleton(services => (INameResolutionTelemetryConsumer)services.GetRequiredService()); implementsAny = true; } if (typeof(INetSecurityTelemetryConsumer).IsAssignableFrom(typeof(TConsumer))) { services.AddSingleton(services => (INetSecurityTelemetryConsumer)services.GetRequiredService()); implementsAny = true; } if (typeof(ISocketsTelemetryConsumer).IsAssignableFrom(typeof(TConsumer))) { services.AddSingleton(services => (ISocketsTelemetryConsumer)services.GetRequiredService()); implementsAny = true; } if (!implementsAny) { throw new ArgumentException("TConsumer must implement at least one I*TelemetryConsumer interface.", nameof(TConsumer)); } services.TryAddSingleton(); services.AddTelemetryListeners(); return services; } /// /// Registers a consumer singleton for every IMetricsConsumer interface it implements. /// public static IServiceCollection AddMetricsConsumer(this IServiceCollection services, object consumer) { var implementsAny = false; if (consumer is IMetricsConsumer forwarderMetricsConsumer) { services.TryAddEnumerable(ServiceDescriptor.Singleton(forwarderMetricsConsumer)); implementsAny = true; } if (consumer is IMetricsConsumer kestrelMetricsConsumer) { services.TryAddEnumerable(ServiceDescriptor.Singleton(kestrelMetricsConsumer)); implementsAny = true; } if (consumer is IMetricsConsumer httpMetricsConsumer) { services.TryAddEnumerable(ServiceDescriptor.Singleton(httpMetricsConsumer)); implementsAny = true; } if (consumer is IMetricsConsumer nameResolutionMetricsConsumer) { services.TryAddEnumerable(ServiceDescriptor.Singleton(nameResolutionMetricsConsumer)); implementsAny = true; } if (consumer is IMetricsConsumer netSecurityMetricsConsumer) { services.TryAddEnumerable(ServiceDescriptor.Singleton(netSecurityMetricsConsumer)); implementsAny = true; } if (consumer is IMetricsConsumer socketsMetricsConsumer) { services.TryAddEnumerable(ServiceDescriptor.Singleton(socketsMetricsConsumer)); implementsAny = true; } if (!implementsAny) { throw new ArgumentException("The consumer must implement at least one IMetricsConsumer interface.", nameof(consumer)); } services.AddTelemetryListeners(); return services; } /// /// Registers a consumer singleton for every IMetricsConsumer interface it implements. /// public static IServiceCollection AddMetricsConsumer<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TConsumer>(this IServiceCollection services) where TConsumer : class { var implementsAny = false; if (typeof(IMetricsConsumer).IsAssignableFrom(typeof(TConsumer))) { services.AddSingleton(services => (IMetricsConsumer)services.GetRequiredService()); implementsAny = true; } if (typeof(IMetricsConsumer).IsAssignableFrom(typeof(TConsumer))) { services.AddSingleton(services => (IMetricsConsumer)services.GetRequiredService()); implementsAny = true; } if (typeof(IMetricsConsumer).IsAssignableFrom(typeof(TConsumer))) { services.AddSingleton(services => (IMetricsConsumer)services.GetRequiredService()); implementsAny = true; } if (typeof(IMetricsConsumer).IsAssignableFrom(typeof(TConsumer))) { services.AddSingleton(services => (IMetricsConsumer)services.GetRequiredService()); implementsAny = true; } if (typeof(IMetricsConsumer).IsAssignableFrom(typeof(TConsumer))) { services.AddSingleton(services => (IMetricsConsumer)services.GetRequiredService()); implementsAny = true; } if (typeof(IMetricsConsumer).IsAssignableFrom(typeof(TConsumer))) { services.AddSingleton(services => (IMetricsConsumer)services.GetRequiredService()); implementsAny = true; } if (!implementsAny) { throw new ArgumentException("TConsumer must implement at least one IMetricsConsumer interface.", nameof(TConsumer)); } services.TryAddSingleton(); services.AddTelemetryListeners(); return services; } } ================================================ FILE: src/TelemetryConsumption/WebSockets/IWebSocketsTelemetryConsumer.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; namespace Yarp.Telemetry.Consumption; /// /// A consumer of Yarp.ReverseProxy.WebSockets EventSource events. /// public interface IWebSocketsTelemetryConsumer { /// /// Called when a WebSockets connection is closed. /// /// Timestamp when the event was fired. /// Timestamp when the connection upgrade completed. /// The reason the WebSocket connection closed. /// Messages read by the destination server. /// Messages sent by the destination server. void OnWebSocketClosed(DateTime timestamp, DateTime establishedTime, WebSocketCloseReason closeReason, long messagesRead, long messagesWritten); } ================================================ FILE: src/TelemetryConsumption/WebSockets/WebSocketCloseReason.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. namespace Yarp.Telemetry.Consumption; /// /// The reason the WebSocket connection closed. /// public enum WebSocketCloseReason : int { Unknown, ClientGracefulClose, ServerGracefulClose, ClientDisconnect, ServerDisconnect, ActivityTimeout, } ================================================ FILE: src/TelemetryConsumption/WebSockets/WebSocketsEventListenerService.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.Diagnostics.Tracing; using Microsoft.Extensions.Logging; namespace Yarp.Telemetry.Consumption; internal sealed class WebSocketsMetrics { } internal sealed class WebSocketsEventListenerService : EventListenerService { protected override string EventSourceName => "Yarp.ReverseProxy.WebSockets"; protected override int NumberOfMetrics => 0; public WebSocketsEventListenerService(ILogger logger, IEnumerable telemetryConsumers, IEnumerable> metricsConsumers) : base(logger, telemetryConsumers, metricsConsumers) { } protected override void OnEvent(IWebSocketsTelemetryConsumer[] consumers, EventWrittenEventArgs eventData) { #pragma warning disable IDE0007 // Use implicit type // Explicit type here to drop the object? signature of payload elements ReadOnlyCollection payload = eventData.Payload!; #pragma warning restore IDE0007 // Use implicit type switch (eventData.EventId) { case 1: Debug.Assert(eventData.EventName == "WebSocketClosed" && payload.Count == 4); { var establishedTime = new DateTime((long)payload[0]); var closeReason = (WebSocketCloseReason)payload[1]; var messagesRead = (long)payload[2]; var messagesWritten = (long)payload[3]; foreach (var consumer in consumers) { consumer.OnWebSocketClosed(eventData.TimeStamp, establishedTime, closeReason, messagesRead, messagesWritten); } } break; } } protected override bool TrySaveMetric(WebSocketsMetrics metrics, string name, double value) { return false; } } ================================================ FILE: src/TelemetryConsumption/Yarp.Telemetry.Consumption.csproj ================================================ Yarp.ReverseProxy extension package for in-process telemetry consumption $(ReleaseTFMs) Library Yarp.Telemetry.Consumption enable true README.md yarp;dotnet;reverse-proxy;aspnetcore;telemetry ================================================ FILE: startvs.cmd ================================================ @ECHO OFF SETLOCAL :: This command launches a Visual Studio solution with environment variables required to use a local version of the .NET Core SDK. :: This tells .NET Core to use the same dotnet.exe that build scripts use SET DOTNET_ROOT=%~dp0.dotnet SET DOTNET_ROOT(x86)=%~dp0.dotnet\x86 :: This tells .NET Core not to go looking for .NET Core in other places SET DOTNET_MULTILEVEL_LOOKUP=0 :: Put our local dotnet.exe on PATH first so Visual Studio knows which one to use SET PATH=%DOTNET_ROOT%;%PATH% SET sln=%~1 IF "%sln%"=="" ( echo Solution not specified, using YARP.slnx SET sln=%~dp0YARP.slnx ) IF NOT EXIST "%DOTNET_ROOT%\dotnet.exe" ( echo .NET Core has not yet been installed. Run `%~dp0restore.cmd` to install tools exit /b 1 ) start "" "%sln%" ================================================ FILE: test/Directory.Build.props ================================================ true XUnitV3 ================================================ FILE: test/Kubernetes.Tests/Certificates/CertificateHelperTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.IO; using System.Linq; using System.Text; using Microsoft.Extensions.Logging; using Moq; using Xunit; using Yarp.Kubernetes.Tests; namespace Yarp.Kubernetes.Controller.Certificates.Tests; public class CertificateHelperTests { private readonly Mock> _mockLogger; private readonly ICertificateHelper _certificateHelper; private readonly byte[] _pemPrivateKey; private readonly byte[] _pemCert; private readonly byte[] _derPrivateKey; private readonly byte[] _derCert; public CertificateHelperTests() { _mockLogger = new Mock>(); _certificateHelper = new CertificateHelper(_mockLogger.Object); _pemCert = ReadManifestData(".Certificates.cert.pem"); _pemPrivateKey = ReadManifestData(".Certificates.key.pem"); _derCert = ReadManifestData(".Certificates.cert.der"); _derPrivateKey = ReadManifestData(".Certificates.key.der"); } [Theory] [InlineData(true, true, true)] [InlineData(false, true, false)] [InlineData(true, false, false)] [InlineData(false, false, false)] public void CertificateConversionFromPem(bool loadCert, bool loadKey, bool expectCert) { // Arrange var cert = loadCert ? _pemCert : (byte[])null; var key = loadKey ? _pemPrivateKey : (byte[])null; var secret = KubeResourceGenerator.CreateSecret("yarp-ingress-tls", "default", cert, key); var namespacedName = NamespacedName.From(secret); // Act var actualCertificate = _certificateHelper.ConvertCertificate(namespacedName, secret); // Assert if (expectCert) { Assert.NotNull(actualCertificate); } else { Assert.Null(actualCertificate); } } [Theory] [InlineData(true, true, true)] [InlineData(false, true, false)] [InlineData(true, false, false)] [InlineData(false, false, false)] public void CertificateConversionFromDer(bool loadCert, bool loadKey, bool expectCert) { // Arrange var cert = loadCert ? _derCert : (byte[])null; var key = loadKey ? _derPrivateKey : (byte[])null; var secret = KubeResourceGenerator.CreateSecret("yarp-ingress-tls", "default", cert, key); var namespacedName = NamespacedName.From(secret); // Act var actualCertificate = _certificateHelper.ConvertCertificate(namespacedName, secret); // Assert if (expectCert) { Assert.NotNull(actualCertificate); } else { Assert.Null(actualCertificate); } } private static byte[] ReadManifestData(string ending) { var assembly = typeof(CertificateHelperTests).Assembly; var resourceName = assembly.GetManifestResourceNames().Single(str => str.EndsWith(ending)); var manifestStream = assembly.GetManifestResourceStream(resourceName); using var reader = new StreamReader(manifestStream); return Encoding.UTF8.GetBytes(reader.ReadToEnd()); } } ================================================ FILE: test/Kubernetes.Tests/Certificates/cert.der ================================================ MIIDSDCCAjACCQCTqER3kAB0EDANBgkqhkiG9w0BAQQFADBmMRMwEQYDVQQKEwpN eSBDb21wYW55MRAwDgYDVQQHEwdNeSBUb3duMRwwGgYDVQQIExNTdGF0ZSBvciBQ cm92aWRlbmNlMQswCQYDVQQGEwJVUzESMBAGA1UEAxMJbG9jYWxob3N0MB4XDTIy MDUyMjA0MDI0M1oXDTMyMDUxOTA0MDI0M1owZjETMBEGA1UEChMKTXkgQ29tcGFu eTEQMA4GA1UEBxMHTXkgVG93bjEcMBoGA1UECBMTU3RhdGUgb3IgUHJvdmlkZW5j ZTELMAkGA1UEBhMCVVMxEjAQBgNVBAMTCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcN AQEBBQADggEPADCCAQoCggEBAL40UpmJAyiKjO0A5sFF0GjYTBNsJB4xeep2z70f 41A2414lhBwlL4EEMTr9XFh8p91w2uIDKbeASXzZg5AMWVEX9W+dBfZUvrIo4fTL e67iV1LbWb4DD8PGHO+M7ZS7ENnOQdhcoM5zTf3I9RgVlsPMXt1IKrBRovjD+C7c UT6UEZYYcRjp3Z4DiYjBPsci0YOlknPwsJf4tsVddN8Mb4PS4Zq6MA/2Ce48GMSH iX6WPy2pL+jjNo9AkUw2rD1mCfqDvVSoeI/4PlvmtcnwcSDBKY97ZhGAf5s243EE KBfRwrKher5ZQDr8QhePkq5NehPcxDU+x1NHLnMIVJnga7sCAwEAATANBgkqhkiG 9w0BAQQFAAOCAQEAgAPecGaSvaoNOnU9njQgxfxVcAIVLPboN4h99Tvalry9ra+i LsdPyiUJB7tmWcDBkZnLoOFEHkrns27KckWKDZwY3SMTT1osQdcLP2CgJLWPX2U2 WYjOOhYRMPCBAI40isPFvv+5T4KbIllAFwPSi3gG+3NCAka1YZq3m/8iR70zhI/B tfmjzQeUsVM4D5Ts2NjLPMVKcwSsZixmGkEZpUDinhuwHAbli86URt1MpsHATK+W A2R7/h+p+j5AX7LBXdUz+h/cbP5ENElrrk1H/82G8D56Beis5womOjYoYDTBy9oO Zvi8b11fgIjeakv7MyVOhf/ZkiMlOQK5R3P92g== ================================================ FILE: test/Kubernetes.Tests/Certificates/cert.pem ================================================ -----BEGIN CERTIFICATE----- MIIDSDCCAjACCQCTqER3kAB0EDANBgkqhkiG9w0BAQQFADBmMRMwEQYDVQQKEwpN eSBDb21wYW55MRAwDgYDVQQHEwdNeSBUb3duMRwwGgYDVQQIExNTdGF0ZSBvciBQ cm92aWRlbmNlMQswCQYDVQQGEwJVUzESMBAGA1UEAxMJbG9jYWxob3N0MB4XDTIy MDUyMjA0MDI0M1oXDTMyMDUxOTA0MDI0M1owZjETMBEGA1UEChMKTXkgQ29tcGFu eTEQMA4GA1UEBxMHTXkgVG93bjEcMBoGA1UECBMTU3RhdGUgb3IgUHJvdmlkZW5j ZTELMAkGA1UEBhMCVVMxEjAQBgNVBAMTCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcN AQEBBQADggEPADCCAQoCggEBAL40UpmJAyiKjO0A5sFF0GjYTBNsJB4xeep2z70f 41A2414lhBwlL4EEMTr9XFh8p91w2uIDKbeASXzZg5AMWVEX9W+dBfZUvrIo4fTL e67iV1LbWb4DD8PGHO+M7ZS7ENnOQdhcoM5zTf3I9RgVlsPMXt1IKrBRovjD+C7c UT6UEZYYcRjp3Z4DiYjBPsci0YOlknPwsJf4tsVddN8Mb4PS4Zq6MA/2Ce48GMSH iX6WPy2pL+jjNo9AkUw2rD1mCfqDvVSoeI/4PlvmtcnwcSDBKY97ZhGAf5s243EE KBfRwrKher5ZQDr8QhePkq5NehPcxDU+x1NHLnMIVJnga7sCAwEAATANBgkqhkiG 9w0BAQQFAAOCAQEAgAPecGaSvaoNOnU9njQgxfxVcAIVLPboN4h99Tvalry9ra+i LsdPyiUJB7tmWcDBkZnLoOFEHkrns27KckWKDZwY3SMTT1osQdcLP2CgJLWPX2U2 WYjOOhYRMPCBAI40isPFvv+5T4KbIllAFwPSi3gG+3NCAka1YZq3m/8iR70zhI/B tfmjzQeUsVM4D5Ts2NjLPMVKcwSsZixmGkEZpUDinhuwHAbli86URt1MpsHATK+W A2R7/h+p+j5AX7LBXdUz+h/cbP5ENElrrk1H/82G8D56Beis5womOjYoYDTBy9oO Zvi8b11fgIjeakv7MyVOhf/ZkiMlOQK5R3P92g== -----END CERTIFICATE----- ================================================ FILE: test/Kubernetes.Tests/Certificates/key.der ================================================ MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC+NFKZiQMoiozt AObBRdBo2EwTbCQeMXnqds+9H+NQNuNeJYQcJS+BBDE6/VxYfKfdcNriAym3gEl8 2YOQDFlRF/VvnQX2VL6yKOH0y3uu4ldS21m+Aw/DxhzvjO2UuxDZzkHYXKDOc039 yPUYFZbDzF7dSCqwUaL4w/gu3FE+lBGWGHEY6d2eA4mIwT7HItGDpZJz8LCX+LbF XXTfDG+D0uGaujAP9gnuPBjEh4l+lj8tqS/o4zaPQJFMNqw9Zgn6g71UqHiP+D5b 5rXJ8HEgwSmPe2YRgH+bNuNxBCgX0cKyoXq+WUA6/EIXj5KuTXoT3MQ1PsdTRy5z CFSZ4Gu7AgMBAAECggEBAIoTA49PZgqNIaZ/DARrwNILipZi34lHk2BAZae+OU6m ucFDbLbdy7FVsMNI3zuhKl7XKR3++86pAy/t2tK8FC6JPPOMQqLCfDhq8zS4bo3S 419Tur70DAKrk5/WZzWb4qyqTSRagaW9EHXV5w5Xxb1XY9oxJEQgzzTVVhn2d8f7 hcXJW6MyVuFnpAJcGbp9fwl2pGoIdG2BXzWq1ri9Gkgh9sTk1+CEHOLw/2d+QS8n K+PnP7LK8q6AI+VhXQPAFTyCfUnSRvtEufGU/wvA0B54kpqGVB4RObwEtSEuK0vq rfnN59TTtrDaFeiRtgWX1gUovZIP8iWALN1So+1oRWECgYEA7iiJCxP27+UxzmvP Z96bCx+latvEg08vOLdlbdbDLgTrv4GA/TLnbv7+Y0EXRjVIeTxfS+d8LA7fuqUp yHQcvd6Xv3jh+a6IF0H/0ongkZxTbuti3bE+WyTHS357MhW1AvPSp8b3BL1wx2Q/ LCO7Du6kdIz3sS60y+qYEECkppECgYEAzHQdDOsKJwk+9QdNQwZuZpwEr6S3YVsz rUA7eHXT4Wak43WXCe2wkKl2DoifS1hIU9KGW9x0BzS2QQVZv+I/hohUF9YQKCnJ cuxBYbA8D/iycvoDpqyV76LiwwXRniRNm94luTQjLODFcck42aeoKkcZkjMWhaUj RWiPFR6Vy4sCgYBVYxEneKP7hNgjo0G8gvJhvZnoQx0k2xoaIp7qD6rw7/C6O6tM nJifkisQ2QCIOohed0fPhhJeFYMffyII7aB0br9HdgbHJ01B0XbwPGDYtAyx4xES XP73XKtbpOB0p5W7lkG7x7k+6NDrnESOBc2GYAd0hio0S4Ok7NpSUWr3EQKBgDwK hVV72Llp/7EZedkLFHTRsJacOrY+gEiKqmxPve9do2Kg78Acq1NwUJkoCg+oV2U2 V/q2HOTY2AT0O00cdidd9cQiOxBwZRZ4xyKXDKxsDouxXE0gNc/v98Pp+4sDgj8Z 194xr3rIb3Ng8m8Iy1vPEXVbx2tr+ZWyhQJgvwDNAoGATprpcJOqWemD9d4sTA8T URs1ihJIcUEVzBSv/mNgw0szs15qFers3rQJgPpS9yjm4oiFNsGT4Cc7bPDRYyb3 90njZTRseMLAm2snMrv34I7saW9AGNotrA5LA5axbBiYmJ9NX75fNNlYMZeKxC/B RO+LjL/JqRyj4ymPMFfzUTo= ================================================ FILE: test/Kubernetes.Tests/Certificates/key.pem ================================================ -----BEGIN PRIVATE KEY----- MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC+NFKZiQMoiozt AObBRdBo2EwTbCQeMXnqds+9H+NQNuNeJYQcJS+BBDE6/VxYfKfdcNriAym3gEl8 2YOQDFlRF/VvnQX2VL6yKOH0y3uu4ldS21m+Aw/DxhzvjO2UuxDZzkHYXKDOc039 yPUYFZbDzF7dSCqwUaL4w/gu3FE+lBGWGHEY6d2eA4mIwT7HItGDpZJz8LCX+LbF XXTfDG+D0uGaujAP9gnuPBjEh4l+lj8tqS/o4zaPQJFMNqw9Zgn6g71UqHiP+D5b 5rXJ8HEgwSmPe2YRgH+bNuNxBCgX0cKyoXq+WUA6/EIXj5KuTXoT3MQ1PsdTRy5z CFSZ4Gu7AgMBAAECggEBAIoTA49PZgqNIaZ/DARrwNILipZi34lHk2BAZae+OU6m ucFDbLbdy7FVsMNI3zuhKl7XKR3++86pAy/t2tK8FC6JPPOMQqLCfDhq8zS4bo3S 419Tur70DAKrk5/WZzWb4qyqTSRagaW9EHXV5w5Xxb1XY9oxJEQgzzTVVhn2d8f7 hcXJW6MyVuFnpAJcGbp9fwl2pGoIdG2BXzWq1ri9Gkgh9sTk1+CEHOLw/2d+QS8n K+PnP7LK8q6AI+VhXQPAFTyCfUnSRvtEufGU/wvA0B54kpqGVB4RObwEtSEuK0vq rfnN59TTtrDaFeiRtgWX1gUovZIP8iWALN1So+1oRWECgYEA7iiJCxP27+UxzmvP Z96bCx+latvEg08vOLdlbdbDLgTrv4GA/TLnbv7+Y0EXRjVIeTxfS+d8LA7fuqUp yHQcvd6Xv3jh+a6IF0H/0ongkZxTbuti3bE+WyTHS357MhW1AvPSp8b3BL1wx2Q/ LCO7Du6kdIz3sS60y+qYEECkppECgYEAzHQdDOsKJwk+9QdNQwZuZpwEr6S3YVsz rUA7eHXT4Wak43WXCe2wkKl2DoifS1hIU9KGW9x0BzS2QQVZv+I/hohUF9YQKCnJ cuxBYbA8D/iycvoDpqyV76LiwwXRniRNm94luTQjLODFcck42aeoKkcZkjMWhaUj RWiPFR6Vy4sCgYBVYxEneKP7hNgjo0G8gvJhvZnoQx0k2xoaIp7qD6rw7/C6O6tM nJifkisQ2QCIOohed0fPhhJeFYMffyII7aB0br9HdgbHJ01B0XbwPGDYtAyx4xES XP73XKtbpOB0p5W7lkG7x7k+6NDrnESOBc2GYAd0hio0S4Ok7NpSUWr3EQKBgDwK hVV72Llp/7EZedkLFHTRsJacOrY+gEiKqmxPve9do2Kg78Acq1NwUJkoCg+oV2U2 V/q2HOTY2AT0O00cdidd9cQiOxBwZRZ4xyKXDKxsDouxXE0gNc/v98Pp+4sDgj8Z 194xr3rIb3Ng8m8Iy1vPEXVbx2tr+ZWyhQJgvwDNAoGATprpcJOqWemD9d4sTA8T URs1ihJIcUEVzBSv/mNgw0szs15qFers3rQJgPpS9yjm4oiFNsGT4Cc7bPDRYyb3 90njZTRseMLAm2snMrv34I7saW9AGNotrA5LA5axbBiYmJ9NX75fNNlYMZeKxC/B RO+LjL/JqRyj4ymPMFfzUTo= -----END PRIVATE KEY----- ================================================ FILE: test/Kubernetes.Tests/Client/ResourceInformerTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using k8s; using k8s.Models; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Threading; using System.Threading.Tasks; using Xunit; using Yarp.Kubernetes.Tests.TestCluster; using Yarp.Kubernetes.Tests.Utils; namespace Yarp.Kubernetes.Controller.Client.Tests; public class ResourceInformerTests { private static (TResources Resources, TShouldBe ShouldBe) LoadTestResource(string name) { var resourcesYaml = File.ReadAllText(Path.Combine("testassets/resource-informer", name, "resources.yaml")); var shouldBeYaml = File.ReadAllText(Path.Combine("testassets/resource-informer", name, "shouldbe.yaml")); var resources = ResourceSerializers.DeserializeYaml(resourcesYaml); var shouldBe = ResourceSerializers.DeserializeYaml(shouldBeYaml); return (resources, shouldBe); } [Fact] public async Task ResourcesAreListedWhenReadyAsyncIsComplete() { using var cancellation = new CancellationTokenSource(Debugger.IsAttached ? TimeSpan.FromMinutes(5) : TimeSpan.FromSeconds(5)); var (resources, shouldBe) = LoadTestResource(nameof(ResourcesAreListedWhenReadyAsyncIsComplete)); using var clusterHost = new TestClusterHostBuilder() .UseInitialResources(resources) .Build(); using var testHost = new HostBuilder() .ConfigureServices((context, services) => { services.AddKubernetesReverseProxy(context.Configuration); services.RegisterResourceInformer(); services.Configure(options => { options.Configuration = KubernetesClientConfiguration.BuildConfigFromConfigObject(clusterHost.KubeConfig); }); }) .Build(); var informer = testHost.Services.GetRequiredService>(); var pods = new Dictionary(); informer.StartWatching(); using var registration = informer.Register((eventType, pod) => { pods[NamespacedName.From(pod)] = pod; }); await clusterHost.StartAsync(cancellation.Token); await testHost.StartAsync(cancellation.Token); await registration.ReadyAsync(cancellation.Token); Assert.Equal(shouldBe, pods.Keys); } [Fact] public async Task ResourcesWithApiGroupAreListed() { using var cancellation = new CancellationTokenSource(Debugger.IsAttached ? TimeSpan.FromMinutes(5) : TimeSpan.FromSeconds(5)); var (resources, shouldBe) = LoadTestResource(nameof(ResourcesWithApiGroupAreListed)); using var clusterHost = new TestClusterHostBuilder() .UseInitialResources(resources) .Build(); using var testHost = new HostBuilder() .ConfigureServices((context, services) => { services.AddKubernetesReverseProxy(context.Configuration); services.RegisterResourceInformer(); services.Configure(options => { options.Configuration = KubernetesClientConfiguration.BuildConfigFromConfigObject(clusterHost.KubeConfig); }); }) .Build(); var informer = testHost.Services.GetRequiredService>(); var deployments = new Dictionary(); informer.StartWatching(); using var registration = informer.Register((eventType, deployment) => { deployments[NamespacedName.From(deployment)] = deployment; }); await clusterHost.StartAsync(cancellation.Token); await testHost.StartAsync(cancellation.Token); await registration.ReadyAsync(cancellation.Token); Assert.Equal(shouldBe, deployments.Keys); } } ================================================ FILE: test/Kubernetes.Tests/Client/SyncResourceInformer.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using k8s; using k8s.Models; using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace Yarp.Kubernetes.Controller.Client.Tests; internal class SyncResourceInformer : IResourceInformer where TResource : class, IKubernetesObject, new() { private readonly object _sync = new(); private ImmutableList _registrations = ImmutableList.Empty; public void PublishUpdate(WatchEventType eventType, TResource resource) { List> callbacks; lock (_sync) { callbacks = _registrations.Select(x => x.Callback).ToList(); } callbacks.ForEach(x => x.Invoke(eventType, resource)); } public Task ReadyAsync(CancellationToken cancellationToken) { return Task.CompletedTask; } public IResourceInformerRegistration Register(ResourceInformerCallback callback) { return new Registration(this, callback); } public IResourceInformerRegistration Register(ResourceInformerCallback> callback) { return new Registration(this, (eventType, resource) => callback(eventType, resource)); } public Task StartAsync(CancellationToken cancellationToken) { return Task.CompletedTask; } public void StartWatching() { } public Task StopAsync(CancellationToken cancellationToken) { return Task.CompletedTask; } internal class Registration : IResourceInformerRegistration { private bool _disposedValue; public Registration(SyncResourceInformer resourceInformer, ResourceInformerCallback callback) { ResourceInformer = resourceInformer; Callback = callback; lock (resourceInformer._sync) { resourceInformer._registrations = resourceInformer._registrations.Add(this); } } ~Registration() { // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method Dispose(disposing: false); } public SyncResourceInformer ResourceInformer { get; } public ResourceInformerCallback Callback { get; } public Task ReadyAsync(CancellationToken cancellationToken) => ResourceInformer.ReadyAsync(cancellationToken); protected virtual void Dispose(bool disposing) { if (!_disposedValue) { lock (ResourceInformer._sync) { ResourceInformer._registrations = ResourceInformer._registrations.Remove(this); } _disposedValue = true; } } public void Dispose() { // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method Dispose(disposing: true); GC.SuppressFinalize(this); } } } ================================================ FILE: test/Kubernetes.Tests/Client/V1DeploymentResourceInformer.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using k8s; using k8s.Autorest; using k8s.Models; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using System.Threading; using System.Threading.Tasks; namespace Yarp.Kubernetes.Controller.Client.Tests; internal class V1DeploymentResourceInformer : ResourceInformer { public V1DeploymentResourceInformer( IKubernetes client, ResourceSelector selector, IHostApplicationLifetime hostApplicationLifetime, ILogger logger) : base(client, selector, hostApplicationLifetime, logger) { } protected override Task> RetrieveResourceListAsync(string resourceVersion = null, ResourceSelector resourceSelector = null, CancellationToken cancellationToken = default) { return Client.AppsV1.ListDeploymentForAllNamespacesWithHttpMessagesAsync(resourceVersion: resourceVersion, fieldSelector: resourceSelector?.FieldSelector, cancellationToken: cancellationToken); } protected override Watcher WatchResourceListAsync(string resourceVersion = null, ResourceSelector resourceSelector = null, Action onEvent = null, Action onError = null, Action onClosed = null) { return Client.AppsV1.WatchListDeploymentForAllNamespaces(resourceVersion: resourceVersion, fieldSelector: resourceSelector?.FieldSelector, onEvent: onEvent, onError: onError, onClosed: onClosed); } } ================================================ FILE: test/Kubernetes.Tests/Client/V1PodResourceInformer.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using k8s; using k8s.Autorest; using k8s.Models; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using System.Threading; using System.Threading.Tasks; namespace Yarp.Kubernetes.Controller.Client.Tests; internal class V1PodResourceInformer : ResourceInformer { public V1PodResourceInformer( IKubernetes client, ResourceSelector selector, IHostApplicationLifetime hostApplicationLifetime, ILogger logger) : base(client, selector, hostApplicationLifetime, logger) { } protected override Task> RetrieveResourceListAsync(string resourceVersion = null, ResourceSelector resourceSelector = null, CancellationToken cancellationToken = default) { return Client.CoreV1.ListPodForAllNamespacesWithHttpMessagesAsync(resourceVersion: resourceVersion, fieldSelector: resourceSelector?.FieldSelector, cancellationToken: cancellationToken); } protected override Watcher WatchResourceListAsync(string resourceVersion = null, ResourceSelector resourceSelector = null, Action onEvent = null, Action onError = null, Action onClosed = null) { return Client.CoreV1.WatchListPodForAllNamespaces(resourceVersion: resourceVersion, fieldSelector: resourceSelector?.FieldSelector, onEvent: onEvent, onError: onError, onClosed: onClosed); } } ================================================ FILE: test/Kubernetes.Tests/Hosting/BackgroundHostedServiceTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using System; using System.Threading; using System.Threading.Tasks; using Xunit; using Yarp.Kubernetes.Tests.Hosting.Fakes; namespace Yarp.Kubernetes.Controller.Hosting.Tests; public class BackgroundHostedServiceTests { [Fact] public async Task StartAndStopUnderHosting() { var latches = new TestLatches(); using var host = new HostBuilder() .ConfigureServices((hbc, services) => { services.AddSingleton(); services.AddSingleton(latches); }) .Build(); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(15)); await host.StartAsync(cts.Token); await latches.RunEnter.WhenSignalAsync(cts.Token); latches.RunResult.Signal(); await latches.RunExit.WhenSignalAsync(cts.Token); await host.StopAsync(cts.Token); } [Fact] public async Task StartAndStopUnderWebHost() { var latches = new TestLatches(); using var host = new WebHostBuilder() .ConfigureServices((hbc, services) => { services.AddSingleton(); services.AddSingleton(); services.AddSingleton(latches); }) .Configure(app => { }) .Build(); // TODO: figure out why the hosting takes so long to unwind naturally // and increase this safety cancellation up from 3 seconds using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3)); var runTask = host.RunAsync(cts.Token); await latches.RunEnter.WhenSignalAsync(cts.Token); latches.RunResult.Signal(); await latches.RunExit.WhenSignalAsync(cts.Token); await runTask; } [Fact] public async Task IfRunAsyncThrowsItComesBackFromHost() { var context = new TestLatches(); using var host = new WebHostBuilder() .ConfigureServices((hbc, services) => { services.AddSingleton(); services.AddSingleton(); services.AddSingleton(context); }) .Configure(app => { }) .Build(); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(15)); var runTask = host.RunAsync(cts.Token); #pragma warning disable CA1303 // Do not pass literals as localized parameters context.RunResult.Throw(new ApplicationException("Unwind")); #pragma warning restore CA1303 // Do not pass literals as localized parameters var ex = await Assert.ThrowsAsync(() => runTask); Assert.Equal("Unwind", Assert.Single(ex.Flatten().InnerExceptions).Message); } } ================================================ FILE: test/Kubernetes.Tests/Hosting/Fakes/FakeBackgroundHostedService.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using System.Threading; using System.Threading.Tasks; using Yarp.Kubernetes.Controller.Hosting; namespace Yarp.Kubernetes.Tests.Hosting.Fakes; public class FakeBackgroundHostedService : BackgroundHostedService { private readonly TestLatches _context; public FakeBackgroundHostedService( TestLatches context, IHostApplicationLifetime hostApplicationLifetime, ILogger logger) : base(hostApplicationLifetime, logger) { _context = context; } public override async Task RunAsync(CancellationToken cancellationToken) { try { _context.RunEnter.Signal(); await _context.RunResult.WhenSignalAsync(cancellationToken).ConfigureAwait(false); } finally { _context.RunExit.Signal(); } } } ================================================ FILE: test/Kubernetes.Tests/Hosting/Fakes/FakeServer.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Http.Features; using System.Threading; using System.Threading.Tasks; namespace Yarp.Kubernetes.Tests.Hosting.Fakes; public sealed class FakeServer : IServer { public IFeatureCollection Features { get; } = new FeatureCollection(); public void Dispose() { } public Task StartAsync(IHttpApplication application, CancellationToken cancellationToken) { return Task.CompletedTask; } public Task StopAsync(CancellationToken cancellationToken) { return Task.CompletedTask; } } ================================================ FILE: test/Kubernetes.Tests/Hosting/Fakes/TestLatch.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Threading; using System.Threading.Tasks; namespace Yarp.Kubernetes.Tests.Hosting.Fakes; public class TestLatch { private readonly TaskCompletionSource _completion = new TaskCompletionSource(); public void Signal() { _completion.SetResult(false); } public void Throw(ApplicationException exception) { _completion.SetException(exception); } public async Task WhenSignalAsync(CancellationToken cancellationToken) { var task = await Task.WhenAny( _completion.Task, Task.Delay(TimeSpan.FromSeconds(90), cancellationToken)) .ConfigureAwait(false); await task.ConfigureAwait(false); } } ================================================ FILE: test/Kubernetes.Tests/Hosting/Fakes/TestLatches.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. namespace Yarp.Kubernetes.Tests.Hosting.Fakes; public class TestLatches { public TestLatch RunEnter { get; } = new TestLatch(); public TestLatch RunResult { get; } = new TestLatch(); public TestLatch RunExit { get; } = new TestLatch(); } ================================================ FILE: test/Kubernetes.Tests/IngressCacheTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Linq; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using k8s; using k8s.Models; using Microsoft.Extensions.Options; using Moq; using Xunit; using Yarp.Kubernetes.Controller; using Yarp.Kubernetes.Controller.Caching; using Yarp.Kubernetes.Controller.Certificates; using Yarp.Kubernetes.Tests.Utils; namespace Yarp.Kubernetes.Tests; public class IngressCacheTests { private readonly Mock> _mockOptions; private readonly Mock _certificateSelector; private readonly Mock _certificateHelper; private readonly IngressCache _cacheUnderTest; private readonly X509Certificate2 _localhostCertificate; public IngressCacheTests(ITestOutputHelper output) { var logger = new TestLogger(output); _mockOptions = new Mock>(); _certificateSelector = new Mock(); _certificateHelper = new Mock(); _mockOptions.SetupGet(o => o.Value).Returns(new YarpOptions { ControllerClass = "microsoft.com/ingress-yarp", DefaultSslCertificate = "default/yarp-ingress-tls" }); _cacheUnderTest = new IngressCache(_mockOptions.Object, _certificateSelector.Object, _certificateHelper.Object, logger); // Generate a certificate for testing var ecdsa = ECDsa.Create(); var req = new CertificateRequest("cn=localhost", ecdsa, HashAlgorithmName.SHA256); _localhostCertificate = req.CreateSelfSigned(DateTimeOffset.Now, DateTimeOffset.Now.AddYears(5)); } [Theory] [InlineData("yarp", "microsoft.com/ingress-yarp", false, 1)] [InlineData("yarp", "microsoft.com/ingress-yarp", true, 1)] [InlineData("yarp-internal", "microsoft.com/ingress-yarp-internal", false, 0)] [InlineData("yarp-internal", "microsoft.com/ingress-yarp-internal", true, 0)] [InlineData(null, null, false, 0)] public void IngressWithClassAnnotationTests(string ingressClassName, string controllerName, bool? isDefault, int expectedIngressCount) { // Arrange if (controllerName is not null) { var ingressClass = KubeResourceGenerator.CreateIngressClass(ingressClassName, controllerName, isDefault); _cacheUnderTest.Update(WatchEventType.Added, ingressClass); } var ingress = KubeResourceGenerator.CreateIngress("ingress-with-class", "ns-test", "yarp"); // Act _cacheUnderTest.Update(WatchEventType.Added, ingress); // Assert var ingresses = _cacheUnderTest.GetIngresses().ToArray(); Assert.Equal(expectedIngressCount, ingresses.Length); } [Theory] [InlineData("yarp", "microsoft.com/ingress-yarp", true, 1)] [InlineData("yarp", "microsoft.com/ingress-yarp", false, 0)] [InlineData("yarp-internal", "microsoft.com/ingress-yarp-internal", false, 0)] [InlineData("yarp-internal", "microsoft.com/ingress-yarp-internal", true, 0)] [InlineData(null, null, false, 0)] public void IngressWithoutClassAnnotationTests(string ingressClassName, string controllerName, bool? isDefault, int expectedIngressCount) { // Arrange if (controllerName is not null) { var ingressClass = KubeResourceGenerator.CreateIngressClass(ingressClassName, controllerName, isDefault); _cacheUnderTest.Update(WatchEventType.Added, ingressClass); } var ingress = KubeResourceGenerator.CreateIngress("ingress-without-class", "ns-test", null); // Act _cacheUnderTest.Update(WatchEventType.Added, ingress); // Assert var ingresses = _cacheUnderTest.GetIngresses().ToArray(); Assert.Equal(expectedIngressCount, ingresses.Length); } [Fact] public void IngressModifiedToRemoveClass() { // Arrange var ingressClass = KubeResourceGenerator.CreateIngressClass("yarp", "microsoft.com/ingress-yarp", false); _cacheUnderTest.Update(WatchEventType.Added, ingressClass); var ingress = KubeResourceGenerator.CreateIngress("ingress-with-class", "ns-test", "yarp"); _cacheUnderTest.Update(WatchEventType.Added, ingress); // Act ingress.Spec.IngressClassName = null; _cacheUnderTest.Update(WatchEventType.Modified, ingress); // Assert var ingresses = _cacheUnderTest.GetIngresses().ToArray(); Assert.Empty(ingresses); } [Fact] public void IngressClassModifiedToAddDefault() { // Arrange var ingressClass = KubeResourceGenerator.CreateIngressClass("yarp", "microsoft.com/ingress-yarp", false); var ingress = KubeResourceGenerator.CreateIngress("ingress-with-class", "ns-test", "yarp"); _cacheUnderTest.Update(WatchEventType.Added, ingressClass); // Act ingressClass.Metadata.Annotations.Add("ingressclass.kubernetes.io/is-default-class", "true"); _cacheUnderTest.Update(WatchEventType.Modified, ingressClass); _cacheUnderTest.Update(WatchEventType.Added, ingress); // Assert var ingresses = _cacheUnderTest.GetIngresses().ToArray(); Assert.Single(ingresses); } [Fact] public void IngressClassDeleted() { // Arrange var ingressClass = KubeResourceGenerator.CreateIngressClass("yarp", "microsoft.com/ingress-yarp", true); var ingress = KubeResourceGenerator.CreateIngress("ingress-with-class", "ns-test", "yarp"); _cacheUnderTest.Update(WatchEventType.Added, ingressClass); // Act _cacheUnderTest.Update(WatchEventType.Deleted, ingressClass); _cacheUnderTest.Update(WatchEventType.Added, ingress); // Assert var ingresses = _cacheUnderTest.GetIngresses().ToArray(); Assert.Empty(ingresses); } [Fact] public void SecretNotMatchDefaultNameIgnored() { // Arrange var secret = KubeResourceGenerator.CreateSecret("yarp", "not-my-tls"); // Act _cacheUnderTest.Update(WatchEventType.Added, secret); // Assert _certificateHelper.Verify(h => h.ConvertCertificate(It.IsAny(), It.IsAny()), Times.Never); _certificateSelector.Verify(s => s.AddCertificate(It.IsAny(), It.IsAny()), Times.Never); } [Fact] public void SecretMatchDefaultNameAdded() { // Arrange var secret = KubeResourceGenerator.CreateSecret("yarp-ingress-tls", "default"); _certificateHelper .Setup(h => h.ConvertCertificate(It.Is(n => n.Name == "yarp-ingress-tls" && n.Namespace == "default"), It.Is(s => s == secret))) .Returns(_localhostCertificate); // Act _cacheUnderTest.Update(WatchEventType.Added, secret); // Assert _certificateHelper.Verify(h => h.ConvertCertificate(It.Is(n => n.Name == "yarp-ingress-tls" && n.Namespace == "default"), It.Is(s => s == secret)), Times.Once); _certificateSelector.Verify(s => s.AddCertificate(It.Is(n => n.Name == "yarp-ingress-tls" && n.Namespace == "default"), It.Is(c => c == _localhostCertificate)), Times.Once); } [Fact] public void SecretMatchDefaultNameModified() { // Arrange var secret = KubeResourceGenerator.CreateSecret("yarp-ingress-tls", "default"); _certificateHelper .Setup(h => h.ConvertCertificate(It.Is(n => n.Name == "yarp-ingress-tls" && n.Namespace == "default"), It.Is(s => s == secret))) .Returns(_localhostCertificate); // Act _cacheUnderTest.Update(WatchEventType.Modified, secret); // Assert _certificateHelper.Verify(h => h.ConvertCertificate(It.Is(n => n.Name == "yarp-ingress-tls" && n.Namespace == "default"), It.Is(s => s == secret)), Times.Once); _certificateSelector.Verify(s => s.AddCertificate(It.Is(n => n.Name == "yarp-ingress-tls" && n.Namespace == "default"), It.Is(c => c == _localhostCertificate)), Times.Once); } [Fact] public void SecretMatchDefaultNameRemoved() { // Arrange var secret = KubeResourceGenerator.CreateSecret("yarp-ingress-tls", "default"); _certificateHelper .Setup(h => h.ConvertCertificate(It.Is(n => n.Name == "yarp-ingress-tls" && n.Namespace == "default"), It.Is(s => s == secret))) .Returns(_localhostCertificate); // Act _cacheUnderTest.Update(WatchEventType.Deleted, secret); // Assert _certificateHelper.Verify(h => h.ConvertCertificate(It.Is(n => n.Name == "yarp-ingress-tls" && n.Namespace == "default"), It.Is(s => s == secret)), Times.Once); _certificateSelector.Verify(s => s.RemoveCertificate(It.Is(n => n.Name == "yarp-ingress-tls" && n.Namespace == "default")), Times.Once); } [Fact] public void SecretMatchDefaultNameCantConvertNotAdded() { // Arrange var secret = KubeResourceGenerator.CreateSecret("yarp-ingress-tls", "default"); _certificateHelper .Setup(h => h.ConvertCertificate(It.Is(n => n.Name == "yarp-ingress-tls" && n.Namespace == "default"), It.Is(s => s == secret))) .Returns((X509Certificate2)null); // Act _cacheUnderTest.Update(WatchEventType.Added, secret); // Assert _certificateHelper.Verify(h => h.ConvertCertificate(It.Is(n => n.Name == "yarp-ingress-tls" && n.Namespace == "default"), It.Is(s => s == secret)), Times.Once); _certificateSelector.Verify(s => s.AddCertificate(It.IsAny(), It.IsAny()), Times.Never); } } ================================================ FILE: test/Kubernetes.Tests/IngressControllerTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using k8s; using k8s.Models; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; using Moq; using System; using System.Threading; using System.Threading.Tasks; using Xunit; using Yarp.Kubernetes.Controller; using Yarp.Kubernetes.Controller.Caching; using Yarp.Kubernetes.Controller.Client.Tests; using Yarp.Kubernetes.Controller.Services; using Yarp.Kubernetes.Tests.Utils; using Yarp.Tests.Common; namespace Yarp.Kubernetes.Tests; public class IngressControllerTests { private readonly Mock _mockCache = new(); private readonly Mock _mockReconciler = new(); private readonly SyncResourceInformer _ingressInformer = new(); private readonly SyncResourceInformer _serviceInformer = new(); private readonly SyncResourceInformer _endpointsInformer = new(); private readonly SyncResourceInformer _ingressClassInformer = new(); private readonly SyncResourceInformer _secretInformer = new(); private readonly Mock _mockHostApplicationLifetime = new(); private readonly Mock> _mockOptions = new(); private readonly IngressController _controller; public IngressControllerTests(ITestOutputHelper output) { var optionsNoWatchSecrets = new YarpOptions() { ServerCertificates = false, }; _mockOptions.Setup(o => o.Value).Returns(optionsNoWatchSecrets); var logger = new TestLogger(output); _controller = new IngressController(_mockCache.Object, _mockReconciler.Object, _ingressInformer, _serviceInformer, _endpointsInformer, _ingressClassInformer, _secretInformer, _mockHostApplicationLifetime.Object, _mockOptions.Object, logger); } [Fact] public async Task ReconciliationContinuesOnReconcilerError() { _mockCache.Setup(x => x.Update(It.IsAny(), It.IsAny())).Returns(true); var awaiter = new SemaphoreSlim(0, 1); _mockReconciler .SetupSequence(x => x.ProcessAsync(It.IsAny())) .Returns(() => { awaiter.Release(); return Task.CompletedTask; }) .Returns(() => { awaiter.Release(); return Task.CompletedTask; }) .Returns(() => { awaiter.Release(); return Task.FromException(new Exception("reconicliation failed")); }) .Returns(() => { awaiter.Release(); return Task.CompletedTask; }); await _controller.StartAsync(CancellationToken.None).DefaultTimeout(); await awaiter.WaitAsync().DefaultTimeout(); _mockReconciler.Verify(x => x.ProcessAsync(It.IsAny()), Times.Exactly(1)); _ingressInformer.PublishUpdate(WatchEventType.Added, new V1Ingress()); await awaiter.WaitAsync().DefaultTimeout(); _mockReconciler.Verify(x => x.ProcessAsync(It.IsAny()), Times.Exactly(2)); _ingressInformer.PublishUpdate(WatchEventType.Added, new V1Ingress()); await awaiter.WaitAsync().DefaultTimeout(); _mockReconciler.Verify(x => x.ProcessAsync(It.IsAny()), Times.AtLeast(3)); await awaiter.WaitAsync().DefaultTimeout(); _mockReconciler.Verify(x => x.ProcessAsync(It.IsAny()), Times.AtLeast(4)); } } ================================================ FILE: test/Kubernetes.Tests/IngressConversionTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using System.Threading.Tasks; using k8s; using k8s.Models; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Moq; using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Newtonsoft.Json.Linq; using Xunit; using Yarp.Kubernetes.Controller; using Yarp.Kubernetes.Controller.Caching; using Yarp.Kubernetes.Controller.Certificates; using Yarp.Kubernetes.Controller.Converters; using JsonSerializer = System.Text.Json.JsonSerializer; namespace Yarp.Kubernetes.Tests; public class IngressConversionTests { public IngressConversionTests() { JsonConvert.DefaultSettings = () => new JsonSerializerSettings() { NullValueHandling = NullValueHandling.Ignore, Converters = { new StringEnumConverter() } }; } [Theory] [InlineData("basic-ingress")] [InlineData("multiple-endpoints-ports")] [InlineData("multiple-endpoints-same-port")] [InlineData("https")] [InlineData("https-service-port-protocol")] [InlineData("exact-match")] [InlineData("annotations")] [InlineData("mapped-port")] [InlineData("port-mismatch")] [InlineData("hostname-routing")] [InlineData("multiple-hosts")] [InlineData("multiple-ingresses")] [InlineData("multiple-ingresses-one-svc")] [InlineData("multiple-namespaces")] [InlineData("route-metadata")] [InlineData("route-queryparameters")] [InlineData("route-headers")] [InlineData("route-order")] [InlineData("route-methods")] [InlineData("missing-svc")] [InlineData("port-diff-name")] [InlineData("external-name-ingress")] public async Task ParsingTests(string name) { var ingressClass = KubeResourceGenerator.CreateIngressClass("yarp", "microsoft.com/ingress-yarp", true); var cache = await GetKubernetesInfo(name, ingressClass); var configContext = new YarpConfigContext(); var ingresses = cache.GetIngresses().ToArray(); foreach (var ingress in ingresses) { if (cache.TryGetReconcileData(new NamespacedName(ingress.Metadata.NamespaceProperty, ingress.Metadata.Name), out var data)) { var ingressContext = new YarpIngressContext(ingress, data.ServiceList, data.EndpointsList); YarpParser.ConvertFromKubernetesIngress(ingressContext, configContext); } } var options = new JsonSerializerOptions { Converters = { new JsonStringEnumConverter() } }; VerifyClusters(JsonSerializer.Serialize(configContext.BuildClusterConfig(), options), name); VerifyRoutes(JsonSerializer.Serialize(configContext.Routes, options), name); } private static void VerifyClusters(string clusterJson, string name) { VerifyJson(clusterJson, name, "clusters.json"); } private static void VerifyRoutes(string routesJson, string name) { VerifyJson(routesJson, name, "routes.json"); } private static string StripNullProperties(string json) { using var reader = new JsonTextReader(new StringReader(json)); var sb = new StringBuilder(); using var sw = new StringWriter(sb); using var writer = new JsonTextWriter(sw); while (reader.Read()) { var token = reader.TokenType; var value = reader.Value; if (reader.TokenType == JsonToken.PropertyName) { reader.Read(); if (reader.TokenType == JsonToken.Null) { continue; } writer.WriteToken(token, value); } writer.WriteToken(reader.TokenType, reader.Value); } return sb.ToString(); } private static void VerifyJson(string json, string name, string fileName) { var other = File.ReadAllText(Path.Combine("testassets", name, fileName)); json = StripNullProperties(json); other = StripNullProperties(other); var actual = JToken.Parse(json); var jOther = JToken.Parse(other); Assert.True(JToken.DeepEquals(actual, jOther), $"Expected: {jOther}\nActual: {actual}"); } private async Task GetKubernetesInfo(string name, V1IngressClass ingressClass) { var mockLogger = new Mock>(); var mockOptions = new Mock>(); var certificateSelector = new Mock(); var loggerHelper = new Mock>(); var certificateHelper = new CertificateHelper(loggerHelper.Object); mockOptions.SetupGet(o => o.Value).Returns(new YarpOptions { ControllerClass = "microsoft.com/ingress-yarp" }); var cache = new IngressCache(mockOptions.Object, certificateSelector.Object, certificateHelper, mockLogger.Object); var typeMap = new Dictionary(); typeMap.Add("networking.k8s.io/v1/Ingress", typeof(V1Ingress)); typeMap.Add("v1/Service", typeof(V1Service)); typeMap.Add("v1/Endpoints", typeof(V1Endpoints)); if (ingressClass is not null) { cache.Update(WatchEventType.Added, ingressClass); } var kubeObjects = await KubernetesYaml.LoadAllFromFileAsync(Path.Combine("testassets", name, "ingress.yaml"), typeMap).ConfigureAwait(false); foreach (var obj in kubeObjects) { if (obj is V1Ingress ingress) { cache.Update(WatchEventType.Added, ingress); } else if (obj is V1Service service) { cache.Update(WatchEventType.Added, service); } else if (obj is V1Endpoints endpoints) { cache.Update(WatchEventType.Added, endpoints); } } return cache; } } ================================================ FILE: test/Kubernetes.Tests/KubeResourceGenerator.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; using System.Globalization; using k8s.Models; namespace Yarp.Kubernetes.Tests; internal static class KubeResourceGenerator { public static V1IngressClass CreateIngressClass(string name, string controller, bool? isDefaultClass) { var ingressClass = new V1IngressClass { Spec = new V1IngressClassSpec { Controller = controller, }, Metadata = new V1ObjectMeta { Name = name, Annotations = new Dictionary(), }, }; if (isDefaultClass.HasValue && isDefaultClass.Value) { ingressClass.Metadata.Annotations.Add("ingressclass.kubernetes.io/is-default-class", isDefaultClass.Value.ToString(CultureInfo.InvariantCulture)); } return ingressClass; } public static V1Ingress CreateIngress(string name, string namespaceName, string ingressClassName) { var ingress = new V1Ingress { Spec = new V1IngressSpec(), Metadata = new V1ObjectMeta { Name = name, NamespaceProperty = namespaceName, } }; if (!string.IsNullOrEmpty(ingressClassName)) { ingress.Spec.IngressClassName = ingressClassName; } return ingress; } public static V1Secret CreateSecret(string name, string namespaceName, byte[] publicData = null, byte[] privateData = null) { var secret = new V1Secret { Metadata = new V1ObjectMeta { Name = name, NamespaceProperty = namespaceName, } }; if (publicData != null) { if (secret.Data == null) { secret.Data = new Dictionary(); } secret.Data["tls.crt"] = publicData; } if (privateData != null) { if (secret.Data == null) { secret.Data = new Dictionary(); } secret.Data["tls.key"] = privateData; } return secret; } } ================================================ FILE: test/Kubernetes.Tests/Management/KubernetesCoreExtensionsTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using k8s; using Microsoft.Extensions.DependencyInjection; using Xunit; namespace Yarp.Kubernetes.Controller.Management.Tests; public class KubernetesCoreExtensionsTests { [Fact] public void KubernetesClientIsAdded() { var services = new ServiceCollection(); services.AddKubernetesCore(); var serviceProvider = services.BuildServiceProvider(); Assert.NotNull(serviceProvider.GetService()); } [Fact] public void ExistingClientIsNotReplaced() { using var client = new k8s.Kubernetes(KubernetesClientConfiguration.BuildDefaultConfig()); var services = new ServiceCollection(); services.AddSingleton(client); services.AddKubernetesCore(); var serviceProvider = services.BuildServiceProvider(); Assert.Same(client, serviceProvider.GetService()); } } ================================================ FILE: test/Kubernetes.Tests/NamespacedNameTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using k8s.Models; using System.Collections.Generic; using Xunit; namespace Yarp.Kubernetes.Controller.Tests; public class NamespacedNameTests { [Fact] public void WorksAsDictionaryKey() { var dictionary = new Dictionary(); var name1 = new NamespacedName("ns", "n1"); var name2 = new NamespacedName("ns", "n2"); var name3 = new NamespacedName("ns", "n3"); dictionary[name1] = "one"; dictionary[name1] = "one again"; dictionary[name2] = "two"; Assert.Contains(new KeyValuePair(name1, "one again"), dictionary); Assert.Contains(new KeyValuePair(name2, "two"), dictionary); Assert.DoesNotContain(name3, dictionary.Keys); } [Theory] [InlineData("ns", "n1", "ns", "n1", true)] [InlineData("ns", "n1", "ns", "n2", false)] [InlineData("ns", "n1", "ns-x", "n1", false)] [InlineData(null, "n1", null, "n1", true)] [InlineData(null, "n1", null, "n2", false)] public void EqualityAndInequality( string namespace1, string name1, string namespace2, string name2, bool shouldBeEqual) { var namespacedName1 = new NamespacedName(namespace1, name1); var namespacedName2 = new NamespacedName(namespace2, name2); var areEqual = namespacedName1 == namespacedName2; var areNotEqual = namespacedName1 != namespacedName2; #pragma warning disable CS1718 // Comparison made to same variable var sameEqual1 = namespacedName1 == namespacedName1; var sameNotEqual1 = namespacedName1 != namespacedName1; var sameEqual2 = namespacedName2 == namespacedName2; var sameNotEqual2 = namespacedName2 != namespacedName2; #pragma warning restore CS1718 // Comparison made to same variable Assert.NotEqual(areNotEqual, areEqual); Assert.Equal(shouldBeEqual, areEqual); Assert.True(sameEqual1); Assert.False(sameNotEqual1); Assert.True(sameEqual2); Assert.False(sameNotEqual2); } [Fact] public void NamespaceAndNameFromResource() { var resource = new V1ConfigMap { ApiVersion = V1ConfigMap.KubeApiVersion, Kind = V1ConfigMap.KubeKind, Metadata = new V1ObjectMeta {Name = "the-name", NamespaceProperty = "the-namespace"} }; var nn = NamespacedName.From(resource); Assert.Equal("the-name", nn.Name); Assert.Equal("the-namespace", nn.Namespace); } [Fact] public void JustNameFromClusterResource() { var resource = new V1ClusterRole { ApiVersion = V1ClusterRole.KubeApiVersion, Kind = V1ClusterRole.KubeKind, Metadata = new V1ObjectMeta { Name = "the-name" } }; var nn = NamespacedName.From(resource); Assert.Equal("the-name", nn.Name); Assert.Null(nn.Namespace); } } ================================================ FILE: test/Kubernetes.Tests/Queues/WorkQueueTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Threading; using System.Threading.Tasks; using Xunit; namespace Yarp.Kubernetes.Controller.Queues.Tests; public class WorkQueueTests { public CancellationTokenSource Cancellation { get; set; } = new CancellationTokenSource(TimeSpan.FromSeconds(5)); [Fact] public async Task NormalUsageIsAddGetDone() { using IWorkQueue queue = new WorkQueue(); Assert.Equal(0, queue.Len()); queue.Add("one"); Assert.Equal(1, queue.Len()); queue.Add("two"); Assert.Equal(2, queue.Len()); var (item1, shutdown1) = await queue.GetAsync(Cancellation.Token); Assert.Equal(1, queue.Len()); queue.Done(item1); Assert.Equal(1, queue.Len()); var (item2, shutdown2) = await queue.GetAsync(Cancellation.Token); Assert.Equal(0, queue.Len()); queue.Done(item2); Assert.Equal(0, queue.Len()); Assert.Equal("one", item1); Assert.False(shutdown1); Assert.Equal("two", item2); Assert.False(shutdown2); } [Fact] public void AddingSameItemAgainHasNoEffect() { using IWorkQueue queue = new WorkQueue(); var len1 = queue.Len(); queue.Add("one"); var len2 = queue.Len(); queue.Add("one"); var len3 = queue.Len(); Assert.Equal(0, len1); Assert.Equal(1, len2); Assert.Equal(1, len3); } [Fact] public async Task CallingAddWhileItemIsBeingProcessedHasNoEffect() { using IWorkQueue queue = new WorkQueue(); var lenOriginal = queue.Len(); queue.Add("one"); var lenAfterAdd = queue.Len(); var (item1, _) = await queue.GetAsync(Cancellation.Token); var lenAfterGet = queue.Len(); queue.Add("one"); var lenAfterAddAgain = queue.Len(); Assert.Equal("one", item1); Assert.Equal(0, lenOriginal); Assert.Equal(1, lenAfterAdd); Assert.Equal(0, lenAfterGet); Assert.Equal(0, lenAfterAddAgain); Assert.Equal(0, queue.Len()); } [Fact] public async Task ItemCanBeAddedAgainAfterDoneIsCalled() { using IWorkQueue queue = new WorkQueue(); var lenOriginal = queue.Len(); queue.Add("one"); var lenAfterAdd = queue.Len(); var (item1, _) = await queue.GetAsync(Cancellation.Token); var lenAfterGet = queue.Len(); queue.Done(item1); var lenAfterDone = queue.Len(); queue.Add("one"); var lenAfterAddAgain = queue.Len(); Assert.Equal("one", item1); Assert.Equal(0, lenOriginal); Assert.Equal(1, lenAfterAdd); Assert.Equal(0, lenAfterGet); Assert.Equal(0, lenAfterDone); Assert.Equal(1, lenAfterAddAgain); Assert.Equal(1, queue.Len()); } [Fact] public async Task IfAddWasCalledDuringProcessingThenItemIsRequeuedByDone() { using IWorkQueue queue = new WorkQueue(); var lenOriginal = queue.Len(); queue.Add("one"); var lenAfterAdd = queue.Len(); var (item1, _) = await queue.GetAsync(Cancellation.Token); var lenAfterGet = queue.Len(); queue.Add("one"); var lenAfterAddAgain = queue.Len(); queue.Done(item1); var lenAfterDone = queue.Len(); var (item2, _) = await queue.GetAsync(Cancellation.Token); var lenAfterGetAgain = queue.Len(); Assert.Equal("one", item1); Assert.Equal("one", item2); Assert.Equal(0, lenOriginal); Assert.Equal(1, lenAfterAdd); Assert.Equal(0, lenAfterGet); Assert.Equal(0, lenAfterAddAgain); Assert.Equal(1, lenAfterDone); Assert.Equal(0, lenAfterGetAgain); Assert.Equal(0, queue.Len()); } [Fact] public async Task GetCompletesOnceAddIsCalled() { using IWorkQueue queue = new WorkQueue(); var getTask = queue.GetAsync(Cancellation.Token); Assert.Equal(0, queue.Len()); Assert.False(getTask.IsCompleted); queue.Add("one"); var (item1, _) = await getTask; Assert.Equal(0, queue.Len()); Assert.True(getTask.IsCompleted); Assert.Equal("one", item1); Assert.Equal(0, queue.Len()); } [Fact] public async Task GetReturnsShutdownTrueAfterShutdownIsCalled() { using IWorkQueue queue = new WorkQueue(); var getTask = queue.GetAsync(Cancellation.Token); Assert.Equal(0, queue.Len()); Assert.False(getTask.IsCompleted); queue.ShutDown(); var (item1, shutdown1) = await getTask; Assert.Equal(0, queue.Len()); Assert.True(getTask.IsCompleted); Assert.True(shutdown1); Assert.Equal(0, queue.Len()); } [Fact] public void ShuttingDownReturnsTrueAfterShutdownIsCalled() { using IWorkQueue queue = new WorkQueue(); var shuttingDownBefore = queue.ShuttingDown(); queue.ShutDown(); var shuttingDownAfter = queue.ShuttingDown(); Assert.False(shuttingDownBefore); Assert.True(shuttingDownAfter); } } ================================================ FILE: test/Kubernetes.Tests/Rate/LimitTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using Xunit; namespace Yarp.Kubernetes.Controller.Rate.Tests; public class LimitTests { [Theory] [InlineData(15, 1, 15)] [InlineData(15, 120, 1800)] [InlineData(15, .1, 1.5)] [InlineData(300, 2, 600)] public void TokensFromDuration(double perSecond, double durationSeconds, double tokens) { var limit = new Limit(perSecond); var tokensFromDuration = limit.TokensFromDuration(TimeSpan.FromSeconds(durationSeconds)); Assert.Equal(tokens, tokensFromDuration); } [Theory] [InlineData(15, 1, 15)] [InlineData(15, 120, 1800)] [InlineData(15, .1, 1.5)] [InlineData(300, 2, 600)] public void DurationFromTokens(double perSecond, double durationSeconds, double tokens) { var limit = new Limit(perSecond); var durationFromTokens = limit.DurationFromTokens(tokens); Assert.Equal(TimeSpan.FromSeconds(durationSeconds), durationFromTokens); } } ================================================ FILE: test/Kubernetes.Tests/Rate/LimiterTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Polly; using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; using Xunit; using Xunit.Sdk; using Yarp.Tests.Common; namespace Yarp.Kubernetes.Controller.Rate.Tests; public class LimiterTests { private readonly DateTimeOffset _startTime = new DateTimeOffset(2020, 10, 14, 12, 34, 56, TimeSpan.Zero); [Fact] public void FirstTokenIsAvailable() { var timeProvider = new TestTimeProvider(_startTime); var limiter = new Limiter(new Limit(10), 1, timeProvider); var allowed = limiter.Allow(); Assert.True(allowed); } [Theory] [InlineData(5)] [InlineData(1)] [InlineData(300)] public void AsManyAsBurstTokensAreAvailableRightAway(int burst) { var timeProvider = new TestTimeProvider(_startTime); var limiter = new Limiter(new Limit(10), burst, timeProvider); var allowed = new List(); foreach (var index in Enumerable.Range(1, burst)) { allowed.Add(limiter.Allow()); } var notAllowed = limiter.Allow(); Assert.All(allowed, item => Assert.True(item)); Assert.False(notAllowed); } [Fact] public void TokensBecomeAvailableAtLimitPerSecondRate() { var timeProvider = new TestTimeProvider(_startTime); var limiter = new Limiter(new Limit(10), 50, timeProvider); var initiallyAllowed = limiter.AllowN(timeProvider.GetUtcNow(), 50); var thenNotAllowed1 = limiter.Allow(); timeProvider.Advance(TimeSpan.FromMilliseconds(100)); var oneTokenAvailable = limiter.Allow(); var thenNotAllowed2 = limiter.Allow(); timeProvider.Advance(TimeSpan.FromMilliseconds(200)); var twoTokensAvailable1 = limiter.Allow(); var twoTokensAvailable2 = limiter.Allow(); var thenNotAllowed3 = limiter.Allow(); Assert.True(initiallyAllowed); Assert.False(thenNotAllowed1); Assert.True(oneTokenAvailable); Assert.False(thenNotAllowed2); Assert.True(twoTokensAvailable1); Assert.True(twoTokensAvailable2); Assert.False(thenNotAllowed3); } [Fact] public void ReserveTellsYouHowLongToWait() { var timeProvider = new TestTimeProvider(_startTime); var limiter = new Limiter(new Limit(10), 50, timeProvider); var initiallyAllowed = limiter.AllowN(timeProvider.GetUtcNow(), 50); var thenNotAllowed1 = limiter.Allow(); var reserveOne = limiter.Reserve(); var delayOne = reserveOne.Delay(); var reserveTwoMore = limiter.Reserve(timeProvider.GetUtcNow(), 2); var delayTwoMore = reserveTwoMore.Delay(); timeProvider.Advance(TimeSpan.FromMilliseconds(450)); var reserveAlreadyAvailable = limiter.Reserve(); var delayAlreadyAvailable = reserveAlreadyAvailable.Delay(); var reserveHalfAvailable = limiter.Reserve(); var delayHalfAvailable = reserveHalfAvailable.Delay(); Assert.True(initiallyAllowed); Assert.False(thenNotAllowed1); Assert.True(reserveOne.Ok); Assert.Equal(TimeSpan.FromMilliseconds(100), delayOne); Assert.True(reserveTwoMore.Ok); Assert.Equal(TimeSpan.FromMilliseconds(300), delayTwoMore); Assert.True(reserveAlreadyAvailable.Ok); Assert.Equal(TimeSpan.Zero, delayAlreadyAvailable); Assert.True(reserveHalfAvailable.Ok); Assert.Equal(TimeSpan.FromMilliseconds(50), delayHalfAvailable); } [Fact(Skip = "https://github.com/dotnet/yarp/issues/1357")] public async Task WaitAsyncCausesPauseLikeReserve() { var limiter = new Limiter(new Limit(10), 5); using var cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(5)); while (cancellation.IsCancellationRequested == false) { var task = limiter.WaitAsync(cancellation.Token); if (!task.IsCompleted) { await task; break; } } var delayOne = new Stopwatch(); delayOne.Start(); await limiter.WaitAsync(cancellation.Token); delayOne.Stop(); var delayTwoMore = new Stopwatch(); delayTwoMore.Start(); await limiter.WaitAsync(2, cancellation.Token); delayTwoMore.Stop(); await Task.Delay(TimeSpan.FromMilliseconds(150)); var delayAlreadyAvailable = new Stopwatch(); delayAlreadyAvailable.Start(); await limiter.WaitAsync(cancellation.Token); delayAlreadyAvailable.Stop(); var delayHalfAvailable = new Stopwatch(); delayHalfAvailable.Start(); await limiter.WaitAsync(cancellation.Token); delayHalfAvailable.Stop(); Assert.InRange(delayOne.Elapsed, TimeSpan.FromMilliseconds(75), TimeSpan.FromMilliseconds(125)); Assert.InRange(delayTwoMore.Elapsed, TimeSpan.FromMilliseconds(175), TimeSpan.FromMilliseconds(225)); Assert.InRange(delayAlreadyAvailable.Elapsed, TimeSpan.Zero, TimeSpan.FromMilliseconds(5)); Assert.InRange(delayHalfAvailable.Elapsed, TimeSpan.FromMilliseconds(25), TimeSpan.FromMilliseconds(75)); } [Fact(Skip = "https://github.com/dotnet/yarp/issues/1357")] public async Task ManyWaitsStackUp() { await Policy .Handle() .RetryAsync(3) .ExecuteAsync(async () => { var limiter = new Limiter(new Limit(10), 5); using var cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(5)); while (cancellation.IsCancellationRequested == false) { var task = limiter.WaitAsync(cancellation.Token); if (!task.IsCompleted) { await task; break; } } var delayOne = new Stopwatch(); delayOne.Start(); var delayTwo = new Stopwatch(); delayTwo.Start(); var delayThree = new Stopwatch(); delayThree.Start(); var waits = new List { limiter.WaitAsync(cancellation.Token), limiter.WaitAsync(cancellation.Token), limiter.WaitAsync(cancellation.Token), }; var taskOne = await Task.WhenAny(waits); await taskOne; delayOne.Stop(); waits.Remove(taskOne); var taskTwo = await Task.WhenAny(waits); await taskTwo; delayTwo.Stop(); waits.Remove(taskTwo); var taskThree = await Task.WhenAny(waits); await taskThree; delayThree.Stop(); waits.Remove(taskThree); Assert.InRange(delayOne.Elapsed, TimeSpan.FromMilliseconds(75), TimeSpan.FromMilliseconds(125)); Assert.InRange(delayTwo.Elapsed, TimeSpan.FromMilliseconds(175), TimeSpan.FromMilliseconds(225)); Assert.InRange(delayThree.Elapsed, TimeSpan.FromMilliseconds(275), TimeSpan.FromMilliseconds(325)); }); } } ================================================ FILE: test/Kubernetes.Tests/Rate/ReservationTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using Xunit; using Yarp.Tests.Common; namespace Yarp.Kubernetes.Controller.Rate.Tests; public class ReservationTests { private readonly DateTimeOffset _startTime = new DateTimeOffset(2020, 10, 14, 12, 34, 56, TimeSpan.Zero); [Fact] public void NotOkayAlwaysReturnsMaxValueDelay() { var timeProvider = new TestTimeProvider(_startTime); var reservation = new Reservation( timeProvider: timeProvider, limiter: default, ok: false); var delay1 = reservation.Delay(); var delayFrom1 = reservation.DelayFrom(timeProvider.GetUtcNow()); timeProvider.Advance(TimeSpan.FromMinutes(3)); var delay2 = reservation.Delay(); var delayFrom2 = reservation.DelayFrom(timeProvider.GetUtcNow()); Assert.Equal(TimeSpan.MaxValue, delay1); Assert.Equal(TimeSpan.MaxValue, delayFrom1); Assert.Equal(TimeSpan.MaxValue, delay2); Assert.Equal(TimeSpan.MaxValue, delayFrom2); } [Fact] public void DelayIsZeroWhenTimeToActIsNowOrEarlier() { var timeProvider = new TestTimeProvider(_startTime); var reservation = new Reservation( timeProvider: timeProvider, limiter: default, ok: true, timeToAct: timeProvider.GetUtcNow(), limit: default); var delay1 = reservation.Delay(); var delayFrom1 = reservation.DelayFrom(timeProvider.GetUtcNow()); timeProvider.Advance(TimeSpan.FromMinutes(3)); var delay2 = reservation.Delay(); var delayFrom2 = reservation.DelayFrom(timeProvider.GetUtcNow()); Assert.Equal(TimeSpan.Zero, delay1); Assert.Equal(TimeSpan.Zero, delayFrom1); Assert.Equal(TimeSpan.Zero, delay2); Assert.Equal(TimeSpan.Zero, delayFrom2); } [Fact] public void DelayGetsSmallerAsTimePasses() { var timeProvider = new TestTimeProvider(_startTime); var reservation = new Reservation( timeProvider: timeProvider, limiter: default, ok: true, timeToAct: timeProvider.GetUtcNow().Add(TimeSpan.FromMinutes(5)), limit: default); var delay1 = reservation.Delay(); timeProvider.Advance(TimeSpan.FromMinutes(3)); var delay2 = reservation.Delay(); timeProvider.Advance(TimeSpan.FromMinutes(3)); var delay3 = reservation.Delay(); Assert.Equal(TimeSpan.FromMinutes(5), delay1); Assert.Equal(TimeSpan.FromMinutes(2), delay2); Assert.Equal(TimeSpan.Zero, delay3); } [Fact] public void DelayFromNotChangedByTimePassing() { var timeProvider = new TestTimeProvider(_startTime); var reservation = new Reservation( timeProvider: timeProvider, limiter: default, ok: true, timeToAct: timeProvider.GetUtcNow().Add(TimeSpan.FromMinutes(5)), limit: default); var twoMinutesPast = timeProvider.GetUtcNow().Subtract(TimeSpan.FromMinutes(2)); var twoMinutesFuture = timeProvider.GetUtcNow().Add(TimeSpan.FromMinutes(2)); var delay1 = reservation.DelayFrom(timeProvider.GetUtcNow()); var delayPast1 = reservation.DelayFrom(twoMinutesPast); var delayFuture1 = reservation.DelayFrom(twoMinutesFuture); timeProvider.Advance(TimeSpan.FromMinutes(3)); var delay2 = reservation.DelayFrom(timeProvider.GetUtcNow()); var delayPast2 = reservation.DelayFrom(twoMinutesPast); var delayFuture2 = reservation.DelayFrom(twoMinutesFuture); Assert.Equal(TimeSpan.FromMinutes(5), delay1); Assert.Equal(TimeSpan.FromMinutes(7), delayPast1); Assert.Equal(TimeSpan.FromMinutes(3), delayFuture1); Assert.Equal(TimeSpan.FromMinutes(2), delay2); Assert.Equal(TimeSpan.FromMinutes(7), delayPast2); Assert.Equal(TimeSpan.FromMinutes(3), delayFuture2); } } ================================================ FILE: test/Kubernetes.Tests/ReconcilerTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Moq; using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Xunit; using Yarp.Kubernetes.Controller; using Yarp.Kubernetes.Controller.Client; using Yarp.Kubernetes.Controller.Caching; using Yarp.Kubernetes.Controller.Configuration; using Yarp.Kubernetes.Controller.Services; using Yarp.Kubernetes.Tests.Utils; using Yarp.ReverseProxy.Configuration; namespace Yarp.Kubernetes.Tests; public class ReconcilerTests { private readonly Mock _mockCache = new(); private readonly Mock _mockUpdateConfig = new(); private readonly Mock _mockIngressResourceStatusUpdater = new(); private readonly Reconciler _reconciler; public ReconcilerTests(ITestOutputHelper output) { var logger = new TestLogger(output); _reconciler = new Reconciler(_mockCache.Object, _mockUpdateConfig.Object, _mockIngressResourceStatusUpdater.Object, logger); } [Fact] public async Task ReconcilerDoesNotStopOnInvalidIngress() { _mockCache .Setup(x => x.GetIngresses()) .Returns(new[] { new IngressData(KubeResourceGenerator.CreateIngress("bad-ingress", "default", "yarp")), new IngressData(KubeResourceGenerator.CreateIngress("good-ingress", "default", "yarp")) }); _mockCache .Setup(x => x.TryGetReconcileData(It.IsAny(), out It.Ref.IsAny)) .Returns(true); _mockCache .Setup(x => x.TryGetReconcileData(new NamespacedName("default", "bad-ingress"), out It.Ref.IsAny)) .Throws(new Exception("poison ingress")); await _reconciler.ProcessAsync(CancellationToken.None); _mockUpdateConfig.Verify(x => x.UpdateAsync(It.IsAny>(), It.IsAny>(), It.IsAny())); } } ================================================ FILE: test/Kubernetes.Tests/TestCluster/Controllers/ResourceApiController.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using k8s.Models; using Microsoft.AspNetCore.Mvc; using System.Threading.Tasks; using Yarp.Kubernetes.Tests.TestCluster.Models; namespace Yarp.Kubernetes.Tests.TestCluster; [Route("api/{version}/{plural}")] public class ResourceApiController : ControllerBase { private readonly ITestCluster _testCluster; public ResourceApiController(ITestCluster testCluster) { _testCluster = testCluster; } [FromRoute] public string Version { get; set; } [FromRoute] public string Plural { get; set; } [HttpGet] public async Task ListAsync(ListParameters parameters) { var list = await _testCluster.ListResourcesAsync(string.Empty, Version, Plural, parameters); var result = new KubernetesList( apiVersion: Version, kind: "PodList", metadata: new V1ListMeta { ContinueProperty = list.Continue, RemainingItemCount = null, ResourceVersion = list.ResourceVersion }, items: list.Items); return new ObjectResult(result); } } ================================================ FILE: test/Kubernetes.Tests/TestCluster/Controllers/ResourceApiGroupController.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using k8s.Models; using Microsoft.AspNetCore.Mvc; using System.Threading.Tasks; using Yarp.Kubernetes.Tests.TestCluster.Models; namespace Yarp.Kubernetes.Tests.TestCluster; [Route("apis/{group}/{version}/{plural}")] public class ResourceApiGroupController : ControllerBase { private readonly ITestCluster _testCluster; public ResourceApiGroupController(ITestCluster testCluster) { _testCluster = testCluster; } [FromRoute] public string Group { get; set; } [FromRoute] public string Version { get; set; } [FromRoute] public string Plural { get; set; } [HttpGet] public async Task ListAsync(ListParameters parameters) { var list = await _testCluster.ListResourcesAsync(Group, Version, Plural, parameters); var result = new KubernetesList( apiVersion: $"{Group}/{Version}", kind: "DeploymentList", metadata: new V1ListMeta { ContinueProperty = list.Continue, RemainingItemCount = null, ResourceVersion = list.ResourceVersion }, items: list.Items); return new ObjectResult(result); } } ================================================ FILE: test/Kubernetes.Tests/TestCluster/ITestCluster.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Http; using System.Threading.Tasks; using Yarp.Kubernetes.Tests.TestCluster.Models; namespace Yarp.Kubernetes.Tests.TestCluster; public interface ITestCluster { Task UnhandledRequest(HttpContext context); Task ListResourcesAsync(string group, string version, string plural, ListParameters parameters); } ================================================ FILE: test/Kubernetes.Tests/TestCluster/ITestClusterHost.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using k8s; using k8s.KubeConfigModels; using Microsoft.Extensions.Hosting; namespace Yarp.Kubernetes.Tests.TestCluster; public interface ITestClusterHost : IHost { K8SConfiguration KubeConfig { get; } IKubernetes Client { get; } ITestCluster Cluster { get; } } ================================================ FILE: test/Kubernetes.Tests/TestCluster/Models/ListParameters.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Mvc; namespace Yarp.Kubernetes.Tests.TestCluster.Models; public class ListParameters { [FromQuery] public string Continue { get; set; } [FromQuery] public string FieldSelector { get; set; } [FromQuery] public string LabelSelector { get; set; } [FromQuery] public int? Limit { get; set; } [FromQuery] public string ResourceVersion { get; set; } [FromQuery] public int? TimeoutSeconds { get; set; } [FromQuery] public bool? Watch { get; set; } [FromQuery] public string Pretty { get; set; } } ================================================ FILE: test/Kubernetes.Tests/TestCluster/Models/ListResult.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; namespace Yarp.Kubernetes.Tests.TestCluster.Models; public class ListResult { public string Continue { get; set; } public string ResourceVersion { get; set; } public IList Items { get; set; } } ================================================ FILE: test/Kubernetes.Tests/TestCluster/Models/ResourceObject.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using k8s; using k8s.Models; using System.Collections.Generic; using System.Text.Json.Serialization; namespace Yarp.Kubernetes.Tests.TestCluster.Models; public class ResourceObject : IKubernetesObject { [JsonPropertyName("apiVersion")] public string ApiVersion { get; set; } [JsonPropertyName("kind")] public string Kind { get; set; } [JsonPropertyName("metadata")] public V1ObjectMeta Metadata { get; set; } [JsonExtensionData] public IDictionary AdditionalData { get; set; } } ================================================ FILE: test/Kubernetes.Tests/TestCluster/TestCluster.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Options; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Yarp.Kubernetes.Tests.TestCluster.Models; using Yarp.Kubernetes.Tests.Utils; namespace Yarp.Kubernetes.Tests.TestCluster; public class TestCluster : ITestCluster { public IList Resources { get; } = new List(); public TestCluster(IOptions options) { ArgumentNullException.ThrowIfNull(options); foreach (var resource in options.Value.InitialResources) { Resources.Add(ResourceSerializers.Convert(resource)); } } public virtual Task UnhandledRequest(HttpContext context) { throw new NotImplementedException(); } public virtual Task ListResourcesAsync(string group, string version, string plural, ListParameters parameters) { ArgumentException.ThrowIfNullOrEmpty(version); ArgumentException.ThrowIfNullOrEmpty(plural); ArgumentNullException.ThrowIfNull(parameters); return Task.FromResult(new ListResult { ResourceVersion = parameters.ResourceVersion, Continue = null, Items = Resources.ToArray(), }); } } ================================================ FILE: test/Kubernetes.Tests/TestCluster/TestClusterHost.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using k8s; using k8s.KubeConfigModels; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using System; using System.Threading; using System.Threading.Tasks; namespace Yarp.Kubernetes.Tests.TestCluster; public class TestClusterHost : ITestClusterHost { private readonly IHost _host; private bool _disposedValue; public TestClusterHost(IHost host, K8SConfiguration kubeConfig, IKubernetes client) { _host = host; KubeConfig = kubeConfig; Client = client; } public IServiceProvider Services => _host.Services; public ITestCluster Cluster => _host.Services.GetRequiredService(); public K8SConfiguration KubeConfig { get; } public IKubernetes Client { get; } public Task StartAsync(CancellationToken cancellationToken = default) => _host.StartAsync(cancellationToken); public Task StopAsync(CancellationToken cancellationToken = default) => _host.StartAsync(cancellationToken); protected virtual void Dispose(bool disposing) { if (!_disposedValue) { if (disposing) { _host.Dispose(); } _disposedValue = true; } } public void Dispose() { // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method Dispose(disposing: true); GC.SuppressFinalize(this); } } ================================================ FILE: test/Kubernetes.Tests/TestCluster/TestClusterHostBuilder.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using k8s; using k8s.KubeConfigModels; using k8s.Models; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using System; using System.Net; using System.Net.Sockets; namespace Yarp.Kubernetes.Tests.TestCluster; public class TestClusterHostBuilder { private readonly IHostBuilder _hostBuilder = new HostBuilder(); public ITestClusterHost Build() { if (string.IsNullOrEmpty(ServerUrl)) { ServerUrl = $"http://{IPAddress.Loopback}:{AvailablePort()}"; } _hostBuilder.ConfigureWebHostDefaults(web => { web .UseStartup() .UseUrls(ServerUrl); }); var host = _hostBuilder.Build(); var kubeConfig = new K8SConfiguration { ApiVersion = "v1", Kind = "Config", CurrentContext = "test-context", Contexts = new[] { new Context { Name = "test-context", ContextDetails = new ContextDetails { Namespace = "test-namespace", Cluster = "test-cluster", User = "test-user", } } }, Clusters = new[] { new Cluster { Name = "test-cluster", ClusterEndpoint = new ClusterEndpoint { Server = ServerUrl, } } }, Users = new[] { new User { Name = "test-user", UserCredentials = new UserCredentials { Token = "test-token", } } }, }; var clientConfiguration = KubernetesClientConfiguration.BuildConfigFromConfigObject(kubeConfig); var client = new k8s.Kubernetes(clientConfiguration); return new TestClusterHost(host, kubeConfig, client); } public TestClusterHostBuilder UseInitialResources(params IKubernetesObject[] resources) { return ConfigureServices(services => { services.Configure(options => { foreach (var resource in resources) { options.InitialResources.Add(resource); } }); }); } public TestClusterHostBuilder ConfigureServices(Action configureDelegate) { _hostBuilder.ConfigureServices(configureDelegate); return this; } public TestClusterHostBuilder ConfigureServices(Action configureDelegate) { _hostBuilder.ConfigureServices(configureDelegate); return this; } public TestClusterHostBuilder Configure(Action configureOptions) { _hostBuilder.ConfigureServices(services => Configure(configureOptions)); return this; } private static int AvailablePort() { using var socket = new Socket(SocketType.Stream, ProtocolType.Tcp); socket.Bind(new IPEndPoint(IPAddress.Any, 0)); return ((IPEndPoint)socket.LocalEndPoint).Port; } public string ServerUrl { get; set; } } ================================================ FILE: test/Kubernetes.Tests/TestCluster/TestClusterOptions.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using k8s; using k8s.Models; using System.Collections.Generic; namespace Yarp.Kubernetes.Tests.TestCluster; public class TestClusterOptions { public IList> InitialResources { get; } = new List>(); } ================================================ FILE: test/Kubernetes.Tests/TestCluster/TestClusterStartup.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; namespace Yarp.Kubernetes.Tests.TestCluster; public class TestClusterStartup { #pragma warning disable CA1822 // Mark members as static public void ConfigureServices(IServiceCollection services) #pragma warning restore CA1822 // Mark members as static { // services.AddTransient(); services.AddControllers(); services.AddSingleton(); } #pragma warning disable CA1822 // Mark members as static public void Configure(IApplicationBuilder app, ITestCluster cluster) #pragma warning restore CA1822 // Mark members as static { if (app is null) { throw new System.ArgumentNullException(nameof(app)); } if (cluster is null) { throw new System.ArgumentNullException(nameof(cluster)); } app.Use(next => async context => { // This is a no-op, but very convenient for setting a breakpoint to see per-request details. await next(context); }); app.UseRouting(); app.UseEndpoints(endpoints => endpoints.MapControllers()); app.Run(cluster.UnhandledRequest); } } ================================================ FILE: test/Kubernetes.Tests/Utils/ResourceSerializers.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Text.Json; using YamlDotNet.Core.Events; using YamlDotNet.Serialization; namespace Yarp.Kubernetes.Tests.Utils; /// /// Class ResourceSerializers implements the resource serializers interface. /// Implements the . /// /// public static class ResourceSerializers { private static readonly IDeserializer _yamlDeserializer = new DeserializerBuilder() .WithNodeTypeResolver(new NonStringScalarTypeResolver()) .Build(); private static readonly JsonSerializerOptions _jsonOptions = new() { PropertyNameCaseInsensitive = true }; public static T DeserializeYaml(string yaml) { var resource = _yamlDeserializer.Deserialize(yaml); return Convert(resource); } public static TResource Convert(object resource) { var json = JsonSerializer.Serialize(resource, _jsonOptions); return JsonSerializer.Deserialize(json, _jsonOptions); } private class NonStringScalarTypeResolver : INodeTypeResolver { bool INodeTypeResolver.Resolve(NodeEvent nodeEvent, ref Type currentType) { if (currentType == typeof(object) && nodeEvent is Scalar) { var scalar = nodeEvent as Scalar; if (scalar.IsPlainImplicit) { // TODO: should use the correct boolean parser (which accepts yes/no) instead of bool.tryparse if (bool.TryParse(scalar.Value, out var _)) { currentType = typeof(bool); return true; } if (int.TryParse(scalar.Value, out var _)) { currentType = typeof(int); return true; } if (double.TryParse(scalar.Value, out var _)) { currentType = typeof(double); return true; } } } return false; } } } ================================================ FILE: test/Kubernetes.Tests/Utils/TestLogger.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using Microsoft.Extensions.Logging; using Xunit; namespace Yarp.Kubernetes.Tests.Utils; public class TestLogger : ILogger { private readonly ITestOutputHelper _output; private readonly LogLevel _minLogLevel; public TestLogger(ITestOutputHelper output, LogLevel minLogLevel = LogLevel.Debug) { _output = output; _minLogLevel = minLogLevel; } public IDisposable BeginScope(TState state) { return null; } public bool IsEnabled(LogLevel logLevel) { return logLevel != LogLevel.None && logLevel >= _minLogLevel; } public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) { _output.WriteLine(formatter.Invoke(state, exception)); } } ================================================ FILE: test/Kubernetes.Tests/Yarp.Kubernetes.Tests.csproj ================================================ $(TestTFMs) Exe $(NoWarn);CS8002 Yarp.Kubernetes ================================================ FILE: test/Kubernetes.Tests/testassets/annotations/clusters.json ================================================ [ { "ClusterId": "frontend.default:80", "LoadBalancingPolicy": "Random", "SessionAffinity": { "Enabled": true, "Policy": "Cookie", "FailurePolicy": "Redistribute", "AffinityKeyName": "Key1", "Cookie": { "Domain": "localhost", "Expiration": null, "HttpOnly": true, "IsEssential": true, "MaxAge": null, "Path": "mypath", "SameSite": "Strict", "SecurePolicy": "Always" } }, "HealthCheck": { "Active": { "Enabled": true, "Interval": "00:00:10", "Timeout": "00:00:10", "Policy": "ConsecutiveFailures", "Path": "/api/health" } }, "HttpClient": { "SslProtocols": "Ssl3", "MaxConnectionsPerServer": 2, "ActivityContextHeaders": null, "WebProxy": null, "DangerousAcceptAnyServerCertificate": true }, "HttpRequest": { "ActivityTimeout": "00:01:00", "Version": "2.0", "VersionPolicy": "RequestVersionExact", "AllowResponseBuffering": false }, "Destinations": { "http://10.244.2.38:80": { "Address": "http://10.244.2.38:80", "Health": null, "Metadata": null } }, "Metadata": null } ] ================================================ FILE: test/Kubernetes.Tests/testassets/annotations/ingress.yaml ================================================ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: minimal-ingress namespace: default annotations: yarp.ingress.kubernetes.io/authorization-policy: authzpolicy yarp.ingress.kubernetes.io/rate-limiter-policy: ratelimiterpolicy yarp.ingress.kubernetes.io/cors-policy: corspolicy yarp.ingress.kubernetes.io/load-balancing: Random yarp.ingress.kubernetes.io/health-check: | Active: Enabled: true Interval: '00:00:10' Timeout: '00:00:10' Policy: ConsecutiveFailures Path: "/api/health" yarp.ingress.kubernetes.io/http-client: | SslProtocols: Ssl3 MaxConnectionsPerServer: 2 DangerousAcceptAnyServerCertificate: true yarp.ingress.kubernetes.io/http-request: | ActivityTimeout: '00:01:00' Version: '2.0' VersionPolicy: 'RequestVersionExact' AllowResponseBuffering: false yarp.ingress.kubernetes.io/session-affinity: | Enabled: true Policy: Cookie FailurePolicy: Redistribute AffinityKeyName: Key1 Cookie: Domain: localhost Expiration: HttpOnly: true IsEssential: true MaxAge: Path: mypath SameSite: Strict SecurePolicy: Always yarp.ingress.kubernetes.io/transforms: | - PathPrefix: "/apis" - RequestHeader: header1 Append: bar spec: rules: - http: paths: - path: /foo pathType: Prefix backend: service: name: frontend port: number: 80 --- apiVersion: v1 kind: Service metadata: name: frontend namespace: default spec: selector: app: frontend ports: - name: http port: 80 targetPort: 80 type: ClusterIP --- apiVersion: v1 kind: Endpoints metadata: name: frontend namespace: default subsets: - addresses: - ip: 10.244.2.38 ports: - name: http port: 80 protocol: TCP ================================================ FILE: test/Kubernetes.Tests/testassets/annotations/routes.json ================================================ [ { "RouteId": "minimal-ingress.default:/foo", "Match": { "Methods": null, "Hosts": [], "Path": "/foo/{**catch-all}", "Headers": null }, "Order": null, "ClusterId": "frontend.default:80", "AuthorizationPolicy": "authzpolicy", "RateLimiterPolicy": "ratelimiterpolicy", "CorsPolicy": "corspolicy", "Metadata": null, "Transforms": [ { "PathPrefix": "/apis" }, { "RequestHeader": "header1", "Append": "bar" } ] } ] ================================================ FILE: test/Kubernetes.Tests/testassets/basic-ingress/clusters.json ================================================ [ { "ClusterId": "frontend.default:80", "LoadBalancingPolicy": null, "SessionAffinity": null, "HealthCheck": null, "HttpClient": null, "HttpRequest": null, "Destinations": { "http://10.244.2.38:80": { "Address": "http://10.244.2.38:80", "Health": null, "Metadata": null } }, "Metadata": null } ] ================================================ FILE: test/Kubernetes.Tests/testassets/basic-ingress/ingress.yaml ================================================ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: minimal-ingress namespace: default spec: rules: - http: paths: - path: /foo pathType: Prefix backend: service: name: frontend port: number: 80 --- apiVersion: v1 kind: Service metadata: name: frontend namespace: default spec: selector: app: frontend ports: - name: http port: 80 targetPort: 80 type: ClusterIP --- apiVersion: v1 kind: Endpoints metadata: name: frontend namespace: default subsets: - addresses: - ip: 10.244.2.38 ports: - name: http port: 80 protocol: TCP ================================================ FILE: test/Kubernetes.Tests/testassets/basic-ingress/routes.json ================================================ [ { "RouteId": "minimal-ingress.default:/foo", "Match": { "Methods": null, "Hosts": [], "Path": "/foo/{**catch-all}", "Headers": null, "QueryParameters": null }, "Order": null, "ClusterId": "frontend.default:80", "AuthorizationPolicy": null, "RateLimiterPolicy": null, "CorsPolicy": null, "Metadata": null, "Transforms": null } ] ================================================ FILE: test/Kubernetes.Tests/testassets/exact-match/clusters.json ================================================ [ { "ClusterId": "frontend.default:80", "LoadBalancingPolicy": null, "SessionAffinity": null, "HealthCheck": null, "HttpClient": null, "HttpRequest": null, "Destinations": { "http://10.244.2.38:80": { "Address": "http://10.244.2.38:80", "Health": null, "Metadata": null } }, "Metadata": null } ] ================================================ FILE: test/Kubernetes.Tests/testassets/exact-match/ingress.yaml ================================================ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: minimal-ingress namespace: default spec: rules: - http: paths: - path: /foo pathType: ExactMatch backend: service: name: frontend port: number: 80 --- apiVersion: v1 kind: Service metadata: name: frontend namespace: default spec: selector: app: frontend ports: - name: http port: 80 targetPort: 80 type: ClusterIP --- apiVersion: v1 kind: Endpoints metadata: name: frontend namespace: default subsets: - addresses: - ip: 10.244.2.38 ports: - name: http port: 80 protocol: TCP ================================================ FILE: test/Kubernetes.Tests/testassets/exact-match/routes.json ================================================ [ { "RouteId": "minimal-ingress.default:/foo", "Match": { "Methods": null, "Hosts": [], "Path": "/foo", "Headers": null, "QueryParameters": null }, "Order": null, "ClusterId": "frontend.default:80", "AuthorizationPolicy": null, "RateLimiterPolicy": null, "CorsPolicy": null, "Metadata": null, "Transforms": null } ] ================================================ FILE: test/Kubernetes.Tests/testassets/external-name-ingress/clusters.json ================================================ [ { "ClusterId": "external-service.default:443", "LoadBalancingPolicy": null, "SessionAffinity": null, "HealthCheck": null, "HttpClient": null, "HttpRequest": null, "Destinations": { "https://external-service.example.com:443": { "Address": "https://external-service.example.com:443", "Health": null, "Metadata": null } }, "Metadata": null } ] ================================================ FILE: test/Kubernetes.Tests/testassets/external-name-ingress/ingress.yaml ================================================ apiVersion: v1 kind: Service metadata: name: external-service namespace: default spec: type: ExternalName externalName: external-service.example.com --- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: external-ingress namespace: default spec: rules: - http: paths: - path: /foo pathType: Prefix backend: service: name: external-service port: number: 443 ================================================ FILE: test/Kubernetes.Tests/testassets/external-name-ingress/routes.json ================================================ [ { "RouteId": "external-ingress.default:/foo", "Match": { "Methods": null, "Hosts": [], "Path": "/foo/{**catch-all}", "Headers": null, "QueryParameters": null }, "Order": null, "ClusterId": "external-service.default:443", "AuthorizationPolicy": null, "RateLimiterPolicy": null, "CorsPolicy": null, "Metadata": null, "Transforms": null } ] ================================================ FILE: test/Kubernetes.Tests/testassets/hostname-routing/clusters.json ================================================ [ { "ClusterId": "frontend.foo:80", "LoadBalancingPolicy": null, "SessionAffinity": null, "HealthCheck": null, "HttpClient": null, "HttpRequest": null, "Destinations": { "http://10.244.2.38:80": { "Address": "http://10.244.2.38:80", "Health": null, "Metadata": null } }, "Metadata": null } ] ================================================ FILE: test/Kubernetes.Tests/testassets/hostname-routing/ingress.yaml ================================================ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: hostname-routing namespace: foo spec: rules: - host: foo.bar.com http: paths: - path: / pathType: Prefix backend: service: name: frontend port: number: 80 --- apiVersion: v1 kind: Service metadata: name: frontend namespace: foo spec: selector: app: frontend ports: - name: http port: 80 targetPort: 80 type: ClusterIP --- apiVersion: v1 kind: Endpoints metadata: name: frontend namespace: foo subsets: - addresses: - ip: 10.244.2.38 ports: - name: http port: 80 protocol: TCP ================================================ FILE: test/Kubernetes.Tests/testassets/hostname-routing/routes.json ================================================ [ { "RouteId": "hostname-routing.foo:foo.bar.com/", "Match": { "Methods": null, "Hosts": [ "foo.bar.com" ], "Path": "/{**catch-all}", "Headers": null, "QueryParameters": null }, "Order": null, "ClusterId": "frontend.foo:80", "AuthorizationPolicy": null, "RateLimiterPolicy": null, "CorsPolicy": null, "Metadata": null, "Transforms": null } ] ================================================ FILE: test/Kubernetes.Tests/testassets/https/clusters.json ================================================ [ { "ClusterId": "frontend.default:443", "LoadBalancingPolicy": null, "SessionAffinity": null, "HealthCheck": null, "HttpClient": null, "HttpRequest": null, "Destinations": { "https://10.244.2.38:443": { "Address": "https://10.244.2.38:443", "Health": null, "Metadata": null } }, "Metadata": null } ] ================================================ FILE: test/Kubernetes.Tests/testassets/https/ingress.yaml ================================================ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: minimal-ingress namespace: default annotations: yarp.ingress.kubernetes.io/backend-protocol: https spec: rules: - http: paths: - path: /foo pathType: Prefix backend: service: name: frontend port: number: 443 --- apiVersion: v1 kind: Service metadata: name: frontend namespace: default spec: selector: app: frontend ports: - name: https port: 443 targetPort: 443 type: ClusterIP --- apiVersion: v1 kind: Endpoints metadata: name: frontend namespace: default subsets: - addresses: - ip: 10.244.2.38 ports: - name: https port: 443 protocol: TCP ================================================ FILE: test/Kubernetes.Tests/testassets/https/routes.json ================================================ [ { "RouteId": "minimal-ingress.default:/foo", "Match": { "Methods": null, "Hosts": [], "Path": "/foo/{**catch-all}", "Headers": null, "QueryParameters": null }, "Order": null, "ClusterId": "frontend.default:443", "AuthorizationPolicy": null, "RateLimiterPolicy": null, "CorsPolicy": null, "Metadata": null, "Transforms": null } ] ================================================ FILE: test/Kubernetes.Tests/testassets/https-service-port-protocol/clusters.json ================================================ [ { "ClusterId": "frontend.default:443", "LoadBalancingPolicy": null, "SessionAffinity": null, "HealthCheck": null, "HttpClient": null, "HttpRequest": null, "Destinations": { "https://10.244.2.38:443": { "Address": "https://10.244.2.38:443", "Health": null, "Metadata": null } }, "Metadata": null }, { "ClusterId": "frontend.default:https", "LoadBalancingPolicy": null, "SessionAffinity": null, "HealthCheck": null, "HttpClient": null, "HttpRequest": null, "Destinations": { "https://10.244.2.38:443": { "Address": "https://10.244.2.38:443", "Health": null, "Metadata": null } }, "Metadata": null }, { "ClusterId": "frontend.default:80", "LoadBalancingPolicy": null, "SessionAffinity": null, "HealthCheck": null, "HttpClient": null, "HttpRequest": null, "Destinations": { "http://10.244.2.38:80": { "Address": "http://10.244.2.38:80", "Health": null, "Metadata": null } }, "Metadata": null } ] ================================================ FILE: test/Kubernetes.Tests/testassets/https-service-port-protocol/ingress.yaml ================================================ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: minimal-ingress namespace: default spec: rules: - http: paths: - path: /foo pathType: Prefix backend: service: name: frontend port: number: 443 - path: /fee pathType: Prefix backend: service: name: frontend port: name: https - path: /faa pathType: Prefix backend: service: name: frontend port: number: 80 --- apiVersion: v1 kind: Service metadata: name: frontend namespace: default spec: selector: app: frontend ports: - name: https port: 443 targetPort: 443 - name: http port: 80 targetPort: 80 type: ClusterIP --- apiVersion: v1 kind: Endpoints metadata: name: frontend namespace: default subsets: - addresses: - ip: 10.244.2.38 ports: - name: https port: 443 protocol: TCP - name: http port: 80 protocol: TCP ================================================ FILE: test/Kubernetes.Tests/testassets/https-service-port-protocol/routes.json ================================================ [ { "RouteId": "minimal-ingress.default:/foo", "Match": { "Methods": null, "Hosts": [], "Path": "/foo/{**catch-all}", "Headers": null, "QueryParameters": null }, "Order": null, "ClusterId": "frontend.default:443", "AuthorizationPolicy": null, "RateLimiterPolicy": null, "CorsPolicy": null, "Metadata": null, "Transforms": null }, { "RouteId": "minimal-ingress.default:/fee", "Match": { "Methods": null, "Hosts": [], "Path": "/fee/{**catch-all}", "Headers": null, "QueryParameters": null }, "Order": null, "ClusterId": "frontend.default:https", "AuthorizationPolicy": null, "RateLimiterPolicy": null, "CorsPolicy": null, "Metadata": null, "Transforms": null }, { "RouteId": "minimal-ingress.default:/faa", "Match": { "Methods": null, "Hosts": [], "Path": "/faa/{**catch-all}", "Headers": null, "QueryParameters": null }, "Order": null, "ClusterId": "frontend.default:80", "AuthorizationPolicy": null, "RateLimiterPolicy": null, "CorsPolicy": null, "Metadata": null, "Transforms": null } ] ================================================ FILE: test/Kubernetes.Tests/testassets/ingress-class-not-set/ingress.yaml ================================================ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: the-ingress namespace: default spec: rules: - http: paths: - path: / pathType: ImplementationSpecific backend: service: name: the-service port: name: http --- apiVersion: v1 kind: Service metadata: name: the-service namespace: default spec: selector: app: repro-1 type: ClusterIP ports: - name: http port: 80 targetPort: 80 --- apiVersion: v1 kind: Endpoints metadata: name: the-service namespace: default subsets: - addresses: - ip: 10.244.1.11 - ip: 10.244.1.12 ports: - name: http port: 80 protocol: TCP ================================================ FILE: test/Kubernetes.Tests/testassets/ingress-class-set/ingress.yaml ================================================ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: the-ingress namespace: default spec: ingressClassName: yarp rules: - http: paths: - path: / pathType: ImplementationSpecific backend: service: name: the-service port: name: http --- apiVersion: v1 kind: Service metadata: name: the-service namespace: default spec: selector: app: repro-1 type: ClusterIP ports: - name: http port: 80 targetPort: 80 --- apiVersion: v1 kind: Endpoints metadata: name: the-service namespace: default subsets: - addresses: - ip: 10.244.1.11 - ip: 10.244.1.12 ports: - name: http port: 80 protocol: TCP ================================================ FILE: test/Kubernetes.Tests/testassets/mapped-port/clusters.json ================================================ [ { "ClusterId": "backend.default:5011", "LoadBalancingPolicy": null, "SessionAffinity": null, "HealthCheck": null, "HttpClient": null, "HttpRequest": null, "Destinations": { "http://10.244.2.33:80": { "Address": "http://10.244.2.33:80", "Health": null, "Metadata": null } }, "Metadata": null } ] ================================================ FILE: test/Kubernetes.Tests/testassets/mapped-port/ingress.yaml ================================================ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: mapped-port namespace: default spec: rules: - http: paths: - path: /foo pathType: Prefix backend: service: name: backend port: number: 5011 --- apiVersion: v1 kind: Service metadata: name: backend namespace: default spec: selector: app: backend ports: - port: 5011 targetPort: 80 type: ClusterIP --- apiVersion: v1 kind: Endpoints metadata: name: backend namespace: default subsets: - addresses: - ip: 10.244.2.33 ports: - port: 80 protocol: TCP ================================================ FILE: test/Kubernetes.Tests/testassets/mapped-port/routes.json ================================================ [ { "RouteId": "mapped-port.default:/foo", "Match": { "Methods": null, "Hosts": [], "Path": "/foo/{**catch-all}", "Headers": null, "QueryParameters": null }, "Order": null, "ClusterId": "backend.default:5011", "AuthorizationPolicy": null, "RateLimiterPolicy": null, "CorsPolicy": null, "Metadata": null, "Transforms": null } ] ================================================ FILE: test/Kubernetes.Tests/testassets/missing-svc/clusters.json ================================================ [ ] ================================================ FILE: test/Kubernetes.Tests/testassets/missing-svc/ingress.yaml ================================================ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: missing-svc namespace: default spec: rules: - http: paths: - path: /foo pathType: Prefix backend: service: name: backend port: number: 80 --- apiVersion: v1 kind: Service metadata: name: frontend namespace: default spec: selector: app: frontend ports: - name: http port: 80 targetPort: 80 type: ClusterIP --- apiVersion: v1 kind: Endpoints metadata: name: frontend namespace: default subsets: - addresses: - ip: 10.244.2.38 ports: - name: http port: 80 protocol: TCP ================================================ FILE: test/Kubernetes.Tests/testassets/missing-svc/routes.json ================================================ [ ] ================================================ FILE: test/Kubernetes.Tests/testassets/multiple-endpoints-ports/clusters.json ================================================ [ { "ClusterId": "frontend.default:80", "LoadBalancingPolicy": null, "SessionAffinity": null, "HealthCheck": null, "HttpClient": null, "HttpRequest": null, "Destinations": { "http://10.244.2.38:80": { "Address": "http://10.244.2.38:80", "Health": null, "Metadata": null } }, "Metadata": null } ] ================================================ FILE: test/Kubernetes.Tests/testassets/multiple-endpoints-ports/ingress.yaml ================================================ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: minimal-ingress namespace: default spec: rules: - http: paths: - path: /foo pathType: Prefix backend: service: name: frontend port: number: 80 --- apiVersion: v1 kind: Service metadata: name: frontend namespace: default spec: selector: app: frontend ports: - name: http port: 80 targetPort: 80 - name: https port: 443 targetPort: 443 type: ClusterIP --- apiVersion: v1 kind: Endpoints metadata: name: frontend namespace: default subsets: - addresses: - ip: 10.244.2.38 ports: - name: http port: 80 protocol: TCP - name: https port: 443 protocol: TCP ================================================ FILE: test/Kubernetes.Tests/testassets/multiple-endpoints-ports/routes.json ================================================ [ { "RouteId": "minimal-ingress.default:/foo", "Match": { "Methods": null, "Hosts": [], "Path": "/foo/{**catch-all}", "Headers": null, "QueryParameters": null }, "Order": null, "ClusterId": "frontend.default:80", "AuthorizationPolicy": null, "RateLimiterPolicy": null, "CorsPolicy": null, "Metadata": null, "Transforms": null } ] ================================================ FILE: test/Kubernetes.Tests/testassets/multiple-endpoints-same-port/clusters.json ================================================ [ { "ClusterId": "frontend.default:80", "LoadBalancingPolicy": null, "SessionAffinity": null, "HealthCheck": null, "HttpClient": null, "HttpRequest": null, "Destinations": { "http://10.244.2.38:80": { "Address": "http://10.244.2.38:80", "Health": null, "Metadata": null } }, "Metadata": null } ] ================================================ FILE: test/Kubernetes.Tests/testassets/multiple-endpoints-same-port/ingress.yaml ================================================ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: minimal-ingress namespace: default spec: rules: - http: paths: - path: /foo pathType: Prefix backend: service: name: frontend port: number: 80 --- apiVersion: v1 kind: Service metadata: name: frontend namespace: default spec: selector: app: frontend ports: - name: http port: 80 targetPort: 80 - name: other_http port: 8080 targetPort: 80 type: ClusterIP --- apiVersion: v1 kind: Endpoints metadata: name: frontend namespace: default subsets: - addresses: - ip: 10.244.2.38 ports: - name: http port: 80 protocol: TCP - name: other_http port: 80 protocol: TCP ================================================ FILE: test/Kubernetes.Tests/testassets/multiple-endpoints-same-port/routes.json ================================================ [ { "RouteId": "minimal-ingress.default:/foo", "Match": { "Methods": null, "Hosts": [], "Path": "/foo/{**catch-all}", "Headers": null, "QueryParameters": null }, "Order": null, "ClusterId": "frontend.default:80", "AuthorizationPolicy": null, "RateLimiterPolicy": null, "CorsPolicy": null, "Metadata": null, "Transforms": null } ] ================================================ FILE: test/Kubernetes.Tests/testassets/multiple-hosts/clusters.json ================================================ [ { "ClusterId": "repro-1-service.foo:http", "LoadBalancingPolicy": null, "SessionAffinity": null, "HealthCheck": null, "HttpClient": null, "HttpRequest": null, "Destinations": { "http://10.244.1.11:80": { "Address": "http://10.244.1.11:80", "Health": null, "Metadata": null } }, "Metadata": null }, { "ClusterId": "repro-2-service.foo:http", "LoadBalancingPolicy": null, "SessionAffinity": null, "HealthCheck": null, "HttpClient": null, "HttpRequest": null, "Destinations": { "http://10.244.2.22:80": { "Address": "http://10.244.2.22:80", "Health": null, "Metadata": null } }, "Metadata": null }, { "ClusterId": "repro-3-service.foo:http", "LoadBalancingPolicy": null, "SessionAffinity": null, "HealthCheck": null, "HttpClient": null, "HttpRequest": null, "Destinations": { "http://10.244.3.33:80": { "Address": "http://10.244.3.33:80", "Health": null, "Metadata": null } }, "Metadata": null } ] ================================================ FILE: test/Kubernetes.Tests/testassets/multiple-hosts/ingress.yaml ================================================ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: repro-1-ingress namespace: foo spec: rules: - host: 'subdomain1.example.com' http: paths: - path: / pathType: Prefix backend: service: name: repro-1-service port: name: http - host: 'subdomain2.example.com' http: paths: - path: / pathType: Prefix backend: service: name: repro-2-service port: name: http - http: paths: - path: / pathType: Prefix backend: service: name: repro-3-service port: name: http --- apiVersion: v1 kind: Service metadata: name: repro-1-service namespace: foo spec: selector: app: repro-1 type: ClusterIP ports: - name: http port: 80 targetPort: 80 --- apiVersion: v1 kind: Endpoints metadata: name: repro-1-service namespace: foo subsets: - addresses: - ip: 10.244.1.11 ports: - name: http port: 80 protocol: TCP --- apiVersion: v1 kind: Service metadata: name: repro-2-service namespace: foo spec: selector: app: repro-2 type: ClusterIP ports: - name: http port: 80 targetPort: 80 --- apiVersion: v1 kind: Endpoints metadata: name: repro-2-service namespace: foo subsets: - addresses: - ip: 10.244.2.22 ports: - name: http port: 80 protocol: TCP --- apiVersion: v1 kind: Service metadata: name: repro-3-service namespace: foo spec: selector: app: repro-2 type: ClusterIP ports: - name: http port: 80 targetPort: 80 --- apiVersion: v1 kind: Endpoints metadata: name: repro-3-service namespace: foo subsets: - addresses: - ip: 10.244.3.33 ports: - name: http port: 80 protocol: TCP ================================================ FILE: test/Kubernetes.Tests/testassets/multiple-hosts/routes.json ================================================ [ { "RouteId": "repro-1-ingress.foo:subdomain1.example.com/", "Match": { "Methods": null, "Hosts": [ "subdomain1.example.com" ], "Path": "/{**catch-all}", "Headers": null, "QueryParameters": null }, "Order": null, "ClusterId": "repro-1-service.foo:http", "AuthorizationPolicy": null, "RateLimiterPolicy": null, "CorsPolicy": null, "Metadata": null, "Transforms": null }, { "RouteId": "repro-1-ingress.foo:subdomain2.example.com/", "Match": { "Methods": null, "Hosts": [ "subdomain2.example.com" ], "Path": "/{**catch-all}", "Headers": null, "QueryParameters": null }, "Order": null, "ClusterId": "repro-2-service.foo:http", "AuthorizationPolicy": null, "RateLimiterPolicy": null, "CorsPolicy": null, "Metadata": null, "Transforms": null }, { "RouteId": "repro-1-ingress.foo:/", "Match": { "Methods": null, "Hosts": [], "Path": "/{**catch-all}", "Headers": null, "QueryParameters": null }, "Order": null, "ClusterId": "repro-3-service.foo:http", "AuthorizationPolicy": null, "RateLimiterPolicy": null, "CorsPolicy": null, "Metadata": null, "Transforms": null } ] ================================================ FILE: test/Kubernetes.Tests/testassets/multiple-ingresses/clusters.json ================================================ [ { "ClusterId": "repro-1-service.foo:http", "LoadBalancingPolicy": null, "SessionAffinity": null, "HealthCheck": null, "HttpClient": null, "HttpRequest": null, "Destinations": { "http://10.244.1.11:80": { "Address": "http://10.244.1.11:80", "Health": null, "Metadata": null }, "http://10.244.1.12:80": { "Address": "http://10.244.1.12:80", "Health": null, "Metadata": null } }, "Metadata": null }, { "ClusterId": "repro-2-service.foo:http", "LoadBalancingPolicy": null, "SessionAffinity": null, "HealthCheck": null, "HttpClient": null, "HttpRequest": null, "Destinations": { "http://10.244.2.22:80": { "Address": "http://10.244.2.22:80", "Health": null, "Metadata": null }, "http://10.244.2.23:80": { "Address": "http://10.244.2.23:80", "Health": null, "Metadata": null } }, "Metadata": null } ] ================================================ FILE: test/Kubernetes.Tests/testassets/multiple-ingresses/ingress.yaml ================================================ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: repro-1-ingress namespace: foo spec: rules: - host: 'subdomain1.example.com' http: paths: - path: / pathType: ImplementationSpecific backend: service: name: repro-1-service port: name: http --- apiVersion: v1 kind: Service metadata: name: repro-1-service namespace: foo spec: selector: app: repro-1 type: ClusterIP ports: - name: http port: 80 targetPort: 80 --- apiVersion: v1 kind: Endpoints metadata: name: repro-1-service namespace: foo subsets: - addresses: - ip: 10.244.1.11 - ip: 10.244.1.12 ports: - name: http port: 80 protocol: TCP --- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: repro-2-ingress namespace: foo spec: rules: - host: 'subdomain2.example.com' http: paths: - path: / pathType: ImplementationSpecific backend: service: name: repro-2-service port: name: http --- apiVersion: v1 kind: Service metadata: name: repro-2-service namespace: foo spec: selector: app: repro-2 type: ClusterIP ports: - name: http port: 80 targetPort: 80 --- apiVersion: v1 kind: Endpoints metadata: name: repro-2-service namespace: foo subsets: - addresses: - ip: 10.244.2.22 - ip: 10.244.2.23 ports: - name: http port: 80 protocol: TCP ================================================ FILE: test/Kubernetes.Tests/testassets/multiple-ingresses/routes.json ================================================ [ { "RouteId": "repro-1-ingress.foo:subdomain1.example.com/", "Match": { "Methods": null, "Hosts": [ "subdomain1.example.com" ], "Path": "/{**catch-all}", "Headers": null, "QueryParameters": null }, "Order": null, "ClusterId": "repro-1-service.foo:http", "AuthorizationPolicy": null, "RateLimiterPolicy": null, "CorsPolicy": null, "Metadata": null, "Transforms": null }, { "RouteId": "repro-2-ingress.foo:subdomain2.example.com/", "Match": { "Methods": null, "Hosts": [ "subdomain2.example.com" ], "Path": "/{**catch-all}", "Headers": null, "QueryParameters": null }, "Order": null, "ClusterId": "repro-2-service.foo:http", "AuthorizationPolicy": null, "RateLimiterPolicy": null, "CorsPolicy": null, "Metadata": null, "Transforms": null } ] ================================================ FILE: test/Kubernetes.Tests/testassets/multiple-ingresses-one-svc/clusters.json ================================================ [ { "ClusterId": "repro-service.foo:http", "LoadBalancingPolicy": null, "SessionAffinity": null, "HealthCheck": null, "HttpClient": null, "HttpRequest": null, "Destinations": { "http://10.244.1.11:80": { "Address": "http://10.244.1.11:80", "Health": null, "Metadata": null }, "http://10.244.1.12:80": { "Address": "http://10.244.1.12:80", "Health": null, "Metadata": null } }, "Metadata": null } ] ================================================ FILE: test/Kubernetes.Tests/testassets/multiple-ingresses-one-svc/ingress.yaml ================================================ apiVersion: v1 kind: Service metadata: name: repro-service namespace: foo spec: selector: app: repro-1 type: ClusterIP ports: - name: http port: 80 targetPort: 80 --- apiVersion: v1 kind: Endpoints metadata: name: repro-service namespace: foo subsets: - addresses: - ip: 10.244.1.11 - ip: 10.244.1.12 ports: - name: http port: 80 protocol: TCP --- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: repro-1-ingress namespace: foo spec: rules: - host: 'subdomain1.example.com' http: paths: - path: / pathType: ImplementationSpecific backend: service: name: repro-service port: name: http --- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: repro-2-ingress namespace: foo spec: rules: - host: 'subdomain2.example.com' http: paths: - path: / pathType: ImplementationSpecific backend: service: name: repro-service port: name: http ================================================ FILE: test/Kubernetes.Tests/testassets/multiple-ingresses-one-svc/routes.json ================================================ [ { "RouteId": "repro-1-ingress.foo:subdomain1.example.com/", "Match": { "Methods": null, "Hosts": [ "subdomain1.example.com" ], "Path": "/{**catch-all}", "Headers": null, "QueryParameters": null }, "Order": null, "ClusterId": "repro-service.foo:http", "AuthorizationPolicy": null, "RateLimiterPolicy": null, "CorsPolicy": null, "Metadata": null, "Transforms": null }, { "RouteId": "repro-2-ingress.foo:subdomain2.example.com/", "Match": { "Methods": null, "Hosts": [ "subdomain2.example.com" ], "Path": "/{**catch-all}", "Headers": null, "QueryParameters": null }, "Order": null, "ClusterId": "repro-service.foo:http", "AuthorizationPolicy": null, "RateLimiterPolicy": null, "CorsPolicy": null, "Metadata": null, "Transforms": null } ] ================================================ FILE: test/Kubernetes.Tests/testassets/multiple-namespaces/clusters.json ================================================ [ { "ClusterId": "the-service.ns-one:http", "LoadBalancingPolicy": null, "SessionAffinity": null, "HealthCheck": null, "HttpClient": null, "HttpRequest": null, "Destinations": { "http://10.244.1.11:80": { "Address": "http://10.244.1.11:80", "Health": null, "Metadata": null }, "http://10.244.1.12:80": { "Address": "http://10.244.1.12:80", "Health": null, "Metadata": null } }, "Metadata": null }, { "ClusterId": "the-service.ns-two:http", "LoadBalancingPolicy": null, "SessionAffinity": null, "HealthCheck": null, "HttpClient": null, "HttpRequest": null, "Destinations": { "http://10.244.2.22:80": { "Address": "http://10.244.2.22:80", "Health": null, "Metadata": null }, "http://10.244.2.23:80": { "Address": "http://10.244.2.23:80", "Health": null, "Metadata": null } }, "Metadata": null } ] ================================================ FILE: test/Kubernetes.Tests/testassets/multiple-namespaces/ingress.yaml ================================================ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: the-ingress namespace: ns-one spec: rules: - host: 'subdomain1.example.com' http: paths: - path: / pathType: ImplementationSpecific backend: service: name: the-service port: name: http --- apiVersion: v1 kind: Service metadata: name: the-service namespace: ns-one spec: selector: app: repro-1 type: ClusterIP ports: - name: http port: 80 targetPort: 80 --- apiVersion: v1 kind: Endpoints metadata: name: the-service namespace: ns-one subsets: - addresses: - ip: 10.244.1.11 - ip: 10.244.1.12 ports: - name: http port: 80 protocol: TCP --- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: the-ingress namespace: ns-two spec: rules: - host: 'subdomain2.example.com' http: paths: - path: / pathType: ImplementationSpecific backend: service: name: the-service port: name: http --- apiVersion: v1 kind: Service metadata: name: the-service namespace: ns-two spec: selector: app: repro-2 type: ClusterIP ports: - name: http port: 80 targetPort: http --- apiVersion: v1 kind: Endpoints metadata: name: the-service namespace: ns-two subsets: - addresses: - ip: 10.244.2.22 - ip: 10.244.2.23 ports: - name: http port: 80 protocol: TCP ================================================ FILE: test/Kubernetes.Tests/testassets/multiple-namespaces/routes.json ================================================ [ { "RouteId": "the-ingress.ns-one:subdomain1.example.com/", "Match": { "Methods": null, "Hosts": [ "subdomain1.example.com" ], "Path": "/{**catch-all}", "Headers": null, "QueryParameters": null }, "Order": null, "ClusterId": "the-service.ns-one:http", "AuthorizationPolicy": null, "RateLimiterPolicy": null, "CorsPolicy": null, "Metadata": null, "Transforms": null }, { "RouteId": "the-ingress.ns-two:subdomain2.example.com/", "Match": { "Methods": null, "Hosts": [ "subdomain2.example.com" ], "Path": "/{**catch-all}", "Headers": null, "QueryParameters": null }, "Order": null, "ClusterId": "the-service.ns-two:http", "AuthorizationPolicy": null, "RateLimiterPolicy": null, "CorsPolicy": null, "Metadata": null, "Transforms": null } ] ================================================ FILE: test/Kubernetes.Tests/testassets/port-diff-name/clusters.json ================================================ [ { "ClusterId": "backend.default:88", "LoadBalancingPolicy": null, "SessionAffinity": null, "HealthCheck": null, "HttpClient": null, "HttpRequest": null, "Destinations": { "http://10.244.2.33:80": { "Address": "http://10.244.2.33:80", "Health": null, "Metadata": null } }, "Metadata": null } ] ================================================ FILE: test/Kubernetes.Tests/testassets/port-diff-name/ingress.yaml ================================================ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: port-diff-name namespace: default spec: rules: - http: paths: - path: /foo pathType: Prefix backend: service: name: backend port: number: 88 --- apiVersion: v1 kind: Service metadata: name: backend namespace: default spec: selector: app: backend ports: - port: 88 name: my-http targetPort: http type: ClusterIP --- apiVersion: v1 kind: Endpoints metadata: name: backend namespace: default subsets: - addresses: - ip: 10.244.2.33 ports: - port: 80 protocol: TCP name: my-http ================================================ FILE: test/Kubernetes.Tests/testassets/port-diff-name/routes.json ================================================ [ { "RouteId": "port-diff-name.default:/foo", "Match": { "Methods": null, "Hosts": [], "Path": "/foo/{**catch-all}", "Headers": null, "QueryParameters": null }, "Order": null, "ClusterId": "backend.default:88", "AuthorizationPolicy": null, "CorsPolicy": null, "Metadata": null, "Transforms": null } ] ================================================ FILE: test/Kubernetes.Tests/testassets/port-mismatch/clusters.json ================================================ [ ] ================================================ FILE: test/Kubernetes.Tests/testassets/port-mismatch/ingress.yaml ================================================ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: port-mismatch namespace: default spec: rules: - http: paths: - path: /foo pathType: Prefix backend: service: name: backend port: number: 5011 --- apiVersion: v1 kind: Service metadata: name: backend namespace: default spec: selector: app: backend ports: - port: 80 targetPort: 80 type: ClusterIP --- apiVersion: v1 kind: Endpoints metadata: name: backend namespace: default subsets: - addresses: - ip: 10.244.2.33 ports: - port: 80 protocol: TCP ================================================ FILE: test/Kubernetes.Tests/testassets/port-mismatch/routes.json ================================================ [ ] ================================================ FILE: test/Kubernetes.Tests/testassets/resource-informer/ResourcesAreListedWhenReadyAsyncIsComplete/resources.yaml ================================================ # pods - apiVersion: v1 kind: Pod metadata: name: pod-1 namespace: the-namespace spec: containers: - name: test image: test - apiVersion: v1 kind: Pod metadata: name: pod-2 namespace: the-namespace spec: containers: - name: test image: test ================================================ FILE: test/Kubernetes.Tests/testassets/resource-informer/ResourcesAreListedWhenReadyAsyncIsComplete/shouldbe.yaml ================================================ - namespace: the-namespace name: pod-1 - namespace: the-namespace name: pod-2 ================================================ FILE: test/Kubernetes.Tests/testassets/resource-informer/ResourcesWithApiGroupAreListed/resources.yaml ================================================ # pods - apiVersion: apps/v1 kind: Deployment metadata: name: deployment-1 namespace: the-namespace spec: template: spec: containers: - name: test image: test - apiVersion: apps/v1 kind: Deployment metadata: name: deployment-2 namespace: the-namespace spec: template: spec: containers: - name: test image: test ================================================ FILE: test/Kubernetes.Tests/testassets/resource-informer/ResourcesWithApiGroupAreListed/shouldbe.yaml ================================================ - namespace: the-namespace name: deployment-1 - namespace: the-namespace name: deployment-2 ================================================ FILE: test/Kubernetes.Tests/testassets/route-headers/clusters.json ================================================ [ { "ClusterId": "frontend.default:80", "LoadBalancingPolicy": null, "SessionAffinity": null, "HealthCheck": null, "HttpClient": null, "HttpRequest": null, "Destinations": { "http://10.244.2.38:80": { "Address": "http://10.244.2.38:80", "Health": null, "Metadata": null } }, "Metadata": null } ] ================================================ FILE: test/Kubernetes.Tests/testassets/route-headers/ingress.yaml ================================================ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: minimal-ingress namespace: default annotations: yarp.ingress.kubernetes.io/route-metadata: | foo: bar another-key: another-value yarp.ingress.kubernetes.io/route-headers: | - Name: the-header-key Values: - the-header-value Mode: Contains IsCaseSensitive: false spec: rules: - http: paths: - path: /foo pathType: Prefix backend: service: name: frontend port: number: 80 --- apiVersion: v1 kind: Service metadata: name: frontend namespace: default spec: selector: app: frontend ports: - name: https port: 80 targetPort: 80 type: ClusterIP --- apiVersion: v1 kind: Endpoints metadata: name: frontend namespace: default subsets: - addresses: - ip: 10.244.2.38 ports: - name: https port: 80 protocol: TCP ================================================ FILE: test/Kubernetes.Tests/testassets/route-headers/routes.json ================================================ [ { "RouteId": "minimal-ingress.default:/foo", "Match": { "Methods": null, "Hosts": [], "Path": "/foo/{**catch-all}", "Headers": [ { "Name": "the-header-key", "Values": [ "the-header-value" ], "Mode": "Contains", "IsCaseSensitive": false } ], "QueryParameters": null }, "Order": null, "ClusterId": "frontend.default:80", "AuthorizationPolicy": null, "RateLimiterPolicy": null, "CorsPolicy": null, "Metadata": { "foo": "bar", "another-key": "another-value" }, "Transforms": null } ] ================================================ FILE: test/Kubernetes.Tests/testassets/route-metadata/clusters.json ================================================ [ { "ClusterId": "frontend.default:80", "LoadBalancingPolicy": null, "SessionAffinity": null, "HealthCheck": null, "HttpClient": null, "HttpRequest": null, "Destinations": { "http://10.244.2.38:80": { "Address": "http://10.244.2.38:80", "Health": null, "Metadata": null } }, "Metadata": null } ] ================================================ FILE: test/Kubernetes.Tests/testassets/route-metadata/ingress.yaml ================================================ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: minimal-ingress namespace: default annotations: yarp.ingress.kubernetes.io/route-metadata: | foo: bar another-key: another-value spec: rules: - http: paths: - path: /foo pathType: Prefix backend: service: name: frontend port: number: 80 --- apiVersion: v1 kind: Service metadata: name: frontend namespace: default spec: selector: app: frontend ports: - name: https port: 80 targetPort: 80 type: ClusterIP --- apiVersion: v1 kind: Endpoints metadata: name: frontend namespace: default subsets: - addresses: - ip: 10.244.2.38 ports: - name: https port: 80 protocol: TCP ================================================ FILE: test/Kubernetes.Tests/testassets/route-metadata/routes.json ================================================ [ { "RouteId": "minimal-ingress.default:/foo", "Match": { "Methods": null, "Hosts": [], "Path": "/foo/{**catch-all}", "Headers": null, "QueryParameters": null }, "Order": null, "ClusterId": "frontend.default:80", "AuthorizationPolicy": null, "RateLimiterPolicy": null, "CorsPolicy": null, "Metadata": { "foo": "bar", "another-key": "another-value" }, "Transforms": null } ] ================================================ FILE: test/Kubernetes.Tests/testassets/route-methods/clusters.json ================================================ [ { "ClusterId": "frontend.default:80", "LoadBalancingPolicy": null, "SessionAffinity": null, "HealthCheck": null, "HttpClient": null, "HttpRequest": null, "Destinations": { "http://10.244.2.38:80": { "Address": "http://10.244.2.38:80", "Health": null, "Metadata": null } }, "Metadata": null } ] ================================================ FILE: test/Kubernetes.Tests/testassets/route-methods/ingress.yaml ================================================ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: first-ingress namespace: default annotations: yarp.ingress.kubernetes.io/route-methods: | - GET spec: rules: - http: paths: - path: /foo pathType: Prefix backend: service: name: frontend port: number: 80 --- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: second-ingress namespace: default annotations: yarp.ingress.kubernetes.io/route-methods: | - POST - PUT spec: rules: - http: paths: - path: /foo pathType: Prefix backend: service: name: frontend port: number: 80 --- apiVersion: v1 kind: Service metadata: name: frontend namespace: default spec: selector: app: frontend ports: - name: https port: 80 targetPort: 80 type: ClusterIP --- apiVersion: v1 kind: Endpoints metadata: name: frontend namespace: default subsets: - addresses: - ip: 10.244.2.38 ports: - name: https port: 80 protocol: TCP ================================================ FILE: test/Kubernetes.Tests/testassets/route-methods/routes.json ================================================ [ { "RouteId": "first-ingress.default:/foo", "Match": { "Methods": ["GET"], "Hosts": [], "Path": "/foo/{**catch-all}", "Headers": null, "QueryParameters": null }, "ClusterId": "frontend.default:80", "AuthorizationPolicy": null, "RateLimiterPolicy": null, "CorsPolicy": null, "Metadata": null, "Transforms": null }, { "RouteId": "second-ingress.default:/foo", "Match": { "Methods": ["POST", "PUT"], "Hosts": [], "Path": "/foo/{**catch-all}", "Headers": null, "QueryParameters": null }, "ClusterId": "frontend.default:80", "AuthorizationPolicy": null, "RateLimiterPolicy": null, "CorsPolicy": null, "Metadata": null, "Transforms": null } ] ================================================ FILE: test/Kubernetes.Tests/testassets/route-order/clusters.json ================================================ [ { "ClusterId": "frontend.default:80", "LoadBalancingPolicy": null, "SessionAffinity": null, "HealthCheck": null, "HttpClient": null, "HttpRequest": null, "Destinations": { "http://10.244.2.38:80": { "Address": "http://10.244.2.38:80", "Health": null, "Metadata": null } }, "Metadata": null } ] ================================================ FILE: test/Kubernetes.Tests/testassets/route-order/ingress.yaml ================================================ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: minimal-ingress namespace: default annotations: yarp.ingress.kubernetes.io/route-order: '10' spec: rules: - http: paths: - path: /foo pathType: Prefix backend: service: name: frontend port: number: 80 --- apiVersion: v1 kind: Service metadata: name: frontend namespace: default spec: selector: app: frontend ports: - name: https port: 80 targetPort: 80 type: ClusterIP --- apiVersion: v1 kind: Endpoints metadata: name: frontend namespace: default subsets: - addresses: - ip: 10.244.2.38 ports: - name: https port: 80 protocol: TCP ================================================ FILE: test/Kubernetes.Tests/testassets/route-order/routes.json ================================================ [ { "RouteId": "minimal-ingress.default:/foo", "Match": { "Methods": null, "Hosts": [], "Path": "/foo/{**catch-all}", "Headers": null, "QueryParameters": null }, "Order": 10, "ClusterId": "frontend.default:80", "AuthorizationPolicy": null, "RateLimiterPolicy": null, "CorsPolicy": null, "Metadata": null, "Transforms": null } ] ================================================ FILE: test/Kubernetes.Tests/testassets/route-queryparameters/clusters.json ================================================ [ { "ClusterId": "frontend.default:80", "LoadBalancingPolicy": null, "SessionAffinity": null, "HealthCheck": null, "HttpClient": null, "HttpRequest": null, "Destinations": { "http://10.244.2.38:80": { "Address": "http://10.244.2.38:80", "Health": null, "Metadata": null } }, "Metadata": null } ] ================================================ FILE: test/Kubernetes.Tests/testassets/route-queryparameters/ingress.yaml ================================================ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: minimal-ingress namespace: default annotations: yarp.ingress.kubernetes.io/route-metadata: | foo: bar another-key: another-value yarp.ingress.kubernetes.io/route-queryparameters: | - Name: the-queryparameter-key Values: - the-queryparameter-value Mode: Contains IsCaseSensitive: false spec: rules: - http: paths: - path: /foo pathType: Prefix backend: service: name: frontend port: number: 80 --- apiVersion: v1 kind: Service metadata: name: frontend namespace: default spec: selector: app: frontend ports: - name: https port: 80 targetPort: 80 type: ClusterIP --- apiVersion: v1 kind: Endpoints metadata: name: frontend namespace: default subsets: - addresses: - ip: 10.244.2.38 ports: - name: https port: 80 protocol: TCP ================================================ FILE: test/Kubernetes.Tests/testassets/route-queryparameters/routes.json ================================================ [ { "RouteId": "minimal-ingress.default:/foo", "Match": { "Methods": null, "Hosts": [], "Path": "/foo/{**catch-all}", "Headers": null, "QueryParameters": [ { "Name": "the-queryparameter-key", "Values": [ "the-queryparameter-value" ], "Mode": "Contains", "IsCaseSensitive": false } ] }, "Order": null, "ClusterId": "frontend.default:80", "AuthorizationPolicy": null, "RateLimiterPolicy": null, "CorsPolicy": null, "Metadata": { "foo": "bar", "another-key": "another-value" }, "Transforms": null } ] ================================================ FILE: test/ReverseProxy.FunctionalTests/Common/Helpers.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Hosting.Server.Features; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.DependencyInjection; using System.Linq; namespace Yarp.ReverseProxy; public static class Helpers { public static string GetAddress(this IHost server) { return server.Services.GetService().Features.Get().Addresses.First(); } } ================================================ FILE: test/ReverseProxy.FunctionalTests/Common/HttpSysTestEnvironment.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Server.HttpSys; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Yarp.ReverseProxy.Configuration; namespace Yarp.ReverseProxy.Common; public class HttpSysTestEnvironment { private readonly Action _configureDestinationServices; private readonly Action _configureDestinationHttpSysOptions; private readonly Action _configureDestinationApp; private readonly Action _configureProxyServices; private readonly Action _configureProxy; private readonly Action _configureProxyApp; private readonly Action _configureProxyPipeline; private readonly Func _configTransformer; public string ClusterId { get; set; } = "cluster1"; public HttpSysTestEnvironment( Action configureDestinationServices, Action configureDestinationHttpSysOptions, Action configureDestinationApp, Action configureProxyServices, Action configureProxy, Action configureProxyApp, Action configureProxyPipeline, Func configTransformer = null) { _configureDestinationServices = configureDestinationServices; _configureDestinationHttpSysOptions = configureDestinationHttpSysOptions; _configureDestinationApp = configureDestinationApp; _configureProxy = configureProxy; _configureProxyApp = configureProxyApp; _configureProxyPipeline = configureProxyPipeline; _configureProxyServices = configureProxyServices ?? (_ => { }); _configTransformer = configTransformer ?? ((ClusterConfig c, RouteConfig r) => (c, r)); } public async Task Invoke(Func clientFunc, CancellationToken cancellationToken = default) { using var destination = CreateHttpSysHost(_configureDestinationServices, _configureDestinationHttpSysOptions, _configureDestinationApp); await destination.StartAsync(cancellationToken); using var proxy = CreateHttpSysProxy( ClusterId, destination.GetAddress(), _configureProxyServices, _configureProxy, _configureProxyApp, _configureProxyPipeline, _configTransformer); await proxy.StartAsync(cancellationToken); try { await clientFunc(proxy.GetAddress()); } finally { await proxy.StopAsync(cancellationToken); await destination.StopAsync(cancellationToken); } } private static IHost CreateHttpSysProxy( string clusterId, string destinationAddress, Action configureServices, Action configureProxy, Action configureProxyApp, Action configureProxyPipeline, Func configTransformer) { return CreateHttpSysHost( services => { configureServices(services); var route = new RouteConfig { RouteId = "route1", ClusterId = clusterId, Match = new RouteMatch { Path = "/{**catchall}" } }; var cluster = new ClusterConfig { ClusterId = clusterId, Destinations = new Dictionary(StringComparer.OrdinalIgnoreCase) { { "destination1", new DestinationConfig() { Address = destinationAddress } } }, }; (cluster, route) = configTransformer(cluster, route); var proxyBuilder = services.AddReverseProxy().LoadFromMemory(new[] { route }, new[] { cluster }); configureProxy(proxyBuilder); }, httpSysOptions => { }, app => { configureProxyApp(app); app.UseRouting(); app.UseEndpoints(builder => { if (configureProxyPipeline != null) { builder.MapReverseProxy(configureProxyPipeline); } else { builder.MapReverseProxy(); } }); }); } private static IHost CreateHttpSysHost( Action configureServices, Action configureHttpSys, Action configureApp) { return CreateHost(webHostBuilder => { Debug.Assert(OperatingSystem.IsWindows()); webHostBuilder .ConfigureServices(configureServices) .UseHttpSys(options => { options.UrlPrefixes.Add(UrlPrefix.Create("http", "localhost", "0", "/")); configureHttpSys(options); }) .Configure(configureApp); }); } private static IHost CreateHost(Action configureWebHost) { return new HostBuilder() .ConfigureAppConfiguration(config => { config.AddInMemoryCollection(new Dictionary() { { "Logging:LogLevel:Microsoft.AspNetCore.Hosting.Diagnostics", "Information" } }); }) .ConfigureLogging((hostingContext, loggingBuilder) => { loggingBuilder.AddConfiguration(hostingContext.Configuration.GetSection("Logging")); loggingBuilder.AddEventSourceLogger(); }) .ConfigureWebHost(configureWebHost) .Build(); } } ================================================ FILE: test/ReverseProxy.FunctionalTests/Common/TestEnvironment.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Net; using System.Net.Http; using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Server.HttpSys; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Xunit; using Yarp.ReverseProxy.Configuration; using Yarp.Tests.Common; namespace Yarp.ReverseProxy.Common; public class TestEnvironment { public ITestOutputHelper TestOutput { get; set; } public HttpProtocols ProxyProtocol { get; set; } = HttpProtocols.Http1AndHttp2; public bool UseHttpsOnProxy { get; set; } public Encoding HeaderEncoding { get; set; } public Action ConfigureProxyServices { get; set; } = _ => { }; public Action ConfigureProxy { get; set; } = _ => { }; public Action ConfigureProxyApp { get; set; } = _ => { }; public string ClusterId { get; set; } = "cluster1"; public Func ConfigTransformer { get; set; } = (a, b) => (a, b); public Version DestinationHttpVersion { get; set; } public HttpVersionPolicy? DestinationHttpVersionPolicy { get; set; } public HttpProtocols DestinationProtocol { get; set; } = HttpProtocols.Http1AndHttp2; public bool UseHttpsOnDestination { get; set; } public bool UseHttpSysOnDestination { get; set; } public Action ConfigureDestinationServices { get; set; } = _ => { }; public Action ConfigureDestinationApp { get; set; } = _ => { }; public TestEnvironment() { } public TestEnvironment(RequestDelegate destinationGetDelegate) { ConfigureDestinationApp = destinationApp => { destinationApp.Run(destinationGetDelegate); }; } public async Task Invoke(Func clientFunc, CancellationToken cancellationToken = default) { using var destination = CreateHost(DestinationProtocol, UseHttpsOnDestination, HeaderEncoding, ConfigureDestinationServices, ConfigureDestinationApp, UseHttpSysOnDestination); await destination.StartAsync(cancellationToken); Exception proxyException = null; using var proxy = CreateProxy(destination.GetAddress(), ex => proxyException = ex); await proxy.StartAsync(cancellationToken); try { await clientFunc(proxy.GetAddress()); } finally { await proxy.StopAsync(cancellationToken); await destination.StopAsync(cancellationToken); } Assert.Null(proxyException); } public IHost CreateProxy(string destinationAddress, Action onProxyException = null) { return CreateHost(ProxyProtocol, UseHttpsOnProxy, HeaderEncoding, services => { ConfigureProxyServices(services); var route = new RouteConfig { RouteId = "route1", ClusterId = ClusterId, Match = new RouteMatch { Path = "/{**catchall}" } }; var cluster = new ClusterConfig { ClusterId = ClusterId, Destinations = new Dictionary(StringComparer.OrdinalIgnoreCase) { { "destination1", new DestinationConfig() { Address = destinationAddress } } }, HttpClient = new HttpClientConfig { DangerousAcceptAnyServerCertificate = UseHttpsOnDestination, RequestHeaderEncoding = HeaderEncoding?.WebName, }, HttpRequest = new Forwarder.ForwarderRequestConfig { Version = DestinationHttpVersion, VersionPolicy = DestinationHttpVersionPolicy, } }; (cluster, route) = ConfigTransformer(cluster, route); var proxyBuilder = services.AddReverseProxy().LoadFromMemory(new[] { route }, new[] { cluster }); ConfigureProxy(proxyBuilder); }, app => { app.Use(async (context, next) => { try { await next(); } catch (Exception ex) { onProxyException?.Invoke(ex); throw; } }); ConfigureProxyApp(app); app.UseRouting(); app.UseEndpoints(builder => { builder.MapReverseProxy(); }); }); } private IHost CreateHost(HttpProtocols protocols, bool useHttps, Encoding requestHeaderEncoding, Action configureServices, Action configureApp, bool useHttpSys = false) { return new HostBuilder() .ConfigureAppConfiguration(config => { config.AddInMemoryCollection(new Dictionary() { { "Logging:LogLevel:Yarp", "Trace" }, { "Logging:LogLevel:Microsoft", "Trace" }, { "Logging:LogLevel:Microsoft.AspNetCore.Hosting.Diagnostics", "Information" } }); }) .ConfigureLogging((hostingContext, loggingBuilder) => { loggingBuilder.AddConfiguration(hostingContext.Configuration.GetSection("Logging")); loggingBuilder.AddEventSourceLogger(); if (TestOutput != null) { loggingBuilder.Services.AddSingleton(new TestLoggerProvider(TestOutput)); } }) .ConfigureWebHost(webHostBuilder => { webHostBuilder .ConfigureServices(configureServices) .UseKestrel(kestrel => { if (requestHeaderEncoding is not null) { kestrel.RequestHeaderEncodingSelector = _ => requestHeaderEncoding; } kestrel.Listen(IPAddress.Loopback, 0, listenOptions => { listenOptions.Protocols = protocols; if (useHttps) { listenOptions.UseHttps(TestResources.GetTestCertificate()); } listenOptions.UseConnectionLogging(); }); }) .Configure(configureApp); if (useHttpSys) { #pragma warning disable CA1416 // Validate platform compatibility webHostBuilder.UseHttpSys(httpSys => { if (useHttps) { httpSys.UrlPrefixes.Add("https://localhost:" + FindHttpSysHttpsPortAsync(TestOutput).Result); } else { httpSys.UrlPrefixes.Add("http://localhost:0"); } }); #pragma warning restore CA1416 // Validate platform compatibility } }).Build(); } private const int BaseHttpsPort = 44300; private const int MaxHttpsPort = 44399; private static int NextHttpsPort = BaseHttpsPort; private static readonly SemaphoreSlim PortLock = new SemaphoreSlim(1); internal static async Task FindHttpSysHttpsPortAsync(ITestOutputHelper output) { await PortLock.WaitAsync(); try { while (NextHttpsPort < MaxHttpsPort) { var port = NextHttpsPort++; using var host = new HostBuilder() .ConfigureAppConfiguration(config => { config.AddInMemoryCollection(new Dictionary() { { "Logging:LogLevel:Microsoft", "Trace" }, }); }) .ConfigureLogging((hostingContext, loggingBuilder) => { loggingBuilder.AddConfiguration(hostingContext.Configuration.GetSection("Logging")); loggingBuilder.AddEventSourceLogger(); loggingBuilder.AddXunit(output); }) .ConfigureWebHost(webHostBuilder => { #pragma warning disable CA1416 // Validate platform compatibility webHostBuilder.UseHttpSys(httpSys => { httpSys.UrlPrefixes.Add("https://localhost:" + port); }); webHostBuilder.Configure(app => { }); #pragma warning restore CA1416 // Validate platform compatibility }).Build(); try { await host.StartAsync(); await host.StopAsync(); return port; } catch (HttpSysException) { } } NextHttpsPort = BaseHttpsPort; } finally { PortLock.Release(); } throw new Exception("Failed to locate a free port."); } } ================================================ FILE: test/ReverseProxy.FunctionalTests/Common/TestUrlHelper.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; namespace Yarp.ReverseProxy; public static class TestUrlHelper { public static string GetTestUrl() { return BuildTestUri().ToString(); } public static Uri BuildTestUri() { return BuildTestUri(Uri.UriSchemeHttp); } internal static Uri BuildTestUri(string scheme) { // Most functional tests use this codepath and should directly bind to dynamic port "0" and scrape // the assigned port from the status message, which should be 100% reliable since the port is bound // once and never released. Binding to dynamic port "0" on "localhost" (both IPv4 and IPv6) is not // supported, so the port is only bound on "127.0.0.1" (IPv4). If a test explicitly requires IPv6, // it should provide a hint URL with "localhost" (IPv4 and IPv6) or "[::1]" (IPv6-only). return new UriBuilder(scheme, "127.0.0.1", 0).Uri; } } ================================================ FILE: test/ReverseProxy.FunctionalTests/DistributedTracingTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; using System.Linq; using System.Net.Http; using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Xunit; using Yarp.ReverseProxy.Common; namespace Yarp.ReverseProxy; public class DistributedTracingTests { // These constants depend on the default behavior of DistributedContextPropagator private const string Baggage = "Correlation-Context"; private const string TraceParent = "traceparent"; private const string TraceState = "tracestate"; private const string RequestId = "Request-Id"; [Theory] [InlineData(ActivityIdFormat.W3C)] [InlineData(ActivityIdFormat.Hierarchical)] public async Task DistributedTracing_Works(ActivityIdFormat idFormat) { var proxyHeaders = new HeaderDictionary(); var downstreamHeaders = new HeaderDictionary(); var test = new TestEnvironment( async context => { foreach (var header in context.Request.Headers) { downstreamHeaders.Add(header); } await context.Response.Body.WriteAsync(Encoding.UTF8.GetBytes("Hello")); }) { ConfigureProxyApp = proxyApp => { proxyApp.Use(next => context => { foreach (var header in context.Request.Headers) { proxyHeaders.Add(header); } return next(context); }); }, }; var clientActivity = new Activity("Foo"); clientActivity.SetIdFormat(idFormat); clientActivity.TraceStateString = "Bar"; clientActivity.AddBaggage("One", "1"); clientActivity.AddBaggage("Two", "2"); clientActivity.Start(); await test.Invoke(async uri => { using var client = new HttpClient(); Assert.Equal("Hello", await client.GetStringAsync(uri)); }); Assert.NotEmpty(proxyHeaders); Assert.NotEmpty(downstreamHeaders); ValidateActivities(idFormat, clientActivity, proxyHeaders, downstreamHeaders); } private static void ValidateActivities(ActivityIdFormat idFormat, Activity client, HeaderDictionary proxy, HeaderDictionary downstream) { var baggage = string.Join(", ", client.Baggage.Select(pair => $"{pair.Key}={pair.Value}")); Assert.Equal(baggage, proxy[Baggage]); Assert.Equal(baggage, downstream[Baggage]); if (idFormat == ActivityIdFormat.W3C) { Assert.True(ActivityContext.TryParse(proxy[TraceParent], proxy[TraceState], out var proxyContext)); Assert.True(ActivityContext.TryParse(downstream[TraceParent], downstream[TraceState], out var downstreamContext)); Assert.Equal(client.TraceStateString, proxyContext.TraceState); Assert.Equal(client.TraceStateString, downstreamContext.TraceState); var proxyTraceId = proxyContext.TraceId.ToHexString(); var proxySpanId = proxyContext.SpanId.ToHexString(); var downstreamTraceId = downstreamContext.TraceId.ToHexString(); var downstreamSpanId = downstreamContext.SpanId.ToHexString(); Assert.Equal(client.TraceId.ToHexString(), proxyTraceId); Assert.Equal(client.TraceId.ToHexString(), downstreamTraceId); Assert.NotEqual(proxySpanId, downstreamSpanId); } else { var proxyId = proxy[RequestId].ToString(); var downstreamId = downstream[RequestId].ToString(); Assert.StartsWith(client.Id, proxyId); Assert.StartsWith(proxyId, downstreamId); Assert.NotEqual(proxyId, downstreamId); } } } ================================================ FILE: test/ReverseProxy.FunctionalTests/Expect100ContinueTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.IO; using System.Net; using System.Net.Http; using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; using Xunit; using Yarp.ReverseProxy.Common; using Yarp.ReverseProxy.Forwarder; namespace Yarp.ReverseProxy; public class Expect100ContinueTests { // HTTP/2 over TLS is not supported on macOS due to missing ALPN support. // See https://github.com/dotnet/runtime/issues/27727 public static bool Http2OverTlsSupported => !RuntimeInformation.IsOSPlatform(OSPlatform.OSX); [Theory(Skip = "Condition not met", SkipUnless = nameof(Http2OverTlsSupported))] [InlineData(HttpProtocols.Http1, HttpProtocols.Http1, true, 200)] [InlineData(HttpProtocols.Http1, HttpProtocols.Http1, false, 200)] [InlineData(HttpProtocols.Http2, HttpProtocols.Http2, true, 200)] [InlineData(HttpProtocols.Http2, HttpProtocols.Http2, false, 200)] [InlineData(HttpProtocols.Http1, HttpProtocols.Http1, true, 400)] [InlineData(HttpProtocols.Http1, HttpProtocols.Http1, false, 400)] [InlineData(HttpProtocols.Http1, HttpProtocols.Http2, true, 400)] [InlineData(HttpProtocols.Http2, HttpProtocols.Http2, true, 400)] public async Task PostExpect100_BodyNotUploadedIfFailed(HttpProtocols proxyProtocol, HttpProtocols destProtocol, bool useContentLength, int destResponseCode) { var headerTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var bodyTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var contentString = new string('a', 1024 * 1024 * 10); var test = new TestEnvironment( async context => { if ((context.Request.Protocol == "HTTP/1.1" && destProtocol != HttpProtocols.Http1) || (context.Request.Protocol == "HTTP/2.0" && destProtocol != HttpProtocols.Http2)) { headerTcs.SetException(new Exception($"Unexpected request protocol {context.Request.Protocol}")); return; } else if (context.Request.Headers.TryGetValue(HeaderNames.Expect, out var expectHeader)) { headerTcs.SetResult(expectHeader); } else { headerTcs.SetException(new Exception("Missing 'Expect' header in request")); return; } if (destResponseCode == 200) { // 100 response code is sent automatically on reading Body. await ReadContent(context, bodyTcs, Encoding.UTF8.GetByteCount(contentString)); } context.Response.StatusCode = destResponseCode; }) { ProxyProtocol = proxyProtocol, UseHttpsOnDestination = true, UseHttpsOnProxy = true, ConfigTransformer = (c, r) => { c = c with { HttpRequest = new ForwarderRequestConfig { Version = destProtocol == HttpProtocols.Http2 ? HttpVersion.Version20 : HttpVersion.Version11, } }; return (c, r); }, ConfigureProxy = proxyBuilder => { proxyBuilder.Services.AddSingleton(); }, }; await test.Invoke(async uri => { await ProcessHttpRequest(new Uri(uri), proxyProtocol, contentString, useContentLength, destResponseCode, false, destResponseCode == 200); Assert.True(headerTcs.Task.IsCompleted); var expectHeader = await headerTcs.Task; var expectValue = Assert.Single(expectHeader); Assert.Equal("100-continue", expectValue); if (destResponseCode == 200) { Assert.True(bodyTcs.Task.IsCompleted); var actualString = await bodyTcs.Task; Assert.Equal(contentString, actualString); } else { Assert.False(bodyTcs.Task.IsCompleted); } }); } [Theory(Skip = "Condition not met", SkipUnless = nameof(Http2OverTlsSupported))] [InlineData(HttpProtocols.Http1, HttpProtocols.Http1, true, true, 200)] [InlineData(HttpProtocols.Http2, HttpProtocols.Http2, true, true, 200)] [InlineData(HttpProtocols.Http1, HttpProtocols.Http2, true, true, 200)] [InlineData(HttpProtocols.Http1, HttpProtocols.Http1, false, false, 400)] [InlineData(HttpProtocols.Http1, HttpProtocols.Http1, false, true, 400)] [InlineData(HttpProtocols.Http1, HttpProtocols.Http1, true, false, 400)] [InlineData(HttpProtocols.Http1, HttpProtocols.Http1, true, true, 400)] [InlineData(HttpProtocols.Http2, HttpProtocols.Http2, false, false, 400)] [InlineData(HttpProtocols.Http2, HttpProtocols.Http2, false, true, 400)] [InlineData(HttpProtocols.Http2, HttpProtocols.Http2, true, false, 400)] [InlineData(HttpProtocols.Http2, HttpProtocols.Http2, true, true, 400)] [InlineData(HttpProtocols.Http1, HttpProtocols.Http2, false, false, 400)] [InlineData(HttpProtocols.Http1, HttpProtocols.Http2, false, true, 400)] [InlineData(HttpProtocols.Http1, HttpProtocols.Http2, true, false, 400)] [InlineData(HttpProtocols.Http1, HttpProtocols.Http2, true, true, 400)] public async Task PostExpect100_ResponseWithPayload(HttpProtocols proxyProtocol, HttpProtocols destProtocol, bool useContentLength, bool cancelResponse, int responseCode) { var requestBodyTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var contentString = new string('a', 1024 * 1024 * 10); var test = new TestEnvironment( async context => { await ReadContent(context, requestBodyTcs, Encoding.UTF8.GetByteCount(contentString)); context.Response.StatusCode = responseCode; var responseBody = Encoding.UTF8.GetBytes(contentString + "Response"); if (useContentLength) { context.Response.Headers.ContentLength = responseBody.Length; } if (cancelResponse) { await context.Response.BodyWriter.WriteAsync(responseBody.AsMemory(0, responseBody.Length / 2)); context.Abort(); } else { await context.Response.Body.WriteAsync(responseBody.AsMemory()); } }) { ProxyProtocol = proxyProtocol, UseHttpsOnDestination = true, UseHttpsOnProxy = true, ConfigTransformer = (c, r) => { c = c with { HttpRequest = new ForwarderRequestConfig { Version = destProtocol == HttpProtocols.Http2 ? HttpVersion.Version20 : HttpVersion.Version11, } }; return (c, r); }, ConfigureProxy = proxyBuilder => { proxyBuilder.Services.AddSingleton(); }, }; await test.Invoke(async uri => { await ProcessHttpRequest(new Uri(uri), proxyProtocol, contentString, useContentLength, responseCode, cancelResponse, true, async response => { Assert.Equal(responseCode, (int)response.StatusCode); var actualResponse = await response.Content.ReadAsStringAsync(); Assert.Equal(contentString + "Response", actualResponse); }); }); } [Theory(Skip = "Condition not met", SkipUnless = nameof(Http2OverTlsSupported))] [InlineData(HttpProtocols.Http1, HttpProtocols.Http1, false)] [InlineData(HttpProtocols.Http2, HttpProtocols.Http2, false)] [InlineData(HttpProtocols.Http1, HttpProtocols.Http2, false)] [InlineData(HttpProtocols.Http2, HttpProtocols.Http1, false)] [InlineData(HttpProtocols.Http1, HttpProtocols.Http1, true)] [InlineData(HttpProtocols.Http2, HttpProtocols.Http2, true)] [InlineData(HttpProtocols.Http1, HttpProtocols.Http2, true)] [InlineData(HttpProtocols.Http2, HttpProtocols.Http1, true)] public async Task PostExpect100_SkipRequestBodyWithUnsuccessfulResponseCode(HttpProtocols proxyProtocol, HttpProtocols destProtocol, bool useContentLength) { var requestBodyTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var contentString = new string('a', 1024 * 1024 * 10); var test = new TestEnvironment( async context => { context.Response.StatusCode = 400; var responseBody = Encoding.UTF8.GetBytes(contentString + "Response"); if (useContentLength) { context.Response.Headers.ContentLength = responseBody.Length; } await context.Response.Body.WriteAsync(responseBody.AsMemory()); }) { ProxyProtocol = proxyProtocol, UseHttpsOnDestination = true, UseHttpsOnProxy = true, ConfigTransformer = (c, r) => { c = c with { HttpRequest = new ForwarderRequestConfig { Version = destProtocol == HttpProtocols.Http2 ? HttpVersion.Version20 : HttpVersion.Version11, } }; return (c, r); }, ConfigureProxy = proxyBuilder => { proxyBuilder.Services.AddSingleton(); }, }; await test.Invoke(async uri => { await ProcessHttpRequest(new Uri(uri), proxyProtocol, contentString, useContentLength, 400, cancelResponse: false, contentRead: false, async response => { Assert.Equal(400, (int)response.StatusCode); var actualResponse = await response.Content.ReadAsStringAsync(); Assert.Equal(contentString + "Response", actualResponse); }); }); } private static async Task ReadContent(Microsoft.AspNetCore.Http.HttpContext context, TaskCompletionSource bodyTcs, int byteCount) { try { var buffer = new byte[byteCount]; var readCount = 0; var totalReadCount = 0; do { readCount = await context.Request.Body.ReadAsync(buffer, totalReadCount, buffer.Length - totalReadCount); totalReadCount += readCount; } while (readCount != 0); var actualString = Encoding.UTF8.GetString(buffer); bodyTcs.SetResult(actualString); } catch (Exception e) { bodyTcs.SetException(e); } } private async Task ProcessHttpRequest( Uri proxyHostUri, HttpProtocols protocol, string contentString, bool useContentLength, int expectedCode, bool cancelResponse, bool contentRead, Func responseAction = null) { using var handler = new SocketsHttpHandler() { Expect100ContinueTimeout = TimeSpan.FromSeconds(60) }; handler.UseProxy = false; handler.AllowAutoRedirect = false; handler.SslOptions.RemoteCertificateValidationCallback = delegate { return true; }; using var client = new HttpClient(handler); using var message = new HttpRequestMessage(HttpMethod.Post, proxyHostUri); message.Version = protocol == HttpProtocols.Http2 ? HttpVersion.Version20 : HttpVersion.Version11; message.Headers.ExpectContinue = true; var content = Encoding.UTF8.GetBytes(contentString); using var contentStream = new MemoryStream(content); message.Content = new StreamContent(contentStream); if (useContentLength) { message.Content.Headers.ContentLength = content.Length; } else { message.Headers.TransferEncodingChunked = true; } if (!cancelResponse) { using var response = await client.SendAsync(message); Assert.Equal(expectedCode, (int)response.StatusCode); if (contentRead) { Assert.Equal(content.Length, contentStream.Position); } else { Assert.Equal(0, contentStream.Position); } if (responseAction is not null) { await responseAction(response); } } else { var exception = await Assert.ThrowsAsync(() => client.SendAsync(message)); Assert.IsAssignableFrom(exception.InnerException); Assert.Equal(content.Length, contentStream.Position); } } private class TestForwarderHttpClientFactory : ForwarderHttpClientFactory { protected override void ConfigureHandler(ForwarderHttpClientContext context, SocketsHttpHandler handler) { base.ConfigureHandler(context, handler); handler.Expect100ContinueTimeout = TimeSpan.FromSeconds(60); } } } ================================================ FILE: test/ReverseProxy.FunctionalTests/HeaderTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.IO; using System.Net; using System.Net.Http; using System.Net.Sockets; using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; using Xunit; using Yarp.ReverseProxy.Common; using Yarp.ReverseProxy.Forwarder; namespace Yarp.ReverseProxy; public class HeaderTests { [Fact] public async Task ProxyAsync_EmptyRequestHeader_Proxied() { var refererReceived = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var customReceived = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); IForwarderErrorFeature proxyError = null; Exception unhandledError = null; var test = new TestEnvironment( context => { if (context.Request.Headers.TryGetValue(HeaderNames.Referer, out var header)) { refererReceived.SetResult(header); } else { refererReceived.SetException(new Exception($"Missing '{HeaderNames.Referer}' header in request")); } if (context.Request.Headers.TryGetValue("custom", out header)) { customReceived.SetResult(header); } else { customReceived.SetException(new Exception($"Missing 'custom' header in request")); } return Task.CompletedTask; }) { ProxyProtocol = HttpProtocols.Http1, ConfigureProxyApp = proxyApp => { proxyApp.Use(async (context, next) => { try { Assert.True(context.Request.Headers.TryGetValue(HeaderNames.Referer, out var header)); var value = Assert.Single(header); Assert.True(StringValues.IsNullOrEmpty(value)); Assert.True(context.Request.Headers.TryGetValue("custom", out header)); value = Assert.Single(header); Assert.True(StringValues.IsNullOrEmpty(value)); await next(); proxyError = context.Features.Get(); } catch (Exception ex) { unhandledError = ex; throw; } }); }, }; await test.Invoke(async proxyUri => { var proxyHostUri = new Uri(proxyUri); using var tcpClient = new TcpClient(proxyHostUri.Host, proxyHostUri.Port); await using var stream = tcpClient.GetStream(); await stream.WriteAsync(Encoding.ASCII.GetBytes("GET / HTTP/1.1\r\n")); await stream.WriteAsync(Encoding.ASCII.GetBytes($"Host: {proxyHostUri.Host}:{proxyHostUri.Port}\r\n")); await stream.WriteAsync(Encoding.ASCII.GetBytes($"Content-Length: 0\r\n")); await stream.WriteAsync(Encoding.ASCII.GetBytes($"Connection: close\r\n")); await stream.WriteAsync(Encoding.ASCII.GetBytes($"Referer: \r\n")); await stream.WriteAsync(Encoding.ASCII.GetBytes($"custom: \r\n")); await stream.WriteAsync(Encoding.ASCII.GetBytes("\r\n")); await stream.WriteAsync(Encoding.ASCII.GetBytes("\r\n")); var buffer = new byte[4096]; var responseBuilder = new StringBuilder(); while (true) { var count = await stream.ReadAsync(buffer); if (count == 0) { break; } responseBuilder.Append(Encoding.ASCII.GetString(buffer, 0, count)); } var response = responseBuilder.ToString(); Assert.Null(proxyError); Assert.Null(unhandledError); Assert.StartsWith("HTTP/1.1 200 OK", response); Assert.True(refererReceived.Task.IsCompleted); var refererHeader = await refererReceived.Task; var referer = Assert.Single(refererHeader); Assert.True(StringValues.IsNullOrEmpty(referer)); Assert.True(customReceived.Task.IsCompleted); var customHeader = await customReceived.Task; var custom = Assert.Single(customHeader); Assert.True(StringValues.IsNullOrEmpty(custom)); }); } [Fact] public async Task ProxyAsync_EmptyResponseHeader_Proxied() { IForwarderErrorFeature proxyError = null; Exception unhandledError = null; var test = new TestEnvironment( context => { context.Response.Headers[HeaderNames.WWWAuthenticate] = ""; context.Response.Headers["custom"] = ""; return Task.CompletedTask; }) { ProxyProtocol = HttpProtocols.Http1, ConfigureProxyApp = proxyApp => { proxyApp.Use(async (context, next) => { try { await next(); Assert.True(context.Response.Headers.TryGetValue(HeaderNames.WWWAuthenticate, out var header)); var value = Assert.Single(header); Assert.True(StringValues.IsNullOrEmpty(value)); Assert.True(context.Response.Headers.TryGetValue("custom", out header)); value = Assert.Single(header); Assert.True(StringValues.IsNullOrEmpty(value)); proxyError = context.Features.Get(); } catch (Exception ex) { unhandledError = ex; throw; } }); }, }; await test.Invoke(async proxyUri => { var proxyHostUri = new Uri(proxyUri); using var tcpClient = new TcpClient(proxyHostUri.Host, proxyHostUri.Port); await using var stream = tcpClient.GetStream(); await stream.WriteAsync(Encoding.ASCII.GetBytes("GET / HTTP/1.1\r\n")); await stream.WriteAsync(Encoding.ASCII.GetBytes($"Host: {proxyHostUri.Host}:{proxyHostUri.Port}\r\n")); await stream.WriteAsync(Encoding.ASCII.GetBytes($"Content-Length: 0\r\n")); await stream.WriteAsync(Encoding.ASCII.GetBytes($"Connection: close\r\n")); await stream.WriteAsync(Encoding.ASCII.GetBytes("\r\n")); await stream.WriteAsync(Encoding.ASCII.GetBytes("\r\n")); var buffer = new byte[4096]; var responseBuilder = new StringBuilder(); while (true) { var count = await stream.ReadAsync(buffer); if (count == 0) { break; } responseBuilder.Append(Encoding.ASCII.GetString(buffer, 0, count)); } var response = responseBuilder.ToString(); Assert.Null(proxyError); Assert.Null(unhandledError); var lines = response.Split("\r\n"); Assert.Equal("HTTP/1.1 200 OK", lines[0]); // Order varies across versions. // Assert.Equal("Content-Length: 0", lines[1]); // Assert.Equal("Connection: close", lines[2]); // Assert.StartsWith("Date: ", lines[3]); // Assert.Equal("Server: Kestrel", lines[4]); Assert.Equal("WWW-Authenticate: ", lines[5]); Assert.Equal("custom: ", lines[6]); Assert.Equal("", lines[7]); }); } [Theory] [InlineData("http://www.ěščřžýáíé.com", "utf-8")] [InlineData("http://www.çáéôîèñøæ.com", "iso-8859-1")] public async Task ProxyAsync_RequestWithEncodedHeaderValue(string headerValue, string encodingName) { var encoding = Encoding.GetEncoding(encodingName); var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); IForwarderErrorFeature proxyError = null; Exception unhandledError = null; var test = new TestEnvironment( context => { if (context.Request.Headers.TryGetValue(HeaderNames.Referer, out var header)) { tcs.SetResult(header); } else { tcs.SetException(new Exception($"Missing '{HeaderNames.Referer}' header in request")); } return Task.CompletedTask; }) { ProxyProtocol = HttpProtocols.Http1, HeaderEncoding = encoding, ConfigureProxyApp = proxyApp => { proxyApp.Use(async (context, next) => { try { Assert.True(context.Request.Headers.TryGetValue(HeaderNames.Referer, out var header)); var value = Assert.Single(header); Assert.Equal(headerValue, value); await next(); proxyError = context.Features.Get(); } catch (Exception ex) { unhandledError = ex; throw; } }); }, }; await test.Invoke(async proxyUri => { var proxyHostUri = new Uri(proxyUri); using var tcpClient = new TcpClient(proxyHostUri.Host, proxyHostUri.Port); await using var stream = tcpClient.GetStream(); await stream.WriteAsync(Encoding.ASCII.GetBytes("GET / HTTP/1.1\r\n")); await stream.WriteAsync(Encoding.ASCII.GetBytes($"Host: {proxyHostUri.Host}:{proxyHostUri.Port}\r\n")); await stream.WriteAsync(Encoding.ASCII.GetBytes($"Content-Length: 0\r\n")); await stream.WriteAsync(Encoding.ASCII.GetBytes($"Connection: close\r\n")); await stream.WriteAsync(Encoding.ASCII.GetBytes($"Referer: ")); await stream.WriteAsync(encoding.GetBytes(headerValue)); await stream.WriteAsync(Encoding.ASCII.GetBytes("\r\n")); await stream.WriteAsync(Encoding.ASCII.GetBytes("\r\n")); var buffer = new byte[4096]; var responseBuilder = new StringBuilder(); while (true) { var count = await stream.ReadAsync(buffer); if (count == 0) { break; } responseBuilder.Append(Encoding.ASCII.GetString(buffer, 0, count)); } var response = responseBuilder.ToString(); Assert.Null(proxyError); Assert.Null(unhandledError); Assert.StartsWith("HTTP/1.1 200 OK", response); Assert.True(tcs.Task.IsCompleted); var refererHeader = await tcs.Task; var referer = Assert.Single(refererHeader); Assert.Equal(headerValue, referer); }); } [Theory] [InlineData("http://www.ěščřžýáíé.com", "utf-8")] [InlineData("http://www.çáéôîèñøæ.com", "iso-8859-1")] public async Task ProxyAsync_ResponseWithEncodedHeaderValue(string headerValue, string encodingName) { var encoding = Encoding.GetEncoding(encodingName); var tcpListener = new TcpListener(IPAddress.Loopback, 0); tcpListener.Start(); var destinationTask = Task.Run(async () => { using var tcpClient = await tcpListener.AcceptTcpClientAsync(); await using var stream = tcpClient.GetStream(); var buffer = new byte[4096]; var requestBuilder = new StringBuilder(); while (true) { var count = await stream.ReadAsync(buffer); if (count == 0) { break; } requestBuilder.Append(encoding.GetString(buffer, 0, count)); // End of the request if (requestBuilder.Length >= 4 && requestBuilder[^4] == '\r' && requestBuilder[^3] == '\n' && requestBuilder[^2] == '\r' && requestBuilder[^1] == '\n') { break; } } await stream.WriteAsync(Encoding.ASCII.GetBytes($"HTTP/1.1 200 OK\r\n")); await stream.WriteAsync(Encoding.ASCII.GetBytes($"Content-Length: 0\r\n")); await stream.WriteAsync(Encoding.ASCII.GetBytes($"Connection: close\r\n")); await stream.WriteAsync(Encoding.ASCII.GetBytes($"Test-Extra: pingu\r\n")); await stream.WriteAsync(Encoding.ASCII.GetBytes($"Location: ")); await stream.WriteAsync(encoding.GetBytes(headerValue)); await stream.WriteAsync(Encoding.ASCII.GetBytes("\r\n")); await stream.WriteAsync(Encoding.ASCII.GetBytes("\r\n")); }); IForwarderErrorFeature proxyError = null; Exception unhandledError = null; var proxyTest = new TestEnvironment() { ProxyProtocol = HttpProtocols.Http1, HeaderEncoding = encoding, ClusterId = "cluster1", ConfigureProxyApp = proxyApp => { proxyApp.Use(async (context, next) => { try { await next(); proxyError = context.Features.Get(); } catch (Exception ex) { unhandledError = ex; throw; } }); }, }; using var proxy = proxyTest.CreateProxy($"http://{tcpListener.LocalEndpoint}"); await proxy.StartAsync(); try { using var httpClient = new HttpClient(); using var response = await httpClient.GetAsync(proxy.GetAddress()); Assert.NotNull(proxyError); Assert.Equal(ForwarderError.ResponseHeaders, proxyError.Error); var ioe = Assert.IsType(proxyError.Exception); Assert.StartsWith("Invalid non-ASCII or control character in header: ", ioe.Message); Assert.Null(unhandledError); Assert.Equal(HttpStatusCode.BadGateway, response.StatusCode); Assert.False(response.Headers.TryGetValues(HeaderNames.Location, out _)); Assert.False(response.Headers.TryGetValues("Test-Extra", out _)); await destinationTask; } finally { await proxy.StopAsync(); tcpListener.Stop(); } } [Fact] public async Task ContentLengthAndTransferEncoding_ContentLengthRemoved() { var proxyTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var appTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var test = new TestEnvironment( context => { try { Assert.Null(context.Request.ContentLength); Assert.Equal("chunked", context.Request.Headers[HeaderNames.TransferEncoding]); appTcs.SetResult(0); } catch (Exception ex) { appTcs.SetException(ex); } return Task.CompletedTask; }) { ProxyProtocol = HttpProtocols.Http1, ConfigureProxyApp = proxyApp => { proxyApp.Use(async (context, next) => { try { // Removed by the server Assert.Null(context.Request.ContentLength); // Set it just to make sure YARP removes it context.Request.ContentLength = 11; Assert.Equal("chunked", context.Request.Headers[HeaderNames.TransferEncoding]); proxyTcs.SetResult(0); } catch (Exception ex) { proxyTcs.SetException(ex); } await next(); }); }, }; await test.Invoke(async proxyUri => { var proxyHostUri = new Uri(proxyUri); using var tcpClient = new TcpClient(proxyHostUri.Host, proxyHostUri.Port); await using var stream = tcpClient.GetStream(); await stream.WriteAsync(Encoding.ASCII.GetBytes("GET / HTTP/1.1\r\n")); await stream.WriteAsync(Encoding.ASCII.GetBytes($"Host: {proxyHostUri.Host}:{proxyHostUri.Port}\r\n")); await stream.WriteAsync(Encoding.ASCII.GetBytes($"Content-Length: 11\r\n")); await stream.WriteAsync(Encoding.ASCII.GetBytes($"Transfer-Encoding: chunked\r\n")); await stream.WriteAsync(Encoding.ASCII.GetBytes($"Connection: close\r\n")); await stream.WriteAsync(Encoding.ASCII.GetBytes("\r\n")); await stream.WriteAsync(Encoding.ASCII.GetBytes($"b\r\n")); await stream.WriteAsync(Encoding.ASCII.GetBytes($"Hello World\r\n")); await stream.WriteAsync(Encoding.ASCII.GetBytes($"0\r\n")); await stream.WriteAsync(Encoding.ASCII.GetBytes($"\r\n")); var buffer = new byte[4096]; var responseBuilder = new StringBuilder(); while (true) { var count = await stream.ReadAsync(buffer); if (count == 0) { break; } responseBuilder.Append(Encoding.ASCII.GetString(buffer, 0, count)); } var response = responseBuilder.ToString(); await proxyTcs.Task; await appTcs.Task; Assert.StartsWith("HTTP/1.1 200 OK", response); }); } [Theory] [MemberData(nameof(RequestMultiHeadersData))] public async Task MultiValueRequestHeaders(string headerName, string[] values, string expectedValues) { var proxyTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var appTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var test = new TestEnvironment( context => { try { Assert.True(context.Request.Headers.TryGetValue(headerName, out var headerValues)); Assert.Single(headerValues); Assert.Equal(expectedValues, headerValues); appTcs.SetResult(0); } catch (Exception ex) { appTcs.SetException(ex); } return Task.CompletedTask; }) { ProxyProtocol = HttpProtocols.Http1, ConfigureProxyApp = proxyApp => { proxyApp.Use(async (context, next) => { try { Assert.True(context.Request.Headers.TryGetValue(headerName, out var headerValues)); Assert.Equal(values.Length, headerValues.Count); for (var i = 0; i < values.Length; ++i) { Assert.Equal(values[i], headerValues[i]); } proxyTcs.SetResult(0); } catch (Exception ex) { proxyTcs.SetException(ex); } await next(); }); }, }; await test.Invoke(async proxyUri => { var proxyHostUri = new Uri(proxyUri); using var tcpClient = new TcpClient(proxyHostUri.Host, proxyHostUri.Port); await using var stream = tcpClient.GetStream(); await stream.WriteAsync(Encoding.ASCII.GetBytes("GET / HTTP/1.1\r\n")); await stream.WriteAsync(Encoding.ASCII.GetBytes($"Host: {proxyHostUri.Host}:{proxyHostUri.Port}\r\n")); foreach (var value in values) { await stream.WriteAsync(Encoding.ASCII.GetBytes($"{headerName}: {value}\r\n")); } await stream.WriteAsync(Encoding.ASCII.GetBytes($"Connection: close\r\n")); await stream.WriteAsync(Encoding.ASCII.GetBytes("\r\n")); await stream.WriteAsync(Encoding.ASCII.GetBytes($"\r\n")); var response = await new StreamReader(stream).ReadToEndAsync(); await proxyTcs.Task; await appTcs.Task; Assert.StartsWith("HTTP/1.1 200 OK", response); }); } public static IEnumerable RequestMultiHeaderNames() { var headers = new[] { HeaderNames.Accept, HeaderNames.AcceptCharset, HeaderNames.AcceptEncoding, HeaderNames.AcceptLanguage, HeaderNames.Via }; foreach (var header in headers) { yield return header; } } public static IEnumerable MultiValues() { var values = new string[][] { new[] { "testA=A_Value", "testB=B_Value", "testC=C_Value" }, new[] { "testA=A_Value, testB=B_Value", "testC=C_Value" }, new[] { "testA=A_Value, testB=B_Value, testC=C_Value" }, }; foreach (var value in values) { yield return value; } } public static IEnumerable RequestMultiHeadersData() { foreach (var header in RequestMultiHeaderNames()) { foreach (var value in MultiValues()) { yield return new object[] { header, value, string.Join(", ", value).TrimEnd() }; } } // Special separator ";" for Cookie header foreach (var value in MultiValues()) { yield return new object[] { HeaderNames.Cookie, value, string.Join("; ", value).TrimEnd() }; } } public static IEnumerable ResponseMultiHeadersData() { foreach (var header in ResponseMultiHeaderNames()) { foreach (var value in MultiValues()) { yield return new object[] { header, value, value }; } } } public static IEnumerable ResponseMultiHeaderNames() { var headers = new[] { HeaderNames.AcceptRanges, HeaderNames.Allow, HeaderNames.ContentEncoding, HeaderNames.ContentLanguage, HeaderNames.ContentRange, HeaderNames.ContentType, HeaderNames.SetCookie, HeaderNames.Via, HeaderNames.Warning, HeaderNames.WWWAuthenticate }; foreach (var header in headers) { yield return header; } } [Theory] [MemberData(nameof(ResponseMultiHeadersData))] public async Task MultiValueResponseHeaders(string headerName, string[] values, string[] expectedValues) { IForwarderErrorFeature proxyError = null; Exception unhandledError = null; var test = new TestEnvironment( context => { Assert.True(context.Response.Headers.TryAdd(headerName, values)); return Task.CompletedTask; }) { ProxyProtocol = HttpProtocols.Http1, ConfigureProxyApp = proxyApp => { proxyApp.Use(async (context, next) => { try { await next(); Assert.True(context.Response.Headers.TryGetValue(headerName, out var header)); Assert.Equal(values.Length, header.Count); for (var i = 0; i < values.Length; ++i) { Assert.Equal(values[i], header[i]); } proxyError = context.Features.Get(); } catch (Exception ex) { unhandledError = ex; throw; } }); }, }; await test.Invoke(async proxyUri => { var proxyHostUri = new Uri(proxyUri); using var tcpClient = new TcpClient(proxyHostUri.Host, proxyHostUri.Port); await using var stream = tcpClient.GetStream(); await stream.WriteAsync(Encoding.ASCII.GetBytes("GET / HTTP/1.1\r\n")); await stream.WriteAsync(Encoding.ASCII.GetBytes($"Host: {proxyHostUri.Host}:{proxyHostUri.Port}\r\n")); await stream.WriteAsync(Encoding.ASCII.GetBytes($"Content-Length: 0\r\n")); await stream.WriteAsync(Encoding.ASCII.GetBytes($"Connection: close\r\n")); await stream.WriteAsync(Encoding.ASCII.GetBytes("\r\n")); await stream.WriteAsync(Encoding.ASCII.GetBytes("\r\n")); var response = await new StreamReader(stream).ReadToEndAsync(); Assert.Null(proxyError); Assert.Null(unhandledError); var lines = response.Split("\r\n"); Assert.Equal("HTTP/1.1 200 OK", lines[0]); foreach (var expected in expectedValues) { Assert.Contains($"{headerName}: {expected}", lines); } }); } } ================================================ FILE: test/ReverseProxy.FunctionalTests/HttpForwarderCancellationTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.IO; using System.Net; using System.Net.Http; using System.Runtime.InteropServices; using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http.Features; using Xunit; using Yarp.ReverseProxy.Common; using Yarp.ReverseProxy.Utilities; namespace Yarp.ReverseProxy; public class HttpForwarderCancellationTests { // HTTP/2 over TLS is not supported on macOS due to missing ALPN support. // See https://github.com/dotnet/runtime/issues/27727 public static bool Http2OverTlsSupported => !RuntimeInformation.IsOSPlatform(OSPlatform.OSX); [Fact(Skip = "Condition not met", SkipUnless = nameof(Http2OverTlsSupported))] public async Task ServerSendsHttp2Reset_ReadToClientIsCanceled() { var readAsyncCalled = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var test = new TestEnvironment( async context => { Assert.Equal("HTTP/2", context.Request.Protocol); await context.Response.Body.WriteAsync(Encoding.UTF8.GetBytes("Hello")); await context.Response.CompleteAsync(); await readAsyncCalled.Task; var resetFeature = context.Features.Get(); Assert.NotNull(resetFeature); resetFeature.Reset(0); // NO_ERROR }) { UseHttpsOnDestination = true, UseHttpsOnProxy = true, ConfigureProxyApp = proxyApp => { proxyApp.Use(next => context => { context.Request.Body = new ReadDelegatingStream(context.Request.Body, async (memory, cancellation) => { Assert.False(cancellation.IsCancellationRequested); readAsyncCalled.SetResult(); var startTime = DateTime.UtcNow; while (DateTime.UtcNow.Subtract(startTime) < TimeSpan.FromSeconds(10)) { cancellation.ThrowIfCancellationRequested(); await Task.Delay(10, cancellation); } throw new InvalidOperationException("Cancellation was not requested"); }); return next(context); }); }, }; await test.Invoke(async uri => { var content = new InfiniteHttpContent(); var request = new HttpRequestMessage(HttpMethod.Post, uri) { Version = HttpVersion.Version20, Content = content }; using var client = new HttpClient(new HttpClientHandler { ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator }); using var response = await client.SendAsync(request); response.EnsureSuccessStatusCode(); var responseString = await response.Content.ReadAsStringAsync(); Assert.Equal("Hello", responseString); await Assert.ThrowsAnyAsync(() => content.Completion.Task); }); } private sealed class InfiniteHttpContent : HttpContent { public TaskCompletionSource Completion { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously); protected override Task SerializeToStreamAsync(Stream stream, TransportContext context) { throw new NotImplementedException(); } protected override async Task SerializeToStreamAsync(Stream stream, TransportContext context, CancellationToken cancellationToken) { var buffer = new byte[1024]; new Random(42).NextBytes(buffer); while (true) { try { await stream.WriteAsync(buffer, cancellationToken); } catch (Exception ex) { Completion.SetException(ex); return; } } } protected override bool TryComputeLength(out long length) { length = -1; return false; } } private sealed class ReadDelegatingStream : DelegatingStream { private readonly Func, CancellationToken, ValueTask> _readAsync; public ReadDelegatingStream(Stream stream, Func, CancellationToken, ValueTask> readAsync) : base(stream) { _readAsync = readAsync; } public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) { return _readAsync(buffer, cancellationToken); } } } ================================================ FILE: test/ReverseProxy.FunctionalTests/HttpProxyCookieTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Net; using System.Net.Http; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; using Xunit; using Yarp.ReverseProxy.Common; namespace Yarp.ReverseProxy; public abstract class HttpProxyCookieTests { public const string CookieAKey = "testA"; public const string CookieAValue = "A_Cookie"; public const string CookieBKey = "testB"; public const string CookieBValue = "B_Cookie"; public static readonly string CookieA = $"{CookieAKey}={CookieAValue}"; public static readonly string CookieB = $"{CookieBKey}={CookieBValue}"; public static readonly string Cookies = $"{CookieA}; {CookieB}"; public abstract HttpProtocols HttpProtocol { get; } public abstract Task ProcessHttpRequest(Uri proxyHostUri); [Fact] public async Task ProxyAsync_RequestWithCookieHeaders() { var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var test = new TestEnvironment( context => { if (context.Request.Headers.TryGetValue(HeaderNames.Cookie, out var cookieHeaders)) { tcs.SetResult(cookieHeaders); } else { tcs.SetException(new Exception("Missing 'Cookie' header in request")); } return Task.CompletedTask; }) { ProxyProtocol = HttpProtocol, ConfigureProxyApp = proxyApp => { proxyApp.UseMiddleware(); }, }; await test.Invoke(async uri => { await ProcessHttpRequest(new Uri(uri)); Assert.True(tcs.Task.IsCompleted); var cookieHeaders = await tcs.Task; var cookies = Assert.Single(cookieHeaders); Assert.Equal(Cookies, cookies); }); } private class CheckCookieHeaderMiddleware { private readonly RequestDelegate _next; public CheckCookieHeaderMiddleware(RequestDelegate next) { _next = next; } public async Task Invoke(HttpContext context) { // Ensure that CookieA is the first and CookieB the last. Assert.True(context.Request.Headers.TryGetValue(HeaderNames.Cookie, out var headerValues)); if (context.Request.Protocol is "HTTP/1.1" or "HTTP/2") { Assert.Single(headerValues); Assert.Equal(Cookies, headerValues); } else { Assert.Fail($"Unexpected HTTP protocol '{context.Request.Protocol}'"); } await _next.Invoke(context); } } } public class HttpProxyCookieTests_Http1 : HttpProxyCookieTests { public override HttpProtocols HttpProtocol => HttpProtocols.Http1; public override async Task ProcessHttpRequest(Uri proxyHostUri) { using var client = new HttpClient(); using var message = new HttpRequestMessage(HttpMethod.Get, proxyHostUri); message.Headers.Add(HeaderNames.Cookie, Cookies); using var response = await client.SendAsync(message); response.EnsureSuccessStatusCode(); } } public class HttpProxyCookieTests_Http2 : HttpProxyCookieTests { public override HttpProtocols HttpProtocol => HttpProtocols.Http2; // HttpClient for H/2 will use different header frames for cookies from a container and message headers. // It will first send message header cookie and than the container one and we expect them in the order of cookieA;cookieB. public override async Task ProcessHttpRequest(Uri proxyHostUri) { using var handler = new HttpClientHandler(); handler.CookieContainer.Add(new System.Net.Cookie(CookieBKey, CookieBValue, path: "/", domain: proxyHostUri.Host)); using var client = new HttpClient(handler); using var message = new HttpRequestMessage(HttpMethod.Get, proxyHostUri); message.Version = HttpVersion.Version20; message.VersionPolicy = HttpVersionPolicy.RequestVersionExact; message.Headers.Add(HeaderNames.Cookie, CookieA); using var response = await client.SendAsync(message); response.EnsureSuccessStatusCode(); } } ================================================ FILE: test/ReverseProxy.FunctionalTests/HttpSysDelegationTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Runtime.CompilerServices; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Xunit; using Yarp.ReverseProxy.Common; using Yarp.ReverseProxy.Configuration; using Yarp.ReverseProxy.Delegation; using Yarp.ReverseProxy.Forwarder; namespace Yarp.ReverseProxy; public partial class HttpSysDelegationTests { [HttpSysDelegationFact] public async Task RequestDelegated() { IHttpSysDelegator delegator = null; IForwarderErrorFeature proxyError = null; Exception unhandledError = null; var expectedRepsone = "Hello World!"; var queueName = nameof(HttpSysDelegationTests) + Random.Shared.Next().ToString("x8"); string urlPrefix = null; var test = new HttpSysTestEnvironment( destinationServices => { }, destinationHttpSysOptions => destinationHttpSysOptions.RequestQueueName = queueName, destinationApp => destinationApp.Run(context => context.Response.WriteAsync(expectedRepsone)), proxyServices => { }, proxyBuilder => { }, proxyApp => { delegator = proxyApp.ApplicationServices.GetRequiredService(); proxyApp.Use(async (context, next) => { try { await next(); proxyError = context.Features.Get(); } catch (Exception ex) { unhandledError = ex; throw; } }); }, proxyPipeline => { proxyPipeline.UseHttpSysDelegation(); }, (cluster, route) => { urlPrefix = cluster.Destinations.First().Value.Address; var destination = new DestinationConfig() { Address = urlPrefix, Metadata = new Dictionary(StringComparer.OrdinalIgnoreCase) { { DelegationExtensions.HttpSysDelegationQueueMetadataKey, queueName }, }, }; cluster = new ClusterConfig { ClusterId = cluster.ClusterId, Destinations = new Dictionary(StringComparer.OrdinalIgnoreCase) { { "destination1", destination }, }, }; return (cluster, route); }); await test.Invoke(async proxyUri => { using var httpClient = new HttpClient(); var response = await httpClient.GetStringAsync(proxyUri); Assert.Null(proxyError); Assert.Null(unhandledError); Assert.Equal(expectedRepsone, response); Assert.NotNull(delegator); delegator.ResetQueue(queueName, urlPrefix); response = await httpClient.GetStringAsync(proxyUri); Assert.Null(proxyError); Assert.Null(unhandledError); Assert.Equal(expectedRepsone, response); }); } private class HttpSysDelegationFactAttribute : FactAttribute { public HttpSysDelegationFactAttribute([CallerFilePath] string sourceFilePath = null, [CallerLineNumber] int sourceLineNumber = -1) : base(sourceFilePath, sourceLineNumber) { // Htty.sys delegation was added to Windows in the 21H2 release but back ported through RS5 (1809) if (!OperatingSystem.IsWindowsVersionAtLeast(10, 0, 1809)) { Skip = "Http.sys tests are only supported on Windows versions >= 10.0.1809"; } } } } ================================================ FILE: test/ReverseProxy.FunctionalTests/PassiveHealthCheckTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.IO; using System.Net; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Xunit; using Yarp.ReverseProxy.Common; using Yarp.ReverseProxy.Configuration; using Yarp.ReverseProxy.Forwarder; using Yarp.ReverseProxy.Health; namespace Yarp.ReverseProxy; public class PassiveHealthCheckTests { private sealed class MockHttpClientFactory : IForwarderHttpClientFactory { private readonly Func> _sendAsync; public MockHttpClientFactory(Func> sendAsync) { _sendAsync = sendAsync; } public HttpMessageInvoker CreateClient(ForwarderHttpClientContext context) { return new HttpMessageInvoker(new MockHandler(_sendAsync)); } private sealed class MockHandler : HttpMessageHandler { private readonly Func> _sendAsync; public MockHandler(Func> sendAsync) { _sendAsync = sendAsync; } protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { return await _sendAsync(request, cancellationToken); } } } [Fact] public async Task PassiveHealthChecksEnabled_MultipleDestinationFailures_ProxyReturnsServiceUnavailable() { var destinationReached = false; IProxyStateLookup lookup = null; string clusterId = null; var test = new TestEnvironment( context => { destinationReached = true; throw new InvalidOperationException(); }) { ConfigTransformer = (c, r) => { c = c with { HealthCheck = new HealthCheckConfig { AvailableDestinationsPolicy = HealthCheckConstants.AvailableDestinations.HealthyAndUnknown, Passive = new PassiveHealthCheckConfig { Enabled = true } } }; clusterId = c.ClusterId; return (c, r); }, ConfigureProxy = proxyBuilder => proxyBuilder.Services.AddSingleton(new MockHttpClientFactory((_, _) => throw new IOException())), ConfigureProxyApp = proxyApp => { lookup = proxyApp.ApplicationServices.GetRequiredService(); }, }; await test.Invoke(async uri => { using var client = new HttpClient(); for (var i = 0; i < 10; i++) { using var response = await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, uri)); Assert.Equal(HttpStatusCode.BadGateway, response.StatusCode); } Assert.NotNull(lookup); Assert.NotNull(clusterId); // The destination list will be updated asynchronously in the background. // Wait until that update takes effect. using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); while (true) { Assert.True(lookup.TryGetCluster(clusterId, out var cluster)); Assert.Single(cluster.DestinationsState.AllDestinations); if (cluster.DestinationsState.AvailableDestinations.Count == 0) { break; } await Task.Delay(10, cts.Token); } for (var i = 0; i < 42; i++) { using var response = await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, uri)); Assert.Equal(HttpStatusCode.ServiceUnavailable, response.StatusCode); } }); Assert.False(destinationReached); } [Fact] public async Task PassiveHealthChecksEnabled_IncompleteClientRequests_ProxyHealthIsUnaffected() { var destinationReached = false; var shouldThrow = true; var requestStartedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var proxySendAsync = async (HttpRequestMessage request, CancellationToken ct) => { requestStartedTcs.SetResult(0); if (shouldThrow) { await Task.Delay(-1, ct); throw new OperationCanceledException(ct); } else { return new HttpResponseMessage((HttpStatusCode)418) { Content = new StringContent("Hello world") }; } }; var test = new TestEnvironment( context => { destinationReached = true; throw new InvalidOperationException(); }) { ConfigTransformer = (c, r) => { c = c with { HealthCheck = new HealthCheckConfig { Passive = new PassiveHealthCheckConfig { Enabled = true } } }; return (c, r); }, ConfigureProxy = proxyBuilder => proxyBuilder.Services.AddSingleton(new MockHttpClientFactory(proxySendAsync)), }; await test.Invoke(async uri => { using var client = new HttpClient(); for (var i = 0; i < 42; i++) { using var cts = new CancellationTokenSource(); _ = requestStartedTcs.Task.ContinueWith(_ => cts.Cancel()); try { await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, uri), cts.Token); Assert.True(false); } catch { } requestStartedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); } shouldThrow = false; using var response = await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, uri)); Assert.Equal(418, (int)response.StatusCode); Assert.Equal("Hello world", await response.Content.ReadAsStringAsync()); }); Assert.False(destinationReached); } } ================================================ FILE: test/ReverseProxy.FunctionalTests/TelemetryConsumptionTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Net; using System.Net.Http; using System.Net.Sockets; using System.Security.Authentication; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Xunit; using Yarp.ReverseProxy.Common; using Yarp.ReverseProxy.Forwarder; using Yarp.Telemetry.Consumption; namespace Yarp.ReverseProxy; public class TelemetryConsumptionTests { public enum RegistrationApproach { WithInstanceHelper, WithGenericHelper, Manual } private static void RegisterTelemetryConsumers(IServiceCollection services, RegistrationApproach approach) { if (approach == RegistrationApproach.WithInstanceHelper) { services.AddTelemetryConsumer(new TelemetryConsumer()); services.AddTelemetryConsumer(new SecondTelemetryConsumer()); } else if (approach == RegistrationApproach.WithGenericHelper) { services.AddTelemetryConsumer(); services.AddTelemetryConsumer(); } else if (approach == RegistrationApproach.Manual) { services.AddSingleton(); services.AddSingleton(services => (IForwarderTelemetryConsumer)services.GetRequiredService()); services.AddSingleton(services => (IKestrelTelemetryConsumer)services.GetRequiredService()); services.AddSingleton(services => (IHttpTelemetryConsumer)services.GetRequiredService()); services.AddSingleton(services => (INameResolutionTelemetryConsumer)services.GetRequiredService()); services.AddSingleton(services => (INetSecurityTelemetryConsumer)services.GetRequiredService()); services.AddSingleton(services => (ISocketsTelemetryConsumer)services.GetRequiredService()); services.AddSingleton(); services.AddSingleton(services => (IForwarderTelemetryConsumer)services.GetRequiredService()); services.AddSingleton(services => (IKestrelTelemetryConsumer)services.GetRequiredService()); services.AddSingleton(services => (IHttpTelemetryConsumer)services.GetRequiredService()); services.AddSingleton(services => (INameResolutionTelemetryConsumer)services.GetRequiredService()); services.AddSingleton(services => (INetSecurityTelemetryConsumer)services.GetRequiredService()); services.AddSingleton(services => (ISocketsTelemetryConsumer)services.GetRequiredService()); services.AddTelemetryListeners(); } } private static void RegisterMetricsConsumers(IServiceCollection services, RegistrationApproach approach) { if (approach == RegistrationApproach.WithInstanceHelper) { services.AddMetricsConsumer(new MetricsConsumer()); } else if (approach == RegistrationApproach.WithGenericHelper) { services.AddMetricsConsumer(); } else if (approach == RegistrationApproach.Manual) { services.AddSingleton(); services.AddSingleton(services => (IMetricsConsumer)services.GetRequiredService()); services.AddSingleton(services => (IMetricsConsumer)services.GetRequiredService()); services.AddSingleton(services => (IMetricsConsumer)services.GetRequiredService()); services.AddSingleton(services => (IMetricsConsumer)services.GetRequiredService()); services.AddSingleton(services => (IMetricsConsumer)services.GetRequiredService()); services.AddSingleton(services => (IMetricsConsumer)services.GetRequiredService()); services.AddTelemetryListeners(); } } private static void VerifyStages(string[] expected, List<(string Stage, DateTime Timestamp)> stages) { Assert.Equal(expected, stages.Select(s => s.Stage).ToArray()); for (var i = 1; i < stages.Count; i++) { Assert.True(stages[i - 1].Timestamp <= stages[i].Timestamp); } } [Theory] [InlineData(RegistrationApproach.WithInstanceHelper)] [InlineData(RegistrationApproach.WithGenericHelper)] [InlineData(RegistrationApproach.Manual)] public async Task TelemetryConsumptionWorks(RegistrationApproach registrationApproach) { var useHttpsOnDestination = !OperatingSystem.IsMacOS(); var test = new TestEnvironment( async context => await context.Response.WriteAsync("Foo")) { UseHttpsOnDestination = useHttpsOnDestination, ClusterId = Guid.NewGuid().ToString(), ConfigureProxy = proxyBuilder => RegisterTelemetryConsumers(proxyBuilder.Services, registrationApproach), }; await test.Invoke(async uri => { using var httpClient = new HttpClient(); await httpClient.GetStringAsync(uri); }); var expected = new[] { "OnConnectionStart-Kestrel", "OnRequestStart-Kestrel", "OnForwarderInvoke", "OnForwarderStart", "OnForwarderStage-SendAsyncStart", "OnRequestStart", "OnConnectStart", "OnConnectStop", "OnHandshakeStart", "OnHandshakeStop", "OnConnectionEstablished", "OnRequestLeftQueue", "OnRequestHeadersStart", "OnRequestHeadersStop", "OnResponseHeadersStart", "OnResponseHeadersStop", "OnRequestStop", "OnForwarderStage-SendAsyncStop", "OnForwarderStage-ResponseContentTransferStart", "OnContentTransferred", "OnForwarderStop", "OnRequestStop-Kestrel", "OnConnectionStop-Kestrel", }; if (!useHttpsOnDestination) { expected = expected.Where(s => !s.Contains("OnHandshake", StringComparison.Ordinal)).ToArray(); } foreach (var consumerType in new[] { typeof(TelemetryConsumer), typeof(SecondTelemetryConsumer) }) { Assert.True(TelemetryConsumer.PerClusterTelemetry.TryGetValue((test.ClusterId, consumerType), out var stages)); VerifyStages(expected, stages); } } [Theory] [InlineData(RegistrationApproach.WithInstanceHelper)] [InlineData(RegistrationApproach.WithGenericHelper)] [InlineData(RegistrationApproach.Manual)] public async Task NonProxyTelemetryConsumptionWorks(RegistrationApproach registrationApproach) { var redirected = false; var test = new TestEnvironment( async context => { if (redirected) { await context.Response.WriteAsync("Foo"); } else { context.Response.Redirect("/foo"); redirected = true; } }) { UseHttpsOnDestination = true, ConfigureProxy = proxyBuilder => RegisterTelemetryConsumers(proxyBuilder.Services, registrationApproach), }; var path = $"/{Guid.NewGuid()}"; await test.Invoke(async uri => { using var httpClient = new HttpClient(); await httpClient.GetStringAsync($"{uri.TrimEnd('/')}{path}"); }); var expected = new[] { "OnRequestStart", "OnConnectStart", "OnConnectStop", "OnConnectionEstablished", "OnRequestLeftQueue", "OnRequestHeadersStart", "OnRequestHeadersStop", "OnResponseHeadersStart", "OnResponseHeadersStop", "OnRedirect", "OnRequestHeadersStart", "OnRequestHeadersStop", "OnResponseHeadersStart", "OnResponseHeadersStop", "OnResponseContentStart", "OnResponseContentStop", "OnRequestStop", }; foreach (var consumerType in new[] { typeof(TelemetryConsumer), typeof(SecondTelemetryConsumer) }) { Assert.True(TelemetryConsumer.PerPathAndQueryTelemetry.TryGetValue((path, consumerType), out var stages)); VerifyStages(expected, stages); } } private class SecondTelemetryConsumer : TelemetryConsumer { } private class TelemetryConsumer : IForwarderTelemetryConsumer, IKestrelTelemetryConsumer, IHttpTelemetryConsumer, INameResolutionTelemetryConsumer, INetSecurityTelemetryConsumer, ISocketsTelemetryConsumer { public static readonly ConcurrentDictionary<(string, Type), List<(string Stage, DateTime Timestamp)>> PerClusterTelemetry = new(); public static readonly ConcurrentDictionary<(string, Type), List<(string Stage, DateTime Timestamp)>> PerPathAndQueryTelemetry = new(); private readonly AsyncLocal> _stages = new(); private void AddStage(string stage, DateTime timestamp) { var stages = _stages.Value ??= new List<(string Stage, DateTime Timestamp)>(); lock (stages) { stages.Add((stage, timestamp)); } } public void OnForwarderStart(DateTime timestamp, string destinationPrefix) => AddStage(nameof(OnForwarderStart), timestamp); public void OnForwarderStop(DateTime timestamp, int statusCode) => AddStage(nameof(OnForwarderStop), timestamp); public void OnForwarderFailed(DateTime timestamp, ForwarderError error) => AddStage(nameof(OnForwarderFailed), timestamp); public void OnForwarderStage(DateTime timestamp, Telemetry.Consumption.ForwarderStage stage) => AddStage($"{nameof(OnForwarderStage)}-{stage}", timestamp); public void OnContentTransferring(DateTime timestamp, bool isRequest, long contentLength, long iops, TimeSpan readTime, TimeSpan writeTime) => AddStage(nameof(OnContentTransferring), timestamp); public void OnContentTransferred(DateTime timestamp, bool isRequest, long contentLength, long iops, TimeSpan readTime, TimeSpan writeTime, TimeSpan firstReadTime) => AddStage(nameof(OnContentTransferred), timestamp); public void OnForwarderInvoke(DateTime timestamp, string clusterId, string routeId, string destinationId) { AddStage(nameof(OnForwarderInvoke), timestamp); PerClusterTelemetry.TryAdd((clusterId, GetType()), _stages.Value); } public void OnRequestStart(DateTime timestamp, string scheme, string host, int port, string pathAndQuery, int versionMajor, int versionMinor, HttpVersionPolicy versionPolicy) { AddStage(nameof(OnRequestStart), timestamp); PerPathAndQueryTelemetry.TryAdd((pathAndQuery, GetType()), _stages.Value); } public void OnRequestStop(DateTime timestamp) => AddStage(nameof(OnRequestStop), timestamp); public void OnRequestFailed(DateTime timestamp) => AddStage(nameof(OnRequestFailed), timestamp); public void OnConnectionEstablished(DateTime timestamp, int versionMajor, int versionMinor) => AddStage(nameof(OnConnectionEstablished), timestamp); public void OnRequestLeftQueue(DateTime timestamp, TimeSpan timeOnQueue, int versionMajor, int versionMinor) => AddStage(nameof(OnRequestLeftQueue), timestamp); public void OnRequestHeadersStart(DateTime timestamp) => AddStage(nameof(OnRequestHeadersStart), timestamp); public void OnRequestHeadersStop(DateTime timestamp) => AddStage(nameof(OnRequestHeadersStop), timestamp); public void OnRequestContentStart(DateTime timestamp) => AddStage(nameof(OnRequestContentStart), timestamp); public void OnRequestContentStop(DateTime timestamp, long contentLength) => AddStage(nameof(OnRequestContentStop), timestamp); public void OnResponseHeadersStart(DateTime timestamp) => AddStage(nameof(OnResponseHeadersStart), timestamp); public void OnResponseHeadersStop(DateTime timestamp) => AddStage(nameof(OnResponseHeadersStop), timestamp); public void OnResponseContentStart(DateTime timestamp) => AddStage(nameof(OnResponseContentStart), timestamp); public void OnResponseContentStop(DateTime timestamp) => AddStage(nameof(OnResponseContentStop), timestamp); public void OnResolutionStart(DateTime timestamp, string hostNameOrAddress) => AddStage(nameof(OnResolutionStart), timestamp); public void OnResolutionStop(DateTime timestamp) => AddStage(nameof(OnResolutionStop), timestamp); public void OnResolutionFailed(DateTime timestamp) => AddStage(nameof(OnResolutionFailed), timestamp); public void OnHandshakeStart(DateTime timestamp, bool isServer, string targetHost) => AddStage(nameof(OnHandshakeStart), timestamp); public void OnHandshakeStop(DateTime timestamp, SslProtocols protocol) => AddStage(nameof(OnHandshakeStop), timestamp); public void OnHandshakeFailed(DateTime timestamp, bool isServer, TimeSpan elapsed, string exceptionMessage) => AddStage(nameof(OnHandshakeFailed), timestamp); public void OnConnectStart(DateTime timestamp, string address) => AddStage(nameof(OnConnectStart), timestamp); public void OnConnectStop(DateTime timestamp) => AddStage(nameof(OnConnectStop), timestamp); public void OnConnectFailed(DateTime timestamp, SocketError error, string exceptionMessage) => AddStage(nameof(OnConnectFailed), timestamp); public void OnConnectionStart(DateTime timestamp, string connectionId, string localEndPoint, string remoteEndPoint) => AddStage($"{nameof(OnConnectionStart)}-Kestrel", timestamp); public void OnRequestStart(DateTime timestamp, string connectionId, string requestId, string httpVersion, string path, string method) => AddStage($"{nameof(OnRequestStart)}-Kestrel", timestamp); public void OnRequestStop(DateTime timestamp, string connectionId, string requestId, string httpVersion, string path, string method) => AddStage($"{nameof(OnRequestStop)}-Kestrel", timestamp); public void OnConnectionStop(DateTime timestamp, string connectionId) => AddStage($"{nameof(OnConnectionStop)}-Kestrel", timestamp); public void OnRedirect(DateTime timestamp, string redirectUri) => AddStage(nameof(OnRedirect), timestamp); } [Theory] [InlineData(RegistrationApproach.WithInstanceHelper)] [InlineData(RegistrationApproach.WithGenericHelper)] [InlineData(RegistrationApproach.Manual)] public async Task MetricsConsumptionWorks(RegistrationApproach registrationApproach) { MetricsOptions.Interval = TimeSpan.FromMilliseconds(10); var test = new TestEnvironment( async context => await context.Response.WriteAsync("Foo")) { UseHttpsOnDestination = true, ConfigureProxy = proxyBuilder => RegisterMetricsConsumers(proxyBuilder.Services, registrationApproach), }; var consumerBox = new MetricsConsumer.MetricsConsumerBox(); MetricsConsumer.ScopeInstance.Value = consumerBox; MetricsConsumer consumer = null; await test.Invoke(async uri => { var httpClient = new HttpClient(); await httpClient.GetStringAsync(uri); consumer = consumerBox.Instance; try { // Do some arbitrary DNS work to get metrics, since we're connecting to localhost _ = await Dns.GetHostAddressesAsync("microsoft.com"); } catch { } await Task.WhenAll( WaitAsync(() => consumer.ProxyMetrics.LastOrDefault()?.RequestsStarted > 0, nameof(ForwarderMetrics)), WaitAsync(() => consumer.KestrelMetrics.LastOrDefault()?.TotalConnections > 0, nameof(KestrelMetrics)), WaitAsync(() => consumer.HttpMetrics.LastOrDefault()?.RequestsStarted > 0, nameof(HttpMetrics)), WaitAsync(() => consumer.SocketsMetrics.LastOrDefault()?.OutgoingConnectionsEstablished > 0, nameof(SocketsMetrics)), WaitAsync(() => consumer.NetSecurityMetrics.LastOrDefault()?.TotalTlsHandshakes > 0, nameof(NetSecurityMetrics)), WaitAsync(() => consumer.NameResolutionMetrics.LastOrDefault()?.DnsLookupsRequested > 0, nameof(NameResolutionMetrics))); }); VerifyTimestamp(consumer.ProxyMetrics.Last().Timestamp); VerifyTimestamp(consumer.KestrelMetrics.Last().Timestamp); VerifyTimestamp(consumer.HttpMetrics.Last().Timestamp); VerifyTimestamp(consumer.SocketsMetrics.Last().Timestamp); VerifyTimestamp(consumer.NetSecurityMetrics.Last().Timestamp); VerifyTimestamp(consumer.NameResolutionMetrics.Last().Timestamp); static void VerifyTimestamp(DateTime timestamp) { var now = DateTime.UtcNow; Assert.InRange(timestamp, now.Subtract(TimeSpan.FromSeconds(10)), now.AddSeconds(10)); } static async Task WaitAsync(Func condition, string name) { var stopwatch = Stopwatch.StartNew(); while (!condition()) { if (stopwatch.Elapsed > TimeSpan.FromSeconds(10)) { throw new TimeoutException($"Timed out waiting for {name}"); } await Task.Delay(10); } } } private sealed class MetricsConsumer : IMetricsConsumer, IMetricsConsumer, IMetricsConsumer, IMetricsConsumer, IMetricsConsumer, IMetricsConsumer { public sealed class MetricsConsumerBox { public MetricsConsumer Instance; } public static readonly AsyncLocal ScopeInstance = new(); public readonly ConcurrentQueue ProxyMetrics = new(); public readonly ConcurrentQueue KestrelMetrics = new(); public readonly ConcurrentQueue HttpMetrics = new(); public readonly ConcurrentQueue SocketsMetrics = new(); public readonly ConcurrentQueue NetSecurityMetrics = new(); public readonly ConcurrentQueue NameResolutionMetrics = new(); public MetricsConsumer() { ScopeInstance.Value.Instance = this; } public void OnMetrics(ForwarderMetrics previous, ForwarderMetrics current) => ProxyMetrics.Enqueue(current); public void OnMetrics(KestrelMetrics previous, KestrelMetrics current) => KestrelMetrics.Enqueue(current); public void OnMetrics(SocketsMetrics previous, SocketsMetrics current) => SocketsMetrics.Enqueue(current); public void OnMetrics(NetSecurityMetrics previous, NetSecurityMetrics current) => NetSecurityMetrics.Enqueue(current); public void OnMetrics(NameResolutionMetrics previous, NameResolutionMetrics current) => NameResolutionMetrics.Enqueue(current); public void OnMetrics(HttpMetrics previous, HttpMetrics current) => HttpMetrics.Enqueue(current); } } ================================================ FILE: test/ReverseProxy.FunctionalTests/TelemetryEnumTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Linq; using Xunit; namespace Yarp.ReverseProxy; public class TelemetryEnumTests { [Theory] [InlineData(typeof(Telemetry.Consumption.ForwarderStage), typeof(Forwarder.ForwarderStage))] [InlineData(typeof(Telemetry.Consumption.WebSocketCloseReason), typeof(WebSocketsTelemetry.WebSocketCloseReason))] public void ExposedEnumsMatchInternalCopies(Type publicEnum, Type internalEnum) { Assert.Equal(internalEnum.GetEnumNames(), publicEnum.GetEnumNames()); Assert.Equal(internalEnum.GetEnumValues().Cast().ToArray(), publicEnum.GetEnumValues().Cast().ToArray()); } } ================================================ FILE: test/ReverseProxy.FunctionalTests/WebSocketTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Net; using System.Net.Http; using System.Net.WebSockets; using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; using Xunit; using Yarp.ReverseProxy.Common; using Yarp.ReverseProxy.Forwarder; using Yarp.ReverseProxy.Transforms; namespace Yarp.ReverseProxy; public class WebSocketTests { private readonly ITestOutputHelper _output; public WebSocketTests(ITestOutputHelper output) { _output = output; } public static IEnumerable WebSocketVersionNegotiation_TestData() { foreach (Version incomingVersion in new[] { HttpVersion.Version11, HttpVersion.Version20 }) { foreach (HttpVersionPolicy versionPolicy in Enum.GetValues()) { foreach (Version destinationVersion in new[] { HttpVersion.Version11, HttpVersion.Version20, HttpVersion.Version30 }) { foreach (HttpProtocols destinationProtocols in new[] { HttpProtocols.Http1, HttpProtocols.Http2, HttpProtocols.Http1AndHttp2 }) { foreach (bool useHttpsOnDestination in new[] { true, false }) { (int version, bool canDowngrade) = (destinationVersion.Major, versionPolicy, useHttpsOnDestination) switch { (1, HttpVersionPolicy.RequestVersionOrHigher, true) => (2, true), (1, _, _) => (1, false), (2, HttpVersionPolicy.RequestVersionOrLower, true) => (2, true), (2, HttpVersionPolicy.RequestVersionOrLower, false) => (1, false), (2, _, _) => (2, false), (3, HttpVersionPolicy.RequestVersionOrLower, true) => (2, true), (3, HttpVersionPolicy.RequestVersionOrLower, false) => (1, false), (3, _, _) => (-1, false), // RequestCreation error _ => throw new Exception() }; ForwarderError? expectedProxyError = version == -1 ? ForwarderError.RequestCreation : null; bool e2eWillFail = expectedProxyError.HasValue; if (version == 2 && destinationProtocols == HttpProtocols.Http1) { // ALPN rejects HTTP/2. if (canDowngrade) { Debug.Assert(useHttpsOnDestination); version = 1; } else { e2eWillFail = true; expectedProxyError = ForwarderError.Request; } } if (version == 1 && destinationProtocols == HttpProtocols.Http2) { // ALPN rejects HTTP/1.1, or the server sends back an error response when not using TLS. e2eWillFail = true; // An error response is just a bad status code, not a failed request from the proxy's perspective. if (useHttpsOnDestination) { expectedProxyError = ForwarderError.Request; } } if (version == 2 && destinationProtocols == HttpProtocols.Http1AndHttp2 && !useHttpsOnDestination) { // No ALPN, Kestrel doesn't know whether to use HTTP/1.1 or HTTP/2, defaulting to HTTP/1.1. // YARP will see an 'HTTP_1_1_REQUIRED' error and return a 502. Debug.Assert(!canDowngrade); e2eWillFail = true; expectedProxyError = ForwarderError.Request; } string expectedVersion = version == 1 ? "HTTP/1.1" : "HTTP/2"; yield return new object[] { incomingVersion, versionPolicy, destinationVersion, destinationProtocols, useHttpsOnDestination, expectedVersion, expectedProxyError, e2eWillFail }; } } } } } } [Theory] [MemberData(nameof(WebSocketVersionNegotiation_TestData))] public async Task WebSocketVersionNegotiation(Version incomingVersion, HttpVersionPolicy versionPolicy, Version requestedDestinationVersion, HttpProtocols destinationProtocols, bool useHttpsOnDestination, string expectedVersion, ForwarderError? expectedProxyError, bool e2eWillFail) { using var cts = CreateTimer(); var test = CreateTestEnvironment(); test.ProxyProtocol = incomingVersion.Major == 1 ? HttpProtocols.Http1 : HttpProtocols.Http2; test.DestinationProtocol = destinationProtocols; test.DestinationHttpVersion = requestedDestinationVersion; test.DestinationHttpVersionPolicy = versionPolicy; test.UseHttpsOnDestination = useHttpsOnDestination; int proxyRequests = 0; ForwarderError? error = null; test.ConfigureProxyApp = builder => { builder.Use(async (context, next) => { proxyRequests++; await next(context); error = context.Features.Get()?.Error; }); }; await test.Invoke(async uri => { using var client = new ClientWebSocket(); client.Options.HttpVersion = incomingVersion; client.Options.HttpVersionPolicy = HttpVersionPolicy.RequestVersionExact; if (e2eWillFail) { var ex = await Assert.ThrowsAsync(() => SendWebSocketRequestAsync(client, uri, expectedVersion, cts.Token)); Assert.IsNotType(ex.InnerException); } else { await SendWebSocketRequestAsync(client, uri, expectedVersion, cts.Token); } }, cts.Token); Assert.Equal(1, proxyRequests); Assert.Equal(expectedProxyError, error); } [Theory] [InlineData(WebSocketMessageType.Binary)] [InlineData(WebSocketMessageType.Text)] public async Task WebSocketMessageTypes(WebSocketMessageType messageType) { using var cts = CreateTimer(); var test = CreateTestEnvironment(); await test.Invoke(async uri => { using var client = new ClientWebSocket(); var webSocketsTarget = uri.Replace("https://", "wss://").Replace("http://", "ws://"); var targetUri = new Uri(new Uri(webSocketsTarget, UriKind.Absolute), "websockets"); await client.ConnectAsync(targetUri, cts.Token); var buffer = new byte[1024]; var textToSend = $"Hello World!"; var numBytes = Encoding.UTF8.GetBytes(textToSend, buffer.AsSpan()); await client.SendAsync(new ArraySegment(buffer, 0, numBytes), messageType, endOfMessage: true, cts.Token); var message = await client.ReceiveAsync(buffer, cts.Token); Assert.Equal(messageType, message.MessageType); Assert.True(message.EndOfMessage); var text = Encoding.UTF8.GetString(buffer.AsSpan(0, message.Count)); Assert.Equal(textToSend, text); await client.CloseAsync(WebSocketCloseStatus.NormalClosure, "Bye", cts.Token); Assert.Equal(WebSocketCloseStatus.NormalClosure, client.CloseStatus); Assert.Equal("Bye", client.CloseStatusDescription); }, cts.Token); } [Fact] public async Task RawUpgradeTest() { using var cts = CreateTimer(); var test = CreateTestEnvironment(); await test.Invoke(async uri => { using var client = WebSocketTests.CreateInvoker(); var targetUri = new Uri(new Uri(uri, UriKind.Absolute), "rawupgrade"); using var request = new HttpRequestMessage(HttpMethod.Get, targetUri); // TODO: https://github.com/dotnet/yarp/issues/255 Until this is fixed the "Upgrade: WebSocket" header is required. request.Headers.TryAddWithoutValidation("Upgrade", "WebSocket"); request.Headers.TryAddWithoutValidation("Connection", "upgrade"); request.Version = new Version(1, 1); var response = await client.SendAsync(request, cts.Token); Assert.Equal(HttpStatusCode.SwitchingProtocols, response.StatusCode); using var rawStream = await response.Content.ReadAsStreamAsync(cts.Token); var buffer = new byte[5]; for (var i = 0; i <= 255; i++) { buffer[0] = (byte)i; await rawStream.WriteAsync(buffer, 0, buffer.Length, cts.Token); var read = await rawStream.ReadAsync(buffer, cts.Token); Assert.Equal(buffer.Length, read); Assert.Equal(i, buffer[0]); } await rawStream.WriteAsync(Encoding.UTF8.GetBytes("close")); while (await rawStream.ReadAsync(buffer, cts.Token) != 0) { } rawStream.Dispose(); }, cts.Token); } [Fact] // https://github.com/dotnet/yarp/issues/255 IIS claims all requests are upgradeable. public async Task FalseUpgradeTest() { using var cts = CreateTimer(); var test = CreateTestEnvironment(forceUpgradable: true); await test.Invoke(async uri => { using var client = WebSocketTests.CreateInvoker(); var targetUri = new Uri(new Uri(uri, UriKind.Absolute), "post"); using var request = new HttpRequestMessage(HttpMethod.Post, targetUri); request.Content = new StringContent("Hello World"); request.Version = new Version(1, 1); var response = await client.SendAsync(request, cts.Token); Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal("Hello World", await response.Content.ReadAsStringAsync(cts.Token)); }, cts.Token); } [Theory] [InlineData(true)] [InlineData(false)] public async Task WebSocket11_To_11(bool useHttps) { using var cts = CreateTimer(); var test = CreateTestEnvironment(); test.ProxyProtocol = HttpProtocols.Http1; test.DestinationProtocol = HttpProtocols.Http1; test.DestinationHttpVersion = HttpVersion.Version11; test.UseHttpsOnProxy = useHttps; test.UseHttpsOnDestination = useHttps; await test.Invoke(async uri => { using var client = new ClientWebSocket(); await SendWebSocketRequestAsync(client, uri, "HTTP/1.1", cts.Token); }, cts.Token); } [Theory] [InlineData(true)] [InlineData(false)] public async Task WebSocket20_To_20(bool useHttps) { using var cts = CreateTimer(); var test = CreateTestEnvironment(); test.ProxyProtocol = HttpProtocols.Http2; test.DestinationProtocol = HttpProtocols.Http2; test.DestinationHttpVersionPolicy = HttpVersionPolicy.RequestVersionExact; test.UseHttpsOnProxy = useHttps; test.UseHttpsOnDestination = useHttps; await test.Invoke(async uri => { using var client = new ClientWebSocket(); client.Options.HttpVersion = HttpVersion.Version20; client.Options.HttpVersionPolicy = HttpVersionPolicy.RequestVersionExact; await SendWebSocketRequestAsync(client, uri, "HTTP/2", cts.Token); }, cts.Token); } [Theory] [InlineData(true)] [InlineData(false)] public async Task WebSocket20_To_11(bool useHttps) { using var cts = CreateTimer(); var test = CreateTestEnvironment(); test.ProxyProtocol = HttpProtocols.Http2; test.DestinationProtocol = HttpProtocols.Http1; test.DestinationHttpVersion = HttpVersion.Version11; test.UseHttpsOnProxy = useHttps; test.UseHttpsOnDestination = useHttps; await test.Invoke(async uri => { using var client = new ClientWebSocket(); client.Options.HttpVersion = HttpVersion.Version20; client.Options.HttpVersionPolicy = HttpVersionPolicy.RequestVersionExact; await SendWebSocketRequestAsync(client, uri, "HTTP/1.1", cts.Token); }, cts.Token); } [Theory] [InlineData(true)] [InlineData(false)] public async Task WebSocket11_To_20(bool useHttps) { using var cts = CreateTimer(); var test = CreateTestEnvironment(); test.ProxyProtocol = HttpProtocols.Http1; test.DestinationProtocol = HttpProtocols.Http2; test.DestinationHttpVersionPolicy = HttpVersionPolicy.RequestVersionExact; test.UseHttpsOnProxy = useHttps; test.UseHttpsOnDestination = useHttps; await test.Invoke(async uri => { using var client = new ClientWebSocket(); client.Options.HttpVersion = HttpVersion.Version11; client.Options.HttpVersionPolicy = HttpVersionPolicy.RequestVersionExact; await SendWebSocketRequestAsync(client, uri, "HTTP/2", cts.Token); }, cts.Token); } [Fact] public async Task WebSocketFallbackFromH2() { using var cts = CreateTimer(); var test = CreateTestEnvironment(); test.ProxyProtocol = HttpProtocols.Http1; // The destination doesn't support HTTP/2, as determined by ALPN test.DestinationProtocol = HttpProtocols.Http1; test.DestinationHttpVersion = HttpVersion.Version20; test.UseHttpsOnDestination = true; await test.Invoke(async uri => { using var client = new ClientWebSocket(); await SendWebSocketRequestAsync(client, uri, "HTTP/1.1", cts.Token); }, cts.Token); } [Fact] public async Task WebSocketFallbackFromH2_FailureInSecondRequestTransform_TreatedAsRequestCreationFailure() { using var cts = CreateTimer(); ForwarderError? error = null; var test = new TestEnvironment() { TestOutput = _output, ProxyProtocol = HttpProtocols.Http1, // The destination doesn't support HTTP/2, as determined by ALPN DestinationProtocol = HttpProtocols.Http1, DestinationHttpVersion = HttpVersion.Version20, UseHttpsOnDestination = true, ConfigureProxy = builder => { builder.AddTransforms(transforms => { transforms.AddRequestTransform(context => { if (context.ProxyRequest.Version.Major == 1) { // This is the second (downgrade) request. throw new Exception("Foo"); } return default; }); }); }, ConfigureProxyApp = builder => { builder.Use(async (context, next) => { await next(context); error = context.Features.Get()?.Error; }); }, }; await test.Invoke(async uri => { try { using var client = new ClientWebSocket(); await SendWebSocketRequestAsync(client, uri, "HTTP/1.1", cts.Token); } catch { } }, cts.Token); Assert.Equal(ForwarderError.RequestCreation, error); } // [Fact] [Fact(Skip = "Manual test only, the CI doesn't always have the IIS Express test cert installed.")] public async Task WebSocketFallbackFromH2WS() { if (!OperatingSystem.IsWindows()) { // This test relies on Windows/HttpSys return; } using var cts = CreateTimer(); var test = CreateTestEnvironment(); test.ProxyProtocol = HttpProtocols.Http1; // The destination supports HTTP/2, but not H2WS test.UseHttpSysOnDestination = true; test.UseHttpsOnDestination = true; await test.Invoke(async uri => { using var client = new ClientWebSocket(); await SendWebSocketRequestAsync(client, uri, "HTTP/1.1", cts.Token); }, cts.Token); } [Theory] [InlineData(HttpVersionPolicy.RequestVersionExact, true)] [InlineData(HttpVersionPolicy.RequestVersionExact, false)] [InlineData(HttpVersionPolicy.RequestVersionOrHigher, true)] public async Task WebSocketCantFallbackFromH2(HttpVersionPolicy policy, bool useHttps) { using var cts = CreateTimer(); var test = CreateTestEnvironment(); test.ProxyProtocol = HttpProtocols.Http1; test.DestinationProtocol = HttpProtocols.Http1; test.DestinationHttpVersion = HttpVersion.Version20; test.DestinationHttpVersionPolicy = policy; test.UseHttpsOnDestination = useHttps; await test.Invoke(async uri => { using var client = new ClientWebSocket(); var webSocketsTarget = uri.Replace("https://", "wss://").Replace("http://", "ws://"); var targetUri = new Uri(new Uri(webSocketsTarget, UriKind.Absolute), "websockets"); using var invoker = CreateInvoker(); var wse = await Assert.ThrowsAsync(() => client.ConnectAsync(targetUri, invoker, cts.Token)); Assert.Equal("The server returned status code '502' when status code '101' was expected.", wse.Message); }, cts.Token); } [Theory] [InlineData(HttpProtocols.Http1)] // Checked by destination [InlineData(HttpProtocols.Http2)] // Checked by proxy public async Task InvalidKeyHeader_400(HttpProtocols destinationProtocol) { using var cts = CreateTimer(); var test = CreateTestEnvironment(); test.ProxyProtocol = HttpProtocols.Http1; test.DestinationProtocol = destinationProtocol; test.DestinationHttpVersionPolicy = HttpVersionPolicy.RequestVersionExact; test.DestinationHttpVersion = destinationProtocol == HttpProtocols.Http1 ? HttpVersion.Version11 : HttpVersion.Version20; test.ConfigureProxyApp = builder => { builder.Use(async (context, next) => { context.Request.Headers[HeaderNames.SecWebSocketKey] = "ThisIsAnIncorrectKeyHeaderLongerThan24Bytes"; var logs = TestLogger.Collect(); await next(context); if (destinationProtocol == HttpProtocols.Http1) { Assert.DoesNotContain(logs, log => log.EventId == EventIds.InvalidSecWebSocketKeyHeader); } else { Assert.Contains(logs, log => log.EventId == EventIds.InvalidSecWebSocketKeyHeader); } }); }; await test.Invoke(async uri => { using var client = new ClientWebSocket(); client.Options.CollectHttpResponseDetails = true; var webSocketsTarget = uri.Replace("https://", "wss://").Replace("http://", "ws://"); var targetUri = new Uri(new Uri(webSocketsTarget, UriKind.Absolute), "websockets"); client.Options.RemoteCertificateValidationCallback = (_, _, _, _) => true; var wse = await Assert.ThrowsAsync(() => client.ConnectAsync(targetUri, cts.Token)); Assert.Equal("The server returned status code '400' when status code '101' was expected.", wse.Message); Assert.Equal(HttpStatusCode.BadRequest, client.HttpStatusCode); // TODO: Assert the version https://github.com/dotnet/runtime/issues/75353 }, cts.Token); } [Fact] public async Task WebSocket20_To_11_WithWellFormedKeyHeader_OriginalKeyIsUsed() { using var cts = CreateTimer(); var clientKey = ProtocolHelper.CreateSecWebSocketKey(); var test = CreateTestEnvironment(); test.ProxyProtocol = HttpProtocols.Http2; test.DestinationProtocol = HttpProtocols.Http1; var originalDestinationApp = test.ConfigureDestinationApp; test.ConfigureDestinationApp = app => { app.Use((context, next) => { Assert.True(context.Request.Headers.TryGetValue(HeaderNames.SecWebSocketKey, out var key)); Assert.Equal(clientKey, key); return next(context); }); originalDestinationApp(app); }; await test.Invoke(async uri => { using var client = new ClientWebSocket(); client.Options.HttpVersion = HttpVersion.Version20; client.Options.HttpVersionPolicy = HttpVersionPolicy.RequestVersionExact; client.Options.SetRequestHeader(HeaderNames.SecWebSocketKey, clientKey); await SendWebSocketRequestAsync(client, uri, "HTTP/1.1", cts.Token); }, cts.Token); } [Fact] public async Task WebSocket20_To_11_WithInvalidKeyHeader_RequestRejected() { using var cts = CreateTimer(); var test = CreateTestEnvironment(); test.ProxyProtocol = HttpProtocols.Http2; test.DestinationProtocol = HttpProtocols.Http1; test.ConfigureProxyApp = builder => { builder.Use(async (context, next) => { var logs = TestLogger.Collect(); await next(context); Assert.Contains(logs, log => log.EventId == EventIds.InvalidSecWebSocketKeyHeader); }); }; await test.Invoke(async uri => { var webSocketsTarget = uri.Replace("http://", "ws://"); var targetUri = new Uri(new Uri(webSocketsTarget, UriKind.Absolute), "websockets"); using var client = new ClientWebSocket(); client.Options.HttpVersion = HttpVersion.Version20; client.Options.HttpVersionPolicy = HttpVersionPolicy.RequestVersionExact; client.Options.CollectHttpResponseDetails = true; client.Options.SetRequestHeader(HeaderNames.SecWebSocketKey, "Foo"); using var invoker = CreateInvoker(); var wse = await Assert.ThrowsAsync(() => client.ConnectAsync(targetUri, invoker, cts.Token)); Assert.Equal("The server returned status code '400' when status code '200' was expected.", wse.Message); Assert.Equal(HttpStatusCode.BadRequest, client.HttpStatusCode); }, cts.Token); } private async Task SendWebSocketRequestAsync(ClientWebSocket client, string uri, string destinationProtocol, CancellationToken token) { var webSocketsTarget = uri.Replace("https://", "wss://").Replace("http://", "ws://"); var targetUri = new Uri(new Uri(webSocketsTarget, UriKind.Absolute), "websocketversion"); using var invoker = CreateInvoker(); await client.ConnectAsync(targetUri, invoker, token); _output.WriteLine("Client connected."); var buffer = new byte[1024]; var textToSend = $"Hello World!"; var numBytes = Encoding.UTF8.GetBytes(textToSend, buffer); await client.SendAsync(buffer.AsMemory(0, numBytes), WebSocketMessageType.Text, endOfMessage: true, token); _output.WriteLine($"Client sent {numBytes}."); var message = await client.ReceiveAsync(buffer, token); _output.WriteLine($"Client received {message.Count}."); Assert.Equal(WebSocketMessageType.Text, message.MessageType); Assert.True(message.EndOfMessage); var text = Encoding.UTF8.GetString(buffer.AsSpan(0, message.Count)); Assert.Equal(destinationProtocol, text); _output.WriteLine($"Client sending Close."); await client.CloseAsync(WebSocketCloseStatus.NormalClosure, "Bye", token); Assert.Equal(WebSocketCloseStatus.NormalClosure, client.CloseStatus); Assert.Equal("Bye", client.CloseStatusDescription); _output.WriteLine($"Client Closed."); } private TestEnvironment CreateTestEnvironment(bool forceUpgradable = false) { return new TestEnvironment() { TestOutput = _output, ConfigureDestinationServices = destinationServices => { destinationServices.AddRouting(); }, ConfigureDestinationApp = destinationApp => { destinationApp.UseWebSockets(); destinationApp.UseRouting(); destinationApp.UseEndpoints(builder => { builder.Map("/websockets", WebSocket); builder.Map("/websocketVersion", WebSocketVersion); builder.Map("/rawupgrade", RawUpgrade); builder.Map("/post", Post); }); }, ConfigureProxyApp = proxyApp => { // Mimic the IIS issue https://github.com/dotnet/yarp/issues/255 proxyApp.Use((context, next) => { if (forceUpgradable && !(context.Features.Get()?.IsUpgradableRequest == true)) { context.Features.Set(new AlwaysUpgradeFeature()); } return next(); }); }, }; static async Task WebSocket(HttpContext httpContext) { var logger = httpContext.RequestServices.GetRequiredService>(); if (!httpContext.WebSockets.IsWebSocketRequest) { httpContext.Response.StatusCode = StatusCodes.Status400BadRequest; logger.LogInformation("Non-WebSocket request refused."); return; } using var webSocket = await httpContext.WebSockets.AcceptWebSocketAsync(); logger.LogInformation("WebSocket accepted."); var buffer = new byte[1024]; while (true) { var message = await webSocket.ReceiveAsync(buffer, httpContext.RequestAborted); if (message.MessageType == WebSocketMessageType.Close) { logger.LogInformation("WebSocket Close received {status}.", message.CloseStatus); await webSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, message.CloseStatusDescription, httpContext.RequestAborted); logger.LogInformation("WebSocket Close sent {status}.", WebSocketCloseStatus.NormalClosure); return; } logger.LogInformation("WebSocket received {count} bytes.", message.Count); await webSocket.SendAsync(buffer[0..message.Count], message.MessageType, message.EndOfMessage, httpContext.RequestAborted); logger.LogInformation("WebSocket sent {count} bytes.", message.Count); } } static async Task WebSocketVersion(HttpContext httpContext) { var logger = httpContext.RequestServices.GetRequiredService>(); if (!httpContext.WebSockets.IsWebSocketRequest) { httpContext.Response.StatusCode = StatusCodes.Status400BadRequest; logger.LogInformation("Non-WebSocket request refused."); return; } using var webSocket = await httpContext.WebSockets.AcceptWebSocketAsync(); logger.LogInformation("WebSocket accepted."); var buffer = new byte[1024]; while (true) { var message = await webSocket.ReceiveAsync(buffer, httpContext.RequestAborted); if (message.MessageType == WebSocketMessageType.Close) { logger.LogInformation("WebSocket Close received {status}.", message.CloseStatus); await webSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, message.CloseStatusDescription, httpContext.RequestAborted); logger.LogInformation("WebSocket Close sent {status}.", WebSocketCloseStatus.NormalClosure); return; } logger.LogInformation("WebSocket received {count} bytes.", message.Count); await webSocket.SendAsync(Encoding.ASCII.GetBytes(httpContext.Request.Protocol), WebSocketMessageType.Text, endOfMessage: true, httpContext.RequestAborted); logger.LogInformation("WebSocket sent {count} bytes.", httpContext.Request.Protocol.Length); } } static async Task RawUpgrade(HttpContext httpContext) { var upgradeFeature = httpContext.Features.Get(); if (upgradeFeature is null || !upgradeFeature.IsUpgradableRequest) { httpContext.Response.StatusCode = StatusCodes.Status426UpgradeRequired; return; } await using var stream = await upgradeFeature.UpgradeAsync(); var buffer = new byte[5]; int read; while ((read = await stream.ReadAsync(buffer, httpContext.RequestAborted)) != 0) { await stream.WriteAsync(buffer, 0, read, httpContext.RequestAborted); if (string.Equals("close", Encoding.UTF8.GetString(buffer, 0, read), StringComparison.Ordinal)) { break; } } } static async Task Post(HttpContext httpContext) { var body = await new StreamReader(httpContext.Request.Body).ReadToEndAsync(); await httpContext.Response.WriteAsync(body); } } private static CancellationTokenSource CreateTimer() { if (Debugger.IsAttached) { return new CancellationTokenSource(); } return new CancellationTokenSource(TimeSpan.FromSeconds(15)); } private static HttpMessageInvoker CreateInvoker() { var handler = new SocketsHttpHandler { AllowAutoRedirect = false, AutomaticDecompression = DecompressionMethods.None, UseCookies = false, UseProxy = false }; handler.SslOptions.RemoteCertificateValidationCallback = (_, _, _, _) => true; return new HttpMessageInvoker(handler); } private class AlwaysUpgradeFeature : IHttpUpgradeFeature { public bool IsUpgradableRequest => true; public Task UpgradeAsync() { throw new InvalidOperationException("This wasn't supposed to get called."); } } } ================================================ FILE: test/ReverseProxy.FunctionalTests/WebSocketsTelemetryTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. #nullable enable using System; using System.Net; using System.Net.Http; using System.Net.WebSockets; using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.Extensions.DependencyInjection; using Xunit; using Yarp.ReverseProxy.Common; using Yarp.Telemetry.Consumption; using Yarp.Tests.Common; namespace Yarp.ReverseProxy; public class WebSocketsTelemetryTests { private readonly ITestOutputHelper _output; public WebSocketsTelemetryTests(ITestOutputHelper output) { _output = output; } [Fact] public async Task NoWebSocketsUpgrade_NoTelemetryWritten() { var telemetry = await TestAsync( async uri => { using var client = new HttpClient(); await client.GetStringAsync(uri); }, (context, webSocket) => throw new InvalidOperationException("Shouldn't be reached")); Assert.Null(telemetry); } [Theory] [InlineData(0, 0, 42)] [InlineData(0, 1, 42)] [InlineData(1, 0, 42)] [InlineData(23, 29, 0)] [InlineData(17, 19, 1)] [InlineData(11, 13, 100)] [InlineData(5, 7, 1_000)] [InlineData(2, 3, 100_000)] public async Task MessagesExchanged_CorrectNumberReported(int read, int written, int messageSize) { var telemetry = await TestAsync( async uri => { using var client = new ClientWebSocket(); await client.ConnectAsync(uri, CancellationToken.None); var webSocket = new WebSocketAdapter(client); await Task.WhenAll( SendMessagesAndCloseAsync(webSocket, read, messageSize), ReceiveAllMessagesAsync(webSocket)); }, async (context, webSocket) => { await Task.WhenAll( SendMessagesAndCloseAsync(webSocket, written, messageSize), ReceiveAllMessagesAsync(webSocket)); }, new TestTimeProvider(new TimeSpan(42))); Assert.NotNull(telemetry); Assert.Equal(42, telemetry!.EstablishedTime.Ticks); Assert.Contains(telemetry.CloseReason, new[] { WebSocketCloseReason.ClientGracefulClose, WebSocketCloseReason.ServerGracefulClose }); Assert.Equal(read, telemetry!.MessagesRead); Assert.Equal(written, telemetry.MessagesWritten); } [Fact] public async Task Http2WebSocketsWork() { var read = 11; var written = 13; var messageSize = 100; var telemetry = await TestAsync( async uri => { using var invoker = CreateInvoker(); using var client = new ClientWebSocket(); client.Options.HttpVersion = HttpVersion.Version20; client.Options.HttpVersionPolicy = HttpVersionPolicy.RequestVersionExact; await client.ConnectAsync(uri, invoker, CancellationToken.None); var webSocket = new WebSocketAdapter(client); await Task.WhenAll( SendMessagesAndCloseAsync(webSocket, read, messageSize), ReceiveAllMessagesAsync(webSocket)); }, async (context, webSocket) => { await Task.WhenAll( SendMessagesAndCloseAsync(webSocket, written, messageSize), ReceiveAllMessagesAsync(webSocket)); }, new TestTimeProvider(new TimeSpan(42)), http2Proxy: true); Assert.NotNull(telemetry); Assert.Equal(42, telemetry!.EstablishedTime.Ticks); Assert.Contains(telemetry.CloseReason, new[] { WebSocketCloseReason.ClientGracefulClose, WebSocketCloseReason.ServerGracefulClose }); Assert.Equal(read, telemetry!.MessagesRead); Assert.Equal(written, telemetry.MessagesWritten); } public enum Behavior { ClosesConnection = 1, SendsClose_WaitsForClose = 2, SendsClose_ClosesConnection = 4 | ClosesConnection, WaitsForClose_SendsClose = 8, WaitsForClose_ClosesConnection = 16 | ClosesConnection, } [Theory] // Both sides close the connection - race between which is noticed first [InlineData(Behavior.ClosesConnection, Behavior.ClosesConnection, WebSocketCloseReason.Unknown, WebSocketCloseReason.ClientDisconnect, WebSocketCloseReason.ServerDisconnect)] // One side sends a graceful close [InlineData(Behavior.SendsClose_ClosesConnection, Behavior.WaitsForClose_ClosesConnection, WebSocketCloseReason.ClientGracefulClose)] [InlineData(Behavior.SendsClose_WaitsForClose, Behavior.WaitsForClose_ClosesConnection, WebSocketCloseReason.ClientGracefulClose)] [InlineData(Behavior.WaitsForClose_ClosesConnection, Behavior.SendsClose_ClosesConnection, WebSocketCloseReason.ServerGracefulClose)] [InlineData(Behavior.WaitsForClose_ClosesConnection, Behavior.SendsClose_WaitsForClose, WebSocketCloseReason.ServerGracefulClose)] // One side sends a graceful close while the other disconnects - race between which is noticed first [InlineData(Behavior.SendsClose_WaitsForClose, Behavior.ClosesConnection, WebSocketCloseReason.ClientGracefulClose, WebSocketCloseReason.ServerDisconnect)] [InlineData(Behavior.SendsClose_ClosesConnection, Behavior.ClosesConnection, WebSocketCloseReason.ClientGracefulClose, WebSocketCloseReason.ServerDisconnect)] [InlineData(Behavior.ClosesConnection, Behavior.SendsClose_ClosesConnection, WebSocketCloseReason.ServerGracefulClose, WebSocketCloseReason.ClientDisconnect)] [InlineData(Behavior.ClosesConnection, Behavior.SendsClose_WaitsForClose, WebSocketCloseReason.ServerGracefulClose, WebSocketCloseReason.ClientDisconnect)] // One side closes the connection while the other is waiting for messages [InlineData(Behavior.ClosesConnection, Behavior.WaitsForClose_SendsClose, WebSocketCloseReason.ClientDisconnect)] [InlineData(Behavior.ClosesConnection, Behavior.WaitsForClose_ClosesConnection, WebSocketCloseReason.ClientDisconnect)] [InlineData(Behavior.WaitsForClose_SendsClose, Behavior.ClosesConnection, WebSocketCloseReason.ServerDisconnect)] [InlineData(Behavior.WaitsForClose_ClosesConnection, Behavior.ClosesConnection, WebSocketCloseReason.ServerDisconnect)] // Graceful, mutual close - other side closes as a reaction to receiving close [InlineData(Behavior.SendsClose_WaitsForClose, Behavior.WaitsForClose_SendsClose, WebSocketCloseReason.ClientGracefulClose)] [InlineData(Behavior.SendsClose_ClosesConnection, Behavior.WaitsForClose_SendsClose, WebSocketCloseReason.ClientGracefulClose)] [InlineData(Behavior.WaitsForClose_SendsClose, Behavior.SendsClose_WaitsForClose, WebSocketCloseReason.ServerGracefulClose)] [InlineData(Behavior.WaitsForClose_SendsClose, Behavior.SendsClose_ClosesConnection, WebSocketCloseReason.ServerGracefulClose)] // Graceful, mutual close - both sides close at the same time - race between which is noticed first [InlineData(Behavior.SendsClose_WaitsForClose, Behavior.SendsClose_WaitsForClose, WebSocketCloseReason.ClientGracefulClose, WebSocketCloseReason.ServerGracefulClose)] [InlineData(Behavior.SendsClose_WaitsForClose, Behavior.SendsClose_ClosesConnection, WebSocketCloseReason.ClientGracefulClose, WebSocketCloseReason.ServerGracefulClose)] [InlineData(Behavior.SendsClose_ClosesConnection, Behavior.SendsClose_WaitsForClose, WebSocketCloseReason.ClientGracefulClose, WebSocketCloseReason.ServerGracefulClose)] [InlineData(Behavior.SendsClose_ClosesConnection, Behavior.SendsClose_ClosesConnection, WebSocketCloseReason.ClientGracefulClose, WebSocketCloseReason.ServerGracefulClose)] public async Task ConnectionClosed_BlameAttributedCorrectly(Behavior clientBehavior, Behavior serverBehavior, params WebSocketCloseReason[] expectedReasons) { var serverSawClose = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var telemetry = await TestAsync( async uri => { using var client = new ClientWebSocket(); // Keep sending messages from the client in order to observe a server disconnect sooner client.Options.KeepAliveInterval = TimeSpan.FromMilliseconds(10); await client.ConnectAsync(uri, CancellationToken.None); var webSocket = new WebSocketAdapter(client); try { await ProcessAsync(webSocket, clientBehavior, client: client); } catch (Exception ex) { _output.WriteLine($"Ignored client exception: {ex}"); Assert.True(serverBehavior.HasFlag(Behavior.ClosesConnection)); } }, async (context, webSocket) => { try { await ProcessAsync(webSocket, serverBehavior, context: context); } catch (Exception ex) { _output.WriteLine($"Ignored destination exception: {ex}"); Assert.True(clientBehavior.HasFlag(Behavior.ClosesConnection)); } }); Assert.NotNull(telemetry); Assert.Contains(telemetry!.CloseReason, expectedReasons); async Task ProcessAsync(WebSocketAdapter webSocket, Behavior behavior, ClientWebSocket? client = null, HttpContext? context = null) { if (behavior == Behavior.SendsClose_WaitsForClose || behavior == Behavior.SendsClose_ClosesConnection) { await webSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "Bye"); } if (behavior == Behavior.SendsClose_WaitsForClose || behavior == Behavior.WaitsForClose_SendsClose || behavior == Behavior.WaitsForClose_ClosesConnection) { await ReceiveAllMessagesAsync(webSocket); if (context is not null) { serverSawClose.SetResult(); } } if (behavior == Behavior.WaitsForClose_SendsClose) { await webSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "Bye"); } if (behavior.HasFlag(Behavior.ClosesConnection)) { if (client is not null && behavior is Behavior.SendsClose_ClosesConnection && serverBehavior is Behavior.WaitsForClose_SendsClose or Behavior.WaitsForClose_ClosesConnection) { // If we're sending a close message and expect the server to receive it, wait before killing the connection. await serverSawClose.Task.WaitAsync(TimeSpan.FromMinutes(1)); } client?.Abort(); if (context is not null) { await context.Response.Body.FlushAsync(); context.Abort(); } } } } [Theory] [InlineData(100, 200, WebSocketCloseReason.ClientGracefulClose)] [InlineData(200, 100, WebSocketCloseReason.ServerGracefulClose)] [InlineData(100, 100, WebSocketCloseReason.ServerGracefulClose)] // Implementation detail public async Task ConnectionClosed_BlameReliesOnCloseTimes(long clientCloseTime, long serverCloseTime, WebSocketCloseReason expectedCloseReason) { var timeProvider = new TestTimeProvider(new TimeSpan(1)); var telemetry = await TestAsync( async uri => { using var client = new ClientWebSocket(); await client.ConnectAsync(uri, CancellationToken.None); var webSocket = new WebSocketAdapter(client); await ProcessAsync(webSocket, timeProvider, clientCloseTime, sendCloseFirst: clientCloseTime <= serverCloseTime); }, async (context, webSocket) => { await ProcessAsync(webSocket, timeProvider, serverCloseTime, sendCloseFirst: serverCloseTime < clientCloseTime); }, timeProvider); Assert.NotNull(telemetry); Assert.Equal(1, telemetry!.EstablishedTime.Ticks); Assert.Equal(expectedCloseReason, telemetry.CloseReason); static async Task ProcessAsync(WebSocketAdapter webSocket, TestTimeProvider timeProvider, long closeTime, bool sendCloseFirst) { await SendAndAcknowledgeMessageAsync(webSocket); var receiveTask = ReceiveAllMessagesAsync(webSocket); if (!sendCloseFirst) { await receiveTask; } lock (timeProvider) { timeProvider.AdvanceTo(TimeSpan.FromTicks(closeTime)); } await webSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "Bye", CancellationToken.None); await receiveTask; } } private static async Task SendAndAcknowledgeMessageAsync(WebSocketAdapter webSocket) { var receiveBuffer = new byte[10]; var sendTask = webSocket.SendAsync("Hello"u8.ToArray(), WebSocketMessageType.Text, endOfMessage: true).AsTask(); var receiveTask = webSocket.ReceiveAsync(receiveBuffer).AsTask(); await Task.WhenAll(sendTask, receiveTask); Assert.Equal("Hello", Encoding.UTF8.GetString(receiveBuffer[..(await receiveTask).Count])); } private static async Task ReceiveAllMessagesAsync(WebSocketAdapter webSocket) { Memory buffer = new byte[1024]; while (true) { var result = await webSocket.ReceiveAsync(buffer); if (result.MessageType == WebSocketMessageType.Close) { break; } } } private static async Task SendMessagesAndCloseAsync(WebSocketAdapter webSocket, int messageCount, int messageSize) { var rng = new Random(42); var buffer = new byte[1024]; for (var i = 0; i < messageCount; i++) { var remaining = messageSize; while (remaining > 1) { var chunkSize = Math.Min(buffer.Length, remaining - 1); remaining -= chunkSize; var chunk = buffer.AsMemory(0, chunkSize); rng.NextBytes(chunk.Span); await webSocket.SendAsync(chunk, WebSocketMessageType.Binary, endOfMessage: false); } await webSocket.SendAsync(buffer.AsMemory(0, remaining), WebSocketMessageType.Binary, endOfMessage: true); } await webSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "Bye", CancellationToken.None); } private class WebSocketAdapter { private readonly ClientWebSocket? _client; private readonly WebSocket? _server; public WebSocketAdapter(ClientWebSocket? client = null, WebSocket? server = null) { Assert.True(client is null ^ server is null); _client = client; _server = server; } public ValueTask ReceiveAsync(Memory buffer, CancellationToken cancellationToken = default) { return _client is not null ? _client.ReceiveAsync(buffer, cancellationToken) : _server!.ReceiveAsync(buffer, cancellationToken); } public ValueTask SendAsync(ReadOnlyMemory buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken = default) { return _client is not null ? _client.SendAsync(buffer, messageType, endOfMessage, cancellationToken) : _server!.SendAsync(buffer, messageType, endOfMessage, cancellationToken); } public Task CloseOutputAsync(WebSocketCloseStatus closeStatus, string? statusDescription, CancellationToken cancellationToken = default) { return _client is not null ? _client.CloseOutputAsync(closeStatus, statusDescription, cancellationToken) : _server!.CloseOutputAsync(closeStatus, statusDescription, cancellationToken); } } private static async Task TestAsync(Func requestDelegate, Func destinationDelegate, TimeProvider? timeProvider = null, bool http2Proxy = false) { var telemetryConsumer = new TelemetryConsumer(); var test = new TestEnvironment() { ConfigureDestinationApp = destinationApp => { destinationApp.UseWebSockets(); destinationApp.Run(async context => { if (context.WebSockets.IsWebSocketRequest) { var webSocket = await context.WebSockets.AcceptWebSocketAsync(); await destinationDelegate(context, new WebSocketAdapter(server: webSocket)); } }); }, ConfigureProxyServices = proxyServices => { if (timeProvider is not null) { proxyServices.AddSingleton(timeProvider); } }, ConfigureProxy = proxyBuilder => { proxyBuilder.Services.AddTelemetryConsumer(telemetryConsumer); }, ConfigureProxyApp = proxyApp => { proxyApp.UseWebSocketsTelemetry(); }, }; if (http2Proxy) { test.ProxyProtocol = HttpProtocols.Http2; } await test.Invoke(async uri => { var webSocketsTarget = uri.Replace("https://", "wss://").Replace("http://", "ws://"); var webSocketsUri = new Uri(webSocketsTarget, UriKind.Absolute); await requestDelegate(webSocketsUri); }); return telemetryConsumer.Telemetry; } private record WebSocketsTelemetry(DateTime Timestamp, DateTime EstablishedTime, WebSocketCloseReason CloseReason, long MessagesRead, long MessagesWritten); private class TelemetryConsumer : IWebSocketsTelemetryConsumer { public WebSocketsTelemetry? Telemetry { get; private set; } public void OnWebSocketClosed(DateTime timestamp, DateTime establishedTime, WebSocketCloseReason closeReason, long messagesRead, long messagesWritten) { Telemetry = new WebSocketsTelemetry(timestamp, establishedTime, closeReason, messagesRead, messagesWritten); } } private static HttpMessageInvoker CreateInvoker() { var handler = new SocketsHttpHandler { AllowAutoRedirect = false, AutomaticDecompression = DecompressionMethods.None, UseCookies = false, UseProxy = false }; handler.SslOptions.RemoteCertificateValidationCallback = (_, _, _, _) => true; return new HttpMessageInvoker(handler); } } ================================================ FILE: test/ReverseProxy.FunctionalTests/Yarp.ReverseProxy.FunctionalTests.csproj ================================================ $(TestTFMs) $(NoWarn);SYSLIB0057 Exe Yarp.ReverseProxy true ================================================ FILE: test/ReverseProxy.Tests/Common/EventAssertExtensions.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Diagnostics.Tracing; using System.Linq; using Xunit; using Yarp.ReverseProxy.Forwarder; namespace Yarp.Tests.Common; internal static class EventAssertExtensions { public static (ForwarderStage Stage, DateTime TimeStamp)[] GetProxyStages(this List events) { return events .Where(e => e.EventName == "ForwarderStage") .Select(e => { var stage = (ForwarderStage)Assert.Single(e.Payload); Assert.InRange(stage, ForwarderStage.SendAsyncStart, ForwarderStage.ResponseUpgrade); return (stage, e.TimeStamp); }) .ToArray(); } public static void AssertContainProxyStages(this List events, bool hasRequestContent = true, bool upgrade = false, bool hasResponseContent = true) { var stages = new List() { ForwarderStage.SendAsyncStart, ForwarderStage.SendAsyncStop, }; if (hasRequestContent) { stages.Add(ForwarderStage.RequestContentTransferStart); } if (upgrade) { stages.Add(ForwarderStage.ResponseUpgrade); } if (hasResponseContent) { stages.Add(ForwarderStage.ResponseContentTransferStart); } events.AssertContainProxyStages(stages.ToArray()); } public static void AssertContainProxyStages(this List events, ForwarderStage[] expectedStages) { var proxyStages = events.GetProxyStages() .Select(s => s.Stage) .ToArray(); var presentStages = proxyStages.ToHashSet(); Assert.Equal(presentStages.Count, proxyStages.Length); foreach (var expectedStage in expectedStages) { Assert.Contains(expectedStage, presentStages); } presentStages.RemoveWhere(s => expectedStages.Contains(s)); Assert.Empty(presentStages); } } ================================================ FILE: test/ReverseProxy.Tests/Common/HttpContentExtensions.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.IO; using System.Net.Http; using System.Threading; using System.Threading.Tasks; namespace Yarp.Tests.Common; internal static class HttpContentExtensions { public static Task CopyToWithCancellationAsync(this HttpContent httpContent, Stream stream) { // StreamCopyHttpContent assumes that the cancellation token passed to it can always be canceled. // This is the case for real callers, so we insert a dummy CTS in tests to allow us to keep the debug assertion. return httpContent.CopyToAsync(stream, new CancellationTokenSource().Token); } } ================================================ FILE: test/ReverseProxy.Tests/Common/MockHttpHandler.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Net.Http; using System.Threading; using System.Threading.Tasks; namespace Yarp.Tests.Common; internal class MockHttpHandler : HttpMessageHandler { private readonly Func> _func; public MockHttpHandler(Func> func) { ArgumentNullException.ThrowIfNull(func); _func = func; } public static HttpMessageInvoker CreateClient(Func> func) { var handler = new MockHttpHandler(func); return new HttpMessageInvoker(handler); } protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { return _func(request, cancellationToken); } } ================================================ FILE: test/ReverseProxy.Tests/Common/TaskExtensions.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Diagnostics; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; namespace Yarp.Tests.Common; /// /// Extensions for the class. /// internal static class TaskExtensions { public static TimeSpan DefaultTimeoutTimeSpan { get; } = TimeSpan.FromSeconds(5); public static Task DefaultTimeout(this ValueTask task) { return task.AsTask().TimeoutAfter(DefaultTimeoutTimeSpan); } public static Task DefaultTimeout(this ValueTask task) { return task.AsTask().TimeoutAfter(DefaultTimeoutTimeSpan); } public static Task DefaultTimeout(this Task task) { return task.TimeoutAfter(DefaultTimeoutTimeSpan); } public static Task DefaultTimeout(this Task task) { return task.TimeoutAfter(DefaultTimeoutTimeSpan); } private static async Task TimeoutAfter(this Task task, TimeSpan timeout, [CallerFilePath] string filePath = null, [CallerLineNumber] int lineNumber = default) { // Don't create a timer if the task is already completed // or the debugger is attached if (task.IsCompleted || Debugger.IsAttached) { return await task; } try { return await task.WaitAsync(timeout); } catch (TimeoutException ex) when (ex.Source == typeof(TaskExtensions).Namespace) { throw new TimeoutException(CreateMessage(timeout, filePath, lineNumber)); } } private static async Task TimeoutAfter(this Task task, TimeSpan timeout, [CallerFilePath] string filePath = null, [CallerLineNumber] int lineNumber = default) { // Don't create a timer if the task is already completed // or the debugger is attached if (task.IsCompleted || Debugger.IsAttached) { await task; return; } var cts = new CancellationTokenSource(); if (task == await Task.WhenAny(task, Task.Delay(timeout, cts.Token))) { cts.Cancel(); await task; } else { throw new TimeoutException(CreateMessage(timeout, filePath, lineNumber)); } } private static string CreateMessage(TimeSpan timeout, string filePath, int lineNumber) => string.IsNullOrEmpty(filePath) ? $"The operation timed out after reaching the limit of {timeout.TotalMilliseconds}ms." : $"The operation at {filePath}:{lineNumber} timed out after reaching the limit of {timeout.TotalMilliseconds}ms."; } ================================================ FILE: test/ReverseProxy.Tests/Common/TestEventListener.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; using System.Diagnostics.Tracing; using System.Threading; namespace Yarp.Tests.Common; internal static class TestEventListener { private static readonly AsyncLocal> _eventsAsyncLocal = new(); #pragma warning disable IDE0052 // Remove unread private members private static readonly InternalEventListener _listener = new(); #pragma warning restore IDE0052 public static List Collect() => _eventsAsyncLocal.Value ??= []; private sealed class InternalEventListener : EventListener { protected override void OnEventSourceCreated(EventSource eventSource) { if (eventSource.Name == "Yarp.ReverseProxy") { EnableEvents(eventSource, EventLevel.LogAlways, EventKeywords.All); } } protected override void OnEventWritten(EventWrittenEventArgs eventData) => _eventsAsyncLocal.Value?.Add(eventData); } } ================================================ FILE: test/ReverseProxy.Tests/Common/TestResources.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Runtime.InteropServices; using System.Security.Cryptography.X509Certificates; using System.Threading; using Xunit; namespace Yarp.Tests.Common; public static class TestResources { private const int MutexTimeout = 120 * 1000; private static readonly Mutex importPfxMutex = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? new Mutex(initiallyOwned: false, "Global\\Yarp.ReverseProxy.Tests.Certificates.LoadPfxCertificate") : null; public static X509Certificate2 GetTestCertificate(string certName = "testCert.pfx") { // On Windows, applications should not import PFX files in parallel to avoid a known system-level // race condition bug in native code which can cause crashes/corruption of the certificate state. if (importPfxMutex is not null) { Assert.True(importPfxMutex.WaitOne(MutexTimeout), "Cannot acquire the global certificate mutex."); } try { return new X509Certificate2(GetCertPath(certName), "testPassword"); } finally { importPfxMutex?.ReleaseMutex(); } } public static IWebProxy GetTestWebProxy(string address = "http://localhost:8080", bool? bypassOnLocal = null, bool? useDefaultCredentials = null) { var webProxy = new WebProxy(new System.Uri(address)); if (bypassOnLocal is not null) { webProxy.BypassProxyOnLocal = bypassOnLocal.Value; } if (useDefaultCredentials is not null) { webProxy.UseDefaultCredentials = useDefaultCredentials.Value; } return webProxy; } public static string GetCertPath(string fileName) { if (fileName is null) { return null; } var basePath = Path.Combine(Directory.GetCurrentDirectory(), "TestCertificates"); return Path.Combine(basePath, fileName); } public static IEnumerable<(string Name, string[] Values)> ParseNameAndValues(string names, string values) => names.Split("; ").Zip(values.Split(", ")).GroupBy(p => p.First, (k, g) => (Name: k, Values: g.Select(i => i.Second).ToArray())); } ================================================ FILE: test/ReverseProxy.Tests/Common/TestTrailersFeature.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http; namespace Yarp.Tests.Common; internal sealed class TestTrailersFeature : IHttpResponseTrailersFeature { public IHeaderDictionary Trailers { get; set; } = new HeaderDictionary(); } ================================================ FILE: test/ReverseProxy.Tests/Configuration/ActiveHealthCheckConfigTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using Xunit; namespace Yarp.ReverseProxy.Configuration.Tests; public class ActiveHealthCheckConfigTests { [Fact] public void Equals_Same_Value_Returns_True() { var options1 = new ActiveHealthCheckConfig { Enabled = true, Interval = TimeSpan.FromSeconds(2), Timeout = TimeSpan.FromSeconds(1), Policy = "Any5xxResponse", Path = "/a", }; var options2 = new ActiveHealthCheckConfig { Enabled = true, Interval = TimeSpan.FromSeconds(2), Timeout = TimeSpan.FromSeconds(1), Policy = "any5xxResponse", Path = "/a", }; var equals = options1.Equals(options2); Assert.True(equals); Assert.Equal(options1.GetHashCode(), options2.GetHashCode()); } [Fact] public void Equals_Different_Value_Returns_False() { var options1 = new ActiveHealthCheckConfig { Enabled = true, Interval = TimeSpan.FromSeconds(2), Timeout = TimeSpan.FromSeconds(1), Policy = "Any5xxResponse", Path = "/a", }; var options2 = new ActiveHealthCheckConfig { Enabled = false, Interval = TimeSpan.FromSeconds(4), Timeout = TimeSpan.FromSeconds(2), Policy = "AnyFailure", Path = "/b", }; var equals = options1.Equals(options2); Assert.False(equals); } [Fact] public void Equals_DifferingQueries_Returns_False() { var options1 = new ActiveHealthCheckConfig { Query = "?key=value1" }; var options2 = new ActiveHealthCheckConfig { Query = "?key=value2" }; var equals = options1.Equals(options2); Assert.False(equals); } [Fact] public void Equals_Second_Null_Returns_False() { var options1 = new ActiveHealthCheckConfig(); var equals = options1.Equals(null); Assert.False(equals); } } ================================================ FILE: test/ReverseProxy.Tests/Configuration/ClusterConfigTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Globalization; using System.Net.Http; using System.Security.Authentication; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using Xunit; using Yarp.ReverseProxy.LoadBalancing; using Yarp.ReverseProxy.Forwarder; namespace Yarp.ReverseProxy.Configuration.Tests; public class ClusterConfigTests { [Fact] public void Equals_Same_Value_Returns_True() { var config1 = new ClusterConfig { ClusterId = "cluster1", Destinations = new Dictionary(StringComparer.OrdinalIgnoreCase) { { "destinationA", new DestinationConfig { Address = "https://localhost:10000/destA", Health = "https://localhost:20000/destA", Metadata = new Dictionary { { "destA-K1", "destA-V1" }, { "destA-K2", "destA-V2" } } } }, { "destinationB", new DestinationConfig { Address = "https://localhost:10000/destB", Health = "https://localhost:20000/destB", Metadata = new Dictionary { { "destB-K1", "destB-V1" }, { "destB-K2", "destB-V2" } } } } }, HealthCheck = new HealthCheckConfig { Passive = new PassiveHealthCheckConfig { Enabled = true, Policy = "FailureRate", ReactivationPeriod = TimeSpan.FromMinutes(5) }, Active = new ActiveHealthCheckConfig { Enabled = true, Interval = TimeSpan.FromSeconds(4), Timeout = TimeSpan.FromSeconds(6), Policy = "Any5xxResponse", Path = "healthCheckPath" } }, LoadBalancingPolicy = LoadBalancingPolicies.Random, SessionAffinity = new SessionAffinityConfig { Enabled = true, FailurePolicy = "Return503Error", Policy = "Cookie", AffinityKeyName = "Key1", Cookie = new SessionAffinityCookieConfig { Domain = "localhost", Expiration = TimeSpan.FromHours(3), HttpOnly = true, IsEssential = true, MaxAge = TimeSpan.FromDays(1), Path = "mypath", SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Strict, SecurePolicy = Microsoft.AspNetCore.Http.CookieSecurePolicy.SameAsRequest } }, HttpClient = new HttpClientConfig { SslProtocols = SslProtocols.Tls11 | SslProtocols.Tls12, MaxConnectionsPerServer = 10, DangerousAcceptAnyServerCertificate = true, RequestHeaderEncoding = Encoding.UTF8.WebName, ResponseHeaderEncoding = Encoding.UTF8.WebName }, HttpRequest = new ForwarderRequestConfig { ActivityTimeout = TimeSpan.FromSeconds(60), Version = Version.Parse("1.0"), VersionPolicy = HttpVersionPolicy.RequestVersionExact, }, Metadata = new Dictionary { { "cluster1-K1", "cluster1-V1" }, { "cluster1-K2", "cluster1-V2" } } }; var config2 = new ClusterConfig { ClusterId = "cluster1", Destinations = new Dictionary(StringComparer.OrdinalIgnoreCase) { { "destinationA", new DestinationConfig { Address = "https://localhost:10000/destA", Health = "https://localhost:20000/destA", Metadata = new Dictionary { { "destA-K1", "destA-V1" }, { "destA-K2", "destA-V2" } } } }, { "destinationB", new DestinationConfig { Address = "https://localhost:10000/destB", Health = "https://localhost:20000/destB", Metadata = new Dictionary { { "destB-K1", "destB-V1" }, { "destB-K2", "destB-V2" } } } } }, HealthCheck = new HealthCheckConfig { Passive = new PassiveHealthCheckConfig { Enabled = true, Policy = "FailureRate", ReactivationPeriod = TimeSpan.FromMinutes(5) }, Active = new ActiveHealthCheckConfig { Enabled = true, Interval = TimeSpan.FromSeconds(4), Timeout = TimeSpan.FromSeconds(6), Policy = "Any5xxResponse", Path = "healthCheckPath" } }, LoadBalancingPolicy = LoadBalancingPolicies.Random, SessionAffinity = new SessionAffinityConfig { Enabled = true, FailurePolicy = "Return503Error", Policy = "Cookie", AffinityKeyName = "Key1", Cookie = new SessionAffinityCookieConfig { Domain = "localhost", Expiration = TimeSpan.FromHours(3), HttpOnly = true, IsEssential = true, MaxAge = TimeSpan.FromDays(1), Path = "mypath", SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Strict, SecurePolicy = Microsoft.AspNetCore.Http.CookieSecurePolicy.SameAsRequest } }, HttpClient = new HttpClientConfig { SslProtocols = SslProtocols.Tls11 | SslProtocols.Tls12, MaxConnectionsPerServer = 10, DangerousAcceptAnyServerCertificate = true, RequestHeaderEncoding = Encoding.UTF8.WebName, ResponseHeaderEncoding = Encoding.UTF8.WebName }, HttpRequest = new ForwarderRequestConfig { ActivityTimeout = TimeSpan.FromSeconds(60), Version = Version.Parse("1.0"), VersionPolicy = HttpVersionPolicy.RequestVersionExact, }, Metadata = new Dictionary { { "cluster1-K1", "cluster1-V1" }, { "cluster1-K2", "cluster1-V2" } } }; var config3 = config1 with { }; // Clone Assert.True(config1.Equals(config2)); Assert.True(config1.Equals(config3)); Assert.Equal(config1.GetHashCode(), config2.GetHashCode()); Assert.Equal(config1.GetHashCode(), config3.GetHashCode()); } [Fact] public void Equals_Different_Value_Returns_False() { var config1 = new ClusterConfig { ClusterId = "cluster1", Destinations = new Dictionary(StringComparer.OrdinalIgnoreCase) { { "destinationA", new DestinationConfig { Address = "https://localhost:10000/destA", Health = "https://localhost:20000/destA", Metadata = new Dictionary { { "destA-K1", "destA-V1" }, { "destA-K2", "destA-V2" } } } }, { "destinationB", new DestinationConfig { Address = "https://localhost:10000/destB", Health = "https://localhost:20000/destB", Metadata = new Dictionary { { "destB-K1", "destB-V1" }, { "destB-K2", "destB-V2" } } } } }, HealthCheck = new HealthCheckConfig { Passive = new PassiveHealthCheckConfig { Enabled = true, Policy = "FailureRate", ReactivationPeriod = TimeSpan.FromMinutes(5) }, Active = new ActiveHealthCheckConfig { Enabled = true, Interval = TimeSpan.FromSeconds(4), Timeout = TimeSpan.FromSeconds(6), Policy = "Any5xxResponse", Path = "healthCheckPath" } }, LoadBalancingPolicy = LoadBalancingPolicies.Random, SessionAffinity = new SessionAffinityConfig { Enabled = true, FailurePolicy = "Return503Error", Policy = "Cookie", AffinityKeyName = "Key1", Cookie = new SessionAffinityCookieConfig { Domain = "localhost", Expiration = TimeSpan.FromHours(3), HttpOnly = true, IsEssential = true, MaxAge = TimeSpan.FromDays(1), Path = "mypath", SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Strict, SecurePolicy = Microsoft.AspNetCore.Http.CookieSecurePolicy.SameAsRequest } }, HttpClient = new HttpClientConfig { SslProtocols = SslProtocols.Tls11 | SslProtocols.Tls12, MaxConnectionsPerServer = 10, DangerousAcceptAnyServerCertificate = true, }, HttpRequest = new ForwarderRequestConfig { ActivityTimeout = TimeSpan.FromSeconds(60), Version = Version.Parse("1.0"), VersionPolicy = HttpVersionPolicy.RequestVersionExact, }, Metadata = new Dictionary { { "cluster1-K1", "cluster1-V1" }, { "cluster1-K2", "cluster1-V2" } } }; Assert.False(config1.Equals(config1 with { ClusterId = "different" })); Assert.False(config1.Equals(config1 with { Destinations = new Dictionary() })); Assert.False(config1.Equals(config1 with { HealthCheck = new HealthCheckConfig() })); Assert.False(config1.Equals(config1 with { LoadBalancingPolicy = "different" })); Assert.False(config1.Equals(config1 with { SessionAffinity = new SessionAffinityConfig { Enabled = true, FailurePolicy = "Return503Error", Policy = "Cookie", AffinityKeyName = "Key1", Cookie = new SessionAffinityCookieConfig { Domain = "localhost", Expiration = TimeSpan.FromHours(3), HttpOnly = true, IsEssential = true, MaxAge = TimeSpan.FromDays(1), Path = "newpath", SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Strict, SecurePolicy = Microsoft.AspNetCore.Http.CookieSecurePolicy.SameAsRequest } } })); Assert.False(config1.Equals(config1 with { HttpClient = new HttpClientConfig { SslProtocols = SslProtocols.Tls12, MaxConnectionsPerServer = 10, DangerousAcceptAnyServerCertificate = true, } })); Assert.False(config1.Equals(config1 with { HttpRequest = new ForwarderRequestConfig() { } })); Assert.False(config1.Equals(config1 with { Metadata = null })); } [Fact] public void Equals_Second_Null_Returns_False() { var config1 = new ClusterConfig(); var equals = config1.Equals(null); Assert.False(equals); } [Fact] public void Cluster_CanBeJsonSerialized() { var cluster1 = new ClusterConfig { ClusterId = "cluster1", LoadBalancingPolicy = LoadBalancingPolicies.Random, SessionAffinity = new SessionAffinityConfig { Enabled = true, FailurePolicy = "Return503Error", Policy = "Cookie", AffinityKeyName = "Key1", Cookie = new SessionAffinityCookieConfig { Domain = "domain", Expiration = TimeSpan.FromDays(1), HttpOnly = true, IsEssential = true, MaxAge = TimeSpan.FromHours(1), Path = "/", SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Unspecified, SecurePolicy = Microsoft.AspNetCore.Http.CookieSecurePolicy.None } }, HealthCheck = new HealthCheckConfig { Passive = new PassiveHealthCheckConfig { Enabled = true, Policy = "FailureRate", ReactivationPeriod = TimeSpan.FromMinutes(5) }, Active = new ActiveHealthCheckConfig { Enabled = true, Interval = TimeSpan.FromSeconds(4), Timeout = TimeSpan.FromSeconds(6), Policy = "Any5xxResponse", Path = "healthCheckPath" } }, HttpClient = new HttpClientConfig { EnableMultipleHttp2Connections = true, SslProtocols = SslProtocols.Tls11 | SslProtocols.Tls12, MaxConnectionsPerServer = 10, DangerousAcceptAnyServerCertificate = true, WebProxy = new WebProxyConfig { Address = new Uri("http://proxy"), BypassOnLocal = false, UseDefaultCredentials = false, } }, HttpRequest = new ForwarderRequestConfig { ActivityTimeout = TimeSpan.FromSeconds(60), Version = Version.Parse("1.0"), VersionPolicy = HttpVersionPolicy.RequestVersionExact, }, Destinations = new Dictionary(StringComparer.OrdinalIgnoreCase) { { "destinationA", new DestinationConfig { Address = "https://localhost:10000/destA", Health = "https://localhost:20000/destA", Metadata = new Dictionary { { "destA-K1", "destA-V1" }, { "destA-K2", "destA-V2" } } } }, { "destinationB", new DestinationConfig { Address = "https://localhost:10000/destB", Health = "https://localhost:20000/destB", Metadata = new Dictionary { { "destB-K1", "destB-V1" }, { "destB-K2", "destB-V2" } } } } }, Metadata = new Dictionary { { "cluster1-K1", "cluster1-V1" }, { "cluster1-K2", "cluster1-V2" } } }; var json = JsonSerializer.Serialize(cluster1); var cluster2 = JsonSerializer.Deserialize(json); Assert.Equal(cluster1.Destinations, cluster2.Destinations); Assert.Equal(cluster1.HealthCheck.Active, cluster2.HealthCheck.Active); Assert.Equal(cluster1.HealthCheck.Passive, cluster2.HealthCheck.Passive); Assert.Equal(cluster1.HealthCheck, cluster2.HealthCheck); Assert.Equal(cluster1.HttpClient, cluster2.HttpClient); Assert.Equal(cluster1.HttpRequest, cluster2.HttpRequest); Assert.Equal(cluster1.Metadata, cluster2.Metadata); Assert.Equal(cluster1.SessionAffinity, cluster2.SessionAffinity); Assert.Equal(cluster1, cluster2); } public class TimeSpanConverter : JsonConverter { public override TimeSpan Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return TimeSpan.Parse(reader.GetString(), CultureInfo.InvariantCulture); } public override void Write(Utf8JsonWriter writer, TimeSpan value, JsonSerializerOptions options) { writer.WriteStringValue(value.ToString(format: null, CultureInfo.InvariantCulture)); } } public class VersionConverter : JsonConverter { public override Version Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { var versionString = reader.GetString(); return Version.Parse(versionString); } public override void Write(Utf8JsonWriter writer, Version value, JsonSerializerOptions options) { writer.WriteStringValue(value.ToString()); } } } ================================================ FILE: test/ReverseProxy.Tests/Configuration/ConfigProvider/ConfigurationConfigProviderTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Net.Http; using System.Reflection; using System.Security.Authentication; using System.Text; using System.Text.Json; using System.Text.Json.Nodes; using System.Threading.Tasks; using Json.Schema; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Moq; using Xunit; using Yarp.ReverseProxy.Forwarder; using Yarp.ReverseProxy.LoadBalancing; namespace Yarp.ReverseProxy.Configuration.ConfigProvider.Tests; public class ConfigurationConfigProviderTests { private static readonly JsonSchema s_yarpSchema = JsonSchema.FromText(File.ReadAllText("ConfigurationSchema.json")); private static readonly EvaluationOptions s_schemaOptions = new() { OutputFormat = OutputFormat.List, RequireFormatValidation = true }; #region JSON test configuration private readonly ConfigurationSnapshot _validConfigurationData = new ConfigurationSnapshot() { Clusters = { { new ClusterConfig { ClusterId = "cluster1", Destinations = new Dictionary(StringComparer.OrdinalIgnoreCase) { { "destinationA", new DestinationConfig { Address = "https://localhost:10000/destA", Health = "https://localhost:20000/destA", Metadata = new Dictionary { { "destA-K1", "destA-V1" }, { "destA-K2", "destA-V2" } }, Host = "localhost" } }, { "destinationB", new DestinationConfig { Address = "https://localhost:10000/destB", Health = "https://localhost:20000/destB", Metadata = new Dictionary { { "destB-K1", "destB-V1" }, { "destB-K2", "destB-V2" }, { "foo", "42" }, { "bar", "True" } }, Host = "localhost" } } }, HealthCheck = new HealthCheckConfig { Passive = new PassiveHealthCheckConfig { Enabled = true, Policy = "FailureRate", ReactivationPeriod = TimeSpan.FromMinutes(5) }, Active = new ActiveHealthCheckConfig { Enabled = true, Interval = TimeSpan.FromSeconds(4), Timeout = TimeSpan.FromSeconds(6), Policy = "Any5xxResponse", Path = "healthCheckPath", Query = "?key=value" }, AvailableDestinationsPolicy = "HealthyOrPanic" }, LoadBalancingPolicy = LoadBalancingPolicies.Random, SessionAffinity = new SessionAffinityConfig { Enabled = true, FailurePolicy = "Return503Error", Policy = "Cookie", AffinityKeyName = "Key1", Cookie = new SessionAffinityCookieConfig { Domain = "localhost", Expiration = TimeSpan.FromHours(3), HttpOnly = true, IsEssential = true, MaxAge = TimeSpan.FromDays(1), Path = "mypath", SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Strict, SecurePolicy = Microsoft.AspNetCore.Http.CookieSecurePolicy.None } }, HttpClient = new HttpClientConfig { SslProtocols = SslProtocols.Tls11 | SslProtocols.Tls12, MaxConnectionsPerServer = 10, DangerousAcceptAnyServerCertificate = true, EnableMultipleHttp2Connections = true, }, HttpRequest = new ForwarderRequestConfig() { ActivityTimeout = TimeSpan.FromSeconds(60), Version = Version.Parse("1.0"), VersionPolicy = HttpVersionPolicy.RequestVersionExact, AllowResponseBuffering = true }, Metadata = new Dictionary { { "cluster1-K1", "cluster1-V1" }, { "cluster1-K2", "cluster1-V2" } } } }, { new ClusterConfig { ClusterId = "cluster2", Destinations = new Dictionary(StringComparer.OrdinalIgnoreCase) { { "destinationC", new DestinationConfig { Address = "https://localhost:10001/destC", Host = "localhost" } }, { "destinationD", new DestinationConfig { Address = "https://localhost:10000/destB", Host = "remotehost" } } }, LoadBalancingPolicy = LoadBalancingPolicies.RoundRobin } } }, Routes = { new RouteConfig { RouteId = "routeA", ClusterId = "cluster1", AuthorizationPolicy = "Default", RateLimiterPolicy = "Default", TimeoutPolicy = "Default", Timeout = TimeSpan.Zero, CorsPolicy = "Default", Order = -1, MaxRequestBodySize = -1, Match = new RouteMatch { Hosts = new List { "host-A" }, Methods = new List { "GET", "POST", "DELETE" }, Path = "/apis/entities", Headers = new[] { new RouteHeader { Name = "header1", Values = new[] { "value1" }, IsCaseSensitive = true, Mode = HeaderMatchMode.HeaderPrefix } }, QueryParameters = new[] { new RouteQueryParameter { Name = "queryparam1", Values = new[] { "value1" }, IsCaseSensitive = true, Mode = QueryParameterMatchMode.Contains } } }, Transforms = new[] { new Dictionary { { "RequestHeadersCopy", "true" } }, new Dictionary { { "PathRemovePrefix", "/apis" }, }, new Dictionary { { "PathPrefix", "/apis" } }, new Dictionary { { "RequestHeader", "header1" }, { "Append", "foo" } } }, Metadata = new Dictionary { { "routeA-K1", "routeA-V1" }, { "routeA-K2", "routeA-V2" } } }, new RouteConfig { RouteId = "routeB", ClusterId = "cluster2", Order = 2, MaxRequestBodySize = 1, Match = new RouteMatch { Hosts = new List { "host-B" }, Methods = new List { "GET" }, Path = "/apis/users", Headers = new[] { new RouteHeader { Name = "header2", Values = new[] { "value2" }, IsCaseSensitive = false, Mode = HeaderMatchMode.ExactHeader } }, QueryParameters = new[] { new RouteQueryParameter { Name = "queryparam2", Values = new[] { "value2" }, IsCaseSensitive = true, Mode = QueryParameterMatchMode.Contains } } } } } }; private const string ValidJsonConfig = """ { "Clusters": { "cluster1": { "LoadBalancingPolicy": "Random", "SessionAffinity": { "Enabled": true, "Policy": "Cookie", "FailurePolicy": "Return503Error", "AffinityKeyName": "Key1", "Cookie": { "Domain": "localhost", "Expiration": "03:0:00", "HttpOnly": true, "IsEssential": "True", "MaxAge": "1.00:00:0", "Path": "mypath", "SameSite": "Strict", "SecurePolicy": "None" } }, "HealthCheck": { "Passive": { "Enabled": true, "Policy": "FailureRate", "ReactivationPeriod": "0:05:00" }, "Active": { "Enabled": true, "Interval": "00:00:04", "Timeout": "00:00:06", "Policy": "Any5xxResponse", "Path": "healthCheckPath", "Query": "?key=value" }, "AvailableDestinationsPolicy": "HealthyOrPanic" }, "HttpClient": { "SslProtocols": [ "Tls11", "Tls12" ], "DangerousAcceptAnyServerCertificate": true, "MaxConnectionsPerServer": 10, "EnableMultipleHttp2Connections": true, "RequestHeaderEncoding": "utf-8", "ResponseHeaderEncoding": "utf-8", "WebProxy": { "Address": "http://localhost:8080", "BypassOnLocal": true, "UseDefaultCredentials": true } }, "HttpRequest": { "ActivityTimeout": "00:01:00", "Version": "1", "VersionPolicy": "RequestVersionExact", "AllowResponseBuffering": true }, "Destinations": { "destinationA": { "Address": "https://localhost:10000/destA", "Health": "https://localhost:20000/destA", "Host": "localhost", "Metadata": { "destA-K1": "destA-V1", "destA-K2": "destA-V2" } }, "destinationB": { "Address": "https://localhost:10000/destB", "Health": "https://localhost:20000/destB", "Host": "localhost", "Metadata": { "destB-K1": "destB-V1", "destB-K2": "destB-V2", "foo": 42, "bar": true } } }, "Metadata": { "cluster1-K1": "cluster1-V1", "cluster1-K2": "cluster1-V2" } }, "cluster2": { "LoadBalancingPolicy": "RoundRobin", "SessionAffinity": null, "HealthCheck": null, "HttpClient": null, "Destinations": { "destinationC": { "Address": "https://localhost:10001/destC", "Host": "localhost", "Metadata": null }, "destinationD": { "Address": "https://localhost:10000/destB", "Host": "remotehost", "Metadata": null } }, "Metadata": null } }, "Routes": { "routeA" : { "Match": { "Methods": [ "GET", "POST", "DELETE" ], "Hosts": [ "host-A" ], "Path": "/apis/entities", "Headers": [ { "Name": "header1", "Values": [ "value1" ], "IsCaseSensitive": true, "Mode": "HeaderPrefix" } ], "QueryParameters": [ { "Name": "queryparam1", "Values": [ "value1" ], "IsCaseSensitive": "true", "Mode": "Contains" } ] }, "Order": -1, "MaxRequestBodySize": -1, "ClusterId": "cluster1", "AuthorizationPolicy": "Default", "RateLimiterPolicy": "Default", "OutputCachePolicy": "Default", "CorsPolicy": "Default", "TimeoutPolicy": "Default", "Timeout": "00:00:01", "Metadata": { "routeA-K1": "routeA-V1", "routeA-K2": "routeA-V2" }, "Transforms": [ { "RequestHeadersCopy": true }, { "PathRemovePrefix": "/apis" }, { "PathPrefix": "/apis" }, { "RequestHeader": "header1", "Append": "foo" } ] }, "routeB" : { "Match": { "Methods": [ "GET" ], "Hosts": [ "host-B" ], "Path": "/apis/users", "Headers": [ { "Name": "header2", "Values": [ "value2" ] } ], "QueryParameters": [ { "Name": "queryparam2", "Values": [ "value2" ], "IsCaseSensitive": true, "Mode": "Contains" } ] }, "Order": 2, "MaxRequestBodySize": 1, "ClusterId": "cluster2", "AuthorizationPolicy": null, "RateLimiterPolicy": null, "OutputCachePolicy": null, "CorsPolicy": null, "Metadata": null, "Transforms": null } } } """; #endregion private static ConfigurationConfigProvider CreateProvider(string json) { var builder = new ConfigurationBuilder(); var stream = new MemoryStream(Encoding.UTF8.GetBytes(json)); var configuration = builder.AddJsonStream(stream).Build(); var logger = new Mock>(); return new ConfigurationConfigProvider(logger.Object, configuration); } [Fact] public void GetConfig_ValidSerializedConfiguration_ConvertToAbstractionsSuccessfully() { var provider = CreateProvider(ValidJsonConfig); var abstractConfig = provider.GetConfig(); VerifyValidAbstractConfig(_validConfigurationData, abstractConfig); } [Fact] public void GetConfig_ValidConfiguration_AllAbstractionsPropertiesAreSet() { var provider = CreateProvider(ValidJsonConfig); var abstractConfig = (ConfigurationSnapshot)provider.GetConfig(); var abstractionsNamespace = typeof(ClusterConfig).Namespace; // Removed incompletely filled out instances. abstractConfig.Clusters = abstractConfig.Clusters.Where(c => c.ClusterId == "cluster1").ToList(); abstractConfig.Routes = abstractConfig.Routes.Where(r => r.RouteId == "routeA").ToList(); VerifyAllPropertiesAreSet(abstractConfig); void VerifyFullyInitialized(object obj, string name) { switch (obj) { case null: Assert.Fail($"Property {name} is not initialized."); break; case Enum m: Assert.NotEqual(0, (int)(object)m); break; case string str: Assert.NotEmpty(str); break; case ValueType v: var equals = Equals(Activator.CreateInstance(v.GetType()), v); Assert.False(equals, $"Property {name} is not initialized."); if (v.GetType().Namespace == abstractionsNamespace) { VerifyAllPropertiesAreSet(v); } break; case IDictionary d: Assert.NotEmpty(d); foreach (var value in d.Values) { VerifyFullyInitialized(value, name); } break; case IEnumerable e: Assert.NotEmpty(e); foreach (var item in e) { VerifyFullyInitialized(item, name); } var type = e.GetType(); if (!type.IsArray && type.Namespace == abstractionsNamespace) { VerifyAllPropertiesAreSet(e); } break; case object o: if (o.GetType().Namespace == abstractionsNamespace) { VerifyAllPropertiesAreSet(o); } break; } } void VerifyAllPropertiesAreSet(object obj) { var properties = obj.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public).Cast(); foreach (var property in properties) { VerifyFullyInitialized(property.GetValue(obj), $"{property.DeclaringType.Name}.{property.Name}"); } } } private void VerifyValidAbstractConfig(IProxyConfig validConfig, IProxyConfig abstractConfig) { Assert.NotNull(abstractConfig); Assert.Equal(2, abstractConfig.Clusters.Count); var cluster1 = validConfig.Clusters.First(c => c.ClusterId == "cluster1"); Assert.Single(abstractConfig.Clusters, c => c.ClusterId == "cluster1"); var abstractCluster1 = abstractConfig.Clusters.Single(c => c.ClusterId == "cluster1"); Assert.Equal(cluster1.Destinations["destinationA"].Address, abstractCluster1.Destinations["destinationA"].Address); Assert.Equal(cluster1.Destinations["destinationA"].Health, abstractCluster1.Destinations["destinationA"].Health); Assert.Equal(cluster1.Destinations["destinationA"].Metadata, abstractCluster1.Destinations["destinationA"].Metadata); Assert.Equal(cluster1.Destinations["destinationA"].Host, abstractCluster1.Destinations["destinationA"].Host); Assert.Equal(cluster1.Destinations["destinationB"].Address, abstractCluster1.Destinations["destinationB"].Address); Assert.Equal(cluster1.Destinations["destinationB"].Health, abstractCluster1.Destinations["destinationB"].Health); Assert.Equal(cluster1.Destinations["destinationB"].Metadata, abstractCluster1.Destinations["destinationB"].Metadata); Assert.Equal(cluster1.Destinations["destinationB"].Host, abstractCluster1.Destinations["destinationB"].Host); Assert.Equal(cluster1.HealthCheck.AvailableDestinationsPolicy, abstractCluster1.HealthCheck.AvailableDestinationsPolicy); Assert.Equal(cluster1.HealthCheck.Passive.Enabled, abstractCluster1.HealthCheck.Passive.Enabled); Assert.Equal(cluster1.HealthCheck.Passive.Policy, abstractCluster1.HealthCheck.Passive.Policy); Assert.Equal(cluster1.HealthCheck.Passive.ReactivationPeriod, abstractCluster1.HealthCheck.Passive.ReactivationPeriod); Assert.Equal(cluster1.HealthCheck.Active.Enabled, abstractCluster1.HealthCheck.Active.Enabled); Assert.Equal(cluster1.HealthCheck.Active.Interval, abstractCluster1.HealthCheck.Active.Interval); Assert.Equal(cluster1.HealthCheck.Active.Timeout, abstractCluster1.HealthCheck.Active.Timeout); Assert.Equal(cluster1.HealthCheck.Active.Policy, abstractCluster1.HealthCheck.Active.Policy); Assert.Equal(cluster1.HealthCheck.Active.Path, abstractCluster1.HealthCheck.Active.Path); Assert.Equal(cluster1.HealthCheck.Active.Query, abstractCluster1.HealthCheck.Active.Query); Assert.Equal(LoadBalancingPolicies.Random, abstractCluster1.LoadBalancingPolicy); Assert.Equal(cluster1.SessionAffinity.Enabled, abstractCluster1.SessionAffinity.Enabled); Assert.Equal(cluster1.SessionAffinity.FailurePolicy, abstractCluster1.SessionAffinity.FailurePolicy); Assert.Equal(cluster1.SessionAffinity.Policy, abstractCluster1.SessionAffinity.Policy); Assert.Equal(cluster1.SessionAffinity.AffinityKeyName, abstractCluster1.SessionAffinity.AffinityKeyName); Assert.Equal(cluster1.SessionAffinity.Cookie.Domain, abstractCluster1.SessionAffinity.Cookie.Domain); Assert.Equal(cluster1.SessionAffinity.Cookie.Expiration, abstractCluster1.SessionAffinity.Cookie.Expiration); Assert.Equal(cluster1.SessionAffinity.Cookie.HttpOnly, abstractCluster1.SessionAffinity.Cookie.HttpOnly); Assert.Equal(cluster1.SessionAffinity.Cookie.IsEssential, abstractCluster1.SessionAffinity.Cookie.IsEssential); Assert.Equal(cluster1.SessionAffinity.Cookie.MaxAge, abstractCluster1.SessionAffinity.Cookie.MaxAge); Assert.Equal(cluster1.SessionAffinity.Cookie.Path, abstractCluster1.SessionAffinity.Cookie.Path); Assert.Equal(cluster1.SessionAffinity.Cookie.SameSite, abstractCluster1.SessionAffinity.Cookie.SameSite); Assert.Equal(cluster1.SessionAffinity.Cookie.SecurePolicy, abstractCluster1.SessionAffinity.Cookie.SecurePolicy); Assert.Equal(cluster1.HttpClient.MaxConnectionsPerServer, abstractCluster1.HttpClient.MaxConnectionsPerServer); Assert.Equal(cluster1.HttpClient.EnableMultipleHttp2Connections, abstractCluster1.HttpClient.EnableMultipleHttp2Connections); Assert.Equal(Encoding.UTF8.WebName, abstractCluster1.HttpClient.RequestHeaderEncoding); Assert.Equal(Encoding.UTF8.WebName, abstractCluster1.HttpClient.ResponseHeaderEncoding); Assert.Equal(SslProtocols.Tls11 | SslProtocols.Tls12, abstractCluster1.HttpClient.SslProtocols); Assert.Equal(cluster1.HttpRequest.ActivityTimeout, abstractCluster1.HttpRequest.ActivityTimeout); Assert.Equal(HttpVersion.Version10, abstractCluster1.HttpRequest.Version); Assert.Equal(cluster1.HttpRequest.VersionPolicy, abstractCluster1.HttpRequest.VersionPolicy); Assert.Equal(cluster1.HttpRequest.AllowResponseBuffering, abstractCluster1.HttpRequest.AllowResponseBuffering); Assert.Equal(cluster1.HttpClient.DangerousAcceptAnyServerCertificate, abstractCluster1.HttpClient.DangerousAcceptAnyServerCertificate); Assert.Equal(cluster1.Metadata, abstractCluster1.Metadata); var cluster2 = validConfig.Clusters.First(c => c.ClusterId == "cluster2"); Assert.Single(abstractConfig.Clusters, c => c.ClusterId == "cluster2"); var abstractCluster2 = abstractConfig.Clusters.Single(c => c.ClusterId == "cluster2"); Assert.Equal(cluster2.Destinations["destinationC"].Address, abstractCluster2.Destinations["destinationC"].Address); Assert.Equal(cluster2.Destinations["destinationC"].Metadata, abstractCluster2.Destinations["destinationC"].Metadata); Assert.Equal(cluster2.Destinations["destinationC"].Host, abstractCluster2.Destinations["destinationC"].Host); Assert.Equal(cluster2.Destinations["destinationD"].Address, abstractCluster2.Destinations["destinationD"].Address); Assert.Equal(cluster2.Destinations["destinationD"].Metadata, abstractCluster2.Destinations["destinationD"].Metadata); Assert.Equal(cluster2.Destinations["destinationD"].Host, abstractCluster2.Destinations["destinationD"].Host); Assert.Equal(LoadBalancingPolicies.RoundRobin, abstractCluster2.LoadBalancingPolicy); Assert.Equal(2, abstractConfig.Routes.Count); VerifyRoute(validConfig, abstractConfig, "routeA"); VerifyRoute(validConfig, abstractConfig, "routeB"); } private void VerifyRoute(IProxyConfig validConfig, IProxyConfig abstractConfig, string routeId) { var route = validConfig.Routes.Single(c => c.RouteId == routeId); Assert.Single(abstractConfig.Routes, r => r.RouteId == routeId); var abstractRoute = abstractConfig.Routes.Single(c => c.RouteId == routeId); Assert.Equal(route.ClusterId, abstractRoute.ClusterId); Assert.Equal(route.Order, abstractRoute.Order); Assert.Equal(route.MaxRequestBodySize, abstractRoute.MaxRequestBodySize); Assert.Equal(route.Match.Hosts, abstractRoute.Match.Hosts); Assert.Equal(route.Match.Methods, abstractRoute.Match.Methods); Assert.Equal(route.Match.Path, abstractRoute.Match.Path); var header = route.Match.Headers.Single(); var expectedHeader = abstractRoute.Match.Headers.Single(); Assert.Equal(header.Name, expectedHeader.Name); Assert.Equal(header.Mode, expectedHeader.Mode); Assert.Equal(header.IsCaseSensitive, expectedHeader.IsCaseSensitive); var queryparam = route.Match.QueryParameters.Single(); var expectedQueryParam = abstractRoute.Match.QueryParameters.Single(); Assert.Equal(queryparam.Name, expectedQueryParam.Name); Assert.Equal(queryparam.Mode, expectedQueryParam.Mode); Assert.Equal(queryparam.IsCaseSensitive, expectedQueryParam.IsCaseSensitive); if (route.Transforms is null) { Assert.Null(abstractRoute.Transforms); } else { Assert.NotNull(abstractRoute.Transforms); Assert.Equal(route.Transforms.Count, abstractRoute.Transforms.Count); for (var i = 0; i < route.Transforms.Count; i++) { var transform = route.Transforms[i]; var expectedTransform = abstractRoute.Transforms[i]; Assert.Equal(transform.Count, expectedTransform.Count); foreach (var kv in transform) { Assert.True(expectedTransform.TryGetValue(kv.Key, out var value)); if (value == "True") { value = "true"; } Assert.Equal(kv.Value, value); } } } } [Fact] public void ValidateSchema_ValidInput() { var results = s_yarpSchema.Evaluate(JsonNode.Parse( $$""" { "ReverseProxy": {{ValidJsonConfig}} } """), s_schemaOptions); var errors = results.Details .Where(d => d.HasErrors) .SelectMany(d => d.Errors!.Select(error => $"Path:${d.InstanceLocation} {error.Key}:{error.Value}")) .ToArray(); Assert.True(results.IsValid); Assert.False(results.HasErrors); Assert.True(results.Details.Count > 300); } [Fact] public async Task ValidateSchema_Samples() { string repoRoot = Path.Combine(Environment.CurrentDirectory, "../../../../../"); repoRoot = Path.GetFullPath(repoRoot); foreach (string file in Directory.EnumerateFiles(repoRoot, "*.json", SearchOption.AllDirectories)) { if (file.Contains("\\obj\\", StringComparison.Ordinal) || file.Contains("/obj/", StringComparison.Ordinal)) { continue; } if (file.Contains("appsettings", StringComparison.OrdinalIgnoreCase)) { var contents = await File.ReadAllTextAsync(file); var document = JsonDocument.Parse(contents, new JsonDocumentOptions { CommentHandling = JsonCommentHandling.Skip }); var results = s_yarpSchema.Evaluate(document.RootElement, s_schemaOptions); if (results.IsValid) { Assert.False(results.HasErrors); if (contents.Contains("\"ReverseProxy\"", StringComparison.OrdinalIgnoreCase)) { Assert.True(results.Details.Count > 5, $"No details for '{file}'"); } } else { Assert.Fail($"Json errors reported for '{file}'"); } } } } } ================================================ FILE: test/ReverseProxy.Tests/Configuration/ConfigProvider/ConfigurationReadingExtensionsTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; using Xunit; namespace Microsoft.Extensions.Configuration.Tests; public class ConfigurationReadingExtensionsTests { [Fact] public void ReadInt32_NegativeNumber() { var configuration = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { ["Key"] = "-1" }) .Build(); var number = configuration.ReadInt32("Key"); Assert.Equal(-1, number); } } ================================================ FILE: test/ReverseProxy.Tests/Configuration/ConfigValidatorTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Cors.Infrastructure; using Microsoft.AspNetCore.RateLimiting; using Microsoft.Extensions.DependencyInjection; using Moq; using Xunit; using Yarp.ReverseProxy.Health; using Yarp.ReverseProxy.LoadBalancing; using Yarp.ReverseProxy.Forwarder; namespace Yarp.ReverseProxy.Configuration.Tests; public class ConfigValidatorTests { private IServiceProvider CreateServices(Action configure = null) { var services = new ServiceCollection(); services.AddReverseProxy(); var passivePolicy = new Mock(); passivePolicy.SetupGet(p => p.Name).Returns("passive0"); services.AddSingleton(passivePolicy.Object); var availableDestinationsPolicy = new Mock(); availableDestinationsPolicy.SetupGet(p => p.Name).Returns("availableDestinations0"); services.AddSingleton(availableDestinationsPolicy.Object); services.AddOptions(); services.AddLogging(); services.AddRouting(); configure?.Invoke(services); return services.BuildServiceProvider(); } [Fact] public void Constructor_Works() { var services = CreateServices(); services.GetRequiredService(); } [Theory] [InlineData("example.com", "/a/", null)] [InlineData("example.com:80", "/a/", null)] [InlineData("\u00FCnicode", "/a/", null)] [InlineData("\u00FCnicode:443", "/a/", null)] [InlineData("example.com", "/a/**", null)] [InlineData("example.com", "/a/**", "GET")] [InlineData(null, "/a/", null)] [InlineData(null, "/a/**", "GET")] [InlineData("example.com", null, "get")] [InlineData("example.com", null, "gEt,put")] [InlineData("example.com", null, "gEt,put,POST,traCE,PATCH,DELETE,Head")] [InlineData("example.com,example2.com", null, "get")] [InlineData("*.example.com", null, null)] [InlineData("a-b.example.com", null, null)] [InlineData("a-b.b-c.example.com", null, null)] public async Task Accepts_ValidRules(string host, string path, string methods) { var route = new RouteConfig { RouteId = "route1", Match = new RouteMatch { Hosts = host?.Split(",") ?? Array.Empty(), Path = path, Methods = methods?.Split(","), }, ClusterId = "cluster1", }; var services = CreateServices(); var validator = services.GetRequiredService(); var result = await validator.ValidateRouteAsync(route); Assert.Empty(result); } [Theory] [InlineData("")] [InlineData(null)] public async Task Rejects_MissingRouteId(string routeId) { var route = new RouteConfig { RouteId = routeId }; var services = CreateServices(); var validator = services.GetRequiredService(); var result = await validator.ValidateRouteAsync(route); Assert.NotEmpty(result); Assert.Contains(result, err => err.Message.Equals("Missing Route Id.")); } [Fact] public async Task Rejects_MissingMatch() { var route = new RouteConfig { RouteId = "route1", ClusterId = "cluster1", }; var services = CreateServices(); var validator = services.GetRequiredService(); var result = await validator.ValidateRouteAsync(route); Assert.NotEmpty(result); Assert.Contains(result, err => err.Message.Equals("Route 'route1' did not set any match criteria, it requires Hosts or Path specified. Set the Path to '/{**catchall}' to match all requests.")); } [Theory] [InlineData(null)] [InlineData("")] [InlineData("xn--nicode-2ya")] [InlineData("Xn--nicode-2ya")] public async Task Rejects_InvalidHost(string host) { var route = new RouteConfig { RouteId = "route1", Match = new RouteMatch { Hosts = new[] { host } }, ClusterId = "cluster1", }; var services = CreateServices(); var validator = services.GetRequiredService(); var result = await validator.ValidateRouteAsync(route); Assert.NotEmpty(result); Assert.Contains(result, err => err.Message.Contains("host name")); } [Theory] [InlineData(null, null)] [InlineData(null, "")] [InlineData("", null)] [InlineData(",", null)] [InlineData("", "")] public async Task Rejects_MissingHostAndPath(string host, string path) { var route = new RouteConfig { RouteId = "route1", ClusterId = "cluster1", Match = new RouteMatch { Hosts = host?.Split(","), Path = path }, }; var services = CreateServices(); var validator = services.GetRequiredService(); var result = await validator.ValidateRouteAsync(route); Assert.NotEmpty(result); Assert.Contains(result, err => err.Message.Equals("Route 'route1' requires Hosts or Path specified. Set the Path to '/{**catchall}' to match all requests.")); } [Theory] [InlineData("/{***a}")] [InlineData("/{")] [InlineData("/}")] [InlineData("/{ab/c}")] public async Task Rejects_InvalidPath(string path) { var route = new RouteConfig { RouteId = "route1", Match = new RouteMatch() { Path = path, }, ClusterId = "cluster1", }; var services = CreateServices(); var validator = services.GetRequiredService(); var result = await validator.ValidateRouteAsync(route); Assert.NotEmpty(result); Assert.Contains(result, err => err.Message.Equals($"Invalid path '{path}' for route 'route1'.")); } [Theory] [InlineData("")] [InlineData("gett")] public async Task Rejects_InvalidMethod(string methods) { var route = new RouteConfig { RouteId = "route1", Match = new RouteMatch { Methods = methods.Split(","), }, ClusterId = "cluster1", }; var services = CreateServices(); var validator = services.GetRequiredService(); var result = await validator.ValidateRouteAsync(route); Assert.NotEmpty(result); Assert.Contains(result, err => err.Message.Equals($"Unsupported HTTP method '{methods}' has been set for route 'route1'.")); } [Theory] [InlineData("get,GET")] [InlineData("get,post,get")] public async Task Rejects_DuplicateMethod(string methods) { var route = new RouteConfig { RouteId = "route1", Match = new RouteMatch { Methods = methods.Split(","), }, ClusterId = "cluster1", }; var services = CreateServices(); var validator = services.GetRequiredService(); var result = await validator.ValidateRouteAsync(route); Assert.NotEmpty(result); Assert.Contains(result, err => err.Message.StartsWith("Duplicate HTTP method")); } [Fact] public async Task Accepts_RouteHeader() { var route = new RouteConfig { RouteId = "route1", Match = new RouteMatch { Path = "/", Headers = new[] { new RouteHeader() { Name = "header1", Values = new[] { "value1" }, } }, }, ClusterId = "cluster1", }; var services = CreateServices(); var validator = services.GetRequiredService(); var result = await validator.ValidateRouteAsync(route); Assert.Empty(result); } [Fact] public async Task Accepts_RouteQueryParameter() { var route = new RouteConfig { RouteId = "route1", Match = new RouteMatch { Path = "/", QueryParameters = new[] { new RouteQueryParameter() { Name = "queryparam1", Values = new[] { "value1" }, } }, }, ClusterId = "cluster1", }; var services = CreateServices(); var validator = services.GetRequiredService(); var result = await validator.ValidateRouteAsync(route); Assert.Empty(result); } [Fact] public async Task Accepts_RouteHeader_ExistsWithNoValue() { var route = new RouteConfig { RouteId = "route1", Match = new RouteMatch { Path = "/", Headers = new[] { new RouteHeader() { Name = "header1", Mode = HeaderMatchMode.Exists } }, }, ClusterId = "cluster1", }; var services = CreateServices(); var validator = services.GetRequiredService(); var result = await validator.ValidateRouteAsync(route); Assert.Empty(result); } [Fact] public async Task Accepts_RouteHeader_NotExistsWithNoValue() { var route = new RouteConfig { RouteId = "route1", Match = new RouteMatch { Path = "/", Headers = new[] { new RouteHeader() { Name = "header1", Mode = HeaderMatchMode.NotExists } }, }, ClusterId = "cluster1", }; var services = CreateServices(); var validator = services.GetRequiredService(); var result = await validator.ValidateRouteAsync(route); Assert.Empty(result); } [Fact] public async Task Accepts_RouteQueryParameter_ExistsWithNoValue() { var route = new RouteConfig { RouteId = "route1", Match = new RouteMatch { Path = "/", QueryParameters = new[] { new RouteQueryParameter() { Name = "queryparam1", Mode = QueryParameterMatchMode.Exists } }, }, ClusterId = "cluster1", }; var services = CreateServices(); var validator = services.GetRequiredService(); var result = await validator.ValidateRouteAsync(route); Assert.Empty(result); } [Fact] public async Task Rejects_NullRouteHeader() { var route = new RouteConfig { RouteId = "route1", Match = new RouteMatch { Path = "/", Headers = new RouteHeader[] { null }, }, ClusterId = "cluster1", }; var services = CreateServices(); var validator = services.GetRequiredService(); var result = await validator.ValidateRouteAsync(route); var ex = Assert.Single(result); Assert.Contains("A null route header has been set for route", ex.Message); } [Fact] public async Task Rejects_NullRouteQueryParameter() { var route = new RouteConfig { RouteId = "route1", Match = new RouteMatch { Path = "/", QueryParameters = new RouteQueryParameter[] { null }, }, ClusterId = "cluster1", }; var services = CreateServices(); var validator = services.GetRequiredService(); var result = await validator.ValidateRouteAsync(route); var ex = Assert.Single(result); Assert.Contains("A null route query parameter has been set for route", ex.Message); } [Theory] [InlineData("", "v1", HeaderMatchMode.ExactHeader, "A null or empty route header name has been set for route")] [InlineData("h1", null, HeaderMatchMode.ExactHeader, "No header values were set on route header")] [InlineData("h1", "v1", HeaderMatchMode.Exists, "Header values were set when using mode 'Exists'")] [InlineData("h1", "v1", HeaderMatchMode.NotExists, "Header values were set when using mode 'NotExists'")] public async Task Rejects_InvalidRouteHeader(string name, string value, HeaderMatchMode mode, string error) { var routeHeader = new RouteHeader() { Name = name, Mode = mode, Values = value is null ? null : new[] { value }, }; var route = new RouteConfig { RouteId = "route1", Match = new RouteMatch { Path = "/", Headers = new[] { routeHeader }, }, ClusterId = "cluster1", }; var services = CreateServices(); var validator = services.GetRequiredService(); var result = await validator.ValidateRouteAsync(route); var ex = Assert.Single(result); Assert.Contains(error, ex.Message); } [Theory] [InlineData("", "v1", QueryParameterMatchMode.Exact, "A null or empty route query parameter name has been set for route")] [InlineData("h1", null, QueryParameterMatchMode.Exact, "No query parameter values were set on route query parameter")] [InlineData("h1", "v1", QueryParameterMatchMode.Exists, "Query parameter values where set when using mode 'Exists'")] public async Task Rejects_InvalidRouteQueryParameter(string name, string value, QueryParameterMatchMode mode, string error) { var routeQueryParameter = new RouteQueryParameter() { Name = name, Mode = mode, Values = value is null ? null : new[] { value }, }; var route = new RouteConfig { RouteId = "route1", Match = new RouteMatch { Path = "/", QueryParameters = new[] { routeQueryParameter }, }, ClusterId = "cluster1", }; var services = CreateServices(); var validator = services.GetRequiredService(); var result = await validator.ValidateRouteAsync(route); var ex = Assert.Single(result); Assert.Contains(error, ex.Message); } [Theory] [InlineData(null)] [InlineData("")] [InlineData("defaulT")] public async Task Accepts_ReservedAuthorizationPolicy(string policy) { var route = new RouteConfig { RouteId = "route1", AuthorizationPolicy = policy, Match = new RouteMatch { Hosts = new[] { "localhost" }, }, ClusterId = "cluster1", }; var services = CreateServices(); var validator = services.GetRequiredService(); var result = await validator.ValidateRouteAsync(route); Assert.Empty(result); } [Fact] public async Task Accepts_CustomAuthorizationPolicy() { var route = new RouteConfig { RouteId = "route1", AuthorizationPolicy = "custom", Match = new RouteMatch { Hosts = new[] { "localhost" }, }, ClusterId = "cluster1", }; var services = CreateServices(services => { services.AddAuthorization(options => { options.AddPolicy("custom", builder => builder.RequireAuthenticatedUser()); }); }); var validator = services.GetRequiredService(); var result = await validator.ValidateRouteAsync(route); Assert.Empty(result); } [Fact] public async Task Rejects_UnknownAuthorizationPolicy() { var route = new RouteConfig { RouteId = "route1", AuthorizationPolicy = "unknown", ClusterId = "cluster1", Match = new RouteMatch(), }; var services = CreateServices(); var validator = services.GetRequiredService(); var result = await validator.ValidateRouteAsync(route); Assert.NotEmpty(result); Assert.Contains(result, err => err.Message.Equals("Authorization policy 'unknown' not found for route 'route1'.")); } [Theory] [InlineData("Default")] [InlineData("Anonymous")] public async Task Rejects_ReservedAuthorizationPolicyIsUsed(string authorizationPolicy) { var route = new RouteConfig { RouteId = "route1", AuthorizationPolicy = authorizationPolicy, ClusterId = "cluster1", Match = new RouteMatch(), }; var services = CreateServices(serviceCollection => { serviceCollection.AddAuthorization(options => { options.AddPolicy(authorizationPolicy, builder => { builder.RequireAuthenticatedUser(); }); }); }); var validator = services.GetRequiredService(); var result = await validator.ValidateRouteAsync(route); Assert.NotEmpty(result); Assert.Contains(result, err => err.Message.Equals($"The application has registered an authorization policy named '{authorizationPolicy}' that conflicts with the reserved authorization policy name used on this route. The registered policy name needs to be changed for this route to function.")); } [Theory] [InlineData(null)] [InlineData("")] [InlineData("disAble")] public async Task Accepts_ReservedTimeoutPolicy(string policy) { var route = new RouteConfig { RouteId = "route1", TimeoutPolicy = policy, Match = new RouteMatch { Hosts = new[] { "localhost" }, }, ClusterId = "cluster1", }; var services = CreateServices(); var validator = services.GetRequiredService(); var result = await validator.ValidateRouteAsync(route); Assert.Empty(result); } [Fact] public async Task Accepts_CustomTimeoutPolicy() { var route = new RouteConfig { RouteId = "route1", TimeoutPolicy = "custom", Match = new RouteMatch { Hosts = new[] { "localhost" }, }, ClusterId = "cluster1", }; var services = CreateServices(services => { services.AddRequestTimeouts(options => { options.AddPolicy("custom", TimeSpan.FromSeconds(1)); }); }); var validator = services.GetRequiredService(); var result = await validator.ValidateRouteAsync(route); Assert.Empty(result); } [Fact] public async Task Accepts_CustomTimeout() { var route = new RouteConfig { RouteId = "route1", Timeout = TimeSpan.FromSeconds(1), Match = new RouteMatch { Hosts = new[] { "localhost" }, }, ClusterId = "cluster1", }; var services = CreateServices(); var validator = services.GetRequiredService(); var result = await validator.ValidateRouteAsync(route); Assert.Empty(result); } [Fact] public async Task Rejects_UnknownTimeoutPolicy() { var route = new RouteConfig { RouteId = "route1", TimeoutPolicy = "unknown", ClusterId = "cluster1", Match = new RouteMatch(), }; var services = CreateServices(); var validator = services.GetRequiredService(); var result = await validator.ValidateRouteAsync(route); Assert.NotEmpty(result); Assert.Contains(result, err => err.Message.Equals("Timeout policy 'unknown' not found for route 'route1'.")); } [Theory] [InlineData(0)] [InlineData(-1)] public async Task Rejects_InvalidTimeouts(int timeout) { var route = new RouteConfig { RouteId = "route1", Timeout = TimeSpan.FromMilliseconds(timeout), ClusterId = "cluster1", Match = new RouteMatch(), }; var services = CreateServices(); var validator = services.GetRequiredService(); var result = await validator.ValidateRouteAsync(route); Assert.NotEmpty(result); Assert.Contains(result, err => err.Message.Equals($"The Timeout value '{TimeSpan.FromMilliseconds(timeout)}' is invalid for route 'route1'. The Timeout must be greater than zero milliseconds.")); } [Fact] public async Task Rejects_TimeoutWithTimeoutPolicy() { var route = new RouteConfig { RouteId = "route1", TimeoutPolicy = "unknown", Timeout = TimeSpan.FromSeconds(1), ClusterId = "cluster1", Match = new RouteMatch(), }; var services = CreateServices(); var validator = services.GetRequiredService(); var result = await validator.ValidateRouteAsync(route); Assert.NotEmpty(result); Assert.Contains(result, err => err.Message.Equals("Timeout policy 'unknown' not found for route 'route1'.")); } [Theory] [InlineData(null)] [InlineData("")] [InlineData("defaulT")] [InlineData("disAble")] public async Task Accepts_ReservedCorsPolicy(string policy) { var route = new RouteConfig { RouteId = "route1", CorsPolicy = policy, Match = new RouteMatch { Hosts = new[] { "localhost" }, }, ClusterId = "cluster1", }; var services = CreateServices(); var validator = services.GetRequiredService(); var result = await validator.ValidateRouteAsync(route); Assert.Empty(result); } [Fact] public async Task Accepts_CustomCorsPolicy() { var route = new RouteConfig { RouteId = "route1", CorsPolicy = "custom", Match = new RouteMatch { Hosts = new[] { "localhost" }, }, ClusterId = "cluster1", }; var services = CreateServices(services => { services.AddCors(options => { options.AddPolicy("custom", new CorsPolicy()); }); }); var validator = services.GetRequiredService(); var result = await validator.ValidateRouteAsync(route); Assert.Empty(result); } [Fact] public async Task Rejects_UnknownCorsPolicy() { var route = new RouteConfig { RouteId = "route1", CorsPolicy = "unknown", ClusterId = "cluster1", Match = new RouteMatch(), }; var services = CreateServices(); var validator = services.GetRequiredService(); var result = await validator.ValidateRouteAsync(route); Assert.NotEmpty(result); Assert.Contains(result, err => err.Message.Equals("CORS policy 'unknown' not found for route 'route1'.")); } [Theory] [InlineData("Default")] [InlineData("Disable")] public async Task Rejects_ReservedCorsPolicyIsUsed(string corsPolicy) { var route = new RouteConfig { RouteId = "route1", CorsPolicy = corsPolicy, ClusterId = "cluster1", Match = new RouteMatch { Hosts = new[] { "localhost" }, }, }; var services = CreateServices(serviceCollection => { serviceCollection.AddCors(options => { options.AddPolicy(corsPolicy, builder => { }); }); }); var validator = services.GetRequiredService(); var result = await validator.ValidateRouteAsync(route); Assert.NotEmpty(result); Assert.Contains(result, err => err.Message.Equals($"The application has registered a CORS policy named '{corsPolicy}' that conflicts with the reserved CORS policy name used on this route. The registered policy name needs to be changed for this route to function.")); } [Theory] [InlineData("Default")] [InlineData("Disable")] public async Task Accepts_BuiltInRateLimiterPolicy(string rateLimiterPolicy) { var route = new RouteConfig { RouteId = "route1", Match = new RouteMatch { Hosts = new[] { "localhost" }, }, ClusterId = "cluster1", RateLimiterPolicy = rateLimiterPolicy }; var services = CreateServices(); var validator = services.GetRequiredService(); var result = await validator.ValidateRouteAsync(route); Assert.Empty(result); } [Theory] [InlineData("Default")] [InlineData("Disable")] public async Task Reports_BuildInRateLimiterPolicyNameConflict(string rateLimiterPolicy) { var route = new RouteConfig { RouteId = "route1", Match = new RouteMatch { Hosts = new[] { "localhost" }, }, ClusterId = "cluster1", RateLimiterPolicy = rateLimiterPolicy }; var services = CreateServices(s => { s.AddRateLimiter(o => o.AddConcurrencyLimiter(rateLimiterPolicy, c => { })); }); var validator = services.GetRequiredService(); var result = await validator.ValidateRouteAsync(route); Assert.NotEmpty(result); Assert.Contains(result, err => err.Message.Contains($"The application has registered a RateLimiter policy named '{rateLimiterPolicy}' that conflicts with the reserved RateLimiter policy name used on this route. The registered policy name needs to be changed for this route to function.")); } [Theory] [InlineData("NotAPolicy")] public async Task Rejects_InvalidRateLimiterPolicy(string rateLimiterPolicy) { var route = new RouteConfig { RouteId = "route1", Match = new RouteMatch { Hosts = new[] { "localhost" }, }, ClusterId = "cluster1", RateLimiterPolicy = rateLimiterPolicy }; var services = CreateServices(); var validator = services.GetRequiredService(); var result = await validator.ValidateRouteAsync(route); Assert.NotEmpty(result); Assert.Contains(result, err => err.Message.Contains($"RateLimiter policy '{rateLimiterPolicy}' not found")); } [Fact] public async Task EmptyCluster_Works() { var services = CreateServices(); var validator = services.GetRequiredService(); var cluster = new ClusterConfig { ClusterId = "cluster1", }; var errors = await validator.ValidateClusterAsync(cluster); Assert.Empty(errors); } [Fact] public async Task DestinationAddress_Works() { var services = CreateServices(); var validator = services.GetRequiredService(); var cluster = new ClusterConfig { ClusterId = "cluster1", Destinations = new Dictionary(StringComparer.OrdinalIgnoreCase) { { "destination1", new DestinationConfig { Address = "https://localhost:1234" } } } }; var errors = await validator.ValidateClusterAsync(cluster); Assert.Empty(errors); } [Theory] [InlineData(null)] [InlineData("")] public async Task DestinationAddressInvalid_Fails(string address) { var services = CreateServices(); var validator = services.GetRequiredService(); var cluster = new ClusterConfig { ClusterId = "cluster1", Destinations = new Dictionary(StringComparer.OrdinalIgnoreCase) { { "destination1", new DestinationConfig { Address = address } } } }; var errors = await validator.ValidateClusterAsync(cluster); var ex = Assert.Single(errors); Assert.Equal("No address found for destination 'destination1' on cluster 'cluster1'.", ex.Message); } [Fact] public async Task LoadBalancingPolicy_KnownPolicy_Works() { var services = CreateServices(); var validator = services.GetRequiredService(); var cluster = new ClusterConfig { ClusterId = "cluster1", LoadBalancingPolicy = LoadBalancingPolicies.RoundRobin }; var errors = await validator.ValidateClusterAsync(cluster); Assert.Empty(errors); } [Fact] public async Task LoadBalancingPolicy_UnknownPolicy_Fails() { var services = CreateServices(); var validator = services.GetRequiredService(); var cluster = new ClusterConfig { ClusterId = "cluster1", LoadBalancingPolicy = "MyCustomPolicy" }; var errors = await validator.ValidateClusterAsync(cluster); var ex = Assert.Single(errors); Assert.Equal("No matching ILoadBalancingPolicy found for the load balancing policy 'MyCustomPolicy' set on the cluster 'cluster1'.", ex.Message); } [Fact] public async Task EnableSessionAffinity_Works() { var services = CreateServices(); var validator = services.GetRequiredService(); var cluster = new ClusterConfig { ClusterId = "cluster1", SessionAffinity = new SessionAffinityConfig { Enabled = true, AffinityKeyName = "SomeKey" } }; var errors = await validator.ValidateClusterAsync(cluster); Assert.Empty(errors); } [Fact] public async Task EnableSessionAffinity_InvalidPolicy_Fails() { var services = CreateServices(); var validator = services.GetRequiredService(); var cluster = new ClusterConfig { ClusterId = "cluster1", SessionAffinity = new SessionAffinityConfig { Enabled = true, FailurePolicy = "Invalid", AffinityKeyName = "SomeKey" } }; var errors = await validator.ValidateClusterAsync(cluster); var ex = Assert.Single(errors); Assert.Equal("No matching IAffinityFailurePolicy found for the affinity failure policy name 'Invalid' set on the cluster 'cluster1'.", ex.Message); } [Theory] [InlineData("")] [InlineData(null)] public async Task EnableSessionAffinity_AffinityIsNotSet_Fails(string key) { var services = CreateServices(); var validator = services.GetRequiredService(); var cluster = new ClusterConfig { ClusterId = "cluster1", SessionAffinity = new SessionAffinityConfig { Enabled = true, AffinityKeyName = key } }; var errors = await validator.ValidateClusterAsync(cluster); var ex = Assert.Single(errors); Assert.Equal("Affinity key name set on the cluster 'cluster1' must not be null.", ex.Message); } [Fact] public async Task Accepts_RequestVersion_Null() { var services = CreateServices(); var validator = services.GetRequiredService(); var cluster = new ClusterConfig { ClusterId = "cluster1", HttpRequest = new ForwarderRequestConfig { Version = null, } }; var errors = await validator.ValidateClusterAsync(cluster); Assert.Empty(errors); } [Theory] [InlineData(1, 0)] [InlineData(1, 1)] [InlineData(2, 0)] public async Task Accepts_RequestVersion(int major, int minor) { var version = new Version(major, minor); var services = CreateServices(); var validator = services.GetRequiredService(); var cluster = new ClusterConfig { ClusterId = "cluster1", HttpRequest = new ForwarderRequestConfig { Version = version, } }; var errors = await validator.ValidateClusterAsync(cluster); Assert.Empty(errors); } [Theory] [InlineData(1, 9)] [InlineData(2, 5)] [InlineData(3, 1)] public async Task Rejects_RequestVersion(int major, int minor) { var version = new Version(major, minor); var services = CreateServices(); var validator = services.GetRequiredService(); var cluster = new ClusterConfig { ClusterId = "cluster1", HttpRequest = new ForwarderRequestConfig { Version = version, } }; var errors = await validator.ValidateClusterAsync(cluster); Assert.Single(errors); Assert.Equal($"Outgoing request version '{cluster.HttpRequest.Version}' is not any of supported HTTP versions (1.0, 1.1, 2 and 3).", errors[0].Message); Assert.IsType(errors[0]); } [Theory] [InlineData(null, null, null, null)] [InlineData(null, null, null, "")] [InlineData(null, null, null, "ConsecutiveFailures")] [InlineData(25, null, null, "ConsecutiveFailures")] [InlineData(25, 10, null, "ConsecutiveFailures")] [InlineData(25, 10, "/api/health", "ConsecutiveFailures")] public async Task EnableActiveHealthCheck_Works(int? interval, int? timeout, string path, string policy) { var services = CreateServices(); var validator = services.GetRequiredService(); var cluster = new ClusterConfig { ClusterId = "cluster1", HealthCheck = new HealthCheckConfig { Active = new ActiveHealthCheckConfig { Enabled = true, Interval = interval is not null ? TimeSpan.FromSeconds(interval.Value) : (TimeSpan?)null, Path = path, Policy = policy, Timeout = timeout is not null ? TimeSpan.FromSeconds(timeout.Value) : (TimeSpan?)null } } }; var errors = await validator.ValidateClusterAsync(cluster); Assert.Empty(errors); } [Theory] [InlineData(-1, null, "ConsecutiveFailures", "Destination probing interval")] [InlineData(null, -1, "ConsecutiveFailures", "Destination probing timeout")] [InlineData(null, null, "NonExistingPolicy", "No matching IActiveHealthCheckPolicy found for the active health check policy")] public async Task EnableActiveHealthCheck_InvalidParameter_ErrorReturned(int? interval, int? timeout, string policy, string expectedError) { var services = CreateServices(); var validator = services.GetRequiredService(); var cluster = new ClusterConfig { ClusterId = "cluster1", HealthCheck = new HealthCheckConfig { Active = new ActiveHealthCheckConfig { Enabled = true, Interval = interval is not null ? TimeSpan.FromSeconds(interval.Value) : (TimeSpan?)null, Policy = policy, Timeout = timeout is not null ? TimeSpan.FromSeconds(timeout.Value) : (TimeSpan?)null } } }; var errors = await validator.ValidateClusterAsync(cluster); Assert.Single(errors); Assert.Contains(expectedError, errors[0].Message); Assert.IsType(errors[0]); } [Theory] [InlineData(null, null)] [InlineData(null, "")] [InlineData(null, "passive0")] [InlineData(25, "passive0")] public async Task EnablePassiveHealthCheck_Works(int? reactivationPeriod, string policy) { var services = CreateServices(); var validator = services.GetRequiredService(); var cluster = new ClusterConfig { ClusterId = "cluster1", HealthCheck = new HealthCheckConfig { Passive = new PassiveHealthCheckConfig { Enabled = true, Policy = policy, ReactivationPeriod = reactivationPeriod is not null ? TimeSpan.FromSeconds(reactivationPeriod.Value) : (TimeSpan?)null } } }; var errors = await validator.ValidateClusterAsync(cluster); Assert.Empty(errors); } [Theory] [InlineData(-1, "passive0", "Unhealthy destination reactivation period")] [InlineData(1, "NonExistingPolicy", "No matching IPassiveHealthCheckPolicy found for the passive health check policy")] public async Task EnablePassiveHealthCheck_InvalidParameter_ErrorReturned(int? reactivationPeriod, string policy, string expectedError) { var services = CreateServices(); var validator = services.GetRequiredService(); var cluster = new ClusterConfig { ClusterId = "cluster1", HealthCheck = new HealthCheckConfig { Passive = new PassiveHealthCheckConfig { Enabled = true, Policy = policy, ReactivationPeriod = reactivationPeriod is not null ? TimeSpan.FromSeconds(reactivationPeriod.Value) : (TimeSpan?)null } } }; var errors = await validator.ValidateClusterAsync(cluster); Assert.Single(errors); Assert.Contains(expectedError, errors[0].Message); Assert.IsType(errors[0]); } [Theory] [InlineData(null)] [InlineData("")] [InlineData("availableDestinations0")] public async Task SetAvailableDestinationsPolicy_Works(string policy) { var services = CreateServices(); var validator = services.GetRequiredService(); var cluster = new ClusterConfig { ClusterId = "cluster1", HealthCheck = new HealthCheckConfig { AvailableDestinationsPolicy = policy } }; var errors = await validator.ValidateClusterAsync(cluster); Assert.Empty(errors); } [Fact] public async Task SetAvailableDestinationsPolicy_Invalid() { var services = CreateServices(); var validator = services.GetRequiredService(); const string policy = "Unknown1"; var cluster = new ClusterConfig { ClusterId = "cluster1", HealthCheck = new HealthCheckConfig { AvailableDestinationsPolicy = policy } }; var errors = await validator.ValidateClusterAsync(cluster); const string expectedError = "No matching IAvailableDestinationsPolicy found for the available destinations policy 'Unknown1' set on the cluster."; Assert.Single(errors); Assert.Contains(expectedError, errors[0].Message); Assert.IsType(errors[0]); } [Fact] public async Task HttpClient_RequestHeaderEncoding_Valid() { var services = CreateServices(); var validator = services.GetRequiredService(); var cluster = new ClusterConfig { ClusterId = "cluster1", HttpClient = new HttpClientConfig { RequestHeaderEncoding = "utf-8" } }; var errors = await validator.ValidateClusterAsync(cluster); Assert.Empty(errors); } [Fact] public async Task HttpClient_RequestHeaderEncoding_Invalid() { var services = CreateServices(); var validator = services.GetRequiredService(); var cluster = new ClusterConfig { ClusterId = "cluster1", HttpClient = new HttpClientConfig { RequestHeaderEncoding = "base64" } }; var errors = await validator.ValidateClusterAsync(cluster); Assert.Single(errors); Assert.Equal("Invalid request header encoding 'base64'.", errors[0].Message); } [Fact] public async Task HttpClient_ResponseHeaderEncoding_Valid() { var services = CreateServices(); var validator = services.GetRequiredService(); var cluster = new ClusterConfig { ClusterId = "cluster1", HttpClient = new HttpClientConfig { ResponseHeaderEncoding = "utf-8" } }; var errors = await validator.ValidateClusterAsync(cluster); Assert.Empty(errors); } [Fact] public async Task HttpClient_ResponseHeaderEncoding_Invalid() { var services = CreateServices(); var validator = services.GetRequiredService(); var cluster = new ClusterConfig { ClusterId = "cluster1", HttpClient = new HttpClientConfig { ResponseHeaderEncoding = "base64" } }; var errors = await validator.ValidateClusterAsync(cluster); Assert.Single(errors); Assert.Equal("Invalid response header encoding 'base64'.", errors[0].Message); } } ================================================ FILE: test/ReverseProxy.Tests/Configuration/DestinationConfigTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; using Xunit; namespace Yarp.ReverseProxy.Configuration.Tests; public class DestinationConfigTests { [Fact] public void Equals_Same_Value_Returns_True() { var options1 = new DestinationConfig { Address = "https://localhost:10000/destA", Health = "https://localhost:20000/destA", Metadata = new Dictionary { { "destA-K1", "destA-V1" }, { "destA-K2", "destA-V2" } } }; var options2 = new DestinationConfig { Address = "https://localhost:10000/DestA", Health = "https://localhost:20000/DestA", Metadata = new Dictionary { { "destA-K1", "destA-V1" }, { "destA-K2", "destA-V2" } } }; var options3 = options1 with { }; // Clone Assert.True(options1.Equals(options2)); Assert.True(options1.Equals(options3)); Assert.Equal(options1.GetHashCode(), options2.GetHashCode()); Assert.Equal(options1.GetHashCode(), options3.GetHashCode()); } [Fact] public void Equals_Different_Value_Returns_False() { var options1 = new DestinationConfig { Address = "https://localhost:10000/destA", Health = "https://localhost:20000/destA", Metadata = new Dictionary { { "destA-K1", "destA-V1" }, { "destA-K2", "destA-V2" } } }; Assert.False(options1.Equals(options1 with { Address = "different" })); Assert.False(options1.Equals(options1 with { Health = null })); Assert.False(options1.Equals(options1 with { Metadata = new Dictionary { { "destB-K1", "destB-V1" }, { "destB-K2", "destB-V2" } } })); } [Fact] public void Equals_Second_Null_Returns_False() { var options1 = new DestinationConfig(); var equals = options1.Equals(null); Assert.False(equals); } } ================================================ FILE: test/ReverseProxy.Tests/Configuration/HealthCheckConfigTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using Xunit; namespace Yarp.ReverseProxy.Configuration.Tests; public class HealthCheckConfigTests { [Fact] public void Equals_Same_Value_Returns_True() { var options1 = new HealthCheckConfig { Active = new ActiveHealthCheckConfig { Enabled = true, Interval = TimeSpan.FromSeconds(2), Timeout = TimeSpan.FromSeconds(1), Policy = "Any5xxResponse", Path = "/a", }, Passive = new PassiveHealthCheckConfig { Enabled = true, Policy = "Passive", ReactivationPeriod = TimeSpan.FromSeconds(5), } }; var options2 = new HealthCheckConfig { Active = new ActiveHealthCheckConfig { Enabled = true, Interval = TimeSpan.FromSeconds(2), Timeout = TimeSpan.FromSeconds(1), Policy = "any5xxResponse", Path = "/a", }, Passive = new PassiveHealthCheckConfig { Enabled = true, Policy = "passive", ReactivationPeriod = TimeSpan.FromSeconds(5), } }; var equals = options1.Equals(options2); Assert.True(equals); Assert.Equal(options1.GetHashCode(), options2.GetHashCode()); } [Fact] public void Equals_Different_Value_Returns_False() { var options1 = new HealthCheckConfig { Active = new ActiveHealthCheckConfig { Enabled = true, Interval = TimeSpan.FromSeconds(2), Timeout = TimeSpan.FromSeconds(1), Policy = "Any5xxResponse", Path = "/a", }, Passive = new PassiveHealthCheckConfig { Enabled = true, Policy = "Passive", ReactivationPeriod = TimeSpan.FromSeconds(5), } }; var options2 = new HealthCheckConfig { Active = new ActiveHealthCheckConfig { Enabled = true, Interval = TimeSpan.FromSeconds(2), Timeout = TimeSpan.FromSeconds(1), Policy = "Different", Path = "/a", }, Passive = new PassiveHealthCheckConfig { Enabled = true, Policy = "Passive", ReactivationPeriod = TimeSpan.FromSeconds(5), } }; var equals = options1.Equals(options2); Assert.False(equals); } [Fact] public void Equals_Second_Null_Returns_False() { var options1 = new HealthCheckConfig(); var equals = options1.Equals(null); Assert.False(equals); } } ================================================ FILE: test/ReverseProxy.Tests/Configuration/HttpClientConfigTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Security.Authentication; using System.Text; using Xunit; namespace Yarp.ReverseProxy.Configuration.Tests; public class HttpClientConfigTests { [Fact] public void Equals_Same_Value_Returns_True() { var options1 = new HttpClientConfig { SslProtocols = SslProtocols.Tls11, DangerousAcceptAnyServerCertificate = false, MaxConnectionsPerServer = 20, WebProxy = new WebProxyConfig() { Address = new Uri("http://localhost:8080"), BypassOnLocal = true, UseDefaultCredentials = true }, RequestHeaderEncoding = Encoding.UTF8.WebName, ResponseHeaderEncoding = Encoding.UTF8.WebName, }; var options2 = new HttpClientConfig { SslProtocols = SslProtocols.Tls11, DangerousAcceptAnyServerCertificate = false, MaxConnectionsPerServer = 20, WebProxy = new WebProxyConfig() { Address = new Uri("http://localhost:8080"), BypassOnLocal = true, UseDefaultCredentials = true }, RequestHeaderEncoding = Encoding.UTF8.WebName, ResponseHeaderEncoding = Encoding.UTF8.WebName, }; var equals = options1.Equals(options2); Assert.True(equals); Assert.True(options1 == options2); Assert.False(options1 != options2); Assert.Equal(options1.GetHashCode(), options2.GetHashCode()); } [Fact] public void Equals_Different_Value_Returns_False() { var options1 = new HttpClientConfig { SslProtocols = SslProtocols.Tls11, DangerousAcceptAnyServerCertificate = false, MaxConnectionsPerServer = 20, RequestHeaderEncoding = Encoding.UTF8.WebName, }; var options2 = new HttpClientConfig { SslProtocols = SslProtocols.Tls12, DangerousAcceptAnyServerCertificate = true, MaxConnectionsPerServer = 20, RequestHeaderEncoding = Encoding.Latin1.WebName, }; var equals = options1.Equals(options2); Assert.False(equals); } [Fact] public void Equals_Same_WebProxyAddress_Returns_True() { var options1 = new HttpClientConfig { WebProxy = new WebProxyConfig() { Address = new Uri("http://localhost:8080"), BypassOnLocal = true, UseDefaultCredentials = true } }; var options2 = new HttpClientConfig { WebProxy = new WebProxyConfig() { Address = new Uri("http://localhost:8080"), BypassOnLocal = true, UseDefaultCredentials = true } }; var equals = options1.Equals(options2); Assert.True(equals); Assert.True(options1 == options2); Assert.False(options1 != options2); } [Fact] public void Equals_Different_WebProxyAddress_Returns_False() { var options1 = new HttpClientConfig { WebProxy = new WebProxyConfig() { Address = new Uri("http://localhost:8080"), BypassOnLocal = true, UseDefaultCredentials = true } }; var options2 = new HttpClientConfig { WebProxy = new WebProxyConfig() { Address = new Uri("http://localhost:9999"), BypassOnLocal = true, UseDefaultCredentials = true } }; var equals = options1.Equals(options2); Assert.False(equals); Assert.True(options1 != options2); Assert.False(options1 == options2); } [Fact] public void Equals_Second_Null_Returns_False() { var options1 = new HttpClientConfig { SslProtocols = SslProtocols.Tls11, DangerousAcceptAnyServerCertificate = false, MaxConnectionsPerServer = 20 }; var equals = options1.Equals(null); Assert.False(equals); } } ================================================ FILE: test/ReverseProxy.Tests/Configuration/PassiveHealthCheckConfigTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using Xunit; namespace Yarp.ReverseProxy.Configuration.Tests; public class PassiveHealthCheckConfigTests { [Fact] public void Equals_Same_Value_Returns_True() { var options1 = new PassiveHealthCheckConfig { Enabled = true, Policy = "Passive", ReactivationPeriod = TimeSpan.FromSeconds(5), }; var options2 = new PassiveHealthCheckConfig { Enabled = true, Policy = "passive", ReactivationPeriod = TimeSpan.FromSeconds(5), }; var equals = options1.Equals(options2); Assert.True(equals); Assert.Equal(options1.GetHashCode(), options2.GetHashCode()); } [Fact] public void Equals_Different_Value_Returns_False() { var options1 = new PassiveHealthCheckConfig { Enabled = true, Policy = "Passive", ReactivationPeriod = TimeSpan.FromSeconds(5), }; var options2 = new PassiveHealthCheckConfig { Enabled = false, Policy = "Passive", ReactivationPeriod = TimeSpan.FromSeconds(1), }; var equals = options1.Equals(options2); Assert.False(equals); } [Fact] public void Equals_Second_Null_Returns_False() { var options1 = new PassiveHealthCheckConfig(); var equals = options1.Equals(null); Assert.False(equals); } } ================================================ FILE: test/ReverseProxy.Tests/Configuration/RouteConfigTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Text.Json; using Xunit; namespace Yarp.ReverseProxy.Configuration.Tests; public class RouteConfigTests { [Fact] public void Equals_Positive() { var a = new RouteConfig() { AuthorizationPolicy = "a", RateLimiterPolicy = "rl", TimeoutPolicy = "t", Timeout = TimeSpan.FromSeconds(1), ClusterId = "c", CorsPolicy = "co", Match = new RouteMatch() { Headers = new[] { new RouteHeader() { Name = "Hi", Values = new[] { "v1", "v2" }, IsCaseSensitive = true, Mode = HeaderMatchMode.HeaderPrefix, } }, Hosts = new[] { "foo:90" }, Methods = new[] { "GET", "POST" }, Path = "/p", }, Metadata = new Dictionary() { { "m", "m1" } }, Order = 1, RouteId = "R", }; var b = new RouteConfig() { AuthorizationPolicy = "A", RateLimiterPolicy = "RL", TimeoutPolicy = "T", Timeout = TimeSpan.FromSeconds(1), ClusterId = "C", CorsPolicy = "Co", Match = new RouteMatch() { Headers = new[] { new RouteHeader() { Name = "hi", Values = new[] { "v1", "v2" }, IsCaseSensitive = true, Mode = HeaderMatchMode.HeaderPrefix, } }, Hosts = new[] { "foo:90" }, Methods = new[] { "GET", "POST" }, Path = "/P" }, Metadata = new Dictionary() { { "m", "m1" } }, Order = 1, RouteId = "r", }; var c = b with { }; // Clone Assert.True(a.Equals(b)); Assert.True(a.Equals(c)); Assert.Equal(a.GetHashCode(), b.GetHashCode()); Assert.Equal(a.GetHashCode(), c.GetHashCode()); } [Fact] public void Equals_Negative() { var a = new RouteConfig() { AuthorizationPolicy = "a", RateLimiterPolicy = "rl", TimeoutPolicy = "t", Timeout = TimeSpan.FromSeconds(1), ClusterId = "c", CorsPolicy = "co", Match = new RouteMatch() { Headers = new[] { new RouteHeader() { Name = "Hi", Values = new[] { "v1", "v2" }, IsCaseSensitive = true, Mode = HeaderMatchMode.HeaderPrefix, } }, Hosts = new[] { "foo:90" }, Methods = new[] { "GET", "POST" }, Path = "/p", }, Metadata = new Dictionary() { { "m", "m1" } }, Order = 1, RouteId = "R", }; var b = a with { AuthorizationPolicy = "b" }; var c = a with { ClusterId = "d" }; var d = a with { CorsPolicy = "p" }; var e = a with { Match = new RouteMatch() }; var f = a with { Metadata = new Dictionary() { { "f", "f1" } } }; var g = a with { Order = null }; var h = a with { RouteId = "h" }; var i = a with { RateLimiterPolicy = "i" }; var j = a with { TimeoutPolicy = "j" }; var k = a with { Timeout = TimeSpan.FromSeconds(107) }; Assert.False(a.Equals(b)); Assert.False(a.Equals(c)); Assert.False(a.Equals(d)); Assert.False(a.Equals(e)); Assert.False(a.Equals(f)); Assert.False(a.Equals(g)); Assert.False(a.Equals(h)); Assert.False(a.Equals(i)); Assert.False(a.Equals(j)); Assert.False(a.Equals(k)); } [Fact] public void Equals_Null_False() { Assert.False(new RouteConfig().Equals(null)); } [Fact] public void RouteConfig_CanBeJsonSerialized() { var route1 = new RouteConfig() { AuthorizationPolicy = "a", RateLimiterPolicy = "rl", ClusterId = "c", CorsPolicy = "co", Match = new RouteMatch() { Headers = new[] { new RouteHeader() { Name = "Hi", Values = new[] { "v1", "v2" }, IsCaseSensitive = true, Mode = HeaderMatchMode.HeaderPrefix, } }, Hosts = new[] { "foo:90" }, Methods = new[] { "GET", "POST" }, Path = "/p", }, Metadata = new Dictionary() { { "m", "m1" } }, Transforms = new[] { new Dictionary { { "key", "value" }, { "key1", "" } } }, Order = 1, RouteId = "R", }; var json = JsonSerializer.Serialize(route1); var route2 = JsonSerializer.Deserialize(json); Assert.Equal(route1, route2); } } ================================================ FILE: test/ReverseProxy.Tests/Configuration/RouteHeaderTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Xunit; namespace Yarp.ReverseProxy.Configuration.Tests; public class RouteHeaderTests { [Theory] [InlineData(true)] [InlineData(false)] public void Equals_Positive(bool isCaseSensitive) { var a = new RouteHeader() { Name = "foo", Mode = HeaderMatchMode.Exists, Values = new[] { "v1", "v2" }, IsCaseSensitive = isCaseSensitive, }; var b = new RouteHeader() { Name = "Foo", Mode = HeaderMatchMode.Exists, Values = new[] { "v1", "v2" }, IsCaseSensitive = isCaseSensitive, }; var c = a with { }; // Clone Assert.True(a.Equals(b)); Assert.True(a.Equals(c)); Assert.Equal(a.GetHashCode(), b.GetHashCode()); Assert.Equal(a.GetHashCode(), c.GetHashCode()); } [Fact] public void Equals_Negative() { var a = new RouteHeader() { Name = "foo", Mode = HeaderMatchMode.Exists, Values = new[] { "v1", "v2" }, IsCaseSensitive = true, }; var b = a with { Name = "bar" }; var c = a with { Mode = HeaderMatchMode.ExactHeader }; var d = a with { Values = new[] { "v1", "v3" } }; var e = a with { IsCaseSensitive = false }; Assert.False(a.Equals(b)); Assert.False(a.Equals(c)); Assert.False(a.Equals(d)); Assert.False(a.Equals(e)); } [Fact] public void Equals_Null_False() { Assert.False(new RouteHeader().Equals(null)); } } ================================================ FILE: test/ReverseProxy.Tests/Configuration/RouteMatchTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Xunit; namespace Yarp.ReverseProxy.Configuration.Tests; public class RouteMatchTests { [Fact] public void Equals_Positive() { var a = new RouteMatch() { Headers = new[] { new RouteHeader() { Name = "Hi", Values = new[] { "v1", "v2" }, IsCaseSensitive = true, Mode = HeaderMatchMode.HeaderPrefix, } }, Hosts = new[] { "foo:90" }, Methods = new[] { "GET", "POST" }, Path = "/p", }; var b = new RouteMatch() { Headers = new[] { new RouteHeader() { Name = "hi", Values = new[] { "v1", "v2" }, IsCaseSensitive = true, Mode = HeaderMatchMode.HeaderPrefix, } }, Hosts = new[] { "foo:90" }, Methods = new[] { "GET", "POST" }, Path = "/P", }; var c = b with { }; // Clone Assert.True(a.Equals(b)); Assert.True(a.Equals(c)); Assert.Equal(a.GetHashCode(), b.GetHashCode()); Assert.Equal(a.GetHashCode(), c.GetHashCode()); } [Fact] public void Equals_Negative() { var a = new RouteMatch() { Headers = new[] { new RouteHeader() { Name = "Hi", Values = new[] { "v1", "v2" }, IsCaseSensitive = true, Mode = HeaderMatchMode.HeaderPrefix, } }, Hosts = new[] { "foo:90" }, Methods = new[] { "GET", "POST" }, Path = "/p", }; var b = a with { Headers = new[] { new RouteHeader() { Name = "Bye", Values = new[] { "v1", "v2" }, IsCaseSensitive = true, Mode = HeaderMatchMode.HeaderPrefix, } } }; var c = a with { Hosts = new[] { "bar:90" } }; var d = a with { Methods = new[] { "PUT", "POST" } }; var e = a with { Path = "/z" }; Assert.False(a.Equals(b)); Assert.False(a.Equals(c)); Assert.False(a.Equals(d)); Assert.False(a.Equals(e)); } [Fact] public void Equals_Null_False() { Assert.False(new RouteMatch().Equals(null)); } } ================================================ FILE: test/ReverseProxy.Tests/Configuration/RouteQueryParameterTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Xunit; namespace Yarp.ReverseProxy.Configuration.Tests; public class RouteQueryParameterTests { [Theory] [InlineData(true)] [InlineData(false)] public void Equals_Positive(bool isCaseSensitive) { var a = new RouteQueryParameter() { Name = "foo", Mode = QueryParameterMatchMode.Exists, Values = new[] { "v1", "v2" }, IsCaseSensitive = isCaseSensitive, }; var b = new RouteQueryParameter() { Name = "Foo", Mode = QueryParameterMatchMode.Exists, Values = new[] { "v1", "v2" }, IsCaseSensitive = isCaseSensitive, }; var c = a with { }; // Clone Assert.True(a.Equals(b)); Assert.True(a.Equals(c)); Assert.Equal(a.GetHashCode(), b.GetHashCode()); Assert.Equal(a.GetHashCode(), c.GetHashCode()); } [Fact] public void Equals_Negative() { var a = new RouteQueryParameter() { Name = "foo", Mode = QueryParameterMatchMode.Exists, Values = new[] { "v1", "v2" }, IsCaseSensitive = true, }; var b = a with { Name = "bar" }; var c = a with { Mode = QueryParameterMatchMode.Exact }; var d = a with { Values = new[] { "v1", "v3" } }; var e = a with { IsCaseSensitive = false }; Assert.False(a.Equals(b)); Assert.False(a.Equals(c)); Assert.False(a.Equals(d)); Assert.False(a.Equals(e)); } [Fact] public void Equals_Null_False() { Assert.False(new RouteQueryParameter().Equals(null)); } } ================================================ FILE: test/ReverseProxy.Tests/Configuration/SessionAffinityConfigTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Xunit; namespace Yarp.ReverseProxy.Configuration.Tests; public class SessionAffinityConfigTests { [Fact] public void Equals_Same_Value_Returns_True() { var options1 = new SessionAffinityConfig { Enabled = true, FailurePolicy = "policy1", Policy = "policy1", AffinityKeyName = "Key1" }; var options2 = new SessionAffinityConfig { Enabled = true, FailurePolicy = "Policy1", Policy = "Policy1", AffinityKeyName = "Key1" }; var equals = options1.Equals(options2); Assert.True(equals); Assert.Equal(options1.GetHashCode(), options2.GetHashCode()); } [Fact] public void Equals_Different_Value_Returns_False() { var options1 = new SessionAffinityConfig { Enabled = true, FailurePolicy = "policy1", Policy = "policy1", AffinityKeyName = "Key1" }; var options2 = new SessionAffinityConfig { Enabled = false, FailurePolicy = "policy2", Policy = "policy2", AffinityKeyName = "Key1" }; var equals = options1.Equals(options2); Assert.False(equals); } [Fact] public void Equals_Second_Null_Returns_False() { var options1 = new SessionAffinityConfig { Enabled = true, FailurePolicy = "policy1", Policy = "policy1", AffinityKeyName = "Key1" }; var equals = options1.Equals(null); Assert.False(equals); } } ================================================ FILE: test/ReverseProxy.Tests/Configuration/YarpOutputCachePolicyProviderTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Xunit; namespace Yarp.ReverseProxy.Configuration; public class YarpOutputCachePolicyProviderTests { [Fact] public async Task GetPolicyAsync_Works() { var services = new ServiceCollection(); services.AddOutputCache(options => { options.AddPolicy("customPolicy", opt => { opt.Expire(TimeSpan.FromSeconds(12)); opt.SetVaryByHost(true); }); }); services.AddReverseProxy(); var provider = services.BuildServiceProvider(); var outputCachePolicyProvider = provider.GetRequiredService(); Assert.Null(await outputCachePolicyProvider.GetPolicyAsync("anotherPolicy")); Assert.NotNull(await outputCachePolicyProvider.GetPolicyAsync("customPolicy")); } } ================================================ FILE: test/ReverseProxy.Tests/Configuration/YarpRateLimiterPolicyProviderTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Threading.Tasks; using System.Threading.RateLimiting; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.RateLimiting; using Microsoft.Extensions.DependencyInjection; using Xunit; namespace Yarp.ReverseProxy.Configuration; public class YarpRateLimiterPolicyProviderTests { [Fact] public async Task GetPolicyAsync_Works() { var services = new ServiceCollection(); services.AddRateLimiter(options => { options.AddFixedWindowLimiter("customPolicy", opt => { opt.PermitLimit = 4; opt.Window = TimeSpan.FromSeconds(12); opt.QueueProcessingOrder = QueueProcessingOrder.OldestFirst; opt.QueueLimit = 2; }); }); services.AddReverseProxy(); var provider = services.BuildServiceProvider(); var rateLimiterPolicyProvider = provider.GetRequiredService(); Assert.Null(await rateLimiterPolicyProvider.GetPolicyAsync("anotherPolicy")); Assert.NotNull(await rateLimiterPolicyProvider.GetPolicyAsync("customPolicy")); } } ================================================ FILE: test/ReverseProxy.Tests/Delegation/HttpSysDelegatorMiddlewareTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Net.Http; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Server.HttpSys; using Xunit; using Yarp.ReverseProxy.Configuration; using Yarp.ReverseProxy.Model; using Yarp.ReverseProxy.Utilities; using Yarp.Tests.Common; namespace Yarp.ReverseProxy.Delegation; public class HttpSysDelegatorMiddlewareTests : TestAutoMockBase { private readonly HttpSysDelegatorMiddleware _sut; private readonly RequestDelegate _next; private readonly DefaultHttpContext _context; private readonly ReverseProxyFeature _proxyFeature; private readonly List _availableDestinations; private Action _nextCallback; private bool _nextCalled; public HttpSysDelegatorMiddlewareTests() { _context = new DefaultHttpContext(); _availableDestinations = new List(); _proxyFeature = new ReverseProxyFeature { AvailableDestinations = _availableDestinations, Cluster = new ClusterModel(new ClusterConfig(), new HttpMessageInvoker(Mock().Object)), }; _context.Features.Set(_proxyFeature); _context.Features.Set(Mock().Object); Mock() .SetupGet(p => p.CanDelegate) .Returns(true); _next = context => { _nextCalled = true; _nextCallback?.Invoke(); return Task.CompletedTask; }; Provide(_next); _sut = Create(); } [Fact] public async Task SingleDelegationDestination_VerifyProxiedDestinationSetAndNextNotCalled() { var destination = CreateDestination("dest1", "queue1"); _availableDestinations.Add(destination); await _sut.Invoke(_context); Assert.Same(destination, _proxyFeature.ProxiedDestination); Assert.False(_nextCalled); } [Fact] public async Task NoDestinations_VerifyNextInvoked() { await _sut.Invoke(_context); Assert.True(_nextCalled); } [Fact] public async Task NoDelegationDestinations_VerifyNextInvoked() { _availableDestinations.Add(CreateDestination("dest1", queueName: null)); await _sut.Invoke(_context); Assert.True(_nextCalled); } [Fact] public async Task MultipleDestinations_OneDelegationAndOneProxyDestination_ProxyChosen_VerifyNextInvokedWithSingleProxyDestination() { var destination1 = CreateDestination("dest1", "queue1"); var destination2 = CreateDestination("dest2", queueName: null); _availableDestinations.Add(destination1); _availableDestinations.Add(destination2); SetupRandomToReturn(1); // return "dest2" _nextCallback = () => { Assert.Single(_proxyFeature.AvailableDestinations); Assert.Same(destination2, _proxyFeature.AvailableDestinations[0]); }; await _sut.Invoke(_context); Assert.True(_nextCalled); } [Fact] public async Task MultipleDestinations_OneDelegationAndOneProxyDestination_DelegationChosen_VerifyProxiedDestinationSetAndNextNotCalled() { var destination1 = CreateDestination("dest1", "queue1"); var destination2 = CreateDestination("dest2", queueName: null); _availableDestinations.Add(destination1); _availableDestinations.Add(destination2); SetupRandomToReturn(0); // return "dest1" await _sut.Invoke(_context); Assert.Same(destination1, _proxyFeature.ProxiedDestination); Assert.False(_nextCalled); } private static DestinationState CreateDestination(string id, string queueName = null) { var metadata = new Dictionary(); if (queueName != null) { metadata.Add(DelegationExtensions.HttpSysDelegationQueueMetadataKey, queueName); } var config = new DestinationConfig() { Address = "http://*:80", Metadata = metadata, }; return new DestinationState(id) { Model = new DestinationModel(config), }; } private void SetupRandomToReturn(int value) { Mock().Setup(m => m.CreateRandomInstance()).Returns(new TestRandom(value)); } private class TestRandom : Random { private readonly int _next; public TestRandom(int next) { _next = next; } public override int Next(int maxValue) { return _next; } } } ================================================ FILE: test/ReverseProxy.Tests/Delegation/HttpSysDelegatorTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.HttpSys; using Moq; using Xunit; using Yarp.ReverseProxy.Configuration; using Yarp.ReverseProxy.Forwarder; using Yarp.ReverseProxy.Model; using Yarp.Tests.Common; namespace Yarp.ReverseProxy.Delegation; public class HttpSysDelegatorTests : TestAutoMockBase { private readonly HttpSysDelegator _delegator; private readonly IClusterChangeListener _changeListener; private readonly DefaultHttpContext _context; public HttpSysDelegatorTests() { Mock() .Setup(m => m.Get()) .Returns(Mock().Object); Mock() .SetupGet(p => p.Features) .Returns(Mock().Object); _delegator = Create(); _changeListener = _delegator; _context = new DefaultHttpContext(); _context.Features.Set(Mock().Object); SetupCanDelegate(true); } [Fact] public void DelegateRequest_VerifyRequestDelegated() { var destination = CreateDestination("dest1", "queue1"); var cluster = CreateCluster("cluster1", destination); _changeListener.OnClusterAdded(cluster); DelegateRequest(destination); VerifyRequestDelegated(); } [Fact] public void DelegateRequest_DestinationRemoved_VerifyRequestDelegated() { var destination = CreateDestination("dest1", "queue1"); var cluster = CreateCluster("cluster1", destination); _changeListener.OnClusterAdded(cluster); _changeListener.OnClusterRemoved(cluster); DelegateRequest(destination); VerifyRequestDelegated(); } [Fact] public void DelegateRequest_DestinationChanged_VerifyRequestDelegated() { var destination = CreateDestination("dest1", "queue1"); var cluster = CreateCluster("cluster1", destination); _changeListener.OnClusterAdded(cluster); destination.Model = CreateDestinationModel("queue2"); _changeListener.OnClusterChanged(cluster); DelegateRequest(destination); VerifyRequestDelegated(); } [Fact] public void DelegateRequest_NoDelegationFeature_VerifyThrows() { var destination = CreateDestination("dest1", "queue1"); var cluster = CreateCluster("cluster1", destination); _changeListener.OnClusterAdded(cluster); _context.Features.Set(null); Assert.ThrowsAny(() => DelegateRequest(destination)); } [Fact] public void DelegateRequest_CanNotDelegate_VerifyThrows() { var destination = CreateDestination("dest1", "queue1"); var cluster = CreateCluster("cluster1", destination); _changeListener.OnClusterAdded(cluster); SetupCanDelegate(false); Assert.ThrowsAny(() => DelegateRequest(destination)); } [Fact] public void DelegateRequest_DelegationRuleNotFound_Verify503StatusAndErrorFeatureSet() { var destination = CreateDestination("dest1", "queue1"); var cluster = CreateCluster("cluster1", destination); DelegateRequest(CreateDestination("dest1", "invalidQueue")); VerifyNoAvailableDestinationsError(); } [Fact] public void DelegateRequest_CreateRuleFailed_Verify503StatusAndErrorFeatureSet() { var destination = CreateDestination("dest1", "queue1"); var cluster = CreateCluster("cluster1", destination); Mock() .Setup(x => x.CreateDelegationRule(It.IsAny(), It.IsAny())) .Throws() .Verifiable(); _changeListener.OnClusterAdded(cluster); DelegateRequest(destination); VerifyNoAvailableDestinationsError(); } [Fact] public void DelegateRequest_DelegationFails_Verify503StatusAndErrorFeatureSet() { var destination = CreateDestination("dest1", "queue1"); var cluster = CreateCluster("cluster1", destination); Mock() .Setup(x => x.DelegateRequest(It.IsAny())) .Throws() .Verifiable(); _changeListener.OnClusterAdded(cluster); DelegateRequest(destination); VerifyDelegationFailedError(); } [Fact] public void ResetQueue() { var destination = CreateDestination("dest1", "queue1"); var cluster = CreateCluster("cluster1", destination); _changeListener.OnClusterAdded(cluster); ResetDelegatorQueue(destination); } [Fact] public void OnClusterAdded_SingleDelegationDestination_RuleCreated() { var destination = CreateDestination("dest1", "queue1"); var cluster = CreateCluster("cluster1", destination); _changeListener.OnClusterAdded(cluster); VerifyDelegationRuleCreated(destination); } [Fact] public void OnClusterAdd_MultipleDelegationDestination_VerifyRulesCreated() { var destination1 = CreateDestination("dest1", "queue1"); var destination2 = CreateDestination("dest2", "queue2"); var cluster = CreateCluster("cluster1", destination1, destination2); _changeListener.OnClusterAdded(cluster); VerifyDelegationRuleCreated(destination1); VerifyDelegationRuleCreated(destination2); } [Fact] public void OnClusterAdd_MultipleDelegationDestinationWithSameQueue_VerifyRuleCreated() { var destination1 = CreateDestination("dest1", "queue1"); var destination2 = CreateDestination("dest2", "queue1"); var cluster = CreateCluster("cluster1", destination1, destination2); _changeListener.OnClusterAdded(cluster); VerifyDelegationRuleCreated(destination1); } [Fact] public void OnClusterAdd_NoDelegationDestinations_VerifyRuleNotCreated() { var destination = CreateDestination("dest1", queueName: null); var cluster = CreateCluster("cluster1", destination); _changeListener.OnClusterAdded(cluster); VerifyDelegationRuleNotCreated(destination); } [Fact] public void OnClusterChanged_RuleExists_NewRuleAdded_VerifyNewRuleCreated() { var destination1 = CreateDestination("dest1", "queue1"); var cluster = CreateCluster("cluster1", destination1); _changeListener.OnClusterAdded(cluster); Mock().Reset(); var destination2 = CreateDestination("dest2", "queue2"); cluster = CreateCluster(cluster.ClusterId, destination1, destination2); _changeListener.OnClusterAdded(cluster); VerifyDelegationRuleNotCreated(destination1); VerifyDelegationRuleCreated(destination2); } [Fact] public void OnClusterChanged_RuleExists_NoNewRuleAdded_VerifyRuleNotCreated() { var destination1 = CreateDestination("dest1", "queue1"); var cluster = CreateCluster("cluster1", destination1); _changeListener.OnClusterAdded(cluster); Mock().Reset(); var destination2 = CreateDestination("dest2", queueName: null); cluster = CreateCluster(cluster.ClusterId, destination1, destination2); _changeListener.OnClusterAdded(cluster); VerifyDelegationRuleNotCreated(destination1); VerifyDelegationRuleNotCreated(destination2); } [Fact] public void OnClusterChanged_NoRuleExists_NewRuleAdded_VerifyNewRuleCreated() { var destination = CreateDestination("dest1", "queue1"); var cluster = CreateCluster("cluster1", destination); _changeListener.OnClusterAdded(cluster); VerifyDelegationRuleCreated(destination); } [Fact] public void OnClusterChanged_NoRuleExists_NoNewRuleAdded_VerifyRuleNotCreated() { var destination = CreateDestination("dest1", queueName: null); var cluster = CreateCluster("cluster1", destination); _changeListener.OnClusterAdded(cluster); VerifyDelegationRuleNotCreated(destination); } private void SetupCanDelegate(bool canDelegate) { Mock() .SetupGet(p => p.CanDelegate) .Returns(canDelegate); } private void DelegateRequest(DestinationState destination) { _delegator.DelegateRequest(_context, destination); } private void ResetDelegatorQueue(DestinationState destination) { _delegator.ResetQueue(destination.GetHttpSysDelegationQueue(), destination.Model.Config.Address); } private void VerifyRequestDelegated() { Mock() .Verify(m => m.DelegateRequest(It.IsAny()), Times.Once()); var errorFeature = _context.Features.Get(); Assert.Null(errorFeature); } private void VerifyNoAvailableDestinationsError() { Assert.Equal(StatusCodes.Status503ServiceUnavailable, _context.Response.StatusCode); var errorFeature = _context.Features.Get(); Assert.NotNull(errorFeature); Assert.Equal(ForwarderError.NoAvailableDestinations, errorFeature.Error); } private void VerifyDelegationFailedError() { Assert.Equal(StatusCodes.Status503ServiceUnavailable, _context.Response.StatusCode); var errorFeature = _context.Features.Get(); Assert.NotNull(errorFeature); Assert.Equal(ForwarderError.Request, errorFeature.Error); Assert.NotNull(errorFeature.Exception); } private void VerifyDelegationRuleCreated(DestinationState destination) { Mock() .Verify(m => m.CreateDelegationRule(destination.GetHttpSysDelegationQueue(), destination.Model.Config.Address), Times.Once()); } private void VerifyDelegationRuleNotCreated(DestinationState destination) { Mock() .Verify(m => m.CreateDelegationRule(destination.GetHttpSysDelegationQueue(), destination.Model.Config.Address), Times.Never()); } private static ClusterState CreateCluster(string id, params DestinationState[] destinations) { var cluster = new ClusterState(id); foreach (var destination in destinations) { cluster.Destinations.TryAdd(destination.DestinationId, destination); } return cluster; } private static DestinationModel CreateDestinationModel(string queueName) { var metadata = new Dictionary(); if (queueName != null) { metadata.Add(DelegationExtensions.HttpSysDelegationQueueMetadataKey, queueName); } var config = new DestinationConfig() { Address = "http://*:80/", Metadata = metadata, }; return new DestinationModel(config); } private static DestinationState CreateDestination(string id, string queueName = null) { return new DestinationState(id) { Model = CreateDestinationModel(queueName), }; } } ================================================ FILE: test/ReverseProxy.Tests/Forwarder/ForwarderHttpClientFactoryTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Reflection; using System.Security.Authentication; using System.Text; using Microsoft.Extensions.Logging; using Xunit; using Yarp.Tests.Common; using Yarp.ReverseProxy.Configuration; namespace Yarp.ReverseProxy.Forwarder.Tests; public class ForwarderHttpClientFactoryTests : TestAutoMockBase { [Fact] public void Constructor_Works() { new ForwarderHttpClientFactory(Mock>().Object); } [Fact] public void CreateClient_Works() { var factory = new ForwarderHttpClientFactory(Mock>().Object); var actual1 = factory.CreateClient(new ForwarderHttpClientContext() { NewConfig = HttpClientConfig.Empty, OldConfig = HttpClientConfig.Empty }); var actual2 = factory.CreateClient(new ForwarderHttpClientContext() { NewConfig = HttpClientConfig.Empty, OldConfig = HttpClientConfig.Empty }); Assert.NotNull(actual1); Assert.NotNull(actual2); Assert.NotSame(actual2, actual1); } [Fact] public void CreateClient_ApplySslProtocols_Success() { var factory = new ForwarderHttpClientFactory(Mock>().Object); var options = new HttpClientConfig { SslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13, }; var client = factory.CreateClient(new ForwarderHttpClientContext { NewConfig = options }); var handler = GetHandler(client); Assert.NotNull(handler); Assert.Equal(SslProtocols.Tls12 | SslProtocols.Tls13, handler.SslOptions.EnabledSslProtocols); VerifyDefaultValues(handler, "SslProtocols"); } [Fact] public void CreateClient_ApplyDangerousAcceptAnyServerCertificate_Success() { var factory = new ForwarderHttpClientFactory(Mock>().Object); var options = new HttpClientConfig { DangerousAcceptAnyServerCertificate = true }; var client = factory.CreateClient(new ForwarderHttpClientContext { NewConfig = options }); var handler = GetHandler(client); Assert.NotNull(handler); Assert.NotNull(handler.SslOptions.RemoteCertificateValidationCallback); Assert.True(handler.SslOptions.RemoteCertificateValidationCallback(default, default, default, default)); VerifyDefaultValues(handler, "DangerousAcceptAnyServerCertificate"); } [Fact] public void CreateClient_ApplyMaxConnectionsPerServer_Success() { var factory = new ForwarderHttpClientFactory(Mock>().Object); var options = new HttpClientConfig { MaxConnectionsPerServer = 22 }; var client = factory.CreateClient(new ForwarderHttpClientContext { NewConfig = options }); var handler = GetHandler(client); Assert.NotNull(handler); Assert.Equal(22, handler.MaxConnectionsPerServer); VerifyDefaultValues(handler, "MaxConnectionsPerServer"); } [Fact] public void CreateClient_ApplyWebProxy_Success() { var factory = new ForwarderHttpClientFactory(Mock>().Object); var options = new HttpClientConfig { WebProxy = new WebProxyConfig() { Address = new Uri("http://localhost:8080"), BypassOnLocal = true, UseDefaultCredentials = true } }; var client = factory.CreateClient(new ForwarderHttpClientContext { NewConfig = options }); var handler = GetHandler(client); Assert.NotNull(handler); Assert.NotNull(handler.Proxy); Assert.True(handler.UseProxy); VerifyDefaultValues(handler, "WebProxy"); } [Fact] public void CreateClient_ApplyRequestHeaderEncoding_Success() { var factory = new ForwarderHttpClientFactory(Mock>().Object); var options = new HttpClientConfig { RequestHeaderEncoding = Encoding.Latin1.WebName }; var client = factory.CreateClient(new ForwarderHttpClientContext { NewConfig = options }); var handler = GetHandler(client); Assert.NotNull(handler); Assert.NotNull(handler.RequestHeaderEncodingSelector); Assert.Equal(Encoding.Latin1, handler.RequestHeaderEncodingSelector(default, default)); VerifyDefaultValues(handler, nameof(SocketsHttpHandler.RequestHeaderEncodingSelector)); } [Fact] public void CreateClient_ApplyResponseHeaderEncoding_Success() { var factory = new ForwarderHttpClientFactory(Mock>().Object); var options = new HttpClientConfig { ResponseHeaderEncoding = Encoding.Latin1.WebName }; var client = factory.CreateClient(new ForwarderHttpClientContext { NewConfig = options }); var handler = GetHandler(client); Assert.NotNull(handler); Assert.NotNull(handler.ResponseHeaderEncodingSelector); Assert.Equal(Encoding.Latin1, handler.ResponseHeaderEncodingSelector(default, default)); VerifyDefaultValues(handler, nameof(SocketsHttpHandler.ResponseHeaderEncodingSelector)); } [Fact] public void CreateClient_OldClientExistsNoConfigChange_ReturnsOldInstance() { var factory = new ForwarderHttpClientFactory(Mock>().Object); var oldClient = new HttpMessageInvoker(new SocketsHttpHandler()); var oldOptions = new HttpClientConfig { SslProtocols = SslProtocols.Tls11 | SslProtocols.Tls12, DangerousAcceptAnyServerCertificate = true, MaxConnectionsPerServer = 10, RequestHeaderEncoding = Encoding.Latin1.WebName, }; var newOptions = oldOptions with { }; // Clone var oldMetadata = new Dictionary { { "key1", "value1" }, { "key2", "value2" } }; var newMetadata = new Dictionary { { "key1", "value1" }, { "key2", "value2" } }; var context = new ForwarderHttpClientContext { ClusterId = "cluster1", OldConfig = oldOptions, OldMetadata = oldMetadata, OldClient = oldClient, NewConfig = newOptions, NewMetadata = newMetadata }; var actualClient = factory.CreateClient(context); Assert.Equal(newOptions, oldOptions); Assert.Same(oldClient, actualClient); } [Theory] [InlineData(true)] [InlineData(false)] public void CreateClient_ApplyEnableMultipleHttp2Connections_Success(bool enableMultipleHttp2Connections) { var factory = new ForwarderHttpClientFactory(Mock>().Object); var options = new HttpClientConfig { EnableMultipleHttp2Connections = enableMultipleHttp2Connections }; var client = factory.CreateClient(new ForwarderHttpClientContext { NewConfig = options }); var handler = GetHandler(client); Assert.Equal(enableMultipleHttp2Connections, handler.EnableMultipleHttp2Connections); } [Theory] [MemberData(nameof(GetChangedHttpClientOptions))] public void CreateClient_OldClientExistsHttpClientOptionsChanged_ReturnsNewInstance(HttpClientConfig oldOptions, HttpClientConfig newOptions) { var factory = new ForwarderHttpClientFactory(Mock>().Object); var oldClient = new HttpMessageInvoker(new SocketsHttpHandler()); var context = new ForwarderHttpClientContext { ClusterId = "cluster1", OldConfig = oldOptions, OldClient = oldClient, NewConfig = newOptions }; var actualClient = factory.CreateClient(context); Assert.NotEqual(newOptions, oldOptions); Assert.NotSame(oldClient, actualClient); } public static IEnumerable GetChangedHttpClientOptions() { return new[] { new object[] { new HttpClientConfig { SslProtocols = SslProtocols.Tls11, DangerousAcceptAnyServerCertificate = true, MaxConnectionsPerServer = null, }, new HttpClientConfig { SslProtocols = SslProtocols.Tls11 | SslProtocols.Tls12, DangerousAcceptAnyServerCertificate = true, MaxConnectionsPerServer = null, }, }, new object[] { new HttpClientConfig { SslProtocols = SslProtocols.Tls11, DangerousAcceptAnyServerCertificate = true, MaxConnectionsPerServer = null, }, new HttpClientConfig { SslProtocols = SslProtocols.Tls11, DangerousAcceptAnyServerCertificate = false, MaxConnectionsPerServer = null, }, }, new object[] { new HttpClientConfig { SslProtocols = SslProtocols.Tls11, DangerousAcceptAnyServerCertificate = true, MaxConnectionsPerServer = null, }, new HttpClientConfig { SslProtocols = SslProtocols.Tls11, DangerousAcceptAnyServerCertificate = false, MaxConnectionsPerServer = null, }, }, new object[] { new HttpClientConfig { SslProtocols = SslProtocols.Tls11, DangerousAcceptAnyServerCertificate = true, MaxConnectionsPerServer = null, }, new HttpClientConfig { SslProtocols = SslProtocols.Tls11, DangerousAcceptAnyServerCertificate = false, MaxConnectionsPerServer = null, }, }, new object[] { new HttpClientConfig { SslProtocols = SslProtocols.Tls11, DangerousAcceptAnyServerCertificate = true, MaxConnectionsPerServer = null, }, new HttpClientConfig { SslProtocols = SslProtocols.Tls11, DangerousAcceptAnyServerCertificate = true, MaxConnectionsPerServer = 10, }, }, new object[] { new HttpClientConfig { SslProtocols = SslProtocols.Tls11, DangerousAcceptAnyServerCertificate = true, MaxConnectionsPerServer = 10, }, new HttpClientConfig { SslProtocols = SslProtocols.Tls11, DangerousAcceptAnyServerCertificate = true, MaxConnectionsPerServer = null, }, }, new object[] { new HttpClientConfig { SslProtocols = SslProtocols.Tls11, DangerousAcceptAnyServerCertificate = true, MaxConnectionsPerServer = 10, }, new HttpClientConfig { SslProtocols = SslProtocols.Tls11, DangerousAcceptAnyServerCertificate = true, MaxConnectionsPerServer = 20, }, }, new object[] { new HttpClientConfig { SslProtocols = SslProtocols.Tls11, DangerousAcceptAnyServerCertificate = true, MaxConnectionsPerServer = 10, EnableMultipleHttp2Connections = true }, new HttpClientConfig { SslProtocols = SslProtocols.Tls11, DangerousAcceptAnyServerCertificate = true, MaxConnectionsPerServer = 10, EnableMultipleHttp2Connections = false }, }, new object[] { new HttpClientConfig { SslProtocols = SslProtocols.Tls11, DangerousAcceptAnyServerCertificate = true, MaxConnectionsPerServer = 10, }, new HttpClientConfig { SslProtocols = SslProtocols.Tls11, DangerousAcceptAnyServerCertificate = true, MaxConnectionsPerServer = 10, RequestHeaderEncoding = Encoding.UTF8.WebName, }, }, new object[] { new HttpClientConfig { SslProtocols = SslProtocols.Tls11, DangerousAcceptAnyServerCertificate = true, MaxConnectionsPerServer = 10, RequestHeaderEncoding = Encoding.UTF8.WebName, }, new HttpClientConfig { SslProtocols = SslProtocols.Tls11, DangerousAcceptAnyServerCertificate = true, MaxConnectionsPerServer = 10, RequestHeaderEncoding = Encoding.Latin1.WebName, }, }, new object[] { new HttpClientConfig { SslProtocols = SslProtocols.Tls11, DangerousAcceptAnyServerCertificate = true, MaxConnectionsPerServer = 10, RequestHeaderEncoding = Encoding.UTF8.WebName, }, new HttpClientConfig { SslProtocols = SslProtocols.Tls11, DangerousAcceptAnyServerCertificate = true, MaxConnectionsPerServer = 10, RequestHeaderEncoding = Encoding.UTF8.WebName, ResponseHeaderEncoding = Encoding.UTF8.WebName, }, }, new object[] { new HttpClientConfig { SslProtocols = SslProtocols.Tls11, DangerousAcceptAnyServerCertificate = true, MaxConnectionsPerServer = 10, RequestHeaderEncoding = Encoding.UTF8.WebName, ResponseHeaderEncoding = Encoding.Latin1.WebName, }, new HttpClientConfig { SslProtocols = SslProtocols.Tls11, DangerousAcceptAnyServerCertificate = true, MaxConnectionsPerServer = 10, RequestHeaderEncoding = Encoding.UTF8.WebName, ResponseHeaderEncoding = Encoding.UTF8.WebName, }, } }; } public static SocketsHttpHandler GetHandler(HttpMessageInvoker client) { var handlerFieldInfo = typeof(HttpMessageInvoker).GetFields(BindingFlags.Instance | BindingFlags.NonPublic).Single(f => f.Name == "_handler"); var handler = handlerFieldInfo.GetValue(client); return (SocketsHttpHandler)handler; } private void VerifyDefaultValues(SocketsHttpHandler actualHandler, params string[] skippedExtractors) { var skippedSet = new HashSet(skippedExtractors); var defaultHandler = new SocketsHttpHandler(); foreach (var extractor in GetAllExtractors().Where(e => !skippedSet.Contains(e.name)).Select(e => e.extractor)) { Assert.Equal(extractor(defaultHandler), extractor(actualHandler)); } } private (string name, Func extractor)[] GetAllExtractors() { return new (string name, Func extractor)[] { ("SslProtocols", h => h.SslOptions.EnabledSslProtocols), ("DangerousAcceptAnyServerCertificate", h => h.SslOptions.RemoteCertificateValidationCallback), ("ClientCertificate", h => h.SslOptions.ClientCertificates), ("MaxConnectionsPerServer", h => h.MaxConnectionsPerServer), ("WebProxy", h => h.Proxy) }; } } ================================================ FILE: test/ReverseProxy.Tests/Forwarder/ForwarderMiddlewareTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Net; using System.Net.Http; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Moq; using Xunit; using Yarp.Tests.Common; using Yarp.ReverseProxy.Configuration; using Yarp.ReverseProxy.Model; namespace Yarp.ReverseProxy.Forwarder.Tests; public class ForwarderMiddlewareTests : TestAutoMockBase { [Fact] public void Constructor_Works() { Create(); } [Fact] public async Task Invoke_Works() { var events = TestEventListener.Collect(); var httpContext = new DefaultHttpContext(); httpContext.Request.Method = "GET"; httpContext.Request.Scheme = "https"; httpContext.Request.Host = new HostString("example.com"); httpContext.Request.Path = "/api/test"; httpContext.Request.QueryString = new QueryString("?a=b&c=d"); var httpClient = new HttpMessageInvoker(new Mock().Object); var httpRequestOptions = new ForwarderRequestConfig { ActivityTimeout = TimeSpan.FromSeconds(60), Version = HttpVersion.Version11, VersionPolicy = HttpVersionPolicy.RequestVersionExact, }; var cluster1 = new ClusterState(clusterId: "cluster1"); var clusterModel = new ClusterModel(new ClusterConfig() { HttpRequest = httpRequestOptions }, httpClient); var destination1 = cluster1.Destinations.GetOrAdd( "destination1", id => new DestinationState(id) { Model = new DestinationModel(new DestinationConfig { Address = "https://localhost:123/a/b/" }) }); var routeConfig = new RouteModel( config: new RouteConfig() { RouteId = "Route-1" }, cluster: cluster1, transformer: HttpTransformer.Default); httpContext.Features.Set( new ReverseProxyFeature() { AvailableDestinations = new List() { destination1 }.AsReadOnly(), Cluster = clusterModel, Route = routeConfig, }); httpContext.Features.Set(cluster1); var tcs1 = new TaskCompletionSource(); var tcs2 = new TaskCompletionSource(); Mock() .Setup(h => h.SendAsync( httpContext, It.Is(uri => uri == "https://localhost:123/a/b/"), httpClient, It.Is(requestOptions => requestOptions.ActivityTimeout == httpRequestOptions.ActivityTimeout && requestOptions.Version == httpRequestOptions.Version && requestOptions.VersionPolicy == httpRequestOptions.VersionPolicy), It.IsAny())) .Returns( async () => { tcs1.TrySetResult(true); await tcs2.Task; return ForwarderError.None; }) .Verifiable(); var sut = Create(); Assert.Equal(0, cluster1.ConcurrencyCounter.Value); Assert.Equal(0, destination1.ConcurrentRequestCount); var task = sut.Invoke(httpContext); if (task.IsFaulted) { // Something went wrong, don't hang the test. await task; } Mock().Verify(); await tcs1.Task; // Wait until we get to the proxying step. Assert.Equal(1, cluster1.ConcurrencyCounter.Value); Assert.Equal(1, destination1.ConcurrentRequestCount); Assert.Same(destination1, httpContext.GetReverseProxyFeature().ProxiedDestination); tcs2.TrySetResult(true); await task; Assert.Equal(0, cluster1.ConcurrencyCounter.Value); Assert.Equal(0, destination1.ConcurrentRequestCount); var invoke = Assert.Single(events, e => e.EventName == "ForwarderInvoke"); Assert.Equal(3, invoke.Payload.Count); Assert.Equal(cluster1.ClusterId, (string)invoke.Payload[0]); Assert.Equal(routeConfig.Config.RouteId, (string)invoke.Payload[1]); Assert.Equal(destination1.DestinationId, (string)invoke.Payload[2]); } [Fact] public async Task NoDestinations_503() { var httpContext = new DefaultHttpContext(); httpContext.Request.Method = "GET"; httpContext.Request.Scheme = "https"; httpContext.Request.Host = new HostString("example.com"); var httpClient = new HttpMessageInvoker(new Mock().Object); var cluster1 = new ClusterState(clusterId: "cluster1"); var clusterModel = new ClusterModel(new ClusterConfig(), httpClient); var routeConfig = new RouteModel( config: new RouteConfig(), cluster: cluster1, transformer: HttpTransformer.Default); httpContext.Features.Set( new ReverseProxyFeature() { AvailableDestinations = Array.Empty(), Cluster = clusterModel, Route = routeConfig, }); Mock() .Setup(h => h.SendAsync( httpContext, It.IsAny(), httpClient, It.IsAny(), It.IsAny())) .Returns(() => throw new NotImplementedException()); var sut = Create(); Assert.Equal(0, cluster1.ConcurrencyCounter.Value); await sut.Invoke(httpContext); Assert.Equal(0, cluster1.ConcurrencyCounter.Value); Mock().Verify(); Assert.Equal(StatusCodes.Status503ServiceUnavailable, httpContext.Response.StatusCode); var errorFeature = httpContext.Features.Get(); Assert.Equal(ForwarderError.NoAvailableDestinations, errorFeature?.Error); Assert.Null(errorFeature.Exception); } } ================================================ FILE: test/ReverseProxy.Tests/Forwarder/HttpForwarderTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Diagnostics.Tracing; using System.IO; using System.IO.Pipelines; using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; using Moq; using Xunit; using Yarp.ReverseProxy.Common; using Yarp.ReverseProxy.Transforms; using Yarp.ReverseProxy.Transforms.Builder.Tests; using Yarp.ReverseProxy.Utilities; using Yarp.Tests.Common; namespace Yarp.ReverseProxy.Forwarder.Tests; public class HttpForwarderTests { private readonly ITestOutputHelper _output; public HttpForwarderTests(ITestOutputHelper output) { _output = output; } private IHttpForwarder CreateProxy() { var services = new ServiceCollection(); services.AddLogging(b => { b.SetMinimumLevel(LogLevel.Trace); b.Services.AddSingleton(new TestLoggerProvider(_output)); }); services.AddHttpForwarder(); var provider = services.BuildServiceProvider(); return provider.GetRequiredService(); } [Fact] public void Constructor_Works() { Assert.NotNull(CreateProxy()); } // Tests normal (as opposed to upgradeable) request proxying. [Fact] public async Task NormalRequest_Works() { var events = TestEventListener.Collect(); var httpContext = new DefaultHttpContext(); httpContext.Request.Method = "POST"; httpContext.Request.Scheme = "http"; httpContext.Request.Host = new HostString("example.com:3456"); httpContext.Request.Path = "/path/base/dropped"; httpContext.Request.Path = "/api/test"; httpContext.Request.QueryString = new QueryString("?a=b&c=d"); httpContext.Request.Headers[":authority"] = "example.com:3456"; httpContext.Request.Headers["x-ms-request-test"] = "request"; httpContext.Request.Headers["Content-Language"] = "requestLanguage"; var requestBody = "request content"; httpContext.Request.Headers["Content-Length"] = requestBody.Length.ToString(); httpContext.Request.Body = StringToStream(requestBody); httpContext.Connection.RemoteIpAddress = IPAddress.Loopback; var proxyResponseStream = new MemoryStream(); httpContext.Response.Body = proxyResponseStream; var destinationPrefix = "https://localhost:123/a/b/"; var targetUri = "https://localhost:123/a/b/api/test?a=b&c=d"; var sut = CreateProxy(); var client = MockHttpHandler.CreateClient( async (HttpRequestMessage request, CancellationToken cancellationToken) => { await Task.Yield(); Assert.Equal(new Version(2, 0), request.Version); Assert.Equal(HttpMethod.Post, request.Method); Assert.Equal(targetUri, request.RequestUri.AbsoluteUri); Assert.Contains("request", request.Headers.GetValues("x-ms-request-test")); Assert.Null(request.Headers.Host); Assert.False(request.Headers.TryGetValues(":authority", out var value)); Assert.NotNull(request.Content); Assert.Contains("requestLanguage", request.Content.Headers.GetValues("Content-Language")); var capturedRequestContent = new MemoryStream(); // Use CopyToAsync as this is what HttpClient and friends use internally await request.Content.CopyToWithCancellationAsync(capturedRequestContent); capturedRequestContent.Position = 0; var capturedContentText = StreamToString(capturedRequestContent); Assert.Equal("request content", capturedContentText); var response = new HttpResponseMessage((HttpStatusCode)234); response.ReasonPhrase = "Test Reason Phrase"; response.Headers.TryAddWithoutValidation("x-ms-response-test", "response"); response.Content = new StreamContent(StringToStream("response content")); response.Content.Headers.TryAddWithoutValidation("Content-Language", "responseLanguage"); return response; }); await sut.SendAsync(httpContext, destinationPrefix, client); Assert.Equal(234, httpContext.Response.StatusCode); var reasonPhrase = httpContext.Features.Get().ReasonPhrase; Assert.Equal("Test Reason Phrase", reasonPhrase); Assert.Contains("response", httpContext.Response.Headers["x-ms-response-test"].ToArray()); Assert.Contains("responseLanguage", httpContext.Response.Headers["Content-Language"].ToArray()); proxyResponseStream.Position = 0; var proxyResponseText = StreamToString(proxyResponseStream); Assert.Equal("response content", proxyResponseText); AssertProxyStartStop(events, destinationPrefix, httpContext.Response.StatusCode); events.AssertContainProxyStages(); } [Fact] public async Task NormalRequestWithTransforms_Works() { var events = TestEventListener.Collect(); var httpContext = new DefaultHttpContext(); httpContext.Request.Method = "POST"; httpContext.Request.Protocol = "http/2"; httpContext.Request.Scheme = "http"; httpContext.Request.Host = new HostString("example.com:3456"); httpContext.Request.Path = "/path/base/dropped"; httpContext.Request.Path = "/api/test"; httpContext.Request.QueryString = new QueryString("?a=b&c=d"); httpContext.Request.Headers[":authority"] = "example.com:3456"; httpContext.Request.Headers["x-ms-request-test"] = "request"; httpContext.Request.Headers["Content-Language"] = "requestLanguage"; httpContext.Request.Body = StringToStream("request content"); httpContext.Connection.RemoteIpAddress = IPAddress.Loopback; httpContext.Features.Set(new TestTrailersFeature()); var proxyResponseStream = new MemoryStream(); httpContext.Response.Body = proxyResponseStream; var destinationPrefix = "https://localhost:123/a/b/"; Uri originalRequestUri = null; var transforms = new DelegateHttpTransforms() { OnRequest = (context, request, destination) => { originalRequestUri = request.RequestUri; request.RequestUri = new Uri(destination + "prefix" + context.Request.Path + context.Request.QueryString); request.Headers.Remove("transformHeader"); request.Headers.TryAddWithoutValidation("transformHeader", "value"); request.Headers.TryAddWithoutValidation("x-ms-request-test", "transformValue"); request.Headers.Host = null; return Task.CompletedTask; }, OnResponse = (context, response) => { context.Response.Headers["transformHeader"] = "value"; context.Response.Headers.Append("x-ms-response-test", "value"); return new(true); }, OnResponseTrailers = (context, response) => { context.Response.AppendTrailer("trailerTransform", "value"); return Task.CompletedTask; } }; var targetUri = "https://localhost:123/a/b/prefix/api/test?a=b&c=d"; var sut = CreateProxy(); var client = MockHttpHandler.CreateClient( async (HttpRequestMessage request, CancellationToken cancellationToken) => { await Task.Yield(); Assert.Equal(new Version(2, 0), request.Version); Assert.Equal(HttpMethod.Post, request.Method); Assert.Equal(targetUri, request.RequestUri.AbsoluteUri); Assert.Equal(new[] { "value" }, request.Headers.GetValues("transformHeader")); Assert.Equal(new[] { "request", "transformValue" }, request.Headers.GetValues("x-ms-request-test")); Assert.Null(request.Headers.Host); Assert.False(request.Headers.TryGetValues(":authority", out var value)); Assert.NotNull(request.Content); Assert.Contains("requestLanguage", request.Content.Headers.GetValues("Content-Language")); var capturedRequestContent = new MemoryStream(); // Use CopyToAsync as this is what HttpClient and friends use internally await request.Content.CopyToWithCancellationAsync(capturedRequestContent); capturedRequestContent.Position = 0; var capturedContentText = StreamToString(capturedRequestContent); Assert.Equal("request content", capturedContentText); var response = new HttpResponseMessage((HttpStatusCode)234); response.ReasonPhrase = "Test Reason Phrase"; response.Headers.TryAddWithoutValidation("x-ms-response-test", "response"); response.Content = new StreamContent(StringToStream("response content")); response.Content.Headers.TryAddWithoutValidation("Content-Language", "responseLanguage"); return response; }); var proxyError = await sut.SendAsync(httpContext, destinationPrefix, client, ForwarderRequestConfig.Empty, transforms); Assert.Null(originalRequestUri); // Should only be set by the transformer Assert.Equal(ForwarderError.None, proxyError); Assert.Equal(234, httpContext.Response.StatusCode); var reasonPhrase = httpContext.Features.Get().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()); Assert.Equal(new[] { "value" }, httpContext.Features.Get().Trailers?["trailerTransform"].ToArray()); proxyResponseStream.Position = 0; var proxyResponseText = StreamToString(proxyResponseStream); Assert.Equal("response content", proxyResponseText); AssertProxyStartStop(events, destinationPrefix, httpContext.Response.StatusCode); events.AssertContainProxyStages(); } [Fact] public async Task NormalRequestWithCopyRequestHeadersDisabled_Works() { var events = TestEventListener.Collect(); var httpContext = new DefaultHttpContext(); httpContext.Request.Method = "POST"; httpContext.Request.Scheme = "http"; httpContext.Request.Host = new HostString("example.com:3456"); httpContext.Request.PathBase = "/api"; httpContext.Request.Path = "/test"; httpContext.Request.QueryString = new QueryString("?a=b&c=d"); httpContext.Request.Headers[":authority"] = "example.com:3456"; httpContext.Request.Headers["x-ms-request-test"] = "request"; httpContext.Request.Headers["Content-Language"] = "requestLanguage"; httpContext.Request.Headers["Transfer-Encoding"] = "chunked"; httpContext.Request.Body = StringToStream("request content"); httpContext.Connection.RemoteIpAddress = IPAddress.Loopback; var proxyResponseStream = new MemoryStream(); httpContext.Response.Body = proxyResponseStream; var destinationPrefix = "https://localhost:123/a/b/"; var transforms = new DelegateHttpTransforms() { CopyRequestHeaders = false, OnRequest = (context, request, destination) => { request.Headers.TryAddWithoutValidation("x-ms-request-test", "transformValue"); return Task.CompletedTask; } }; var targetUri = "https://localhost:123/a/b/test?a=b&c=d"; var sut = CreateProxy(); var client = MockHttpHandler.CreateClient( async (HttpRequestMessage request, CancellationToken cancellationToken) => { await Task.Yield(); Assert.Equal(new Version(2, 0), request.Version); Assert.Equal(HttpMethod.Post, request.Method); Assert.Equal(targetUri, request.RequestUri.AbsoluteUri); Assert.Equal(new[] { "transformValue" }, request.Headers.GetValues("x-ms-request-test")); Assert.Null(request.Headers.Host); Assert.False(request.Headers.TryGetValues(":authority", out var _)); Assert.NotNull(request.Content); Assert.False(request.Content.Headers.TryGetValues("Content-Language", out var _)); var capturedRequestContent = new MemoryStream(); // Use CopyToAsync as this is what HttpClient and friends use internally await request.Content.CopyToWithCancellationAsync(capturedRequestContent); capturedRequestContent.Position = 0; var capturedContentText = StreamToString(capturedRequestContent); Assert.Equal("request content", capturedContentText); var response = new HttpResponseMessage((HttpStatusCode)234); response.ReasonPhrase = "Test Reason Phrase"; response.Headers.TryAddWithoutValidation("x-ms-response-test", "response"); response.Content = new StreamContent(StringToStream("response content")); response.Content.Headers.TryAddWithoutValidation("Content-Language", "responseLanguage"); return response; }); var proxyError = await sut.SendAsync(httpContext, destinationPrefix, client, ForwarderRequestConfig.Empty, transforms); Assert.Equal(ForwarderError.None, proxyError); Assert.Equal(234, httpContext.Response.StatusCode); var reasonPhrase = httpContext.Features.Get().ReasonPhrase; Assert.Equal("Test Reason Phrase", reasonPhrase); Assert.Contains("response", httpContext.Response.Headers["x-ms-response-test"].ToArray()); Assert.Contains("responseLanguage", httpContext.Response.Headers["Content-Language"].ToArray()); proxyResponseStream.Position = 0; var proxyResponseText = StreamToString(proxyResponseStream); Assert.Equal("response content", proxyResponseText); AssertProxyStartStop(events, destinationPrefix, httpContext.Response.StatusCode); events.AssertContainProxyStages(); } [Fact] public async Task TransformRequestAsync_ReplaceBody() { var events = TestEventListener.Collect(); var replaced = "should be replaced"; var replacing = "request content"; var httpContext = new DefaultHttpContext(); httpContext.Request.Method = "POST"; httpContext.Request.Protocol = "HTTP/2"; httpContext.Request.Body = StringToStream(replaced); var destinationPrefix = "https://localhost/"; var transforms = new DelegateHttpTransforms() { CopyRequestHeaders = true, OnRequest = (context, request, destination) => { context.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(replacing)); return Task.CompletedTask; } }; var sut = CreateProxy(); var client = MockHttpHandler.CreateClient( async (HttpRequestMessage request, CancellationToken cancellationToken) => { await Task.Yield(); Assert.Equal(new Version(2, 0), request.Version); Assert.Equal(HttpMethod.Post, request.Method); Assert.NotNull(request.Content); var capturedRequestContent = new MemoryStream(); // Use CopyToAsync as this is what HttpClient and friends use internally await request.Content.CopyToWithCancellationAsync(capturedRequestContent); capturedRequestContent.Position = 0; var capturedContentText = StreamToString(capturedRequestContent); Assert.Equal(replacing, capturedContentText); return new HttpResponseMessage(HttpStatusCode.OK) { Content = new ByteArrayContent(Array.Empty()) }; }); var proxyError = await sut.SendAsync(httpContext, destinationPrefix, client, ForwarderRequestConfig.Empty, transforms); Assert.Equal(ForwarderError.None, proxyError); Assert.Equal(StatusCodes.Status200OK, httpContext.Response.StatusCode); var resultStream = (MemoryStream)httpContext.Request.Body; Assert.Equal(Encoding.UTF8.GetBytes(replacing), resultStream.ToArray()); AssertProxyStartStop(events, destinationPrefix, httpContext.Response.StatusCode); events.AssertContainProxyStages(); } [Fact] public async Task TransformRequestAsync_SetsStatus_ShortCircuits() { var events = TestEventListener.Collect(); var httpContext = new DefaultHttpContext(); httpContext.Request.Method = "POST"; httpContext.Request.Protocol = "HTTP/2"; var destinationPrefix = "https://localhost/"; var transforms = new DelegateHttpTransforms() { CopyRequestHeaders = true, OnRequest = (context, request, destination) => { context.Response.StatusCode = 401; return Task.CompletedTask; } }; var sut = CreateProxy(); var client = MockHttpHandler.CreateClient( (HttpRequestMessage request, CancellationToken cancellationToken) => { throw new NotImplementedException(); }); var proxyError = await sut.SendAsync(httpContext, destinationPrefix, client, ForwarderRequestConfig.Empty, transforms); Assert.Equal(ForwarderError.None, proxyError); Assert.Equal(StatusCodes.Status401Unauthorized, httpContext.Response.StatusCode); AssertProxyStartStop(events, destinationPrefix, httpContext.Response.StatusCode); events.AssertContainProxyStages(Array.Empty()); } [Fact] public async Task TransformRequestAsync_StartsResponse_ShortCircuits() { var events = TestEventListener.Collect(); var httpContext = new DefaultHttpContext(); var responseBody = new TestResponseBody(); httpContext.Features.Set(responseBody); httpContext.Features.Set(responseBody); httpContext.Request.Method = "POST"; httpContext.Request.Protocol = "HTTP/2"; var destinationPrefix = "https://localhost/"; var transforms = new DelegateHttpTransforms() { CopyRequestHeaders = true, OnRequest = (context, request, destination) => { return context.Response.StartAsync(); } }; var sut = CreateProxy(); var client = MockHttpHandler.CreateClient( (HttpRequestMessage request, CancellationToken cancellationToken) => { throw new NotImplementedException(); }); var proxyError = await sut.SendAsync(httpContext, destinationPrefix, client, ForwarderRequestConfig.Empty, transforms); Assert.Equal(ForwarderError.None, proxyError); Assert.Equal(StatusCodes.Status200OK, httpContext.Response.StatusCode); Assert.True(httpContext.Response.HasStarted); AssertProxyStartStop(events, destinationPrefix, httpContext.Response.StatusCode); events.AssertContainProxyStages(Array.Empty()); } [Fact] public async Task TransformRequestAsync_WritesToResponse_ShortCircuits() { var events = TestEventListener.Collect(); var httpContext = new DefaultHttpContext(); var resultStream = new MemoryStream(); var responseBody = new TestResponseBody(resultStream); httpContext.Features.Set(responseBody); httpContext.Features.Set(responseBody); httpContext.Request.Method = "POST"; httpContext.Request.Protocol = "HTTP/2"; var destinationPrefix = "https://localhost/"; var transforms = new DelegateHttpTransforms() { CopyRequestHeaders = true, OnRequest = (context, request, destination) => { return context.Response.Body.WriteAsync(Encoding.UTF8.GetBytes("Hello World")).AsTask(); } }; var sut = CreateProxy(); var client = MockHttpHandler.CreateClient( (HttpRequestMessage request, CancellationToken cancellationToken) => { throw new NotImplementedException(); }); var proxyError = await sut.SendAsync(httpContext, destinationPrefix, client, ForwarderRequestConfig.Empty, transforms); Assert.Equal(ForwarderError.None, proxyError); Assert.Equal(StatusCodes.Status200OK, httpContext.Response.StatusCode); Assert.True(httpContext.Response.HasStarted); Assert.Equal("Hello World", Encoding.UTF8.GetString(resultStream.ToArray())); AssertProxyStartStop(events, destinationPrefix, httpContext.Response.StatusCode); events.AssertContainProxyStages(Array.Empty()); } [Theory] [InlineData(true)] [InlineData(false)] public async Task TransformRequestAsync_ModifiesRequestContent_Throws(bool originalHasBody) { var events = TestEventListener.Collect(); TestLogger.Collect(); var httpContext = new DefaultHttpContext(); httpContext.Request.Method = "POST"; httpContext.Features.Set(new TestBodyDetector { CanHaveBody = originalHasBody }); var destinationPrefix = "https://localhost/foo"; var transforms = new DelegateHttpTransforms() { CopyRequestHeaders = true, OnRequest = (context, request, destination) => { request.Content = new StringContent("modified content"); return Task.CompletedTask; } }; var sut = CreateProxy(); var client = MockHttpHandler.CreateClient( (HttpRequestMessage request, CancellationToken cancellationToken) => { throw new NotImplementedException(); }); var proxyError = await sut.SendAsync(httpContext, destinationPrefix, client, ForwarderRequestConfig.Empty, transforms); AssertErrorInfo(ForwarderError.RequestCreation, StatusCodes.Status502BadGateway, proxyError, httpContext, destinationPrefix); Assert.Empty(events.GetProxyStages()); } // Tests proxying an upgradeable request. [Theory] [InlineData("WebSocket")] [InlineData("SPDY/3.1")] public async Task UpgradableRequest_Works(string upgradeHeader) { var events = TestEventListener.Collect(); var httpContext = new DefaultHttpContext(); httpContext.Request.Method = "GET"; httpContext.Request.Scheme = "http"; httpContext.Request.Host = new HostString("example.com:3456"); httpContext.Request.Path = "/api/test"; httpContext.Request.QueryString = new QueryString("?a=b&c=d"); httpContext.Request.Headers[":authority"] = "example.com:3456"; httpContext.Request.Headers["x-ms-request-test"] = "request"; httpContext.Connection.RemoteIpAddress = IPAddress.Loopback; httpContext.Request.Headers.Upgrade = upgradeHeader; var downstreamStream = new DuplexStream( readStream: StringToStream("request content"), writeStream: new MemoryStream()); DuplexStream upstreamStream = null; var upgradeFeatureMock = new Mock(); upgradeFeatureMock.SetupGet(u => u.IsUpgradableRequest).Returns(true); upgradeFeatureMock.Setup(u => u.UpgradeAsync()).ReturnsAsync(downstreamStream); httpContext.Features.Set(upgradeFeatureMock.Object); var destinationPrefix = "https://localhost:123/a/b/"; var targetUri = "https://localhost:123/a/b/api/test?a=b&c=d"; var sut = CreateProxy(); var client = MockHttpHandler.CreateClient( async (HttpRequestMessage request, CancellationToken cancellationToken) => { await Task.Yield(); Assert.Equal(new Version(1, 1), request.Version); Assert.Equal(HttpMethod.Get, request.Method); Assert.Equal(targetUri, request.RequestUri.AbsoluteUri); Assert.Contains("request", request.Headers.GetValues("x-ms-request-test")); Assert.Null(request.Headers.Host); Assert.False(request.Headers.TryGetValues(":authority", out var value)); Assert.Null(request.Content); var response = new HttpResponseMessage(HttpStatusCode.SwitchingProtocols); response.Headers.TryAddWithoutValidation("x-ms-response-test", "response"); upstreamStream = new DuplexStream( readStream: StringToStream("response content"), writeStream: new MemoryStream()); response.Content = new RawStreamContent(upstreamStream); return response; }); await sut.SendAsync(httpContext, destinationPrefix, client, new ForwarderRequestConfig() { Version = HttpVersion.Version11, }); Assert.Equal(StatusCodes.Status101SwitchingProtocols, httpContext.Response.StatusCode); Assert.Contains("response", httpContext.Response.Headers["x-ms-response-test"].ToArray()); downstreamStream.WriteStream.Position = 0; var returnedToDownstream = StreamToString(downstreamStream.WriteStream); Assert.Equal("response content", returnedToDownstream); Assert.NotNull(upstreamStream); upstreamStream.WriteStream.Position = 0; var sentToUpstream = StreamToString(upstreamStream.WriteStream); Assert.Equal("request content", sentToUpstream); AssertProxyStartStop(events, destinationPrefix, httpContext.Response.StatusCode); events.AssertContainProxyStages(upgrade: true); } // Tests proxying an upgradeable request where the destination refused to upgrade. // We should still proxy back the response. [Fact] public async Task UpgradableRequestFailsToUpgrade_ProxiesResponse() { var events = TestEventListener.Collect(); var httpContext = new DefaultHttpContext(); httpContext.Request.Method = "GET"; httpContext.Request.Scheme = "https"; httpContext.Request.Host = new HostString("example.com"); httpContext.Request.Path = "/api/test"; httpContext.Request.QueryString = new QueryString("?a=b&c=d"); httpContext.Request.Headers[":host"] = "example.com"; httpContext.Request.Headers["x-ms-request-test"] = "request"; httpContext.Request.Headers.Upgrade = "WebSocket"; var proxyResponseStream = new MemoryStream(); httpContext.Response.Body = proxyResponseStream; var upgradeFeatureMock = new Mock(MockBehavior.Strict); upgradeFeatureMock.SetupGet(u => u.IsUpgradableRequest).Returns(true); httpContext.Features.Set(upgradeFeatureMock.Object); var destinationPrefix = "https://localhost:123/a/b/"; var targetUri = "https://localhost:123/a/b/api/test?a=b&c=d"; var sut = CreateProxy(); var client = MockHttpHandler.CreateClient( async (HttpRequestMessage request, CancellationToken cancellationToken) => { await Task.Yield(); Assert.Equal(new Version(1, 1), request.Version); Assert.Equal(HttpMethod.Get, request.Method); Assert.Equal(targetUri, request.RequestUri.AbsoluteUri); Assert.Contains("request", request.Headers.GetValues("x-ms-request-test")); Assert.Null(request.Content); var response = new HttpResponseMessage((HttpStatusCode)234); response.ReasonPhrase = "Test Reason Phrase"; response.Headers.TryAddWithoutValidation("x-ms-response-test", "response"); response.Content = new StreamContent(StringToStream("response content")); response.Content.Headers.TryAddWithoutValidation("Content-Language", "responseLanguage"); return response; }); await sut.SendAsync(httpContext, destinationPrefix, client, new ForwarderRequestConfig() { Version = HttpVersion.Version11, }); Assert.Equal(234, httpContext.Response.StatusCode); var reasonPhrase = httpContext.Features.Get().ReasonPhrase; Assert.Equal("Test Reason Phrase", reasonPhrase); Assert.Contains("response", httpContext.Response.Headers["x-ms-response-test"].ToArray()); Assert.Contains("responseLanguage", httpContext.Response.Headers["Content-Language"].ToArray()); proxyResponseStream.Position = 0; var proxyResponseText = StreamToString(proxyResponseStream); Assert.Equal("response content", proxyResponseText); AssertProxyStartStop(events, destinationPrefix, httpContext.Response.StatusCode); events.AssertContainProxyStages(hasRequestContent: false, upgrade: false); } [Fact] public async Task UpgradableSpdyRequest_DisallowedByVersionPolicy_Fails() { var events = TestEventListener.Collect(); TestLogger.Collect(); var httpContext = new DefaultHttpContext(); httpContext.Request.Method = "GET"; httpContext.Request.Headers.Upgrade = "SPDY/3.1"; var upgradeFeatureMock = new Mock(); upgradeFeatureMock.SetupGet(u => u.IsUpgradableRequest).Returns(true); httpContext.Features.Set(upgradeFeatureMock.Object); var destinationPrefix = "https://localhost:123/a/b/"; var sut = CreateProxy(); var client = MockHttpHandler.CreateClient((_, _) => throw new InvalidOperationException("Unreachable")); var requestConfig = new ForwarderRequestConfig { Version = HttpVersion.Version20, VersionPolicy = HttpVersionPolicy.RequestVersionOrHigher }; var error = await sut.SendAsync(httpContext, destinationPrefix, client, requestConfig); var ex = AssertErrorInfo(ForwarderError.RequestCreation, StatusCodes.Status502BadGateway, error, httpContext, destinationPrefix); Assert.Contains("SPDY requests require HTTP/1.1 support", ex.Message); // Error thrown before sending the request. events.AssertContainProxyStages([]); } [Fact] public async Task UpgradableRequest_CancelsIfIdle() { var events = TestEventListener.Collect(); var httpContext = new DefaultHttpContext(); httpContext.Request.Method = "GET"; httpContext.Request.Scheme = "http"; httpContext.Request.Path = "/api/test"; httpContext.Connection.RemoteIpAddress = IPAddress.Loopback; httpContext.Request.Headers.Upgrade = "WebSocket"; var idleTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var downstreamStream = new DuplexStream( readStream: new StallStream(ct => { ct.Register(() => idleTcs.TrySetCanceled()); return idleTcs.Task; }), writeStream: new MemoryStream()); DuplexStream upstreamStream = null; var upgradeFeatureMock = new Mock(); upgradeFeatureMock.SetupGet(u => u.IsUpgradableRequest).Returns(true); upgradeFeatureMock.Setup(u => u.UpgradeAsync()).ReturnsAsync(downstreamStream); httpContext.Features.Set(upgradeFeatureMock.Object); var destinationPrefix = "https://localhost:123/a/b/"; var sut = CreateProxy(); var client = MockHttpHandler.CreateClient( async (HttpRequestMessage request, CancellationToken cancellationToken) => { await Task.Yield(); Assert.Equal(new Version(1, 1), request.Version); Assert.Equal(HttpMethod.Get, request.Method); Assert.Null(request.Content); var response = new HttpResponseMessage(HttpStatusCode.SwitchingProtocols); upstreamStream = new DuplexStream( readStream: new StallStream(ct => { ct.Register(() => idleTcs.TrySetCanceled()); return idleTcs.Task; }), writeStream: new MemoryStream()); response.Content = new RawStreamContent(upstreamStream); return response; }); var result = await sut.SendAsync(httpContext, destinationPrefix, client, new ForwarderRequestConfig { Version = HttpVersion.Version11, ActivityTimeout = TimeSpan.FromSeconds(1), }).DefaultTimeout(); Assert.Equal(StatusCodes.Status101SwitchingProtocols, httpContext.Response.StatusCode); Assert.Equal(ForwarderError.UpgradeActivityTimeout, result); events.AssertContainProxyStages(upgrade: true); } [Theory] [InlineData("TRACE", "HTTP/1.1", "")] [InlineData("TRACE", "HTTP/2", "")] [InlineData("GET", "HTTP/1.1", "")] [InlineData("GET", "HTTP/2", "")] [InlineData("GET", "HTTP/1.1", "Content-Length:0")] [InlineData("HEAD", "HTTP/1.1", "")] [InlineData("POST", "HTTP/1.1", "")] [InlineData("POST", "HTTP/1.1", "Content-Length:0")] [InlineData("POST", "HTTP/1.1", "Content-Length:0;Content-Type:text/plain")] [InlineData("POST", "HTTP/2", "Content-Length:0")] [InlineData("POST", "HTTP/2", "Content-Length:0;Content-Type:text/plain")] [InlineData("PATCH", "HTTP/1.1", "")] [InlineData("DELETE", "HTTP/1.1", "")] [InlineData("Unknown", "HTTP/1.1", "")] // [InlineData("CONNECT", "HTTP/1.1", "")] Blocked in HttpUtilities.GetHttpMethod [InlineData("GET", "HTTP/1.1", "Allow:Foo")] [InlineData("GET", "HTTP/1.1", "Content-Disposition:Foo")] [InlineData("GET", "HTTP/1.1", "Content-Encoding:Foo")] [InlineData("GET", "HTTP/1.1", "Content-Language:Foo")] [InlineData("GET", "HTTP/1.1", "Content-Location:Foo")] [InlineData("GET", "HTTP/1.1", "Content-MD5:Foo")] [InlineData("GET", "HTTP/1.1", "Content-Range:Foo")] [InlineData("GET", "HTTP/1.1", "Content-Type:Foo")] [InlineData("GET", "HTTP/1.1", "Expires:Foo")] [InlineData("GET", "HTTP/1.1", "Last-Modified:Foo")] public async Task RequestWithoutBodies_NoHttpContent(string method, string protocol, string headerList) { var events = TestEventListener.Collect(); var httpContext = new DefaultHttpContext(); httpContext.Request.Method = method; httpContext.Request.Protocol = protocol; var headers = headerList .Split(';', StringSplitOptions.RemoveEmptyEntries) .Select(header => (Key: header.Split(':')[0], Value: header.Split(':')[1])) .ToArray(); foreach (var (key, value) in headers) { httpContext.Request.Headers[key] = value; } var destinationPrefix = "https://localhost/"; var sut = CreateProxy(); var client = MockHttpHandler.CreateClient( async (HttpRequestMessage request, CancellationToken cancellationToken) => { Assert.Equal(new Version(2, 0), request.Version); Assert.Equal(method, request.Method.Method, StringComparer.OrdinalIgnoreCase); // When adding content-specific headers, we will inject an EmptyHttpContent if no other content is present if (headers.Any()) { Assert.NotNull(request.Content); Assert.IsType(request.Content); Assert.Empty(await request.Content.ReadAsByteArrayAsync(cancellationToken)); foreach (var (key, value) in headers) { Assert.True(request.Content.Headers.TryGetValues(key, out var values)); Assert.Equal(value, Assert.Single(values)); } // If a custom content is injected, so is a "Content-Length: 0" header Assert.True(request.Content.Headers.TryGetValues(HeaderNames.ContentLength, out var contentLength)); Assert.Equal("0", Assert.Single(contentLength)); } else { Assert.Null(request.Content); } return new HttpResponseMessage(HttpStatusCode.OK) { Content = new ByteArrayContent(Array.Empty()) }; }); await sut.SendAsync(httpContext, destinationPrefix, client); Assert.Equal(StatusCodes.Status200OK, httpContext.Response.StatusCode); AssertProxyStartStop(events, destinationPrefix, httpContext.Response.StatusCode); events.AssertContainProxyStages(hasRequestContent: false); } [Theory] [InlineData("POST", "HTTP/2", "", "")] [InlineData("PATCH", "HTTP/2", "", "")] [InlineData("UNKNOWN", "HTTP/2", "", "")] [InlineData("UNKNOWN", "HTTP/1.1", "Content-Length:10", "aaaaaaaaaa")] [InlineData("UNKNOWN", "HTTP/1.1", "transfer-encoding:Chunked", "")] [InlineData("GET", "HTTP/1.1", "Content-Length:10", "aaaaaaaaaa")] [InlineData("GET", "HTTP/2", "Content-Length:10", "aaaaaaaaaa")] [InlineData("HEAD", "HTTP/1.1", "transfer-encoding:Chunked", "")] [InlineData("HEAD", "HTTP/2", "transfer-encoding:Chunked", "")] public async Task RequestWithBodies_HasHttpContent(string method, string protocol, string headers, string body) { var events = TestEventListener.Collect(); var httpContext = new DefaultHttpContext(); httpContext.Request.Method = method; httpContext.Request.Protocol = protocol; foreach (var header in headers.Split(';', StringSplitOptions.RemoveEmptyEntries)) { var parts = header.Split(':'); var key = parts[0]; var value = parts[1]; httpContext.Request.Headers[key] = value; } httpContext.Request.Body = StringToStream(body); var destinationPrefix = "https://localhost/"; var sut = CreateProxy(); var client = MockHttpHandler.CreateClient( async (HttpRequestMessage request, CancellationToken cancellationToken) => { Assert.Equal(new Version(2, 0), request.Version); Assert.Equal(method, request.Method.Method, StringComparer.OrdinalIgnoreCase); Assert.NotNull(request.Content); // Must consume the body await request.Content.CopyToWithCancellationAsync(Stream.Null); return new HttpResponseMessage(HttpStatusCode.OK) { Content = new ByteArrayContent(Array.Empty()) }; }); await sut.SendAsync(httpContext, destinationPrefix, client); Assert.Equal(StatusCodes.Status200OK, httpContext.Response.StatusCode); AssertProxyStartStop(events, destinationPrefix, httpContext.Response.StatusCode); events.AssertContainProxyStages(); } [Theory] [InlineData("1.1", 1, "")] [InlineData("1.1", 1, "aa")] [InlineData("1.1", 2, "a")] [InlineData("2.0", 1, "")] [InlineData("2.0", 1, "aa")] [InlineData("2.0", 2, "a")] public async Task RequestWithBodies_WrongContentLength(string version, long contentLength, string body) { TestEventListener.Collect(); TestLogger.Collect(); var httpContext = new DefaultHttpContext(); httpContext.Request.Method = "POST"; httpContext.Request.ContentLength = contentLength; httpContext.Request.Body = StringToStream(body); var destinationPrefix = "https://localhost/"; var sut = CreateProxy(); var client = MockHttpHandler.CreateClient( async (HttpRequestMessage request, CancellationToken cancellationToken) => { Assert.Equal(new Version(version), request.Version); Assert.NotNull(request.Content); // Must throw try { await request.Content.CopyToWithCancellationAsync(Stream.Null); } catch (HttpRequestException ex) { Assert.Contains("Content-Length", ex.InnerException.InnerException.Message); throw; } return new HttpResponseMessage(HttpStatusCode.OK) { Content = new ByteArrayContent(Array.Empty()) }; }); var options = new ForwarderRequestConfig { Version = new Version(version), }; var proxyError = await sut.SendAsync(httpContext, destinationPrefix, client, options); AssertErrorInfoAndStages(ForwarderError.RequestBodyClient, StatusCodes.Status400BadRequest, proxyError, httpContext, destinationPrefix, ForwarderStage.RequestContentTransferStart); } [Fact] public async Task RequestWithBodies_WithoutContentLength() { var events = TestEventListener.Collect(); var httpContext = new DefaultHttpContext(); httpContext.Request.Method = "POST"; httpContext.Request.Protocol = "HTTP/2"; httpContext.Request.Body = StringToStream("request content"); var destinationPrefix = "https://localhost/"; var sut = CreateProxy(); var client = MockHttpHandler.CreateClient( async (HttpRequestMessage request, CancellationToken cancellationToken) => { Assert.Equal(new Version(2, 0), request.Version); Assert.NotNull(request.Content); // Must consume the body await request.Content.CopyToWithCancellationAsync(Stream.Null); return new HttpResponseMessage(HttpStatusCode.OK) { Content = new ByteArrayContent(Array.Empty()) }; }); await sut.SendAsync(httpContext, destinationPrefix, client); Assert.Equal(StatusCodes.Status200OK, httpContext.Response.StatusCode); AssertProxyStartStop(events, destinationPrefix, httpContext.Response.StatusCode); events.AssertContainProxyStages(); } [Fact] public async Task BodyDetectionFeatureSaysNo_NoHttpContent() { var events = TestEventListener.Collect(); var httpContext = new DefaultHttpContext(); httpContext.Request.Method = HttpMethods.Post; httpContext.Features.Set(new TestBodyDetector() { CanHaveBody = false }); var destinationPrefix = "https://localhost/"; var sut = CreateProxy(); var client = MockHttpHandler.CreateClient( (HttpRequestMessage request, CancellationToken cancellationToken) => { Assert.Equal(new Version(2, 0), request.Version); Assert.Null(request.Content); var response = new HttpResponseMessage(HttpStatusCode.OK) { Content = new ByteArrayContent(Array.Empty()) }; return Task.FromResult(response); }); await sut.SendAsync(httpContext, destinationPrefix, client); Assert.Equal(StatusCodes.Status200OK, httpContext.Response.StatusCode); AssertProxyStartStop(events, destinationPrefix, httpContext.Response.StatusCode); events.AssertContainProxyStages(hasRequestContent: false); } [Fact] public async Task BodyDetectionFeatureSaysYes_HasHttpContent() { var events = TestEventListener.Collect(); var httpContext = new DefaultHttpContext(); httpContext.Request.Method = HttpMethods.Get; httpContext.Features.Set(new TestBodyDetector() { CanHaveBody = true }); var destinationPrefix = "https://localhost/"; var sut = CreateProxy(); var client = MockHttpHandler.CreateClient( async (HttpRequestMessage request, CancellationToken cancellationToken) => { Assert.Equal(new Version(2, 0), request.Version); Assert.NotNull(request.Content); // Must consume the body await request.Content.CopyToWithCancellationAsync(Stream.Null); return new HttpResponseMessage(HttpStatusCode.OK) { Content = new ByteArrayContent(Array.Empty()) }; }); await sut.SendAsync(httpContext, destinationPrefix, client); Assert.Equal(StatusCodes.Status200OK, httpContext.Response.StatusCode); AssertProxyStartStop(events, destinationPrefix, httpContext.Response.StatusCode); events.AssertContainProxyStages(); } private class TestBodyDetector : IHttpRequestBodyDetectionFeature { public bool CanHaveBody { get; set; } } [Theory] [InlineData("testA=A_Value, testB=B_Value, testC=C_Value")] public async Task RequestWithCookieHeaders(params string[] cookies) { var events = TestEventListener.Collect(); var httpContext = new DefaultHttpContext(); httpContext.Request.Method = "GET"; httpContext.Request.Headers[HeaderNames.Cookie] = cookies; var destinationPrefix = "https://localhost/"; var sut = CreateProxy(); var client = MockHttpHandler.CreateClient( (HttpRequestMessage request, CancellationToken cancellationToken) => { // "testA=A_Cookie; testB=B_Cookie; testC=C_Cookie" var expectedCookieString = string.Join("; ", cookies); Assert.Equal(new Version(2, 0), request.Version); Assert.Equal("GET", request.Method.Method, StringComparer.OrdinalIgnoreCase); Assert.Null(request.Content); Assert.True(request.Headers.TryGetValues(HeaderNames.Cookie, out var cookieHeaders)); Assert.NotNull(cookieHeaders); var cookie = Assert.Single(cookieHeaders); Assert.Equal(expectedCookieString, cookie); var response = new HttpResponseMessage(HttpStatusCode.OK) { Content = new ByteArrayContent(Array.Empty()) }; return Task.FromResult(response); }); await sut.SendAsync(httpContext, destinationPrefix, client); Assert.Null(httpContext.Features.Get()); Assert.Equal(StatusCodes.Status200OK, httpContext.Response.StatusCode); AssertProxyStartStop(events, destinationPrefix, httpContext.Response.StatusCode); events.AssertContainProxyStages(hasRequestContent: false); } [Theory] [MemberData(nameof(RequestMultiHeadersData))] public async Task RequestWithMultiHeaders(string version, string headerName, string[] headers) { var events = TestEventListener.Collect(); var httpContext = new DefaultHttpContext(); httpContext.Request.Method = "GET"; httpContext.Request.Headers[headerName] = headers; var destinationPrefix = "https://localhost/"; var sut = CreateProxy(); var client = MockHttpHandler.CreateClient( (HttpRequestMessage request, CancellationToken cancellationToken) => { Assert.Equal(new Version(version), request.Version); Assert.Equal("GET", request.Method.Method, StringComparer.OrdinalIgnoreCase); IEnumerable sentHeaders; if (headerName.StartsWith("Content")) { Assert.True(request.Content.Headers.TryGetValues(headerName, out sentHeaders)); } else { Assert.True(request.Headers.TryGetValues(headerName, out sentHeaders)); } Assert.NotNull(sentHeaders); AreEqualIgnoringEmptyStrings(sentHeaders, headers); var response = new HttpResponseMessage(HttpStatusCode.OK) { Content = new ByteArrayContent(Array.Empty()) }; return Task.FromResult(response); }); await sut.SendAsync(httpContext, destinationPrefix, client, new ForwarderRequestConfig { Version = new Version(version) }); Assert.Null(httpContext.Features.Get()); Assert.Equal(StatusCodes.Status200OK, httpContext.Response.StatusCode); AssertProxyStartStop(events, destinationPrefix, httpContext.Response.StatusCode); events.AssertContainProxyStages(hasRequestContent: false); } [Theory] [MemberData(nameof(RequestEmptyMultiHeadersData))] public async Task RequestWithEmptyMultiHeaders(string version, string headerName, string[] headers) { var events = TestEventListener.Collect(); var httpContext = new DefaultHttpContext(); httpContext.Request.Method = "GET"; httpContext.Request.Headers[headerName] = headers; var destinationPrefix = "https://localhost/"; var sut = CreateProxy(); var client = MockHttpHandler.CreateClient( (HttpRequestMessage request, CancellationToken cancellationToken) => { Assert.Equal(new Version(version), request.Version); Assert.Equal("GET", request.Method.Method, StringComparer.OrdinalIgnoreCase); HeaderStringValues sentHeaders; if (headerName.StartsWith("Content")) { Assert.True(request.Content.Headers.NonValidated.TryGetValues(headerName, out sentHeaders)); } else { Assert.True(request.Headers.NonValidated.TryGetValues(headerName, out sentHeaders)); } Assert.Equal(sentHeaders, headers); var response = new HttpResponseMessage(HttpStatusCode.OK) { Content = new ByteArrayContent(Array.Empty()) }; return Task.FromResult(response); }); await sut.SendAsync(httpContext, destinationPrefix, client, new ForwarderRequestConfig { Version = new Version(version) }); Assert.Null(httpContext.Features.Get()); Assert.Equal(StatusCodes.Status200OK, httpContext.Response.StatusCode); AssertProxyStartStop(events, destinationPrefix, httpContext.Response.StatusCode); events.AssertContainProxyStages(hasRequestContent: false); } internal static void AreEqualIgnoringEmptyStrings(IEnumerable left, IEnumerable right) => Assert.Equal(left.Where(s => !string.IsNullOrEmpty(s)), right.Where(s => !string.IsNullOrEmpty(s))); public static IEnumerable RequestMultiHeaderNames() { var headers = new[] { HeaderNames.Accept, HeaderNames.AcceptCharset, HeaderNames.AcceptEncoding, HeaderNames.AcceptLanguage, HeaderNames.ContentEncoding, HeaderNames.ContentLanguage, HeaderNames.ContentType, HeaderNames.Via }; foreach (var header in headers) { yield return header; } } public static IEnumerable ResponseMultiHeaderNames() { var headers = new[] { HeaderNames.AcceptRanges, HeaderNames.Allow, HeaderNames.ContentEncoding, HeaderNames.ContentLanguage, HeaderNames.ContentRange, HeaderNames.ContentType, HeaderNames.SetCookie, HeaderNames.Via, HeaderNames.Warning, HeaderNames.WWWAuthenticate }; foreach (var header in headers) { yield return header; } } public static IEnumerable MultiValues() { var values = new string[][] { new[] { "testA=A_Value", "testB=B_Value", "testC=C_Value" }, new[] { "testA=A_Value, testB=B_Value", "testC=C_Value" }, new[] { "testA=A_Value", "", "testB=B_Value, testC=C_Value" }, new[] { "testA=A_Value, testB=B_Value, testC=C_Value" } }; foreach (var value in values) { yield return value; } } public static IEnumerable RequestMultiHeadersData() { foreach (var header in RequestMultiHeaderNames()) { foreach (var value in MultiValues()) { foreach (var version in new[] { "1.1", "2.0" }) { yield return new object[] { version, header, value }; } } } } public static IEnumerable ResponseMultiHeadersData() { foreach (var header in ResponseMultiHeaderNames()) { foreach (var version in new[] { "1.1", "2.0" }) { foreach (var value in MultiValues()) { yield return new object[] { version, header, value }; } yield return new object[] { version, header, new[] { "", "" } }; } } } public static IEnumerable RequestEmptyMultiHeadersData() { foreach (var header in RequestMultiHeaderNames()) { foreach (var version in new[] { "1.1", "2.0" }) { yield return new object[] { version, header, new[] { "", "" } }; } } } public static IEnumerable ResponseEmptyMultiHeadersData() { foreach (var header in ResponseMultiHeaderNames()) { foreach (var version in new[] { "1.1", "2.0" }) { yield return new object[] { version, header, new[] { "", "" } }; } } } [Fact] public async Task OptionsWithVersion() { var events = TestEventListener.Collect(); // Use any non-default value var version = new Version(3, 0); var versionPolicy = HttpVersionPolicy.RequestVersionExact; var httpContext = new DefaultHttpContext(); httpContext.Request.Method = "GET"; var destinationPrefix = "https://localhost/"; var sut = CreateProxy(); var client = MockHttpHandler.CreateClient( (HttpRequestMessage request, CancellationToken cancellationToken) => { Assert.Equal(version, request.Version); Assert.Equal(versionPolicy, request.VersionPolicy); Assert.Equal("GET", request.Method.Method, StringComparer.OrdinalIgnoreCase); Assert.Null(request.Content); var response = new HttpResponseMessage(HttpStatusCode.OK) { Content = new ByteArrayContent(Array.Empty()) }; return Task.FromResult(response); }); var options = new ForwarderRequestConfig { Version = version, VersionPolicy = versionPolicy, }; var proxyError = await sut.SendAsync(httpContext, destinationPrefix, client, options); Assert.Equal(ForwarderError.None, proxyError); Assert.Null(httpContext.Features.Get()); Assert.Equal(StatusCodes.Status200OK, httpContext.Response.StatusCode); AssertProxyStartStop(events, destinationPrefix, httpContext.Response.StatusCode); events.AssertContainProxyStages(hasRequestContent: false); } [Fact] public async Task OptionsWithVersion_Transformed() { var events = TestEventListener.Collect(); // Use any non-default value var version = new Version(0, 9); var transformedVersion = new Version(3, 0); var versionPolicy = HttpVersionPolicy.RequestVersionExact; var transformedVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher; var httpContext = new DefaultHttpContext(); httpContext.Request.Method = "GET"; var destinationPrefix = "https://localhost/"; var sut = CreateProxy(); var client = MockHttpHandler.CreateClient( (HttpRequestMessage request, CancellationToken cancellationToken) => { Assert.Equal(transformedVersion, request.Version); Assert.Equal(transformedVersionPolicy, request.VersionPolicy); Assert.Equal("GET", request.Method.Method, StringComparer.OrdinalIgnoreCase); Assert.Null(request.Content); var response = new HttpResponseMessage(HttpStatusCode.OK) { Content = new ByteArrayContent(Array.Empty()) }; return Task.FromResult(response); }); var transforms = new DelegateHttpTransforms() { CopyRequestHeaders = false, OnRequest = (context, request, destination) => { Assert.Equal(version, request.Version); request.Version = transformedVersion; Assert.Equal(versionPolicy, request.VersionPolicy); request.VersionPolicy = transformedVersionPolicy; return Task.CompletedTask; } }; var requestOptions = new ForwarderRequestConfig { Version = version, VersionPolicy = versionPolicy, }; var proxyError = await sut.SendAsync(httpContext, destinationPrefix, client, requestOptions, transforms); Assert.Equal(ForwarderError.None, proxyError); Assert.Null(httpContext.Features.Get()); Assert.Equal(StatusCodes.Status200OK, httpContext.Response.StatusCode); AssertProxyStartStop(events, destinationPrefix, httpContext.Response.StatusCode); events.AssertContainProxyStages(hasRequestContent: false); } [Fact] public async Task UnableToConnect_Returns502() { TestEventListener.Collect(); TestLogger.Collect(); var httpContext = new DefaultHttpContext(); httpContext.Request.Method = "GET"; httpContext.Request.Host = new HostString("example.com:3456"); var proxyResponseStream = new MemoryStream(); httpContext.Response.Body = proxyResponseStream; var destinationPrefix = "https://localhost:123/"; var sut = CreateProxy(); var client = MockHttpHandler.CreateClient( (HttpRequestMessage request, CancellationToken cancellationToken) => { throw new HttpRequestException("No connection could be made because the target machine actively refused it."); }); var proxyError = await sut.SendAsync(httpContext, destinationPrefix, client); AssertErrorInfoAndStages(ForwarderError.Request, StatusCodes.Status502BadGateway, proxyError, httpContext, destinationPrefix); Assert.Equal(0, proxyResponseStream.Length); } [Fact] public async Task UnableToConnectWithBody_Returns502() { TestEventListener.Collect(); TestLogger.Collect(); var httpContext = new DefaultHttpContext(); httpContext.Request.Method = "POST"; httpContext.Request.Host = new HostString("example.com:3456"); httpContext.Request.Body = new MemoryStream(new byte[1]); httpContext.Request.ContentLength = 1; var proxyResponseStream = new MemoryStream(); httpContext.Response.Body = proxyResponseStream; var destinationPrefix = "https://localhost:123/"; var sut = CreateProxy(); var client = MockHttpHandler.CreateClient( (HttpRequestMessage request, CancellationToken cancellationToken) => { throw new HttpRequestException("No connection could be made because the target machine actively refused it."); }); var proxyError = await sut.SendAsync(httpContext, destinationPrefix, client); AssertErrorInfoAndStages(ForwarderError.Request, StatusCodes.Status502BadGateway, proxyError, httpContext, destinationPrefix); Assert.Equal(0, proxyResponseStream.Length); } [Fact] public async Task RequestTimedOut_Returns504() { TestEventListener.Collect(); TestLogger.Collect(); var httpContext = new DefaultHttpContext(); httpContext.Request.Method = "GET"; httpContext.Request.Host = new HostString("example.com:3456"); var proxyResponseStream = new MemoryStream(); httpContext.Response.Body = proxyResponseStream; var destinationPrefix = "https://localhost:123/"; var sut = CreateProxy(); var client = MockHttpHandler.CreateClient( (HttpRequestMessage request, CancellationToken cancellationToken) => { cancellationToken.WaitHandle.WaitOne(); cancellationToken.ThrowIfCancellationRequested(); return Task.FromResult(new HttpResponseMessage()); }); // Time out immediately var requestOptions = new ForwarderRequestConfig { ActivityTimeout = TimeSpan.FromTicks(1) }; var proxyError = await sut.SendAsync(httpContext, destinationPrefix, client, requestOptions); AssertErrorInfoAndStages(ForwarderError.RequestTimedOut, StatusCodes.Status504GatewayTimeout, proxyError, httpContext, destinationPrefix); Assert.Equal(0, proxyResponseStream.Length); } [Fact] public async Task RequestConnectTimedOut_Returns504() { TestEventListener.Collect(); TestLogger.Collect(); var httpContext = new DefaultHttpContext(); httpContext.Request.Method = "GET"; httpContext.Request.Host = new HostString("example.com:3456"); var proxyResponseStream = new MemoryStream(); httpContext.Response.Body = proxyResponseStream; var destinationPrefix = "https://microsoft.com:123/"; // Port that doesn't accept connections var sut = CreateProxy(); using var client = new HttpMessageInvoker(new SocketsHttpHandler { // Time out immediately ConnectTimeout = TimeSpan.FromTicks(1) }); var proxyError = await sut.SendAsync(httpContext, destinationPrefix, client); AssertErrorInfoAndStages(ForwarderError.RequestTimedOut, StatusCodes.Status504GatewayTimeout, proxyError, httpContext, destinationPrefix); Assert.Equal(0, proxyResponseStream.Length); } [Fact] public async Task RequestCanceled_Returns400() { TestEventListener.Collect(); TestLogger.Collect(); var httpContext = new DefaultHttpContext(); httpContext.Request.Method = "GET"; httpContext.Request.Host = new HostString("example.com:3456"); httpContext.RequestAborted = new CancellationToken(canceled: true); var proxyResponseStream = new MemoryStream(); httpContext.Response.Body = proxyResponseStream; var destinationPrefix = "https://localhost:123/"; var sut = CreateProxy(); var client = MockHttpHandler.CreateClient( (HttpRequestMessage request, CancellationToken cancellationToken) => { cancellationToken.ThrowIfCancellationRequested(); return Task.FromResult(new HttpResponseMessage()); }); var proxyError = await sut.SendAsync(httpContext, destinationPrefix, client); AssertErrorInfoAndStages(ForwarderError.RequestCanceled, StatusCodes.Status400BadRequest, proxyError, httpContext, destinationPrefix); Assert.Equal(0, proxyResponseStream.Length); } [Fact] public async Task RequestWithBodyTimedOut_Returns504() { TestEventListener.Collect(); TestLogger.Collect(); var httpContext = new DefaultHttpContext(); httpContext.Request.Method = "POST"; httpContext.Request.Host = new HostString("example.com:3456"); httpContext.Request.Body = new MemoryStream(new byte[1]); httpContext.Request.ContentLength = 1; var proxyResponseStream = new MemoryStream(); httpContext.Response.Body = proxyResponseStream; var destinationPrefix = "https://localhost:123/"; var sut = CreateProxy(); var client = MockHttpHandler.CreateClient( (HttpRequestMessage request, CancellationToken cancellationToken) => { cancellationToken.WaitHandle.WaitOne(); cancellationToken.ThrowIfCancellationRequested(); return Task.FromResult(new HttpResponseMessage()); }); // Time out immediately var requestOptions = new ForwarderRequestConfig { ActivityTimeout = TimeSpan.FromTicks(1) }; var proxyError = await sut.SendAsync(httpContext, destinationPrefix, client, requestOptions); AssertErrorInfoAndStages(ForwarderError.RequestTimedOut, StatusCodes.Status504GatewayTimeout, proxyError, httpContext, destinationPrefix); Assert.Equal(0, proxyResponseStream.Length); } [Fact] public async Task RequestWithBody_KeptAliveByActivity() { var events = TestEventListener.Collect(); var reads = 0; var expectedReads = 6; var httpContext = new DefaultHttpContext(); httpContext.Request.Method = "POST"; httpContext.Request.Body = new CallbackReadStream(async (memory, ct) => { if (memory.Length == 0 || reads >= expectedReads) { return 0; } reads++; await Task.Delay(TimeSpan.FromMilliseconds(250), ct); memory.Span[0] = (byte)'a'; return 1; }); httpContext.Request.ContentLength = expectedReads; var proxyResponseStream = new MemoryStream(); httpContext.Response.Body = proxyResponseStream; var destinationPrefix = "https://localhost:123/"; var sut = CreateProxy(); var client = MockHttpHandler.CreateClient( async (HttpRequestMessage request, CancellationToken cancellationToken) => { Assert.Equal(new Version(2, 0), request.Version); Assert.Equal("POST", request.Method.Method, StringComparer.OrdinalIgnoreCase); Assert.NotNull(request.Content); // Must consume the body var body = new MemoryStream(); await request.Content.CopyToWithCancellationAsync(body); Assert.Equal(expectedReads, body.Length); cancellationToken.ThrowIfCancellationRequested(); return new HttpResponseMessage(HttpStatusCode.OK) { Content = new ByteArrayContent(Array.Empty()) }; }); var requestOptions = new ForwarderRequestConfig { ActivityTimeout = TimeSpan.FromSeconds(1) }; var proxyError = await sut.SendAsync(httpContext, destinationPrefix, client, requestOptions); Assert.Equal(ForwarderError.None, proxyError); Assert.Equal(StatusCodes.Status200OK, httpContext.Response.StatusCode); Assert.Equal(0, proxyResponseStream.Length); AssertProxyStartStop(events, destinationPrefix, httpContext.Response.StatusCode); events.AssertContainProxyStages([ForwarderStage.SendAsyncStart, ForwarderStage.SendAsyncStop, ForwarderStage.RequestContentTransferStart, ForwarderStage.ResponseContentTransferStart]); } [Fact] public async Task RequestWithBodyCanceled_Returns400() { TestEventListener.Collect(); TestLogger.Collect(); var httpContext = new DefaultHttpContext(); httpContext.Request.Method = "POST"; httpContext.Request.Host = new HostString("example.com:3456"); httpContext.Request.Body = new MemoryStream(new byte[1]); httpContext.Request.ContentLength = 1; httpContext.RequestAborted = new CancellationToken(canceled: true); var proxyResponseStream = new MemoryStream(); httpContext.Response.Body = proxyResponseStream; var destinationPrefix = "https://localhost:123/"; var sut = CreateProxy(); var client = MockHttpHandler.CreateClient( (HttpRequestMessage request, CancellationToken cancellationToken) => { cancellationToken.ThrowIfCancellationRequested(); return Task.FromResult(new HttpResponseMessage()); }); var proxyError = await sut.SendAsync(httpContext, destinationPrefix, client); AssertErrorInfoAndStages(ForwarderError.RequestCanceled, StatusCodes.Status400BadRequest, proxyError, httpContext, destinationPrefix); Assert.Equal(0, proxyResponseStream.Length); } [Fact] public async Task RequestBodyClientErrorBeforeResponseError_Returns400() { TestEventListener.Collect(); TestLogger.Collect(); var httpContext = new DefaultHttpContext(); httpContext.Request.Method = "POST"; httpContext.Request.Host = new HostString("example.com:3456"); httpContext.Request.Body = new ThrowStream(throwOnFirstRead: true); httpContext.Request.ContentLength = 1; var proxyResponseStream = new MemoryStream(); httpContext.Response.Body = proxyResponseStream; var destinationPrefix = "https://localhost:123/"; var sut = CreateProxy(); var client = MockHttpHandler.CreateClient( async (HttpRequestMessage request, CancellationToken cancellationToken) => { // Should throw. await request.Content.CopyToWithCancellationAsync(Stream.Null); return new HttpResponseMessage(); }); var proxyError = await sut.SendAsync(httpContext, destinationPrefix, client); AssertErrorInfoAndStages(ForwarderError.RequestBodyClient, StatusCodes.Status400BadRequest, proxyError, httpContext, destinationPrefix, ForwarderStage.RequestContentTransferStart); Assert.Equal(0, proxyResponseStream.Length); } [Theory] [InlineData(StatusCodes.Status413PayloadTooLarge)] public async Task NonGenericRequestBodyClientErrorCode_ReturnsNonGenericClientErrorCode(int statusCode) { TestEventListener.Collect(); TestLogger.Collect(); var httpContext = new DefaultHttpContext(); httpContext.Request.Method = "POST"; httpContext.Request.Host = new HostString("example.com:3456"); httpContext.Request.ContentLength = 1; httpContext.Request.Body = new ThrowBadHttpRequestExceptionStream(statusCode); var destinationPrefix = "https://localhost/"; var sut = CreateProxy(); var client = MockHttpHandler.CreateClient( async (request, _) => { Assert.NotNull(request.Content); await request.Content.CopyToWithCancellationAsync(Stream.Null); return new HttpResponseMessage(HttpStatusCode.OK) { Content = new ByteArrayContent(Array.Empty()) }; }); var proxyError = await sut.SendAsync(httpContext, destinationPrefix, client); AssertErrorInfoAndStages(ForwarderError.RequestBodyClient, statusCode, proxyError, httpContext, destinationPrefix, ForwarderStage.RequestContentTransferStart); } [Fact] public async Task RequestBodyDestinationErrorBeforeResponseError_Returns502() { TestEventListener.Collect(); TestLogger.Collect(); var httpContext = new DefaultHttpContext(); httpContext.Request.Method = "POST"; httpContext.Request.Host = new HostString("example.com:3456"); httpContext.Request.Body = new MemoryStream(new byte[1]); httpContext.Request.ContentLength = 1; var proxyResponseStream = new MemoryStream(); httpContext.Response.Body = proxyResponseStream; var destinationPrefix = "https://localhost:123/"; var sut = CreateProxy(); var client = MockHttpHandler.CreateClient( async (HttpRequestMessage request, CancellationToken cancellationToken) => { // Doesn't throw for destination errors await request.Content.CopyToWithCancellationAsync(new ThrowStream()); throw new HttpRequestException(); }); var proxyError = await sut.SendAsync(httpContext, destinationPrefix, client); AssertErrorInfoAndStages(ForwarderError.RequestBodyDestination, StatusCodes.Status502BadGateway, proxyError, httpContext, destinationPrefix, ForwarderStage.RequestContentTransferStart); Assert.Equal(0, proxyResponseStream.Length); } [Fact] public async Task RequestBodyCanceledBeforeResponseError_Returns502() { TestEventListener.Collect(); TestLogger.Collect(); var httpContext = new DefaultHttpContext(); httpContext.Request.Method = "POST"; httpContext.Request.Host = new HostString("example.com:3456"); httpContext.Request.Body = new MemoryStream(new byte[1]); httpContext.Request.ContentLength = 1; httpContext.RequestAborted = new CancellationToken(canceled: true); var proxyResponseStream = new MemoryStream(); httpContext.Response.Body = proxyResponseStream; var destinationPrefix = "https://localhost:123/"; var sut = CreateProxy(); var client = MockHttpHandler.CreateClient( async (HttpRequestMessage request, CancellationToken cancellationToken) => { // should throw try { await request.Content.CopyToWithCancellationAsync(new MemoryStream()); } catch (OperationCanceledException) { } throw new HttpRequestException(); }); var proxyError = await sut.SendAsync(httpContext, destinationPrefix, client); AssertErrorInfoAndStages(ForwarderError.RequestBodyCanceled, StatusCodes.Status400BadRequest, proxyError, httpContext, destinationPrefix); Assert.Equal(0, proxyResponseStream.Length); } [Fact] public async Task ResponseBodySuppressedByTransform_ReturnsStatusCodeAndHeaders() { var events = TestEventListener.Collect(); var httpContext = new DefaultHttpContext(); httpContext.Request.Method = "GET"; httpContext.Request.Host = new HostString("example.com:3456"); var proxyResponseStream = new MemoryStream(); httpContext.Response.Body = proxyResponseStream; var destinationPrefix = "https://localhost:123/"; var sut = CreateProxy(); var client = MockHttpHandler.CreateClient( (HttpRequestMessage request, CancellationToken cancellationToken) => { var message = new HttpResponseMessage() { StatusCode = HttpStatusCode.UnprocessableEntity, Content = new StreamContent(new ThrowStream(throwOnFirstRead: true)) }; message.Headers.AcceptRanges.Add("bytes"); return Task.FromResult(message); }); var proxyError = await sut.SendAsync(httpContext, destinationPrefix, client, ForwarderRequestConfig.Empty, new DelegateHttpTransforms() { OnResponse = (context, proxyResponse) => { Assert.Equal(HttpStatusCode.UnprocessableEntity, proxyResponse.StatusCode); Assert.Equal(StatusCodes.Status422UnprocessableEntity, context.Response.StatusCode); return new(false); } }); Assert.Equal(ForwarderError.None, proxyError); Assert.Equal(StatusCodes.Status422UnprocessableEntity, httpContext.Response.StatusCode); Assert.Equal(0, proxyResponseStream.Length); Assert.Equal("bytes", httpContext.Response.Headers[HeaderNames.AcceptRanges]); Assert.Null(httpContext.Features.Get()); AssertProxyStartStop(events, destinationPrefix, httpContext.Response.StatusCode); events.AssertContainProxyStages(hasRequestContent: false, hasResponseContent: false); } [Fact] public async Task ResponseBodyDestinationErrorFirstRead_Returns502() { TestEventListener.Collect(); TestLogger.Collect(); var httpContext = new DefaultHttpContext(); httpContext.Request.Method = "GET"; httpContext.Request.Host = new HostString("example.com:3456"); var proxyResponseStream = new MemoryStream(); httpContext.Response.Body = proxyResponseStream; var destinationPrefix = "https://localhost:123/"; var sut = CreateProxy(); var client = MockHttpHandler.CreateClient( (HttpRequestMessage request, CancellationToken cancellationToken) => { var message = new HttpResponseMessage() { Content = new StreamContent(new ThrowStream(throwOnFirstRead: true)) }; message.Headers.AcceptRanges.Add("bytes"); return Task.FromResult(message); }); var proxyError = await sut.SendAsync(httpContext, destinationPrefix, client); AssertErrorInfoAndStages(ForwarderError.ResponseBodyDestination, StatusCodes.Status502BadGateway, proxyError, httpContext, destinationPrefix, ForwarderStage.SendAsyncStop, ForwarderStage.ResponseContentTransferStart); Assert.Equal(0, proxyResponseStream.Length); Assert.Empty(httpContext.Response.Headers); } [Fact] public async Task ResponseBodyDestinationErrorSecondRead_Aborted() { var events = TestEventListener.Collect(); TestLogger.Collect(); var httpContext = new DefaultHttpContext(); httpContext.Request.Method = "GET"; httpContext.Request.Host = new HostString("example.com:3456"); var responseBody = new TestResponseBody(); httpContext.Features.Set(responseBody); httpContext.Features.Set(responseBody); httpContext.Features.Set(responseBody); var destinationPrefix = "https://localhost:123/"; var sut = CreateProxy(); var client = MockHttpHandler.CreateClient( (HttpRequestMessage request, CancellationToken cancellationToken) => { var message = new HttpResponseMessage() { Content = new StreamContent(new ThrowStream(throwOnFirstRead: false)) }; message.Headers.AcceptRanges.Add("bytes"); return Task.FromResult(message); }); var proxyError = await sut.SendAsync(httpContext, destinationPrefix, client); AssertErrorInfo(ForwarderError.ResponseBodyDestination, StatusCodes.Status200OK, proxyError, httpContext, destinationPrefix); Assert.Equal(1, responseBody.InnerStream.Length); Assert.True(responseBody.Aborted); Assert.Equal("bytes", httpContext.Response.Headers[HeaderNames.AcceptRanges]); events.AssertContainProxyStages(hasRequestContent: false); } [Fact] public async Task ResponseBodyClientError_Aborted() { var events = TestEventListener.Collect(); TestLogger.Collect(); var httpContext = new DefaultHttpContext(); httpContext.Request.Method = "GET"; httpContext.Request.Host = new HostString("example.com:3456"); var responseBody = new TestResponseBody(new ThrowStream()); httpContext.Features.Set(responseBody); httpContext.Features.Set(responseBody); httpContext.Features.Set(responseBody); var destinationPrefix = "https://localhost:123/"; var sut = CreateProxy(); var client = MockHttpHandler.CreateClient( (HttpRequestMessage request, CancellationToken cancellationToken) => { var message = new HttpResponseMessage() { Content = new StreamContent(new MemoryStream(new byte[1])) }; message.Headers.AcceptRanges.Add("bytes"); return Task.FromResult(message); }); var proxyError = await sut.SendAsync(httpContext, destinationPrefix, client); AssertErrorInfo(ForwarderError.ResponseBodyClient, StatusCodes.Status200OK, proxyError, httpContext, destinationPrefix); Assert.True(responseBody.Aborted); Assert.Equal("bytes", httpContext.Response.Headers[HeaderNames.AcceptRanges]); events.AssertContainProxyStages(hasRequestContent: false); } [Fact] public async Task ResponseBodyCancelled_502() { var events = TestEventListener.Collect(); TestLogger.Collect(); var httpContext = new DefaultHttpContext(); httpContext.Request.Method = "GET"; httpContext.Request.Host = new HostString("example.com:3456"); var responseBody = new TestResponseBody(); httpContext.Features.Set(responseBody); httpContext.Features.Set(responseBody); httpContext.Features.Set(responseBody); httpContext.RequestAborted = new CancellationToken(canceled: true); var destinationPrefix = "https://localhost:123/"; var sut = CreateProxy(); var client = MockHttpHandler.CreateClient( (HttpRequestMessage request, CancellationToken cancellationToken) => { var message = new HttpResponseMessage() { Content = new StreamContent(new MemoryStream(new byte[1])) }; message.Headers.AcceptRanges.Add("bytes"); return Task.FromResult(message); }); var proxyError = await sut.SendAsync(httpContext, destinationPrefix, client); AssertErrorInfo(ForwarderError.ResponseBodyCanceled, StatusCodes.Status502BadGateway, proxyError, httpContext, destinationPrefix); Assert.False(responseBody.Aborted); Assert.Empty(httpContext.Response.Headers); events.AssertContainProxyStages(hasRequestContent: false); } [Fact] public async Task ResponseBodyCancelledAfterStart_Aborted() { var events = TestEventListener.Collect(); TestLogger.Collect(); var httpContext = new DefaultHttpContext(); httpContext.Request.Method = "GET"; httpContext.Request.Host = new HostString("example.com:3456"); var responseBody = new TestResponseBody(); httpContext.Features.Set(responseBody); httpContext.Features.Set(responseBody); httpContext.Features.Set(responseBody); var destinationPrefix = "https://localhost:123/"; var cts = new CancellationTokenSource(); var sut = CreateProxy(); var client = MockHttpHandler.CreateClient( (HttpRequestMessage request, CancellationToken cancellationToken) => { var message = new HttpResponseMessage() { Content = new StreamContent(new CallbackReadStream((_, _) => { responseBody.HasStarted = true; cts.Cancel(); cts.Token.ThrowIfCancellationRequested(); throw new NotImplementedException(); })) }; message.Headers.AcceptRanges.Add("bytes"); return Task.FromResult(message); }); var proxyError = await sut.SendAsync(httpContext, destinationPrefix, client, ForwarderRequestConfig.Empty, HttpTransformer.Empty, cts.Token); AssertErrorInfo(ForwarderError.ResponseBodyCanceled, StatusCodes.Status200OK, proxyError, httpContext, destinationPrefix); Assert.True(responseBody.Aborted); Assert.Equal("bytes", httpContext.Response.Headers[HeaderNames.AcceptRanges]); events.AssertContainProxyStages(hasRequestContent: false); } [Theory] [InlineData(false)] [InlineData(true)] [InlineData(null)] public async Task ResponseBodyDisableBuffering_Success(bool? enableBuffering) { var httpContext = new DefaultHttpContext(); httpContext.Request.Method = "GET"; var responseBody = new TestResponseBody(); httpContext.Features.Set(responseBody); httpContext.Features.Set(responseBody); httpContext.Features.Set(responseBody); var destinationPrefix = "https://localhost:123/"; var sut = CreateProxy(); var client = MockHttpHandler.CreateClient( (HttpRequestMessage request, CancellationToken cancellationToken) => { var message = new HttpResponseMessage() { Content = new StreamContent(new MemoryStream(new byte[1])) }; message.Headers.AcceptRanges.Add("bytes"); return Task.FromResult(message); }); var requestConfig = ForwarderRequestConfig.Empty with { AllowResponseBuffering = enableBuffering }; var proxyError = await sut.SendAsync(httpContext, destinationPrefix, client, requestConfig, HttpTransformer.Default); Assert.Equal(StatusCodes.Status200OK, httpContext.Response.StatusCode); Assert.Equal(enableBuffering != true, responseBody.BufferingDisabled); } [Fact] public async Task RequestBodyCanceledAfterResponse_Reported() { var events = TestEventListener.Collect(); TestLogger.Collect(); var waitTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var httpContext = new DefaultHttpContext(); httpContext.Request.Method = "POST"; httpContext.Request.Host = new HostString("example.com:3456"); httpContext.Request.Body = new StallStream(waitTcs.Task); httpContext.Request.ContentLength = 1; var proxyResponseStream = new MemoryStream(); httpContext.Response.Body = proxyResponseStream; using var longTokenSource = new CancellationTokenSource(); httpContext.RequestAborted = longTokenSource.Token; var destinationPrefix = "https://localhost:123/"; var sut = CreateProxy(); var client = MockHttpHandler.CreateClient( (HttpRequestMessage request, CancellationToken cancellationToken) => { // Background copy _ = request.Content.CopyToWithCancellationAsync(new MemoryStream()); // Make sure the request isn't canceled until the response finishes copying. return Task.FromResult(new HttpResponseMessage() { Content = new StreamContent(new OnCompletedReadStream(() => { longTokenSource.Cancel(); waitTcs.SetResult(0); })) }); }); var proxyError = await sut.SendAsync(httpContext, destinationPrefix, client); AssertErrorInfo(ForwarderError.RequestBodyCanceled, StatusCodes.Status200OK, proxyError, httpContext, destinationPrefix); Assert.Equal(0, proxyResponseStream.Length); events.AssertContainProxyStages(); } [Fact] public async Task RequestBodyClientErrorAfterResponse_Reported() { var events = TestEventListener.Collect(); TestLogger.Collect(); var waitTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var httpContext = new DefaultHttpContext(); httpContext.Request.Method = "POST"; httpContext.Request.Host = new HostString("example.com:3456"); httpContext.Request.Body = new StallStream(waitTcs.Task); httpContext.Request.ContentLength = 1; var proxyResponseStream = new MemoryStream(); httpContext.Response.Body = proxyResponseStream; var destinationPrefix = "https://localhost:123/"; var sut = CreateProxy(); var client = MockHttpHandler.CreateClient( (HttpRequestMessage request, CancellationToken cancellationToken) => { // Background copy _ = request.Content.CopyToWithCancellationAsync(new MemoryStream()); // Make sure the request isn't canceled until the response finishes copying. return Task.FromResult(new HttpResponseMessage() { Content = new StreamContent(new OnCompletedReadStream(() => waitTcs.SetResult(0))) }); }); var proxyError = await sut.SendAsync(httpContext, destinationPrefix, client); AssertErrorInfo(ForwarderError.RequestBodyClient, StatusCodes.Status200OK, proxyError, httpContext, destinationPrefix); Assert.Equal(0, proxyResponseStream.Length); events.AssertContainProxyStages(); } [Fact] public async Task RequestBodyDestinationErrorAfterResponse_Reported() { var events = TestEventListener.Collect(); TestLogger.Collect(); var waitTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var httpContext = new DefaultHttpContext(); httpContext.Request.Method = "POST"; httpContext.Request.Host = new HostString("example.com:3456"); httpContext.Request.Body = new MemoryStream(new byte[1]); httpContext.Request.ContentLength = 1; var proxyResponseStream = new MemoryStream(); httpContext.Response.Body = proxyResponseStream; var destinationPrefix = "https://localhost:123/"; var sut = CreateProxy(); var client = MockHttpHandler.CreateClient( (HttpRequestMessage request, CancellationToken cancellationToken) => { // Background copy _ = request.Content.CopyToWithCancellationAsync(new StallStream(waitTcs.Task)); // Make sure the request isn't canceled until the response finishes copying. return Task.FromResult(new HttpResponseMessage() { Content = new StreamContent(new OnCompletedReadStream(() => waitTcs.SetResult(0))) }); }); var proxyError = await sut.SendAsync(httpContext, destinationPrefix, client); AssertErrorInfo(ForwarderError.RequestBodyDestination, StatusCodes.Status200OK, proxyError, httpContext, destinationPrefix); Assert.Equal(0, proxyResponseStream.Length); events.AssertContainProxyStages(); } [Fact] public async Task UpgradableRequest_RequestBodyCopyError_CancelsResponseBody() { var events = TestEventListener.Collect(); TestLogger.Collect(); var httpContext = new DefaultHttpContext(); httpContext.Request.Method = "GET"; httpContext.Request.Scheme = "http"; httpContext.Request.Host = new HostString("example.com:3456"); httpContext.Request.Headers.Upgrade = "WebSocket"; var downstreamStream = new DuplexStream( readStream: new ThrowStream(), writeStream: new MemoryStream()); DuplexStream upstreamStream = null; var upgradeFeatureMock = new Mock(); upgradeFeatureMock.SetupGet(u => u.IsUpgradableRequest).Returns(true); upgradeFeatureMock.Setup(u => u.UpgradeAsync()).ReturnsAsync(downstreamStream); httpContext.Features.Set(upgradeFeatureMock.Object); var destinationPrefix = "https://localhost/"; var sut = CreateProxy(); var client = MockHttpHandler.CreateClient( (HttpRequestMessage request, CancellationToken cancellationToken) => { Assert.Equal(new Version(1, 1), request.Version); Assert.Equal(HttpMethod.Get, request.Method); Assert.Null(request.Content); var response = new HttpResponseMessage(HttpStatusCode.SwitchingProtocols); upstreamStream = new DuplexStream( readStream: new StallStream(ct => { var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); ct.Register(() => tcs.SetResult(0)); return tcs.Task.DefaultTimeout(); }), writeStream: new MemoryStream()); response.Content = new RawStreamContent(upstreamStream); return Task.FromResult(response); }); var proxyError = await sut.SendAsync(httpContext, destinationPrefix, client, new ForwarderRequestConfig() { Version = HttpVersion.Version11, }); AssertErrorInfo(ForwarderError.UpgradeRequestClient, StatusCodes.Status101SwitchingProtocols, proxyError, httpContext, destinationPrefix); events.AssertContainProxyStages(upgrade: true); } [Fact] public async Task UpgradableRequest_ResponseBodyCopyError_CancelsRequestBody() { var events = TestEventListener.Collect(); TestLogger.Collect(); var httpContext = new DefaultHttpContext(); httpContext.Request.Method = "GET"; httpContext.Request.Scheme = "http"; httpContext.Request.Host = new HostString("example.com:3456"); httpContext.Request.Headers.Upgrade = "WebSocket"; var downstreamStream = new DuplexStream( readStream: new StallStream(ct => { var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); ct.Register(() => tcs.SetResult(0)); return tcs.Task.DefaultTimeout(); }), writeStream: new MemoryStream()); DuplexStream upstreamStream = null; var upgradeFeatureMock = new Mock(); upgradeFeatureMock.SetupGet(u => u.IsUpgradableRequest).Returns(true); upgradeFeatureMock.Setup(u => u.UpgradeAsync()).ReturnsAsync(downstreamStream); httpContext.Features.Set(upgradeFeatureMock.Object); var destinationPrefix = "https://localhost/"; var sut = CreateProxy(); var client = MockHttpHandler.CreateClient( (HttpRequestMessage request, CancellationToken cancellationToken) => { Assert.Equal(new Version(1, 1), request.Version); Assert.Equal(HttpMethod.Get, request.Method); Assert.Null(request.Content); var response = new HttpResponseMessage(HttpStatusCode.SwitchingProtocols); upstreamStream = new DuplexStream( readStream: new ThrowStream(), writeStream: new MemoryStream()); response.Content = new RawStreamContent(upstreamStream); return Task.FromResult(response); }); var proxyError = await sut.SendAsync(httpContext, destinationPrefix, client, new ForwarderRequestConfig() { Version = HttpVersion.Version11, }); AssertErrorInfo(ForwarderError.UpgradeResponseDestination, StatusCodes.Status101SwitchingProtocols, proxyError, httpContext, destinationPrefix); events.AssertContainProxyStages(upgrade: true); } [Fact] public async Task WithHttpClient_Fails() { var httpClient = new HttpClient(); var httpContext = new DefaultHttpContext(); var destinationPrefix = ""; var transforms = HttpTransformer.Default; var requestOptions = ForwarderRequestConfig.Empty; var proxy = CreateProxy(); await Assert.ThrowsAsync(async () => await proxy.SendAsync(httpContext, destinationPrefix, httpClient, requestOptions, transforms)); } [Theory] [InlineData("HTTP/1.1", "1.1")] [InlineData("HTTP/2", "2.0")] public async Task Expect100ContinueWithFailedResponse_ReturnResponse(string fromProtocol, string toProtocol) { var httpContext = new DefaultHttpContext(); httpContext.Request.Method = "POST"; httpContext.Request.Protocol = fromProtocol; httpContext.Request.Headers[HeaderNames.Expect] = "100-continue"; var content = Encoding.UTF8.GetBytes(new string('a', 1024 * 1024 * 10)); httpContext.Request.Headers[HeaderNames.ContentLength] = content.Length.ToString(); using var contentStream = new MemoryStream(content); httpContext.Request.Body = contentStream; var destinationPrefix = "https://localhost:123/a/b/"; var sut = CreateProxy(); var client = MockHttpHandler.CreateClient( async (HttpRequestMessage request, CancellationToken cancellationToken) => { await Task.Yield(); Assert.NotNull(request.Content); return new HttpResponseMessage(HttpStatusCode.Conflict); }); await sut.SendAsync(httpContext, destinationPrefix, client, new ForwarderRequestConfig { Version = Version.Parse(toProtocol) }); Assert.Equal(0, contentStream.Position); Assert.Equal((int)HttpStatusCode.Conflict, httpContext.Response.StatusCode); } [Theory] [InlineData("1.1", false, "Connection: upgrade; Upgrade: test123", null, "Connection; Upgrade")] [InlineData("1.1", false, "Connection: keep-alive; Keep-Alive: timeout=100", null, "Connection; Keep-Alive")] [InlineData("1.1", true, "Connection: upgrade; Upgrade: websocket", "Connection: upgrade; Upgrade: websocket", null)] [InlineData("1.1", true, "Connection: upgrade, keep-alive; Upgrade: websocket; Keep-Alive: timeout=100", "Connection: upgrade; Upgrade: websocket", "Keep-Alive")] [InlineData("1.1", true, "Connection: keep-alive; Upgrade: websocket; Keep-Alive: timeout=100", null, "Connection; Upgrade; Keep-Alive")] [InlineData("1.1", true, "Foo: bar; Upgrade: websocket", "Foo: bar", "Upgrade")] [InlineData("1.1", true, "Foo: bar; Connection: upgrade", "Foo: bar", "Connection")] [InlineData("1.1", false, "Foo: bar", "Foo: bar", null)] [InlineData("2.0", false, "Connection: keep-alive; Keep-Alive: timeout=100", null, "Connection; Keep-Alive")] [InlineData("2.0", false, "Connection: upgrade; Upgrade: websocket", null, "Connection; Upgrade")] [InlineData("2.0", false, "Foo: bar", "Foo: bar", null)] public async Task ResponseToNonUpgradeableRequest_RemoveAllConnectionHeaders(string protocol, bool upgrade, string responseHeadersList, string preservedHeadersList, string removedHeadersList) { var events = TestEventListener.Collect(); var responseHeaders = responseHeadersList.Split("; "); var preservedHeaders = preservedHeadersList?.Split("; ") ?? Enumerable.Empty(); var removedHeaders = removedHeadersList?.Split("; ") ?? Enumerable.Empty(); var httpContext = new DefaultHttpContext(); httpContext.Request.Method = "GET"; if (upgrade) { var upgradeFeature = new Mock(); upgradeFeature.SetupGet(f => f.IsUpgradableRequest).Returns(true); upgradeFeature.Setup(f => f.UpgradeAsync()).ReturnsAsync(httpContext.Request.Body); httpContext.Features.Set(upgradeFeature.Object); httpContext.Request.Headers[HeaderNames.Upgrade] = "WebSocket"; } var destinationPrefix = "https://localhost:123/a/b/"; var sut = CreateProxy(); var client = MockHttpHandler.CreateClient( async (HttpRequestMessage request, CancellationToken cancellationToken) => { await Task.Yield(); var response = new HttpResponseMessage(upgrade ? HttpStatusCode.SwitchingProtocols : HttpStatusCode.OK); response.Content = new StringContent("Foo"); foreach (var header in responseHeaders) { (var headerName, var headerValues) = GetHeaderNameAndValues(header); response.Headers.TryAddWithoutValidation(headerName, headerValues); } return response; }); await sut.SendAsync(httpContext, destinationPrefix, client, new ForwarderRequestConfig { Version = Version.Parse(protocol) }); Assert.Equal(upgrade ? (int)HttpStatusCode.SwitchingProtocols : (int)HttpStatusCode.OK, httpContext.Response.StatusCode); foreach (var preservedHeader in preservedHeaders) { (var headerName, var expectedValues) = GetHeaderNameAndValues(preservedHeader); var actualValues = httpContext.Response.Headers[headerName]; Assert.Equal(expectedValues, actualValues); } foreach (var removedHeaderName in removedHeaders) { Assert.False(httpContext.Response.Headers.TryGetValue(removedHeaderName, out _)); } AssertProxyStartStop(events, destinationPrefix, httpContext.Response.StatusCode); events.AssertContainProxyStages(hasRequestContent: upgrade, upgrade); } [Theory] [InlineData("1.1", false, "Connection: upgrade; Upgrade: test123", null, "Connection; Upgrade")] [InlineData("1.1", false, "Connection: keep-alive; Keep-Alive: timeout=100", null, "Connection; Keep-Alive")] [InlineData("1.1", true, "Connection: upgrade; Upgrade: websocket", "Connection: upgrade; Upgrade: websocket", null)] [InlineData("1.1", true, "Connection: upgrade; Upgrade: SPDY/", "Connection: upgrade; Upgrade: SPDY/", null)] [InlineData("1.1", true, "Connection: upgrade, keep-alive; Upgrade: websocket; Keep-Alive: timeout=100", "Connection: upgrade; Upgrade: websocket", "Keep-Alive")] [InlineData("1.1", false, "Foo: bar", "Foo: bar", null)] [InlineData("2.0", false, "Connection: keep-alive; Keep-Alive: timeout=100", null, "Connection; Keep-Alive")] [InlineData("2.0", false, "Connection: upgrade; Upgrade: websocket", null, "Connection; Upgrade")] [InlineData("2.0", false, "Foo: bar", "Foo: bar", null)] public async Task NonUpgradableRequest_RemoveAllConnectionHeaders(string protocol, bool upgrade, string addHeadersList, string preservedHeadersList, string removedHeadersList) { var addHeaders = addHeadersList.Split("; ").Select(GetHeaderNameAndValues); var preservedHeaders = (preservedHeadersList?.Split("; ") ?? Enumerable.Empty()).Select(GetHeaderNameAndValues); var removedHeaders = removedHeadersList?.Split("; ") ?? Enumerable.Empty(); var httpContext = new DefaultHttpContext(); httpContext.Request.Method = "GET"; if (upgrade) { var upgradeFeature = new Mock(); upgradeFeature.SetupGet(f => f.IsUpgradableRequest).Returns(true); httpContext.Features.Set(upgradeFeature.Object); } foreach (var (name, value) in addHeaders) { httpContext.Request.Headers[name] = value; } var destinationPrefix = "https://localhost:123/a/b/"; var sut = CreateProxy(); var client = MockHttpHandler.CreateClient( async (HttpRequestMessage request, CancellationToken cancellationToken) => { await Task.Yield(); foreach (var (name, value) in preservedHeaders) { var actualValues = string.Join(", ", request.Headers.GetValues(name)); Assert.Equal(value, actualValues); } foreach (var removedHeaderName in removedHeaders) { Assert.False(request.Headers.TryGetValues(removedHeaderName, out _)); } var response = new HttpResponseMessage(HttpStatusCode.OK); return response; }); await sut.SendAsync(httpContext, destinationPrefix, client, new ForwarderRequestConfig { Version = Version.Parse(protocol) }); Assert.Equal((int)HttpStatusCode.OK, httpContext.Response.StatusCode); } [Theory] [MemberData(nameof(GetProhibitedHeaders))] [MemberData(nameof(GetHeadersWithNewLines))] public async Task Request_RemoveProhibitedHeaders(string protocol, string prohibitedHeadersList) { const string PreservedHeaderName = "Foo"; const string PreservedHeaderValue = "bar"; var prohibitedHeaders = prohibitedHeadersList.Split("; ").Select(GetHeaderNameAndValues); var httpContext = new DefaultHttpContext(); httpContext.Request.Method = "GET"; foreach (var (name, value) in prohibitedHeaders) { httpContext.Request.Headers[name] = value; } httpContext.Request.Headers[PreservedHeaderName] = PreservedHeaderValue; var destinationPrefix = "https://localhost:123/a/b/"; var sut = CreateProxy(); var client = MockHttpHandler.CreateClient( async (HttpRequestMessage request, CancellationToken cancellationToken) => { await Task.Yield(); Assert.Equal(PreservedHeaderValue, string.Join(", ", request.Headers.GetValues(PreservedHeaderName))); foreach (var (name, _) in prohibitedHeaders) { Assert.False(request.Headers.TryGetValues(name, out _)); } var response = new HttpResponseMessage(HttpStatusCode.OK); return response; }); await sut.SendAsync(httpContext, destinationPrefix, client, new ForwarderRequestConfig { Version = Version.Parse(protocol) }); Assert.Equal((int)HttpStatusCode.OK, httpContext.Response.StatusCode); } [Theory] [MemberData(nameof(ResponseMultiHeadersData))] public async Task ResponseWithMultiHeaders(string version, string headerName, string[] headers) { var httpContext = new DefaultHttpContext(); httpContext.Request.Method = "GET"; var destinationPrefix = "https://localhost:123/a/b/"; var sut = CreateProxy(); var client = MockHttpHandler.CreateClient( async (HttpRequestMessage request, CancellationToken cancellationToken) => { await Task.Yield(); var response = new HttpResponseMessage(HttpStatusCode.OK); if (!response.Headers.TryAddWithoutValidation(headerName, headers)) { Assert.True(response.Content.Headers.TryAddWithoutValidation(headerName, headers)); } return response; }); await sut.SendAsync(httpContext, destinationPrefix, client, new ForwarderRequestConfig { Version = new Version(version) }); Assert.Equal((int)HttpStatusCode.OK, httpContext.Response.StatusCode); Assert.True(httpContext.Response.Headers.TryGetValue(headerName, out var sentHeaders)); Assert.True(sentHeaders.Equals(headers)); } [Theory] [MemberData(nameof(GetProhibitedHeaders))] public async Task Response_RemoveProhibitedHeaders(string protocol, string prohibitedHeadersList) { const string PreservedHeaderName = "Foo"; const string PreservedHeaderValue = "bar"; var prohibitedHeaders = prohibitedHeadersList.Split("; ").Select(GetHeaderNameAndValues); var httpContext = new DefaultHttpContext(); httpContext.Request.Method = "GET"; var destinationPrefix = "https://localhost:123/a/b/"; var sut = CreateProxy(); var client = MockHttpHandler.CreateClient( async (HttpRequestMessage request, CancellationToken cancellationToken) => { await Task.Yield(); var response = new HttpResponseMessage(HttpStatusCode.OK); foreach (var (name, value) in prohibitedHeaders) { response.Headers.TryAddWithoutValidation(name, value); } response.Headers.TryAddWithoutValidation(PreservedHeaderName, PreservedHeaderValue); return response; }); await sut.SendAsync(httpContext, destinationPrefix, client, new ForwarderRequestConfig { Version = Version.Parse(protocol) }); Assert.Equal((int)HttpStatusCode.OK, httpContext.Response.StatusCode); Assert.Equal(PreservedHeaderValue, string.Join(", ", httpContext.Response.Headers[PreservedHeaderName])); foreach (var (name, _) in prohibitedHeaders) { Assert.False(httpContext.Response.Headers.TryGetValue(name, out _)); } } [Theory] [InlineData(true)] [InlineData(false)] public async Task RequestFailure_ResponseTransformsAreCalled(bool failureInRequestTransform) { var events = TestEventListener.Collect(); TestLogger.Collect(); var httpContext = new DefaultHttpContext(); httpContext.Request.Method = "GET"; httpContext.Request.Host = new HostString("example.com:3456"); var destinationPrefix = "https://localhost:123/"; var sut = CreateProxy(); var client = MockHttpHandler.CreateClient( (HttpRequestMessage request, CancellationToken cancellationToken) => { throw new Exception(); }); var responseTransformWithNullResponseCalled = false; var transformer = new DelegateHttpTransforms { OnRequest = (context, request, prefix) => { if (failureInRequestTransform) { throw new Exception(); } return Task.CompletedTask; }, OnResponse = (context, response) => { if (response is null) { responseTransformWithNullResponseCalled = true; } return new ValueTask(true); } }; var proxyError = await sut.SendAsync(httpContext, destinationPrefix, client, ForwarderRequestConfig.Empty, transformer); Assert.True(responseTransformWithNullResponseCalled); var expectedError = failureInRequestTransform ? ForwarderError.RequestCreation : ForwarderError.Request; AssertErrorInfo(expectedError, StatusCodes.Status502BadGateway, proxyError, httpContext, destinationPrefix); events.AssertContainProxyStages(failureInRequestTransform ? [] : [ForwarderStage.SendAsyncStart]); } [Theory] [InlineData(true)] [InlineData(false)] public async Task RequestFailure_CancellationExceptionInResponseTransformIsIgnored(bool throwOce) { TestEventListener.Collect(); TestLogger.Collect(); var httpContext = new DefaultHttpContext(); httpContext.Request.Method = "GET"; httpContext.Request.Host = new HostString("example.com:3456"); var destinationPrefix = "https://localhost:123/"; var sut = CreateProxy(); var client = MockHttpHandler.CreateClient( (HttpRequestMessage request, CancellationToken cancellationToken) => { throw new Exception(); }); var responseTransformWithNullResponseCalled = false; var transformer = new DelegateHttpTransforms { OnResponse = (context, response) => { if (response is null) { responseTransformWithNullResponseCalled = true; throw throwOce ? new OperationCanceledException("Foo") : new InvalidOperationException("Bar"); } return new ValueTask(true); } }; var proxyError = ForwarderError.None; Exception exceptionThrownBySendAsync = null; try { proxyError = await sut.SendAsync(httpContext, destinationPrefix, client, ForwarderRequestConfig.Empty, transformer); } catch (Exception ex) { exceptionThrownBySendAsync = ex; } Assert.True(responseTransformWithNullResponseCalled); if (throwOce) { Assert.Null(exceptionThrownBySendAsync); Assert.Equal(ForwarderError.Request, proxyError); } else { Assert.NotNull(exceptionThrownBySendAsync); } AssertErrorInfoAndStages(ForwarderError.Request, StatusCodes.Status502BadGateway, ForwarderError.Request, httpContext, destinationPrefix); } public enum CancellationScenario { RequestAborted, ActivityTimeout, ManualCancellationToken, } [Theory] [InlineData(CancellationScenario.RequestAborted)] [InlineData(CancellationScenario.ActivityTimeout)] [InlineData(CancellationScenario.ManualCancellationToken)] public async Task ForwarderCancellations_CancellationsAreVisibleInTransforms(CancellationScenario cancellationScenario) { var events = TestEventListener.Collect(); TestLogger.Collect(); using var requestAbortedCts = new CancellationTokenSource(); using var parameterCts = new CancellationTokenSource(); var httpContext = new DefaultHttpContext(); httpContext.Request.Method = "GET"; httpContext.Request.Host = new HostString("example.com:3456"); httpContext.RequestAborted = requestAbortedCts.Token; var destinationPrefix = "https://localhost:123/"; var sut = CreateProxy(); var client = MockHttpHandler.CreateClient( (HttpRequestMessage request, CancellationToken cancellationToken) => { throw new InvalidOperationException(); }); var requestConfig = new ForwarderRequestConfig(); if (cancellationScenario == CancellationScenario.ActivityTimeout) { requestConfig = requestConfig with { ActivityTimeout = TimeSpan.FromMilliseconds(42) }; } var ctWasAlreadyCancelled = false; var inTheTransformsTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var transformer = TransformBuilderTests.CreateTransformBuilder().CreateInternal(context => { context.AddRequestTransform(async context => { ctWasAlreadyCancelled = context.CancellationToken.IsCancellationRequested; inTheTransformsTcs.SetResult(); await Task.Delay(-1, context.CancellationToken); }); }); var proxyTask = sut.SendAsync(httpContext, destinationPrefix, client, requestConfig, transformer, parameterCts.Token); await inTheTransformsTcs.Task; if (cancellationScenario == CancellationScenario.RequestAborted) { requestAbortedCts.Cancel(); } else if (cancellationScenario == CancellationScenario.ManualCancellationToken) { parameterCts.Cancel(); } var proxyError = await proxyTask; if (cancellationScenario != CancellationScenario.ManualCancellationToken) { Assert.False(ctWasAlreadyCancelled); } var expectedError = cancellationScenario == CancellationScenario.ActivityTimeout ? ForwarderError.RequestTimedOut : ForwarderError.RequestCanceled; var expectedStatusCode = cancellationScenario switch { CancellationScenario.ActivityTimeout => StatusCodes.Status504GatewayTimeout, CancellationScenario.RequestAborted => StatusCodes.Status400BadRequest, CancellationScenario.ManualCancellationToken => StatusCodes.Status502BadGateway, _ => throw new NotImplementedException(cancellationScenario.ToString()), }; AssertErrorInfo(expectedError, expectedStatusCode, proxyError, httpContext, destinationPrefix); events.AssertContainProxyStages([]); } public static IEnumerable GetProhibitedHeaders() { var headers = new[] { "Connection: close", "Upgrade: test123", "Transfer-Encoding: deflate", "Keep-Alive: timeout=100", "Proxy-Connection: value", "Proxy-Authenticate: value", "Proxy-Authentication-Info: value", "Proxy-Authorization: value", "Proxy-Features: value", "Proxy-Instruction: value", "Security-Scheme: value", "ALPN: value", "Close: value", "TE: value", "HTTP2-Settings: value", "Upgrade-Insecure-Requests: value", "Alt-Svc: value", }; foreach (var header in headers) { yield return new object[] { "1.1", header }; yield return new object[] { "2.0", header }; } } public static IEnumerable GetHeadersWithNewLines() { var headers = new[] { "valid-name-1: \rfoo", "valid-name-2: bar\n", "valid-name-3: foo\r\nbar", "valid-name-4: foo\r\n bar", }; foreach (var header in headers) { yield return new object[] { "1.1", header }; yield return new object[] { "2.0", header }; } } private static TException AssertErrorInfoAndStages( ForwarderError expectedError, int expectedStatusCode, ForwarderError error, HttpContext context, string destinationPrefix, params ForwarderStage[] otherStages) where TException : Exception { TException exception = AssertErrorInfo(expectedError, expectedStatusCode, error, context, destinationPrefix); TestEventListener.Collect().AssertContainProxyStages([ForwarderStage.SendAsyncStart, .. otherStages]); return exception; } private static TException AssertErrorInfo( ForwarderError expectedError, int expectedStatusCode, ForwarderError error, HttpContext context, string destinationPrefix) where TException : Exception { Assert.Equal(expectedError, error); Assert.Equal(expectedStatusCode, context.Response.StatusCode); var errorFeature = context.Features.Get(); Assert.NotNull(errorFeature); Assert.Equal(error, errorFeature.Error); Assert.IsAssignableFrom(errorFeature.Exception); var errorIsRequestCancelled = error is ForwarderError.RequestCanceled or ForwarderError.RequestBodyCanceled or ForwarderError.ResponseBodyCanceled or ForwarderError.UpgradeRequestCanceled or ForwarderError.UpgradeResponseCanceled; var expectedId = errorIsRequestCancelled ? EventIds.ForwardingRequestCancelled : EventIds.ForwardingError; var log = Assert.Single(TestLogger.Collect(), l => l.EventId == expectedId); Assert.Equal(typeof(HttpForwarder).FullName, log.CategoryName); Assert.Contains(error.ToString(), log.Message); Assert.NotNull(log.Exception); AssertProxyStartFailedStop(TestEventListener.Collect(), destinationPrefix, context.Response.StatusCode, errorFeature.Error); return (TException)errorFeature.Exception; } private static void AssertProxyStartStop(List events, string destinationPrefix, int statusCode) { AssertProxyStartFailedStop(events, destinationPrefix, statusCode, error: null); } private static void AssertProxyStartFailedStop(List events, string destinationPrefix, int statusCode, ForwarderError? error) { var start = Assert.Single(events, e => e.EventName == "ForwarderStart"); var prefixActual = (string)Assert.Single(start.Payload); Assert.Equal(destinationPrefix, prefixActual); var stop = Assert.Single(events, e => e.EventName == "ForwarderStop"); var statusActual = (int)Assert.Single(stop.Payload); Assert.Equal(statusCode, statusActual); Assert.True(start.TimeStamp <= stop.TimeStamp); if (error is null) { Assert.DoesNotContain(events, e => e.EventName == "ForwarderFailed"); } else { var failed = Assert.Single(events, e => e.EventName == "ForwarderFailed"); var errorActual = (ForwarderError)Assert.Single(failed.Payload); Assert.Equal(error.Value, errorActual); Assert.True(start.TimeStamp <= failed.TimeStamp); Assert.True(failed.TimeStamp <= stop.TimeStamp); } } private static MemoryStream StringToStream(string text) { var stream = new MemoryStream(Encoding.UTF8.GetBytes(text)); return stream; } private static string StreamToString(Stream stream) { using var reader = new StreamReader(stream, Encoding.UTF8, leaveOpen: true); return reader.ReadToEnd(); } private static (string Name, string Values) GetHeaderNameAndValues(string fullHeader) { var headerNameEnd = fullHeader.IndexOf(": "); return (fullHeader[..headerNameEnd], fullHeader[(headerNameEnd + 2)..]); } private class DuplexStream : Stream { public DuplexStream(Stream readStream, Stream writeStream) { ArgumentNullException.ThrowIfNull(readStream); ArgumentNullException.ThrowIfNull(writeStream); ReadStream = readStream; WriteStream = writeStream; } public Stream ReadStream { get; } public Stream WriteStream { get; } public override bool CanRead => true; public override bool CanSeek => false; public override bool CanWrite => true; public override long Length => throw new NotImplementedException(); public override long Position { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } public override int Read(byte[] buffer, int offset, int count) { return ReadStream.Read(buffer, offset, count); } public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { return ReadStream.ReadAsync(buffer, offset, count, cancellationToken); } public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) { return ReadStream.ReadAsync(buffer, cancellationToken); } public override void Write(byte[] buffer, int offset, int count) { WriteStream.Write(buffer, offset, count); } public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { return WriteStream.WriteAsync(buffer, offset, count, cancellationToken); } public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) { return WriteStream.WriteAsync(buffer, cancellationToken); } public override void Flush() { } public override long Seek(long offset, SeekOrigin origin) { throw new NotImplementedException(); } public override void SetLength(long value) { throw new NotImplementedException(); } } /// /// Replacement for which just returns the raw stream, /// whereas wraps it in a read-only stream. /// We need to return the raw internal stream to test full duplex proxying. /// private class RawStreamContent : HttpContent { private readonly Stream _stream; public RawStreamContent(Stream stream) { ArgumentNullException.ThrowIfNull(stream); _stream = stream; } protected override Task CreateContentReadStreamAsync() { return Task.FromResult(_stream); } protected override Task SerializeToStreamAsync(Stream stream, TransportContext context) { throw new NotImplementedException(); } protected override bool TryComputeLength(out long length) { throw new NotImplementedException(); } } private class ThrowStream : DelegatingStream { private bool _firstRead = true; public ThrowStream(bool throwOnFirstRead = true) : base(Stream.Null) { ThrowOnFirstRead = throwOnFirstRead; } public bool ThrowOnFirstRead { get; } public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) { if (buffer.Length == 0) { return new ValueTask(0); } cancellationToken.ThrowIfCancellationRequested(); if (_firstRead && !ThrowOnFirstRead) { _firstRead = false; return new ValueTask(1); } throw new IOException("Fake connection issue"); } public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); throw new IOException("Fake connection issue"); } } private class StallStream : DelegatingStream { public StallStream(Task until) : this(_ => until) { } public StallStream(Func onStallAction) : base(Stream.Null) { OnStallAction = onStallAction; } public Func OnStallAction { get; } public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) { await OnStallAction(cancellationToken); cancellationToken.ThrowIfCancellationRequested(); throw new IOException(); } public override async ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) { await OnStallAction(cancellationToken); cancellationToken.ThrowIfCancellationRequested(); throw new IOException(); } } private class CallbackReadStream : DelegatingStream { public CallbackReadStream(Func, CancellationToken, ValueTask> onReadAsync) : base(Stream.Null) { OnReadAsync = onReadAsync; } public Func, CancellationToken, ValueTask> OnReadAsync { get; } public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) { return OnReadAsync(buffer, cancellationToken); } public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) { throw new IOException(); } } private class TestResponseBody : DelegatingStream, IHttpResponseBodyFeature, IHttpResponseFeature, IHttpRequestLifetimeFeature { public TestResponseBody() : this(new MemoryStream()) { } public TestResponseBody(Stream innerStream) : base(innerStream) { InnerStream = innerStream; } public Stream InnerStream { get; } public bool Aborted { get; private set; } public Stream Stream => this; public PipeWriter Writer => throw new NotImplementedException(); public bool BufferingDisabled { get; set; } public int StatusCode { get; set; } = 200; public string ReasonPhrase { get; set; } public IHeaderDictionary Headers { get; set; } = new HeaderDictionary(); public Stream Body { get => this; set => throw new NotImplementedException(); } public bool HasStarted { get; set; } public CancellationToken RequestAborted { get; set; } public void Abort() { Aborted = true; } public Task CompleteAsync() { throw new NotImplementedException(); } public void DisableBuffering() { BufferingDisabled = true; } public void OnCompleted(Func callback, object state) { throw new NotImplementedException(); } public void OnStarting(Func callback, object state) { throw new NotImplementedException(); } public Task SendFileAsync(string path, long offset, long? count, CancellationToken cancellationToken = default) { throw new NotImplementedException(); } public Task StartAsync(CancellationToken cancellationToken = default) { OnStart(); return Task.CompletedTask; } public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) { OnStart(); return base.WriteAsync(buffer, cancellationToken); } private void OnStart() { if (!HasStarted) { HasStarted = true; } } } private class OnCompletedReadStream : DelegatingStream { public OnCompletedReadStream(Action onCompleted) : base(Stream.Null) { OnCompleted = onCompleted; } public Action OnCompleted { get; } public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) { if (buffer.Length != 0) { OnCompleted(); } return new ValueTask(0); } } private class DelegateHttpTransforms : HttpTransformer { public bool CopyRequestHeaders { get; set; } = true; public Func OnRequest { get; set; } = (_, _, _) => Task.CompletedTask; public Func> OnResponse { get; set; } = (_, _) => new(true); public Func OnResponseTrailers { get; set; } = (_, _) => Task.CompletedTask; public override async ValueTask TransformRequestAsync(HttpContext httpContext, HttpRequestMessage proxyRequest, string destinationPrefix, CancellationToken cancellationToken) { if (CopyRequestHeaders) { await base.TransformRequestAsync(httpContext, proxyRequest, destinationPrefix, cancellationToken); } await OnRequest(httpContext, proxyRequest, destinationPrefix); } public override async ValueTask TransformResponseAsync(HttpContext httpContext, HttpResponseMessage proxyResponse, CancellationToken cancellationToken) { await base.TransformResponseAsync(httpContext, proxyResponse, cancellationToken); return await OnResponse(httpContext, proxyResponse); } public override async ValueTask TransformResponseTrailersAsync(HttpContext httpContext, HttpResponseMessage proxyResponse, CancellationToken cancellationToken) { await base.TransformResponseTrailersAsync(httpContext, proxyResponse, cancellationToken); await OnResponseTrailers(httpContext, proxyResponse); } } private class ThrowBadHttpRequestExceptionStream : DelegatingStream { public ThrowBadHttpRequestExceptionStream(int statusCode) : base(Stream.Null) { StatusCode = statusCode; } private int StatusCode { get; } public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) { if (buffer.Length == 0) { return new ValueTask(0); } cancellationToken.ThrowIfCancellationRequested(); throw new BadHttpRequestException(ReasonPhrases.GetReasonPhrase(StatusCode), StatusCode); } } } ================================================ FILE: test/ReverseProxy.Tests/Forwarder/HttpTransformerTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.Net.Http.Headers; using Xunit; using Yarp.ReverseProxy.Transforms; using Yarp.ReverseProxy.Transforms.Builder.Tests; using Yarp.Tests.Common; namespace Yarp.ReverseProxy.Forwarder.Tests; public class HttpTransformerTests { private static readonly string[] RestrictedHeaders = new[] { HeaderNames.Connection, HeaderNames.TransferEncoding, HeaderNames.KeepAlive, HeaderNames.Upgrade, HeaderNames.ProxyConnection, HeaderNames.ProxyAuthenticate, "Proxy-Authentication-Info", HeaderNames.ProxyAuthorization, "Proxy-Features", "Proxy-Instruction", "Security-Scheme", "ALPN", "Close", "HTTP2-Settings", HeaderNames.UpgradeInsecureRequests, HeaderNames.TE, HeaderNames.AltSvc, }; [Fact] public async Task TransformRequestAsync_RemovesRestrictedHeaders() { var transformer = HttpTransformer.Default; var httpContext = new DefaultHttpContext(); var proxyRequest = new HttpRequestMessage(HttpMethod.Get, "https://localhost"); foreach (var header in RestrictedHeaders) { httpContext.Request.Headers[header] = "value"; } await transformer.TransformRequestAsync(httpContext, proxyRequest, "prefix", CancellationToken.None); foreach (var header in RestrictedHeaders) { Assert.False(proxyRequest.Headers.Contains(header)); } Assert.Null(proxyRequest.Content); } [Fact] public async Task TransformRequestAsync_KeepOriginalHost() { var transformer = HttpTransformer.Empty; var httpContext = new DefaultHttpContext(); var proxyRequest = new HttpRequestMessage(HttpMethod.Get, "https://localhost"); httpContext.Request.Host = new HostString("example.com:3456"); await transformer.TransformRequestAsync(httpContext, proxyRequest, "prefix", CancellationToken.None); Assert.Equal("example.com:3456", proxyRequest.Headers.Host); } [Fact] public async Task TransformRequestAsync_TETrailers_Copied() { var transformer = HttpTransformer.Default; var httpContext = new DefaultHttpContext(); httpContext.Request.Protocol = "HTTP/2"; var proxyRequest = new HttpRequestMessage(HttpMethod.Get, "https://localhost"); httpContext.Request.Headers[HeaderNames.TE] = "traiLers"; await transformer.TransformRequestAsync(httpContext, proxyRequest, "prefix", CancellationToken.None); Assert.True(proxyRequest.Headers.TryGetValues(HeaderNames.TE, out var values)); var value = Assert.Single(values); Assert.Equal("traiLers", value); Assert.Null(proxyRequest.Content); } [Fact] public async Task TransformRequestAsync_ContentLengthAndTransferEncoding_ContentLengthRemoved() { var transformer = HttpTransformer.Default; var httpContext = new DefaultHttpContext(); var proxyRequest = new HttpRequestMessage(HttpMethod.Get, "https://localhost") { Content = new ByteArrayContent(Array.Empty()) }; httpContext.Request.Headers[HeaderNames.TransferEncoding] = "chUnked"; httpContext.Request.Headers[HeaderNames.ContentLength] = "10"; await transformer.TransformRequestAsync(httpContext, proxyRequest, "prefix", CancellationToken.None); Assert.False(proxyRequest.Content.Headers.TryGetValues(HeaderNames.ContentLength, out var _)); // Transfer-Encoding is on the restricted list and removed. HttpClient will re-add it if required. Assert.False(proxyRequest.Headers.TryGetValues(HeaderNames.TransferEncoding, out var _)); } [Fact] public async Task TransformRequestAsync_SetDestinationPrefix() { const string updatedDestinationPrefix = "https://contoso.com"; var transformer = TransformBuilderTests.CreateTransformBuilder().CreateInternal(context => { context.AddRequestTransform(transformContext => { transformContext.DestinationPrefix = updatedDestinationPrefix; return ValueTask.CompletedTask; }); }); var httpContext = new DefaultHttpContext(); var proxyRequest = new HttpRequestMessage(HttpMethod.Get, requestUri: (string)null) { Content = new ByteArrayContent(Array.Empty()), }; await transformer.TransformRequestAsync(httpContext, proxyRequest, "https://localhost", CancellationToken.None); Assert.Equal(new Uri(updatedDestinationPrefix), proxyRequest.RequestUri); } [Theory] [InlineData(HttpStatusCode.Continue)] [InlineData(HttpStatusCode.SwitchingProtocols)] [InlineData(HttpStatusCode.NoContent)] [InlineData(HttpStatusCode.ResetContent)] public async Task TransformResponseAsync_ContentLength0OnBodylessStatusCode_ContentLengthRemoved(HttpStatusCode statusCode) { var transformer = HttpTransformer.Default; var httpContext = new DefaultHttpContext(); var proxyResponse = new HttpResponseMessage(statusCode) { Content = new ByteArrayContent(Array.Empty()) }; Assert.Equal(0, proxyResponse.Content.Headers.ContentLength); await transformer.TransformResponseAsync(httpContext, proxyResponse, CancellationToken.None); Assert.False(httpContext.Response.Headers.ContainsKey(HeaderNames.ContentLength)); } [Fact] public async Task TransformResponseAsync_RemovesRestrictedHeaders() { var transformer = HttpTransformer.Default; var httpContext = new DefaultHttpContext(); var proxyResponse = new HttpResponseMessage() { Content = new ByteArrayContent(Array.Empty()) }; foreach (var header in RestrictedHeaders) { if (!proxyResponse.Headers.TryAddWithoutValidation(header, "value")) { Assert.True(proxyResponse.Content.Headers.TryAddWithoutValidation(header, "value")); } } await transformer.TransformResponseAsync(httpContext, proxyResponse, CancellationToken.None); foreach (var header in RestrictedHeaders) { Assert.False(httpContext.Response.Headers.ContainsKey(header)); } } [Theory] [InlineData(true)] [InlineData(false)] public async Task TransformResponseAsync_StrictTransportSecurity_CopiedIfNotPresent(bool alreadyPresent) { var transformer = HttpTransformer.Default; var httpContext = new DefaultHttpContext(); var proxyResponse = new HttpResponseMessage() { Content = new ByteArrayContent(Array.Empty()) }; if (alreadyPresent) { httpContext.Response.Headers.StrictTransportSecurity = "max-age=31536000; includeSubDomains"; } Assert.True(proxyResponse.Headers.TryAddWithoutValidation(HeaderNames.StrictTransportSecurity, "max-age=31000; preload")); await transformer.TransformResponseAsync(httpContext, proxyResponse, CancellationToken.None); var result = httpContext.Response.Headers.StrictTransportSecurity; if (alreadyPresent) { Assert.Equal("max-age=31536000; includeSubDomains", result); } else { Assert.Equal("max-age=31000; preload", result); } } [Fact] public async Task TransformResponseAsync_ContentLengthAndTransferEncoding_ContentLengthRemoved() { var transformer = HttpTransformer.Default; var httpContext = new DefaultHttpContext(); var proxyResponse = new HttpResponseMessage() { Content = new ByteArrayContent(new byte[10]) }; proxyResponse.Headers.TransferEncodingChunked = true; Assert.Equal(10, proxyResponse.Content.Headers.ContentLength); await transformer.TransformResponseAsync(httpContext, proxyResponse, CancellationToken.None); Assert.False(httpContext.Response.Headers.ContainsKey(HeaderNames.ContentLength)); // Transfer-Encoding is on the restricted list and removed. HttpClient will re-add it if required. Assert.False(httpContext.Response.Headers.ContainsKey(HeaderNames.TransferEncoding)); } [Fact] public async Task TransformResponseTrailersAsync_RemovesRestrictedHeaders() { var transformer = HttpTransformer.Default; var httpContext = new DefaultHttpContext(); var trailersFeature = new TestTrailersFeature(); httpContext.Features.Set(trailersFeature); var proxyResponse = new HttpResponseMessage(); foreach (var header in RestrictedHeaders) { Assert.True(proxyResponse.TrailingHeaders.TryAddWithoutValidation(header, "value")); } await transformer.TransformResponseTrailersAsync(httpContext, proxyResponse, CancellationToken.None); foreach (var header in RestrictedHeaders) { Assert.False(trailersFeature.Trailers.ContainsKey(header)); } } public enum ImplementationType { StructuredTransformer, DerivedWithoutCT, DerivedWithCT, } public static IEnumerable ImplementationTypes_MemberData() => Enum.GetValues().Select(i => new object[] { i }); [Theory] [MemberData(nameof(ImplementationTypes_MemberData))] public async Task DerivedImplementation_TransformRequestAsync_DerivedImplementationCalled(ImplementationType implementationType) { var implementationCalled = 0; var transformer = GetTransformerImplementation(implementationType, () => implementationCalled++); var httpContext = new DefaultHttpContext(); var proxyRequest = new HttpRequestMessage(); var destinationPrefix = "http://destinationhost:9090/path"; using var cts = new CancellationTokenSource(); await transformer.TransformRequestAsync(httpContext, proxyRequest, destinationPrefix, cts.Token); Assert.Equal(1, implementationCalled); } [Theory] [MemberData(nameof(ImplementationTypes_MemberData))] public async Task DerivedImplementation_TransformResponseAsync_DerivedImplementationCalled(ImplementationType implementationType) { var implementationCalled = 0; var transformer = GetTransformerImplementation(implementationType, () => implementationCalled++); var httpContext = new DefaultHttpContext(); var proxyResponse = new HttpResponseMessage(); using var cts = new CancellationTokenSource(); await transformer.TransformResponseAsync(httpContext, proxyResponse, cts.Token); Assert.Equal(1, implementationCalled); } [Theory] [MemberData(nameof(ImplementationTypes_MemberData))] public async Task DerivedImplementation_TransformResponseTrailersAsync_DerivedImplementationCalled(ImplementationType implementationType) { var implementationCalled = 0; var transformer = GetTransformerImplementation(implementationType, () => implementationCalled++); var httpContext = new DefaultHttpContext(); var proxyResponse = new HttpResponseMessage(); httpContext.Features.Set(new TestTrailersFeature()); using var cts = new CancellationTokenSource(); await transformer.TransformResponseTrailersAsync(httpContext, proxyResponse, cts.Token); Assert.Equal(1, implementationCalled); } private static HttpTransformer GetTransformerImplementation(ImplementationType implementationType, Action callback) { return implementationType switch { ImplementationType.StructuredTransformer => TransformBuilderTests.CreateTransformBuilder().CreateInternal(context => { context.AddRequestTransform(context => { callback(); return default; }); context.AddResponseTransform(context => { callback(); return default; }); context.AddResponseTrailersTransform(context => { callback(); return default; }); }), ImplementationType.DerivedWithoutCT => new DerivedTransformerWithoutCT { Callback = callback }, ImplementationType.DerivedWithCT => new DerivedTransformerWithCT { Callback = callback }, _ => throw new InvalidOperationException(implementationType.ToString()) }; } private sealed class DerivedTransformerWithoutCT : HttpTransformer { public Action Callback { get; set; } #pragma warning disable CS0672 // We're intentionally testing the obsolete overloads #pragma warning disable CS0618 public override ValueTask TransformRequestAsync(HttpContext httpContext, HttpRequestMessage proxyRequest, string destinationPrefix) { Callback(); return base.TransformRequestAsync(httpContext, proxyRequest, destinationPrefix); } public override ValueTask TransformResponseAsync(HttpContext httpContext, HttpResponseMessage proxyResponse) { Callback(); return base.TransformResponseAsync(httpContext, proxyResponse); } public override ValueTask TransformResponseTrailersAsync(HttpContext httpContext, HttpResponseMessage proxyResponse) { Callback(); return base.TransformResponseTrailersAsync(httpContext, proxyResponse); } #pragma warning restore CS0618 #pragma warning restore CS0672 } private sealed class DerivedTransformerWithCT : HttpTransformer { public Action Callback { get; set; } public override ValueTask TransformRequestAsync(HttpContext httpContext, HttpRequestMessage proxyRequest, string destinationPrefix, CancellationToken cancellationToken) { Callback(); return base.TransformRequestAsync(httpContext, proxyRequest, destinationPrefix, cancellationToken); } public override ValueTask TransformResponseAsync(HttpContext httpContext, HttpResponseMessage proxyResponse, CancellationToken cancellationToken) { Callback(); return base.TransformResponseAsync(httpContext, proxyResponse, cancellationToken); } public override ValueTask TransformResponseTrailersAsync(HttpContext httpContext, HttpResponseMessage proxyResponse, CancellationToken cancellationToken) { Callback(); return base.TransformResponseTrailersAsync(httpContext, proxyResponse, cancellationToken); } } } ================================================ FILE: test/ReverseProxy.Tests/Forwarder/RequestUtilitiesTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Net.Http; using Microsoft.AspNetCore.Http; using Xunit; namespace Yarp.ReverseProxy.Forwarder.Tests; public class RequestUtilitiesTests { [Fact] public void GetHttpMethod_Get_Works() { Assert.Same(HttpMethod.Get, RequestUtilities.GetHttpMethod("GET")); } [Fact] public void GetHttpMethod_Post_Works() { Assert.Same(HttpMethod.Post, RequestUtilities.GetHttpMethod("POST")); } [Fact] public void GetHttpMethod_Put_Works() { Assert.Same(HttpMethod.Put, RequestUtilities.GetHttpMethod("PUT")); } [Fact] public void GetHttpMethod_Delete_Works() { Assert.Same(HttpMethod.Delete, RequestUtilities.GetHttpMethod("DELETE")); } [Fact] public void GetHttpMethod_Options_Works() { Assert.Same(HttpMethod.Options, RequestUtilities.GetHttpMethod("OPTIONS")); } [Fact] public void GetHttpMethod_Head_Works() { Assert.Same(HttpMethod.Head, RequestUtilities.GetHttpMethod("HEAD")); } [Fact] public void GetHttpMethod_Patch_Works() { Assert.Same(HttpMethod.Patch, RequestUtilities.GetHttpMethod("PATCH")); } [Fact] public void GetHttpMethod_Trace_Works() { Assert.Same(HttpMethod.Trace, RequestUtilities.GetHttpMethod("TRACE")); } [Fact] public void GetHttpMethod_Unknown_Works() { Assert.Same("Unknown", RequestUtilities.GetHttpMethod("Unknown").Method); } [Fact] public void GetHttpMethod_Connect_Throws() { Assert.Throws(() => RequestUtilities.GetHttpMethod("CONNECT")); } [Theory] [InlineData(" GET")] [InlineData("GET ")] [InlineData("G;ET")] public void GetHttpMethod_Invalid_Throws(string method) { Assert.Throws(() => RequestUtilities.GetHttpMethod(method)); } [Theory] [InlineData("http://localhost", "", "", "http://localhost/")] [InlineData("http://localhost/", "", "", "http://localhost/")] [InlineData("http://localhost", "/", "", "http://localhost/")] [InlineData("http://localhost/", "/", "", "http://localhost/")] [InlineData("http://localhost", "", "?query", "http://localhost/?query")] [InlineData("http://localhost", "/path", "?query", "http://localhost/path?query")] [InlineData("http://localhost", "/path/", "?query", "http://localhost/path/?query")] [InlineData("http://localhost/", "/path", "?query", "http://localhost/path?query")] [InlineData("http://localhost/base", "", "", "http://localhost/base")] [InlineData("http://localhost/base", "", "?query", "http://localhost/base?query")] [InlineData("http://localhost/base", "/path", "?query", "http://localhost/base/path?query")] [InlineData("http://localhost/base/", "/path", "?query", "http://localhost/base/path?query")] [InlineData("http://localhost/base/", "/path/", "?query", "http://localhost/base/path/?query")] [InlineData("http://localhost/base/", "/path/你好", "?query%E4%BD%A0%E5%A5%BD", "http://localhost/base/path/%E4%BD%A0%E5%A5%BD?query%E4%BD%A0%E5%A5%BD")] // pchar = unreserved / pct-encoded / sub-delims / ":" / "@" // unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" // sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "=" [InlineData("http://localhost/base/", "/path/!$&'()*+,;=:@/%?#[]", "?query", "http://localhost/base/path/!$&'()*+,;=:@/%25%3F%23%5B%5D?query")] // PathString should be fully un-escaped to start with and QueryString should be fully escaped. [InlineData("http://localhost/base/", "/path/%2F%20", "?query%20", "http://localhost/base/path/%252F%2520?query%20")] public void MakeDestinationAddress(string destinationPrefix, string path, string query, string expected) { var uri = RequestUtilities.MakeDestinationAddress(destinationPrefix, new PathString(path), new QueryString(query)); Assert.Equal(expected, uri.AbsoluteUri); } // https://datatracker.ietf.org/doc/html/rfc3986/#appendix-A // pchar = unreserved / pct-encoded / sub-delims / ":" / "@" // pct-encoded = "%" HEXDIG HEXDIG // unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" // reserved = gen-delims / sub-delims // gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@" // sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "=" [Fact] public void ValidPathCharacters() { var valids = new char[] { '!', '$', '&', '\'', '(', ')', '*', '+', ',', '-', '.', '/', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '=', '@', 'A', 'B', 'C', 'D', 'E', 'F','G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '_', 'a', 'b', 'c', 'd', 'e', 'f','g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '~' }; foreach (var c in valids) { Assert.Equal($"/{c}", RequestUtilities.EncodePath($"/{c}")); } } [Fact] public void InvalidPathCharacters() { var invalids = new char[] { // Controls (char)0x00, (char)0x01, (char)0x02, (char)0x03, (char)0x04, (char)0x05, (char)0x06, (char)0x00, (char)0x07, (char)0x08, (char)0x09, (char)0x0A, (char)0x0B, (char)0x0C, (char)0x0D, (char)0x0E, (char)0x0F, (char)0x10, (char)0x11, (char)0x02, (char)0x13, (char)0x14, (char)0x15, (char)0x16, (char)0x10, (char)0x17, (char)0x18, (char)0x19, (char)0x1A, (char)0x1B, (char)0x1C, (char)0x1D, (char)0x1E, (char)0x1F, ' ', '"', '#', '%', '<', '>', '?', '[', '\\', ']', '^', '`', '{', '|', '}' }; foreach (var c in invalids) { Assert.Equal($"/%{(int)c:X2}", RequestUtilities.EncodePath($"/{c}")); } } [Theory] [InlineData(null, "a", "a")] [InlineData("a", "", "a;")] [InlineData("", "a", ";a")] [InlineData("a", "b", "a;b")] [InlineData(null, "a;b", "a;b")] [InlineData("a;b", "", "a;b;")] [InlineData("", "a;b", ";a;b")] [InlineData("a;b", "c", "a;b;c")] [InlineData("a", "b;c", "a;b;c")] [InlineData("a;b", "c;d", "a;b;c;d")] [InlineData("a", "b c", "a;b c")] [InlineData("a b", "c", "a b;c")] public void Concat(string stringValues, string inputHeaderStringValues, string expectedOutput) { var request = new HttpRequestMessage(); foreach (var value in inputHeaderStringValues.Split(';')) { request.Headers.TryAddWithoutValidation("foo", value); } request.Headers.TryAddWithoutValidation("bar", inputHeaderStringValues.Split(';')); var headerStringValues = request.Headers.NonValidated["foo"]; var actualValues = RequestUtilities.Concat(stringValues?.Split(';'), headerStringValues); Assert.Equal(expectedOutput.Split(';'), actualValues); headerStringValues = request.Headers.NonValidated["bar"]; actualValues = RequestUtilities.Concat(stringValues?.Split(';'), headerStringValues); Assert.Equal(expectedOutput.Split(';'), actualValues); } [Theory] [InlineData("a")] [InlineData("a b")] [InlineData("a", "b")] [InlineData("a", "b c", "d")] [InlineData("")] [InlineData("", "")] [InlineData("a", "")] [InlineData("", "a")] [InlineData("", "a", "b")] [InlineData("", "a", "")] [InlineData("a", "", "b")] public void TryGetValues(params string[] headerValues) { var request = new HttpRequestMessage(); foreach (var value in headerValues) { request.Headers.TryAddWithoutValidation("foo", value); } request.Headers.TryAddWithoutValidation("bar", headerValues); Assert.True(RequestUtilities.TryGetValues(request.Headers, "foo", out var actualValues)); Assert.Equal(headerValues, actualValues); Assert.True(RequestUtilities.TryGetValues(request.Headers, "bar", out actualValues)); Assert.Equal(headerValues, actualValues); } } ================================================ FILE: test/ReverseProxy.Tests/Forwarder/ReverseProxyServiceCollectionTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Net.Http; using Microsoft.Extensions.DependencyInjection; using Xunit; namespace Yarp.ReverseProxy.Forwarder; public class ReverseProxyServiceCollectionTests { [Fact] public void ConfigureHttpClient_Works() { new ServiceCollection() .AddReverseProxy() .ConfigureHttpClient((_, _) => { }); } [Fact] public void ConfigureHttpClient_ThrowIfCustomServiceAdded() { Assert.Throws(() => { new ServiceCollection() .AddSingleton() .AddReverseProxy() .ConfigureHttpClient((_, _) => { }); }); } private class CustomForwarderHttpClientFactory : IForwarderHttpClientFactory { public HttpMessageInvoker CreateClient(ForwarderHttpClientContext context) { throw new NotImplementedException(); } } } ================================================ FILE: test/ReverseProxy.Tests/Forwarder/StreamCopierTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Diagnostics.Tracing; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using Xunit; using Yarp.Tests.Common; using Yarp.ReverseProxy.Utilities; namespace Yarp.ReverseProxy.Forwarder.Tests; public class StreamCopierTests : TestAutoMockBase { [Theory] [InlineData(true)] [InlineData(false)] public async Task CopyAsync_Works(bool isRequest) { var events = TestEventListener.Collect(); const int SourceSize = (128 * 1024) - 3; var sourceBytes = Enumerable.Range(0, SourceSize).Select(i => (byte)(i % 256)).ToArray(); var source = new MemoryStream(sourceBytes); var destination = new MemoryStream(); using var cts = ActivityCancellationTokenSource.Rent(TimeSpan.FromSeconds(10), CancellationToken.None); await StreamCopier.CopyAsync(isRequest, source, destination, SourceSize, TimeProvider.System, cts, cts.Token); Assert.False(cts.Token.IsCancellationRequested); Assert.Equal(sourceBytes, destination.ToArray()); AssertContentTransferred(events, isRequest, SourceSize); } [Theory] [InlineData(true)] [InlineData(false)] public async Task SourceThrows_Reported(bool isRequest) { var events = TestEventListener.Collect(); var timeProvider = new TestTimeProvider(); var sourceWaitTime = TimeSpan.FromMilliseconds(12345); var source = new SlowStream(new ThrowStream(), timeProvider, sourceWaitTime); var destination = new MemoryStream(); using var cts = ActivityCancellationTokenSource.Rent(TimeSpan.FromSeconds(10), CancellationToken.None); var (result, error) = await StreamCopier.CopyAsync(isRequest, source, destination, StreamCopier.UnknownLength, timeProvider, cts, cts.Token); Assert.Equal(StreamCopyResult.InputError, result); Assert.IsAssignableFrom(error); AssertContentTransferred(events, isRequest, contentLength: 0, iops: 1, firstReadTime: sourceWaitTime, readTime: sourceWaitTime, writeTime: TimeSpan.Zero); } [Theory] [InlineData(true)] [InlineData(false)] public async Task DestinationThrows_Reported(bool isRequest) { var events = TestEventListener.Collect(); const int SourceSize = 10; const int BytesPerRead = 3; var timeProvider = new TestTimeProvider(); var sourceWaitTime = TimeSpan.FromMilliseconds(12345); var destinationWaitTime = TimeSpan.FromMilliseconds(42); var source = new SlowStream(new MemoryStream(new byte[SourceSize]), timeProvider, sourceWaitTime) { MaxBytesPerRead = BytesPerRead }; var destination = new SlowStream(new ThrowStream(), timeProvider, destinationWaitTime); using var cts = ActivityCancellationTokenSource.Rent(TimeSpan.FromSeconds(10), CancellationToken.None); var (result, error) = await StreamCopier.CopyAsync(isRequest, source, destination, SourceSize, timeProvider, cts, cts.Token); Assert.Equal(StreamCopyResult.OutputError, result); Assert.IsAssignableFrom(error); AssertContentTransferred(events, isRequest, contentLength: BytesPerRead, iops: 1, firstReadTime: sourceWaitTime, readTime: sourceWaitTime, writeTime: destinationWaitTime); } [Theory] [InlineData(true)] [InlineData(false)] public async Task Cancelled_Reported(bool isRequest) { var events = TestEventListener.Collect(); var source = new MemoryStream(new byte[10]); var destination = new MemoryStream(); var requestCts = new CancellationTokenSource(); using var cts = ActivityCancellationTokenSource.Rent(TimeSpan.FromSeconds(10), requestCts.Token); requestCts.Cancel(); var (result, error) = await StreamCopier.CopyAsync(isRequest, source, destination, StreamCopier.UnknownLength, new TestTimeProvider(), cts, cts.Token); Assert.Equal(StreamCopyResult.Canceled, result); Assert.IsAssignableFrom(error); AssertContentTransferred(events, isRequest, contentLength: 0, iops: 1, firstReadTime: TimeSpan.Zero, readTime: TimeSpan.Zero, writeTime: TimeSpan.Zero); } [Theory] [InlineData(true)] [InlineData(false)] public async Task SlowStreams_TelemetryReportsCorrectTime(bool isRequest) { var events = TestEventListener.Collect(); const int SourceSize = 3; var sourceBytes = new byte[SourceSize]; var source = new MemoryStream(sourceBytes); var destination = new MemoryStream(); var timeProvider = new TestTimeProvider(); var sourceWaitTime = TimeSpan.FromMilliseconds(12345); var destinationWaitTime = TimeSpan.FromMilliseconds(42); using var cts = ActivityCancellationTokenSource.Rent(TimeSpan.FromSeconds(10), CancellationToken.None); await StreamCopier.CopyAsync( isRequest, new SlowStream(source, timeProvider, sourceWaitTime), new SlowStream(destination, timeProvider, destinationWaitTime), SourceSize, timeProvider, cts, cts.Token); Assert.Equal(sourceBytes, destination.ToArray()); AssertContentTransferred(events, isRequest, SourceSize, iops: SourceSize + 1, firstReadTime: sourceWaitTime, readTime: (SourceSize + 1) * sourceWaitTime, writeTime: SourceSize * destinationWaitTime); } [Theory] [InlineData(true)] [InlineData(false)] public async Task LongContentTransfer_TelemetryReportsTransferringEvents(bool isRequest) { var events = TestEventListener.Collect(); const int SourceSize = 123; var sourceBytes = new byte[SourceSize]; var source = new MemoryStream(sourceBytes); var destination = new MemoryStream(); var timeProvider = new TestTimeProvider(); var sourceWaitTime = TimeSpan.FromMilliseconds(789); // Every second read triggers ContentTransferring var destinationWaitTime = TimeSpan.FromMilliseconds(42); const int BytesPerRead = 3; var contentReads = (int)Math.Ceiling((double)SourceSize / BytesPerRead); using var cts = ActivityCancellationTokenSource.Rent(TimeSpan.FromSeconds(10), CancellationToken.None); await StreamCopier.CopyAsync( isRequest, new SlowStream(source, timeProvider, sourceWaitTime) { MaxBytesPerRead = BytesPerRead }, new SlowStream(destination, timeProvider, destinationWaitTime), SourceSize, timeProvider, cts, cts.Token); Assert.Equal(sourceBytes, destination.ToArray()); AssertContentTransferred(events, isRequest, SourceSize, iops: contentReads + 1, firstReadTime: sourceWaitTime, readTime: (contentReads + 1) * sourceWaitTime, writeTime: contentReads * destinationWaitTime); var transferringEvents = events.Where(e => e.EventName == "ContentTransferring").ToArray(); Assert.Equal(contentReads / 2, transferringEvents.Length); for (var i = 0; i < transferringEvents.Length; i++) { var payload = transferringEvents[i].Payload; Assert.Equal(5, payload.Count); Assert.Equal(isRequest, (bool)payload[0]); var contentLength = (long)payload[1]; var iops = (long)payload[2]; Assert.Equal((i + 1) * 2, iops); if (contentLength % BytesPerRead == 0) { Assert.Equal(iops * BytesPerRead, contentLength); } else { Assert.Equal(transferringEvents.Length - 1, i); Assert.Equal(SourceSize, contentLength); } var readTime = new TimeSpan((long)payload[3]); Assert.Equal(iops * sourceWaitTime, readTime, new ApproximateTimeSpanComparer()); var writeTime = new TimeSpan((long)payload[4]); Assert.Equal(iops * destinationWaitTime, writeTime, new ApproximateTimeSpanComparer()); } } private static void AssertContentTransferred( List events, bool isRequest, long contentLength, long? iops = null, TimeSpan? firstReadTime = null, TimeSpan? readTime = null, TimeSpan? writeTime = null) { var contentTransferred = Assert.Single(events, e => e.EventName == "ContentTransferred"); var payload = contentTransferred.Payload; Assert.Equal(6, payload.Count); Assert.Equal(isRequest, (bool)payload[0]); Assert.Equal(contentLength, (long)payload[1]); var actualIops = (long)payload[2]; if (iops.HasValue) { Assert.Equal(iops.Value, actualIops); } else { Assert.InRange(actualIops, 1, contentLength + 1); } if (readTime.HasValue) { Assert.Equal(readTime.Value, new TimeSpan((long)payload[3]), new ApproximateTimeSpanComparer()); } if (writeTime.HasValue) { Assert.Equal(writeTime.Value, new TimeSpan((long)payload[4]), new ApproximateTimeSpanComparer()); } if (firstReadTime.HasValue) { Assert.Equal(firstReadTime.Value, new TimeSpan((long)payload[5]), new ApproximateTimeSpanComparer()); if (readTime.HasValue) { Assert.True(firstReadTime.Value <= readTime.Value); } } var stages = events.GetProxyStages(); var startStage = isRequest ? ForwarderStage.RequestContentTransferStart : ForwarderStage.ResponseContentTransferStart; var startTime = Assert.Single(stages, s => s.Stage == startStage).TimeStamp; Assert.True(startTime <= contentTransferred.TimeStamp); } private class ThrowStream : DelegatingStream { public ThrowStream() : base(Stream.Null) { } public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) { throw new IOException("Fake connection issue"); } public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) { throw new IOException("Fake connection issue"); } } private class SlowStream : DelegatingStream { private readonly TimeSpan _waitTime; private readonly TestTimeProvider _timeProvider; public int MaxBytesPerRead { get; set; } = 1; public SlowStream(Stream innerStream, TestTimeProvider timeProvider, TimeSpan waitTime) : base(innerStream) { _timeProvider = timeProvider; _waitTime = waitTime; } public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) { if (buffer.Length == 0) { return new ValueTask(0); } _timeProvider.Advance(_waitTime); return base.ReadAsync(buffer.Slice(0, Math.Min(buffer.Length, MaxBytesPerRead)), cancellationToken); } public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) { _timeProvider.Advance(_waitTime); return base.WriteAsync(buffer, cancellationToken); } } private class ApproximateTimeSpanComparer : IEqualityComparer { public TimeSpan Precision { get; set; } = TimeSpan.FromMilliseconds(0.1); public bool Equals(TimeSpan x, TimeSpan y) => x > y ? x - y <= Precision : y - x <= Precision; public int GetHashCode(TimeSpan obj) => 42; } } ================================================ FILE: test/ReverseProxy.Tests/Forwarder/StreamCopyHttpContentTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.IO; using System.Linq; using System.Net.Http; using System.Reflection; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging.Abstractions; using Moq; using Xunit; using Yarp.ReverseProxy.Utilities; using Yarp.Tests.Common; namespace Yarp.ReverseProxy.Forwarder.Tests; public class StreamCopyHttpContentTests { private static StreamCopyHttpContent CreateContent(HttpContext context = null, bool isStreamingRequest = false, TimeProvider timeProvider = null, ActivityCancellationTokenSource contentCancellation = null) { context ??= new DefaultHttpContext(); timeProvider ??= TimeProvider.System; contentCancellation ??= ActivityCancellationTokenSource.Rent(TimeSpan.FromSeconds(10), CancellationToken.None); return new StreamCopyHttpContent(context, isStreamingRequest, timeProvider, NullLogger.Instance, contentCancellation); } [Fact] public async Task CopyToAsync_InvokesStreamCopier() { const int SourceSize = (128 * 1024) - 3; var sourceBytes = Enumerable.Range(0, SourceSize).Select(i => (byte)(i % 256)).ToArray(); var context = new DefaultHttpContext(); context.Request.Body = new MemoryStream(sourceBytes); var destination = new MemoryStream(); var sut = CreateContent(context); Assert.False(sut.ConsumptionTask.IsCompleted); Assert.False(sut.Started); await sut.CopyToWithCancellationAsync(destination); Assert.True(sut.Started); Assert.True(sut.ConsumptionTask.IsCompleted); Assert.Equal(sourceBytes, destination.ToArray()); } [Theory] [InlineData(false)] // we expect to always flush at least once to trigger sending request headers [InlineData(true)] public async Task CopyToAsync_AutoFlushing(bool autoFlush) { // Must be same as StreamCopier constant. const int DefaultBufferSize = 65536; const int SourceSize = (128 * 1024) - 3; var expectedFlushes = 0; if (autoFlush) { // How many buffers is needed to send the source rounded up. expectedFlushes = (SourceSize - 1) / DefaultBufferSize + 1; } // Explicit flush after headers are sent. expectedFlushes++; var sourceBytes = Enumerable.Range(0, SourceSize).Select(i => (byte)(i % 256)).ToArray(); var context = new DefaultHttpContext(); context.Request.Body = new MemoryStream(sourceBytes); var destination = new MemoryStream(); var flushCountingDestination = new FlushCountingStream(destination); var sut = CreateContent(context, autoFlush); Assert.False(sut.ConsumptionTask.IsCompleted); Assert.False(sut.Started); await sut.CopyToWithCancellationAsync(flushCountingDestination); Assert.True(sut.Started); Assert.True(sut.ConsumptionTask.IsCompleted); Assert.Equal(sourceBytes, destination.ToArray()); Assert.Equal(expectedFlushes, flushCountingDestination.NumFlushes); } [Fact] public async Task CopyToAsync_AsyncSequencing() { var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var source = new Mock(); source.Setup(s => s.ReadAsync(It.IsAny>(), It.IsAny())).Returns(() => new ValueTask(tcs.Task)); var context = new DefaultHttpContext(); context.Request.Body = source.Object; var destination = new MemoryStream(); var sut = CreateContent(context); Assert.False(sut.ConsumptionTask.IsCompleted); Assert.False(sut.Started); var task = sut.CopyToWithCancellationAsync(destination); Assert.True(sut.Started); // This should happen synchronously Assert.False(sut.ConsumptionTask.IsCompleted); // This cannot happen until the tcs releases it tcs.TrySetResult(0); await task; Assert.True(sut.ConsumptionTask.IsCompleted); } [Fact] public Task ReadAsStreamAsync_Throws() { var sut = CreateContent(); Func func = () => sut.ReadAsStreamAsync(); return Assert.ThrowsAsync(func); } [Fact] public void AllowDuplex_ReturnsTrue() { var sut = CreateContent(); // This is an internal property that HttpClient and friends use internally and which must be true // to support duplex channels. This test helps detect regressions or changes in undocumented behavior // in .NET Core, and it passes as of .NET Core 3.1. var allowDuplexProperty = typeof(HttpContent).GetProperty("AllowDuplex", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); Assert.NotNull(allowDuplexProperty); var allowDuplex = (bool)allowDuplexProperty.GetValue(sut); Assert.True(allowDuplex); } [Fact] public async Task SerializeToStreamAsync_RespectsContentCancellation() { var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var source = new ReadDelegatingStream(new MemoryStream(), async (buffer, cancellation) => { if (buffer.Length == 0) { return 0; } Assert.False(cancellation.IsCancellationRequested); await tcs.Task; Assert.True(cancellation.IsCancellationRequested); return 0; }); var context = new DefaultHttpContext(); context.Request.Body = source; using var contentCts = ActivityCancellationTokenSource.Rent(TimeSpan.FromSeconds(10), CancellationToken.None); var sut = CreateContent(context, contentCancellation: contentCts); var copyToTask = sut.CopyToWithCancellationAsync(new MemoryStream()); contentCts.Cancel(); tcs.SetResult(0); await copyToTask; } [Fact] public async Task SerializeToStreamAsync_CanBeCanceledExternally() { var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var source = new ReadDelegatingStream(new MemoryStream(), async (buffer, cancellation) => { if (buffer.Length == 0) { return 0; } Assert.False(cancellation.IsCancellationRequested); await tcs.Task; Assert.True(cancellation.IsCancellationRequested); return 0; }); var context = new DefaultHttpContext(); context.Request.Body = source; var sut = CreateContent(context); using var cts = new CancellationTokenSource(); var copyToTask = sut.CopyToAsync(new MemoryStream(), cts.Token); cts.Cancel(); tcs.SetResult(0); await copyToTask; } private class FlushCountingStream : DelegatingStream { public FlushCountingStream(Stream stream) : base(stream) { } public int NumFlushes { get; private set; } public override async Task FlushAsync(CancellationToken cancellationToken) { await base.FlushAsync(cancellationToken); NumFlushes++; } } private sealed class ReadDelegatingStream : DelegatingStream { private readonly Func, CancellationToken, ValueTask> _readAsync; public ReadDelegatingStream(Stream stream, Func, CancellationToken, ValueTask> readAsync) : base(stream) { _readAsync = readAsync; } public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) { return _readAsync(buffer, cancellationToken); } } } ================================================ FILE: test/ReverseProxy.Tests/Health/ActiveHealthCheckMonitorTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Moq; using Xunit; using Yarp.Tests.Common; using Yarp.ReverseProxy.Configuration; using Yarp.ReverseProxy.Model; namespace Yarp.ReverseProxy.Health.Tests; public class ActiveHealthCheckMonitorTests { private readonly TimeSpan Interval0 = TimeSpan.FromSeconds(10); private readonly TimeSpan Interval1 = TimeSpan.FromSeconds(20); [Fact] public async Task CheckHealthAsync_ActiveHealthCheckIsEnabledForCluster_SendProbe() { var policy0 = new Mock(); policy0.SetupGet(p => p.Name).Returns("policy0"); var policy1 = new Mock(); policy1.SetupGet(p => p.Name).Returns("policy1"); var options = Options.Create(new ActiveHealthCheckMonitorOptions { DefaultInterval = TimeSpan.FromSeconds(60), DefaultTimeout = TimeSpan.FromSeconds(5) }); var clusters = new List(); var monitor = new ActiveHealthCheckMonitor(options, new[] { policy0.Object, policy1.Object }, new DefaultProbingRequestFactory(), new Mock().Object, GetLogger()); var httpClient0 = GetHttpClient(); var cluster0 = GetClusterInfo("cluster0", "policy0", true, httpClient0.Object); clusters.Add(cluster0); var httpClient1 = GetHttpClient(); var cluster1 = GetClusterInfo("cluster1", "policy0", false, httpClient1.Object); clusters.Add(cluster1); var httpClient2 = GetHttpClient(); var cluster2 = GetClusterInfo("cluster2", "policy1", true, httpClient2.Object); clusters.Add(cluster2); Assert.False(monitor.InitialProbeCompleted); await monitor.CheckHealthAsync(clusters); Assert.True(monitor.InitialProbeCompleted); VerifySentProbeAndResult(cluster0, httpClient0, policy0, new[] { ("https://localhost:20000/cluster0/api/health/", 1), ("https://localhost:20001/cluster0/api/health/", 1) }); httpClient1.Verify(c => c.SendAsync(It.IsAny(), It.IsAny()), Times.Never); VerifySentProbeAndResult(cluster2, httpClient2, policy1, new[] { ("https://localhost:20000/cluster2/api/health/", 1), ("https://localhost:20001/cluster2/api/health/", 1) }); } [Theory] [InlineData(false)] // Test old API (CreateRequest with Models) via default implementation [InlineData(true)] // Test new API (CreateRequestAsync with State) public async Task CheckHealthAsync_CustomUserAgentSpecified_UserAgentUnchanged(bool overrideAsyncMethod) { var policy = new Mock(); policy.SetupGet(p => p.Name).Returns("policy"); var requestFactory = new Mock(); HttpRequestMessage CreateCustomRequest() { var request = new HttpRequestMessage(HttpMethod.Get, "https://localhost:20000/cluster/api/health/"); request.Headers.UserAgent.ParseAdd("FooBar/9001"); return request; } if (overrideAsyncMethod) { requestFactory.Setup(p => p.CreateRequestAsync(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(() => ValueTask.FromResult(CreateCustomRequest())); } else { // Test the old API - the default implementation of CreateRequestAsync should call this requestFactory.Setup(p => p.CreateRequest(It.IsAny(), It.IsAny())) .Returns(CreateCustomRequest); // Use default interface implementation for CreateRequestAsync requestFactory.CallBase = true; } var options = Options.Create(new ActiveHealthCheckMonitorOptions()); var monitor = new ActiveHealthCheckMonitor(options, new[] { policy.Object }, requestFactory.Object, new Mock().Object, GetLogger()); var httpClient = GetHttpClient(); var cluster = GetClusterInfo("cluster", "policy", true, httpClient.Object, destinationCount: 1); await monitor.CheckHealthAsync(new[] { cluster }); VerifySentProbeAndResult(cluster, httpClient, policy, new[] { ("https://localhost:20000/cluster/api/health/", 1) }, userAgent: @"^FooBar\/9001$"); } [Fact] public async Task CheckHealthAsync_FactoryCancelledExternally_ProbePassedToPolicyWithException() { var policy = new Mock(); policy.SetupGet(p => p.Name).Returns("policy"); var externalCts = new CancellationTokenSource(); var requestFactory = new Mock(); // First destination: factory throws OperationCanceledException (external cancellation) // Second destination: succeeds normally var callCount = 0; requestFactory.Setup(p => p.CreateRequestAsync(It.IsAny(), It.IsAny(), It.IsAny())) .Returns((cluster, destination, ct) => { callCount++; if (callCount == 1) { // Simulate external cancellation (not timeout) - throw without the timeout CTS being cancelled throw new OperationCanceledException(externalCts.Token); } return ValueTask.FromResult(new HttpRequestMessage(HttpMethod.Get, $"https://localhost:20000/{destination.DestinationId}/health/")); }); var options = Options.Create(new ActiveHealthCheckMonitorOptions { DefaultTimeout = TimeSpan.FromSeconds(30) }); var monitor = new ActiveHealthCheckMonitor(options, new[] { policy.Object }, requestFactory.Object, new Mock().Object, GetLogger()); var httpClient = GetHttpClient(); var cluster = GetClusterInfo("cluster", "policy", true, httpClient.Object, destinationCount: 2); await monitor.CheckHealthAsync(new[] { cluster }); // Policy should receive 2 results: one with exception (cancelled), one successful policy.Verify( p => p.ProbingCompleted( cluster, It.Is>(r => r.Count == 2 && r.Any(x => x.Exception is OperationCanceledException) && r.Any(x => x.Response != null && x.Response.StatusCode == HttpStatusCode.OK))) , Times.Once); policy.Verify(p => p.Name); policy.VerifyNoOtherCalls(); } [Fact] public async Task CheckHealthAsync_SendAsyncCancelledExternally_ProbePassedToPolicyWithException() { var policy = new Mock(); policy.SetupGet(p => p.Name).Returns("policy"); var externalCts = new CancellationTokenSource(); var options = Options.Create(new ActiveHealthCheckMonitorOptions { DefaultTimeout = TimeSpan.FromSeconds(30) }); var monitor = new ActiveHealthCheckMonitor(options, new[] { policy.Object }, new DefaultProbingRequestFactory(), new Mock().Object, GetLogger()); // First destination: SendAsync throws OperationCanceledException (external cancellation) // Second destination: succeeds normally var callCount = 0; var httpClient = new Mock(() => new HttpMessageInvoker(new Mock().Object)); httpClient.Setup(c => c.SendAsync(It.IsAny(), It.IsAny())) .Returns((request, ct) => { callCount++; if (callCount == 1) { // Simulate external cancellation (not timeout) - throw without the timeout CTS being cancelled throw new OperationCanceledException(externalCts.Token); } return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) { Version = request.Version }); }); var cluster = GetClusterInfo("cluster", "policy", true, httpClient.Object, destinationCount: 2); await monitor.CheckHealthAsync(new[] { cluster }); // Policy should receive 2 results: one with exception (cancelled), one successful policy.Verify( p => p.ProbingCompleted( cluster, It.Is>(r => r.Count == 2 && r.Any(x => x.Exception is OperationCanceledException) && r.Any(x => x.Response != null && x.Response.StatusCode == HttpStatusCode.OK))) , Times.Once); policy.Verify(p => p.Name); policy.VerifyNoOtherCalls(); } [Fact] public async Task CheckHealthAsync_TimeoutCancellation_TreatedAsError() { var policy = new Mock(); policy.SetupGet(p => p.Name).Returns("policy"); var options = Options.Create(new ActiveHealthCheckMonitorOptions { DefaultTimeout = TimeSpan.FromMilliseconds(1) }); var monitor = new ActiveHealthCheckMonitor(options, new[] { policy.Object }, new DefaultProbingRequestFactory(), new Mock().Object, GetLogger()); var tcs = new TaskCompletionSource(); var httpClient = new Mock(() => new HttpMessageInvoker(new Mock().Object)); httpClient.Setup(c => c.SendAsync(It.IsAny(), It.IsAny())) .Returns((request, ct) => { // Register callback to cancel the TCS when timeout occurs ct.Register(() => tcs.TrySetCanceled(ct)); return tcs.Task; }); var cluster = GetClusterInfo("cluster", "policy", true, httpClient.Object, destinationCount: 1); await monitor.CheckHealthAsync(new[] { cluster }); // Policy should receive 1 result with an exception (timeout is an error, not skipped) policy.Verify( p => p.ProbingCompleted( cluster, It.Is>(r => r.Count == 1 && r[0].Response == null && r[0].Exception is OperationCanceledException)), Times.Once); policy.Verify(p => p.Name); policy.VerifyNoOtherCalls(); } [Fact] public async Task ProbeCluster_ProbingTimerFired_SendProbesAndReceiveResponses() { var policy0 = new Mock(); policy0.SetupGet(p => p.Name).Returns("policy0"); var policy1 = new Mock(); policy1.SetupGet(p => p.Name).Returns("policy1"); var options = Options.Create(new ActiveHealthCheckMonitorOptions { DefaultInterval = TimeSpan.FromSeconds(60), DefaultTimeout = TimeSpan.FromSeconds(5) }); var timeProvider = new TestTimeProvider(); var monitor = new ActiveHealthCheckMonitor(options, new[] { policy0.Object, policy1.Object }, new DefaultProbingRequestFactory(), timeProvider, GetLogger()); var httpClient0 = GetHttpClient(); var cluster0 = GetClusterInfo("cluster0", "policy0", true, httpClient0.Object, Interval0); monitor.OnClusterAdded(cluster0); var httpClient2 = GetHttpClient(); var cluster2 = GetClusterInfo("cluster2", "policy1", true, httpClient2.Object, Interval1); monitor.OnClusterAdded(cluster2); Assert.False(monitor.InitialProbeCompleted); await monitor.CheckHealthAsync(Array.Empty()); Assert.True(monitor.InitialProbeCompleted); timeProvider.FireAllTimers(); Assert.Equal(2, timeProvider.TimerCount); timeProvider.VerifyTimer(0, Interval0); timeProvider.VerifyTimer(1, Interval1); VerifySentProbeAndResult(cluster0, httpClient0, policy0, new[] { ("https://localhost:20000/cluster0/api/health/", 1), ("https://localhost:20001/cluster0/api/health/", 1) }, policyCallTimes: 1); VerifySentProbeAndResult(cluster2, httpClient2, policy1, new[] { ("https://localhost:20000/cluster2/api/health/", 1), ("https://localhost:20001/cluster2/api/health/", 1) }, policyCallTimes: 1); GC.KeepAlive(monitor); // The timer does not keep a strong reference to the scheduler } [Fact] public async Task ProbeCluster_ClusterRemoved_StopSendingProbes() { var policy0 = new Mock(); policy0.SetupGet(p => p.Name).Returns("policy0"); var policy1 = new Mock(); policy1.SetupGet(p => p.Name).Returns("policy1"); var options = Options.Create(new ActiveHealthCheckMonitorOptions { DefaultInterval = TimeSpan.FromSeconds(60), DefaultTimeout = TimeSpan.FromSeconds(5) }); var timeProvider = new TestTimeProvider(); var monitor = new ActiveHealthCheckMonitor(options, new[] { policy0.Object, policy1.Object }, new DefaultProbingRequestFactory(), timeProvider, GetLogger()); var httpClient0 = GetHttpClient(); var cluster0 = GetClusterInfo("cluster0", "policy0", true, httpClient0.Object, interval: Interval0); monitor.OnClusterAdded(cluster0); var httpClient2 = GetHttpClient(); var cluster2 = GetClusterInfo("cluster2", "policy1", true, httpClient2.Object, interval: Interval1); monitor.OnClusterAdded(cluster2); Assert.False(monitor.InitialProbeCompleted); await monitor.CheckHealthAsync(Array.Empty()); Assert.True(monitor.InitialProbeCompleted); timeProvider.FireAllTimers(); Assert.Equal(2, timeProvider.TimerCount); timeProvider.VerifyTimer(0, Interval0); timeProvider.VerifyTimer(1, Interval1); VerifySentProbeAndResult(cluster0, httpClient0, policy0, new[] { ("https://localhost:20000/cluster0/api/health/", 1), ("https://localhost:20001/cluster0/api/health/", 1) }, policyCallTimes: 1); VerifySentProbeAndResult(cluster2, httpClient2, policy1, new[] { ("https://localhost:20000/cluster2/api/health/", 1), ("https://localhost:20001/cluster2/api/health/", 1) }, policyCallTimes: 1); monitor.OnClusterRemoved(cluster2); timeProvider.FireTimer(0); timeProvider.AssertTimerDisposed(1); VerifySentProbeAndResult(cluster0, httpClient0, policy0, new[] { ("https://localhost:20000/cluster0/api/health/", 2), ("https://localhost:20001/cluster0/api/health/", 2) }, policyCallTimes: 2); VerifySentProbeAndResult(cluster2, httpClient2, policy1, new[] { ("https://localhost:20000/cluster2/api/health/", 1), ("https://localhost:20001/cluster2/api/health/", 1) }, policyCallTimes: 1); GC.KeepAlive(monitor); // The timer does not keep a strong reference to the scheduler } [Fact] public async Task ProbeCluster_ClusterAdded_StartSendingProbes() { var policy0 = new Mock(); policy0.SetupGet(p => p.Name).Returns("policy0"); var policy1 = new Mock(); policy1.SetupGet(p => p.Name).Returns("policy1"); var options = Options.Create(new ActiveHealthCheckMonitorOptions { DefaultInterval = TimeSpan.FromSeconds(60), DefaultTimeout = TimeSpan.FromSeconds(5) }); var timeProvider = new TestTimeProvider(); var monitor = new ActiveHealthCheckMonitor(options, new[] { policy0.Object, policy1.Object }, new DefaultProbingRequestFactory(), timeProvider, GetLogger()); var httpClient0 = GetHttpClient(); var cluster0 = GetClusterInfo("cluster0", "policy0", true, httpClient0.Object, interval: Interval0); monitor.OnClusterAdded(cluster0); Assert.False(monitor.InitialProbeCompleted); await monitor.CheckHealthAsync(Array.Empty()); Assert.True(monitor.InitialProbeCompleted); timeProvider.FireAllTimers(); Assert.Equal(1, timeProvider.TimerCount); timeProvider.VerifyTimer(0, Interval0); VerifySentProbeAndResult(cluster0, httpClient0, policy0, new[] { ("https://localhost:20000/cluster0/api/health/", 1), ("https://localhost:20001/cluster0/api/health/", 1) }, policyCallTimes: 1); var httpClient2 = GetHttpClient(); var cluster2 = GetClusterInfo("cluster2", "policy1", true, httpClient2.Object, interval: Interval1); monitor.OnClusterAdded(cluster2); timeProvider.FireAllTimers(); Assert.Equal(2, timeProvider.TimerCount); timeProvider.VerifyTimer(0, Interval0); timeProvider.VerifyTimer(1, Interval1); VerifySentProbeAndResult(cluster0, httpClient0, policy0, new[] { ("https://localhost:20000/cluster0/api/health/", 2), ("https://localhost:20001/cluster0/api/health/", 2) }, policyCallTimes: 2); VerifySentProbeAndResult(cluster2, httpClient2, policy1, new[] { ("https://localhost:20000/cluster2/api/health/", 1), ("https://localhost:20001/cluster2/api/health/", 1) }, policyCallTimes: 1); GC.KeepAlive(monitor); // The timer does not keep a strong reference to the scheduler } [Fact] public async Task ProbeCluster_ClusterChanged_SendProbesToNewHealthEndpoint() { var policy0 = new Mock(); policy0.SetupGet(p => p.Name).Returns("policy0"); var policy1 = new Mock(); policy1.SetupGet(p => p.Name).Returns("policy1"); var options = Options.Create(new ActiveHealthCheckMonitorOptions { DefaultInterval = TimeSpan.FromSeconds(60), DefaultTimeout = TimeSpan.FromSeconds(5) }); var timeProvider = new TestTimeProvider(); var monitor = new ActiveHealthCheckMonitor(options, new[] { policy0.Object, policy1.Object }, new DefaultProbingRequestFactory(), timeProvider, GetLogger()); var httpClient0 = GetHttpClient(); var cluster0 = GetClusterInfo("cluster0", "policy0", true, httpClient0.Object, interval: Interval0); monitor.OnClusterAdded(cluster0); var httpClient2 = GetHttpClient(); var cluster2 = GetClusterInfo("cluster2", "policy1", true, httpClient2.Object, interval: Interval1); monitor.OnClusterAdded(cluster2); Assert.False(monitor.InitialProbeCompleted); await monitor.CheckHealthAsync(Array.Empty()); Assert.True(monitor.InitialProbeCompleted); timeProvider.FireAllTimers(); Assert.Equal(2, timeProvider.TimerCount); timeProvider.VerifyTimer(0, Interval0); timeProvider.VerifyTimer(1, Interval1); VerifySentProbeAndResult(cluster0, httpClient0, policy0, new[] { ("https://localhost:20000/cluster0/api/health/", 1), ("https://localhost:20001/cluster0/api/health/", 1) }, policyCallTimes: 1); VerifySentProbeAndResult(cluster2, httpClient2, policy1, new[] { ("https://localhost:20000/cluster2/api/health/", 1), ("https://localhost:20001/cluster2/api/health/", 1) }, policyCallTimes: 1); foreach (var destination in cluster2.Destinations.Values) { var d = cluster2.Destinations.GetOrAdd(destination.DestinationId, id => new DestinationState(id)); d.Model = new DestinationModel(new DestinationConfig { Address = destination.Model.Config.Address }); } monitor.OnClusterChanged(cluster2); timeProvider.FireAllTimers(); Assert.Equal(2, timeProvider.TimerCount); timeProvider.VerifyTimer(0, Interval0); timeProvider.VerifyTimer(1, Interval1); VerifySentProbeAndResult(cluster0, httpClient0, policy0, new[] { ("https://localhost:20000/cluster0/api/health/", 2), ("https://localhost:20001/cluster0/api/health/", 2) }, policyCallTimes: 2); VerifySentProbeAndResult(cluster2, httpClient2, policy1, new[] { ("https://localhost:10000/cluster2/api/health/", 1), ("https://localhost:10001/cluster2/api/health/", 1) }, policyCallTimes: 2); GC.KeepAlive(monitor); // The timer does not keep a strong reference to the scheduler } [Fact] public async Task ProbeCluster_ClusterChanged_StopSendingProbes() { var policy0 = new Mock(); policy0.SetupGet(p => p.Name).Returns("policy0"); var policy1 = new Mock(); policy1.SetupGet(p => p.Name).Returns("policy1"); var options = Options.Create(new ActiveHealthCheckMonitorOptions { DefaultInterval = TimeSpan.FromSeconds(60), DefaultTimeout = TimeSpan.FromSeconds(5) }); var timeProvider = new TestTimeProvider(); var monitor = new ActiveHealthCheckMonitor(options, new[] { policy0.Object, policy1.Object }, new DefaultProbingRequestFactory(), timeProvider, GetLogger()); var httpClient0 = GetHttpClient(); var cluster0 = GetClusterInfo("cluster0", "policy0", true, httpClient0.Object, interval: Interval0); monitor.OnClusterAdded(cluster0); var httpClient2 = GetHttpClient(); var cluster2 = GetClusterInfo("cluster2", "policy1", true, httpClient2.Object, interval: Interval1); monitor.OnClusterAdded(cluster2); Assert.False(monitor.InitialProbeCompleted); await monitor.CheckHealthAsync(Array.Empty()); Assert.True(monitor.InitialProbeCompleted); timeProvider.FireAllTimers(); Assert.Equal(2, timeProvider.TimerCount); timeProvider.VerifyTimer(0, Interval0); timeProvider.VerifyTimer(1, Interval1); VerifySentProbeAndResult(cluster0, httpClient0, policy0, new[] { ("https://localhost:20000/cluster0/api/health/", 1), ("https://localhost:20001/cluster0/api/health/", 1) }, policyCallTimes: 1); VerifySentProbeAndResult(cluster2, httpClient2, policy1, new[] { ("https://localhost:20000/cluster2/api/health/", 1), ("https://localhost:20001/cluster2/api/health/", 1) }, policyCallTimes: 1); var healthCheckConfig = new HealthCheckConfig { Passive = new PassiveHealthCheckConfig { Enabled = true, Policy = "passive0", }, Active = new ActiveHealthCheckConfig { Policy = cluster2.Model.Config.HealthCheck.Active.Policy, } }; cluster2.Model = new ClusterModel(new ClusterConfig { ClusterId = cluster2.ClusterId, HealthCheck = healthCheckConfig }, cluster2.Model.HttpClient); monitor.OnClusterChanged(cluster2); timeProvider.FireTimer(0); timeProvider.AssertTimerDisposed(1); VerifySentProbeAndResult(cluster0, httpClient0, policy0, new[] { ("https://localhost:20000/cluster0/api/health/", 2), ("https://localhost:20001/cluster0/api/health/", 2) }, policyCallTimes: 2); GC.KeepAlive(monitor); // The timer does not keep a strong reference to the scheduler } [Fact] public async Task ProbeCluster_UnsuccessfulResponseReceivedOrExceptionThrown_ReportItToPolicy() { var policy = new Mock(); policy.SetupGet(p => p.Name).Returns("policy0"); var options = Options.Create(new ActiveHealthCheckMonitorOptions { DefaultInterval = TimeSpan.FromSeconds(60), DefaultTimeout = TimeSpan.FromSeconds(5) }); var clusters = new List(); var monitor = new ActiveHealthCheckMonitor(options, new[] { policy.Object }, new DefaultProbingRequestFactory(), new Mock().Object, GetLogger()); var httpClient = new Mock(() => new HttpMessageInvoker(new Mock().Object)); httpClient.Setup(c => c.SendAsync(It.IsAny(), It.IsAny())) .Returns((HttpRequestMessage m, CancellationToken t) => GetResponse(m, t)); var cluster = GetClusterInfo("cluster0", "policy0", true, httpClient.Object, destinationCount: 3); clusters.Add(cluster); Assert.False(monitor.InitialProbeCompleted); await monitor.CheckHealthAsync(clusters); Assert.True(monitor.InitialProbeCompleted); policy.Verify( p => p.ProbingCompleted( cluster, It.Is>( r => r.Count == 3 && r.Single(i => i.Destination.DestinationId == "destination0").Response.StatusCode == HttpStatusCode.InternalServerError && r.Single(i => i.Destination.DestinationId == "destination0").Exception == null && r.Single(i => i.Destination.DestinationId == "destination1").Response == null && r.Single(i => i.Destination.DestinationId == "destination1").Exception.GetType() == typeof(InvalidOperationException) && r.Single(i => i.Destination.DestinationId == "destination2").Response.StatusCode == HttpStatusCode.OK && r.Single(i => i.Destination.DestinationId == "destination2").Exception == null)), Times.Once); policy.Verify(p => p.Name); policy.VerifyNoOtherCalls(); GC.KeepAlive(monitor); // The timer does not keep a strong reference to the scheduler async Task GetResponse(HttpRequestMessage m, CancellationToken t) { return await Task.Run(() => { switch (m.RequestUri.AbsoluteUri) { case "https://localhost:20000/cluster0/api/health/": return new HttpResponseMessage(HttpStatusCode.InternalServerError) { Version = m.Version }; case "https://localhost:20001/cluster0/api/health/": throw new InvalidOperationException(); default: return new HttpResponseMessage(HttpStatusCode.OK) { Version = m.Version }; } }); } } [Fact] public async Task ForceCheckAll_PolicyThrowsException_SkipItAndSetIsFullyInitializedFlag() { var policy = new Mock(); policy.SetupGet(p => p.Name).Returns("policy0"); policy.Setup(p => p.ProbingCompleted(It.IsAny(), It.IsAny>())).Throws(); var options = Options.Create(new ActiveHealthCheckMonitorOptions { DefaultInterval = TimeSpan.FromSeconds(60), DefaultTimeout = TimeSpan.FromSeconds(5) }); var clusters = new List(); var monitor = new ActiveHealthCheckMonitor(options, new[] { policy.Object }, new DefaultProbingRequestFactory(), new Mock().Object, GetLogger()); var httpClient = GetHttpClient(); var cluster = GetClusterInfo("cluster0", "policy0", true, httpClient.Object); clusters.Add(cluster); Assert.False(monitor.InitialProbeCompleted); await monitor.CheckHealthAsync(clusters); Assert.True(monitor.InitialProbeCompleted); policy.Verify(p => p.ProbingCompleted(It.IsAny(), It.IsAny>()), Times.Once); policy.Verify(p => p.Name); policy.VerifyNoOtherCalls(); } [Theory] [InlineData(HttpStatusCode.OK, HttpStatusCode.OK, HttpStatusCode.OK)] [InlineData(HttpStatusCode.InternalServerError, HttpStatusCode.OK, HttpStatusCode.OK)] [InlineData(HttpStatusCode.InternalServerError, HttpStatusCode.InternalServerError, HttpStatusCode.InternalServerError)] [InlineData(HttpStatusCode.OK, HttpStatusCode.InternalServerError, HttpStatusCode.OK)] [InlineData(HttpStatusCode.BadRequest, HttpStatusCode.OK, HttpStatusCode.OK)] [InlineData(HttpStatusCode.OK, HttpStatusCode.OK, HttpStatusCode.BadRequest)] public async Task InitialDestinationsProbed_TrueAfterTheFirstProbe_AllReturns(HttpStatusCode firstResult, HttpStatusCode secondResult, HttpStatusCode thirdResult) { var policy = new Mock(); policy.SetupGet(p => p.Name).Returns("policy0"); var options = Options.Create(new ActiveHealthCheckMonitorOptions { DefaultInterval = TimeSpan.FromSeconds(60), DefaultTimeout = Timeout.InfiniteTimeSpan }); var clusters = new List(); var monitor = new ActiveHealthCheckMonitor(options, new[] { policy.Object }, new DefaultProbingRequestFactory(), new Mock().Object, GetLogger()); var tcs0 = new TaskCompletionSource(); var httpClient0 = GetHttpClient(tcs0.Task); var cluster0 = GetClusterInfo("cluster0", "policy0", true, httpClient0.Object, destinationCount: 1); clusters.Add(cluster0); var tcs1 = new TaskCompletionSource(); var httpClient1 = GetHttpClient(tcs1.Task); var cluster1 = GetClusterInfo("cluster1", "policy0", true, httpClient1.Object, destinationCount: 1); clusters.Add(cluster1); var tcs2 = new TaskCompletionSource(); var httpClient2 = GetHttpClient(tcs2.Task); var cluster2 = GetClusterInfo("cluster2", "policy0", true, httpClient2.Object, destinationCount: 1); clusters.Add(cluster2); Assert.False(monitor.InitialProbeCompleted); var healthCheckTask = monitor.CheckHealthAsync(clusters); Assert.False(healthCheckTask.IsCompleted); Assert.False(monitor.InitialProbeCompleted); tcs0.SetResult(new HttpResponseMessage(firstResult)); Assert.False(healthCheckTask.IsCompleted); Assert.False(monitor.InitialProbeCompleted); tcs1.SetResult(new HttpResponseMessage(secondResult)); Assert.False(healthCheckTask.IsCompleted); Assert.False(monitor.InitialProbeCompleted); tcs2.SetResult(new HttpResponseMessage(thirdResult)); await healthCheckTask; Assert.True(monitor.InitialProbeCompleted); policy.Verify(p => p.ProbingCompleted(It.IsAny(), It.IsAny>()), Times.Exactly(3)); policy.Verify(p => p.Name); policy.VerifyNoOtherCalls(); } [Fact] public async Task InitialDestinationsProbed_TrueAfterTheFirstProbe_OneTimesOut() { var policy = new Mock(); policy.SetupGet(p => p.Name).Returns("policy0"); var options = Options.Create(new ActiveHealthCheckMonitorOptions { DefaultInterval = TimeSpan.FromSeconds(60), DefaultTimeout = TimeSpan.FromMilliseconds(1) }); var clusters = new List(); var monitor = new ActiveHealthCheckMonitor(options, new[] { policy.Object }, new DefaultProbingRequestFactory(), new Mock().Object, GetLogger()); var assertsCompletedMre = new ManualResetEventSlim(false); var tcs0 = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var httpClient0 = GetHttpClient(tcs0.Task); var cluster0 = GetClusterInfo("cluster0", "policy0", true, httpClient0.Object, destinationCount: 1); clusters.Add(cluster0); var tcs1 = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var httpClient1 = GetHttpClient(tcs1.Task, () => { assertsCompletedMre.Wait(); tcs1.SetCanceled(); }); var cluster1 = GetClusterInfo("cluster1", "policy0", true, httpClient1.Object, destinationCount: 1); clusters.Add(cluster1); Assert.False(monitor.InitialProbeCompleted); var healthCheckTask = monitor.CheckHealthAsync(clusters); Assert.False(healthCheckTask.IsCompleted); Assert.False(monitor.InitialProbeCompleted); tcs0.SetResult(new HttpResponseMessage(HttpStatusCode.OK)); Assert.False(healthCheckTask.IsCompleted); Assert.False(monitor.InitialProbeCompleted); assertsCompletedMre.Set(); // Never set result to the second destination for it to time out. await healthCheckTask; Assert.True(monitor.InitialProbeCompleted); policy.Verify(p => p.ProbingCompleted(It.IsAny(), It.IsAny>()), Times.Exactly(2)); policy.Verify(p => p.Name); policy.VerifyNoOtherCalls(); } [Fact] public async Task InitialDestinationsProbed_TrueAfterTheFirstProbe_AllTimeOut() { var policy = new Mock(); policy.SetupGet(p => p.Name).Returns("policy0"); var options = Options.Create(new ActiveHealthCheckMonitorOptions { DefaultInterval = TimeSpan.FromSeconds(60), DefaultTimeout = TimeSpan.FromMilliseconds(1) }); var clusters = new List(); var monitor = new ActiveHealthCheckMonitor(options, new[] { policy.Object }, new DefaultProbingRequestFactory(), new Mock().Object, GetLogger()); var tcs0 = new TaskCompletionSource(); var httpClient0 = GetHttpClient(tcs0.Task, () => tcs0.SetCanceled()); var cluster0 = GetClusterInfo("cluster0", "policy0", true, httpClient0.Object, destinationCount: 1); clusters.Add(cluster0); var tcs1 = new TaskCompletionSource(); var httpClient1 = GetHttpClient(tcs1.Task, () => tcs1.SetCanceled()); var cluster1 = GetClusterInfo("cluster1", "policy0", true, httpClient1.Object, destinationCount: 1); clusters.Add(cluster1); Assert.False(monitor.InitialProbeCompleted); var healthCheckTask = monitor.CheckHealthAsync(clusters); // Never set results to the either of the destination for them to time out. await healthCheckTask; Assert.True(monitor.InitialProbeCompleted); policy.Verify(p => p.ProbingCompleted(It.IsAny(), It.IsAny>()), Times.Exactly(2)); policy.Verify(p => p.Name); policy.VerifyNoOtherCalls(); } [Fact] public async Task InitialDestinationsProbed_TrueAfterTheFirstProbe_OneThrows() { var policy = new Mock(); policy.SetupGet(p => p.Name).Returns("policy0"); var options = Options.Create(new ActiveHealthCheckMonitorOptions { DefaultInterval = TimeSpan.FromSeconds(60), DefaultTimeout = Timeout.InfiniteTimeSpan }); var clusters = new List(); var monitor = new ActiveHealthCheckMonitor(options, new[] { policy.Object }, new DefaultProbingRequestFactory(), new Mock().Object, GetLogger()); var tcs0 = new TaskCompletionSource(); var httpClient0 = GetHttpClient(tcs0.Task); var cluster0 = GetClusterInfo("cluster0", "policy0", true, httpClient0.Object, destinationCount: 1); clusters.Add(cluster0); var tcs1 = new TaskCompletionSource(); var httpClient1 = GetHttpClient(tcs1.Task); var cluster1 = GetClusterInfo("cluster1", "policy0", true, httpClient1.Object, destinationCount: 1); clusters.Add(cluster1); Assert.False(monitor.InitialProbeCompleted); var healthCheckTask = monitor.CheckHealthAsync(clusters); Assert.False(healthCheckTask.IsCompleted); Assert.False(monitor.InitialProbeCompleted); tcs0.SetException(new Exception()); Assert.False(healthCheckTask.IsCompleted); Assert.False(monitor.InitialProbeCompleted); tcs1.SetResult(new HttpResponseMessage(HttpStatusCode.OK)); await healthCheckTask; Assert.True(monitor.InitialProbeCompleted); policy.Verify(p => p.ProbingCompleted(It.IsAny(), It.IsAny>()), Times.Exactly(2)); policy.Verify(p => p.Name); policy.VerifyNoOtherCalls(); } [Fact] public async Task InitialDestinationsProbed_TrueAfterTheFirstProbe_AllThrow() { var policy = new Mock(); policy.SetupGet(p => p.Name).Returns("policy0"); var options = Options.Create(new ActiveHealthCheckMonitorOptions { DefaultInterval = TimeSpan.FromSeconds(60), DefaultTimeout = Timeout.InfiniteTimeSpan }); var clusters = new List(); var monitor = new ActiveHealthCheckMonitor(options, new[] { policy.Object }, new DefaultProbingRequestFactory(), new Mock().Object, GetLogger()); var tcs0 = new TaskCompletionSource(); var httpClient0 = GetHttpClient(tcs0.Task); var cluster0 = GetClusterInfo("cluster0", "policy0", true, httpClient0.Object, destinationCount: 1); clusters.Add(cluster0); var tcs1 = new TaskCompletionSource(); var httpClient1 = GetHttpClient(tcs1.Task); var cluster1 = GetClusterInfo("cluster1", "policy0", true, httpClient1.Object, destinationCount: 1); clusters.Add(cluster1); Assert.False(monitor.InitialProbeCompleted); var healthCheckTask = monitor.CheckHealthAsync(clusters); Assert.False(healthCheckTask.IsCompleted); Assert.False(monitor.InitialProbeCompleted); tcs0.SetException(new Exception()); Assert.False(healthCheckTask.IsCompleted); Assert.False(monitor.InitialProbeCompleted); tcs1.SetException(new Exception()); await healthCheckTask; Assert.True(monitor.InitialProbeCompleted); policy.Verify(p => p.ProbingCompleted(It.IsAny(), It.IsAny>()), Times.Exactly(2)); policy.Verify(p => p.Name); policy.VerifyNoOtherCalls(); } private static void VerifySentProbeAndResult(ClusterState cluster, Mock httpClient, Mock policy, (string RequestUri, int Times)[] probes, int policyCallTimes = 1, string userAgent = @"^YARP\/\S+? \([^\)]+?\)$") { foreach (var probe in probes) { httpClient.Verify( c => c.SendAsync( It.Is(m => m.RequestUri.AbsoluteUri == probe.RequestUri && Regex.IsMatch(m.Headers.UserAgent.ToString(), userAgent)), It.IsAny()), Times.Exactly(probe.Times)); } httpClient.VerifyNoOtherCalls(); policy.Verify( p => p.ProbingCompleted( cluster, It.Is>(r => cluster.Destinations.Values.All(d => r.Any(i => i.Destination == d && i.Response.StatusCode == HttpStatusCode.OK)))), Times.Exactly(policyCallTimes)); policy.Verify(p => p.Name); policy.VerifyNoOtherCalls(); } private ClusterState GetClusterInfo(string id, string policy, bool activeCheckEnabled, HttpMessageInvoker httpClient, TimeSpan? interval = null, TimeSpan? timeout = null, int destinationCount = 2) { var clusterModel = new ClusterModel( new ClusterConfig { ClusterId = id, HealthCheck = new HealthCheckConfig { Active = new ActiveHealthCheckConfig { Enabled = activeCheckEnabled, Interval = interval, Timeout = timeout, Policy = policy, Path = "/api/health/", } } }, httpClient); var clusterState = new ClusterState(id); clusterState.Model = clusterModel; for (var i = 0; i < destinationCount; i++) { var destinationModel = new DestinationModel(new DestinationConfig { Address = $"https://localhost:1000{i}/{id}/", Health = $"https://localhost:2000{i}/{id}/" }); var destinationId = $"destination{i}"; clusterState.Destinations.GetOrAdd(destinationId, id => new DestinationState(id) { Model = destinationModel }); } clusterState.DestinationsState = new ClusterDestinationsState(clusterState.Destinations.Values.ToList(), clusterState.Destinations.Values.ToList()); return clusterState; } private Mock GetHttpClient(Task task = null, Action cancellation = null) { var httpClient = new Mock(() => new HttpMessageInvoker(new Mock().Object)); httpClient.Setup(c => c.SendAsync(It.IsAny(), It.IsAny())) .Returns((HttpRequestMessage m, CancellationToken c) => { if (cancellation is not null) { c.Register(_ => cancellation(), null); } return task ?? Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) { Version = m.Version }); }); return httpClient; } private static ILogger GetLogger() { return new Mock>().Object; } } ================================================ FILE: test/ReverseProxy.Tests/Health/ClusterDestinationsUpdaterTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; using System.Linq; using System.Net.Http; using Moq; using Xunit; using Yarp.ReverseProxy.Configuration; using Yarp.ReverseProxy.Model; namespace Yarp.ReverseProxy.Health.Tests; public class ClusterDestinationsUpdaterTests { [Fact] public void UpdateAllDestinations_UseDestinationsCollectionAsSource() { var cluster = GetCluster("policy1"); var destination0 = cluster.Destinations.GetOrAdd("d0", id => new DestinationState(id)); var destination1 = cluster.Destinations.GetOrAdd("d1", id => new DestinationState(id)); var destination2 = cluster.Destinations.GetOrAdd("d2", id => new DestinationState(id)); var expectedAll = new[] { destination0, destination1, destination2 }; var expectedAvailable = new[] { destination0, destination2 }; var policy0 = new StubPolicy("policy0", destination1); var policy1 = new StubPolicy("policy1", destination1); var updater = new ClusterDestinationsUpdater(new[] { policy0, policy1 }); updater.UpdateAllDestinations(cluster); AssertEquals(expectedAll, cluster.DestinationsState.AllDestinations); AssertEquals(expectedAvailable, cluster.DestinationsState.AvailableDestinations); Assert.False(policy0.IsCalled); Assert.Null(policy0.TakenDestinations); Assert.True(policy1.IsCalled); AssertEquals(expectedAll, policy1.TakenDestinations); } [Fact] public void UpdateAvailableDestinations_UseAllDestinationsAsSource() { var cluster = GetCluster("policy1"); var allDestinations = new[] { new DestinationState("d0"), new DestinationState("d1"), new DestinationState("d2") }; cluster.DestinationsState = new ClusterDestinationsState(allDestinations, new[] { allDestinations[0], allDestinations[1] }); var expectedAvailable = new[] { allDestinations[0], allDestinations[2] }; var policy0 = new StubPolicy("policy0", allDestinations[1]); var policy1 = new StubPolicy("policy1", allDestinations[1]); var updater = new ClusterDestinationsUpdater(new[] { policy0, policy1 }); updater.UpdateAvailableDestinations(cluster); Assert.Empty(cluster.Destinations); AssertEquals(allDestinations, cluster.DestinationsState.AllDestinations); AssertEquals(expectedAvailable, cluster.DestinationsState.AvailableDestinations); Assert.False(policy0.IsCalled); Assert.Null(policy0.TakenDestinations); Assert.True(policy1.IsCalled); AssertEquals(allDestinations, policy1.TakenDestinations); } private static void AssertEquals(IEnumerable actual, IEnumerable expected) { Assert.Equal(actual.OrderBy(d => d.DestinationId).Select(d => d.DestinationId), expected.OrderBy(d => d.DestinationId).Select(d => d.DestinationId)); } private static ClusterState GetCluster(string policyName) { var cluster = new ClusterState("cluster1") { Model = new ClusterModel( new ClusterConfig { ClusterId = "cluster1", HealthCheck = new HealthCheckConfig { AvailableDestinationsPolicy = policyName } }, httpClient: new HttpMessageInvoker(new Mock().Object)) }; return cluster; } private class StubPolicy : IAvailableDestinationsPolicy { private readonly DestinationState _skipDestination; public bool IsCalled { get; private set; } public IReadOnlyList TakenDestinations { get; private set; } public StubPolicy(string name, DestinationState skipDestination) { Name = name; _skipDestination = skipDestination; } public string Name { get; } public IReadOnlyList GetAvailableDestinations(ClusterConfig config, IReadOnlyList allDestinations) { IsCalled = true; TakenDestinations = allDestinations; return allDestinations.Where(p => p != _skipDestination).ToArray(); } } } ================================================ FILE: test/ReverseProxy.Tests/Health/ConsecutiveFailuresHealthPolicyTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; using Microsoft.Extensions.Options; using Xunit; using Yarp.ReverseProxy.Configuration; using Yarp.ReverseProxy.Model; namespace Yarp.ReverseProxy.Health.Tests; public class ConsecutiveFailuresHealthPolicyTests { [Fact] public void ProbingCompleted_FailureThresholdExceeded_MarkDestinationUnhealthy() { var options = Options.Create(new ConsecutiveFailuresHealthPolicyOptions { DefaultThreshold = 2 }); var policy = new ConsecutiveFailuresHealthPolicy(options, new DestinationHealthUpdaterStub()); var cluster0 = GetClusterInfo("cluster0", destinationCount: 2); var cluster1 = GetClusterInfo("cluster1", destinationCount: 2, failureThreshold: 3); var probingResults0 = new[] { new DestinationProbingResult(cluster0.Destinations.Values.First(), new HttpResponseMessage(HttpStatusCode.InternalServerError), null), new DestinationProbingResult(cluster0.Destinations.Values.Skip(1).First(), new HttpResponseMessage(HttpStatusCode.OK), null) }; var probingResults1 = new[] { new DestinationProbingResult(cluster1.Destinations.Values.First(), new HttpResponseMessage(HttpStatusCode.OK), null), new DestinationProbingResult(cluster1.Destinations.Values.Skip(1).First(), null, new InvalidOperationException()) }; Assert.Equal(HealthCheckConstants.ActivePolicy.ConsecutiveFailures, policy.Name); // Initial state Assert.All(cluster0.Destinations.Values, d => Assert.Equal(DestinationHealth.Unknown, d.Health.Active)); Assert.All(cluster1.Destinations.Values, d => Assert.Equal(DestinationHealth.Unknown, d.Health.Active)); // First probing attempt policy.ProbingCompleted(cluster0, probingResults0); Assert.Equal(DestinationHealth.Unknown, cluster0.Destinations.Values.First().Health.Active); Assert.Equal(DestinationHealth.Healthy, cluster0.Destinations.Values.Skip(1).First().Health.Active); policy.ProbingCompleted(cluster1, probingResults1); Assert.Equal(DestinationHealth.Healthy, cluster1.Destinations.Values.First().Health.Active); Assert.Equal(DestinationHealth.Unknown, cluster1.Destinations.Values.Skip(1).First().Health.Active); // Second probing attempt policy.ProbingCompleted(cluster0, probingResults0); Assert.Equal(DestinationHealth.Unhealthy, cluster0.Destinations.Values.First().Health.Active); Assert.Equal(DestinationHealth.Healthy, cluster0.Destinations.Values.Skip(1).First().Health.Active); policy.ProbingCompleted(cluster1, probingResults1); Assert.Equal(DestinationHealth.Healthy, cluster1.Destinations.Values.First().Health.Active); Assert.Equal(DestinationHealth.Unknown, cluster1.Destinations.Values.Skip(1).First().Health.Active); // Third probing attempt policy.ProbingCompleted(cluster0, probingResults0); Assert.Equal(DestinationHealth.Unhealthy, cluster0.Destinations.Values.First().Health.Active); Assert.Equal(DestinationHealth.Healthy, cluster0.Destinations.Values.Skip(1).First().Health.Active); policy.ProbingCompleted(cluster1, probingResults1); Assert.Equal(DestinationHealth.Healthy, cluster1.Destinations.Values.First().Health.Active); Assert.Equal(DestinationHealth.Unhealthy, cluster1.Destinations.Values.Skip(1).First().Health.Active); Assert.All(cluster0.Destinations.Values, d => Assert.Equal(DestinationHealth.Unknown, d.Health.Passive)); Assert.All(cluster1.Destinations.Values, d => Assert.Equal(DestinationHealth.Unknown, d.Health.Passive)); } [Fact] public void ProbingCompleted_SuccessfulResponse_MarkDestinationHealthy() { var options = Options.Create(new ConsecutiveFailuresHealthPolicyOptions { DefaultThreshold = 2 }); var policy = new ConsecutiveFailuresHealthPolicy(options, new DestinationHealthUpdaterStub()); var cluster = GetClusterInfo("cluster0", destinationCount: 2); var probingResults = new[] { new DestinationProbingResult(cluster.Destinations.Values.First(), new HttpResponseMessage(HttpStatusCode.InternalServerError), null), new DestinationProbingResult(cluster.Destinations.Values.Skip(1).First(), new HttpResponseMessage(HttpStatusCode.OK), null) }; for (var i = 0; i < 2; i++) { policy.ProbingCompleted(cluster, probingResults); } Assert.Equal(DestinationHealth.Unhealthy, cluster.Destinations.Values.First().Health.Active); Assert.Equal(DestinationHealth.Healthy, cluster.Destinations.Values.Skip(1).First().Health.Active); policy.ProbingCompleted(cluster, new[] { new DestinationProbingResult(cluster.Destinations.Values.First(), new HttpResponseMessage(HttpStatusCode.OK), null) }); Assert.Equal(DestinationHealth.Healthy, cluster.Destinations.Values.First().Health.Active); Assert.Equal(DestinationHealth.Healthy, cluster.Destinations.Values.Skip(1).First().Health.Active); Assert.All(cluster.Destinations.Values, d => Assert.Equal(DestinationHealth.Unknown, d.Health.Passive)); } [Fact] public void ProbingCompleted_EmptyProbingResultList_DoNothing() { var options = Options.Create(new ConsecutiveFailuresHealthPolicyOptions { DefaultThreshold = 2 }); var policy = new ConsecutiveFailuresHealthPolicy(options, new DestinationHealthUpdaterStub()); var cluster = GetClusterInfo("cluster0", destinationCount: 2); var probingResults = new[] { new DestinationProbingResult(cluster.Destinations.Values.First(), new HttpResponseMessage(HttpStatusCode.InternalServerError), null), new DestinationProbingResult(cluster.Destinations.Values.Skip(1).First(), new HttpResponseMessage(HttpStatusCode.OK), null) }; for (var i = 0; i < 2; i++) { policy.ProbingCompleted(cluster, probingResults); } Assert.Equal(DestinationHealth.Unhealthy, cluster.Destinations.Values.First().Health.Active); Assert.Equal(DestinationHealth.Healthy, cluster.Destinations.Values.Skip(1).First().Health.Active); policy.ProbingCompleted(cluster, Array.Empty()); Assert.Equal(DestinationHealth.Unhealthy, cluster.Destinations.Values.First().Health.Active); Assert.Equal(DestinationHealth.Healthy, cluster.Destinations.Values.Skip(1).First().Health.Active); } private ClusterState GetClusterInfo(string id, int destinationCount, int? failureThreshold = null) { var metadata = failureThreshold is not null ? new Dictionary { { ConsecutiveFailuresHealthPolicyOptions.ThresholdMetadataName, failureThreshold.ToString() } } : null; var clusterModel = new ClusterModel( new ClusterConfig { ClusterId = id, HealthCheck = new HealthCheckConfig() { Active = new ActiveHealthCheckConfig { Enabled = true, Policy = "policy", Path = "/api/health/", }, }, Metadata = metadata, }, new HttpMessageInvoker(new HttpClientHandler())); var clusterState = new ClusterState(id); clusterState.Model = clusterModel; for (var i = 0; i < destinationCount; i++) { var destinationModel = new DestinationModel(new DestinationConfig { Address = $"https://localhost:1000{i}/{id}/", Health = $"https://localhost:2000{i}/{id}/" }); var destinationId = $"destination{i}"; clusterState.Destinations.GetOrAdd(destinationId, id => new DestinationState(id) { Model = destinationModel }); } clusterState.DestinationsState = new ClusterDestinationsState(clusterState.Destinations.Values.ToList(), clusterState.Destinations.Values.ToList()); return clusterState; } private class DestinationHealthUpdaterStub : IDestinationHealthUpdater { public void SetActive(ClusterState cluster, IEnumerable newHealthStates) { foreach (var newHealthState in newHealthStates) { newHealthState.Destination.Health.Active = newHealthState.NewActiveHealth; } var destinations = cluster.Destinations.Values.ToList(); cluster.DestinationsState = new ClusterDestinationsState(destinations, destinations); } public void SetPassive(ClusterState cluster, DestinationState destination, DestinationHealth newHealth, TimeSpan reactivationPeriod) { throw new NotImplementedException(); } } } ================================================ FILE: test/ReverseProxy.Tests/Health/DefaultProbingRequestFactoryTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Net; using System.Net.Http; using Xunit; using Yarp.ReverseProxy.Configuration; using Yarp.ReverseProxy.Model; using Yarp.ReverseProxy.Forwarder; namespace Yarp.ReverseProxy.Health.Tests; public class DefaultProbingRequestFactoryTests { [Theory] [InlineData("https://localhost:10000/", null, null, null, "https://localhost:10000/")] [InlineData("https://localhost:10000/", "https://localhost:20000/", null, null, "https://localhost:20000/")] [InlineData("https://localhost:10000/", null, "/api/health/", null, "https://localhost:10000/api/health/")] [InlineData("https://localhost:10000/", "https://localhost:20000/", "/api/health/", null, "https://localhost:20000/api/health/")] [InlineData("https://localhost:10000/api", "https://localhost:20000/", "/health/", null, "https://localhost:20000/health/")] [InlineData("https://localhost:10000/", "https://localhost:20000/api", "/health/", null, "https://localhost:20000/api/health/")] [InlineData("https://localhost:10000/", null, null, "?key=value", "https://localhost:10000/?key=value")] [InlineData("https://localhost:10000/", "https://localhost:20000/", null, "?key=value", "https://localhost:20000/?key=value")] [InlineData("https://localhost:10000/", null, "/api/health/", "?key=value", "https://localhost:10000/api/health/?key=value")] [InlineData("https://localhost:10000/", "https://localhost:20000/", "/api/health/", "?key=value", "https://localhost:20000/api/health/?key=value")] [InlineData("https://localhost:10000/api", "https://localhost:20000/", "/health/", "?key=value", "https://localhost:20000/health/?key=value")] [InlineData("https://localhost:10000/", "https://localhost:20000/api", "/health/", "?key=value", "https://localhost:20000/api/health/?key=value")] [InlineData("https://localhost:10000/", "https://localhost:20000/api", "/health?foo=bar", "?key=value", "https://localhost:20000/api/health%3Ffoo=bar?key=value")] public void CreateRequest_HealthEndpointIsNotDefined_UseDestinationAddress(string address, string health, string healthPath, string query, string expectedRequestUri) { var clusterModel = GetClusterConfig("cluster0", new ActiveHealthCheckConfig() { Enabled = true, Policy = "policy", Path = healthPath, Query = query, }, HttpVersion.Version20); var destinationModel = new DestinationModel(new DestinationConfig { Address = address, Health = health }); var factory = new DefaultProbingRequestFactory(); var request = factory.CreateRequest(clusterModel, destinationModel); Assert.Equal(expectedRequestUri, request.RequestUri.AbsoluteUri); } [Theory] [InlineData("1.0")] [InlineData(null)] public void CreateRequest_RequestVersionProperties(string versionString) { var version = versionString is not null ? Version.Parse(versionString) : null; var clusterModel = GetClusterConfig("cluster0", new ActiveHealthCheckConfig() { Enabled = true, Policy = "policy", }, version, HttpVersionPolicy.RequestVersionExact); var destinationModel = new DestinationModel(new DestinationConfig { Address = "https://localhost:10000/" }); var factory = new DefaultProbingRequestFactory(); var request = factory.CreateRequest(clusterModel, destinationModel); Assert.Equal(version ?? HttpVersion.Version20, request.Version); Assert.Equal(HttpVersionPolicy.RequestVersionExact, request.VersionPolicy); } private ClusterModel GetClusterConfig(string id, ActiveHealthCheckConfig healthCheckOptions, Version version, HttpVersionPolicy versionPolicy = HttpVersionPolicy.RequestVersionExact) { return new ClusterModel( new ClusterConfig { ClusterId = id, HealthCheck = new HealthCheckConfig() { Active = healthCheckOptions, }, HttpRequest = new ForwarderRequestConfig { ActivityTimeout = TimeSpan.FromSeconds(60), Version = version, VersionPolicy = versionPolicy, } }, new HttpMessageInvoker(new HttpClientHandler())); } } ================================================ FILE: test/ReverseProxy.Tests/Health/DestinationHealthUpdaterTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Linq; using System.Net.Http; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Moq; using Xunit; using Yarp.Tests.Common; using Yarp.ReverseProxy.Configuration; using Yarp.ReverseProxy.Model; namespace Yarp.ReverseProxy.Health.Tests; public class DestinationHealthUpdaterTests { [Fact] public async Task SetPassiveAsync_DestinationBecameUnhealthy_SetUnhealthyAndScheduleReactivation() { var destination = new DestinationState("destination0"); destination.Health.Active = DestinationHealth.Healthy; destination.Health.Passive = DestinationHealth.Healthy; var cluster = CreateCluster(passive: true, active: false, destination); var timeProvider = new TestTimeProvider(); var updater = new DestinationHealthUpdater(timeProvider, GetClusterUpdater(), new Mock>().Object); await updater.SetPassiveAsync(cluster, destination, DestinationHealth.Unhealthy, TimeSpan.FromSeconds(2)); timeProvider.VerifyTimer(0, TimeSpan.FromSeconds(2)); Assert.Empty(cluster.DestinationsState.AvailableDestinations); Assert.Equal(DestinationHealth.Healthy, destination.Health.Active); Assert.Equal(DestinationHealth.Unhealthy, destination.Health.Passive); timeProvider.FireAllTimers(); GC.KeepAlive(updater); // The timer does not keep a strong reference to the scheduler Assert.Equal(DestinationHealth.Healthy, destination.Health.Active); Assert.Equal(DestinationHealth.Unknown, destination.Health.Passive); Assert.Single(cluster.DestinationsState.AvailableDestinations); Assert.Same(destination, cluster.DestinationsState.AvailableDestinations[0]); timeProvider.AssertTimerDisposed(0); } [Fact] public async Task SetPassiveAsync_DestinationBecameHealthy_SetNewState() { var destination = new DestinationState("destination0"); destination.Health.Active = DestinationHealth.Healthy; destination.Health.Passive = DestinationHealth.Unhealthy; var cluster = CreateCluster(passive: true, active: false, destination); var timeProvider = new TestTimeProvider(); var updater = new DestinationHealthUpdater(timeProvider, GetClusterUpdater(), new Mock>().Object); await updater.SetPassiveAsync(cluster, destination, DestinationHealth.Healthy, TimeSpan.FromSeconds(2)); Assert.Equal(0, timeProvider.TimerCount); Assert.Equal(DestinationHealth.Healthy, destination.Health.Active); Assert.Equal(DestinationHealth.Healthy, destination.Health.Passive); Assert.Single(cluster.DestinationsState.AvailableDestinations); Assert.Same(destination, cluster.DestinationsState.AvailableDestinations[0]); } [Theory] [InlineData(DestinationHealth.Unhealthy)] [InlineData(DestinationHealth.Healthy)] [InlineData(DestinationHealth.Unknown)] public async Task SetPassiveAsync_HealthSateIsNotChanged_DoNothing(DestinationHealth health) { var destination = new DestinationState("destination0"); destination.Health.Active = DestinationHealth.Healthy; destination.Health.Passive = health; var cluster = CreateCluster(passive: true, active: false, destination); var timeProvider = new TestTimeProvider(); var updater = new DestinationHealthUpdater(timeProvider, GetClusterUpdater(), new Mock>().Object); await updater.SetPassiveAsync(cluster, destination, health, TimeSpan.FromSeconds(2)); Assert.Equal(0, timeProvider.TimerCount); Assert.Equal(DestinationHealth.Healthy, destination.Health.Active); Assert.Equal(health, destination.Health.Passive); } [Fact] public void SetActive_ChangedAndUnchangedHealthStates_SetChangedStates() { var destination0 = new DestinationState("destination0"); destination0.Health.Active = DestinationHealth.Healthy; destination0.Health.Passive = DestinationHealth.Healthy; var destination1 = new DestinationState("destination1"); destination1.Health.Active = DestinationHealth.Healthy; destination1.Health.Passive = DestinationHealth.Healthy; var destination2 = new DestinationState("destination2"); destination2.Health.Active = DestinationHealth.Unhealthy; destination2.Health.Passive = DestinationHealth.Healthy; var destination3 = new DestinationState("destination3"); destination3.Health.Active = DestinationHealth.Unhealthy; destination3.Health.Passive = DestinationHealth.Healthy; var cluster = CreateCluster(passive: false, active: true, destination0, destination1, destination2, destination3); var updater = new DestinationHealthUpdater(new Mock().Object, GetClusterUpdater(), new Mock>().Object); var newHealthStates = new[] { new NewActiveDestinationHealth(destination0, DestinationHealth.Unhealthy), new NewActiveDestinationHealth(destination1, DestinationHealth.Healthy), new NewActiveDestinationHealth(destination2, DestinationHealth.Unhealthy), new NewActiveDestinationHealth(destination3, DestinationHealth.Healthy) }; updater.SetActive(cluster, newHealthStates); foreach (var newHealthState in newHealthStates) { Assert.Equal(newHealthState.NewActiveHealth, newHealthState.Destination.Health.Active); Assert.Equal(DestinationHealth.Healthy, newHealthState.Destination.Health.Passive); } Assert.Equal(2, cluster.DestinationsState.AvailableDestinations.Count); Assert.Contains(cluster.DestinationsState.AvailableDestinations, d => d == destination1); Assert.Contains(cluster.DestinationsState.AvailableDestinations, d => d == destination3); } private static ClusterState CreateCluster(bool passive, bool active, params DestinationState[] destinations) { var cluster = new ClusterState("cluster0"); cluster.Model = new ClusterModel( new ClusterConfig { ClusterId = cluster.ClusterId, HealthCheck = new HealthCheckConfig() { Passive = new PassiveHealthCheckConfig() { Policy = "policy0", Enabled = passive, }, Active = new ActiveHealthCheckConfig() { Enabled = active, Policy = "policy1", }, }, }, new HttpMessageInvoker(new HttpClientHandler())); foreach (var destination in destinations) { cluster.Destinations.TryAdd(destination.DestinationId, destination); } cluster.DestinationsState = new ClusterDestinationsState(destinations, destinations); return cluster; } private IClusterDestinationsUpdater GetClusterUpdater() { var result = new Mock(MockBehavior.Strict); result.Setup(u => u.UpdateAvailableDestinations(It.IsAny())).Callback((ClusterState c) => { var availableDestinations = c.Destinations.Values .Where(d => d.Health.Active != DestinationHealth.Unhealthy && d.Health.Passive != DestinationHealth.Unhealthy) .ToList(); c.DestinationsState = new ClusterDestinationsState(c.DestinationsState.AllDestinations, availableDestinations); }); return result.Object; } } ================================================ FILE: test/ReverseProxy.Tests/Health/EntityActionSchedulerTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Threading; using System.Threading.Tasks; using Xunit; using Yarp.Tests.Common; namespace Yarp.ReverseProxy.Health.Tests; // It uses a real TimerFactory to verify scheduling work E2E. public class EntityActionSchedulerTests { private readonly TimeSpan Period0 = TimeSpan.FromSeconds(20); private readonly TimeSpan Period1 = TimeSpan.FromSeconds(10); [Fact] public void Schedule_AutoStartEnabledRunOnceDisabled_StartsAutomaticallyAndRunsIndefinitely() { var entity0 = new Entity { Id = "entity0" }; var entity1 = new Entity { Id = "entity1" }; var timeProvider = new TestTimeProvider(); Entity lastInvokedEntity = null; using var scheduler = new EntityActionScheduler(e => { lastInvokedEntity = e; return Task.CompletedTask; }, autoStart: true, runOnce: false, timeProvider); scheduler.ScheduleEntity(entity0, TimeSpan.FromMilliseconds(20000)); scheduler.ScheduleEntity(entity1, TimeSpan.FromMilliseconds(10000)); VerifyEntities(scheduler, entity0, entity1); Assert.Equal(2, timeProvider.TimerCount); timeProvider.VerifyTimer(0, Period0); timeProvider.VerifyTimer(1, Period1); timeProvider.FireTimer(1); Assert.Same(entity1, lastInvokedEntity); timeProvider.FireTimer(0); Assert.Same(entity0, lastInvokedEntity); timeProvider.FireTimer(1); Assert.Same(entity1, lastInvokedEntity); timeProvider.FireTimer(0); Assert.Same(entity0, lastInvokedEntity); VerifyEntities(scheduler, entity0, entity1); Assert.Equal(2, timeProvider.TimerCount); timeProvider.VerifyTimer(0, Period0); timeProvider.VerifyTimer(1, Period1); } [Fact] public void Schedule_AutoStartDisabledRunOnceEnabled_StartsManuallyAndRunsEachRegistrationOnlyOnce() { var entity0 = new Entity { Id = "entity0" }; var entity1 = new Entity { Id = "entity1" }; Entity lastInvokedEntity = null; var timeProvider = new TestTimeProvider(); using var scheduler = new EntityActionScheduler(e => { lastInvokedEntity = e; return Task.CompletedTask; }, autoStart: false, runOnce: true, timeProvider); scheduler.ScheduleEntity(entity0, Period0); scheduler.ScheduleEntity(entity1, Period1); Assert.Equal(2, timeProvider.TimerCount); timeProvider.VerifyTimer(0, Timeout.InfiniteTimeSpan); timeProvider.VerifyTimer(1, Timeout.InfiniteTimeSpan); scheduler.Start(); VerifyEntities(scheduler, entity0, entity1); Assert.Equal(2, timeProvider.TimerCount); timeProvider.VerifyTimer(0, Period0); timeProvider.VerifyTimer(1, Period1); timeProvider.FireTimer(1); Assert.Same(entity1, lastInvokedEntity); VerifyEntities(scheduler, entity0); timeProvider.FireTimer(0); Assert.Same(entity0, lastInvokedEntity); Assert.False(scheduler.IsScheduled(entity0)); Assert.False(scheduler.IsScheduled(entity1)); timeProvider.AssertTimerDisposed(0); timeProvider.AssertTimerDisposed(1); } [Fact] public void Unschedule_EntityUnscheduledBeforeFirstCall_CallbackNotInvoked() { var entity0 = new Entity { Id = "entity0" }; var entity1 = new Entity { Id = "entity1" }; Entity lastInvokedEntity = null; var timeProvider = new TestTimeProvider(); using var scheduler = new EntityActionScheduler(e => { lastInvokedEntity = e; return Task.CompletedTask; }, autoStart: false, runOnce: false, timeProvider); scheduler.ScheduleEntity(entity0, Period0); scheduler.ScheduleEntity(entity1, Period1); VerifyEntities(scheduler, entity0, entity1); Assert.Equal(2, timeProvider.TimerCount); timeProvider.VerifyTimer(0, Timeout.InfiniteTimeSpan); timeProvider.VerifyTimer(1, Timeout.InfiniteTimeSpan); scheduler.UnscheduleEntity(entity1); VerifyEntities(scheduler, entity0); timeProvider.AssertTimerDisposed(1); scheduler.Start(); timeProvider.VerifyTimer(0, Period0); timeProvider.FireTimer(0); Assert.Same(entity0, lastInvokedEntity); VerifyEntities(scheduler, entity0); } [Fact] public void Unschedule_EntityUnscheduledAfterFirstCall_CallbackInvokedOnlyOnce() { var entity0 = new Entity { Id = "entity0" }; var entity1 = new Entity { Id = "entity1" }; Entity lastInvokedEntity = null; var timeProvider = new TestTimeProvider(); using var scheduler = new EntityActionScheduler(e => { lastInvokedEntity = e; return Task.CompletedTask; }, autoStart: true, runOnce: false, timeProvider); scheduler.ScheduleEntity(entity0, Period0); scheduler.ScheduleEntity(entity1, Period1); VerifyEntities(scheduler, entity0, entity1); timeProvider.FireTimer(1); Assert.Same(entity1, lastInvokedEntity); timeProvider.FireTimer(0); Assert.Same(entity0, lastInvokedEntity); scheduler.UnscheduleEntity(entity1); VerifyEntities(scheduler, entity0); timeProvider.AssertTimerDisposed(1); timeProvider.FireTimer(0); Assert.Same(entity0, lastInvokedEntity); VerifyEntities(scheduler, entity0); } [Fact] public void ChangePeriod_PeriodChangedTimerNotStarted_PeriodChangedBeforeFirstCall() { var entity = new Entity { Id = "entity0" }; Entity lastInvokedEntity = null; var timeProvider = new TestTimeProvider(); using var scheduler = new EntityActionScheduler(e => { lastInvokedEntity = e; return Task.CompletedTask; }, autoStart: false, runOnce: false, timeProvider); scheduler.ScheduleEntity(entity, Period0); timeProvider.VerifyTimer(0, Timeout.InfiniteTimeSpan); var newPeriod = Period1; scheduler.ChangePeriod(entity, newPeriod); timeProvider.VerifyTimer(0, Timeout.InfiniteTimeSpan); scheduler.Start(); timeProvider.VerifyTimer(0, Period1); timeProvider.FireTimer(0); Assert.Same(entity, lastInvokedEntity); } [Fact] public void ChangePeriod_TimerStartedPeriodChangedAfterFirstCall_PeriodChangedBeforeNextCall() { var entity = new Entity { Id = "entity0" }; Entity lastInvokedEntity = null; var timeProvider = new TestTimeProvider(); using var scheduler = new EntityActionScheduler(e => { lastInvokedEntity = e; return Task.CompletedTask; }, autoStart: true, runOnce: false, timeProvider); scheduler.ScheduleEntity(entity, Period0); timeProvider.VerifyTimer(0, Period0); timeProvider.FireTimer(0); var newPeriod = Period1; scheduler.ChangePeriod(entity, newPeriod); timeProvider.VerifyTimer(0, Period1); Assert.Same(entity, lastInvokedEntity); } private void VerifyEntities(EntityActionScheduler scheduler, params Entity[] entities) { var actualCount = 0; foreach (var entity in entities) { Assert.True(scheduler.IsScheduled(entity)); actualCount++; } Assert.Equal(entities.Length, actualCount); } private class Entity { public string Id { get; set; } } } ================================================ FILE: test/ReverseProxy.Tests/Health/HealthyAndUnknownDestinationsPolicyTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; using Xunit; using Yarp.ReverseProxy.Configuration; using Yarp.ReverseProxy.Model; namespace Yarp.ReverseProxy.Health.Tests; public class HealthyAndUnknownDestinationsPolicyTests { [Fact] public void GetAvailableDestinations_HealthChecksEnabled_FilterOutUnhealthy() { var cluster = new ClusterConfig() { ClusterId = "cluster1", HealthCheck = new HealthCheckConfig { Active = new ActiveHealthCheckConfig { Enabled = true }, Passive = new PassiveHealthCheckConfig { Enabled = true } } }; var allDestinations = new[] { new DestinationState("d1") { Health = { Active = DestinationHealth.Healthy } }, new DestinationState("d2") { Health = { Active = DestinationHealth.Unhealthy } }, new DestinationState("d3") { Health = { Active = DestinationHealth.Unhealthy, Passive = DestinationHealth.Healthy } }, new DestinationState("d4") { Health = { Passive = DestinationHealth.Unhealthy } }, new DestinationState("d5") { Health = { Passive = DestinationHealth.Healthy } }, new DestinationState("d6") { Health = { Active = DestinationHealth.Healthy, Passive = DestinationHealth.Unhealthy } }, new DestinationState("d7") { Health = { Active = DestinationHealth.Unhealthy, Passive = DestinationHealth.Unhealthy } }, new DestinationState("d8") }; var policy = new HealthyAndUnknownDestinationsPolicy(); var availableDestinations = policy.GetAvailableDestinations(cluster, allDestinations); Assert.Equal(3, availableDestinations.Count); Assert.Same(allDestinations[0], availableDestinations[0]); Assert.Same(allDestinations[4], availableDestinations[1]); Assert.Same(allDestinations[7], availableDestinations[2]); } [Theory] [MemberData(nameof(GetDisabledHealthChecksCases))] public void GetAvailableDestinations_HealthChecksDisabled_ReturnAll(HealthCheckConfig config) { var cluster = new ClusterConfig() { ClusterId = "cluster1", HealthCheck = config }; var allDestinations = new[] { new DestinationState("d1") { Health = { Active = DestinationHealth.Healthy } }, new DestinationState("d2") { Health = { Active = DestinationHealth.Unhealthy, Passive = DestinationHealth.Healthy } }, new DestinationState("d3") { Health = { Passive = DestinationHealth.Healthy } }, new DestinationState("d4"), new DestinationState("d5") { Health = { Active = DestinationHealth.Healthy, Passive = DestinationHealth.Unhealthy } }, new DestinationState("d6") { Health = { Active = DestinationHealth.Unhealthy, Passive = DestinationHealth.Unhealthy } } }; var policy = new HealthyAndUnknownDestinationsPolicy(); var availableDestinations = policy.GetAvailableDestinations(cluster, allDestinations); Assert.Equal(6, availableDestinations.Count); Assert.Same(allDestinations[0], availableDestinations[0]); Assert.Same(allDestinations[1], availableDestinations[1]); Assert.Same(allDestinations[2], availableDestinations[2]); Assert.Same(allDestinations[3], availableDestinations[3]); Assert.Same(allDestinations[4], availableDestinations[4]); Assert.Same(allDestinations[5], availableDestinations[5]); } [Theory] [InlineData(true, DestinationHealth.Unhealthy, true, DestinationHealth.Healthy, false)] [InlineData(false, DestinationHealth.Unhealthy, true, DestinationHealth.Healthy, true)] [InlineData(true, DestinationHealth.Healthy, true, DestinationHealth.Unhealthy, false)] [InlineData(true, DestinationHealth.Healthy, false, DestinationHealth.Unhealthy, true)] [InlineData(false, DestinationHealth.Unhealthy, false, DestinationHealth.Unhealthy, true)] [InlineData(true, DestinationHealth.Unhealthy, true, DestinationHealth.Unhealthy, false)] public void GetAvailableDestinations_OneHealthCheckDisabled_UseUnknownState(bool activeEnabled, DestinationHealth active, bool passiveEnabled, DestinationHealth passive, bool isAvailable) { var cluster = new ClusterConfig() { ClusterId = "cluster1", HealthCheck = new HealthCheckConfig { Active = new ActiveHealthCheckConfig { Enabled = activeEnabled }, Passive = new PassiveHealthCheckConfig { Enabled = passiveEnabled } } }; var policy = new HealthyAndUnknownDestinationsPolicy(); var destination = new DestinationState("d0") { Health = { Active = active, Passive = passive } }; var availableDestinations = policy.GetAvailableDestinations(cluster, new[] { destination }); if (isAvailable) { Assert.Single(availableDestinations, destination); } else { Assert.Empty(availableDestinations); } } public static IEnumerable GetDisabledHealthChecksCases() { yield return new[] { new HealthCheckConfig() }; yield return new[] { (object)null }; } } ================================================ FILE: test/ReverseProxy.Tests/Health/HealthyOrPanicDestinationsPolicyTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Xunit; using Yarp.ReverseProxy.Configuration; using Yarp.ReverseProxy.Model; namespace Yarp.ReverseProxy.Health.Tests; public class HealthyOrPanicDestinationsPolicyTests { [Fact] public void GetAvailableDestinations_SomeDestinationsAreHealthy_ReturnOnlyHealthy() { var cluster = GetClusterConfig(); var allDestinations = new[] { new DestinationState("d1") { Health = { Active = DestinationHealth.Healthy } }, new DestinationState("d2") { Health = { Active = DestinationHealth.Unhealthy } }, new DestinationState("d2") { Health = { Passive = DestinationHealth.Healthy } }, new DestinationState("d4") }; var policy = new HealthyOrPanicDestinationsPolicy(); var availableDestinations = policy.GetAvailableDestinations(cluster, allDestinations); Assert.Equal(3, availableDestinations.Count); Assert.Same(allDestinations[0], availableDestinations[0]); Assert.Same(allDestinations[2], availableDestinations[1]); Assert.Same(allDestinations[3], availableDestinations[2]); } [Fact] public void GetAvailableDestinations_AllDestinationsAreUnhealthy_ReturnAll() { var cluster = GetClusterConfig(); var allDestinations = new[] { new DestinationState("d1") { Health = { Active = DestinationHealth.Unhealthy } }, new DestinationState("d2") { Health = { Passive = DestinationHealth.Unhealthy } }, new DestinationState("d2") { Health = { Active = DestinationHealth.Unhealthy, Passive = DestinationHealth.Healthy } }, new DestinationState("d4") { Health = { Active = DestinationHealth.Unhealthy, Passive = DestinationHealth.Unhealthy } } }; var policy = new HealthyOrPanicDestinationsPolicy(); var availableDestinations = policy.GetAvailableDestinations(cluster, allDestinations); Assert.Equal(4, availableDestinations.Count); Assert.Same(allDestinations[0], availableDestinations[0]); Assert.Same(allDestinations[1], availableDestinations[1]); Assert.Same(allDestinations[2], availableDestinations[2]); Assert.Same(allDestinations[3], availableDestinations[3]); } private static ClusterConfig GetClusterConfig() { return new ClusterConfig() { ClusterId = "cluster1", HealthCheck = new HealthCheckConfig { Active = new ActiveHealthCheckConfig { Enabled = true }, Passive = new PassiveHealthCheckConfig { Enabled = true } } }; } } ================================================ FILE: test/ReverseProxy.Tests/Health/PassiveHealthCheckMiddlewareTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Linq; using System.Net.Http; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Moq; using Xunit; using Yarp.ReverseProxy.Configuration; using Yarp.ReverseProxy.Model; using Yarp.ReverseProxy.Forwarder; namespace Yarp.ReverseProxy.Health.Tests; public class PassiveHealthCheckMiddlewareTests { [Fact] public async Task Invoke_PassiveHealthCheckIsEnabled_CallPolicy() { var policies = new[] { GetPolicy("policy0"), GetPolicy("policy1") }; var cluster0 = GetClusterInfo("cluster0", "policy0"); var cluster1 = GetClusterInfo("cluster1", "policy1"); var nextInvoked = false; var middleware = new PassiveHealthCheckMiddleware(c => { nextInvoked = true; return Task.CompletedTask; }, policies.Select(p => p.Object)); var context0 = GetContext(cluster0, selectedDestination: 1, error: null); await middleware.Invoke(context0); Assert.True(nextInvoked); policies[0].Verify(p => p.RequestProxied(context0, cluster0, cluster0.DestinationsState.AllDestinations[1]), Times.Once); policies[0].VerifyGet(p => p.Name, Times.Once); policies[0].VerifyNoOtherCalls(); policies[1].VerifyGet(p => p.Name, Times.Once); policies[1].VerifyNoOtherCalls(); nextInvoked = false; var error = new ForwarderErrorFeature(ForwarderError.Request, null); var context1 = GetContext(cluster1, selectedDestination: 0, error); await middleware.Invoke(context1); Assert.True(nextInvoked); policies[1].Verify(p => p.RequestProxied(context1, cluster1, cluster1.DestinationsState.AllDestinations[0]), Times.Once); policies[1].VerifyNoOtherCalls(); policies[0].VerifyNoOtherCalls(); } [Fact] public async Task Invoke_PassiveHealthCheckIsDisabled_DoNothing() { var policies = new[] { GetPolicy("policy0"), GetPolicy("policy1") }; var cluster0 = GetClusterInfo("cluster0", "policy0", enabled: false); var nextInvoked = false; var middleware = new PassiveHealthCheckMiddleware(c => { nextInvoked = true; return Task.CompletedTask; }, policies.Select(p => p.Object)); var context0 = GetContext(cluster0, selectedDestination: 0, error: null); await middleware.Invoke(context0); Assert.True(nextInvoked); policies[0].VerifyGet(p => p.Name, Times.Once); policies[0].VerifyNoOtherCalls(); policies[1].VerifyGet(p => p.Name, Times.Once); policies[1].VerifyNoOtherCalls(); } [Fact] public async Task Invoke_PassiveHealthCheckIsEnabledButNoDestinationSelected_DoNothing() { var policies = new[] { GetPolicy("policy0"), GetPolicy("policy1") }; var cluster0 = GetClusterInfo("cluster0", "policy0"); var nextInvoked = false; var middleware = new PassiveHealthCheckMiddleware(c => { nextInvoked = true; return Task.CompletedTask; }, policies.Select(p => p.Object)); var context0 = GetContext(cluster0, selectedDestination: 1, error: null); context0.GetReverseProxyFeature().ProxiedDestination = null; await middleware.Invoke(context0); Assert.True(nextInvoked); policies[0].VerifyGet(p => p.Name, Times.Once); policies[0].VerifyNoOtherCalls(); policies[1].VerifyGet(p => p.Name, Times.Once); policies[1].VerifyNoOtherCalls(); } private HttpContext GetContext(ClusterState cluster, int selectedDestination, IForwarderErrorFeature error) { var context = new DefaultHttpContext(); context.Features.Set(GetProxyFeature(cluster, cluster.DestinationsState.AllDestinations[selectedDestination])); context.Features.Set(error); return context; } private Mock GetPolicy(string name) { var policy = new Mock(); policy.SetupGet(p => p.Name).Returns(name); return policy; } private IReverseProxyFeature GetProxyFeature(ClusterState clusterState, DestinationState destination) { return new ReverseProxyFeature() { ProxiedDestination = destination, Cluster = clusterState.Model, Route = new RouteModel(new RouteConfig(), clusterState, HttpTransformer.Default), }; } private ClusterState GetClusterInfo(string id, string policy, bool enabled = true) { var clusterModel = new ClusterModel( new ClusterConfig { ClusterId = id, HealthCheck = new HealthCheckConfig { Passive = new PassiveHealthCheckConfig { Enabled = enabled, Policy = policy, } } }, new HttpMessageInvoker(new HttpClientHandler())); var clusterState = new ClusterState(id); clusterState.Model = clusterModel; clusterState.Destinations.GetOrAdd("destination0", id => new DestinationState(id)); clusterState.Destinations.GetOrAdd("destination1", id => new DestinationState(id)); clusterState.DestinationsState = new ClusterDestinationsState(clusterState.Destinations.Values.ToList(), clusterState.Destinations.Values.ToList()); return clusterState; } } ================================================ FILE: test/ReverseProxy.Tests/Health/TransportFailureRateHealthPolicyTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Net.Http; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Options; using Moq; using Xunit; using Yarp.Tests.Common; using Yarp.ReverseProxy.Configuration; using Yarp.ReverseProxy.Model; using Yarp.ReverseProxy.Forwarder; namespace Yarp.ReverseProxy.Health.Tests; public class TransportFailureRateHealthPolicyTests { [Fact] public void RequestProxied_FailureRateLimitExceeded_MarkDestinationUnhealthy() { var options = Options.Create( new TransportFailureRateHealthPolicyOptions { DefaultFailureRateLimit = 0.5, DetectionWindowSize = TimeSpan.FromSeconds(30), MinimalTotalCountThreshold = 1 }); var timeProvider = new TestTimeProvider(TimeSpan.FromMilliseconds(10000)); var healthUpdater = new Mock(); var policy = new TransportFailureRateHealthPolicy(options, timeProvider, healthUpdater.Object); Assert.Equal(HealthCheckConstants.PassivePolicy.TransportFailureRate, policy.Name); var reactivationPeriod0 = TimeSpan.FromSeconds(60); var reactivationPeriod1 = TimeSpan.FromSeconds(100); var cluster0 = GetClusterInfo("cluster0", destinationCount: 2); var cluster1 = GetClusterInfo("cluster1", destinationCount: 2, failureRateLimit: 0.61, reactivationPeriod1); // Initial state Assert.All(cluster0.Destinations.Values, d => Assert.Equal(DestinationHealth.Unknown, d.Health.Passive)); Assert.All(cluster1.Destinations.Values, d => Assert.Equal(DestinationHealth.Unknown, d.Health.Passive)); // Successful requests for (var i = 0; i < 3; i++) { policy.RequestProxied(new DefaultHttpContext(), cluster0, cluster0.Destinations.Values.First()); policy.RequestProxied(new DefaultHttpContext(), cluster0, cluster0.Destinations.Values.Skip(1).First()); policy.RequestProxied(new DefaultHttpContext(), cluster1, cluster1.Destinations.Values.First()); policy.RequestProxied(new DefaultHttpContext(), cluster1, cluster1.Destinations.Values.Skip(1).First()); timeProvider.Advance(TimeSpan.FromMilliseconds(4000)); } healthUpdater.Verify(u => u.SetPassive(cluster0, cluster0.Destinations.Values.First(), DestinationHealth.Healthy, reactivationPeriod0), Times.Exactly(3)); healthUpdater.Verify(u => u.SetPassive(cluster0, cluster0.Destinations.Values.Skip(1).First(), DestinationHealth.Healthy, reactivationPeriod0), Times.Exactly(3)); healthUpdater.Verify(u => u.SetPassive(cluster1, cluster1.Destinations.Values.First(), DestinationHealth.Healthy, reactivationPeriod1), Times.Exactly(3)); healthUpdater.Verify(u => u.SetPassive(cluster1, cluster1.Destinations.Values.Skip(1).First(), DestinationHealth.Healthy, reactivationPeriod1), Times.Exactly(3)); healthUpdater.VerifyNoOtherCalls(); // Failed requests for (var i = 0; i < 3; i++) { policy.RequestProxied(GetFailedRequestContext(ForwarderError.RequestTimedOut), cluster0, cluster0.Destinations.Values.Skip(1).First()); policy.RequestProxied(GetFailedRequestContext(ForwarderError.Request), cluster1, cluster1.Destinations.Values.First()); timeProvider.Advance(TimeSpan.FromMilliseconds(4000)); } healthUpdater.Verify(u => u.SetPassive(cluster0, cluster0.Destinations.Values.Skip(1).First(), DestinationHealth.Healthy, reactivationPeriod0), Times.Exactly(5)); healthUpdater.Verify(u => u.SetPassive(cluster0, cluster0.Destinations.Values.Skip(1).First(), DestinationHealth.Unhealthy, reactivationPeriod0), Times.Once); healthUpdater.Verify(u => u.SetPassive(cluster1, cluster1.Destinations.Values.First(), DestinationHealth.Healthy, reactivationPeriod1), Times.Exactly(6)); healthUpdater.VerifyNoOtherCalls(); // Two more failed requests policy.RequestProxied(GetFailedRequestContext(ForwarderError.Request), cluster1, cluster1.Destinations.Values.First()); // End of the detection window timeProvider.Advance(TimeSpan.FromMilliseconds(6000)); policy.RequestProxied(GetFailedRequestContext(ForwarderError.Request), cluster1, cluster1.Destinations.Values.First()); healthUpdater.Verify(u => u.SetPassive(cluster1, cluster1.Destinations.Values.First(), DestinationHealth.Healthy, reactivationPeriod1), Times.Exactly(7)); healthUpdater.Verify(u => u.SetPassive(cluster1, cluster1.Destinations.Values.First(), DestinationHealth.Unhealthy, reactivationPeriod1), Times.Once); healthUpdater.VerifyNoOtherCalls(); } [Fact] public void RequestProxied_FailureMovedOutOfDetectionWindow_MarkDestinationHealthy() { var options = Options.Create( new TransportFailureRateHealthPolicyOptions { DefaultFailureRateLimit = 0.5, DetectionWindowSize = TimeSpan.FromSeconds(30), MinimalTotalCountThreshold = 1 }); var timeProvider = new TestTimeProvider(TimeSpan.FromMilliseconds(10000)); var healthUpdater = new Mock(); var policy = new TransportFailureRateHealthPolicy(options, timeProvider, healthUpdater.Object); var cluster = GetClusterInfo("cluster0", destinationCount: 2); // Initial failed requests for (var i = 0; i < 2; i++) { policy.RequestProxied(GetFailedRequestContext(ForwarderError.RequestTimedOut), cluster, cluster.Destinations.Values.Skip(1).First()); timeProvider.Advance(TimeSpan.FromMilliseconds(1000)); } healthUpdater.Verify(u => u.SetPassive(cluster, cluster.Destinations.Values.Skip(1).First(), DestinationHealth.Unhealthy, TimeSpan.FromSeconds(60)), Times.Exactly(2)); healthUpdater.VerifyNoOtherCalls(); // Successful requests for (var i = 0; i < 4; i++) { policy.RequestProxied(new DefaultHttpContext(), cluster, cluster.Destinations.Values.First()); policy.RequestProxied(new DefaultHttpContext(), cluster, cluster.Destinations.Values.Skip(1).First()); timeProvider.Advance(TimeSpan.FromMilliseconds(5000)); } healthUpdater.Verify(u => u.SetPassive(cluster, cluster.Destinations.Values.First(), DestinationHealth.Healthy, TimeSpan.FromSeconds(60)), Times.Exactly(4)); healthUpdater.Verify(u => u.SetPassive(cluster, cluster.Destinations.Values.Skip(1).First(), DestinationHealth.Healthy, TimeSpan.FromSeconds(60)), Times.Exactly(2)); healthUpdater.Verify(u => u.SetPassive(cluster, cluster.Destinations.Values.Skip(1).First(), DestinationHealth.Unhealthy, TimeSpan.FromSeconds(60)), Times.Exactly(4)); healthUpdater.VerifyNoOtherCalls(); // Failed requests for (var i = 0; i < 2; i++) { policy.RequestProxied(GetFailedRequestContext(ForwarderError.RequestTimedOut), cluster, cluster.Destinations.Values.Skip(1).First()); timeProvider.Advance(TimeSpan.FromMilliseconds(1)); } healthUpdater.Verify(u => u.SetPassive(cluster, cluster.Destinations.Values.Skip(1).First(), DestinationHealth.Healthy, TimeSpan.FromSeconds(60)), Times.Exactly(3)); healthUpdater.Verify(u => u.SetPassive(cluster, cluster.Destinations.Values.Skip(1).First(), DestinationHealth.Unhealthy, TimeSpan.FromSeconds(60)), Times.Exactly(5)); healthUpdater.VerifyNoOtherCalls(); // Shift the detection window to the future timeProvider.Advance(TimeSpan.FromMilliseconds(10998)); // New successful requests for (var i = 0; i < 2; i++) { policy.RequestProxied(new DefaultHttpContext(), cluster, cluster.Destinations.Values.Skip(1).First()); timeProvider.Advance(TimeSpan.FromMilliseconds(1)); } // New failed request, but 2 oldest failures have moved out of the detection window policy.RequestProxied(GetFailedRequestContext(ForwarderError.RequestTimedOut), cluster, cluster.Destinations.Values.Skip(1).First()); healthUpdater.Verify(u => u.SetPassive(cluster, cluster.Destinations.Values.Skip(1).First(), DestinationHealth.Healthy, TimeSpan.FromSeconds(60)), Times.Exactly(6)); healthUpdater.VerifyNoOtherCalls(); } [Fact] public void RequestProxied_FailedAndReactivationLessDetection_UseDetectionPeriodForReactivation() { var options = Options.Create( new TransportFailureRateHealthPolicyOptions { DefaultFailureRateLimit = 0.5, DetectionWindowSize = TimeSpan.FromSeconds(30), MinimalTotalCountThreshold = 1 }); var timeProvider = new TestTimeProvider(TimeSpan.FromMilliseconds(10000)); var healthUpdater = new Mock(); var policy = new TransportFailureRateHealthPolicy(options, timeProvider, healthUpdater.Object); var cluster = GetClusterInfo("cluster0", destinationCount: 2, reactivationPeriod: TimeSpan.FromSeconds(10)); // Initial failed requests for (var i = 0; i < 2; i++) { policy.RequestProxied(GetFailedRequestContext(ForwarderError.RequestTimedOut), cluster, cluster.Destinations.Values.Skip(1).First()); timeProvider.Advance(TimeSpan.FromMilliseconds(1000)); } healthUpdater.Verify(u => u.SetPassive(cluster, cluster.Destinations.Values.Skip(1).First(), DestinationHealth.Unhealthy, TimeSpan.FromSeconds(30)), Times.Exactly(2)); healthUpdater.VerifyNoOtherCalls(); // Simulate a reactivation timeProvider.Advance(TimeSpan.FromMilliseconds(31000)); cluster.Destinations.Values.Skip(1).First().Health.Passive = DestinationHealth.Unknown; // One successful request to the reactivated destination policy.RequestProxied(new DefaultHttpContext(), cluster, cluster.Destinations.Values.Skip(1).First()); timeProvider.Advance(TimeSpan.FromMilliseconds(100)); healthUpdater.Verify(u => u.SetPassive(cluster, cluster.Destinations.Values.Skip(1).First(), DestinationHealth.Healthy, TimeSpan.FromSeconds(30)), Times.Exactly(1)); healthUpdater.Verify(u => u.SetPassive(cluster, cluster.Destinations.Values.Skip(1).First(), DestinationHealth.Unhealthy, TimeSpan.FromSeconds(30)), Times.Exactly(2)); healthUpdater.VerifyNoOtherCalls(); } [Fact] public void RequestProxied_MultipleConcurrentRequests_MarkDestinationUnhealthyAndHealthyAgain() { var options = Options.Create( new TransportFailureRateHealthPolicyOptions { DefaultFailureRateLimit = 0.5, DetectionWindowSize = TimeSpan.FromSeconds(30), MinimalTotalCountThreshold = 1 }); var timeProvider = new TestTimeProvider(TimeSpan.FromMilliseconds(10000)); var healthUpdater = new Mock(); var reactivationPeriod = TimeSpan.FromSeconds(40); var policy = new TransportFailureRateHealthPolicy(options, timeProvider, healthUpdater.Object); var cluster = GetClusterInfo("cluster0", destinationCount: 2, reactivationPeriod: reactivationPeriod); // Initial state Assert.All(cluster.Destinations.Values, d => Assert.Equal(DestinationHealth.Unknown, d.Health.Passive)); // Initial successful requests for (var i = 0; i < 2; i++) { policy.RequestProxied(new DefaultHttpContext(), cluster, cluster.Destinations.Values.Skip(1).First()); } healthUpdater.Verify(u => u.SetPassive(cluster, cluster.Destinations.Values.Skip(1).First(), DestinationHealth.Healthy, reactivationPeriod), Times.Exactly(2)); healthUpdater.VerifyNoOtherCalls(); // Concurrent failed requests. // They are 'concurrent' because the timeProvider is not updated. for (var i = 0; i < 2; i++) { policy.RequestProxied(GetFailedRequestContext(ForwarderError.RequestTimedOut), cluster, cluster.Destinations.Values.Skip(1).First()); } healthUpdater.Verify(u => u.SetPassive(cluster, cluster.Destinations.Values.Skip(1).First(), DestinationHealth.Healthy, reactivationPeriod), Times.Exactly(3)); healthUpdater.Verify(u => u.SetPassive(cluster, cluster.Destinations.Values.Skip(1).First(), DestinationHealth.Unhealthy, reactivationPeriod), Times.Once); healthUpdater.VerifyNoOtherCalls(); // More successful requests for (var i = 0; i < 2; i++) { policy.RequestProxied(new DefaultHttpContext(), cluster, cluster.Destinations.Values.Skip(1).First()); timeProvider.Advance(TimeSpan.FromMilliseconds(100)); } healthUpdater.Verify(u => u.SetPassive(cluster, cluster.Destinations.Values.Skip(1).First(), DestinationHealth.Healthy, reactivationPeriod), Times.Exactly(5)); healthUpdater.Verify(u => u.SetPassive(cluster, cluster.Destinations.Values.Skip(1).First(), DestinationHealth.Unhealthy, reactivationPeriod), Times.Once); healthUpdater.VerifyNoOtherCalls(); // More failed requests for (var i = 0; i < 2; i++) { policy.RequestProxied(GetFailedRequestContext(ForwarderError.RequestTimedOut), cluster, cluster.Destinations.Values.Skip(1).First()); timeProvider.Advance(TimeSpan.FromMilliseconds(100)); } healthUpdater.Verify(u => u.SetPassive(cluster, cluster.Destinations.Values.Skip(1).First(), DestinationHealth.Healthy, reactivationPeriod), Times.Exactly(6)); healthUpdater.Verify(u => u.SetPassive(cluster, cluster.Destinations.Values.Skip(1).First(), DestinationHealth.Unhealthy, reactivationPeriod), Times.Exactly(2)); healthUpdater.VerifyNoOtherCalls(); policy.RequestProxied(new DefaultHttpContext(), cluster, cluster.Destinations.Values.First()); healthUpdater.Verify(u => u.SetPassive(cluster, cluster.Destinations.Values.First(), DestinationHealth.Healthy, reactivationPeriod), Times.Once); healthUpdater.VerifyNoOtherCalls(); } private HttpContext GetFailedRequestContext(ForwarderError error) { var errorFeature = new ForwarderErrorFeature(error, null); var context = new DefaultHttpContext(); context.Features.Set(errorFeature); return context; } private ClusterState GetClusterInfo(string id, int destinationCount, double? failureRateLimit = null, TimeSpan? reactivationPeriod = null) { var metadata = failureRateLimit is not null ? new Dictionary { { TransportFailureRateHealthPolicyOptions.FailureRateLimitMetadataName, failureRateLimit?.ToString(CultureInfo.InvariantCulture) } } : null; var clusterModel = new ClusterModel( new ClusterConfig { ClusterId = id, HealthCheck = new HealthCheckConfig { Passive = new PassiveHealthCheckConfig { Enabled = true, Policy = "policy", ReactivationPeriod = reactivationPeriod, } }, Metadata = metadata, }, new HttpMessageInvoker(new HttpClientHandler())); var clusterState = new ClusterState(id); clusterState.Model = clusterModel; for (var i = 0; i < destinationCount; i++) { var destinationModel = new DestinationModel(new DestinationConfig { Address = $"https://localhost:1000{i}/{id}/", Health = $"https://localhost:2000{i}/{id}/" }); var destinationId = $"destination{i}"; clusterState.Destinations.GetOrAdd(destinationId, id => new DestinationState(id) { Model = destinationModel }); } return clusterState; } } ================================================ FILE: test/ReverseProxy.Tests/Limits/LimitsMiddlewareTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Net.Http; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.Logging.Abstractions; using Xunit; using Yarp.ReverseProxy.Configuration; using Yarp.ReverseProxy.Forwarder; using Yarp.ReverseProxy.Model; namespace Yarp.ReverseProxy.Limits.Tests; public class LimitsMiddlewareTests { [Fact] public void Constructor_Works() { CreateMiddleware(); } [Fact] public async Task MissingFeature_NoOps() { var context = CreateContext(10, null); var sut = CreateMiddleware(); await sut.Invoke(context); } [Theory] [InlineData(true, null, null, null)] [InlineData(true, 10L, null, 10L)] [InlineData(true, null, 10L, null)] [InlineData(true, 10L, 11L, 10L)] [InlineData(false, null, null, null)] [InlineData(false, 10L, null, 10L)] [InlineData(false, null, 10L, 10L)] [InlineData(false, null, -1L, null)] [InlineData(false, 10L, -1L, null)] [InlineData(false, 10L, 11L, 11L)] public async Task Invoke_CombinationsWork(bool readOnly, long? serverLimit, long? routeLimit, long? expected) { var feature = new FakeBodySizeFeature() { IsReadOnly = readOnly, MaxRequestBodySize = serverLimit }; var context = CreateContext(routeLimit, feature); var sut = CreateMiddleware(); await sut.Invoke(context); Assert.Equal(expected, feature.MaxRequestBodySize); } private static LimitsMiddleware CreateMiddleware() { return new LimitsMiddleware( _ => Task.CompletedTask, NullLogger.Instance); } private static HttpContext CreateContext(long? bodySizeLimit, IHttpMaxRequestBodySizeFeature feature) { var cluster = new ClusterState("cluster1") { Model = new ClusterModel(new ClusterConfig(), new HttpMessageInvoker(new HttpClientHandler())) }; var context = new DefaultHttpContext(); var route = new RouteModel(new RouteConfig() { MaxRequestBodySize = bodySizeLimit }, cluster, HttpTransformer.Default); context.Features.Set( new ReverseProxyFeature() { Route = route, Cluster = cluster.Model }); context.Features.Set(cluster); var endpoint = new Endpoint(default, new EndpointMetadataCollection(route), string.Empty); context.SetEndpoint(endpoint); context.Features.Set(feature); return context; } private class FakeBodySizeFeature : IHttpMaxRequestBodySizeFeature { public bool IsReadOnly { get; set; } public long? MaxRequestBodySize { get; set; } } } ================================================ FILE: test/ReverseProxy.Tests/LoadBalancing/LoadBalancerMiddlewareTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Net.Http; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Moq; using Xunit; using Yarp.ReverseProxy.Configuration; using Yarp.ReverseProxy.Model; using Yarp.ReverseProxy.Forwarder; namespace Yarp.ReverseProxy.LoadBalancing.Tests; public class LoadBalancerMiddlewareTests { private static LoadBalancingMiddleware CreateMiddleware(RequestDelegate next, params ILoadBalancingPolicy[] loadBalancingPolicies) { var logger = new Mock>(); logger .Setup(l => l.IsEnabled(It.IsAny())) .Returns(true); return new LoadBalancingMiddleware( next, logger.Object, loadBalancingPolicies); } [Fact] public void Constructor_Works() { CreateMiddleware(_ => Task.CompletedTask); } [Fact] public async Task PickDestination_UnsupportedPolicy_Throws() { const string PolicyName = "NonExistentPolicy"; var context = CreateContext(PolicyName, new[] { new DestinationState("destination1"), new DestinationState("destination2") }); var sut = CreateMiddleware(_ => Task.CompletedTask); 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); } [Fact] public async Task PickDestination_SingleDestinations_ShortCircuit() { var context = CreateContext(LoadBalancingPolicies.FirstAlphabetical, new[] { new DestinationState("destination1") }); var sut = CreateMiddleware(_ => Task.CompletedTask); await sut.Invoke(context); var feature = context.Features.Get(); Assert.NotNull(feature); Assert.NotNull(feature.AvailableDestinations); Assert.Single(feature.AvailableDestinations); Assert.Same("destination1", feature.AvailableDestinations[0].DestinationId); Assert.Equal(200, context.Response.StatusCode); } [Fact] public async Task Invoke_Works() { // Selects the alphabetically first available destination. var context = CreateContext(LoadBalancingPolicies.FirstAlphabetical, new[] { new DestinationState("destination2"), new DestinationState("destination1"), }); var sut = CreateMiddleware(_ => Task.CompletedTask, new FirstLoadBalancingPolicy()); await sut.Invoke(context); var feature = context.Features.Get(); Assert.NotNull(feature); Assert.NotNull(feature.AvailableDestinations); Assert.Single(feature.AvailableDestinations); Assert.Same("destination1", feature.AvailableDestinations[0].DestinationId); Assert.Equal(200, context.Response.StatusCode); } [Fact] public async Task Invoke_WithoutDestinations_503() { var context = CreateContext(LoadBalancingPolicies.FirstAlphabetical, Array.Empty()); var sut = CreateMiddleware(context => { context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable; return Task.CompletedTask; }); await sut.Invoke(context); var feature = context.Features.Get(); Assert.NotNull(feature); Assert.NotNull(feature.AvailableDestinations); Assert.Empty(feature.AvailableDestinations); Assert.Equal(503, context.Response.StatusCode); } [Fact] public async Task Invoke_ServiceReturnsNoResults_FallThrough() { const string PolicyName = "CustomPolicy"; var context = CreateContext(PolicyName, new[] { new DestinationState("destination1"), new DestinationState("destination2") }); var policy = new Mock(); policy .Setup(p => p.Name) .Returns(PolicyName); policy .Setup(p => p.PickDestination(It.IsAny(), It.IsAny(), It.IsAny>())) .Returns((DestinationState)null); var sut = CreateMiddleware(context => { context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable; return Task.CompletedTask; }, policy.Object); await sut.Invoke(context); var feature = context.Features.Get(); Assert.NotNull(feature); Assert.NotNull(feature.AvailableDestinations); Assert.Empty(feature.AvailableDestinations); Assert.Equal(503, context.Response.StatusCode); } [Fact] public async Task Invoke_NoPolicySpecified_DefaultsToPowerOfTwoChoices() { var destinations = new[] { new DestinationState("destination1"), new DestinationState("destination2") }; var context = CreateContext(loadBalancingPolicy: null, destinations); var policy = new Mock(); policy .Setup(p => p.Name) .Returns(LoadBalancingPolicies.PowerOfTwoChoices); policy .Setup(p => p.PickDestination(It.IsAny(), It.IsAny(), It.IsAny>())) .Returns((DestinationState)destinations[0]); var sut = CreateMiddleware(_ => Task.CompletedTask, policy.Object); await sut.Invoke(context); policy.Verify(p => p.PickDestination(context, It.IsAny(), destinations), Times.Once); } private static HttpContext CreateContext(string loadBalancingPolicy, IReadOnlyList destinations) { var cluster = new ClusterState("cluster1") { Model = new ClusterModel(new ClusterConfig { LoadBalancingPolicy = loadBalancingPolicy }, new HttpMessageInvoker(new HttpClientHandler())) }; var context = new DefaultHttpContext(); var route = new RouteModel(new RouteConfig(), cluster, HttpTransformer.Default); context.Features.Set( new ReverseProxyFeature() { AvailableDestinations = destinations, Route = route, Cluster = cluster.Model }); context.Features.Set(cluster); var endpoint = new Endpoint(default, new EndpointMetadataCollection(route), string.Empty); context.SetEndpoint(endpoint); return context; } } ================================================ FILE: test/ReverseProxy.Tests/LoadBalancing/LoadBalancingPoliciesTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Linq; using Microsoft.AspNetCore.Http; using Xunit; using Yarp.Tests.Common; using Yarp.ReverseProxy.Configuration; using Yarp.ReverseProxy.Model; using Yarp.ReverseProxy.Forwarder; using Yarp.ReverseProxy.Utilities; namespace Yarp.ReverseProxy.LoadBalancing.Tests; public class LoadBalancingPoliciesTests : TestAutoMockBase { public LoadBalancingPoliciesTests() { RandomFactory = new TestRandomFactory() { Instance = RandomInstance }; Provide(RandomFactory); } private TestRandom RandomInstance { get; set; } = new TestRandom(); private TestRandomFactory RandomFactory { get; set; } [Fact] public void PickDestination_FirstWithDestinations_Works() { var destinations = new[] { new DestinationState("d1"), new DestinationState("d2"), new DestinationState("d3") }; var context = new DefaultHttpContext(); var loadBalancer = Create(); for (var i = 0; i < 10; i++) { var result = loadBalancer.PickDestination(context, cluster: null, availableDestinations: destinations); Assert.Same(destinations[0], result); result.ConcurrentRequestCount++; } } [Fact] public void PickDestination_Random_Works() { var destinations = new[] { new DestinationState("d1"), new DestinationState("d2"), new DestinationState("d3") }; const int Iterations = 10; var random = new Random(42); RandomInstance.Sequence = Enumerable.Range(0, Iterations).Select(_ => random.Next(destinations.Length)).ToArray(); var context = new DefaultHttpContext(); var loadBalancer = Create(); for (var i = 0; i < Iterations; i++) { var result = loadBalancer.PickDestination(context, cluster: null, availableDestinations: destinations); Assert.Same(destinations[RandomInstance.Sequence[i]], result); result.ConcurrentRequestCount++; } } [Fact] public void PickDestination_PowerOfTwoChoices_SkipBusiestConnection() { var destinations = new[] { new DestinationState("d1"), new DestinationState("d2"), new DestinationState("d3") }; destinations[0].ConcurrentRequestCount++; const int Iterations = 100; var random = new Random(42); RandomInstance.Sequence = Enumerable.Range(0, Iterations * Iterations).Select(_ => random.Next(destinations.Length)).ToArray(); var context = new DefaultHttpContext(); var loadBalancer = Create(); for (var i = 0; i < Iterations; i++) { var result = loadBalancer.PickDestination(context, cluster: null, availableDestinations: destinations); var groupByLoad = destinations.GroupBy(d => d.ConcurrentRequestCount); var busiestGroup = groupByLoad.OrderByDescending(g => g.Key).First(); if (busiestGroup.Count() == 1) { Assert.True(result.ConcurrentRequestCount < busiestGroup.Key); } result.ConcurrentRequestCount++; } } [Fact] public void PickDestination_PowerOfTwoChoices_LeastLoaded() { var destinations = new[] { new DestinationState("d1") {ConcurrentRequestCount = 1000}, new DestinationState("d2") }; const int Iterations = 100; var random = new Random(42); RandomInstance.Sequence = Enumerable.Range(0, Iterations * Iterations).Select(_ => random.Next(destinations.Length)).ToArray(); var context = new DefaultHttpContext(); var loadBalancer = Create(); for (var i = 0; i < Iterations; i++) { var result = loadBalancer.PickDestination(context, cluster: null, availableDestinations: destinations); Assert.Same(destinations[1], result); } } [Fact] public void PickDestination_LeastRequests_Works() { var destinations = new[] { new DestinationState("d1"), new DestinationState("d2"), new DestinationState("d3") }; destinations[0].ConcurrentRequestCount++; var context = new DefaultHttpContext(); var loadBalancer = Create(); for (var i = 0; i < 10; i++) { var result = loadBalancer.PickDestination(context, cluster: null, availableDestinations: destinations); Assert.Same(destinations.OrderBy(d => d.ConcurrentRequestCount).First(), result); result.ConcurrentRequestCount++; } } [Fact] public void PickDestination_RoundRobin_Works() { var destinations = new[] { new DestinationState("d1"), new DestinationState("d2"), new DestinationState("d3") }; destinations[0].ConcurrentRequestCount++; var context = new DefaultHttpContext(); var cluster = new ClusterState("cluster1"); var routeConfig = new RouteModel(new RouteConfig(), cluster, HttpTransformer.Default); var feature = new ReverseProxyFeature() { Route = routeConfig, }; context.Features.Set(feature); var loadBalancer = Create(); for (var i = 0; i < 10; i++) { var result = loadBalancer.PickDestination(context, cluster, availableDestinations: destinations); Assert.Same(destinations[i % destinations.Length], result); result.ConcurrentRequestCount++; } } } ================================================ FILE: test/ReverseProxy.Tests/Management/ProxyConfigManagerTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Linq; using System.Net.Sockets; 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.Hosting.Server; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Primitives; using Moq; using Xunit; using Yarp.ReverseProxy.Configuration; using Yarp.ReverseProxy.Forwarder; using Yarp.ReverseProxy.Forwarder.Tests; using Yarp.ReverseProxy.Health; using Yarp.ReverseProxy.Model; using Yarp.ReverseProxy.Routing; using Yarp.ReverseProxy.ServiceDiscovery; using Yarp.Tests.Common; namespace Yarp.ReverseProxy.Management.Tests; public class ProxyConfigManagerTests { private static IServiceProvider CreateServices( List routes, List clusters, Action configureProxy = null, IEnumerable configListeners = null, IDestinationResolver destinationResolver = null) { var serviceCollection = new ServiceCollection(); serviceCollection.AddLogging(); serviceCollection.AddRouting(); var proxyBuilder = serviceCollection.AddReverseProxy().LoadFromMemory(routes, clusters); serviceCollection.TryAddSingleton(new Mock().Object); serviceCollection.TryAddSingleton(new Mock().Object); var activeHealthPolicy = new Mock(); activeHealthPolicy.SetupGet(p => p.Name).Returns("activePolicyA"); serviceCollection.AddSingleton(activeHealthPolicy.Object); configureProxy?.Invoke(proxyBuilder); if (configListeners is not null) { foreach (var configListener in configListeners) { serviceCollection.AddSingleton(configListener); } } if (destinationResolver is not null) { serviceCollection.AddSingleton(destinationResolver); } var services = serviceCollection.BuildServiceProvider(); var routeBuilder = services.GetRequiredService(); routeBuilder.SetProxyPipeline(context => Task.CompletedTask); return services; } private static IServiceProvider CreateServices( IEnumerable configProviders, Action configureProxy = null, IEnumerable configListeners = null) { var serviceCollection = new ServiceCollection(); serviceCollection.AddLogging(); serviceCollection.AddRouting(); var proxyBuilder = serviceCollection.AddReverseProxy(); foreach (var configProvider in configProviders) { serviceCollection.AddSingleton(configProvider); } serviceCollection.TryAddSingleton(new Mock().Object); serviceCollection.TryAddSingleton(new Mock().Object); var activeHealthPolicy = new Mock(); activeHealthPolicy.SetupGet(p => p.Name).Returns("activePolicyA"); serviceCollection.AddSingleton(activeHealthPolicy.Object); configureProxy?.Invoke(proxyBuilder); if (configListeners is not null) { foreach (var configListener in configListeners) { serviceCollection.AddSingleton(configListener); } } var services = serviceCollection.BuildServiceProvider(); var routeBuilder = services.GetRequiredService(); routeBuilder.SetProxyPipeline(context => Task.CompletedTask); return services; } [Fact] public void Constructor_Works() { var services = CreateServices(new List(), new List()); _ = services.GetRequiredService(); } [Fact] public async Task NullRoutes_StartsEmpty() { var services = CreateServices(null, new List()); var manager = services.GetRequiredService(); var dataSource = await manager.InitialLoadAsync(); Assert.NotNull(dataSource); var endpoints = dataSource.Endpoints; Assert.Empty(endpoints); } [Fact] public async Task NullClusters_StartsEmpty() { var services = CreateServices(new List(), null); var manager = services.GetRequiredService(); var dataSource = await manager.InitialLoadAsync(); Assert.NotNull(dataSource); var endpoints = dataSource.Endpoints; Assert.Empty(endpoints); } [Fact] public async Task Endpoints_StartsEmpty() { var services = CreateServices(new List(), new List()); var manager = services.GetRequiredService(); var dataSource = await manager.InitialLoadAsync(); Assert.NotNull(dataSource); var endpoints = dataSource.Endpoints; Assert.Empty(endpoints); } [Fact] public async Task Lookup_StartsEmpty() { var services = CreateServices(new List(), new List()); var manager = services.GetRequiredService(); var lookup = services.GetRequiredService(); await manager.InitialLoadAsync(); Assert.Empty(lookup.GetRoutes()); Assert.Empty(lookup.GetClusters()); Assert.False(lookup.TryGetRoute("route1", out var _)); Assert.False(lookup.TryGetCluster("cluster1", out var _)); } [Fact] public async Task GetChangeToken_InitialValue() { var services = CreateServices(new List(), new List()); var manager = services.GetRequiredService(); var dataSource = await manager.InitialLoadAsync(); Assert.NotNull(dataSource); var changeToken = dataSource.GetChangeToken(); Assert.NotNull(changeToken); Assert.True(changeToken.ActiveChangeCallbacks); Assert.False(changeToken.HasChanged); } [Fact] public async Task BuildConfig_OneClusterOneDestinationOneRoute_Works() { const string TestAddress = "https://localhost:123/"; var cluster = new ClusterConfig { ClusterId = "cluster1", Destinations = new Dictionary(StringComparer.OrdinalIgnoreCase) { { "d1", new DestinationConfig { Address = TestAddress } } } }; var route = new RouteConfig { RouteId = "route1", ClusterId = "cluster1", Match = new RouteMatch { Path = "/" } }; var services = CreateServices(new List() { route }, new List() { cluster }); var manager = services.GetRequiredService(); var lookup = services.GetRequiredService(); var dataSource = await manager.InitialLoadAsync(); Assert.NotNull(dataSource); var endpoints = dataSource.Endpoints; var endpoint = Assert.Single(endpoints); var routeConfig = endpoint.Metadata.GetMetadata(); Assert.NotNull(routeConfig); Assert.Equal("route1", routeConfig.Config.RouteId); Assert.True(lookup.TryGetRoute("route1", out var routeModel)); Assert.Equal(route, routeModel.Config); routeModel = Assert.Single(lookup.GetRoutes()); Assert.Equal(route, routeModel.Config); var clusterState = routeConfig.Cluster; Assert.NotNull(clusterState); Assert.Equal("cluster1", clusterState.ClusterId); Assert.NotNull(clusterState.Destinations); Assert.NotNull(clusterState.Model); Assert.NotNull(clusterState.Model.HttpClient); Assert.Same(clusterState, routeConfig.Cluster); Assert.True(lookup.TryGetCluster("cluster1", out clusterState)); Assert.Equal(cluster, clusterState.Model.Config); clusterState = Assert.Single(lookup.GetClusters()); Assert.Equal(cluster, clusterState.Model.Config); var actualDestinations = clusterState.Destinations.Values; var destination = Assert.Single(actualDestinations); Assert.Equal("d1", destination.DestinationId); Assert.NotNull(destination.Model); Assert.Equal(TestAddress, destination.Model.Config.Address); } [Fact] public async Task BuildConfig_DuplicateRouteIds_Throws() { var route = new RouteConfig { RouteId = "route1", ClusterId = "cluster1", Match = new RouteMatch { Path = "/" } }; var services = CreateServices(new List { route, route }, new List()); var manager = services.GetRequiredService(); var ex = await Assert.ThrowsAsync(() => manager.InitialLoadAsync()); Assert.Contains("Duplicate route 'route1'", ex.ToString()); } [Fact] public async Task BuildConfig_DuplicateClusterIds_Throws() { var cluster = new ClusterConfig { ClusterId = "cluster1" }; var route = new RouteConfig { RouteId = "route1", ClusterId = "cluster1", Match = new RouteMatch { Path = "/" } }; var services = CreateServices(new List { route }, new List { cluster, cluster }); var manager = services.GetRequiredService(); var ex = await Assert.ThrowsAsync(() => manager.InitialLoadAsync()); Assert.Contains("Duplicate cluster 'cluster1'", ex.ToString()); } [Fact] public async Task BuildConfig_TwoDistinctConfigs_Works() { const string TestAddress = "https://localhost:123/"; var cluster1 = new ClusterConfig { ClusterId = "cluster1", Destinations = new Dictionary(StringComparer.OrdinalIgnoreCase) { { "d1", new DestinationConfig { Address = TestAddress } } } }; var route1 = new RouteConfig { RouteId = "route1", ClusterId = "cluster1", Match = new RouteMatch { Path = "/" } }; var cluster2 = new ClusterConfig { ClusterId = "cluster2", Destinations = new Dictionary(StringComparer.OrdinalIgnoreCase) { { "d2", new DestinationConfig { Address = TestAddress } } } }; var route2 = new RouteConfig { RouteId = "route2", ClusterId = "cluster2", Match = new RouteMatch { Path = "/" } }; var config1 = new InMemoryConfigProvider(new List() { route1 }, new List() { cluster1 }); var config2 = new InMemoryConfigProvider(new List() { route2 }, new List() { cluster2 }); var services = CreateServices(new[] { config1, config2 }); var manager = services.GetRequiredService(); var dataSource = await manager.InitialLoadAsync(); Assert.NotNull(dataSource); var endpoints = dataSource.Endpoints; Assert.Equal(2, endpoints.Count); // The order is unstable because routes are stored in a dictionary. var routeConfig = endpoints.Single(e => string.Equals(e.DisplayName, "route1")).Metadata.GetMetadata(); Assert.NotNull(routeConfig); Assert.Equal("route1", routeConfig.Config.RouteId); var clusterState = routeConfig.Cluster; Assert.NotNull(clusterState); Assert.Equal("cluster1", clusterState.ClusterId); Assert.NotNull(clusterState.Destinations); Assert.NotNull(clusterState.Model); Assert.NotNull(clusterState.Model.HttpClient); Assert.Same(clusterState, routeConfig.Cluster); var actualDestinations = clusterState.Destinations.Values; var destination = Assert.Single(actualDestinations); Assert.Equal("d1", destination.DestinationId); Assert.NotNull(destination.Model); Assert.Equal(TestAddress, destination.Model.Config.Address); routeConfig = endpoints.Single(e => string.Equals(e.DisplayName, "route2")).Metadata.GetMetadata(); Assert.NotNull(routeConfig); Assert.Equal("route2", routeConfig.Config.RouteId); clusterState = routeConfig.Cluster; Assert.NotNull(clusterState); Assert.Equal("cluster2", clusterState.ClusterId); Assert.NotNull(clusterState.Destinations); Assert.NotNull(clusterState.Model); Assert.NotNull(clusterState.Model.HttpClient); Assert.Same(clusterState, routeConfig.Cluster); actualDestinations = clusterState.Destinations.Values; destination = Assert.Single(actualDestinations); Assert.Equal("d2", destination.DestinationId); Assert.NotNull(destination.Model); Assert.Equal(TestAddress, destination.Model.Config.Address); } [Fact] public async Task BuildConfig_TwoOverlappingConfigs_Works() { const string TestAddress = "https://localhost:123/"; var cluster1 = new ClusterConfig { ClusterId = "cluster1", Destinations = new Dictionary(StringComparer.OrdinalIgnoreCase) { { "d1", new DestinationConfig { Address = TestAddress } } } }; var cluster2 = new ClusterConfig { ClusterId = "cluster2", Destinations = new Dictionary(StringComparer.OrdinalIgnoreCase) { { "d2", new DestinationConfig { Address = TestAddress } } } }; var route1 = new RouteConfig { RouteId = "route1", ClusterId = "cluster1", Match = new RouteMatch { Path = "/" } }; var route2 = new RouteConfig { RouteId = "route2", ClusterId = "cluster2", Match = new RouteMatch { Path = "/" } }; var config1 = new InMemoryConfigProvider(new List() { route2 }, new List() { cluster1 }); var config2 = new InMemoryConfigProvider(new List() { route1 }, new List() { cluster2 }); var services = CreateServices(new[] { config1, config2 }); var manager = services.GetRequiredService(); var dataSource = await manager.InitialLoadAsync(); Assert.NotNull(dataSource); var endpoints = dataSource.Endpoints; Assert.Equal(2, endpoints.Count); // The order is unstable because routes are stored in a dictionary. var routeConfig = endpoints.Single(e => string.Equals(e.DisplayName, "route1")).Metadata.GetMetadata(); Assert.NotNull(routeConfig); Assert.Equal("route1", routeConfig.Config.RouteId); var clusterState = routeConfig.Cluster; Assert.NotNull(clusterState); Assert.Equal("cluster1", clusterState.ClusterId); Assert.NotNull(clusterState.Destinations); Assert.NotNull(clusterState.Model); Assert.NotNull(clusterState.Model.HttpClient); Assert.Same(clusterState, routeConfig.Cluster); var actualDestinations = clusterState.Destinations.Values; var destination = Assert.Single(actualDestinations); Assert.Equal("d1", destination.DestinationId); Assert.NotNull(destination.Model); Assert.Equal(TestAddress, destination.Model.Config.Address); routeConfig = endpoints.Single(e => string.Equals(e.DisplayName, "route2")).Metadata.GetMetadata(); Assert.NotNull(routeConfig); Assert.Equal("route2", routeConfig.Config.RouteId); clusterState = routeConfig.Cluster; Assert.NotNull(clusterState); Assert.Equal("cluster2", clusterState.ClusterId); Assert.NotNull(clusterState.Destinations); Assert.NotNull(clusterState.Model); Assert.NotNull(clusterState.Model.HttpClient); Assert.Same(clusterState, routeConfig.Cluster); actualDestinations = clusterState.Destinations.Values; destination = Assert.Single(actualDestinations); Assert.Equal("d2", destination.DestinationId); Assert.NotNull(destination.Model); Assert.Equal(TestAddress, destination.Model.Config.Address); } private class FakeConfigChangeListener : IConfigChangeListener { public bool? HasApplyingSucceeded { get; private set; } public bool DidAtLeastOneErrorOccurWhileLoading { get; private set; } public string[] EventuallyLoaded; public string[] SuccessfullyApplied; public string[] FailedApplied; public FakeConfigChangeListener() { Reset(); } public void Reset() { DidAtLeastOneErrorOccurWhileLoading = false; HasApplyingSucceeded = null; EventuallyLoaded = Array.Empty(); SuccessfullyApplied = Array.Empty(); FailedApplied = Array.Empty(); } public void ConfigurationLoadingFailed(IProxyConfigProvider configProvider, Exception ex) { DidAtLeastOneErrorOccurWhileLoading = true; } public void ConfigurationLoaded(IReadOnlyList proxyConfigs) { EventuallyLoaded = proxyConfigs.Select(c => c.RevisionId).ToArray(); } public void ConfigurationApplyingFailed(IReadOnlyList proxyConfigs, Exception ex) { HasApplyingSucceeded = false; FailedApplied = proxyConfigs.Select(c => c.RevisionId).ToArray(); } public void ConfigurationApplied(IReadOnlyList proxyConfigs) { HasApplyingSucceeded = true; SuccessfullyApplied = proxyConfigs.Select(c => c.RevisionId).ToArray(); } } private class ConfigChangeListenerCounter : IConfigChangeListener { public int NumberOfLoadedConfigurations { get; private set; } public int NumberOfFailedConfigurationLoads { get; private set; } public int NumberOfAppliedConfigurations { get; private set; } public int NumberOfFailedConfigurationApplications { get; private set; } public ConfigChangeListenerCounter() { Reset(); } public void Reset() { NumberOfLoadedConfigurations = 0; NumberOfFailedConfigurationLoads = 0; NumberOfAppliedConfigurations = 0; NumberOfFailedConfigurationApplications = 0; } public void ConfigurationLoadingFailed(IProxyConfigProvider configProvider, Exception ex) { NumberOfFailedConfigurationLoads++; } public void ConfigurationLoaded(IReadOnlyList proxyConfigs) { NumberOfLoadedConfigurations += proxyConfigs.Count; } public void ConfigurationApplyingFailed(IReadOnlyList proxyConfigs, Exception ex) { NumberOfFailedConfigurationApplications += proxyConfigs.Count; } public void ConfigurationApplied(IReadOnlyList proxyConfigs) { NumberOfAppliedConfigurations += proxyConfigs.Count; } } private class InMemoryConfig : IProxyConfig { private readonly CancellationTokenSource _cts = new CancellationTokenSource(); public InMemoryConfig(IReadOnlyList routes, IReadOnlyList clusters, string revisionId) { RevisionId = revisionId; Routes = routes; Clusters = clusters; ChangeToken = new CancellationChangeToken(_cts.Token); } public string RevisionId { get; } public IReadOnlyList Routes { get; } public IReadOnlyList Clusters { get; } public IChangeToken ChangeToken { get; } internal void SignalChange() { _cts.Cancel(); } } private class OnDemandFailingInMemoryConfigProvider : IProxyConfigProvider { private volatile InMemoryConfig _config; public OnDemandFailingInMemoryConfigProvider( InMemoryConfig config) { _config = config; } public OnDemandFailingInMemoryConfigProvider( IReadOnlyList routes, IReadOnlyList clusters, string revisionId) : this(new InMemoryConfig(routes, clusters, revisionId)) { } public IProxyConfig GetConfig() { if (ShouldConfigLoadingFail) { return null; } return _config; } public void Update(IReadOnlyList routes, IReadOnlyList clusters, string revisionId) { Update(new InMemoryConfig(routes, clusters, revisionId)); } public void Update(InMemoryConfig config) { var oldConfig = Interlocked.Exchange(ref _config, config); oldConfig.SignalChange(); } public bool ShouldConfigLoadingFail { get; set; } } [Fact] public async Task BuildConfig_CanBeNotifiedOfProxyConfigSuccessfulAndFailedLoading() { var configProviderA = new OnDemandFailingInMemoryConfigProvider(new List() { }, new List() { }, "A1"); var configProviderB = new OnDemandFailingInMemoryConfigProvider(new List() { }, new List() { }, "B1"); var configChangeListenerCounter = new ConfigChangeListenerCounter(); var fakeConfigChangeListener = new FakeConfigChangeListener(); var services = CreateServices(new[] { configProviderA, configProviderB }, null, new IConfigChangeListener[] { fakeConfigChangeListener, configChangeListenerCounter }); var manager = services.GetRequiredService(); await manager.InitialLoadAsync(); Assert.Equal(2, configChangeListenerCounter.NumberOfLoadedConfigurations); Assert.Equal(0, configChangeListenerCounter.NumberOfFailedConfigurationLoads); Assert.Equal(2, configChangeListenerCounter.NumberOfAppliedConfigurations); Assert.Equal(0, configChangeListenerCounter.NumberOfFailedConfigurationApplications); Assert.False(fakeConfigChangeListener.DidAtLeastOneErrorOccurWhileLoading); Assert.Equal(new[] { "A1", "B1" }, fakeConfigChangeListener.EventuallyLoaded); Assert.True(fakeConfigChangeListener.HasApplyingSucceeded); Assert.Equal(new[] { "A1", "B1" }, fakeConfigChangeListener.SuccessfullyApplied); Assert.Empty(fakeConfigChangeListener.FailedApplied); const string TestAddress = "https://localhost:123/"; var cluster1 = new ClusterConfig { ClusterId = "cluster1", Destinations = new Dictionary(StringComparer.OrdinalIgnoreCase) { { "d1", new DestinationConfig { Address = TestAddress } } } }; var cluster2 = new ClusterConfig { ClusterId = "cluster2", Destinations = new Dictionary(StringComparer.OrdinalIgnoreCase) { { "d2", new DestinationConfig { Address = TestAddress } } } }; var route1 = new RouteConfig { RouteId = "route1", ClusterId = "cluster1", Match = new RouteMatch { Path = "/" } }; var route2 = new RouteConfig { RouteId = "route2", ClusterId = "cluster2", Match = new RouteMatch { Path = "/" } }; fakeConfigChangeListener.Reset(); configChangeListenerCounter.Reset(); configProviderA.Update(new List() { route1 }, new List() { cluster1 }, "A2"); Assert.Equal(2, configChangeListenerCounter.NumberOfLoadedConfigurations); Assert.Equal(0, configChangeListenerCounter.NumberOfFailedConfigurationLoads); Assert.Equal(2, configChangeListenerCounter.NumberOfAppliedConfigurations); Assert.Equal(0, configChangeListenerCounter.NumberOfFailedConfigurationApplications); Assert.False(fakeConfigChangeListener.DidAtLeastOneErrorOccurWhileLoading); Assert.Equal(new[] { "A2", "B1" }, fakeConfigChangeListener.EventuallyLoaded); Assert.True(fakeConfigChangeListener.HasApplyingSucceeded); Assert.Equal(new[] { "A2", "B1" }, fakeConfigChangeListener.SuccessfullyApplied); Assert.Empty(fakeConfigChangeListener.FailedApplied); configProviderB.ShouldConfigLoadingFail = true; fakeConfigChangeListener.Reset(); configChangeListenerCounter.Reset(); configProviderB.Update(new List() { route2 }, new List() { cluster2 }, "B2"); Assert.Equal(2, configChangeListenerCounter.NumberOfLoadedConfigurations); Assert.Equal(1, configChangeListenerCounter.NumberOfFailedConfigurationLoads); Assert.Equal(2, configChangeListenerCounter.NumberOfAppliedConfigurations); Assert.Equal(0, configChangeListenerCounter.NumberOfFailedConfigurationApplications); Assert.True(fakeConfigChangeListener.DidAtLeastOneErrorOccurWhileLoading); Assert.Equal(new[] { "A2", "B1" }, fakeConfigChangeListener.EventuallyLoaded); Assert.True(fakeConfigChangeListener.HasApplyingSucceeded); Assert.Equal(new[] { "A2", "B1" }, fakeConfigChangeListener.SuccessfullyApplied); Assert.Empty(fakeConfigChangeListener.FailedApplied); } [Fact] public async Task BuildConfig_CanBeNotifiedOfProxyConfigSuccessfulAndFailedUpdating() { var configProviderA = new InMemoryConfigProvider(new List() { }, new List() { }, "A1"); var configProviderB = new InMemoryConfigProvider(new List() { }, new List() { }, "B1"); var configChangeListenerCounter = new ConfigChangeListenerCounter(); var fakeConfigChangeListener = new FakeConfigChangeListener(); var services = CreateServices(new[] { configProviderA, configProviderB }, null, new IConfigChangeListener[] { fakeConfigChangeListener, configChangeListenerCounter }); var manager = services.GetRequiredService(); await manager.InitialLoadAsync(); Assert.Equal(2, configChangeListenerCounter.NumberOfLoadedConfigurations); Assert.Equal(0, configChangeListenerCounter.NumberOfFailedConfigurationLoads); Assert.Equal(2, configChangeListenerCounter.NumberOfAppliedConfigurations); Assert.Equal(0, configChangeListenerCounter.NumberOfFailedConfigurationApplications); Assert.False(fakeConfigChangeListener.DidAtLeastOneErrorOccurWhileLoading); Assert.Equal(new[] { "A1", "B1" }, fakeConfigChangeListener.EventuallyLoaded); Assert.True(fakeConfigChangeListener.HasApplyingSucceeded); Assert.Equal(new[] { "A1", "B1" }, fakeConfigChangeListener.SuccessfullyApplied); Assert.Empty(fakeConfigChangeListener.FailedApplied); const string TestAddress = "https://localhost:123/"; var cluster1 = new ClusterConfig { ClusterId = "cluster1", Destinations = new Dictionary(StringComparer.OrdinalIgnoreCase) { { "d1", new DestinationConfig { Address = TestAddress } } } }; var cluster2 = new ClusterConfig { ClusterId = "cluster2", Destinations = new Dictionary(StringComparer.OrdinalIgnoreCase) { { "d2", new DestinationConfig { Address = TestAddress } } } }; var route1 = new RouteConfig { RouteId = "route1", ClusterId = "cluster1", Match = new RouteMatch { Path = "/" } }; var route2 = new RouteConfig { RouteId = "route2", ClusterId = "cluster2", // Missing Match here will be caught by the analysis }; fakeConfigChangeListener.Reset(); configChangeListenerCounter.Reset(); configProviderA.Update(new List() { route1 }, new List() { cluster1 }, "A2"); Assert.Equal(2, configChangeListenerCounter.NumberOfLoadedConfigurations); Assert.Equal(0, configChangeListenerCounter.NumberOfFailedConfigurationLoads); Assert.Equal(2, configChangeListenerCounter.NumberOfAppliedConfigurations); Assert.Equal(0, configChangeListenerCounter.NumberOfFailedConfigurationApplications); Assert.False(fakeConfigChangeListener.DidAtLeastOneErrorOccurWhileLoading); Assert.Equal(new[] { "A2", "B1" }, fakeConfigChangeListener.EventuallyLoaded); Assert.True(fakeConfigChangeListener.HasApplyingSucceeded); Assert.Equal(new[] { "A2", "B1" }, fakeConfigChangeListener.SuccessfullyApplied); Assert.Empty(fakeConfigChangeListener.FailedApplied); fakeConfigChangeListener.Reset(); configChangeListenerCounter.Reset(); configProviderB.Update(new List() { route2 }, new List() { cluster2 }, "B2"); Assert.Equal(2, configChangeListenerCounter.NumberOfLoadedConfigurations); Assert.Equal(0, configChangeListenerCounter.NumberOfFailedConfigurationLoads); Assert.Equal(0, configChangeListenerCounter.NumberOfAppliedConfigurations); Assert.Equal(2, configChangeListenerCounter.NumberOfFailedConfigurationApplications); Assert.False(fakeConfigChangeListener.DidAtLeastOneErrorOccurWhileLoading); Assert.Equal(new[] { "A2", "B2" }, fakeConfigChangeListener.EventuallyLoaded); Assert.False(fakeConfigChangeListener.HasApplyingSucceeded); Assert.Empty(fakeConfigChangeListener.SuccessfullyApplied); Assert.Equal(new[] { "A2", "B2" }, fakeConfigChangeListener.FailedApplied); } public class DummyProxyConfig : IProxyConfig { public IReadOnlyList Routes => throw new NotImplementedException(); public IReadOnlyList Clusters => throw new NotImplementedException(); public IChangeToken ChangeToken => throw new NotImplementedException(); } [Fact] public void IProxyConfigDerivedTypes_RevisionIdIsAutomaticallySet() { IProxyConfig config = new DummyProxyConfig(); Assert.NotNull(config.RevisionId); Assert.NotEmpty(config.RevisionId); Assert.Same(config.RevisionId, config.RevisionId); } [Fact] public async Task InitialLoadAsync_ProxyHttpClientOptionsSet_CreateAndSetHttpClient() { const string TestAddress = "https://localhost:123/"; var cluster = new ClusterConfig { ClusterId = "cluster1", Destinations = new Dictionary(StringComparer.OrdinalIgnoreCase) { { "d1", new DestinationConfig { Address = TestAddress } } }, HttpClient = new HttpClientConfig { SslProtocols = SslProtocols.Tls11 | SslProtocols.Tls12, MaxConnectionsPerServer = 10, RequestHeaderEncoding = Encoding.UTF8.WebName, ResponseHeaderEncoding = Encoding.UTF8.WebName, }, HealthCheck = new HealthCheckConfig { Active = new ActiveHealthCheckConfig { Enabled = true } } }; var route = new RouteConfig { RouteId = "route1", ClusterId = "cluster1", Match = new RouteMatch { Path = "/" } }; var services = CreateServices(new List() { route }, new List() { cluster }); var manager = services.GetRequiredService(); var dataSource = await manager.InitialLoadAsync(); Assert.NotNull(dataSource); var endpoint = Assert.Single(dataSource.Endpoints); var routeConfig = endpoint.Metadata.GetMetadata(); var clusterState = routeConfig.Cluster; Assert.Equal("cluster1", clusterState.ClusterId); var clusterModel = clusterState.Model; Assert.NotNull(clusterModel.HttpClient); Assert.Equal(SslProtocols.Tls11 | SslProtocols.Tls12, clusterModel.Config.HttpClient.SslProtocols); Assert.Equal(10, clusterModel.Config.HttpClient.MaxConnectionsPerServer); Assert.Equal(Encoding.UTF8.WebName, clusterModel.Config.HttpClient.RequestHeaderEncoding); Assert.Equal(Encoding.UTF8.WebName, clusterModel.Config.HttpClient.ResponseHeaderEncoding); var handler = ForwarderHttpClientFactoryTests.GetHandler(clusterModel.HttpClient); Assert.Equal(SslProtocols.Tls11 | SslProtocols.Tls12, handler.SslOptions.EnabledSslProtocols); Assert.Equal(10, handler.MaxConnectionsPerServer); Assert.Equal(Encoding.UTF8, handler.RequestHeaderEncodingSelector(default, default)); var activeMonitor = (ActiveHealthCheckMonitor)services.GetRequiredService(); Assert.True(activeMonitor.Scheduler.IsScheduled(clusterState)); } [Fact] public async Task GetChangeToken_SignalsChange() { var services = CreateServices(new List(), new List()); var inMemoryConfig = (InMemoryConfigProvider)services.GetRequiredService(); var configManager = services.GetRequiredService(); var dataSource = await configManager.InitialLoadAsync(); _ = configManager.Endpoints; // Lazily creates endpoints the first time, activates change notifications. var signaled1 = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var signaled2 = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); IReadOnlyList readEndpoints1 = null; IReadOnlyList readEndpoints2 = null; var changeToken1 = dataSource.GetChangeToken(); changeToken1.RegisterChangeCallback( _ => { readEndpoints1 = dataSource.Endpoints; signaled1.SetResult(1); }, null); // updating should signal the current change token Assert.False(signaled1.Task.IsCompleted); inMemoryConfig.Update(new List() { new RouteConfig() { RouteId = "r1", Match = new RouteMatch { Path = "/" } } }, new List()); await signaled1.Task.DefaultTimeout(); var changeToken2 = dataSource.GetChangeToken(); changeToken2.RegisterChangeCallback( _ => { readEndpoints2 = dataSource.Endpoints; signaled2.SetResult(1); }, null); // updating again should only signal the new change token Assert.False(signaled2.Task.IsCompleted); inMemoryConfig.Update(new List() { new RouteConfig() { RouteId = "r2", Match = new RouteMatch { Path = "/" } } }, new List()); await signaled2.Task.DefaultTimeout(); Assert.NotNull(readEndpoints1); Assert.NotNull(readEndpoints2); } [Fact] public async Task GetChangeToken_MultipleConfigs_SignalsChange() { var config1 = new InMemoryConfigProvider(new List(), new List()); var config2 = new InMemoryConfigProvider(new List(), new List()); var services = CreateServices(new[] { config1, config2 }); var configManager = services.GetRequiredService(); var dataSource = await configManager.InitialLoadAsync(); _ = configManager.Endpoints; // Lazily creates endpoints the first time, activates change notifications. var signaled1 = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var signaled2 = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); IReadOnlyList readEndpoints1 = null; IReadOnlyList readEndpoints2 = null; var changeToken1 = dataSource.GetChangeToken(); changeToken1.RegisterChangeCallback( _ => { readEndpoints1 = dataSource.Endpoints; signaled1.SetResult(1); }, null); // updating should signal the current change token Assert.False(signaled1.Task.IsCompleted); config1.Update(new List() { new RouteConfig() { RouteId = "r1", Match = new RouteMatch { Path = "/" } } }, new List()); await signaled1.Task.DefaultTimeout(); var changeToken2 = dataSource.GetChangeToken(); changeToken2.RegisterChangeCallback( _ => { readEndpoints2 = dataSource.Endpoints; signaled2.SetResult(1); }, null); // updating again should only signal the new change token Assert.False(signaled2.Task.IsCompleted); config2.Update(new List() { new RouteConfig() { RouteId = "r2", Match = new RouteMatch { Path = "/" } } }, new List()); await signaled2.Task.DefaultTimeout(); var endpoint = Assert.Single(readEndpoints1); Assert.Equal("r1", endpoint.DisplayName); Assert.NotNull(readEndpoints2); Assert.Equal(2, readEndpoints2.Count); // Ordering is unstable due to dictionary storage. readEndpoints2.Single(e => string.Equals(e.DisplayName, "r1")); readEndpoints2.Single(e => string.Equals(e.DisplayName, "r2")); } [Fact] public async Task ChangeConfig_ActiveHealthCheckIsEnabled_RunInitialCheck() { var endpoints = new List() { new RouteConfig() { RouteId = "r1", ClusterId = "c1", Match = new RouteMatch { Path = "/" } } }; var clusters = new List() { new ClusterConfig { ClusterId = "c1" } }; var services = CreateServices(endpoints, clusters); var inMemoryConfig = (InMemoryConfigProvider)services.GetRequiredService(); var configManager = services.GetRequiredService(); var dataSource = await configManager.InitialLoadAsync(); var endpoint = Assert.Single(dataSource.Endpoints); var routeConfig = endpoint.Metadata.GetMetadata(); var clusterState = routeConfig.Cluster; var activeMonitor = (ActiveHealthCheckMonitor)services.GetRequiredService(); Assert.False(activeMonitor.Scheduler.IsScheduled(clusterState)); var signaled = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var changeToken = dataSource.GetChangeToken(); changeToken.RegisterChangeCallback( _ => { signaled.SetResult(1); }, null); // updating should signal the current change token Assert.False(signaled.Task.IsCompleted); inMemoryConfig.Update( endpoints, new List() { new ClusterConfig { ClusterId = "c1", HealthCheck = new HealthCheckConfig { Active = new ActiveHealthCheckConfig { Enabled = true } } } }); await signaled.Task.DefaultTimeout(); Assert.True(activeMonitor.Scheduler.IsScheduled(clusterState)); } [Fact] public async Task ChangeConfig_DestinationChange_IsReflectedOnRouteConfiguration() { var endpoints = new List() { new RouteConfig() { RouteId = "r1", ClusterId = "c1", Match = new RouteMatch { Path = "/" } } }; var clusters = new List() { new ClusterConfig { ClusterId = "c1", Destinations = new Dictionary(StringComparer.OrdinalIgnoreCase) { { "d1", new DestinationConfig() { Address = "http://d1" } } } } }; var services = CreateServices(endpoints, clusters); var inMemoryConfig = (InMemoryConfigProvider)services.GetRequiredService(); var configManager = services.GetRequiredService(); var dataSource = await configManager.InitialLoadAsync(); var endpoint = Assert.Single(dataSource.Endpoints); var routeConfig = endpoint.Metadata.GetMetadata(); Assert.Equal("http://d1", Assert.Single(routeConfig.Cluster.Destinations).Value.Model.Config.Address); Assert.Equal(1, routeConfig.Cluster.Revision); inMemoryConfig.Update( endpoints, new List() { new ClusterConfig { ClusterId = "c1", Destinations = new Dictionary(StringComparer.OrdinalIgnoreCase) { { "d1", new DestinationConfig() { Address = "http://d1-v2" } } } } }); var destinationConfig = Assert.Single(routeConfig.Cluster.Destinations).Value.Model.Config; Assert.Equal("http://d1-v2", destinationConfig.Address); Assert.Same(destinationConfig, Assert.Single(routeConfig.Cluster.DestinationsState.AllDestinations).Model.Config); Assert.Same(destinationConfig, Assert.Single(routeConfig.Cluster.Model.Config.Destinations).Value); // Destination changes do not affect this property Assert.Equal(1, routeConfig.Cluster.Revision); } [Fact] public async Task LoadAsync_RequestVersionValidationError_Throws() { const string TestAddress = "https://localhost:123/"; var cluster = new ClusterConfig { ClusterId = "cluster1", Destinations = new Dictionary(StringComparer.OrdinalIgnoreCase) { { "d1", new DestinationConfig { Address = TestAddress } } }, HttpRequest = new ForwarderRequestConfig() { Version = new Version(1, 2) } }; var services = CreateServices(new List(), new List() { cluster }); var configManager = services.GetRequiredService(); var ioEx = await Assert.ThrowsAsync(() => configManager.InitialLoadAsync()); Assert.Equal("Unable to load or apply the proxy configuration.", ioEx.Message); var agex = Assert.IsType(ioEx.InnerException); Assert.Single(agex.InnerExceptions); var argex = Assert.IsType(agex.InnerExceptions.First()); Assert.StartsWith("Outgoing request version", argex.Message); } [Fact] public async Task LoadAsync_RouteValidationError_Throws() { var routeName = "route1"; var route1 = new RouteConfig { RouteId = routeName, Match = new RouteMatch { Hosts = null }, ClusterId = "cluster1" }; var services = CreateServices(new List() { route1 }, new List()); var configManager = services.GetRequiredService(); var ioEx = await Assert.ThrowsAsync(() => configManager.InitialLoadAsync()); Assert.Equal("Unable to load or apply the proxy configuration.", ioEx.Message); var agex = Assert.IsType(ioEx.InnerException); Assert.Single(agex.InnerExceptions); var argex = Assert.IsType(agex.InnerExceptions.First()); Assert.StartsWith($"Route '{routeName}' requires Hosts or Path specified", argex.Message); } [Fact] public async Task LoadAsync_MultipleSourcesWithValidationErrors_Throws() { var route1 = new RouteConfig { RouteId = "route1", Match = new RouteMatch { Hosts = null }, ClusterId = "cluster1" }; var provider1 = new InMemoryConfigProvider(new List() { route1 }, new List()); var cluster1 = new ClusterConfig { ClusterId = "cluster1", HttpClient = new HttpClientConfig { MaxConnectionsPerServer = -1 } }; var provider2 = new InMemoryConfigProvider(new List(), new List() { cluster1 }); var services = CreateServices(new[] { provider1, provider2 }); var configManager = services.GetRequiredService(); var ioEx = await Assert.ThrowsAsync(() => configManager.InitialLoadAsync()); Assert.Equal("Unable to load or apply the proxy configuration.", ioEx.Message); var agex = Assert.IsType(ioEx.InnerException); Assert.Equal(2, agex.InnerExceptions.Count); var argex = Assert.IsType(agex.InnerExceptions.First()); Assert.StartsWith($"Route 'route1' requires Hosts or Path specified", argex.Message); argex = Assert.IsType(agex.InnerExceptions.Skip(1).First()); Assert.StartsWith($"Max connections per server limit set on the cluster 'cluster1' must be positive.", argex.Message); } [Fact] public async Task LoadAsync_ConfigFilterRouteActions_CanFixBrokenRoute() { var route1 = new RouteConfig { RouteId = "route1", Match = new RouteMatch { Hosts = null }, Order = 1, ClusterId = "cluster1" }; var services = CreateServices(new List() { route1 }, new List(), proxyBuilder => { proxyBuilder.AddConfigFilter(); }); var configManager = services.GetRequiredService(); var dataSource = await configManager.InitialLoadAsync(); var endpoints = dataSource.Endpoints; Assert.Single(endpoints); var endpoint = endpoints.Single(); Assert.Same(route1.RouteId, endpoint.DisplayName); var hostMetadata = endpoint.Metadata.GetMetadata(); Assert.NotNull(hostMetadata); var host = Assert.Single(hostMetadata.Hosts); Assert.Equal("example.com", host); } private class FixRouteHostFilter : IProxyConfigFilter { public ValueTask ConfigureClusterAsync(ClusterConfig cluster, CancellationToken cancel) { return new ValueTask(cluster); } public ValueTask ConfigureRouteAsync(RouteConfig route, ClusterConfig cluster, CancellationToken cancel) { return new ValueTask(route with { Match = route.Match with { Hosts = new[] { "example.com" } } }); } } private class ClusterAndRouteFilter : IProxyConfigFilter { public ValueTask ConfigureClusterAsync(ClusterConfig cluster, CancellationToken cancel) { return new ValueTask(cluster with { HealthCheck = new HealthCheckConfig() { Active = new ActiveHealthCheckConfig { Enabled = true, Interval = TimeSpan.FromSeconds(12), Policy = "activePolicyA" } } }); } public ValueTask ConfigureRouteAsync(RouteConfig route, ClusterConfig cluster, CancellationToken cancel) { string order; if (cluster is not null) { order = cluster.Metadata["Order"]; } else { order = "12"; } return new ValueTask(route with { Order = int.Parse(order) }); } } [Fact] public async Task LoadAsync_ConfigFilterConfiguresCluster_Works() { var route1 = new RouteConfig { RouteId = "route1", ClusterId = "cluster1", Match = new RouteMatch { Path = "/" } }; var route2 = new RouteConfig { RouteId = "route2", ClusterId = "cluster2", Match = new RouteMatch { Path = "/" } }; var cluster = new ClusterConfig() { ClusterId = "cluster1", Destinations = new Dictionary(StringComparer.OrdinalIgnoreCase) { { "d1", new DestinationConfig() { Address = "http://localhost" } } }, Metadata = new Dictionary { ["Order"] = "47" } }; var services = CreateServices(new List() { route1, route2 }, new List() { cluster }, proxyBuilder => { proxyBuilder.AddConfigFilter(); }); var manager = services.GetRequiredService(); var dataSource = await manager.InitialLoadAsync(); Assert.NotNull(dataSource); Assert.Equal(2, dataSource.Endpoints.Count); var endpoint1 = Assert.Single(dataSource.Endpoints, e => e.DisplayName == "route1"); var routeConfig1 = endpoint1.Metadata.GetMetadata(); Assert.Equal(47, routeConfig1.Config.Order); var clusterState1 = routeConfig1.Cluster; Assert.NotNull(clusterState1); Assert.True(clusterState1.Model.Config.HealthCheck.Active.Enabled); Assert.Equal(TimeSpan.FromSeconds(12), clusterState1.Model.Config.HealthCheck.Active.Interval); var destination = Assert.Single(clusterState1.DestinationsState.AllDestinations); Assert.Equal("http://localhost", destination.Model.Config.Address); var endpoint2 = Assert.Single(dataSource.Endpoints, e => e.DisplayName == "route2"); var routeConfig2 = endpoint2.Metadata.GetMetadata(); Assert.Equal(12, routeConfig2.Config.Order); } private class ClusterAndRouteThrows : IProxyConfigFilter { public ValueTask ConfigureClusterAsync(ClusterConfig cluster, CancellationToken cancel) { throw new NotFiniteNumberException("Test exception"); } public ValueTask ConfigureRouteAsync(RouteConfig route, ClusterConfig cluster, CancellationToken cancel) { throw new NotFiniteNumberException("Test exception"); } } [Fact] public async Task LoadAsync_ConfigFilterClusterActionThrows_Throws() { var cluster = new ClusterConfig() { ClusterId = "cluster1", Destinations = new Dictionary(StringComparer.OrdinalIgnoreCase) { { "d1", new DestinationConfig() { Address = "http://localhost" } } } }; var services = CreateServices(new List(), new List() { cluster }, proxyBuilder => { proxyBuilder.AddConfigFilter(); proxyBuilder.AddConfigFilter(); }); var configManager = services.GetRequiredService(); var ioEx = await Assert.ThrowsAsync(() => configManager.InitialLoadAsync()); Assert.Equal("Unable to load or apply the proxy configuration.", ioEx.Message); var agex = Assert.IsType(ioEx.InnerException); Assert.Single(agex.InnerExceptions); Assert.IsType(agex.InnerExceptions.First().InnerException); } [Fact] public async Task LoadAsync_ConfigFilterRouteActionThrows_Throws() { var route1 = new RouteConfig { RouteId = "route1", Match = new RouteMatch { Hosts = new[] { "example.com" } }, Order = 1, ClusterId = "cluster1" }; var route2 = new RouteConfig { RouteId = "route2", Match = new RouteMatch { Hosts = new[] { "example2.com" } }, Order = 1, ClusterId = "cluster2" }; var services = CreateServices(new List() { route1, route2 }, new List(), proxyBuilder => { proxyBuilder.AddConfigFilter(); proxyBuilder.AddConfigFilter(); }); var configManager = services.GetRequiredService(); var ioEx = await Assert.ThrowsAsync(() => configManager.InitialLoadAsync()); Assert.Equal("Unable to load or apply the proxy configuration.", ioEx.Message); var agex = Assert.IsType(ioEx.InnerException); Assert.Equal(2, agex.InnerExceptions.Count); Assert.IsType(agex.InnerExceptions.First().InnerException); Assert.IsType(agex.InnerExceptions.Skip(1).First().InnerException); } private class FakeDestinationResolver : IDestinationResolver { private readonly Func, CancellationToken, ValueTask> _delegate; public FakeDestinationResolver( Func, CancellationToken, ValueTask> @delegate) { _delegate = @delegate; } public ValueTask ResolveDestinationsAsync(IReadOnlyDictionary destinations, CancellationToken cancellationToken) => _delegate(destinations, cancellationToken); } private class TestConfigChangeListener : IConfigChangeListener { private readonly bool _includeLoad; private readonly bool _includeApply; public Channel Events { get; } = Channel.CreateUnbounded(); public TestConfigChangeListener(bool includeLoad = true, bool includeApply = true) { _includeLoad = includeLoad; _includeApply = includeApply; } public void ConfigurationApplied(IReadOnlyList proxyConfigs) { if (!_includeApply) { return; } Assert.True(Events.Writer.TryWrite(new ConfigurationAppliedEvent(proxyConfigs))); } public void ConfigurationApplyingFailed(IReadOnlyList proxyConfigs, Exception exception) { if (!_includeApply) { return; } Assert.True(Events.Writer.TryWrite(new ConfigurationApplyingFailedEvent(proxyConfigs, exception))); } public void ConfigurationLoaded(IReadOnlyList proxyConfigs) { if (!_includeLoad) { return; } Assert.True(Events.Writer.TryWrite(new ConfigurationLoadedEvent(proxyConfigs))); } public void ConfigurationLoadingFailed(IProxyConfigProvider configProvider, Exception exception) { if (!_includeLoad) { return; } Assert.True(Events.Writer.TryWrite(new ConfigurationLoadingFailedEvent(configProvider, exception))); } public record ConfigChangeListenerEvent { }; public record ConfigurationAppliedEvent(IReadOnlyList ProxyConfigs) : ConfigChangeListenerEvent; public record ConfigurationApplyingFailedEvent(IReadOnlyList ProxyConfigs, Exception exception) : ConfigChangeListenerEvent; public record ConfigurationLoadedEvent(IReadOnlyList ProxyConfigs) : ConfigChangeListenerEvent; public record ConfigurationLoadingFailedEvent(IProxyConfigProvider ConfigProvider, Exception Exception) : ConfigChangeListenerEvent; } [Fact] public async Task LoadAsync_DestinationResolver_Initial_ThrowsSync() { var throwResolver = new FakeDestinationResolver((destinations, cancellation) => throw new InvalidOperationException("Throwing!")); var cluster = new ClusterConfig() { ClusterId = "cluster1", Destinations = new Dictionary(StringComparer.OrdinalIgnoreCase) { { "d1", new DestinationConfig() { Address = "http://localhost" } } } }; var services = CreateServices( new List(), new List() { cluster }, destinationResolver: throwResolver); var configManager = services.GetRequiredService(); var ioEx = await Assert.ThrowsAsync(() => configManager.InitialLoadAsync()); Assert.Equal("Unable to load or apply the proxy configuration.", ioEx.Message); var innerExc = Assert.IsType(ioEx.InnerException); Assert.Equal("Throwing!", innerExc.Message); } [Fact] public async Task LoadAsync_DestinationResolver_Initial_ThrowsAsync() { var throwResolver = new FakeDestinationResolver((destinations, cancellation) => ValueTask.FromException(new InvalidOperationException("Throwing!"))); var cluster = new ClusterConfig() { ClusterId = "cluster1", Destinations = new Dictionary(StringComparer.OrdinalIgnoreCase) { { "d1", new DestinationConfig() { Address = "http://localhost" } } } }; var services = CreateServices(new List(), new List() { cluster }, destinationResolver: throwResolver); var configManager = services.GetRequiredService(); var ioEx = await Assert.ThrowsAsync(() => configManager.InitialLoadAsync()); Assert.Equal("Unable to load or apply the proxy configuration.", ioEx.Message); var innerExc1 = Assert.IsType(ioEx.InnerException); Assert.Equal("Error resolving destinations for cluster cluster1", innerExc1.Message); var innerExc2 = Assert.IsType(innerExc1.InnerException); Assert.Equal("Throwing!", innerExc2.Message); } [Fact] public async Task LoadAsync_DestinationResolver_Successful() { var destinationsToExpand = new Dictionary(StringComparer.OrdinalIgnoreCase) { { "d1", new DestinationConfig() { Address = "http://localhost" } } }; var syncExpandResolver = new FakeDestinationResolver((destinations, cancellation) => { var expandedDestinations = new Dictionary(); foreach (var destKvp in destinations) { expandedDestinations[$"{destKvp.Key}-1"] = new DestinationConfig { Address = "http://127.0.0.1:8080" }; expandedDestinations[$"{destKvp.Key}-2"] = new DestinationConfig { Address = "http://127.1.1.1:8080" }; } var result = new ResolvedDestinationCollection(expandedDestinations, null); return new(result); }); var cluster1 = new ClusterConfig() { ClusterId = "cluster1", Destinations = destinationsToExpand }; var services = CreateServices(new List(), new List() { cluster1 }, destinationResolver: syncExpandResolver); var configManager = services.GetRequiredService(); await configManager.InitialLoadAsync(); Assert.True(configManager.TryGetCluster(cluster1.ClusterId, out var cluster)); var expectedDestinations = new Dictionary(StringComparer.OrdinalIgnoreCase) { { "d1-1", new DestinationConfig() { Address = "http://127.0.0.1:8080" } }, { "d1-2", new DestinationConfig() { Address = "http://127.1.1.1:8080" } } }; var actualDestinations = cluster.Destinations.ToDictionary(static k => k.Key, static v => v.Value.Model.Config); Assert.Equal(expectedDestinations, actualDestinations); } [Fact] public async Task LoadAsync_DestinationResolver_Dns() { var destinationsToExpand = new Dictionary(StringComparer.OrdinalIgnoreCase) { { "d1", new DestinationConfig() { Address = "http://localhost/a/b/c", Health = "http://localhost/healthz" } }, { "d2", new DestinationConfig() { Address = "http://localhost:8080/a/b/c", Health = "http://localhost:8080/healthz"} }, { "d3", new DestinationConfig() { Address = "https://localhost/a/b/c", Health = "https://localhost/healthz" } }, { "d4", new DestinationConfig() { Address = "https://localhost:8443/a/b/c", Health = "https://localhost:8443/healthz", Host = "overriddenhost" } } }; var cluster1 = new ClusterConfig() { ClusterId = "cluster1", Destinations = destinationsToExpand }; var services = CreateServices( new List(), new List() { cluster1 }, configureProxy: rp => rp.AddDnsDestinationResolver(o => o.AddressFamily = AddressFamily.InterNetwork)); var configManager = services.GetRequiredService(); await configManager.InitialLoadAsync(); Assert.True(configManager.TryGetCluster(cluster1.ClusterId, out var cluster)); var expectedDestinations = new Dictionary(StringComparer.OrdinalIgnoreCase) { { "d1[127.0.0.1]", new DestinationConfig() { Address = "http://127.0.0.1/a/b/c", Health = "http://127.0.0.1/healthz", Host = "localhost" } }, { "d2[127.0.0.1]", new DestinationConfig() { Address = "http://127.0.0.1:8080/a/b/c", Health = "http://127.0.0.1:8080/healthz", Host = "localhost:8080" } }, { "d3[127.0.0.1]", new DestinationConfig() { Address = "https://127.0.0.1/a/b/c", Health = "https://127.0.0.1/healthz", Host = "localhost" } }, { "d4[127.0.0.1]", new DestinationConfig() { Address = "https://127.0.0.1:8443/a/b/c", Health = "https://127.0.0.1:8443/healthz", Host = "overriddenhost" } } }; var actualDestinations = cluster.Destinations.ToDictionary(static k => k.Key, static v => v.Value.Model.Config); Assert.Equal(expectedDestinations, actualDestinations); } [Fact] public async Task LoadAsync_DestinationResolver_ReloadResolution() { var configListener = new TestConfigChangeListener(includeApply: false); var destinationsToExpand = new Dictionary(StringComparer.OrdinalIgnoreCase) { { "d1", new DestinationConfig() { Address = "http://localhost" } } }; var cts = new[] { new CancellationTokenSource() }; var signaled = new[] { 0 }; var syncExpandResolver = new FakeDestinationResolver((destinations, cancellation) => { signaled[0]++; var expandedDestinations = new Dictionary(); foreach (var destKvp in destinations) { expandedDestinations[$"{destKvp.Key}-1"] = new DestinationConfig { Address = $"http://127.0.0.1:8080/{signaled[0]}" }; expandedDestinations[$"{destKvp.Key}-2"] = new DestinationConfig { Address = $"http://127.1.1.1:8080/{signaled[0]}" }; } var result = new ResolvedDestinationCollection(expandedDestinations, new CancellationChangeToken(cts[0].Token)); return new(result); }); var cluster1 = new ClusterConfig() { ClusterId = "cluster1", Destinations = destinationsToExpand }; var services = CreateServices( new List(), new List() { cluster1 }, configListeners: new[] { configListener }, destinationResolver: syncExpandResolver); var configManager = services.GetRequiredService(); await configManager.InitialLoadAsync(); var configEvent = await configListener.Events.Reader.ReadAsync(); var configLoadEvent = Assert.IsType(configEvent); Assert.True(configManager.TryGetCluster(cluster1.ClusterId, out var cluster)); var expectedDestinations = new Dictionary(StringComparer.OrdinalIgnoreCase) { { "d1-1", new DestinationConfig() { Address = "http://127.0.0.1:8080/1" } }, { "d1-2", new DestinationConfig() { Address = "http://127.1.1.1:8080/1" } } }; var actualDestinations = cluster.Destinations.ToDictionary(static k => k.Key, static v => v.Value.Model.Config); Assert.Equal(expectedDestinations, actualDestinations); // Trigger the change token and wait for a subsequent load var initialCts = cts[0]; cts[0] = new(); initialCts.Cancel(); configEvent = await configListener.Events.Reader.ReadAsync(); configLoadEvent = Assert.IsType(configEvent); Assert.True(configManager.TryGetCluster(cluster1.ClusterId, out cluster)); expectedDestinations = new Dictionary(StringComparer.OrdinalIgnoreCase) { { "d1-1", new DestinationConfig() { Address = "http://127.0.0.1:8080/2" } }, { "d1-2", new DestinationConfig() { Address = "http://127.1.1.1:8080/2" } } }; actualDestinations = cluster.Destinations.ToDictionary(static k => k.Key, static v => v.Value.Model.Config); Assert.Equal(expectedDestinations, actualDestinations); } [Fact] public async Task LoadAsync_DestinationResolver_Reload_ThrowsSync() { var configListener = new TestConfigChangeListener(includeApply: false); var cts = new CancellationTokenSource(); var syncThrowResolver = new FakeDestinationResolver((destinations, cancellation) => { if (cts.IsCancellationRequested) { throw new InvalidOperationException("Throwing!"); } else { return new(new ResolvedDestinationCollection(destinations, new CancellationChangeToken(cts.Token))); } }); var cluster = new ClusterConfig() { ClusterId = "cluster1", Destinations = new Dictionary(StringComparer.OrdinalIgnoreCase) { { "d1", new DestinationConfig() { Address = "http://localhost" } } } }; var services = CreateServices( new List(), new List() { cluster }, configListeners: new[] { configListener }, destinationResolver: syncThrowResolver); var configManager = services.GetRequiredService(); await configManager.InitialLoadAsync(); // Read the successful load event Assert.IsType(await configListener.Events.Reader.ReadAsync()); // Trigger invalidation cts.Cancel(); // Read the failure event var configLoadException = Assert.IsType(await configListener.Events.Reader.ReadAsync()); var ex = configLoadException.Exception; Assert.Equal("Throwing!", ex.Message); } [Fact] public async Task LoadAsync_DestinationResolver_Reload_ThrowsAsync() { var configListener = new TestConfigChangeListener(includeApply: false); var cts = new CancellationTokenSource(); var syncThrowResolver = new FakeDestinationResolver(async (destinations, cancellation) => { await Task.Yield(); if (cts.IsCancellationRequested) { throw new InvalidOperationException("Throwing!"); } else { return new ResolvedDestinationCollection(destinations, new CancellationChangeToken(cts.Token)); } }); var cluster = new ClusterConfig() { ClusterId = "cluster1", Destinations = new Dictionary(StringComparer.OrdinalIgnoreCase) { { "d1", new DestinationConfig() { Address = "http://localhost" } } } }; var services = CreateServices( new List(), new List() { cluster }, configListeners: new[] { configListener }, destinationResolver: syncThrowResolver); var configManager = services.GetRequiredService(); await configManager.InitialLoadAsync(); // Read the successful load event Assert.IsType(await configListener.Events.Reader.ReadAsync()); // Trigger invalidation cts.Cancel(); // Read the failure event var configLoadException = Assert.IsType(await configListener.Events.Reader.ReadAsync()); var innerExc1 = Assert.IsType(configLoadException.Exception); Assert.Equal("Error resolving destinations for cluster cluster1", innerExc1.Message); var innerExc2 = Assert.IsType(innerExc1.InnerException); Assert.Equal("Throwing!", innerExc2.Message); } } ================================================ FILE: test/ReverseProxy.Tests/Model/DestinationStateTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using Xunit; namespace Yarp.ReverseProxy.Model.Tests; public class DestinationStateTests { [Fact] public void DestinationInfoEnumerator() { var destinationInfo = new DestinationState("destination1"); var list = new List(); foreach (var item in destinationInfo) { list.Add(item); } var first = Assert.Single(list); Assert.Same(destinationInfo, first); } [Fact] public void DestinationInfoReadOnlyList() { var destinationInfo = new DestinationState("destination2"); IReadOnlyList list = destinationInfo; Assert.Single(list); Assert.Same(destinationInfo, list[0]); Assert.Throws(() => list[1]); } } ================================================ FILE: test/ReverseProxy.Tests/Model/HttpContextFeaturesExtensions.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Net.Http; using Microsoft.AspNetCore.Http; using Xunit; using Yarp.ReverseProxy.Configuration; using Yarp.ReverseProxy.Forwarder; namespace Yarp.ReverseProxy.Model.Tests; public class HttpContextFeaturesExtensions { [Fact] public void ReassignProxyRequest_Success() { var client = new HttpMessageInvoker(new SocketsHttpHandler()); var context = new DefaultHttpContext(); var d1 = new DestinationState("d1"); var d2 = new DestinationState("d2"); var cc1 = new ClusterConfig() { ClusterId = "c1" }; var cm1 = new ClusterModel(cc1, client); var cs1 = new ClusterState("c1") { Model = cm1 }; var r1 = new RouteModel(new RouteConfig() { RouteId = "r1" }, cs1, HttpTransformer.Empty); var feature = new ReverseProxyFeature() { AllDestinations = d1, AvailableDestinations = d1, Cluster = cm1, Route = r1, ProxiedDestination = d1, }; context.Features.Set(feature); var cc2 = new ClusterConfig() { ClusterId = "cc2" }; var cm2 = new ClusterModel(cc2, client); var cs2 = new ClusterState("cs2") { DestinationsState = new ClusterDestinationsState(d2, d2), Model = cm2, }; context.ReassignProxyRequest(cs2); var newFeature = context.GetReverseProxyFeature(); Assert.NotSame(feature, newFeature); Assert.Same(d2, newFeature.AllDestinations); Assert.Same(d2, newFeature.AvailableDestinations); Assert.Same(d1, newFeature.ProxiedDestination); // Copied unmodified. Assert.Same(cm2, newFeature.Cluster); Assert.Same(r1, newFeature.Route); // Begin testing ReassignProxyRequest(route, cluster) overload var r2 = new RouteModel(new RouteConfig() { RouteId = "r2" }, cs2, HttpTransformer.Empty); context.ReassignProxyRequest(r2, cs2); var newFeatureOverload = context.GetReverseProxyFeature(); Assert.NotSame(newFeature, newFeatureOverload); Assert.Same(d2, newFeatureOverload.AllDestinations); // Unmodified Assert.Same(d2, newFeatureOverload.AvailableDestinations); // Unmodified Assert.Same(d1, newFeatureOverload.ProxiedDestination); // Unmodified Assert.Same(cm2, newFeatureOverload.Cluster); // Unmodified Assert.Same(r2, newFeatureOverload.Route); // Asset route update } } ================================================ FILE: test/ReverseProxy.Tests/Model/ProxyPipelineInitializerMiddlewareTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Timeouts; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Patterns; using Moq; using Xunit; using Yarp.Tests.Common; using Yarp.ReverseProxy.Configuration; using Yarp.ReverseProxy.Forwarder; using System.Diagnostics; namespace Yarp.ReverseProxy.Model.Tests; public class ProxyPipelineInitializerMiddlewareTests : TestAutoMockBase { public ProxyPipelineInitializerMiddlewareTests() { Provide(context => { context.Response.StatusCode = StatusCodes.Status418ImATeapot; return Task.CompletedTask; }); } [Fact] public void Constructor_Works() { Create(); } [Fact] public async Task Invoke_SetsFeatures() { var httpClient = new HttpMessageInvoker(new Mock().Object); var cluster1 = new ClusterState(clusterId: "cluster1"); cluster1.Model = new ClusterModel(new ClusterConfig(), httpClient); var destination1 = cluster1.Destinations.GetOrAdd( "destination1", id => new DestinationState(id) { Model = new DestinationModel(new DestinationConfig { Address = "https://localhost:123/a/b/" }) }); cluster1.DestinationsState = new ClusterDestinationsState(new[] { destination1 }, new[] { destination1 }); var aspNetCoreEndpoints = new List(); var routeConfig = new RouteModel( config: new RouteConfig(), cluster1, HttpTransformer.Default); var aspNetCoreEndpoint = CreateAspNetCoreEndpoint(routeConfig); aspNetCoreEndpoints.Add(aspNetCoreEndpoint); var httpContext = new DefaultHttpContext(); httpContext.SetEndpoint(aspNetCoreEndpoint); var sut = Create(); await sut.Invoke(httpContext); var proxyFeature = httpContext.GetReverseProxyFeature(); Assert.NotNull(proxyFeature); Assert.NotNull(proxyFeature.AvailableDestinations); Assert.Single(proxyFeature.AvailableDestinations); Assert.Same(destination1, proxyFeature.AvailableDestinations[0]); Assert.Same(cluster1.Model, proxyFeature.Cluster); Assert.Equal(StatusCodes.Status418ImATeapot, httpContext.Response.StatusCode); } [Fact] public async Task Invoke_NoHealthyEndpoints_CallsNext() { var httpClient = new HttpMessageInvoker(new Mock().Object); var cluster1 = new ClusterState(clusterId: "cluster1"); cluster1.Model = new ClusterModel( new ClusterConfig() { HealthCheck = new HealthCheckConfig { Active = new ActiveHealthCheckConfig { Enabled = true, Timeout = Timeout.InfiniteTimeSpan, Interval = Timeout.InfiniteTimeSpan, Policy = "Any5xxResponse", } } }, httpClient); var destination1 = cluster1.Destinations.GetOrAdd( "destination1", id => new DestinationState(id) { Model = new DestinationModel(new DestinationConfig { Address = "https://localhost:123/a/b/" }), Health = { Active = DestinationHealth.Unhealthy }, }); cluster1.DestinationsState = new ClusterDestinationsState(new[] { destination1 }, Array.Empty()); var aspNetCoreEndpoints = new List(); var routeConfig = new RouteModel( config: new RouteConfig(), cluster: cluster1, transformer: HttpTransformer.Default); var aspNetCoreEndpoint = CreateAspNetCoreEndpoint(routeConfig); aspNetCoreEndpoints.Add(aspNetCoreEndpoint); var httpContext = new DefaultHttpContext(); httpContext.SetEndpoint(aspNetCoreEndpoint); var sut = Create(); await sut.Invoke(httpContext); var feature = httpContext.Features.Get(); Assert.NotNull(feature); Assert.Single(feature.AllDestinations, destination1); Assert.Empty(feature.AvailableDestinations); Assert.Equal(StatusCodes.Status418ImATeapot, httpContext.Response.StatusCode); } [Theory] [InlineData(1)] [InlineData(Timeout.Infinite)] public async Task Invoke_MissingTimeoutMiddleware_RefuseRequest(int timeoutMs) { var httpClient = new HttpMessageInvoker(new Mock().Object); var cluster1 = new ClusterState(clusterId: "cluster1") { Model = new ClusterModel(new ClusterConfig(), httpClient) }; var aspNetCoreEndpoints = new List(); var routeConfig = new RouteModel( config: new RouteConfig(), cluster: cluster1, transformer: HttpTransformer.Default); var aspNetCoreEndpoint = CreateAspNetCoreEndpoint(routeConfig, builder => { builder.Metadata.Add(new RequestTimeoutAttribute(timeoutMs)); }); aspNetCoreEndpoints.Add(aspNetCoreEndpoint); var httpContext = new DefaultHttpContext(); httpContext.SetEndpoint(aspNetCoreEndpoint); var sut = Create(); if (timeoutMs == Timeout.Infinite || Debugger.IsAttached) { // If the timeout was infinite or the debugger is attached, we shouldn't refuse the request. await sut.Invoke(httpContext); } else { await Assert.ThrowsAsync(() => sut.Invoke(httpContext)); } } private static Endpoint CreateAspNetCoreEndpoint(RouteModel routeConfig, Action configure = null) { var endpointBuilder = new RouteEndpointBuilder( requestDelegate: httpContext => Task.CompletedTask, routePattern: RoutePatternFactory.Parse("/"), order: 0); endpointBuilder.Metadata.Add(routeConfig); configure?.Invoke(endpointBuilder); return endpointBuilder.Build(); } } ================================================ FILE: test/ReverseProxy.Tests/Routing/HeaderMatcherPolicyTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Matching; using Microsoft.AspNetCore.Routing.Patterns; using Xunit; using Yarp.ReverseProxy.Configuration; namespace Yarp.ReverseProxy.Routing.Tests; public class HeaderMatcherPolicyTests { [Fact] public void Comparer_SortOrder_SingleRuleEqual() { // Most specific to least var endpoints = new[] { (0, CreateEndpoint("header", new[] { "abc" }, HeaderMatchMode.ExactHeader, isCaseSensitive: true)), (0, CreateEndpoint("header", new[] { "abc" }, HeaderMatchMode.ExactHeader)), (0, CreateEndpoint("header", new[] { "abc", "def" }, HeaderMatchMode.ExactHeader)), (0, CreateEndpoint("header2", new[] { "abc", "def" }, HeaderMatchMode.ExactHeader)), (0, CreateEndpoint("header", new[] { "abc" }, HeaderMatchMode.HeaderPrefix, isCaseSensitive: true)), (0, CreateEndpoint("header", new[] { "abc" }, HeaderMatchMode.HeaderPrefix)), (0, CreateEndpoint("header", new[] { "abc", "def" }, HeaderMatchMode.HeaderPrefix)), (0, CreateEndpoint("header2", new[] { "abc", "def" }, HeaderMatchMode.HeaderPrefix)), (0, CreateEndpoint("header", Array.Empty(), HeaderMatchMode.Exists, isCaseSensitive: true)), (0, CreateEndpoint("header", Array.Empty(), HeaderMatchMode.Exists)), (0, CreateEndpoint("header", Array.Empty(), HeaderMatchMode.Exists, isCaseSensitive: true)), (0, CreateEndpoint("header", Array.Empty(), HeaderMatchMode.Exists)), }; var sut = new HeaderMatcherPolicy(); for (var i = 0; i < endpoints.Length; i++) { for (var j = 0; j < endpoints.Length; j++) { var a = endpoints[i]; var b = endpoints[j]; var actual = sut.Comparer.Compare(a.Item2, b.Item2); var expected = a.Item1 < b.Item1 ? -1 : a.Item1 > b.Item1 ? 1 : 0; if (actual != expected) { Assert.Fail($"Error comparing [{i}] to [{j}], expected {expected}, found {actual}."); } } } } [Fact] public void Comparer_MultipleHeaders_SortOrder() { // Most specific to least var endpoints = new[] { (0, CreateEndpoint(new[] { new HeaderMatcher("header", Array.Empty(), HeaderMatchMode.Exists, isCaseSensitive: true), new HeaderMatcher("header", new[] { "abc" }, HeaderMatchMode.HeaderPrefix, isCaseSensitive: true), new HeaderMatcher("header", new[] { "cbcabc" }, HeaderMatchMode.Contains, isCaseSensitive: true), new HeaderMatcher("header", new[] { "abc" }, HeaderMatchMode.ExactHeader, isCaseSensitive: true) })), (1, CreateEndpoint(new[] { new HeaderMatcher("header", new[] { "cbcabc" }, HeaderMatchMode.Contains, isCaseSensitive: true), new HeaderMatcher("header", new[] { "abc" }, HeaderMatchMode.ExactHeader, isCaseSensitive: true) })), (1, CreateEndpoint(new[] { new HeaderMatcher("header", Array.Empty(), HeaderMatchMode.Exists, isCaseSensitive: true), new HeaderMatcher("header", new[] { "abc" }, HeaderMatchMode.ExactHeader, isCaseSensitive: true) })), (2, CreateEndpoint("header", new[] { "abc" })), (3, CreateEndpoint(Array.Empty())), }; var sut = new HeaderMatcherPolicy(); for (var i = 0; i < endpoints.Length; i++) { for (var j = 0; j < endpoints.Length; j++) { var a = endpoints[i]; var b = endpoints[j]; var actual = sut.Comparer.Compare(a.Item2, b.Item2); var expected = a.Item1 < b.Item1 ? -1 : a.Item1 > b.Item1 ? 1 : 0; if (actual != expected) { Assert.Fail($"Error comparing [{i}] to [{j}], expected {expected}, found {actual}."); } } } } [Fact] public void AppliesToEndpoints_AppliesScenarios() { var scenarios = new[] { CreateEndpoint("org-id", Array.Empty(), HeaderMatchMode.Exists), CreateEndpoint("org-id", new[] { "abc" }), CreateEndpoint("org-id", new[] { "abc", "def" }), CreateEndpoint("org-id", Array.Empty(), HeaderMatchMode.Exists, isDynamic: true), CreateEndpoint("org-id", new[] { "abc" }, isDynamic: true), CreateEndpoint("org-id", null, HeaderMatchMode.Exists, isDynamic: true), CreateEndpoint(new[] { new HeaderMatcher("header", Array.Empty(), HeaderMatchMode.Exists, isCaseSensitive: true), new HeaderMatcher("header", new[] { "abc" }, HeaderMatchMode.ExactHeader, isCaseSensitive: true) }) }; var sut = new HeaderMatcherPolicy(); var endpointSelectorPolicy = (IEndpointSelectorPolicy)sut; for (var i = 0; i < scenarios.Length; i++) { var result = endpointSelectorPolicy.AppliesToEndpoints(new[] { scenarios[i] }); Assert.True(result, $"scenario {i}"); } } [Fact] public void AppliesToEndpoints_NoMetadata_DoesNotApply() { var endpoint = CreateEndpoint(Array.Empty()); var sut = new HeaderMatcherPolicy(); var endpointSelectorPolicy = (IEndpointSelectorPolicy)sut; var result = endpointSelectorPolicy.AppliesToEndpoints(new[] { endpoint }); Assert.False(result); } [Theory] [InlineData(null, HeaderMatchMode.Exists, false)] [InlineData("", HeaderMatchMode.Exists, false)] [InlineData("abc", HeaderMatchMode.Exists, true)] [InlineData(null, HeaderMatchMode.NotExists, true)] [InlineData("", HeaderMatchMode.NotExists, true)] [InlineData("abc", HeaderMatchMode.NotExists, false)] public async Task ApplyAsync_MatchingScenarios_AnyHeaderValue(string incomingHeaderValue, HeaderMatchMode mode, bool shouldMatch) { var context = new DefaultHttpContext(); if (incomingHeaderValue is not null) { context.Request.Headers["org-id"] = incomingHeaderValue; } var endpoint = CreateEndpoint("org-id", Array.Empty(), mode); var candidates = new CandidateSet(new[] { endpoint }, new RouteValueDictionary[1], new int[1]); var sut = new HeaderMatcherPolicy(); await sut.ApplyAsync(context, candidates); Assert.Equal(shouldMatch, candidates.IsValidCandidate(0)); } [Theory] [InlineData("abc", HeaderMatchMode.ExactHeader, false, null, false)] [InlineData("abc", HeaderMatchMode.ExactHeader, false, "", false)] [InlineData("abc", HeaderMatchMode.ExactHeader, false, "abc", true)] [InlineData("abc", HeaderMatchMode.ExactHeader, false, "aBC", true)] [InlineData("abc", HeaderMatchMode.ExactHeader, false, "abcd", false)] [InlineData("abc", HeaderMatchMode.ExactHeader, false, "ab", false)] [InlineData("abc", HeaderMatchMode.ExactHeader, true, "", false)] [InlineData("abc", HeaderMatchMode.ExactHeader, true, "abc", true)] [InlineData("abc", HeaderMatchMode.ExactHeader, true, "aBC", false)] [InlineData("abc", HeaderMatchMode.ExactHeader, true, "abcd", false)] [InlineData("abc", HeaderMatchMode.ExactHeader, true, "ab", false)] [InlineData("abc", HeaderMatchMode.ExactHeader, false, ";", false)] [InlineData("abc", HeaderMatchMode.ExactHeader, false, "ab;c", false)] [InlineData("abc", HeaderMatchMode.ExactHeader, false, ";abc", true)] [InlineData("abc", HeaderMatchMode.ExactHeader, false, ";abC", true)] [InlineData("abc", HeaderMatchMode.ExactHeader, false, "abc;", true)] [InlineData("abc", HeaderMatchMode.ExactHeader, false, "Abc;", true)] [InlineData("abc", HeaderMatchMode.ExactHeader, false, "abc;def", true)] [InlineData("abc", HeaderMatchMode.ExactHeader, false, "ABC;DEF", true)] [InlineData("abc", HeaderMatchMode.ExactHeader, false, "def;abc", true)] [InlineData("abc", HeaderMatchMode.ExactHeader, false, "abc;aBc", true)] [InlineData("abc", HeaderMatchMode.ExactHeader, false, "def;ab c", false)] [InlineData("abc", HeaderMatchMode.ExactHeader, false, "bcd;efg", false)] [InlineData("abc", HeaderMatchMode.ExactHeader, false, "\"abc", false)] [InlineData("abc", HeaderMatchMode.ExactHeader, false, "abc\"", false)] [InlineData("abc", HeaderMatchMode.ExactHeader, false, "\"abc\"", true)] [InlineData("abc", HeaderMatchMode.ExactHeader, false, " \"abc\"", true)] [InlineData("abc", HeaderMatchMode.ExactHeader, false, "\"abc\" ", false)] [InlineData("abc", HeaderMatchMode.ExactHeader, false, "\"abc\", ", true)] [InlineData("abc", HeaderMatchMode.ExactHeader, false, "\"ab\", \"abc", false)] [InlineData("abc", HeaderMatchMode.ExactHeader, false, "\"ab\", \"abc\"", true)] [InlineData("abc", HeaderMatchMode.ExactHeader, false, "ab\", \"abc", false)] [InlineData("abc", HeaderMatchMode.ExactHeader, false, "ab\"\",\"abc", false)] [InlineData("abc", HeaderMatchMode.ExactHeader, false, "ab\"\",\"abc\"", true)] [InlineData("abc", HeaderMatchMode.ExactHeader, false, "\"\"ab\"\"c", false)] [InlineData("abc", HeaderMatchMode.ExactHeader, false, "\"\"ab\"\"c,\"abc,\"", false)] [InlineData("abc", HeaderMatchMode.ExactHeader, false, "\"\"ab\"\"c,\"abc,\",abc", true)] [InlineData("abc", HeaderMatchMode.ExactHeader, true, ";", false)] [InlineData("abc", HeaderMatchMode.ExactHeader, true, "ab;c", false)] [InlineData("abc", HeaderMatchMode.ExactHeader, true, ";abc", true)] [InlineData("abc", HeaderMatchMode.ExactHeader, true, "abc;", true)] [InlineData("abc", HeaderMatchMode.ExactHeader, true, "abc;def", true)] [InlineData("abc", HeaderMatchMode.ExactHeader, true, "abc;abc", true)] [InlineData("abc", HeaderMatchMode.ExactHeader, true, "def;abc", true)] [InlineData("abc", HeaderMatchMode.ExactHeader, true, "def;abC", false)] [InlineData("abc", HeaderMatchMode.ExactHeader, true, "def;ab c", false)] [InlineData("abc", HeaderMatchMode.ExactHeader, true, "bcd;efg", false)] [InlineData("abc", HeaderMatchMode.HeaderPrefix, false, "", false)] [InlineData("abc", HeaderMatchMode.HeaderPrefix, false, "abc", true)] [InlineData("abc", HeaderMatchMode.HeaderPrefix, false, "aBC", true)] [InlineData("abc", HeaderMatchMode.HeaderPrefix, false, "abcd", true)] [InlineData("abc", HeaderMatchMode.HeaderPrefix, false, "ab", false)] [InlineData("abc", HeaderMatchMode.HeaderPrefix, true, "", false)] [InlineData("abc", HeaderMatchMode.HeaderPrefix, true, "abc", true)] [InlineData("abc", HeaderMatchMode.HeaderPrefix, true, "aBC", false)] [InlineData("abc", HeaderMatchMode.HeaderPrefix, true, "abcd", true)] [InlineData("abc", HeaderMatchMode.HeaderPrefix, true, "aBCd", false)] [InlineData("abc", HeaderMatchMode.HeaderPrefix, true, "ab", false)] [InlineData("abc", HeaderMatchMode.HeaderPrefix, false, ";", false)] [InlineData("abc", HeaderMatchMode.HeaderPrefix, false, "abc;", true)] [InlineData("abc", HeaderMatchMode.HeaderPrefix, false, ";aBc", true)] [InlineData("abc", HeaderMatchMode.HeaderPrefix, false, "abd;abC", true)] [InlineData("abc", HeaderMatchMode.HeaderPrefix, false, "abd;abe", false)] [InlineData("abc", HeaderMatchMode.HeaderPrefix, true, "abc;", true)] [InlineData("abc", HeaderMatchMode.HeaderPrefix, true, ";abc", true)] [InlineData("abc", HeaderMatchMode.HeaderPrefix, true, "abd;abc", true)] [InlineData("abc", HeaderMatchMode.HeaderPrefix, true, "abd;abe", false)] [InlineData("abc", HeaderMatchMode.HeaderPrefix, true, "ab\"c", false)] [InlineData("abc", HeaderMatchMode.HeaderPrefix, true, "ab\"c\"", false)] [InlineData("abc", HeaderMatchMode.HeaderPrefix, true, "\"abc", false)] [InlineData("abc", HeaderMatchMode.HeaderPrefix, true, "\"abc\"", true)] [InlineData("abc", HeaderMatchMode.HeaderPrefix, true, " \"abc\"", true)] [InlineData("abc", HeaderMatchMode.HeaderPrefix, true, " \"abc", false)] [InlineData("abc", HeaderMatchMode.HeaderPrefix, true, "\"abc\" ", false)] [InlineData("abc", HeaderMatchMode.HeaderPrefix, true, "ab,abc", true)] [InlineData("abc", HeaderMatchMode.HeaderPrefix, true, "ab, abc", true)] [InlineData("abc", HeaderMatchMode.HeaderPrefix, true, "\"ab, abc\"", false)] [InlineData("abc", HeaderMatchMode.HeaderPrefix, true, "\"ab\", abc", true)] [InlineData("abc", HeaderMatchMode.HeaderPrefix, true, "\"ab\", abc\"", true)] [InlineData("abc", HeaderMatchMode.HeaderPrefix, true, "\"ab\"\"\"\", abc\"", false)] [InlineData("abc", HeaderMatchMode.HeaderPrefix, true, "\"ab\"\"\"\"\", abc\"", true)] [InlineData("abc", HeaderMatchMode.Contains, false, "", false)] [InlineData("abc", HeaderMatchMode.Contains, false, "ababc", true)] [InlineData("abc", HeaderMatchMode.Contains, false, "zaBCz", true)] [InlineData("abc", HeaderMatchMode.Contains, false, "dcbaabcd", true)] [InlineData("abc", HeaderMatchMode.Contains, false, "ababab", false)] [InlineData("abc", HeaderMatchMode.Contains, true, "", false)] [InlineData("abc", HeaderMatchMode.Contains, true, "abcc", true)] [InlineData("abc", HeaderMatchMode.Contains, true, "aaaBC", false)] [InlineData("abc", HeaderMatchMode.Contains, true, "bbabcdb", true)] [InlineData("abc", HeaderMatchMode.Contains, true, "aBCcba", false)] [InlineData("abc", HeaderMatchMode.Contains, true, "baab", false)] [InlineData("abc", HeaderMatchMode.Contains, false, ";", false)] [InlineData("abc", HeaderMatchMode.Contains, false, "ababc;", true)] [InlineData("abc", HeaderMatchMode.Contains, false, ";ababc", true)] [InlineData("abc", HeaderMatchMode.Contains, false, "ab;cd", false)] [InlineData("abc", HeaderMatchMode.Contains, false, "ab;cd;abcd", true)] [InlineData("abc", HeaderMatchMode.Contains, false, "abc;abc;def", true)] [InlineData("abc", HeaderMatchMode.Contains, false, "\"abc", true)] [InlineData("abc", HeaderMatchMode.Contains, false, "abc\"", true)] [InlineData("abc", HeaderMatchMode.Contains, false, "\"abc\"", true)] [InlineData("abc", HeaderMatchMode.Contains, false, "ab\"c", false)] [InlineData("abc", HeaderMatchMode.NotContains, false, null, true)] [InlineData("abc", HeaderMatchMode.NotContains, false, "", true)] [InlineData("abc", HeaderMatchMode.NotContains, false, "ababc", false)] [InlineData("abc", HeaderMatchMode.NotContains, false, "zaBCz", false)] [InlineData("abc", HeaderMatchMode.NotContains, false, "dcbaabcd", false)] [InlineData("abc", HeaderMatchMode.NotContains, false, "ababab", true)] [InlineData("abc", HeaderMatchMode.NotContains, true, null, true)] [InlineData("abc", HeaderMatchMode.NotContains, true, "", true)] [InlineData("abc", HeaderMatchMode.NotContains, true, "abcc", false)] [InlineData("abc", HeaderMatchMode.NotContains, true, "aaaBC", true)] [InlineData("abc", HeaderMatchMode.NotContains, true, "bbabcdb", false)] [InlineData("abc", HeaderMatchMode.NotContains, true, "aBCcba", true)] [InlineData("abc", HeaderMatchMode.NotContains, true, "baab", true)] [InlineData("abc", HeaderMatchMode.NotContains, false, ";abc", false)] [InlineData("abc", HeaderMatchMode.NotContains, false, "abc;", false)] [InlineData("abc", HeaderMatchMode.NotContains, false, "ab;cd", true)] [InlineData("abc", HeaderMatchMode.NotContains, false, "ababc;abc", false)] [InlineData("abc", HeaderMatchMode.NotContains, false, "abc;def", false)] [InlineData("abc", HeaderMatchMode.NotContains, false, "ab\"c", true)] [InlineData("abc", HeaderMatchMode.NotContains, false, "\"abc\"", false)] public async Task ApplyAsync_MatchingScenarios_OneHeaderValue( string headerValue, HeaderMatchMode headerValueMatchMode, bool isCaseSensitive, string incomingHeaderValues, bool shouldMatch) { var context = new DefaultHttpContext(); if (incomingHeaderValues is not null) { context.Request.Headers["org-id"] = incomingHeaderValues.Split(';'); } var endpoint = CreateEndpoint("org-id", new[] { headerValue }, headerValueMatchMode, isCaseSensitive); var candidates = new CandidateSet(new[] { endpoint }, new RouteValueDictionary[1], new int[1]); var sut = new HeaderMatcherPolicy(); await sut.ApplyAsync(context, candidates); Assert.Equal(shouldMatch, candidates.IsValidCandidate(0)); } [Theory] [InlineData("abc", "def", HeaderMatchMode.ExactHeader, false, null, false)] [InlineData("abc", "def", HeaderMatchMode.ExactHeader, false, "", false)] [InlineData("abc", "def", HeaderMatchMode.ExactHeader, false, "abc", true)] [InlineData("abc", "def", HeaderMatchMode.ExactHeader, false, "aBc", true)] [InlineData("abc", "def", HeaderMatchMode.ExactHeader, false, "abcd", false)] [InlineData("abc", "def", HeaderMatchMode.ExactHeader, false, "def", true)] [InlineData("abc", "def", HeaderMatchMode.ExactHeader, false, "deF", true)] [InlineData("abc", "def", HeaderMatchMode.ExactHeader, false, "defg", false)] [InlineData("abc", "def", HeaderMatchMode.ExactHeader, true, null, false)] [InlineData("abc", "def", HeaderMatchMode.ExactHeader, true, "", false)] [InlineData("abc", "def", HeaderMatchMode.ExactHeader, true, "abc", true)] [InlineData("abc", "def", HeaderMatchMode.ExactHeader, true, "aBC", false)] [InlineData("abc", "def", HeaderMatchMode.ExactHeader, true, "aBCd", false)] [InlineData("abc", "def", HeaderMatchMode.ExactHeader, true, "def", true)] [InlineData("abc", "def", HeaderMatchMode.ExactHeader, true, "DEFg", false)] [InlineData("abc", "def", HeaderMatchMode.ExactHeader, true, "dEf", false)] [InlineData("abc", "def", HeaderMatchMode.ExactHeader, true, ";", false)] [InlineData("abc", "def", HeaderMatchMode.ExactHeader, true, "abc;a", true)] [InlineData("abc", "def", HeaderMatchMode.ExactHeader, true, "a;abc", true)] [InlineData("abc", "def", HeaderMatchMode.ExactHeader, true, "abc;def", true)] [InlineData("abc", "def", HeaderMatchMode.ExactHeader, true, "ab;def", true)] [InlineData("abc", "def", HeaderMatchMode.ExactHeader, true, "ab;cdef", false)] [InlineData("abc", "def", HeaderMatchMode.ExactHeader, true, "ab;\"def\"", true)] [InlineData("abc", "def", HeaderMatchMode.ExactHeader, true, "\"abc,def\"", false)] [InlineData("abc", "def", HeaderMatchMode.ExactHeader, true, "\"abc\",def", true)] [InlineData("abc", "def", HeaderMatchMode.ExactHeader, true, " \"abc\",def", true)] [InlineData("abc", "def", HeaderMatchMode.HeaderPrefix, false, null, false)] [InlineData("abc", "def", HeaderMatchMode.HeaderPrefix, false, "", false)] [InlineData("abc", "def", HeaderMatchMode.HeaderPrefix, false, "abc", true)] [InlineData("abc", "def", HeaderMatchMode.HeaderPrefix, false, "aBc", true)] [InlineData("abc", "def", HeaderMatchMode.HeaderPrefix, false, "abcd", true)] [InlineData("abc", "def", HeaderMatchMode.HeaderPrefix, false, "abcD", true)] [InlineData("abc", "def", HeaderMatchMode.HeaderPrefix, false, "def", true)] [InlineData("abc", "def", HeaderMatchMode.HeaderPrefix, false, "deF", true)] [InlineData("abc", "def", HeaderMatchMode.HeaderPrefix, false, "defg", true)] [InlineData("abc", "def", HeaderMatchMode.HeaderPrefix, false, "defG", true)] [InlineData("abc", "def", HeaderMatchMode.HeaderPrefix, false, "aabc", false)] [InlineData("abc", "def", HeaderMatchMode.HeaderPrefix, true, null, false)] [InlineData("abc", "def", HeaderMatchMode.HeaderPrefix, true, "", false)] [InlineData("abc", "def", HeaderMatchMode.HeaderPrefix, true, "abc", true)] [InlineData("abc", "def", HeaderMatchMode.HeaderPrefix, true, "aBC", false)] [InlineData("abc", "def", HeaderMatchMode.HeaderPrefix, true, "aBCd", false)] [InlineData("abc", "def", HeaderMatchMode.HeaderPrefix, true, "def", true)] [InlineData("abc", "def", HeaderMatchMode.HeaderPrefix, true, "DEFg", false)] [InlineData("abc", "def", HeaderMatchMode.HeaderPrefix, true, "aabc", false)] [InlineData("abc", "def", HeaderMatchMode.HeaderPrefix, true, ";", false)] [InlineData("abc", "def", HeaderMatchMode.HeaderPrefix, true, "ab;cde;fgh", false)] [InlineData("abc", "def", HeaderMatchMode.HeaderPrefix, true, "abcd;e", true)] [InlineData("abc", "def", HeaderMatchMode.HeaderPrefix, true, "abcd;defg", true)] [InlineData("abc", "def", HeaderMatchMode.HeaderPrefix, true, "Abcd;defg", true)] [InlineData("abc", "def", HeaderMatchMode.HeaderPrefix, true, "Abcd;Defg", false)] [InlineData("abc", "def", HeaderMatchMode.HeaderPrefix, true, "a;defg", true)] [InlineData("abc", "def", HeaderMatchMode.HeaderPrefix, true, "abcd;", true)] [InlineData("abc", "def", HeaderMatchMode.HeaderPrefix, true, ";def", true)] [InlineData("abc", "def", HeaderMatchMode.HeaderPrefix, true, " \"abc\",def", true)] [InlineData("abc", "def", HeaderMatchMode.HeaderPrefix, true, "ab, \"def\"", true)] [InlineData("abc", "def", HeaderMatchMode.HeaderPrefix, true, "ab, def\"", true)] [InlineData("abc", "def", HeaderMatchMode.HeaderPrefix, true, "ab, \"def", false)] [InlineData("abc", "def", HeaderMatchMode.HeaderPrefix, true, "\"\"ab\",def", false)] [InlineData("abc", "def", HeaderMatchMode.HeaderPrefix, true, "\"\"ab\",def\"", false)] [InlineData("abc", "def", HeaderMatchMode.HeaderPrefix, true, "\"\"ab\"\",def\"", true)] [InlineData("abc", "def", HeaderMatchMode.Contains, false, null, false)] [InlineData("abc", "def", HeaderMatchMode.Contains, false, "", false)] [InlineData("abc", "def", HeaderMatchMode.Contains, false, "aabc", true)] [InlineData("abc", "def", HeaderMatchMode.Contains, false, "baBc", true)] [InlineData("abc", "def", HeaderMatchMode.Contains, false, "ababcd", true)] [InlineData("abc", "def", HeaderMatchMode.Contains, false, "dcabcD", true)] [InlineData("abc", "def", HeaderMatchMode.Contains, false, "fdeff", true)] [InlineData("abc", "def", HeaderMatchMode.Contains, false, "edeF", true)] [InlineData("abc", "def", HeaderMatchMode.Contains, false, "adefg", true)] [InlineData("abc", "def", HeaderMatchMode.Contains, false, "abdefG", true)] [InlineData("abc", "def", HeaderMatchMode.Contains, false, "ddaabc", true)] [InlineData("abc", "def", HeaderMatchMode.Contains, false, "abcdef", true)] [InlineData("abc", "def", HeaderMatchMode.Contains, true, null, false)] [InlineData("abc", "def", HeaderMatchMode.Contains, true, "", false)] [InlineData("abc", "def", HeaderMatchMode.Contains, true, "cabca", true)] [InlineData("abc", "def", HeaderMatchMode.Contains, true, "aBCa", false)] [InlineData("abc", "def", HeaderMatchMode.Contains, true, "CaBCdd", false)] [InlineData("abc", "def", HeaderMatchMode.Contains, true, "DEFdef", true)] [InlineData("abc", "def", HeaderMatchMode.Contains, true, "defDEFg", true)] [InlineData("abc", "def", HeaderMatchMode.Contains, true, "bbaabc", true)] [InlineData("abc", "def", HeaderMatchMode.Contains, true, ";", false)] [InlineData("abc", "def", HeaderMatchMode.Contains, true, "cabca;", true)] [InlineData("abc", "def", HeaderMatchMode.Contains, true, ";cabca", true)] [InlineData("abc", "def", HeaderMatchMode.Contains, true, "ab;cd;ef", false)] [InlineData("abc", "def", HeaderMatchMode.Contains, true, "aBCa;deFg", false)] [InlineData("abc", "def", HeaderMatchMode.Contains, true, "aBCa;defg", true)] [InlineData("abc", "def", HeaderMatchMode.Contains, true, "abcd;d", true)] [InlineData("abc", "ABC", HeaderMatchMode.Contains, true, "abc;d", true)] [InlineData("abc", "ABC", HeaderMatchMode.Contains, true, "ABC;d", true)] [InlineData("abc", "ABC", HeaderMatchMode.Contains, true, "abC;d", false)] [InlineData("abc", "ABC", HeaderMatchMode.Contains, true, "abcABC;d", true)] [InlineData("abc", "def", HeaderMatchMode.Contains, true, "\"abc, def\"", true)] [InlineData("abc", "def", HeaderMatchMode.Contains, true, "\"abc\", def\"", true)] [InlineData("abc", "def", HeaderMatchMode.Contains, true, "ab\"cde\"f", false)] [InlineData("abc", "def", HeaderMatchMode.NotContains, false, null, true)] [InlineData("abc", "def", HeaderMatchMode.NotContains, false, "", true)] [InlineData("abc", "def", HeaderMatchMode.NotContains, false, "aabc", false)] [InlineData("abc", "def", HeaderMatchMode.NotContains, false, "baBc", false)] [InlineData("abc", "def", HeaderMatchMode.NotContains, false, "ababcd", false)] [InlineData("abc", "def", HeaderMatchMode.NotContains, false, "dcabcD", false)] [InlineData("abc", "def", HeaderMatchMode.NotContains, false, "def", false)] [InlineData("abc", "def", HeaderMatchMode.NotContains, false, "ghi", true)] [InlineData("abc", "def", HeaderMatchMode.NotContains, true, null, true)] [InlineData("abc", "def", HeaderMatchMode.NotContains, true, "", true)] [InlineData("abc", "def", HeaderMatchMode.NotContains, true, "cabca", false)] [InlineData("abc", "def", HeaderMatchMode.NotContains, true, "aBCa", true)] [InlineData("abc", "def", HeaderMatchMode.NotContains, true, "CaBCdd", true)] [InlineData("abc", "def", HeaderMatchMode.NotContains, true, "DEFdef", false)] [InlineData("abc", "def", HeaderMatchMode.NotContains, true, "DEFg", true)] [InlineData("abc", "def", HeaderMatchMode.NotContains, true, "bbaabc", false)] [InlineData("abc", "def", HeaderMatchMode.NotContains, true, "defG", false)] [InlineData("abc", "def", HeaderMatchMode.NotContains, true, "bbaabc;", false)] [InlineData("abc", "def", HeaderMatchMode.NotContains, true, ";bbaabc", false)] [InlineData("abc", "def", HeaderMatchMode.NotContains, true, "ab;cd;ef", true)] [InlineData("abc", "def", HeaderMatchMode.NotContains, true, "a;defg", false)] [InlineData("abc", "def", HeaderMatchMode.NotContains, true, "ab;cdef", false)] [InlineData("abc", "def", HeaderMatchMode.NotContains, true, "abc;def", false)] [InlineData("abc", "def", HeaderMatchMode.NotContains, true, "Abc;cdef", false)] [InlineData("abc", "def", HeaderMatchMode.NotContains, true, "Abc;cdEf", true)] public async Task ApplyAsync_MatchingScenarios_TwoHeaderValues( string header1Value, string header2Value, HeaderMatchMode headerValueMatchMode, bool isCaseSensitive, string incomingHeaderValues, bool shouldMatch) { var context = new DefaultHttpContext(); if (incomingHeaderValues is not null) { context.Request.Headers["org-id"] = incomingHeaderValues.Split(';'); } var endpoint = CreateEndpoint("org-id", new[] { header1Value, header2Value }, headerValueMatchMode, isCaseSensitive); var candidates = new CandidateSet(new[] { endpoint }, new RouteValueDictionary[1], new int[1]); var sut = new HeaderMatcherPolicy(); await sut.ApplyAsync(context, candidates); Assert.Equal(shouldMatch, candidates.IsValidCandidate(0)); } [Theory] [InlineData(HeaderMatchMode.Contains, true, false)] [InlineData(HeaderMatchMode.Contains, false, false)] [InlineData(HeaderMatchMode.NotContains, true, true)] [InlineData(HeaderMatchMode.NotContains, false, true)] [InlineData(HeaderMatchMode.HeaderPrefix, true, false)] [InlineData(HeaderMatchMode.HeaderPrefix, false, false)] [InlineData(HeaderMatchMode.ExactHeader, true, false)] [InlineData(HeaderMatchMode.ExactHeader, false, false)] [InlineData(HeaderMatchMode.NotExists, true, true)] [InlineData(HeaderMatchMode.NotExists, false, true)] [InlineData(HeaderMatchMode.Exists, true, false)] [InlineData(HeaderMatchMode.Exists, false, false)] public async Task ApplyAsync_MatchingScenarios_MissingHeader( HeaderMatchMode headerValueMatchMode, bool isCaseSensitive, bool shouldMatch) { var context = new DefaultHttpContext(); var headerValues = new[] { "bar" }; if (headerValueMatchMode == HeaderMatchMode.Exists || headerValueMatchMode == HeaderMatchMode.NotExists) { headerValues = null; } var endpoint = CreateEndpoint("foo", headerValues, headerValueMatchMode, isCaseSensitive); var candidates = new CandidateSet(new[] { endpoint }, new RouteValueDictionary[1], new int[1]); var sut = new HeaderMatcherPolicy(); await sut.ApplyAsync(context, candidates); Assert.Equal(shouldMatch, candidates.IsValidCandidate(0)); } [Theory] [InlineData("Foo", "abc", HeaderMatchMode.ExactHeader, "ab, abc", true)] [InlineData("Foo", "abc", HeaderMatchMode.ExactHeader, "ab; abc", false)] [InlineData("Cookie", "abc", HeaderMatchMode.ExactHeader, "ab, abc", false)] [InlineData("Cookie", "abc", HeaderMatchMode.ExactHeader, "ab; abc", true)] [InlineData("Set-Cookie", "abc", HeaderMatchMode.ExactHeader, "ab, abc", true)] [InlineData("Set-Cookie", "abc", HeaderMatchMode.ExactHeader, "ab; abc", false)] [InlineData("Cookie", "abc", HeaderMatchMode.ExactHeader, "\"ab\"; abc", true)] [InlineData("Cookie", "abc", HeaderMatchMode.ExactHeader, "ab; \"abc\"", true)] [InlineData("Cookie", "abc", HeaderMatchMode.ExactHeader, "\"ab\"; \"abc\"", true)] [InlineData("Cookie", "abc", HeaderMatchMode.ExactHeader, "abc;", true)] [InlineData("Cookie", "abc", HeaderMatchMode.ExactHeader, " abc;", true)] [InlineData("Cookie", "abc", HeaderMatchMode.ExactHeader, " \"abc\";", true)] [InlineData("Cookie", "abc", HeaderMatchMode.ExactHeader, "\"abc;\"", false)] [InlineData("Cookie", "abc", HeaderMatchMode.ExactHeader, "\"abc;\" \"abc\"", false)] public async Task ApplyAsync_Cookie_UsesDifferentSeparator( string headerName, string headerValue, HeaderMatchMode headerValueMatchMode, string incomingHeaderValue, bool shouldMatch) { var context = new DefaultHttpContext(); context.Request.Headers[headerName] = incomingHeaderValue; var endpoint = CreateEndpoint(headerName, new[] { headerValue }, headerValueMatchMode, true); var candidates = new CandidateSet(new[] { endpoint }, new RouteValueDictionary[1], new int[1]); var sut = new HeaderMatcherPolicy(); await sut.ApplyAsync(context, candidates); Assert.Equal(shouldMatch, candidates.IsValidCandidate(0)); } [Theory] [InlineData(false, false, false)] [InlineData(false, true, false)] [InlineData(true, false, false)] [InlineData(true, true, true)] public async Task ApplyAsync_MultipleRules_RequiresAllHeaders(bool sendHeader1, bool sendHeader2, bool shouldMatch) { var endpoint = CreateEndpoint(new[] { new HeaderMatcher("header1", new[] { "value1" }, HeaderMatchMode.ExactHeader, isCaseSensitive: false), new HeaderMatcher("header2", new[] { "value2" }, HeaderMatchMode.ExactHeader, isCaseSensitive: false) }); var context = new DefaultHttpContext(); if (sendHeader1) { context.Request.Headers["header1"] = "value1"; } if (sendHeader2) { context.Request.Headers["header2"] = "value2"; } var candidates = new CandidateSet(new[] { endpoint }, new RouteValueDictionary[1], new int[1]); var sut = new HeaderMatcherPolicy(); await sut.ApplyAsync(context, candidates); Assert.Equal(shouldMatch, candidates.IsValidCandidate(0)); } private static Endpoint CreateEndpoint( string headerName, string[] headerValues, HeaderMatchMode mode = HeaderMatchMode.ExactHeader, bool isCaseSensitive = false, bool isDynamic = false) { return CreateEndpoint(new[] { new HeaderMatcher(headerName, headerValues, mode, isCaseSensitive) }, isDynamic); } private static Endpoint CreateEndpoint(IReadOnlyList matchers, bool isDynamic = false) { var builder = new RouteEndpointBuilder(_ => Task.CompletedTask, RoutePatternFactory.Parse("/"), 0); builder.Metadata.Add(new HeaderMetadata(matchers)); if (isDynamic) { builder.Metadata.Add(new DynamicEndpointMetadata()); } return builder.Build(); } private class DynamicEndpointMetadata : IDynamicEndpointMetadata { public bool IsDynamic => true; } } ================================================ FILE: test/ReverseProxy.Tests/Routing/ProxyEndpointFactoryTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Cors.Infrastructure; using Microsoft.AspNetCore.Http.Timeouts; using Microsoft.AspNetCore.RateLimiting; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.Extensions.DependencyInjection; using Xunit; using Yarp.ReverseProxy.Configuration; using Yarp.ReverseProxy.Model; using Yarp.ReverseProxy.Forwarder; namespace Yarp.ReverseProxy.Routing.Tests; public class ProxyEndpointFactoryTests { private IServiceProvider CreateServices() { var serviceCollection = new ServiceCollection(); serviceCollection.AddSingleton(); return serviceCollection.BuildServiceProvider(); } [Fact] public void Constructor_Works() { var services = CreateServices(); _ = services.GetRequiredService(); } [Fact] public void AddEndpoint_HostAndPath_Works() { var services = CreateServices(); var factory = services.GetRequiredService(); factory.SetProxyPipeline(context => Task.CompletedTask); var route = new RouteConfig { RouteId = "route1", Match = new RouteMatch { Hosts = new[] { "example.com" }, Path = "/a", }, Order = 12, }; var cluster = new ClusterState("cluster1"); var routeState = new RouteState("route1"); var (routeEndpoint, routeConfig) = CreateEndpoint(factory, routeState, route, cluster); Assert.Same(cluster, routeConfig.Cluster); Assert.Equal("route1", routeEndpoint.DisplayName); Assert.Same(routeConfig, routeEndpoint.Metadata.GetMetadata()); Assert.Equal("/a", routeEndpoint.RoutePattern.RawText); Assert.Equal(12, routeEndpoint.Order); Assert.False(routeConfig.HasConfigChanged(route, cluster, routeState.ClusterRevision)); var hostMetadata = routeEndpoint.Metadata.GetMetadata(); Assert.NotNull(hostMetadata); Assert.Single(hostMetadata.Hosts); Assert.Equal("example.com", hostMetadata.Hosts[0]); } private (RouteEndpoint routeEndpoint, RouteModel routeConfig) CreateEndpoint(ProxyEndpointFactory factory, RouteState routeState, RouteConfig routeConfig, ClusterState clusterState) { routeState.ClusterRevision = clusterState.Revision; var routeModel = new RouteModel(routeConfig, clusterState, HttpTransformer.Default); var endpoint = factory.CreateEndpoint(routeModel, Array.Empty>()); var routeEndpoint = Assert.IsType(endpoint); return (routeEndpoint, routeModel); } [Fact] public void AddEndpoint_JustHost_Works() { var services = CreateServices(); var factory = services.GetRequiredService(); factory.SetProxyPipeline(context => Task.CompletedTask); var route = new RouteConfig { RouteId = "route1", Match = new RouteMatch { Hosts = new[] { "example.com" }, }, Order = 12, }; var cluster = new ClusterState("cluster1"); var routeState = new RouteState("route1"); var (routeEndpoint, routeConfig) = CreateEndpoint(factory, routeState, route, cluster); Assert.Same(cluster, routeConfig.Cluster); Assert.Equal("route1", routeEndpoint.DisplayName); Assert.Same(routeConfig, routeEndpoint.Metadata.GetMetadata()); Assert.Equal("/{**catchall}", routeEndpoint.RoutePattern.RawText); Assert.Equal(12, routeEndpoint.Order); Assert.False(routeConfig.HasConfigChanged(route, cluster, routeState.ClusterRevision)); var hostMetadata = routeEndpoint.Metadata.GetMetadata(); Assert.NotNull(hostMetadata); Assert.Single(hostMetadata.Hosts); Assert.Equal("example.com", hostMetadata.Hosts[0]); } [Fact] public void AddEndpoint_JustHostWithWildcard_Works() { var services = CreateServices(); var factory = services.GetRequiredService(); factory.SetProxyPipeline(context => Task.CompletedTask); var route = new RouteConfig { RouteId = "route1", Match = new RouteMatch { Hosts = new[] { "*.example.com" }, }, Order = 12, }; var cluster = new ClusterState("cluster1"); var routeState = new RouteState("route1"); var (routeEndpoint, routeConfig) = CreateEndpoint(factory, routeState, route, cluster); Assert.Same(cluster, routeConfig.Cluster); Assert.Equal("route1", routeEndpoint.DisplayName); Assert.Same(routeConfig, routeEndpoint.Metadata.GetMetadata()); Assert.Equal("/{**catchall}", routeEndpoint.RoutePattern.RawText); Assert.Equal(12, routeEndpoint.Order); Assert.False(routeConfig.HasConfigChanged(route, cluster, routeState.ClusterRevision)); var hostMetadata = routeEndpoint.Metadata.GetMetadata(); Assert.NotNull(hostMetadata); Assert.Single(hostMetadata.Hosts); Assert.Equal("*.example.com", hostMetadata.Hosts[0]); } [Fact] public void AddEndpoint_JustPath_Works() { var services = CreateServices(); var factory = services.GetRequiredService(); factory.SetProxyPipeline(context => Task.CompletedTask); var route = new RouteConfig { RouteId = "route1", Match = new RouteMatch { Path = "/a", }, Order = 12, }; var cluster = new ClusterState("cluster1"); var routeState = new RouteState("route1"); var (routeEndpoint, routeConfig) = CreateEndpoint(factory, routeState, route, cluster); Assert.Same(cluster, routeConfig.Cluster); Assert.Equal("route1", routeEndpoint.DisplayName); Assert.Same(routeConfig, routeEndpoint.Metadata.GetMetadata()); Assert.Equal("/a", routeEndpoint.RoutePattern.RawText); Assert.Equal(12, routeEndpoint.Order); Assert.False(routeConfig.HasConfigChanged(route, cluster, routeState.ClusterRevision)); var hostMetadata = routeEndpoint.Metadata.GetMetadata(); Assert.Null(hostMetadata); } [Fact] public void AddEndpoint_NullMatchers_Works() { var services = CreateServices(); var factory = services.GetRequiredService(); factory.SetProxyPipeline(context => Task.CompletedTask); var route = new RouteConfig { RouteId = "route1", Order = 12, Match = new RouteMatch() }; var cluster = new ClusterState("cluster1"); var routeState = new RouteState("route1"); var (routeEndpoint, routeConfig) = CreateEndpoint(factory, routeState, route, cluster); Assert.Same(cluster, routeConfig.Cluster); Assert.Equal("route1", routeEndpoint.DisplayName); Assert.Same(routeConfig, routeEndpoint.Metadata.GetMetadata()); Assert.Equal("/{**catchall}", routeEndpoint.RoutePattern.RawText); Assert.Equal(12, routeEndpoint.Order); Assert.False(routeConfig.HasConfigChanged(route, cluster, routeState.ClusterRevision)); var hostMetadata = routeEndpoint.Metadata.GetMetadata(); Assert.Null(hostMetadata); } [Fact] public void AddEndpoint_InvalidPath_BubblesOutException() { var services = CreateServices(); var factory = services.GetRequiredService(); factory.SetProxyPipeline(context => Task.CompletedTask); var route = new RouteConfig { RouteId = "route1", Match = new RouteMatch { Path = "/{invalid", }, Order = 12, }; var cluster = new ClusterState("cluster1"); var routeState = new RouteState("route1"); Action action = () => CreateEndpoint(factory, routeState, route, cluster); Assert.Throws(action); } [Fact] public void AddEndpoint_DefaultAuth_Works() { var services = CreateServices(); var factory = services.GetRequiredService(); factory.SetProxyPipeline(context => Task.CompletedTask); var route = new RouteConfig { RouteId = "route1", AuthorizationPolicy = "defaulT", Order = 12, Match = new RouteMatch(), }; var cluster = new ClusterState("cluster1"); var routeState = new RouteState("route1"); var (routeEndpoint, _) = CreateEndpoint(factory, routeState, route, cluster); var attribute = Assert.IsType(routeEndpoint.Metadata.GetMetadata()); Assert.Null(attribute.Policy); } [Fact] public void AddEndpoint_AnonymousAuth_Works() { var services = CreateServices(); var factory = services.GetRequiredService(); factory.SetProxyPipeline(context => Task.CompletedTask); var route = new RouteConfig { RouteId = "route1", AuthorizationPolicy = "AnonymouS", Order = 12, Match = new RouteMatch(), }; var cluster = new ClusterState("cluster1"); var routeState = new RouteState("route1"); var (routeEndpoint, _) = CreateEndpoint(factory, routeState, route, cluster); Assert.IsType(routeEndpoint.Metadata.GetMetadata()); } [Fact] public void AddEndpoint_CustomAuth_Works() { var services = CreateServices(); var factory = services.GetRequiredService(); factory.SetProxyPipeline(context => Task.CompletedTask); var route = new RouteConfig { RouteId = "route1", AuthorizationPolicy = "custom", Order = 12, Match = new RouteMatch(), }; var cluster = new ClusterState("cluster1"); var routeState = new RouteState("route1"); var (routeEndpoint, _) = CreateEndpoint(factory, routeState, route, cluster); var attribute = Assert.IsType(routeEndpoint.Metadata.GetMetadata()); Assert.Equal("custom", attribute.Policy); } [Fact] public void AddEndpoint_NoAuth_Works() { var services = CreateServices(); var factory = services.GetRequiredService(); factory.SetProxyPipeline(context => Task.CompletedTask); var route = new RouteConfig { RouteId = "route1", Order = 12, Match = new RouteMatch(), }; var cluster = new ClusterState("cluster1"); var routeState = new RouteState("route1"); var (routeEndpoint, _) = CreateEndpoint(factory, routeState, route, cluster); Assert.Null(routeEndpoint.Metadata.GetMetadata()); Assert.Null(routeEndpoint.Metadata.GetMetadata()); } [Fact] public void AddEndpoint_DefaultRateLimiter_Works() { var services = CreateServices(); var factory = services.GetRequiredService(); factory.SetProxyPipeline(context => Task.CompletedTask); var route = new RouteConfig { RouteId = "route1", RateLimiterPolicy = "defaulT", Order = 12, Match = new RouteMatch(), }; var cluster = new ClusterState("cluster1"); var routeState = new RouteState("route1"); var (routeEndpoint, _) = CreateEndpoint(factory, routeState, route, cluster); Assert.Null(routeEndpoint.Metadata.GetMetadata()); Assert.Null(routeEndpoint.Metadata.GetMetadata()); } [Fact] public void AddEndpoint_CustomRateLimiter_Works() { var services = CreateServices(); var factory = services.GetRequiredService(); factory.SetProxyPipeline(context => Task.CompletedTask); var route = new RouteConfig { RouteId = "route1", RateLimiterPolicy = "custom", Order = 12, Match = new RouteMatch(), }; var cluster = new ClusterState("cluster1"); var routeState = new RouteState("route1"); var (routeEndpoint, _) = CreateEndpoint(factory, routeState, route, cluster); var attribute = routeEndpoint.Metadata.GetMetadata(); Assert.NotNull(attribute); Assert.Equal("custom", attribute.PolicyName); Assert.Null(routeEndpoint.Metadata.GetMetadata()); } [Fact] public void AddEndpoint_DisableRateLimiter_Works() { var services = CreateServices(); var factory = services.GetRequiredService(); factory.SetProxyPipeline(context => Task.CompletedTask); var route = new RouteConfig { RouteId = "route1", RateLimiterPolicy = "disAble", Order = 12, Match = new RouteMatch(), }; var cluster = new ClusterState("cluster1"); var routeState = new RouteState("route1"); var (routeEndpoint, _) = CreateEndpoint(factory, routeState, route, cluster); Assert.NotNull(routeEndpoint.Metadata.GetMetadata()); Assert.Null(routeEndpoint.Metadata.GetMetadata()); } [Fact] public void AddEndpoint_NoRateLimiter_Works() { var services = CreateServices(); var factory = services.GetRequiredService(); factory.SetProxyPipeline(context => Task.CompletedTask); var route = new RouteConfig { RouteId = "route1", Order = 12, Match = new RouteMatch(), }; var cluster = new ClusterState("cluster1"); var routeState = new RouteState("route1"); var (routeEndpoint, _) = CreateEndpoint(factory, routeState, route, cluster); Assert.Null(routeEndpoint.Metadata.GetMetadata()); Assert.Null(routeEndpoint.Metadata.GetMetadata()); } [Fact] public void AddEndpoint_CustomTimeoutPolicy_Works() { var services = CreateServices(); var factory = services.GetRequiredService(); factory.SetProxyPipeline(context => Task.CompletedTask); var route = new RouteConfig { RouteId = "route1", TimeoutPolicy = "custom", Order = 12, Match = new RouteMatch(), }; var cluster = new ClusterState("cluster1"); var routeState = new RouteState("route1"); var (routeEndpoint, _) = CreateEndpoint(factory, routeState, route, cluster); var attribute = routeEndpoint.Metadata.GetMetadata(); Assert.NotNull(attribute); Assert.Equal("custom", attribute.PolicyName); Assert.Null(attribute.Timeout); Assert.Null(routeEndpoint.Metadata.GetMetadata()); } [Fact] public void AddEndpoint_CustomTimeout_Works() { var services = CreateServices(); var factory = services.GetRequiredService(); factory.SetProxyPipeline(context => Task.CompletedTask); var route = new RouteConfig { RouteId = "route1", Timeout = TimeSpan.FromSeconds(5), Order = 12, Match = new RouteMatch(), }; var cluster = new ClusterState("cluster1"); var routeState = new RouteState("route1"); var (routeEndpoint, _) = CreateEndpoint(factory, routeState, route, cluster); var attribute = routeEndpoint.Metadata.GetMetadata(); Assert.NotNull(attribute); Assert.Null(attribute.PolicyName); Assert.Equal(TimeSpan.FromSeconds(5), attribute.Timeout); Assert.Null(routeEndpoint.Metadata.GetMetadata()); } [Fact] public void AddEndpoint_DisableTimeoutPolicy_Works() { var services = CreateServices(); var factory = services.GetRequiredService(); factory.SetProxyPipeline(context => Task.CompletedTask); var route = new RouteConfig { RouteId = "route1", TimeoutPolicy = "disAble", Order = 12, Match = new RouteMatch(), }; var cluster = new ClusterState("cluster1"); var routeState = new RouteState("route1"); var (routeEndpoint, _) = CreateEndpoint(factory, routeState, route, cluster); Assert.NotNull(routeEndpoint.Metadata.GetMetadata()); Assert.Null(routeEndpoint.Metadata.GetMetadata()); } [Fact] public void AddEndpoint_NoTimeoutPolicy_Works() { var services = CreateServices(); var factory = services.GetRequiredService(); factory.SetProxyPipeline(context => Task.CompletedTask); var route = new RouteConfig { RouteId = "route1", Order = 12, Match = new RouteMatch(), }; var cluster = new ClusterState("cluster1"); var routeState = new RouteState("route1"); var (routeEndpoint, _) = CreateEndpoint(factory, routeState, route, cluster); Assert.Null(routeEndpoint.Metadata.GetMetadata()); Assert.Null(routeEndpoint.Metadata.GetMetadata()); } [Fact] public void AddEndpoint_DefaultCors_Works() { var services = CreateServices(); var factory = services.GetRequiredService(); factory.SetProxyPipeline(context => Task.CompletedTask); var route = new RouteConfig { RouteId = "route1", CorsPolicy = "defaulT", Order = 12, Match = new RouteMatch(), }; var cluster = new ClusterState("cluster1"); var routeState = new RouteState("route1"); var (routeEndpoint, _) = CreateEndpoint(factory, routeState, route, cluster); var attribute = Assert.IsType(routeEndpoint.Metadata.GetMetadata()); Assert.Null(attribute.PolicyName); Assert.Null(routeEndpoint.Metadata.GetMetadata()); } [Fact] public void AddEndpoint_CustomCors_Works() { var services = CreateServices(); var factory = services.GetRequiredService(); factory.SetProxyPipeline(context => Task.CompletedTask); var route = new RouteConfig { RouteId = "route1", CorsPolicy = "custom", Order = 12, Match = new RouteMatch(), }; var cluster = new ClusterState("cluster1"); var routeState = new RouteState("route1"); var (routeEndpoint, _) = CreateEndpoint(factory, routeState, route, cluster); var attribute = Assert.IsType(routeEndpoint.Metadata.GetMetadata()); Assert.Equal("custom", attribute.PolicyName); Assert.Null(routeEndpoint.Metadata.GetMetadata()); } [Fact] public void AddEndpoint_DisableCors_Works() { var services = CreateServices(); var factory = services.GetRequiredService(); factory.SetProxyPipeline(context => Task.CompletedTask); var route = new RouteConfig { RouteId = "route1", CorsPolicy = "disAble", Order = 12, Match = new RouteMatch(), }; var cluster = new ClusterState("cluster1"); var routeState = new RouteState("route1"); var (routeEndpoint, _) = CreateEndpoint(factory, routeState, route, cluster); Assert.IsType(routeEndpoint.Metadata.GetMetadata()); Assert.Null(routeEndpoint.Metadata.GetMetadata()); } [Fact] public void AddEndpoint_NoCors_Works() { var services = CreateServices(); var factory = services.GetRequiredService(); factory.SetProxyPipeline(context => Task.CompletedTask); var route = new RouteConfig { RouteId = "route1", Order = 12, Match = new RouteMatch(), }; var cluster = new ClusterState("cluster1"); var routeState = new RouteState("route1"); var (routeEndpoint, _) = CreateEndpoint(factory, routeState, route, cluster); Assert.Null(routeEndpoint.Metadata.GetMetadata()); Assert.Null(routeEndpoint.Metadata.GetMetadata()); } [Fact] public void BuildEndpoints_Header_Works() { var services = CreateServices(); var factory = services.GetRequiredService(); factory.SetProxyPipeline(context => Task.CompletedTask); var route = new RouteConfig { RouteId = "route1", Match = new RouteMatch { Path = "/", Headers = new[] { new RouteHeader() { Name = "header1", Values = new[] { "value1" }, Mode = HeaderMatchMode.HeaderPrefix, IsCaseSensitive = true, } } }, }; var cluster = new ClusterState("cluster1"); var routeState = new RouteState("route1"); var (routeEndpoint, routeConfig) = CreateEndpoint(factory, routeState, route, cluster); Assert.Same(cluster, routeConfig.Cluster); Assert.Equal("route1", routeEndpoint.DisplayName); var headerMetadata = routeEndpoint.Metadata.GetMetadata(); Assert.NotNull(headerMetadata); var matchers = headerMetadata.Matchers; Assert.Single(matchers); var matcher = matchers.Single(); Assert.Equal("header1", matcher.Name); Assert.Equal(new[] { "value1" }, matcher.Values); Assert.Equal(HeaderMatchMode.HeaderPrefix, matcher.Mode); Assert.Equal(StringComparison.Ordinal, matcher.Comparison); Assert.False(routeConfig.HasConfigChanged(route, cluster, routeState.ClusterRevision)); } [Fact] public void BuildEndpoints_Headers_Works() { var services = CreateServices(); var factory = services.GetRequiredService(); factory.SetProxyPipeline(context => Task.CompletedTask); var route = new RouteConfig { RouteId = "route1", Match = new RouteMatch { Path = "/", Headers = new[] { new RouteHeader() { Name = "header1", Values = new[] { "value1" }, Mode = HeaderMatchMode.HeaderPrefix, IsCaseSensitive = true, }, new RouteHeader() { Name = "header2", Mode = HeaderMatchMode.Exists, } } }, }; var cluster = new ClusterState("cluster1"); var routeState = new RouteState("route1"); var (routeEndpoint, routeConfig) = CreateEndpoint(factory, routeState, route, cluster); Assert.Same(cluster, routeConfig.Cluster); Assert.Equal("route1", routeEndpoint.DisplayName); var metadata = routeEndpoint.Metadata.GetMetadata(); Assert.Equal(2, metadata.Matchers.Length); var firstMetadata = metadata.Matchers.First(); Assert.NotNull(firstMetadata); Assert.Equal("header1", firstMetadata.Name); Assert.Equal(new[] { "value1" }, firstMetadata.Values); Assert.Equal(HeaderMatchMode.HeaderPrefix, firstMetadata.Mode); Assert.Equal(StringComparison.Ordinal, firstMetadata.Comparison); var secondMetadata = metadata.Matchers.Skip(1).Single(); Assert.NotNull(secondMetadata); Assert.Equal("header2", secondMetadata.Name); Assert.Same(Array.Empty(), secondMetadata.Values); Assert.Equal(HeaderMatchMode.Exists, secondMetadata.Mode); Assert.Equal(StringComparison.OrdinalIgnoreCase, secondMetadata.Comparison); Assert.False(routeConfig.HasConfigChanged(route, cluster, routeState.ClusterRevision)); } } ================================================ FILE: test/ReverseProxy.Tests/Routing/QueryMatcherPolicyTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Matching; using Microsoft.AspNetCore.Routing.Patterns; using Xunit; using Yarp.ReverseProxy.Configuration; namespace Yarp.ReverseProxy.Routing.Tests; public class QueryParameterMatcherPolicyTests { [Fact] public void Comparer_SortOrder_SingleRuleEqual() { // Most specific to least var endpoints = new[] { (0, CreateEndpoint("queryparam", new[] { "abc" }, QueryParameterMatchMode.Exact, isCaseSensitive: true)), (0, CreateEndpoint("queryparam", new[] { "abc" }, QueryParameterMatchMode.Exact)), (0, CreateEndpoint("queryparam", new[] { "abc", "def" }, QueryParameterMatchMode.Exact)), (0, CreateEndpoint("queryparam2", new[] { "abc", "def" }, QueryParameterMatchMode.Exact)), (0, CreateEndpoint("queryparam", new[] { "abc" }, QueryParameterMatchMode.Contains, isCaseSensitive: true)), (0, CreateEndpoint("queryparam", new[] { "abc" }, QueryParameterMatchMode.Contains)), (0, CreateEndpoint("queryparam", new[] { "abc", "def" }, QueryParameterMatchMode.Contains)), (0, CreateEndpoint("queryparam2", new[] { "abc", "def" }, QueryParameterMatchMode.Contains)), (0, CreateEndpoint("queryparam", new[] { "abc" }, QueryParameterMatchMode.Prefix)), (0, CreateEndpoint("queryparam", new[] { "abc", "def" }, QueryParameterMatchMode.Prefix)), (0, CreateEndpoint("queryparam2", new[] { "abc", "def" }, QueryParameterMatchMode.Prefix)), (0, CreateEndpoint("queryparam", Array.Empty(), QueryParameterMatchMode.Exists, isCaseSensitive: true)), (0, CreateEndpoint("queryparam", Array.Empty(), QueryParameterMatchMode.Exists)), (0, CreateEndpoint("queryparam", Array.Empty(), QueryParameterMatchMode.Exists, isCaseSensitive: true)), (0, CreateEndpoint("queryparam", Array.Empty(), QueryParameterMatchMode.Exists)), }; var sut = new QueryParameterMatcherPolicy(); for (var i = 0; i < endpoints.Length; i++) { for (var j = 0; j < endpoints.Length; j++) { var a = endpoints[i]; var b = endpoints[j]; var actual = sut.Comparer.Compare(a.Item2, b.Item2); var expected = a.Item1 < b.Item1 ? -1 : a.Item1 > b.Item1 ? 1 : 0; if (actual != expected) { Assert.Fail($"Error comparing [{i}] to [{j}], expected {expected}, found {actual}."); } } } } [Fact] public void Comparer_MultipleQueryParameters_SortOrder() { // Most specific to least var endpoints = new[] { (0, CreateEndpoint(new[] { new QueryParameterMatcher("queryparam", Array.Empty(), QueryParameterMatchMode.Exists, isCaseSensitive: true), new QueryParameterMatcher("queryparam", new[] { "abc" }, QueryParameterMatchMode.Prefix, isCaseSensitive: true), new QueryParameterMatcher("queryparam", new[] { "abc" }, QueryParameterMatchMode.Contains, isCaseSensitive: true), new QueryParameterMatcher("queryparam", new[] { "abc" }, QueryParameterMatchMode.Exact, isCaseSensitive: true) })), (1, CreateEndpoint(new[] { new QueryParameterMatcher("queryparam", new[] { "abc" }, QueryParameterMatchMode.Contains, isCaseSensitive: true), new QueryParameterMatcher("queryparam", new[] { "abc" }, QueryParameterMatchMode.Exact, isCaseSensitive: true) })), (1, CreateEndpoint(new[] { new QueryParameterMatcher("queryparam", Array.Empty(), QueryParameterMatchMode.Exists, isCaseSensitive: true), new QueryParameterMatcher("queryparam", new[] { "abc" }, QueryParameterMatchMode.Exact, isCaseSensitive: true) })), (2, CreateEndpoint("queryparam", new[] { "abc" })), (3, CreateEndpoint(Array.Empty())), }; var sut = new QueryParameterMatcherPolicy(); for (var i = 0; i < endpoints.Length; i++) { for (var j = 0; j < endpoints.Length; j++) { var a = endpoints[i]; var b = endpoints[j]; var actual = sut.Comparer.Compare(a.Item2, b.Item2); var expected = a.Item1 < b.Item1 ? -1 : a.Item1 > b.Item1 ? 1 : 0; if (actual != expected) { Assert.Fail($"Error comparing [{i}] to [{j}], expected {expected}, found {actual}."); } } } } [Fact] public void AppliesToEndpoints_AppliesScenarios() { var scenarios = new[] { CreateEndpoint("org-id", Array.Empty(), QueryParameterMatchMode.Exists), CreateEndpoint("org-id", new[] { "abc" }), CreateEndpoint("org-id", new[] { "abc", "def" }), CreateEndpoint("org-id", Array.Empty(), QueryParameterMatchMode.Exists, isDynamic: true), CreateEndpoint("org-id", new[] { "abc" }, isDynamic: true), CreateEndpoint("org-id", null, QueryParameterMatchMode.Exists, isDynamic: true), CreateEndpoint(new[] { new QueryParameterMatcher("queryParam", Array.Empty(), QueryParameterMatchMode.Exists, isCaseSensitive: true), new QueryParameterMatcher("queryParam", new[] { "abc" }, QueryParameterMatchMode.Exact, isCaseSensitive: true) }) }; var sut = new QueryParameterMatcherPolicy(); var endpointSelectorPolicy = (IEndpointSelectorPolicy)sut; for (var i = 0; i < scenarios.Length; i++) { var result = endpointSelectorPolicy.AppliesToEndpoints(new[] { scenarios[i] }); Assert.True(result, $"scenario {i}"); } } [Fact] public void AppliesToEndpoints_NoMetadata_DoesNotApply() { var endpoint = CreateEndpoint(Array.Empty()); var sut = new QueryParameterMatcherPolicy(); var endpointSelectorPolicy = (IEndpointSelectorPolicy)sut; var result = endpointSelectorPolicy.AppliesToEndpoints(new[] { endpoint }); Assert.False(result); } [Theory] [InlineData(null, false)] [InlineData("", false)] [InlineData("abc", true)] public async Task ApplyAsync_MatchingScenarios_AnyQueryParamValue(string incomingQueryParamValue, bool shouldMatch) { var context = new DefaultHttpContext(); if (incomingQueryParamValue is not null) { var queryStr = "?org-id=" + incomingQueryParamValue; context.Request.QueryString = new QueryString(queryStr); } var endpoint = CreateEndpoint("org-id", Array.Empty(), QueryParameterMatchMode.Exists); var candidates = new CandidateSet(new[] { endpoint }, new RouteValueDictionary[1], new int[1]); var sut = new QueryParameterMatcherPolicy(); await sut.ApplyAsync(context, candidates); Assert.Equal(shouldMatch, candidates.IsValidCandidate(0)); } [Theory] [InlineData("abc", QueryParameterMatchMode.Exact, false, null, false)] [InlineData("abc", QueryParameterMatchMode.Exact, false, "", false)] [InlineData("abc", QueryParameterMatchMode.Exact, false, "abc", true)] [InlineData("abc", QueryParameterMatchMode.Exact, false, "aBC", true)] [InlineData("abc", QueryParameterMatchMode.Exact, false, "abcd", false)] [InlineData("abc", QueryParameterMatchMode.Exact, false, "ab", false)] [InlineData("abc", QueryParameterMatchMode.Exact, true, "", false)] [InlineData("abc", QueryParameterMatchMode.Exact, true, "abc", true)] [InlineData("abc", QueryParameterMatchMode.Exact, true, "aBC", false)] [InlineData("abc", QueryParameterMatchMode.Exact, true, "abcd", false)] [InlineData("abc", QueryParameterMatchMode.Exact, true, "ab", false)] [InlineData("abc", QueryParameterMatchMode.Exact, true, "ab;cd", false)] [InlineData("abc", QueryParameterMatchMode.Exact, true, "a;abc", true)] [InlineData("val ue", QueryParameterMatchMode.Contains, false, "val%20ue", true)] [InlineData("value", QueryParameterMatchMode.Contains, false, "val%20ue", false)] [InlineData("abc", QueryParameterMatchMode.Contains, false, "", false)] [InlineData("abc", QueryParameterMatchMode.Contains, false, "aabc", true)] [InlineData("abc", QueryParameterMatchMode.Contains, false, "zaBCz", true)] [InlineData("abc", QueryParameterMatchMode.Contains, false, "sabcd", true)] [InlineData("abc", QueryParameterMatchMode.Contains, false, "aaab", false)] [InlineData("abc", QueryParameterMatchMode.Contains, true, "", false)] [InlineData("abc", QueryParameterMatchMode.Contains, true, "abcaa", true)] [InlineData("abc", QueryParameterMatchMode.Contains, true, "cbcaBC", false)] [InlineData("abc", QueryParameterMatchMode.Contains, true, "ababcd", true)] [InlineData("abc", QueryParameterMatchMode.Contains, true, "aaaBCd", false)] [InlineData("abc", QueryParameterMatchMode.Contains, true, "baba", false)] [InlineData("abc", QueryParameterMatchMode.Prefix, false, "", false)] [InlineData("abc", QueryParameterMatchMode.Prefix, false, "abc", true)] [InlineData("abc", QueryParameterMatchMode.Prefix, false, "aBC", true)] [InlineData("abc", QueryParameterMatchMode.Prefix, false, "abcd", true)] [InlineData("abc", QueryParameterMatchMode.Prefix, false, "ab", false)] [InlineData("abc", QueryParameterMatchMode.Prefix, true, "", false)] [InlineData("abc", QueryParameterMatchMode.Prefix, true, "abc", true)] [InlineData("abc", QueryParameterMatchMode.Prefix, true, "aBC", false)] [InlineData("abc", QueryParameterMatchMode.Prefix, true, "abcd", true)] [InlineData("abc", QueryParameterMatchMode.Prefix, true, "aBCd", false)] [InlineData("abc", QueryParameterMatchMode.Prefix, true, "ab", false)] [InlineData("abc", QueryParameterMatchMode.NotContains, false, "", true)] [InlineData("abc", QueryParameterMatchMode.NotContains, false, "aabc", false)] [InlineData("abc", QueryParameterMatchMode.NotContains, false, "zaBCz", false)] [InlineData("abc", QueryParameterMatchMode.NotContains, false, "sabcd", false)] [InlineData("abc", QueryParameterMatchMode.NotContains, false, "aaab", true)] [InlineData("abc", QueryParameterMatchMode.NotContains, true, "", true)] [InlineData("abc", QueryParameterMatchMode.NotContains, true, "abcaa", false)] [InlineData("abc", QueryParameterMatchMode.NotContains, true, "cbcaBC", true)] [InlineData("abc", QueryParameterMatchMode.NotContains, true, "ababcd", false)] [InlineData("abc", QueryParameterMatchMode.NotContains, true, "aaaBCd", true)] [InlineData("abc", QueryParameterMatchMode.NotContains, true, "baba", true)] public async Task ApplyAsync_MatchingScenarios_OneQueryParamValue( string queryParamValue, QueryParameterMatchMode queryParamValueMatchMode, bool isCaseSensitive, string incomingQueryParamValue, bool shouldMatch) { var context = new DefaultHttpContext(); if (incomingQueryParamValue is not null) { var queryStr = "?org-id=" + string.Join("&org-id=", incomingQueryParamValue?.Split(';') ?? new[] { "" }); context.Request.QueryString = new QueryString(queryStr); } var endpoint = CreateEndpoint("org-id", new[] { queryParamValue }, queryParamValueMatchMode, isCaseSensitive); var candidates = new CandidateSet(new[] { endpoint }, new RouteValueDictionary[1], new int[1]); var sut = new QueryParameterMatcherPolicy(); await sut.ApplyAsync(context, candidates); Assert.Equal(shouldMatch, candidates.IsValidCandidate(0)); } [Theory] [InlineData("abc", "def", QueryParameterMatchMode.Exact, false, null, false)] [InlineData("abc", "def", QueryParameterMatchMode.Exact, false, "", false)] [InlineData("abc", "def", QueryParameterMatchMode.Exact, false, "abc", true)] [InlineData("abc", "def", QueryParameterMatchMode.Exact, false, "aBc", true)] [InlineData("abc", "def", QueryParameterMatchMode.Exact, false, "abcd", false)] [InlineData("abc", "def", QueryParameterMatchMode.Exact, false, "def", true)] [InlineData("abc", "def", QueryParameterMatchMode.Exact, false, "deF", true)] [InlineData("abc", "def", QueryParameterMatchMode.Exact, false, "defg", false)] [InlineData("abc", "def", QueryParameterMatchMode.Exact, true, null, false)] [InlineData("abc", "def", QueryParameterMatchMode.Exact, true, "", false)] [InlineData("abc", "def", QueryParameterMatchMode.Exact, true, "abc", true)] [InlineData("abc", "def", QueryParameterMatchMode.Exact, true, "aBC", false)] [InlineData("abc", "def", QueryParameterMatchMode.Exact, true, "aBCd", false)] [InlineData("abc", "def", QueryParameterMatchMode.Exact, true, "def", true)] [InlineData("abc", "def", QueryParameterMatchMode.Exact, true, "DEFg", false)] [InlineData("abc", "def", QueryParameterMatchMode.Exact, true, "dEf", false)] [InlineData("abc", "def", QueryParameterMatchMode.Exact, true, ";", false)] [InlineData("abc", "def", QueryParameterMatchMode.Exact, true, "abc;a", true)] [InlineData("abc", "def", QueryParameterMatchMode.Exact, true, "a;abc", true)] [InlineData("abc", "def", QueryParameterMatchMode.Exact, true, "abc;def", true)] [InlineData("abc", "def", QueryParameterMatchMode.Exact, true, "ab;def", true)] [InlineData("abc", "def", QueryParameterMatchMode.Exact, true, "ab;cdef", false)] [InlineData("abc", "def", QueryParameterMatchMode.Prefix, false, null, false)] [InlineData("abc", "def", QueryParameterMatchMode.Prefix, false, "", false)] [InlineData("abc", "def", QueryParameterMatchMode.Prefix, false, "abc", true)] [InlineData("abc", "def", QueryParameterMatchMode.Prefix, false, "aBc", true)] [InlineData("abc", "def", QueryParameterMatchMode.Prefix, false, "abcd", true)] [InlineData("abc", "def", QueryParameterMatchMode.Prefix, false, "abcD", true)] [InlineData("abc", "def", QueryParameterMatchMode.Prefix, false, "def", true)] [InlineData("abc", "def", QueryParameterMatchMode.Prefix, false, "deF", true)] [InlineData("abc", "def", QueryParameterMatchMode.Prefix, false, "defg", true)] [InlineData("abc", "def", QueryParameterMatchMode.Prefix, false, "defG", true)] [InlineData("abc", "def", QueryParameterMatchMode.Prefix, false, "abcA", true)] [InlineData("abc", "def", QueryParameterMatchMode.Prefix, false, "aabc", false)] [InlineData("abc", "def", QueryParameterMatchMode.Prefix, true, null, false)] [InlineData("abc", "def", QueryParameterMatchMode.Prefix, true, "", false)] [InlineData("abc", "def", QueryParameterMatchMode.Prefix, true, "abc", true)] [InlineData("abc", "def", QueryParameterMatchMode.Prefix, true, "aBC", false)] [InlineData("abc", "def", QueryParameterMatchMode.Prefix, true, "aBCd", false)] [InlineData("abc", "def", QueryParameterMatchMode.Prefix, true, "def", true)] [InlineData("abc", "def", QueryParameterMatchMode.Prefix, true, "DEFg", false)] [InlineData("abc", "def", QueryParameterMatchMode.Prefix, true, "abcc", true)] [InlineData("val ue", "def", QueryParameterMatchMode.Contains, false, "val%20ue&aabb", true)] [InlineData("value", "def", QueryParameterMatchMode.Contains, false, "val%20ue&aabb", false)] [InlineData("abc", "def", QueryParameterMatchMode.Prefix, true, "aabc", false)] [InlineData("abc", "def", QueryParameterMatchMode.Prefix, true, ";", false)] [InlineData("abc", "def", QueryParameterMatchMode.Prefix, true, "ab;cde;fgh", false)] [InlineData("abc", "def", QueryParameterMatchMode.Prefix, true, "abcd;e", true)] [InlineData("abc", "def", QueryParameterMatchMode.Prefix, true, "abcd;defg", true)] [InlineData("abc", "def", QueryParameterMatchMode.Prefix, true, "Abcd;defg", true)] [InlineData("abc", "def", QueryParameterMatchMode.Prefix, true, "Abcd;Defg", false)] [InlineData("abc", "def", QueryParameterMatchMode.Prefix, true, "a;defg", true)] [InlineData("abc", "def", QueryParameterMatchMode.Prefix, true, "abcd;", true)] [InlineData("abc", "def", QueryParameterMatchMode.Prefix, true, ";def", true)] [InlineData("abc", "def", QueryParameterMatchMode.Contains, false, null, false)] [InlineData("abc", "def", QueryParameterMatchMode.Contains, false, "", false)] [InlineData("abc", "def", QueryParameterMatchMode.Contains, false, "aaaabc", true)] [InlineData("abc", "def", QueryParameterMatchMode.Contains, false, "aBc", true)] [InlineData("abc", "def", QueryParameterMatchMode.Contains, false, "aabcdd", true)] [InlineData("abc", "def", QueryParameterMatchMode.Contains, false, "ddabcD", true)] [InlineData("abc", "def", QueryParameterMatchMode.Contains, false, "dedef", true)] [InlineData("abc", "def", QueryParameterMatchMode.Contains, false, "adeFF", true)] [InlineData("abc", "def", QueryParameterMatchMode.Contains, false, "degdefg", true)] [InlineData("abc", "def", QueryParameterMatchMode.Contains, false, "efgdefG", true)] [InlineData("abc", "def", QueryParameterMatchMode.Contains, false, "AAabc", true)] [InlineData("abc", "def", QueryParameterMatchMode.Contains, true, null, false)] [InlineData("abc", "def", QueryParameterMatchMode.Contains, true, "", false)] [InlineData("abc", "def", QueryParameterMatchMode.Contains, true, "abc", true)] [InlineData("abc", "def", QueryParameterMatchMode.Contains, true, "aBCcba", false)] [InlineData("abc", "def", QueryParameterMatchMode.Contains, true, "abaBCd", false)] [InlineData("abc", "def", QueryParameterMatchMode.Contains, true, "efdeff", true)] [InlineData("abc", "def", QueryParameterMatchMode.Contains, true, "FDEDEFg", false)] [InlineData("abc", "def", QueryParameterMatchMode.Contains, true, "aabc", true)] [InlineData("abc", "def", QueryParameterMatchMode.NotContains, false, "aaa", true)] [InlineData("abc", "def", QueryParameterMatchMode.NotContains, false, "Abc", false)] [InlineData("abc", "def", QueryParameterMatchMode.NotContains, false, "def", false)] [InlineData("abc", "def", QueryParameterMatchMode.NotContains, false, "", true)] [InlineData("abc", "def", QueryParameterMatchMode.Contains, true, "cabca", true)] [InlineData("abc", "def", QueryParameterMatchMode.Contains, true, "aBCa", false)] [InlineData("abc", "def", QueryParameterMatchMode.Contains, true, "CaBCdd", false)] [InlineData("abc", "def", QueryParameterMatchMode.Contains, true, "DEFdef", true)] [InlineData("abc", "def", QueryParameterMatchMode.Contains, true, "defDEFg", true)] [InlineData("abc", "def", QueryParameterMatchMode.Contains, true, "bbaabc", true)] [InlineData("abc", "def", QueryParameterMatchMode.Contains, true, ";", false)] [InlineData("abc", "def", QueryParameterMatchMode.Contains, true, "cabca;", true)] [InlineData("abc", "def", QueryParameterMatchMode.Contains, true, ";cabca", true)] [InlineData("abc", "def", QueryParameterMatchMode.Contains, true, "ab;cd;ef", false)] [InlineData("abc", "def", QueryParameterMatchMode.Contains, true, "aBCa;deFg", false)] [InlineData("abc", "def", QueryParameterMatchMode.Contains, true, "aBCa;defg", true)] [InlineData("abc", "def", QueryParameterMatchMode.Contains, true, "abcd;d", true)] [InlineData("abc", "ABC", QueryParameterMatchMode.Contains, true, "abc;d", true)] [InlineData("abc", "ABC", QueryParameterMatchMode.Contains, true, "ABC;d", true)] [InlineData("abc", "ABC", QueryParameterMatchMode.Contains, true, "abC;d", false)] [InlineData("abc", "ABC", QueryParameterMatchMode.Contains, true, "abcABC;d", true)] [InlineData("abc", "def", QueryParameterMatchMode.NotContains, false, null, true)] [InlineData("abc", "def", QueryParameterMatchMode.NotContains, true, "aaa", true)] [InlineData("abc", "def", QueryParameterMatchMode.NotContains, true, "Abc", true)] [InlineData("abc", "def", QueryParameterMatchMode.NotContains, true, "def", false)] [InlineData("abc", "def", QueryParameterMatchMode.NotContains, true, "", true)] [InlineData("abc", "def", QueryParameterMatchMode.NotContains, false, "aabc", false)] [InlineData("abc", "def", QueryParameterMatchMode.NotContains, false, "baBc", false)] [InlineData("abc", "def", QueryParameterMatchMode.NotContains, false, "ababcd", false)] [InlineData("abc", "def", QueryParameterMatchMode.NotContains, false, "dcabcD", false)] [InlineData("abc", "def", QueryParameterMatchMode.NotContains, false, "ghi", true)] [InlineData("abc", "def", QueryParameterMatchMode.NotContains, true, null, true)] [InlineData("abc", "def", QueryParameterMatchMode.NotContains, true, "cabca", false)] [InlineData("abc", "def", QueryParameterMatchMode.NotContains, true, "aBCa", true)] [InlineData("abc", "def", QueryParameterMatchMode.NotContains, true, "CaBCdd", true)] [InlineData("abc", "def", QueryParameterMatchMode.NotContains, true, "DEFdef", false)] [InlineData("abc", "def", QueryParameterMatchMode.NotContains, true, "DEFg", true)] [InlineData("abc", "def", QueryParameterMatchMode.NotContains, true, "bbaabc", false)] [InlineData("abc", "def", QueryParameterMatchMode.NotContains, true, "defG", false)] [InlineData("abc", "def", QueryParameterMatchMode.NotContains, true, "bbaabc;", false)] [InlineData("abc", "def", QueryParameterMatchMode.NotContains, true, ";bbaabc", false)] [InlineData("abc", "def", QueryParameterMatchMode.NotContains, true, "ab;cd;ef", true)] [InlineData("abc", "def", QueryParameterMatchMode.NotContains, true, "a;defg", false)] [InlineData("abc", "def", QueryParameterMatchMode.NotContains, true, "ab;cdef", false)] [InlineData("abc", "def", QueryParameterMatchMode.NotContains, true, "abc;def", false)] [InlineData("abc", "def", QueryParameterMatchMode.NotContains, true, "Abc;cdef", false)] [InlineData("abc", "def", QueryParameterMatchMode.NotContains, true, "Abc;cdEf", true)] public async Task ApplyAsync_MatchingScenarios_TwoQueryParamValues( string queryParam1Value, string queryParam2Value, QueryParameterMatchMode queryParamValueMatchMode, bool isCaseSensitive, string incomingQueryParamValue, bool shouldMatch) { var context = new DefaultHttpContext(); if (incomingQueryParamValue is not null) { var queryStr = "?org-id=" + string.Join("&org-id=", incomingQueryParamValue?.Split(';') ?? new[] { "" }); context.Request.QueryString = new QueryString(queryStr); } var endpoint = CreateEndpoint("org-id", new[] { queryParam1Value, queryParam2Value }, queryParamValueMatchMode, isCaseSensitive); var candidates = new CandidateSet(new[] { endpoint }, new RouteValueDictionary[1], new int[1]); var sut = new QueryParameterMatcherPolicy(); await sut.ApplyAsync(context, candidates); Assert.Equal(shouldMatch, candidates.IsValidCandidate(0)); } [Theory] [InlineData(QueryParameterMatchMode.NotContains, true, true)] [InlineData(QueryParameterMatchMode.NotContains, false, true)] [InlineData(QueryParameterMatchMode.Exists, true, false)] [InlineData(QueryParameterMatchMode.Exists, false, false)] [InlineData(QueryParameterMatchMode.Contains, true, false)] [InlineData(QueryParameterMatchMode.Contains, false, false)] [InlineData(QueryParameterMatchMode.Exact, true, false)] [InlineData(QueryParameterMatchMode.Exact, false, false)] [InlineData(QueryParameterMatchMode.Prefix, true, false)] [InlineData(QueryParameterMatchMode.Prefix, false, false)] public async Task ApplyAsync_MatchingScenarios_MissingParam( QueryParameterMatchMode queryParamValueMatchMode, bool isCaseSensitive, bool shouldMatch) { var context = new DefaultHttpContext(); var queryParamValues = new[] { "bar" }; if (queryParamValueMatchMode == QueryParameterMatchMode.Exists) { queryParamValues = null; } var endpoint = CreateEndpoint("foo", queryParamValues, queryParamValueMatchMode, isCaseSensitive); var candidates = new CandidateSet(new[] { endpoint }, new RouteValueDictionary[1], new int[1]); var sut = new QueryParameterMatcherPolicy(); await sut.ApplyAsync(context, candidates); Assert.Equal(shouldMatch, candidates.IsValidCandidate(0)); } [Theory] [InlineData(false, false, false)] [InlineData(false, true, false)] [InlineData(true, false, false)] [InlineData(true, true, true)] public async Task ApplyAsync_MultipleRules_RequiresAllQueryParameter(bool sendQueryParam1, bool sendQueryParam2, bool shouldMatch) { var endpoint = CreateEndpoint(new[] { new QueryParameterMatcher("queryParam1", new[] { "value1" }, QueryParameterMatchMode.Exact, isCaseSensitive: false), new QueryParameterMatcher("queryParam2", new[] { "value2" }, QueryParameterMatchMode.Exact, isCaseSensitive: false) }); var context = new DefaultHttpContext(); var queryStr = new List(); if (sendQueryParam1) { queryStr.Add("queryParam1=value1"); } if (sendQueryParam2) { queryStr.Add("queryParam2=value2"); } context.Request.QueryString = new QueryString("?" + string.Join("&", queryStr)); var candidates = new CandidateSet(new[] { endpoint }, new RouteValueDictionary[1], new int[1]); var sut = new QueryParameterMatcherPolicy(); await sut.ApplyAsync(context, candidates); Assert.Equal(shouldMatch, candidates.IsValidCandidate(0)); } private static Endpoint CreateEndpoint( string queryParamName, string[] queryParamValues, QueryParameterMatchMode mode = QueryParameterMatchMode.Exact, bool isCaseSensitive = false, bool isDynamic = false) { return CreateEndpoint(new[] { new QueryParameterMatcher(queryParamName, queryParamValues, mode, isCaseSensitive) }, isDynamic); } private static Endpoint CreateEndpoint(IReadOnlyList matchers, bool isDynamic = false) { var builder = new RouteEndpointBuilder(_ => Task.CompletedTask, RoutePatternFactory.Parse("/"), 0); builder.Metadata.Add(new QueryParameterMetadata(matchers)); if (isDynamic) { builder.Metadata.Add(new DynamicEndpointMetadata()); } return builder.Build(); } private class DynamicEndpointMetadata : IDynamicEndpointMetadata { public bool IsDynamic => true; } } ================================================ FILE: test/ReverseProxy.Tests/Routing/ReverseProxyConventionBuilderTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Net.Http; using System.Threading.Tasks; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Patterns; using Xunit; using Yarp.ReverseProxy.Configuration; using Yarp.ReverseProxy.Model; using Yarp.ReverseProxy.Forwarder; namespace Microsoft.AspNetCore.Builder.Tests; public class ReverseProxyConventionBuilderTests { [Fact] public void ReverseProxyConventionBuilder_Configure_Works() { var configured = false; var conventions = new List>(); var builder = new ReverseProxyConventionBuilder(conventions); builder.ConfigureEndpoints(builder => { configured = true; }); var routeConfig = new RouteConfig(); var cluster = new ClusterConfig(); var endpointBuilder = CreateEndpointBuilder(routeConfig, cluster); var action = Assert.Single(conventions); action(endpointBuilder); Assert.True(configured); } [Fact] public void ReverseProxyConventionBuilder_ConfigureWithProxy_Works() { var configured = false; var conventions = new List>(); var builder = new ReverseProxyConventionBuilder(conventions); builder.ConfigureEndpoints((builder, proxy) => { configured = true; }); var routeConfig = new RouteConfig(); var cluster = new ClusterConfig(); var endpointBuilder = CreateEndpointBuilder(routeConfig, cluster); var action = Assert.Single(conventions); action(endpointBuilder); Assert.True(configured); } [Fact] public void ReverseProxyConventionBuilder_ConfigureWithProxyAndCluster_Works() { var configured = false; var conventions = new List>(); var builder = new ReverseProxyConventionBuilder(conventions); builder.ConfigureEndpoints((builder, proxy, cluster) => { configured = true; }); var routeConfig = new RouteConfig(); var cluster = new ClusterConfig(); var endpointBuilder = CreateEndpointBuilder(routeConfig, cluster); var action = Assert.Single(conventions); action(endpointBuilder); Assert.True(configured); } private static RouteEndpointBuilder CreateEndpointBuilder(RouteConfig routeConfig, ClusterConfig cluster) { var endpointBuilder = new RouteEndpointBuilder(context => Task.CompletedTask, RoutePatternFactory.Parse(""), 0); var routeModel = new RouteModel( routeConfig, new ClusterState("cluster-1") { Model = new ClusterModel(cluster, new HttpMessageInvoker(new HttpClientHandler())) }, HttpTransformer.Default); endpointBuilder.Metadata.Add(routeModel); return endpointBuilder; } } ================================================ FILE: test/ReverseProxy.Tests/Routing/RoutingTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Xunit; using Yarp.ReverseProxy.Configuration; namespace Yarp.ReverseProxy.Routing.Tests; public class RoutingTests { [Fact] public async Task PathRouting_Works() { var routes = new[] { new RouteConfig() { RouteId = "route1", ClusterId = "cluster1", Match = new RouteMatch { Path = "/api/{**catchall}" } } }; using var host = await CreateHostAsync(routes); var client = host.GetTestClient(); // Positive var response = await client.GetAsync("/api/extra"); response.EnsureSuccessStatusCode(); Assert.Equal("route1", response.Headers.GetValues("route").SingleOrDefault()); // Negative response = await client.GetAsync("/"); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } [Fact] public async Task HostRouting_Works() { var routes = new[] { new RouteConfig() { RouteId = "route1", ClusterId = "cluster1", Match = new RouteMatch { Hosts = new[] { "*.example.com" } } } }; using var host = await CreateHostAsync(routes); var client = host.GetTestClient(); // Positive var response = await client.GetAsync("http://foo.example.com/"); response.EnsureSuccessStatusCode(); Assert.Equal("route1", response.Headers.GetValues("route").SingleOrDefault()); // Negative response = await client.GetAsync("http://example.com"); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } [Fact] public async Task HeaderRouting_OneHeader_Works() { var routes = new[] { new RouteConfig() { RouteId = "route1", ClusterId = "cluster1", Match = new RouteMatch { Path = "/{**catchall}", Headers = new[] { new RouteHeader() { Name = "header1", Values = new[] { "value1" }, } } } } }; using var host = await CreateHostAsync(routes); var client = host.GetTestClient(); // Positive var request = new HttpRequestMessage(); request.Headers.Add("header1", "value1"); var response = await client.SendAsync(request); response.EnsureSuccessStatusCode(); Assert.Equal("route1", response.Headers.GetValues("route").SingleOrDefault()); // Negative response = await client.GetAsync("/"); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); request = new HttpRequestMessage(); request.Headers.Add("header2", "value1"); response = await client.SendAsync(request); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); request = new HttpRequestMessage(); request.Headers.Add("header1", "v"); response = await client.SendAsync(request); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); request = new HttpRequestMessage(); request.Headers.Add("header1", (string)null); response = await client.SendAsync(request); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } [Fact] public async Task HeaderRouting_MultipleHeaders_Works() { var routes = new[] { new RouteConfig() { RouteId = "route1", ClusterId = "cluster1", Match = new RouteMatch { Path = "/{**catchall}", Headers = new[] { new RouteHeader() { Name = "header1", Values = new[] { "value1" }, } } } }, new RouteConfig() { RouteId = "route2", ClusterId = "cluster1", Match = new RouteMatch { Path = "/{**catchall}", Headers = new[] { new RouteHeader() { Name = "header2", Values = new[] { "value2" }, } } } }, new RouteConfig() { RouteId = "route3", ClusterId = "cluster1", Match = new RouteMatch { Path = "/{**catchall}", Headers = new[] { new RouteHeader() { Name = "header1", Values = new[] { "value1" }, }, new RouteHeader() { Name = "header2", Values = new[] { "value2" }, } } } } }; using var host = await CreateHostAsync(routes); var client = host.GetTestClient(); // Check for the most specific match var request = new HttpRequestMessage(); request.Headers.Add("header1", "value1"); var response = await client.SendAsync(request); response.EnsureSuccessStatusCode(); Assert.Equal("route1", response.Headers.GetValues("route").SingleOrDefault()); request = new HttpRequestMessage(); request.Headers.Add("header1", "value1"); request.Headers.Add("header2", "value3"); response = await client.SendAsync(request); response.EnsureSuccessStatusCode(); Assert.Equal("route1", response.Headers.GetValues("route").SingleOrDefault()); request = new HttpRequestMessage(); request.Headers.Add("header2", "value2"); response = await client.SendAsync(request); response.EnsureSuccessStatusCode(); Assert.Equal("route2", response.Headers.GetValues("route").SingleOrDefault()); request = new HttpRequestMessage(); request.Headers.Add("header1", "value3"); request.Headers.Add("header2", "value2"); response = await client.SendAsync(request); response.EnsureSuccessStatusCode(); Assert.Equal("route2", response.Headers.GetValues("route").SingleOrDefault()); request = new HttpRequestMessage(); request.Headers.Add("header1", "value1"); request.Headers.Add("header2", "value2"); response = await client.SendAsync(request); response.EnsureSuccessStatusCode(); Assert.Equal("route3", response.Headers.GetValues("route").SingleOrDefault()); // Negative response = await client.GetAsync("/"); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); request = new HttpRequestMessage(); request.Headers.Add("header1", "value2"); request.Headers.Add("header2", "value1"); response = await client.SendAsync(request); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } [Fact] public async Task Precedence_PathMethodHostHeaders() { var routes = new[] { new RouteConfig() { RouteId = "route1", ClusterId = "cluster1", Match = new RouteMatch { Path = "/route1" } }, new RouteConfig() { RouteId = "route2", ClusterId = "cluster1", Match = new RouteMatch { Path = "/{**catchall}", Methods = new[] { "GET" }, } }, new RouteConfig() { RouteId = "route3", ClusterId = "cluster1", Match = new RouteMatch { Hosts = new[] { "localhost" } } }, new RouteConfig() { RouteId = "route4", ClusterId = "cluster1", Match = new RouteMatch { Path = "/{**catchall}", Headers = new[] { new RouteHeader() { Name = "header1", Values = new[] { "value1" }, }, } } } }; using var host = await CreateHostAsync(routes); var client = host.GetTestClient(); // Check for the highest priority match // Path var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/route1"); request.Headers.Add("header1", "value1"); var response = await client.SendAsync(request); response.EnsureSuccessStatusCode(); Assert.Equal("route1", response.Headers.GetValues("route").SingleOrDefault()); // Method request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/"); request.Headers.Add("header1", "value1"); response = await client.SendAsync(request); response.EnsureSuccessStatusCode(); Assert.Equal("route2", response.Headers.GetValues("route").SingleOrDefault()); // Host request = new HttpRequestMessage(HttpMethod.Post, "http://localhost/"); request.Headers.Add("header1", "value1"); response = await client.SendAsync(request); response.EnsureSuccessStatusCode(); Assert.Equal("route3", response.Headers.GetValues("route").SingleOrDefault()); // Header request = new HttpRequestMessage(HttpMethod.Post, "http://example/"); request.Headers.Add("header1", "value1"); response = await client.SendAsync(request); response.EnsureSuccessStatusCode(); Assert.Equal("route4", response.Headers.GetValues("route").SingleOrDefault()); } public static Task CreateHostAsync(IReadOnlyList routes) { var clusters = new[] { new ClusterConfig() { ClusterId = "cluster1", Destinations = new Dictionary(StringComparer.OrdinalIgnoreCase) { { "d1", new DestinationConfig() { Address = "http://localhost/" } } } } }; return new HostBuilder() .ConfigureWebHost(webHost => { webHost.UseTestServer(); webHost.ConfigureServices(services => { services.AddReverseProxy() .LoadFromMemory(routes, clusters); }); webHost.Configure(appBuilder => { appBuilder.UseRouting(); appBuilder.UseEndpoints(endpoints => { endpoints.MapReverseProxy(proxyApp => { proxyApp.Run(context => { var endpoint = context.GetEndpoint(); context.Response.Headers["route"] = endpoint.DisplayName; return Task.CompletedTask; }); }); }); }); }).StartAsync(); } } ================================================ FILE: test/ReverseProxy.Tests/SessionAffinity/AffinitizeTransformProviderTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Linq; using Moq; using Xunit; using Yarp.ReverseProxy.Configuration; using Yarp.ReverseProxy.Transforms.Builder; namespace Yarp.ReverseProxy.SessionAffinity.Tests; public class AffinitizeTransformProviderTests { [Fact] public void EnableSessionAffinity_AddsTransform() { var affinityPolicy = new Mock(MockBehavior.Strict); affinityPolicy.SetupGet(p => p.Name).Returns("Policy"); var transformProvider = new AffinitizeTransformProvider(new[] { affinityPolicy.Object }); var cluster = new ClusterConfig { ClusterId = "cluster1", SessionAffinity = new SessionAffinityConfig { Enabled = true, Policy = "Policy", AffinityKeyName = "Key1" } }; 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 affinityPolicy = new Mock(MockBehavior.Strict); affinityPolicy.SetupGet(p => p.Name).Returns("Policy"); var transformProvider = new AffinitizeTransformProvider(new[] { affinityPolicy.Object }); var cluster = new ClusterConfig { ClusterId = "cluster1", SessionAffinity = new SessionAffinityConfig { Enabled = true, Policy = "Invalid", AffinityKeyName = "Key1" } }; var validationContext = new TransformClusterValidationContext() { Cluster = cluster, }; transformProvider.ValidateCluster(validationContext); var ex = Assert.Single(validationContext.Errors); Assert.Equal("No matching ISessionAffinityPolicy found for the session affinity policy 'Invalid' set on the cluster 'cluster1'.", ex.Message); var builderContext = new TransformBuilderContext() { Cluster = cluster, }; ex = Assert.Throws(() => transformProvider.Apply(builderContext)); Assert.Equal($"No {typeof(ISessionAffinityPolicy).FullName} was found for the id 'Invalid'. (Parameter 'id')", ex.Message); } } ================================================ FILE: test/ReverseProxy.Tests/SessionAffinity/AffinitizeTransformTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Moq; using Xunit; using Yarp.ReverseProxy.Configuration; using Yarp.ReverseProxy.Forwarder; using Yarp.ReverseProxy.Model; using Yarp.ReverseProxy.Transforms; namespace Yarp.ReverseProxy.SessionAffinity.Tests; public class AffinitizeTransformTests { [Fact] public async Task ApplyAsync_InvokeAffinitizeRequest() { var cluster = GetCluster(); var destination = cluster.Destinations.Values.First(); var provider = new Mock(MockBehavior.Strict); provider .Setup(p => p.AffinitizeResponseAsync( It.IsAny(), It.IsAny(), It.IsNotNull(), It.IsAny(), It.IsAny())) .Returns(new ValueTask()); var transform = new AffinitizeTransform(provider.Object); var context = new DefaultHttpContext(); context.Features.Set(new ReverseProxyFeature() { Cluster = cluster.Model, Route = new RouteModel(new RouteConfig(), cluster, HttpTransformer.Default), ProxiedDestination = destination, }); var transformContext = new ResponseTransformContext() { HttpContext = context, }; await transform.ApplyAsync(transformContext); provider.Verify(); } internal ClusterState GetCluster() { var cluster = new ClusterState("cluster-1"); cluster.Destinations.GetOrAdd("dest-A", id => new DestinationState(id)); cluster.Model = new ClusterModel(new ClusterConfig { SessionAffinity = new SessionAffinityConfig { Enabled = true, Policy = "Policy-B", FailurePolicy = "Policy-1", AffinityKeyName = "Key1" } }, new HttpMessageInvoker(new Mock().Object)); var destinations = cluster.Destinations.Values.ToList(); cluster.DestinationsState = new ClusterDestinationsState(destinations, destinations); return cluster; } } ================================================ FILE: test/ReverseProxy.Tests/SessionAffinity/AffinityTestHelper.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Text; using Microsoft.AspNetCore.DataProtection; using Microsoft.Extensions.Logging; using Moq; namespace Yarp.ReverseProxy.SessionAffinity.Tests; public static class AffinityTestHelper { public static Mock> GetLogger() { var result = new Mock>(); result.Setup(l => l.IsEnabled(It.IsAny())).Returns(true); return result; } public static Mock GetDataProtector() { var protector = new Mock(); protector.Setup(p => p.CreateProtector(It.IsAny())).Returns(protector.Object); protector.Setup(p => p.Protect(It.IsAny())).Returns((byte[] b) => b); protector.Setup(p => p.Unprotect(It.IsAny())).Returns((byte[] b) => b); return protector; } public static string ToUTF8BytesInBase64(this string text) { return Convert.ToBase64String(Encoding.UTF8.GetBytes(text)); } } ================================================ FILE: test/ReverseProxy.Tests/SessionAffinity/ArrCookieSessionAffinityPolicyTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Security.Cryptography; using System.Text; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging.Abstractions; using Xunit; using Yarp.ReverseProxy.Configuration; using Yarp.ReverseProxy.Model; using Yarp.Tests.Common; namespace Yarp.ReverseProxy.SessionAffinity.Tests; public class ArrCookieSessionAffinityPolicyTests { private readonly SessionAffinityConfig _config = new() { Enabled = true, Policy = "ArrCookie", FailurePolicy = "Return503Error", AffinityKeyName = "My.Affinity", Cookie = new SessionAffinityCookieConfig { Domain = "mydomain.my", HttpOnly = false, IsEssential = true, MaxAge = TimeSpan.FromHours(1), Path = "/some", SameSite = SameSiteMode.Lax, SecurePolicy = CookieSecurePolicy.Always, } }; private readonly IReadOnlyList _destinations = new[] { new DestinationState("dest-A"), new DestinationState("dest-B"), new DestinationState("dest-C") }; [Fact] public void FindAffinitizedDestination_AffinityKeyIsNotSetOnRequest_ReturnKeyNotSet() { var policy = new ArrCookieSessionAffinityPolicy( new TestTimeProvider(), NullLogger.Instance); Assert.Equal(SessionAffinityConstants.Policies.ArrCookie, policy.Name); var context = new DefaultHttpContext(); context.Request.Headers["Cookie"] = new[] { $"Some-Cookie=ZZZ" }; var cluster = new ClusterState("cluster"); var affinityResult = policy.FindAffinitizedDestinations(context, cluster, _config, _destinations); Assert.Equal(AffinityStatus.AffinityKeyNotSet, affinityResult.Status); Assert.Null(affinityResult.Destinations); } [Fact] public void FindAffinitizedDestination_AffinityKeyIsSetOnRequest_Success() { var policy = new ArrCookieSessionAffinityPolicy( new TestTimeProvider(), NullLogger.Instance); var context = new DefaultHttpContext(); var affinitizedDestination = _destinations[1]; context.Request.Headers["Cookie"] = GetCookieWithAffinity(affinitizedDestination); var cluster = new ClusterState("cluster"); var affinityResult = policy.FindAffinitizedDestinations(context, cluster, _config, _destinations); Assert.Equal(AffinityStatus.OK, affinityResult.Status); Assert.Single(affinityResult.Destinations); Assert.Same(affinitizedDestination, affinityResult.Destinations[0]); } [Fact] public void AffinitizedRequest_CustomConfigAffinityKeyIsNotExtracted_SetKeyOnResponse() { var policy = new ArrCookieSessionAffinityPolicy( new TestTimeProvider(), NullLogger.Instance); var context = new DefaultHttpContext(); policy.AffinitizeResponse(context, new ClusterState("cluster"), _config, _destinations[1]); var affinityCookieHeader = context.Response.Headers["Set-Cookie"]; Assert.Equal("My.Affinity=920A160FA519353932B655488361A944531650016793761EE7224DE632863B13; max-age=3600; domain=mydomain.my; path=/some; secure; samesite=lax", affinityCookieHeader); } [Fact] public void AffinitizeRequest_CookieConfigSpecified_UseIt() { var policy = new ArrCookieSessionAffinityPolicy( new TestTimeProvider(), NullLogger.Instance); var context = new DefaultHttpContext(); policy.AffinitizeResponse(context, new ClusterState("cluster"), _config, _destinations[1]); var affinityCookieHeader = context.Response.Headers["Set-Cookie"]; Assert.Equal("My.Affinity=920A160FA519353932B655488361A944531650016793761EE7224DE632863B13; max-age=3600; domain=mydomain.my; path=/some; secure; samesite=lax", affinityCookieHeader); } [Fact] public void AffinitizedRequest_AffinityKeyIsExtracted_DoNothing() { var policy = new ArrCookieSessionAffinityPolicy( new TestTimeProvider(), NullLogger.Instance); var context = new DefaultHttpContext(); var affinitizedDestination = _destinations[0]; context.Request.Headers["Cookie"] = GetCookieWithAffinity(affinitizedDestination); var cluster = new ClusterState("cluster"); var affinityResult = policy.FindAffinitizedDestinations(context, cluster, _config, _destinations); Assert.Equal(AffinityStatus.OK, affinityResult.Status); policy.AffinitizeResponse(context, cluster, _config, affinitizedDestination); Assert.False(context.Response.Headers.ContainsKey("Cookie")); } private string[] GetCookieWithAffinity(DestinationState affinitizedDestination) { var destinationIdBytes = Encoding.Unicode.GetBytes(affinitizedDestination.DestinationId.ToLowerInvariant()); var hashBytes = SHA256.HashData(destinationIdBytes); var value = Convert.ToHexString(hashBytes); return new[] { $"Some-Cookie=ZZZ", $"{_config.AffinityKeyName}={value}" }; } } ================================================ FILE: test/ReverseProxy.Tests/SessionAffinity/BaseSessionAffinityPolicyTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Linq; using System.Text; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Moq; using Xunit; using Yarp.ReverseProxy.Configuration; using Yarp.ReverseProxy.Model; namespace Yarp.ReverseProxy.SessionAffinity.Tests; public class BaseSesstionAffinityPolicyTests { private const string InvalidKeyNull = "!invalid key - null!"; private const string InvalidKeyThrow = "!invalid key - throw!"; private const string KeyName = "StubAffinityKey"; private readonly SessionAffinityConfig _defaultOptions = new SessionAffinityConfig { Enabled = true, Policy = "Stub", FailurePolicy = "Return503Error", AffinityKeyName = "StubAffinityKey" }; [Theory] [MemberData(nameof(FindAffinitizedDestinationsCases))] public void Request_FindAffinitizedDestinations( HttpContext context, DestinationState[] allDestinations, AffinityStatus expectedStatus, DestinationState expectedDestination, byte[] expectedEncryptedKey, bool unprotectCalled, LogLevel? expectedLogLevel, EventId expectedEventId) { var dataProtector = GetDataProtector(); var logger = AffinityTestHelper.GetLogger>(); var provider = new ProviderStub(dataProtector.Object, logger.Object); var cluster = new ClusterState("cluster"); var affinityResult = provider.FindAffinitizedDestinations(context, cluster, _defaultOptions, allDestinations); if (unprotectCalled) { dataProtector.Verify(p => p.Unprotect(It.Is(b => b.SequenceEqual(expectedEncryptedKey))), Times.Once); } Assert.Equal(expectedStatus, affinityResult.Status); Assert.Same(expectedDestination, affinityResult.Destinations?.FirstOrDefault()); if (expectedLogLevel is not null) { logger.Verify( l => l.Log(expectedLogLevel.Value, expectedEventId, It.IsAny(), It.IsAny(), (Func)It.IsAny()), Times.Once); } if (expectedDestination is not null) { Assert.Single(affinityResult.Destinations); } else { Assert.Null(affinityResult.Destinations); } } [Fact] public void FindAffinitizedDestination_AffinityDisabledOnCluster_ReturnsAffinityDisabled() { var provider = new ProviderStub(GetDataProtector().Object, AffinityTestHelper.GetLogger>().Object); var options = new SessionAffinityConfig { Enabled = false, Policy = _defaultOptions.Policy, FailurePolicy = _defaultOptions.FailurePolicy, AffinityKeyName = _defaultOptions.AffinityKeyName }; var cluster = new ClusterState("cluster"); Assert.Throws(() => provider.FindAffinitizedDestinations(new DefaultHttpContext(), cluster, options, new[] { new DestinationState("1") })); } [Fact] public void AffinitizeRequest_AffinityDisabled_DoNothing() { var dataProtector = GetDataProtector(); var provider = new ProviderStub(dataProtector.Object, AffinityTestHelper.GetLogger>().Object); Assert.Throws(() => provider.AffinitizeResponse(new DefaultHttpContext(), new ClusterState("cluster"), new SessionAffinityConfig(), new DestinationState("id"))); } [Fact] public void AffinitizeRequest_RequestIsAffinitized_DoNothing() { var dataProtector = GetDataProtector(); var provider = new ProviderStub(dataProtector.Object, AffinityTestHelper.GetLogger>().Object); var context = new DefaultHttpContext(); provider.DirectlySetExtractedKeyOnContext(context, "ExtractedKey"); provider.AffinitizeResponse(context, new ClusterState("cluster"), _defaultOptions, new DestinationState("id")); Assert.Null(provider.LastSetEncryptedKey); dataProtector.Verify(p => p.Protect(It.IsAny()), Times.Never); } [Fact] public void AffinitizeRequest_RequestIsNotAffinitized_SetAffinityKey() { var dataProtector = GetDataProtector(); var provider = new ProviderStub(dataProtector.Object, AffinityTestHelper.GetLogger>().Object); var destination = new DestinationState("dest-A"); provider.AffinitizeResponse(new DefaultHttpContext(), new ClusterState("cluster"), _defaultOptions, destination); Assert.Equal("ZGVzdC1B", provider.LastSetEncryptedKey); var keyBytes = Encoding.UTF8.GetBytes(destination.DestinationId); dataProtector.Verify(p => p.Protect(It.Is(b => b.SequenceEqual(keyBytes))), Times.Once); } [Fact] public void Ctor_MandatoryArgumentIsNull_Throw() { Assert.Throws(() => new ProviderStub(null, new Mock().Object)); // CreateDataProtector will return null Assert.Throws(() => new ProviderStub(new Mock().Object, new Mock().Object)); Assert.Throws(() => new ProviderStub(GetDataProtector().Object, null)); } public static IEnumerable FindAffinitizedDestinationsCases() { var destinations = new[] { new DestinationState("dest-A"), new DestinationState("dest-B"), new DestinationState("dest-C") }; yield return new object[] { GetHttpContext(new[] { ("SomeKey", "SomeValue") }), destinations, AffinityStatus.AffinityKeyNotSet, null, null, false, null, null }; yield return new object[] { GetHttpContext(new[] { (KeyName, "dest-B") }), destinations, AffinityStatus.OK, destinations[1], Encoding.UTF8.GetBytes("dest-B"), true, null, null }; yield return new object[] { GetHttpContext(new[] { (KeyName, "dest-Z") }), destinations, AffinityStatus.DestinationNotFound, null, Encoding.UTF8.GetBytes("dest-Z"), true, LogLevel.Warning, EventIds.DestinationMatchingToAffinityKeyNotFound }; yield return new object[] { GetHttpContext(new[] { (KeyName, "dest-B") }), Array.Empty(), AffinityStatus.DestinationNotFound, null, Encoding.UTF8.GetBytes("dest-B"), true, LogLevel.Warning, EventIds.AffinityCannotBeEstablishedBecauseNoDestinationsFoundOnCluster }; yield return new object[] { GetHttpContext(new[] { (KeyName, "/////") }, false), destinations, AffinityStatus.AffinityKeyExtractionFailed, null, Encoding.UTF8.GetBytes(InvalidKeyNull), false, LogLevel.Error, EventIds.RequestAffinityKeyDecryptionFailed }; yield return new object[] { GetHttpContext(new[] { (KeyName, InvalidKeyNull) }), destinations, AffinityStatus.AffinityKeyExtractionFailed, null, Encoding.UTF8.GetBytes(InvalidKeyNull), true, LogLevel.Error, EventIds.RequestAffinityKeyDecryptionFailed }; yield return new object[] { GetHttpContext(new[] { (KeyName, InvalidKeyThrow) }), destinations, AffinityStatus.AffinityKeyExtractionFailed, null, Encoding.UTF8.GetBytes(InvalidKeyThrow), true, LogLevel.Error, EventIds.RequestAffinityKeyDecryptionFailed }; } private static HttpContext GetHttpContext((string Key, string Value)[] items, bool encodeToBase64 = true) { var context = new DefaultHttpContext { Items = items.ToDictionary(i => (object)i.Key, i => encodeToBase64 ? (object)Convert.ToBase64String(Encoding.UTF8.GetBytes(i.Value)) : i.Value) }; return context; } private Mock GetDataProtector() { var result = new Mock(); var nullBytes = Encoding.UTF8.GetBytes(InvalidKeyNull); var throwBytes = Encoding.UTF8.GetBytes(InvalidKeyThrow); result.Setup(p => p.Protect(It.IsAny())).Returns((byte[] k) => k); result.Setup(p => p.Unprotect(It.IsAny())).Returns((byte[] k) => k); result.Setup(p => p.Unprotect(It.Is(b => b.SequenceEqual(nullBytes)))).Returns((byte[])null); result.Setup(p => p.Unprotect(It.Is(b => b.SequenceEqual(throwBytes)))).Throws(); result.Setup(p => p.CreateProtector(It.IsAny())).Returns(result.Object); return result; } private class ProviderStub : BaseEncryptedSessionAffinityPolicy { public static readonly string KeyNameSetting = "AffinityKeyName"; public ProviderStub(IDataProtectionProvider dataProtectionProvider, ILogger logger) : base(dataProtectionProvider, logger) { } public override string Name => "Stub"; public string LastSetEncryptedKey { get; private set; } public void DirectlySetExtractedKeyOnContext(HttpContext context, string key) { context.Items[AffinityKeyId] = key; } protected override string GetDestinationAffinityKey(DestinationState destination) { return destination.DestinationId; } protected override (string Key, bool ExtractedSuccessfully) GetRequestAffinityKey(HttpContext context, ClusterState cluster, SessionAffinityConfig options) { Assert.Equal(Name, options.Policy); // HttpContext.Items is used here to store the request affinity key for simplicity. // In real world scenario, a provider will extract it from request (e.g. header, cookie, etc.) var encryptedKey = context.Items.TryGetValue(options.AffinityKeyName, out var requestKey) ? requestKey : null; return Unprotect((string)encryptedKey); } protected override void SetAffinityKey(HttpContext context, ClusterState cluster, SessionAffinityConfig options, string unencryptedKey) { var encryptedKey = Protect(unencryptedKey); context.Items[options.AffinityKeyName] = encryptedKey; LastSetEncryptedKey = encryptedKey; } } } ================================================ FILE: test/ReverseProxy.Tests/SessionAffinity/CookieSessionAffinityPolicyTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using Microsoft.AspNetCore.Http; using Xunit; using Yarp.Tests.Common; using Yarp.ReverseProxy.Configuration; using Yarp.ReverseProxy.Model; namespace Yarp.ReverseProxy.SessionAffinity.Tests; public class CookieSessionAffinityPolicyTests { private readonly SessionAffinityConfig _config = new SessionAffinityConfig { Enabled = true, Policy = "Cookie", FailurePolicy = "Return503Error", AffinityKeyName = "My.Affinity", Cookie = new SessionAffinityCookieConfig { Domain = "mydomain.my", HttpOnly = false, IsEssential = true, MaxAge = TimeSpan.FromHours(1), Path = "/some", SameSite = SameSiteMode.Lax, SecurePolicy = CookieSecurePolicy.Always, } }; private readonly IReadOnlyList _destinations = new[] { new DestinationState("dest-A"), new DestinationState("dest-B"), new DestinationState("dest-C") }; [Fact] public void FindAffinitizedDestination_AffinityKeyIsNotSetOnRequest_ReturnKeyNotSet() { var policy = new CookieSessionAffinityPolicy( AffinityTestHelper.GetDataProtector().Object, new TestTimeProvider(), AffinityTestHelper.GetLogger().Object); Assert.Equal(SessionAffinityConstants.Policies.Cookie, policy.Name); var context = new DefaultHttpContext(); context.Request.Headers["Cookie"] = new[] { $"Some-Cookie=ZZZ" }; var cluster = new ClusterState("cluster"); var affinityResult = policy.FindAffinitizedDestinations(context, cluster, _config, _destinations); Assert.Equal(AffinityStatus.AffinityKeyNotSet, affinityResult.Status); Assert.Null(affinityResult.Destinations); } [Fact] public void FindAffinitizedDestination_AffinityKeyIsSetOnRequest_Success() { var policy = new CookieSessionAffinityPolicy( AffinityTestHelper.GetDataProtector().Object, new TestTimeProvider(), AffinityTestHelper.GetLogger().Object); var context = new DefaultHttpContext(); var affinitizedDestination = _destinations[1]; context.Request.Headers["Cookie"] = GetCookieWithAffinity(affinitizedDestination); var cluster = new ClusterState("cluster"); var affinityResult = policy.FindAffinitizedDestinations(context, cluster, _config, _destinations); Assert.Equal(AffinityStatus.OK, affinityResult.Status); Assert.Single(affinityResult.Destinations); Assert.Same(affinitizedDestination, affinityResult.Destinations[0]); } [Fact] public void AffinitizedRequest_CustomConfigAffinityKeyIsNotExtracted_SetKeyOnResponse() { var policy = new CookieSessionAffinityPolicy( AffinityTestHelper.GetDataProtector().Object, new TestTimeProvider(), AffinityTestHelper.GetLogger().Object); var context = new DefaultHttpContext(); policy.AffinitizeResponse(context, new ClusterState("cluster"), _config, _destinations[1]); var affinityCookieHeader = context.Response.Headers["Set-Cookie"]; Assert.Equal("My.Affinity=ZGVzdC1C; max-age=3600; domain=mydomain.my; path=/some; secure; samesite=lax", affinityCookieHeader); } [Fact] public void AffinitizeRequest_CookieConfigSpecified_UseIt() { var policy = new CookieSessionAffinityPolicy( AffinityTestHelper.GetDataProtector().Object, new TestTimeProvider(), AffinityTestHelper.GetLogger().Object); var context = new DefaultHttpContext(); policy.AffinitizeResponse(context, new ClusterState("cluster"), _config, _destinations[1]); var affinityCookieHeader = context.Response.Headers["Set-Cookie"]; Assert.Equal("My.Affinity=ZGVzdC1C; max-age=3600; domain=mydomain.my; path=/some; secure; samesite=lax", affinityCookieHeader); } [Fact] public void AffinitizedRequest_AffinityKeyIsExtracted_DoNothing() { var policy = new CookieSessionAffinityPolicy( AffinityTestHelper.GetDataProtector().Object, new TestTimeProvider(), AffinityTestHelper.GetLogger().Object); var context = new DefaultHttpContext(); var affinitizedDestination = _destinations[0]; context.Request.Headers["Cookie"] = GetCookieWithAffinity(affinitizedDestination); var cluster = new ClusterState("cluster"); var affinityResult = policy.FindAffinitizedDestinations(context, cluster, _config, _destinations); Assert.Equal(AffinityStatus.OK, affinityResult.Status); policy.AffinitizeResponse(context, cluster, _config, affinitizedDestination); Assert.False(context.Response.Headers.ContainsKey("Cookie")); } private string[] GetCookieWithAffinity(DestinationState affinitizedDestination) { return new[] { $"Some-Cookie=ZZZ", $"{_config.AffinityKeyName}={affinitizedDestination.DestinationId.ToUTF8BytesInBase64()}" }; } } ================================================ FILE: test/ReverseProxy.Tests/SessionAffinity/CustomHeaderSessionAffinityPolicyTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; using Microsoft.AspNetCore.Http; using Xunit; using Yarp.ReverseProxy.Configuration; using Yarp.ReverseProxy.Model; namespace Yarp.ReverseProxy.SessionAffinity.Tests; public class CustomHeaderSessionAffinityPolicyTests { private const string AffinityHeaderName = "X-MyAffinity"; private readonly SessionAffinityConfig _defaultOptions = new SessionAffinityConfig { Enabled = true, Policy = "Cookie", FailurePolicy = "Return503Error", AffinityKeyName = AffinityHeaderName }; private readonly IReadOnlyList _destinations = new[] { new DestinationState("dest-A"), new DestinationState("dest-B"), new DestinationState("dest-C") }; [Fact] public void FindAffinitizedDestination_AffinityKeyIsNotSetOnRequest_ReturnKeyNotSet() { var policy = new CustomHeaderSessionAffinityPolicy(AffinityTestHelper.GetDataProtector().Object, AffinityTestHelper.GetLogger().Object); Assert.Equal(SessionAffinityConstants.Policies.CustomHeader, policy.Name); var context = new DefaultHttpContext(); context.Request.Headers["SomeHeader"] = new[] { "SomeValue" }; var cluster = new ClusterState("cluster"); var affinityResult = policy.FindAffinitizedDestinations(context, cluster, _defaultOptions, _destinations); Assert.Equal(AffinityStatus.AffinityKeyNotSet, affinityResult.Status); Assert.Null(affinityResult.Destinations); } [Fact] public void FindAffinitizedDestination_AffinityKeyIsSetOnRequest_Success() { var policy = new CustomHeaderSessionAffinityPolicy(AffinityTestHelper.GetDataProtector().Object, AffinityTestHelper.GetLogger().Object); var context = new DefaultHttpContext(); context.Request.Headers["SomeHeader"] = new[] { "SomeValue" }; var affinitizedDestination = _destinations[1]; context.Request.Headers[AffinityHeaderName] = new[] { affinitizedDestination.DestinationId.ToUTF8BytesInBase64() }; var cluster = new ClusterState("cluster"); var affinityResult = policy.FindAffinitizedDestinations(context, cluster, _defaultOptions, _destinations); Assert.Equal(AffinityStatus.OK, affinityResult.Status); Assert.Single(affinityResult.Destinations); Assert.Same(affinitizedDestination, affinityResult.Destinations[0]); } [Fact] public void AffinitizedRequest_AffinityKeyIsNotExtracted_SetKeyOnResponse() { var policy = new CustomHeaderSessionAffinityPolicy(AffinityTestHelper.GetDataProtector().Object, AffinityTestHelper.GetLogger().Object); var context = new DefaultHttpContext(); var chosenDestination = _destinations[1]; var expectedAffinityHeaderValue = chosenDestination.DestinationId.ToUTF8BytesInBase64(); policy.AffinitizeResponse(context, new ClusterState("cluster"), _defaultOptions, chosenDestination); Assert.True(context.Response.Headers.ContainsKey(AffinityHeaderName)); Assert.Equal(expectedAffinityHeaderValue, context.Response.Headers[AffinityHeaderName]); } [Fact] public void AffinitizedRequest_AffinityKeyIsExtracted_DoNothing() { var policy = new CustomHeaderSessionAffinityPolicy(AffinityTestHelper.GetDataProtector().Object, AffinityTestHelper.GetLogger().Object); var context = new DefaultHttpContext(); context.Request.Headers["SomeHeader"] = new[] { "SomeValue" }; var affinitizedDestination = _destinations[1]; context.Request.Headers[AffinityHeaderName] = new[] { affinitizedDestination.DestinationId.ToUTF8BytesInBase64() }; var cluster = new ClusterState("cluster"); var affinityResult = policy.FindAffinitizedDestinations(context, cluster, _defaultOptions, _destinations); Assert.Equal(AffinityStatus.OK, affinityResult.Status); policy.AffinitizeResponse(context, cluster, _defaultOptions, affinitizedDestination); Assert.False(context.Response.Headers.ContainsKey(AffinityHeaderName)); } } ================================================ FILE: test/ReverseProxy.Tests/SessionAffinity/HashCookieSessionAffinityPolicyTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.IO.Hashing; using System.Text; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging.Abstractions; using Xunit; using Yarp.ReverseProxy.Configuration; using Yarp.ReverseProxy.Model; using Yarp.Tests.Common; namespace Yarp.ReverseProxy.SessionAffinity.Tests; public class HashCookieSessionAffinityPolicyTests { private readonly SessionAffinityConfig _config = new() { Enabled = true, Policy = "HashCookie", FailurePolicy = "Return503Error", AffinityKeyName = "My.Affinity", Cookie = new SessionAffinityCookieConfig { Domain = "mydomain.my", HttpOnly = false, IsEssential = true, MaxAge = TimeSpan.FromHours(1), Path = "/some", SameSite = SameSiteMode.Lax, SecurePolicy = CookieSecurePolicy.Always, } }; private readonly IReadOnlyList _destinations = new[] { new DestinationState("dest-A"), new DestinationState("dest-B"), new DestinationState("dest-C") }; [Fact] public void FindAffinitizedDestination_AffinityKeyIsNotSetOnRequest_ReturnKeyNotSet() { var policy = new HashCookieSessionAffinityPolicy( new TestTimeProvider(), NullLogger.Instance); Assert.Equal(SessionAffinityConstants.Policies.HashCookie, policy.Name); var context = new DefaultHttpContext(); context.Request.Headers["Cookie"] = new[] { $"Some-Cookie=ZZZ" }; var cluster = new ClusterState("cluster"); var affinityResult = policy.FindAffinitizedDestinations(context, cluster, _config, _destinations); Assert.Equal(AffinityStatus.AffinityKeyNotSet, affinityResult.Status); Assert.Null(affinityResult.Destinations); } [Fact] public void FindAffinitizedDestination_AffinityKeyIsSetOnRequest_Success() { var policy = new HashCookieSessionAffinityPolicy( new TestTimeProvider(), NullLogger.Instance); var context = new DefaultHttpContext(); var affinitizedDestination = _destinations[1]; context.Request.Headers["Cookie"] = GetCookieWithAffinity(affinitizedDestination); var cluster = new ClusterState("cluster"); var affinityResult = policy.FindAffinitizedDestinations(context, cluster, _config, _destinations); Assert.Equal(AffinityStatus.OK, affinityResult.Status); Assert.Single(affinityResult.Destinations); Assert.Same(affinitizedDestination, affinityResult.Destinations[0]); } [Fact] public void AffinitizedRequest_CustomConfigAffinityKeyIsNotExtracted_SetKeyOnResponse() { var policy = new HashCookieSessionAffinityPolicy( new TestTimeProvider(), NullLogger.Instance); var context = new DefaultHttpContext(); policy.AffinitizeResponse(context, new ClusterState("cluster"), _config, _destinations[1]); var affinityCookieHeader = context.Response.Headers["Set-Cookie"]; Assert.Equal("My.Affinity=53c079ed4c377b0d; max-age=3600; domain=mydomain.my; path=/some; secure; samesite=lax", affinityCookieHeader); } [Fact] public void AffinitizeRequest_CookieConfigSpecified_UseIt() { var policy = new HashCookieSessionAffinityPolicy( new TestTimeProvider(), NullLogger.Instance); var context = new DefaultHttpContext(); policy.AffinitizeResponse(context, new ClusterState("cluster"), _config, _destinations[1]); var affinityCookieHeader = context.Response.Headers["Set-Cookie"]; Assert.Equal("My.Affinity=53c079ed4c377b0d; max-age=3600; domain=mydomain.my; path=/some; secure; samesite=lax", affinityCookieHeader); } [Fact] public void AffinitizedRequest_AffinityKeyIsExtracted_DoNothing() { var policy = new HashCookieSessionAffinityPolicy( new TestTimeProvider(), NullLogger.Instance); var context = new DefaultHttpContext(); var affinitizedDestination = _destinations[0]; context.Request.Headers["Cookie"] = GetCookieWithAffinity(affinitizedDestination); var cluster = new ClusterState("cluster"); var affinityResult = policy.FindAffinitizedDestinations(context, cluster, _config, _destinations); Assert.Equal(AffinityStatus.OK, affinityResult.Status); policy.AffinitizeResponse(context, cluster, _config, affinitizedDestination); Assert.False(context.Response.Headers.ContainsKey("Cookie")); } private string[] GetCookieWithAffinity(DestinationState affinitizedDestination) { var destinationIdBytes = Encoding.Unicode.GetBytes(affinitizedDestination.DestinationId.ToUpperInvariant()); var hashBytes = XxHash64.Hash(destinationIdBytes); var value = Convert.ToHexString(hashBytes).ToLowerInvariant(); return new[] { $"Some-Cookie=ZZZ", $"{_config.AffinityKeyName}={value}" }; } } ================================================ FILE: test/ReverseProxy.Tests/SessionAffinity/RedistributeAffinityFailurePolicyTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Xunit; namespace Yarp.ReverseProxy.SessionAffinity.Tests; public class RedistributeAffinityFailurePolicyTests { [Theory] [InlineData(AffinityStatus.AffinityKeyExtractionFailed)] [InlineData(AffinityStatus.DestinationNotFound)] public async Task Handle_FailedAffinityStatus_ReturnTrue(AffinityStatus status) { var policy = new RedistributeAffinityFailurePolicy(); Assert.Equal(SessionAffinityConstants.FailurePolicies.Redistribute, policy.Name); Assert.True(await policy.Handle(new DefaultHttpContext(), cluster: null, affinityStatus: status)); } [Theory] [InlineData(AffinityStatus.OK)] [InlineData(AffinityStatus.AffinityKeyNotSet)] public async Task Handle_SuccessfulAffinityStatus_Throw(AffinityStatus status) { var policy = new RedistributeAffinityFailurePolicy(); var context = new DefaultHttpContext(); await Assert.ThrowsAsync(() => policy.Handle(context, cluster: null, affinityStatus: status)); } } ================================================ FILE: test/ReverseProxy.Tests/SessionAffinity/Return503ErrorAffinityFailurePolicyTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Xunit; namespace Yarp.ReverseProxy.SessionAffinity.Tests; public class Return503ErrorAffinityFailurePolicyTests { [Theory] [InlineData(AffinityStatus.DestinationNotFound)] [InlineData(AffinityStatus.AffinityKeyExtractionFailed)] public async Task Handle_FaultyAffinityStatus_RespondWith503(AffinityStatus status) { var policy = new Return503ErrorAffinityFailurePolicy(); var context = new DefaultHttpContext(); Assert.Equal(SessionAffinityConstants.FailurePolicies.Return503Error, policy.Name); Assert.False(await policy.Handle(context, cluster: null, affinityStatus: status)); Assert.Equal(503, context.Response.StatusCode); } [Theory] [InlineData(AffinityStatus.OK)] [InlineData(AffinityStatus.AffinityKeyNotSet)] public async Task Handle_SuccessfulAffinityStatus_Throw(AffinityStatus status) { var policy = new Return503ErrorAffinityFailurePolicy(); var context = new DefaultHttpContext(); await Assert.ThrowsAsync(() => policy.Handle(context, cluster: null, affinityStatus: status)); } } ================================================ FILE: test/ReverseProxy.Tests/SessionAffinity/SessionAffinityMiddlewareTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Moq; using Xunit; using Yarp.ReverseProxy.Configuration; using Yarp.ReverseProxy.Model; using Yarp.ReverseProxy.Forwarder; using System.Threading; namespace Yarp.ReverseProxy.SessionAffinity.Tests; public class SessionAffinityMiddlewareTests { protected const string AffinitizedDestinationName = "dest-B"; protected readonly ClusterModel ClusterConfig = new ClusterModel(new ClusterConfig { ClusterId = "cluster-1", SessionAffinity = new SessionAffinityConfig { Enabled = true, Policy = "Policy-B", FailurePolicy = "Policy-1", AffinityKeyName = "Key1" } }, new HttpMessageInvoker(new Mock().Object)); [Theory] [InlineData(AffinityStatus.AffinityKeyNotSet, null)] [InlineData(AffinityStatus.OK, AffinitizedDestinationName)] public async Task Invoke_SuccessfulFlow_CallNext(AffinityStatus status, string foundDestinationId) { var cluster = GetCluster(); var endpoint = GetEndpoint(cluster); DestinationState foundDestination = null; if (foundDestinationId is not null) { cluster.Destinations.TryGetValue(foundDestinationId, out foundDestination); } var invokedMode = string.Empty; const string expectedMode = "Policy-B"; var policies = RegisterAffinityPolicies( true, cluster.Destinations.Values.ToList(), ("Policy-A", AffinityStatus.DestinationNotFound, (DestinationState)null, (Action)(p => throw new InvalidOperationException($"Policy {p.Name} call is not expected."))), (expectedMode, status, foundDestination, p => invokedMode = p.Name)); var nextInvoked = false; var middleware = new SessionAffinityMiddleware(c => { nextInvoked = true; return Task.CompletedTask; }, policies.Select(p => p.Object), Array.Empty(), new Mock>().Object); var context = new DefaultHttpContext(); context.SetEndpoint(endpoint); var destinationFeature = GetReverseProxyFeature(cluster); context.Features.Set(destinationFeature); await middleware.Invoke(context); Assert.Equal(expectedMode, invokedMode); Assert.True(nextInvoked); policies[0].VerifyGet(p => p.Name, Times.Once); policies[0].VerifyNoOtherCalls(); policies[1].VerifyAll(); if (foundDestinationId is not null) { Assert.Single(destinationFeature.AvailableDestinations); Assert.Equal(foundDestinationId, destinationFeature.AvailableDestinations[0].DestinationId); } else { Assert.True(cluster.Destinations.Values.SequenceEqual(destinationFeature.AvailableDestinations)); } } [Theory] [InlineData(AffinityStatus.DestinationNotFound, true)] [InlineData(AffinityStatus.DestinationNotFound, false)] [InlineData(AffinityStatus.AffinityKeyExtractionFailed, true)] [InlineData(AffinityStatus.AffinityKeyExtractionFailed, false)] public async Task Invoke_ErrorFlow_CallFailurePolicy(AffinityStatus affinityStatus, bool keepProcessing) { var cluster = GetCluster(); var endpoint = GetEndpoint(cluster); var policies = RegisterAffinityPolicies(true, cluster.Destinations.Values.ToList(), ("Policy-B", affinityStatus, null, _ => { })); var invokedPolicy = string.Empty; const string expectedPolicy = "Policy-1"; var failurePolicies = RegisterFailurePolicies( affinityStatus, ("Policy-0", false, p => throw new InvalidOperationException($"Policy {p.Name} call is not expected.")), (expectedPolicy, keepProcessing, p => invokedPolicy = p.Name)); var nextInvoked = false; var logger = AffinityTestHelper.GetLogger(); var middleware = new SessionAffinityMiddleware(c => { nextInvoked = true; return Task.CompletedTask; }, policies.Select(p => p.Object), failurePolicies.Select(p => p.Object), logger.Object); var context = new DefaultHttpContext(); var destinationFeature = GetReverseProxyFeature(cluster); context.SetEndpoint(endpoint); context.Features.Set(destinationFeature); await middleware.Invoke(context); Assert.Equal(expectedPolicy, invokedPolicy); Assert.Equal(keepProcessing, nextInvoked); failurePolicies[0].VerifyGet(p => p.Name, Times.Once); failurePolicies[0].VerifyNoOtherCalls(); failurePolicies[1].VerifyAll(); if (!keepProcessing) { logger.Verify( l => l.Log(LogLevel.Warning, EventIds.AffinityResolutionFailedForCluster, It.IsAny(), null, (Func)It.IsAny()), Times.Once); } } internal ClusterState GetCluster() { var cluster = new ClusterState("cluster-1"); var destinationManager = cluster.Destinations; destinationManager.GetOrAdd("dest-A", id => new DestinationState(id)); destinationManager.GetOrAdd(AffinitizedDestinationName, id => new DestinationState(id)); destinationManager.GetOrAdd("dest-C", id => new DestinationState(id)); cluster.Model = ClusterConfig; cluster.DestinationsState = new ClusterDestinationsState(destinationManager.Values.ToList(), destinationManager.Values.ToList()); return cluster; } internal IReadOnlyList> RegisterAffinityPolicies( bool lookupMiddlewareTest, IReadOnlyList expectedDestinations, params (string Mode, AffinityStatus? Status, DestinationState Destinations, Action Callback)[] prototypes) { var result = new List>(); foreach (var (mode, status, destinations, callback) in prototypes) { var policy = new Mock(MockBehavior.Strict); policy.SetupGet(p => p.Name).Returns(mode); if (lookupMiddlewareTest) { policy.Setup(p => p.FindAffinitizedDestinationsAsync( It.IsAny(), It.IsAny(), ClusterConfig.Config.SessionAffinity, expectedDestinations, It.IsAny())) .Returns(new ValueTask(new AffinityResult(destinations, status.Value))) .Callback(() => callback(policy.Object)); } else { policy.Setup(p => p.AffinitizeResponseAsync( It.IsAny(), It.IsAny(), ClusterConfig.Config.SessionAffinity, expectedDestinations[0], It.IsAny())) .Returns(new ValueTask()) .Callback(() => callback(policy.Object)); } result.Add(policy); } return result.AsReadOnly(); } internal IReadOnlyList> RegisterFailurePolicies(AffinityStatus expectedStatus, params (string Name, bool Handled, Action Callback)[] prototypes) { var result = new List>(); foreach (var (name, handled, callback) in prototypes) { var policy = new Mock(MockBehavior.Strict); policy.SetupGet(p => p.Name).Returns(name); policy.Setup(p => p.Handle(It.IsAny(), It.IsAny(), expectedStatus)) .ReturnsAsync(handled) .Callback(() => callback(policy.Object)); result.Add(policy); } return result.AsReadOnly(); } internal IReverseProxyFeature GetReverseProxyFeature(ClusterState cluster) { return new ReverseProxyFeature() { AvailableDestinations = cluster.Destinations.Values.ToList(), Route = new RouteModel(new RouteConfig(), cluster: cluster, HttpTransformer.Default), Cluster = cluster.Model, }; } internal Endpoint GetEndpoint(ClusterState cluster) { var routeConfig = new RouteConfig(); var routeModel = new RouteModel(routeConfig, cluster, HttpTransformer.Default); var endpoint = new Endpoint(default, new EndpointMetadataCollection(routeModel), string.Empty); return endpoint; } } ================================================ FILE: test/ReverseProxy.Tests/Transforms/Builder/TransformBuilderTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.HttpOverrides; using Microsoft.Extensions.DependencyInjection; using Xunit; using Yarp.ReverseProxy.Configuration; using Yarp.ReverseProxy.Model; using Yarp.Tests.Common; namespace Yarp.ReverseProxy.Transforms.Builder.Tests; public class TransformBuilderTests { [Fact] public void CreateBuilder_Success() { CreateTransformBuilder(); } [Fact] public void NullTransforms_AddsDefaults() { NullOrEmptyTransforms_AddsDefaults(null); } [Fact] public void EmptyTransforms_AddsDefaults() { NullOrEmptyTransforms_AddsDefaults(new List>()); } private void NullOrEmptyTransforms_AddsDefaults(IReadOnlyList> transforms) { var transformBuilder = CreateTransformBuilder(); var route = new RouteConfig { Transforms = transforms }; var errors = transformBuilder.ValidateRoute(route); Assert.Empty(errors); var results = transformBuilder.BuildInternal(route, new ClusterConfig()); Assert.NotNull(results); Assert.Null(results.ShouldCopyRequestHeaders); Assert.Null(results.ShouldCopyResponseHeaders); Assert.Null(results.ShouldCopyResponseTrailers); Assert.Empty(results.ResponseTransforms); Assert.Empty(results.ResponseTrailerTransforms); Assert.Equal(6, results.RequestTransforms.Length); var hostTransform = Assert.Single(results.RequestTransforms.OfType()); Assert.False(hostTransform.UseOriginalHost); var forTransform = Assert.Single(results.RequestTransforms.OfType()); Assert.Equal(ForwardedHeadersDefaults.XForwardedForHeaderName, forTransform.HeaderName); var xHostTransform = Assert.Single(results.RequestTransforms.OfType()); Assert.Equal(ForwardedHeadersDefaults.XForwardedHostHeaderName, xHostTransform.HeaderName); var prefixTransform = Assert.Single(results.RequestTransforms.OfType()); Assert.Equal("X-Forwarded-Prefix", prefixTransform.HeaderName); var protoTransform = Assert.Single(results.RequestTransforms.OfType()); Assert.Equal(ForwardedHeadersDefaults.XForwardedProtoHeaderName, protoTransform.HeaderName); var removeForwardedTransform = Assert.Single(results.RequestTransforms.OfType()); Assert.Equal(ForwardedTransformActions.Remove, removeForwardedTransform.TransformAction); } [Fact] public void CreateTransforms_ExecutesAction() { var transformBuilder = CreateTransformBuilder(); var executed = false; var results = transformBuilder.CreateInternal(_ => { executed = true; }); Assert.True(executed); } [Fact] public void CreateTransforms_AddsDefaults() { var transformBuilder = CreateTransformBuilder(); var results = transformBuilder.CreateInternal(_ => { }); Assert.NotNull(results); Assert.Null(results.ShouldCopyRequestHeaders); Assert.Null(results.ShouldCopyResponseHeaders); Assert.Null(results.ShouldCopyResponseTrailers); Assert.Empty(results.ResponseTransforms); Assert.Empty(results.ResponseTrailerTransforms); Assert.Equal(6, results.RequestTransforms.Length); var hostTransform = Assert.Single(results.RequestTransforms.OfType()); Assert.False(hostTransform.UseOriginalHost); var forTransform = Assert.Single(results.RequestTransforms.OfType()); Assert.Equal(ForwardedHeadersDefaults.XForwardedForHeaderName, forTransform.HeaderName); var xHostTransform = Assert.Single(results.RequestTransforms.OfType()); Assert.Equal(ForwardedHeadersDefaults.XForwardedHostHeaderName, xHostTransform.HeaderName); var prefixTransform = Assert.Single(results.RequestTransforms.OfType()); Assert.Equal("X-Forwarded-Prefix", prefixTransform.HeaderName); var protoTransform = Assert.Single(results.RequestTransforms.OfType()); Assert.Equal(ForwardedHeadersDefaults.XForwardedProtoHeaderName, protoTransform.HeaderName); var removeForwardedTransform = Assert.Single(results.RequestTransforms.OfType()); Assert.Equal(ForwardedTransformActions.Remove, removeForwardedTransform.TransformAction); } [Fact] public void EmptyTransform_Error() { var transformBuilder = CreateTransformBuilder(); var transforms = new[] { new Dictionary(StringComparer.OrdinalIgnoreCase), // Empty }; var route = new RouteConfig() { Transforms = transforms }; var errors = transformBuilder.ValidateRoute(route); var error = Assert.Single(errors); Assert.Equal("Unknown transform: ", error.Message); var nie = Assert.Throws(() => transformBuilder.BuildInternal(route, new ClusterConfig())); Assert.Equal("Unknown transform: ", nie.Message); } [Fact] public void UnknownTransforms_Error() { var transformBuilder = CreateTransformBuilder(); var transforms = new[] { new Dictionary(StringComparer.OrdinalIgnoreCase) // Unknown transform { { "string1", "value1" }, { "string2", "value2" } }, new Dictionary(StringComparer.OrdinalIgnoreCase) // Unknown transform { { "string3", "value3" }, { "string4", "value4" } }, }; var route = new RouteConfig() { Transforms = transforms }; 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, new ClusterConfig())); // First error reported Assert.Equal("Unknown transform: string1;string2", ex.Message); } [Fact] public void CallsTransformFactories() { var factory1 = new TestTransformFactory("1"); var factory2 = new TestTransformFactory("2"); var factory3 = new TestTransformFactory("3"); var builder = new TransformBuilder(new ServiceCollection().BuildServiceProvider(), new[] { factory1, factory2, factory3 }, Array.Empty()); var route = new RouteConfig().WithTransform(transform => { transform["2"] = "B"; }); 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, new ClusterConfig()); Assert.Equal(1, factory1.BuildCalls); Assert.Equal(1, factory2.BuildCalls); Assert.Equal(0, factory3.BuildCalls); Assert.Single(transforms.ResponseTrailerTransforms); } [Fact] public void CallsTransformProviders() { var provider1 = new TestTransformProvider(); var provider2 = new TestTransformProvider(); var provider3 = new TestTransformProvider(); var builder = new TransformBuilder(new ServiceCollection().BuildServiceProvider(), Array.Empty(), new[] { provider1, provider2, provider3 }); var route = new RouteConfig(); var errors = builder.ValidateRoute(route); Assert.Empty(errors); Assert.Equal(1, provider1.ValidateRouteCalls); Assert.Equal(1, provider2.ValidateRouteCalls); Assert.Equal(1, provider3.ValidateRouteCalls); var cluster = new ClusterConfig(); 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); Assert.Equal(3, transforms.ResponseTrailerTransforms.Length); } [Fact] public void DefaultsCanBeDisabled() { var transformBuilder = CreateTransformBuilder(); var transforms = new[] { new Dictionary(StringComparer.OrdinalIgnoreCase) { { "RequestHeadersCopy", "false" } }, new Dictionary(StringComparer.OrdinalIgnoreCase) { { "X-Forwarded", "Off" } }, }; var route = new RouteConfig() { Transforms = transforms }; var errors = transformBuilder.ValidateRoute(route); Assert.Empty(errors); var results = transformBuilder.BuildInternal(route, new ClusterConfig()); Assert.NotNull(results); Assert.False(results.ShouldCopyRequestHeaders); Assert.Single(results.RequestTransforms); Assert.Empty(results.ResponseTransforms); Assert.Empty(results.ResponseTrailerTransforms); } [Theory] [InlineData(null, null, false)] [InlineData(null, true, false)] [InlineData(null, false, false)] [InlineData(true, null, false)] [InlineData(false, null, false)] [InlineData(true, true, false)] [InlineData(true, false, false)] [InlineData(false, true, false)] [InlineData(false, false, false)] [InlineData(null, null, true)] [InlineData(null, true, true)] [InlineData(null, false, true)] [InlineData(true, null, true)] [InlineData(false, null, true)] [InlineData(true, true, true)] [InlineData(true, false, true)] [InlineData(false, true, true)] [InlineData(false, false, true)] public async Task UseOriginalHost(bool? useOriginalHost, bool? copyHeaders, bool hasDestinationHost) { var transformBuilder = CreateTransformBuilder(); var transforms = new List>(); // Disable default forwarders transforms.Add(new Dictionary(StringComparer.OrdinalIgnoreCase) { { "X-Forwarded", "Off" } }); if (useOriginalHost.HasValue) { transforms.Add(new Dictionary(StringComparer.OrdinalIgnoreCase) { { "RequestHeaderOriginalHost", useOriginalHost.ToString() } }); } if (copyHeaders.HasValue) { transforms.Add(new Dictionary(StringComparer.OrdinalIgnoreCase) { { "RequestHeadersCopy", copyHeaders.ToString() } }); } var route = new RouteConfig() { Transforms = transforms }; var errors = transformBuilder.ValidateRoute(route); Assert.Empty(errors); var destinationHost = hasDestinationHost ? "d1-host" : null; var clusterConfig = new ClusterConfig { ClusterId = "cluster1", Destinations = new Dictionary { ["d1"] = new DestinationConfig { Address = "https://localhost", Host = destinationHost } } }; var results = transformBuilder.BuildInternal(route, clusterConfig); Assert.NotNull(results); Assert.Equal(copyHeaders, results.ShouldCopyRequestHeaders); Assert.Empty(results.ResponseTransforms); Assert.Empty(results.ResponseTrailerTransforms); var httpContext = new DefaultHttpContext(); httpContext.Features.Set(new ReverseProxyFeature { ProxiedDestination = new DestinationState("d1") { Model = new(clusterConfig.Destinations.Single().Value) } }); httpContext.Request.Host = new HostString("StartHost"); var proxyRequest = new HttpRequestMessage(); var destinationPrefix = "http://destinationhost:9090/path"; await results.TransformRequestAsync(httpContext, proxyRequest, destinationPrefix, CancellationToken.None); // We expect the host to be flowed as long as it is being explicitly flowed or it wasn't suppressed and headers are being copied. if (useOriginalHost.GetValueOrDefault(false)) { Assert.Equal("StartHost", proxyRequest.Headers.Host); } else if (destinationHost is not null) { // Otherwise, fall back to the destination config host, which will be null if it's not set. Assert.Equal(destinationHost, proxyRequest.Headers.Host); } else { // Otherwise, the host should be null Assert.Null(proxyRequest.Headers.Host); } } [Theory] [InlineData(null, null)] [InlineData(null, true)] [InlineData(null, false)] [InlineData(true, null)] [InlineData(false, null)] [InlineData(true, true)] [InlineData(true, false)] [InlineData(false, true)] [InlineData(false, false)] // https://github.com/dotnet/yarp/issues/859 // Verify that a custom host works no matter what combination of // useOriginalHost and copyHeaders is used. public async Task UseCustomHost(bool? useOriginalHost, bool? copyHeaders) { var transformBuilder = CreateTransformBuilder(); var transforms = new List>(); // Disable default forwarders transforms.Add(new Dictionary(StringComparer.OrdinalIgnoreCase) { { "X-Forwarded", "Off" } }); transforms.Add(new Dictionary(StringComparer.OrdinalIgnoreCase) { { "RequestHeader", "Host" }, { "Set", "CustomHost" } }); if (useOriginalHost.HasValue) { transforms.Add(new Dictionary(StringComparer.OrdinalIgnoreCase) { { "RequestHeaderOriginalHost", useOriginalHost.ToString() } }); } if (copyHeaders.HasValue) { transforms.Add(new Dictionary(StringComparer.OrdinalIgnoreCase) { { "RequestHeadersCopy", copyHeaders.ToString() } }); } var route = new RouteConfig() { Transforms = transforms }; var errors = transformBuilder.ValidateRoute(route); Assert.Empty(errors); var clusterConfig = new ClusterConfig { ClusterId = "cluster1", Destinations = new Dictionary { ["d1"] = new DestinationConfig { Address = "https://localhost", Host = "d1-host" } } }; var results = transformBuilder.BuildInternal(route, clusterConfig); Assert.Equal(copyHeaders, results.ShouldCopyRequestHeaders); var httpContext = new DefaultHttpContext(); httpContext.Features.Set(new ReverseProxyFeature { ProxiedDestination = new DestinationState("d1") { Model = new(clusterConfig.Destinations.Single().Value) } }); httpContext.Request.Host = new HostString("StartHost"); var proxyRequest = new HttpRequestMessage(); var destinationPrefix = "http://destinationhost:9090/path"; await results.TransformRequestAsync(httpContext, proxyRequest, destinationPrefix, CancellationToken.None); Assert.Equal("CustomHost", proxyRequest.Headers.Host); } [Fact] public void DefaultsCanBeOverriddenByForwarded() { var transformBuilder = CreateTransformBuilder(); var transforms = new[] { new Dictionary(StringComparer.OrdinalIgnoreCase) { { "RequestHeadersCopy", "false" } }, new Dictionary(StringComparer.OrdinalIgnoreCase) { { "Forwarded", "proto" } }, }; var route = new RouteConfig() { Transforms = transforms }; var errors = transformBuilder.ValidateRoute(route); Assert.Empty(errors); var results = transformBuilder.BuildInternal(route, new ClusterConfig()); Assert.Equal(6, results.RequestTransforms.Length); Assert.All( results.RequestTransforms.Skip(1).SkipLast(1).Select(t => (dynamic)t), t => { Assert.StartsWith("X-Forwarded-", t.HeaderName); Assert.Equal(ForwardedTransformActions.Remove, t.TransformAction); }); var transform = results.RequestTransforms[0]; var forwardedTransform = Assert.IsType(transform); Assert.True(forwardedTransform.ProtoEnabled); } [Fact] public async Task CallerCallsOverloadsWithoutCT_AllTransformsAreCalled() { var requestTransformsCalled = 0; var responseTransformsCalled = 0; var responseTrailerTransformsCalled = 0; var transformer = CreateTransformBuilder().CreateInternal(context => { context.AddRequestTransform(context => { requestTransformsCalled++; return default; }); context.AddResponseTransform(context => { responseTransformsCalled++; return default; }); context.AddResponseTrailersTransform(context => { responseTrailerTransformsCalled++; return default; }); }); var httpContext = new DefaultHttpContext(); var proxyRequest = new HttpRequestMessage(); var proxyResponse = new HttpResponseMessage(); var destinationPrefix = "http://destinationhost:9090/path"; httpContext.Features.Set(new TestTrailersFeature()); #pragma warning disable CS0618 // We're intentionally testing the obsolete overloads await transformer.TransformRequestAsync(httpContext, proxyRequest, destinationPrefix); await transformer.TransformResponseAsync(httpContext, proxyResponse); await transformer.TransformResponseTrailersAsync(httpContext, proxyResponse); #pragma warning restore CS0618 Assert.Equal(1, requestTransformsCalled); Assert.Equal(1, responseTransformsCalled); Assert.Equal(1, responseTrailerTransformsCalled); } internal static TransformBuilder CreateTransformBuilder() { var serviceCollection = new ServiceCollection(); serviceCollection.AddLogging(); serviceCollection.AddReverseProxy(); using var services = serviceCollection.BuildServiceProvider(); return (TransformBuilder)services.GetRequiredService(); } private class TestTransformFactory : ITransformFactory { private readonly string _v; public int ValidationCalls { get; set; } public int BuildCalls { get; set; } public TestTransformFactory(string v) { _v = v; } public bool Validate(TransformRouteValidationContext context, IReadOnlyDictionary transformValues) { Assert.NotNull(context.Services); Assert.NotNull(context.Route); Assert.NotNull(context.Errors); ValidationCalls++; return transformValues.TryGetValue(_v, out var _); } public bool Build(TransformBuilderContext context, IReadOnlyDictionary transformValues) { Assert.NotNull(context.Services); Assert.NotNull(context.Route); BuildCalls++; if (transformValues.TryGetValue(_v, out var _)) { context.AddResponseTrailersTransform(context => default); return true; } return false; } } private class TestTransformProvider : ITransformProvider { public int ValidateRouteCalls { get; set; } public int ValidateClusterCalls { get; set; } public int ApplyCalls { get; set; } public void ValidateRoute(TransformRouteValidationContext context) { Assert.NotNull(context.Services); Assert.NotNull(context.Route); Assert.NotNull(context.Errors); ValidateRouteCalls++; } public void ValidateCluster(TransformClusterValidationContext context) { Assert.NotNull(context.Services); Assert.NotNull(context.Cluster); Assert.NotNull(context.Errors); ValidateClusterCalls++; } public void Apply(TransformBuilderContext context) { Assert.NotNull(context.Services); Assert.NotNull(context.Route); Assert.NotNull(context.Cluster); ApplyCalls++; context.AddResponseTrailer("key", "value"); } } } ================================================ FILE: test/ReverseProxy.Tests/Transforms/DestinationPrefixTransformTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Threading.Tasks; using Xunit; namespace Yarp.ReverseProxy.Transforms.Tests; public class DestinationPrefixTransformTests { [Fact] public async Task UpdateDestinationPrefix() { const string newDestinationPrefix = "http://localhost:8080"; var context = new RequestTransformContext() { DestinationPrefix = "http://contoso.com:5000" }; var transform = new DestinationPrefixTransform(newDestinationPrefix); await transform.ApplyAsync(context); } private class DestinationPrefixTransform(string newDestinationPrefix) : RequestTransform { public override ValueTask ApplyAsync(RequestTransformContext context) { context.DestinationPrefix = newDestinationPrefix; return ValueTask.CompletedTask; } } } ================================================ FILE: test/ReverseProxy.Tests/Transforms/ForwardedTransformExtensionsTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Linq; using Microsoft.Extensions.DependencyInjection; using Xunit; using Yarp.Tests.Common; using Yarp.ReverseProxy.Configuration; using Yarp.ReverseProxy.Transforms.Builder; using Yarp.ReverseProxy.Utilities; namespace Yarp.ReverseProxy.Transforms.Tests; public class ForwardedTransformExtensionsTests : TransformExtensionsTestsBase { private readonly ForwardedTransformFactory _factory = new ForwardedTransformFactory(new TestRandomFactory()); [Theory] [InlineData(ForwardedTransformActions.Set, null, null, null, null)] [InlineData(ForwardedTransformActions.Append, ForwardedTransformActions.Set, null, null, null)] [InlineData(ForwardedTransformActions.Append, null, ForwardedTransformActions.Set, null, null)] [InlineData(ForwardedTransformActions.Append, null, null, ForwardedTransformActions.Set, null)] [InlineData(ForwardedTransformActions.Append, null, null, null, ForwardedTransformActions.Set)] [InlineData(ForwardedTransformActions.Append, ForwardedTransformActions.Off, null, null, null)] [InlineData(ForwardedTransformActions.Append, null, ForwardedTransformActions.Off, null, null)] [InlineData(ForwardedTransformActions.Append, null, null, ForwardedTransformActions.Off, null)] [InlineData(ForwardedTransformActions.Append, null, null, null, ForwardedTransformActions.Off)] [InlineData(ForwardedTransformActions.Set, ForwardedTransformActions.Append, ForwardedTransformActions.Remove, ForwardedTransformActions.Off, ForwardedTransformActions.Remove)] public void WithTransformXForwarded( ForwardedTransformActions xDefault, ForwardedTransformActions? xFor, ForwardedTransformActions? xHost, ForwardedTransformActions? xProto, ForwardedTransformActions? xPrefix) { var routeConfig = new RouteConfig(); var prefix = "prefix-"; routeConfig = routeConfig.WithTransformXForwarded(prefix, xDefault, xFor, xHost, xProto, xPrefix); var builderContext = ValidateAndBuild(routeConfig, _factory); if (xFor != ForwardedTransformActions.Off) { ValidateXForwardedTransform("For", prefix, xFor ?? xDefault, builderContext.RequestTransforms.OfType().Single()); } else { Assert.Empty(builderContext.RequestTransforms.OfType()); } if (xHost != ForwardedTransformActions.Off) { ValidateXForwardedTransform("Host", prefix, xHost ?? xDefault, builderContext.RequestTransforms.OfType().Single()); } else { Assert.Empty(builderContext.RequestTransforms.OfType()); } if (xProto != ForwardedTransformActions.Off) { ValidateXForwardedTransform("Proto", prefix, xProto ?? xDefault, builderContext.RequestTransforms.OfType().Single()); } else { Assert.Empty(builderContext.RequestTransforms.OfType()); } if (xPrefix != ForwardedTransformActions.Off) { ValidateXForwardedTransform("Prefix", prefix, xPrefix ?? xDefault, builderContext.RequestTransforms.OfType().Single()); } else { Assert.Empty(builderContext.RequestTransforms.OfType()); } } [Theory] [MemberData(nameof(GetAddXForwardedCases))] public void AddXForwarded(Func addFunc, string transformName, ForwardedTransformActions action) { var builderContext = CreateBuilderContext(); addFunc(builderContext, "prefix-" + transformName, action); ValidateXForwarded(builderContext, transformName, "prefix-", action); } public static IEnumerable GetAddXForwardedCases() { var actions = (ForwardedTransformActions[])Enum.GetValues(typeof(ForwardedTransformActions)); var addTransformFuncs = new (Func, string)[] { (ForwardedTransformExtensions.AddXForwardedFor, "For"), (ForwardedTransformExtensions.AddXForwardedPrefix, "Prefix"), (ForwardedTransformExtensions.AddXForwardedHost, "Host"), (ForwardedTransformExtensions.AddXForwardedProto, "Proto") }; return addTransformFuncs.Join(actions, _ => true, _ => true, (t, a) => new object[] { t.Item1, t.Item2, a }); } private static void ValidateXForwarded(TransformBuilderContext builderContext, string transformName, string headerPrefix, ForwardedTransformActions action) { Assert.False(builderContext.UseDefaultForwarders); if (action == ForwardedTransformActions.Off) { Assert.Empty(builderContext.RequestTransforms); } else { var transform = Assert.Single(builderContext.RequestTransforms); ValidateXForwardedTransform(transformName, headerPrefix, action, transform); } } private static void ValidateXForwardedTransform(string transformName, string headerPrefix, ForwardedTransformActions action, RequestTransform transform) { Assert.Equal($"RequestHeaderXForwarded{transformName}Transform", transform.GetType().Name); Assert.Equal(headerPrefix + transformName, ((dynamic)transform).HeaderName); Assert.Equal(action, ((dynamic)transform).TransformAction); } [Theory] [InlineData(NodeFormat.Random, true, true, NodeFormat.Random, ForwardedTransformActions.Append)] [InlineData(NodeFormat.RandomAndPort, true, true, NodeFormat.Random, ForwardedTransformActions.Set)] [InlineData(NodeFormat.None, false, false, NodeFormat.None, ForwardedTransformActions.Append)] [InlineData(NodeFormat.None, false, false, NodeFormat.None, ForwardedTransformActions.Set)] [InlineData(NodeFormat.None, false, true, NodeFormat.Random, ForwardedTransformActions.Append)] [InlineData(NodeFormat.None, false, true, NodeFormat.Random, ForwardedTransformActions.Set)] [InlineData(NodeFormat.None, false, true, NodeFormat.None, ForwardedTransformActions.Set)] [InlineData(NodeFormat.None, false, true, NodeFormat.RandomAndPort, ForwardedTransformActions.Set)] [InlineData(NodeFormat.None, false, true, NodeFormat.Unknown, ForwardedTransformActions.Set)] [InlineData(NodeFormat.None, false, true, NodeFormat.UnknownAndPort, ForwardedTransformActions.Set)] [InlineData(NodeFormat.None, false, true, NodeFormat.Ip, ForwardedTransformActions.Set)] [InlineData(NodeFormat.None, false, true, NodeFormat.IpAndPort, ForwardedTransformActions.Set)] public void WithTransformForwarded(NodeFormat forFormat, bool useHost, bool useProto, NodeFormat byFormat, ForwardedTransformActions action) { var routeConfig = new RouteConfig(); routeConfig = routeConfig.WithTransformForwarded(useHost, useProto, forFormat, byFormat, action); var builderContext = ValidateAndBuild(routeConfig, _factory, CreateServices()); ValidateForwarded(builderContext, useHost, useProto, forFormat, byFormat, action, true); } [Theory] [InlineData(NodeFormat.Random, true, true, NodeFormat.Random, ForwardedTransformActions.Append, true)] [InlineData(NodeFormat.Random, true, true, NodeFormat.Random, ForwardedTransformActions.Append, false)] [InlineData(NodeFormat.RandomAndPort, true, true, NodeFormat.Random, ForwardedTransformActions.Set, true)] [InlineData(NodeFormat.RandomAndPort, true, true, NodeFormat.Random, ForwardedTransformActions.Set, false)] [InlineData(NodeFormat.None, false, false, NodeFormat.None, ForwardedTransformActions.Append, true)] [InlineData(NodeFormat.None, false, false, NodeFormat.None, ForwardedTransformActions.Append, false)] [InlineData(NodeFormat.None, false, false, NodeFormat.None, ForwardedTransformActions.Set, true)] [InlineData(NodeFormat.None, false, false, NodeFormat.None, ForwardedTransformActions.Set, false)] [InlineData(NodeFormat.None, false, true, NodeFormat.Random, ForwardedTransformActions.Append, true)] [InlineData(NodeFormat.None, false, true, NodeFormat.Random, ForwardedTransformActions.Append, false)] [InlineData(NodeFormat.None, false, true, NodeFormat.Random, ForwardedTransformActions.Set, true)] [InlineData(NodeFormat.None, false, true, NodeFormat.Random, ForwardedTransformActions.Set, false)] [InlineData(NodeFormat.None, false, true, NodeFormat.None, ForwardedTransformActions.Set, true)] [InlineData(NodeFormat.None, false, true, NodeFormat.None, ForwardedTransformActions.Set, false)] [InlineData(NodeFormat.None, false, true, NodeFormat.RandomAndPort, ForwardedTransformActions.Set, true)] [InlineData(NodeFormat.None, false, true, NodeFormat.RandomAndPort, ForwardedTransformActions.Set, false)] [InlineData(NodeFormat.None, false, true, NodeFormat.Unknown, ForwardedTransformActions.Set, true)] [InlineData(NodeFormat.None, false, true, NodeFormat.Unknown, ForwardedTransformActions.Set, false)] [InlineData(NodeFormat.None, false, true, NodeFormat.UnknownAndPort, ForwardedTransformActions.Set, true)] [InlineData(NodeFormat.None, false, true, NodeFormat.UnknownAndPort, ForwardedTransformActions.Set, false)] [InlineData(NodeFormat.None, false, true, NodeFormat.Ip, ForwardedTransformActions.Set, true)] [InlineData(NodeFormat.None, false, true, NodeFormat.Ip, ForwardedTransformActions.Set, false)] [InlineData(NodeFormat.None, false, true, NodeFormat.IpAndPort, ForwardedTransformActions.Set, true)] [InlineData(NodeFormat.None, false, true, NodeFormat.IpAndPort, ForwardedTransformActions.Set, false)] public void AddForwarded(NodeFormat forFormat, bool useHost, bool useProto, NodeFormat byFormat, ForwardedTransformActions action, bool removeAllXForwardedHeaders) { var builderContext = CreateBuilderContext(services: CreateServices()); builderContext.AddForwarded(useHost, useProto, forFormat, byFormat, action, removeAllXForwardedHeaders); ValidateForwarded(builderContext, useHost, useProto, forFormat, byFormat, action, removeAllXForwardedHeaders); } private static void ValidateForwarded(TransformBuilderContext builderContext, bool useHost, bool useProto, NodeFormat forFormat, NodeFormat byFormat, ForwardedTransformActions action, bool removeAllXForwardedHeaders) { Assert.False(builderContext.UseDefaultForwarders); if (byFormat != NodeFormat.None || forFormat != NodeFormat.None || useHost || useProto) { if (removeAllXForwardedHeaders) { Assert.Equal(5, builderContext.RequestTransforms.Count); Assert.All( builderContext.RequestTransforms.Skip(1).Select(t => (dynamic)t), t => { Assert.StartsWith("X-Forwarded-", t.HeaderName); Assert.Equal(ForwardedTransformActions.Remove, t.TransformAction); }); } else { Assert.Equal(1, builderContext.RequestTransforms.Count); var xForwardedTransforms = builderContext.RequestTransforms.Skip(1).Cast().Where(requestTransform => requestTransform.HeaderName.ToLowerInvariant().StartsWith("x-forwarded")).ToList(); Assert.Empty(xForwardedTransforms); } var transform = builderContext.RequestTransforms[0]; var requestHeaderForwardedTransform = Assert.IsType(transform); Assert.Equal(action, requestHeaderForwardedTransform.TransformAction); Assert.Equal(useHost, requestHeaderForwardedTransform.HostEnabled); Assert.Equal(useProto, requestHeaderForwardedTransform.ProtoEnabled); Assert.Equal(byFormat, requestHeaderForwardedTransform.ByFormat); Assert.Equal(forFormat, requestHeaderForwardedTransform.ForFormat); } else { Assert.Empty(builderContext.RequestTransforms); } } [Fact] public void WithTransformClientCertHeader() { var routeConfig = new RouteConfig(); routeConfig = routeConfig.WithTransformClientCertHeader("name"); var builderContext = ValidateAndBuild(routeConfig, _factory); var transform = Assert.Single(builderContext.RequestTransforms); var certTransform = Assert.IsType(transform); Assert.Equal("name", certTransform.HeaderName); } [Fact] public void AddClientCertHeader() { var builderContext = CreateBuilderContext(); builderContext.AddClientCertHeader("name"); var transform = Assert.Single(builderContext.RequestTransforms); var certTransform = Assert.IsType(transform); Assert.Equal("name", certTransform.HeaderName); } private static IServiceProvider CreateServices() { var collection = new ServiceCollection(); collection.AddSingleton(); return collection.BuildServiceProvider(); } } ================================================ FILE: test/ReverseProxy.Tests/Transforms/HttpMethodChangeTransformTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Net.Http; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Xunit; namespace Yarp.ReverseProxy.Transforms.Tests; public class HttpMethodChangeTransformTests { [Theory] [InlineData("PUT", "POST", "PUT", "POST")] [InlineData("PUT", "POST", "POST", "POST")] [InlineData("PUT", "POST", "GET", "GET")] public async Task HttpMethodChange_Works(string fromMethod, string toMethod, string requestMethod, string expected) { var httpContext = new DefaultHttpContext(); var request = new HttpRequestMessage() { Method = new HttpMethod(requestMethod) }; var context = new RequestTransformContext() { HttpContext = httpContext, ProxyRequest = request, }; var transform = new HttpMethodChangeTransform(fromMethod, toMethod); await transform.ApplyAsync(context); Assert.Equal(expected, request.Method.Method); } } ================================================ FILE: test/ReverseProxy.Tests/Transforms/HttpMethodTransformExtensionsTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Net.Http; using Microsoft.AspNetCore.Http; using Xunit; using Yarp.ReverseProxy.Configuration; using Yarp.ReverseProxy.Transforms.Builder; namespace Yarp.ReverseProxy.Transforms.Tests; public class HttpMethodTransformExtensionsTests : TransformExtensionsTestsBase { private readonly HttpMethodTransformFactory _factory = new(); [Fact] public void WithTransformHttpMethodChange() { var routeConfig = new RouteConfig(); routeConfig = routeConfig.WithTransformHttpMethodChange(HttpMethods.Put, HttpMethods.Post); var builderContext = ValidateAndBuild(routeConfig, _factory); ValidateHttpMethod(builderContext); } [Fact] public void AddHttpMethodChange() { var builderContext = CreateBuilderContext(); builderContext.AddHttpMethodChange(HttpMethods.Put, HttpMethods.Post); ValidateHttpMethod(builderContext); } private static void ValidateHttpMethod(TransformBuilderContext builderContext) { var requestTransform = Assert.Single(builderContext.RequestTransforms); var httpMethodTransform = Assert.IsType(requestTransform); Assert.Equal(HttpMethod.Put, httpMethodTransform.FromMethod); Assert.Equal(HttpMethod.Post, httpMethodTransform.ToMethod); } } ================================================ FILE: test/ReverseProxy.Tests/Transforms/PathRouteValuesTransformTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Template; using Microsoft.Extensions.DependencyInjection; using Xunit; namespace Yarp.ReverseProxy.Transforms.Tests; public class PathRouteValuesTransformTests { [Theory] [InlineData("/{a}/{b}/{c}", "/6/7/8")] [InlineData("/{a}/foo/{b}/{c}/{d}", "/6/foo/7/8")] // Unknown value (d) dropped [InlineData("/{a}/foo/{b}", "/6/foo/7")] // Extra values (c) dropped public async Task ReplacesPatternWithRouteValues(string transformValue, string expected) { var serviceCollection = new ServiceCollection(); serviceCollection.AddOptions(); serviceCollection.AddRouting(); using var services = serviceCollection.BuildServiceProvider(); var routeValues = new Dictionary { { "a", "6" }, { "b", "7" }, { "c", "8" }, }; var httpContext = new DefaultHttpContext(); httpContext.Request.RouteValues = new RouteValueDictionary(routeValues); var context = new RequestTransformContext() { Path = "/", HttpContext = httpContext }; var transform = new PathRouteValuesTransform(transformValue, services.GetRequiredService()); await transform.ApplyAsync(context); Assert.Equal(expected, context.Path); // The transform should not modify the original request's route values Assert.Equal(routeValues, httpContext.Request.RouteValues); } [Fact] public async Task RouteValuesWithSlashesNotEncoded() { var serviceCollection = new ServiceCollection(); serviceCollection.AddOptions(); serviceCollection.AddRouting(); using var services = serviceCollection.BuildServiceProvider(); var routeValues = new Dictionary { { "a", "abc" }, { "b", "def" }, { "remainder", "klm/nop/qrs" }, }; var httpContext = new DefaultHttpContext(); httpContext.Request.RouteValues = new RouteValueDictionary(routeValues); var context = new RequestTransformContext() { Path = "/", HttpContext = httpContext }; var transform = new PathRouteValuesTransform("/{a}/{b}/{**remainder}", services.GetRequiredService()); await transform.ApplyAsync(context); Assert.Equal("/abc/def/klm/nop/qrs", context.Path.Value); // The transform should not modify the original request's route values Assert.Equal(routeValues, httpContext.Request.RouteValues); } } ================================================ FILE: test/ReverseProxy.Tests/Transforms/PathStringTransformTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Threading.Tasks; using Xunit; namespace Yarp.ReverseProxy.Transforms.Tests; public class PathStringTransformTests { [Theory] [InlineData("/foo", "Set", "/value", "/value")] [InlineData("/foo", "Set", "", "")] [InlineData("/foo", "Prefix", "/value", "/value/foo")] [InlineData("/value/foo", "RemovePrefix", "/value", "/foo")] public async Task Set_Path_Success(string initialValue, string modeString, string transformValue, string expected) { // We can't put an internal type in a public test API parameter. var mode = Enum.Parse(modeString); var context = new RequestTransformContext() { Path = initialValue }; var transform = new PathStringTransform(mode, transformValue); await transform.ApplyAsync(context); Assert.Equal(expected, context.Path); } } ================================================ FILE: test/ReverseProxy.Tests/Transforms/PathTransformExtensionsTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing.Template; using Microsoft.Extensions.DependencyInjection; using Xunit; using Yarp.ReverseProxy.Configuration; using Yarp.ReverseProxy.Transforms.Builder; namespace Yarp.ReverseProxy.Transforms.Tests; public class PathTransformExtensionsTests : TransformExtensionsTestsBase { private readonly PathTransformFactory _factory; public PathTransformExtensionsTests() { var serviceCollection = new ServiceCollection(); serviceCollection.AddOptions(); serviceCollection.AddRouting(); var services = serviceCollection.BuildServiceProvider(); _factory = new PathTransformFactory(services.GetRequiredService()); } [Fact] public void WithTransformPathSet() { var routeConfig = new RouteConfig(); routeConfig = routeConfig.WithTransformPathSet(new PathString("/path#")); var builderContext = ValidateAndBuild(routeConfig, _factory); ValidatePathSet(builderContext); } [Fact] public void AddPathSet() { var builderContext = CreateBuilderContext(); builderContext.AddPathSet(new PathString("/path#")); ValidatePathSet(builderContext); } private static void ValidatePathSet(TransformBuilderContext builderContext) { var requestTransform = Assert.Single(builderContext.RequestTransforms); var pathStringTransform = Assert.IsType(requestTransform); Assert.Equal(PathStringTransform.PathTransformMode.Set, pathStringTransform.Mode); Assert.Equal("/path#", pathStringTransform.Value.Value); } [Fact] public void WithTransformPathRemovePrefix() { var routeConfig = new RouteConfig(); routeConfig = routeConfig.WithTransformPathRemovePrefix(new PathString("/path#")); var builderContext = ValidateAndBuild(routeConfig, _factory); ValidatePathRemovePrefix(builderContext); } [Fact] public void AddPathRemovePrefix() { var builderContext = CreateBuilderContext(); builderContext.AddPathRemovePrefix(new PathString("/path#")); ValidatePathRemovePrefix(builderContext); } private static void ValidatePathRemovePrefix(TransformBuilderContext builderContext) { var requestTransform = Assert.Single(builderContext.RequestTransforms); var pathStringTransform = Assert.IsType(requestTransform); Assert.Equal(PathStringTransform.PathTransformMode.RemovePrefix, pathStringTransform.Mode); Assert.Equal("/path#", pathStringTransform.Value.Value); } [Fact] public void WithTransformPathPrefix() { var routeConfig = new RouteConfig(); routeConfig = routeConfig.WithTransformPathPrefix(new PathString("/path#")); var builderContext = ValidateAndBuild(routeConfig, _factory); ValidatePathPrefix(builderContext); } [Fact] public void AddPathPrefix() { var builderContext = CreateBuilderContext(); builderContext.AddPathPrefix(new PathString("/path#")); ValidatePathPrefix(builderContext); } private static void ValidatePathPrefix(TransformBuilderContext builderContext) { var requestTransform = Assert.Single(builderContext.RequestTransforms); var pathStringTransform = Assert.IsType(requestTransform); Assert.Equal(PathStringTransform.PathTransformMode.Prefix, pathStringTransform.Mode); Assert.Equal("/path#", pathStringTransform.Value.Value); } [Fact] public void WithTransformPathRouteValues() { var routeConfig = new RouteConfig(); routeConfig = routeConfig.WithTransformPathRouteValues(new PathString("/path#")); var builderContext = ValidateAndBuild(routeConfig, _factory); ValidatePathRouteValues(builderContext); } [Fact] public void AddPathRouteValues() { var serviceCollection = new ServiceCollection(); serviceCollection.AddOptions(); serviceCollection.AddRouting(); var services = serviceCollection.BuildServiceProvider(); var builderContext = CreateBuilderContext(services: services); builderContext.AddPathRouteValues(new PathString("/path#")); ValidatePathRouteValues(builderContext); } private static void ValidatePathRouteValues(TransformBuilderContext builderContext) { var requestTransform = Assert.Single(builderContext.RequestTransforms); var pathRouteValuesTransform = Assert.IsType(requestTransform); Assert.Equal("/path#", pathRouteValuesTransform.Pattern.RawText); } } ================================================ FILE: test/ReverseProxy.Tests/Transforms/QueryParameterFromRouteTransformTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Template; using Xunit; namespace Yarp.ReverseProxy.Transforms.Tests; public class QueryParameterFromRouteTransformTests { [Theory] [InlineData("/{a}/{b}/{c}", "a", "?z=6")] [InlineData("/{a}/{b}/{c}", "c", "?z=8")] [InlineData("/{a}/{*remainder}", "remainder", "?z=7%2F8")] public async Task Append_AddsQueryParameterWithRouteValue(string pattern, string routeValueKey, string expected) { const string path = "/6/7/8"; var routeValues = new RouteValueDictionary(); var templateMatcher = new TemplateMatcher(TemplateParser.Parse(pattern), new RouteValueDictionary()); templateMatcher.TryMatch(path, routeValues); var httpContext = new DefaultHttpContext(); httpContext.Request.RouteValues = routeValues; var context = new RequestTransformContext() { Path = path, Query = new QueryTransformContext(httpContext.Request), HttpContext = httpContext }; var transform = new QueryParameterRouteTransform(QueryStringTransformMode.Append, "z", routeValueKey); await transform.ApplyAsync(context); Assert.Equal(expected, context.Query.QueryString.Value); } [Fact] public void Append_IgnoresExistingQueryParameter() { const string path = "/6/7/8"; var routeValues = new RouteValueDictionary(); var templateMatcher = new TemplateMatcher(TemplateParser.Parse("/{a}/{b}/{c}"), new RouteValueDictionary()); templateMatcher.TryMatch(path, routeValues); var httpContext = new DefaultHttpContext(); httpContext.Request.RouteValues = routeValues; httpContext.Request.QueryString = new QueryString("?z=1"); var context = new RequestTransformContext() { Path = path, Query = new QueryTransformContext(httpContext.Request), HttpContext = httpContext }; var transform = new QueryParameterRouteTransform(QueryStringTransformMode.Append, "z", "a"); transform.ApplyAsync(context); Assert.Equal("?z=1&z=6", context.Query.QueryString.Value); } [Fact] public void Set_OverwritesExistingQueryParameter() { const string path = "/6/7/8"; var routeValues = new RouteValueDictionary(); var templateMatcher = new TemplateMatcher(TemplateParser.Parse("/{a}/{b}/{c}"), new RouteValueDictionary()); templateMatcher.TryMatch(path, routeValues); var httpContext = new DefaultHttpContext(); httpContext.Request.RouteValues = routeValues; httpContext.Request.QueryString = new QueryString("?z=1"); var context = new RequestTransformContext() { Path = path, Query = new QueryTransformContext(httpContext.Request), HttpContext = httpContext }; var transform = new QueryParameterRouteTransform(QueryStringTransformMode.Set, "z", "a"); transform.ApplyAsync(context); Assert.Equal("?z=6", context.Query.QueryString.Value); } [Fact] public void Set_AddsNewQueryParameter() { const string path = "/6/7/8"; var routeValues = new RouteValueDictionary(); var templateMatcher = new TemplateMatcher(TemplateParser.Parse("/{a}/{b}/{c}"), new RouteValueDictionary()); templateMatcher.TryMatch(path, routeValues); var httpContext = new DefaultHttpContext(); httpContext.Request.RouteValues = routeValues; var context = new RequestTransformContext() { Path = path, Query = new QueryTransformContext(httpContext.Request), HttpContext = httpContext }; var transform = new QueryParameterRouteTransform(QueryStringTransformMode.Set, "z", "a"); transform.ApplyAsync(context); Assert.Equal("?z=6", context.Query.QueryString.Value); } } ================================================ FILE: test/ReverseProxy.Tests/Transforms/QueryParameterFromStaticTransformTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Xunit; namespace Yarp.ReverseProxy.Transforms.Tests; public class QueryParameterFromStaticTransformTests { [Fact] public async Task Append_AddsQueryStringParameterWithStaticValue() { var httpContext = new DefaultHttpContext(); var context = new RequestTransformContext() { Query = new QueryTransformContext(httpContext.Request), HttpContext = httpContext }; var transform = new QueryParameterFromStaticTransform(QueryStringTransformMode.Append, "z", "foo"); await transform.ApplyAsync(context); Assert.Equal("?z=foo", context.Query.QueryString.Value); } [Fact] public async Task Append_IgnoresExistingQueryStringParameter() { var httpContext = new DefaultHttpContext(); httpContext.Request.QueryString = new QueryString("?z=1"); var context = new RequestTransformContext() { Query = new QueryTransformContext(httpContext.Request), HttpContext = httpContext }; var transform = new QueryParameterFromStaticTransform(QueryStringTransformMode.Append, "z", "foo"); await transform.ApplyAsync(context); Assert.Equal("?z=1&z=foo", context.Query.QueryString.Value); } [Fact] public async Task Set_OverwritesExistingQueryStringParameter() { var httpContext = new DefaultHttpContext(); httpContext.Request.QueryString = new QueryString("?z=1"); var context = new RequestTransformContext() { Query = new QueryTransformContext(httpContext.Request), HttpContext = httpContext }; var transform = new QueryParameterFromStaticTransform(QueryStringTransformMode.Set, "z", "foo"); await transform.ApplyAsync(context); Assert.Equal("?z=foo", context.Query.QueryString.Value); } [Fact] public async Task Set_AddsNewQueryStringParameter() { var httpContext = new DefaultHttpContext(); var context = new RequestTransformContext() { Query = new QueryTransformContext(httpContext.Request), HttpContext = httpContext }; var transform = new QueryParameterFromStaticTransform(QueryStringTransformMode.Set, "z", "foo"); await transform.ApplyAsync(context); Assert.Equal("?z=foo", context.Query.QueryString.Value); } [Fact] public async Task Set_AddsNewEmptyQueryStringParameter() { var httpContext = new DefaultHttpContext(); var context = new RequestTransformContext() { Query = new QueryTransformContext(httpContext.Request), HttpContext = httpContext }; var transform = new QueryParameterFromStaticTransform(QueryStringTransformMode.Set, "z", ""); await transform.ApplyAsync(context); Assert.Equal("?z=", context.Query.QueryString.Value); } [Fact] public async Task Set_OverwritesExistingParamWithEmptyValue() { var httpContext = new DefaultHttpContext(); httpContext.Request.QueryString = new QueryString("?z=foo"); var context = new RequestTransformContext() { Query = new QueryTransformContext(httpContext.Request), HttpContext = httpContext }; var transform = new QueryParameterFromStaticTransform(QueryStringTransformMode.Set, "z", ""); await transform.ApplyAsync(context); Assert.Equal("?z=", context.Query.QueryString.Value); } [Fact] public async Task Set_AddNewEmptyParamToExistingQueryStringParameter() { var httpContext = new DefaultHttpContext(); httpContext.Request.QueryString = new QueryString("?x=1"); var context = new RequestTransformContext() { Query = new QueryTransformContext(httpContext.Request), HttpContext = httpContext }; var transform = new QueryParameterFromStaticTransform(QueryStringTransformMode.Set, "z", ""); await transform.ApplyAsync(context); Assert.Equal("?x=1&z=", context.Query.QueryString.Value); } [Fact] public async Task Append_AddsNewEmptyQueryStringParameter() { var httpContext = new DefaultHttpContext(); var context = new RequestTransformContext() { Query = new QueryTransformContext(httpContext.Request), HttpContext = httpContext }; var transform = new QueryParameterFromStaticTransform(QueryStringTransformMode.Append, "z", ""); await transform.ApplyAsync(context); Assert.Equal("?z=", context.Query.QueryString.Value); } [Fact] public async Task Append_AppendsEmptyValueToExistingParam() { var httpContext = new DefaultHttpContext(); httpContext.Request.QueryString = new QueryString("?z=foo"); var context = new RequestTransformContext() { Query = new QueryTransformContext(httpContext.Request), HttpContext = httpContext }; var transform = new QueryParameterFromStaticTransform(QueryStringTransformMode.Append, "z", ""); await transform.ApplyAsync(context); Assert.Equal("?z=foo&z=", context.Query.QueryString.Value); } } ================================================ FILE: test/ReverseProxy.Tests/Transforms/QueryParameterRemoveTransformTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Xunit; namespace Yarp.ReverseProxy.Transforms.Tests; public class QueryParameterRemoveTransformTests { [Fact] public async Task RemovesExistingQueryParameter() { var httpContext = new DefaultHttpContext(); httpContext.Request.QueryString = new QueryString("?z=1"); var context = new RequestTransformContext() { Query = new QueryTransformContext(httpContext.Request) }; var transform = new QueryParameterRemoveTransform("z"); await transform.ApplyAsync(context); Assert.False(context.Query.QueryString.HasValue); } [Fact] public async Task LeavesOtherQueryParameters() { var httpContext = new DefaultHttpContext(); httpContext.Request.QueryString = new QueryString("?z=1&a=2"); var context = new RequestTransformContext() { Query = new QueryTransformContext(httpContext.Request), }; var transform = new QueryParameterRemoveTransform("z"); await transform.ApplyAsync(context); Assert.Equal("?a=2", context.Query.QueryString.Value); } [Fact] public async Task DoesNotFailOnNonExistingQueryParameter() { var httpContext = new DefaultHttpContext(); httpContext.Request.QueryString = new QueryString("?z=1"); var context = new RequestTransformContext() { Query = new QueryTransformContext(httpContext.Request), }; var transform = new QueryParameterRemoveTransform("a"); await transform.ApplyAsync(context); Assert.Equal("?z=1", context.Query.QueryString.Value); } } ================================================ FILE: test/ReverseProxy.Tests/Transforms/QueryTransformContextTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Http; using Xunit; namespace Yarp.ReverseProxy.Transforms.Tests; public class QueryTransformContextTests { [Fact] public void Collection_TryGetValue_CaseInsensitive() { var httpContext = new DefaultHttpContext { Request = { QueryString = new QueryString("?z=1") } }; var queryTransformContext = new QueryTransformContext(httpContext.Request); queryTransformContext.Collection.TryGetValue("Z", out var result); Assert.Equal("1", result); } [Fact] public void Collection_RemoveKey_CaseInsensitive() { var httpContext = new DefaultHttpContext { Request = { QueryString = new QueryString("?z=1") } }; var queryTransformContext = new QueryTransformContext(httpContext.Request); queryTransformContext.Collection.Remove("Z"); Assert.False(queryTransformContext.QueryString.HasValue); } } ================================================ FILE: test/ReverseProxy.Tests/Transforms/QueryTransformExtensionsTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Xunit; using Yarp.ReverseProxy.Configuration; using Yarp.ReverseProxy.Transforms.Builder; namespace Yarp.ReverseProxy.Transforms.Tests; public class QueryTransformExtensionsTests : TransformExtensionsTestsBase { private readonly QueryTransformFactory _factory = new(); [Theory] [InlineData(false)] [InlineData(true)] public void WithTransformQueryRouteValue(bool append) { var routeConfig = new RouteConfig(); routeConfig = routeConfig.WithTransformQueryRouteValue("key", "value", append); var builderContext = ValidateAndBuild(routeConfig, _factory); ValidateQueryRouteParameter(append, builderContext); } [Theory] [InlineData(false)] [InlineData(true)] public void AddQueryRouteValue(bool append) { var builderContext = CreateBuilderContext(); builderContext.AddQueryRouteValue("key", "value", append); ValidateQueryRouteParameter(append, builderContext); } private static void ValidateQueryRouteParameter(bool append, TransformBuilderContext builderContext) { var requestTransform = Assert.Single(builderContext.RequestTransforms); var queryParameterRouteTransform = Assert.IsType(requestTransform); Assert.Equal("key", queryParameterRouteTransform.Key); Assert.Equal("value", queryParameterRouteTransform.RouteValueKey); var expectedMode = append ? QueryStringTransformMode.Append : QueryStringTransformMode.Set; Assert.Equal(expectedMode, queryParameterRouteTransform.Mode); } [Theory] [InlineData(false)] [InlineData(true)] public void WithTransformQueryValue(bool append) { var routeConfig = new RouteConfig(); routeConfig = routeConfig.WithTransformQueryValue("key", "value", append); var builderContext = ValidateAndBuild(routeConfig, _factory); ValidateQueryValue(append, builderContext); } [Theory] [InlineData(false)] [InlineData(true)] public void AddQueryValue(bool append) { var builderContext = CreateBuilderContext(); builderContext.AddQueryValue("key", "value", append); ValidateQueryValue(append, builderContext); } private static void ValidateQueryValue(bool append, TransformBuilderContext builderContext) { var requestTransform = Assert.Single(builderContext.RequestTransforms); var queryParameterFromStaticTransform = Assert.IsType(requestTransform); Assert.Equal("key", queryParameterFromStaticTransform.Key); Assert.Equal("value", queryParameterFromStaticTransform.Value); var expectedMode = append ? QueryStringTransformMode.Append : QueryStringTransformMode.Set; Assert.Equal(expectedMode, queryParameterFromStaticTransform.Mode); } [Fact] public void WithTransformQueryRemoveKey() { var routeConfig = new RouteConfig(); routeConfig = routeConfig.WithTransformQueryRemoveKey("key"); var builderContext = ValidateAndBuild(routeConfig, _factory); ValidateQueryRemoveKey(builderContext); } [Fact] public void AddQueryRemoveKey() { var builderContext = CreateBuilderContext(); builderContext.AddQueryRemoveKey("key"); ValidateQueryRemoveKey(builderContext); } private static void ValidateQueryRemoveKey(TransformBuilderContext builderContext) { var requestTransform = Assert.Single(builderContext.RequestTransforms); var removeQueryParameterTransform = Assert.IsType(requestTransform); Assert.Equal("key", removeQueryParameterTransform.Key); } } ================================================ FILE: test/ReverseProxy.Tests/Transforms/RequestHeaderClientCertTransformTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.IO; using System.Linq; using System.Net.Http; using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Xunit; namespace Yarp.ReverseProxy.Transforms.Tests; public class RequestHeaderClientCertTransformTests { [Fact] public async Task NoCert_NoOp() { var httpContext = new DefaultHttpContext(); var proxyRequest = new HttpRequestMessage(); var transform = new RequestHeaderClientCertTransform("Name"); await transform.ApplyAsync(new RequestTransformContext() { HttpContext = httpContext, ProxyRequest = proxyRequest }); Assert.Empty(proxyRequest.Headers); } [Fact] public async Task Cert_Encoded() { var httpContext = new DefaultHttpContext(); var proxyRequest = new HttpRequestMessage(); httpContext.Connection.ClientCertificate = Certificates.SelfSignedValidWithClientEku; var transform = new RequestHeaderClientCertTransform("Name"); await transform.ApplyAsync(new RequestTransformContext() { HttpContext = httpContext, ProxyRequest = proxyRequest }); var expected = Convert.ToBase64String(Certificates.SelfSignedValidWithClientEku.RawData); Assert.Equal(expected, proxyRequest.Headers.GetValues("Name").Single()); } [Fact] public async Task ExistingHeader_NoCert_RemovesHeader() { var httpContext = new DefaultHttpContext(); var proxyRequest = new HttpRequestMessage(); proxyRequest.Headers.Add("Name", "OtherValue"); var transform = new RequestHeaderClientCertTransform("Name"); await transform.ApplyAsync(new RequestTransformContext() { HttpContext = httpContext, ProxyRequest = proxyRequest, HeadersCopied = true }); Assert.Empty(proxyRequest.Headers); } [Fact] public async Task ExistingHeader_Replaced() { var httpContext = new DefaultHttpContext(); httpContext.Connection.ClientCertificate = Certificates.SelfSignedValidWithClientEku; var proxyRequest = new HttpRequestMessage(); proxyRequest.Headers.Add("Name", "OtherValue"); var transform = new RequestHeaderClientCertTransform("Name"); await transform.ApplyAsync(new RequestTransformContext() { HttpContext = httpContext, ProxyRequest = proxyRequest, HeadersCopied = true }); var expected = Convert.ToBase64String(Certificates.SelfSignedValidWithClientEku.RawData); Assert.Equal(expected, proxyRequest.Headers.GetValues("Name").Single()); } private static class Certificates { public static X509Certificate2 SelfSignedValidWithClientEku { get; private set; } = new X509Certificate2(GetFullyQualifiedFilePath("validSelfSignedClientEkuCertificate.cer")); private static string GetFullyQualifiedFilePath(string filename) { var filePath = Path.Combine(AppContext.BaseDirectory, filename); if (!File.Exists(filePath)) { throw new FileNotFoundException(filePath); } return filePath; } } } ================================================ FILE: test/ReverseProxy.Tests/Transforms/RequestHeaderForwardedTransformTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Net; using System.Net.Http; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Xunit; using Yarp.ReverseProxy.Utilities; namespace Yarp.ReverseProxy.Transforms.Tests; public class RequestHeaderForwardedTransformTests { [Theory] // Using "|" to represent multi-line headers [InlineData("", "https", ForwardedTransformActions.Set, "proto=https")] [InlineData("", "https", ForwardedTransformActions.Append, "proto=https")] [InlineData("", "https", ForwardedTransformActions.Remove, "")] [InlineData("existing,Header", "https", ForwardedTransformActions.Set, "proto=https")] [InlineData("existing|Header", "https", ForwardedTransformActions.Set, "proto=https")] [InlineData("existing,Header", "https", ForwardedTransformActions.Append, "existing,Header|proto=https")] [InlineData("existing|Header", "https", ForwardedTransformActions.Append, "existing|Header|proto=https")] [InlineData("existing,Header", "https", ForwardedTransformActions.Remove, "")] [InlineData("existing|Header", "https", ForwardedTransformActions.Remove, "")] public async Task Proto_Added(string startValue, string scheme, ForwardedTransformActions action, string expected) { var randomFactory = new TestRandomFactory(); var httpContext = new DefaultHttpContext(); httpContext.Request.Scheme = scheme; var proxyRequest = new HttpRequestMessage(); proxyRequest.Headers.Add("Forwarded", startValue.Split("|", StringSplitOptions.RemoveEmptyEntries)); var transform = new RequestHeaderForwardedTransform(randomFactory, forFormat: NodeFormat.None, byFormat: NodeFormat.None, host: false, proto: true, action); await transform.ApplyAsync(new RequestTransformContext() { HttpContext = httpContext, ProxyRequest = proxyRequest, HeadersCopied = true, }); if (string.IsNullOrEmpty(expected)) { Assert.False(proxyRequest.Headers.TryGetValues("Forwarded", out _)); } else { Assert.Equal(expected.Split("|", StringSplitOptions.RemoveEmptyEntries), proxyRequest.Headers.GetValues("Forwarded")); } } [Theory] // Using "|" to represent multi-line headers [InlineData("", "myHost", ForwardedTransformActions.Set, "host=\"myHost\"")] [InlineData("", "myHost", ForwardedTransformActions.Append, "host=\"myHost\"")] [InlineData("", "ho本st", ForwardedTransformActions.Set, "host=\"xn--host-6j1i\"")] [InlineData("", "myHost:80", ForwardedTransformActions.Set, "host=\"myHost:80\"")] [InlineData("", "ho本st:80", ForwardedTransformActions.Set, "host=\"xn--host-6j1i:80\"")] [InlineData("existing,Header", "myHost", ForwardedTransformActions.Set, "host=\"myHost\"")] [InlineData("existing|Header", "myHost", ForwardedTransformActions.Set, "host=\"myHost\"")] [InlineData("existing|Header", "myHost:80", ForwardedTransformActions.Set, "host=\"myHost:80\"")] [InlineData("existing,Header", "myHost", ForwardedTransformActions.Append, "existing,Header|host=\"myHost\"")] [InlineData("existing|Header", "myHost", ForwardedTransformActions.Append, "existing|Header|host=\"myHost\"")] [InlineData("existing|Header", "myHost:80", ForwardedTransformActions.Append, "existing|Header|host=\"myHost:80\"")] public async Task Host_Added(string startValue, string host, ForwardedTransformActions action, string expected) { var randomFactory = new TestRandomFactory(); var httpContext = new DefaultHttpContext(); httpContext.Request.Host = new HostString(host); var proxyRequest = new HttpRequestMessage(); proxyRequest.Headers.Add("Forwarded", startValue.Split("|", StringSplitOptions.RemoveEmptyEntries)); var transform = new RequestHeaderForwardedTransform(randomFactory, forFormat: NodeFormat.None, byFormat: NodeFormat.None, host: true, proto: false, action); await transform.ApplyAsync(new RequestTransformContext() { HttpContext = httpContext, ProxyRequest = proxyRequest, HeadersCopied = true, }); Assert.Equal(expected.Split("|", StringSplitOptions.RemoveEmptyEntries), proxyRequest.Headers.GetValues("Forwarded")); } [Theory] // Using "|" to represent multi-line headers [InlineData("", "", 2, NodeFormat.Ip, ForwardedTransformActions.Set, "for=unknown")] // Missing IP falls back to Unknown [InlineData("", "", 0, NodeFormat.IpAndPort, ForwardedTransformActions.Append, "for=unknown")] // Missing port excluded [InlineData("", "", 2, NodeFormat.IpAndPort, ForwardedTransformActions.Append, "for=\"unknown:2\"")] [InlineData("", "::1", 2, NodeFormat.Unknown, ForwardedTransformActions.Set, "for=unknown")] [InlineData("", "::1", 2, NodeFormat.UnknownAndPort, ForwardedTransformActions.Append, "for=\"unknown:2\"")] [InlineData("", "::1", 2, NodeFormat.UnknownAndRandomPort, ForwardedTransformActions.Append, "for=\"unknown:_abcdefghi\"")] [InlineData("", "::1", 2, NodeFormat.Ip, ForwardedTransformActions.Set, "for=\"[::1]\"")] [InlineData("", "::1", 0, NodeFormat.IpAndPort, ForwardedTransformActions.Append, "for=\"[::1]\"")] [InlineData("", "::1", 2, NodeFormat.IpAndPort, ForwardedTransformActions.Append, "for=\"[::1]:2\"")] [InlineData("", "::1", 2, NodeFormat.IpAndRandomPort, ForwardedTransformActions.Append, "for=\"[::1]:_abcdefghi\"")] [InlineData("", "127.0.0.1", 2, NodeFormat.Ip, ForwardedTransformActions.Set, "for=127.0.0.1")] [InlineData("", "127.0.0.1", 2, NodeFormat.IpAndPort, ForwardedTransformActions.Append, "for=\"127.0.0.1:2\"")] [InlineData("", "127.0.0.1", 2, NodeFormat.IpAndRandomPort, ForwardedTransformActions.Append, "for=\"127.0.0.1:_abcdefghi\"")] [InlineData("", "::ffff:127.0.0.1", 2, NodeFormat.Ip, ForwardedTransformActions.Set, "for=127.0.0.1")] [InlineData("", "::ffff:127.0.0.1", 2, NodeFormat.IpAndPort, ForwardedTransformActions.Append, "for=\"127.0.0.1:2\"")] [InlineData("", "::ffff:127.0.0.1", 2, NodeFormat.IpAndRandomPort, ForwardedTransformActions.Append, "for=\"127.0.0.1:_abcdefghi\"")] [InlineData("", "::1", 2, NodeFormat.Random, ForwardedTransformActions.Set, "for=_abcdefghi")] [InlineData("", "::1", 2, NodeFormat.RandomAndPort, ForwardedTransformActions.Append, "for=\"_abcdefghi:2\"")] [InlineData("", "::1", 2, NodeFormat.RandomAndRandomPort, ForwardedTransformActions.Append, "for=\"_abcdefghi:_jklmnopqr\"")] [InlineData("existing,header", "::1", 2, NodeFormat.Random, ForwardedTransformActions.Set, "for=_abcdefghi")] [InlineData("existing,header", "::1", 2, NodeFormat.RandomAndPort, ForwardedTransformActions.Append, "existing,header|for=\"_abcdefghi:2\"")] [InlineData("existing|header", "::1", 2, NodeFormat.RandomAndPort, ForwardedTransformActions.Append, "existing|header|for=\"_abcdefghi:2\"")] [InlineData("existing,header", "::1", 2, NodeFormat.RandomAndRandomPort, ForwardedTransformActions.Append, "existing,header|for=\"_abcdefghi:_jklmnopqr\"")] [InlineData("existing|header", "::1", 2, NodeFormat.RandomAndRandomPort, ForwardedTransformActions.Append, "existing|header|for=\"_abcdefghi:_jklmnopqr\"")] public async Task For_Added(string startValue, string ip, int port, NodeFormat format, ForwardedTransformActions action, string expected) { var randomFactory = new TestRandomFactory(); 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; var proxyRequest = new HttpRequestMessage(); proxyRequest.Headers.Add("Forwarded", startValue.Split("|", StringSplitOptions.RemoveEmptyEntries)); var transform = new RequestHeaderForwardedTransform(randomFactory, forFormat: format, byFormat: NodeFormat.None, host: false, proto: false, action); await transform.ApplyAsync(new RequestTransformContext() { HttpContext = httpContext, ProxyRequest = proxyRequest, HeadersCopied = true, }); Assert.Equal(expected.Split("|", StringSplitOptions.RemoveEmptyEntries), proxyRequest.Headers.GetValues("Forwarded")); } [Theory] // Using "|" to represent multi-line headers [InlineData("", "", 2, NodeFormat.Ip, ForwardedTransformActions.Set, "by=unknown")] // Missing IP falls back to Unknown [InlineData("", "", 0, NodeFormat.IpAndPort, ForwardedTransformActions.Append, "by=unknown")] // Missing port excluded [InlineData("", "", 2, NodeFormat.IpAndPort, ForwardedTransformActions.Append, "by=\"unknown:2\"")] [InlineData("", "", 2, NodeFormat.IpAndRandomPort, ForwardedTransformActions.Append, "by=\"unknown:_abcdefghi\"")] [InlineData("", "::1", 2, NodeFormat.Unknown, ForwardedTransformActions.Set, "by=unknown")] [InlineData("", "::1", 2, NodeFormat.UnknownAndPort, ForwardedTransformActions.Append, "by=\"unknown:2\"")] [InlineData("", "::1", 2, NodeFormat.UnknownAndRandomPort, ForwardedTransformActions.Append, "by=\"unknown:_abcdefghi\"")] [InlineData("", "::1", 2, NodeFormat.Ip, ForwardedTransformActions.Set, "by=\"[::1]\"")] [InlineData("", "::1", 0, NodeFormat.IpAndPort, ForwardedTransformActions.Append, "by=\"[::1]\"")] [InlineData("", "::1", 2, NodeFormat.IpAndPort, ForwardedTransformActions.Append, "by=\"[::1]:2\"")] [InlineData("", "::1", 2, NodeFormat.IpAndRandomPort, ForwardedTransformActions.Append, "by=\"[::1]:_abcdefghi\"")] [InlineData("", "127.0.0.1", 2, NodeFormat.Ip, ForwardedTransformActions.Set, "by=127.0.0.1")] [InlineData("", "127.0.0.1", 2, NodeFormat.IpAndPort, ForwardedTransformActions.Append, "by=\"127.0.0.1:2\"")] [InlineData("", "127.0.0.1", 2, NodeFormat.IpAndRandomPort, ForwardedTransformActions.Append, "by=\"127.0.0.1:_abcdefghi\"")] [InlineData("", "::ffff:127.0.0.1", 2, NodeFormat.Ip, ForwardedTransformActions.Set, "by=127.0.0.1")] [InlineData("", "::ffff:127.0.0.1", 2, NodeFormat.IpAndPort, ForwardedTransformActions.Append, "by=\"127.0.0.1:2\"")] [InlineData("", "::ffff:127.0.0.1", 2, NodeFormat.IpAndRandomPort, ForwardedTransformActions.Append, "by=\"127.0.0.1:_abcdefghi\"")] [InlineData("", "::1", 2, NodeFormat.Random, ForwardedTransformActions.Set, "by=_abcdefghi")] [InlineData("", "::1", 2, NodeFormat.RandomAndPort, ForwardedTransformActions.Append, "by=\"_abcdefghi:2\"")] [InlineData("", "::1", 2, NodeFormat.RandomAndRandomPort, ForwardedTransformActions.Append, "by=\"_abcdefghi:_jklmnopqr\"")] [InlineData("existing,header", "::1", 2, NodeFormat.Random, ForwardedTransformActions.Set, "by=_abcdefghi")] [InlineData("existing,header", "::1", 2, NodeFormat.RandomAndPort, ForwardedTransformActions.Append, "existing,header|by=\"_abcdefghi:2\"")] [InlineData("existing|header", "::1", 2, NodeFormat.RandomAndPort, ForwardedTransformActions.Append, "existing|header|by=\"_abcdefghi:2\"")] [InlineData("existing,header", "::1", 2, NodeFormat.RandomAndRandomPort, ForwardedTransformActions.Append, "existing,header|by=\"_abcdefghi:_jklmnopqr\"")] [InlineData("existing|header", "::1", 2, NodeFormat.RandomAndRandomPort, ForwardedTransformActions.Append, "existing|header|by=\"_abcdefghi:_jklmnopqr\"")] public async Task By_Added(string startValue, string ip, int port, NodeFormat format, ForwardedTransformActions action, string expected) { var randomFactory = new TestRandomFactory(); 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; var proxyRequest = new HttpRequestMessage(); proxyRequest.Headers.Add("Forwarded", startValue.Split("|", StringSplitOptions.RemoveEmptyEntries)); var transform = new RequestHeaderForwardedTransform(randomFactory, forFormat: NodeFormat.None, byFormat: format, host: false, proto: false, action); await transform.ApplyAsync(new RequestTransformContext() { HttpContext = httpContext, ProxyRequest = proxyRequest, HeadersCopied = true, }); Assert.Equal(expected.Split("|", StringSplitOptions.RemoveEmptyEntries), proxyRequest.Headers.GetValues("Forwarded")); } [Theory] // Using "|" to represent multi-line headers [InlineData("", ForwardedTransformActions.Set, "proto=https;host=\"myHost:80\";for=\"[::1]:10\";by=_abcdefghi")] [InlineData("", ForwardedTransformActions.Append, "proto=https;host=\"myHost:80\";for=\"[::1]:10\";by=_abcdefghi")] [InlineData("otherHeader", ForwardedTransformActions.Set, "proto=https;host=\"myHost:80\";for=\"[::1]:10\";by=_abcdefghi")] [InlineData("otherHeader", ForwardedTransformActions.Append, "otherHeader|proto=https;host=\"myHost:80\";for=\"[::1]:10\";by=_abcdefghi")] public async Task AllValues_Added(string startValue, ForwardedTransformActions action, string expected) { var randomFactory = new TestRandomFactory(); 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); httpContext.Connection.RemoteIpAddress = IPAddress.IPv6Loopback; httpContext.Connection.RemotePort = 10; var proxyRequest = new HttpRequestMessage(); proxyRequest.Headers.Add("Forwarded", startValue.Split("|", StringSplitOptions.RemoveEmptyEntries)); var transform = new RequestHeaderForwardedTransform(randomFactory, forFormat: NodeFormat.IpAndPort, byFormat: NodeFormat.Random, host: true, proto: true, action); await transform.ApplyAsync(new RequestTransformContext() { HttpContext = httpContext, ProxyRequest = proxyRequest, HeadersCopied = true, }); Assert.Equal(expected.Split("|", StringSplitOptions.RemoveEmptyEntries), proxyRequest.Headers.GetValues("Forwarded")); } internal class TestRandomFactory : IRandomFactory { internal TestRandom Instance { get; set; } public Random CreateRandomInstance() { return Instance; } } public class TestRandom : Random { public int[] Sequence { get; set; } public int Offset { get; set; } public override int Next(int maxValue) { return Sequence[Offset++]; } } } ================================================ FILE: test/ReverseProxy.Tests/Transforms/RequestHeaderRemoveTransformTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Linq; using System.Net.Http; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Xunit; using Yarp.Tests.Common; namespace Yarp.ReverseProxy.Transforms.Tests; public class RequestHeaderRemoveTransformTests { [Theory] [InlineData("header1", "value1", "header1", "")] [InlineData("header1", "value1", "headerX", "header1")] [InlineData("header1; header2; header3", "value1, value2, value3", "header2", "header1; header3")] [InlineData("header1; header2; header3", "value1, value2, value3", "headerX", "header1; header2; header3")] [InlineData("header1; header2; header2; header3", "value1, value2-1, value2-2, value3", "header2", "header1; header3")] public async Task RemoveHeader_Success(string names, string values, string removedHeader, string expected) { var httpContext = new DefaultHttpContext(); var proxyRequest = new HttpRequestMessage(); foreach (var pair in TestResources.ParseNameAndValues(names, values)) { proxyRequest.Headers.Add(pair.Name, pair.Values); } var transform = new RequestHeaderRemoveTransform(removedHeader); await transform.ApplyAsync(new RequestTransformContext() { HttpContext = httpContext, ProxyRequest = proxyRequest, HeadersCopied = true, }); var expectedHeaders = expected.Split("; ", StringSplitOptions.RemoveEmptyEntries); Assert.Equal(expectedHeaders, proxyRequest.Headers.Select(h => h.Key)); } } ================================================ FILE: test/ReverseProxy.Tests/Transforms/RequestHeaderRouteValueTransformTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Net.Http; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Template; using Xunit; using Yarp.ReverseProxy.Transforms; namespace Yarp.ReverseProxy.Tests.Transforms; public class RequestHeaderRouteValueTransformTests { [Theory] [InlineData("defaultHeader", "value", "/{a}/{b}/{c}", "a", "value;6", true)] [InlineData("defaultHeader", "value", "/{a}/{b}/{c}", "notInRoute", "value", true)] [InlineData("defaultHeader", "value", "/{a}/{b}/{c}", "notInRoute", "value", false)] [InlineData("defaultHeader", "value", "/{a}/{b}/{c}", "a", "6", false)] [InlineData("h1", "value", "/{a}/{b}/{c}", "a", "6", false)] [InlineData("h1", "value", "/{a}/{b}/{c}", "b", "7", false)] [InlineData("h1", "value", "/{a}/{*remainder}", "remainder", "7/8", false)] public async Task AddsRequestHeaderRouteValue_SetHeader(string headerName, string defaultHeaderStartValue, string pattern, string routeValueKey, string expected, bool append) { // Arrange const string path = "/6/7/8"; var routeValues = new RouteValueDictionary(); var templateMatcher = new TemplateMatcher(TemplateParser.Parse(pattern), new RouteValueDictionary()); templateMatcher.TryMatch(path, routeValues); var httpContext = new DefaultHttpContext(); httpContext.Request.RouteValues = routeValues; var proxyRequest = new HttpRequestMessage(); proxyRequest.Headers.Add("defaultHeader", defaultHeaderStartValue.Split(";", StringSplitOptions.RemoveEmptyEntries)); var context = new RequestTransformContext() { Path = path, HttpContext = httpContext, ProxyRequest = proxyRequest, HeadersCopied = true }; // Act var transform = new RequestHeaderRouteValueTransform(headerName, routeValueKey, append); await transform.ApplyAsync(context); // Assert Assert.Equal(expected.Split(";", StringSplitOptions.RemoveEmptyEntries), proxyRequest.Headers.GetValues(headerName)); } } ================================================ FILE: test/ReverseProxy.Tests/Transforms/RequestHeaderValueTransformTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Net.Http; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Xunit; namespace Yarp.ReverseProxy.Transforms.Tests; public class RequestHeaderValueTransformTests { [Theory] // Using ";" to represent multi-line headers [InlineData("", "new,value", false, "new,value")] [InlineData("", "new,value", true, "new,value")] [InlineData("start", "new,value", false, "new,value")] [InlineData("start,value", "new,value", false, "new,value")] [InlineData("start;value", "new,value", false, "new,value")] [InlineData("start", "new,value", true, "start;new,value")] [InlineData("start,value", "new,value", true, "start,value;new,value")] [InlineData("start;value", "new,value", true, "start;value;new,value")] public async Task AddHeader_Success(string startValue, string value, bool append, string expected) { var httpContext = new DefaultHttpContext(); var proxyRequest = new HttpRequestMessage(); proxyRequest.Headers.Add("name", startValue.Split(";", StringSplitOptions.RemoveEmptyEntries)); var transform = new RequestHeaderValueTransform("name", value, append); await transform.ApplyAsync(new RequestTransformContext() { HttpContext = httpContext, ProxyRequest = proxyRequest, HeadersCopied = true, }); Assert.Equal(expected.Split(";", StringSplitOptions.RemoveEmptyEntries), proxyRequest.Headers.GetValues("name")); } } ================================================ FILE: test/ReverseProxy.Tests/Transforms/RequestHeaderXForwardedForTransformTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Net; using System.Net.Http; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Xunit; namespace Yarp.ReverseProxy.Transforms.Tests; public class RequestHeaderXForwardedForTransformTests { [Theory] // Using ";" to represent multi-line headers [InlineData("", "", ForwardedTransformActions.Set, "")] [InlineData("", "", ForwardedTransformActions.Append, "")] [InlineData("", "", ForwardedTransformActions.Remove, "")] [InlineData("", "::1", ForwardedTransformActions.Set, "::1")] [InlineData("", "127.0.0.1", ForwardedTransformActions.Set, "127.0.0.1")] [InlineData("", "::ffff:127.0.0.1", ForwardedTransformActions.Set, "127.0.0.1")] [InlineData("", "127.0.0.1", ForwardedTransformActions.Append, "127.0.0.1")] [InlineData("", "::ffff:127.0.0.1", ForwardedTransformActions.Append, "127.0.0.1")] [InlineData("", "127.0.0.1", ForwardedTransformActions.Remove, "")] [InlineData("existing,Header", "", ForwardedTransformActions.Set, "")] [InlineData("existing;Header", "", ForwardedTransformActions.Set, "")] [InlineData("existing,Header", "", ForwardedTransformActions.Append, "existing,Header")] [InlineData("existing;Header", "", ForwardedTransformActions.Append, "existing;Header")] [InlineData("existing;Header", "", ForwardedTransformActions.Remove, "")] [InlineData("existing,Header", "127.0.0.1", ForwardedTransformActions.Set, "127.0.0.1")] [InlineData("existing;Header", "127.0.0.1", ForwardedTransformActions.Set, "127.0.0.1")] [InlineData("existing,Header", "127.0.0.1", ForwardedTransformActions.Append, "existing,Header;127.0.0.1")] [InlineData("existing;Header", "127.0.0.1", ForwardedTransformActions.Append, "existing;Header;127.0.0.1")] [InlineData("existing;Header", "127.0.0.1", ForwardedTransformActions.Remove, "")] public async Task RemoteIp_Added(string startValue, string remoteIp, ForwardedTransformActions action, string expected) { var httpContext = new DefaultHttpContext(); httpContext.Connection.RemoteIpAddress = string.IsNullOrEmpty(remoteIp) ? null : IPAddress.Parse(remoteIp); var proxyRequest = new HttpRequestMessage(); proxyRequest.Headers.Add("name", startValue.Split(";", StringSplitOptions.RemoveEmptyEntries)); var transform = new RequestHeaderXForwardedForTransform("name", action); await transform.ApplyAsync(new RequestTransformContext() { HttpContext = httpContext, ProxyRequest = proxyRequest, HeadersCopied = true, }); if (string.IsNullOrEmpty(expected)) { Assert.False(proxyRequest.Headers.TryGetValues("name", out var _)); } else { Assert.Equal(expected.Split(";", StringSplitOptions.RemoveEmptyEntries), proxyRequest.Headers.GetValues("name")); } } } ================================================ FILE: test/ReverseProxy.Tests/Transforms/RequestHeaderXForwardedHostTransformTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Net.Http; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Xunit; namespace Yarp.ReverseProxy.Transforms.Tests; public class RequestHeaderXForwardedHostTransformTests { [Theory] // Using ";" to represent multi-line headers [InlineData("", "", ForwardedTransformActions.Set, "")] [InlineData("", "", ForwardedTransformActions.Append, "")] [InlineData("", "", ForwardedTransformActions.Remove, "")] [InlineData("", "host", ForwardedTransformActions.Set, "host")] [InlineData("", "host:80", ForwardedTransformActions.Append, "host:80")] [InlineData("", "host:80", ForwardedTransformActions.Remove, "")] [InlineData("", "ho本st", ForwardedTransformActions.Set, "xn--host-6j1i")] [InlineData("", "::1", ForwardedTransformActions.Set, "::1")] [InlineData("", "[::1]:80", ForwardedTransformActions.Set, "[::1]:80")] [InlineData("existing,Header", "", ForwardedTransformActions.Set, "")] [InlineData("existing;Header", "", ForwardedTransformActions.Set, "")] [InlineData("existing,Header", "", ForwardedTransformActions.Append, "existing,Header")] [InlineData("existing;Header", "", ForwardedTransformActions.Append, "existing;Header")] [InlineData("existing;Header", "", ForwardedTransformActions.Remove, "")] [InlineData("existing,Header", "host", ForwardedTransformActions.Set, "host")] [InlineData("existing;Header", "host", ForwardedTransformActions.Set, "host")] [InlineData("existing,Header", "host:80", ForwardedTransformActions.Append, "existing,Header;host:80")] [InlineData("existing;Header", "host", ForwardedTransformActions.Append, "existing;Header;host")] [InlineData("existing;Header", "host", ForwardedTransformActions.Remove, "")] public async Task Host_Added(string startValue, string host, ForwardedTransformActions action, string expected) { var httpContext = new DefaultHttpContext(); httpContext.Request.Host = string.IsNullOrEmpty(host) ? new HostString() : new HostString(host); var proxyRequest = new HttpRequestMessage(); proxyRequest.Headers.Add("name", startValue.Split(";", StringSplitOptions.RemoveEmptyEntries)); var transform = new RequestHeaderXForwardedHostTransform("name", action); await transform.ApplyAsync(new RequestTransformContext() { HttpContext = httpContext, ProxyRequest = proxyRequest, HeadersCopied = true, }); if (string.IsNullOrEmpty(expected)) { Assert.False(proxyRequest.Headers.TryGetValues("name", out var _)); } else { Assert.Equal(expected.Split(";", StringSplitOptions.RemoveEmptyEntries), proxyRequest.Headers.GetValues("name")); } } } ================================================ FILE: test/ReverseProxy.Tests/Transforms/RequestHeaderXForwardedPrefixTransformTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Net.Http; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Xunit; namespace Yarp.ReverseProxy.Transforms.Tests; public class RequestHeaderXForwardedPrefixTransformTests { [Theory] // Using ";" to represent multi-line headers [InlineData("", "", ForwardedTransformActions.Set, "")] [InlineData("", "", ForwardedTransformActions.Append, "")] [InlineData("", "", ForwardedTransformActions.Remove, "")] [InlineData("", "/", ForwardedTransformActions.Set, "/")] [InlineData("", "/", ForwardedTransformActions.Append, "/")] [InlineData("", "/base", ForwardedTransformActions.Set, "/base")] [InlineData("", "/base", ForwardedTransformActions.Append, "/base")] [InlineData("", "/base", ForwardedTransformActions.Remove, "")] [InlineData("", "/base/value", ForwardedTransformActions.Set, "/base/value")] [InlineData("", "/base/value", ForwardedTransformActions.Append, "/base/value")] [InlineData("", "/base本", ForwardedTransformActions.Set, "/base%E6%9C%AC")] [InlineData("existing,Header", "", ForwardedTransformActions.Set, "")] [InlineData("existing;Header", "", ForwardedTransformActions.Set, "")] [InlineData("existing,Header", "", ForwardedTransformActions.Append, "existing,Header")] [InlineData("existing;Header", "", ForwardedTransformActions.Append, "existing;Header")] [InlineData("existing;Header", "", ForwardedTransformActions.Remove, "")] [InlineData("existing,Header", "/base", ForwardedTransformActions.Set, "/base")] [InlineData("existing;Header", "/base", ForwardedTransformActions.Set, "/base")] [InlineData("existing,Header", "/base", ForwardedTransformActions.Append, "existing,Header;/base")] [InlineData("existing;Header", "/base", ForwardedTransformActions.Append, "existing;Header;/base")] [InlineData("existing;Header", "/base", ForwardedTransformActions.Remove, "")] public async Task PathBase_Added(string startValue, string pathBase, ForwardedTransformActions action, string expected) { var httpContext = new DefaultHttpContext(); httpContext.Request.PathBase = string.IsNullOrEmpty(pathBase) ? new PathString() : new PathString(pathBase); var proxyRequest = new HttpRequestMessage(); proxyRequest.Headers.Add("name", startValue.Split(";", StringSplitOptions.RemoveEmptyEntries)); var transform = new RequestHeaderXForwardedPrefixTransform("name", action); await transform.ApplyAsync(new RequestTransformContext() { HttpContext = httpContext, ProxyRequest = proxyRequest, HeadersCopied = true, }); if (string.IsNullOrEmpty(expected)) { Assert.False(proxyRequest.Headers.TryGetValues("name", out var _)); } else { Assert.Equal(expected.Split(";", StringSplitOptions.RemoveEmptyEntries), proxyRequest.Headers.GetValues("name")); } } } ================================================ FILE: test/ReverseProxy.Tests/Transforms/RequestHeaderXForwardedProtoTransformTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Net.Http; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Xunit; namespace Yarp.ReverseProxy.Transforms.Tests; public class RequestHeaderXForwardedProtoTransformTests { [Theory] // Using ";" to represent multi-line headers [InlineData("", "http", ForwardedTransformActions.Set, "http")] [InlineData("", "http", ForwardedTransformActions.Append, "http")] [InlineData("", "http", ForwardedTransformActions.Remove, "")] [InlineData("existing,Header", "http", ForwardedTransformActions.Set, "http")] [InlineData("existing;Header", "http", ForwardedTransformActions.Set, "http")] [InlineData("existing,Header", "http", ForwardedTransformActions.Append, "existing,Header;http")] [InlineData("existing;Header", "http", ForwardedTransformActions.Append, "existing;Header;http")] [InlineData("existing;Header", "http", ForwardedTransformActions.Remove, "")] public async Task Scheme_Added(string startValue, string scheme, ForwardedTransformActions action, string expected) { var httpContext = new DefaultHttpContext(); httpContext.Request.Scheme = scheme; var proxyRequest = new HttpRequestMessage(); proxyRequest.Headers.Add("name", startValue.Split(";", StringSplitOptions.RemoveEmptyEntries)); var transform = new RequestHeaderXForwardedProtoTransform("name", action); await transform.ApplyAsync(new RequestTransformContext() { HttpContext = httpContext, ProxyRequest = proxyRequest, HeadersCopied = true, }); if (string.IsNullOrEmpty(expected)) { Assert.False(proxyRequest.Headers.TryGetValues("name", out var _)); } else { Assert.Equal(expected.Split(";", StringSplitOptions.RemoveEmptyEntries), proxyRequest.Headers.GetValues("name")); } } } ================================================ FILE: test/ReverseProxy.Tests/Transforms/RequestHeadersAllowedTransformTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Linq; using System.Net.Http; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Net.Http.Headers; using Xunit; namespace Yarp.ReverseProxy.Transforms.Tests; public class RequestHeadersAllowedTransformTests { [Theory] [InlineData("", 0)] [InlineData("header1", 1)] [InlineData("header1;header2", 2)] [InlineData("header1;header2;header3", 3)] [InlineData("header1;header2;header2;header3", 3)] public async Task AllowedHeaders_Copied(string names, int expected) { var httpContext = new DefaultHttpContext(); var proxyRequest = new HttpRequestMessage(); httpContext.Request.Headers["header1"] = "value1"; httpContext.Request.Headers["header2"] = "value2"; httpContext.Request.Headers["header3"] = "value3"; httpContext.Request.Headers["header4"] = "value4"; httpContext.Request.Headers["header5"] = "value5"; httpContext.Request.Headers.ContentLength = 0; var allowed = names.Split(';'); var transform = new RequestHeadersAllowedTransform(allowed); var transformContext = new RequestTransformContext() { HttpContext = httpContext, ProxyRequest = proxyRequest, HeadersCopied = false, }; await transform.ApplyAsync(transformContext); Assert.True(transformContext.HeadersCopied); Assert.Equal(expected, proxyRequest.Headers.Count()); foreach (var header in proxyRequest.Headers) { Assert.Contains(header.Key, allowed, StringComparer.OrdinalIgnoreCase); } } [Theory] [InlineData("", 0)] [InlineData("Allow", 1)] [InlineData("content-disposition;header2", 1)] [InlineData("content-length;content-Location;Content-Type", 3)] [InlineData("Allow;Content-Disposition;Content-Encoding;Content-Language;Content-Location;Content-MD5;Content-Range;Content-Type;Expires;Last-Modified;Content-Length", 11)] public async Task ContentHeaders_CopiedIfAllowed(string names, int expected) { var httpContext = new DefaultHttpContext(); var proxyRequest = new HttpRequestMessage(); httpContext.Request.Headers[HeaderNames.Allow] = "value1"; httpContext.Request.Headers[HeaderNames.ContentDisposition] = "value2"; httpContext.Request.Headers[HeaderNames.ContentEncoding] = "value3"; httpContext.Request.Headers[HeaderNames.ContentLanguage] = "value4"; httpContext.Request.Headers[HeaderNames.ContentLocation] = "value5"; httpContext.Request.Headers[HeaderNames.ContentMD5] = "value6"; httpContext.Request.Headers[HeaderNames.ContentRange] = "value7"; httpContext.Request.Headers[HeaderNames.ContentType] = "value8"; httpContext.Request.Headers[HeaderNames.Expires] = "value9"; httpContext.Request.Headers[HeaderNames.LastModified] = "value10"; httpContext.Request.Headers.ContentLength = 0; var allowed = names.Split(';'); var transform = new RequestHeadersAllowedTransform(allowed); var transformContext = new RequestTransformContext() { HttpContext = httpContext, ProxyRequest = proxyRequest, HeadersCopied = false, }; await transform.ApplyAsync(transformContext); Assert.True(transformContext.HeadersCopied); Assert.Empty(proxyRequest.Headers); var content = proxyRequest.Content; if (expected == 0) { Assert.Null(content); return; } Assert.Equal(expected, content.Headers.Count()); foreach (var header in content.Headers) { Assert.Contains(header.Key, allowed, StringComparer.OrdinalIgnoreCase); } } [Theory] [InlineData("", 0)] [InlineData("connection", 1)] [InlineData("Transfer-Encoding;Keep-Alive", 2)] // See https://github.com/dotnet/yarp/blob/51d797986b1fea03500a1ad173d13a1176fb5552/src/ReverseProxy/Forwarder/RequestUtilities.cs#L61-L83 public async Task RestrictedHeaders_CopiedIfAllowed(string names, int expected) { var httpContext = new DefaultHttpContext(); var proxyRequest = new HttpRequestMessage(); httpContext.Request.Headers[HeaderNames.Connection] = "value1"; httpContext.Request.Headers[HeaderNames.TransferEncoding] = "value2"; httpContext.Request.Headers[HeaderNames.KeepAlive] = "value3"; var allowed = names.Split(';'); var transform = new RequestHeadersAllowedTransform(allowed); var transformContext = new RequestTransformContext() { HttpContext = httpContext, ProxyRequest = proxyRequest, HeadersCopied = false, }; await transform.ApplyAsync(transformContext); Assert.True(transformContext.HeadersCopied); Assert.Equal(expected, proxyRequest.Headers.Count()); foreach (var header in proxyRequest.Headers) { Assert.Contains(header.Key, allowed, StringComparer.OrdinalIgnoreCase); } } } ================================================ FILE: test/ReverseProxy.Tests/Transforms/RequestHeadersTransformExtensionsTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Linq; using Xunit; using Yarp.ReverseProxy.Configuration; using Yarp.ReverseProxy.Transforms.Builder; namespace Yarp.ReverseProxy.Transforms.Tests; public class RequestHeadersTransformExtensionsTests : TransformExtensionsTestsBase { private readonly RequestHeadersTransformFactory _factory = new(); [Theory] [InlineData(true)] [InlineData(false)] public void WithTransformCopyRequestHeaders(bool copy) { var routeConfig = new RouteConfig(); routeConfig = routeConfig.WithTransformCopyRequestHeaders(copy); var builderContext = ValidateAndBuild(routeConfig, _factory); Assert.Equal(copy, builderContext.CopyRequestHeaders); } [Theory] [InlineData(true)] [InlineData(false)] public void WithTransformUseOriginalHostHeader(bool useOriginal) { var routeConfig = new RouteConfig(); routeConfig = routeConfig.WithTransformUseOriginalHostHeader(useOriginal); var builderContext = ValidateAndBuild(routeConfig, _factory); var transform = Assert.Single(builderContext.RequestTransforms); var hostTransform = Assert.IsType(transform); Assert.Equal(useOriginal, hostTransform.UseOriginalHost); } [Theory] [InlineData(true)] [InlineData(false)] public void WithTransformRequestHeader(bool append) { var routeConfig = new RouteConfig(); routeConfig = routeConfig.WithTransformRequestHeader("name", "value", append); var builderContext = ValidateAndBuild(routeConfig, _factory); ValidateRequestHeader(append, builderContext); } [Theory] [InlineData(false)] [InlineData(true)] public void WithTransformRequestHeaderRouteValue(bool append) { var routeConfig = new RouteConfig(); routeConfig = routeConfig.WithTransformRequestHeaderRouteValue("key", "value", append); var builderContext = ValidateAndBuild(routeConfig, _factory); ValidateHeaderRouteParameter(append, builderContext); } [Theory] [InlineData(true)] [InlineData(false)] public void AddRequestHeader(bool append) { var builderContext = CreateBuilderContext(); builderContext.AddRequestHeader("name", "value", append); ValidateRequestHeader(append, builderContext); } [Theory] [InlineData(false)] [InlineData(true)] public void AddRequestHeaderRouteValue(bool append) { var builderContext = CreateBuilderContext(); builderContext.AddRequestHeaderRouteValue("key", "value", append); ValidateHeaderRouteParameter(append, builderContext); } [Fact] public void WithTransformRequestHeaderRemove() { var routeConfig = new RouteConfig(); routeConfig = routeConfig.WithTransformRequestHeaderRemove("MyHeader"); var builderContext = ValidateAndBuild(routeConfig, _factory); var transform = Assert.Single(builderContext.RequestTransforms) as RequestHeaderRemoveTransform; Assert.Equal("MyHeader", transform.HeaderName); } [Fact] public void AddRequestHeaderRemove() { var builderContext = CreateBuilderContext(); builderContext.AddRequestHeaderRemove("MyHeader"); var transform = Assert.Single(builderContext.RequestTransforms) as RequestHeaderRemoveTransform; Assert.Equal("MyHeader", transform.HeaderName); } [Fact] public void WithTransformRequestHeadersAllowed() { var routeConfig = new RouteConfig(); routeConfig = routeConfig.WithTransformRequestHeadersAllowed("header1", "Header2"); var builderContext = ValidateAndBuild(routeConfig, _factory); var transform = Assert.Single(builderContext.RequestTransforms) as RequestHeadersAllowedTransform; Assert.Equal(new[] { "header1", "Header2" }, transform.AllowedHeaders); Assert.False(builderContext.CopyRequestHeaders); } [Fact] public void AddRequestHeadersAllowed() { var builderContext = CreateBuilderContext(); builderContext.AddRequestHeadersAllowed("header1", "Header2"); var transform = Assert.Single(builderContext.RequestTransforms) as RequestHeadersAllowedTransform; Assert.Equal(new[] { "header1", "Header2" }, transform.AllowedHeaders); Assert.False(builderContext.CopyRequestHeaders); } private static void ValidateRequestHeader(bool append, TransformBuilderContext builderContext) { var requestHeaderValueTransform = Assert.Single(builderContext.RequestTransforms.OfType(), t => t.HeaderName == "name"); Assert.Equal("value", requestHeaderValueTransform.Value); Assert.Equal(append, requestHeaderValueTransform.Append); } private static void ValidateHeaderRouteParameter(bool append, TransformBuilderContext builderContext) { var requestTransform = Assert.Single(builderContext.RequestTransforms); var requestHeaderFromRouteTransform = Assert.IsType(requestTransform); Assert.Equal("key", requestHeaderFromRouteTransform.HeaderName); Assert.Equal("value", requestHeaderFromRouteTransform.RouteValueKey); var expectedMode = append; Assert.Equal(expectedMode, requestHeaderFromRouteTransform.Append); } } ================================================ FILE: test/ReverseProxy.Tests/Transforms/RequestTransformTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Linq; using System.Net.Http; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; using Xunit; using Yarp.ReverseProxy.Forwarder; namespace Yarp.ReverseProxy.Transforms.Tests; public class RequestTransformTests { [Theory] [InlineData(true)] [InlineData(false)] public void TakeHeader_RemovesAndReturnsProxyRequestHeader(bool copiedHeaders) { var httpContext = new DefaultHttpContext(); httpContext.Request.Headers["name"] = "value0"; var proxyRequest = new HttpRequestMessage(); proxyRequest.Headers.Add("Name", "value1"); proxyRequest.Content = new StringContent("hello world"); proxyRequest.Content.Headers.Add("Name", "value2"); var result = RequestTransform.TakeHeader(new RequestTransformContext() { HttpContext = httpContext, ProxyRequest = proxyRequest, HeadersCopied = copiedHeaders, }, "name"); Assert.Equal("value1", result); Assert.False(proxyRequest.Headers.TryGetValues("name", out var _)); Assert.Equal(new[] { "value2" }, proxyRequest.Content.Headers.GetValues("name")); } [Theory] [InlineData(true)] [InlineData(false)] public void TakeHeaderFromContent_RemovesAndReturnsProxyContentHeader(bool copiedHeaders) { var httpContext = new DefaultHttpContext(); httpContext.Request.ContentType = "value0"; var proxyRequest = new HttpRequestMessage(); proxyRequest.Content = new StringContent("hello world"); var result = RequestTransform.TakeHeader(new RequestTransformContext() { HttpContext = httpContext, ProxyRequest = proxyRequest, HeadersCopied = copiedHeaders, }, HeaderNames.ContentType); Assert.Equal("text/plain; charset=utf-8", result); Assert.False(proxyRequest.Content.Headers.TryGetValues(HeaderNames.ContentType, out var _)); } [Fact] public void TakeHeader_HeadersNotCopied_ReturnsHttpRequestHeader() { var httpContext = new DefaultHttpContext(); httpContext.Request.Headers["name"] = "value0"; var proxyRequest = new HttpRequestMessage(); var result = RequestTransform.TakeHeader(new RequestTransformContext() { HttpContext = httpContext, ProxyRequest = proxyRequest, HeadersCopied = false, }, "name"); Assert.Equal("value0", result); } [Fact] public void TakeHeader_HeadersCopied_ReturnsNothing() { var httpContext = new DefaultHttpContext(); httpContext.Request.Headers["name"] = "value0"; var proxyRequest = new HttpRequestMessage(); var result = RequestTransform.TakeHeader(new RequestTransformContext() { HttpContext = httpContext, ProxyRequest = proxyRequest, HeadersCopied = true, }, "name"); Assert.Equal(StringValues.Empty, result); } [Theory] [InlineData("header1", "header1", "")] [InlineData("header1", "headerX", "header1")] [InlineData("header1; header2; header3", "header2", "header1; header3")] [InlineData("header1", "Content-Encoding", "header1")] [InlineData("header1; Content-Encoding", "Content-Encoding", "header1")] [InlineData("header1; Content-Encoding", "header1", "Content-Encoding")] [InlineData("header1; Content-Encoding", "Content-Type", "header1; Content-Encoding")] [InlineData("header1; Content-Encoding", "headerX", "header1; Content-Encoding")] [InlineData("header1; Content-Encoding; Accept-Encoding", "header1", "Content-Encoding; Accept-Encoding")] [InlineData("header1; Content-Encoding; Accept-Encoding", "Content-Encoding", "header1; Accept-Encoding")] [InlineData("header1; Content-Encoding; Accept-Encoding", "Accept-Encoding", "header1; Content-Encoding")] [InlineData("header1; Content-Encoding; Accept-Encoding", "headerX", "header1; Content-Encoding; Accept-Encoding")] public void RemoveHeader_RemovesProxyRequestHeader(string names, string removedHeader, string expected) { var httpContext = new DefaultHttpContext(); var proxyRequest = new HttpRequestMessage() { Content = new EmptyHttpContent() }; foreach (var name in names.Split("; ")) { httpContext.Request.Headers[name] = "value0"; RequestUtilities.AddHeader(proxyRequest, name, "value1"); } RequestTransform.RemoveHeader(new RequestTransformContext() { HttpContext = httpContext, ProxyRequest = proxyRequest }, removedHeader); foreach (var name in names.Split("; ")) { Assert.True(httpContext.Request.Headers.TryGetValue(name, out var value)); Assert.Equal("value0", value); } var expectedHeaders = expected.Split("; ", System.StringSplitOptions.RemoveEmptyEntries).OrderBy(h => h); var remainingHeaders = proxyRequest.Headers.Union(proxyRequest.Content.Headers).OrderBy(h => h.Key); Assert.Equal(expectedHeaders, remainingHeaders.Select(h => h.Key)); Assert.All(remainingHeaders, h => Assert.Equal("value1", Assert.Single(h.Value))); } } ================================================ FILE: test/ReverseProxy.Tests/Transforms/ResponseHeaderRemoveTransformTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Linq; using System.Net.Http; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Xunit; using Yarp.Tests.Common; namespace Yarp.ReverseProxy.Transforms.Tests; public class ResponseHeaderRemoveTransformTests { [Theory] [InlineData("header1", "value1", 200, ResponseCondition.Success, "header1", "")] [InlineData("header1", "value1", 404, ResponseCondition.Success, "header1", "header1")] [InlineData("header1", "value1", 200, ResponseCondition.Failure, "header1", "header1")] [InlineData("header1", "value1", 404, ResponseCondition.Failure, "header1", "")] [InlineData("header1", "value1", 200, ResponseCondition.Always, "header1", "")] [InlineData("header1", "value1", 404, ResponseCondition.Always, "header1", "")] [InlineData("header1", "value1", 200, ResponseCondition.Success, "headerX", "header1")] [InlineData("header1", "value1", 404, ResponseCondition.Success, "headerX", "header1")] [InlineData("header1", "value1", 200, ResponseCondition.Always, "headerX", "header1")] [InlineData("header1", "value1", 404, ResponseCondition.Always, "headerX", "header1")] [InlineData("header1; header2; header3", "value1, value2, value3", 200, ResponseCondition.Success, "header2", "header1; header3")] [InlineData("header1; header2; header3", "value1, value2, value3", 404, ResponseCondition.Success, "header2", "header1; header2; header3")] [InlineData("header1; header2; header3", "value1, value2, value3", 200, ResponseCondition.Always, "header2", "header1; header3")] [InlineData("header1; header2; header3", "value1, value2, value3", 404, ResponseCondition.Always, "header2", "header1; header3")] [InlineData("header1; header2; header3", "value1, value2, value3", 200, ResponseCondition.Success, "headerX", "header1; header2; header3")] [InlineData("header1; header2; header3", "value1, value2, value3", 404, ResponseCondition.Success, "headerX", "header1; header2; header3")] [InlineData("header1; header2; header3", "value1, value2, value3", 200, ResponseCondition.Always, "headerX", "header1; header2; header3")] [InlineData("header1; header2; header3", "value1, value2, value3", 404, ResponseCondition.Always, "headerX", "header1; header2; header3")] [InlineData("header1; header2; header2; header3", "value1, value2-1, value2-2, value3", 200, ResponseCondition.Success, "header2", "header1; header3")] [InlineData("header1; header2; header2; header3", "value1, value2-1, value2-2, value3", 404, ResponseCondition.Success, "header2", "header1; header2; header3")] [InlineData("header1; header2; header2; header3", "value1, value2-1, value2-2, value3", 200, ResponseCondition.Always, "header2", "header1; header3")] [InlineData("header1; header2; header2; header3", "value1, value2-1, value2-2, value3", 404, ResponseCondition.Always, "header2", "header1; header3")] public async Task RemoveHeader_Success(string names, string values, int status, ResponseCondition condition, string removedHeader, string expected) { var httpContext = new DefaultHttpContext(); httpContext.Response.StatusCode = status; var proxyResponse = new HttpResponseMessage(); foreach (var (name, subvalues) in TestResources.ParseNameAndValues(names, values)) { httpContext.Response.Headers[name] = subvalues; } var transform = new ResponseHeaderRemoveTransform(removedHeader, condition); await transform.ApplyAsync(new ResponseTransformContext() { HttpContext = httpContext, ProxyResponse = proxyResponse, HeadersCopied = true, }); var expectedHeaders = expected.Split("; ", StringSplitOptions.RemoveEmptyEntries); Assert.Equal(expectedHeaders, httpContext.Response.Headers.Select(h => h.Key)); } [Theory] [InlineData(ResponseCondition.Always)] [InlineData(ResponseCondition.Success)] [InlineData(ResponseCondition.Failure)] public async Task RemoveHeader_ResponseNull_DoNothing(ResponseCondition condition) { var httpContext = new DefaultHttpContext(); httpContext.Response.StatusCode = 502; var transform = new ResponseHeaderRemoveTransform("header1", condition); await transform.ApplyAsync(new ResponseTransformContext() { HttpContext = httpContext, ProxyResponse = null, HeadersCopied = false, }); } } ================================================ FILE: test/ReverseProxy.Tests/Transforms/ResponseHeaderValueTransformTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Net.Http; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Xunit; namespace Yarp.ReverseProxy.Transforms.Tests; public class ResponseHeaderValueTransformTests { [Theory] // Using ";" to represent multi-line headers [InlineData("", 400, "new", false, ResponseCondition.Success, "", false)] [InlineData("", 502, "new", false, ResponseCondition.Success, "", true)] [InlineData("", 200, "new", false, ResponseCondition.Success, "new", false)] [InlineData("", 400, "new", false, ResponseCondition.Always, "new", false)] [InlineData("", 200, "new", false, ResponseCondition.Always, "new", false)] [InlineData("", 502, "new", false, ResponseCondition.Always, "new", true)] [InlineData("", 502, "new", false, ResponseCondition.Failure, "new", false)] [InlineData("", 502, "new", false, ResponseCondition.Failure, "new", true)] [InlineData("", 200, "new", false, ResponseCondition.Failure, "", false)] [InlineData("start", 400, "new", false, ResponseCondition.Success, "start", false)] [InlineData("start", 200, "new", false, ResponseCondition.Success, "new", false)] [InlineData("start", 502, "new", false, ResponseCondition.Success, "start", true)] [InlineData("start", 400, "new", false, ResponseCondition.Always, "new", false)] [InlineData("start", 200, "new", false, ResponseCondition.Always, "new", false)] [InlineData("start", 400, "new", true, ResponseCondition.Success, "start", false)] [InlineData("start", 200, "new", true, ResponseCondition.Success, "start;new", false)] [InlineData("start", 400, "new", true, ResponseCondition.Always, "start;new", false)] [InlineData("start", 200, "new", true, ResponseCondition.Always, "start;new", false)] [InlineData("start,value", 400, "new", true, ResponseCondition.Success, "start,value", false)] [InlineData("start,value", 200, "new", true, ResponseCondition.Success, "start,value;new", false)] [InlineData("start,value", 400, "new", true, ResponseCondition.Always, "start,value;new", false)] [InlineData("start,value", 200, "new", true, ResponseCondition.Always, "start,value;new", false)] [InlineData("start;value", 400, "new", true, ResponseCondition.Success, "start;value", false)] [InlineData("start;value", 200, "new", true, ResponseCondition.Success, "start;value;new", false)] [InlineData("start;value", 400, "new", true, ResponseCondition.Always, "start;value;new", false)] [InlineData("start;value", 200, "new", true, ResponseCondition.Always, "start;value;new", false)] public async Task AddResponseHeader_Success(string startValue, int status, string value, bool append, ResponseCondition condition, string expected, bool responseNull) { var httpContext = new DefaultHttpContext(); httpContext.Response.Headers["name"] = startValue.Split(";", System.StringSplitOptions.RemoveEmptyEntries); httpContext.Response.StatusCode = status; var transformContext = new ResponseTransformContext() { HttpContext = httpContext, ProxyResponse = responseNull ? null : new HttpResponseMessage(), HeadersCopied = true, }; var transform = new ResponseHeaderValueTransform("name", value, append, condition); await transform.ApplyAsync(transformContext); Assert.Equal(expected.Split(";", System.StringSplitOptions.RemoveEmptyEntries), httpContext.Response.Headers["name"]); } } ================================================ FILE: test/ReverseProxy.Tests/Transforms/ResponseHeadersAllowedTransformTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Linq; using System.Net.Http; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Net.Http.Headers; using Xunit; namespace Yarp.ReverseProxy.Transforms.Tests; public class ResponseHeadersAllowedTransformTests { [Theory] [InlineData("", 0)] [InlineData("header1", 1)] [InlineData("header1;header2", 2)] [InlineData("header1;header2;header3", 3)] [InlineData("header1;header2;header2;header3", 3)] public async Task AllowedHeaders_Copied(string names, int expected) { var httpContext = new DefaultHttpContext(); var proxyResponse = new HttpResponseMessage(); proxyResponse.Headers.TryAddWithoutValidation("header1", "value1"); proxyResponse.Headers.TryAddWithoutValidation("header2", "value2"); proxyResponse.Headers.TryAddWithoutValidation("header3", "value3"); proxyResponse.Headers.TryAddWithoutValidation("header4", "value4"); proxyResponse.Headers.TryAddWithoutValidation("header5", "value5"); var allowed = names.Split(';'); var transform = new ResponseHeadersAllowedTransform(allowed); var transformContext = new ResponseTransformContext() { HttpContext = httpContext, ProxyResponse = proxyResponse, HeadersCopied = false, }; await transform.ApplyAsync(transformContext); Assert.True(transformContext.HeadersCopied); Assert.Equal(expected, httpContext.Response.Headers.Count()); foreach (var header in httpContext.Response.Headers) { Assert.Contains(header.Key, allowed, StringComparer.OrdinalIgnoreCase); } } [Theory] [InlineData("", 0)] [InlineData("Allow", 1)] [InlineData("content-disposition;header0", 2)] [InlineData("content-length;content-Location;Content-Type", 3)] [InlineData("Allow;Content-Disposition;Content-Encoding;Content-Language;Content-Location;Content-MD5;Content-Range;Content-Type;Expires;Last-Modified;Content-Length", 11)] public async Task ContentHeaders_CopiedIfAllowed(string names, int expected) { var httpContext = new DefaultHttpContext(); var proxyResponse = new HttpResponseMessage(); proxyResponse.Content = new StringContent(""); proxyResponse.Content.Headers.TryAddWithoutValidation("header0", "value0"); proxyResponse.Content.Headers.TryAddWithoutValidation(HeaderNames.Allow, "value1"); proxyResponse.Content.Headers.TryAddWithoutValidation(HeaderNames.ContentDisposition, "value2"); proxyResponse.Content.Headers.TryAddWithoutValidation(HeaderNames.ContentEncoding, "value3"); proxyResponse.Content.Headers.TryAddWithoutValidation(HeaderNames.ContentLanguage, "value4"); proxyResponse.Content.Headers.TryAddWithoutValidation(HeaderNames.ContentLocation, "value5"); proxyResponse.Content.Headers.TryAddWithoutValidation(HeaderNames.ContentMD5, "value6"); proxyResponse.Content.Headers.TryAddWithoutValidation(HeaderNames.ContentRange, "value7"); proxyResponse.Content.Headers.TryAddWithoutValidation(HeaderNames.ContentType, "value8"); proxyResponse.Content.Headers.TryAddWithoutValidation(HeaderNames.Expires, "value9"); proxyResponse.Content.Headers.TryAddWithoutValidation(HeaderNames.LastModified, "value10"); proxyResponse.Content.Headers.TryAddWithoutValidation(HeaderNames.ContentLength, "0"); var allowed = names.Split(';'); var transform = new ResponseHeadersAllowedTransform(allowed); var transformContext = new ResponseTransformContext() { HttpContext = httpContext, ProxyResponse = proxyResponse, HeadersCopied = false, }; await transform.ApplyAsync(transformContext); Assert.True(transformContext.HeadersCopied); Assert.Equal(expected, httpContext.Response.Headers.Count()); foreach (var header in httpContext.Response.Headers) { Assert.Contains(header.Key, allowed, StringComparer.OrdinalIgnoreCase); } } [Theory] [InlineData("", 0)] [InlineData("connection", 1)] [InlineData("Transfer-Encoding;Keep-Alive", 2)] // See https://github.com/dotnet/yarp/blob/51d797986b1fea03500a1ad173d13a1176fb5552/src/ReverseProxy/Forwarder/RequestUtilities.cs#L61-L83 public async Task RestrictedHeaders_CopiedIfAllowed(string names, int expected) { var httpContext = new DefaultHttpContext(); var proxyResponse = new HttpResponseMessage(); proxyResponse.Headers.TryAddWithoutValidation(HeaderNames.Connection, "value1"); proxyResponse.Headers.TryAddWithoutValidation(HeaderNames.TransferEncoding, "value2"); proxyResponse.Headers.TryAddWithoutValidation(HeaderNames.KeepAlive, "value3"); var allowed = names.Split(';'); var transform = new ResponseHeadersAllowedTransform(allowed); var transformContext = new ResponseTransformContext() { HttpContext = httpContext, ProxyResponse = proxyResponse, HeadersCopied = false, }; await transform.ApplyAsync(transformContext); Assert.True(transformContext.HeadersCopied); Assert.Equal(expected, httpContext.Response.Headers.Count()); foreach (var header in httpContext.Response.Headers) { Assert.Contains(header.Key, allowed, StringComparer.OrdinalIgnoreCase); } } [Fact] public async Task ProxyResponseNull_DoNothing() { var httpContext = new DefaultHttpContext(); httpContext.Response.StatusCode = 502; var transform = new ResponseHeadersAllowedTransform(new[] { "header1" }); var transformContext = new ResponseTransformContext() { HttpContext = httpContext, ProxyResponse = null, HeadersCopied = false, }; await transform.ApplyAsync(transformContext); } } ================================================ FILE: test/ReverseProxy.Tests/Transforms/ResponseTrailerRemoveTransformTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Linq; using System.Net.Http; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Xunit; using Yarp.Tests.Common; namespace Yarp.ReverseProxy.Transforms.Tests; public class ResponseTrailerRemoveTransformTests { [Theory] [InlineData("header1", "value1", 200, ResponseCondition.Success, "header1", "")] [InlineData("header1", "value1", 404, ResponseCondition.Success, "header1", "header1")] [InlineData("header1", "value1", 200, ResponseCondition.Failure, "header1", "header1")] [InlineData("header1", "value1", 404, ResponseCondition.Failure, "header1", "")] [InlineData("header1", "value1", 200, ResponseCondition.Always, "header1", "")] [InlineData("header1", "value1", 404, ResponseCondition.Always, "header1", "")] [InlineData("header1", "value1", 200, ResponseCondition.Success, "headerX", "header1")] [InlineData("header1", "value1", 404, ResponseCondition.Success, "headerX", "header1")] [InlineData("header1", "value1", 200, ResponseCondition.Always, "headerX", "header1")] [InlineData("header1", "value1", 404, ResponseCondition.Always, "headerX", "header1")] [InlineData("header1; header2; header3", "value1, value2, value3", 200, ResponseCondition.Success, "header2", "header1; header3")] [InlineData("header1; header2; header3", "value1, value2, value3", 404, ResponseCondition.Success, "header2", "header1; header2; header3")] [InlineData("header1; header2; header3", "value1, value2, value3", 200, ResponseCondition.Always, "header2", "header1; header3")] [InlineData("header1; header2; header3", "value1, value2, value3", 404, ResponseCondition.Always, "header2", "header1; header3")] [InlineData("header1; header2; header3", "value1, value2, value3", 200, ResponseCondition.Success, "headerX", "header1; header2; header3")] [InlineData("header1; header2; header3", "value1, value2, value3", 404, ResponseCondition.Success, "headerX", "header1; header2; header3")] [InlineData("header1; header2; header3", "value1, value2, value3", 200, ResponseCondition.Always, "headerX", "header1; header2; header3")] [InlineData("header1; header2; header3", "value1, value2, value3", 404, ResponseCondition.Always, "headerX", "header1; header2; header3")] [InlineData("header1; header2; header2; header3", "value1, value2-1, value2-2, value3", 200, ResponseCondition.Success, "header2", "header1; header3")] [InlineData("header1; header2; header2; header3", "value1, value2-1, value2-2, value3", 404, ResponseCondition.Success, "header2", "header1; header2; header3")] [InlineData("header1; header2; header2; header3", "value1, value2-1, value2-2, value3", 200, ResponseCondition.Always, "header2", "header1; header3")] [InlineData("header1; header2; header2; header3", "value1, value2-1, value2-2, value3", 404, ResponseCondition.Always, "header2", "header1; header3")] public async Task RemoveTrailerFromFeature_Success(string names, string values, int status, ResponseCondition condition, string removedHeader, string expected) { var httpContext = new DefaultHttpContext(); httpContext.Response.StatusCode = status; var trailerFeature = new TestTrailersFeature(); httpContext.Features.Set(trailerFeature); var proxyResponse = new HttpResponseMessage(); foreach (var (name, subvalues) in TestResources.ParseNameAndValues(names, values)) { trailerFeature.Trailers[name] = subvalues; } var transform = new ResponseTrailerRemoveTransform(removedHeader, condition); await transform.ApplyAsync(new ResponseTrailersTransformContext() { HttpContext = httpContext, ProxyResponse = proxyResponse, HeadersCopied = true, }); var expectedHeaders = expected.Split("; ", StringSplitOptions.RemoveEmptyEntries); Assert.Equal(expectedHeaders, trailerFeature.Trailers.Select(h => h.Key)); } } ================================================ FILE: test/ReverseProxy.Tests/Transforms/ResponseTrailerValueTransformTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Net.Http; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Xunit; using Yarp.Tests.Common; namespace Yarp.ReverseProxy.Transforms.Tests; public class ResponseTrailerValueTransformTests { [Theory] // Using ";" to represent multi-line headers [InlineData("", 400, "new", false, ResponseCondition.Success, "")] [InlineData("", 200, "new", false, ResponseCondition.Success, "new")] [InlineData("", 400, "new", false, ResponseCondition.Failure, "new")] [InlineData("", 200, "new", false, ResponseCondition.Failure, "")] [InlineData("", 400, "new", false, ResponseCondition.Always, "new")] [InlineData("", 200, "new", false, ResponseCondition.Always, "new")] [InlineData("start", 400, "new", false, ResponseCondition.Success, "start")] [InlineData("start", 200, "new", false, ResponseCondition.Success, "new")] [InlineData("start", 400, "new", false, ResponseCondition.Always, "new")] [InlineData("start", 200, "new", false, ResponseCondition.Always, "new")] [InlineData("start", 400, "new", true, ResponseCondition.Success, "start")] [InlineData("start", 200, "new", true, ResponseCondition.Success, "start;new")] [InlineData("start", 400, "new", true, ResponseCondition.Always, "start;new")] [InlineData("start", 200, "new", true, ResponseCondition.Always, "start;new")] [InlineData("start,value", 400, "new", true, ResponseCondition.Success, "start,value")] [InlineData("start,value", 200, "new", true, ResponseCondition.Success, "start,value;new")] [InlineData("start,value", 400, "new", true, ResponseCondition.Always, "start,value;new")] [InlineData("start,value", 200, "new", true, ResponseCondition.Always, "start,value;new")] [InlineData("start;value", 400, "new", true, ResponseCondition.Success, "start;value")] [InlineData("start;value", 200, "new", true, ResponseCondition.Success, "start;value;new")] [InlineData("start;value", 400, "new", true, ResponseCondition.Always, "start;value;new")] [InlineData("start;value", 200, "new", true, ResponseCondition.Always, "start;value;new")] public async Task AddResponseTrailer_Success(string startValue, int status, string value, bool append, ResponseCondition condition, string expected) { var httpContext = new DefaultHttpContext(); var trailerFeature = new TestTrailersFeature(); httpContext.Features.Set(trailerFeature); trailerFeature.Trailers["name"] = startValue.Split(";", System.StringSplitOptions.RemoveEmptyEntries); httpContext.Response.StatusCode = status; var transformContext = new ResponseTrailersTransformContext() { HttpContext = httpContext, ProxyResponse = new HttpResponseMessage(), HeadersCopied = true, }; var transform = new ResponseTrailerValueTransform("name", value, append, condition); await transform.ApplyAsync(transformContext); Assert.Equal(expected.Split(";", System.StringSplitOptions.RemoveEmptyEntries), trailerFeature.Trailers["name"]); } } ================================================ FILE: test/ReverseProxy.Tests/Transforms/ResponseTrailersAllowedTransformTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Linq; using System.Net.Http; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.Net.Http.Headers; using Xunit; using Yarp.Tests.Common; namespace Yarp.ReverseProxy.Transforms.Tests; public class ResponseTrailersAllowedTransformTests { [Theory] [InlineData("", 0)] [InlineData("header1", 1)] [InlineData("header1;header2", 2)] [InlineData("header1;header2;header3", 3)] [InlineData("header1;header2;header2;header3", 3)] public async Task AllowedHeaders_Copied(string names, int expected) { var httpContext = new DefaultHttpContext(); var trailerFeature = new TestTrailersFeature(); httpContext.Features.Set(trailerFeature); var proxyResponse = new HttpResponseMessage(); proxyResponse.TrailingHeaders.TryAddWithoutValidation("header1", "value1"); proxyResponse.TrailingHeaders.TryAddWithoutValidation("header2", "value2"); proxyResponse.TrailingHeaders.TryAddWithoutValidation("header3", "value3"); proxyResponse.TrailingHeaders.TryAddWithoutValidation("header4", "value4"); proxyResponse.TrailingHeaders.TryAddWithoutValidation("header5", "value5"); var allowed = names.Split(';'); var transform = new ResponseTrailersAllowedTransform(allowed); var transformContext = new ResponseTrailersTransformContext() { HttpContext = httpContext, ProxyResponse = proxyResponse, HeadersCopied = false, }; await transform.ApplyAsync(transformContext); Assert.True(transformContext.HeadersCopied); Assert.Equal(expected, trailerFeature.Trailers.Count()); foreach (var header in trailerFeature.Trailers) { Assert.Contains(header.Key, allowed, StringComparer.OrdinalIgnoreCase); } } [Theory] [InlineData("", 0)] [InlineData("connection", 1)] [InlineData("Transfer-Encoding;Keep-Alive", 2)] // See https://github.com/dotnet/yarp/blob/51d797986b1fea03500a1ad173d13a1176fb5552/src/ReverseProxy/Forwarder/RequestUtilities.cs#L61-L83 public async Task RestrictedHeaders_CopiedIfAllowed(string names, int expected) { var httpContext = new DefaultHttpContext(); var trailerFeature = new TestTrailersFeature(); httpContext.Features.Set(trailerFeature); var proxyResponse = new HttpResponseMessage(); proxyResponse.TrailingHeaders.TryAddWithoutValidation(HeaderNames.Connection, "value1"); proxyResponse.TrailingHeaders.TryAddWithoutValidation(HeaderNames.TransferEncoding, "value2"); proxyResponse.TrailingHeaders.TryAddWithoutValidation(HeaderNames.KeepAlive, "value3"); var allowed = names.Split(';'); var transform = new ResponseTrailersAllowedTransform(allowed); var transformContext = new ResponseTrailersTransformContext() { HttpContext = httpContext, ProxyResponse = proxyResponse, HeadersCopied = false, }; await transform.ApplyAsync(transformContext); Assert.True(transformContext.HeadersCopied); Assert.Equal(expected, trailerFeature.Trailers.Count()); foreach (var header in trailerFeature.Trailers) { Assert.Contains(header.Key, allowed, StringComparer.OrdinalIgnoreCase); } } } ================================================ FILE: test/ReverseProxy.Tests/Transforms/ResponseTrailersTransformTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Net.Http; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.Primitives; using Xunit; using Yarp.Tests.Common; namespace Yarp.ReverseProxy.Transforms.Tests; public class ResponseTrailersTransformTests { [Theory] [InlineData(true)] [InlineData(false)] public void TakeHeader_RemovesAndReturnsHttpResponseTrailer(bool copiedHeaders) { var httpContext = new DefaultHttpContext(); var trailerFeature = new TestTrailersFeature(); httpContext.Features.Set(trailerFeature); trailerFeature.Trailers["name"] = "value0"; var proxyResponse = new HttpResponseMessage(); proxyResponse.TrailingHeaders.Add("Name", "value1"); var result = ResponseTrailersTransform.TakeHeader(new ResponseTrailersTransformContext() { HttpContext = httpContext, ProxyResponse = proxyResponse, HeadersCopied = copiedHeaders, }, "name"); Assert.Equal("value0", result); Assert.False(trailerFeature.Trailers.TryGetValue("name", out var _)); Assert.Equal(new[] { "value1" }, proxyResponse.TrailingHeaders.GetValues("name")); } [Fact] public void TakeHeader_HeadersNotCopied_ReturnsHttpResponseMessageTrailer() { var httpContext = new DefaultHttpContext(); var trailerFeature = new TestTrailersFeature(); httpContext.Features.Set(trailerFeature); var proxyResponse = new HttpResponseMessage(); proxyResponse.TrailingHeaders.Add("Name", "value1"); var result = ResponseTrailersTransform.TakeHeader(new ResponseTrailersTransformContext() { HttpContext = httpContext, ProxyResponse = proxyResponse, HeadersCopied = false, }, "name"); Assert.Equal("value1", result); Assert.False(trailerFeature.Trailers.TryGetValue("name", out var _)); Assert.Equal(new[] { "value1" }, proxyResponse.TrailingHeaders.GetValues("name")); } [Fact] public void TakeHeader_HeadersCopied_ReturnsNothing() { var httpContext = new DefaultHttpContext(); var trailerFeature = new TestTrailersFeature(); httpContext.Features.Set(trailerFeature); var proxyResponse = new HttpResponseMessage(); proxyResponse.TrailingHeaders.Add("Name", "value1"); var result = ResponseTrailersTransform.TakeHeader(new ResponseTrailersTransformContext() { HttpContext = httpContext, ProxyResponse = proxyResponse, HeadersCopied = true, }, "name"); Assert.Equal(StringValues.Empty, result); Assert.False(trailerFeature.Trailers.TryGetValue("name", out var _)); Assert.Equal(new[] { "value1" }, proxyResponse.TrailingHeaders.GetValues("name")); } } ================================================ FILE: test/ReverseProxy.Tests/Transforms/ResponseTransformExtensionsTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Xunit; using Yarp.ReverseProxy.Configuration; using Yarp.ReverseProxy.Transforms.Builder; namespace Yarp.ReverseProxy.Transforms.Tests; public class ResponseTransformExtensionsTests : TransformExtensionsTestsBase { private readonly ResponseTransformFactory _factory = new(); [Theory] [InlineData(true)] [InlineData(false)] public void WithTransformCopyResponseHeaders(bool copy) { var routeConfig = new RouteConfig(); routeConfig = routeConfig.WithTransformCopyResponseHeaders(copy); var builderContext = ValidateAndBuild(routeConfig, _factory); Assert.Equal(copy, builderContext.CopyResponseHeaders); } [Theory] [InlineData(true)] [InlineData(false)] public void WithTransformCopyResponseTrailers(bool copy) { var routeConfig = new RouteConfig(); routeConfig = routeConfig.WithTransformCopyResponseTrailers(copy); var builderContext = ValidateAndBuild(routeConfig, _factory); Assert.Equal(copy, builderContext.CopyResponseTrailers); } [Theory] [InlineData(false, ResponseCondition.Success)] [InlineData(false, ResponseCondition.Always)] [InlineData(false, ResponseCondition.Failure)] [InlineData(true, ResponseCondition.Success)] [InlineData(true, ResponseCondition.Always)] [InlineData(true, ResponseCondition.Failure)] public void WithTransformResponseHeader(bool append, ResponseCondition condition) { var routeConfig = new RouteConfig(); routeConfig = routeConfig.WithTransformResponseHeader("name", "value", append, condition); var builderContext = ValidateAndBuild(routeConfig, _factory); ValidateResponseHeader(builderContext, append, condition); } [Theory] [InlineData(false, ResponseCondition.Success)] [InlineData(false, ResponseCondition.Always)] [InlineData(false, ResponseCondition.Failure)] [InlineData(true, ResponseCondition.Success)] [InlineData(true, ResponseCondition.Always)] [InlineData(true, ResponseCondition.Failure)] public void AddResponseHeader(bool append, ResponseCondition condition) { var builderContext = CreateBuilderContext(); builderContext.AddResponseHeader("name", "value", append, condition); ValidateResponseHeader(builderContext, append, condition); } private static void ValidateResponseHeader(TransformBuilderContext builderContext, bool append, ResponseCondition condition) { var responseTransform = Assert.Single(builderContext.ResponseTransforms); var responseHeaderValueTransform = Assert.IsType(responseTransform); Assert.Equal("name", responseHeaderValueTransform.HeaderName); Assert.Equal("value", responseHeaderValueTransform.Value); Assert.Equal(append, responseHeaderValueTransform.Append); Assert.Equal(condition, responseHeaderValueTransform.Condition); } [Fact] public void WithTransformResponseHeaderRemove() { var routeConfig = new RouteConfig(); routeConfig = routeConfig.WithTransformResponseHeaderRemove("MyHeader"); var builderContext = ValidateAndBuild(routeConfig, _factory); var transform = Assert.Single(builderContext.ResponseTransforms) as ResponseHeaderRemoveTransform; Assert.Equal("MyHeader", transform.HeaderName); } [Fact] public void AddResponseHeaderRemove() { var builderContext = CreateBuilderContext(); builderContext.AddResponseHeaderRemove("MyHeader"); var transform = Assert.Single(builderContext.ResponseTransforms) as ResponseHeaderRemoveTransform; Assert.Equal("MyHeader", transform.HeaderName); } [Fact] public void WithTransformResponseHeadersAllowed() { var routeConfig = new RouteConfig(); routeConfig = routeConfig.WithTransformResponseHeadersAllowed("header1", "Header2"); var builderContext = ValidateAndBuild(routeConfig, _factory); var transform = Assert.Single(builderContext.ResponseTransforms) as ResponseHeadersAllowedTransform; Assert.Equal(new[] { "header1", "Header2" }, transform.AllowedHeaders); Assert.False(builderContext.CopyResponseHeaders); } [Fact] public void AddResponseHeadersAllowed() { var builderContext = CreateBuilderContext(); builderContext.AddResponseHeadersAllowed("header1", "Header2"); var transform = Assert.Single(builderContext.ResponseTransforms) as ResponseHeadersAllowedTransform; Assert.Equal(new[] { "header1", "Header2" }, transform.AllowedHeaders); Assert.False(builderContext.CopyResponseHeaders); } [Theory] [InlineData(false, ResponseCondition.Success)] [InlineData(false, ResponseCondition.Always)] [InlineData(false, ResponseCondition.Failure)] [InlineData(true, ResponseCondition.Success)] [InlineData(true, ResponseCondition.Always)] [InlineData(true, ResponseCondition.Failure)] public void WithTransformResponseTrailer(bool append, ResponseCondition condition) { var routeConfig = new RouteConfig(); routeConfig = routeConfig.WithTransformResponseTrailer("name", "value", append, condition); var builderContext = ValidateAndBuild(routeConfig, _factory); ValidateResponseTrailer(builderContext, append, condition); } [Theory] [InlineData(false, ResponseCondition.Success)] [InlineData(false, ResponseCondition.Always)] [InlineData(false, ResponseCondition.Failure)] [InlineData(true, ResponseCondition.Success)] [InlineData(true, ResponseCondition.Always)] [InlineData(true, ResponseCondition.Failure)] public void AddResponseTrailer(bool append, ResponseCondition condition) { var builderContext = CreateBuilderContext(); builderContext.AddResponseTrailer("name", "value", append, condition); ValidateResponseTrailer(builderContext, append, condition); } private static void ValidateResponseTrailer(TransformBuilderContext builderContext, bool append, ResponseCondition condition) { var responseTransform = Assert.Single(builderContext.ResponseTrailersTransforms); var responseHeaderValueTransform = Assert.IsType(responseTransform); Assert.Equal("name", responseHeaderValueTransform.HeaderName); Assert.Equal("value", responseHeaderValueTransform.Value); Assert.Equal(append, responseHeaderValueTransform.Append); Assert.Equal(condition, responseHeaderValueTransform.Condition); } [Fact] public void WithTransformResponseTrailerRemove() { var routeConfig = new RouteConfig(); routeConfig = routeConfig.WithTransformResponseTrailerRemove("MyHeader"); var builderContext = ValidateAndBuild(routeConfig, _factory); var transform = Assert.Single(builderContext.ResponseTrailersTransforms) as ResponseTrailerRemoveTransform; Assert.Equal("MyHeader", transform.HeaderName); } [Fact] public void AddResponseTrailerRemove() { var builderContext = CreateBuilderContext(); builderContext.AddResponseTrailerRemove("MyHeader"); var transform = Assert.Single(builderContext.ResponseTrailersTransforms) as ResponseTrailerRemoveTransform; Assert.Equal("MyHeader", transform.HeaderName); } [Fact] public void WithTransformResponseTrailersAllowed() { var routeConfig = new RouteConfig(); routeConfig = routeConfig.WithTransformResponseTrailersAllowed("header1", "Header2"); var builderContext = ValidateAndBuild(routeConfig, _factory); var transform = Assert.Single(builderContext.ResponseTrailersTransforms) as ResponseTrailersAllowedTransform; Assert.Equal(new[] { "header1", "Header2" }, transform.AllowedHeaders); Assert.False(builderContext.CopyResponseTrailers); } [Fact] public void AddResponseTrailersAllowed() { var builderContext = CreateBuilderContext(); builderContext.AddResponseTrailersAllowed("header1", "Header2"); var transform = Assert.Single(builderContext.ResponseTrailersTransforms) as ResponseTrailersAllowedTransform; Assert.Equal(new[] { "header1", "Header2" }, transform.AllowedHeaders); Assert.False(builderContext.CopyResponseTrailers); } } ================================================ FILE: test/ReverseProxy.Tests/Transforms/ResponseTransformTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Net.Http; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; using Xunit; namespace Yarp.ReverseProxy.Transforms.Tests; public class ResponseTransformTests { [Theory] [InlineData(true)] [InlineData(false)] public void TakeHeader_RemovesAndReturnsHttpResponseHeader(bool copiedHeaders) { var httpContext = new DefaultHttpContext(); httpContext.Response.Headers["name"] = "value0"; var proxyResponse = new HttpResponseMessage(); proxyResponse.Headers.Add("Name", "value1"); proxyResponse.Content = new StringContent("hello world"); proxyResponse.Content.Headers.Add("Name", "value2"); var result = ResponseTransform.TakeHeader(new ResponseTransformContext() { HttpContext = httpContext, ProxyResponse = proxyResponse, HeadersCopied = copiedHeaders, }, "name"); Assert.Equal("value0", result); Assert.False(httpContext.Response.Headers.TryGetValue("name", out var _)); Assert.Equal(new[] { "value1" }, proxyResponse.Headers.GetValues("name")); Assert.Equal(new[] { "value2" }, proxyResponse.Content.Headers.GetValues("name")); } [Fact] public void TakeHeader_HeadersNotCopied_ReturnsHttpResponseMessageHeader() { var httpContext = new DefaultHttpContext(); var proxyResponse = new HttpResponseMessage(); proxyResponse.Headers.Add("Name", "value1"); proxyResponse.Content = new StringContent("hello world"); proxyResponse.Content.Headers.Add("Name", "value2"); var result = ResponseTransform.TakeHeader(new ResponseTransformContext() { HttpContext = httpContext, ProxyResponse = proxyResponse, HeadersCopied = false, }, "name"); Assert.Equal("value1", result); } [Fact] public void TakeHeader_HeadersNotCopied_ReturnsHttpContentHeader() { var httpContext = new DefaultHttpContext(); var proxyResponse = new HttpResponseMessage(); proxyResponse.Content = new StringContent("hello world"); proxyResponse.Content.Headers.Add("Name", "value2"); var result = ResponseTransform.TakeHeader(new ResponseTransformContext() { HttpContext = httpContext, ProxyResponse = proxyResponse, HeadersCopied = false, }, "name"); Assert.Equal("value2", result); } [Fact] public void TakeHeader_HeadersCopied_ReturnsNothing() { var httpContext = new DefaultHttpContext(); var proxyResponse = new HttpResponseMessage(); proxyResponse.Headers.Add("Name", "value1"); proxyResponse.Content = new StringContent("hello world"); proxyResponse.Content.Headers.Add("Name", "value2"); var result = ResponseTransform.TakeHeader(new ResponseTransformContext() { HttpContext = httpContext, ProxyResponse = proxyResponse, HeadersCopied = true, }, "name"); Assert.Equal(StringValues.Empty, result); } [Fact] public void TakeHeader_ResponseNull_ReturnsNothing() { var httpContext = new DefaultHttpContext(); var result = ResponseTransform.TakeHeader(new ResponseTransformContext() { HttpContext = httpContext, ProxyResponse = null, HeadersCopied = false, }, "name"); Assert.Equal(StringValues.Empty, result); } } ================================================ FILE: test/ReverseProxy.Tests/Transforms/TransformBuilderContextFuncExtensionsTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Xunit; namespace Yarp.ReverseProxy.Transforms.Tests; public class TransformBuilderContextFuncExtensionsTests : TransformExtensionsTestsBase { [Fact] public void AddRequestTransform() { var builderContext = CreateBuilderContext(); builderContext.AddRequestTransform(context => { return default; }); var requestTransform = Assert.Single(builderContext.RequestTransforms); Assert.IsType(requestTransform); } [Fact] public void AddResponseTransform() { var builderContext = CreateBuilderContext(); builderContext.AddResponseTransform(context => { return default; }); var responseTransform = Assert.Single(builderContext.ResponseTransforms); Assert.IsType(responseTransform); } [Fact] public void AddResponseTrailersTransform() { var builderContext = CreateBuilderContext(); builderContext.AddResponseTrailersTransform(context => { return default; }); var responseTrailersTransform = Assert.Single(builderContext.ResponseTrailersTransforms); Assert.IsType(responseTrailersTransform); } } ================================================ FILE: test/ReverseProxy.Tests/Transforms/TransformExtensionsTestsBase.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using Xunit; using Yarp.ReverseProxy.Configuration; using Yarp.ReverseProxy.Transforms.Builder; namespace Yarp.ReverseProxy.Transforms.Tests; public abstract class TransformExtensionsTestsBase { protected static TransformBuilderContext CreateBuilderContext(IServiceProvider services = null) => new() { Route = new RouteConfig(), Services = services, }; protected static TransformBuilderContext ValidateAndBuild(RouteConfig routeConfig, ITransformFactory factory, IServiceProvider serviceProvider = null) { var transformValues = Assert.Single(routeConfig.Transforms); var validationContext = new TransformRouteValidationContext { Route = routeConfig }; Assert.True(factory.Validate(validationContext, transformValues)); Assert.Empty(validationContext.Errors); var builderContext = CreateBuilderContext(serviceProvider); Assert.True(factory.Build(builderContext, transformValues)); return builderContext; } } ================================================ FILE: test/ReverseProxy.Tests/Utilities/ActivityCancellationTokenSourceTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Xunit; namespace Yarp.ReverseProxy.Utilities.Tests; public class ActivityCancellationTokenSourceTests { [Fact] public void ActivityCancellationTokenSource_PoolsSources() { HashSet sources = []; for (var i = 0; i < 1_000; i++) { var source = ActivityCancellationTokenSource.Rent(TimeSpan.FromMinutes(10), CancellationToken.None); source.Return(); sources.Add(source); } Assert.True(sources.Count < 1000); } [Fact] public void ActivityCancellationTokenSource_DoesNotPoolsCanceledSources() { var cts = ActivityCancellationTokenSource.Rent(TimeSpan.FromSeconds(10), CancellationToken.None); cts.Cancel(); var cts2 = ActivityCancellationTokenSource.Rent(TimeSpan.FromSeconds(10), CancellationToken.None); Assert.NotSame(cts, cts2); } [Fact] public void ActivityCancellationTokenSource_RespectsLinkedToken1() { var linkedCts = new CancellationTokenSource(); var cts = ActivityCancellationTokenSource.Rent(TimeSpan.FromSeconds(10), linkedCts.Token); linkedCts.Cancel(); Assert.True(cts.CancelledByLinkedToken); Assert.True(cts.IsCancellationRequested); } [Fact] public void ActivityCancellationTokenSource_RespectsLinkedToken2() { var linkedCts = new CancellationTokenSource(); var cts = ActivityCancellationTokenSource.Rent(TimeSpan.FromSeconds(10), default, linkedCts.Token); linkedCts.Cancel(); Assert.True(cts.CancelledByLinkedToken); Assert.True(cts.IsCancellationRequested); } [Fact] public void ActivityCancellationTokenSource_RespectsBothLinkedTokens() { var linkedCts1 = new CancellationTokenSource(); var linkedCts2 = new CancellationTokenSource(); var cts = ActivityCancellationTokenSource.Rent(TimeSpan.FromSeconds(10), linkedCts1.Token, linkedCts2.Token); linkedCts1.Cancel(); linkedCts2.Cancel(); Assert.True(cts.CancelledByLinkedToken); Assert.True(cts.IsCancellationRequested); } [Fact] public void ActivityCancellationTokenSource_ClearsRegistrations() { var linkedCts1 = new CancellationTokenSource(); var linkedCts2 = new CancellationTokenSource(); var cts = ActivityCancellationTokenSource.Rent(TimeSpan.FromSeconds(10), linkedCts1.Token, linkedCts2.Token); cts.Return(); linkedCts1.Cancel(); linkedCts2.Cancel(); Assert.False(cts.IsCancellationRequested); } [Fact] public async Task ActivityCancellationTokenSource_RespectsTimeout() { var cts = ActivityCancellationTokenSource.Rent(TimeSpan.FromMilliseconds(1), CancellationToken.None); for (var i = 0; i < 1000; i++) { if (cts.IsCancellationRequested) { Assert.False(cts.CancelledByLinkedToken); return; } await Task.Delay(1); } Assert.Fail("Cts was not canceled"); } } ================================================ FILE: test/ReverseProxy.Tests/Utilities/AtomicCounterTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Threading.Tasks; using Xunit; namespace Yarp.ReverseProxy.Utilities.Tests; public class AtomicCounterTests { [Fact] public void Constructor_Works() { new AtomicCounter(); } [Fact] public void Increment_ThreadSafety() { const int Iterations = 100_000; var counter = new AtomicCounter(); Parallel.For(0, Iterations, i => { counter.Increment(); }); Assert.Equal(Iterations, counter.Value); } [Fact] public void Decrement_ThreadSafety() { const int Iterations = 100_000; var counter = new AtomicCounter(); Parallel.For(0, Iterations, i => { counter.Decrement(); }); Assert.Equal(-Iterations, counter.Value); } } ================================================ FILE: test/ReverseProxy.Tests/Utilities/CaseInsensitiveEqualHelperTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Xunit; namespace Yarp.ReverseProxy.Utilities.Tests; public class CaseInsensitiveEqualHelperTests { [Fact] public void Equals_Same_Instance_Returns_True() { var list1 = new string[] { "item1", "item2" }; var equals = CaseInsensitiveEqualHelper.Equals(list1, list1); Assert.True(equals); } [Fact] public void Equals_Empty_List_Returns_True() { var list1 = System.Array.Empty(); var list2 = System.Array.Empty(); var equals = CaseInsensitiveEqualHelper.Equals(list1, list2); Assert.True(equals); } [Fact] public void Equals_List_Same_Value_Returns_True() { var list1 = new string[] { "item1", "item2" }; var list2 = new string[] { "item1", "item2" }; var equals = CaseInsensitiveEqualHelper.Equals(list1, list2); Assert.True(equals); } [Fact] public void Equals_List_Different_Value_Returns_False() { var list1 = new string[] { "item1", "item2" }; var list2 = new string[] { "item3", "item4" }; var equals = CaseInsensitiveEqualHelper.Equals(list1, list2); Assert.False(equals); } [Fact] public void Equals_First_List_Null_Returns_False() { var list2 = new string[] { "item1", "item2" }; var equals = CaseInsensitiveEqualHelper.Equals(null, list2); Assert.False(equals); } [Fact] public void Equals_Second_List_Null_Returns_False() { var list1 = new string[] { "item1", "item2" }; var equals = CaseInsensitiveEqualHelper.Equals(list1, null); Assert.False(equals); } [Fact] public void Equals_Null_List_Returns_True() { var equals = CaseInsensitiveEqualHelper.Equals(list1: null, list2: null); Assert.True(equals); } } ================================================ FILE: test/ReverseProxy.Tests/Utilities/RandomFactoryTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Xunit; namespace Yarp.ReverseProxy.Utilities.Tests; public class RandomFactoryTests { [Fact] public void RandomFactory_Work() { // Set up the factory. var factory = new RandomFactory(); // Create random class object. var random = factory.CreateRandomInstance(); // Validate. Assert.NotNull(random); // Validate functionality var num = random.Next(5); Assert.InRange(num, 0, 5); } } ================================================ FILE: test/ReverseProxy.Tests/Utilities/TlsFrameHelperTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. using System; using System.Collections.Generic; using System.Linq; using System.Net.Security; using System.Security.Authentication; using Xunit; namespace Yarp.ReverseProxy.Utilities.Tls.Tests; public class TlsFrameHelperTests { [Fact] public void SniHelper_ValidData_Ok() { InvalidClientHello(s_validClientHello, -1, shouldPass: true); } [Theory] [MemberData(nameof(InvalidClientHelloData))] public void SniHelper_InvalidData_Fails(int id, byte[] clientHello) { InvalidClientHello(clientHello, id, shouldPass: false); } [Fact] public void SniHelper_TruncatedData_Fails() { // moving inside one test because there are more than 3000 cases and they overflow subresults foreach ((int id, byte[] clientHello) in InvalidClientHelloDataTruncatedBytes()) { InvalidClientHello(clientHello, id, shouldPass: false); } } private void InvalidClientHello(byte[] clientHello, int id, bool shouldPass) { var ret = TlsFrameHelper.GetServerName(clientHello); if (shouldPass) { Assert.NotNull(ret); } else { Assert.Null(ret); } } [Fact] public void TlsFrameHelper_ValidData_Ok() { TlsFrameHelper.TlsFrameInfo info = default; Assert.True(TlsFrameHelper.TryGetFrameInfo(s_validClientHello, ref info)); Assert.Equal(SslProtocols.Tls12, info.Header.Version); Assert.Equal(203, info.Header.Length); Assert.Equal(SslProtocols.Tls12, info.SupportedVersions); Assert.Equal(TlsFrameHelper.ApplicationProtocolInfo.None, info.ApplicationProtocols); } [Fact] public void TlsFrameHelper_Tls12ClientHello_Ok() { TlsFrameHelper.TlsFrameInfo info = default; Assert.True(TlsFrameHelper.TryGetFrameInfo(s_Tls12ClientHello, ref info)); Assert.Equal(SslProtocols.Tls, info.Header.Version); Assert.Equal(SslProtocols.Tls | SslProtocols.Tls12, info.SupportedVersions); Assert.Equal(TlsFrameHelper.ApplicationProtocolInfo.Http11 | TlsFrameHelper.ApplicationProtocolInfo.Http2, info.ApplicationProtocols); Assert.Equal(46, info.TlsCipherSuites.Length); int expectedCiphersCount = 0; for (int i = 0; i < info.TlsCipherSuites.Length; i++) { // spotcheck on ciphers if (info.TlsCipherSuites.Span[i] == TlsCipherSuite.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384 || info.TlsCipherSuites.Span[i] == TlsCipherSuite.TLS_RSA_WITH_AES_128_GCM_SHA256 || info.TlsCipherSuites.Span[i] == TlsCipherSuite.TLS_RSA_WITH_RC4_128_SHA) { expectedCiphersCount++; } } Assert.Equal(3, expectedCiphersCount); } [Fact] public void TlsFrameHelper_Tls13ClientHello_Ok() { TlsFrameHelper.TlsFrameInfo info = default; Assert.True(TlsFrameHelper.TryGetFrameInfo(s_Tls13ClientHello, ref info)); Assert.Equal(SslProtocols.Tls, info.Header.Version); Assert.Equal(SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12 | SslProtocols.Tls13, info.SupportedVersions); Assert.Equal(TlsFrameHelper.ApplicationProtocolInfo.Other, info.ApplicationProtocols); Assert.Equal(6, info.TlsCipherSuites.Length); int expectedCiphersCount = 0; for (int i = 0; i < info.TlsCipherSuites.Length; i++) { if (info.TlsCipherSuites.Span[i] == TlsCipherSuite.TLS_AES_256_GCM_SHA384 || info.TlsCipherSuites.Span[i] == TlsCipherSuite.TLS_CHACHA20_POLY1305_SHA256 || info.TlsCipherSuites.Span[i] == TlsCipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) { expectedCiphersCount++; } } Assert.Equal(3, expectedCiphersCount); } [Fact] public void TlsFrameHelper_UnifiedClientHello_Ok() { TlsFrameHelper.TlsFrameInfo info = default; Assert.True(TlsFrameHelper.TryGetFrameInfo(s_UnifiedHello, ref info)); #pragma warning disable CS0618 // Ssl2 and Ssl3 are obsolete Assert.Equal(SslProtocols.Ssl2, info.Header.Version); Assert.Equal(SslProtocols.Ssl2 | SslProtocols.Tls, info.SupportedVersions); #pragma warning restore CS0618 Assert.Equal(TlsContentType.Handshake, info.Header.Type); Assert.Equal(TlsFrameHelper.ApplicationProtocolInfo.None, info.ApplicationProtocols); Assert.Equal(TlsHandshakeType.ClientHello, info.HandshakeType); } [Fact] public void TlsFrameHelper_TlsClientHelloNoExtensions_Ok() { TlsFrameHelper.TlsFrameInfo info = default; Assert.True(TlsFrameHelper.TryGetFrameInfo(s_TlsClientHelloNoExtensions, ref info)); Assert.Equal(TlsFrameHelper.ParsingStatus.Ok, info.ParsingStatus); Assert.Equal(SslProtocols.Tls12, info.Header.Version); Assert.Equal(SslProtocols.Tls12, info.SupportedVersions); Assert.Equal(TlsContentType.Handshake, info.Header.Type); Assert.Equal(TlsFrameHelper.ApplicationProtocolInfo.None, info.ApplicationProtocols); Assert.Equal(TlsHandshakeType.ClientHello, info.HandshakeType); } [Fact] public void TlsFrameHelper_Tls12ServerHello_Ok() { TlsFrameHelper.TlsFrameInfo info = default; Assert.True(TlsFrameHelper.TryGetFrameInfo(s_Tls12ServerHello, ref info)); Assert.Equal(TlsFrameHelper.ParsingStatus.Ok, info.ParsingStatus); Assert.Equal(SslProtocols.Tls12, info.Header.Version); Assert.Equal(SslProtocols.Tls12, info.SupportedVersions); Assert.Equal(TlsFrameHelper.ApplicationProtocolInfo.Http2, info.ApplicationProtocols); } [Fact] public void TlsFrameHelper_FragmentedClientHello_Fails() { TlsFrameHelper.TlsFrameInfo info = default; Assert.False(TlsFrameHelper.TryGetFrameInfo(s_Tls13FragmentedClientHello, ref info)); Assert.Equal(TlsFrameHelper.ParsingStatus.InvalidFrame, info.ParsingStatus); } public static IEnumerable InvalidClientHelloData() { int id = 0; foreach (byte[] invalidClientHello in InvalidClientHello()) { id++; yield return new object[] { id, invalidClientHello }; } } public static IEnumerable> InvalidClientHelloDataTruncatedBytes() { // converting to base64 first to remove duplicated test cases var uniqueInvalidHellos = new HashSet(); foreach (byte[] invalidClientHello in InvalidClientHello()) { for (int i = 0; i < invalidClientHello.Length; i++) { uniqueInvalidHellos.Add(Convert.ToBase64String(invalidClientHello.Take(i).ToArray())); } } for (int i = 0; i < s_validClientHello.Length; i++) { uniqueInvalidHellos.Add(Convert.ToBase64String(s_validClientHello.Take(i).ToArray())); } int id = 0; foreach (string invalidClientHello in uniqueInvalidHellos) { id++; yield return new Tuple(id, Convert.FromBase64String(invalidClientHello)); } } private static readonly byte[] s_validClientHello = new byte[] { // SslPlainText.(ContentType+ProtocolVersion) 0x16, 0x03, 0x03, // SslPlainText.length 0x00, 0xCB, // Handshake.msg_type (client hello) 0x01, // Handshake.length 0x00, 0x00, 0xC7, // ClientHello.client_version 0x03, 0x03, // ClientHello.random 0x0C, 0x3C, 0x85, 0x78, 0xCA, 0x67, 0x70, 0xAA, 0x38, 0xCB, 0x28, 0xBC, 0xDC, 0x3E, 0x30, 0xBF, 0x11, 0x96, 0x95, 0x1A, 0xB9, 0xF0, 0x99, 0xA4, 0x91, 0x09, 0x13, 0xB4, 0x89, 0x94, 0x27, 0x2E, // ClientHello.SessionId 0x00, // ClientHello.cipher_suites 0x00, 0x2A, 0xC0, 0x2C, 0xC0, 0x2B, 0xC0, 0x30, 0xC0, 0x2F, 0x00, 0x9F, 0x00, 0x9E, 0xC0, 0x24, 0xC0, 0x23, 0xC0, 0x28, 0xC0, 0x27, 0xC0, 0x0A, 0xC0, 0x09, 0xC0, 0x14, 0xC0, 0x13, 0x00, 0x9D, 0x00, 0x9C, 0x00, 0x3D, 0x00, 0x3C, 0x00, 0x35, 0x00, 0x2F, 0x00, 0x0A, // ClientHello.compression_methods 0x01, 0x01, // ClientHello.extension_list_length 0x00, 0x74, // Extension.extension_type (server_name) 0x00, 0x00, // ServerNameListExtension.length 0x00, 0x39, // ServerName.length 0x00, 0x37, // ServerName.type 0x00, // HostName.length 0x00, 0x34, // HostName.bytes 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, // Extension.extension_type (00 0A) 0x00, 0x0A, // Extension 0A 0x00, 0x08, 0x00, 0x06, 0x00, 0x1D, 0x00, 0x17, 0x00, 0x18, // Extension.extension_type (00 0B) 0x00, 0x0B, // Extension 0B 0x00, 0x02, 0x01, 0x00, // Extension.extension_type (00 0D) 0x00, 0x0D, // Extension 0D 0x00, 0x14, 0x00, 0x12, 0x04, 0x01, 0x05, 0x01, 0x02, 0x01, 0x04, 0x03, 0x05, 0x03, 0x02, 0x03, 0x02, 0x02, 0x06, 0x01, 0x06, 0x03, // Extension.extension_type (00 23) 0x00, 0x23, // Extension 00 23 0x00, 0x00, // Extension.extension_type (00 17) 0x00, 0x17, // Extension 17 0x00, 0x00, // Extension.extension_type (FF 01) 0xFF, 0x01, // Extension FF01 0x00, 0x01, 0x00 }; private static readonly byte[] s_Tls12ClientHello = new byte[] { // SslPlainText.(ContentType+ProtocolVersion) 0x16, 0x03, 0x01, // SslPlainText.length 0x00, 0xD1, // Handshake.msg_type (client hello) 0x01, // Handshake.length 0x00, 0x00, 0xCD, // ClientHello.client_version 0x03, 0x03, // ClientHello.random 0x0C, 0x3C, 0x85, 0x78, 0xCA, 0x67, 0x70, 0xAA, 0x38, 0xCB, 0x28, 0xBC, 0xDC, 0x3E, 0x30, 0xBF, 0x11, 0x96, 0x95, 0x1A, 0xB9, 0xF0, 0x99, 0xA4, 0x91, 0x09, 0x13, 0xB4, 0x89, 0x94, 0x27, 0x2E, // ClientHello.SessionId 0x00, // ClientHello.cipher_suites_length 0x00, 0x5C, // ClientHello.cipher_suites 0xC0, 0x30, 0xC0, 0x2C, 0xC0, 0x28, 0xC0, 0x24, 0xC0, 0x14, 0xC0, 0x0A, 0x00, 0x9f, 0x00, 0x6B, 0x00, 0x39, 0xCC, 0xA9, 0xCC, 0xA8, 0xCC, 0xAA, 0xFF, 0x85, 0x00, 0xC4, 0x00, 0x88, 0x00, 0x81, 0x00, 0x9D, 0x00, 0x3D, 0x00, 0x35, 0x00, 0xC0, 0x00, 0x84, 0xC0, 0x2f, 0xC0, 0x2B, 0xC0, 0x27, 0xC0, 0x23, 0xC0, 0x13, 0xC0, 0x09, 0x00, 0x9E, 0x00, 0x67, 0x00, 0x33, 0x00, 0xBE, 0x00, 0x45, 0x00, 0x9C, 0x00, 0x3C, 0x00, 0x2F, 0x00, 0xBA, 0x00, 0x41, 0xC0, 0x11, 0xC0, 0x07, 0x00, 0x05, 0x00, 0x04, 0xC0, 0x12, 0xC0, 0x08, 0x00, 0x16, 0x00, 0x0a, 0x00, 0xff, // ClientHello.compression_methods 0x01, 0x01, // ClientHello.extension_list_length 0x00, 0x48, // Extension.extension_type (ec_point_formats) 0x00, 0x0b, 0x00, 0x02, 0x01, 0x00, // Extension.extension_type (supported_groups) 0x00, 0x0A, 0x00, 0x08, 0x00, 0x06, 0x00, 0x1D, 0x00, 0x17, 0x00, 0x18, // Extension.extension_type (session_ticket) 0x00, 0x23, 0x00, 0x00, // Extension.extension_type (signature_algorithms) 0x00, 0x0D, 0x00, 0x1C, 0x00, 0x1A, 0x06, 0x01, 0x06, 0x03, 0xEF, 0xEF, 0x05, 0x01, 0x05, 0x03, 0x04, 0x01, 0x04, 0x03, 0xEE, 0xEE, 0xED, 0xED, 0x03, 0x01, 0x03, 0x03, 0x02, 0x01, 0x02, 0x03, // Extension.extension_type (application_level_Protocol) 0x00, 0x10, 0x00, 0x0e, 0x00, 0x0C, 0x02, 0x68, 0x32, 0x08, 0x68, 0x74, 0x74, 0x70, 0x2F, 0x31, 0x2E, 0x31 }; private static readonly byte[] s_Tls13ClientHello = new byte[] { // SslPlainText.(ContentType+ProtocolVersion) 0x16, 0x03, 0x01, // SslPlainText.length 0x01, 0x08, // Handshake.msg_type (client hello) 0x01, // Handshake.length 0x00, 0x01, 0x04, // ClientHello.client_version 0x03, 0x03, // ClientHello.random 0x0C, 0x3C, 0x85, 0x78, 0xCA, 0x67, 0x70, 0xAA, 0x38, 0xCB, 0x28, 0xBC, 0xDC, 0x3E, 0x30, 0xBF, 0x11, 0x96, 0x95, 0x1A, 0xB9, 0xF0, 0x99, 0xA4, 0x91, 0x09, 0x13, 0xB4, 0x89, 0x94, 0x27, 0x2E, // ClientHello.SessionId_Length 0x20, // ClientHello.SessionId 0x0C, 0x3C, 0x85, 0x78, 0xCA, 0x67, 0x70, 0xAA, 0x38, 0xCB, 0x28, 0xBC, 0xDC, 0x3E, 0x30, 0xBF, 0x11, 0x96, 0x95, 0x1A, 0xB9, 0xF0, 0x99, 0xA4, 0x91, 0x09, 0x13, 0xB4, 0x89, 0x94, 0x27, 0x2E, // ClientHello.cipher_suites_length 0x00, 0x0C, // ClientHello.cipher_suites 0x13, 0x02, 0x13, 0x03, 0x13, 0x01, 0xC0, 0x14, 0xc0, 0x30, 0x00, 0xFF, // ClientHello.compression_methods 0x01, 0x00, // ClientHello.extension_list_length 0x00, 0xAF, // Extension.extension_type (server_name) (10.211.55.2) 0x00, 0x00, 0x00, 0x10, 0x00, 0x0e, 0x00, 0x00, 0x0B, 0x31, 0x30, 0x2E, 0x32, 0x31, 0x31, 0x2E, 0x35, 0x35, 0x2E, 0x32, // Extension.extension_type (ec_point_formats) 0x00, 0x0B, 0x00, 0x04, 0x03, 0x00, 0x01, 0x02, // Extension.extension_type (supported_groups) 0x00, 0x0A, 0x00, 0x0C, 0x00, 0x0A, 0x00, 0x1D, 0x00, 0x17, 0x00, 0x1E, 0x00, 0x19, 0x00, 0x18, // Extension.extension_type (application_level_Protocol) (boo) 0x00, 0x10, 0x00, 0x06, 0x00, 0x04, 0x03, 0x62, 0x6f, 0x6f, // Extension.extension_type (encrypt_then_mac) 0x00, 0x16, 0x00, 0x00, // Extension.extension_type (extended_master_key_secret) 0x00, 0x17, 0x00, 0x00, // Extension.extension_type (signature_algorithms) 0x00, 0x0D, 0x00, 0x30, 0x00, 0x2E, 0x06, 0x03, 0xEF, 0xEF, 0x05, 0x01, 0x05, 0x03, 0x06, 0x03, 0xEF, 0xEF, 0x05, 0x01, 0x05, 0x03, 0x06, 0x03, 0xEF, 0xEF, 0x05, 0x01, 0x05, 0x03, 0x04, 0x01, 0x04, 0x03, 0xEE, 0xEE, 0xED, 0xED, 0x03, 0x01, 0x03, 0x03, 0x02, 0x01, 0x02, 0x03, 0x03, 0x01, 0x03, 0x03, 0x02, 0x01, // Extension.extension_type (supported_versions) 0x00, 0x2B, 0x00, 0x09, 0x08, 0x03, 0x04, 0x03, 0x03, 0x03, 0x02, 0x03, 0x01, // Extension.extension_type (psk_key_exchange_modes) 0x00, 0x2D, 0x00, 0x02, 0x01, 0x01, // Extension.extension_type (key_share) 0x00, 0x33, 0x00, 0x26, 0x00, 0x24, 0x00, 0x1D, 0x00, 0x20, 0x04, 0x01, 0x04, 0x03, 0xEE, 0xEE, 0xED, 0xED, 0x03, 0x01, 0x03, 0x03, 0x02, 0x01, 0x02, 0x03, 0x04, 0x01, 0x04, 0x03, 0xEE, 0xEE, 0xED, 0xED, 0x03, 0x01, 0x03, 0x03, 0x02, 0x01, 0x02, 0x03 }; private static readonly byte[] s_Tls12ServerHello = new byte[] { // SslPlainText.(ContentType+ProtocolVersion) 0x16, 0x03, 0x03, // SslPlainText.length 0x00, 0x64, // Handshake.msg_type (srever hello) 0x02, // Handshake.length 0x00, 0x00, 0x60, // ServerHello.client_version 0x03, 0x03, // ServerHello.random 0x0C, 0x3C, 0x85, 0x78, 0xCA, 0x67, 0x70, 0xAA, 0x38, 0xCB, 0x28, 0xBC, 0xDC, 0x3E, 0x30, 0xBF, 0x11, 0x96, 0x95, 0x1A, 0xB9, 0xF0, 0x99, 0xA4, 0x91, 0x09, 0x13, 0xB4, 0x89, 0x94, 0x27, 0x2E, // ServerHello.SessionId_Length 0x20, // ServerHello.SessionId 0x0C, 0x3C, 0x85, 0x78, 0xCA, 0x67, 0x70, 0xAA, 0x38, 0xCB, 0x28, 0xBC, 0xDC, 0x3E, 0x30, 0xBF, 0x11, 0x96, 0x95, 0x1A, 0xB9, 0xF0, 0x99, 0xA4, 0x91, 0x09, 0x13, 0xB4, 0x89, 0x94, 0x27, 0x2E, // ServerHello.cipher_suite 0xC0, 0x2B, // ServerHello.compression_method 0x00, // ClientHello.extension_list_length 0x00, 0x18, // Extension.extension_type (extended_master_secreet) 0x00, 0x17, 0x00, 0x00, // Extension.extension_type (renegotiation_info) 0xFF, 0x01, 0x00, 0x01, 0x00, // Extension.extension_type (ec_point_formats) 0x00, 0x0B, 0x00, 0x02, 0x01, 0x00, // Extension.extension_type (application_level_Protocol) 0x00, 0x10, 0x00, 0x05, 0x00, 0x03, 0x02, 0x68, 0x32, }; private static readonly byte[] s_UnifiedHello = new byte[] { // Length 0x80, 0x49, // ClientHello 0x01, // Version 0x03, 0x01, 0x00, 0x30, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x2F, 0x00, 0x00, 0x35, 0x00, 0x00, 0x04, 0x00, 0x00, 0x05, 0x00, 0x00, 0x0A, 0x01, 0x00, 0x80, 0x07, 0x00, 0xC0, 0x03, 0x00, 0x80, 0x00, 0x00, 0x09, 0x06, 0x00, 0x40, 0x00, 0x00, 0x64, 0x00, 0x00, 0x62, 0x00, 0x00, 0x03, 0x00, 0x00, 0x06, 0x02, 0x00, 0x80, 0x04, 0x00, 0x80, 0x5B, 0x0B, 0xA1, 0xEB, 0xBF, 0x2D, 0x57, 0xF5, 0xD1, 0x0F, 0x52, 0x3B, 0x12, 0x9C, 0xF8, 0xD4, }; private static readonly byte[] s_TlsClientHelloNoExtensions = new byte[] { 0x16, 0x03, 0x03, 0x00, 0x39, 0x01, 0x00, 0x00, 0x35, 0x03, 0x03, 0x62, 0x5d, 0x50, 0x2a, 0x41, 0x2f, 0xd8, 0xc3, 0x65, 0x35, 0xea, 0x01, 0x70, 0x03, 0x7e, 0x7e, 0x2d, 0xd4, 0xfe, 0x93, 0x39, 0xa4, 0x04, 0x66, 0xbb, 0x46, 0x91, 0x41, 0xc3, 0x48, 0x87, 0x3d, 0x00, 0x00, 0x0e, 0x00, 0x3d, 0x00, 0x3c, 0x00, 0x0a, 0x00, 0x35, 0x00, 0x2f, 0x00, 0x05, 0x00, 0x04, 0x01, 0x00 }; private static readonly byte[] s_Tls13FragmentedClientHello = new byte[] { // SslPlainText.(ContentType+ProtocolVersion) 0x16, 0x03, 0x01, // SslPlainText.length 0x00, 0x04, // Fragmented // Handshake.msg_type (client hello) 0x01, // Handshake.length 0x00, 0x01, 0x04, // Extra fragment header // SslPlainText.(ContentType+ProtocolVersion) 0x16, 0x03, 0x01, // SslPlainText.length 0x01, 0x04, // ClientHello.client_version 0x03, 0x03, // ClientHello.random 0x0C, 0x3C, 0x85, 0x78, 0xCA, 0x67, 0x70, 0xAA, 0x38, 0xCB, 0x28, 0xBC, 0xDC, 0x3E, 0x30, 0xBF, 0x11, 0x96, 0x95, 0x1A, 0xB9, 0xF0, 0x99, 0xA4, 0x91, 0x09, 0x13, 0xB4, 0x89, 0x94, 0x27, 0x2E, // ClientHello.SessionId_Length 0x20, // ClientHello.SessionId 0x0C, 0x3C, 0x85, 0x78, 0xCA, 0x67, 0x70, 0xAA, 0x38, 0xCB, 0x28, 0xBC, 0xDC, 0x3E, 0x30, 0xBF, 0x11, 0x96, 0x95, 0x1A, 0xB9, 0xF0, 0x99, 0xA4, 0x91, 0x09, 0x13, 0xB4, 0x89, 0x94, 0x27, 0x2E, // ClientHello.cipher_suites_length 0x00, 0x0C, // ClientHello.cipher_suites 0x13, 0x02, 0x13, 0x03, 0x13, 0x01, 0xC0, 0x14, 0xc0, 0x30, 0x00, 0xFF, // ClientHello.compression_methods 0x01, 0x00, // ClientHello.extension_list_length 0x00, 0xAF, // Extension.extension_type (server_name) (10.211.55.2) 0x00, 0x00, 0x00, 0x10, 0x00, 0x0e, 0x00, 0x00, 0x0B, 0x31, 0x30, 0x2E, 0x32, 0x31, 0x31, 0x2E, 0x35, 0x35, 0x2E, 0x32, // Extension.extension_type (ec_point_formats) 0x00, 0x0B, 0x00, 0x04, 0x03, 0x00, 0x01, 0x02, // Extension.extension_type (supported_groups) 0x00, 0x0A, 0x00, 0x0C, 0x00, 0x0A, 0x00, 0x1D, 0x00, 0x17, 0x00, 0x1E, 0x00, 0x19, 0x00, 0x18, // Extension.extension_type (application_level_Protocol) (boo) 0x00, 0x10, 0x00, 0x06, 0x00, 0x04, 0x03, 0x62, 0x6f, 0x6f, // Extension.extension_type (encrypt_then_mac) 0x00, 0x16, 0x00, 0x00, // Extension.extension_type (extended_master_key_secret) 0x00, 0x17, 0x00, 0x00, // Extension.extension_type (signature_algorithms) 0x00, 0x0D, 0x00, 0x30, 0x00, 0x2E, 0x06, 0x03, 0xEF, 0xEF, 0x05, 0x01, 0x05, 0x03, 0x06, 0x03, 0xEF, 0xEF, 0x05, 0x01, 0x05, 0x03, 0x06, 0x03, 0xEF, 0xEF, 0x05, 0x01, 0x05, 0x03, 0x04, 0x01, 0x04, 0x03, 0xEE, 0xEE, 0xED, 0xED, 0x03, 0x01, 0x03, 0x03, 0x02, 0x01, 0x02, 0x03, 0x03, 0x01, 0x03, 0x03, 0x02, 0x01, // Extension.extension_type (supported_versions) 0x00, 0x2B, 0x00, 0x09, 0x08, 0x03, 0x04, 0x03, 0x03, 0x03, 0x02, 0x03, 0x01, // Extension.extension_type (psk_key_exchange_modes) 0x00, 0x2D, 0x00, 0x02, 0x01, 0x01, // Extension.extension_type (key_share) 0x00, 0x33, 0x00, 0x26, 0x00, 0x24, 0x00, 0x1D, 0x00, 0x20, 0x04, 0x01, 0x04, 0x03, 0xEE, 0xEE, 0xED, 0xED, 0x03, 0x01, 0x03, 0x03, 0x02, 0x01, 0x02, 0x03, 0x04, 0x01, 0x04, 0x03, 0xEE, 0xEE, 0xED, 0xED, 0x03, 0x01, 0x03, 0x03, 0x02, 0x01, 0x02, 0x03 }; private static IEnumerable InvalidClientHello() { // This test covers following test cases: // - Length of structure off by 1 (search for "length off by 1") // - Length of structure is max length (search for "max length") // - Type is invalid or unknown (i.e. SslPlainText.ClientType is not 0x16 - search for "unknown") // - Invalid utf-8 characters // in each case sni will be null or will cause parsing error - we only expect some parsing errors, // anything else is considered a bug yield return new byte[] { // SslPlainText.(ContentType+ProtocolVersion) 0x16, 0x03, 0x03, // SslPlainText.length 0x00, 0xCB, // Handshake.msg_type (client hello) 0x01, // Handshake.length 0x00, 0x00, 0xC7, // ClientHello.client_version 0x03, 0x03, // ClientHello.random 0x58, 0xAA, 0x5F, 0xE7, 0x22, 0xCF, 0x9F, 0x59, 0x8A, 0xC5, 0x8B, 0x87, 0xC7, 0x62, 0x32, 0x98, 0xD4, 0xD8, 0xA2, 0xBE, 0x77, 0xCE, 0xA9, 0xCE, 0x42, 0x25, 0x5A, 0x8B, 0xEE, 0x16, 0x80, 0xF1, // ClientHello.SessionId 0x00, // ClientHello.cipher_suites 0x00, 0x2A, 0xC0, 0x2C, 0xC0, 0x2B, 0xC0, 0x30, 0xC0, 0x2F, 0x00, 0x9F, 0x00, 0x9E, 0xC0, 0x24, 0xC0, 0x23, 0xC0, 0x28, 0xC0, 0x27, 0xC0, 0x0A, 0xC0, 0x09, 0xC0, 0x14, 0xC0, 0x13, 0x00, 0x9D, 0x00, 0x9C, 0x00, 0x3D, 0x00, 0x3C, 0x00, 0x35, 0x00, 0x2F, 0x00, 0x0A, // ClientHello.compression_methods 0x01, 0x01, // ClientHello.extension_list_length 0x00, 0x74, // Extension.extension_type (server_name) 0x00, 0x00, // ServerNameListExtension.length 0x00, 0x39, // ServerName.length 0x00, 0x37, // ServerName.type 0x00, // HostName.length 0x00, 0x34, // HostName.bytes 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, // Extension.extension_type (00 0A) 0x00, 0x0A, // Extension 0A 0x00, 0x08, 0x00, 0x06, 0x00, 0x1D, 0x00, 0x17, 0x00, 0x18, // Extension.extension_type (00 0B) 0x00, 0x0B, // Extension 0B 0x00, 0x02, 0x01, 0x00, // Extension.extension_type (00 0D) 0x00, 0x0D, // Extension 0D 0x00, 0x14, 0x00, 0x12, 0x04, 0x01, 0x05, 0x01, 0x02, 0x01, 0x04, 0x03, 0x05, 0x03, 0x02, 0x03, 0x02, 0x02, 0x06, 0x01, 0x06, 0x03, // Extension.extension_type (00 23) 0x00, 0x23, // Extension 00 23 0x00, 0x00, // Extension.extension_type (00 17) 0x00, 0x17, // Extension 17 0x00, 0x00, // Extension.extension_type (FF 01) 0xFF, 0x01, // Extension FF01 - length off by 1 0x00, 0x02, 0x00 }; // #2 yield return new byte[] { // SslPlainText.(ContentType+ProtocolVersion) 0x16, 0x03, 0x03, // SslPlainText.length 0x00, 0xCB, // Handshake.msg_type (client hello) 0x01, // Handshake.length 0x00, 0x00, 0xC7, // ClientHello.client_version 0x03, 0x03, // ClientHello.random 0x58, 0xAA, 0x5F, 0xE7, 0x22, 0xCF, 0x9F, 0x59, 0x8A, 0xC5, 0x8B, 0x87, 0xC7, 0x62, 0x32, 0x98, 0xD4, 0xD8, 0xA2, 0xBE, 0x77, 0xCE, 0xA9, 0xCE, 0x42, 0x25, 0x5A, 0x8B, 0xEE, 0x16, 0x80, 0xF1, // ClientHello.SessionId 0x00, // ClientHello.cipher_suites 0x00, 0x2A, 0xC0, 0x2C, 0xC0, 0x2B, 0xC0, 0x30, 0xC0, 0x2F, 0x00, 0x9F, 0x00, 0x9E, 0xC0, 0x24, 0xC0, 0x23, 0xC0, 0x28, 0xC0, 0x27, 0xC0, 0x0A, 0xC0, 0x09, 0xC0, 0x14, 0xC0, 0x13, 0x00, 0x9D, 0x00, 0x9C, 0x00, 0x3D, 0x00, 0x3C, 0x00, 0x35, 0x00, 0x2F, 0x00, 0x0A, // ClientHello.compression_methods 0x01, 0x01, // ClientHello.extension_list_length 0x00, 0x74, // Extension.extension_type (server_name) 0x00, 0x00, // ServerNameListExtension.length 0x00, 0x39, // ServerName.length 0x00, 0x37, // ServerName.type 0x00, // HostName.length 0x00, 0x34, // HostName.bytes 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, // Extension.extension_type (00 0A) 0x00, 0x0A, // Extension 0A 0x00, 0x08, 0x00, 0x06, 0x00, 0x1D, 0x00, 0x17, 0x00, 0x18, // Extension.extension_type (00 0B) 0x00, 0x0B, // Extension 0B 0x00, 0x02, 0x01, 0x00, // Extension.extension_type (00 0D) 0x00, 0x0D, // Extension 0D 0x00, 0x14, 0x00, 0x12, 0x04, 0x01, 0x05, 0x01, 0x02, 0x01, 0x04, 0x03, 0x05, 0x03, 0x02, 0x03, 0x02, 0x02, 0x06, 0x01, 0x06, 0x03, // Extension.extension_type (00 23) 0x00, 0x23, // Extension 00 23 0x00, 0x00, // Extension.extension_type (00 17) 0x00, 0x17, // Extension 17 0x00, 0x00, // Extension.extension_type (FF 01) 0xFF, 0x01, // Extension FF01 - max length 0xFF, 0xFF, 0x00 }; // #3 yield return new byte[] { // SslPlainText.(ContentType+ProtocolVersion) 0x16, 0x03, 0x03, // SslPlainText.length 0x00, 0xCB, // Handshake.msg_type (client hello) 0x01, // Handshake.length 0x00, 0x00, 0xC7, // ClientHello.client_version 0x03, 0x03, // ClientHello.random 0x58, 0xAA, 0x5F, 0xE7, 0x22, 0xCF, 0x9F, 0x59, 0x8A, 0xC5, 0x8B, 0x87, 0xC7, 0x62, 0x32, 0x98, 0xD4, 0xD8, 0xA2, 0xBE, 0x77, 0xCE, 0xA9, 0xCE, 0x42, 0x25, 0x5A, 0x8B, 0xEE, 0x16, 0x80, 0xF1, // ClientHello.SessionId 0x00, // ClientHello.cipher_suites 0x00, 0x2A, 0xC0, 0x2C, 0xC0, 0x2B, 0xC0, 0x30, 0xC0, 0x2F, 0x00, 0x9F, 0x00, 0x9E, 0xC0, 0x24, 0xC0, 0x23, 0xC0, 0x28, 0xC0, 0x27, 0xC0, 0x0A, 0xC0, 0x09, 0xC0, 0x14, 0xC0, 0x13, 0x00, 0x9D, 0x00, 0x9C, 0x00, 0x3D, 0x00, 0x3C, 0x00, 0x35, 0x00, 0x2F, 0x00, 0x0A, // ClientHello.compression_methods 0x01, 0x01, // ClientHello.extension_list_length 0x00, 0x74, // Extension.extension_type (server_name) 0x00, 0x00, // ServerNameListExtension.length 0x00, 0x39, // ServerName.length 0x00, 0x37, // ServerName.type 0x00, // HostName.length 0x00, 0x34, // HostName.bytes 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, // Extension.extension_type (00 0A) 0x00, 0x0A, // Extension 0A 0x00, 0x08, 0x00, 0x06, 0x00, 0x1D, 0x00, 0x17, 0x00, 0x18, // Extension.extension_type (00 0B) 0x00, 0x0B, // Extension 0B 0x00, 0x02, 0x01, 0x00, // Extension.extension_type (00 0D) 0x00, 0x0D, // Extension 0D 0x00, 0x14, 0x00, 0x12, 0x04, 0x01, 0x05, 0x01, 0x02, 0x01, 0x04, 0x03, 0x05, 0x03, 0x02, 0x03, 0x02, 0x02, 0x06, 0x01, 0x06, 0x03, // Extension.extension_type (00 23) 0x00, 0x23, // Extension 00 23 0x00, 0x00, // Extension.extension_type (00 17) 0x00, 0x17, // Extension 17 - length off by 1 0x00, 0x01, // Extension.extension_type (FF 01) 0xFF, 0x01, // Extension FF01 0x00, 0x01, 0x00 }; // #4 yield return new byte[] { // SslPlainText.(ContentType+ProtocolVersion) 0x16, 0x03, 0x03, // SslPlainText.length 0x00, 0xCB, // Handshake.msg_type (client hello) 0x01, // Handshake.length 0x00, 0x00, 0xC7, // ClientHello.client_version 0x03, 0x03, // ClientHello.random 0x58, 0xAA, 0x5F, 0xE7, 0x22, 0xCF, 0x9F, 0x59, 0x8A, 0xC5, 0x8B, 0x87, 0xC7, 0x62, 0x32, 0x98, 0xD4, 0xD8, 0xA2, 0xBE, 0x77, 0xCE, 0xA9, 0xCE, 0x42, 0x25, 0x5A, 0x8B, 0xEE, 0x16, 0x80, 0xF1, // ClientHello.SessionId 0x00, // ClientHello.cipher_suites 0x00, 0x2A, 0xC0, 0x2C, 0xC0, 0x2B, 0xC0, 0x30, 0xC0, 0x2F, 0x00, 0x9F, 0x00, 0x9E, 0xC0, 0x24, 0xC0, 0x23, 0xC0, 0x28, 0xC0, 0x27, 0xC0, 0x0A, 0xC0, 0x09, 0xC0, 0x14, 0xC0, 0x13, 0x00, 0x9D, 0x00, 0x9C, 0x00, 0x3D, 0x00, 0x3C, 0x00, 0x35, 0x00, 0x2F, 0x00, 0x0A, // ClientHello.compression_methods 0x01, 0x01, // ClientHello.extension_list_length 0x00, 0x74, // Extension.extension_type (server_name) 0x00, 0x00, // ServerNameListExtension.length 0x00, 0x39, // ServerName.length 0x00, 0x37, // ServerName.type 0x00, // HostName.length 0x00, 0x34, // HostName.bytes 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, // Extension.extension_type (00 0A) 0x00, 0x0A, // Extension 0A 0x00, 0x08, 0x00, 0x06, 0x00, 0x1D, 0x00, 0x17, 0x00, 0x18, // Extension.extension_type (00 0B) 0x00, 0x0B, // Extension 0B 0x00, 0x02, 0x01, 0x00, // Extension.extension_type (00 0D) 0x00, 0x0D, // Extension 0D 0x00, 0x14, 0x00, 0x12, 0x04, 0x01, 0x05, 0x01, 0x02, 0x01, 0x04, 0x03, 0x05, 0x03, 0x02, 0x03, 0x02, 0x02, 0x06, 0x01, 0x06, 0x03, // Extension.extension_type (00 23) 0x00, 0x23, // Extension 00 23 0x00, 0x00, // Extension.extension_type (00 17) 0x00, 0x17, // Extension 17 - max length 0xFF, 0xFF, // Extension.extension_type (FF 01) 0xFF, 0x01, // Extension FF01 0x00, 0x01, 0x00 }; // #5 yield return new byte[] { // SslPlainText.(ContentType+ProtocolVersion) 0x16, 0x03, 0x03, // SslPlainText.length 0x00, 0xCB, // Handshake.msg_type (client hello) 0x01, // Handshake.length 0x00, 0x00, 0xC7, // ClientHello.client_version 0x03, 0x03, // ClientHello.random 0x58, 0xAA, 0x5F, 0xE7, 0x22, 0xCF, 0x9F, 0x59, 0x8A, 0xC5, 0x8B, 0x87, 0xC7, 0x62, 0x32, 0x98, 0xD4, 0xD8, 0xA2, 0xBE, 0x77, 0xCE, 0xA9, 0xCE, 0x42, 0x25, 0x5A, 0x8B, 0xEE, 0x16, 0x80, 0xF1, // ClientHello.SessionId 0x00, // ClientHello.cipher_suites 0x00, 0x2A, 0xC0, 0x2C, 0xC0, 0x2B, 0xC0, 0x30, 0xC0, 0x2F, 0x00, 0x9F, 0x00, 0x9E, 0xC0, 0x24, 0xC0, 0x23, 0xC0, 0x28, 0xC0, 0x27, 0xC0, 0x0A, 0xC0, 0x09, 0xC0, 0x14, 0xC0, 0x13, 0x00, 0x9D, 0x00, 0x9C, 0x00, 0x3D, 0x00, 0x3C, 0x00, 0x35, 0x00, 0x2F, 0x00, 0x0A, // ClientHello.compression_methods 0x01, 0x01, // ClientHello.extension_list_length 0x00, 0x74, // Extension.extension_type (server_name) 0x00, 0x00, // ServerNameListExtension.length 0x00, 0x39, // ServerName.length 0x00, 0x37, // ServerName.type 0x00, // HostName.length 0x00, 0x34, // HostName.bytes 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, // Extension.extension_type (00 0A) 0x00, 0x0A, // Extension 0A 0x00, 0x08, 0x00, 0x06, 0x00, 0x1D, 0x00, 0x17, 0x00, 0x18, // Extension.extension_type (00 0B) 0x00, 0x0B, // Extension 0B 0x00, 0x02, 0x01, 0x00, // Extension.extension_type (00 0D) 0x00, 0x0D, // Extension 0D 0x00, 0x14, 0x00, 0x12, 0x04, 0x01, 0x05, 0x01, 0x02, 0x01, 0x04, 0x03, 0x05, 0x03, 0x02, 0x03, 0x02, 0x02, 0x06, 0x01, 0x06, 0x03, // Extension.extension_type (00 23) 0x00, 0x23, // Extension 00 23 - length off by 1 0x00, 0x01, // Extension.extension_type (00 17) 0x00, 0x17, // Extension 17 0x00, 0x00, // Extension.extension_type (FF 01) 0xFF, 0x01, // Extension FF01 0x00, 0x01, 0x00 }; // #6 yield return new byte[] { // SslPlainText.(ContentType+ProtocolVersion) 0x16, 0x03, 0x03, // SslPlainText.length 0x00, 0xCB, // Handshake.msg_type (client hello) 0x01, // Handshake.length 0x00, 0x00, 0xC7, // ClientHello.client_version 0x03, 0x03, // ClientHello.random 0x58, 0xAA, 0x5F, 0xE7, 0x22, 0xCF, 0x9F, 0x59, 0x8A, 0xC5, 0x8B, 0x87, 0xC7, 0x62, 0x32, 0x98, 0xD4, 0xD8, 0xA2, 0xBE, 0x77, 0xCE, 0xA9, 0xCE, 0x42, 0x25, 0x5A, 0x8B, 0xEE, 0x16, 0x80, 0xF1, // ClientHello.SessionId 0x00, // ClientHello.cipher_suites 0x00, 0x2A, 0xC0, 0x2C, 0xC0, 0x2B, 0xC0, 0x30, 0xC0, 0x2F, 0x00, 0x9F, 0x00, 0x9E, 0xC0, 0x24, 0xC0, 0x23, 0xC0, 0x28, 0xC0, 0x27, 0xC0, 0x0A, 0xC0, 0x09, 0xC0, 0x14, 0xC0, 0x13, 0x00, 0x9D, 0x00, 0x9C, 0x00, 0x3D, 0x00, 0x3C, 0x00, 0x35, 0x00, 0x2F, 0x00, 0x0A, // ClientHello.compression_methods 0x01, 0x01, // ClientHello.extension_list_length 0x00, 0x74, // Extension.extension_type (server_name) 0x00, 0x00, // ServerNameListExtension.length 0x00, 0x39, // ServerName.length 0x00, 0x37, // ServerName.type 0x00, // HostName.length 0x00, 0x34, // HostName.bytes 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, // Extension.extension_type (00 0A) 0x00, 0x0A, // Extension 0A 0x00, 0x08, 0x00, 0x06, 0x00, 0x1D, 0x00, 0x17, 0x00, 0x18, // Extension.extension_type (00 0B) 0x00, 0x0B, // Extension 0B 0x00, 0x02, 0x01, 0x00, // Extension.extension_type (00 0D) 0x00, 0x0D, // Extension 0D 0x00, 0x14, 0x00, 0x12, 0x04, 0x01, 0x05, 0x01, 0x02, 0x01, 0x04, 0x03, 0x05, 0x03, 0x02, 0x03, 0x02, 0x02, 0x06, 0x01, 0x06, 0x03, // Extension.extension_type (00 23) 0x00, 0x23, // Extension 00 23 - max length 0xFF, 0xFF, // Extension.extension_type (00 17) 0x00, 0x17, // Extension 17 0x00, 0x00, // Extension.extension_type (FF 01) 0xFF, 0x01, // Extension FF01 0x00, 0x01, 0x00 }; // #7 yield return new byte[] { // SslPlainText.(ContentType+ProtocolVersion) 0x16, 0x03, 0x03, // SslPlainText.length 0x00, 0xCB, // Handshake.msg_type (client hello) 0x01, // Handshake.length 0x00, 0x00, 0xC7, // ClientHello.client_version 0x03, 0x03, // ClientHello.random 0x58, 0xAA, 0x5F, 0xE7, 0x22, 0xCF, 0x9F, 0x59, 0x8A, 0xC5, 0x8B, 0x87, 0xC7, 0x62, 0x32, 0x98, 0xD4, 0xD8, 0xA2, 0xBE, 0x77, 0xCE, 0xA9, 0xCE, 0x42, 0x25, 0x5A, 0x8B, 0xEE, 0x16, 0x80, 0xF1, // ClientHello.SessionId 0x00, // ClientHello.cipher_suites 0x00, 0x2A, 0xC0, 0x2C, 0xC0, 0x2B, 0xC0, 0x30, 0xC0, 0x2F, 0x00, 0x9F, 0x00, 0x9E, 0xC0, 0x24, 0xC0, 0x23, 0xC0, 0x28, 0xC0, 0x27, 0xC0, 0x0A, 0xC0, 0x09, 0xC0, 0x14, 0xC0, 0x13, 0x00, 0x9D, 0x00, 0x9C, 0x00, 0x3D, 0x00, 0x3C, 0x00, 0x35, 0x00, 0x2F, 0x00, 0x0A, // ClientHello.compression_methods 0x01, 0x01, // ClientHello.extension_list_length 0x00, 0x74, // Extension.extension_type (server_name) 0x00, 0x00, // ServerNameListExtension.length 0x00, 0x39, // ServerName.length 0x00, 0x37, // ServerName.type 0x00, // HostName.length 0x00, 0x34, // HostName.bytes 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, // Extension.extension_type (00 0A) 0x00, 0x0A, // Extension 0A 0x00, 0x08, 0x00, 0x06, 0x00, 0x1D, 0x00, 0x17, 0x00, 0x18, // Extension.extension_type (00 0B) 0x00, 0x0B, // Extension 0B 0x00, 0x02, 0x01, 0x00, // Extension.extension_type (00 0D) 0x00, 0x0D, // Extension 0D - length off by 1 0x00, 0x15, 0x00, 0x12, 0x04, 0x01, 0x05, 0x01, 0x02, 0x01, 0x04, 0x03, 0x05, 0x03, 0x02, 0x03, 0x02, 0x02, 0x06, 0x01, 0x06, 0x03, // Extension.extension_type (00 23) 0x00, 0x23, // Extension 00 23 0x00, 0x00, // Extension.extension_type (00 17) 0x00, 0x17, // Extension 17 0x00, 0x00, // Extension.extension_type (FF 01) 0xFF, 0x01, // Extension FF01 0x00, 0x01, 0x00 }; // #8 yield return new byte[] { // SslPlainText.(ContentType+ProtocolVersion) 0x16, 0x03, 0x03, // SslPlainText.length 0x00, 0xCB, // Handshake.msg_type (client hello) 0x01, // Handshake.length 0x00, 0x00, 0xC7, // ClientHello.client_version 0x03, 0x03, // ClientHello.random 0x58, 0xAA, 0x5F, 0xE7, 0x22, 0xCF, 0x9F, 0x59, 0x8A, 0xC5, 0x8B, 0x87, 0xC7, 0x62, 0x32, 0x98, 0xD4, 0xD8, 0xA2, 0xBE, 0x77, 0xCE, 0xA9, 0xCE, 0x42, 0x25, 0x5A, 0x8B, 0xEE, 0x16, 0x80, 0xF1, // ClientHello.SessionId 0x00, // ClientHello.cipher_suites 0x00, 0x2A, 0xC0, 0x2C, 0xC0, 0x2B, 0xC0, 0x30, 0xC0, 0x2F, 0x00, 0x9F, 0x00, 0x9E, 0xC0, 0x24, 0xC0, 0x23, 0xC0, 0x28, 0xC0, 0x27, 0xC0, 0x0A, 0xC0, 0x09, 0xC0, 0x14, 0xC0, 0x13, 0x00, 0x9D, 0x00, 0x9C, 0x00, 0x3D, 0x00, 0x3C, 0x00, 0x35, 0x00, 0x2F, 0x00, 0x0A, // ClientHello.compression_methods 0x01, 0x01, // ClientHello.extension_list_length 0x00, 0x74, // Extension.extension_type (server_name) 0x00, 0x00, // ServerNameListExtension.length 0x00, 0x39, // ServerName.length 0x00, 0x37, // ServerName.type 0x00, // HostName.length 0x00, 0x34, // HostName.bytes 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, // Extension.extension_type (00 0A) 0x00, 0x0A, // Extension 0A 0x00, 0x08, 0x00, 0x06, 0x00, 0x1D, 0x00, 0x17, 0x00, 0x18, // Extension.extension_type (00 0B) 0x00, 0x0B, // Extension 0B 0x00, 0x02, 0x01, 0x00, // Extension.extension_type (00 0D) 0x00, 0x0D, // Extension 0D - max length 0xFF, 0xFF, 0x00, 0x12, 0x04, 0x01, 0x05, 0x01, 0x02, 0x01, 0x04, 0x03, 0x05, 0x03, 0x02, 0x03, 0x02, 0x02, 0x06, 0x01, 0x06, 0x03, // Extension.extension_type (00 23) 0x00, 0x23, // Extension 00 23 0x00, 0x00, // Extension.extension_type (00 17) 0x00, 0x17, // Extension 17 0x00, 0x00, // Extension.extension_type (FF 01) 0xFF, 0x01, // Extension FF01 0x00, 0x01, 0x00 }; // #9 yield return new byte[] { // SslPlainText.(ContentType+ProtocolVersion) 0x16, 0x03, 0x03, // SslPlainText.length 0x00, 0xCB, // Handshake.msg_type (client hello) 0x01, // Handshake.length 0x00, 0x00, 0xC7, // ClientHello.client_version 0x03, 0x03, // ClientHello.random 0x58, 0xAA, 0x5F, 0xE7, 0x22, 0xCF, 0x9F, 0x59, 0x8A, 0xC5, 0x8B, 0x87, 0xC7, 0x62, 0x32, 0x98, 0xD4, 0xD8, 0xA2, 0xBE, 0x77, 0xCE, 0xA9, 0xCE, 0x42, 0x25, 0x5A, 0x8B, 0xEE, 0x16, 0x80, 0xF1, // ClientHello.SessionId 0x00, // ClientHello.cipher_suites 0x00, 0x2A, 0xC0, 0x2C, 0xC0, 0x2B, 0xC0, 0x30, 0xC0, 0x2F, 0x00, 0x9F, 0x00, 0x9E, 0xC0, 0x24, 0xC0, 0x23, 0xC0, 0x28, 0xC0, 0x27, 0xC0, 0x0A, 0xC0, 0x09, 0xC0, 0x14, 0xC0, 0x13, 0x00, 0x9D, 0x00, 0x9C, 0x00, 0x3D, 0x00, 0x3C, 0x00, 0x35, 0x00, 0x2F, 0x00, 0x0A, // ClientHello.compression_methods 0x01, 0x01, // ClientHello.extension_list_length 0x00, 0x74, // Extension.extension_type (server_name) 0x00, 0x00, // ServerNameListExtension.length 0x00, 0x39, // ServerName.length 0x00, 0x37, // ServerName.type 0x00, // HostName.length 0x00, 0x34, // HostName.bytes 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, // Extension.extension_type (00 0A) 0x00, 0x0A, // Extension 0A 0x00, 0x08, 0x00, 0x06, 0x00, 0x1D, 0x00, 0x17, 0x00, 0x18, // Extension.extension_type (00 0B) 0x00, 0x0B, // Extension 0B - length off by 1 0x00, 0x03, 0x01, 0x00, // Extension.extension_type (00 0D) 0x00, 0x0D, // Extension 0D 0x00, 0x14, 0x00, 0x12, 0x04, 0x01, 0x05, 0x01, 0x02, 0x01, 0x04, 0x03, 0x05, 0x03, 0x02, 0x03, 0x02, 0x02, 0x06, 0x01, 0x06, 0x03, // Extension.extension_type (00 23) 0x00, 0x23, // Extension 00 23 0x00, 0x00, // Extension.extension_type (00 17) 0x00, 0x17, // Extension 17 0x00, 0x00, // Extension.extension_type (FF 01) 0xFF, 0x01, // Extension FF01 0x00, 0x01, 0x00 }; // #10 yield return new byte[] { // SslPlainText.(ContentType+ProtocolVersion) 0x16, 0x03, 0x03, // SslPlainText.length 0x00, 0xCB, // Handshake.msg_type (client hello) 0x01, // Handshake.length 0x00, 0x00, 0xC7, // ClientHello.client_version 0x03, 0x03, // ClientHello.random 0x58, 0xAA, 0x5F, 0xE7, 0x22, 0xCF, 0x9F, 0x59, 0x8A, 0xC5, 0x8B, 0x87, 0xC7, 0x62, 0x32, 0x98, 0xD4, 0xD8, 0xA2, 0xBE, 0x77, 0xCE, 0xA9, 0xCE, 0x42, 0x25, 0x5A, 0x8B, 0xEE, 0x16, 0x80, 0xF1, // ClientHello.SessionId 0x00, // ClientHello.cipher_suites 0x00, 0x2A, 0xC0, 0x2C, 0xC0, 0x2B, 0xC0, 0x30, 0xC0, 0x2F, 0x00, 0x9F, 0x00, 0x9E, 0xC0, 0x24, 0xC0, 0x23, 0xC0, 0x28, 0xC0, 0x27, 0xC0, 0x0A, 0xC0, 0x09, 0xC0, 0x14, 0xC0, 0x13, 0x00, 0x9D, 0x00, 0x9C, 0x00, 0x3D, 0x00, 0x3C, 0x00, 0x35, 0x00, 0x2F, 0x00, 0x0A, // ClientHello.compression_methods 0x01, 0x01, // ClientHello.extension_list_length 0x00, 0x74, // Extension.extension_type (server_name) 0x00, 0x00, // ServerNameListExtension.length 0x00, 0x39, // ServerName.length 0x00, 0x37, // ServerName.type 0x00, // HostName.length 0x00, 0x34, // HostName.bytes 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, // Extension.extension_type (00 0A) 0x00, 0x0A, // Extension 0A 0x00, 0x08, 0x00, 0x06, 0x00, 0x1D, 0x00, 0x17, 0x00, 0x18, // Extension.extension_type (00 0B) 0x00, 0x0B, // Extension 0B - max length 0xFF, 0xFF, 0x01, 0x00, // Extension.extension_type (00 0D) 0x00, 0x0D, // Extension 0D 0x00, 0x14, 0x00, 0x12, 0x04, 0x01, 0x05, 0x01, 0x02, 0x01, 0x04, 0x03, 0x05, 0x03, 0x02, 0x03, 0x02, 0x02, 0x06, 0x01, 0x06, 0x03, // Extension.extension_type (00 23) 0x00, 0x23, // Extension 00 23 0x00, 0x00, // Extension.extension_type (00 17) 0x00, 0x17, // Extension 17 0x00, 0x00, // Extension.extension_type (FF 01) 0xFF, 0x01, // Extension FF01 0x00, 0x01, 0x00 }; // #11 yield return new byte[] { // SslPlainText.(ContentType+ProtocolVersion) 0x16, 0x03, 0x03, // SslPlainText.length 0x00, 0xCB, // Handshake.msg_type (client hello) 0x01, // Handshake.length 0x00, 0x00, 0xC7, // ClientHello.client_version 0x03, 0x03, // ClientHello.random 0x58, 0xAA, 0x5F, 0xE7, 0x22, 0xCF, 0x9F, 0x59, 0x8A, 0xC5, 0x8B, 0x87, 0xC7, 0x62, 0x32, 0x98, 0xD4, 0xD8, 0xA2, 0xBE, 0x77, 0xCE, 0xA9, 0xCE, 0x42, 0x25, 0x5A, 0x8B, 0xEE, 0x16, 0x80, 0xF1, // ClientHello.SessionId 0x00, // ClientHello.cipher_suites 0x00, 0x2A, 0xC0, 0x2C, 0xC0, 0x2B, 0xC0, 0x30, 0xC0, 0x2F, 0x00, 0x9F, 0x00, 0x9E, 0xC0, 0x24, 0xC0, 0x23, 0xC0, 0x28, 0xC0, 0x27, 0xC0, 0x0A, 0xC0, 0x09, 0xC0, 0x14, 0xC0, 0x13, 0x00, 0x9D, 0x00, 0x9C, 0x00, 0x3D, 0x00, 0x3C, 0x00, 0x35, 0x00, 0x2F, 0x00, 0x0A, // ClientHello.compression_methods 0x01, 0x01, // ClientHello.extension_list_length 0x00, 0x74, // Extension.extension_type (server_name) 0x00, 0x00, // ServerNameListExtension.length 0x00, 0x39, // ServerName.length 0x00, 0x37, // ServerName.type 0x00, // HostName.length 0x00, 0x34, // HostName.bytes 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, // Extension.extension_type (00 0A) 0x00, 0x0A, // Extension 0A - length off by 1 0x00, 0x09, 0x00, 0x06, 0x00, 0x1D, 0x00, 0x17, 0x00, 0x18, // Extension.extension_type (00 0B) 0x00, 0x0B, // Extension 0B 0x00, 0x02, 0x01, 0x00, // Extension.extension_type (00 0D) 0x00, 0x0D, // Extension 0D 0x00, 0x14, 0x00, 0x12, 0x04, 0x01, 0x05, 0x01, 0x02, 0x01, 0x04, 0x03, 0x05, 0x03, 0x02, 0x03, 0x02, 0x02, 0x06, 0x01, 0x06, 0x03, // Extension.extension_type (00 23) 0x00, 0x23, // Extension 00 23 0x00, 0x00, // Extension.extension_type (00 17) 0x00, 0x17, // Extension 17 0x00, 0x00, // Extension.extension_type (FF 01) 0xFF, 0x01, // Extension FF01 0x00, 0x01, 0x00 }; // #10 yield return new byte[] { // SslPlainText.(ContentType+ProtocolVersion) 0x16, 0x03, 0x03, // SslPlainText.length 0x00, 0xCB, // Handshake.msg_type (client hello) 0x01, // Handshake.length 0x00, 0x00, 0xC7, // ClientHello.client_version 0x03, 0x03, // ClientHello.random 0x58, 0xAA, 0x5F, 0xE7, 0x22, 0xCF, 0x9F, 0x59, 0x8A, 0xC5, 0x8B, 0x87, 0xC7, 0x62, 0x32, 0x98, 0xD4, 0xD8, 0xA2, 0xBE, 0x77, 0xCE, 0xA9, 0xCE, 0x42, 0x25, 0x5A, 0x8B, 0xEE, 0x16, 0x80, 0xF1, // ClientHello.SessionId 0x00, // ClientHello.cipher_suites 0x00, 0x2A, 0xC0, 0x2C, 0xC0, 0x2B, 0xC0, 0x30, 0xC0, 0x2F, 0x00, 0x9F, 0x00, 0x9E, 0xC0, 0x24, 0xC0, 0x23, 0xC0, 0x28, 0xC0, 0x27, 0xC0, 0x0A, 0xC0, 0x09, 0xC0, 0x14, 0xC0, 0x13, 0x00, 0x9D, 0x00, 0x9C, 0x00, 0x3D, 0x00, 0x3C, 0x00, 0x35, 0x00, 0x2F, 0x00, 0x0A, // ClientHello.compression_methods 0x01, 0x01, // ClientHello.extension_list_length 0x00, 0x74, // Extension.extension_type (server_name) 0x00, 0x00, // ServerNameListExtension.length 0x00, 0x39, // ServerName.length 0x00, 0x37, // ServerName.type 0x00, // HostName.length 0x00, 0x34, // HostName.bytes 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, // Extension.extension_type (00 0A) 0x00, 0x0A, // Extension 0A - max length 0xFF, 0xFF, 0x00, 0x06, 0x00, 0x1D, 0x00, 0x17, 0x00, 0x18, // Extension.extension_type (00 0B) 0x00, 0x0B, // Extension 0B 0x00, 0x02, 0x01, 0x00, // Extension.extension_type (00 0D) 0x00, 0x0D, // Extension 0D 0x00, 0x14, 0x00, 0x12, 0x04, 0x01, 0x05, 0x01, 0x02, 0x01, 0x04, 0x03, 0x05, 0x03, 0x02, 0x03, 0x02, 0x02, 0x06, 0x01, 0x06, 0x03, // Extension.extension_type (00 23) 0x00, 0x23, // Extension 00 23 0x00, 0x00, // Extension.extension_type (00 17) 0x00, 0x17, // Extension 17 0x00, 0x00, // Extension.extension_type (FF 01) 0xFF, 0x01, // Extension FF01 0x00, 0x01, 0x00 }; // #13 yield return new byte[] { // SslPlainText.(ContentType+ProtocolVersion) 0x16, 0x03, 0x03, // SslPlainText.length 0x00, 0xCB, // Handshake.msg_type (client hello) 0x01, // Handshake.length 0x00, 0x00, 0xC7, // ClientHello.client_version 0x03, 0x03, // ClientHello.random 0x58, 0xAA, 0x5F, 0xE7, 0x22, 0xCF, 0x9F, 0x59, 0x8A, 0xC5, 0x8B, 0x87, 0xC7, 0x62, 0x32, 0x98, 0xD4, 0xD8, 0xA2, 0xBE, 0x77, 0xCE, 0xA9, 0xCE, 0x42, 0x25, 0x5A, 0x8B, 0xEE, 0x16, 0x80, 0xF1, // ClientHello.SessionId 0x00, // ClientHello.cipher_suites 0x00, 0x2A, 0xC0, 0x2C, 0xC0, 0x2B, 0xC0, 0x30, 0xC0, 0x2F, 0x00, 0x9F, 0x00, 0x9E, 0xC0, 0x24, 0xC0, 0x23, 0xC0, 0x28, 0xC0, 0x27, 0xC0, 0x0A, 0xC0, 0x09, 0xC0, 0x14, 0xC0, 0x13, 0x00, 0x9D, 0x00, 0x9C, 0x00, 0x3D, 0x00, 0x3C, 0x00, 0x35, 0x00, 0x2F, 0x00, 0x0A, // ClientHello.compression_methods 0x01, 0x01, // ClientHello.extension_list_length 0x00, 0x74, // Extension.extension_type (server_name) 0x00, 0x00, // ServerNameListExtension.length 0x00, 0x39, // ServerName.length 0x00, 0x37, // ServerName.type 0x00, // HostName.length - length off by 1 0x00, 0x35, // HostName.bytes 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, // Extension.extension_type (00 0A) 0x00, 0x0A, // Extension 0A 0x00, 0x08, 0x00, 0x06, 0x00, 0x1D, 0x00, 0x17, 0x00, 0x18, // Extension.extension_type (00 0B) 0x00, 0x0B, // Extension 0B 0x00, 0x02, 0x01, 0x00, // Extension.extension_type (00 0D) 0x00, 0x0D, // Extension 0D 0x00, 0x14, 0x00, 0x12, 0x04, 0x01, 0x05, 0x01, 0x02, 0x01, 0x04, 0x03, 0x05, 0x03, 0x02, 0x03, 0x02, 0x02, 0x06, 0x01, 0x06, 0x03, // Extension.extension_type (00 23) 0x00, 0x23, // Extension 00 23 0x00, 0x00, // Extension.extension_type (00 17) 0x00, 0x17, // Extension 17 0x00, 0x00, // Extension.extension_type (FF 01) 0xFF, 0x01, // Extension FF01 0x00, 0x01, 0x00 }; // #14 yield return new byte[] { // SslPlainText.(ContentType+ProtocolVersion) 0x16, 0x03, 0x03, // SslPlainText.length 0x00, 0xCB, // Handshake.msg_type (client hello) 0x01, // Handshake.length 0x00, 0x00, 0xC7, // ClientHello.client_version 0x03, 0x03, // ClientHello.random 0x58, 0xAA, 0x5F, 0xE7, 0x22, 0xCF, 0x9F, 0x59, 0x8A, 0xC5, 0x8B, 0x87, 0xC7, 0x62, 0x32, 0x98, 0xD4, 0xD8, 0xA2, 0xBE, 0x77, 0xCE, 0xA9, 0xCE, 0x42, 0x25, 0x5A, 0x8B, 0xEE, 0x16, 0x80, 0xF1, // ClientHello.SessionId 0x00, // ClientHello.cipher_suites 0x00, 0x2A, 0xC0, 0x2C, 0xC0, 0x2B, 0xC0, 0x30, 0xC0, 0x2F, 0x00, 0x9F, 0x00, 0x9E, 0xC0, 0x24, 0xC0, 0x23, 0xC0, 0x28, 0xC0, 0x27, 0xC0, 0x0A, 0xC0, 0x09, 0xC0, 0x14, 0xC0, 0x13, 0x00, 0x9D, 0x00, 0x9C, 0x00, 0x3D, 0x00, 0x3C, 0x00, 0x35, 0x00, 0x2F, 0x00, 0x0A, // ClientHello.compression_methods 0x01, 0x01, // ClientHello.extension_list_length 0x00, 0x74, // Extension.extension_type (server_name) 0x00, 0x00, // ServerNameListExtension.length 0x00, 0x39, // ServerName.length 0x00, 0x37, // ServerName.type 0x00, // HostName.length - max length 0xFF, 0xFF, // HostName.bytes 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, // Extension.extension_type (00 0A) 0x00, 0x0A, // Extension 0A 0x00, 0x08, 0x00, 0x06, 0x00, 0x1D, 0x00, 0x17, 0x00, 0x18, // Extension.extension_type (00 0B) 0x00, 0x0B, // Extension 0B 0x00, 0x02, 0x01, 0x00, // Extension.extension_type (00 0D) 0x00, 0x0D, // Extension 0D 0x00, 0x14, 0x00, 0x12, 0x04, 0x01, 0x05, 0x01, 0x02, 0x01, 0x04, 0x03, 0x05, 0x03, 0x02, 0x03, 0x02, 0x02, 0x06, 0x01, 0x06, 0x03, // Extension.extension_type (00 23) 0x00, 0x23, // Extension 00 23 0x00, 0x00, // Extension.extension_type (00 17) 0x00, 0x17, // Extension 17 0x00, 0x00, // Extension.extension_type (FF 01) 0xFF, 0x01, // Extension FF01 0x00, 0x01, 0x00 }; // #15 yield return new byte[] { // SslPlainText.(ContentType+ProtocolVersion) 0x16, 0x03, 0x03, // SslPlainText.length 0x00, 0xCB, // Handshake.msg_type (client hello) 0x01, // Handshake.length 0x00, 0x00, 0xC7, // ClientHello.client_version 0x03, 0x03, // ClientHello.random 0x58, 0xAA, 0x5F, 0xE7, 0x22, 0xCF, 0x9F, 0x59, 0x8A, 0xC5, 0x8B, 0x87, 0xC7, 0x62, 0x32, 0x98, 0xD4, 0xD8, 0xA2, 0xBE, 0x77, 0xCE, 0xA9, 0xCE, 0x42, 0x25, 0x5A, 0x8B, 0xEE, 0x16, 0x80, 0xF1, // ClientHello.SessionId 0x00, // ClientHello.cipher_suites 0x00, 0x2A, 0xC0, 0x2C, 0xC0, 0x2B, 0xC0, 0x30, 0xC0, 0x2F, 0x00, 0x9F, 0x00, 0x9E, 0xC0, 0x24, 0xC0, 0x23, 0xC0, 0x28, 0xC0, 0x27, 0xC0, 0x0A, 0xC0, 0x09, 0xC0, 0x14, 0xC0, 0x13, 0x00, 0x9D, 0x00, 0x9C, 0x00, 0x3D, 0x00, 0x3C, 0x00, 0x35, 0x00, 0x2F, 0x00, 0x0A, // ClientHello.compression_methods 0x01, 0x01, // ClientHello.extension_list_length 0x00, 0x74, // Extension.extension_type (server_name) 0x00, 0x00, // ServerNameListExtension.length 0x00, 0x39, // ServerName.length 0x00, 0x37, // ServerName.type - unknown 0x01, // HostName.length 0x00, 0x34, // HostName.bytes 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, // Extension.extension_type (00 0A) 0x00, 0x0A, // Extension 0A 0x00, 0x08, 0x00, 0x06, 0x00, 0x1D, 0x00, 0x17, 0x00, 0x18, // Extension.extension_type (00 0B) 0x00, 0x0B, // Extension 0B 0x00, 0x02, 0x01, 0x00, // Extension.extension_type (00 0D) 0x00, 0x0D, // Extension 0D 0x00, 0x14, 0x00, 0x12, 0x04, 0x01, 0x05, 0x01, 0x02, 0x01, 0x04, 0x03, 0x05, 0x03, 0x02, 0x03, 0x02, 0x02, 0x06, 0x01, 0x06, 0x03, // Extension.extension_type (00 23) 0x00, 0x23, // Extension 00 23 0x00, 0x00, // Extension.extension_type (00 17) 0x00, 0x17, // Extension 17 0x00, 0x00, // Extension.extension_type (FF 01) 0xFF, 0x01, // Extension FF01 0x00, 0x01, 0x00 }; // #16 yield return new byte[] { // SslPlainText.(ContentType+ProtocolVersion) 0x16, 0x03, 0x03, // SslPlainText.length 0x00, 0xCB, // Handshake.msg_type (client hello) 0x01, // Handshake.length 0x00, 0x00, 0xC7, // ClientHello.client_version 0x03, 0x03, // ClientHello.random 0x58, 0xAA, 0x5F, 0xE7, 0x22, 0xCF, 0x9F, 0x59, 0x8A, 0xC5, 0x8B, 0x87, 0xC7, 0x62, 0x32, 0x98, 0xD4, 0xD8, 0xA2, 0xBE, 0x77, 0xCE, 0xA9, 0xCE, 0x42, 0x25, 0x5A, 0x8B, 0xEE, 0x16, 0x80, 0xF1, // ClientHello.SessionId 0x00, // ClientHello.cipher_suites 0x00, 0x2A, 0xC0, 0x2C, 0xC0, 0x2B, 0xC0, 0x30, 0xC0, 0x2F, 0x00, 0x9F, 0x00, 0x9E, 0xC0, 0x24, 0xC0, 0x23, 0xC0, 0x28, 0xC0, 0x27, 0xC0, 0x0A, 0xC0, 0x09, 0xC0, 0x14, 0xC0, 0x13, 0x00, 0x9D, 0x00, 0x9C, 0x00, 0x3D, 0x00, 0x3C, 0x00, 0x35, 0x00, 0x2F, 0x00, 0x0A, // ClientHello.compression_methods 0x01, 0x01, // ClientHello.extension_list_length 0x00, 0x74, // Extension.extension_type (server_name) 0x00, 0x00, // ServerNameListExtension.length 0x00, 0x39, // ServerName.length - length off by 1 0x00, 0x38, // ServerName.type 0x00, // HostName.length 0x00, 0x34, // HostName.bytes 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, // Extension.extension_type (00 0A) 0x00, 0x0A, // Extension 0A 0x00, 0x08, 0x00, 0x06, 0x00, 0x1D, 0x00, 0x17, 0x00, 0x18, // Extension.extension_type (00 0B) 0x00, 0x0B, // Extension 0B 0x00, 0x02, 0x01, 0x00, // Extension.extension_type (00 0D) 0x00, 0x0D, // Extension 0D 0x00, 0x14, 0x00, 0x12, 0x04, 0x01, 0x05, 0x01, 0x02, 0x01, 0x04, 0x03, 0x05, 0x03, 0x02, 0x03, 0x02, 0x02, 0x06, 0x01, 0x06, 0x03, // Extension.extension_type (00 23) 0x00, 0x23, // Extension 00 23 0x00, 0x00, // Extension.extension_type (00 17) 0x00, 0x17, // Extension 17 0x00, 0x00, // Extension.extension_type (FF 01) 0xFF, 0x01, // Extension FF01 0x00, 0x01, 0x00 }; // #17 yield return new byte[] { // SslPlainText.(ContentType+ProtocolVersion) 0x16, 0x03, 0x03, // SslPlainText.length 0x00, 0xCB, // Handshake.msg_type (client hello) 0x01, // Handshake.length 0x00, 0x00, 0xC7, // ClientHello.client_version 0x03, 0x03, // ClientHello.random 0x58, 0xAA, 0x5F, 0xE7, 0x22, 0xCF, 0x9F, 0x59, 0x8A, 0xC5, 0x8B, 0x87, 0xC7, 0x62, 0x32, 0x98, 0xD4, 0xD8, 0xA2, 0xBE, 0x77, 0xCE, 0xA9, 0xCE, 0x42, 0x25, 0x5A, 0x8B, 0xEE, 0x16, 0x80, 0xF1, // ClientHello.SessionId 0x00, // ClientHello.cipher_suites 0x00, 0x2A, 0xC0, 0x2C, 0xC0, 0x2B, 0xC0, 0x30, 0xC0, 0x2F, 0x00, 0x9F, 0x00, 0x9E, 0xC0, 0x24, 0xC0, 0x23, 0xC0, 0x28, 0xC0, 0x27, 0xC0, 0x0A, 0xC0, 0x09, 0xC0, 0x14, 0xC0, 0x13, 0x00, 0x9D, 0x00, 0x9C, 0x00, 0x3D, 0x00, 0x3C, 0x00, 0x35, 0x00, 0x2F, 0x00, 0x0A, // ClientHello.compression_methods 0x01, 0x01, // ClientHello.extension_list_length 0x00, 0x74, // Extension.extension_type (server_name) 0x00, 0x00, // ServerNameListExtension.length 0x00, 0x39, // ServerName.length - max length 0xFF, 0xFF, // ServerName.type 0x00, // HostName.length 0x00, 0x34, // HostName.bytes 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, // Extension.extension_type (00 0A) 0x00, 0x0A, // Extension 0A 0x00, 0x08, 0x00, 0x06, 0x00, 0x1D, 0x00, 0x17, 0x00, 0x18, // Extension.extension_type (00 0B) 0x00, 0x0B, // Extension 0B 0x00, 0x02, 0x01, 0x00, // Extension.extension_type (00 0D) 0x00, 0x0D, // Extension 0D 0x00, 0x14, 0x00, 0x12, 0x04, 0x01, 0x05, 0x01, 0x02, 0x01, 0x04, 0x03, 0x05, 0x03, 0x02, 0x03, 0x02, 0x02, 0x06, 0x01, 0x06, 0x03, // Extension.extension_type (00 23) 0x00, 0x23, // Extension 00 23 0x00, 0x00, // Extension.extension_type (00 17) 0x00, 0x17, // Extension 17 0x00, 0x00, // Extension.extension_type (FF 01) 0xFF, 0x01, // Extension FF01 0x00, 0x01, 0x00 }; // #18 yield return new byte[] { // SslPlainText.(ContentType+ProtocolVersion) 0x16, 0x03, 0x03, // SslPlainText.length 0x00, 0xCB, // Handshake.msg_type (client hello) 0x01, // Handshake.length 0x00, 0x00, 0xC7, // ClientHello.client_version 0x03, 0x03, // ClientHello.random 0x58, 0xAA, 0x5F, 0xE7, 0x22, 0xCF, 0x9F, 0x59, 0x8A, 0xC5, 0x8B, 0x87, 0xC7, 0x62, 0x32, 0x98, 0xD4, 0xD8, 0xA2, 0xBE, 0x77, 0xCE, 0xA9, 0xCE, 0x42, 0x25, 0x5A, 0x8B, 0xEE, 0x16, 0x80, 0xF1, // ClientHello.SessionId 0x00, // ClientHello.cipher_suites 0x00, 0x2A, 0xC0, 0x2C, 0xC0, 0x2B, 0xC0, 0x30, 0xC0, 0x2F, 0x00, 0x9F, 0x00, 0x9E, 0xC0, 0x24, 0xC0, 0x23, 0xC0, 0x28, 0xC0, 0x27, 0xC0, 0x0A, 0xC0, 0x09, 0xC0, 0x14, 0xC0, 0x13, 0x00, 0x9D, 0x00, 0x9C, 0x00, 0x3D, 0x00, 0x3C, 0x00, 0x35, 0x00, 0x2F, 0x00, 0x0A, // ClientHello.compression_methods 0x01, 0x01, // ClientHello.extension_list_length 0x00, 0x74, // Extension.extension_type (server_name) 0x00, 0x00, // ServerNameListExtension.length - length off by 1 0x00, 0x3A, // ServerName.length 0x00, 0x37, // ServerName.type 0x00, // HostName.length 0x00, 0x34, // HostName.bytes 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, // Extension.extension_type (00 0A) 0x00, 0x0A, // Extension 0A 0x00, 0x08, 0x00, 0x06, 0x00, 0x1D, 0x00, 0x17, 0x00, 0x18, // Extension.extension_type (00 0B) 0x00, 0x0B, // Extension 0B 0x00, 0x02, 0x01, 0x00, // Extension.extension_type (00 0D) 0x00, 0x0D, // Extension 0D 0x00, 0x14, 0x00, 0x12, 0x04, 0x01, 0x05, 0x01, 0x02, 0x01, 0x04, 0x03, 0x05, 0x03, 0x02, 0x03, 0x02, 0x02, 0x06, 0x01, 0x06, 0x03, // Extension.extension_type (00 23) 0x00, 0x23, // Extension 00 23 0x00, 0x00, // Extension.extension_type (00 17) 0x00, 0x17, // Extension 17 0x00, 0x00, // Extension.extension_type (FF 01) 0xFF, 0x01, // Extension FF01 0x00, 0x01, 0x00 }; // #19 yield return new byte[] { // SslPlainText.(ContentType+ProtocolVersion) 0x16, 0x03, 0x03, // SslPlainText.length 0x00, 0xCB, // Handshake.msg_type (client hello) 0x01, // Handshake.length 0x00, 0x00, 0xC7, // ClientHello.client_version 0x03, 0x03, // ClientHello.random 0x58, 0xAA, 0x5F, 0xE7, 0x22, 0xCF, 0x9F, 0x59, 0x8A, 0xC5, 0x8B, 0x87, 0xC7, 0x62, 0x32, 0x98, 0xD4, 0xD8, 0xA2, 0xBE, 0x77, 0xCE, 0xA9, 0xCE, 0x42, 0x25, 0x5A, 0x8B, 0xEE, 0x16, 0x80, 0xF1, // ClientHello.SessionId 0x00, // ClientHello.cipher_suites 0x00, 0x2A, 0xC0, 0x2C, 0xC0, 0x2B, 0xC0, 0x30, 0xC0, 0x2F, 0x00, 0x9F, 0x00, 0x9E, 0xC0, 0x24, 0xC0, 0x23, 0xC0, 0x28, 0xC0, 0x27, 0xC0, 0x0A, 0xC0, 0x09, 0xC0, 0x14, 0xC0, 0x13, 0x00, 0x9D, 0x00, 0x9C, 0x00, 0x3D, 0x00, 0x3C, 0x00, 0x35, 0x00, 0x2F, 0x00, 0x0A, // ClientHello.compression_methods 0x01, 0x01, // ClientHello.extension_list_length 0x00, 0x74, // Extension.extension_type (server_name) 0x00, 0x00, // ServerNameListExtension.length - max length 0xFF, 0xFF, // ServerName.length 0x00, 0x37, // ServerName.type 0x00, // HostName.length 0x00, 0x34, // HostName.bytes 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, // Extension.extension_type (00 0A) 0x00, 0x0A, // Extension 0A 0x00, 0x08, 0x00, 0x06, 0x00, 0x1D, 0x00, 0x17, 0x00, 0x18, // Extension.extension_type (00 0B) 0x00, 0x0B, // Extension 0B 0x00, 0x02, 0x01, 0x00, // Extension.extension_type (00 0D) 0x00, 0x0D, // Extension 0D 0x00, 0x14, 0x00, 0x12, 0x04, 0x01, 0x05, 0x01, 0x02, 0x01, 0x04, 0x03, 0x05, 0x03, 0x02, 0x03, 0x02, 0x02, 0x06, 0x01, 0x06, 0x03, // Extension.extension_type (00 23) 0x00, 0x23, // Extension 00 23 0x00, 0x00, // Extension.extension_type (00 17) 0x00, 0x17, // Extension 17 0x00, 0x00, // Extension.extension_type (FF 01) 0xFF, 0x01, // Extension FF01 0x00, 0x01, 0x00 }; // #20 yield return new byte[] { // SslPlainText.(ContentType+ProtocolVersion) 0x16, 0x03, 0x03, // SslPlainText.length 0x00, 0xCB, // Handshake.msg_type (client hello) 0x01, // Handshake.length 0x00, 0x00, 0xC7, // ClientHello.client_version 0x03, 0x03, // ClientHello.random 0x58, 0xAA, 0x5F, 0xE7, 0x22, 0xCF, 0x9F, 0x59, 0x8A, 0xC5, 0x8B, 0x87, 0xC7, 0x62, 0x32, 0x98, 0xD4, 0xD8, 0xA2, 0xBE, 0x77, 0xCE, 0xA9, 0xCE, 0x42, 0x25, 0x5A, 0x8B, 0xEE, 0x16, 0x80, 0xF1, // ClientHello.SessionId 0x00, // ClientHello.cipher_suites 0x00, 0x2A, 0xC0, 0x2C, 0xC0, 0x2B, 0xC0, 0x30, 0xC0, 0x2F, 0x00, 0x9F, 0x00, 0x9E, 0xC0, 0x24, 0xC0, 0x23, 0xC0, 0x28, 0xC0, 0x27, 0xC0, 0x0A, 0xC0, 0x09, 0xC0, 0x14, 0xC0, 0x13, 0x00, 0x9D, 0x00, 0x9C, 0x00, 0x3D, 0x00, 0x3C, 0x00, 0x35, 0x00, 0x2F, 0x00, 0x0A, // ClientHello.compression_methods 0x01, 0x01, // ClientHello.extension_list_length 0x00, 0x74, // Extension.extension_type (server_name) - unknown 0x01, 0x00, // ServerNameListExtension.length 0x00, 0x39, // ServerName.length 0x00, 0x37, // ServerName.type 0x00, // HostName.length 0x00, 0x34, // HostName.bytes 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, // Extension.extension_type (00 0A) 0x00, 0x0A, // Extension 0A 0x00, 0x08, 0x00, 0x06, 0x00, 0x1D, 0x00, 0x17, 0x00, 0x18, // Extension.extension_type (00 0B) 0x00, 0x0B, // Extension 0B 0x00, 0x02, 0x01, 0x00, // Extension.extension_type (00 0D) 0x00, 0x0D, // Extension 0D 0x00, 0x14, 0x00, 0x12, 0x04, 0x01, 0x05, 0x01, 0x02, 0x01, 0x04, 0x03, 0x05, 0x03, 0x02, 0x03, 0x02, 0x02, 0x06, 0x01, 0x06, 0x03, // Extension.extension_type (00 23) 0x00, 0x23, // Extension 00 23 0x00, 0x00, // Extension.extension_type (00 17) 0x00, 0x17, // Extension 17 0x00, 0x00, // Extension.extension_type (FF 01) 0xFF, 0x01, // Extension FF01 0x00, 0x01, 0x00 }; // #21 yield return new byte[] { // SslPlainText.(ContentType+ProtocolVersion) 0x16, 0x03, 0x03, // SslPlainText.length 0x00, 0xCB, // Handshake.msg_type (client hello) 0x01, // Handshake.length 0x00, 0x00, 0xC7, // ClientHello.client_version 0x03, 0x03, // ClientHello.random 0x58, 0xAA, 0x5F, 0xE7, 0x22, 0xCF, 0x9F, 0x59, 0x8A, 0xC5, 0x8B, 0x87, 0xC7, 0x62, 0x32, 0x98, 0xD4, 0xD8, 0xA2, 0xBE, 0x77, 0xCE, 0xA9, 0xCE, 0x42, 0x25, 0x5A, 0x8B, 0xEE, 0x16, 0x80, 0xF1, // ClientHello.SessionId 0x00, // ClientHello.cipher_suites 0x00, 0x2A, 0xC0, 0x2C, 0xC0, 0x2B, 0xC0, 0x30, 0xC0, 0x2F, 0x00, 0x9F, 0x00, 0x9E, 0xC0, 0x24, 0xC0, 0x23, 0xC0, 0x28, 0xC0, 0x27, 0xC0, 0x0A, 0xC0, 0x09, 0xC0, 0x14, 0xC0, 0x13, 0x00, 0x9D, 0x00, 0x9C, 0x00, 0x3D, 0x00, 0x3C, 0x00, 0x35, 0x00, 0x2F, 0x00, 0x0A, // ClientHello.compression_methods 0x01, 0x01, // ClientHello.extension_list_length - length off by 1 0x00, 0x75, // Extension.extension_type (server_name) 0x00, 0x00, // ServerNameListExtension.length 0x00, 0x39, // ServerName.length 0x00, 0x37, // ServerName.type 0x00, // HostName.length 0x00, 0x34, // HostName.bytes 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, // Extension.extension_type (00 0A) 0x00, 0x0A, // Extension 0A 0x00, 0x08, 0x00, 0x06, 0x00, 0x1D, 0x00, 0x17, 0x00, 0x18, // Extension.extension_type (00 0B) 0x00, 0x0B, // Extension 0B 0x00, 0x02, 0x01, 0x00, // Extension.extension_type (00 0D) 0x00, 0x0D, // Extension 0D 0x00, 0x14, 0x00, 0x12, 0x04, 0x01, 0x05, 0x01, 0x02, 0x01, 0x04, 0x03, 0x05, 0x03, 0x02, 0x03, 0x02, 0x02, 0x06, 0x01, 0x06, 0x03, // Extension.extension_type (00 23) 0x00, 0x23, // Extension 00 23 0x00, 0x00, // Extension.extension_type (00 17) 0x00, 0x17, // Extension 17 0x00, 0x00, // Extension.extension_type (FF 01) 0xFF, 0x01, // Extension FF01 0x00, 0x01, 0x00 }; // #22 yield return new byte[] { // SslPlainText.(ContentType+ProtocolVersion) 0x16, 0x03, 0x03, // SslPlainText.length 0x00, 0xCB, // Handshake.msg_type (client hello) 0x01, // Handshake.length 0x00, 0x00, 0xC7, // ClientHello.client_version 0x03, 0x03, // ClientHello.random 0x58, 0xAA, 0x5F, 0xE7, 0x22, 0xCF, 0x9F, 0x59, 0x8A, 0xC5, 0x8B, 0x87, 0xC7, 0x62, 0x32, 0x98, 0xD4, 0xD8, 0xA2, 0xBE, 0x77, 0xCE, 0xA9, 0xCE, 0x42, 0x25, 0x5A, 0x8B, 0xEE, 0x16, 0x80, 0xF1, // ClientHello.SessionId 0x00, // ClientHello.cipher_suites 0x00, 0x2A, 0xC0, 0x2C, 0xC0, 0x2B, 0xC0, 0x30, 0xC0, 0x2F, 0x00, 0x9F, 0x00, 0x9E, 0xC0, 0x24, 0xC0, 0x23, 0xC0, 0x28, 0xC0, 0x27, 0xC0, 0x0A, 0xC0, 0x09, 0xC0, 0x14, 0xC0, 0x13, 0x00, 0x9D, 0x00, 0x9C, 0x00, 0x3D, 0x00, 0x3C, 0x00, 0x35, 0x00, 0x2F, 0x00, 0x0A, // ClientHello.compression_methods 0x01, 0x01, // ClientHello.extension_list_length - max length 0xFF, 0xFF, // Extension.extension_type (server_name) 0x00, 0x00, // ServerNameListExtension.length 0x00, 0x39, // ServerName.length 0x00, 0x37, // ServerName.type 0x00, // HostName.length 0x00, 0x34, // HostName.bytes 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, // Extension.extension_type (00 0A) 0x00, 0x0A, // Extension 0A 0x00, 0x08, 0x00, 0x06, 0x00, 0x1D, 0x00, 0x17, 0x00, 0x18, // Extension.extension_type (00 0B) 0x00, 0x0B, // Extension 0B 0x00, 0x02, 0x01, 0x00, // Extension.extension_type (00 0D) 0x00, 0x0D, // Extension 0D 0x00, 0x14, 0x00, 0x12, 0x04, 0x01, 0x05, 0x01, 0x02, 0x01, 0x04, 0x03, 0x05, 0x03, 0x02, 0x03, 0x02, 0x02, 0x06, 0x01, 0x06, 0x03, // Extension.extension_type (00 23) 0x00, 0x23, // Extension 00 23 0x00, 0x00, // Extension.extension_type (00 17) 0x00, 0x17, // Extension 17 0x00, 0x00, // Extension.extension_type (FF 01) 0xFF, 0x01, // Extension FF01 0x00, 0x01, 0x00 }; // #23 yield return new byte[] { // SslPlainText.(ContentType+ProtocolVersion) 0x16, 0x03, 0x03, // SslPlainText.length 0x00, 0xCB, // Handshake.msg_type (client hello) 0x01, // Handshake.length 0x00, 0x00, 0xC7, // ClientHello.client_version 0x03, 0x03, // ClientHello.random 0x58, 0xAA, 0x5F, 0xE7, 0x22, 0xCF, 0x9F, 0x59, 0x8A, 0xC5, 0x8B, 0x87, 0xC7, 0x62, 0x32, 0x98, 0xD4, 0xD8, 0xA2, 0xBE, 0x77, 0xCE, 0xA9, 0xCE, 0x42, 0x25, 0x5A, 0x8B, 0xEE, 0x16, 0x80, 0xF1, // ClientHello.SessionId 0x00, // ClientHello.cipher_suites 0x00, 0x2A, 0xC0, 0x2C, 0xC0, 0x2B, 0xC0, 0x30, 0xC0, 0x2F, 0x00, 0x9F, 0x00, 0x9E, 0xC0, 0x24, 0xC0, 0x23, 0xC0, 0x28, 0xC0, 0x27, 0xC0, 0x0A, 0xC0, 0x09, 0xC0, 0x14, 0xC0, 0x13, 0x00, 0x9D, 0x00, 0x9C, 0x00, 0x3D, 0x00, 0x3C, 0x00, 0x35, 0x00, 0x2F, 0x00, 0x0A, // ClientHello.compression_methods - length off by 1 0x02, 0x01, // ClientHello.extension_list_length 0x00, 0x74, // Extension.extension_type (server_name) 0x00, 0x00, // ServerNameListExtension.length 0x00, 0x39, // ServerName.length 0x00, 0x37, // ServerName.type 0x00, // HostName.length 0x00, 0x34, // HostName.bytes 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, // Extension.extension_type (00 0A) 0x00, 0x0A, // Extension 0A 0x00, 0x08, 0x00, 0x06, 0x00, 0x1D, 0x00, 0x17, 0x00, 0x18, // Extension.extension_type (00 0B) 0x00, 0x0B, // Extension 0B 0x00, 0x02, 0x01, 0x00, // Extension.extension_type (00 0D) 0x00, 0x0D, // Extension 0D 0x00, 0x14, 0x00, 0x12, 0x04, 0x01, 0x05, 0x01, 0x02, 0x01, 0x04, 0x03, 0x05, 0x03, 0x02, 0x03, 0x02, 0x02, 0x06, 0x01, 0x06, 0x03, // Extension.extension_type (00 23) 0x00, 0x23, // Extension 00 23 0x00, 0x00, // Extension.extension_type (00 17) 0x00, 0x17, // Extension 17 0x00, 0x00, // Extension.extension_type (FF 01) 0xFF, 0x01, // Extension FF01 0x00, 0x01, 0x00 }; // #24 yield return new byte[] { // SslPlainText.(ContentType+ProtocolVersion) 0x16, 0x03, 0x03, // SslPlainText.length 0x00, 0xCB, // Handshake.msg_type (client hello) 0x01, // Handshake.length 0x00, 0x00, 0xC7, // ClientHello.client_version 0x03, 0x03, // ClientHello.random 0x58, 0xAA, 0x5F, 0xE7, 0x22, 0xCF, 0x9F, 0x59, 0x8A, 0xC5, 0x8B, 0x87, 0xC7, 0x62, 0x32, 0x98, 0xD4, 0xD8, 0xA2, 0xBE, 0x77, 0xCE, 0xA9, 0xCE, 0x42, 0x25, 0x5A, 0x8B, 0xEE, 0x16, 0x80, 0xF1, // ClientHello.SessionId 0x00, // ClientHello.cipher_suites 0x00, 0x2A, 0xC0, 0x2C, 0xC0, 0x2B, 0xC0, 0x30, 0xC0, 0x2F, 0x00, 0x9F, 0x00, 0x9E, 0xC0, 0x24, 0xC0, 0x23, 0xC0, 0x28, 0xC0, 0x27, 0xC0, 0x0A, 0xC0, 0x09, 0xC0, 0x14, 0xC0, 0x13, 0x00, 0x9D, 0x00, 0x9C, 0x00, 0x3D, 0x00, 0x3C, 0x00, 0x35, 0x00, 0x2F, 0x00, 0x0A, // ClientHello.compression_methods - max length 0xFF, 0x01, // ClientHello.extension_list_length 0x00, 0x74, // Extension.extension_type (server_name) 0x00, 0x00, // ServerNameListExtension.length 0x00, 0x39, // ServerName.length 0x00, 0x37, // ServerName.type 0x00, // HostName.length 0x00, 0x34, // HostName.bytes 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, // Extension.extension_type (00 0A) 0x00, 0x0A, // Extension 0A 0x00, 0x08, 0x00, 0x06, 0x00, 0x1D, 0x00, 0x17, 0x00, 0x18, // Extension.extension_type (00 0B) 0x00, 0x0B, // Extension 0B 0x00, 0x02, 0x01, 0x00, // Extension.extension_type (00 0D) 0x00, 0x0D, // Extension 0D 0x00, 0x14, 0x00, 0x12, 0x04, 0x01, 0x05, 0x01, 0x02, 0x01, 0x04, 0x03, 0x05, 0x03, 0x02, 0x03, 0x02, 0x02, 0x06, 0x01, 0x06, 0x03, // Extension.extension_type (00 23) 0x00, 0x23, // Extension 00 23 0x00, 0x00, // Extension.extension_type (00 17) 0x00, 0x17, // Extension 17 0x00, 0x00, // Extension.extension_type (FF 01) 0xFF, 0x01, // Extension FF01 0x00, 0x01, 0x00 }; // #25 yield return new byte[] { // SslPlainText.(ContentType+ProtocolVersion) 0x16, 0x03, 0x03, // SslPlainText.length 0x00, 0xCB, // Handshake.msg_type (client hello) 0x01, // Handshake.length 0x00, 0x00, 0xC7, // ClientHello.client_version 0x03, 0x03, // ClientHello.random 0x58, 0xAA, 0x5F, 0xE7, 0x22, 0xCF, 0x9F, 0x59, 0x8A, 0xC5, 0x8B, 0x87, 0xC7, 0x62, 0x32, 0x98, 0xD4, 0xD8, 0xA2, 0xBE, 0x77, 0xCE, 0xA9, 0xCE, 0x42, 0x25, 0x5A, 0x8B, 0xEE, 0x16, 0x80, 0xF1, // ClientHello.SessionId 0x00, // ClientHello.cipher_suites - length off by 1 0x00, 0x2B, 0xC0, 0x2C, 0xC0, 0x2B, 0xC0, 0x30, 0xC0, 0x2F, 0x00, 0x9F, 0x00, 0x9E, 0xC0, 0x24, 0xC0, 0x23, 0xC0, 0x28, 0xC0, 0x27, 0xC0, 0x0A, 0xC0, 0x09, 0xC0, 0x14, 0xC0, 0x13, 0x00, 0x9D, 0x00, 0x9C, 0x00, 0x3D, 0x00, 0x3C, 0x00, 0x35, 0x00, 0x2F, 0x00, 0x0A, // ClientHello.compression_methods 0x01, 0x01, // ClientHello.extension_list_length 0x00, 0x74, // Extension.extension_type (server_name) 0x00, 0x00, // ServerNameListExtension.length 0x00, 0x39, // ServerName.length 0x00, 0x37, // ServerName.type 0x00, // HostName.length 0x00, 0x34, // HostName.bytes 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, // Extension.extension_type (00 0A) 0x00, 0x0A, // Extension 0A 0x00, 0x08, 0x00, 0x06, 0x00, 0x1D, 0x00, 0x17, 0x00, 0x18, // Extension.extension_type (00 0B) 0x00, 0x0B, // Extension 0B 0x00, 0x02, 0x01, 0x00, // Extension.extension_type (00 0D) 0x00, 0x0D, // Extension 0D 0x00, 0x14, 0x00, 0x12, 0x04, 0x01, 0x05, 0x01, 0x02, 0x01, 0x04, 0x03, 0x05, 0x03, 0x02, 0x03, 0x02, 0x02, 0x06, 0x01, 0x06, 0x03, // Extension.extension_type (00 23) 0x00, 0x23, // Extension 00 23 0x00, 0x00, // Extension.extension_type (00 17) 0x00, 0x17, // Extension 17 0x00, 0x00, // Extension.extension_type (FF 01) 0xFF, 0x01, // Extension FF01 0x00, 0x01, 0x00 }; // #26 yield return new byte[] { // SslPlainText.(ContentType+ProtocolVersion) 0x16, 0x03, 0x03, // SslPlainText.length 0x00, 0xCB, // Handshake.msg_type (client hello) 0x01, // Handshake.length 0x00, 0x00, 0xC7, // ClientHello.client_version 0x03, 0x03, // ClientHello.random 0x58, 0xAA, 0x5F, 0xE7, 0x22, 0xCF, 0x9F, 0x59, 0x8A, 0xC5, 0x8B, 0x87, 0xC7, 0x62, 0x32, 0x98, 0xD4, 0xD8, 0xA2, 0xBE, 0x77, 0xCE, 0xA9, 0xCE, 0x42, 0x25, 0x5A, 0x8B, 0xEE, 0x16, 0x80, 0xF1, // ClientHello.SessionId 0x00, // ClientHello.cipher_suites - max length 0xFF, 0xFF, 0xC0, 0x2C, 0xC0, 0x2B, 0xC0, 0x30, 0xC0, 0x2F, 0x00, 0x9F, 0x00, 0x9E, 0xC0, 0x24, 0xC0, 0x23, 0xC0, 0x28, 0xC0, 0x27, 0xC0, 0x0A, 0xC0, 0x09, 0xC0, 0x14, 0xC0, 0x13, 0x00, 0x9D, 0x00, 0x9C, 0x00, 0x3D, 0x00, 0x3C, 0x00, 0x35, 0x00, 0x2F, 0x00, 0x0A, // ClientHello.compression_methods 0x01, 0x01, // ClientHello.extension_list_length 0x00, 0x74, // Extension.extension_type (server_name) 0x00, 0x00, // ServerNameListExtension.length 0x00, 0x39, // ServerName.length 0x00, 0x37, // ServerName.type 0x00, // HostName.length 0x00, 0x34, // HostName.bytes 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, // Extension.extension_type (00 0A) 0x00, 0x0A, // Extension 0A 0x00, 0x08, 0x00, 0x06, 0x00, 0x1D, 0x00, 0x17, 0x00, 0x18, // Extension.extension_type (00 0B) 0x00, 0x0B, // Extension 0B 0x00, 0x02, 0x01, 0x00, // Extension.extension_type (00 0D) 0x00, 0x0D, // Extension 0D 0x00, 0x14, 0x00, 0x12, 0x04, 0x01, 0x05, 0x01, 0x02, 0x01, 0x04, 0x03, 0x05, 0x03, 0x02, 0x03, 0x02, 0x02, 0x06, 0x01, 0x06, 0x03, // Extension.extension_type (00 23) 0x00, 0x23, // Extension 00 23 0x00, 0x00, // Extension.extension_type (00 17) 0x00, 0x17, // Extension 17 0x00, 0x00, // Extension.extension_type (FF 01) 0xFF, 0x01, // Extension FF01 0x00, 0x01, 0x00 }; // #27 yield return new byte[] { // SslPlainText.(ContentType+ProtocolVersion) 0x16, 0x03, 0x03, // SslPlainText.length 0x00, 0xCB, // Handshake.msg_type (client hello) 0x01, // Handshake.length 0x00, 0x00, 0xC7, // ClientHello.client_version 0x03, 0x03, // ClientHello.random 0x58, 0xAA, 0x5F, 0xE7, 0x22, 0xCF, 0x9F, 0x59, 0x8A, 0xC5, 0x8B, 0x87, 0xC7, 0x62, 0x32, 0x98, 0xD4, 0xD8, 0xA2, 0xBE, 0x77, 0xCE, 0xA9, 0xCE, 0x42, 0x25, 0x5A, 0x8B, 0xEE, 0x16, 0x80, 0xF1, // ClientHello.SessionId - length off by 1 0x01, // ClientHello.cipher_suites 0x00, 0x2A, 0xC0, 0x2C, 0xC0, 0x2B, 0xC0, 0x30, 0xC0, 0x2F, 0x00, 0x9F, 0x00, 0x9E, 0xC0, 0x24, 0xC0, 0x23, 0xC0, 0x28, 0xC0, 0x27, 0xC0, 0x0A, 0xC0, 0x09, 0xC0, 0x14, 0xC0, 0x13, 0x00, 0x9D, 0x00, 0x9C, 0x00, 0x3D, 0x00, 0x3C, 0x00, 0x35, 0x00, 0x2F, 0x00, 0x0A, // ClientHello.compression_methods 0x01, 0x01, // ClientHello.extension_list_length 0x00, 0x74, // Extension.extension_type (server_name) 0x00, 0x00, // ServerNameListExtension.length 0x00, 0x39, // ServerName.length 0x00, 0x37, // ServerName.type 0x00, // HostName.length 0x00, 0x34, // HostName.bytes 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, // Extension.extension_type (00 0A) 0x00, 0x0A, // Extension 0A 0x00, 0x08, 0x00, 0x06, 0x00, 0x1D, 0x00, 0x17, 0x00, 0x18, // Extension.extension_type (00 0B) 0x00, 0x0B, // Extension 0B 0x00, 0x02, 0x01, 0x00, // Extension.extension_type (00 0D) 0x00, 0x0D, // Extension 0D 0x00, 0x14, 0x00, 0x12, 0x04, 0x01, 0x05, 0x01, 0x02, 0x01, 0x04, 0x03, 0x05, 0x03, 0x02, 0x03, 0x02, 0x02, 0x06, 0x01, 0x06, 0x03, // Extension.extension_type (00 23) 0x00, 0x23, // Extension 00 23 0x00, 0x00, // Extension.extension_type (00 17) 0x00, 0x17, // Extension 17 0x00, 0x00, // Extension.extension_type (FF 01) 0xFF, 0x01, // Extension FF01 0x00, 0x01, 0x00 }; // #28 yield return new byte[] { // SslPlainText.(ContentType+ProtocolVersion) 0x16, 0x03, 0x03, // SslPlainText.length 0x00, 0xCB, // Handshake.msg_type (client hello) 0x01, // Handshake.length 0x00, 0x00, 0xC7, // ClientHello.client_version 0x03, 0x03, // ClientHello.random 0x58, 0xAA, 0x5F, 0xE7, 0x22, 0xCF, 0x9F, 0x59, 0x8A, 0xC5, 0x8B, 0x87, 0xC7, 0x62, 0x32, 0x98, 0xD4, 0xD8, 0xA2, 0xBE, 0x77, 0xCE, 0xA9, 0xCE, 0x42, 0x25, 0x5A, 0x8B, 0xEE, 0x16, 0x80, 0xF1, // ClientHello.SessionId - max length 0xFF, // ClientHello.cipher_suites 0x00, 0x2A, 0xC0, 0x2C, 0xC0, 0x2B, 0xC0, 0x30, 0xC0, 0x2F, 0x00, 0x9F, 0x00, 0x9E, 0xC0, 0x24, 0xC0, 0x23, 0xC0, 0x28, 0xC0, 0x27, 0xC0, 0x0A, 0xC0, 0x09, 0xC0, 0x14, 0xC0, 0x13, 0x00, 0x9D, 0x00, 0x9C, 0x00, 0x3D, 0x00, 0x3C, 0x00, 0x35, 0x00, 0x2F, 0x00, 0x0A, // ClientHello.compression_methods 0x01, 0x01, // ClientHello.extension_list_length 0x00, 0x74, // Extension.extension_type (server_name) 0x00, 0x00, // ServerNameListExtension.length 0x00, 0x39, // ServerName.length 0x00, 0x37, // ServerName.type 0x00, // HostName.length 0x00, 0x34, // HostName.bytes 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, // Extension.extension_type (00 0A) 0x00, 0x0A, // Extension 0A 0x00, 0x08, 0x00, 0x06, 0x00, 0x1D, 0x00, 0x17, 0x00, 0x18, // Extension.extension_type (00 0B) 0x00, 0x0B, // Extension 0B 0x00, 0x02, 0x01, 0x00, // Extension.extension_type (00 0D) 0x00, 0x0D, // Extension 0D 0x00, 0x14, 0x00, 0x12, 0x04, 0x01, 0x05, 0x01, 0x02, 0x01, 0x04, 0x03, 0x05, 0x03, 0x02, 0x03, 0x02, 0x02, 0x06, 0x01, 0x06, 0x03, // Extension.extension_type (00 23) 0x00, 0x23, // Extension 00 23 0x00, 0x00, // Extension.extension_type (00 17) 0x00, 0x17, // Extension 17 0x00, 0x00, // Extension.extension_type (FF 01) 0xFF, 0x01, // Extension FF01 0x00, 0x01, 0x00 }; // #29 yield return new byte[] { // SslPlainText.(ContentType+ProtocolVersion) 0x16, 0x03, 0x03, // SslPlainText.length 0x00, 0xCB, // Handshake.msg_type (client hello) 0x01, // Handshake.length - length off by 1 0x00, 0x00, 0xC8, // ClientHello.client_version 0x03, 0x03, // ClientHello.random 0x58, 0xAA, 0x5F, 0xE7, 0x22, 0xCF, 0x9F, 0x59, 0x8A, 0xC5, 0x8B, 0x87, 0xC7, 0x62, 0x32, 0x98, 0xD4, 0xD8, 0xA2, 0xBE, 0x77, 0xCE, 0xA9, 0xCE, 0x42, 0x25, 0x5A, 0x8B, 0xEE, 0x16, 0x80, 0xF1, // ClientHello.SessionId 0x00, // ClientHello.cipher_suites 0x00, 0x2A, 0xC0, 0x2C, 0xC0, 0x2B, 0xC0, 0x30, 0xC0, 0x2F, 0x00, 0x9F, 0x00, 0x9E, 0xC0, 0x24, 0xC0, 0x23, 0xC0, 0x28, 0xC0, 0x27, 0xC0, 0x0A, 0xC0, 0x09, 0xC0, 0x14, 0xC0, 0x13, 0x00, 0x9D, 0x00, 0x9C, 0x00, 0x3D, 0x00, 0x3C, 0x00, 0x35, 0x00, 0x2F, 0x00, 0x0A, // ClientHello.compression_methods 0x01, 0x01, // ClientHello.extension_list_length 0x00, 0x74, // Extension.extension_type (server_name) 0x00, 0x00, // ServerNameListExtension.length 0x00, 0x39, // ServerName.length 0x00, 0x37, // ServerName.type 0x00, // HostName.length 0x00, 0x34, // HostName.bytes 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, // Extension.extension_type (00 0A) 0x00, 0x0A, // Extension 0A 0x00, 0x08, 0x00, 0x06, 0x00, 0x1D, 0x00, 0x17, 0x00, 0x18, // Extension.extension_type (00 0B) 0x00, 0x0B, // Extension 0B 0x00, 0x02, 0x01, 0x00, // Extension.extension_type (00 0D) 0x00, 0x0D, // Extension 0D 0x00, 0x14, 0x00, 0x12, 0x04, 0x01, 0x05, 0x01, 0x02, 0x01, 0x04, 0x03, 0x05, 0x03, 0x02, 0x03, 0x02, 0x02, 0x06, 0x01, 0x06, 0x03, // Extension.extension_type (00 23) 0x00, 0x23, // Extension 00 23 0x00, 0x00, // Extension.extension_type (00 17) 0x00, 0x17, // Extension 17 0x00, 0x00, // Extension.extension_type (FF 01) 0xFF, 0x01, // Extension FF01 0x00, 0x01, 0x00 }; // #30 yield return new byte[] { // SslPlainText.(ContentType+ProtocolVersion) 0x16, 0x03, 0x03, // SslPlainText.length 0x00, 0xCB, // Handshake.msg_type (client hello) 0x01, // Handshake.length - max length 0xFF, 0xFF, 0xFF, // ClientHello.client_version 0x03, 0x03, // ClientHello.random 0x58, 0xAA, 0x5F, 0xE7, 0x22, 0xCF, 0x9F, 0x59, 0x8A, 0xC5, 0x8B, 0x87, 0xC7, 0x62, 0x32, 0x98, 0xD4, 0xD8, 0xA2, 0xBE, 0x77, 0xCE, 0xA9, 0xCE, 0x42, 0x25, 0x5A, 0x8B, 0xEE, 0x16, 0x80, 0xF1, // ClientHello.SessionId 0x00, // ClientHello.cipher_suites 0x00, 0x2A, 0xC0, 0x2C, 0xC0, 0x2B, 0xC0, 0x30, 0xC0, 0x2F, 0x00, 0x9F, 0x00, 0x9E, 0xC0, 0x24, 0xC0, 0x23, 0xC0, 0x28, 0xC0, 0x27, 0xC0, 0x0A, 0xC0, 0x09, 0xC0, 0x14, 0xC0, 0x13, 0x00, 0x9D, 0x00, 0x9C, 0x00, 0x3D, 0x00, 0x3C, 0x00, 0x35, 0x00, 0x2F, 0x00, 0x0A, // ClientHello.compression_methods 0x01, 0x01, // ClientHello.extension_list_length 0x00, 0x74, // Extension.extension_type (server_name) 0x00, 0x00, // ServerNameListExtension.length 0x00, 0x39, // ServerName.length 0x00, 0x37, // ServerName.type 0x00, // HostName.length 0x00, 0x34, // HostName.bytes 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, // Extension.extension_type (00 0A) 0x00, 0x0A, // Extension 0A 0x00, 0x08, 0x00, 0x06, 0x00, 0x1D, 0x00, 0x17, 0x00, 0x18, // Extension.extension_type (00 0B) 0x00, 0x0B, // Extension 0B 0x00, 0x02, 0x01, 0x00, // Extension.extension_type (00 0D) 0x00, 0x0D, // Extension 0D 0x00, 0x14, 0x00, 0x12, 0x04, 0x01, 0x05, 0x01, 0x02, 0x01, 0x04, 0x03, 0x05, 0x03, 0x02, 0x03, 0x02, 0x02, 0x06, 0x01, 0x06, 0x03, // Extension.extension_type (00 23) 0x00, 0x23, // Extension 00 23 0x00, 0x00, // Extension.extension_type (00 17) 0x00, 0x17, // Extension 17 0x00, 0x00, // Extension.extension_type (FF 01) 0xFF, 0x01, // Extension FF01 0x00, 0x01, 0x00 }; // #31 yield return new byte[] { // SslPlainText.(ContentType+ProtocolVersion) 0x16, 0x03, 0x03, // SslPlainText.length 0x00, 0xCB, // Handshake.msg_type (client hello) - unknown 0x00, // Handshake.length 0x00, 0x00, 0xC7, // ClientHello.client_version 0x03, 0x03, // ClientHello.random 0x58, 0xAA, 0x5F, 0xE7, 0x22, 0xCF, 0x9F, 0x59, 0x8A, 0xC5, 0x8B, 0x87, 0xC7, 0x62, 0x32, 0x98, 0xD4, 0xD8, 0xA2, 0xBE, 0x77, 0xCE, 0xA9, 0xCE, 0x42, 0x25, 0x5A, 0x8B, 0xEE, 0x16, 0x80, 0xF1, // ClientHello.SessionId 0x00, // ClientHello.cipher_suites 0x00, 0x2A, 0xC0, 0x2C, 0xC0, 0x2B, 0xC0, 0x30, 0xC0, 0x2F, 0x00, 0x9F, 0x00, 0x9E, 0xC0, 0x24, 0xC0, 0x23, 0xC0, 0x28, 0xC0, 0x27, 0xC0, 0x0A, 0xC0, 0x09, 0xC0, 0x14, 0xC0, 0x13, 0x00, 0x9D, 0x00, 0x9C, 0x00, 0x3D, 0x00, 0x3C, 0x00, 0x35, 0x00, 0x2F, 0x00, 0x0A, // ClientHello.compression_methods 0x01, 0x01, // ClientHello.extension_list_length 0x00, 0x74, // Extension.extension_type (server_name) 0x00, 0x00, // ServerNameListExtension.length 0x00, 0x39, // ServerName.length 0x00, 0x37, // ServerName.type 0x00, // HostName.length 0x00, 0x34, // HostName.bytes 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, // Extension.extension_type (00 0A) 0x00, 0x0A, // Extension 0A 0x00, 0x08, 0x00, 0x06, 0x00, 0x1D, 0x00, 0x17, 0x00, 0x18, // Extension.extension_type (00 0B) 0x00, 0x0B, // Extension 0B 0x00, 0x02, 0x01, 0x00, // Extension.extension_type (00 0D) 0x00, 0x0D, // Extension 0D 0x00, 0x14, 0x00, 0x12, 0x04, 0x01, 0x05, 0x01, 0x02, 0x01, 0x04, 0x03, 0x05, 0x03, 0x02, 0x03, 0x02, 0x02, 0x06, 0x01, 0x06, 0x03, // Extension.extension_type (00 23) 0x00, 0x23, // Extension 00 23 0x00, 0x00, // Extension.extension_type (00 17) 0x00, 0x17, // Extension 17 0x00, 0x00, // Extension.extension_type (FF 01) 0xFF, 0x01, // Extension FF01 0x00, 0x01, 0x00 }; // #32 yield return new byte[] { // SslPlainText.(ContentType+ProtocolVersion) 0x16, 0x03, 0x03, // SslPlainText.length - length off by 1 0x00, 0xCC, // Handshake.msg_type (client hello) 0x01, // Handshake.length 0x00, 0x00, 0xC7, // ClientHello.client_version 0x03, 0x03, // ClientHello.random 0x58, 0xAA, 0x5F, 0xE7, 0x22, 0xCF, 0x9F, 0x59, 0x8A, 0xC5, 0x8B, 0x87, 0xC7, 0x62, 0x32, 0x98, 0xD4, 0xD8, 0xA2, 0xBE, 0x77, 0xCE, 0xA9, 0xCE, 0x42, 0x25, 0x5A, 0x8B, 0xEE, 0x16, 0x80, 0xF1, // ClientHello.SessionId 0x00, // ClientHello.cipher_suites 0x00, 0x2A, 0xC0, 0x2C, 0xC0, 0x2B, 0xC0, 0x30, 0xC0, 0x2F, 0x00, 0x9F, 0x00, 0x9E, 0xC0, 0x24, 0xC0, 0x23, 0xC0, 0x28, 0xC0, 0x27, 0xC0, 0x0A, 0xC0, 0x09, 0xC0, 0x14, 0xC0, 0x13, 0x00, 0x9D, 0x00, 0x9C, 0x00, 0x3D, 0x00, 0x3C, 0x00, 0x35, 0x00, 0x2F, 0x00, 0x0A, // ClientHello.compression_methods 0x01, 0x01, // ClientHello.extension_list_length 0x00, 0x74, // Extension.extension_type (server_name) 0x00, 0x00, // ServerNameListExtension.length 0x00, 0x39, // ServerName.length 0x00, 0x37, // ServerName.type 0x00, // HostName.length 0x00, 0x34, // HostName.bytes 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, // Extension.extension_type (00 0A) 0x00, 0x0A, // Extension 0A 0x00, 0x08, 0x00, 0x06, 0x00, 0x1D, 0x00, 0x17, 0x00, 0x18, // Extension.extension_type (00 0B) 0x00, 0x0B, // Extension 0B 0x00, 0x02, 0x01, 0x00, // Extension.extension_type (00 0D) 0x00, 0x0D, // Extension 0D 0x00, 0x14, 0x00, 0x12, 0x04, 0x01, 0x05, 0x01, 0x02, 0x01, 0x04, 0x03, 0x05, 0x03, 0x02, 0x03, 0x02, 0x02, 0x06, 0x01, 0x06, 0x03, // Extension.extension_type (00 23) 0x00, 0x23, // Extension 00 23 0x00, 0x00, // Extension.extension_type (00 17) 0x00, 0x17, // Extension 17 0x00, 0x00, // Extension.extension_type (FF 01) 0xFF, 0x01, // Extension FF01 0x00, 0x01, 0x00 }; // #33 yield return new byte[] { // SslPlainText.(ContentType+ProtocolVersion) 0x16, 0x03, 0x03, // SslPlainText.length - max length 0xFF, 0xFF, // Handshake.msg_type (client hello) 0x01, // Handshake.length 0x00, 0x00, 0xC7, // ClientHello.client_version 0x03, 0x03, // ClientHello.random 0x58, 0xAA, 0x5F, 0xE7, 0x22, 0xCF, 0x9F, 0x59, 0x8A, 0xC5, 0x8B, 0x87, 0xC7, 0x62, 0x32, 0x98, 0xD4, 0xD8, 0xA2, 0xBE, 0x77, 0xCE, 0xA9, 0xCE, 0x42, 0x25, 0x5A, 0x8B, 0xEE, 0x16, 0x80, 0xF1, // ClientHello.SessionId 0x00, // ClientHello.cipher_suites 0x00, 0x2A, 0xC0, 0x2C, 0xC0, 0x2B, 0xC0, 0x30, 0xC0, 0x2F, 0x00, 0x9F, 0x00, 0x9E, 0xC0, 0x24, 0xC0, 0x23, 0xC0, 0x28, 0xC0, 0x27, 0xC0, 0x0A, 0xC0, 0x09, 0xC0, 0x14, 0xC0, 0x13, 0x00, 0x9D, 0x00, 0x9C, 0x00, 0x3D, 0x00, 0x3C, 0x00, 0x35, 0x00, 0x2F, 0x00, 0x0A, // ClientHello.compression_methods 0x01, 0x01, // ClientHello.extension_list_length 0x00, 0x74, // Extension.extension_type (server_name) 0x00, 0x00, // ServerNameListExtension.length 0x00, 0x39, // ServerName.length 0x00, 0x37, // ServerName.type 0x00, // HostName.length 0x00, 0x34, // HostName.bytes 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, // Extension.extension_type (00 0A) 0x00, 0x0A, // Extension 0A 0x00, 0x08, 0x00, 0x06, 0x00, 0x1D, 0x00, 0x17, 0x00, 0x18, // Extension.extension_type (00 0B) 0x00, 0x0B, // Extension 0B 0x00, 0x02, 0x01, 0x00, // Extension.extension_type (00 0D) 0x00, 0x0D, // Extension 0D 0x00, 0x14, 0x00, 0x12, 0x04, 0x01, 0x05, 0x01, 0x02, 0x01, 0x04, 0x03, 0x05, 0x03, 0x02, 0x03, 0x02, 0x02, 0x06, 0x01, 0x06, 0x03, // Extension.extension_type (00 23) 0x00, 0x23, // Extension 00 23 0x00, 0x00, // Extension.extension_type (00 17) 0x00, 0x17, // Extension 17 0x00, 0x00, // Extension.extension_type (FF 01) 0xFF, 0x01, // Extension FF01 0x00, 0x01, 0x00 }; // #34 yield return new byte[] { // SslPlainText.(ContentType+ProtocolVersion) - unknown 0x01, 0x03, 0x04, // SslPlainText.length 0x00, 0xCB, // Handshake.msg_type (client hello) 0x01, // Handshake.length 0x00, 0x00, 0xC7, // ClientHello.client_version 0x03, 0x03, // ClientHello.random 0x58, 0xAA, 0x5F, 0xE7, 0x22, 0xCF, 0x9F, 0x59, 0x8A, 0xC5, 0x8B, 0x87, 0xC7, 0x62, 0x32, 0x98, 0xD4, 0xD8, 0xA2, 0xBE, 0x77, 0xCE, 0xA9, 0xCE, 0x42, 0x25, 0x5A, 0x8B, 0xEE, 0x16, 0x80, 0xF1, // ClientHello.SessionId 0x00, // ClientHello.cipher_suites 0x00, 0x2A, 0xC0, 0x2C, 0xC0, 0x2B, 0xC0, 0x30, 0xC0, 0x2F, 0x00, 0x9F, 0x00, 0x9E, 0xC0, 0x24, 0xC0, 0x23, 0xC0, 0x28, 0xC0, 0x27, 0xC0, 0x0A, 0xC0, 0x09, 0xC0, 0x14, 0xC0, 0x13, 0x00, 0x9D, 0x00, 0x9C, 0x00, 0x3D, 0x00, 0x3C, 0x00, 0x35, 0x00, 0x2F, 0x00, 0x0A, // ClientHello.compression_methods 0x01, 0x01, // ClientHello.extension_list_length 0x00, 0x74, // Extension.extension_type (server_name) 0x00, 0x00, // ServerNameListExtension.length 0x00, 0x39, // ServerName.length 0x00, 0x37, // ServerName.type 0x00, // HostName.length 0x00, 0x34, // HostName.bytes 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, // Extension.extension_type (00 0A) 0x00, 0x0A, // Extension 0A 0x00, 0x08, 0x00, 0x06, 0x00, 0x1D, 0x00, 0x17, 0x00, 0x18, // Extension.extension_type (00 0B) 0x00, 0x0B, // Extension 0B 0x00, 0x02, 0x01, 0x00, // Extension.extension_type (00 0D) 0x00, 0x0D, // Extension 0D 0x00, 0x14, 0x00, 0x12, 0x04, 0x01, 0x05, 0x01, 0x02, 0x01, 0x04, 0x03, 0x05, 0x03, 0x02, 0x03, 0x02, 0x02, 0x06, 0x01, 0x06, 0x03, // Extension.extension_type (00 23) 0x00, 0x23, // Extension 00 23 0x00, 0x00, // Extension.extension_type (00 17) 0x00, 0x17, // Extension 17 0x00, 0x00, // Extension.extension_type (FF 01) 0xFF, 0x01, // Extension FF01 0x00, 0x01, 0x00 }; // #35 yield return new byte[] { // SslPlainText.(ContentType+ProtocolVersion) 0x16, 0x03, 0x03, // SslPlainText.length 0x00, 0xCB, // Handshake.msg_type (client hello) 0x01, // Handshake.length 0x00, 0x00, 0xC7, // ClientHello.client_version 0x03, 0x03, // ClientHello.random 0x0C, 0x3C, 0x85, 0x78, 0xCA, 0x67, 0x70, 0xAA, 0x38, 0xCB, 0x28, 0xBC, 0xDC, 0x3E, 0x30, 0xBF, 0x11, 0x96, 0x95, 0x1A, 0xB9, 0xF0, 0x99, 0xA4, 0x91, 0x09, 0x13, 0xB4, 0x89, 0x94, 0x27, 0x2E, // ClientHello.SessionId 0x00, // ClientHello.cipher_suites 0x00, 0x2A, 0xC0, 0x2C, 0xC0, 0x2B, 0xC0, 0x30, 0xC0, 0x2F, 0x00, 0x9F, 0x00, 0x9E, 0xC0, 0x24, 0xC0, 0x23, 0xC0, 0x28, 0xC0, 0x27, 0xC0, 0x0A, 0xC0, 0x09, 0xC0, 0x14, 0xC0, 0x13, 0x00, 0x9D, 0x00, 0x9C, 0x00, 0x3D, 0x00, 0x3C, 0x00, 0x35, 0x00, 0x2F, 0x00, 0x0A, // ClientHello.compression_methods 0x01, 0x01, // ClientHello.extension_list_length 0x00, 0x74, // Extension.extension_type (server_name) 0x00, 0x00, // ServerNameListExtension.length 0x00, 0x39, // ServerName.length 0x00, 0x37, // ServerName.type 0x00, // HostName.length 0x00, 0x34, // HostName.bytes 0x80, 0x80, 0x80, 0x80, 0x61, // 0x80 0x80 0x80 0x80 is a forbidden utf-8 sequence 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, // Extension.extension_type (00 0A) 0x00, 0x0A, // Extension 0A 0x00, 0x08, 0x00, 0x06, 0x00, 0x1D, 0x00, 0x17, 0x00, 0x18, // Extension.extension_type (00 0B) 0x00, 0x0B, // Extension 0B 0x00, 0x02, 0x01, 0x00, // Extension.extension_type (00 0D) 0x00, 0x0D, // Extension 0D 0x00, 0x14, 0x00, 0x12, 0x04, 0x01, 0x05, 0x01, 0x02, 0x01, 0x04, 0x03, 0x05, 0x03, 0x02, 0x03, 0x02, 0x02, 0x06, 0x01, 0x06, 0x03, // Extension.extension_type (00 23) 0x00, 0x23, // Extension 00 23 0x00, 0x00, // Extension.extension_type (00 17) 0x00, 0x17, // Extension 17 0x00, 0x00, // Extension.extension_type (FF 01) 0xFF, 0x01, // Extension FF01 0x00, 0x01, 0x00 }; } } ================================================ FILE: test/ReverseProxy.Tests/WebSocketsTelemetry/WebSocketsParserTests.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.IO; using System.Text; using Xunit; using Yarp.Tests.Common; namespace Yarp.ReverseProxy.WebSocketsTelemetry.Tests; public abstract class WebSocketsParserTests { protected abstract bool IsServer { get; } private int MaskSize => IsServer ? 4 : 0; private WebSocketsParser CreateParser(TimeProvider timeProvider = null) => new(timeProvider ?? TimeProvider.System, IsServer); private ReadOnlySpan GetHeader(int opcode, int length, bool endOfMessage = true) { var header = new byte[2 + MaskSize + (length < 126 ? 0 : (length < 65536 ? 2 : 8))]; Assert.InRange(opcode, 0, 15); header[0] = (byte)opcode; if (endOfMessage) { header[0] |= 0x80; } if (length < 126) { header[1] = (byte)length; } else { header[1] = (byte)(length < 65536 ? 126 : 127); var i = header.Length - MaskSize - 1; while (length != 0) { header[i--] = (byte)(length % 256); length /= 256; } } if (IsServer) { header[1] |= 0x80; } return header; } private ReadOnlySpan GetCloseFrame(int length = 0) => GetBinaryMessageFrame(Encoding.UTF8.GetBytes(new string('a', length)), opcode: 8); private ReadOnlySpan GetPingFrame(int length = 0) => GetBinaryMessageFrame(Encoding.UTF8.GetBytes(new string('a', length)), opcode: 9); private ReadOnlySpan GetPongFrame(int length = 0) => GetBinaryMessageFrame(Encoding.UTF8.GetBytes(new string('a', length)), opcode: 10); private ReadOnlySpan GetTextMessageFrame(string message, bool continuation = false, bool endOfMessage = true) { var messageBytes = Encoding.UTF8.GetBytes(message); var header = GetHeader(opcode: continuation ? 0 : 1, length: messageBytes.Length, endOfMessage); var frame = new byte[header.Length + messageBytes.Length]; header.CopyTo(frame); messageBytes.CopyTo(frame, header.Length); return frame; } private ReadOnlySpan GetBinaryMessageFrame(ReadOnlySpan message, bool continuation = false, bool endOfMessage = true, int opcode = 2) { var header = GetHeader(opcode: continuation ? 0 : opcode, length: message.Length, endOfMessage); var frame = new byte[header.Length + message.Length]; header.CopyTo(frame); message.CopyTo(frame.AsSpan(header.Length)); return frame; } [Fact] public void CustomClockIsUsedForCloseTime() { var timeProvider = new TestTimeProvider(new TimeSpan(42)); var parser = CreateParser(timeProvider); Assert.Null(parser.CloseTime); parser.Consume(GetCloseFrame()); Assert.NotNull(parser.CloseTime); Assert.Equal(timeProvider.GetUtcNow(), parser.CloseTime.Value); } [Fact] public void MessagesAreCountedCorrectly() { var parser = CreateParser(); // Whole messages parser.Consume(GetTextMessageFrame("Foo")); Assert.Equal(1, parser.MessageCount); parser.Consume(GetBinaryMessageFrame(new byte[] { 4, 2 })); Assert.Equal(2, parser.MessageCount); // Continuations parser.Consume(GetTextMessageFrame("Hello, ", endOfMessage: false)); Assert.Equal(2, parser.MessageCount); parser.Consume(GetTextMessageFrame("world", continuation: true, endOfMessage: false)); Assert.Equal(2, parser.MessageCount); parser.Consume(GetTextMessageFrame("!", continuation: true, endOfMessage: true)); Assert.Equal(3, parser.MessageCount); parser.Consume(GetBinaryMessageFrame(new byte[] { 4 }, endOfMessage: false)); Assert.Equal(3, parser.MessageCount); parser.Consume(GetBinaryMessageFrame(new byte[] { 2 }, continuation: true, endOfMessage: true)); Assert.Equal(4, parser.MessageCount); // Large messages parser.Consume(GetTextMessageFrame(new string('a', 1_000))); Assert.Equal(5, parser.MessageCount); parser.Consume(GetTextMessageFrame(new string('b', 100_000))); Assert.Equal(6, parser.MessageCount); parser.Consume(GetBinaryMessageFrame(Encoding.UTF8.GetBytes(new string('c', 1_000)))); Assert.Equal(7, parser.MessageCount); parser.Consume(GetBinaryMessageFrame(Encoding.UTF8.GetBytes(new string('d', 100_000)))); Assert.Equal(8, parser.MessageCount); // Large messages with continuations parser.Consume(GetTextMessageFrame(new string('a', 1_000), endOfMessage: false)); Assert.Equal(8, parser.MessageCount); parser.Consume(GetTextMessageFrame(new string('b', 1_000), continuation: true, endOfMessage: true)); Assert.Equal(9, parser.MessageCount); parser.Consume(GetBinaryMessageFrame(Encoding.UTF8.GetBytes(new string('c', 1_000)), endOfMessage: false)); Assert.Equal(9, parser.MessageCount); parser.Consume(GetBinaryMessageFrame(Encoding.UTF8.GetBytes(new string('d', 1_000)), continuation: true, endOfMessage: true)); Assert.Equal(10, parser.MessageCount); // Fragmented frames parser.Consume(Array.Empty()); Assert.Equal(10, parser.MessageCount); ConsumeInFragments(ref parser, GetBinaryMessageFrame(Encoding.UTF8.GetBytes(new string('a', 1_000)))); Assert.Equal(11, parser.MessageCount); var ms = new MemoryStream(); for (var i = (int)parser.MessageCount; i < 500; i++) { // Control frames are not counted if (i % 7 == 0) { ms.Write(GetPingFrame()); } if (i % 13 == 0) { ms.Write(GetPongFrame()); } switch (i % 4) { case 0: ms.Write(GetTextMessageFrame(new string('a', i))); break; case 1: ms.Write(GetBinaryMessageFrame(Encoding.UTF8.GetBytes(new string('b', i)))); break; case 2: ms.Write(GetTextMessageFrame(new string('a', i), endOfMessage: false)); ms.Write(GetTextMessageFrame(new string('b', i), continuation: true, endOfMessage: false)); ms.Write(GetTextMessageFrame(new string('c', i), continuation: true, endOfMessage: true)); break; case 3: ms.Write(GetBinaryMessageFrame(Encoding.UTF8.GetBytes(new string('a', i)), endOfMessage: false)); ms.Write(GetBinaryMessageFrame(Encoding.UTF8.GetBytes(new string('b', i)), continuation: true, endOfMessage: false)); ms.Write(GetBinaryMessageFrame(Encoding.UTF8.GetBytes(new string('c', i)), continuation: true, endOfMessage: true)); break; } } ConsumeInFragments(ref parser, ms.ToArray()); Assert.Equal(500, parser.MessageCount); // Control frames are not counted parser.Consume(GetPingFrame()); parser.Consume(GetPingFrame(length: 10)); parser.Consume(GetPongFrame()); parser.Consume(GetPongFrame(length: 10)); parser.Consume(GetCloseFrame()); parser.Consume(GetCloseFrame(length: 10)); Assert.Equal(500, parser.MessageCount); // Messages are still counted after a close frame parser.Consume(GetTextMessageFrame("Foo")); Assert.Equal(501, parser.MessageCount); static void ConsumeInFragments(ref WebSocketsParser parser, ReadOnlySpan message) { var rng = new Random(42); while (message.Length != 0) { var fragmentLength = Math.Min(message.Length, rng.Next(0, 150)); parser.Consume(message[..fragmentLength]); message = message[fragmentLength..]; } } } } public sealed class WebSocketsParserTests_Client : WebSocketsParserTests { protected override bool IsServer => false; } public sealed class WebSocketsParserTests_Server : WebSocketsParserTests { protected override bool IsServer => true; } ================================================ FILE: test/ReverseProxy.Tests/Yarp.ReverseProxy.Tests.csproj ================================================ $(TestTFMs) Exe Yarp.ReverseProxy $(NoWarn);SYSLIB0039;SYSLIB0057 PreserveNewest ================================================ FILE: test/Tests.Common/TestAutoMockBase.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using Autofac.Core; using Autofac.Extras.Moq; using Moq; namespace Yarp.Tests.Common; /// /// Automatically generates mocks for interfaces on the Class under test. /// public class TestAutoMockBase : IDisposable { private bool _isDisposed; /// /// Initializes a new instance of the class. /// public TestAutoMockBase() { AutoMock = AutoMock.GetLoose(); } /// /// Gets the mocks. /// protected AutoMock AutoMock { get; private set; } /// /// Resets the mocks. /// public void ResetMocks() { AutoMock.Dispose(); AutoMock = AutoMock.GetLoose(); } /// /// Creates an object of using the dependency container. /// /// The type of the object to create. /// The parameters. /// /// Instance of . /// public virtual TService Create(params Parameter[] parameters) where TService : class { return AutoMock.Create(parameters); } /// /// Creates a mock of the specified abstraction. /// /// The type of the dependency to mock. /// A mock of the type. public Mock Mock() where TDependencyToMock : class { return AutoMock.Mock(); } /// /// Provide the specified abstraction to the dependency injection container. /// /// The type of the service. /// The instance. public void Provide(TService instance) where TService : class { ArgumentNullException.ThrowIfNull(instance); AutoMock.Provide(instance); } /// /// Provide the specified concrete type to the dependency injection container. /// /// The type of the service. /// The type that implements the service. /// An instance of that implements . public TImplementation Provide() where TService : class where TImplementation : TService { return (TImplementation)AutoMock.Provide(); } /// public virtual void Dispose() { if (!_isDisposed) { AutoMock.Dispose(); _isDisposed = true; } } } ================================================ FILE: test/Tests.Common/TestLogger.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Threading; using Microsoft.Extensions.Logging; namespace Yarp.ReverseProxy.Common; public sealed class TestLogger(ILogger xunitLogger, string categoryName) : ILogger { public record LogEntry(string CategoryName, LogLevel LogLevel, EventId EventId, string Message, Exception Exception); private static readonly AsyncLocal> _logsAsyncLocal = new(); public static List Collect() => _logsAsyncLocal.Value ??= []; public IDisposable BeginScope(TState state) where TState : notnull => xunitLogger.BeginScope(state); public bool IsEnabled(LogLevel logLevel) => true; public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) { _logsAsyncLocal.Value?.Add(new LogEntry(categoryName, logLevel, eventId, formatter(state, exception), exception)); xunitLogger.Log(logLevel, eventId, state, exception, formatter); } } ================================================ FILE: test/Tests.Common/TestLoggerProvider.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Testing; using Xunit; namespace Yarp.ReverseProxy.Common; public sealed class TestLoggerProvider(ITestOutputHelper output) : ILoggerProvider { private readonly XunitLoggerProvider _xunitLoggerProvider = new(output); public ILogger CreateLogger(string categoryName) => new TestLogger(_xunitLoggerProvider.CreateLogger(categoryName), categoryName); public void Dispose() => _xunitLoggerProvider.Dispose(); } ================================================ FILE: test/Tests.Common/TestRandom.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; namespace Yarp.Tests.Common; public class TestRandom : Random { public int[] Sequence { get; set; } public int Offset { get; set; } public override int Next(int maxValue) { return Sequence[Offset++]; } } ================================================ FILE: test/Tests.Common/TestRandomFactory.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using Yarp.ReverseProxy.Utilities; namespace Yarp.Tests.Common; public class TestRandomFactory : IRandomFactory { public TestRandom Instance { get; set; } public Random CreateRandomInstance() { return Instance; } } ================================================ FILE: test/Tests.Common/TestTimeProvider.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Xunit; namespace Yarp.Tests.Common; /// /// Simulates passage of time, used for testing. /// /// /// This timer doesn't track real time, but instead tracks virtual time. /// Time only advances when any of the following methods are called: /// /// /// /// /// public class TestTimeProvider : TimeProvider { private readonly List _timers = new(); private TimeSpan _currentTime; public int TimerCount => _timers.Count; // Mess with the frequency to check for bad assumptions in the code. public override long TimestampFrequency => TimeSpan.TicksPerSecond * 7; /// /// Initializes a new instance of the class. /// /// Initial value for current time. Zero if not specified. public TestTimeProvider(TimeSpan? initialTime = null) { _currentTime = initialTime ?? TimeSpan.Zero; } public TestTimeProvider(DateTimeOffset initialTime) { _currentTime = initialTime - DateTimeOffset.UnixEpoch; } /// /// Advances time by the specified amount. /// /// How much to advance by. public void Advance(TimeSpan howMuch) { AdvanceTo(_currentTime + howMuch); } /// /// Advances time to the specified point. /// /// Advances until it equals . public void AdvanceTo(TimeSpan targetTime) { if (targetTime < _currentTime) { throw new InvalidOperationException("Time should not flow backwards"); } // We could use this to fire timers, but timers are currently fired manually by tests. _currentTime = targetTime; } public override DateTimeOffset GetUtcNow() => new DateTime(_currentTime.Ticks, DateTimeKind.Utc); // Mess with the frequency to check for bad assumptions in the code. public override long GetTimestamp() => _currentTime.Ticks * 7; public override ITimer CreateTimer(TimerCallback callback, object state, TimeSpan dueTime, TimeSpan period) { Assert.Equal(Timeout.InfiniteTimeSpan, period); var timer = new TestTimer(callback, state, dueTime, period); _timers.Add(timer); return timer; } public void FireTimer(int idx) { _timers[idx].Fire(); } public void FireAllTimers() { for (var i = 0; i < _timers.Count; i++) { FireTimer(i); } } public void VerifyTimer(int idx, TimeSpan dueTime) { Assert.Equal(dueTime, _timers[idx].DueTime); } public void AssertTimerDisposed(int idx) { Assert.True(_timers[idx].IsDisposed); } private sealed class TestTimer : ITimer { public TestTimer(TimerCallback callback, object state, TimeSpan dueTime, TimeSpan period) { Callback = callback; State = state; DueTime = dueTime; Period = period; } public TimeSpan DueTime { get; private set; } public TimeSpan Period { get; private set; } public TimerCallback Callback { get; private set; } public object State { get; private set; } public bool IsDisposed { get; private set; } public bool Change(TimeSpan dueTime, TimeSpan period) { DueTime = dueTime; Period = period; return true; } public void Fire() { Callback(State); } public void Dispose() { IsDisposed = true; } public ValueTask DisposeAsync() { IsDisposed = true; return default; } } } ================================================ FILE: test/Tests.Common/XunitLoggerFactoryExtensions.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging.Testing; using Xunit; namespace Microsoft.Extensions.Logging; public static class XunitLoggerFactoryExtensions { public static ILoggingBuilder AddXunit(this ILoggingBuilder builder, ITestOutputHelper output) { builder.Services.AddSingleton(new XunitLoggerProvider(output)); return builder; } public static ILoggingBuilder AddXunit(this ILoggingBuilder builder, ITestOutputHelper output, LogLevel minLevel) { builder.Services.AddSingleton(new XunitLoggerProvider(output, minLevel)); return builder; } public static ILoggingBuilder AddXunit(this ILoggingBuilder builder, ITestOutputHelper output, LogLevel minLevel, DateTimeOffset? logStart) { builder.Services.AddSingleton(new XunitLoggerProvider(output, minLevel, logStart)); return builder; } public static ILoggerFactory AddXunit(this ILoggerFactory loggerFactory, ITestOutputHelper output) { loggerFactory.AddProvider(new XunitLoggerProvider(output)); return loggerFactory; } public static ILoggerFactory AddXunit(this ILoggerFactory loggerFactory, ITestOutputHelper output, LogLevel minLevel) { loggerFactory.AddProvider(new XunitLoggerProvider(output, minLevel)); return loggerFactory; } public static ILoggerFactory AddXunit(this ILoggerFactory loggerFactory, ITestOutputHelper output, LogLevel minLevel, DateTimeOffset? logStart) { loggerFactory.AddProvider(new XunitLoggerProvider(output, minLevel, logStart)); return loggerFactory; } } ================================================ FILE: test/Tests.Common/XunitLoggerProvider.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Globalization; using System.Linq; using System.Text; using System.Threading; using Xunit; namespace Microsoft.Extensions.Logging.Testing; public class XunitLoggerProvider : ILoggerProvider { // Used to distinguish when multiple apps are running as part of the same test. private static int InstanceCount; private readonly int _providerInstanceId = Interlocked.Increment(ref InstanceCount); private readonly ITestOutputHelper _output; private readonly LogLevel _minLevel; private readonly DateTimeOffset? _logStart; public XunitLoggerProvider(ITestOutputHelper output) : this(output, LogLevel.Trace) { } public XunitLoggerProvider(ITestOutputHelper output, LogLevel minLevel) : this(output, minLevel, null) { } public XunitLoggerProvider(ITestOutputHelper output, LogLevel minLevel, DateTimeOffset? logStart) { _output = output; _minLevel = minLevel; _logStart = logStart; } public ILogger CreateLogger(string categoryName) { return new XunitLogger(_output, categoryName, _minLevel, _logStart, _providerInstanceId); } public void Dispose() { } } public class XunitLogger : ILogger { private static readonly string[] NewLineChars = new[] { Environment.NewLine }; private readonly string _category; private readonly LogLevel _minLogLevel; private readonly ITestOutputHelper _output; private readonly DateTimeOffset? _logStart; private readonly int _providerInstanceId; public XunitLogger(ITestOutputHelper output, string category, LogLevel minLogLevel, DateTimeOffset? logStart, int providerInstanceId) { _minLogLevel = minLogLevel; _category = category; _output = output; _logStart = logStart; _providerInstanceId = providerInstanceId; } public void Log( LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) { if (!IsEnabled(logLevel)) { return; } // Buffer the message into a single string in order to avoid shearing the message when running across multiple threads. var messageBuilder = new StringBuilder(); var timestamp = _logStart.HasValue ? $"{(DateTimeOffset.UtcNow - _logStart.Value).TotalSeconds.ToString("N3", CultureInfo.InvariantCulture)}s" : DateTimeOffset.UtcNow.ToString("s", CultureInfo.InvariantCulture); var firstLinePrefix = $"| [{timestamp}] I:{_providerInstanceId} {_category} {logLevel}: "; var lines = formatter(state, exception).Split(NewLineChars, StringSplitOptions.RemoveEmptyEntries); messageBuilder.AppendLine(firstLinePrefix + lines.FirstOrDefault() ?? string.Empty); var additionalLinePrefix = "|" + new string(' ', firstLinePrefix.Length - 1); foreach (var line in lines.Skip(1)) { messageBuilder.AppendLine(additionalLinePrefix + line); } if (exception != null) { lines = exception.ToString().Split(NewLineChars, StringSplitOptions.RemoveEmptyEntries); additionalLinePrefix = "| "; foreach (var line in lines) { messageBuilder.AppendLine(additionalLinePrefix + line); } } // Remove the last line-break, because ITestOutputHelper only has WriteLine. var message = messageBuilder.ToString(); if (message.EndsWith(Environment.NewLine, StringComparison.Ordinal)) { message = message.Substring(0, message.Length - Environment.NewLine.Length); } try { _output.WriteLine(message); } catch (Exception) { // We could fail because we're on a background thread and our captured ITestOutputHelper is // busted (if the test "completed" before the background thread fired). // So, ignore this. There isn't really anything we can do but hope the // caller has additional loggers registered } } public bool IsEnabled(LogLevel logLevel) => logLevel >= _minLogLevel; public IDisposable BeginScope(TState state) => new NullScope(); private sealed class NullScope : IDisposable { public void Dispose() { } } } ================================================ FILE: test/Tests.Common/Yarp.Tests.Common.csproj ================================================ $(TestTFMs) Library Yarp.Common.Tests false true ================================================ FILE: test.cmd ================================================ @echo off powershell -ExecutionPolicy ByPass -NoProfile -command "& """%~dp0eng\common\Build.ps1""" -test %*" ================================================ FILE: test.sh ================================================ #!/usr/bin/env bash source="${BASH_SOURCE[0]}" # resolve $SOURCE until the file is no longer a symlink while [[ -h $source ]]; do scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" source="$(readlink "$source")" # if $source was a relative symlink, we need to resolve it relative to the path where the # symlink file was located [[ $source != /* ]] && source="$scriptroot/$source" done scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" "$scriptroot/eng/common/build.sh" --test $@ ================================================ FILE: testassets/BenchmarkApp/BenchmarkApp.csproj ================================================ $(TestTFMs) $(NoWarn);SYSLIB0057 Always ================================================ FILE: testassets/BenchmarkApp/Program.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Diagnostics; using System.IO; using System.Net.Http; using System.Net; using System.Security.Cryptography.X509Certificates; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Crank.EventSources; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Yarp.ReverseProxy.Forwarder; using Yarp.ReverseProxy.Transforms.Builder; BenchmarksEventSource.MeasureAspNetVersion(); BenchmarksEventSource.MeasureNetCoreAppVersion(); var config = new ConfigurationBuilder() .AddEnvironmentVariables(prefix: "ASPNETCORE_") .AddCommandLine(args) .AddJsonFile("appsettings.json", optional: true) .Build(); var builder = new WebHostBuilder() .ConfigureLogging(loggerFactory => { if (Enum.TryParse(config["LogLevel"], out LogLevel logLevel)) { Console.WriteLine($"Console Logging enabled with level '{logLevel}'"); loggerFactory.AddConsole().SetMinimumLevel(logLevel); } }) .UseKestrel((context, kestrelOptions) => { kestrelOptions.ConfigureHttpsDefaults(httpsOptions => { httpsOptions.ServerCertificate = new X509Certificate2(Path.Combine(context.HostingEnvironment.ContentRootPath, "testCert.pfx"), "testPassword"); }); }) .UseContentRoot(Directory.GetCurrentDirectory()) .UseConfiguration(config) .ConfigureServices(services => { services.AddHttpForwarder(); }) ; builder.Configure(app => { var forwarder = app.ApplicationServices.GetRequiredService(); var clusterUrl = GetClusterUrl(); var httpClient = new HttpMessageInvoker(CreateHandler()); var transformer = CreateHttpTransformer(app); app.Run(async context => { await forwarder.SendAsync(context, clusterUrl, httpClient, ForwarderRequestConfig.Empty, transformer); }); }); builder.Build().Run(); string GetClusterUrl() { var clusterUrls = config["clusterUrls"]; if (string.IsNullOrWhiteSpace(clusterUrls)) { throw new ArgumentException("--clusterUrls is required"); } var clusterUrl = clusterUrls.Split(';')[0]; Console.WriteLine($"ClusterUrl: {clusterUrl}"); return clusterUrl; } static SocketsHttpHandler CreateHandler() { var handler = new SocketsHttpHandler { UseProxy = false, AllowAutoRedirect = false, AutomaticDecompression = DecompressionMethods.None, UseCookies = false, EnableMultipleHttp2Connections = true, ActivityHeadersPropagator = new ReverseProxyPropagator(DistributedContextPropagator.Current), ConnectTimeout = TimeSpan.FromSeconds(15), }; handler.SslOptions.RemoteCertificateValidationCallback = delegate { return true; }; return handler; } static HttpTransformer CreateHttpTransformer(IApplicationBuilder app) { var transformBuilder = app.ApplicationServices.GetRequiredService(); return transformBuilder.Create(context => { context.UseDefaultForwarders = false; }); } ================================================ FILE: testassets/BenchmarkApp/Properties/launchSettings.json ================================================ { "profiles": { "BenchmarkApp": { "commandName": "Project", "commandLineArgs": "--urls http://localhost:5000 --clusterUrls http://httpbin.org", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } } } } ================================================ FILE: testassets/BenchmarkApp/README.md ================================================ ### Crank command to test against a local BenchmarkServer 1. Follow the [Crank Getting Started Guide](https://github.com/dotnet/crank/blob/master/docs/getting_started.md) to install Microsoft.Crank.Controller and Microsoft.Crank.Agent globally. 2. In one shell, run `crank-agent` 3. In another shell, run `crank` as follows: ```bash crank ` --config https://raw.githubusercontent.com/aspnet/Benchmarks/master/scenarios/proxy.benchmarks.yml ` --scenario proxy-yarp ` --profile local ` --load.variables.duration 5 ` --variable path=/?s=1024 ` --variable serverScheme=https ` --variable downstreamScheme=https ` --load.variables.transport http2 ` --downstream.variables.httpProtocol http2 ``` ================================================ FILE: testassets/BenchmarkApp/appsettings.json ================================================ { "AllowedHosts": "*", "LogLevel": "" } ================================================ FILE: testassets/Directory.Build.props ================================================ true false ================================================ FILE: testassets/ReverseProxy.Code/Controllers/HealthController.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Mvc; using Yarp.ReverseProxy.Health; namespace Yarp.ReverseProxy.Sample.Controllers; /// /// Controller for health check api. /// [ApiController] public class HealthController : ControllerBase { private readonly IActiveHealthCheckMonitor _healthCheckMonitor; /// /// Initializes a new instance of the class. /// public HealthController(IActiveHealthCheckMonitor healthCheckMonitor) { _healthCheckMonitor = healthCheckMonitor; } /// /// Returns 200 if Proxy is healthy. /// [HttpGet] [Route("/api/health")] public IActionResult CheckHealth() { // TODO: Implement health controller, use guid in route. return _healthCheckMonitor.InitialProbeCompleted ? Ok() : StatusCode(503); } } ================================================ FILE: testassets/ReverseProxy.Code/ForwarderMetricsConsumer.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using Yarp.Telemetry.Consumption; namespace Yarp.ReverseProxy.Sample; public sealed class ForwarderMetricsConsumer : IMetricsConsumer { public void OnMetrics(ForwarderMetrics previous, ForwarderMetrics current) { var elapsed = current.Timestamp - previous.Timestamp; var newRequests = current.RequestsStarted - previous.RequestsStarted; Console.Title = $"Proxied {current.RequestsStarted} requests ({newRequests} in the last {(int)elapsed.TotalMilliseconds} ms)"; } } ================================================ FILE: testassets/ReverseProxy.Code/ForwarderTelemetryConsumer.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Threading; using Microsoft.AspNetCore.Http; using Yarp.Telemetry.Consumption; namespace Yarp.ReverseProxy.Sample; public sealed class ForwarderTelemetryConsumer : IForwarderTelemetryConsumer { private readonly IHttpContextAccessor _httpContextAccessor; private readonly AsyncLocal _startTime = new(); public ForwarderTelemetryConsumer(IHttpContextAccessor httpContextAccessor) { _httpContextAccessor = httpContextAccessor; } public void OnForwarderStart(DateTime timestamp, string destinationPrefix) { _startTime.Value = timestamp; } public void OnForwarderStop(DateTime timestamp, int statusCode) { if (_startTime.Value is DateTime startTime) { var elapsed = timestamp - startTime; var path = _httpContextAccessor.HttpContext.Request.Path; Console.WriteLine($"Spent {elapsed.TotalMilliseconds:N2} ms proxying {path}"); } } } ================================================ FILE: testassets/ReverseProxy.Code/MyTransformFactory.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Net.Http; using Yarp.ReverseProxy.Transforms; using Yarp.ReverseProxy.Transforms.Builder; namespace Yarp.ReverseProxy.Sample; internal sealed 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 => { transformContext.ProxyRequest.Options.Set(new HttpRequestOptionsKey("CustomTransform"), value); return default; }); return true; } return false; } } ================================================ FILE: testassets/ReverseProxy.Code/MyTransformProvider.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Net.Http; using Yarp.ReverseProxy.Transforms; using Yarp.ReverseProxy.Transforms.Builder; namespace Yarp.ReverseProxy.Sample; internal sealed class MyTransformProvider : ITransformProvider { public void ValidateRoute(TransformRouteValidationContext context) { // Check all routes for a custom property and validate the associated transform data. if (context.Route.Metadata?.TryGetValue("CustomMetadata", out var value) ?? false) { if (string.IsNullOrEmpty(value)) { context.Errors.Add(new ArgumentException("A non-empty CustomMetadata value is required")); } } } public void ValidateCluster(TransformClusterValidationContext context) { // Check all clusters for a custom property and validate the associated transform data. if (context.Cluster.Metadata?.TryGetValue("CustomMetadata", out var 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. if ((transformBuildContext.Route.Metadata?.TryGetValue("CustomMetadata", out var value) ?? false) || (transformBuildContext.Cluster?.Metadata?.TryGetValue("CustomMetadata", out value) ?? false)) { if (string.IsNullOrEmpty(value)) { throw new ArgumentException("A non-empty CustomMetadata value is required"); } transformBuildContext.AddRequestTransform(transformContext => { transformContext.ProxyRequest.Options.Set(new HttpRequestOptionsKey("CustomMetadata"), value); return default; }); } } } ================================================ FILE: testassets/ReverseProxy.Code/Program.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you 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.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Yarp.ReverseProxy; using Yarp.ReverseProxy.Configuration; using Yarp.ReverseProxy.Model; using Yarp.ReverseProxy.Sample; using Yarp.ReverseProxy.Transforms; using Yarp.Telemetry.Consumption; var builder = WebApplication.CreateBuilder(args); var services = builder.Services; services.AddControllers(); var routes = new[] { new RouteConfig() { RouteId = "route1", ClusterId = "cluster1", Match = new RouteMatch { Path = "{**catch-all}" }, Timeout = TimeSpan.FromSeconds(5), } }; var clusters = new[] { new ClusterConfig() { ClusterId = "cluster1", SessionAffinity = new SessionAffinityConfig { Enabled = true, Policy = "Cookie", AffinityKeyName = ".Yarp.ReverseProxy.Affinity" }, Destinations = new Dictionary(StringComparer.OrdinalIgnoreCase) { { "destination1", new DestinationConfig() { Address = "https://localhost:10000" } } } }, new ClusterConfig() { ClusterId = "cluster2", Destinations = new Dictionary(StringComparer.OrdinalIgnoreCase) { { "destination2", new DestinationConfig() { Address = "https://localhost:10001" } } } } }; services.AddReverseProxy() .LoadFromMemory(routes, clusters) .ConfigureHttpClient((context, handler) => { handler.Expect100ContinueTimeout = TimeSpan.FromMilliseconds(300); }) .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); }); } transformBuilderContext.AddResponseTransform(context => { // Suppress the response body from errors. // The status code was already copied. if (context.ProxyResponse?.IsSuccessStatusCode == false) { context.SuppressResponseBody = true; } return default; }); }); services.AddHttpContextAccessor(); services.AddSingleton, ForwarderMetricsConsumer>(); services.AddTelemetryConsumer(); services.AddTelemetryListeners(); services.AddRequestTimeouts(o => { o.DefaultPolicy = new Microsoft.AspNetCore.Http.Timeouts.RequestTimeoutPolicy() { Timeout = TimeSpan.FromSeconds(1), TimeoutStatusCode = StatusCodes.Status418ImATeapot, }; }); var app = builder.Build(); app.UseAuthorization(); app.UseRequestTimeouts(); app.MapControllers(); app.MapReverseProxy(proxyPipeline => { // Custom endpoint selection proxyPipeline.Use((context, next) => { var lookup = context.RequestServices.GetRequiredService(); if (lookup.TryGetCluster("cluster2", out var cluster)) { context.ReassignProxyRequest(cluster); } 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.UseSessionAffinity(); proxyPipeline.UseLoadBalancing(); }); app.Run(); ================================================ FILE: testassets/ReverseProxy.Code/Properties/launchSettings.json ================================================ { "iisSettings": { "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { "applicationUrl": "https://localhost:44356/", "sslPort": 44356 } }, "profiles": { "ReverseProxy.Code": { "commandName": "Project", "launchBrowser": false, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } } } } ================================================ FILE: testassets/ReverseProxy.Code/ReverseProxy.Code.csproj ================================================ $(TestTFMs) Exe Yarp.ReverseProxy.Sample ================================================ FILE: testassets/ReverseProxy.Code/TokenService.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Security.Claims; using System.Threading.Tasks; namespace Yarp.ReverseProxy.Sample; internal sealed class TokenService { internal Task GetAuthTokenAsync(ClaimsPrincipal user) { return Task.FromResult(user.Identity.Name); } } ================================================ FILE: testassets/ReverseProxy.Code/appsettings.Development.json ================================================ { "Logging": { "LogLevel": { "Default": "Debug", "Microsoft": "Debug", "Microsoft.Hosting.Lifetime": "Information" } } } ================================================ FILE: testassets/ReverseProxy.Code/appsettings.json ================================================ { "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } }, "AllowedHosts": "*" } ================================================ FILE: testassets/ReverseProxy.Config/Controllers/HealthController.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Mvc; namespace Yarp.ReverseProxy.Sample.Controllers; /// /// Controller for health check api. /// [ApiController] public class HealthController : ControllerBase { /// /// Returns 200 if Proxy is healthy. /// [HttpGet] [Route("/api/health")] public IActionResult CheckHealth() { return Ok(); } } ================================================ FILE: testassets/ReverseProxy.Config/CustomConfigFilter.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Configuration; using Yarp.ReverseProxy.Configuration; using Yarp.ReverseProxy.Health; namespace Yarp.ReverseProxy.Sample; public sealed class CustomConfigFilter : IProxyConfigFilter { public ValueTask ConfigureClusterAsync(ClusterConfig 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 HealthCheckConfig { Active = new ActiveHealthCheckConfig { Enabled = true, Policy = HealthCheckConstants.ActivePolicy.ConsecutiveFailures, }, Passive = cluster.HealthCheck?.Passive, } }; } // Or wrap the metadata in config sugar var config = new ConfigurationBuilder().AddInMemoryCollection(cluster.Metadata).Build(); if (config.GetValue("CustomHealth")) { cluster = cluster with { HealthCheck = new HealthCheckConfig { Active = new ActiveHealthCheckConfig { Enabled = true, Policy = HealthCheckConstants.ActivePolicy.ConsecutiveFailures, }, Passive = cluster.HealthCheck?.Passive, } }; } return new ValueTask(cluster); } public ValueTask ConfigureRouteAsync(RouteConfig route, ClusterConfig cluster, 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); } } ================================================ FILE: testassets/ReverseProxy.Config/Program.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using Yarp.ReverseProxy.Model; using Yarp.ReverseProxy.Sample; var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); builder.Services.AddReverseProxy() .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy")) .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy2")) .AddConfigFilter(); var app = builder.Build(); app.MapControllers(); app.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.UseSessionAffinity(); proxyPipeline.UseLoadBalancing(); proxyPipeline.UsePassiveHealthChecks(); }).ConfigureEndpoints((builder, route) => builder.WithDisplayName($"ReverseProxy {route.RouteId}-{route.ClusterId}")); app.Run(); ================================================ FILE: testassets/ReverseProxy.Config/Properties/launchSettings.json ================================================ { "iisSettings": { "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { "applicationUrl": "https://localhost:44356/", "sslPort": 44356 } }, "profiles": { "ReverseProxy.Config": { "commandName": "Project", "launchBrowser": false, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } } } } ================================================ FILE: testassets/ReverseProxy.Config/ReverseProxy.Config.csproj ================================================ $(TestTFMs) Exe Yarp.ReverseProxy.Sample ================================================ FILE: testassets/ReverseProxy.Config/appsettings.Development.json ================================================ { "Logging": { "LogLevel": { "Default": "Debug", "Microsoft": "Debug", "Microsoft.Hosting.Lifetime": "Information" } } } ================================================ FILE: testassets/ReverseProxy.Config/appsettings.json ================================================ { "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, "Policy": "Cookie", "AffinityKeyName": ".Yarp.Affinity" }, "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": { "route1": { "ClusterId": "cluster1", "Match": { "Methods": [ "GET", "POST" ], "Hosts": [ "localhost" ], "Path": "/api/{action}" } }, "route2": { "ClusterId": "cluster2", "Match": { "Hosts": [ "localhost" ], "Path": "/api/{plugin}/stuff/{**remainder}" }, "Transforms": [ { "PathPattern": "/foo/{plugin}/bar/{**remainder}" }, { "X-Forwarded": "Append", "HeaderPrefix": "X-Forwarded-" }, { "Forwarded": "by,host,for,proto", "ByFormat": "Random", "ForFormat": "IpAndPort" }, { "ClientCert": "X-Client-Cert" }, { "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" } ] } } }, "ReverseProxy2": { "Clusters": { "cluster3": { "LoadBalancingPolicy": "Random", "SessionAffinity": { "Enabled": "true", "Policy": "Cookie", "AffinityKeyName": ".Yarp.Affinity" }, "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/" } } } }, "Routes": { "route3": { "ClusterId": "cluster3", "Match": { "Methods": [ "GET", "POST" ], "Hosts": [ "localhost" ], "Path": "/api2/{action}" } } } } } ================================================ FILE: testassets/ReverseProxy.Direct/Program.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.AspNetCore.Connections; using Yarp.ReverseProxy.Sample; using Yarp.ReverseProxy.Forwarder; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Yarp.ReverseProxy.Transforms; using System.Net.Http; using System.Threading; using System; using System.Net; using System.Diagnostics; using Yarp.ReverseProxy.Transforms.Builder; var builder = WebApplication.CreateBuilder(args); builder.WebHost.ConfigureKestrel(kestrel => { var logger = kestrel.ApplicationServices.GetRequiredService>(); kestrel.ListenAnyIP(5001, portOptions => { portOptions.Use(async (connectionContext, next) => { await TlsFilter.ProcessAsync(connectionContext, next, logger); }); portOptions.UseHttps(); }); }); builder.Services.AddHttpForwarder(); var app = builder.Build(); var httpClient = new HttpMessageInvoker(new SocketsHttpHandler { UseProxy = false, AllowAutoRedirect = false, AutomaticDecompression = DecompressionMethods.None, UseCookies = false, EnableMultipleHttp2Connections = true, ActivityHeadersPropagator = new ReverseProxyPropagator(DistributedContextPropagator.Current), ConnectTimeout = TimeSpan.FromSeconds(15), }); var transformBuilder = app.Services.GetRequiredService(); var transformer = transformBuilder.Create(context => { context.AddQueryRemoveKey("param1"); context.AddQueryValue("area", "xx2", false); context.AddOriginalHost(false); }); // or var transformer = new CustomTransformer(); // or var transformer = HttpTransformer.Default; var requestConfig = new ForwarderRequestConfig { ActivityTimeout = TimeSpan.FromSeconds(100) }; app.MapForwarder("/{**catch-all}", "https://example.com", requestConfig, transformer, httpClient); app.Run(); internal sealed class CustomTransformer : HttpTransformer { public override async ValueTask TransformRequestAsync(HttpContext httpContext, HttpRequestMessage proxyRequest, string destinationPrefix, CancellationToken cancellationToken) { // Copy all request headers await base.TransformRequestAsync(httpContext, proxyRequest, destinationPrefix, cancellationToken); // 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; } public override ValueTask TransformResponseAsync(HttpContext httpContext, HttpResponseMessage proxyResponse, CancellationToken cancellationToken) { // Suppress the response body from errors. // The status code was already copied. if (!proxyResponse.IsSuccessStatusCode) { return new ValueTask(false); } return base.TransformResponseAsync(httpContext, proxyResponse, cancellationToken); } } ================================================ FILE: testassets/ReverseProxy.Direct/Properties/launchSettings.json ================================================ { "iisSettings": { "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { "applicationUrl": "https://localhost:44356/", "sslPort": 44356 } }, "profiles": { "ReverseProxy.Direct": { "commandName": "Project", "launchBrowser": false, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } } } } ================================================ FILE: testassets/ReverseProxy.Direct/ReverseProxy.Direct.csproj ================================================ $(TestTFMs) Exe Yarp.ReverseProxy.Sample ================================================ FILE: testassets/ReverseProxy.Direct/TlsFilter.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Buffers; using System.IO.Pipelines; using System.Security.Authentication; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Connections; using Microsoft.Extensions.Logging; using Yarp.ReverseProxy.Utilities.Tls; namespace Yarp.ReverseProxy.Sample; public static class TlsFilter { // Use reasonable limits. Parsing across multiple segments has an O(N^2) worst case, so limit the N. private const int ClientHelloTimeoutMs = 10_000; private const int MaxClientHelloSize = 10 * 1024; // 10 KB // This sniffs the TLS handshake and rejects requests that meat specific criteria. internal static async Task ProcessAsync(ConnectionContext connectionContext, Func next, ILogger logger) { using (var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(connectionContext.ConnectionClosed)) { timeoutCts.CancelAfter(ClientHelloTimeoutMs); var input = connectionContext.Transport.Input; // Count how many bytes we've examined so we never go backwards, Pipes don't allow that. var minBytesExamined = 0L; while (true) { var result = await input.ReadAsync(timeoutCts.Token); var buffer = result.Buffer; if (result.IsCompleted || result.IsCanceled) { return; } if (buffer.Length == 0) { continue; } if (!TryReadTlsFrame(buffer, logger, out var frameInfo) && frameInfo.ParsingStatus == TlsFrameHelper.ParsingStatus.IncompleteFrame) { // We didn't find a TLS frame, we need to read more data. minBytesExamined = buffer.Length; if (minBytesExamined >= MaxClientHelloSize) { logger.LogInformation("Client Hello too large. Aborting."); return; } input.AdvanceTo(buffer.Start, buffer.End); continue; } // We're done. We either have a frame we can analyze, or we're giving up. var examined = buffer.Slice(buffer.Start, minBytesExamined).End; input.AdvanceTo(buffer.Start, examined); if (frameInfo.ParsingStatus != TlsFrameHelper.ParsingStatus.Ok || frameInfo.HandshakeType != TlsHandshakeType.ClientHello) { logger.LogInformation("Invalid or unexpected TLS frame. Aborting."); return; } // Perform any additional validation on the Client Hello here. // Rate limiting, throttling checks, J4A fingerprinting, logging, etc. can be performed here as well. if (!TryProcessClientHello(frameInfo, logger)) { // Abort the connection. return; } // All checks passed, we can continue processing the request. #if !NET10_0_OR_GREATER // Workaround for https://github.com/dotnet/runtime/issues/107213, which was fixed in .NET 10. if (minBytesExamined > 0) { connectionContext.Transport = new DuplexPipe( PipeReader.Create(input.AsStream(), new StreamPipeReaderOptions(bufferSize: Math.Max(4096, (int)minBytesExamined))), connectionContext.Transport.Output); } #endif break; } } await next(); } /// Process the Client Hello and returns whether it passed validation. private static bool TryProcessClientHello(TlsFrameHelper.TlsFrameInfo clientHello, ILogger logger) { // This is a sample demonstrating several checks you can perform on the Client Hello. // Replace the logic in this method with your own validation logic. string sni = clientHello.TargetName; if (string.IsNullOrEmpty(sni)) { logger.LogInformation("Expected SNI to be specified."); return false; } if (!AllowHost(sni)) { logger.LogInformation("Unexpected SNI: {sni}.", sni); return false; } if (!clientHello.SupportedVersions.HasFlag(SslProtocols.Tls12) && !clientHello.SupportedVersions.HasFlag(SslProtocols.Tls13)) { logger.LogInformation("Client for '{sni}' does not support TLS 1.2 or 1.3.", sni); return false; } if (!clientHello.ApplicationProtocols.HasFlag(TlsFrameHelper.ApplicationProtocolInfo.Http2)) { logger.LogInformation("Client for '{sni}' does not support HTTP/2.", sni); return false; } // All checks passed, we can continue processing the request. return true; } private static bool AllowHost(string targetName) { return targetName.Equals("localhost", StringComparison.OrdinalIgnoreCase) || targetName.Equals("contoso.com", StringComparison.OrdinalIgnoreCase); } /// Attempt to parse the first TLS frame from the and indicate whether more data is needed. private static bool TryReadTlsFrame(ReadOnlySequence buffer, ILogger logger, out TlsFrameHelper.TlsFrameInfo frame) { frame = default; // Try to process the first segment first. var data = buffer.First.Span; if (TlsFrameHelper.TryGetFrameInfo(data, ref frame)) { // This is the common fast path. return true; } if (frame.ParsingStatus != TlsFrameHelper.ParsingStatus.IncompleteFrame) { // The input is invalid, reading more data won't help. return false; } if (buffer.IsSingleSegment) { // We only have one segment and it didn't contain a valid TLS frame. We'll have to read more data. return false; } // We have multiple segments. TlsFrameHelper only works with a single span, so we need to combine them. // This may happen on every new read, which is why we limit how much data we're willing to process. var pooledBuffer = ArrayPool.Shared.Rent((int)buffer.Length); buffer.CopyTo(pooledBuffer); data = pooledBuffer.AsSpan(0, (int)buffer.Length); bool success = TlsFrameHelper.TryGetFrameInfo(data, ref frame); ArrayPool.Shared.Return(pooledBuffer); if (success) { logger.LogDebug("Parsed multi-segment TLS frame after {length} bytes", buffer.Length); } return success; } private sealed class DuplexPipe(PipeReader input, PipeWriter output) : IDuplexPipe { public PipeReader Input { get; } = input; public PipeWriter Output { get; } = output; } } ================================================ FILE: testassets/ReverseProxy.Direct/appsettings.Development.json ================================================ { "Logging": { "LogLevel": { "Default": "Debug", "Microsoft": "Debug", "Microsoft.Hosting.Lifetime": "Information" } } } ================================================ FILE: testassets/ReverseProxy.Direct/appsettings.json ================================================ { "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } }, "AllowedHosts": "*" } ================================================ FILE: testassets/TestClient/CommandLineArgs.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; namespace SampleClient; internal sealed class CommandLineArgs { private CommandLineArgs() { } public bool Help { get; private set; } public string Scenario { get; private set; } public string Target { get; private set; } = "https://localhost:1443/"; public static CommandLineArgs Parse(string[] args) { var result = new CommandLineArgs(); var i = 0; for (i = 0; i < args.Length; i++) { switch (args[i]) { case "--help": case "-h": case "-?": case "/?": result.Help = true; break; case "--scenario": case "-s": result.Scenario = args[++i]; break; case "--target": case "-t": result.Target = args[++i]; break; } } if (i < args.Length) { return ParseRemainder(result, args.AsSpan().Slice(i)); } return result; static CommandLineArgs ParseRemainder(CommandLineArgs result, Span remainder) { if (remainder.Length == 0) { throw new ArgumentException("Expected additional args."); } if (remainder.Length > 1) { throw new ArgumentException($"Unexpected arg '{remainder[1]}'."); } result.Scenario = remainder[0]; return result; } } public static void ShowHelp() { Console.WriteLine("ReverseProxy SampleClient.\n"); Console.WriteLine("--scenario , -s : Runs only the specified scenario."); Console.WriteLine( "--target , -t : Sets the target uri. By default, 'https://localhost:1443/' is used."); Console.WriteLine("--help, -h, -?, /?: Shows this help information."); } } ================================================ FILE: testassets/TestClient/Program.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Linq; using System.Threading; using SampleClient; using SampleClient.Scenarios; CommandLineArgs parsedArgs = null; try { parsedArgs = CommandLineArgs.Parse(args); } catch (ArgumentException) { // Do nothing, we will show help right after. } if (parsedArgs is null || parsedArgs.Help) { CommandLineArgs.ShowHelp(); return 1; } var scenarioFactories = new Dictionary>(StringComparer.OrdinalIgnoreCase) { {"Http1", () => new Http1Scenario()}, {"Http2", () => new Http2Scenario()}, {"Http2PostExpectContinue", () => new Http2PostExpectContinueScenario()}, {"RawUpgrade", () => new RawUpgradeScenario()}, {"WebSockets", () => new WebSocketsScenario()}, {"SessionAffinity", () => new SessionAffinityScenario()} }; if (string.IsNullOrEmpty(parsedArgs.Scenario)) { // Execute all scenarios var success = true; foreach (var kvp in scenarioFactories.OrderBy(kvp => kvp.Key)) { Console.WriteLine(); Console.WriteLine($"Executing scenario '{kvp.Key}'..."); try { var scenario = kvp.Value(); await scenario.ExecuteAsync(parsedArgs, CancellationToken.None); } catch (Exception ex) { Console.WriteLine($"Unexpected exception: {ex}"); success = false; } } Console.WriteLine(); Console.ForegroundColor = success ? ConsoleColor.Green : ConsoleColor.Red; Console.WriteLine($"All scenarios completed {(success ? "successfully" : "with errors")}."); Console.ResetColor(); Console.WriteLine("Press any key to exit."); Console.ReadKey(); return success ? 0 : 1; } if (!scenarioFactories.TryGetValue(parsedArgs.Scenario, out var scenarioFactory)) { Console.WriteLine($"Unknown scenario '{parsedArgs.Scenario}'. Supported values: "); foreach (var scenarioName in scenarioFactories.Keys.OrderBy(k => k)) { Console.WriteLine($" {scenarioName}"); } Console.WriteLine(); return 1; } Console.WriteLine($"Executing scenario '{parsedArgs.Scenario}'."); try { var scenario = scenarioFactory(); await scenario.ExecuteAsync(parsedArgs, CancellationToken.None); } catch (Exception ex) { Console.WriteLine($"Unexpected exception: {ex}"); return 1; } Console.ForegroundColor = ConsoleColor.Green; Console.WriteLine("All scenarios completed successfully!"); Console.ResetColor(); Console.WriteLine("Press any key to exit."); Console.ReadKey(); return 0; ================================================ FILE: testassets/TestClient/Properties/launchSettings.json ================================================ { "profiles": { "TestClient": { "commandName": "Project", "commandLineArgs": "-t https://localhost:5001" } } } ================================================ FILE: testassets/TestClient/Scenarios/Http1Scenario.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Diagnostics; using System.Net; using System.Net.Http; using System.Text.Json; using System.Threading; using System.Threading.Tasks; namespace SampleClient.Scenarios; internal sealed class Http1Scenario : IScenario { public async Task ExecuteAsync(CommandLineArgs args, CancellationToken cancellation) { using var handler = new HttpClientHandler { AllowAutoRedirect = false, AutomaticDecompression = DecompressionMethods.None, UseCookies = false, UseProxy = false }; using var client = new HttpMessageInvoker(handler); var targetUri = new Uri(new Uri(args.Target, UriKind.Absolute), "api/dump"); var stopwatch = Stopwatch.StartNew(); var request = new HttpRequestMessage(HttpMethod.Get, targetUri) { Version = new Version(1, 1) }; Console.WriteLine($"Calling {targetUri} with HTTP/1.1"); var response = await client.SendAsync(request, cancellation); Console.WriteLine($"Received response: {(int)response.StatusCode} in {stopwatch.ElapsedMilliseconds} ms"); response.EnsureSuccessStatusCode(); var body = await response.Content.ReadAsStringAsync(cancellation); var json = JsonDocument.Parse(body); Console.WriteLine( "Received response:" + $"{Environment.NewLine}" + $"{JsonSerializer.Serialize(json.RootElement, new JsonSerializerOptions { WriteIndented = true })}"); response.EnsureSuccessStatusCode(); } } ================================================ FILE: testassets/TestClient/Scenarios/Http2PostExpectContinueScenario.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Diagnostics; using System.Net; using System.Net.Http; using System.Threading; using System.Threading.Tasks; namespace SampleClient.Scenarios; /// /// Verifies that YARP correctly handles the case where the client specifies /// Expect: 100-continue and the destination fails early without accepting the request body. /// This scenario can be encountered in real world scenarios, usually when authentication fails on the destination. /// The Expect: 100-continue behavior causes the request body copy to not even start on YARP in this case. /// internal sealed class Http2PostExpectContinueScenario : IScenario { public async Task ExecuteAsync(CommandLineArgs args, CancellationToken cancellation) { using (var handler = new HttpClientHandler { AllowAutoRedirect = false, AutomaticDecompression = DecompressionMethods.None, UseCookies = false, UseProxy = false, }) using (var client = new HttpMessageInvoker(handler)) { var targetUri = new Uri(new Uri(args.Target, UriKind.Absolute), "api/skipbody"); var stopwatch = Stopwatch.StartNew(); var request = new HttpRequestMessage(HttpMethod.Post, targetUri); request.Version = new Version(2, 0); request.Headers.ExpectContinue = true; request.Content = new StringContent(new string('a', 1024 * 1024 * 10)); Console.WriteLine($"Calling {targetUri} with HTTP/2"); var response = await client.SendAsync(request, cancellation); Console.WriteLine($"Received response: {(int)response.StatusCode} in {stopwatch.ElapsedMilliseconds} ms"); if (response.StatusCode != HttpStatusCode.Conflict) { throw new InvalidOperationException($"Expected status 409 Conflict!"); } } } } ================================================ FILE: testassets/TestClient/Scenarios/Http2Scenario.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Diagnostics; using System.Net; using System.Net.Http; using System.Text.Json; using System.Threading; using System.Threading.Tasks; namespace SampleClient.Scenarios; internal sealed class Http2Scenario : IScenario { public async Task ExecuteAsync(CommandLineArgs args, CancellationToken cancellation) { using var handler = new HttpClientHandler { AllowAutoRedirect = false, AutomaticDecompression = DecompressionMethods.None, UseCookies = false, UseProxy = false }; using var client = new HttpMessageInvoker(handler); var targetUri = new Uri(new Uri(args.Target, UriKind.Absolute), "api/dump"); var stopwatch = Stopwatch.StartNew(); var request = new HttpRequestMessage(HttpMethod.Get, targetUri) { Version = new Version(2, 0) }; Console.WriteLine($"Calling {targetUri} with HTTP/2"); var response = await client.SendAsync(request, cancellation); Console.WriteLine($"Received response: {(int)response.StatusCode} in {stopwatch.ElapsedMilliseconds} ms"); response.EnsureSuccessStatusCode(); var body = await response.Content.ReadAsStringAsync(cancellation); var json = JsonDocument.Parse(body); Console.WriteLine( $"Received response:" + $"{Environment.NewLine}" + $"{JsonSerializer.Serialize(json.RootElement, new JsonSerializerOptions { WriteIndented = true })}"); response.EnsureSuccessStatusCode(); } } ================================================ FILE: testassets/TestClient/Scenarios/IScenario.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Threading; using System.Threading.Tasks; namespace SampleClient.Scenarios; /// /// Interface for the implementation of a scenario that can be executed asynchronously. /// internal interface IScenario { /// /// Executes the scenario asynchronously. /// Task ExecuteAsync(CommandLineArgs args, CancellationToken cancellation); } ================================================ FILE: testassets/TestClient/Scenarios/RawUpgradeScenario.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Diagnostics; using System.Net; using System.Net.Http; using System.Threading; using System.Threading.Tasks; namespace SampleClient.Scenarios; internal sealed class RawUpgradeScenario : IScenario { public async Task ExecuteAsync(CommandLineArgs args, CancellationToken cancellation) { using var handler = new HttpClientHandler { AllowAutoRedirect = false, AutomaticDecompression = DecompressionMethods.None, UseCookies = false, UseProxy = false }; using var client = new HttpMessageInvoker(handler); var targetUri = new Uri(new Uri(args.Target, UriKind.Absolute), "api/rawupgrade"); var stopwatch = Stopwatch.StartNew(); var request = new HttpRequestMessage(HttpMethod.Get, targetUri); request.Headers.TryAddWithoutValidation("Connection", "upgrade"); request.Version = new Version(1, 1); Console.WriteLine($"Calling {targetUri} with upgradable HTTP/1.1"); var response = await client.SendAsync(request, cancellation); Console.WriteLine($"Received response: {(int)response.StatusCode} in {stopwatch.ElapsedMilliseconds} ms"); if (response.StatusCode != HttpStatusCode.SwitchingProtocols) { throw new InvalidOperationException("Expected status 101 Switching Protocols!"); } var rawStream = await response.Content.ReadAsStreamAsync(cancellation); Console.WriteLine("Acquired upgraded stream. Testing bidirectional echo..."); stopwatch.Restart(); var buffer = new byte[1]; for (var i = 0; i <= 255; i++) { buffer[0] = (byte)i; await rawStream.WriteAsync(buffer, cancellation); var read = await rawStream.ReadAsync(buffer, cancellation); if (i == 255) { if (read != 0) { throw new Exception($"Read {read} bytes, expected 0 after sending Goodbye."); } Console.WriteLine(); } else { if (read != 1) { throw new Exception($"Read {read} bytes, expected 1."); } if (buffer[0] != i) { throw new Exception($"Received {buffer[0]}, expected {i}."); } Console.Write("."); } } Console.WriteLine($"256 ping/pong's completed in {stopwatch.ElapsedMilliseconds} ms."); } } ================================================ FILE: testassets/TestClient/Scenarios/SessionAffinityScenario.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Diagnostics; using System.Linq; using System.Net; using System.Net.Http; using System.Text.Json; using System.Threading; using System.Threading.Tasks; namespace SampleClient.Scenarios; internal sealed class SessionAffinityScenario : IScenario { public async Task ExecuteAsync(CommandLineArgs args, CancellationToken cancellation) { using var handler = new HttpClientHandler { AllowAutoRedirect = false, AutomaticDecompression = DecompressionMethods.None, // Session affinity key will be stored in a cookie UseCookies = true, UseProxy = false }; using var client = new HttpMessageInvoker(handler); var targetUri = new Uri(new Uri(args.Target, UriKind.Absolute), "api/dump"); var stopwatch = Stopwatch.StartNew(); var request0 = new HttpRequestMessage(HttpMethod.Get, targetUri) { Version = new Version(1, 1) }; Console.WriteLine($"Sending first request to {targetUri} with HTTP/1.1"); var response0 = await client.SendAsync(request0, cancellation); PrintDuration(stopwatch, response0); PrintAffinityCookie(handler, targetUri, response0); await ReadAndPrintBody(response0, cancellation); stopwatch.Reset(); var request1 = new HttpRequestMessage(HttpMethod.Get, targetUri) { Version = new Version(1, 1) }; Console.WriteLine($"Sending second request to {targetUri} with HTTP/1.1"); var response1 = await client.SendAsync(request1, cancellation); PrintDuration(stopwatch, response1); PrintAffinityCookie(handler, targetUri, response1); await ReadAndPrintBody(response1, cancellation); } private static void PrintDuration(Stopwatch stopwatch, HttpResponseMessage response) { Console.WriteLine($"Received response: {(int)response.StatusCode} in {stopwatch.ElapsedMilliseconds} ms"); response.EnsureSuccessStatusCode(); } private static void PrintAffinityCookie(HttpClientHandler handler, Uri targetUri, HttpResponseMessage response) { if (response.Headers.TryGetValues("Set-Cookie", out var setCookieValue)) { Console.WriteLine($"Received header Set-Cookie: {setCookieValue.ToArray()[0]}"); } else { Console.WriteLine($"Response doesn't have Set-Cookie header."); } var affinityCookie = handler.CookieContainer.GetCookies(targetUri)[".Yarp.Affinity"]; Console.WriteLine($"Affinity key stored on a cookie {affinityCookie.Value}"); } private static async Task ReadAndPrintBody(HttpResponseMessage response, CancellationToken cancellation) { var body = await response.Content.ReadAsStringAsync(cancellation); var json = JsonDocument.Parse(body); Console.WriteLine( "Received response:" + $"{Environment.NewLine}" + $"{JsonSerializer.Serialize(json.RootElement, new JsonSerializerOptions { WriteIndented = true })}"); response.EnsureSuccessStatusCode(); } } ================================================ FILE: testassets/TestClient/Scenarios/WebSocketsScenario.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Diagnostics; using System.Net.WebSockets; using System.Text; using System.Threading; using System.Threading.Tasks; namespace SampleClient.Scenarios; internal sealed class WebSocketsScenario : IScenario { public async Task ExecuteAsync(CommandLineArgs args, CancellationToken cancellation) { var client = new ClientWebSocket(); client.Options.AddSubProtocol("chat"); var webSocketsTarget = args.Target.Replace("https://", "wss://").Replace("http://", "ws://"); var targetUri = new Uri(new Uri(webSocketsTarget, UriKind.Absolute), "api/websockets"); Console.WriteLine($"Establishing WebSockets channel with {targetUri}..."); var stopwatch = Stopwatch.StartNew(); await client.ConnectAsync(targetUri, cancellation); Console.WriteLine($"Channel established in {stopwatch.ElapsedMilliseconds} ms."); Console.WriteLine("Sending text messages..."); var buffer = new byte[1024]; stopwatch.Restart(); for (var i = 0; i < 256; i++) { var textToSend = $"Hello {i}"; var numBytes = Encoding.UTF8.GetBytes(textToSend, buffer.AsSpan()); await client.SendAsync(new ArraySegment(buffer, 0, numBytes), WebSocketMessageType.Text, endOfMessage: true, cancellation); var message = await client.ReceiveAsync(buffer, cancellation); if (message.MessageType != WebSocketMessageType.Text) { throw new Exception($"Expected to receive a text message, got '{message.MessageType}' instead."); } if (!message.EndOfMessage) { throw new Exception("Expected to receive EndOfMessage = true."); } var text = Encoding.UTF8.GetString(buffer.AsSpan(0, message.Count)); if (text != textToSend) { throw new Exception($"Expected to receive '{textToSend}', but got '{text}'."); } Console.Write("."); } Console.WriteLine(); Console.WriteLine($"Completed 256 text messages in {stopwatch.ElapsedMilliseconds} ms."); Console.WriteLine("Sending binary messages..."); stopwatch.Restart(); for (var i = 0; i < 256; i++) { var textToSend = $"Hello {i}"; var numBytes = Encoding.UTF8.GetBytes(textToSend, buffer.AsSpan()); await client.SendAsync(new ArraySegment(buffer, 0, numBytes), WebSocketMessageType.Binary, endOfMessage: true, cancellation); var message = await client.ReceiveAsync(buffer, cancellation); if (message.MessageType != WebSocketMessageType.Binary) { throw new Exception($"Expected to receive a text message, got '{message.MessageType}' instead."); } if (!message.EndOfMessage) { throw new Exception("Expected to receive EndOfMessage = true."); } var text = Encoding.UTF8.GetString(buffer.AsSpan(0, message.Count)); if (text != textToSend) { throw new Exception($"Expected to receive '{textToSend}', but got '{text}'."); } Console.Write("."); } Console.WriteLine(); Console.WriteLine($"Completed 256 binary messages in {stopwatch.ElapsedMilliseconds} ms."); await client.CloseAsync(WebSocketCloseStatus.NormalClosure, "Bye", cancellation); } } ================================================ FILE: testassets/TestClient/TestClient.csproj ================================================ $(LatestDevTFM) Exe ================================================ FILE: testassets/TestServer/AssemblyInfo.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. ================================================ FILE: testassets/TestServer/Controllers/HealthController.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Mvc; namespace Yarp.ReverseProxy.Sample.Controllers; /// /// Controller for active health check probes. /// [ApiController] public class HealthController : ControllerBase { private static volatile int _count; /// /// Returns 200 if server is healthy. /// [HttpGet] [Route("/api/health")] public IActionResult CheckHealth() { _count++; // Simulate temporary health degradation. return _count % 10 < 4 ? Ok() : StatusCode(500); } } ================================================ FILE: testassets/TestServer/Controllers/HttpController.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; 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 409 response without consuming the request body. /// This is used to exercise Expect:100-continue behavior. /// [HttpPost] [Route("/api/skipbody")] public IActionResult SkipBody() { return StatusCode(StatusCodes.Status409Conflict); } /// /// Returns a 409 response without consuming the request body. /// This is used to exercise Expect:100-continue behavior. /// [HttpGet] [Route("/api/slow")] public async Task Slow() { await Task.Delay(TimeSpan.FromSeconds(3)); return StatusCode(StatusCodes.Status200OK); } /// /// 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.Append(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 is not 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 is not 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; } } } } ================================================ FILE: testassets/TestServer/Controllers/UpgradeController.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.IO; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; namespace SampleServer.Controllers; /// /// Sample controller. /// [ApiController] public class UpgradeController : ControllerBase { private readonly ILogger _logger; /// /// Initializes a new instance of the class. /// public UpgradeController(ILogger logger) { _logger = logger; } /// /// Upgrades the connection to a raw socket stream, then implements a simple byte ping/pong server. /// Note that this does not use WebSockets, and relies solely on HTTP/1.1 connection upgrade mechanism. /// [HttpGet] [Route("/api/rawupgrade")] public async Task RawUpgrade() { var upgradeFeature = HttpContext.Features.Get(); if (upgradeFeature is null || !upgradeFeature.IsUpgradableRequest) { HttpContext.Response.StatusCode = StatusCodes.Status426UpgradeRequired; return; } await using var stream = await upgradeFeature.UpgradeAsync(); _logger.LogInformation("Upgraded connection."); await RunPingPongAsync(stream); _logger.LogInformation("Finished."); } /// /// Simple echo protocol that echo's each received byte. /// 255 is treated as a special "goodbye" message, which causes us to drop the connection. /// private async Task RunPingPongAsync(Stream stream) { var buffer = new byte[1]; while (await stream.ReadAsync(buffer, HttpContext.RequestAborted) != 0) { if (buffer[0] == 255) { // Goodbye break; } await stream.WriteAsync(buffer); } } } ================================================ FILE: testassets/TestServer/Controllers/WebSocketsController.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you 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); } } } ================================================ FILE: testassets/TestServer/Program.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers() .AddJsonOptions(options => options.JsonSerializerOptions.WriteIndented = true); var app = builder.Build(); app.UseWebSockets(); app.MapControllers(); app.Run(); ================================================ FILE: testassets/TestServer/Properties/launchSettings.json ================================================ { "profiles": { "TestServer": { "commandName": "Project", "launchBrowser": false, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, "applicationUrl": "https://localhost:10000;https://localhost:10001;http://localhost:10010;http://localhost:10011" } } } ================================================ FILE: testassets/TestServer/TestServer.csproj ================================================ $(LatestDevTFM) Exe SampleServer ================================================ FILE: testassets/TestServer/appsettings.Development.json ================================================ { "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Information", "Microsoft.Hosting.Lifetime": "Information" } } } ================================================ FILE: testassets/TestServer/appsettings.json ================================================ { "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } }, "AllowedHosts": "*" }