From a2893063ab98a088810c96457a1d61a4a35829e3 Mon Sep 17 00:00:00 2001 From: rmorris Date: Fri, 5 Mar 2021 20:16:42 +0000 Subject: [PATCH] Revert back to page-relative URLs for servers, redirects, config etc. --- README.md | 63 +++++-------------- src/Directory.Build.props | 2 +- .../ReDocBuilderExtensions.cs | 2 +- .../ReDocMiddleware.cs | 9 ++- .../SwaggerMiddleware.cs | 16 +++-- .../SwaggerOptions.cs | 7 +-- .../SwaggerUIBuilderExtensions.cs | 2 +- .../SwaggerUIMiddleware.cs | 8 ++- .../ReDocIntegrationTests.cs | 2 +- .../SwaggerIntegrationTests.cs | 21 ------- .../SwaggerUIIntegrationTests.cs | 2 +- test/WebSites/Basic/Startup.cs | 1 + 12 files changed, 43 insertions(+), 92 deletions(-) diff --git a/README.md b/README.md index eea3067ef9..5f7ddf0d92 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Once you have an API that can describe itself in Swagger, you've opened the trea |Swashbuckle Version|ASP.NET Core|Swagger / OpenAPI Spec.|swagger-ui|ReDoc UI| |----------|----------|----------|----------|----------| |[master](https://github.com/domaindrivendev/Swashbuckle.AspNetCore/tree/master/README.md)|>= 2.0.0|2.0, 3.0|3.42.0|2.0.0-rc.40| -|[6.0.7](https://github.com/domaindrivendev/Swashbuckle.AspNetCore/tree/v6.0.7)|>= 2.0.0|2.0, 3.0|3.42.0|2.0.0-rc.40| +|[6.1.0](https://github.com/domaindrivendev/Swashbuckle.AspNetCore/tree/v6.1.0)|>= 2.0.0|2.0, 3.0|3.42.0|2.0.0-rc.40| |[5.6.3](https://github.com/domaindrivendev/Swashbuckle.AspNetCore/tree/v5.6.3)|>= 2.0.0|2.0, 3.0|3.32.5|2.0.0-rc.40| |[4.0.0](https://github.com/domaindrivendev/Swashbuckle.AspNetCore/tree/v4.0.0)|>= 2.0.0, < 3.0.0|2.0|3.19.5|1.22.2| |[3.0.0](https://github.com/domaindrivendev/Swashbuckle.AspNetCore/tree/v3.0.0)|>= 1.0.4, < 3.0.0|2.0|3.17.1|1.20.0| @@ -33,8 +33,8 @@ Once you have an API that can describe itself in Swagger, you've opened the trea 1. Install the standard Nuget package into your ASP.NET Core application. ``` - Package Manager : Install-Package Swashbuckle.AspNetCore -Version 6.0.7 - CLI : dotnet add package --version 6.0.7 Swashbuckle.AspNetCore + Package Manager : Install-Package Swashbuckle.AspNetCore -Version 6.1.0 + CLI : dotnet add package --version 6.1.0 Swashbuckle.AspNetCore ``` 2. In the `ConfigureServices` method of `Startup.cs`, register the Swagger generator, defining one or more Swagger documents. @@ -81,7 +81,7 @@ Once you have an API that can describe itself in Swagger, you've opened the trea ```csharp app.UseSwaggerUI(c => { - c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); + c.SwaggerEndpoint("v1/swagger.json", "My API V1"); }); ``` @@ -98,8 +98,8 @@ If you're using **System.Text.Json (STJ)**, then the setup described above will If you're using **Newtonsoft**, then you'll need to install a separate package and explicitly opt-in to ensure that *Newtonsoft* settings/attributes are automatically honored by the Swagger generator: ``` -Package Manager : Install-Package Swashbuckle.AspNetCore.Newtonsoft -Version 6.0.7 -CLI : dotnet add package --version 6.0.7 Swashbuckle.AspNetCore.Newtonsoft +Package Manager : Install-Package Swashbuckle.AspNetCore.Newtonsoft -Version 6.1.0 +CLI : dotnet add package --version 6.1.0 Swashbuckle.AspNetCore.Newtonsoft ``` ```csharp @@ -185,7 +185,7 @@ The steps described above will get you up and running with minimal setup. Howeve * [Change the Path for Swagger JSON Endpoints](#change-the-path-for-swagger-json-endpoints) * [Modify Swagger with Request Context](#modify-swagger-with-request-context) * [Serialize Swagger JSON in the 2.0 format](#serialize-swagger-in-the-20-format) - * [Working with Reverse Proxies and Load Balancers](#working-with-reverse-proxies-and-load-balancers) + * [Working with Virtual Directories and Reverse Proxies](#working-with-virtual-directories-and-reverse-proxies) * [Swashbuckle.AspNetCore.SwaggerGen](#swashbuckleaspnetcoreswaggergen) @@ -288,54 +288,21 @@ app.UseSwagger(c => }); ``` -### Working with Reverse Proxies and Load Balancers ### +### Working with Virtual Directories and Reverse Proxies ### -To ensure applications work correctly behind proxies and load balancers, Microsoft provides the [Forwarded Headers Middleware](https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/proxy-load-balancer?view=aspnetcore-5.0). This is particularly important for applications that generate links and redirects because information in the request like the original scheme (HTTP/HTTPS) and host is obscured before it reaches the app. By convention, most proxies forward this information in `X-Forwarded-*` headers, and so the Forwarded Headers Middleware can be configured to read these values and fill in the associated fields on `HttpContext`. +Virtual directories and reverse proxies can cause issues for applications that generate links and redirects, particularly if the app returns *absolute* URLs based on the `Host` header and other information from the current request. To avoid these issues, Swasbuckle uses *relative* URLs where possible, and encourages their use when configuring the SwaggerUI and ReDoc middleware. -The `Swagger` and `SwaggerUI` middleware generate links and redirects, and so to ensure they work correctly behind reverse proxies and load balancers, you'll need to insert the Forwarded Headers Middleware: +For example, to wire up the SwaggerUI middleware, you provide the URL to one or more OpenAPI/Swagger documents. This is the URL that the swagger-ui, a client-side application, will call to retrieve your API metadata. To ensure this works behind virtual directories and reverse proxies, you should express this relative to the `RoutePrefix` of the swagger-ui itself: ```csharp -app.UseForwardedHeaders(new ForwardedHeadersOptions -{ - ForwardedHeaders = ForwardedHeaders.All -}); - -app.UseSwagger(); - app.UseSwaggerUI(c => { - c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); + c.RoutePrefix = "swagger"; + c.SwaggerEndpoint("v1/swagger.json", "My API V1"); }); ``` -_NOTE: Depending on your proxy setup, you may need to tweak the Forwarded Headers Middleware (e.g. if the proxy uses non-standard header names). You can refer to the [Microsoft docs](https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/proxy-load-balancer?view=aspnetcore-5.0) for further guidance on this._ - -#### Dealing with Proxies that change the request path #### - -Some proxies trim the request path before forwarding. For example, an original path of `/foo/api/products` may be forwarded to the app as `/api/products`. Handling this case is a little more involved. Firstly, you'll need to insert custom middleware to set the `PathBase` to the appropriate path prefix (if it's known) OR to read it from a header if provided by the proxy: - -```csharp -app.Use((context, next) => -{ - if (context.Request.Headers.TryGetValue("X-Forwarded-Prefix", out var value)) - context.Request.PathBase = value.First(); - - return next(); -}); - -app.UseSwagger(); -``` - -Additionally, you'll need to adjust the `SwaggerUI` configuration to use a _page-relative_ syntax (i.e. no leading `/`) for the Swagger JSON endpoint instead of a root-relative syntax. - -```csharp -app.UseSwaggerUI(c => -{ - c.SwaggerEndpoint("v1/swagger.json", "V1 Docs"); -}) -``` - -This way, the swagger-ui will look for the Swagger JSON at a URL that's _relative_ to itself (e.g. `/swagger/v1/swagger.json`), and will therefore be agnostic of the host and path prefix (if any) that preceeds it. +_NOTE: In previous versions of the docs, you may have seen this expressed as a root-relative link (e.g. `/swagger/v1/swagger.json`). This won't work if your app is hosted on an IIS virtual directory or behind a proxy that trims the request path before forwarding. If you switch to the *page-relative* syntax shown above, it should work in all cases._ ## Swashbuckle.AspNetCore.SwaggerGen ## @@ -1528,7 +1495,7 @@ It's packaged as a [.NET Core Tool](https://docs.microsoft.com/en-us/dotnet/core 1. Install as a [global tool](https://docs.microsoft.com/en-us/dotnet/core/tools/global-tools#install-a-global-tool) ``` - dotnet tool install -g --version 6.0.7 Swashbuckle.AspNetCore.Cli + dotnet tool install -g --version 6.1.0 Swashbuckle.AspNetCore.Cli ``` 2. Verify that the tool was installed correctly @@ -1559,7 +1526,7 @@ It's packaged as a [.NET Core Tool](https://docs.microsoft.com/en-us/dotnet/core 2. Install as a [local tool](https://docs.microsoft.com/en-us/dotnet/core/tools/global-tools#install-a-local-tool) ``` - dotnet tool install --version 6.0.7 Swashbuckle.AspNetCore.Cli + dotnet tool install --version 6.1.0 Swashbuckle.AspNetCore.Cli ``` 3. Verify that the tool was installed correctly diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 4c804f0286..6d5e136d1d 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -6,7 +6,7 @@ true MIT https://licenses.nuget.org/MIT - 6.0.7 + 6.1.0 diff --git a/src/Swashbuckle.AspNetCore.ReDoc/ReDocBuilderExtensions.cs b/src/Swashbuckle.AspNetCore.ReDoc/ReDocBuilderExtensions.cs index 88dee10f0e..9bd4ab17b2 100644 --- a/src/Swashbuckle.AspNetCore.ReDoc/ReDocBuilderExtensions.cs +++ b/src/Swashbuckle.AspNetCore.ReDoc/ReDocBuilderExtensions.cs @@ -32,7 +32,7 @@ public static IApplicationBuilder UseReDoc( // To simplify the common case, use a default that will work with the SwaggerMiddleware defaults if (options.SpecUrl == null) { - options.SpecUrl = "/swagger/v1/swagger.json"; + options.SpecUrl = "../swagger/v1/swagger.json"; } return app.UseReDoc(options); diff --git a/src/Swashbuckle.AspNetCore.ReDoc/ReDocMiddleware.cs b/src/Swashbuckle.AspNetCore.ReDoc/ReDocMiddleware.cs index 85eb3a674d..0e41d7ac37 100644 --- a/src/Swashbuckle.AspNetCore.ReDoc/ReDocMiddleware.cs +++ b/src/Swashbuckle.AspNetCore.ReDoc/ReDocMiddleware.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.IO; +using System.Linq; using System.Reflection; using System.Text; using System.Text.Json; @@ -9,7 +10,6 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.StaticFiles; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Logging; @@ -53,9 +53,12 @@ public async Task Invoke(HttpContext httpContext) // If the RoutePrefix is requested (with or without trailing slash), redirect to index URL if (httpMethod == "GET" && Regex.IsMatch(path, $"^/?{Regex.Escape(_options.RoutePrefix)}/?$", RegexOptions.IgnoreCase)) { - var indexUrl = httpContext.Request.GetEncodedUrl().TrimEnd('/') + "/index.html"; + // Use relative redirect to support proxy environments + var relativeIndexUrl = string.IsNullOrEmpty(path) || path.EndsWith("/") + ? "index.html" + : $"{path.Split('/').Last()}/index.html"; - RespondWithRedirect(httpContext.Response, indexUrl); + RespondWithRedirect(httpContext.Response, relativeIndexUrl); return; } diff --git a/src/Swashbuckle.AspNetCore.Swagger/SwaggerMiddleware.cs b/src/Swashbuckle.AspNetCore.Swagger/SwaggerMiddleware.cs index da2ccd1f65..e44a202aa8 100644 --- a/src/Swashbuckle.AspNetCore.Swagger/SwaggerMiddleware.cs +++ b/src/Swashbuckle.AspNetCore.Swagger/SwaggerMiddleware.cs @@ -1,13 +1,10 @@ -using System; -using System.Globalization; +using System.Globalization; using System.IO; -using System.Resources; using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Template; -using Microsoft.Extensions.Primitives; using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Writers; @@ -38,13 +35,14 @@ public async Task Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvid try { - var host = httpContext.Request.Host.HasValue - ? $"{httpContext.Request.Scheme ?? "http"}://{httpContext.Request.Host}" + var basePath = httpContext.Request.PathBase.HasValue + ? httpContext.Request.PathBase.Value : null; - var basePath = httpContext.Request.PathBase; - - var swagger = swaggerProvider.GetSwagger(documentName, host, basePath); + var swagger = swaggerProvider.GetSwagger( + documentName: documentName, + host: null, + basePath: basePath); // One last opportunity to modify the Swagger Document - this time with request context foreach (var filter in _options.PreSerializeFilters) diff --git a/src/Swashbuckle.AspNetCore.Swagger/SwaggerOptions.cs b/src/Swashbuckle.AspNetCore.Swagger/SwaggerOptions.cs index 7c67aef443..582e85d7c3 100644 --- a/src/Swashbuckle.AspNetCore.Swagger/SwaggerOptions.cs +++ b/src/Swashbuckle.AspNetCore.Swagger/SwaggerOptions.cs @@ -14,18 +14,17 @@ public SwaggerOptions() } /// - /// Sets a custom route for the Swagger JSON endpoint(s). Must include the {documentName} parameter + /// Sets a custom route for the Swagger JSON/YAML endpoint(s). Must include the {documentName} parameter /// public string RouteTemplate { get; set; } = "swagger/{documentName}/swagger.{json|yaml}"; - /// - /// Return Swagger JSON in the V2 format rather than V3 + /// Return Swagger JSON/YAML in the V2 format rather than V3 /// public bool SerializeAsV2 { get; set; } /// - /// Actions that can be applied SwaggerDocument's before they're serialized to JSON. + /// Actions that can be applied to an OpenApiDocument before it's serialized. /// Useful for setting metadata that's derived from the current request /// public List> PreSerializeFilters { get; private set; } diff --git a/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIBuilderExtensions.cs b/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIBuilderExtensions.cs index 88cdc1d7c7..4121633572 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIBuilderExtensions.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIBuilderExtensions.cs @@ -32,7 +32,7 @@ public static IApplicationBuilder UseSwaggerUI( // To simplify the common case, use a default that will work with the SwaggerMiddleware defaults if (options.ConfigObject.Urls == null) { - options.ConfigObject.Urls = new[] { new UrlDescriptor { Name = "V1 Docs", Url = "/swagger/v1/swagger.json" } }; + options.ConfigObject.Urls = new[] { new UrlDescriptor { Name = "V1 Docs", Url = "v1/swagger.json" } }; } return app.UseSwaggerUI(options); diff --git a/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIMiddleware.cs b/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIMiddleware.cs index a853662d9f..aaef90c704 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIMiddleware.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIMiddleware.cs @@ -14,6 +14,7 @@ using Microsoft.Extensions.FileProviders; using Microsoft.AspNetCore.StaticFiles; using Microsoft.AspNetCore.Http.Extensions; +using System.Linq; #if NETSTANDARD2_0 using IWebHostEnvironment = Microsoft.AspNetCore.Hosting.IHostingEnvironment; @@ -53,9 +54,12 @@ public async Task Invoke(HttpContext httpContext) // If the RoutePrefix is requested (with or without trailing slash), redirect to index URL if (httpMethod == "GET" && Regex.IsMatch(path, $"^/?{Regex.Escape(_options.RoutePrefix)}/?$", RegexOptions.IgnoreCase)) { - var indexUrl = httpContext.Request.GetEncodedUrl().TrimEnd('/') + "/index.html"; + // Use relative redirect to support proxy environments + var relativeIndexUrl = string.IsNullOrEmpty(path) || path.EndsWith("/") + ? "index.html" + : $"{path.Split('/').Last()}/index.html"; - RespondWithRedirect(httpContext.Response, indexUrl); + RespondWithRedirect(httpContext.Response, relativeIndexUrl); return; } diff --git a/test/Swashbuckle.AspNetCore.IntegrationTests/ReDocIntegrationTests.cs b/test/Swashbuckle.AspNetCore.IntegrationTests/ReDocIntegrationTests.cs index b665ad0451..24f0432a01 100644 --- a/test/Swashbuckle.AspNetCore.IntegrationTests/ReDocIntegrationTests.cs +++ b/test/Swashbuckle.AspNetCore.IntegrationTests/ReDocIntegrationTests.cs @@ -15,7 +15,7 @@ public async Task RoutePrefix_RedirectsToIndexUrl() var response = await client.GetAsync("/api-docs"); Assert.Equal(HttpStatusCode.MovedPermanently, response.StatusCode); - Assert.Equal("http://localhost/api-docs/index.html", response.Headers.Location.ToString()); + Assert.Equal("api-docs/index.html", response.Headers.Location.ToString()); } [Fact] diff --git a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerIntegrationTests.cs b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerIntegrationTests.cs index 5e85d21669..ec55d29fa0 100644 --- a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerIntegrationTests.cs +++ b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerIntegrationTests.cs @@ -94,27 +94,6 @@ public async Task SwaggerEndpoint_ReturnsCorrectPriceExample_ForDifferentCulture } } - [Theory] - [InlineData("http://tempuri.org", "http://tempuri.org")] - [InlineData("https://tempuri.org", "https://tempuri.org")] - [InlineData("http://tempuri.org:8080", "http://tempuri.org:8080")] - public async Task SwaggerEndpoint_InfersServerMetadata_FromRequestHeaders( - string clientBaseAddress, - string expectedServerUrl) - { - var client = new TestSite(typeof(Basic.Startup)).BuildClient(); - client.BaseAddress = new Uri(clientBaseAddress); - - var swaggerResponse = await client.GetAsync($"swagger/v1/swagger.json"); - - swaggerResponse.EnsureSuccessStatusCode(); - var contentStream = await swaggerResponse.Content.ReadAsStreamAsync(); - var openApiDoc = new OpenApiStreamReader().Read(contentStream, out _); - Assert.NotNull(openApiDoc.Servers); - Assert.Equal(1, openApiDoc.Servers.Count); - Assert.Equal(expectedServerUrl, openApiDoc.Servers[0].Url); - } - [Theory] [InlineData("/swagger/v1/swagger.json", "openapi", "3.0.1")] [InlineData("/swagger/v1/swaggerv2.json", "swagger", "2.0")] diff --git a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerUIIntegrationTests.cs b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerUIIntegrationTests.cs index 154a6cdc76..5e8209ae42 100644 --- a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerUIIntegrationTests.cs +++ b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerUIIntegrationTests.cs @@ -14,7 +14,7 @@ public async Task RoutePrefix_RedirectsToIndexUrl() var response = await client.GetAsync("/swagger"); Assert.Equal(HttpStatusCode.MovedPermanently, response.StatusCode); - Assert.Equal("http://localhost/swagger/index.html", response.Headers.Location.ToString()); + Assert.Equal("swagger/index.html", response.Headers.Location.ToString()); } [Fact] diff --git a/test/WebSites/Basic/Startup.cs b/test/WebSites/Basic/Startup.cs index 6150965d33..a4e97a9364 100644 --- a/test/WebSites/Basic/Startup.cs +++ b/test/WebSites/Basic/Startup.cs @@ -99,6 +99,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseSwaggerUI(c => { c.RoutePrefix = ""; // serve the UI at root + c.SwaggerEndpoint("/swagger/v1/swagger.json", "V1 Docs"); }); } }