Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public class ConfigurationServerController : ServerControllerBase
private readonly GlobalSettings _globalSettings;
private readonly IBackOfficeExternalLoginProviders _externalLoginProviders;
private readonly IHostingEnvironment _hostingEnvironment;
private readonly SignalRSettings _signalRSettings;

/// <summary>
/// Initializes a new instance of the <see cref="ConfigurationServerController"/> class.
Expand All @@ -36,13 +37,38 @@ public class ConfigurationServerController : ServerControllerBase
/// <param name="globalSettings">The global settings options.</param>
/// <param name="externalLoginProviders">The external login providers for back office.</param>
/// <param name="hostingEnvironment">The hosting environment.</param>
/// <param name="signalRSettings">The SignalR settings options.</param>
[ActivatorUtilitiesConstructor]
public ConfigurationServerController(IOptions<SecuritySettings> securitySettings, IOptions<GlobalSettings> globalSettings, IBackOfficeExternalLoginProviders externalLoginProviders, IHostingEnvironment hostingEnvironment)
public ConfigurationServerController(
IOptions<SecuritySettings> securitySettings,
IOptions<GlobalSettings> globalSettings,
IBackOfficeExternalLoginProviders externalLoginProviders,
IHostingEnvironment hostingEnvironment,
IOptions<SignalRSettings> signalRSettings)
{
_securitySettings = securitySettings.Value;
_globalSettings = globalSettings.Value;
_externalLoginProviders = externalLoginProviders;
_hostingEnvironment = hostingEnvironment;
_signalRSettings = signalRSettings.Value;
}

/// <summary>
/// Initializes a new instance of the <see cref="Umbraco.Cms.Api.Management.Controllers.Server.ConfigurationServerController"/> class.
/// </summary>
/// <param name="securitySettings">The <see cref="SecuritySettings"/> options.</param>
/// <param name="globalSettings">The <see cref="GlobalSettings"/> options.</param>
/// <param name="externalLoginProviders">The external login providers used for back office authentication.</param>
/// <param name="hostingEnvironment">The hosting environment.</param>
[Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 19.")]
public ConfigurationServerController(IOptions<SecuritySettings> securitySettings, IOptions<GlobalSettings> globalSettings, IBackOfficeExternalLoginProviders externalLoginProviders, IHostingEnvironment hostingEnvironment)
: this(
securitySettings,
globalSettings,
externalLoginProviders,
hostingEnvironment,
StaticServiceProvider.Instance.GetRequiredService<IOptions<SignalRSettings>>())
{
}

/// <summary>
Expand Down Expand Up @@ -78,6 +104,10 @@ public Task<IActionResult> Configuration(CancellationToken cancellationToken)
VersionCheckPeriod = _globalSettings.VersionCheckPeriod,
AllowLocalLogin = _externalLoginProviders.HasDenyLocalLogin() is false,
UmbracoCssPath = _hostingEnvironment.ToAbsolute(_globalSettings.UmbracoCssPath),
SignalR = new SignalRClientSettingsResponseModel
{
SkipNegotiation = _signalRSettings.ClientShouldSkipNegotiation,
},
};

return Task.FromResult<IActionResult>(Ok(responseModel));
Expand Down
42 changes: 21 additions & 21 deletions src/Umbraco.Cms.Api.Management/OpenApi.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

31 changes: 27 additions & 4 deletions src/Umbraco.Cms.Api.Management/Routing/BackOfficeAreaRoutes.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http.Connections;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Api.Management.Controllers.Security;
using Umbraco.Cms.Api.Management.ServerEvents;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Web.Common.Routing;
using Umbraco.Extensions;
Expand All @@ -15,12 +20,22 @@ namespace Umbraco.Cms.Api.Management.Routing;
public sealed class BackOfficeAreaRoutes : IAreaRoutes
{
private readonly IRuntimeState _runtimeState;
private readonly SignalRSettings _signalRSettings;

/// <summary>
/// Initializes a new instance of the <see cref="BackOfficeAreaRoutes" /> class.
/// </summary>
public BackOfficeAreaRoutes(IRuntimeState runtimeState)
=> _runtimeState = runtimeState;
[Obsolete("Use the non obsoleted constructor instead. Scheduled for removal in v19")]
Comment thread
Migaroez marked this conversation as resolved.
Outdated
public BackOfficeAreaRoutes(IRuntimeState runtimeState): this(runtimeState, StaticServiceProvider.Instance.GetRequiredService<IOptions<SignalRSettings>>()){}
Comment thread
Migaroez marked this conversation as resolved.
Outdated

/// <summary>
/// Initializes a new instance of the <see cref="BackOfficeAreaRoutes" /> class.
/// </summary>
public BackOfficeAreaRoutes(IRuntimeState runtimeState, IOptions<SignalRSettings> signalRSettings)
{
_runtimeState = runtimeState;
_signalRSettings = signalRSettings.Value;
}


/// <inheritdoc />
Expand All @@ -30,8 +45,16 @@ public void CreateRoutes(IEndpointRouteBuilder endpoints)
{
MapMinimalBackOffice(endpoints);

endpoints.MapHub<BackofficeHub>(Constants.System.UmbracoPathSegment + Constants.Web.BackofficeSignalRHub);
endpoints.MapHub<ServerEventHub>(Constants.System.UmbracoPathSegment + Constants.Web.ServerEventSignalRHub);
endpoints.MapHub<BackofficeHub>(Constants.System.UmbracoPathSegment + Constants.Web.BackofficeSignalRHub, ConfigureHubEndpoint);
endpoints.MapHub<ServerEventHub>(Constants.System.UmbracoPathSegment + Constants.Web.ServerEventSignalRHub, ConfigureHubEndpoint);
}
}

private void ConfigureHubEndpoint(HttpConnectionDispatcherOptions options)
Comment thread
Migaroez marked this conversation as resolved.
Outdated
{
if (_signalRSettings.ClientShouldSkipNegotiation)
{
options.Transports = HttpTransportType.WebSockets;
Comment thread
AndyButland marked this conversation as resolved.
Outdated
}
}

Expand Down
35 changes: 33 additions & 2 deletions src/Umbraco.Cms.Api.Management/Routing/PreviewRoutes.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http.Connections;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Api.Management.Preview;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Web.Common.Routing;

Expand All @@ -13,13 +18,30 @@ namespace Umbraco.Cms.Api.Management.Routing;
public sealed class PreviewRoutes : IAreaRoutes
{
private readonly IRuntimeState _runtimeState;
private readonly SignalRSettings _signalRSettings;

/// <summary>
/// Initializes a new instance of the <see cref="Umbraco.Cms.Api.Management.Routing.PreviewRoutes"/> class, configuring preview routing based on the application's runtime state.
/// </summary>
/// <param name="runtimeState">An instance representing the current runtime state of the Umbraco application.</param>
[Obsolete("Use the non obsoleted constructor instead. Scheduled for removal in v19")]
Comment thread
Migaroez marked this conversation as resolved.
Outdated
public PreviewRoutes(IRuntimeState runtimeState)
=> _runtimeState = runtimeState;
: this(
runtimeState,
StaticServiceProvider.Instance.GetRequiredService<IOptions<SignalRSettings>>())
{
}

/// <summary>
/// Initializes a new instance of the <see cref="Umbraco.Cms.Api.Management.Routing.PreviewRoutes"/> class, configuring preview routing based on the application's runtime state.
/// </summary>
/// <param name="runtimeState">An instance representing the current runtime state of the Umbraco application.</param>
/// <param name="signalRSettings">The SignalR settings options.</param>
public PreviewRoutes(IRuntimeState runtimeState, IOptions<SignalRSettings> signalRSettings)
{
_runtimeState = runtimeState;
_signalRSettings = signalRSettings.Value;
}

/// <summary>
/// Creates the preview routes on the specified endpoint route builder.
Expand All @@ -29,7 +51,15 @@ public void CreateRoutes(IEndpointRouteBuilder endpoints)
{
if (_runtimeState.Level is RuntimeLevel.Install or RuntimeLevel.Upgrade or RuntimeLevel.Upgrading or RuntimeLevel.Run)
{
endpoints.MapHub<PreviewHub>(GetPreviewHubRoute());
endpoints.MapHub<PreviewHub>(GetPreviewHubRoute(), ConfigureHubEndpoint);
}
}

private void ConfigureHubEndpoint(HttpConnectionDispatcherOptions options)
{
if (_signalRSettings.ClientShouldSkipNegotiation)
{
options.Transports = HttpTransportType.WebSockets;
}
}

Expand All @@ -41,3 +71,4 @@ public void CreateRoutes(IEndpointRouteBuilder endpoints)
/// </returns>
public string GetPreviewHubRoute() => $"/{Constants.System.UmbracoPathSegment}/{nameof(PreviewHub)}";
}

Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,9 @@ public class ServerConfigurationResponseModel
/// Gets or sets the relative or absolute path to the Umbraco CSS file used by the application.
/// </summary>
public string UmbracoCssPath { get; set; } = string.Empty;

/// <summary>
/// Gets or sets the client-side SignalR settings.
/// </summary>
public SignalRClientSettingsResponseModel SignalR { get; set; } = new();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace Umbraco.Cms.Api.Management.ViewModels.Server;

/// <summary>
/// Represents client-side SignalR settings returned by the server configuration endpoint.
/// </summary>
public class SignalRClientSettingsResponseModel
{
/// <summary>Gets or sets a value indicating whether the client should skip the SignalR negotiate round-trip.</summary>
public bool SkipNegotiation { get; set; }
}
38 changes: 38 additions & 0 deletions src/Umbraco.Core/Configuration/Models/SignalRSettings.cs
Comment thread
Migaroez marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using System.ComponentModel;

namespace Umbraco.Cms.Core.Configuration.Models;

/// <summary>
/// Typed configuration options for SignalR settings.
/// </summary>
/// <remarks>
/// <para>
/// When <see cref="ClientShouldSkipNegotiation"/> is enabled, all hub endpoints are restricted
/// to WebSocket transport and the client skips the negotiate round-trip. The setting is forwarded
/// to the client via the <c>/umbraco/management/api/v1/server/configuration</c> endpoint.
/// </para>
/// <para>
/// Downstream packages (e.g. Umbraco Cloud) can configure these settings via
/// <c>IConfigureOptions&lt;SignalRSettings&gt;</c> or <c>appsettings.json</c>
/// under <c>Umbraco:CMS:SignalR</c>.
/// </para>
/// </remarks>
[UmbracoOptions(Constants.Configuration.ConfigSignalR)]
public class SignalRSettings
{
internal const bool StaticClientShouldSkipNegotiation = false;

/// <summary>
/// Gets or sets a value indicating whether the client should skip the SignalR negotiate
/// round-trip and connect directly via WebSockets.
/// </summary>
/// <remarks>
/// When <c>true</c>, the server restricts all hub endpoints to the WebSocket transport only
/// (via <c>HttpConnectionDispatcherOptions.Transports</c>) and the client is instructed to
/// set <c>skipNegotiation = true</c> with <c>transport = WebSockets</c>. This eliminates the
/// negotiate HTTP request that causes failures in load-balanced deployments without sticky sessions.
/// This is safe for self-hosted SignalR but must <b>not</b> be used with Azure SignalR Service.
/// </remarks>
[DefaultValue(StaticClientShouldSkipNegotiation)]
public bool ClientShouldSkipNegotiation { get; set; } = StaticClientShouldSkipNegotiation;
}
5 changes: 5 additions & 0 deletions src/Umbraco.Core/Constants-Configuration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,11 @@ public static class Configuration
/// </summary>
public const string ConfigWebsite = ConfigPrefix + "Website";

/// <summary>
/// The configuration key for SignalR settings.
/// </summary>
public const string ConfigSignalR = ConfigPrefix + "SignalR";

/// <summary>
/// Contains constants for named options used in configuration.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,8 @@
.AddUmbracoOptions<SystemDateMigrationSettings>()
.AddUmbracoOptions<DistributedJobSettings>()
.AddUmbracoOptions<BackOfficeTokenCookieSettings>()
.AddUmbracoOptions<WebsiteSettings>();
.AddUmbracoOptions<WebsiteSettings>()
.AddUmbracoOptions<SignalRSettings>();

Check warning on line 107 in src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (main)

❌ Getting worse: Large Method

AddConfiguration increases from 71 to 72 lines of code, threshold = 70. Large functions with many lines of code are generally harder to understand and lower the code health. Avoid adding more lines to this function.

// Configure connection string and ensure it's updated when the configuration changes
builder.Services.AddSingleton<IConfigureOptions<ConnectionStrings>, ConfigureConnectionStrings>();
Expand Down
Loading
Loading