From 99e11eaeedb065e1597278455846befc4e0f27f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAnior=20Santana?= Date: Sun, 30 Jun 2024 22:41:17 +0100 Subject: [PATCH 1/5] Move inline css and js to external files for SwaggerUI and ReDoc --- .../ReDocMiddleware.cs | 33 ++++-- .../Swashbuckle.AspNetCore.ReDoc.csproj | 7 ++ src/Swashbuckle.AspNetCore.ReDoc/index.css | 4 + src/Swashbuckle.AspNetCore.ReDoc/index.html | 13 +-- src/Swashbuckle.AspNetCore.ReDoc/index.js | 1 + .../SwaggerUIMiddleware.cs | 41 ++++--- .../Swashbuckle.AspNetCore.SwaggerUI.csproj | 5 +- .../index.html | 102 +----------------- src/Swashbuckle.AspNetCore.SwaggerUI/index.js | 75 +++++++++++++ .../ReDocIntegrationTests.cs | 18 +++- .../SwaggerUIIntegrationTests.cs | 42 +++++--- 11 files changed, 196 insertions(+), 145 deletions(-) create mode 100644 src/Swashbuckle.AspNetCore.ReDoc/index.css create mode 100644 src/Swashbuckle.AspNetCore.ReDoc/index.js create mode 100644 src/Swashbuckle.AspNetCore.SwaggerUI/index.js diff --git a/src/Swashbuckle.AspNetCore.ReDoc/ReDocMiddleware.cs b/src/Swashbuckle.AspNetCore.ReDoc/ReDocMiddleware.cs index cd28dbdfb0..c226122a32 100644 --- a/src/Swashbuckle.AspNetCore.ReDoc/ReDocMiddleware.cs +++ b/src/Swashbuckle.AspNetCore.ReDoc/ReDocMiddleware.cs @@ -56,7 +56,7 @@ public async Task Invoke(HttpContext httpContext) var path = httpContext.Request.Path.Value; // 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)) + if (httpMethod == "GET" && Regex.IsMatch(path, $"^/?{Regex.Escape(_options.RoutePrefix)}/?$", RegexOptions.IgnoreCase)) { // Use relative redirect to support proxy environments var relativeIndexUrl = string.IsNullOrEmpty(path) || path.EndsWith("/") @@ -67,9 +67,11 @@ public async Task Invoke(HttpContext httpContext) return; } - if (httpMethod == "GET" && Regex.IsMatch(path, $"/{_options.RoutePrefix}/?index.html", RegexOptions.IgnoreCase)) + var match = Regex.Match(path, $"^/{Regex.Escape(_options.RoutePrefix)}/?(index.html|index.css|index.js)$", RegexOptions.IgnoreCase); + + if (httpMethod == "GET" && match.Success) { - await RespondWithIndexHtml(httpContext.Response); + await RespondWithFile(httpContext.Response, match.Groups[1].Value); return; } @@ -97,12 +99,31 @@ private void RespondWithRedirect(HttpResponse response, string location) response.Headers["Location"] = location; } - private async Task RespondWithIndexHtml(HttpResponse response) + private async Task RespondWithFile(HttpResponse response, string fileName) { response.StatusCode = 200; - response.ContentType = "text/html"; - using (var stream = _options.IndexStream()) + Stream stream; + + switch (fileName) + { + case "index.css": + response.ContentType = "text/css"; + stream = typeof(ReDocMiddleware).GetTypeInfo().Assembly + .GetManifestResourceStream($"Swashbuckle.AspNetCore.ReDoc.{fileName}"); + break; + case "index.js": + response.ContentType = "application/javascript"; + stream = typeof(ReDocMiddleware).GetTypeInfo().Assembly + .GetManifestResourceStream($"Swashbuckle.AspNetCore.ReDoc.{fileName}"); + break; + default: + response.ContentType = "text/html;charset=utf-8"; + stream = _options.IndexStream(); + break; + } + + using (stream) { // Inject arguments before writing to response var htmlBuilder = new StringBuilder(new StreamReader(stream).ReadToEnd()); diff --git a/src/Swashbuckle.AspNetCore.ReDoc/Swashbuckle.AspNetCore.ReDoc.csproj b/src/Swashbuckle.AspNetCore.ReDoc/Swashbuckle.AspNetCore.ReDoc.csproj index ab861e7bf2..ad8acf84a8 100644 --- a/src/Swashbuckle.AspNetCore.ReDoc/Swashbuckle.AspNetCore.ReDoc.csproj +++ b/src/Swashbuckle.AspNetCore.ReDoc/Swashbuckle.AspNetCore.ReDoc.csproj @@ -11,7 +11,14 @@ + + + + + + + diff --git a/src/Swashbuckle.AspNetCore.ReDoc/index.css b/src/Swashbuckle.AspNetCore.ReDoc/index.css new file mode 100644 index 0000000000..10a8a247a3 --- /dev/null +++ b/src/Swashbuckle.AspNetCore.ReDoc/index.css @@ -0,0 +1,4 @@ +body { + margin: 0; + padding: 0; +} diff --git a/src/Swashbuckle.AspNetCore.ReDoc/index.html b/src/Swashbuckle.AspNetCore.ReDoc/index.html index 28bcc5cdb0..b3752bcfef 100644 --- a/src/Swashbuckle.AspNetCore.ReDoc/index.html +++ b/src/Swashbuckle.AspNetCore.ReDoc/index.html @@ -10,19 +10,12 @@ - + %(HeadContent)
- + - \ No newline at end of file + diff --git a/src/Swashbuckle.AspNetCore.ReDoc/index.js b/src/Swashbuckle.AspNetCore.ReDoc/index.js new file mode 100644 index 0000000000..48ead65adb --- /dev/null +++ b/src/Swashbuckle.AspNetCore.ReDoc/index.js @@ -0,0 +1 @@ +Redoc.init('%(SpecUrl)', JSON.parse('%(ConfigObject)'), document.getElementById('redoc-container')); \ No newline at end of file diff --git a/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIMiddleware.cs b/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIMiddleware.cs index 190e8c2ee4..2f8f926289 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIMiddleware.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIMiddleware.cs @@ -68,8 +68,16 @@ public async Task Invoke(HttpContext httpContext) var isGet = HttpMethods.IsGet(httpMethod); + var match = Regex.Match(path, $"^/{Regex.Escape(_options.RoutePrefix)}/?(index.html|index.js)$", RegexOptions.IgnoreCase); + + if (isGet && match.Success) + { + await RespondWithFile(httpContext.Response, match.Groups[1].Value); + return; + } + // If the RoutePrefix is requested (with or without trailing slash), redirect to index URL - if (isGet && Regex.IsMatch(path, $"^/?{Regex.Escape(_options.RoutePrefix)}/?$", RegexOptions.IgnoreCase)) + if (isGet && Regex.IsMatch(path, $"^/?{Regex.Escape(_options.RoutePrefix)}/?$", RegexOptions.IgnoreCase)) { // Use relative redirect to support proxy environments var relativeIndexUrl = string.IsNullOrEmpty(path) || path.EndsWith("/") @@ -80,12 +88,6 @@ public async Task Invoke(HttpContext httpContext) return; } - if (isGet && Regex.IsMatch(path, $"^/{Regex.Escape(_options.RoutePrefix)}/?index.html$", RegexOptions.IgnoreCase)) - { - await RespondWithIndexHtml(httpContext.Response); - return; - } - await _staticFileMiddleware.Invoke(httpContext); } @@ -110,23 +112,36 @@ private static void RespondWithRedirect(HttpResponse response, string location) response.Headers["Location"] = location; } - private async Task RespondWithIndexHtml(HttpResponse response) + private async Task RespondWithFile(HttpResponse response, string fileName) { response.StatusCode = 200; - response.ContentType = "text/html;charset=utf-8"; - using (var stream = _options.IndexStream()) + Stream stream; + + if (fileName == "index.js") + { + response.ContentType = "application/javascript"; + stream = typeof(SwaggerUIMiddleware).GetTypeInfo().Assembly + .GetManifestResourceStream($"Swashbuckle.AspNetCore.SwaggerUI.{fileName}"); + } + else + { + response.ContentType = "text/html;charset=utf-8"; + stream = _options.IndexStream(); + } + + using (stream) { using var reader = new StreamReader(stream); // Inject arguments before writing to response - var htmlBuilder = new StringBuilder(await reader.ReadToEndAsync()); + var content = new StringBuilder(await reader.ReadToEndAsync()); foreach (var entry in GetIndexArguments()) { - htmlBuilder.Replace(entry.Key, entry.Value); + content.Replace(entry.Key, entry.Value); } - await response.WriteAsync(htmlBuilder.ToString(), Encoding.UTF8); + await response.WriteAsync(content.ToString(), Encoding.UTF8); } } diff --git a/src/Swashbuckle.AspNetCore.SwaggerUI/Swashbuckle.AspNetCore.SwaggerUI.csproj b/src/Swashbuckle.AspNetCore.SwaggerUI/Swashbuckle.AspNetCore.SwaggerUI.csproj index 10d562b646..5b71c9f6bd 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerUI/Swashbuckle.AspNetCore.SwaggerUI.csproj +++ b/src/Swashbuckle.AspNetCore.SwaggerUI/Swashbuckle.AspNetCore.SwaggerUI.csproj @@ -6,7 +6,7 @@ true $(NoWarn);1591 swagger;documentation;discovery;help;webapi;aspnet;aspnetcore - true + true netstandard2.0;netcoreapp3.0;net5.0;net6.0;net7.0;net8.0 @@ -16,7 +16,10 @@ + + + diff --git a/src/Swashbuckle.AspNetCore.SwaggerUI/index.html b/src/Swashbuckle.AspNetCore.SwaggerUI/index.html index cfe58ed83e..8c2c05c675 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerUI/index.html +++ b/src/Swashbuckle.AspNetCore.SwaggerUI/index.html @@ -5,113 +5,17 @@ %(DocumentTitle) + - - %(HeadContent) + %(HeadContent)
- - - - + diff --git a/src/Swashbuckle.AspNetCore.SwaggerUI/index.js b/src/Swashbuckle.AspNetCore.SwaggerUI/index.js new file mode 100644 index 0000000000..2e1e0464f1 --- /dev/null +++ b/src/Swashbuckle.AspNetCore.SwaggerUI/index.js @@ -0,0 +1,75 @@ +// Workaround for https://github.com/swagger-api/swagger-editor/issues/1371 +if (window.navigator.userAgent.indexOf("Edge") > -1) { + console.log("Removing native Edge fetch in favor of swagger-ui's polyfill") + window.fetch = undefined; +} + +/* Source: https://gist.github.com/lamberta/3768814 + * Parse a string function definition and return a function object. Does not use eval. + * @param {string} str + * @return {function} + * + * Example: + * var f = function (x, y) { return x * y; }; + * var g = parseFunction(f.toString()); + * g(33, 3); //=> 99 + */ +function parseFunction(str) { + if (!str) return void (0); + + var fn_body_idx = str.indexOf('{'), + fn_body = str.substring(fn_body_idx + 1, str.lastIndexOf('}')), + fn_declare = str.substring(0, fn_body_idx), + fn_params = fn_declare.substring(fn_declare.indexOf('(') + 1, fn_declare.lastIndexOf(')')), + args = fn_params.split(','); + + args.push(fn_body); + + function Fn() { + return Function.apply(this, args); + } + Fn.prototype = Function.prototype; + + return new Fn(); +} + +window.onload = function () { + var configObject = JSON.parse('%(ConfigObject)'); + var oauthConfigObject = JSON.parse('%(OAuthConfigObject)'); + + // Workaround for https://github.com/swagger-api/swagger-ui/issues/5945 + configObject.urls.forEach(function (item) { + if (item.url.startsWith("http") || item.url.startsWith("/")) return; + item.url = window.location.href.replace("index.html", item.url).split('#')[0]; + }); + + // If validatorUrl is not explicitly provided, disable the feature by setting to null + if (!configObject.hasOwnProperty("validatorUrl")) + configObject.validatorUrl = null + + // If oauth2RedirectUrl isn't specified, use the built-in default + if (!configObject.hasOwnProperty("oauth2RedirectUrl")) + configObject.oauth2RedirectUrl = (new URL("oauth2-redirect.html", window.location.href)).href; + + // Apply mandatory parameters + configObject.dom_id = "#swagger-ui"; + configObject.presets = [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset]; + configObject.layout = "StandaloneLayout"; + + // Parse and add interceptor functions + var interceptors = JSON.parse('%(Interceptors)'); + if (interceptors.RequestInterceptorFunction) + configObject.requestInterceptor = parseFunction(interceptors.RequestInterceptorFunction); + if (interceptors.ResponseInterceptorFunction) + configObject.responseInterceptor = parseFunction(interceptors.ResponseInterceptorFunction); + + // Begin Swagger UI call region + + const ui = SwaggerUIBundle(configObject); + + ui.initOAuth(oauthConfigObject); + + // End Swagger UI call region + + window.ui = ui +} diff --git a/test/Swashbuckle.AspNetCore.IntegrationTests/ReDocIntegrationTests.cs b/test/Swashbuckle.AspNetCore.IntegrationTests/ReDocIntegrationTests.cs index 048d23221c..541655444c 100644 --- a/test/Swashbuckle.AspNetCore.IntegrationTests/ReDocIntegrationTests.cs +++ b/test/Swashbuckle.AspNetCore.IntegrationTests/ReDocIntegrationTests.cs @@ -19,6 +19,18 @@ public async Task RoutePrefix_RedirectsToIndexUrl() Assert.Equal("api-docs/index.html", response.Headers.Location.ToString()); } + [Fact] + public async Task RedocMiddleware_ReturnsInitializerScript() + { + var client = new TestSite(typeof(ReDocApp.Startup)).BuildClient(); + + var response = await client.GetAsync("/api-docs/index.js"); + var indexContent = await response.Content.ReadAsStringAsync(); + + Assert.Contains("Redoc.init", indexContent); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + [Fact] public async Task IndexUrl_ReturnsEmbeddedVersionOfTheRedocUI() { @@ -28,7 +40,6 @@ public async Task IndexUrl_ReturnsEmbeddedVersionOfTheRedocUI() var jsResponse = await client.GetAsync("/api-docs/redoc.standalone.js"); var indexContent = await indexResponse.Content.ReadAsStringAsync(); - Assert.Contains("Redoc.init", indexContent); Assert.Equal(HttpStatusCode.OK, jsResponse.StatusCode); } @@ -41,13 +52,12 @@ public async Task IndexUrl_IgnoresUrlCase() var jsResponse = await client.GetAsync("/Api-Docs/redoc.standalone.js"); var indexContent = await indexResponse.Content.ReadAsStringAsync(); - Assert.Contains("Redoc.init", indexContent); Assert.Equal(HttpStatusCode.OK, jsResponse.StatusCode); } [Theory] - [InlineData("/redoc/1.0/index.html", "/swagger/1.0/swagger.json")] - [InlineData("/redoc/2.0/index.html", "/swagger/2.0/swagger.json")] + [InlineData("/redoc/1.0/index.js", "/swagger/1.0/swagger.json")] + [InlineData("/redoc/2.0/index.js", "/swagger/2.0/swagger.json")] public async Task RedocMiddleware_CanBeConfiguredMultipleTimes(string redocUrl, string swaggerPath) { var client = new TestSite(typeof(MultipleVersions.Startup)).BuildClient(); diff --git a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerUIIntegrationTests.cs b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerUIIntegrationTests.cs index 9ac7bf5dee..489adca559 100644 --- a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerUIIntegrationTests.cs +++ b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerUIIntegrationTests.cs @@ -26,25 +26,43 @@ public async Task RoutePrefix_RedirectsToPathRelativeIndexUrl( } [Theory] - [InlineData(typeof(Basic.Startup), "/index.html", "/swagger-ui.js", "/swagger-ui.css")] - [InlineData(typeof(CustomUIConfig.Startup), "/swagger/index.html", "/swagger/swagger-ui.js", "/swagger/swagger-ui.css")] + [InlineData(typeof(Basic.Startup), "/index.js")] + [InlineData(typeof(CustomUIConfig.Startup), "/swagger/index.js")] + public async Task SwaggerUIMiddleware_ReturnsInitializerScript( + Type startupType, + string indexJsPath) + { + var client = new TestSite(startupType).BuildClient(); + + var indexResponse = await client.GetAsync(indexJsPath); + Assert.Equal(HttpStatusCode.OK, indexResponse.StatusCode); + + var indexContent = await indexResponse.Content.ReadAsStringAsync(); + Assert.Contains("SwaggerUIBundle", indexContent); + } + + [Theory] + [InlineData(typeof(Basic.Startup), "/index.html", "/swagger-ui.js", "/index.css", "/swagger-ui.css")] + [InlineData(typeof(CustomUIConfig.Startup), "/swagger/index.html", "/swagger/swagger-ui.js", "swagger/index.css", "/swagger/swagger-ui.css")] public async Task IndexUrl_ReturnsEmbeddedVersionOfTheSwaggerUI( Type startupType, string indexPath, - string jsPath, - string cssPath) + string swaggerUijsPath, + string indexCssPath, + string swaggerUiCssPath) { var client = new TestSite(startupType).BuildClient(); var indexResponse = await client.GetAsync(indexPath); Assert.Equal(HttpStatusCode.OK, indexResponse.StatusCode); - var indexContent = await indexResponse.Content.ReadAsStringAsync(); - Assert.Contains("SwaggerUIBundle", indexContent); - var jsResponse = await client.GetAsync(jsPath); + var jsResponse = await client.GetAsync(swaggerUijsPath); Assert.Equal(HttpStatusCode.OK, jsResponse.StatusCode); - var cssResponse = await client.GetAsync(cssPath); + var cssResponse = await client.GetAsync(indexCssPath); + Assert.Equal(HttpStatusCode.OK, cssResponse.StatusCode); + + cssResponse = await client.GetAsync(swaggerUiCssPath); Assert.Equal(HttpStatusCode.OK, cssResponse.StatusCode); } @@ -76,7 +94,7 @@ public async Task IndexUrl_ReturnsInterceptors_IfConfigured() { var client = new TestSite(typeof(CustomUIConfig.Startup)).BuildClient(); - var response = await client.GetAsync("/swagger/index.html"); + var response = await client.GetAsync("/swagger/index.js"); var content = await response.Content.ReadAsStringAsync(); Assert.Contains("\"RequestInterceptorFunction\":", content); @@ -84,9 +102,9 @@ public async Task IndexUrl_ReturnsInterceptors_IfConfigured() } [Theory] - [InlineData("/swagger/index.html", new [] { "Version 1.0", "Version 2.0" })] - [InlineData("/swagger/1.0/index.html", new [] { "Version 1.0" })] - [InlineData("/swagger/2.0/index.html", new [] { "Version 2.0" })] + [InlineData("/swagger/index.js", new[] { "Version 1.0", "Version 2.0" })] + [InlineData("/swagger/1.0/index.js", new[] { "Version 1.0" })] + [InlineData("/swagger/2.0/index.js", new[] { "Version 2.0" })] public async Task SwaggerUIMiddleware_CanBeConfiguredMultipleTimes(string swaggerUiUrl, string[] versions) { var client = new TestSite(typeof(MultipleVersions.Startup)).BuildClient(); From 91024d803236d45e298fc666cc54d08673d25cd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAnior=20Santana?= Date: Tue, 2 Jul 2024 22:32:23 +0100 Subject: [PATCH 2/5] Address code review - Add/improve unit tests - Define charset when serving js files - Tidy up code --- .../ReDocMiddleware.cs | 42 ++++++----- .../SwaggerUIMiddleware.cs | 38 +++++----- .../Swashbuckle.AspNetCore.SwaggerUI.csproj | 3 +- .../index.html | 6 +- .../ReDocIntegrationTests.cs | 52 ++++++++----- .../SwaggerUIIntegrationTests.cs | 74 +++++++++++-------- 6 files changed, 121 insertions(+), 94 deletions(-) diff --git a/src/Swashbuckle.AspNetCore.ReDoc/ReDocMiddleware.cs b/src/Swashbuckle.AspNetCore.ReDoc/ReDocMiddleware.cs index c226122a32..1a4274b93b 100644 --- a/src/Swashbuckle.AspNetCore.ReDoc/ReDocMiddleware.cs +++ b/src/Swashbuckle.AspNetCore.ReDoc/ReDocMiddleware.cs @@ -53,26 +53,30 @@ public ReDocMiddleware( public async Task Invoke(HttpContext httpContext) { var httpMethod = httpContext.Request.Method; - var path = httpContext.Request.Path.Value; - // 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)) + if (HttpMethods.IsGet(httpMethod)) { - // Use relative redirect to support proxy environments - var relativeIndexUrl = string.IsNullOrEmpty(path) || path.EndsWith("/") - ? "index.html" - : $"{path.Split('/').Last()}/index.html"; + var path = httpContext.Request.Path.Value; - RespondWithRedirect(httpContext.Response, relativeIndexUrl); - return; - } + // If the RoutePrefix is requested (with or without trailing slash), redirect to index URL + if (Regex.IsMatch(path, $"^/?{Regex.Escape(_options.RoutePrefix)}/?$", RegexOptions.IgnoreCase)) + { + // Use relative redirect to support proxy environments + var relativeIndexUrl = string.IsNullOrEmpty(path) || path.EndsWith("/") + ? "index.html" + : $"{path.Split('/').Last()}/index.html"; - var match = Regex.Match(path, $"^/{Regex.Escape(_options.RoutePrefix)}/?(index.html|index.css|index.js)$", RegexOptions.IgnoreCase); + RespondWithRedirect(httpContext.Response, relativeIndexUrl); + return; + } - if (httpMethod == "GET" && match.Success) - { - await RespondWithFile(httpContext.Response, match.Groups[1].Value); - return; + var match = Regex.Match(path, $"^/{Regex.Escape(_options.RoutePrefix)}/?(index.(html|css|js))$", RegexOptions.IgnoreCase); + + if (match.Success) + { + await RespondWithFile(httpContext.Response, match.Groups[1].Value); + return; + } } await _staticFileMiddleware.Invoke(httpContext); @@ -113,7 +117,7 @@ private async Task RespondWithFile(HttpResponse response, string fileName) .GetManifestResourceStream($"Swashbuckle.AspNetCore.ReDoc.{fileName}"); break; case "index.js": - response.ContentType = "application/javascript"; + response.ContentType = "application/javascript;charset=utf-8"; stream = typeof(ReDocMiddleware).GetTypeInfo().Assembly .GetManifestResourceStream($"Swashbuckle.AspNetCore.ReDoc.{fileName}"); break; @@ -126,13 +130,13 @@ private async Task RespondWithFile(HttpResponse response, string fileName) using (stream) { // Inject arguments before writing to response - var htmlBuilder = new StringBuilder(new StreamReader(stream).ReadToEnd()); + var content = new StringBuilder(new StreamReader(stream).ReadToEnd()); foreach (var entry in GetIndexArguments()) { - htmlBuilder.Replace(entry.Key, entry.Value); + content.Replace(entry.Key, entry.Value); } - await response.WriteAsync(htmlBuilder.ToString(), Encoding.UTF8); + await response.WriteAsync(content.ToString(), Encoding.UTF8); } } diff --git a/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIMiddleware.cs b/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIMiddleware.cs index 2f8f926289..21099e49bf 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIMiddleware.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIMiddleware.cs @@ -64,28 +64,30 @@ public SwaggerUIMiddleware( public async Task Invoke(HttpContext httpContext) { var httpMethod = httpContext.Request.Method; - var path = httpContext.Request.Path.Value; - var isGet = HttpMethods.IsGet(httpMethod); + if (HttpMethods.IsGet(httpMethod)) + { + var path = httpContext.Request.Path.Value; - var match = Regex.Match(path, $"^/{Regex.Escape(_options.RoutePrefix)}/?(index.html|index.js)$", RegexOptions.IgnoreCase); + // If the RoutePrefix is requested (with or without trailing slash), redirect to index URL + if (Regex.IsMatch(path, $"^/?{Regex.Escape(_options.RoutePrefix)}/?$", RegexOptions.IgnoreCase)) + { + // Use relative redirect to support proxy environments + var relativeIndexUrl = string.IsNullOrEmpty(path) || path.EndsWith("/") + ? "index.html" + : $"{path.Split('/').Last()}/index.html"; - if (isGet && match.Success) - { - await RespondWithFile(httpContext.Response, match.Groups[1].Value); - return; - } + RespondWithRedirect(httpContext.Response, relativeIndexUrl); + return; + } - // If the RoutePrefix is requested (with or without trailing slash), redirect to index URL - if (isGet && Regex.IsMatch(path, $"^/?{Regex.Escape(_options.RoutePrefix)}/?$", RegexOptions.IgnoreCase)) - { - // Use relative redirect to support proxy environments - var relativeIndexUrl = string.IsNullOrEmpty(path) || path.EndsWith("/") - ? "index.html" - : $"{path.Split('/').Last()}/index.html"; + var match = Regex.Match(path, $"^/{Regex.Escape(_options.RoutePrefix)}/?(index.(html|js))$", RegexOptions.IgnoreCase); - RespondWithRedirect(httpContext.Response, relativeIndexUrl); - return; + if (match.Success) + { + await RespondWithFile(httpContext.Response, match.Groups[1].Value); + return; + } } await _staticFileMiddleware.Invoke(httpContext); @@ -120,7 +122,7 @@ private async Task RespondWithFile(HttpResponse response, string fileName) if (fileName == "index.js") { - response.ContentType = "application/javascript"; + response.ContentType = "application/javascript;charset=utf-8"; stream = typeof(SwaggerUIMiddleware).GetTypeInfo().Assembly .GetManifestResourceStream($"Swashbuckle.AspNetCore.SwaggerUI.{fileName}"); } diff --git a/src/Swashbuckle.AspNetCore.SwaggerUI/Swashbuckle.AspNetCore.SwaggerUI.csproj b/src/Swashbuckle.AspNetCore.SwaggerUI/Swashbuckle.AspNetCore.SwaggerUI.csproj index 5b71c9f6bd..68b99042d6 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerUI/Swashbuckle.AspNetCore.SwaggerUI.csproj +++ b/src/Swashbuckle.AspNetCore.SwaggerUI/Swashbuckle.AspNetCore.SwaggerUI.csproj @@ -6,7 +6,7 @@ true $(NoWarn);1591 swagger;documentation;discovery;help;webapi;aspnet;aspnetcore - true + true netstandard2.0;netcoreapp3.0;net5.0;net6.0;net7.0;net8.0
@@ -16,7 +16,6 @@ - diff --git a/src/Swashbuckle.AspNetCore.SwaggerUI/index.html b/src/Swashbuckle.AspNetCore.SwaggerUI/index.html index 8c2c05c675..cea46ec4e0 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerUI/index.html +++ b/src/Swashbuckle.AspNetCore.SwaggerUI/index.html @@ -14,8 +14,8 @@
- - - + + + diff --git a/test/Swashbuckle.AspNetCore.IntegrationTests/ReDocIntegrationTests.cs b/test/Swashbuckle.AspNetCore.IntegrationTests/ReDocIntegrationTests.cs index 541655444c..69c635a7d5 100644 --- a/test/Swashbuckle.AspNetCore.IntegrationTests/ReDocIntegrationTests.cs +++ b/test/Swashbuckle.AspNetCore.IntegrationTests/ReDocIntegrationTests.cs @@ -20,27 +20,33 @@ public async Task RoutePrefix_RedirectsToIndexUrl() } [Fact] - public async Task RedocMiddleware_ReturnsInitializerScript() + public async Task IndexUrl_ReturnsEmbeddedVersionOfTheRedocUI() { var client = new TestSite(typeof(ReDocApp.Startup)).BuildClient(); - var response = await client.GetAsync("/api-docs/index.js"); - var indexContent = await response.Content.ReadAsStringAsync(); + var htmlResponse = await client.GetAsync("/api-docs/index.html"); + var cssResponse = await client.GetAsync("/api-docs/index.css"); + var jsResponse = await client.GetAsync("/api-docs/redoc.standalone.js"); - Assert.Contains("Redoc.init", indexContent); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(HttpStatusCode.OK, htmlResponse.StatusCode); + Assert.Equal(HttpStatusCode.OK, cssResponse.StatusCode); + Assert.Equal(HttpStatusCode.OK, jsResponse.StatusCode); } [Fact] - public async Task IndexUrl_ReturnsEmbeddedVersionOfTheRedocUI() + public async Task RedocMiddleware_ReturnsInitializerScript() { var client = new TestSite(typeof(ReDocApp.Startup)).BuildClient(); - var indexResponse = await client.GetAsync("/api-docs/index.html"); - var jsResponse = await client.GetAsync("/api-docs/redoc.standalone.js"); + var response = await client.GetAsync("/api-docs/index.js"); + var content = await response.Content.ReadAsStringAsync(); - var indexContent = await indexResponse.Content.ReadAsStringAsync(); - Assert.Equal(HttpStatusCode.OK, jsResponse.StatusCode); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Contains("Redoc.init", content); + Assert.DoesNotContain("%(DocumentTitle)", content); + Assert.DoesNotContain("%(HeadContent)", content); + Assert.DoesNotContain("%(SpecUrl)", content); + Assert.DoesNotContain("%(ConfigObject)", content); } [Fact] @@ -48,24 +54,30 @@ public async Task IndexUrl_IgnoresUrlCase() { var client = new TestSite(typeof(ReDocApp.Startup)).BuildClient(); - var indexResponse = await client.GetAsync("/Api-Docs/index.html"); - var jsResponse = await client.GetAsync("/Api-Docs/redoc.standalone.js"); + var htmlResponse = await client.GetAsync("/Api-Docs/index.html"); + var cssResponse = await client.GetAsync("/Api-Docs/index.css"); + var jsInitResponse = await client.GetAsync("/Api-Docs/index.js"); + var jsRedocResponse = await client.GetAsync("/Api-Docs/redoc.standalone.js"); - var indexContent = await indexResponse.Content.ReadAsStringAsync(); - Assert.Equal(HttpStatusCode.OK, jsResponse.StatusCode); + Assert.Equal(HttpStatusCode.OK, htmlResponse.StatusCode); + Assert.Equal(HttpStatusCode.OK, cssResponse.StatusCode); + Assert.Equal(HttpStatusCode.OK, jsInitResponse.StatusCode); + Assert.Equal(HttpStatusCode.OK, jsRedocResponse.StatusCode); } [Theory] - [InlineData("/redoc/1.0/index.js", "/swagger/1.0/swagger.json")] - [InlineData("/redoc/2.0/index.js", "/swagger/2.0/swagger.json")] - public async Task RedocMiddleware_CanBeConfiguredMultipleTimes(string redocUrl, string swaggerPath) + [InlineData("/redoc/1.0/index.html", "/redoc/1.0/index.js", "/swagger/1.0/swagger.json")] + [InlineData("/redoc/2.0/index.html", "/redoc/2.0/index.js", "/swagger/2.0/swagger.json")] + public async Task RedocMiddleware_CanBeConfiguredMultipleTimes(string htmlUrl, string jsUrl, string swaggerPath) { var client = new TestSite(typeof(MultipleVersions.Startup)).BuildClient(); - var response = await client.GetAsync(redocUrl); - var content = await response.Content.ReadAsStringAsync(); + var htmlResponse = await client.GetAsync(htmlUrl); + var jsResponse = await client.GetAsync(jsUrl); + var content = await jsResponse.Content.ReadAsStringAsync(); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(HttpStatusCode.OK, htmlResponse.StatusCode); + Assert.Equal(HttpStatusCode.OK, jsResponse.StatusCode); Assert.Contains(swaggerPath, content); } } diff --git a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerUIIntegrationTests.cs b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerUIIntegrationTests.cs index 489adca559..fa52f555da 100644 --- a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerUIIntegrationTests.cs +++ b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerUIIntegrationTests.cs @@ -25,36 +25,20 @@ public async Task RoutePrefix_RedirectsToPathRelativeIndexUrl( Assert.Equal(expectedRedirectPath, response.Headers.Location.ToString()); } - [Theory] - [InlineData(typeof(Basic.Startup), "/index.js")] - [InlineData(typeof(CustomUIConfig.Startup), "/swagger/index.js")] - public async Task SwaggerUIMiddleware_ReturnsInitializerScript( - Type startupType, - string indexJsPath) - { - var client = new TestSite(startupType).BuildClient(); - - var indexResponse = await client.GetAsync(indexJsPath); - Assert.Equal(HttpStatusCode.OK, indexResponse.StatusCode); - - var indexContent = await indexResponse.Content.ReadAsStringAsync(); - Assert.Contains("SwaggerUIBundle", indexContent); - } - [Theory] [InlineData(typeof(Basic.Startup), "/index.html", "/swagger-ui.js", "/index.css", "/swagger-ui.css")] [InlineData(typeof(CustomUIConfig.Startup), "/swagger/index.html", "/swagger/swagger-ui.js", "swagger/index.css", "/swagger/swagger-ui.css")] public async Task IndexUrl_ReturnsEmbeddedVersionOfTheSwaggerUI( Type startupType, - string indexPath, + string htmlPath, string swaggerUijsPath, string indexCssPath, string swaggerUiCssPath) { var client = new TestSite(startupType).BuildClient(); - var indexResponse = await client.GetAsync(indexPath); - Assert.Equal(HttpStatusCode.OK, indexResponse.StatusCode); + var htmlResponse = await client.GetAsync(htmlPath); + Assert.Equal(HttpStatusCode.OK, htmlResponse.StatusCode); var jsResponse = await client.GetAsync(swaggerUijsPath); Assert.Equal(HttpStatusCode.OK, jsResponse.StatusCode); @@ -66,6 +50,30 @@ public async Task IndexUrl_ReturnsEmbeddedVersionOfTheSwaggerUI( Assert.Equal(HttpStatusCode.OK, cssResponse.StatusCode); } + [Theory] + [InlineData(typeof(Basic.Startup), "/index.js")] + [InlineData(typeof(CustomUIConfig.Startup), "/swagger/index.js")] + public async Task SwaggerUIMiddleware_ReturnsInitializerScript( + Type startupType, + string indexJsPath) + { + var client = new TestSite(startupType).BuildClient(); + + var jsResponse = await client.GetAsync(indexJsPath); + Assert.Equal(HttpStatusCode.OK, jsResponse.StatusCode); + + var jsContent = await jsResponse.Content.ReadAsStringAsync(); + Assert.Contains("SwaggerUIBundle", jsContent); + Assert.DoesNotContain("%(DocumentTitle)", jsContent); + Assert.DoesNotContain("%(HeadContent)", jsContent); + Assert.DoesNotContain("%(StylesPath)", jsContent); + Assert.DoesNotContain("%(ScriptBundlePath)", jsContent); + Assert.DoesNotContain("%(ScriptPresetsPath)", jsContent); + Assert.DoesNotContain("%(ConfigObject)", jsContent); + Assert.DoesNotContain("%(OAuthConfigObject)", jsContent); + Assert.DoesNotContain("%(Interceptors)", jsContent); + } + [Fact] public async Task IndexUrl_ReturnsCustomPageTitleAndStylesheets_IfConfigured() { @@ -102,17 +110,19 @@ public async Task IndexUrl_ReturnsInterceptors_IfConfigured() } [Theory] - [InlineData("/swagger/index.js", new[] { "Version 1.0", "Version 2.0" })] - [InlineData("/swagger/1.0/index.js", new[] { "Version 1.0" })] - [InlineData("/swagger/2.0/index.js", new[] { "Version 2.0" })] - public async Task SwaggerUIMiddleware_CanBeConfiguredMultipleTimes(string swaggerUiUrl, string[] versions) + [InlineData("/swagger/index.html", "/swagger/index.js", new[] { "Version 1.0", "Version 2.0" })] + [InlineData("/swagger/1.0/index.html", "/swagger/1.0/index.js", new[] { "Version 1.0" })] + [InlineData("/swagger/2.0/index.html", "/swagger/2.0/index.js", new[] { "Version 2.0" })] + public async Task SwaggerUIMiddleware_CanBeConfiguredMultipleTimes(string htmlUrl, string jsUrl, string[] versions) { var client = new TestSite(typeof(MultipleVersions.Startup)).BuildClient(); - var response = await client.GetAsync(swaggerUiUrl); - var content = await response.Content.ReadAsStringAsync(); + var htmlResponse = await client.GetAsync(htmlUrl); + var jsResponse = await client.GetAsync(jsUrl); + var content = await jsResponse.Content.ReadAsStringAsync(); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(HttpStatusCode.OK, htmlResponse.StatusCode); + Assert.Equal(HttpStatusCode.OK, jsResponse.StatusCode); foreach (var version in versions) { Assert.Contains(version, content); @@ -124,20 +134,20 @@ public async Task SwaggerUIMiddleware_CanBeConfiguredMultipleTimes(string swagge [InlineData(typeof(CustomUIConfig.Startup), "/swagger/index.html", "/ext/custom-stylesheet.css", "/ext/custom-javascript.js", "/ext/custom-javascript.js")] public async Task IndexUrl_Returns_ExpectedAssetPaths( Type startupType, - string indexPath, + string htmlPath, string cssPath, string scriptBundlePath, string scriptPresetsPath) { var client = new TestSite(startupType).BuildClient(); - var indexResponse = await client.GetAsync(indexPath); - Assert.Equal(HttpStatusCode.OK, indexResponse.StatusCode); - var content = await indexResponse.Content.ReadAsStringAsync(); + var htmlResponse = await client.GetAsync(htmlPath); + Assert.Equal(HttpStatusCode.OK, htmlResponse.StatusCode); + var content = await htmlResponse.Content.ReadAsStringAsync(); Assert.Contains($"", content); - Assert.Contains($"