From 72de0c5916fa4caade7c7cded10b0873d8f1cb56 Mon Sep 17 00:00:00 2001 From: Angelo Breuer <46497296+angelobreuer@users.noreply.github.com> Date: Sat, 20 Apr 2024 01:26:45 +0200 Subject: [PATCH] feat: Add initial bootstrapping for voice server interception --- .../Extensions/ServiceCollectionExtensions.cs | 17 ++++ .../IVoiceServerSessionManager.cs | 10 ++ .../Lavalink4NET.Experiments.Receive.csproj | 14 +++ .../LavalinkReceiveVoiceServerInterceptor.cs | 40 ++++++++ .../LavalinkVoiceServer.cs | 98 +++++++++++++++++++ .../Program.cs | 2 + .../VoiceServerSessionManager.cs | 38 +++++++ src/Lavalink4NET.sln | 14 ++- 8 files changed, 229 insertions(+), 4 deletions(-) create mode 100644 experiments/Lavalink4NET.Experiments.Receive/Extensions/ServiceCollectionExtensions.cs create mode 100644 experiments/Lavalink4NET.Experiments.Receive/IVoiceServerSessionManager.cs create mode 100644 experiments/Lavalink4NET.Experiments.Receive/Lavalink4NET.Experiments.Receive.csproj create mode 100644 experiments/Lavalink4NET.Experiments.Receive/LavalinkReceiveVoiceServerInterceptor.cs create mode 100644 experiments/Lavalink4NET.Experiments.Receive/LavalinkVoiceServer.cs create mode 100644 experiments/Lavalink4NET.Experiments.Receive/Program.cs create mode 100644 experiments/Lavalink4NET.Experiments.Receive/VoiceServerSessionManager.cs diff --git a/experiments/Lavalink4NET.Experiments.Receive/Extensions/ServiceCollectionExtensions.cs b/experiments/Lavalink4NET.Experiments.Receive/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 00000000..eab20b23 --- /dev/null +++ b/experiments/Lavalink4NET.Experiments.Receive/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,17 @@ +namespace Lavalink4NET.Experiments.Receive.Extensions; + +using Lavalink4NET.Players; +using Microsoft.Extensions.DependencyInjection.Extensions; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddLavalinkReceive(this IServiceCollection services) + { + ArgumentNullException.ThrowIfNull(services); + + services.TryAddSingleton(); + services.Replace(ServiceDescriptor.Singleton()); + + return services; + } +} diff --git a/experiments/Lavalink4NET.Experiments.Receive/IVoiceServerSessionManager.cs b/experiments/Lavalink4NET.Experiments.Receive/IVoiceServerSessionManager.cs new file mode 100644 index 00000000..c6e25803 --- /dev/null +++ b/experiments/Lavalink4NET.Experiments.Receive/IVoiceServerSessionManager.cs @@ -0,0 +1,10 @@ +namespace Lavalink4NET.Experiments.Receive; + +using Lavalink4NET.Clients; + +interface IVoiceServerSessionManager +{ + Guid Allocate(ulong guildId, VoiceServer voiceServer); + + bool TryResolve(Guid sessionId, out ulong guildId, out VoiceServer voiceServer); +} diff --git a/experiments/Lavalink4NET.Experiments.Receive/Lavalink4NET.Experiments.Receive.csproj b/experiments/Lavalink4NET.Experiments.Receive/Lavalink4NET.Experiments.Receive.csproj new file mode 100644 index 00000000..adc3ba7b --- /dev/null +++ b/experiments/Lavalink4NET.Experiments.Receive/Lavalink4NET.Experiments.Receive.csproj @@ -0,0 +1,14 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + diff --git a/experiments/Lavalink4NET.Experiments.Receive/LavalinkReceiveVoiceServerInterceptor.cs b/experiments/Lavalink4NET.Experiments.Receive/LavalinkReceiveVoiceServerInterceptor.cs new file mode 100644 index 00000000..55066cb3 --- /dev/null +++ b/experiments/Lavalink4NET.Experiments.Receive/LavalinkReceiveVoiceServerInterceptor.cs @@ -0,0 +1,40 @@ +namespace Lavalink4NET.Experiments.Receive; + +using System.Threading; +using System.Threading.Tasks; +using Lavalink4NET.Clients; +using Lavalink4NET.Players; + +internal sealed class LavalinkReceiveVoiceServerInterceptor : ILavalinkVoiceServerInterceptor +{ + private readonly IVoiceServerSessionManager _sessionManager; + private readonly ILogger _logger; + + public LavalinkReceiveVoiceServerInterceptor( + IVoiceServerSessionManager sessionManager, + ILogger logger) + { + ArgumentNullException.ThrowIfNull(sessionManager); + ArgumentNullException.ThrowIfNull(logger); + + _sessionManager = sessionManager; + _logger = logger; + } + + public ValueTask InterceptAsync( + ulong guildId, + VoiceServer voiceServer, + CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + var sessionToken = _sessionManager.Allocate(guildId, voiceServer); + var proxiedVoiceServer = new VoiceServer(sessionToken.ToString("N"), "localhost:16389"); + + _logger.LogInformation( + "Mapping voice server '{OriginalEndpoint}' ({OriginalToken}) to '{ProxiedEndpoint}' ({ProxiedToken})", + voiceServer.Endpoint, voiceServer.Token, proxiedVoiceServer.Endpoint, proxiedVoiceServer.Token); + + return new ValueTask(proxiedVoiceServer); + } +} diff --git a/experiments/Lavalink4NET.Experiments.Receive/LavalinkVoiceServer.cs b/experiments/Lavalink4NET.Experiments.Receive/LavalinkVoiceServer.cs new file mode 100644 index 00000000..cccfae69 --- /dev/null +++ b/experiments/Lavalink4NET.Experiments.Receive/LavalinkVoiceServer.cs @@ -0,0 +1,98 @@ +namespace Lavalink4NET.Experiments.Receive; + +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets; +using Microsoft.AspNetCore.WebSockets; +using Microsoft.Extensions.Options; +using Microsoft.Net.Http.Headers; + +internal sealed class LavalinkVoiceServer : IHttpApplication +{ + private readonly IVoiceServerSessionManager _serverSessionManager; + private readonly KestrelServer _kestrelServer; + private readonly WebSocketMiddleware _webSocketMiddleware; + + public LavalinkVoiceServer( + IVoiceServerSessionManager serverSessionManager, + ILoggerFactory loggerFactory) + { + ArgumentNullException.ThrowIfNull(serverSessionManager); + + _serverSessionManager = serverSessionManager; + + var kestrelServerOptions = new KestrelServerOptions { AddServerHeader = false, }; + kestrelServerOptions.ListenLocalhost(16389); + + var socketTransportOptions = new SocketTransportOptions { Backlog = 4, }; + var socketTransportFactory = new SocketTransportFactory(Options.Create(socketTransportOptions), loggerFactory); + + _kestrelServer = new KestrelServer( + options: Options.Create(kestrelServerOptions), + transportFactory: socketTransportFactory, + loggerFactory: loggerFactory); + + var webSocketOptions = new WebSocketOptions { }; + + _webSocketMiddleware = new WebSocketMiddleware( + next: ProcessRequestInternalAsync, + options: Options.Create(webSocketOptions), + loggerFactory: loggerFactory); + } + + public async ValueTask StartAsync(CancellationToken cancellationToken = default) + { + await _kestrelServer + .StartAsync(this, cancellationToken) + .ConfigureAwait(false); + } + + public async ValueTask StopAsync(CancellationToken cancellationToken = default) + { + await _kestrelServer + .StopAsync(cancellationToken) + .ConfigureAwait(false); + } + + HttpContext IHttpApplication.CreateContext(IFeatureCollection contextFeatures) + { + ArgumentNullException.ThrowIfNull(contextFeatures); + + return new DefaultHttpContext(contextFeatures); + } + + void IHttpApplication.DisposeContext(HttpContext context, Exception? exception) + { + } + + Task IHttpApplication.ProcessRequestAsync(HttpContext context) + { + ArgumentNullException.ThrowIfNull(context); + + return _webSocketMiddleware.Invoke(context); + } + + private async Task ProcessRequestInternalAsync(HttpContext httpContext) + { + ArgumentNullException.ThrowIfNull(httpContext); + + var cancellationToken = httpContext.RequestAborted; + cancellationToken.ThrowIfCancellationRequested(); + + if (!httpContext.WebSockets.IsWebSocketRequest) + { + httpContext.Response.StatusCode = StatusCodes.Status426UpgradeRequired; + httpContext.Response.Headers[HeaderNames.Upgrade] = "websocket"; + httpContext.Response.Headers[HeaderNames.Connection] = "Upgrade"; + + return; + } + + var webSocketAcceptContext = new WebSocketAcceptContext { }; + + var webSocket = await httpContext.WebSockets + .AcceptWebSocketAsync(webSocketAcceptContext) + .ConfigureAwait(false); + } +} \ No newline at end of file diff --git a/experiments/Lavalink4NET.Experiments.Receive/Program.cs b/experiments/Lavalink4NET.Experiments.Receive/Program.cs new file mode 100644 index 00000000..3751555c --- /dev/null +++ b/experiments/Lavalink4NET.Experiments.Receive/Program.cs @@ -0,0 +1,2 @@ +// See https://aka.ms/new-console-template for more information +Console.WriteLine("Hello, World!"); diff --git a/experiments/Lavalink4NET.Experiments.Receive/VoiceServerSessionManager.cs b/experiments/Lavalink4NET.Experiments.Receive/VoiceServerSessionManager.cs new file mode 100644 index 00000000..32d26230 --- /dev/null +++ b/experiments/Lavalink4NET.Experiments.Receive/VoiceServerSessionManager.cs @@ -0,0 +1,38 @@ +namespace Lavalink4NET.Experiments.Receive; + +using System.Collections.Concurrent; +using Lavalink4NET.Clients; + +internal sealed class VoiceServerSessionManager : IVoiceServerSessionManager +{ + private readonly ConcurrentDictionary _voiceServers; + + public VoiceServerSessionManager() + { + _voiceServers = new ConcurrentDictionary(); + } + + public Guid Allocate(ulong guildId, VoiceServer voiceServer) + { + var sessionId = Guid.NewGuid(); + _voiceServers.TryAdd(sessionId, (guildId, voiceServer)); + + return sessionId; + } + + public bool TryResolve(Guid sessionId, out ulong guildId, out VoiceServer voiceServer) + { + if (_voiceServers.TryGetValue(sessionId, out var pair)) + { + guildId = pair.GuildId; + voiceServer = pair.VoiceServer; + + return true; + } + + guildId = default; + voiceServer = default; + + return false; + } +} \ No newline at end of file diff --git a/src/Lavalink4NET.sln b/src/Lavalink4NET.sln index 4aab41d1..7853d93a 100644 --- a/src/Lavalink4NET.sln +++ b/src/Lavalink4NET.sln @@ -88,13 +88,15 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lavalink4NET.Integrations.L EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lavalink4NET.Integrations.Lavasrc.Tests", "..\tests\Lavalink4NET.Integrations.Lavasrc.Tests\Lavalink4NET.Integrations.Lavasrc.Tests.csproj", "{5779F765-5F0D-422C-984A-7E44EAE737C8}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lavalink4NET.NetCord", "Lavalink4NET.NetCord\Lavalink4NET.NetCord.csproj", "{8587F98B-CFE1-4559-9614-ED3B2B0C4F4E}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lavalink4NET.NetCord", "Lavalink4NET.NetCord\Lavalink4NET.NetCord.csproj", "{8587F98B-CFE1-4559-9614-ED3B2B0C4F4E}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lavalink4NET.Samples.NetCord", "..\samples\Lavalink4NET.Samples.NetCord\Lavalink4NET.Samples.NetCord.csproj", "{02FE863F-D979-439A-9A51-C4EA69D58D29}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lavalink4NET.Samples.NetCord", "..\samples\Lavalink4NET.Samples.NetCord\Lavalink4NET.Samples.NetCord.csproj", "{02FE863F-D979-439A-9A51-C4EA69D58D29}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lavalink4NET.Integrations.LyricsJava", "Lavalink4NET.Integrations.LyricsJava\Lavalink4NET.Integrations.LyricsJava.csproj", "{9A30E985-6D67-41D4-A12F-F1ADCD2ED0FE}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lavalink4NET.Integrations.LyricsJava", "Lavalink4NET.Integrations.LyricsJava\Lavalink4NET.Integrations.LyricsJava.csproj", "{9A30E985-6D67-41D4-A12F-F1ADCD2ED0FE}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lavalink4NET.Integrations.LyricsJava.Tests", "Lavalink4NET.Integrations.LyricsJava.Tests\Lavalink4NET.Integrations.LyricsJava.Tests.csproj", "{176B0345-DF57-42B4-A8FD-4E6436D9554C}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lavalink4NET.Integrations.LyricsJava.Tests", "Lavalink4NET.Integrations.LyricsJava.Tests\Lavalink4NET.Integrations.LyricsJava.Tests.csproj", "{176B0345-DF57-42B4-A8FD-4E6436D9554C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lavalink4NET.Experiments.Receive", "..\experiments\Lavalink4NET.Experiments.Receive\Lavalink4NET.Experiments.Receive.csproj", "{F7EC54EF-8C23-4586-BC86-D9B7B702CE14}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -258,6 +260,10 @@ Global {176B0345-DF57-42B4-A8FD-4E6436D9554C}.Debug|Any CPU.Build.0 = Debug|Any CPU {176B0345-DF57-42B4-A8FD-4E6436D9554C}.Release|Any CPU.ActiveCfg = Release|Any CPU {176B0345-DF57-42B4-A8FD-4E6436D9554C}.Release|Any CPU.Build.0 = Release|Any CPU + {F7EC54EF-8C23-4586-BC86-D9B7B702CE14}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F7EC54EF-8C23-4586-BC86-D9B7B702CE14}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F7EC54EF-8C23-4586-BC86-D9B7B702CE14}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F7EC54EF-8C23-4586-BC86-D9B7B702CE14}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE