Remotely/Server/Hubs/ViewerHub.cs

306 lines
10 KiB
C#

using Remotely.Server.Enums;
using Remotely.Server.Filters;
using Remotely.Server.Models;
using Remotely.Server.Services;
using Remotely.Shared.Interfaces;
using Remotely.Shared.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;
namespace Remotely.Server.Hubs;
[ServiceFilter(typeof(ViewerAuthorizationFilter))]
public class ViewerHub : Hub<IViewerHubClient>
{
private readonly IHubContext<DesktopHub, IDesktopHubClient> _desktopHub;
private readonly IHubContext<AgentHub, IAgentHubClient> _agentHub;
private readonly IDataService _dataService;
private readonly ISessionRecordingSink _sessionRecordingSink;
private readonly IRemoteControlSessionCache _desktopSessionCache;
private readonly ILogger<ViewerHub> _logger;
private readonly IDesktopStreamCache _streamCache;
public ViewerHub(
IRemoteControlSessionCache desktopSessionCache,
IDesktopStreamCache streamCache,
IHubContext<AgentHub, IAgentHubClient> agentHub,
IHubContext<DesktopHub, IDesktopHubClient> desktopHub,
ISessionRecordingSink sessionRecordingSink,
IDataService dataService,
ILogger<ViewerHub> logger)
{
_desktopSessionCache = desktopSessionCache;
_streamCache = streamCache;
_desktopHub = desktopHub;
_agentHub = agentHub;
_dataService = dataService;
_sessionRecordingSink = sessionRecordingSink;
_logger = logger;
}
private string RequesterDisplayName
{
get
{
if (Context.Items.TryGetValue(nameof(RequesterDisplayName), out var result) &&
result is string requesterName)
{
return requesterName;
}
return string.Empty;
}
set
{
Context.Items[nameof(RequesterDisplayName)] = value;
}
}
private RemoteControlSession SessionInfo
{
get
{
if (Context.Items.TryGetValue(nameof(SessionInfo), out var result) &&
result is RemoteControlSession session)
{
return session;
}
var newSession = new RemoteControlSession();
Context.Items[nameof(SessionInfo)] = newSession;
return newSession;
}
set
{
Context.Items[nameof(SessionInfo)] = value;
}
}
public async Task<Result> ChangeWindowsSession(int targetWindowsSession)
{
try
{
if (SessionInfo.Mode != RemoteControlMode.Unattended)
{
return Result.Fail("Only available in unattended mode.");
}
SessionInfo.ViewerList.Remove(Context.ConnectionId);
await _desktopHub.Clients
.Client(SessionInfo.DesktopConnectionId)
.ViewerDisconnected(Context.ConnectionId);
SessionInfo = SessionInfo.CreateNew();
_desktopSessionCache.AddOrUpdate($"{SessionInfo.UnattendedSessionId}", SessionInfo);
await _agentHub.Clients
.Client(SessionInfo.AgentConnectionId)
.ChangeWindowsSession(
Context.ConnectionId,
$"{SessionInfo.UnattendedSessionId}",
SessionInfo.AccessKey,
SessionInfo.UserConnectionId,
SessionInfo.RequesterUserName,
SessionInfo.OrganizationName,
SessionInfo.OrganizationId,
targetWindowsSession);
return Result.Ok();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error while changing Windows session.");
return Result.Fail("An error occurred while changing Windows session.");
}
}
public async IAsyncEnumerable<byte[]> GetDesktopStream()
{
var sessionResult = await _streamCache.WaitForStreamSession(
SessionInfo.StreamId,
Context.ConnectionId,
TimeSpan.FromSeconds(30));
if (!sessionResult.IsSuccess)
{
_logger.LogError("Timed out while waiting for desktop stream.");
await Clients.Caller.ShowMessage("Request timed out");
yield break;
}
var signaler = sessionResult.Value;
if (signaler.Stream is null)
{
_logger.LogError("Stream was null.");
yield break;
}
SessionInfo.StreamerState = StreamerState.Connected;
try
{
await foreach (var chunk in signaler.Stream)
{
yield return chunk;
}
}
finally
{
signaler.EndSignal.Release();
_logger.LogInformation("Streaming session ended for {sessionId}.", SessionInfo.StreamId);
}
}
public async Task<RemoteControlViewerOptions> GetViewerOptions()
{
var settings = await _dataService.GetSettings();
return new RemoteControlViewerOptions()
{
ShouldRecordSession = settings.EnableRemoteControlRecording
};
}
public async Task InvokeCtrlAltDel()
{
try
{
await _agentHub.Clients.Client(SessionInfo.AgentConnectionId).InvokeCtrlAltDel();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error while invoking Ctrl+Alt+Del.");
}
}
public override async Task OnDisconnectedAsync(Exception? exception)
{
if (!string.IsNullOrWhiteSpace(SessionInfo.DesktopConnectionId))
{
await _desktopHub.Clients
.Client(SessionInfo.DesktopConnectionId)
.ViewerDisconnected(Context.ConnectionId);
}
SessionInfo.ViewerList.Remove(Context.ConnectionId);
await base.OnDisconnectedAsync(exception);
}
public Task SendDtoToClient(byte[] dtoWrapper)
{
if (string.IsNullOrWhiteSpace(SessionInfo.DesktopConnectionId))
{
return Task.CompletedTask;
}
return _desktopHub.Clients
.Client(SessionInfo.DesktopConnectionId)
.SendDtoToClient(dtoWrapper, Context.ConnectionId);
}
public async Task<Result> SendScreenCastRequestToDevice(string sessionId, string accessKey, string requesterName)
{
if (string.IsNullOrWhiteSpace(sessionId))
{
return Result.Fail("Session ID cannot be empty.");
}
if (!_desktopSessionCache.TryGetValue(sessionId, out var session))
{
return Result.Fail("Session ID not found.");
}
if (session.Mode == RemoteControlMode.Unattended &&
accessKey != session.AccessKey)
{
_logger.LogError("Access key does not match for unattended session. " +
"Session ID: {sessionId}. " +
"Requester Name: {requesterName}. " +
"Requester Connection ID: {connectionId}",
sessionId,
requesterName,
Context.ConnectionId);
return Result.Fail("Authorization failed.");
}
SessionInfo = session;
SessionInfo.ViewerList.Add(Context.ConnectionId);
SessionInfo.StreamId = Guid.NewGuid();
RequesterDisplayName = requesterName;
if (Context.User?.Identity?.IsAuthenticated == true)
{
SessionInfo.RequesterUserName = Context.User.Identity.Name ?? string.Empty;
}
var logMessage = $"Remote control session requested. " +
$"Login ID (if logged in): {Context.User?.Identity?.Name}. " +
$"Machine Name: {SessionInfo.MachineName}. " +
$"Stream ID: {SessionInfo.StreamId}. " +
$"Requester Name (if specified): {RequesterDisplayName}. " +
$"Connection ID: {Context.ConnectionId}. User ID: {Context.UserIdentifier}. " +
$"Screen Caster Connection ID: {SessionInfo.DesktopConnectionId}. " +
$"Mode: {SessionInfo.Mode}. " +
$"Requester IP Address: {Context.GetHttpContext()?.Connection?.RemoteIpAddress}";
_logger.LogInformation("{msg}", logMessage);
if (SessionInfo.Mode == RemoteControlMode.Unattended)
{
if (SessionInfo.RequireConsent)
{
var request = new RemoteControlAccessRequest(
Context.ConnectionId,
RequesterDisplayName,
SessionInfo.OrganizationName);
var result = await _desktopHub.Clients
.Client(SessionInfo.DesktopConnectionId)
.PromptForAccess(request);
if (result != Shared.Enums.PromptForAccessResult.Accepted)
{
return Result.Fail($"Access request failed. Reason: {result}");
}
}
SessionInfo.RequireConsent = false;
await _desktopHub.Clients
.Client(SessionInfo.DesktopConnectionId)
.GetScreenCast(
Context.ConnectionId,
RequesterDisplayName,
SessionInfo.NotifyUserOnStart,
SessionInfo.StreamId);
}
else
{
SessionInfo.Mode = RemoteControlMode.Attended;
await _desktopHub.Clients
.Client(SessionInfo.DesktopConnectionId)
.RequestScreenCast(
Context.ConnectionId,
RequesterDisplayName,
SessionInfo.NotifyUserOnStart,
SessionInfo.StreamId);
}
return Result.Ok();
}
public async Task StoreSessionRecording(IAsyncEnumerable<byte[]> webmStream)
{
try
{
await _sessionRecordingSink.SinkWebmStream(webmStream, SessionInfo);
}
catch (OperationCanceledException)
{
_logger.LogInformation("Session recording stopped for stream {streamId}.", SessionInfo.StreamId);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error while storing session recording for stream {streamId}.", SessionInfo.StreamId);
}
}
}