From 36cbc83c63403b285577234485e3cac519b983fd Mon Sep 17 00:00:00 2001 From: ChiaraBm Date: Sun, 13 Apr 2025 00:09:06 +0200 Subject: [PATCH] Removed old manual access token checking and switched to asp.net jwt handling. Removed old console subscriber handling and switched to full signal r solution + asp.net core auth --- .../Client/ServerFileSystemController.cs | 4 +- .../Controllers/Client/ServersController.cs | 2 +- .../Services/NodeBootService.cs | 28 ++++ .../Services/NodeService.cs | 3 +- .../Abstractions/Server.Initialize.cs | 18 ++- .../Abstractions/Server.cs | 18 ++- .../Helpers/AccessTokenHelper.cs | 51 ------ .../Helpers/ServerWebSocketConnection.cs | 146 ------------------ .../Controllers/Servers/DownloadController.cs | 46 +----- .../Controllers/Servers/UploadController.cs | 23 +-- .../Http/Hubs/ServerWebSocketHub.cs | 39 ++--- .../Services/ServerService.cs | 10 +- .../Services/ServerWebSocketService.cs | 68 -------- MoonlightServers.Daemon/Startup.cs | 91 ++++++++++- .../UI/Views/Client/Manage.razor | 14 +- 15 files changed, 181 insertions(+), 380 deletions(-) create mode 100644 MoonlightServers.ApiServer/Services/NodeBootService.cs delete mode 100644 MoonlightServers.Daemon/Helpers/AccessTokenHelper.cs delete mode 100644 MoonlightServers.Daemon/Helpers/ServerWebSocketConnection.cs delete mode 100644 MoonlightServers.Daemon/Services/ServerWebSocketService.cs diff --git a/MoonlightServers.ApiServer/Http/Controllers/Client/ServerFileSystemController.cs b/MoonlightServers.ApiServer/Http/Controllers/Client/ServerFileSystemController.cs index fb45c8d..c717952 100644 --- a/MoonlightServers.ApiServer/Http/Controllers/Client/ServerFileSystemController.cs +++ b/MoonlightServers.ApiServer/Http/Controllers/Client/ServerFileSystemController.cs @@ -98,7 +98,7 @@ public class ServerFileSystemController : Controller url += server.Node.UseSsl ? "https://" : "http://"; url += $"{server.Node.Fqdn}:{server.Node.HttpPort}/"; - url += $"api/servers/upload?token={accessToken}"; + url += $"api/servers/upload?access_token={accessToken}"; return new ServerFilesUploadResponse() { @@ -126,7 +126,7 @@ public class ServerFileSystemController : Controller url += server.Node.UseSsl ? "https://" : "http://"; url += $"{server.Node.Fqdn}:{server.Node.HttpPort}/"; - url += $"api/servers/download?token={accessToken}"; + url += $"api/servers/download?access_token={accessToken}"; return new ServerFilesDownloadResponse() { diff --git a/MoonlightServers.ApiServer/Http/Controllers/Client/ServersController.cs b/MoonlightServers.ApiServer/Http/Controllers/Client/ServersController.cs index 41909fb..f059f5f 100644 --- a/MoonlightServers.ApiServer/Http/Controllers/Client/ServersController.cs +++ b/MoonlightServers.ApiServer/Http/Controllers/Client/ServersController.cs @@ -132,7 +132,7 @@ public class ServersController : Controller { parameters.Add("type", "websocket"); parameters.Add("serverId", server.Id); - }, TimeSpan.FromMinutes(10)); + }, TimeSpan.FromSeconds(30)); var url = ""; diff --git a/MoonlightServers.ApiServer/Services/NodeBootService.cs b/MoonlightServers.ApiServer/Services/NodeBootService.cs new file mode 100644 index 0000000..a9c8ac6 --- /dev/null +++ b/MoonlightServers.ApiServer/Services/NodeBootService.cs @@ -0,0 +1,28 @@ +namespace MoonlightServers.ApiServer.Services; + +public class NodeBootService : IHostedLifecycleService +{ + public async Task StartedAsync(CancellationToken cancellationToken) + { + // TODO: Add node boot calls here + } + + #region Unused + + public Task StartAsync(CancellationToken cancellationToken) + => Task.CompletedTask; + + public Task StopAsync(CancellationToken cancellationToken) + => Task.CompletedTask; + + public Task StartingAsync(CancellationToken cancellationToken) + => Task.CompletedTask; + + public Task StoppedAsync(CancellationToken cancellationToken) + => Task.CompletedTask; + + public Task StoppingAsync(CancellationToken cancellationToken) + => Task.CompletedTask; + + #endregion +} \ No newline at end of file diff --git a/MoonlightServers.ApiServer/Services/NodeService.cs b/MoonlightServers.ApiServer/Services/NodeService.cs index 1704071..176f938 100644 --- a/MoonlightServers.ApiServer/Services/NodeService.cs +++ b/MoonlightServers.ApiServer/Services/NodeService.cs @@ -32,7 +32,8 @@ public class NodeService node.Token )), SecurityAlgorithms.HmacSha256 - ) + ), + Audience = node.TokenId }; var securityToken = jwtSecurityTokenHandler.CreateJwtSecurityToken(securityTokenDescriptor); diff --git a/MoonlightServers.Daemon/Abstractions/Server.Initialize.cs b/MoonlightServers.Daemon/Abstractions/Server.Initialize.cs index b5e37c7..229a16a 100644 --- a/MoonlightServers.Daemon/Abstractions/Server.Initialize.cs +++ b/MoonlightServers.Daemon/Abstractions/Server.Initialize.cs @@ -1,5 +1,6 @@ using System.Text.RegularExpressions; using Docker.DotNet.Models; +using Microsoft.AspNetCore.SignalR; using MoonlightServers.Daemon.Enums; using MoonlightServers.Daemon.Extensions; using Stateless; @@ -98,21 +99,28 @@ public partial class Server Logger.LogInformation("State: {state}", transition.Destination); }); - // Proxy the events so outside subscribes can react to it + // Proxy the events so outside subscribes can react to it and notify websockets StateMachine.OnTransitionCompletedAsync(async transition => { + // Notify all clients interested in the server + await WebSocketHub.Clients + .Group(Id.ToString()) //TODO: Consider saving the string value in memory + .SendAsync("StateChanged", transition.Destination.ToString()); + + // Notify all external listeners if (OnStateChanged != null) - { await OnStateChanged(transition.Destination); - } }); Console.OnOutput += (async message => { + // Notify all clients interested in the server + await WebSocketHub.Clients + .Group(Id.ToString()) //TODO: Consider saving the string value in memory + .SendAsync("ConsoleOutput", message); + if (OnConsoleOutput != null) - { await OnConsoleOutput(message); - } }); return Task.CompletedTask; diff --git a/MoonlightServers.Daemon/Abstractions/Server.cs b/MoonlightServers.Daemon/Abstractions/Server.cs index 26c8727..e5bf503 100644 --- a/MoonlightServers.Daemon/Abstractions/Server.cs +++ b/MoonlightServers.Daemon/Abstractions/Server.cs @@ -1,5 +1,7 @@ using Docker.DotNet.Models; +using Microsoft.AspNetCore.SignalR; using MoonlightServers.Daemon.Enums; +using MoonlightServers.Daemon.Http.Hubs; using MoonlightServers.Daemon.Models; using MoonlightServers.Daemon.Models.Cache; using Stateless; @@ -11,24 +13,26 @@ public partial class Server // Exposed configuration/state values public int Id => Configuration.Id; public ServerState State => StateMachine.State; - + // Exposed container names and ids public string RuntimeContainerName { get; private set; } public string? RuntimeContainerId { get; private set; } public string InstallationContainerName { get; private set; } public string? InstallationContainerId { get; private set; } - + // Events - public event Func OnStateChanged; - public event Func OnConsoleOutput; - + public event Func? OnStateChanged; + public event Func? OnConsoleOutput; + // Private stuff private readonly ILogger Logger; private readonly IServiceProvider ServiceProvider; private readonly ServerConsole Console; + private readonly IHubContext WebSocketHub; + private StateMachine StateMachine; private ServerConfiguration Configuration; private CancellationTokenSource Cancellation; @@ -36,12 +40,14 @@ public partial class Server public Server( ILogger logger, IServiceProvider serviceProvider, - ServerConfiguration configuration + ServerConfiguration configuration, + IHubContext webSocketHub ) { Logger = logger; ServiceProvider = serviceProvider; Configuration = configuration; + WebSocketHub = webSocketHub; Console = new(); Cancellation = new(); diff --git a/MoonlightServers.Daemon/Helpers/AccessTokenHelper.cs b/MoonlightServers.Daemon/Helpers/AccessTokenHelper.cs deleted file mode 100644 index bb28c52..0000000 --- a/MoonlightServers.Daemon/Helpers/AccessTokenHelper.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System.IdentityModel.Tokens.Jwt; -using System.Security.Claims; -using System.Text; -using System.Text.Json; -using Microsoft.IdentityModel.Tokens; -using MoonCore.Attributes; -using MoonCore.Extended.Helpers; -using MoonlightServers.Daemon.Configuration; - -namespace MoonlightServers.Daemon.Helpers; - -[Singleton] -public class AccessTokenHelper -{ - private readonly AppConfiguration Configuration; - - public AccessTokenHelper(AppConfiguration configuration) - { - Configuration = configuration; - } - -// TODO: Improve - public bool Process(string accessToken, out Claim[] claims) - { - var jwtSecurityTokenHandler = new JwtSecurityTokenHandler(); - - try - { - var data = jwtSecurityTokenHandler.ValidateToken(accessToken, new() - { - ClockSkew = TimeSpan.Zero, - ValidateLifetime = true, - ValidateAudience = false, - ValidateIssuer = false, - ValidateActor = false, - IssuerSigningKey = new SymmetricSecurityKey( - Encoding.UTF8.GetBytes(Configuration.Security.Token) - ) - }, out var _); - - claims = data.Claims.ToArray(); - - return true; - } - catch (Exception e) - { - claims = []; - return false; - } - } -} \ No newline at end of file diff --git a/MoonlightServers.Daemon/Helpers/ServerWebSocketConnection.cs b/MoonlightServers.Daemon/Helpers/ServerWebSocketConnection.cs deleted file mode 100644 index 470b0e8..0000000 --- a/MoonlightServers.Daemon/Helpers/ServerWebSocketConnection.cs +++ /dev/null @@ -1,146 +0,0 @@ -using Microsoft.AspNetCore.SignalR; -using MoonlightServers.Daemon.Abstractions; -using MoonlightServers.Daemon.Enums; -using MoonlightServers.Daemon.Http.Hubs; -using MoonlightServers.Daemon.Models; -using MoonlightServers.Daemon.Services; - -namespace MoonlightServers.Daemon.Helpers; - -public class ServerWebSocketConnection -{ - private readonly ServerService ServerService; - private readonly ILogger Logger; - private readonly AccessTokenHelper AccessTokenHelper; - private readonly IHubContext HubContext; - - private int ServerId = -1; - private Server Server; - private bool IsInitialized = false; - private string ConnectionId; - - public ServerWebSocketConnection( - ServerService serverService, - ILogger logger, - AccessTokenHelper accessTokenHelper, - IHubContext hubContext - ) - { - ServerService = serverService; - Logger = logger; - AccessTokenHelper = accessTokenHelper; - HubContext = hubContext; - } - - public Task Initialize(HubCallerContext context) => Task.CompletedTask; - - public async Task Authenticate(HubCallerContext context, string accessToken) - { - // Validate access token - if (!AccessTokenHelper.Process(accessToken, out var accessData)) - { - Logger.LogDebug("Received invalid or expired access token"); - - await HubContext.Clients.Client(context.ConnectionId).SendAsync( - "Error", - "Received invalid or expired access token" - ); - - return; - } - - // Validate access token data - if (accessData.All(x => x.Type != "type") || accessData.All(x => x.Type != "serverId")) - { - Logger.LogDebug("Received invalid access token: Required parameters are missing"); - - await HubContext.Clients.Client(context.ConnectionId).SendAsync( - "Error", - "Received invalid access token: Required parameters are missing" - ); - - return; - } - - // Validate access token type - var type = accessData.First(x => x.Type == "type").Value; - - if (type != "websocket") - { - Logger.LogDebug("Received invalid access token: Invalid type '{type}'", type); - - await HubContext.Clients.Client(context.ConnectionId).SendAsync( - "Error", - $"Received invalid access token: Invalid type '{type}'" - ); - - return; - } - - var serverId = int.Parse(accessData.First(x => x.Type == "serverId").Value); - - // Check that the access token isn't for another server - if (ServerId != -1 && ServerId == serverId) - { - Logger.LogDebug("Received invalid access token: Server id not valid for this session. Current server id: {serverId}", ServerId); - - await HubContext.Clients.Client(context.ConnectionId).SendAsync( - "Error", - $"Received invalid access token: Server id not valid for this session. Current server id: {ServerId}" - ); - - return; - } - - var server = ServerService.GetServer(serverId); - - // Check i the server actually exists - if (server == null) - { - Logger.LogDebug("Received invalid access token: No server found with the requested id"); - - await HubContext.Clients.Client(context.ConnectionId).SendAsync( - "Error", - "Received invalid access token: No server found with the requested id" - ); - - return; - } - - // Set values - Server = server; - ServerId = serverId; - ConnectionId = context.ConnectionId; - - if(IsInitialized) - return; - - IsInitialized = true; - - // Setup event handlers - Server.OnConsoleOutput += HandleConsoleOutput; - Server.OnStateChanged += HandleStateChange; - - Logger.LogTrace("Authenticated and initialized server console connection '{id}'", context.ConnectionId); - } - - public Task Destroy(HubCallerContext context) - { - Logger.LogTrace("Destroyed server console connection '{id}'", context.ConnectionId); - - Server.OnConsoleOutput -= HandleConsoleOutput; - Server.OnStateChanged -= HandleStateChange; - - return Task.CompletedTask; - } - - #region Event Handlers - - private async Task HandleStateChange(ServerState state) - => await HubContext.Clients.Client(ConnectionId).SendAsync("StateChanged", state.ToString()); - - private async Task HandleConsoleOutput(string line) - => await HubContext.Clients.Client(ConnectionId).SendAsync("ConsoleOutput", line); - - #endregion -} \ No newline at end of file diff --git a/MoonlightServers.Daemon/Http/Controllers/Servers/DownloadController.cs b/MoonlightServers.Daemon/Http/Controllers/Servers/DownloadController.cs index ea445f5..54bebb7 100644 --- a/MoonlightServers.Daemon/Http/Controllers/Servers/DownloadController.cs +++ b/MoonlightServers.Daemon/Http/Controllers/Servers/DownloadController.cs @@ -2,66 +2,34 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using MoonCore.Exceptions; using MoonlightServers.Daemon.Configuration; -using MoonlightServers.Daemon.Helpers; using MoonlightServers.Daemon.Services; namespace MoonlightServers.Daemon.Http.Controllers.Servers; -[AllowAnonymous] [ApiController] [Route("api/servers/download")] +[Authorize(AuthenticationSchemes = "accessToken", Policy = "serverDownload")] public class DownloadController : Controller { - private readonly AccessTokenHelper AccessTokenHelper; - private readonly AppConfiguration Configuration; private readonly ServerService ServerService; - public DownloadController( - AccessTokenHelper accessTokenHelper, - ServerService serverService, - AppConfiguration configuration - ) + public DownloadController(ServerService serverService) { - AccessTokenHelper = accessTokenHelper; ServerService = serverService; - Configuration = configuration; } [HttpGet] - public async Task Download([FromQuery] string token) + public async Task Download() { - #region Token validation - - if (!AccessTokenHelper.Process(token, out var claims)) - throw new HttpApiException("Invalid access token provided", 401); - - var typeClaim = claims.FirstOrDefault(x => x.Type == "type"); - - if (typeClaim == null || typeClaim.Value != "download") - throw new HttpApiException("Invalid access token provided: Missing or invalid type", 401); - - var serverIdClaim = claims.FirstOrDefault(x => x.Type == "serverId"); - - if (serverIdClaim == null || !int.TryParse(serverIdClaim.Value, out var serverId)) - throw new HttpApiException("Invalid access token provided: Missing or invalid server id", 401); - - var pathClaim = claims.FirstOrDefault(x => x.Type == "path"); - - if(pathClaim == null || string.IsNullOrEmpty(pathClaim.Value)) - throw new HttpApiException("Invalid access token provided: Missing or invalid path", 401); - - #endregion + var serverId = int.Parse(User.Claims.First(x => x.Type == "serverId").Value); + var path = User.Claims.First(x => x.Type == "path").Value; var server = ServerService.GetServer(serverId); if (server == null) throw new HttpApiException("No server with this id found", 404); - var path = pathClaim.Value; - - await server.FileSystem.Read(path, async dataStream => - { - await Results.File(dataStream).ExecuteAsync(HttpContext); - }); + await server.FileSystem.Read(path, + async dataStream => { await Results.File(dataStream).ExecuteAsync(HttpContext); }); } } \ No newline at end of file diff --git a/MoonlightServers.Daemon/Http/Controllers/Servers/UploadController.cs b/MoonlightServers.Daemon/Http/Controllers/Servers/UploadController.cs index 437ab83..e5e2a86 100644 --- a/MoonlightServers.Daemon/Http/Controllers/Servers/UploadController.cs +++ b/MoonlightServers.Daemon/Http/Controllers/Servers/UploadController.cs @@ -9,30 +9,26 @@ using MoonlightServers.Daemon.Services; namespace MoonlightServers.Daemon.Http.Controllers.Servers; [ApiController] -[AllowAnonymous] [Route("api/servers/upload")] +[Authorize(AuthenticationSchemes = "accessToken", Policy = "serverUpload")] public class UploadController : Controller { - private readonly AccessTokenHelper AccessTokenHelper; private readonly AppConfiguration Configuration; private readonly ServerService ServerService; private readonly long ChunkSize = ByteConverter.FromMegaBytes(20).Bytes; // TODO config public UploadController( - AccessTokenHelper accessTokenHelper, ServerService serverService, AppConfiguration configuration ) { - AccessTokenHelper = accessTokenHelper; ServerService = serverService; Configuration = configuration; } [HttpPost] public async Task Upload( - [FromQuery] string token, [FromQuery] long totalSize, // TODO: Add limit in config [FromQuery] int chunkId, [FromQuery] string path @@ -50,22 +46,7 @@ public class UploadController : Controller #endregion - #region Token validation - - if (!AccessTokenHelper.Process(token, out var claims)) - throw new HttpApiException("Invalid access token provided", 401); - - var typeClaim = claims.FirstOrDefault(x => x.Type == "type"); - - if (typeClaim == null || typeClaim.Value != "upload") - throw new HttpApiException("Invalid access token provided: Missing or invalid type", 401); - - var serverIdClaim = claims.FirstOrDefault(x => x.Type == "serverId"); - - if (serverIdClaim == null || !int.TryParse(serverIdClaim.Value, out var serverId)) - throw new HttpApiException("Invalid access token provided: Missing or invalid server id", 401); - - #endregion + var serverId = int.Parse(User.Claims.First(x => x.Type == "serverId").Value); #region Chunk calculation and validation diff --git a/MoonlightServers.Daemon/Http/Hubs/ServerWebSocketHub.cs b/MoonlightServers.Daemon/Http/Hubs/ServerWebSocketHub.cs index ec57757..82394b9 100644 --- a/MoonlightServers.Daemon/Http/Hubs/ServerWebSocketHub.cs +++ b/MoonlightServers.Daemon/Http/Hubs/ServerWebSocketHub.cs @@ -1,43 +1,28 @@ +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.SignalR; -using MoonlightServers.Daemon.Services; namespace MoonlightServers.Daemon.Http.Hubs; +[Authorize(AuthenticationSchemes = "accessToken", Policy = "serverWebsocket")] public class ServerWebSocketHub : Hub { private readonly ILogger Logger; - private readonly ServerWebSocketService WebSocketService; - public ServerWebSocketHub(ILogger logger, ServerWebSocketService webSocketService) + public ServerWebSocketHub(ILogger logger) { Logger = logger; - WebSocketService = webSocketService; } - #region Connection Handlers - public override async Task OnConnectedAsync() - => await WebSocketService.InitializeClient(Context); - - public override async Task OnDisconnectedAsync(Exception? exception) - => await WebSocketService.DestroyClient(Context); - - #endregion - - #region Methods - - [HubMethodName("Authenticate")] - public async Task Authenticate(string accessToken) { - try - { - await WebSocketService.AuthenticateClient(Context, accessToken); - } - catch (Exception e) - { - Logger.LogError("An unhandled error occured in the Authenticate method: {e}", e); - } - } + // The policies validated already the type and the token so we can assume we are authenticated + // and just start adding ourselves into the desired group + + var serverId = Context.User!.Claims.First(x => x.Type == "serverId").Value; - #endregion + await Groups.AddToGroupAsync( + Context.ConnectionId, + serverId + ); + } } \ No newline at end of file diff --git a/MoonlightServers.Daemon/Services/ServerService.cs b/MoonlightServers.Daemon/Services/ServerService.cs index b91f99f..abdbadf 100644 --- a/MoonlightServers.Daemon/Services/ServerService.cs +++ b/MoonlightServers.Daemon/Services/ServerService.cs @@ -1,11 +1,13 @@ using Docker.DotNet; using Docker.DotNet.Models; +using Microsoft.AspNetCore.SignalR; using MoonCore.Attributes; using MoonCore.Exceptions; using MoonCore.Models; using MoonlightServers.Daemon.Abstractions; using MoonlightServers.Daemon.Enums; using MoonlightServers.Daemon.Extensions; +using MoonlightServers.Daemon.Http.Hubs; using MoonlightServers.Daemon.Models.Cache; using MoonlightServers.DaemonShared.PanelSide.Http.Responses; @@ -19,6 +21,7 @@ public class ServerService : IHostedLifecycleService private readonly RemoteService RemoteService; private readonly IServiceProvider ServiceProvider; private readonly ILoggerFactory LoggerFactory; + private readonly IHubContext WebSocketHub; private CancellationTokenSource Cancellation = new(); private bool IsInitialized = false; @@ -26,13 +29,15 @@ public class ServerService : IHostedLifecycleService RemoteService remoteService, ILogger logger, IServiceProvider serviceProvider, - ILoggerFactory loggerFactory + ILoggerFactory loggerFactory, + IHubContext webSocketHub ) { RemoteService = remoteService; Logger = logger; ServiceProvider = serviceProvider; LoggerFactory = loggerFactory; + WebSocketHub = webSocketHub; } public async Task Initialize() //TODO: Add initialize call from panel @@ -190,7 +195,8 @@ public class ServerService : IHostedLifecycleService var server = new Server( LoggerFactory.CreateLogger($"Server {serverConfiguration.Id}"), ServiceProvider, - serverConfiguration + serverConfiguration, + WebSocketHub ); await server.Initialize(existingContainers); diff --git a/MoonlightServers.Daemon/Services/ServerWebSocketService.cs b/MoonlightServers.Daemon/Services/ServerWebSocketService.cs deleted file mode 100644 index f27436f..0000000 --- a/MoonlightServers.Daemon/Services/ServerWebSocketService.cs +++ /dev/null @@ -1,68 +0,0 @@ -using Microsoft.AspNetCore.SignalR; -using MoonCore.Attributes; -using MoonlightServers.Daemon.Helpers; -using MoonlightServers.Daemon.Http.Hubs; - -namespace MoonlightServers.Daemon.Services; - -[Singleton] -public class ServerWebSocketService -{ - private readonly ILogger Logger; - private readonly IServiceProvider ServiceProvider; - - private readonly Dictionary Connections = new(); - - public ServerWebSocketService( - ILogger logger, - IServiceProvider serviceProvider - ) - { - Logger = logger; - ServiceProvider = serviceProvider; - } - - public async Task InitializeClient(HubCallerContext context) - { - var connection = new ServerWebSocketConnection( - ServiceProvider.GetRequiredService(), - ServiceProvider.GetRequiredService>(), - ServiceProvider.GetRequiredService(), - ServiceProvider.GetRequiredService>() - ); - - lock (Connections) - Connections[context.ConnectionId] = connection; - - await connection.Initialize(context); - } - - public async Task AuthenticateClient(HubCallerContext context, string accessToken) - { - ServerWebSocketConnection? connection; - - lock (Connections) - connection = Connections.GetValueOrDefault(context.ConnectionId); - - if(connection == null) - return; - - await connection.Authenticate(context, accessToken); - } - - public async Task DestroyClient(HubCallerContext context) - { - ServerWebSocketConnection? connection; - - lock (Connections) - connection = Connections.GetValueOrDefault(context.ConnectionId); - - if(connection == null) - return; - - await connection.Destroy(context); - - lock (Connections) - Connections.Remove(context.ConnectionId); - } -} \ No newline at end of file diff --git a/MoonlightServers.Daemon/Startup.cs b/MoonlightServers.Daemon/Startup.cs index 1481722..3f8ad68 100644 --- a/MoonlightServers.Daemon/Startup.cs +++ b/MoonlightServers.Daemon/Startup.cs @@ -1,5 +1,9 @@ +using System.Text; using System.Text.Json; using Docker.DotNet; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Http.Connections; +using Microsoft.IdentityModel.Tokens; using MoonCore.EnvConfiguration; using MoonCore.Extended.Extensions; using MoonCore.Extensions; @@ -50,8 +54,8 @@ public class Startup await BuildWebApplication(); await UseBase(); - await UseAuth(); await UseCors(); + await UseAuth(); await UseBaseMiddleware(); await MapBase(); @@ -273,7 +277,11 @@ public class Startup private Task MapHubs() { - WebApplication.MapHub("api/servers/ws"); + WebApplication.MapHub("api/servers/ws", options => + { + options.AllowStatefulReconnects = false; + options.CloseOnAuthenticationExpiration = true; + }); return Task.CompletedTask; } @@ -286,8 +294,11 @@ public class Startup { //TODO: IMPORTANT: CHANGE !!! WebApplicationBuilder.Services.AddCors(x => - x.AddDefaultPolicy(builder => - builder.AllowAnyHeader().AllowAnyOrigin().AllowAnyMethod().Build() + x.AddDefaultPolicy(builder => builder + .SetIsOriginAllowed(_ => true) + .AllowAnyMethod() + .AllowAnyHeader() + .AllowCredentials() ) ); @@ -308,12 +319,78 @@ public class Startup { WebApplicationBuilder.Services .AddAuthentication("token") - .AddScheme("token", options => + .AddScheme("token", + options => { options.Token = Configuration.Security.Token; }) + .AddJwtBearer("accessToken", options => { - options.Token = Configuration.Security.Token; + options.TokenValidationParameters = new TokenValidationParameters() + { + ClockSkew = TimeSpan.Zero, + ValidateAudience = true, + ValidateIssuer = false, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidateActor = false, + IssuerSigningKeys = + [ + new SymmetricSecurityKey( + Encoding.UTF8.GetBytes(Configuration.Security.Token) + ) + ], + AudienceValidator = (audiences, _, _) + => audiences.Contains(Configuration.Security.TokenId) + }; + + // Some requests like the signal r websockets or the upload and download endpoints require the user to provide + // the access token via the query instead of the header field due to client limitations. To reuse the authentication + // on all these requests as we do for all other endpoints which require an access token, we are defining + // the custom behaviour to load the access token from the query for these specific endpoints below + options.Events = new JwtBearerEvents() + { + OnMessageReceived = context => + { + if ( + !context.HttpContext.Request.Path.StartsWithSegments("/api/servers/ws") && + !context.HttpContext.Request.Path.StartsWithSegments("/api/servers/upload") && + !context.HttpContext.Request.Path.StartsWithSegments("/api/servers/download") + ) + { + return Task.CompletedTask; + } + + var accessToken = context.Request.Query["access_token"]; + + if (string.IsNullOrEmpty(accessToken)) + return Task.CompletedTask; + + context.Token = accessToken; + + return Task.CompletedTask; + } + }; }); - WebApplicationBuilder.Services.AddAuthorization(); + WebApplicationBuilder.Services.AddAuthorization(options => + { + // We are defining the access token policies here. Because the same jwt secret is used by the panel + // to generate jwt access tokens for all sorts of daemon related stuff we need to separate + // the type of access token using the type parameter provided in the claims. + + options.AddPolicy("serverWebsocket", builder => + { + builder.RequireClaim("type", "websocket"); + }); + + options.AddPolicy("serverUpload", builder => + { + builder.RequireClaim("type", "upload"); + }); + + options.AddPolicy("serverDownload", builder => + { + builder.RequireClaim("type", "download"); + }); + }); return Task.CompletedTask; } diff --git a/MoonlightServers.Frontend/UI/Views/Client/Manage.razor b/MoonlightServers.Frontend/UI/Views/Client/Manage.razor index 4ce9491..db5c20c 100644 --- a/MoonlightServers.Frontend/UI/Views/Client/Manage.razor +++ b/MoonlightServers.Frontend/UI/Views/Client/Manage.razor @@ -1,6 +1,7 @@ @page "/servers/{ServerId:int}" @page "/servers/{ServerId:int}/{TabPath:alpha}" +@using Microsoft.AspNetCore.Http.Connections @using Microsoft.AspNetCore.SignalR.Client @using MoonlightServers.Shared.Http.Responses.Users.Servers @using MoonCore.Blazor.Tailwind.Components @@ -224,7 +225,15 @@ // Build signal r HubConnection = new HubConnectionBuilder() - .WithUrl(websocketDetails.Target) + .WithUrl(websocketDetails.Target, options => + { + options.AccessTokenProvider = async () => + { + var details = await ServerService.GetWebSocket(ServerId); + return details.AccessToken; + }; + }) + .WithAutomaticReconnect() .Build(); // Define handlers @@ -246,9 +255,6 @@ // Connect await HubConnection.StartAsync(); - - // Authenticate - await HubConnection.SendAsync("Authenticate", websocketDetails.AccessToken); } catch (HttpApiException e) {