Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<UseArtifactsOutput>true</UseArtifactsOutput>
<VersionPrefix>8.0.1</VersionPrefix>
<VersionPrefix>8.1.0</VersionPrefix>
<WarnOnPackingNonPackableProject>false</WarnOnPackingNonPackableProject>
</PropertyGroup>
<PropertyGroup Condition=" '$(GITHUB_ACTIONS)' != '' AND '$(DEPENDABOT_JOB_ID)' == '' ">
Expand Down
2 changes: 1 addition & 1 deletion src/Swashbuckle.AspNetCore.Cli/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ private static string PrepareCommandLine(string[] args, IDictionary<string, stri
"exec --depsfile {0} --runtimeconfig {1} {2} _{3} {4}", // note the underscore prepended to the command name
EscapePath(depsFile),
EscapePath(runtimeConfig),
EscapePath(typeof(Program).GetTypeInfo().Assembly.Location),
EscapePath(typeof(Program).Assembly.Location),
commandName,
string.Join(" ", subProcessArguments.Select(EscapePath))
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Swashbuckle.AspNetCore.ReDoc.ReDocOptions.CacheLifetime.get -> System.TimeSpan?
Swashbuckle.AspNetCore.ReDoc.ReDocOptions.CacheLifetime.set -> void
61 changes: 59 additions & 2 deletions src/Swashbuckle.AspNetCore.ReDoc/ReDocMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System.Security.Cryptography;

#if NET
using System.Diagnostics.CodeAnalysis;
Expand All @@ -23,6 +24,8 @@ internal sealed class ReDocMiddleware
{
private const string EmbeddedFileNamespace = "Swashbuckle.AspNetCore.ReDoc.node_modules.redoc.bundles";

private static readonly string ReDocVersion = GetReDocVersion();

private readonly ReDocOptions _options;
private readonly StaticFileMiddleware _staticFileMiddleware;
private readonly JsonSerializerOptions _jsonSerializerOptions;
Expand Down Expand Up @@ -100,12 +103,47 @@ private static StaticFileMiddleware CreateStaticFileMiddleware(
var staticFileOptions = new StaticFileOptions
{
RequestPath = string.IsNullOrEmpty(options.RoutePrefix) ? string.Empty : $"/{options.RoutePrefix}",
FileProvider = new EmbeddedFileProvider(typeof(ReDocMiddleware).GetTypeInfo().Assembly, EmbeddedFileNamespace),
FileProvider = new EmbeddedFileProvider(typeof(ReDocMiddleware).Assembly, EmbeddedFileNamespace),
OnPrepareResponse = (context) => SetCacheHeaders(context.Context.Response, options),
};

return new StaticFileMiddleware(next, hostingEnv, Options.Create(staticFileOptions), loggerFactory);
}

private static string GetReDocVersion()
{
return typeof(ReDocMiddleware).Assembly
.GetCustomAttributes<AssemblyMetadataAttribute>()
.Where((p) => p.Key is "ReDocVersion")
.Select((p) => p.Value)
.DefaultIfEmpty(string.Empty)
.FirstOrDefault();
}

private static void SetCacheHeaders(HttpResponse response, ReDocOptions options, string etag = null)
{
var headers = response.GetTypedHeaders();

if (options.CacheLifetime is { } maxAge)
{
headers.CacheControl = new()
{
MaxAge = maxAge,
Private = true,
};
}
else
{
headers.CacheControl = new()
{
NoCache = true,
NoStore = true,
};
}

headers.ETag = new($"\"{etag ?? ReDocVersion}\"", isWeak: true);
}

private static void RespondWithRedirect(HttpResponse response, string location)
{
response.StatusCode = StatusCodes.Status301MovedPermanently;
Expand Down Expand Up @@ -147,10 +185,29 @@ private async Task RespondWithFile(HttpResponse response, string fileName)
content.Replace(entry.Key, entry.Value);
}

await response.WriteAsync(content.ToString(), Encoding.UTF8);
var text = content.ToString();
var etag = HashText(text);

SetCacheHeaders(response, _options, etag);

await response.WriteAsync(text, Encoding.UTF8);
}
}

private static string HashText(string text)
{
var buffer = Encoding.UTF8.GetBytes(text);

#if NET
var hash = SHA1.HashData(buffer);
#else
using var sha = SHA1.Create();
var hash = sha.ComputeHash(buffer);
#endif

return Convert.ToBase64String(hash);
}

#if NET
[UnconditionalSuppressMessage(
"AOT",
Expand Down
8 changes: 8 additions & 0 deletions src/Swashbuckle.AspNetCore.ReDoc/ReDocOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,12 @@ public class ReDocOptions
/// Gets or sets the optional <see cref="System.Text.Json.JsonSerializerOptions"/> to use.
/// </summary>
public JsonSerializerOptions JsonSerializerOptions { get; set; }

/// <summary>
/// Gets or sets the cache lifetime to use for the ReDoc files, if any.
/// </summary>
/// <remarks>
/// The default value is 7 days.
/// </remarks>
public TimeSpan? CacheLifetime { get; set; } = TimeSpan.FromDays(7);
}
6 changes: 2 additions & 4 deletions src/Swashbuckle.AspNetCore.ReDoc/ResourceHelper.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
using System.Reflection;

namespace Swashbuckle.AspNetCore.ReDoc;
namespace Swashbuckle.AspNetCore.ReDoc;

internal static class ResourceHelper
{
public static Stream GetEmbeddedResource(string fileName)
{
return typeof(ResourceHelper).GetTypeInfo().Assembly
return typeof(ResourceHelper).Assembly
.GetManifestResourceStream($"Swashbuckle.AspNetCore.ReDoc.{fileName}");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,28 @@
<Error Condition="'$(ErrorCode)' != '0'" Text="Node.js/npm is required to build this project." />
</Target>

<!-- Embed the ReDoc version into the [AssemblyMetadata] attributes -->
<UsingTask TaskName="GetReDocVersion" TaskFactory="RoslynCodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll">
<ParameterGroup>
<PackageJsonPath ParameterType="System.String" Required="true" />
<ReDocVersion ParameterType="System.String" Output="true" />
</ParameterGroup>
<Task>
<Code Type="Fragment" Language="cs"><![CDATA[
var packageJson = System.IO.File.ReadAllText(PackageJsonPath);
var pattern = @"""redoc"":\s*""([^""]+)""";
var match = System.Text.RegularExpressions.Regex.Match(packageJson, pattern);
ReDocVersion = match.Groups[1].Value;
]]></Code>
</Task>
</UsingTask>
<Target Name="AddCustomAssemblyMetadata" BeforeTargets="GetAssemblyAttributes">
<GetReDocVersion PackageJsonPath="$(MSBuildThisFileDirectory)\package.json">
<Output TaskParameter="ReDocVersion" PropertyName="ReDocVersion" />
</GetReDocVersion>
<ItemGroup>
<AssemblyMetadata Include="ReDocVersion" Value="$(ReDocVersion)" />
</ItemGroup>
</Target>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIOptions.CacheLifetime.get -> System.TimeSpan?
Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIOptions.CacheLifetime.set -> void
6 changes: 2 additions & 4 deletions src/Swashbuckle.AspNetCore.SwaggerUI/ResourceHelper.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
using System.Reflection;

namespace Swashbuckle.AspNetCore.SwaggerUI;
namespace Swashbuckle.AspNetCore.SwaggerUI;

internal static class ResourceHelper
{
public static Stream GetEmbeddedResource(string fileName)
{
return typeof(ResourceHelper).GetTypeInfo().Assembly
return typeof(ResourceHelper).Assembly
.GetManifestResourceStream($"Swashbuckle.AspNetCore.SwaggerUI.{fileName}");
}
}
74 changes: 66 additions & 8 deletions src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIMiddleware.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Text;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System.Reflection;
using System.Security.Cryptography;

#if !NET
#if NET
using System.Diagnostics.CodeAnalysis;
using Microsoft.AspNetCore.Hosting;
#else
using System.Text.Json.Serialization;
using IWebHostEnvironment = Microsoft.AspNetCore.Hosting.IHostingEnvironment;
#endif

Expand All @@ -22,6 +24,8 @@ internal sealed partial class SwaggerUIMiddleware
{
private const string EmbeddedFileNamespace = "Swashbuckle.AspNetCore.SwaggerUI.node_modules.swagger_ui_dist";

private static readonly string SwaggerUIVersion = GetSwaggerUIVersion();

private readonly SwaggerUIOptions _options;
private readonly StaticFileMiddleware _staticFileMiddleware;
private readonly JsonSerializerOptions _jsonSerializerOptions;
Expand Down Expand Up @@ -106,12 +110,47 @@ private static StaticFileMiddleware CreateStaticFileMiddleware(
var staticFileOptions = new StaticFileOptions
{
RequestPath = string.IsNullOrEmpty(options.RoutePrefix) ? string.Empty : $"/{options.RoutePrefix}",
FileProvider = new EmbeddedFileProvider(typeof(SwaggerUIMiddleware).GetTypeInfo().Assembly, EmbeddedFileNamespace),
FileProvider = new EmbeddedFileProvider(typeof(SwaggerUIMiddleware).Assembly, EmbeddedFileNamespace),
OnPrepareResponse = (context) => SetCacheHeaders(context.Context.Response, options),
};

return new StaticFileMiddleware(next, hostingEnv, Options.Create(staticFileOptions), loggerFactory);
}

private static string GetSwaggerUIVersion()
{
return typeof(SwaggerUIMiddleware).Assembly
.GetCustomAttributes<AssemblyMetadataAttribute>()
.Where((p) => p.Key is "SwaggerUIVersion")
.Select((p) => p.Value)
.DefaultIfEmpty(string.Empty)
.FirstOrDefault();
}

private static void SetCacheHeaders(HttpResponse response, SwaggerUIOptions options, string etag = null)
{
var headers = response.GetTypedHeaders();

if (options.CacheLifetime is { } maxAge)
{
headers.CacheControl = new()
{
MaxAge = maxAge,
Private = true,
};
}
else
{
headers.CacheControl = new()
{
NoCache = true,
NoStore = true,
};
}

headers.ETag = new($"\"{etag ?? SwaggerUIVersion}\"", isWeak: true);
}

private static void RespondWithRedirect(HttpResponse response, string location)
{
response.StatusCode = StatusCodes.Status301MovedPermanently;
Expand Down Expand Up @@ -150,10 +189,29 @@ private async Task RespondWithFile(HttpResponse response, string fileName)
content.Replace(entry.Key, entry.Value);
}

await response.WriteAsync(content.ToString(), Encoding.UTF8);
var text = content.ToString();
var etag = HashText(text);

SetCacheHeaders(response, _options, etag);

await response.WriteAsync(text, Encoding.UTF8);
}
}

private static string HashText(string text)
{
var buffer = Encoding.UTF8.GetBytes(text);

#if NET
var hash = SHA1.HashData(buffer);
#else
using var sha = SHA1.Create();
var hash = sha.ComputeHash(buffer);
#endif

return Convert.ToBase64String(hash);
}

#if NET
[UnconditionalSuppressMessage(
"AOT",
Expand Down
8 changes: 8 additions & 0 deletions src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,12 @@ public class SwaggerUIOptions
/// Gets or sets the relative URL path to the route that exposes the values of the configured <see cref="ConfigObject.Urls"/> values.
/// </summary>
public string SwaggerDocumentUrlsPath { get; set; } = "documentUrls";

/// <summary>
/// Gets or sets the cache lifetime to use for the SwaggerUI files, if any.
/// </summary>
/// <remarks>
/// The default value is 7 days.
/// </remarks>
public TimeSpan? CacheLifetime { get; set; } = TimeSpan.FromDays(7);
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,28 @@
<Error Condition="'$(ErrorCode)' != '0'" Text="Node.js/npm is required to build this project." />
</Target>

<!-- Embed the swagger-ui version into the [AssemblyMetadata] attributes -->
<UsingTask TaskName="GetSwaggerUIVersion" TaskFactory="RoslynCodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll">
<ParameterGroup>
<PackageJsonPath ParameterType="System.String" Required="true" />
<SwaggerUIVersion ParameterType="System.String" Output="true" />
</ParameterGroup>
<Task>
<Code Type="Fragment" Language="cs"><![CDATA[
var packageJson = System.IO.File.ReadAllText(PackageJsonPath);
var pattern = @"""swagger-ui-dist"":\s*""([^""]+)""";
var match = System.Text.RegularExpressions.Regex.Match(packageJson, pattern);
SwaggerUIVersion = match.Groups[1].Value;
]]></Code>
</Task>
</UsingTask>
<Target Name="AddCustomAssemblyMetadata" BeforeTargets="GetAssemblyAttributes">
<GetSwaggerUIVersion PackageJsonPath="$(MSBuildThisFileDirectory)\package.json">
<Output TaskParameter="SwaggerUIVersion" PropertyName="SwaggerUIVersion" />
</GetSwaggerUIVersion>
<ItemGroup>
<AssemblyMetadata Include="SwaggerUIVersion" Value="$(SwaggerUIVersion)" />
</ItemGroup>
</Target>

</Project>
Loading