Skip to content

Commit

Permalink
Merge pull request #2048 from domaindrivendev/revert-absolute-urls
Browse files Browse the repository at this point in the history
Revert back to page-relative URLs for servers, redirects, config etc.
  • Loading branch information
domaindrivendev authored Mar 6, 2021
2 parents bf57514 + a289306 commit 494f15f
Show file tree
Hide file tree
Showing 12 changed files with 43 additions and 92 deletions.
63 changes: 15 additions & 48 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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|
Expand All @@ -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.
Expand Down Expand Up @@ -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");
});
```
Expand All @@ -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
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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. `<some-url>/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 ##

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<LicenseUrl>https://licenses.nuget.org/MIT</LicenseUrl>
<VersionPrefix>6.0.7</VersionPrefix>
<VersionPrefix>6.1.0</VersionPrefix>
</PropertyGroup>

<ItemGroup>
Expand Down
2 changes: 1 addition & 1 deletion src/Swashbuckle.AspNetCore.ReDoc/ReDocBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
9 changes: 6 additions & 3 deletions src/Swashbuckle.AspNetCore.ReDoc/ReDocMiddleware.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.Json;
Expand All @@ -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;
Expand Down Expand Up @@ -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;
}

Expand Down
16 changes: 7 additions & 9 deletions src/Swashbuckle.AspNetCore.Swagger/SwaggerMiddleware.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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)
Expand Down
7 changes: 3 additions & 4 deletions src/Swashbuckle.AspNetCore.Swagger/SwaggerOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,17 @@ public SwaggerOptions()
}

/// <summary>
/// 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
/// </summary>
public string RouteTemplate { get; set; } = "swagger/{documentName}/swagger.{json|yaml}";


/// <summary>
/// Return Swagger JSON in the V2 format rather than V3
/// Return Swagger JSON/YAML in the V2 format rather than V3
/// </summary>
public bool SerializeAsV2 { get; set; }

/// <summary>
/// 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
/// </summary>
public List<Action<OpenApiDocument, HttpRequest>> PreSerializeFilters { get; private set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
8 changes: 6 additions & 2 deletions src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
1 change: 1 addition & 0 deletions test/WebSites/Basic/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
}
}
Expand Down

0 comments on commit 494f15f

Please sign in to comment.