Add encrypted managed UDP relay and native voice conformance
This commit is contained in:
@@ -13,6 +13,7 @@ namespace VoiceCat.Server;
|
||||
public sealed class VoiceServer : IAsyncDisposable
|
||||
{
|
||||
private readonly Socket listener;
|
||||
private readonly MediaRelay media;
|
||||
private readonly ServerCredentials credentials;
|
||||
private readonly AccountStore accounts;
|
||||
private readonly IReadOnlyList<Voicecat.V1.Channel> channels;
|
||||
@@ -24,10 +25,12 @@ public sealed class VoiceServer : IAsyncDisposable
|
||||
private readonly List<Task> connections = [];
|
||||
private ulong nextSession;
|
||||
private uint nextUser;
|
||||
private uint nextSsrc;
|
||||
private readonly Task accepting;
|
||||
private int disposed;
|
||||
|
||||
public IPEndPoint EndPoint => (IPEndPoint)listener.LocalEndPoint!;
|
||||
public IPEndPoint MediaEndPoint => media.EndPoint;
|
||||
public event Action<Exception>? ConnectionFailed;
|
||||
|
||||
public VoiceServer(string directory, IPEndPoint endpoint, bool allowGuests = true, string name = "VoiceCat Server")
|
||||
@@ -42,6 +45,8 @@ public sealed class VoiceServer : IAsyncDisposable
|
||||
listener = new Socket(endpoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
|
||||
listener.Bind(endpoint);
|
||||
listener.Listen(64);
|
||||
media = new((IPEndPoint)listener.LocalEndPoint!);
|
||||
media.Failed += exception => ConnectionFailed?.Invoke(exception);
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -96,7 +101,8 @@ public sealed class VoiceServer : IAsyncDisposable
|
||||
Reject(session, "Unsupported protocol version or banned address.");
|
||||
break;
|
||||
}
|
||||
var hello = new ServerHello { ProtoVersion = 2, ServerName = name, ServerVersion = "0.1.0-dotnet", ServerIdentityFingerprint = ByteString.CopyFrom(SHA256.HashData(credentials.Identity.PublicKey)) };
|
||||
session.Media = new(RandomNumberGenerator.GetBytes(16), await session.Connection.TakeMediaCryptoAsync(shutdown.Token).ConfigureAwait(false));
|
||||
var hello = new ServerHello { ProtoVersion = 2, ServerName = name, ServerVersion = "0.1.0-dotnet", UdpPort = checked((uint)media.EndPoint.Port), ServerIdentityFingerprint = ByteString.CopyFrom(SHA256.HashData(credentials.Identity.PublicKey)) };
|
||||
if (allowGuests) hello.AuthMethods.Add("guest");
|
||||
hello.AuthMethods.Add("password");
|
||||
session.Connection.TrySend(new() { RequestId = envelope.RequestId, ServerHello = hello });
|
||||
@@ -114,8 +120,14 @@ public sealed class VoiceServer : IAsyncDisposable
|
||||
case Envelope.BodyOneofCase.TextMessage: RelayText(session, envelope.TextMessage); break;
|
||||
case Envelope.BodyOneofCase.Subscribe: SendSnapshot(session); break;
|
||||
case Envelope.BodyOneofCase.JoinChannel: Join(session, envelope.RequestId, envelope.JoinChannel.ChannelId); break;
|
||||
case Envelope.BodyOneofCase.SubscribeVoice:
|
||||
session.Connection.TrySend(new() { RequestId = envelope.RequestId, VoiceSubscriptionResult = new() { Error = "Managed media relay is not implemented yet." } });
|
||||
case Envelope.BodyOneofCase.SubscribeVoice: SubscribeVoice(session, envelope.RequestId, true); break;
|
||||
case Envelope.BodyOneofCase.UnsubscribeVoice: SubscribeVoice(session, envelope.RequestId, false); break;
|
||||
case Envelope.BodyOneofCase.StreamAnnounce: AnnounceStream(session, envelope.RequestId, envelope.StreamAnnounce); break;
|
||||
case Envelope.BodyOneofCase.StreamStop: StopStream(session, envelope.StreamStop.StreamId); break;
|
||||
case Envelope.BodyOneofCase.StreamState: UpdateStream(session, envelope.StreamState); break;
|
||||
case Envelope.BodyOneofCase.UdpBinding:
|
||||
if (!envelope.UdpBinding.Ack && CryptographicOperations.FixedTimeEquals(envelope.UdpBinding.UdpToken.Span, session.Media!.Token))
|
||||
session.Connection.TrySend(new() { RequestId = envelope.RequestId, UdpBinding = new() { Ack = true } });
|
||||
break;
|
||||
default:
|
||||
session.Connection.TrySend(new() { RequestId = envelope.RequestId, GenericResult = new() { Code = 1, Message = "Operation is not implemented by this server checkpoint." } });
|
||||
@@ -133,6 +145,8 @@ public sealed class VoiceServer : IAsyncDisposable
|
||||
lock (gate)
|
||||
{
|
||||
sessions.Remove(session.Id);
|
||||
if (session.User is null) session.Media?.Dispose();
|
||||
else PublishMedia();
|
||||
if (session.User is not null) Broadcast(new() { UserEvent = new() { Kind = UserEvent.Types.Kind.Left, LeftId = session.User.Id } });
|
||||
}
|
||||
await session.Connection.DisposeAsync().ConfigureAwait(false);
|
||||
@@ -170,9 +184,10 @@ public sealed class VoiceServer : IAsyncDisposable
|
||||
session.User = user;
|
||||
session.Connection.TrySend(new() { RequestId = requestId, AuthResult = new()
|
||||
{
|
||||
Ok = true, SessionId = session.Id, Self = user.Clone(),
|
||||
Ok = true, SessionId = session.Id, Self = user.Clone(), UdpToken = ByteString.CopyFrom(session.Media!.Token),
|
||||
Permissions = new() { IsAdmin = admin, CanAdminAccounts = admin, CanBan = admin, CanKick = admin, CanMoveUsers = admin, CanCreateTempChannel = admin }
|
||||
} });
|
||||
PublishMedia();
|
||||
Broadcast(new() { UserEvent = new() { Kind = UserEvent.Types.Kind.Joined, User = user.Clone() } }, session.Id);
|
||||
SendSnapshot(session);
|
||||
}
|
||||
@@ -199,7 +214,9 @@ public sealed class VoiceServer : IAsyncDisposable
|
||||
session.Connection.TrySend(new() { RequestId = requestId, JoinChannelResult = new() { Error = "Channel unavailable." } });
|
||||
return;
|
||||
}
|
||||
session.User!.ChannelId = channelId;
|
||||
if (session.User!.ChannelId != channelId) session.User.Streams.Clear();
|
||||
session.User.ChannelId = channelId;
|
||||
PublishMedia();
|
||||
var result = new JoinChannelResult { Ok = true, ChannelId = channelId, Audio = channel.Audio.Clone() };
|
||||
result.Members.Add(sessions.Values.Where(peer => peer.User?.ChannelId == channelId).Select(peer => peer.User!.Clone()));
|
||||
session.Connection.TrySend(new() { RequestId = requestId, JoinChannelResult = result });
|
||||
@@ -229,6 +246,69 @@ public sealed class VoiceServer : IAsyncDisposable
|
||||
}
|
||||
}
|
||||
|
||||
private void PublishMedia()
|
||||
{
|
||||
media.Publish(sessions.Values.Where(peer => peer.User is not null).Select(peer => new MediaRoute(
|
||||
peer.Media!, peer.User!.ChannelId, peer.User.VoiceSubscribed, peer.User.ServerMuted, peer.User.SelfDeafened || peer.User.ServerDeafened,
|
||||
peer.User.Streams.Select(stream => stream.Ssrc).ToArray())).ToArray());
|
||||
}
|
||||
|
||||
private void BroadcastUser(Session session) => Broadcast(new() { UserEvent = new() { Kind = UserEvent.Types.Kind.Updated, User = session.User!.Clone() } });
|
||||
|
||||
private void SubscribeVoice(Session session, ulong requestId, bool subscribed)
|
||||
{
|
||||
lock (gate)
|
||||
{
|
||||
session.User!.VoiceSubscribed = subscribed;
|
||||
if (!subscribed) session.User.Streams.Clear();
|
||||
PublishMedia();
|
||||
session.Connection.TrySend(new() { RequestId = requestId, VoiceSubscriptionResult = new() { Ok = true, Subscribed = subscribed } });
|
||||
BroadcastUser(session);
|
||||
}
|
||||
}
|
||||
|
||||
private void AnnounceStream(Session session, ulong requestId, StreamAnnounce request)
|
||||
{
|
||||
lock (gate)
|
||||
{
|
||||
if (!session.User!.VoiceSubscribed || !Enum.IsDefined(request.Kind) || request.Label.Length > 128 || session.User.Streams.Count >= 16 ||
|
||||
nextSsrc == uint.MaxValue || session.NextStream == uint.MaxValue || request.RequestedAudio?.BitrateBps is > 0 and < 500)
|
||||
{
|
||||
session.Connection.TrySend(new() { RequestId = requestId, StreamAnnounceResult = new() { Error = "Voice subscription required, invalid stream, or stream limit reached." } });
|
||||
return;
|
||||
}
|
||||
AudioConfig audio = channels.First(channel => channel.Id == session.User.ChannelId).Audio.Clone();
|
||||
if (request.RequestedAudio?.BitrateBps > 0) audio.BitrateBps = Math.Min(audio.BitrateBps, request.RequestedAudio.BitrateBps);
|
||||
var stream = new StreamInfo { StreamId = ++session.NextStream, Ssrc = ++nextSsrc, Kind = request.Kind, Label = request.Label, Audio = audio };
|
||||
session.User.Streams.Add(stream);
|
||||
PublishMedia();
|
||||
session.Connection.TrySend(new() { RequestId = requestId, StreamAnnounceResult = new() { Ok = true, StreamId = stream.StreamId, Ssrc = stream.Ssrc, EffectiveAudio = audio.Clone() } });
|
||||
BroadcastUser(session);
|
||||
}
|
||||
}
|
||||
|
||||
private void StopStream(Session session, uint streamId)
|
||||
{
|
||||
lock (gate)
|
||||
{
|
||||
StreamInfo? stream = session.User!.Streams.FirstOrDefault(candidate => candidate.StreamId == streamId);
|
||||
if (stream is null) return;
|
||||
session.User.Streams.Remove(stream);
|
||||
PublishMedia();
|
||||
BroadcastUser(session);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateStream(Session session, StreamStateUpdate update)
|
||||
{
|
||||
lock (gate)
|
||||
{
|
||||
StreamInfo? stream = session.User!.Streams.FirstOrDefault(candidate => candidate.StreamId == update.StreamId);
|
||||
if (stream is null) return;
|
||||
Broadcast(new() { StreamState = new() { UserId = session.User.Id, StreamId = stream.StreamId, Muted = update.Muted, Talking = update.Talking } });
|
||||
}
|
||||
}
|
||||
|
||||
private void Broadcast(Envelope envelope, ulong excluded = 0)
|
||||
{
|
||||
foreach (Session recipient in sessions.Values.Where(peer => peer.Id != excluded && peer.User is not null)) recipient.Connection.TrySend(envelope);
|
||||
@@ -248,9 +328,8 @@ public sealed class VoiceServer : IAsyncDisposable
|
||||
}
|
||||
finally
|
||||
{
|
||||
accounts.Dispose();
|
||||
credentials.Dispose();
|
||||
shutdown.Dispose();
|
||||
try { await media.DisposeAsync().ConfigureAwait(false); }
|
||||
finally { accounts.Dispose(); credentials.Dispose(); shutdown.Dispose(); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -261,5 +340,7 @@ public sealed class VoiceServer : IAsyncDisposable
|
||||
public string Address { get; } = address;
|
||||
public bool HelloReceived { get; set; }
|
||||
public User? User { get; set; }
|
||||
public MediaPeer? Media { get; set; }
|
||||
public uint NextStream;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user