Skip to content

Commit 8e242dc

Browse files
authored
Merge pull request #456
Implemented SignalR (+ Scaling)
2 parents 8573fff + efca9cf commit 8e242dc

File tree

15 files changed

+194
-71
lines changed

15 files changed

+194
-71
lines changed

Moonlight.ApiServer/Configuration/AppConfiguration.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ public record AppConfiguration
3030
[YamlMember(Description = "\nSettings for open telemetry")]
3131
public OpenTelemetryData OpenTelemetry { get; set; } = new();
3232

33+
[YamlMember(Description = "\nConfiguration for the realtime communication solution SignalR")]
34+
public SignalRData SignalR { get; set; } = new();
35+
3336
public static AppConfiguration CreateEmpty()
3437
{
3538
return new AppConfiguration()
@@ -47,6 +50,15 @@ public static AppConfiguration CreateEmpty()
4750
};
4851
}
4952

53+
public record SignalRData
54+
{
55+
[YamlMember(Description =
56+
"\nWhether to use redis (or any other redis compatible solution) to scale out SignalR hubs. This is required when using multiple api server replicas")]
57+
public bool UseRedis { get; set; } = false;
58+
59+
public string RedisConnectionString { get; set; } = "";
60+
}
61+
5062
public record FilesData
5163
{
5264
[YamlMember(Description = "The maximum file size limit a combine operation is allowed to process")]

Moonlight.ApiServer/Extensions/ZipArchiveExtensions.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ namespace Moonlight.ApiServer.Extensions;
55

66
public static class ZipArchiveExtensions
77
{
8-
public static async Task AddBinary(this ZipArchive archive, string name, byte[] bytes)
8+
public static async Task AddBinaryAsync(this ZipArchive archive, string name, byte[] bytes)
99
{
1010
var entry = archive.CreateEntry(name);
1111
await using var dataStream = entry.Open();
@@ -14,13 +14,13 @@ public static async Task AddBinary(this ZipArchive archive, string name, byte[]
1414
await dataStream.FlushAsync();
1515
}
1616

17-
public static async Task AddText(this ZipArchive archive, string name, string content)
17+
public static async Task AddTextAsync(this ZipArchive archive, string name, string content)
1818
{
1919
var data = Encoding.UTF8.GetBytes(content);
20-
await archive.AddBinary(name, data);
20+
await archive.AddBinaryAsync(name, data);
2121
}
2222

23-
public static async Task AddFile(this ZipArchive archive, string name, string path)
23+
public static async Task AddFileAsync(this ZipArchive archive, string name, string path)
2424
{
2525
var fs = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
2626

Moonlight.ApiServer/Http/Controllers/Admin/Sys/DiagnoseController.cs

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,21 +21,16 @@ public DiagnoseController(DiagnoseService diagnoseService)
2121
}
2222

2323
[HttpPost]
24-
public async Task Diagnose([FromBody] GenerateDiagnoseRequest request)
24+
public async Task<ActionResult> Diagnose([FromBody] GenerateDiagnoseRequest request)
2525
{
26-
var stream = await DiagnoseService.GenerateDiagnose(request.Providers);
27-
28-
await Results.Stream(
29-
stream,
30-
contentType: "application/zip",
31-
fileDownloadName: "diagnose.zip"
32-
)
33-
.ExecuteAsync(HttpContext);
26+
var stream = await DiagnoseService.GenerateDiagnoseAsync(request.Providers);
27+
28+
return File(stream, "application/zip", "diagnose.zip");
3429
}
3530

3631
[HttpGet("providers")]
37-
public async Task<DiagnoseProvideResponse[]> GetProviders()
32+
public async Task<ActionResult<DiagnoseProvideResponse[]>> GetProviders()
3833
{
39-
return await DiagnoseService.GetProviders();
34+
return await DiagnoseService.GetProvidersAsync();
4035
}
4136
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using Microsoft.AspNetCore.Authorization;
2+
using Microsoft.AspNetCore.SignalR;
3+
4+
namespace Moonlight.ApiServer.Http.Hubs;
5+
6+
[Authorize(Policy = "permissions:admin.system.diagnose")]
7+
public class DiagnoseHub : Hub
8+
{
9+
[HubMethodName("Ping")]
10+
public async Task Ping()
11+
{
12+
await Clients.All.SendAsync("Pong");
13+
}
14+
}
Lines changed: 22 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
using System.Diagnostics;
12
using System.IO.Compression;
2-
using System.Text.Json;
3+
using MoonCore.Yaml;
34
using Moonlight.ApiServer.Configuration;
45
using Moonlight.ApiServer.Extensions;
56
using Moonlight.ApiServer.Interfaces;
@@ -8,11 +9,11 @@ namespace Moonlight.ApiServer.Implementations.Diagnose;
89

910
public class CoreConfigDiagnoseProvider : IDiagnoseProvider
1011
{
11-
private readonly AppConfiguration Config;
12+
private readonly AppConfiguration Configuration;
1213

13-
public CoreConfigDiagnoseProvider(AppConfiguration config)
14+
public CoreConfigDiagnoseProvider(AppConfiguration configuration)
1415
{
15-
Config = config;
16+
Configuration = configuration;
1617
}
1718

1819
private string CheckForNullOrEmpty(string? content)
@@ -22,29 +23,25 @@ private string CheckForNullOrEmpty(string? content)
2223
: "ISNOTEMPTY";
2324
}
2425

25-
public async Task ModifyZipArchive(ZipArchive archive)
26+
public async Task ModifyZipArchiveAsync(ZipArchive archive)
2627
{
27-
var json = JsonSerializer.Serialize(Config);
28-
var config = JsonSerializer.Deserialize<AppConfiguration>(json);
29-
30-
if (config == null)
28+
try
3129
{
32-
await archive.AddText("core/config.txt", "Could not fetch config.");
33-
return;
30+
var configString = YamlSerializer.Serialize(Configuration);
31+
var configuration = YamlSerializer.Deserialize<AppConfiguration>(configString);
32+
33+
configuration.Database.Password = CheckForNullOrEmpty(configuration.Database.Password);
34+
configuration.Authentication.Secret = CheckForNullOrEmpty(configuration.Authentication.Secret);
35+
configuration.SignalR.RedisConnectionString = CheckForNullOrEmpty(configuration.SignalR.RedisConnectionString);
36+
37+
await archive.AddTextAsync(
38+
"core/config.txt",
39+
YamlSerializer.Serialize(configuration)
40+
);
41+
}
42+
catch (Exception e)
43+
{
44+
await archive.AddTextAsync("core/config.txt", $"Unable to load config: {e.ToStringDemystified()}");
3445
}
35-
36-
config.Database.Password = CheckForNullOrEmpty(config.Database.Password);
37-
config.Authentication.Secret = CheckForNullOrEmpty(config.Authentication.Secret);
38-
39-
await archive.AddText(
40-
"core/config.txt",
41-
JsonSerializer.Serialize(
42-
config,
43-
new JsonSerializerOptions()
44-
{
45-
WriteIndented = true
46-
}
47-
)
48-
);
4946
}
5047
}

Moonlight.ApiServer/Implementations/Diagnose/LogsDiagnoseProvider.cs

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,16 @@ namespace Moonlight.ApiServer.Implementations.Diagnose;
66

77
public class LogsDiagnoseProvider : IDiagnoseProvider
88
{
9-
public async Task ModifyZipArchive(ZipArchive archive)
9+
public async Task ModifyZipArchiveAsync(ZipArchive archive)
1010
{
11-
var path = Path.Combine("storage", "logs", "latest.log");
11+
var path = Path.Combine("storage", "logs", "moonlight.log");
1212

13-
if (!File.Exists(path))
13+
if (File.Exists(path))
1414
{
15-
await archive.AddText("logs.txt", "Logs file latest.log has not been found");
16-
return;
15+
var logsContent = await File.ReadAllTextAsync(path);
16+
await archive.AddTextAsync("logs.txt", logsContent);
1717
}
18-
19-
var logsContent = await File.ReadAllTextAsync(path);
20-
await archive.AddText("logs.txt", logsContent);
18+
else
19+
await archive.AddTextAsync("logs.txt", "Logs file moonlight.log has not been found");
2120
}
2221
}

Moonlight.ApiServer/Interfaces/IDiagnoseProvider.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@ namespace Moonlight.ApiServer.Interfaces;
44

55
public interface IDiagnoseProvider
66
{
7-
public Task ModifyZipArchive(ZipArchive archive);
7+
public Task ModifyZipArchiveAsync(ZipArchive archive);
88
}

Moonlight.ApiServer/Moonlight.ApiServer.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.20"/>
2626
<PackageReference Include="Hangfire.Core" Version="1.8.20"/>
2727
<PackageReference Include="Hangfire.EntityFrameworkCore" Version="0.7.0"/>
28+
<PackageReference Include="Microsoft.AspNetCore.SignalR.StackExchangeRedis" Version="9.0.9" />
2829
<PackageReference Include="MoonCore" Version="1.9.7" />
2930
<PackageReference Include="MoonCore.Extended" Version="1.3.7" />
3031
<PackageReference Include="MoonCore.PluginFramework.Generator" Version="1.0.2"/>

Moonlight.ApiServer/Services/DiagnoseService.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ ILogger<DiagnoseService> logger
2222
Logger = logger;
2323
}
2424

25-
public Task<DiagnoseProvideResponse[]> GetProviders()
25+
public Task<DiagnoseProvideResponse[]> GetProvidersAsync()
2626
{
2727
var availableProviders = new List<DiagnoseProvideResponse>();
2828

@@ -48,7 +48,7 @@ public Task<DiagnoseProvideResponse[]> GetProviders()
4848
);
4949
}
5050

51-
public async Task<MemoryStream> GenerateDiagnose(string[] requestedProviders)
51+
public async Task<MemoryStream> GenerateDiagnoseAsync(string[] requestedProviders)
5252
{
5353
IDiagnoseProvider[] providers;
5454

@@ -78,7 +78,7 @@ public async Task<MemoryStream> GenerateDiagnose(string[] requestedProviders)
7878

7979
foreach (var provider in providers)
8080
{
81-
await provider.ModifyZipArchive(zipArchive);
81+
await provider.ModifyZipArchiveAsync(zipArchive);
8282
}
8383

8484
zipArchive.Dispose();

Moonlight.ApiServer/Startup/Startup.Auth.cs

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,18 @@ private Task RegisterAuth()
2323
// we want to use the ApiKey scheme for authenticating the request
2424
options.ForwardDefaultSelector = context =>
2525
{
26-
if (!context.Request.Headers.TryGetValue("Authorization", out var authHeader))
27-
return "Session";
28-
29-
var auth = authHeader.FirstOrDefault();
30-
31-
if (string.IsNullOrEmpty(auth) || !auth.StartsWith("Bearer "))
32-
return "Session";
33-
34-
return "ApiKey";
26+
var headers = context.Request.Headers;
27+
28+
// For regular api calls
29+
if (headers.ContainsKey("Authorization"))
30+
return "ApiKey";
31+
32+
// For websocket requests which cannot use the Authorization header
33+
if (headers.Upgrade == "websocket" && headers.Connection == "Upgrade" && context.Request.Query.ContainsKey("access_token"))
34+
return "ApiKey";
35+
36+
// Regular user traffic/auth
37+
return "Session";
3538
};
3639
})
3740
.AddJwtBearer("ApiKey", null, options =>
@@ -63,6 +66,16 @@ private Task RegisterAuth()
6366

6467
if (!result)
6568
context.Fail("API key has been deleted");
69+
},
70+
71+
OnMessageReceived = context =>
72+
{
73+
var accessToken = context.Request.Query["access_token"];
74+
75+
if (!string.IsNullOrEmpty(accessToken))
76+
context.Token = accessToken;
77+
78+
return Task.CompletedTask;
6679
}
6780
};
6881
})

0 commit comments

Comments
 (0)