191 lines
		
	
	
		
			6.3 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
			
		
		
	
	
			191 lines
		
	
	
		
			6.3 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
 | 
						|
{
 | 
						|
    private static readonly List<string> AllowedDeviceAlternative = ["watch"];
 | 
						|
 | 
						|
    [Route("/ws")]
 | 
						|
    [Authorize]
 | 
						|
    [SwaggerIgnore]
 | 
						|
    public async Task<ActionResult> TheGateway([FromQuery] string? deviceAlt)
 | 
						|
    {
 | 
						|
        if (string.IsNullOrWhiteSpace(deviceAlt))
 | 
						|
            deviceAlt = null;
 | 
						|
        if (deviceAlt is not null && !AllowedDeviceAlternative.Contains(deviceAlt))
 | 
						|
            return BadRequest("Unsupported device alternative: " + deviceAlt);
 | 
						|
 | 
						|
        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
 | 
						|
        )
 | 
						|
        {
 | 
						|
            return Unauthorized();
 | 
						|
        }
 | 
						|
 | 
						|
        var accountId = Guid.Parse(currentUser.Id!);
 | 
						|
        var deviceId = currentSession.Challenge?.DeviceId ?? Guid.NewGuid().ToString();
 | 
						|
 | 
						|
        if (string.IsNullOrEmpty(deviceId))
 | 
						|
            return BadRequest("Unable to get device ID from session.");
 | 
						|
        if (deviceAlt is not null)
 | 
						|
            deviceId = $"{deviceId}+{deviceAlt}";
 | 
						|
 | 
						|
        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 new EmptyResult();
 | 
						|
        }
 | 
						|
 | 
						|
        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}"
 | 
						|
            );
 | 
						|
        }
 | 
						|
 | 
						|
        return new EmptyResult();
 | 
						|
    }
 | 
						|
 | 
						|
    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);
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 |