Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Observe the route template constraints in the Swagger middleware #2418

Merged
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion Swashbuckle.AspNetCore.sln
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@


Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31717.71
Expand Down Expand Up @@ -91,6 +91,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Swashbuckle.AspNetCore.Test
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MinimalApp", "test\WebSites\MinimalApp\MinimalApp.csproj", "{3D0126CB-5439-483C-B2D5-4B4BE111D15C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TopLevelSwaggerDoc", "test\WebSites\TopLevelSwaggerDoc\TopLevelSwaggerDoc.csproj", "{6EA75DA8-9B1F-468E-9425-37F01A129B0F}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{3BA087DA-788C-43D6-9D8B-1EF017014A4A}"
ProjectSection(SolutionItems) = preProject
.github\actionlint-matcher.json = .github\actionlint-matcher.json
Expand Down Expand Up @@ -501,6 +503,18 @@ Global
{3D0126CB-5439-483C-B2D5-4B4BE111D15C}.Release|x64.Build.0 = Release|Any CPU
{3D0126CB-5439-483C-B2D5-4B4BE111D15C}.Release|x86.ActiveCfg = Release|Any CPU
{3D0126CB-5439-483C-B2D5-4B4BE111D15C}.Release|x86.Build.0 = Release|Any CPU
{6EA75DA8-9B1F-468E-9425-37F01A129B0F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6EA75DA8-9B1F-468E-9425-37F01A129B0F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6EA75DA8-9B1F-468E-9425-37F01A129B0F}.Debug|x64.ActiveCfg = Debug|Any CPU
{6EA75DA8-9B1F-468E-9425-37F01A129B0F}.Debug|x64.Build.0 = Debug|Any CPU
{6EA75DA8-9B1F-468E-9425-37F01A129B0F}.Debug|x86.ActiveCfg = Debug|Any CPU
{6EA75DA8-9B1F-468E-9425-37F01A129B0F}.Debug|x86.Build.0 = Debug|Any CPU
{6EA75DA8-9B1F-468E-9425-37F01A129B0F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6EA75DA8-9B1F-468E-9425-37F01A129B0F}.Release|Any CPU.Build.0 = Release|Any CPU
{6EA75DA8-9B1F-468E-9425-37F01A129B0F}.Release|x64.ActiveCfg = Release|Any CPU
{6EA75DA8-9B1F-468E-9425-37F01A129B0F}.Release|x64.Build.0 = Release|Any CPU
{6EA75DA8-9B1F-468E-9425-37F01A129B0F}.Release|x86.ActiveCfg = Release|Any CPU
{6EA75DA8-9B1F-468E-9425-37F01A129B0F}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -539,6 +553,7 @@ Global
{76692D68-C38C-4A7D-B3DA-DA78A393E266} = {0ADCB223-F375-45AB-8BC4-834EC9C69554}
{66590FBA-5FDD-4AC9-AF91-26ADAB33CCB8} = {0ADCB223-F375-45AB-8BC4-834EC9C69554}
{3D0126CB-5439-483C-B2D5-4B4BE111D15C} = {DB3F57FC-1472-4F03-B551-43394DA3C5EB}
{6EA75DA8-9B1F-468E-9425-37F01A129B0F} = {DB3F57FC-1472-4F03-B551-43394DA3C5EB}
{3BA087DA-788C-43D6-9D8B-1EF017014A4A} = {FA1B4021-0A97-4F68-B966-148191F6AAA8}
{A0EC16BE-C520-4FCF-BB54-2D79CD255F00} = {3BA087DA-788C-43D6-9D8B-1EF017014A4A}
EndGlobalSection
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
using System;
using System.Linq;
using Microsoft.AspNetCore.Routing;
using System;

#if (!NETSTANDARD2_0)
using System.Linq;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.AspNetCore.Routing.Template;
#endif

using Microsoft.Extensions.DependencyInjection;
Expand All @@ -19,7 +20,11 @@ public static class SwaggerBuilderExtensions
/// </summary>
public static IApplicationBuilder UseSwagger(this IApplicationBuilder app, SwaggerOptions options)
{
#if (!NETSTANDARD2_0)
return app.UseMiddleware<SwaggerMiddleware>(options, app.ApplicationServices.GetRequiredService<TemplateBinderFactory>());
#else
return app.UseMiddleware<SwaggerMiddleware>(options);
#endif
}

/// <summary>
Expand All @@ -42,7 +47,7 @@ public static IApplicationBuilder UseSwagger(
#if (!NETSTANDARD2_0)
public static IEndpointConventionBuilder MapSwagger(
this IEndpointRouteBuilder endpoints,
string pattern = "/swagger/{documentName}/swagger.{json|yaml}",
string pattern = "/swagger/{documentName}/swagger.{extension:regex(^(json|ya?ml)$)}",
Action<SwaggerEndpointOptions> setupAction = null)
{
if (!RoutePatternFactory.Parse(pattern).Parameters.Any(x => x.Name == "documentName"))
Expand Down
49 changes: 43 additions & 6 deletions src/Swashbuckle.AspNetCore.Swagger/SwaggerMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
#if !NETSTANDARD
martincostello marked this conversation as resolved.
Show resolved Hide resolved
using Microsoft.AspNetCore.Routing.Patterns;
#endif
using Microsoft.AspNetCore.Routing.Template;
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi.Writers;
Expand All @@ -15,6 +18,9 @@ public class SwaggerMiddleware
private readonly RequestDelegate _next;
private readonly SwaggerOptions _options;
private readonly TemplateMatcher _requestMatcher;
#if !NETSTANDARD
private readonly TemplateBinder _templateBinder;
#endif

public SwaggerMiddleware(
RequestDelegate next,
Expand All @@ -25,9 +31,19 @@ public SwaggerMiddleware(
_requestMatcher = new TemplateMatcher(TemplateParser.Parse(_options.RouteTemplate), new RouteValueDictionary());
}

#if !NETSTANDARD
public SwaggerMiddleware(
RequestDelegate next,
SwaggerOptions options,
TemplateBinderFactory templateBinderFactory) : this(next, options)
{
_templateBinder = templateBinderFactory.Create(RoutePatternFactory.Parse(_options.RouteTemplate));
martincostello marked this conversation as resolved.
Show resolved Hide resolved
}
#endif

public async Task Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)
{
if (!RequestingSwaggerDocument(httpContext.Request, out string documentName))
if (!RequestingSwaggerDocument(httpContext.Request, out string documentName, out string extension))
{
await _next(httpContext);
return;
Expand Down Expand Up @@ -57,7 +73,7 @@ public async Task Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvid
filter(swagger, httpContext.Request);
}

if (Path.GetExtension(httpContext.Request.Path.Value) == ".yaml")
if (extension is ".yaml" or ".yml")
martincostello marked this conversation as resolved.
Show resolved Hide resolved
{
await RespondWithSwaggerYaml(httpContext.Response, swagger);
}
Expand All @@ -72,16 +88,37 @@ public async Task Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvid
}
}

private bool RequestingSwaggerDocument(HttpRequest request, out string documentName)
private bool RequestingSwaggerDocument(HttpRequest request, out string documentName, out string extension)
{
documentName = null;
extension = null;
if (request.Method != "GET") return false;

var routeValues = new RouteValueDictionary();
if (!_requestMatcher.TryMatch(request.Path, routeValues) || !routeValues.ContainsKey("documentName")) return false;
if (_requestMatcher.TryMatch(request.Path, routeValues))
{
#if !NETSTANDARD
if (_templateBinder != null && !_templateBinder.TryProcessConstraints(request.HttpContext, routeValues, out _, out _))
{
return false;
}
#endif
if (routeValues.TryGetValue("documentName", out var documentNameObject) && documentNameObject is string documentNameString)
{
documentName = documentNameString;
if (routeValues.TryGetValue("extension", out var extensionObject))
{
extension = $".{extensionObject}";
}
else
{
extension = Path.GetExtension(request.Path.Value);
}
return true;
}
}

documentName = routeValues["documentName"].ToString();
return true;
return false;
}

private void RespondWithNotFound(HttpResponse response)
Expand Down
2 changes: 1 addition & 1 deletion src/Swashbuckle.AspNetCore.Swagger/SwaggerOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public SwaggerOptions()
/// <summary>
/// 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}";
public string RouteTemplate { get; set; } = "swagger/{documentName}/swagger.{extension:regex(^(json|ya?ml)$)}";

/// <summary>
/// Return Swagger JSON/YAML in the V2 format rather than V3
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Testing;
using Xunit;

namespace Swashbuckle.AspNetCore.IntegrationTests
{
public class SwaggerAndSwaggerUIIntegrationTests
{
[Theory]
[InlineData("/swagger/index.html", "text/html")]
[InlineData("/swagger/v1.json", "application/json")]
[InlineData("/swagger/v1.yaml", "text/yaml")]
[InlineData("/swagger/v1.yml", "text/yaml")]
public async Task SwaggerDocWithoutSubdirectory(string path, string mediaType)
{
var client = new WebApplicationFactory<TopLevelSwaggerDoc.Program>().CreateClient();

var response = await client.GetAsync(path);

Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(mediaType, response.Content.Headers.ContentType?.MediaType);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<AssemblyOriginatorKeyFile>Swashbuckle.AspNetCore.IntegrationTests.snk</AssemblyOriginatorKeyFile>
Expand All @@ -19,10 +19,11 @@
<ProjectReference Include="..\WebSites\OAuth2Integration\OAuth2Integration.csproj" />
<ProjectReference Include="..\WebSites\ReDoc\ReDoc.csproj" />
<ProjectReference Include="..\WebSites\TestFirst\TestFirst.csproj" />
<ProjectReference Include="..\WebSites\TopLevelSwaggerDoc\TopLevelSwaggerDoc.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.TestHost" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="Microsoft.OpenApi.Readers" />
<PackageReference Include="xunit" />
Expand Down
19 changes: 19 additions & 0 deletions test/WebSites/TopLevelSwaggerDoc/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using Microsoft.OpenApi.Models;

namespace TopLevelSwaggerDoc;

public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddMvcCore().AddApiExplorer();
builder.Services.AddSwaggerGen(c => c.SwaggerDoc("v1", new OpenApiInfo { Title = "Test API", Version = "1" }));
var app = builder.Build();

app.UseSwagger(c => c.RouteTemplate = c.RouteTemplate.Replace("swagger/{documentName}/swagger.", "swagger/{documentName}."));
app.UseSwaggerUI(c => c.SwaggerEndpoint("v1.json", "Test API"));

app.Run();
}
}
28 changes: 28 additions & 0 deletions test/WebSites/TopLevelSwaggerDoc/Properties/launchSettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:12841",
"sslPort": 44316
}
},
"profiles": {
"TopLevelSwaggerDoc": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7028;http://localhost:5225",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
15 changes: 15 additions & 0 deletions test/WebSites/TopLevelSwaggerDoc/TopLevelSwaggerDoc.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\..\src\Swashbuckle.AspNetCore.Swagger\Swashbuckle.AspNetCore.Swagger.csproj" />
<ProjectReference Include="..\..\..\src\Swashbuckle.AspNetCore.SwaggerGen\Swashbuckle.AspNetCore.SwaggerGen.csproj" />
<ProjectReference Include="..\..\..\src\Swashbuckle.AspNetCore.SwaggerUI\Swashbuckle.AspNetCore.SwaggerUI.csproj" />
</ItemGroup>

</Project>
8 changes: 8 additions & 0 deletions test/WebSites/TopLevelSwaggerDoc/appsettings.Development.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
9 changes: 9 additions & 0 deletions test/WebSites/TopLevelSwaggerDoc/appsettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
Loading