165 lines
		
	
	
		
			5.5 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
			
		
		
	
	
			165 lines
		
	
	
		
			5.5 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
using System.Net.WebSockets;
 | 
						|
using DysonNetwork.Shared.Proto;
 | 
						|
using DysonNetwork.Shared.Stream;
 | 
						|
using Microsoft.AspNetCore.Authorization;
 | 
						|
using Microsoft.AspNetCore.Mvc;
 | 
						|
using NATS.Client.Core;
 | 
						|
using NATS.Net;
 | 
						|
using Swashbuckle.AspNetCore.Annotations;
 | 
						|
using WebSocketPacket = DysonNetwork.Shared.Models.WebSocketPacket;
 | 
						|
 | 
						|
namespace DysonNetwork.Ring.Connection;
 | 
						|
 | 
						|
[ApiController]
 | 
						|
public class WebSocketController(
 | 
						|
    WebSocketService ws,
 | 
						|
    ILogger<WebSocketContext> logger,
 | 
						|
    INatsConnection nats
 | 
						|
) : ControllerBase
 | 
						|
{
 | 
						|
    [Route("/ws")]
 | 
						|
    [Authorize]
 | 
						|
    [SwaggerIgnore]
 | 
						|
    public async Task TheGateway()
 | 
						|
    {
 | 
						|
        HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue);
 | 
						|
        HttpContext.Items.TryGetValue("CurrentSession", out var currentSessionValue);
 | 
						|
        if (currentUserValue is not Account currentUser ||
 | 
						|
            currentSessionValue is not AuthSession currentSession)
 | 
						|
        {
 | 
						|
            HttpContext.Response.StatusCode = StatusCodes.Status401Unauthorized;
 | 
						|
            return;
 | 
						|
        }
 | 
						|
 | 
						|
        var accountId = Guid.Parse(currentUser.Id!);
 | 
						|
        var deviceId = currentSession.Challenge?.DeviceId ?? Guid.NewGuid().ToString();
 | 
						|
 | 
						|
        if (string.IsNullOrEmpty(deviceId))
 | 
						|
        {
 | 
						|
            HttpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
 | 
						|
            return;
 | 
						|
        }
 | 
						|
 | 
						|
        var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync(new WebSocketAcceptContext
 | 
						|
            { KeepAliveInterval = TimeSpan.FromSeconds(60) });
 | 
						|
        var cts = new CancellationTokenSource();
 | 
						|
        var connectionKey = (accountId, deviceId);
 | 
						|
 | 
						|
        if (!ws.TryAdd(connectionKey, webSocket, cts))
 | 
						|
        {
 | 
						|
            await webSocket.SendAsync(
 | 
						|
                new ArraySegment<byte>(new WebSocketPacket
 | 
						|
                {
 | 
						|
                    Type = "error.dupe",
 | 
						|
                    ErrorMessage = "Too many connections from the same device and account."
 | 
						|
                }.ToBytes()),
 | 
						|
                WebSocketMessageType.Binary,
 | 
						|
                true,
 | 
						|
                CancellationToken.None
 | 
						|
            );
 | 
						|
            await webSocket.CloseAsync(
 | 
						|
                WebSocketCloseStatus.PolicyViolation,
 | 
						|
                "Too many connections from the same device and account.",
 | 
						|
                CancellationToken.None
 | 
						|
            );
 | 
						|
            return;
 | 
						|
        }
 | 
						|
 | 
						|
        logger.LogDebug(
 | 
						|
            $"Connection established with user @{currentUser.Name}#{currentUser.Id} and device #{deviceId}");
 | 
						|
 | 
						|
        // Broadcast WebSocket connected event
 | 
						|
        await nats.PublishAsync(
 | 
						|
            WebSocketConnectedEvent.Type,
 | 
						|
            GrpcTypeHelper.ConvertObjectToByteString(new WebSocketConnectedEvent
 | 
						|
            {
 | 
						|
                AccountId = accountId,
 | 
						|
                DeviceId = deviceId,
 | 
						|
                IsOffline = false
 | 
						|
            }).ToByteArray(),
 | 
						|
            cancellationToken: cts.Token
 | 
						|
        );
 | 
						|
 | 
						|
        try
 | 
						|
        {
 | 
						|
            await _ConnectionEventLoop(deviceId, currentUser, webSocket, cts.Token);
 | 
						|
        }
 | 
						|
        catch (WebSocketException ex) when (ex.Message.Contains("The remote party closed the WebSocket connection without completing the close handshake"))
 | 
						|
        {
 | 
						|
            logger.LogDebug(
 | 
						|
                "WebSocket disconnected with user @{UserName}#{UserId} and device #{DeviceId} - client closed connection without proper handshake",
 | 
						|
                currentUser.Name,
 | 
						|
                currentUser.Id,
 | 
						|
                deviceId
 | 
						|
            );
 | 
						|
        }
 | 
						|
        catch (Exception ex)
 | 
						|
        {
 | 
						|
            logger.LogError(ex,
 | 
						|
                "WebSocket disconnected with user @{UserName}#{UserId} and device #{DeviceId} unexpectedly",
 | 
						|
                currentUser.Name,
 | 
						|
                currentUser.Id,
 | 
						|
                deviceId
 | 
						|
            );
 | 
						|
        }
 | 
						|
        finally
 | 
						|
        {
 | 
						|
            ws.Disconnect(connectionKey);
 | 
						|
 | 
						|
            // Broadcast WebSocket disconnected event
 | 
						|
            await nats.PublishAsync(
 | 
						|
                WebSocketDisconnectedEvent.Type,
 | 
						|
                GrpcTypeHelper.ConvertObjectToByteString(new WebSocketDisconnectedEvent
 | 
						|
                {
 | 
						|
                    AccountId = accountId,
 | 
						|
                    DeviceId = deviceId,
 | 
						|
                    IsOffline = !WebSocketService.GetAccountIsConnected(accountId)
 | 
						|
                }).ToByteArray(),
 | 
						|
                cancellationToken: cts.Token
 | 
						|
            );
 | 
						|
 | 
						|
            logger.LogDebug(
 | 
						|
                $"Connection disconnected with user @{currentUser.Name}#{currentUser.Id} and device #{deviceId}"
 | 
						|
            );
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    private async Task _ConnectionEventLoop(
 | 
						|
        string deviceId,
 | 
						|
        Account currentUser,
 | 
						|
        WebSocket webSocket,
 | 
						|
        CancellationToken cancellationToken
 | 
						|
    )
 | 
						|
    {
 | 
						|
        var connectionKey = (AccountId: Guid.Parse(currentUser.Id), DeviceId: deviceId);
 | 
						|
 | 
						|
        var buffer = new byte[1024 * 4];
 | 
						|
        try
 | 
						|
        {
 | 
						|
            while (true)
 | 
						|
            {
 | 
						|
                var receiveResult = await webSocket.ReceiveAsync(
 | 
						|
                    new ArraySegment<byte>(buffer),
 | 
						|
                    cancellationToken
 | 
						|
                );
 | 
						|
 | 
						|
                if (receiveResult.CloseStatus.HasValue)
 | 
						|
                    break;
 | 
						|
 | 
						|
                var packet = WebSocketPacket.FromBytes(buffer[..receiveResult.Count]);
 | 
						|
                await ws.HandlePacket(currentUser, connectionKey.DeviceId, packet, webSocket);
 | 
						|
            }
 | 
						|
        }
 | 
						|
        catch (OperationCanceledException)
 | 
						|
        {
 | 
						|
            if (
 | 
						|
                webSocket.State != WebSocketState.Closed
 | 
						|
                && webSocket.State != WebSocketState.Aborted
 | 
						|
            )
 | 
						|
            {
 | 
						|
                ws.Disconnect(connectionKey);
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 |