diff --git a/src/Components/Endpoints/src/Builder/RazorComponentEndpointFactory.cs b/src/Components/Endpoints/src/Builder/RazorComponentEndpointFactory.cs index 117aaea090f3..8f79f6c191b8 100644 --- a/src/Components/Endpoints/src/Builder/RazorComponentEndpointFactory.cs +++ b/src/Components/Endpoints/src/Builder/RazorComponentEndpointFactory.cs @@ -51,6 +51,12 @@ internal void AddEndpoints( builder.Metadata.Add(new RootComponentMetadata(rootComponent)); builder.Metadata.Add(configuredRenderModesMetadata); + builder.RequestDelegate = static httpContext => + { + var invoker = httpContext.RequestServices.GetRequiredService(); + return invoker.Render(httpContext); + }; + foreach (var convention in conventions) { convention(builder); @@ -67,12 +73,6 @@ internal void AddEndpoints( // The display name is for debug purposes by endpoint routing. builder.DisplayName = $"{builder.RoutePattern.RawText} ({pageDefinition.DisplayName})"; - builder.RequestDelegate = httpContext => - { - var invoker = httpContext.RequestServices.GetRequiredService(); - return invoker.Render(httpContext); - }; - endpoints.Add(builder.Build()); } } diff --git a/src/Components/Server/src/Builder/ComponentEndpointRouteBuilderExtensions.cs b/src/Components/Server/src/Builder/ComponentEndpointRouteBuilderExtensions.cs index 2ce561133ef4..0173fda4315d 100644 --- a/src/Components/Server/src/Builder/ComponentEndpointRouteBuilderExtensions.cs +++ b/src/Components/Server/src/Builder/ComponentEndpointRouteBuilderExtensions.cs @@ -115,7 +115,7 @@ private static IEndpointConventionBuilder GetBlazorEndpoint(IEndpointRouteBuilde .WithDisplayName("Blazor static files"); blazorEndpoint.Add((builder) => ((RouteEndpointBuilder)builder).Order = int.MinValue); - + #if DEBUG // We only need to serve the sourcemap when working on the framework, not in the distributed packages endpoints.Map("/_framework/blazor.server.js.map", app.Build()) diff --git a/src/Components/Server/src/Builder/InternalServerRenderMode.cs b/src/Components/Server/src/Builder/InternalServerRenderMode.cs index 4bff43cca838..cd89bccacfc1 100644 --- a/src/Components/Server/src/Builder/InternalServerRenderMode.cs +++ b/src/Components/Server/src/Builder/InternalServerRenderMode.cs @@ -1,10 +1,11 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Components.Web; -namespace Microsoft.AspNetCore.Builder; +namespace Microsoft.AspNetCore.Components.Server; -internal class InternalServerRenderMode : InteractiveServerRenderMode +internal class InternalServerRenderMode(ServerComponentsEndpointOptions options) : InteractiveServerRenderMode { + public ServerComponentsEndpointOptions? Options { get; } = options; } diff --git a/src/Components/Server/src/Builder/ServerComponentsEndpointOptions.cs b/src/Components/Server/src/Builder/ServerComponentsEndpointOptions.cs new file mode 100644 index 000000000000..f77fd8cd6d66 --- /dev/null +++ b/src/Components/Server/src/Builder/ServerComponentsEndpointOptions.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Components.Server; + +/// +/// Options to configure interactive Server components. +/// +public class ServerComponentsEndpointOptions +{ + /// + /// Gets or sets the frame-ancestors Content-Security-Policy to set in the + /// when is set. + /// + /// + /// Setting this value to will prevent the policy from being + /// automatically applied, which might make the app vulnerable. Care must be taken to apply + /// a policy in this case whenever the first document is rendered. + /// + /// + /// A content security policy provides defense against security threats that can occur if + /// the app uses compression and can be embedded in other origins. When compression is enabled, + /// embedding the app inside an iframe from other origins is prohibited. + /// + /// + /// For more details see the security recommendations for Interactive Server Components in + /// the official documentation. + /// + /// + public string? ContentSecurityFrameAncestorPolicy { get; set; } = "'self'"; + + /// + /// Gets or sets a function to configure the for the websocket connections + /// used by the server components. + /// By default, a policy that enables compression and sets a Content Security Policy for the frame ancestors + /// defined in will be applied. + /// + public Func? ConfigureWebsocketOptions { get; set; } = EnableCompressionDefaults; + + private static WebSocketAcceptContext EnableCompressionDefaults(HttpContext context) => + new() { DangerousEnableCompression = true }; +} diff --git a/src/Components/Server/src/Builder/ServerRazorComponentsEndpointConventionBuilderExtensions.cs b/src/Components/Server/src/Builder/ServerRazorComponentsEndpointConventionBuilderExtensions.cs index 90ec6aea2e0f..e51496b05105 100644 --- a/src/Components/Server/src/Builder/ServerRazorComponentsEndpointConventionBuilderExtensions.cs +++ b/src/Components/Server/src/Builder/ServerRazorComponentsEndpointConventionBuilderExtensions.cs @@ -1,8 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.AspNetCore.Components.Endpoints; using Microsoft.AspNetCore.Components.Endpoints.Infrastructure; +using Microsoft.AspNetCore.Components.Server; using Microsoft.AspNetCore.Components.Web; +using Microsoft.AspNetCore.SignalR; namespace Microsoft.AspNetCore.Builder; @@ -17,7 +20,44 @@ public static class ServerRazorComponentsEndpointConventionBuilderExtensions /// The . public static RazorComponentsEndpointConventionBuilder AddInteractiveServerRenderMode(this RazorComponentsEndpointConventionBuilder builder) { - ComponentEndpointConventionBuilderHelper.AddRenderMode(builder, new InternalServerRenderMode()); + return AddInteractiveServerRenderMode(builder, null); + } + + /// + /// Maps the Blazor to the default path. + /// + /// The . + /// A callback to configure server endpoint options. + /// The . + public static RazorComponentsEndpointConventionBuilder AddInteractiveServerRenderMode( + this RazorComponentsEndpointConventionBuilder builder, + Action? callback = null) + { + var options = new ServerComponentsEndpointOptions(); + callback?.Invoke(options); + + ComponentEndpointConventionBuilderHelper.AddRenderMode(builder, new InternalServerRenderMode(options)); + + if (options.ConfigureWebsocketOptions is not null && options.ContentSecurityFrameAncestorPolicy != null) + { + builder.Add(b => + { + for (var i = 0; i < b.Metadata.Count; i++) + { + var metadata = b.Metadata[i]; + if (metadata is ComponentTypeMetadata) + { + var original = b.RequestDelegate; + b.RequestDelegate = async context => + { + context.Response.Headers.Add("Content-Security-Policy", $"frame-ancestors {options.ContentSecurityFrameAncestorPolicy}"); + await original(context); + }; + } + } + }); + } + return builder; } } diff --git a/src/Components/Server/src/DependencyInjection/ServerRazorComponentsBuilderExtensions.cs b/src/Components/Server/src/DependencyInjection/ServerRazorComponentsBuilderExtensions.cs index 4a52c20e7d6e..5f30a0997719 100644 --- a/src/Components/Server/src/DependencyInjection/ServerRazorComponentsBuilderExtensions.cs +++ b/src/Components/Server/src/DependencyInjection/ServerRazorComponentsBuilderExtensions.cs @@ -2,12 +2,17 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.CodeAnalysis; +using System.Net.WebSockets; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Endpoints.Infrastructure; using Microsoft.AspNetCore.Components.Server; using Microsoft.AspNetCore.Components.Web; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Connections; +using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.DependencyInjection.Extensions; namespace Microsoft.Extensions.DependencyInjection; @@ -60,11 +65,46 @@ public override IEnumerable GetEndpointBuilders( throw new InvalidOperationException("Invalid render mode. Use AddInteractiveServerRenderMode() to configure the Server render mode."); } - return Array.Empty(); + return []; } var endpointRouteBuilder = new EndpointRouteBuilder(Services, applicationBuilder); - endpointRouteBuilder.MapBlazorHub(); + var hub = endpointRouteBuilder.MapBlazorHub("/_blazor"); + + if (renderMode is InternalServerRenderMode { Options.ConfigureWebsocketOptions: { } configureConnection }) + { + hub.Finally(c => + { + for (var i = 0; i < c.Metadata.Count; i++) + { + var metadata = c.Metadata[i]; + if (metadata is NegotiateMetadata) + { + return; + } + + if (metadata is HubMetadata) + { + var originalDelegate = c.RequestDelegate; + var builder = endpointRouteBuilder.CreateApplicationBuilder(); + builder.UseWebSockets(); + builder.Use(static (ctx, nxt) => + { + if (ctx.WebSockets.IsWebSocketRequest) + { + var currentFeature = ctx.Features.Get(); + + ctx.Features.Set(new ServerComponentsSocketFeature(currentFeature!)); + } + return nxt(ctx); + }); + builder.Run(originalDelegate); + c.RequestDelegate = builder.Build(); + return; + } + } + }); + } return endpointRouteBuilder.GetEndpoints(); } @@ -115,6 +155,18 @@ internal IEnumerable GetEndpoints() } } } + + } + + private sealed class ServerComponentsSocketFeature(IHttpWebSocketFeature originalFeature) : IHttpWebSocketFeature + { + public bool IsWebSocketRequest => originalFeature.IsWebSocketRequest; + + public Task AcceptAsync(WebSocketAcceptContext context) + { + context.DangerousEnableCompression = true; + return originalFeature.AcceptAsync(context); + } } } } diff --git a/src/Components/Server/src/PublicAPI.Unshipped.txt b/src/Components/Server/src/PublicAPI.Unshipped.txt index 7dc5c58110bf..6a7c825f9e13 100644 --- a/src/Components/Server/src/PublicAPI.Unshipped.txt +++ b/src/Components/Server/src/PublicAPI.Unshipped.txt @@ -1 +1,8 @@ #nullable enable +Microsoft.AspNetCore.Components.Server.ServerComponentsEndpointOptions +Microsoft.AspNetCore.Components.Server.ServerComponentsEndpointOptions.ConfigureWebsocketOptions.get -> System.Func? +Microsoft.AspNetCore.Components.Server.ServerComponentsEndpointOptions.ConfigureWebsocketOptions.set -> void +Microsoft.AspNetCore.Components.Server.ServerComponentsEndpointOptions.ContentSecurityFrameAncestorPolicy.get -> string? +Microsoft.AspNetCore.Components.Server.ServerComponentsEndpointOptions.ContentSecurityFrameAncestorPolicy.set -> void +Microsoft.AspNetCore.Components.Server.ServerComponentsEndpointOptions.ServerComponentsEndpointOptions() -> void +static Microsoft.AspNetCore.Builder.ServerRazorComponentsEndpointConventionBuilderExtensions.AddInteractiveServerRenderMode(this Microsoft.AspNetCore.Builder.RazorComponentsEndpointConventionBuilder! builder, System.Action? callback = null) -> Microsoft.AspNetCore.Builder.RazorComponentsEndpointConventionBuilder! diff --git a/src/Components/test/E2ETest/Infrastructure/ServerFixtures/AspNetSiteServerFixture.cs b/src/Components/test/E2ETest/Infrastructure/ServerFixtures/AspNetSiteServerFixture.cs index 5bd6846cf3b9..e39b3b8ffcf5 100644 --- a/src/Components/test/E2ETest/Infrastructure/ServerFixtures/AspNetSiteServerFixture.cs +++ b/src/Components/test/E2ETest/Infrastructure/ServerFixtures/AspNetSiteServerFixture.cs @@ -3,6 +3,7 @@ using System.Reflection; using Microsoft.AspNetCore.E2ETesting; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; namespace Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; @@ -17,6 +18,8 @@ public class AspNetSiteServerFixture : WebHostServerFixture public BuildWebHost BuildWebHostMethod { get; set; } + public Action UpdateHostServices { get; set; } + public GetContentRoot GetContentRootMethod { get; set; } = DefaultGetContentRoot; public AspNetEnvironment Environment { get; set; } = AspNetEnvironment.Production; @@ -40,12 +43,16 @@ protected override IHost CreateWebHost() host = E2ETestOptions.Instance.Sauce.HostName; } - return BuildWebHostMethod(new[] + var result = BuildWebHostMethod(new[] { "--urls", $"http://{host}:0", "--contentroot", sampleSitePath, "--environment", Environment.ToString(), }.Concat(AdditionalArguments).ToArray()); + + UpdateHostServices?.Invoke(result.Services); + + return result; } private static string DefaultGetContentRoot(Assembly assembly) diff --git a/src/Components/test/E2ETest/ServerExecutionTests/WebSocketCompressionTests.cs b/src/Components/test/E2ETest/ServerExecutionTests/WebSocketCompressionTests.cs new file mode 100644 index 000000000000..a2bf52f68956 --- /dev/null +++ b/src/Components/test/E2ETest/ServerExecutionTests/WebSocketCompressionTests.cs @@ -0,0 +1,116 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.RegularExpressions; +using Components.TestServer.RazorComponents; +using Microsoft.AspNetCore.Components.E2ETest.Infrastructure; +using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; +using Microsoft.AspNetCore.E2ETesting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.VisualStudio.TestPlatform.Utilities; +using OpenQA.Selenium; +using TestServer; +using Xunit.Abstractions; + +namespace Microsoft.AspNetCore.Components.E2ETests.ServerExecutionTests; + +public abstract partial class AllowedWebSocketCompressionTests( + BrowserFixture browserFixture, + BasicTestAppServerSiteFixture> serverFixture, + ITestOutputHelper output) + : ServerTestBase>>(browserFixture, serverFixture, output) +{ + [Fact] + public void EmbeddingServerAppInsideIframe_Works() + { + Navigate("/subdir/iframe"); + + var logs = Browser.GetBrowserLogs(LogLevel.Severe); + + Assert.Empty(logs); + + // Get the iframe element from the page, and inspect its contents for a p element with id inside-iframe + var iframe = Browser.FindElement(By.TagName("iframe")); + Browser.SwitchTo().Frame(iframe); + Browser.Exists(By.Id("inside-iframe")); + } +} + +public abstract partial class BlockedWebSocketCompressionTests( + BrowserFixture browserFixture, + BasicTestAppServerSiteFixture> serverFixture, + ITestOutputHelper output) + : ServerTestBase>>(browserFixture, serverFixture, output) +{ + [Fact] + public void EmbeddingServerAppInsideIframe_WithCompressionEnabled_Fails() + { + Navigate("/subdir/iframe"); + + var logs = Browser.GetBrowserLogs(LogLevel.Severe); + + Assert.True(logs.Count > 0); + + Assert.Matches(ParseErrorMessage(), logs[0].Message); + } + + [GeneratedRegex(@"security - Refused to frame 'http://\d+\.\d+\.\d+\.\d+:\d+/' because an ancestor violates the following Content Security Policy directive: ""frame-ancestors 'none'"".")] + private static partial Regex ParseErrorMessage(); +} + +public partial class DefaultConfigurationWebSocketCompressionTests( + BrowserFixture browserFixture, + BasicTestAppServerSiteFixture> serverFixture, + ITestOutputHelper output) + : AllowedWebSocketCompressionTests(browserFixture, serverFixture, output) +{ +} + +public partial class CustomConfigurationCallbackWebSocketCompressionTests : AllowedWebSocketCompressionTests +{ + public CustomConfigurationCallbackWebSocketCompressionTests( + BrowserFixture browserFixture, + BasicTestAppServerSiteFixture> serverFixture, + ITestOutputHelper output) : base(browserFixture, serverFixture, output) + { + serverFixture.UpdateHostServices = services => + { + var configuration = services.GetService(); + configuration.ConnectionDispatcherOptions = context => new() { DangerousEnableCompression = true }; + }; + } +} + +public partial class CompressionDisabledWebSocketCompressionTests : AllowedWebSocketCompressionTests +{ + public CompressionDisabledWebSocketCompressionTests( + BrowserFixture browserFixture, + BasicTestAppServerSiteFixture> serverFixture, + ITestOutputHelper output) : base( + browserFixture, serverFixture, output) + { + serverFixture.UpdateHostServices = services => + { + var configuration = services.GetService(); + configuration.IsCompressionEnabled = false; + configuration.ConnectionDispatcherOptions = null; + }; + } +} + +public partial class NoneAncestorWebSocketCompressionTests : BlockedWebSocketCompressionTests +{ + public NoneAncestorWebSocketCompressionTests( + BrowserFixture browserFixture, + BasicTestAppServerSiteFixture> serverFixture, + ITestOutputHelper output) + : base(browserFixture, serverFixture, output) + { + serverFixture.UpdateHostServices = services => + { + var configuration = services.GetService(); + configuration.CspPolicy = "'none'"; + }; + } +} + diff --git a/src/Components/test/testassets/Components.TestServer/AuthenticationStartup.cs b/src/Components/test/testassets/Components.TestServer/AuthenticationStartup.cs index aa4557072b65..f92842b82d1b 100644 --- a/src/Components/test/testassets/Components.TestServer/AuthenticationStartup.cs +++ b/src/Components/test/testassets/Components.TestServer/AuthenticationStartup.cs @@ -2,7 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Globalization; +using System.Net.WebSockets; using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Http.Features; namespace TestServer; @@ -54,15 +56,37 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseStaticFiles(); app.UseRouting(); + app.UseWebSockets(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); endpoints.MapRazorPages(); - endpoints.MapBlazorHub(); + endpoints.MapBlazorHub() + .AddEndpointFilter(async (context, next) => + { + if (context.HttpContext.WebSockets.IsWebSocketRequest) + { + var currentFeature = context.HttpContext.Features.Get(); + + context.HttpContext.Features.Set(new ServerComponentsSocketFeature(currentFeature!)); + } + return await next(context); + }); _configureMode(endpoints); }); }); } + + private sealed class ServerComponentsSocketFeature(IHttpWebSocketFeature originalFeature) : IHttpWebSocketFeature + { + public bool IsWebSocketRequest => originalFeature.IsWebSocketRequest; + + public Task AcceptAsync(WebSocketAcceptContext context) + { + context.DangerousEnableCompression = true; + return originalFeature.AcceptAsync(context); + } + } } public class AuthenticationStartup : AuthenticationStartupBase diff --git a/src/Components/test/testassets/Components.TestServer/DeferredComponentContentStartup.cs b/src/Components/test/testassets/Components.TestServer/DeferredComponentContentStartup.cs index c8568d263905..ae1e9dce2e65 100644 --- a/src/Components/test/testassets/Components.TestServer/DeferredComponentContentStartup.cs +++ b/src/Components/test/testassets/Components.TestServer/DeferredComponentContentStartup.cs @@ -2,7 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Globalization; +using System.Net.WebSockets; using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Http.Features; namespace TestServer; @@ -35,12 +37,31 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseAuthentication(); app.UseRouting(); + app.UseWebSockets(); app.UseEndpoints(endpoints => { endpoints.MapRazorPages(); endpoints.MapFallbackToPage("/DeferredComponentContentHost"); - endpoints.MapBlazorHub(); + endpoints.MapBlazorHub().AddEndpointFilter(async (context, next) => + { + if (context.HttpContext.WebSockets.IsWebSocketRequest) + { + var currentFeature = context.HttpContext.Features.Get(); context.HttpContext.Features.Set(new ServerComponentsSocketFeature(currentFeature!)); + } + return await next(context); + }); }); }); } + + private sealed class ServerComponentsSocketFeature(IHttpWebSocketFeature originalFeature) : IHttpWebSocketFeature + { + public bool IsWebSocketRequest => originalFeature.IsWebSocketRequest; + + public Task AcceptAsync(WebSocketAcceptContext context) + { + context.DangerousEnableCompression = true; + return originalFeature.AcceptAsync(context); + } + } } diff --git a/src/Components/test/testassets/Components.TestServer/HotReloadStartup.cs b/src/Components/test/testassets/Components.TestServer/HotReloadStartup.cs index 960e534d5c4f..d9b275f5851a 100644 --- a/src/Components/test/testassets/Components.TestServer/HotReloadStartup.cs +++ b/src/Components/test/testassets/Components.TestServer/HotReloadStartup.cs @@ -2,7 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Globalization; +using System.Net.WebSockets; using Microsoft.AspNetCore.Components.HotReload; +using Microsoft.AspNetCore.Http.Features; namespace TestServer; @@ -34,11 +36,30 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseBlazorFrameworkFiles(); app.UseStaticFiles(); app.UseRouting(); + app.UseWebSockets(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); - endpoints.MapBlazorHub(); + endpoints.MapBlazorHub().AddEndpointFilter(async (context, next) => + { + if (context.HttpContext.WebSockets.IsWebSocketRequest) + { + var currentFeature = context.HttpContext.Features.Get(); context.HttpContext.Features.Set(new ServerComponentsSocketFeature(currentFeature!)); + } + return await next(context); + }); endpoints.MapFallbackToPage("/_ServerHost"); }); } + + private sealed class ServerComponentsSocketFeature(IHttpWebSocketFeature originalFeature) : IHttpWebSocketFeature + { + public bool IsWebSocketRequest => originalFeature.IsWebSocketRequest; + + public Task AcceptAsync(WebSocketAcceptContext context) + { + context.DangerousEnableCompression = true; + return originalFeature.AcceptAsync(context); + } + } } diff --git a/src/Components/test/testassets/Components.TestServer/InternationalizationStartup.cs b/src/Components/test/testassets/Components.TestServer/InternationalizationStartup.cs index 5cac7343ec15..2ed1cfdb3ff2 100644 --- a/src/Components/test/testassets/Components.TestServer/InternationalizationStartup.cs +++ b/src/Components/test/testassets/Components.TestServer/InternationalizationStartup.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.AspNetCore.Http.Features; +using System.Net.WebSockets; using Microsoft.AspNetCore.Localization; namespace TestServer; @@ -49,12 +51,31 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) }); app.UseRouting(); + app.UseWebSockets(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); - endpoints.MapBlazorHub(); + endpoints.MapBlazorHub().AddEndpointFilter(async (context, next) => + { + if (context.HttpContext.WebSockets.IsWebSocketRequest) + { + var currentFeature = context.HttpContext.Features.Get(); context.HttpContext.Features.Set(new ServerComponentsSocketFeature(currentFeature!)); + } + return await next(context); + }); endpoints.MapFallbackToPage("/_ServerHost"); }); }); } + + private sealed class ServerComponentsSocketFeature(IHttpWebSocketFeature originalFeature) : IHttpWebSocketFeature + { + public bool IsWebSocketRequest => originalFeature.IsWebSocketRequest; + + public Task AcceptAsync(WebSocketAcceptContext context) + { + context.DangerousEnableCompression = true; + return originalFeature.AcceptAsync(context); + } + } } diff --git a/src/Components/test/testassets/Components.TestServer/LockedNavigationStartup.cs b/src/Components/test/testassets/Components.TestServer/LockedNavigationStartup.cs index 13a3c95fc053..490e24e7a3c1 100644 --- a/src/Components/test/testassets/Components.TestServer/LockedNavigationStartup.cs +++ b/src/Components/test/testassets/Components.TestServer/LockedNavigationStartup.cs @@ -2,7 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Globalization; +using System.Net.WebSockets; using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Http.Features; namespace TestServer; @@ -35,12 +37,31 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseAuthentication(); app.UseRouting(); + app.UseWebSockets(); app.UseEndpoints(endpoints => { endpoints.MapRazorPages(); endpoints.MapFallbackToPage("/LockedNavigationHost"); - endpoints.MapBlazorHub(); + endpoints.MapBlazorHub().AddEndpointFilter(async (context, next) => + { + if (context.HttpContext.WebSockets.IsWebSocketRequest) + { + var currentFeature = context.HttpContext.Features.Get(); context.HttpContext.Features.Set(new ServerComponentsSocketFeature(currentFeature!)); + } + return await next(context); + }); }); }); } + + private sealed class ServerComponentsSocketFeature(IHttpWebSocketFeature originalFeature) : IHttpWebSocketFeature + { + public bool IsWebSocketRequest => originalFeature.IsWebSocketRequest; + + public Task AcceptAsync(WebSocketAcceptContext context) + { + context.DangerousEnableCompression = true; + return originalFeature.AcceptAsync(context); + } + } } diff --git a/src/Components/test/testassets/Components.TestServer/MultipleComponents.cs b/src/Components/test/testassets/Components.TestServer/MultipleComponents.cs index ad43d536e0bf..1069b6e8df65 100644 --- a/src/Components/test/testassets/Components.TestServer/MultipleComponents.cs +++ b/src/Components/test/testassets/Components.TestServer/MultipleComponents.cs @@ -2,7 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Globalization; +using System.Net.WebSockets; using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Http.Features; namespace TestServer; @@ -54,12 +56,31 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseAuthentication(); app.UseRouting(); + app.UseWebSockets(); app.UseEndpoints(endpoints => { endpoints.MapRazorPages(); endpoints.MapFallbackToPage("/MultipleComponents"); - endpoints.MapBlazorHub(); + endpoints.MapBlazorHub().AddEndpointFilter(async (context, next) => + { + if (context.HttpContext.WebSockets.IsWebSocketRequest) + { + var currentFeature = context.HttpContext.Features.Get(); context.HttpContext.Features.Set(new ServerComponentsSocketFeature(currentFeature!)); + } + return await next(context); + }); }); }); } + + private sealed class ServerComponentsSocketFeature(IHttpWebSocketFeature originalFeature) : IHttpWebSocketFeature + { + public bool IsWebSocketRequest => originalFeature.IsWebSocketRequest; + + public Task AcceptAsync(WebSocketAcceptContext context) + { + context.DangerousEnableCompression = true; + return originalFeature.AcceptAsync(context); + } + } } diff --git a/src/Components/test/testassets/Components.TestServer/PrerenderedStartup.cs b/src/Components/test/testassets/Components.TestServer/PrerenderedStartup.cs index 0d436f8e443e..e0c126125526 100644 --- a/src/Components/test/testassets/Components.TestServer/PrerenderedStartup.cs +++ b/src/Components/test/testassets/Components.TestServer/PrerenderedStartup.cs @@ -2,8 +2,10 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Globalization; +using System.Net.WebSockets; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Components.WebAssembly.Services; +using Microsoft.AspNetCore.Http.Features; namespace TestServer; @@ -44,12 +46,31 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseAuthentication(); app.UseRouting(); + app.UseWebSockets(); app.UseEndpoints(endpoints => { endpoints.MapRazorPages(); endpoints.MapFallbackToPage("/PrerenderedHost"); - endpoints.MapBlazorHub(); + endpoints.MapBlazorHub().AddEndpointFilter(async (context, next) => + { + if (context.HttpContext.WebSockets.IsWebSocketRequest) + { + var currentFeature = context.HttpContext.Features.Get(); context.HttpContext.Features.Set(new ServerComponentsSocketFeature(currentFeature!)); + } + return await next(context); + }); }); }); } + + private sealed class ServerComponentsSocketFeature(IHttpWebSocketFeature originalFeature) : IHttpWebSocketFeature + { + public bool IsWebSocketRequest => originalFeature.IsWebSocketRequest; + + public Task AcceptAsync(WebSocketAcceptContext context) + { + context.DangerousEnableCompression = true; + return originalFeature.AcceptAsync(context); + } + } } diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs b/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs index be68ef2ffd7e..d6d8cdd6c0c7 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs +++ b/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs @@ -36,6 +36,7 @@ public void ConfigureServices(IServiceCollection services) services.AddHttpContextAccessor(); services.AddSingleton(); services.AddCascadingAuthenticationState(); + services.AddSingleton(); var circuitContextAccessor = new TestCircuitContextAccessor(); services.AddSingleton(circuitContextAccessor); @@ -65,11 +66,21 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseRouting(); UseFakeAuthState(app); app.UseAntiforgery(); - app.UseEndpoints(endpoints => + _ = app.UseEndpoints(endpoints => { - endpoints.MapRazorComponents() + _ = endpoints.MapRazorComponents() .AddAdditionalAssemblies(Assembly.Load("Components.WasmMinimal")) - .AddInteractiveServerRenderMode() + .AddInteractiveServerRenderMode(options => + { + var config = app.ApplicationServices.GetRequiredService(); + options.ConfigureWebsocketOptions = config.IsCompressionEnabled ? + _ => new() { DangerousEnableCompression = true } : null; + + options.ContentSecurityFrameAncestorPolicy = config.CspPolicy; + + options.ConfigureWebsocketOptions = config.ConnectionDispatcherOptions ?? (config.IsCompressionEnabled ? + (context) => new() { DangerousEnableCompression = true } : null); + }) .AddInteractiveWebAssemblyRenderMode(options => options.PathPrefix = "/WasmMinimal"); NotEnabledStreamingRenderingComponent.MapEndpoints(endpoints); diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/CanNotEmbedAppInsideIFrameWhenUsingCompression.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/CanNotEmbedAppInsideIFrameWhenUsingCompression.razor new file mode 100644 index 000000000000..99fe7c2646e2 --- /dev/null +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/CanNotEmbedAppInsideIFrameWhenUsingCompression.razor @@ -0,0 +1,3 @@ +@page "/iframe" + + diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/EmbeddedInsideIFrame.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/EmbeddedInsideIFrame.razor new file mode 100644 index 000000000000..fbb1940670b6 --- /dev/null +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/EmbeddedInsideIFrame.razor @@ -0,0 +1,4 @@ +@page "/embedded" +@rendermode Microsoft.AspNetCore.Components.Web.RenderMode.InteractiveServer + +

This is some content embedded inside an iframe

diff --git a/src/Components/test/testassets/Components.TestServer/SaveState.cs b/src/Components/test/testassets/Components.TestServer/SaveState.cs index d2105487a9f2..b5bb7819fbd2 100644 --- a/src/Components/test/testassets/Components.TestServer/SaveState.cs +++ b/src/Components/test/testassets/Components.TestServer/SaveState.cs @@ -2,7 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Globalization; +using System.Net.WebSockets; using BasicTestApp; +using Microsoft.AspNetCore.Http.Features; namespace TestServer; @@ -38,11 +40,34 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseBlazorFrameworkFiles(); app.UseStaticFiles(); app.UseRouting(); + app.UseWebSockets(); app.UseEndpoints(endpoints => { endpoints.MapRazorPages(); - endpoints.MapBlazorHub(); + endpoints.MapBlazorHub() + .AddEndpointFilter(async (context, next) => + { + if (context.HttpContext.WebSockets.IsWebSocketRequest) + { + var currentFeature = context.HttpContext.Features.Get(); + + context.HttpContext.Features.Set(new ServerComponentsSocketFeature(currentFeature!)); + } + return await next(context); + }); + endpoints.MapFallbackToPage("/SaveState"); }); } + + private sealed class ServerComponentsSocketFeature(IHttpWebSocketFeature originalFeature) : IHttpWebSocketFeature + { + public bool IsWebSocketRequest => originalFeature.IsWebSocketRequest; + + public Task AcceptAsync(WebSocketAcceptContext context) + { + context.DangerousEnableCompression = true; + return originalFeature.AcceptAsync(context); + } + } } diff --git a/src/Components/test/testassets/Components.TestServer/ServerStartup.cs b/src/Components/test/testassets/Components.TestServer/ServerStartup.cs index 9ad6efaf6cf6..ca1db15a993c 100644 --- a/src/Components/test/testassets/Components.TestServer/ServerStartup.cs +++ b/src/Components/test/testassets/Components.TestServer/ServerStartup.cs @@ -2,9 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Globalization; +using System.Net.WebSockets; using Microsoft.AspNetCore.Components.Server.Circuits; using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.Http.Features; namespace TestServer; @@ -76,12 +78,34 @@ public virtual void Configure(IApplicationBuilder app, IWebHostEnvironment env, app.UseStaticFiles(); app.UseRouting(); + app.UseWebSockets(); app.UseEndpoints(endpoints => { - endpoints.MapBlazorHub(); + endpoints.MapBlazorHub() + .AddEndpointFilter(async (context, next) => + { + if (context.HttpContext.WebSockets.IsWebSocketRequest) + { + var currentFeature = context.HttpContext.Features.Get(); + + context.HttpContext.Features.Set(new ServerComponentsSocketFeature(currentFeature!)); + } + return await next(context); + }); endpoints.MapControllerRoute("mvc", "{controller}/{action}"); endpoints.MapFallbackToPage("/_ServerHost"); }); }); } + + private sealed class ServerComponentsSocketFeature(IHttpWebSocketFeature originalFeature) : IHttpWebSocketFeature + { + public bool IsWebSocketRequest => originalFeature.IsWebSocketRequest; + + public Task AcceptAsync(WebSocketAcceptContext context) + { + context.DangerousEnableCompression = true; + return originalFeature.AcceptAsync(context); + } + } } diff --git a/src/Components/test/testassets/Components.TestServer/TransportsServerStartup.cs b/src/Components/test/testassets/Components.TestServer/TransportsServerStartup.cs index a3088dfb61ed..f76cf3073ed7 100644 --- a/src/Components/test/testassets/Components.TestServer/TransportsServerStartup.cs +++ b/src/Components/test/testassets/Components.TestServer/TransportsServerStartup.cs @@ -1,6 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Net.WebSockets; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.SignalR; + namespace TestServer; public class TransportsServerStartup : ServerStartup @@ -23,9 +27,20 @@ public override void Configure(IApplicationBuilder app, IWebHostEnvironment env, app.UseStaticFiles(); app.UseRouting(); + app.UseWebSockets(); app.UseEndpoints(endpoints => { - endpoints.MapBlazorHub(); + endpoints.MapBlazorHub() + .AddEndpointFilter(async (context, next) => + { + if (context.HttpContext.WebSockets.IsWebSocketRequest) + { + var currentFeature = context.HttpContext.Features.Get(); + + context.HttpContext.Features.Set(new ServerComponentsSocketFeature(currentFeature!)); + } + return await next(context); + }); endpoints.MapFallbackToPage("/_ServerHost"); }); }); @@ -50,14 +65,35 @@ public override void Configure(IApplicationBuilder app, IWebHostEnvironment env, app.UseStaticFiles(); app.UseRouting(); + app.UseWebSockets(); app.UseEndpoints(endpoints => { endpoints.MapBlazorHub(configureOptions: options => { options.Transports = Microsoft.AspNetCore.Http.Connections.HttpTransportType.WebSockets; - }); + }).AddEndpointFilter(async (context, next) => + { + if (context.HttpContext.WebSockets.IsWebSocketRequest) + { + var currentFeature = context.HttpContext.Features.Get(); + + context.HttpContext.Features.Set(new ServerComponentsSocketFeature(currentFeature!)); + } + return await next(context); + }); endpoints.MapFallbackToPage("/_ServerHost"); }); }); } + + private sealed class ServerComponentsSocketFeature(IHttpWebSocketFeature originalFeature) : IHttpWebSocketFeature + { + public bool IsWebSocketRequest => originalFeature.IsWebSocketRequest; + + public Task AcceptAsync(WebSocketAcceptContext context) + { + context.DangerousEnableCompression = true; + return originalFeature.AcceptAsync(context); + } + } } diff --git a/src/Components/test/testassets/Components.TestServer/WebSocketCompressionConfiguration.cs b/src/Components/test/testassets/Components.TestServer/WebSocketCompressionConfiguration.cs new file mode 100644 index 000000000000..e6b0344e83ed --- /dev/null +++ b/src/Components/test/testassets/Components.TestServer/WebSocketCompressionConfiguration.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Http.Connections; + +namespace TestServer; + +public class WebSocketCompressionConfiguration +{ + public bool IsCompressionEnabled { get; set; } = true; + + public string CspPolicy { get; set; } = "'self'"; + + public Func ConnectionDispatcherOptions { get; set; } = (context) => new WebSocketAcceptContext + { + DangerousEnableCompression = true + }; +}