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);
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| }
 |