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
This commit is contained in:
@@ -98,7 +98,7 @@ public class ServerFileSystemController : Controller
|
|||||||
|
|
||||||
url += server.Node.UseSsl ? "https://" : "http://";
|
url += server.Node.UseSsl ? "https://" : "http://";
|
||||||
url += $"{server.Node.Fqdn}:{server.Node.HttpPort}/";
|
url += $"{server.Node.Fqdn}:{server.Node.HttpPort}/";
|
||||||
url += $"api/servers/upload?token={accessToken}";
|
url += $"api/servers/upload?access_token={accessToken}";
|
||||||
|
|
||||||
return new ServerFilesUploadResponse()
|
return new ServerFilesUploadResponse()
|
||||||
{
|
{
|
||||||
@@ -126,7 +126,7 @@ public class ServerFileSystemController : Controller
|
|||||||
|
|
||||||
url += server.Node.UseSsl ? "https://" : "http://";
|
url += server.Node.UseSsl ? "https://" : "http://";
|
||||||
url += $"{server.Node.Fqdn}:{server.Node.HttpPort}/";
|
url += $"{server.Node.Fqdn}:{server.Node.HttpPort}/";
|
||||||
url += $"api/servers/download?token={accessToken}";
|
url += $"api/servers/download?access_token={accessToken}";
|
||||||
|
|
||||||
return new ServerFilesDownloadResponse()
|
return new ServerFilesDownloadResponse()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ public class ServersController : Controller
|
|||||||
{
|
{
|
||||||
parameters.Add("type", "websocket");
|
parameters.Add("type", "websocket");
|
||||||
parameters.Add("serverId", server.Id);
|
parameters.Add("serverId", server.Id);
|
||||||
}, TimeSpan.FromMinutes(10));
|
}, TimeSpan.FromSeconds(30));
|
||||||
|
|
||||||
var url = "";
|
var url = "";
|
||||||
|
|
||||||
|
|||||||
28
MoonlightServers.ApiServer/Services/NodeBootService.cs
Normal file
28
MoonlightServers.ApiServer/Services/NodeBootService.cs
Normal file
@@ -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
|
||||||
|
}
|
||||||
@@ -32,7 +32,8 @@ public class NodeService
|
|||||||
node.Token
|
node.Token
|
||||||
)),
|
)),
|
||||||
SecurityAlgorithms.HmacSha256
|
SecurityAlgorithms.HmacSha256
|
||||||
)
|
),
|
||||||
|
Audience = node.TokenId
|
||||||
};
|
};
|
||||||
|
|
||||||
var securityToken = jwtSecurityTokenHandler.CreateJwtSecurityToken(securityTokenDescriptor);
|
var securityToken = jwtSecurityTokenHandler.CreateJwtSecurityToken(securityTokenDescriptor);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using Docker.DotNet.Models;
|
using Docker.DotNet.Models;
|
||||||
|
using Microsoft.AspNetCore.SignalR;
|
||||||
using MoonlightServers.Daemon.Enums;
|
using MoonlightServers.Daemon.Enums;
|
||||||
using MoonlightServers.Daemon.Extensions;
|
using MoonlightServers.Daemon.Extensions;
|
||||||
using Stateless;
|
using Stateless;
|
||||||
@@ -98,21 +99,28 @@ public partial class Server
|
|||||||
Logger.LogInformation("State: {state}", transition.Destination);
|
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 =>
|
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)
|
if (OnStateChanged != null)
|
||||||
{
|
|
||||||
await OnStateChanged(transition.Destination);
|
await OnStateChanged(transition.Destination);
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Console.OnOutput += (async message =>
|
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)
|
if (OnConsoleOutput != null)
|
||||||
{
|
|
||||||
await OnConsoleOutput(message);
|
await OnConsoleOutput(message);
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
using Docker.DotNet.Models;
|
using Docker.DotNet.Models;
|
||||||
|
using Microsoft.AspNetCore.SignalR;
|
||||||
using MoonlightServers.Daemon.Enums;
|
using MoonlightServers.Daemon.Enums;
|
||||||
|
using MoonlightServers.Daemon.Http.Hubs;
|
||||||
using MoonlightServers.Daemon.Models;
|
using MoonlightServers.Daemon.Models;
|
||||||
using MoonlightServers.Daemon.Models.Cache;
|
using MoonlightServers.Daemon.Models.Cache;
|
||||||
using Stateless;
|
using Stateless;
|
||||||
@@ -20,8 +22,8 @@ public partial class Server
|
|||||||
public string? InstallationContainerId { get; private set; }
|
public string? InstallationContainerId { get; private set; }
|
||||||
|
|
||||||
// Events
|
// Events
|
||||||
public event Func<ServerState, Task> OnStateChanged;
|
public event Func<ServerState, Task>? OnStateChanged;
|
||||||
public event Func<string, Task> OnConsoleOutput;
|
public event Func<string, Task>? OnConsoleOutput;
|
||||||
|
|
||||||
// Private stuff
|
// Private stuff
|
||||||
|
|
||||||
@@ -29,6 +31,8 @@ public partial class Server
|
|||||||
private readonly IServiceProvider ServiceProvider;
|
private readonly IServiceProvider ServiceProvider;
|
||||||
private readonly ServerConsole Console;
|
private readonly ServerConsole Console;
|
||||||
|
|
||||||
|
private readonly IHubContext<ServerWebSocketHub> WebSocketHub;
|
||||||
|
|
||||||
private StateMachine<ServerState, ServerTrigger> StateMachine;
|
private StateMachine<ServerState, ServerTrigger> StateMachine;
|
||||||
private ServerConfiguration Configuration;
|
private ServerConfiguration Configuration;
|
||||||
private CancellationTokenSource Cancellation;
|
private CancellationTokenSource Cancellation;
|
||||||
@@ -36,12 +40,14 @@ public partial class Server
|
|||||||
public Server(
|
public Server(
|
||||||
ILogger logger,
|
ILogger logger,
|
||||||
IServiceProvider serviceProvider,
|
IServiceProvider serviceProvider,
|
||||||
ServerConfiguration configuration
|
ServerConfiguration configuration,
|
||||||
|
IHubContext<ServerWebSocketHub> webSocketHub
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
Logger = logger;
|
Logger = logger;
|
||||||
ServiceProvider = serviceProvider;
|
ServiceProvider = serviceProvider;
|
||||||
Configuration = configuration;
|
Configuration = configuration;
|
||||||
|
WebSocketHub = webSocketHub;
|
||||||
|
|
||||||
Console = new();
|
Console = new();
|
||||||
Cancellation = new();
|
Cancellation = new();
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<ServerWebSocketConnection> Logger;
|
|
||||||
private readonly AccessTokenHelper AccessTokenHelper;
|
|
||||||
private readonly IHubContext<ServerWebSocketHub> HubContext;
|
|
||||||
|
|
||||||
private int ServerId = -1;
|
|
||||||
private Server Server;
|
|
||||||
private bool IsInitialized = false;
|
|
||||||
private string ConnectionId;
|
|
||||||
|
|
||||||
public ServerWebSocketConnection(
|
|
||||||
ServerService serverService,
|
|
||||||
ILogger<ServerWebSocketConnection> logger,
|
|
||||||
AccessTokenHelper accessTokenHelper,
|
|
||||||
IHubContext<ServerWebSocketHub> 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
|
|
||||||
}
|
|
||||||
@@ -2,66 +2,34 @@ using Microsoft.AspNetCore.Authorization;
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using MoonCore.Exceptions;
|
using MoonCore.Exceptions;
|
||||||
using MoonlightServers.Daemon.Configuration;
|
using MoonlightServers.Daemon.Configuration;
|
||||||
using MoonlightServers.Daemon.Helpers;
|
|
||||||
using MoonlightServers.Daemon.Services;
|
using MoonlightServers.Daemon.Services;
|
||||||
|
|
||||||
namespace MoonlightServers.Daemon.Http.Controllers.Servers;
|
namespace MoonlightServers.Daemon.Http.Controllers.Servers;
|
||||||
|
|
||||||
[AllowAnonymous]
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("api/servers/download")]
|
[Route("api/servers/download")]
|
||||||
|
[Authorize(AuthenticationSchemes = "accessToken", Policy = "serverDownload")]
|
||||||
public class DownloadController : Controller
|
public class DownloadController : Controller
|
||||||
{
|
{
|
||||||
private readonly AccessTokenHelper AccessTokenHelper;
|
|
||||||
private readonly AppConfiguration Configuration;
|
|
||||||
private readonly ServerService ServerService;
|
private readonly ServerService ServerService;
|
||||||
|
|
||||||
public DownloadController(
|
public DownloadController(ServerService serverService)
|
||||||
AccessTokenHelper accessTokenHelper,
|
|
||||||
ServerService serverService,
|
|
||||||
AppConfiguration configuration
|
|
||||||
)
|
|
||||||
{
|
{
|
||||||
AccessTokenHelper = accessTokenHelper;
|
|
||||||
ServerService = serverService;
|
ServerService = serverService;
|
||||||
Configuration = configuration;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task Download([FromQuery] string token)
|
public async Task Download()
|
||||||
{
|
{
|
||||||
#region Token validation
|
var serverId = int.Parse(User.Claims.First(x => x.Type == "serverId").Value);
|
||||||
|
var path = User.Claims.First(x => x.Type == "path").Value;
|
||||||
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 server = ServerService.GetServer(serverId);
|
var server = ServerService.GetServer(serverId);
|
||||||
|
|
||||||
if (server == null)
|
if (server == null)
|
||||||
throw new HttpApiException("No server with this id found", 404);
|
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);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -9,30 +9,26 @@ using MoonlightServers.Daemon.Services;
|
|||||||
namespace MoonlightServers.Daemon.Http.Controllers.Servers;
|
namespace MoonlightServers.Daemon.Http.Controllers.Servers;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[AllowAnonymous]
|
|
||||||
[Route("api/servers/upload")]
|
[Route("api/servers/upload")]
|
||||||
|
[Authorize(AuthenticationSchemes = "accessToken", Policy = "serverUpload")]
|
||||||
public class UploadController : Controller
|
public class UploadController : Controller
|
||||||
{
|
{
|
||||||
private readonly AccessTokenHelper AccessTokenHelper;
|
|
||||||
private readonly AppConfiguration Configuration;
|
private readonly AppConfiguration Configuration;
|
||||||
private readonly ServerService ServerService;
|
private readonly ServerService ServerService;
|
||||||
|
|
||||||
private readonly long ChunkSize = ByteConverter.FromMegaBytes(20).Bytes; // TODO config
|
private readonly long ChunkSize = ByteConverter.FromMegaBytes(20).Bytes; // TODO config
|
||||||
|
|
||||||
public UploadController(
|
public UploadController(
|
||||||
AccessTokenHelper accessTokenHelper,
|
|
||||||
ServerService serverService,
|
ServerService serverService,
|
||||||
AppConfiguration configuration
|
AppConfiguration configuration
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
AccessTokenHelper = accessTokenHelper;
|
|
||||||
ServerService = serverService;
|
ServerService = serverService;
|
||||||
Configuration = configuration;
|
Configuration = configuration;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
public async Task Upload(
|
public async Task Upload(
|
||||||
[FromQuery] string token,
|
|
||||||
[FromQuery] long totalSize, // TODO: Add limit in config
|
[FromQuery] long totalSize, // TODO: Add limit in config
|
||||||
[FromQuery] int chunkId,
|
[FromQuery] int chunkId,
|
||||||
[FromQuery] string path
|
[FromQuery] string path
|
||||||
@@ -50,22 +46,7 @@ public class UploadController : Controller
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Token validation
|
var serverId = int.Parse(User.Claims.First(x => x.Type == "serverId").Value);
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
#region Chunk calculation and validation
|
#region Chunk calculation and validation
|
||||||
|
|
||||||
|
|||||||
@@ -1,43 +1,28 @@
|
|||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.SignalR;
|
using Microsoft.AspNetCore.SignalR;
|
||||||
using MoonlightServers.Daemon.Services;
|
|
||||||
|
|
||||||
namespace MoonlightServers.Daemon.Http.Hubs;
|
namespace MoonlightServers.Daemon.Http.Hubs;
|
||||||
|
|
||||||
|
[Authorize(AuthenticationSchemes = "accessToken", Policy = "serverWebsocket")]
|
||||||
public class ServerWebSocketHub : Hub
|
public class ServerWebSocketHub : Hub
|
||||||
{
|
{
|
||||||
private readonly ILogger<ServerWebSocketHub> Logger;
|
private readonly ILogger<ServerWebSocketHub> Logger;
|
||||||
private readonly ServerWebSocketService WebSocketService;
|
|
||||||
|
|
||||||
public ServerWebSocketHub(ILogger<ServerWebSocketHub> logger, ServerWebSocketService webSocketService)
|
public ServerWebSocketHub(ILogger<ServerWebSocketHub> logger)
|
||||||
{
|
{
|
||||||
Logger = logger;
|
Logger = logger;
|
||||||
WebSocketService = webSocketService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Connection Handlers
|
|
||||||
|
|
||||||
public override async Task OnConnectedAsync()
|
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
|
// 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
|
||||||
await WebSocketService.AuthenticateClient(Context, accessToken);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Logger.LogError("An unhandled error occured in the Authenticate method: {e}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
var serverId = Context.User!.Claims.First(x => x.Type == "serverId").Value;
|
||||||
|
|
||||||
|
await Groups.AddToGroupAsync(
|
||||||
|
Context.ConnectionId,
|
||||||
|
serverId
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
using Docker.DotNet;
|
using Docker.DotNet;
|
||||||
using Docker.DotNet.Models;
|
using Docker.DotNet.Models;
|
||||||
|
using Microsoft.AspNetCore.SignalR;
|
||||||
using MoonCore.Attributes;
|
using MoonCore.Attributes;
|
||||||
using MoonCore.Exceptions;
|
using MoonCore.Exceptions;
|
||||||
using MoonCore.Models;
|
using MoonCore.Models;
|
||||||
using MoonlightServers.Daemon.Abstractions;
|
using MoonlightServers.Daemon.Abstractions;
|
||||||
using MoonlightServers.Daemon.Enums;
|
using MoonlightServers.Daemon.Enums;
|
||||||
using MoonlightServers.Daemon.Extensions;
|
using MoonlightServers.Daemon.Extensions;
|
||||||
|
using MoonlightServers.Daemon.Http.Hubs;
|
||||||
using MoonlightServers.Daemon.Models.Cache;
|
using MoonlightServers.Daemon.Models.Cache;
|
||||||
using MoonlightServers.DaemonShared.PanelSide.Http.Responses;
|
using MoonlightServers.DaemonShared.PanelSide.Http.Responses;
|
||||||
|
|
||||||
@@ -19,6 +21,7 @@ public class ServerService : IHostedLifecycleService
|
|||||||
private readonly RemoteService RemoteService;
|
private readonly RemoteService RemoteService;
|
||||||
private readonly IServiceProvider ServiceProvider;
|
private readonly IServiceProvider ServiceProvider;
|
||||||
private readonly ILoggerFactory LoggerFactory;
|
private readonly ILoggerFactory LoggerFactory;
|
||||||
|
private readonly IHubContext<ServerWebSocketHub> WebSocketHub;
|
||||||
private CancellationTokenSource Cancellation = new();
|
private CancellationTokenSource Cancellation = new();
|
||||||
private bool IsInitialized = false;
|
private bool IsInitialized = false;
|
||||||
|
|
||||||
@@ -26,13 +29,15 @@ public class ServerService : IHostedLifecycleService
|
|||||||
RemoteService remoteService,
|
RemoteService remoteService,
|
||||||
ILogger<ServerService> logger,
|
ILogger<ServerService> logger,
|
||||||
IServiceProvider serviceProvider,
|
IServiceProvider serviceProvider,
|
||||||
ILoggerFactory loggerFactory
|
ILoggerFactory loggerFactory,
|
||||||
|
IHubContext<ServerWebSocketHub> webSocketHub
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
RemoteService = remoteService;
|
RemoteService = remoteService;
|
||||||
Logger = logger;
|
Logger = logger;
|
||||||
ServiceProvider = serviceProvider;
|
ServiceProvider = serviceProvider;
|
||||||
LoggerFactory = loggerFactory;
|
LoggerFactory = loggerFactory;
|
||||||
|
WebSocketHub = webSocketHub;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Initialize() //TODO: Add initialize call from panel
|
public async Task Initialize() //TODO: Add initialize call from panel
|
||||||
@@ -190,7 +195,8 @@ public class ServerService : IHostedLifecycleService
|
|||||||
var server = new Server(
|
var server = new Server(
|
||||||
LoggerFactory.CreateLogger($"Server {serverConfiguration.Id}"),
|
LoggerFactory.CreateLogger($"Server {serverConfiguration.Id}"),
|
||||||
ServiceProvider,
|
ServiceProvider,
|
||||||
serverConfiguration
|
serverConfiguration,
|
||||||
|
WebSocketHub
|
||||||
);
|
);
|
||||||
|
|
||||||
await server.Initialize(existingContainers);
|
await server.Initialize(existingContainers);
|
||||||
|
|||||||
@@ -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<ServerWebSocketService> Logger;
|
|
||||||
private readonly IServiceProvider ServiceProvider;
|
|
||||||
|
|
||||||
private readonly Dictionary<string, ServerWebSocketConnection> Connections = new();
|
|
||||||
|
|
||||||
public ServerWebSocketService(
|
|
||||||
ILogger<ServerWebSocketService> logger,
|
|
||||||
IServiceProvider serviceProvider
|
|
||||||
)
|
|
||||||
{
|
|
||||||
Logger = logger;
|
|
||||||
ServiceProvider = serviceProvider;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task InitializeClient(HubCallerContext context)
|
|
||||||
{
|
|
||||||
var connection = new ServerWebSocketConnection(
|
|
||||||
ServiceProvider.GetRequiredService<ServerService>(),
|
|
||||||
ServiceProvider.GetRequiredService<ILogger<ServerWebSocketConnection>>(),
|
|
||||||
ServiceProvider.GetRequiredService<AccessTokenHelper>(),
|
|
||||||
ServiceProvider.GetRequiredService<IHubContext<ServerWebSocketHub>>()
|
|
||||||
);
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Docker.DotNet;
|
using Docker.DotNet;
|
||||||
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
|
using Microsoft.AspNetCore.Http.Connections;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
using MoonCore.EnvConfiguration;
|
using MoonCore.EnvConfiguration;
|
||||||
using MoonCore.Extended.Extensions;
|
using MoonCore.Extended.Extensions;
|
||||||
using MoonCore.Extensions;
|
using MoonCore.Extensions;
|
||||||
@@ -50,8 +54,8 @@ public class Startup
|
|||||||
await BuildWebApplication();
|
await BuildWebApplication();
|
||||||
|
|
||||||
await UseBase();
|
await UseBase();
|
||||||
await UseAuth();
|
|
||||||
await UseCors();
|
await UseCors();
|
||||||
|
await UseAuth();
|
||||||
await UseBaseMiddleware();
|
await UseBaseMiddleware();
|
||||||
|
|
||||||
await MapBase();
|
await MapBase();
|
||||||
@@ -273,7 +277,11 @@ public class Startup
|
|||||||
|
|
||||||
private Task MapHubs()
|
private Task MapHubs()
|
||||||
{
|
{
|
||||||
WebApplication.MapHub<ServerWebSocketHub>("api/servers/ws");
|
WebApplication.MapHub<ServerWebSocketHub>("api/servers/ws", options =>
|
||||||
|
{
|
||||||
|
options.AllowStatefulReconnects = false;
|
||||||
|
options.CloseOnAuthenticationExpiration = true;
|
||||||
|
});
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
@@ -286,8 +294,11 @@ public class Startup
|
|||||||
{
|
{
|
||||||
//TODO: IMPORTANT: CHANGE !!!
|
//TODO: IMPORTANT: CHANGE !!!
|
||||||
WebApplicationBuilder.Services.AddCors(x =>
|
WebApplicationBuilder.Services.AddCors(x =>
|
||||||
x.AddDefaultPolicy(builder =>
|
x.AddDefaultPolicy(builder => builder
|
||||||
builder.AllowAnyHeader().AllowAnyOrigin().AllowAnyMethod().Build()
|
.SetIsOriginAllowed(_ => true)
|
||||||
|
.AllowAnyMethod()
|
||||||
|
.AllowAnyHeader()
|
||||||
|
.AllowCredentials()
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -308,12 +319,78 @@ public class Startup
|
|||||||
{
|
{
|
||||||
WebApplicationBuilder.Services
|
WebApplicationBuilder.Services
|
||||||
.AddAuthentication("token")
|
.AddAuthentication("token")
|
||||||
.AddScheme<TokenAuthOptions, TokenAuthScheme>("token", options =>
|
.AddScheme<TokenAuthOptions, TokenAuthScheme>("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;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
@page "/servers/{ServerId:int}"
|
@page "/servers/{ServerId:int}"
|
||||||
@page "/servers/{ServerId:int}/{TabPath:alpha}"
|
@page "/servers/{ServerId:int}/{TabPath:alpha}"
|
||||||
|
|
||||||
|
@using Microsoft.AspNetCore.Http.Connections
|
||||||
@using Microsoft.AspNetCore.SignalR.Client
|
@using Microsoft.AspNetCore.SignalR.Client
|
||||||
@using MoonlightServers.Shared.Http.Responses.Users.Servers
|
@using MoonlightServers.Shared.Http.Responses.Users.Servers
|
||||||
@using MoonCore.Blazor.Tailwind.Components
|
@using MoonCore.Blazor.Tailwind.Components
|
||||||
@@ -224,7 +225,15 @@
|
|||||||
|
|
||||||
// Build signal r
|
// Build signal r
|
||||||
HubConnection = new HubConnectionBuilder()
|
HubConnection = new HubConnectionBuilder()
|
||||||
.WithUrl(websocketDetails.Target)
|
.WithUrl(websocketDetails.Target, options =>
|
||||||
|
{
|
||||||
|
options.AccessTokenProvider = async () =>
|
||||||
|
{
|
||||||
|
var details = await ServerService.GetWebSocket(ServerId);
|
||||||
|
return details.AccessToken;
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.WithAutomaticReconnect()
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
// Define handlers
|
// Define handlers
|
||||||
@@ -246,9 +255,6 @@
|
|||||||
|
|
||||||
// Connect
|
// Connect
|
||||||
await HubConnection.StartAsync();
|
await HubConnection.StartAsync();
|
||||||
|
|
||||||
// Authenticate
|
|
||||||
await HubConnection.SendAsync("Authenticate", websocketDetails.AccessToken);
|
|
||||||
}
|
}
|
||||||
catch (HttpApiException e)
|
catch (HttpApiException e)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user