First commit

This commit is contained in:
olcxja 2026-04-24 07:38:15 +02:00
commit 0ac6ff9196
26 changed files with 2836 additions and 0 deletions

View file

@ -0,0 +1,8 @@
using System.Text.Json.Serialization;
namespace LarpixVoice;
[JsonSerializable(typeof(string[]))]
internal partial class JsonAppSerializerContext : JsonSerializerContext
{
}

View file

@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<NoDefaultLaunchSettingsFile>True</NoDefaultLaunchSettingsFile>
</PropertyGroup>
</Project>

95
LarpixVoice/Program.cs Normal file
View file

@ -0,0 +1,95 @@
using System.Collections.Concurrent;
namespace LarpixVoice;
public class Program
{
public static ConcurrentDictionary<string, ConcurrentDictionary<ulong, Session>> Rooms = new ();
public static ulong TotalBytesSent = 0;
public static ulong TotalBytesReceived = 0;
private static ulong CurrentBandwidthKps = 0;
public static void Main(string[] args)
{
Utils.LoadEnv();
Task.Run(async () =>
{
while (true)
{
await Task.Delay(1000);
ulong sentDelta = Interlocked.Read(ref TotalBytesSent);
ulong receivedDelta = Interlocked.Read(ref TotalBytesReceived);
Interlocked.Exchange(ref TotalBytesReceived, 0);
Interlocked.Exchange(ref TotalBytesSent, 0);
CurrentBandwidthKps = (sentDelta + receivedDelta);
}
});
var builder = WebApplication.CreateBuilder(args);
builder.Logging.ClearProviders();
builder.WebHost.ConfigureKestrel(serverOptions =>
{
serverOptions.AddServerHeader = false;
serverOptions.ListenAnyIP(Utils.PORT);
});
var app = builder.Build();
app.UseWebSockets();
app.Use(async (HttpContext context, Func<Task> next) =>
{
try
{
context.Response.StatusCode = 200;
var path = context.Request.Path.Value;
var url = $"{context.Request.Scheme}://{context.Request.Host}";
if (path == "" || path?.Length > 512)
{
context.Response.Redirect(url + "/");
return;
}
context.Response.Headers["Access-Control-Allow-Origin"] = "*";//(context.Request.Headers["Origin"]).ToString().Replace("http://", "https://");
context.Response.Headers["Access-Control-Allow-Methods"]= "GET, POST, PUT, DELETE, OPTIONS";
context.Response.Headers["Access-Control-Allow-Headers"]= "Content-Type, Authorization";
context.Response.Headers["Access-Control-Allow-Credentials"]= "true";
switch (path)
{
case "/ws":
await Requests.Websocket(context);
return;
case "/load":
await context.Response.WriteAsync(Interlocked.Read(ref CurrentBandwidthKps).ToString());
return;
case "/room/users":
string roomId = context.Request.Query["room"].ToString();
context.Response.ContentType = "application/json";
if (Rooms.TryGetValue(roomId, out var room))
{
string[] users = room.Keys.Select(k => k.ToString()).ToArray();
await context.Response.WriteAsJsonAsync(users, JsonAppSerializerContext.Default.StringArray);
}
else
{
await context.Response.WriteAsJsonAsync(Array.Empty<string>(), JsonAppSerializerContext.Default.StringArray);
}
return;
default:
await next();
return;
}
}
catch (Exception)
{
}
});
Console.WriteLine("Starting server at port: " + Utils.PORT);
app.Run();
}
}

124
LarpixVoice/Requests.cs Normal file
View file

@ -0,0 +1,124 @@
using System.Buffers;
using System.Collections.Concurrent;
using System.Net.WebSockets;
using static LarpixVoice.Program;
namespace LarpixVoice;
public class Requests
{
public static async Task Websocket(HttpContext context)
{
if (!context.WebSockets.IsWebSocketRequest)
{
context.Response.StatusCode = 400;
return;
}
string roomId = context.Request.Query["room"].ToString();
string userIdStr = context.Request.Query["userId"].ToString();
string secret = context.Request.Query["secret"].ToString();
if (string.IsNullOrEmpty(roomId) || !ulong.TryParse(userIdStr, out ulong clientId))
{
context.Response.StatusCode = 400;
return;
}
using var webSocket = await context.WebSockets.AcceptWebSocketAsync();
var roomClients = Rooms.GetOrAdd(roomId, _ => new ConcurrentDictionary<ulong, Session>());
if (roomClients.TryGetValue(clientId, out var oldSession) && oldSession.Socket.State == WebSocketState.Open)
{
try
{
await oldSession.Socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Kicked", CancellationToken.None);
}
catch
{
}
}
var currentSession = new Session(webSocket);
roomClients[clientId] = currentSession;
_ = Task.Run(async () =>
{
await foreach (var packet in currentSession.SendQueue.Reader.ReadAllAsync())
{
try
{
if (currentSession.Socket.State == WebSocketState.Open)
{
await currentSession.Socket.SendAsync(new ArraySegment<byte>(packet.Buffer, 0, packet.Length),
WebSocketMessageType.Binary, true, CancellationToken.None);
Interlocked.Add(ref TotalBytesSent, (ulong)packet.Length);
}
}
catch
{
}
finally
{
ArrayPool<byte>.Shared.Return(packet.Buffer);
}
}
});
var receiveBuffer = ArrayPool<byte>.Shared.Rent(1024 * 1024);
try
{
while (webSocket.State == WebSocketState.Open)
{
int totalBytes = 0;
WebSocketReceiveResult result;
do
{
result = await webSocket.ReceiveAsync(
new ArraySegment<byte>(receiveBuffer, totalBytes, receiveBuffer.Length - totalBytes),
CancellationToken.None);
totalBytes += result.Count;
Interlocked.Add(ref TotalBytesReceived, (ulong)result.Count);
} while (!result.EndOfMessage);
if (result.MessageType == WebSocketMessageType.Close) break;
if (result.MessageType == WebSocketMessageType.Binary)
{
int payloadLength = totalBytes + 8;
foreach (var (id, client) in roomClients)
{
if (id != clientId && client.Socket.State == WebSocketState.Open)
{
byte[] payload = ArrayPool<byte>.Shared.Rent(payloadLength);
BitConverter.TryWriteBytes(payload, clientId);
Array.Copy(receiveBuffer, 0, payload, 8, totalBytes);
if (!client.SendQueue.Writer.TryWrite((payload, payloadLength)))
ArrayPool<byte>.Shared.Return(payload);
}
}
}
}
}
catch (WebSocketException)
{
}
finally
{
ArrayPool<byte>.Shared.Return(receiveBuffer);
if (roomClients.TryGetValue(clientId, out var session) && session == currentSession)
{
roomClients.TryRemove(clientId, out _);
session.SendQueue.Writer.Complete();
}
if (roomClients.IsEmpty) Rooms.TryRemove(roomId, out _);
}
}
}

19
LarpixVoice/Session.cs Normal file
View file

@ -0,0 +1,19 @@
using System.Net.WebSockets;
using System.Threading.Channels;
namespace LarpixVoice;
public class Session
{
public WebSocket Socket { get; }
public Channel<(byte[] Buffer, int Length)> SendQueue { get; }
public Session(WebSocket socket)
{
Socket = socket;
SendQueue = Channel.CreateBounded<(byte[] Buffer, int Length)>(new BoundedChannelOptions(256)
{
FullMode = BoundedChannelFullMode.Wait
});
}
}

42
LarpixVoice/Utils.cs Normal file
View file

@ -0,0 +1,42 @@
namespace LarpixVoice;
public class Utils
{
public static int PORT = 8091;
public static int MAIN_PORT = 8090;
public static string SERVER_KEY = "SecretThingOMGSoHardToGuess";
public static void LoadEnv()
{
if (File.Exists(".env"))
{
foreach (var line in File.ReadAllLines(".env"))
{
var trimmed = line.Trim();
if (string.IsNullOrWhiteSpace(trimmed) || trimmed.StartsWith("#")) continue;
var parts = trimmed.Split('=', 2, StringSplitOptions.None);
if (parts.Length == 2)
{
var key = parts[0].Trim();
var val = parts[1].Trim();
Environment.SetEnvironmentVariable(key, val);
}
}
}
string? port = Environment.GetEnvironmentVariable("PORT_VOICE");
if (port != null)
{
PORT = int.Parse(port);
}
port = Environment.GetEnvironmentVariable("PORT");
if (port != null)
{
MAIN_PORT = int.Parse(port);
}
SERVER_KEY = Environment.GetEnvironmentVariable("SERVER_KEY") ?? SERVER_KEY;
}
}

View file

@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

View file

@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}