From 4f554454b17aa159688240f69ef2ea7d41bb3540 Mon Sep 17 00:00:00 2001 From: jacalvar Date: Fri, 13 May 2022 17:33:27 +0200 Subject: [PATCH 01/18] [Blazor][SignalR] Proof of concept. Enabling compression in websockets --- src/Components/Samples/BlazorServerApp/Startup.cs | 2 +- .../src/Internal/Transports/WebSocketsServerTransport.cs | 3 ++- .../common/Http.Connections/src/PublicAPI.Unshipped.txt | 2 ++ src/SignalR/common/Http.Connections/src/WebSocketOptions.cs | 6 ++++++ 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/Components/Samples/BlazorServerApp/Startup.cs b/src/Components/Samples/BlazorServerApp/Startup.cs index d630a127c242..b4eb99bdc028 100644 --- a/src/Components/Samples/BlazorServerApp/Startup.cs +++ b/src/Components/Samples/BlazorServerApp/Startup.cs @@ -44,7 +44,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseEndpoints(endpoints => { - endpoints.MapBlazorHub(); + endpoints.MapBlazorHub(options => options.WebSockets.WebSocketAcceptContextFactory = context => new WebSocketAcceptContext { DangerousEnableCompression = true}); endpoints.MapFallbackToPage("/_Host"); }); } diff --git a/src/SignalR/common/Http.Connections/src/Internal/Transports/WebSocketsServerTransport.cs b/src/SignalR/common/Http.Connections/src/Internal/Transports/WebSocketsServerTransport.cs index bc1c7c93645e..e088f5088359 100644 --- a/src/SignalR/common/Http.Connections/src/Internal/Transports/WebSocketsServerTransport.cs +++ b/src/SignalR/common/Http.Connections/src/Internal/Transports/WebSocketsServerTransport.cs @@ -39,8 +39,9 @@ public async Task ProcessRequestAsync(HttpContext context, CancellationTok Debug.Assert(context.WebSockets.IsWebSocketRequest, "Not a websocket request"); var subProtocol = _options.SubProtocolSelector?.Invoke(context.WebSockets.WebSocketRequestedProtocols); + var acceptContext = _options.WebSocketAcceptContextFactory?.Invoke(context); - using (var ws = await context.WebSockets.AcceptWebSocketAsync(subProtocol)) + using (var ws = await (acceptContext != null ? context.WebSockets.AcceptWebSocketAsync(acceptContext) : context.WebSockets.AcceptWebSocketAsync(subProtocol))) { Log.SocketOpened(_logger, subProtocol); diff --git a/src/SignalR/common/Http.Connections/src/PublicAPI.Unshipped.txt b/src/SignalR/common/Http.Connections/src/PublicAPI.Unshipped.txt index 7dc5c58110bf..fde2e9049cdf 100644 --- a/src/SignalR/common/Http.Connections/src/PublicAPI.Unshipped.txt +++ b/src/SignalR/common/Http.Connections/src/PublicAPI.Unshipped.txt @@ -1 +1,3 @@ #nullable enable +Microsoft.AspNetCore.Http.Connections.WebSocketOptions.WebSocketAcceptContextFactory.get -> System.Func? +Microsoft.AspNetCore.Http.Connections.WebSocketOptions.WebSocketAcceptContextFactory.set -> void diff --git a/src/SignalR/common/Http.Connections/src/WebSocketOptions.cs b/src/SignalR/common/Http.Connections/src/WebSocketOptions.cs index 139aad4beb91..f542815e6475 100644 --- a/src/SignalR/common/Http.Connections/src/WebSocketOptions.cs +++ b/src/SignalR/common/Http.Connections/src/WebSocketOptions.cs @@ -26,4 +26,10 @@ public class WebSocketOptions // https://github.com/aspnet/HttpAbstractions/blob/a6bdb9b1ec6ed99978a508e71a7f131be7e4d9fb/src/Microsoft.AspNetCore.Http.Abstractions/WebSocketManager.cs#L23 // Unfortunately, IList does not implement IReadOnlyList :( public Func, string>? SubProtocolSelector { get; set; } + + /// + /// Gets or sets a delegate that will be called when a new WebSocket is established to configure additional details about the connection + /// like the use of compression. + /// + public Func? WebSocketAcceptContextFactory { get; set; } } From 2d8a2481ed8917cb44d7b63958da1345a74da1a7 Mon Sep 17 00:00:00 2001 From: jacalvar Date: Mon, 15 Jan 2024 15:09:58 +0100 Subject: [PATCH 02/18] Enable websocket compression --- .../Samples/BlazorServerApp/Startup.cs | 2 +- .../src/Builder/InternalServerRenderMode.cs | 9 +++++++-- ...onentsEndpointConventionBuilderExtensions.cs | 17 +++++++++++++++++ .../ServerRazorComponentsBuilderExtensions.cs | 10 +++++++++- .../Server/src/PublicAPI.Unshipped.txt | 1 + .../AuthenticationStartup.cs | 2 +- .../BlazorWebServerStartup.cs | 2 +- .../DeferredComponentContentStartup.cs | 2 +- .../Components.TestServer/HotReloadStartup.cs | 2 +- .../InternationalizationStartup.cs | 2 +- .../LockedNavigationStartup.cs | 2 +- .../Components.TestServer/MultipleComponents.cs | 2 +- .../Components.TestServer/PrerenderedStartup.cs | 2 +- .../Components.TestServer/SaveState.cs | 2 +- .../Components.TestServer/ServerStartup.cs | 2 +- .../TransportsServerStartup.cs | 2 +- .../testassets/ComponentsApp.Server/Startup.cs | 2 +- 17 files changed, 47 insertions(+), 16 deletions(-) diff --git a/src/Components/Samples/BlazorServerApp/Startup.cs b/src/Components/Samples/BlazorServerApp/Startup.cs index b4eb99bdc028..d630a127c242 100644 --- a/src/Components/Samples/BlazorServerApp/Startup.cs +++ b/src/Components/Samples/BlazorServerApp/Startup.cs @@ -44,7 +44,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseEndpoints(endpoints => { - endpoints.MapBlazorHub(options => options.WebSockets.WebSocketAcceptContextFactory = context => new WebSocketAcceptContext { DangerousEnableCompression = true}); + endpoints.MapBlazorHub(); endpoints.MapFallbackToPage("/_Host"); }); } diff --git a/src/Components/Server/src/Builder/InternalServerRenderMode.cs b/src/Components/Server/src/Builder/InternalServerRenderMode.cs index 4bff43cca838..a4bed6f6b9ad 100644 --- a/src/Components/Server/src/Builder/InternalServerRenderMode.cs +++ b/src/Components/Server/src/Builder/InternalServerRenderMode.cs @@ -1,10 +1,15 @@ -// 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. + +// 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; +using Microsoft.AspNetCore.Http.Connections; namespace Microsoft.AspNetCore.Builder; -internal class InternalServerRenderMode : InteractiveServerRenderMode +internal class InternalServerRenderMode(Action? configureOptions = null) : InteractiveServerRenderMode { + public Action? ConfigureConnectionOptions { get; } = configureOptions; } diff --git a/src/Components/Server/src/Builder/ServerRazorComponentsEndpointConventionBuilderExtensions.cs b/src/Components/Server/src/Builder/ServerRazorComponentsEndpointConventionBuilderExtensions.cs index 90ec6aea2e0f..7fbd79c436fb 100644 --- a/src/Components/Server/src/Builder/ServerRazorComponentsEndpointConventionBuilderExtensions.cs +++ b/src/Components/Server/src/Builder/ServerRazorComponentsEndpointConventionBuilderExtensions.cs @@ -3,6 +3,8 @@ using Microsoft.AspNetCore.Components.Endpoints.Infrastructure; using Microsoft.AspNetCore.Components.Web; +using Microsoft.AspNetCore.Http.Connections; +using Microsoft.AspNetCore.SignalR; namespace Microsoft.AspNetCore.Builder; @@ -20,4 +22,19 @@ public static RazorComponentsEndpointConventionBuilder AddInteractiveServerRende ComponentEndpointConventionBuilderHelper.AddRenderMode(builder, new InternalServerRenderMode()); return builder; } + + /// + ///Maps the Blazor to the default path. + /// + /// The . + /// A callback to configure dispatcher options. + /// The . + public static RazorComponentsEndpointConventionBuilder AddInteractiveServerRenderMode( + this RazorComponentsEndpointConventionBuilder builder, + Action configureOptions) + { + ArgumentNullException.ThrowIfNull(configureOptions); + ComponentEndpointConventionBuilderHelper.AddRenderMode(builder, new InternalServerRenderMode(configureOptions)); + return builder; + } } diff --git a/src/Components/Server/src/DependencyInjection/ServerRazorComponentsBuilderExtensions.cs b/src/Components/Server/src/DependencyInjection/ServerRazorComponentsBuilderExtensions.cs index 4a52c20e7d6e..6d6708773d01 100644 --- a/src/Components/Server/src/DependencyInjection/ServerRazorComponentsBuilderExtensions.cs +++ b/src/Components/Server/src/DependencyInjection/ServerRazorComponentsBuilderExtensions.cs @@ -64,7 +64,15 @@ public override IEnumerable GetEndpointBuilders( } var endpointRouteBuilder = new EndpointRouteBuilder(Services, applicationBuilder); - endpointRouteBuilder.MapBlazorHub(); + if (renderMode is InternalServerRenderMode { ConfigureConnectionOptions: var configureOptions } && + configureOptions != null) + { + endpointRouteBuilder.MapBlazorHub("/_blazor", configureOptions); + } + else + { + endpointRouteBuilder.MapBlazorHub(); + } return endpointRouteBuilder.GetEndpoints(); } diff --git a/src/Components/Server/src/PublicAPI.Unshipped.txt b/src/Components/Server/src/PublicAPI.Unshipped.txt index 7dc5c58110bf..066f325fe057 100644 --- a/src/Components/Server/src/PublicAPI.Unshipped.txt +++ b/src/Components/Server/src/PublicAPI.Unshipped.txt @@ -1 +1,2 @@ #nullable enable +static Microsoft.AspNetCore.Builder.ServerRazorComponentsEndpointConventionBuilderExtensions.AddInteractiveServerRenderMode(this Microsoft.AspNetCore.Builder.RazorComponentsEndpointConventionBuilder! builder, System.Action! configureOptions) -> Microsoft.AspNetCore.Builder.RazorComponentsEndpointConventionBuilder! diff --git a/src/Components/test/testassets/Components.TestServer/AuthenticationStartup.cs b/src/Components/test/testassets/Components.TestServer/AuthenticationStartup.cs index aa4557072b65..28818516e080 100644 --- a/src/Components/test/testassets/Components.TestServer/AuthenticationStartup.cs +++ b/src/Components/test/testassets/Components.TestServer/AuthenticationStartup.cs @@ -58,7 +58,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { endpoints.MapControllers(); endpoints.MapRazorPages(); - endpoints.MapBlazorHub(); + endpoints.MapBlazorHub(options => options.WebSockets.WebSocketAcceptContextFactory = context => new WebSocketAcceptContext { DangerousEnableCompression = true }); _configureMode(endpoints); }); }); diff --git a/src/Components/test/testassets/Components.TestServer/BlazorWebServerStartup.cs b/src/Components/test/testassets/Components.TestServer/BlazorWebServerStartup.cs index 5deffb0d94b1..65591d61bbd5 100644 --- a/src/Components/test/testassets/Components.TestServer/BlazorWebServerStartup.cs +++ b/src/Components/test/testassets/Components.TestServer/BlazorWebServerStartup.cs @@ -44,7 +44,7 @@ public virtual void Configure(IApplicationBuilder app, IWebHostEnvironment env, app.UseEndpoints(endpoints => { endpoints.MapRazorComponents() - .AddInteractiveServerRenderMode(); + .AddInteractiveServerRenderMode(options => options.WebSockets.WebSocketAcceptContextFactory = context => new WebSocketAcceptContext { DangerousEnableCompression = true }); }); }); } diff --git a/src/Components/test/testassets/Components.TestServer/DeferredComponentContentStartup.cs b/src/Components/test/testassets/Components.TestServer/DeferredComponentContentStartup.cs index c8568d263905..49beda07e299 100644 --- a/src/Components/test/testassets/Components.TestServer/DeferredComponentContentStartup.cs +++ b/src/Components/test/testassets/Components.TestServer/DeferredComponentContentStartup.cs @@ -39,7 +39,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { endpoints.MapRazorPages(); endpoints.MapFallbackToPage("/DeferredComponentContentHost"); - endpoints.MapBlazorHub(); + endpoints.MapBlazorHub(options => options.WebSockets.WebSocketAcceptContextFactory = context => new WebSocketAcceptContext { DangerousEnableCompression = true }); }); }); } diff --git a/src/Components/test/testassets/Components.TestServer/HotReloadStartup.cs b/src/Components/test/testassets/Components.TestServer/HotReloadStartup.cs index 960e534d5c4f..4ee59a692f84 100644 --- a/src/Components/test/testassets/Components.TestServer/HotReloadStartup.cs +++ b/src/Components/test/testassets/Components.TestServer/HotReloadStartup.cs @@ -37,7 +37,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseEndpoints(endpoints => { endpoints.MapControllers(); - endpoints.MapBlazorHub(); + endpoints.MapBlazorHub(options => options.WebSockets.WebSocketAcceptContextFactory = context => new WebSocketAcceptContext { DangerousEnableCompression = true }); endpoints.MapFallbackToPage("/_ServerHost"); }); } diff --git a/src/Components/test/testassets/Components.TestServer/InternationalizationStartup.cs b/src/Components/test/testassets/Components.TestServer/InternationalizationStartup.cs index 5cac7343ec15..c7e4e325cf7b 100644 --- a/src/Components/test/testassets/Components.TestServer/InternationalizationStartup.cs +++ b/src/Components/test/testassets/Components.TestServer/InternationalizationStartup.cs @@ -52,7 +52,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseEndpoints(endpoints => { endpoints.MapControllers(); - endpoints.MapBlazorHub(); + endpoints.MapBlazorHub(options => options.WebSockets.WebSocketAcceptContextFactory = context => new WebSocketAcceptContext { DangerousEnableCompression = true }); endpoints.MapFallbackToPage("/_ServerHost"); }); }); diff --git a/src/Components/test/testassets/Components.TestServer/LockedNavigationStartup.cs b/src/Components/test/testassets/Components.TestServer/LockedNavigationStartup.cs index 13a3c95fc053..4d3f548135d6 100644 --- a/src/Components/test/testassets/Components.TestServer/LockedNavigationStartup.cs +++ b/src/Components/test/testassets/Components.TestServer/LockedNavigationStartup.cs @@ -39,7 +39,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { endpoints.MapRazorPages(); endpoints.MapFallbackToPage("/LockedNavigationHost"); - endpoints.MapBlazorHub(); + endpoints.MapBlazorHub(options => options.WebSockets.WebSocketAcceptContextFactory = context => new WebSocketAcceptContext { DangerousEnableCompression = true }); }); }); } diff --git a/src/Components/test/testassets/Components.TestServer/MultipleComponents.cs b/src/Components/test/testassets/Components.TestServer/MultipleComponents.cs index ad43d536e0bf..1c8a46018293 100644 --- a/src/Components/test/testassets/Components.TestServer/MultipleComponents.cs +++ b/src/Components/test/testassets/Components.TestServer/MultipleComponents.cs @@ -58,7 +58,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { endpoints.MapRazorPages(); endpoints.MapFallbackToPage("/MultipleComponents"); - endpoints.MapBlazorHub(); + endpoints.MapBlazorHub(options => options.WebSockets.WebSocketAcceptContextFactory = context => new WebSocketAcceptContext { DangerousEnableCompression = true }); }); }); } diff --git a/src/Components/test/testassets/Components.TestServer/PrerenderedStartup.cs b/src/Components/test/testassets/Components.TestServer/PrerenderedStartup.cs index 0d436f8e443e..b95dda7c2ad1 100644 --- a/src/Components/test/testassets/Components.TestServer/PrerenderedStartup.cs +++ b/src/Components/test/testassets/Components.TestServer/PrerenderedStartup.cs @@ -48,7 +48,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { endpoints.MapRazorPages(); endpoints.MapFallbackToPage("/PrerenderedHost"); - endpoints.MapBlazorHub(); + endpoints.MapBlazorHub(options => options.WebSockets.WebSocketAcceptContextFactory = context => new WebSocketAcceptContext { DangerousEnableCompression = true }); }); }); } diff --git a/src/Components/test/testassets/Components.TestServer/SaveState.cs b/src/Components/test/testassets/Components.TestServer/SaveState.cs index d2105487a9f2..87747cf99539 100644 --- a/src/Components/test/testassets/Components.TestServer/SaveState.cs +++ b/src/Components/test/testassets/Components.TestServer/SaveState.cs @@ -41,7 +41,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseEndpoints(endpoints => { endpoints.MapRazorPages(); - endpoints.MapBlazorHub(); + endpoints.MapBlazorHub(options => options.WebSockets.WebSocketAcceptContextFactory = context => new WebSocketAcceptContext { DangerousEnableCompression = true }); endpoints.MapFallbackToPage("/SaveState"); }); } diff --git a/src/Components/test/testassets/Components.TestServer/ServerStartup.cs b/src/Components/test/testassets/Components.TestServer/ServerStartup.cs index 9ad6efaf6cf6..38e82f84d6af 100644 --- a/src/Components/test/testassets/Components.TestServer/ServerStartup.cs +++ b/src/Components/test/testassets/Components.TestServer/ServerStartup.cs @@ -78,7 +78,7 @@ public virtual void Configure(IApplicationBuilder app, IWebHostEnvironment env, app.UseRouting(); app.UseEndpoints(endpoints => { - endpoints.MapBlazorHub(); + endpoints.MapBlazorHub(options => options.WebSockets.WebSocketAcceptContextFactory = context => new WebSocketAcceptContext { DangerousEnableCompression = true }); endpoints.MapControllerRoute("mvc", "{controller}/{action}"); endpoints.MapFallbackToPage("/_ServerHost"); }); diff --git a/src/Components/test/testassets/Components.TestServer/TransportsServerStartup.cs b/src/Components/test/testassets/Components.TestServer/TransportsServerStartup.cs index a3088dfb61ed..f5510cd3f517 100644 --- a/src/Components/test/testassets/Components.TestServer/TransportsServerStartup.cs +++ b/src/Components/test/testassets/Components.TestServer/TransportsServerStartup.cs @@ -25,7 +25,7 @@ public override void Configure(IApplicationBuilder app, IWebHostEnvironment env, app.UseRouting(); app.UseEndpoints(endpoints => { - endpoints.MapBlazorHub(); + endpoints.MapBlazorHub(options => options.WebSockets.WebSocketAcceptContextFactory = context => new WebSocketAcceptContext { DangerousEnableCompression = true }); endpoints.MapFallbackToPage("/_ServerHost"); }); }); diff --git a/src/Components/test/testassets/ComponentsApp.Server/Startup.cs b/src/Components/test/testassets/ComponentsApp.Server/Startup.cs index fbfb8fef4959..57659998de08 100644 --- a/src/Components/test/testassets/ComponentsApp.Server/Startup.cs +++ b/src/Components/test/testassets/ComponentsApp.Server/Startup.cs @@ -34,7 +34,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { endpoints.MapRazorPages(); endpoints.MapControllers(); - endpoints.MapBlazorHub(); + endpoints.MapBlazorHub(options => options.WebSockets.WebSocketAcceptContextFactory = context => new WebSocketAcceptContext { DangerousEnableCompression = true }); endpoints.MapFallbackToPage("/_Host"); }); } From 1f140af312ec1f48c187b010a265b592d49ccb67 Mon Sep 17 00:00:00 2001 From: jacalvar Date: Fri, 26 Jan 2024 09:58:52 +0100 Subject: [PATCH 03/18] Cleanups --- .../src/Builder/InternalServerRenderMode.cs | 7 +++--- .../ServerComponentsEndpointOptions.cs | 23 +++++++++++++++++++ ...entsEndpointConventionBuilderExtensions.cs | 12 ++++++---- .../ServerRazorComponentsBuilderExtensions.cs | 18 +++++++++++++-- .../Server/src/PublicAPI.Unshipped.txt | 12 +++++++++- .../BlazorWebServerStartup.cs | 6 ++--- .../Transports/WebSocketsServerTransport.cs | 2 +- 7 files changed, 63 insertions(+), 17 deletions(-) create mode 100644 src/Components/Server/src/Builder/ServerComponentsEndpointOptions.cs diff --git a/src/Components/Server/src/Builder/InternalServerRenderMode.cs b/src/Components/Server/src/Builder/InternalServerRenderMode.cs index a4bed6f6b9ad..7431d6c4af5c 100644 --- a/src/Components/Server/src/Builder/InternalServerRenderMode.cs +++ b/src/Components/Server/src/Builder/InternalServerRenderMode.cs @@ -5,11 +5,10 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Components.Web; -using Microsoft.AspNetCore.Http.Connections; -namespace Microsoft.AspNetCore.Builder; +namespace Microsoft.AspNetCore.Components.Server; -internal class InternalServerRenderMode(Action? configureOptions = null) : InteractiveServerRenderMode +internal class InternalServerRenderMode(ServerComponentsEndpointOptions options = null) : InteractiveServerRenderMode { - public Action? ConfigureConnectionOptions { get; } = configureOptions; + 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..55da297c67a3 --- /dev/null +++ b/src/Components/Server/src/Builder/ServerComponentsEndpointOptions.cs @@ -0,0 +1,23 @@ +// 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 Microsoft.AspNetCore.Components.Server; + +/// +/// Options to configure interactive Server components. +/// +public class ServerComponentsEndpointOptions +{ + /// + /// Gets or sets a value that indicates whether compression is enabled for the WebSocket connections. + /// + public bool EnableWebSocketCompression { get; set; } + + /// + /// Gets or sets a callback to configure the underlying . + /// If set, this callback takes precedence over . + /// + public Action ConnectionOptions { get; set; } +} diff --git a/src/Components/Server/src/Builder/ServerRazorComponentsEndpointConventionBuilderExtensions.cs b/src/Components/Server/src/Builder/ServerRazorComponentsEndpointConventionBuilderExtensions.cs index 7fbd79c436fb..70e283f8d24c 100644 --- a/src/Components/Server/src/Builder/ServerRazorComponentsEndpointConventionBuilderExtensions.cs +++ b/src/Components/Server/src/Builder/ServerRazorComponentsEndpointConventionBuilderExtensions.cs @@ -2,8 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Components.Endpoints.Infrastructure; +using Microsoft.AspNetCore.Components.Server; using Microsoft.AspNetCore.Components.Web; -using Microsoft.AspNetCore.Http.Connections; using Microsoft.AspNetCore.SignalR; namespace Microsoft.AspNetCore.Builder; @@ -27,14 +27,16 @@ public static RazorComponentsEndpointConventionBuilder AddInteractiveServerRende ///Maps the Blazor to the default path. /// /// The . - /// A callback to configure dispatcher options. + /// A callback to configure server endpoint options. /// The . public static RazorComponentsEndpointConventionBuilder AddInteractiveServerRenderMode( this RazorComponentsEndpointConventionBuilder builder, - Action configureOptions) + Action? callback = null) { - ArgumentNullException.ThrowIfNull(configureOptions); - ComponentEndpointConventionBuilderHelper.AddRenderMode(builder, new InternalServerRenderMode(configureOptions)); + var options = new ServerComponentsEndpointOptions(); + callback?.Invoke(options); + + ComponentEndpointConventionBuilderHelper.AddRenderMode(builder, new InternalServerRenderMode(options)); return builder; } } diff --git a/src/Components/Server/src/DependencyInjection/ServerRazorComponentsBuilderExtensions.cs b/src/Components/Server/src/DependencyInjection/ServerRazorComponentsBuilderExtensions.cs index 6d6708773d01..64b58ae4fe87 100644 --- a/src/Components/Server/src/DependencyInjection/ServerRazorComponentsBuilderExtensions.cs +++ b/src/Components/Server/src/DependencyInjection/ServerRazorComponentsBuilderExtensions.cs @@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Components.Endpoints.Infrastructure; using Microsoft.AspNetCore.Components.Server; using Microsoft.AspNetCore.Components.Web; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -64,10 +65,20 @@ public override IEnumerable GetEndpointBuilders( } var endpointRouteBuilder = new EndpointRouteBuilder(Services, applicationBuilder); - if (renderMode is InternalServerRenderMode { ConfigureConnectionOptions: var configureOptions } && + if (renderMode is InternalServerRenderMode { Options: var configureOptions } && configureOptions != null) { - endpointRouteBuilder.MapBlazorHub("/_blazor", configureOptions); + if (configureOptions.ConnectionOptions != null) + { + endpointRouteBuilder.MapBlazorHub("/_blazor", configureOptions.ConnectionOptions); + } + else if (configureOptions.EnableWebSocketCompression) + { + endpointRouteBuilder.MapBlazorHub("/_blazor", options => + { + options.WebSockets.WebSocketAcceptContextFactory = EnableCompressionDefaults; + }); + } } else { @@ -77,6 +88,9 @@ public override IEnumerable GetEndpointBuilders( return endpointRouteBuilder.GetEndpoints(); } + private static WebSocketAcceptContext EnableCompressionDefaults(HttpContext context) => + new() { DangerousEnableCompression = true }; + public override bool Supports(IComponentRenderMode renderMode) { return renderMode switch diff --git a/src/Components/Server/src/PublicAPI.Unshipped.txt b/src/Components/Server/src/PublicAPI.Unshipped.txt index 066f325fe057..bac604945a54 100644 --- a/src/Components/Server/src/PublicAPI.Unshipped.txt +++ b/src/Components/Server/src/PublicAPI.Unshipped.txt @@ -1,2 +1,12 @@ #nullable enable -static Microsoft.AspNetCore.Builder.ServerRazorComponentsEndpointConventionBuilderExtensions.AddInteractiveServerRenderMode(this Microsoft.AspNetCore.Builder.RazorComponentsEndpointConventionBuilder! builder, System.Action! configureOptions) -> Microsoft.AspNetCore.Builder.RazorComponentsEndpointConventionBuilder! +Microsoft.AspNetCore.Builder.ServerComponentsEndpointOptions +Microsoft.AspNetCore.Builder.ServerComponentsEndpointOptions.EnableWebSocketCompression.get -> bool +Microsoft.AspNetCore.Builder.ServerComponentsEndpointOptions.EnableWebSocketCompression.set -> void +Microsoft.AspNetCore.Builder.ServerComponentsEndpointOptions.ServerComponentsEndpointOptions() -> void +Microsoft.AspNetCore.Components.Server.ServerComponentsEndpointOptions +Microsoft.AspNetCore.Components.Server.ServerComponentsEndpointOptions.ConnectionOptions.get -> System.Action! +Microsoft.AspNetCore.Components.Server.ServerComponentsEndpointOptions.ConnectionOptions.set -> void +Microsoft.AspNetCore.Components.Server.ServerComponentsEndpointOptions.EnableWebSocketCompression.get -> bool +Microsoft.AspNetCore.Components.Server.ServerComponentsEndpointOptions.EnableWebSocketCompression.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/testassets/Components.TestServer/BlazorWebServerStartup.cs b/src/Components/test/testassets/Components.TestServer/BlazorWebServerStartup.cs index 65591d61bbd5..074c58201e61 100644 --- a/src/Components/test/testassets/Components.TestServer/BlazorWebServerStartup.cs +++ b/src/Components/test/testassets/Components.TestServer/BlazorWebServerStartup.cs @@ -1,10 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Globalization; using Components.TestServer.RazorComponents; -using Microsoft.AspNetCore.Components.Server.Circuits; -using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.DataProtection; namespace TestServer; @@ -44,7 +41,8 @@ public virtual void Configure(IApplicationBuilder app, IWebHostEnvironment env, app.UseEndpoints(endpoints => { endpoints.MapRazorComponents() - .AddInteractiveServerRenderMode(options => options.WebSockets.WebSocketAcceptContextFactory = context => new WebSocketAcceptContext { DangerousEnableCompression = true }); + .AddInteractiveWebAssemblyRenderMode() + .AddInteractiveServerRenderMode(options => options.EnableWebSocketCompression = true); }); }); } diff --git a/src/SignalR/common/Http.Connections/src/Internal/Transports/WebSocketsServerTransport.cs b/src/SignalR/common/Http.Connections/src/Internal/Transports/WebSocketsServerTransport.cs index e088f5088359..00504dc8c3f5 100644 --- a/src/SignalR/common/Http.Connections/src/Internal/Transports/WebSocketsServerTransport.cs +++ b/src/SignalR/common/Http.Connections/src/Internal/Transports/WebSocketsServerTransport.cs @@ -43,7 +43,7 @@ public async Task ProcessRequestAsync(HttpContext context, CancellationTok using (var ws = await (acceptContext != null ? context.WebSockets.AcceptWebSocketAsync(acceptContext) : context.WebSockets.AcceptWebSocketAsync(subProtocol))) { - Log.SocketOpened(_logger, subProtocol); + Log.SocketOpened(_logger, acceptContext != null ? acceptContext.SubProtocol : subProtocol); try { From c144ef2da47f938db62edf982cc0eb0d09d66782 Mon Sep 17 00:00:00 2001 From: jacalvar Date: Fri, 26 Jan 2024 10:06:21 +0100 Subject: [PATCH 04/18] Cleanups --- src/Components/Server/src/PublicAPI.Unshipped.txt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Components/Server/src/PublicAPI.Unshipped.txt b/src/Components/Server/src/PublicAPI.Unshipped.txt index bac604945a54..2bcacebe5626 100644 --- a/src/Components/Server/src/PublicAPI.Unshipped.txt +++ b/src/Components/Server/src/PublicAPI.Unshipped.txt @@ -1,8 +1,4 @@ #nullable enable -Microsoft.AspNetCore.Builder.ServerComponentsEndpointOptions -Microsoft.AspNetCore.Builder.ServerComponentsEndpointOptions.EnableWebSocketCompression.get -> bool -Microsoft.AspNetCore.Builder.ServerComponentsEndpointOptions.EnableWebSocketCompression.set -> void -Microsoft.AspNetCore.Builder.ServerComponentsEndpointOptions.ServerComponentsEndpointOptions() -> void Microsoft.AspNetCore.Components.Server.ServerComponentsEndpointOptions Microsoft.AspNetCore.Components.Server.ServerComponentsEndpointOptions.ConnectionOptions.get -> System.Action! Microsoft.AspNetCore.Components.Server.ServerComponentsEndpointOptions.ConnectionOptions.set -> void From 9208894bf7cbed75272fe496316abbd76d5db971 Mon Sep 17 00:00:00 2001 From: jacalvar Date: Fri, 26 Jan 2024 14:37:18 +0100 Subject: [PATCH 05/18] Apply CSP policy --- .../Builder/RazorComponentEndpointFactory.cs | 49 +++++++++++++++++-- .../src/Builder/InternalServerRenderMode.cs | 2 +- .../ServerComponentsEndpointOptions.cs | 24 +++++++++ ...entsEndpointConventionBuilderExtensions.cs | 19 ++++++- .../Server/src/PublicAPI.Unshipped.txt | 2 + 5 files changed, 88 insertions(+), 8 deletions(-) diff --git a/src/Components/Endpoints/src/Builder/RazorComponentEndpointFactory.cs b/src/Components/Endpoints/src/Builder/RazorComponentEndpointFactory.cs index 117aaea090f3..c7abd1ff9417 100644 --- a/src/Components/Endpoints/src/Builder/RazorComponentEndpointFactory.cs +++ b/src/Components/Endpoints/src/Builder/RazorComponentEndpointFactory.cs @@ -67,12 +67,51 @@ 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); - }; + ApplyEndpointFilters(builder); endpoints.Add(builder.Build()); } + + private static void ApplyEndpointFilters(RouteEndpointBuilder builder) + { + if (builder.FilterFactories.Count > 0) + { + + EndpointFilterDelegate del = static async invocationContext => + { + var httpContext = invocationContext.HttpContext; + var invoker = httpContext.RequestServices.GetRequiredService(); + await invoker.Render(httpContext); + return null; + }; + + var context = new EndpointFilterFactoryContext + { + MethodInfo = typeof(IRazorComponentEndpointInvoker).GetMethod(nameof(IRazorComponentEndpointInvoker.Render))!, + ApplicationServices = builder.ApplicationServices, + }; + + var initialFilteredInvocation = del; + + for (var i = builder.FilterFactories.Count - 1; i >= 0; i--) + { + var filterFactory = builder.FilterFactories[i]; + del = filterFactory(context, del); + } + + builder.RequestDelegate = async context => + { + var invocationContext = EndpointFilterInvocationContext.Create(context); + await del(invocationContext); + }; + } + else + { + builder.RequestDelegate = static httpContext => + { + var invoker = httpContext.RequestServices.GetRequiredService(); + return invoker.Render(httpContext); + }; + } + } } diff --git a/src/Components/Server/src/Builder/InternalServerRenderMode.cs b/src/Components/Server/src/Builder/InternalServerRenderMode.cs index 7431d6c4af5c..f5c8883879a6 100644 --- a/src/Components/Server/src/Builder/InternalServerRenderMode.cs +++ b/src/Components/Server/src/Builder/InternalServerRenderMode.cs @@ -10,5 +10,5 @@ namespace Microsoft.AspNetCore.Components.Server; internal class InternalServerRenderMode(ServerComponentsEndpointOptions options = null) : InteractiveServerRenderMode { - public ServerComponentsEndpointOptions? Options { get; } = options; + public ServerComponentsEndpointOptions? Options { get; } = options ?? new() { EnableWebSocketCompression = true }; } diff --git a/src/Components/Server/src/Builder/ServerComponentsEndpointOptions.cs b/src/Components/Server/src/Builder/ServerComponentsEndpointOptions.cs index 55da297c67a3..23f50bace580 100644 --- a/src/Components/Server/src/Builder/ServerComponentsEndpointOptions.cs +++ b/src/Components/Server/src/Builder/ServerComponentsEndpointOptions.cs @@ -1,6 +1,7 @@ // 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; using Microsoft.AspNetCore.Http.Connections; namespace Microsoft.AspNetCore.Components.Server; @@ -15,6 +16,29 @@ public class ServerComponentsEndpointOptions /// public bool EnableWebSocketCompression { get; set; } + /// + /// Gets or sets the frame-ancestors Content-Security-Policy to set in the + /// when is enabled. + /// + /// + /// 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 is prohibited. + /// + /// + /// This restriction can be relaxed to 'self' or any other trusted origin at the expense + /// of exposing the app if an attacker is able to run code from one of those origins. + /// For more details see the security recommendations for Interactive Server Components in + /// the official documentation. + /// + /// + public string? ContentSecurityFrameAncestorPolicy { get; set; } = "'none'"; + /// /// Gets or sets a callback to configure the underlying . /// If set, this callback takes precedence over . diff --git a/src/Components/Server/src/Builder/ServerRazorComponentsEndpointConventionBuilderExtensions.cs b/src/Components/Server/src/Builder/ServerRazorComponentsEndpointConventionBuilderExtensions.cs index 70e283f8d24c..ebe73366da46 100644 --- a/src/Components/Server/src/Builder/ServerRazorComponentsEndpointConventionBuilderExtensions.cs +++ b/src/Components/Server/src/Builder/ServerRazorComponentsEndpointConventionBuilderExtensions.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Components.Endpoints.Infrastructure; using Microsoft.AspNetCore.Components.Server; using Microsoft.AspNetCore.Components.Web; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.SignalR; namespace Microsoft.AspNetCore.Builder; @@ -19,8 +20,7 @@ public static class ServerRazorComponentsEndpointConventionBuilderExtensions /// The . public static RazorComponentsEndpointConventionBuilder AddInteractiveServerRenderMode(this RazorComponentsEndpointConventionBuilder builder) { - ComponentEndpointConventionBuilderHelper.AddRenderMode(builder, new InternalServerRenderMode()); - return builder; + return AddInteractiveServerRenderMode(builder, null); } /// @@ -37,6 +37,21 @@ public static RazorComponentsEndpointConventionBuilder AddInteractiveServerRende callback?.Invoke(options); ComponentEndpointConventionBuilderHelper.AddRenderMode(builder, new InternalServerRenderMode(options)); + + if (options.EnableWebSocketCompression && options.ContentSecurityFrameAncestorPolicy != null) + { + builder.AddEndpointFilter(new RequireCspFilter(options.ContentSecurityFrameAncestorPolicy)); + } + return builder; } + + private class RequireCspFilter(string policy) : IEndpointFilter + { + public ValueTask InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next) + { + context.HttpContext.Response.Headers.Add("Content-Security-Policy", $"frame-ancestors {policy}"); + return next(context); + } + } } diff --git a/src/Components/Server/src/PublicAPI.Unshipped.txt b/src/Components/Server/src/PublicAPI.Unshipped.txt index 2bcacebe5626..6feb47deb083 100644 --- a/src/Components/Server/src/PublicAPI.Unshipped.txt +++ b/src/Components/Server/src/PublicAPI.Unshipped.txt @@ -2,6 +2,8 @@ Microsoft.AspNetCore.Components.Server.ServerComponentsEndpointOptions Microsoft.AspNetCore.Components.Server.ServerComponentsEndpointOptions.ConnectionOptions.get -> System.Action! Microsoft.AspNetCore.Components.Server.ServerComponentsEndpointOptions.ConnectionOptions.set -> void +Microsoft.AspNetCore.Components.Server.ServerComponentsEndpointOptions.ContentSecurityFrameAncestorPolicy.get -> string? +Microsoft.AspNetCore.Components.Server.ServerComponentsEndpointOptions.ContentSecurityFrameAncestorPolicy.set -> void Microsoft.AspNetCore.Components.Server.ServerComponentsEndpointOptions.EnableWebSocketCompression.get -> bool Microsoft.AspNetCore.Components.Server.ServerComponentsEndpointOptions.EnableWebSocketCompression.set -> void Microsoft.AspNetCore.Components.Server.ServerComponentsEndpointOptions.ServerComponentsEndpointOptions() -> void From 6bc5586c22f7b0142697bfa8df395de6d40ce7f9 Mon Sep 17 00:00:00 2001 From: jacalvar Date: Fri, 26 Jan 2024 16:44:56 +0100 Subject: [PATCH 06/18] Add E2E tests --- .../src/Builder/InternalServerRenderMode.cs | 4 +- .../ServerComponentsEndpointOptions.cs | 7 +- ...entsEndpointConventionBuilderExtensions.cs | 2 +- .../ServerRazorComponentsBuilderExtensions.cs | 4 +- .../Server/src/PublicAPI.Unshipped.txt | 4 +- .../ServerFixtures/AspNetSiteServerFixture.cs | 9 +- .../WebSocketCompressionTests.cs | 117 ++++++++++++++++++ .../BlazorWebServerStartup.cs | 6 +- .../RazorComponentEndpointsStartup.cs | 9 +- ...dAppInsideIFrameWhenUsingCompression.razor | 3 + .../Pages/EmbeddedInsideIFrame.razor | 4 + .../WebSocketCompressionConfiguration.cs | 15 +++ 12 files changed, 170 insertions(+), 14 deletions(-) create mode 100644 src/Components/test/E2ETest/ServerExecutionTests/WebSocketCompressionTests.cs create mode 100644 src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/CanNotEmbedAppInsideIFrameWhenUsingCompression.razor create mode 100644 src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/EmbeddedInsideIFrame.razor create mode 100644 src/Components/test/testassets/Components.TestServer/WebSocketCompressionConfiguration.cs diff --git a/src/Components/Server/src/Builder/InternalServerRenderMode.cs b/src/Components/Server/src/Builder/InternalServerRenderMode.cs index f5c8883879a6..eeed3a688f4e 100644 --- a/src/Components/Server/src/Builder/InternalServerRenderMode.cs +++ b/src/Components/Server/src/Builder/InternalServerRenderMode.cs @@ -8,7 +8,7 @@ namespace Microsoft.AspNetCore.Components.Server; -internal class InternalServerRenderMode(ServerComponentsEndpointOptions options = null) : InteractiveServerRenderMode +internal class InternalServerRenderMode(ServerComponentsEndpointOptions options) : InteractiveServerRenderMode { - public ServerComponentsEndpointOptions? Options { get; } = options ?? new() { EnableWebSocketCompression = true }; + public ServerComponentsEndpointOptions? Options { get; } = options; } diff --git a/src/Components/Server/src/Builder/ServerComponentsEndpointOptions.cs b/src/Components/Server/src/Builder/ServerComponentsEndpointOptions.cs index 23f50bace580..d1c5e8c7f6c3 100644 --- a/src/Components/Server/src/Builder/ServerComponentsEndpointOptions.cs +++ b/src/Components/Server/src/Builder/ServerComponentsEndpointOptions.cs @@ -14,7 +14,7 @@ public class ServerComponentsEndpointOptions /// /// Gets or sets a value that indicates whether compression is enabled for the WebSocket connections. /// - public bool EnableWebSocketCompression { get; set; } + public bool EnableWebSocketCompression { get; set; } = true; /// /// Gets or sets the frame-ancestors Content-Security-Policy to set in the @@ -41,7 +41,8 @@ public class ServerComponentsEndpointOptions /// /// Gets or sets a callback to configure the underlying . - /// If set, this callback takes precedence over . + /// If set, will be applied independent of the value of + /// . /// - public Action ConnectionOptions { get; set; } + public Action ConfigureConnectionOptions { get; set; } } diff --git a/src/Components/Server/src/Builder/ServerRazorComponentsEndpointConventionBuilderExtensions.cs b/src/Components/Server/src/Builder/ServerRazorComponentsEndpointConventionBuilderExtensions.cs index ebe73366da46..d0f5fd632d22 100644 --- a/src/Components/Server/src/Builder/ServerRazorComponentsEndpointConventionBuilderExtensions.cs +++ b/src/Components/Server/src/Builder/ServerRazorComponentsEndpointConventionBuilderExtensions.cs @@ -38,7 +38,7 @@ public static RazorComponentsEndpointConventionBuilder AddInteractiveServerRende ComponentEndpointConventionBuilderHelper.AddRenderMode(builder, new InternalServerRenderMode(options)); - if (options.EnableWebSocketCompression && options.ContentSecurityFrameAncestorPolicy != null) + if ((options.EnableWebSocketCompression || options.ConfigureConnectionOptions is not null) && options.ContentSecurityFrameAncestorPolicy != null) { builder.AddEndpointFilter(new RequireCspFilter(options.ContentSecurityFrameAncestorPolicy)); } diff --git a/src/Components/Server/src/DependencyInjection/ServerRazorComponentsBuilderExtensions.cs b/src/Components/Server/src/DependencyInjection/ServerRazorComponentsBuilderExtensions.cs index 64b58ae4fe87..fde50820c631 100644 --- a/src/Components/Server/src/DependencyInjection/ServerRazorComponentsBuilderExtensions.cs +++ b/src/Components/Server/src/DependencyInjection/ServerRazorComponentsBuilderExtensions.cs @@ -68,9 +68,9 @@ public override IEnumerable GetEndpointBuilders( if (renderMode is InternalServerRenderMode { Options: var configureOptions } && configureOptions != null) { - if (configureOptions.ConnectionOptions != null) + if (configureOptions.ConfigureConnectionOptions != null) { - endpointRouteBuilder.MapBlazorHub("/_blazor", configureOptions.ConnectionOptions); + endpointRouteBuilder.MapBlazorHub("/_blazor", configureOptions.ConfigureConnectionOptions); } else if (configureOptions.EnableWebSocketCompression) { diff --git a/src/Components/Server/src/PublicAPI.Unshipped.txt b/src/Components/Server/src/PublicAPI.Unshipped.txt index 6feb47deb083..eb5d3b08c5cc 100644 --- a/src/Components/Server/src/PublicAPI.Unshipped.txt +++ b/src/Components/Server/src/PublicAPI.Unshipped.txt @@ -1,7 +1,7 @@ #nullable enable Microsoft.AspNetCore.Components.Server.ServerComponentsEndpointOptions -Microsoft.AspNetCore.Components.Server.ServerComponentsEndpointOptions.ConnectionOptions.get -> System.Action! -Microsoft.AspNetCore.Components.Server.ServerComponentsEndpointOptions.ConnectionOptions.set -> void +Microsoft.AspNetCore.Components.Server.ServerComponentsEndpointOptions.ConfigureConnectionOptions.get -> System.Action! +Microsoft.AspNetCore.Components.Server.ServerComponentsEndpointOptions.ConfigureConnectionOptions.set -> void Microsoft.AspNetCore.Components.Server.ServerComponentsEndpointOptions.ContentSecurityFrameAncestorPolicy.get -> string? Microsoft.AspNetCore.Components.Server.ServerComponentsEndpointOptions.ContentSecurityFrameAncestorPolicy.set -> void Microsoft.AspNetCore.Components.Server.ServerComponentsEndpointOptions.EnableWebSocketCompression.get -> bool 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..9589fb1d2a3c --- /dev/null +++ b/src/Components/test/E2ETest/ServerExecutionTests/WebSocketCompressionTests.cs @@ -0,0 +1,117 @@ +// 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) + : BlockedWebSocketCompressionTests(browserFixture, serverFixture, output) +{ +} + +public partial class CustomConfigurationCallbackWebSocketCompressionTests : BlockedWebSocketCompressionTests +{ + public CustomConfigurationCallbackWebSocketCompressionTests( + BrowserFixture browserFixture, + BasicTestAppServerSiteFixture> serverFixture, + ITestOutputHelper output) : base(browserFixture, serverFixture, output) + { + serverFixture.UpdateHostServices = services => + { + var configuration = services.GetService(); + configuration.ConnectionDispatcherOptions = options => + options.WebSockets.WebSocketAcceptContextFactory = context => + new Http.WebSocketAcceptContext { 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; + }; + } +} + +public partial class SelfFrameAncestorWebSocketCompressionTests : AllowedWebSocketCompressionTests +{ + public SelfFrameAncestorWebSocketCompressionTests( + BrowserFixture browserFixture, + BasicTestAppServerSiteFixture> serverFixture, + ITestOutputHelper output) + : base(browserFixture, serverFixture, output) + { + serverFixture.UpdateHostServices = services => + { + var configuration = services.GetService(); + configuration.CspPolicy = "'self'"; + }; + } +} + diff --git a/src/Components/test/testassets/Components.TestServer/BlazorWebServerStartup.cs b/src/Components/test/testassets/Components.TestServer/BlazorWebServerStartup.cs index 074c58201e61..5deffb0d94b1 100644 --- a/src/Components/test/testassets/Components.TestServer/BlazorWebServerStartup.cs +++ b/src/Components/test/testassets/Components.TestServer/BlazorWebServerStartup.cs @@ -1,7 +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.Globalization; using Components.TestServer.RazorComponents; +using Microsoft.AspNetCore.Components.Server.Circuits; +using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.DataProtection; namespace TestServer; @@ -41,8 +44,7 @@ public virtual void Configure(IApplicationBuilder app, IWebHostEnvironment env, app.UseEndpoints(endpoints => { endpoints.MapRazorComponents() - .AddInteractiveWebAssemblyRenderMode() - .AddInteractiveServerRenderMode(options => options.EnableWebSocketCompression = true); + .AddInteractiveServerRenderMode(); }); }); } diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs b/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs index be68ef2ffd7e..14ce82c63423 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); @@ -69,7 +70,13 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { endpoints.MapRazorComponents() .AddAdditionalAssemblies(Assembly.Load("Components.WasmMinimal")) - .AddInteractiveServerRenderMode() + .AddInteractiveServerRenderMode(options => + { + var config = app.ApplicationServices.GetRequiredService(); + options.EnableWebSocketCompression = config.IsCompressionEnabled; + options.ContentSecurityFrameAncestorPolicy = config.CspPolicy; + options.ConfigureConnectionOptions = config.ConnectionDispatcherOptions; + }) .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/WebSocketCompressionConfiguration.cs b/src/Components/test/testassets/Components.TestServer/WebSocketCompressionConfiguration.cs new file mode 100644 index 000000000000..662ef4551688 --- /dev/null +++ b/src/Components/test/testassets/Components.TestServer/WebSocketCompressionConfiguration.cs @@ -0,0 +1,15 @@ +// 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; } = "'none'"; + + public Action ConnectionDispatcherOptions { get; set; } +} From 208f098af39e624b5a464b85760305ec31fbe0dc Mon Sep 17 00:00:00 2001 From: jacalvar Date: Fri, 26 Jan 2024 19:03:50 +0100 Subject: [PATCH 07/18] Default to 'self' --- .../src/Builder/ServerComponentsEndpointOptions.cs | 6 ++---- .../ServerExecutionTests/WebSocketCompressionTests.cs | 10 +++++----- .../WebSocketCompressionConfiguration.cs | 2 +- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/Components/Server/src/Builder/ServerComponentsEndpointOptions.cs b/src/Components/Server/src/Builder/ServerComponentsEndpointOptions.cs index d1c5e8c7f6c3..2f4b68abdf72 100644 --- a/src/Components/Server/src/Builder/ServerComponentsEndpointOptions.cs +++ b/src/Components/Server/src/Builder/ServerComponentsEndpointOptions.cs @@ -28,16 +28,14 @@ public class ServerComponentsEndpointOptions /// /// 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 is prohibited. + /// embedding the app inside an iframe from other origins is prohibited. /// /// - /// This restriction can be relaxed to 'self' or any other trusted origin at the expense - /// of exposing the app if an attacker is able to run code from one of those origins. /// For more details see the security recommendations for Interactive Server Components in /// the official documentation. /// /// - public string? ContentSecurityFrameAncestorPolicy { get; set; } = "'none'"; + public string? ContentSecurityFrameAncestorPolicy { get; set; } = "'self'"; /// /// Gets or sets a callback to configure the underlying . diff --git a/src/Components/test/E2ETest/ServerExecutionTests/WebSocketCompressionTests.cs b/src/Components/test/E2ETest/ServerExecutionTests/WebSocketCompressionTests.cs index 9589fb1d2a3c..98d37b116aeb 100644 --- a/src/Components/test/E2ETest/ServerExecutionTests/WebSocketCompressionTests.cs +++ b/src/Components/test/E2ETest/ServerExecutionTests/WebSocketCompressionTests.cs @@ -62,11 +62,11 @@ public partial class DefaultConfigurationWebSocketCompressionTests( BrowserFixture browserFixture, BasicTestAppServerSiteFixture> serverFixture, ITestOutputHelper output) - : BlockedWebSocketCompressionTests(browserFixture, serverFixture, output) + : AllowedWebSocketCompressionTests(browserFixture, serverFixture, output) { } -public partial class CustomConfigurationCallbackWebSocketCompressionTests : BlockedWebSocketCompressionTests +public partial class CustomConfigurationCallbackWebSocketCompressionTests : AllowedWebSocketCompressionTests { public CustomConfigurationCallbackWebSocketCompressionTests( BrowserFixture browserFixture, @@ -99,9 +99,9 @@ public CompressionDisabledWebSocketCompressionTests( } } -public partial class SelfFrameAncestorWebSocketCompressionTests : AllowedWebSocketCompressionTests +public partial class NoneAncestorWebSocketCompressionTests : BlockedWebSocketCompressionTests { - public SelfFrameAncestorWebSocketCompressionTests( + public NoneAncestorWebSocketCompressionTests( BrowserFixture browserFixture, BasicTestAppServerSiteFixture> serverFixture, ITestOutputHelper output) @@ -110,7 +110,7 @@ public SelfFrameAncestorWebSocketCompressionTests( serverFixture.UpdateHostServices = services => { var configuration = services.GetService(); - configuration.CspPolicy = "'self'"; + configuration.CspPolicy = "'none'"; }; } } diff --git a/src/Components/test/testassets/Components.TestServer/WebSocketCompressionConfiguration.cs b/src/Components/test/testassets/Components.TestServer/WebSocketCompressionConfiguration.cs index 662ef4551688..291d825d4532 100644 --- a/src/Components/test/testassets/Components.TestServer/WebSocketCompressionConfiguration.cs +++ b/src/Components/test/testassets/Components.TestServer/WebSocketCompressionConfiguration.cs @@ -9,7 +9,7 @@ public class WebSocketCompressionConfiguration { public bool IsCompressionEnabled { get; set; } = true; - public string CspPolicy { get; set; } = "'none'"; + public string CspPolicy { get; set; } = "'self'"; public Action ConnectionDispatcherOptions { get; set; } } From 03f1079d0a57d40144e088e3615b6bde9bb986fc Mon Sep 17 00:00:00 2001 From: jacalvar Date: Thu, 1 Feb 2024 13:24:47 +0100 Subject: [PATCH 08/18] Simplify settings by just providing the callback --- .../Builder/ServerComponentsEndpointOptions.cs | 16 +++++++--------- ...nentsEndpointConventionBuilderExtensions.cs | 2 +- .../ServerRazorComponentsBuilderExtensions.cs | 18 ++---------------- 3 files changed, 10 insertions(+), 26 deletions(-) diff --git a/src/Components/Server/src/Builder/ServerComponentsEndpointOptions.cs b/src/Components/Server/src/Builder/ServerComponentsEndpointOptions.cs index 2f4b68abdf72..f8bb2cdc74a5 100644 --- a/src/Components/Server/src/Builder/ServerComponentsEndpointOptions.cs +++ b/src/Components/Server/src/Builder/ServerComponentsEndpointOptions.cs @@ -11,14 +11,9 @@ namespace Microsoft.AspNetCore.Components.Server; /// public class ServerComponentsEndpointOptions { - /// - /// Gets or sets a value that indicates whether compression is enabled for the WebSocket connections. - /// - public bool EnableWebSocketCompression { get; set; } = true; - /// /// Gets or sets the frame-ancestors Content-Security-Policy to set in the - /// when is enabled. + /// when is set. /// /// /// Setting this value to will prevent the policy from being @@ -39,8 +34,11 @@ public class ServerComponentsEndpointOptions /// /// Gets or sets a callback to configure the underlying . - /// If set, will be applied independent of the value of - /// . + /// By default, a policy that enables compression and sets a Content Security Policy for the frame ancestors + /// defined in will be applied. /// - public Action ConfigureConnectionOptions { get; set; } + public Action ConfigureConnectionOptions { 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 d0f5fd632d22..6411bbaf6256 100644 --- a/src/Components/Server/src/Builder/ServerRazorComponentsEndpointConventionBuilderExtensions.cs +++ b/src/Components/Server/src/Builder/ServerRazorComponentsEndpointConventionBuilderExtensions.cs @@ -38,7 +38,7 @@ public static RazorComponentsEndpointConventionBuilder AddInteractiveServerRende ComponentEndpointConventionBuilderHelper.AddRenderMode(builder, new InternalServerRenderMode(options)); - if ((options.EnableWebSocketCompression || options.ConfigureConnectionOptions is not null) && options.ContentSecurityFrameAncestorPolicy != null) + if (options.ConfigureConnectionOptions is not null && options.ContentSecurityFrameAncestorPolicy != null) { builder.AddEndpointFilter(new RequireCspFilter(options.ContentSecurityFrameAncestorPolicy)); } diff --git a/src/Components/Server/src/DependencyInjection/ServerRazorComponentsBuilderExtensions.cs b/src/Components/Server/src/DependencyInjection/ServerRazorComponentsBuilderExtensions.cs index fde50820c631..a901db0dfbcd 100644 --- a/src/Components/Server/src/DependencyInjection/ServerRazorComponentsBuilderExtensions.cs +++ b/src/Components/Server/src/DependencyInjection/ServerRazorComponentsBuilderExtensions.cs @@ -65,20 +65,9 @@ public override IEnumerable GetEndpointBuilders( } var endpointRouteBuilder = new EndpointRouteBuilder(Services, applicationBuilder); - if (renderMode is InternalServerRenderMode { Options: var configureOptions } && - configureOptions != null) + if (renderMode is InternalServerRenderMode { Options.ConfigureConnectionOptions: { } configureConnection }) { - if (configureOptions.ConfigureConnectionOptions != null) - { - endpointRouteBuilder.MapBlazorHub("/_blazor", configureOptions.ConfigureConnectionOptions); - } - else if (configureOptions.EnableWebSocketCompression) - { - endpointRouteBuilder.MapBlazorHub("/_blazor", options => - { - options.WebSockets.WebSocketAcceptContextFactory = EnableCompressionDefaults; - }); - } + endpointRouteBuilder.MapBlazorHub("/_blazor", configureOptions.ConfigureConnectionOptions); } else { @@ -88,9 +77,6 @@ public override IEnumerable GetEndpointBuilders( return endpointRouteBuilder.GetEndpoints(); } - private static WebSocketAcceptContext EnableCompressionDefaults(HttpContext context) => - new() { DangerousEnableCompression = true }; - public override bool Supports(IComponentRenderMode renderMode) { return renderMode switch From 49f9824a5bca1ac915faadbe6a3e7d74c8f5a870 Mon Sep 17 00:00:00 2001 From: jacalvar Date: Thu, 1 Feb 2024 13:30:29 +0100 Subject: [PATCH 09/18] Address feedback --- .../Endpoints/src/Builder/RazorComponentEndpointFactory.cs | 3 --- .../Server/src/Builder/InternalServerRenderMode.cs | 5 ----- .../Server/src/Builder/ServerComponentsEndpointOptions.cs | 4 ++-- ...rverRazorComponentsEndpointConventionBuilderExtensions.cs | 4 ++-- src/Components/Server/src/PublicAPI.Unshipped.txt | 2 -- 5 files changed, 4 insertions(+), 14 deletions(-) diff --git a/src/Components/Endpoints/src/Builder/RazorComponentEndpointFactory.cs b/src/Components/Endpoints/src/Builder/RazorComponentEndpointFactory.cs index c7abd1ff9417..5535809638aa 100644 --- a/src/Components/Endpoints/src/Builder/RazorComponentEndpointFactory.cs +++ b/src/Components/Endpoints/src/Builder/RazorComponentEndpointFactory.cs @@ -76,7 +76,6 @@ private static void ApplyEndpointFilters(RouteEndpointBuilder builder) { if (builder.FilterFactories.Count > 0) { - EndpointFilterDelegate del = static async invocationContext => { var httpContext = invocationContext.HttpContext; @@ -91,8 +90,6 @@ private static void ApplyEndpointFilters(RouteEndpointBuilder builder) ApplicationServices = builder.ApplicationServices, }; - var initialFilteredInvocation = del; - for (var i = builder.FilterFactories.Count - 1; i >= 0; i--) { var filterFactory = builder.FilterFactories[i]; diff --git a/src/Components/Server/src/Builder/InternalServerRenderMode.cs b/src/Components/Server/src/Builder/InternalServerRenderMode.cs index eeed3a688f4e..ccbc206ea195 100644 --- a/src/Components/Server/src/Builder/InternalServerRenderMode.cs +++ b/src/Components/Server/src/Builder/InternalServerRenderMode.cs @@ -1,11 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -// 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.Components.Server; internal class InternalServerRenderMode(ServerComponentsEndpointOptions options) : InteractiveServerRenderMode diff --git a/src/Components/Server/src/Builder/ServerComponentsEndpointOptions.cs b/src/Components/Server/src/Builder/ServerComponentsEndpointOptions.cs index f8bb2cdc74a5..dc2780c0e8d5 100644 --- a/src/Components/Server/src/Builder/ServerComponentsEndpointOptions.cs +++ b/src/Components/Server/src/Builder/ServerComponentsEndpointOptions.cs @@ -22,7 +22,7 @@ public class ServerComponentsEndpointOptions /// /// /// 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 + /// 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. /// /// @@ -37,7 +37,7 @@ public class ServerComponentsEndpointOptions /// By default, a policy that enables compression and sets a Content Security Policy for the frame ancestors /// defined in will be applied. ///
- public Action ConfigureConnectionOptions { get; set; } = EnableCompressionDefaults; + public Action? ConfigureConnectionOptions { 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 6411bbaf6256..a977e58e951a 100644 --- a/src/Components/Server/src/Builder/ServerRazorComponentsEndpointConventionBuilderExtensions.cs +++ b/src/Components/Server/src/Builder/ServerRazorComponentsEndpointConventionBuilderExtensions.cs @@ -24,7 +24,7 @@ public static RazorComponentsEndpointConventionBuilder AddInteractiveServerRende } /// - ///Maps the Blazor to the default path. + /// Maps the Blazor to the default path. /// /// The . /// A callback to configure server endpoint options. @@ -46,7 +46,7 @@ public static RazorComponentsEndpointConventionBuilder AddInteractiveServerRende return builder; } - private class RequireCspFilter(string policy) : IEndpointFilter + private sealed class RequireCspFilter(string policy) : IEndpointFilter { public ValueTask InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next) { diff --git a/src/Components/Server/src/PublicAPI.Unshipped.txt b/src/Components/Server/src/PublicAPI.Unshipped.txt index eb5d3b08c5cc..07cb1d88d202 100644 --- a/src/Components/Server/src/PublicAPI.Unshipped.txt +++ b/src/Components/Server/src/PublicAPI.Unshipped.txt @@ -4,7 +4,5 @@ Microsoft.AspNetCore.Components.Server.ServerComponentsEndpointOptions.Configure Microsoft.AspNetCore.Components.Server.ServerComponentsEndpointOptions.ConfigureConnectionOptions.set -> void Microsoft.AspNetCore.Components.Server.ServerComponentsEndpointOptions.ContentSecurityFrameAncestorPolicy.get -> string? Microsoft.AspNetCore.Components.Server.ServerComponentsEndpointOptions.ContentSecurityFrameAncestorPolicy.set -> void -Microsoft.AspNetCore.Components.Server.ServerComponentsEndpointOptions.EnableWebSocketCompression.get -> bool -Microsoft.AspNetCore.Components.Server.ServerComponentsEndpointOptions.EnableWebSocketCompression.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! From 669ebea971ae44490d8f517c3fb04bd0bbbc4000 Mon Sep 17 00:00:00 2001 From: jacalvar Date: Thu, 1 Feb 2024 14:14:22 +0100 Subject: [PATCH 10/18] Fix build --- .../Server/src/Builder/InternalServerRenderMode.cs | 2 ++ .../Server/src/Builder/ServerComponentsEndpointOptions.cs | 3 ++- .../ServerRazorComponentsBuilderExtensions.cs | 3 +-- src/Components/Server/src/PublicAPI.Unshipped.txt | 2 +- .../RazorComponentEndpointsStartup.cs | 7 ++++--- 5 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/Components/Server/src/Builder/InternalServerRenderMode.cs b/src/Components/Server/src/Builder/InternalServerRenderMode.cs index ccbc206ea195..cd89bccacfc1 100644 --- a/src/Components/Server/src/Builder/InternalServerRenderMode.cs +++ b/src/Components/Server/src/Builder/InternalServerRenderMode.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.Components.Web; + namespace Microsoft.AspNetCore.Components.Server; internal class InternalServerRenderMode(ServerComponentsEndpointOptions options) : InteractiveServerRenderMode diff --git a/src/Components/Server/src/Builder/ServerComponentsEndpointOptions.cs b/src/Components/Server/src/Builder/ServerComponentsEndpointOptions.cs index dc2780c0e8d5..08a7782ac563 100644 --- a/src/Components/Server/src/Builder/ServerComponentsEndpointOptions.cs +++ b/src/Components/Server/src/Builder/ServerComponentsEndpointOptions.cs @@ -37,7 +37,8 @@ public class ServerComponentsEndpointOptions /// By default, a policy that enables compression and sets a Content Security Policy for the frame ancestors /// defined in will be applied. ///
- public Action? ConfigureConnectionOptions { get; set; } = EnableCompressionDefaults; + public Action? ConfigureConnectionOptions { get; set; } = + options => options.WebSockets.WebSocketAcceptContextFactory = EnableCompressionDefaults; private static WebSocketAcceptContext EnableCompressionDefaults(HttpContext context) => new() { DangerousEnableCompression = true }; diff --git a/src/Components/Server/src/DependencyInjection/ServerRazorComponentsBuilderExtensions.cs b/src/Components/Server/src/DependencyInjection/ServerRazorComponentsBuilderExtensions.cs index a901db0dfbcd..7b0fdc12e117 100644 --- a/src/Components/Server/src/DependencyInjection/ServerRazorComponentsBuilderExtensions.cs +++ b/src/Components/Server/src/DependencyInjection/ServerRazorComponentsBuilderExtensions.cs @@ -7,7 +7,6 @@ using Microsoft.AspNetCore.Components.Endpoints.Infrastructure; using Microsoft.AspNetCore.Components.Server; using Microsoft.AspNetCore.Components.Web; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -67,7 +66,7 @@ public override IEnumerable GetEndpointBuilders( var endpointRouteBuilder = new EndpointRouteBuilder(Services, applicationBuilder); if (renderMode is InternalServerRenderMode { Options.ConfigureConnectionOptions: { } configureConnection }) { - endpointRouteBuilder.MapBlazorHub("/_blazor", configureOptions.ConfigureConnectionOptions); + endpointRouteBuilder.MapBlazorHub("/_blazor", configureConnection); } else { diff --git a/src/Components/Server/src/PublicAPI.Unshipped.txt b/src/Components/Server/src/PublicAPI.Unshipped.txt index 07cb1d88d202..2dceac0f7cff 100644 --- a/src/Components/Server/src/PublicAPI.Unshipped.txt +++ b/src/Components/Server/src/PublicAPI.Unshipped.txt @@ -1,6 +1,6 @@ #nullable enable Microsoft.AspNetCore.Components.Server.ServerComponentsEndpointOptions -Microsoft.AspNetCore.Components.Server.ServerComponentsEndpointOptions.ConfigureConnectionOptions.get -> System.Action! +Microsoft.AspNetCore.Components.Server.ServerComponentsEndpointOptions.ConfigureConnectionOptions.get -> System.Action? Microsoft.AspNetCore.Components.Server.ServerComponentsEndpointOptions.ConfigureConnectionOptions.set -> void Microsoft.AspNetCore.Components.Server.ServerComponentsEndpointOptions.ContentSecurityFrameAncestorPolicy.get -> string? Microsoft.AspNetCore.Components.Server.ServerComponentsEndpointOptions.ContentSecurityFrameAncestorPolicy.set -> void diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs b/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs index 14ce82c63423..dc6f6bd377d8 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs +++ b/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs @@ -66,14 +66,15 @@ 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(options => { var config = app.ApplicationServices.GetRequiredService(); - options.EnableWebSocketCompression = config.IsCompressionEnabled; + options.ConfigureConnectionOptions = config.IsCompressionEnabled ? + (opts) => opts.WebSockets.WebSocketAcceptContextFactory = _ => new() { DangerousEnableCompression = true } : null; options.ContentSecurityFrameAncestorPolicy = config.CspPolicy; options.ConfigureConnectionOptions = config.ConnectionDispatcherOptions; }) From 294063273780928481821fe1ed59fc3cc940a9a1 Mon Sep 17 00:00:00 2001 From: jacalvar Date: Thu, 1 Feb 2024 16:36:14 +0100 Subject: [PATCH 11/18] Fix tests --- .../Components.TestServer/RazorComponentEndpointsStartup.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs b/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs index dc6f6bd377d8..fd60be46b157 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs +++ b/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs @@ -76,7 +76,8 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) options.ConfigureConnectionOptions = config.IsCompressionEnabled ? (opts) => opts.WebSockets.WebSocketAcceptContextFactory = _ => new() { DangerousEnableCompression = true } : null; options.ContentSecurityFrameAncestorPolicy = config.CspPolicy; - options.ConfigureConnectionOptions = config.ConnectionDispatcherOptions; + options.ConfigureConnectionOptions = config.ConnectionDispatcherOptions ?? (config.IsCompressionEnabled ? + opts => opts.WebSockets.WebSocketAcceptContextFactory = (context) => new() { DangerousEnableCompression = true } : null); }) .AddInteractiveWebAssemblyRenderMode(options => options.PathPrefix = "/WasmMinimal"); From 8e34e508893d4ef145a3a3f5c2bddd1286f165ba Mon Sep 17 00:00:00 2001 From: jacalvar Date: Sat, 3 Feb 2024 10:32:17 +0100 Subject: [PATCH 12/18] Alternative approach that avoids configuring compression via SignalR --- .../ServerComponentsEndpointOptions.cs | 3 +- .../ServerRazorComponentsBuilderExtensions.cs | 30 +++++++++++++++---- .../WebSocketCompressionTests.cs | 4 +-- .../RazorComponentEndpointsStartup.cs | 4 +-- .../WebSocketCompressionConfiguration.cs | 2 +- 5 files changed, 30 insertions(+), 13 deletions(-) diff --git a/src/Components/Server/src/Builder/ServerComponentsEndpointOptions.cs b/src/Components/Server/src/Builder/ServerComponentsEndpointOptions.cs index 08a7782ac563..adae30ec39f1 100644 --- a/src/Components/Server/src/Builder/ServerComponentsEndpointOptions.cs +++ b/src/Components/Server/src/Builder/ServerComponentsEndpointOptions.cs @@ -37,8 +37,7 @@ public class ServerComponentsEndpointOptions /// By default, a policy that enables compression and sets a Content Security Policy for the frame ancestors /// defined in will be applied. ///
- public Action? ConfigureConnectionOptions { get; set; } = - options => options.WebSockets.WebSocketAcceptContextFactory = EnableCompressionDefaults; + public Func? ConfigureConnectionOptions { get; set; } = EnableCompressionDefaults; private static WebSocketAcceptContext EnableCompressionDefaults(HttpContext context) => new() { DangerousEnableCompression = true }; diff --git a/src/Components/Server/src/DependencyInjection/ServerRazorComponentsBuilderExtensions.cs b/src/Components/Server/src/DependencyInjection/ServerRazorComponentsBuilderExtensions.cs index 7b0fdc12e117..e3b79bcf7d23 100644 --- a/src/Components/Server/src/DependencyInjection/ServerRazorComponentsBuilderExtensions.cs +++ b/src/Components/Server/src/DependencyInjection/ServerRazorComponentsBuilderExtensions.cs @@ -2,11 +2,14 @@ // 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.Features; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -64,13 +67,19 @@ public override IEnumerable GetEndpointBuilders( } var endpointRouteBuilder = new EndpointRouteBuilder(Services, applicationBuilder); + var hub = endpointRouteBuilder.MapBlazorHub("/_blazor"); if (renderMode is InternalServerRenderMode { Options.ConfigureConnectionOptions: { } configureConnection }) { - endpointRouteBuilder.MapBlazorHub("/_blazor", configureConnection); - } - else - { - endpointRouteBuilder.MapBlazorHub(); + hub.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); + }); } return endpointRouteBuilder.GetEndpoints(); @@ -122,6 +131,17 @@ 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/test/E2ETest/ServerExecutionTests/WebSocketCompressionTests.cs b/src/Components/test/E2ETest/ServerExecutionTests/WebSocketCompressionTests.cs index 98d37b116aeb..712109583b71 100644 --- a/src/Components/test/E2ETest/ServerExecutionTests/WebSocketCompressionTests.cs +++ b/src/Components/test/E2ETest/ServerExecutionTests/WebSocketCompressionTests.cs @@ -76,9 +76,7 @@ public CustomConfigurationCallbackWebSocketCompressionTests( serverFixture.UpdateHostServices = services => { var configuration = services.GetService(); - configuration.ConnectionDispatcherOptions = options => - options.WebSockets.WebSocketAcceptContextFactory = context => - new Http.WebSocketAcceptContext { DangerousEnableCompression = true }; + configuration.ConnectionDispatcherOptions = context => new() { DangerousEnableCompression = true }; }; } } diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs b/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs index fd60be46b157..b0b515a3dc5c 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs +++ b/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs @@ -74,10 +74,10 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { var config = app.ApplicationServices.GetRequiredService(); options.ConfigureConnectionOptions = config.IsCompressionEnabled ? - (opts) => opts.WebSockets.WebSocketAcceptContextFactory = _ => new() { DangerousEnableCompression = true } : null; + _ => new() { DangerousEnableCompression = true } : null; options.ContentSecurityFrameAncestorPolicy = config.CspPolicy; options.ConfigureConnectionOptions = config.ConnectionDispatcherOptions ?? (config.IsCompressionEnabled ? - opts => opts.WebSockets.WebSocketAcceptContextFactory = (context) => new() { DangerousEnableCompression = true } : null); + (context) => new() { DangerousEnableCompression = true } : null); }) .AddInteractiveWebAssemblyRenderMode(options => options.PathPrefix = "/WasmMinimal"); diff --git a/src/Components/test/testassets/Components.TestServer/WebSocketCompressionConfiguration.cs b/src/Components/test/testassets/Components.TestServer/WebSocketCompressionConfiguration.cs index 291d825d4532..c14f95133aad 100644 --- a/src/Components/test/testassets/Components.TestServer/WebSocketCompressionConfiguration.cs +++ b/src/Components/test/testassets/Components.TestServer/WebSocketCompressionConfiguration.cs @@ -11,5 +11,5 @@ public class WebSocketCompressionConfiguration public string CspPolicy { get; set; } = "'self'"; - public Action ConnectionDispatcherOptions { get; set; } + public Func ConnectionDispatcherOptions { get; set; } } From bcbd6df527f3c3f0f40fa2aa385e04a6961af42a Mon Sep 17 00:00:00 2001 From: jacalvar Date: Sat, 3 Feb 2024 10:33:15 +0100 Subject: [PATCH 13/18] Undo SignalR changes --- .../src/Internal/Transports/WebSocketsServerTransport.cs | 5 ++--- .../common/Http.Connections/src/PublicAPI.Unshipped.txt | 2 -- src/SignalR/common/Http.Connections/src/WebSocketOptions.cs | 6 ------ 3 files changed, 2 insertions(+), 11 deletions(-) diff --git a/src/SignalR/common/Http.Connections/src/Internal/Transports/WebSocketsServerTransport.cs b/src/SignalR/common/Http.Connections/src/Internal/Transports/WebSocketsServerTransport.cs index 00504dc8c3f5..bc1c7c93645e 100644 --- a/src/SignalR/common/Http.Connections/src/Internal/Transports/WebSocketsServerTransport.cs +++ b/src/SignalR/common/Http.Connections/src/Internal/Transports/WebSocketsServerTransport.cs @@ -39,11 +39,10 @@ public async Task ProcessRequestAsync(HttpContext context, CancellationTok Debug.Assert(context.WebSockets.IsWebSocketRequest, "Not a websocket request"); var subProtocol = _options.SubProtocolSelector?.Invoke(context.WebSockets.WebSocketRequestedProtocols); - var acceptContext = _options.WebSocketAcceptContextFactory?.Invoke(context); - using (var ws = await (acceptContext != null ? context.WebSockets.AcceptWebSocketAsync(acceptContext) : context.WebSockets.AcceptWebSocketAsync(subProtocol))) + using (var ws = await context.WebSockets.AcceptWebSocketAsync(subProtocol)) { - Log.SocketOpened(_logger, acceptContext != null ? acceptContext.SubProtocol : subProtocol); + Log.SocketOpened(_logger, subProtocol); try { diff --git a/src/SignalR/common/Http.Connections/src/PublicAPI.Unshipped.txt b/src/SignalR/common/Http.Connections/src/PublicAPI.Unshipped.txt index fde2e9049cdf..7dc5c58110bf 100644 --- a/src/SignalR/common/Http.Connections/src/PublicAPI.Unshipped.txt +++ b/src/SignalR/common/Http.Connections/src/PublicAPI.Unshipped.txt @@ -1,3 +1 @@ #nullable enable -Microsoft.AspNetCore.Http.Connections.WebSocketOptions.WebSocketAcceptContextFactory.get -> System.Func? -Microsoft.AspNetCore.Http.Connections.WebSocketOptions.WebSocketAcceptContextFactory.set -> void diff --git a/src/SignalR/common/Http.Connections/src/WebSocketOptions.cs b/src/SignalR/common/Http.Connections/src/WebSocketOptions.cs index f542815e6475..139aad4beb91 100644 --- a/src/SignalR/common/Http.Connections/src/WebSocketOptions.cs +++ b/src/SignalR/common/Http.Connections/src/WebSocketOptions.cs @@ -26,10 +26,4 @@ public class WebSocketOptions // https://github.com/aspnet/HttpAbstractions/blob/a6bdb9b1ec6ed99978a508e71a7f131be7e4d9fb/src/Microsoft.AspNetCore.Http.Abstractions/WebSocketManager.cs#L23 // Unfortunately, IList does not implement IReadOnlyList :( public Func, string>? SubProtocolSelector { get; set; } - - /// - /// Gets or sets a delegate that will be called when a new WebSocket is established to configure additional details about the connection - /// like the use of compression. - /// - public Func? WebSocketAcceptContextFactory { get; set; } } From 60454e8fe15dd2842dcffe04b530739868f745ee Mon Sep 17 00:00:00 2001 From: jacalvar Date: Sat, 3 Feb 2024 10:56:34 +0100 Subject: [PATCH 14/18] Clean up API and fix build --- .../Server/src/Builder/ServerComponentsEndpointOptions.cs | 8 ++++---- ...rRazorComponentsEndpointConventionBuilderExtensions.cs | 2 +- .../ServerRazorComponentsBuilderExtensions.cs | 2 +- src/Components/Server/src/PublicAPI.Unshipped.txt | 4 ++-- .../RazorComponentEndpointsStartup.cs | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Components/Server/src/Builder/ServerComponentsEndpointOptions.cs b/src/Components/Server/src/Builder/ServerComponentsEndpointOptions.cs index adae30ec39f1..f77fd8cd6d66 100644 --- a/src/Components/Server/src/Builder/ServerComponentsEndpointOptions.cs +++ b/src/Components/Server/src/Builder/ServerComponentsEndpointOptions.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Connections; namespace Microsoft.AspNetCore.Components.Server; @@ -13,7 +12,7 @@ public class ServerComponentsEndpointOptions { /// /// Gets or sets the frame-ancestors Content-Security-Policy to set in the - /// when is set. + /// when is set. /// /// /// Setting this value to will prevent the policy from being @@ -33,11 +32,12 @@ public class ServerComponentsEndpointOptions public string? ContentSecurityFrameAncestorPolicy { get; set; } = "'self'"; /// - /// Gets or sets a callback to configure the underlying . + /// 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? ConfigureConnectionOptions { get; set; } = EnableCompressionDefaults; + 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 a977e58e951a..d56e7330636d 100644 --- a/src/Components/Server/src/Builder/ServerRazorComponentsEndpointConventionBuilderExtensions.cs +++ b/src/Components/Server/src/Builder/ServerRazorComponentsEndpointConventionBuilderExtensions.cs @@ -38,7 +38,7 @@ public static RazorComponentsEndpointConventionBuilder AddInteractiveServerRende ComponentEndpointConventionBuilderHelper.AddRenderMode(builder, new InternalServerRenderMode(options)); - if (options.ConfigureConnectionOptions is not null && options.ContentSecurityFrameAncestorPolicy != null) + if (options.ConfigureWebsocketOptions is not null && options.ContentSecurityFrameAncestorPolicy != null) { builder.AddEndpointFilter(new RequireCspFilter(options.ContentSecurityFrameAncestorPolicy)); } diff --git a/src/Components/Server/src/DependencyInjection/ServerRazorComponentsBuilderExtensions.cs b/src/Components/Server/src/DependencyInjection/ServerRazorComponentsBuilderExtensions.cs index e3b79bcf7d23..79d1462e4601 100644 --- a/src/Components/Server/src/DependencyInjection/ServerRazorComponentsBuilderExtensions.cs +++ b/src/Components/Server/src/DependencyInjection/ServerRazorComponentsBuilderExtensions.cs @@ -68,7 +68,7 @@ public override IEnumerable GetEndpointBuilders( var endpointRouteBuilder = new EndpointRouteBuilder(Services, applicationBuilder); var hub = endpointRouteBuilder.MapBlazorHub("/_blazor"); - if (renderMode is InternalServerRenderMode { Options.ConfigureConnectionOptions: { } configureConnection }) + if (renderMode is InternalServerRenderMode { Options.ConfigureWebsocketOptions: { } configureConnection }) { hub.AddEndpointFilter(async (context, next) => { diff --git a/src/Components/Server/src/PublicAPI.Unshipped.txt b/src/Components/Server/src/PublicAPI.Unshipped.txt index 2dceac0f7cff..6a7c825f9e13 100644 --- a/src/Components/Server/src/PublicAPI.Unshipped.txt +++ b/src/Components/Server/src/PublicAPI.Unshipped.txt @@ -1,7 +1,7 @@ #nullable enable Microsoft.AspNetCore.Components.Server.ServerComponentsEndpointOptions -Microsoft.AspNetCore.Components.Server.ServerComponentsEndpointOptions.ConfigureConnectionOptions.get -> System.Action? -Microsoft.AspNetCore.Components.Server.ServerComponentsEndpointOptions.ConfigureConnectionOptions.set -> void +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 diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs b/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs index b0b515a3dc5c..0a7e14db50e3 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs +++ b/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs @@ -73,10 +73,10 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) .AddInteractiveServerRenderMode(options => { var config = app.ApplicationServices.GetRequiredService(); - options.ConfigureConnectionOptions = config.IsCompressionEnabled ? + options.ConfigureWebsocketOptions = config.IsCompressionEnabled ? _ => new() { DangerousEnableCompression = true } : null; options.ContentSecurityFrameAncestorPolicy = config.CspPolicy; - options.ConfigureConnectionOptions = config.ConnectionDispatcherOptions ?? (config.IsCompressionEnabled ? + options.ConfigureWebsocketOptions = config.ConnectionDispatcherOptions ?? (config.IsCompressionEnabled ? (context) => new() { DangerousEnableCompression = true } : null); }) .AddInteractiveWebAssemblyRenderMode(options => options.PathPrefix = "/WasmMinimal"); From ca95d24ace7eba164c0fb13aeed06b6a4274b4b8 Mon Sep 17 00:00:00 2001 From: jacalvar Date: Sat, 3 Feb 2024 11:21:04 +0100 Subject: [PATCH 15/18] Update tests --- .../ServerRazorComponentsBuilderExtensions.cs | 1 + .../AuthenticationStartup.cs | 25 ++++++++++++++++- .../DeferredComponentContentStartup.cs | 22 ++++++++++++++- .../Components.TestServer/HotReloadStartup.cs | 22 ++++++++++++++- .../InternationalizationStartup.cs | 22 ++++++++++++++- .../LockedNavigationStartup.cs | 22 ++++++++++++++- .../MultipleComponents.cs | 22 ++++++++++++++- .../PrerenderedStartup.cs | 22 ++++++++++++++- .../Components.TestServer/SaveState.cs | 26 +++++++++++++++++- .../Components.TestServer/ServerStartup.cs | 25 ++++++++++++++++- .../TransportsServerStartup.cs | 27 ++++++++++++++++++- 11 files changed, 226 insertions(+), 10 deletions(-) diff --git a/src/Components/Server/src/DependencyInjection/ServerRazorComponentsBuilderExtensions.cs b/src/Components/Server/src/DependencyInjection/ServerRazorComponentsBuilderExtensions.cs index 79d1462e4601..de796dd7d35d 100644 --- a/src/Components/Server/src/DependencyInjection/ServerRazorComponentsBuilderExtensions.cs +++ b/src/Components/Server/src/DependencyInjection/ServerRazorComponentsBuilderExtensions.cs @@ -133,6 +133,7 @@ internal IEnumerable GetEndpoints() } } + private sealed class ServerComponentsSocketFeature(IHttpWebSocketFeature originalFeature) : IHttpWebSocketFeature { public bool IsWebSocketRequest => originalFeature.IsWebSocketRequest; diff --git a/src/Components/test/testassets/Components.TestServer/AuthenticationStartup.cs b/src/Components/test/testassets/Components.TestServer/AuthenticationStartup.cs index 28818516e080..6f859d733c7c 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; @@ -58,11 +60,32 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { endpoints.MapControllers(); endpoints.MapRazorPages(); - endpoints.MapBlazorHub(options => options.WebSockets.WebSocketAcceptContextFactory = context => new WebSocketAcceptContext { DangerousEnableCompression = true }); + 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 49beda07e299..ba53515345d7 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; @@ -39,8 +41,26 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { endpoints.MapRazorPages(); endpoints.MapFallbackToPage("/DeferredComponentContentHost"); - endpoints.MapBlazorHub(options => options.WebSockets.WebSocketAcceptContextFactory = context => new WebSocketAcceptContext { DangerousEnableCompression = true }); + 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 4ee59a692f84..5ffd44e00f11 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; @@ -37,8 +39,26 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseEndpoints(endpoints => { endpoints.MapControllers(); - endpoints.MapBlazorHub(options => options.WebSockets.WebSocketAcceptContextFactory = context => new WebSocketAcceptContext { DangerousEnableCompression = true }); + 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 c7e4e325cf7b..177c4c010d96 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; @@ -52,9 +54,27 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseEndpoints(endpoints => { endpoints.MapControllers(); - endpoints.MapBlazorHub(options => options.WebSockets.WebSocketAcceptContextFactory = context => new WebSocketAcceptContext { DangerousEnableCompression = true }); + 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 4d3f548135d6..7292eaf29797 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; @@ -39,8 +41,26 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { endpoints.MapRazorPages(); endpoints.MapFallbackToPage("/LockedNavigationHost"); - endpoints.MapBlazorHub(options => options.WebSockets.WebSocketAcceptContextFactory = context => new WebSocketAcceptContext { DangerousEnableCompression = true }); + 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 1c8a46018293..e98fa3842e80 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; @@ -58,8 +60,26 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { endpoints.MapRazorPages(); endpoints.MapFallbackToPage("/MultipleComponents"); - endpoints.MapBlazorHub(options => options.WebSockets.WebSocketAcceptContextFactory = context => new WebSocketAcceptContext { DangerousEnableCompression = true }); + 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 b95dda7c2ad1..46932d92a40f 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; @@ -48,8 +50,26 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { endpoints.MapRazorPages(); endpoints.MapFallbackToPage("/PrerenderedHost"); - endpoints.MapBlazorHub(options => options.WebSockets.WebSocketAcceptContextFactory = context => new WebSocketAcceptContext { DangerousEnableCompression = true }); + 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/SaveState.cs b/src/Components/test/testassets/Components.TestServer/SaveState.cs index 87747cf99539..5fff6668231e 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; @@ -41,8 +43,30 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseEndpoints(endpoints => { endpoints.MapRazorPages(); - endpoints.MapBlazorHub(options => options.WebSockets.WebSocketAcceptContextFactory = context => new WebSocketAcceptContext { DangerousEnableCompression = true }); + 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 38e82f84d6af..09144b137949 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; @@ -78,10 +80,31 @@ public virtual void Configure(IApplicationBuilder app, IWebHostEnvironment env, app.UseRouting(); app.UseEndpoints(endpoints => { - endpoints.MapBlazorHub(options => options.WebSockets.WebSocketAcceptContextFactory = context => new WebSocketAcceptContext { DangerousEnableCompression = true }); + 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 f5510cd3f517..c03df2c81a35 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 @@ -25,7 +29,17 @@ public override void Configure(IApplicationBuilder app, IWebHostEnvironment env, app.UseRouting(); app.UseEndpoints(endpoints => { - endpoints.MapBlazorHub(options => options.WebSockets.WebSocketAcceptContextFactory = context => new WebSocketAcceptContext { DangerousEnableCompression = true }); + 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"); }); }); @@ -60,4 +74,15 @@ public override void Configure(IApplicationBuilder app, IWebHostEnvironment env, }); }); } + + private sealed class ServerComponentsSocketFeature(IHttpWebSocketFeature originalFeature) : IHttpWebSocketFeature + { + public bool IsWebSocketRequest => originalFeature.IsWebSocketRequest; + + public Task AcceptAsync(WebSocketAcceptContext context) + { + context.DangerousEnableCompression = true; + return originalFeature.AcceptAsync(context); + } + } } From 9384ce744decb984451d28dd38960565926e64f0 Mon Sep 17 00:00:00 2001 From: jacalvar Date: Sat, 3 Feb 2024 12:25:08 +0100 Subject: [PATCH 16/18] Fix tests --- ...ComponentEndpointRouteBuilderExtensions.cs | 36 +++++++++++++++++-- .../ServerRazorComponentsBuilderExtensions.cs | 2 +- .../WebSocketCompressionConfiguration.cs | 5 ++- 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/src/Components/Server/src/Builder/ComponentEndpointRouteBuilderExtensions.cs b/src/Components/Server/src/Builder/ComponentEndpointRouteBuilderExtensions.cs index 2ce561133ef4..8d985f224664 100644 --- a/src/Components/Server/src/Builder/ComponentEndpointRouteBuilderExtensions.cs +++ b/src/Components/Server/src/Builder/ComponentEndpointRouteBuilderExtensions.cs @@ -1,9 +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 System.Net.WebSockets; using Microsoft.AspNetCore.Components.Server; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Connections; +using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.SignalR; using Microsoft.AspNetCore.StaticFiles; @@ -76,7 +78,26 @@ public static ComponentEndpointConventionBuilder MapBlazorHub( ArgumentNullException.ThrowIfNull(path); ArgumentNullException.ThrowIfNull(configureOptions); - var hubEndpoint = endpoints.MapHub(path, configureOptions); + var hubEndpoint = endpoints.MapHub(path, configureOptions) + .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); + }); + + hubEndpoint.Add(c => + { + var originalDelegate = c.RequestDelegate; + var builder = endpoints.CreateApplicationBuilder(); + builder.UseWebSockets(); + builder.Run(originalDelegate); + c.RequestDelegate = builder.Build(); + }); var disconnectEndpoint = endpoints.Map( (path.EndsWith('/') ? path : path + "/") + "disconnect/", @@ -115,7 +136,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()) @@ -125,4 +146,15 @@ private static IEndpointConventionBuilder GetBlazorEndpoint(IEndpointRouteBuilde return blazorEndpoint; } + + 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/DependencyInjection/ServerRazorComponentsBuilderExtensions.cs b/src/Components/Server/src/DependencyInjection/ServerRazorComponentsBuilderExtensions.cs index de796dd7d35d..4bb2c06142dc 100644 --- a/src/Components/Server/src/DependencyInjection/ServerRazorComponentsBuilderExtensions.cs +++ b/src/Components/Server/src/DependencyInjection/ServerRazorComponentsBuilderExtensions.cs @@ -63,7 +63,7 @@ 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); diff --git a/src/Components/test/testassets/Components.TestServer/WebSocketCompressionConfiguration.cs b/src/Components/test/testassets/Components.TestServer/WebSocketCompressionConfiguration.cs index c14f95133aad..e6b0344e83ed 100644 --- a/src/Components/test/testassets/Components.TestServer/WebSocketCompressionConfiguration.cs +++ b/src/Components/test/testassets/Components.TestServer/WebSocketCompressionConfiguration.cs @@ -11,5 +11,8 @@ public class WebSocketCompressionConfiguration public string CspPolicy { get; set; } = "'self'"; - public Func ConnectionDispatcherOptions { get; set; } + public Func ConnectionDispatcherOptions { get; set; } = (context) => new WebSocketAcceptContext + { + DangerousEnableCompression = true + }; } From ed7087f868a5f376773a7caae463d6b5f51899ce Mon Sep 17 00:00:00 2001 From: jacalvar Date: Sat, 3 Feb 2024 13:32:54 +0100 Subject: [PATCH 17/18] Cleanup implementation --- .../Builder/RazorComponentEndpointFactory.cs | 48 +++---------------- ...ComponentEndpointRouteBuilderExtensions.cs | 34 +------------ ...entsEndpointConventionBuilderExtensions.cs | 28 ++++++----- .../ServerRazorComponentsBuilderExtensions.cs | 34 +++++++++++-- .../WebSocketCompressionTests.cs | 1 + .../RazorComponentEndpointsStartup.cs | 2 + 6 files changed, 56 insertions(+), 91 deletions(-) diff --git a/src/Components/Endpoints/src/Builder/RazorComponentEndpointFactory.cs b/src/Components/Endpoints/src/Builder/RazorComponentEndpointFactory.cs index 5535809638aa..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,48 +73,6 @@ internal void AddEndpoints( // The display name is for debug purposes by endpoint routing. builder.DisplayName = $"{builder.RoutePattern.RawText} ({pageDefinition.DisplayName})"; - ApplyEndpointFilters(builder); - endpoints.Add(builder.Build()); } - - private static void ApplyEndpointFilters(RouteEndpointBuilder builder) - { - if (builder.FilterFactories.Count > 0) - { - EndpointFilterDelegate del = static async invocationContext => - { - var httpContext = invocationContext.HttpContext; - var invoker = httpContext.RequestServices.GetRequiredService(); - await invoker.Render(httpContext); - return null; - }; - - var context = new EndpointFilterFactoryContext - { - MethodInfo = typeof(IRazorComponentEndpointInvoker).GetMethod(nameof(IRazorComponentEndpointInvoker.Render))!, - ApplicationServices = builder.ApplicationServices, - }; - - for (var i = builder.FilterFactories.Count - 1; i >= 0; i--) - { - var filterFactory = builder.FilterFactories[i]; - del = filterFactory(context, del); - } - - builder.RequestDelegate = async context => - { - var invocationContext = EndpointFilterInvocationContext.Create(context); - await del(invocationContext); - }; - } - else - { - builder.RequestDelegate = static httpContext => - { - var invoker = httpContext.RequestServices.GetRequiredService(); - return invoker.Render(httpContext); - }; - } - } } diff --git a/src/Components/Server/src/Builder/ComponentEndpointRouteBuilderExtensions.cs b/src/Components/Server/src/Builder/ComponentEndpointRouteBuilderExtensions.cs index 8d985f224664..0173fda4315d 100644 --- a/src/Components/Server/src/Builder/ComponentEndpointRouteBuilderExtensions.cs +++ b/src/Components/Server/src/Builder/ComponentEndpointRouteBuilderExtensions.cs @@ -1,11 +1,9 @@ // 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.Components.Server; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Connections; -using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.SignalR; using Microsoft.AspNetCore.StaticFiles; @@ -78,26 +76,7 @@ public static ComponentEndpointConventionBuilder MapBlazorHub( ArgumentNullException.ThrowIfNull(path); ArgumentNullException.ThrowIfNull(configureOptions); - var hubEndpoint = endpoints.MapHub(path, configureOptions) - .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); - }); - - hubEndpoint.Add(c => - { - var originalDelegate = c.RequestDelegate; - var builder = endpoints.CreateApplicationBuilder(); - builder.UseWebSockets(); - builder.Run(originalDelegate); - c.RequestDelegate = builder.Build(); - }); + var hubEndpoint = endpoints.MapHub(path, configureOptions); var disconnectEndpoint = endpoints.Map( (path.EndsWith('/') ? path : path + "/") + "disconnect/", @@ -146,15 +125,4 @@ private static IEndpointConventionBuilder GetBlazorEndpoint(IEndpointRouteBuilde return blazorEndpoint; } - - 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/Builder/ServerRazorComponentsEndpointConventionBuilderExtensions.cs b/src/Components/Server/src/Builder/ServerRazorComponentsEndpointConventionBuilderExtensions.cs index d56e7330636d..e51496b05105 100644 --- a/src/Components/Server/src/Builder/ServerRazorComponentsEndpointConventionBuilderExtensions.cs +++ b/src/Components/Server/src/Builder/ServerRazorComponentsEndpointConventionBuilderExtensions.cs @@ -1,10 +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 Microsoft.AspNetCore.Components.Endpoints; using Microsoft.AspNetCore.Components.Endpoints.Infrastructure; using Microsoft.AspNetCore.Components.Server; using Microsoft.AspNetCore.Components.Web; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.SignalR; namespace Microsoft.AspNetCore.Builder; @@ -40,18 +40,24 @@ public static RazorComponentsEndpointConventionBuilder AddInteractiveServerRende if (options.ConfigureWebsocketOptions is not null && options.ContentSecurityFrameAncestorPolicy != null) { - builder.AddEndpointFilter(new RequireCspFilter(options.ContentSecurityFrameAncestorPolicy)); + 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; } - - private sealed class RequireCspFilter(string policy) : IEndpointFilter - { - public ValueTask InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next) - { - context.HttpContext.Response.Headers.Add("Content-Security-Policy", $"frame-ancestors {policy}"); - return next(context); - } - } } diff --git a/src/Components/Server/src/DependencyInjection/ServerRazorComponentsBuilderExtensions.cs b/src/Components/Server/src/DependencyInjection/ServerRazorComponentsBuilderExtensions.cs index 4bb2c06142dc..5f30a0997719 100644 --- a/src/Components/Server/src/DependencyInjection/ServerRazorComponentsBuilderExtensions.cs +++ b/src/Components/Server/src/DependencyInjection/ServerRazorComponentsBuilderExtensions.cs @@ -9,8 +9,10 @@ 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; @@ -68,17 +70,39 @@ public override IEnumerable GetEndpointBuilders( var endpointRouteBuilder = new EndpointRouteBuilder(Services, applicationBuilder); var hub = endpointRouteBuilder.MapBlazorHub("/_blazor"); + if (renderMode is InternalServerRenderMode { Options.ConfigureWebsocketOptions: { } configureConnection }) { - hub.AddEndpointFilter(async (context, next) => + hub.Finally(c => { - if (context.HttpContext.WebSockets.IsWebSocketRequest) + for (var i = 0; i < c.Metadata.Count; i++) { - var currentFeature = context.HttpContext.Features.Get(); + var metadata = c.Metadata[i]; + if (metadata is NegotiateMetadata) + { + return; + } - context.HttpContext.Features.Set(new ServerComponentsSocketFeature(currentFeature!)); + 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 await next(context); }); } diff --git a/src/Components/test/E2ETest/ServerExecutionTests/WebSocketCompressionTests.cs b/src/Components/test/E2ETest/ServerExecutionTests/WebSocketCompressionTests.cs index 712109583b71..a2bf52f68956 100644 --- a/src/Components/test/E2ETest/ServerExecutionTests/WebSocketCompressionTests.cs +++ b/src/Components/test/E2ETest/ServerExecutionTests/WebSocketCompressionTests.cs @@ -93,6 +93,7 @@ public CompressionDisabledWebSocketCompressionTests( { var configuration = services.GetService(); configuration.IsCompressionEnabled = false; + configuration.ConnectionDispatcherOptions = null; }; } } diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs b/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs index 0a7e14db50e3..d6d8cdd6c0c7 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs +++ b/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs @@ -75,7 +75,9 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 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); }) From f7452f4d1fa1700a11415fb43660da020c7a4489 Mon Sep 17 00:00:00 2001 From: jacalvar Date: Sat, 3 Feb 2024 13:49:47 +0100 Subject: [PATCH 18/18] Cleanup implementation --- .../Components.TestServer/AuthenticationStartup.cs | 1 + .../DeferredComponentContentStartup.cs | 1 + .../Components.TestServer/HotReloadStartup.cs | 1 + .../InternationalizationStartup.cs | 1 + .../LockedNavigationStartup.cs | 1 + .../Components.TestServer/MultipleComponents.cs | 1 + .../Components.TestServer/PrerenderedStartup.cs | 1 + .../testassets/Components.TestServer/SaveState.cs | 1 + .../Components.TestServer/ServerStartup.cs | 1 + .../TransportsServerStartup.cs | 13 ++++++++++++- .../test/testassets/ComponentsApp.Server/Startup.cs | 2 +- 11 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/Components/test/testassets/Components.TestServer/AuthenticationStartup.cs b/src/Components/test/testassets/Components.TestServer/AuthenticationStartup.cs index 6f859d733c7c..f92842b82d1b 100644 --- a/src/Components/test/testassets/Components.TestServer/AuthenticationStartup.cs +++ b/src/Components/test/testassets/Components.TestServer/AuthenticationStartup.cs @@ -56,6 +56,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseStaticFiles(); app.UseRouting(); + app.UseWebSockets(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); diff --git a/src/Components/test/testassets/Components.TestServer/DeferredComponentContentStartup.cs b/src/Components/test/testassets/Components.TestServer/DeferredComponentContentStartup.cs index ba53515345d7..ae1e9dce2e65 100644 --- a/src/Components/test/testassets/Components.TestServer/DeferredComponentContentStartup.cs +++ b/src/Components/test/testassets/Components.TestServer/DeferredComponentContentStartup.cs @@ -37,6 +37,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseAuthentication(); app.UseRouting(); + app.UseWebSockets(); app.UseEndpoints(endpoints => { endpoints.MapRazorPages(); diff --git a/src/Components/test/testassets/Components.TestServer/HotReloadStartup.cs b/src/Components/test/testassets/Components.TestServer/HotReloadStartup.cs index 5ffd44e00f11..d9b275f5851a 100644 --- a/src/Components/test/testassets/Components.TestServer/HotReloadStartup.cs +++ b/src/Components/test/testassets/Components.TestServer/HotReloadStartup.cs @@ -36,6 +36,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseBlazorFrameworkFiles(); app.UseStaticFiles(); app.UseRouting(); + app.UseWebSockets(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); diff --git a/src/Components/test/testassets/Components.TestServer/InternationalizationStartup.cs b/src/Components/test/testassets/Components.TestServer/InternationalizationStartup.cs index 177c4c010d96..2ed1cfdb3ff2 100644 --- a/src/Components/test/testassets/Components.TestServer/InternationalizationStartup.cs +++ b/src/Components/test/testassets/Components.TestServer/InternationalizationStartup.cs @@ -51,6 +51,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) }); app.UseRouting(); + app.UseWebSockets(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); diff --git a/src/Components/test/testassets/Components.TestServer/LockedNavigationStartup.cs b/src/Components/test/testassets/Components.TestServer/LockedNavigationStartup.cs index 7292eaf29797..490e24e7a3c1 100644 --- a/src/Components/test/testassets/Components.TestServer/LockedNavigationStartup.cs +++ b/src/Components/test/testassets/Components.TestServer/LockedNavigationStartup.cs @@ -37,6 +37,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseAuthentication(); app.UseRouting(); + app.UseWebSockets(); app.UseEndpoints(endpoints => { endpoints.MapRazorPages(); diff --git a/src/Components/test/testassets/Components.TestServer/MultipleComponents.cs b/src/Components/test/testassets/Components.TestServer/MultipleComponents.cs index e98fa3842e80..1069b6e8df65 100644 --- a/src/Components/test/testassets/Components.TestServer/MultipleComponents.cs +++ b/src/Components/test/testassets/Components.TestServer/MultipleComponents.cs @@ -56,6 +56,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseAuthentication(); app.UseRouting(); + app.UseWebSockets(); app.UseEndpoints(endpoints => { endpoints.MapRazorPages(); diff --git a/src/Components/test/testassets/Components.TestServer/PrerenderedStartup.cs b/src/Components/test/testassets/Components.TestServer/PrerenderedStartup.cs index 46932d92a40f..e0c126125526 100644 --- a/src/Components/test/testassets/Components.TestServer/PrerenderedStartup.cs +++ b/src/Components/test/testassets/Components.TestServer/PrerenderedStartup.cs @@ -46,6 +46,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseAuthentication(); app.UseRouting(); + app.UseWebSockets(); app.UseEndpoints(endpoints => { endpoints.MapRazorPages(); diff --git a/src/Components/test/testassets/Components.TestServer/SaveState.cs b/src/Components/test/testassets/Components.TestServer/SaveState.cs index 5fff6668231e..b5bb7819fbd2 100644 --- a/src/Components/test/testassets/Components.TestServer/SaveState.cs +++ b/src/Components/test/testassets/Components.TestServer/SaveState.cs @@ -40,6 +40,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseBlazorFrameworkFiles(); app.UseStaticFiles(); app.UseRouting(); + app.UseWebSockets(); app.UseEndpoints(endpoints => { endpoints.MapRazorPages(); diff --git a/src/Components/test/testassets/Components.TestServer/ServerStartup.cs b/src/Components/test/testassets/Components.TestServer/ServerStartup.cs index 09144b137949..ca1db15a993c 100644 --- a/src/Components/test/testassets/Components.TestServer/ServerStartup.cs +++ b/src/Components/test/testassets/Components.TestServer/ServerStartup.cs @@ -78,6 +78,7 @@ public virtual void Configure(IApplicationBuilder app, IWebHostEnvironment env, app.UseStaticFiles(); app.UseRouting(); + app.UseWebSockets(); app.UseEndpoints(endpoints => { endpoints.MapBlazorHub() diff --git a/src/Components/test/testassets/Components.TestServer/TransportsServerStartup.cs b/src/Components/test/testassets/Components.TestServer/TransportsServerStartup.cs index c03df2c81a35..f76cf3073ed7 100644 --- a/src/Components/test/testassets/Components.TestServer/TransportsServerStartup.cs +++ b/src/Components/test/testassets/Components.TestServer/TransportsServerStartup.cs @@ -27,6 +27,7 @@ public override void Configure(IApplicationBuilder app, IWebHostEnvironment env, app.UseStaticFiles(); app.UseRouting(); + app.UseWebSockets(); app.UseEndpoints(endpoints => { endpoints.MapBlazorHub() @@ -64,12 +65,22 @@ 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"); }); }); diff --git a/src/Components/test/testassets/ComponentsApp.Server/Startup.cs b/src/Components/test/testassets/ComponentsApp.Server/Startup.cs index 57659998de08..fbfb8fef4959 100644 --- a/src/Components/test/testassets/ComponentsApp.Server/Startup.cs +++ b/src/Components/test/testassets/ComponentsApp.Server/Startup.cs @@ -34,7 +34,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { endpoints.MapRazorPages(); endpoints.MapControllers(); - endpoints.MapBlazorHub(options => options.WebSockets.WebSocketAcceptContextFactory = context => new WebSocketAcceptContext { DangerousEnableCompression = true }); + endpoints.MapBlazorHub(); endpoints.MapFallbackToPage("/_Host"); }); }