mirror of
https://github.com/immense/Remotely.git
synced 2025-10-26 11:27:15 +00:00
328 lines
11 KiB
C#
328 lines
11 KiB
C#
using Remotely.Server.Enums;
|
|
using Remotely.Server.Models;
|
|
using Remotely.Server.Services;
|
|
using Remotely.Shared.Enums;
|
|
using Remotely.Shared.Interfaces;
|
|
using Microsoft.AspNetCore.SignalR;
|
|
|
|
namespace Remotely.Server.Hubs;
|
|
|
|
public class DesktopHub : Hub<IDesktopHubClient>
|
|
{
|
|
private readonly IAgentHubSessionCache _agentCache;
|
|
private readonly ILogger<DesktopHub> _logger;
|
|
private readonly IRemoteControlSessionCache _sessionCache;
|
|
private readonly IDesktopStreamCache _streamCache;
|
|
private readonly IHubContext<ViewerHub, IViewerHubClient> _viewerHub;
|
|
private readonly IHubContext<AgentHub, IAgentHubClient> _agentHub;
|
|
|
|
public DesktopHub(
|
|
IRemoteControlSessionCache sessionCache,
|
|
IDesktopStreamCache streamCache,
|
|
IAgentHubSessionCache agentCache,
|
|
IHubContext<AgentHub, IAgentHubClient> agentHub,
|
|
IHubContext<ViewerHub, IViewerHubClient> viewerHub,
|
|
ILogger<DesktopHub> logger)
|
|
{
|
|
_sessionCache = sessionCache;
|
|
_agentCache = agentCache;
|
|
_streamCache = streamCache;
|
|
_viewerHub = viewerHub;
|
|
_agentHub = agentHub;
|
|
_logger = logger;
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
private HashSet<string> ViewerList => SessionInfo.ViewerList;
|
|
|
|
public async Task DisconnectViewer(string viewerID, bool notifyViewer)
|
|
{
|
|
ViewerList.Remove(viewerID);
|
|
|
|
if (notifyViewer)
|
|
{
|
|
await _viewerHub.Clients.Client(viewerID).ViewerRemoved();
|
|
}
|
|
}
|
|
|
|
public async Task<string> GetSessionID()
|
|
{
|
|
using var scope = _logger.BeginScope(nameof(GetSessionID));
|
|
|
|
SessionInfo.Mode = RemoteControlMode.Attended;
|
|
|
|
var random = new Random();
|
|
var sessionId = string.Empty;
|
|
|
|
while (true)
|
|
{
|
|
sessionId = "";
|
|
for (var i = 0; i < 3; i++)
|
|
{
|
|
sessionId += random.Next(0, 999).ToString().PadLeft(3, '0');
|
|
}
|
|
|
|
SessionInfo.AttendedSessionId = sessionId;
|
|
if (_sessionCache.TryAdd(sessionId, SessionInfo))
|
|
{
|
|
break;
|
|
}
|
|
await Task.Yield();
|
|
}
|
|
|
|
SessionInfo.SetSessionReadyState(true);
|
|
return sessionId;
|
|
}
|
|
|
|
public Task NotifyRequesterUnattendedReady()
|
|
{
|
|
using var scope = _logger.BeginScope(nameof(NotifyRequesterUnattendedReady));
|
|
|
|
if (!_sessionCache.TryGetValue($"{SessionInfo.UnattendedSessionId}", out var session))
|
|
{
|
|
_logger.LogError("Connection not found in cache.");
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
session.SetSessionReadyState(true);
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public async Task NotifySessionChanged(SessionSwitchReasonEx reason, int currentSessionId)
|
|
{
|
|
SessionInfo.StreamerState = StreamerState.ChangingSessions | StreamerState.DisconnectExpected;
|
|
await _viewerHub.Clients.Clients(ViewerList).ShowMessage("Changing sessions");
|
|
await NotifySessionChangedImpl(reason, currentSessionId);
|
|
}
|
|
|
|
public async Task NotifySessionEnding(SessionEndReasonsEx reason)
|
|
{
|
|
switch (reason)
|
|
{
|
|
case SessionEndReasonsEx.Logoff:
|
|
SessionInfo.StreamerState = StreamerState.WindowsLoggingOff | StreamerState.DisconnectExpected;
|
|
await _viewerHub.Clients.Clients(ViewerList).ShowMessage("Windows session ending");
|
|
await NotifySessionChangedImpl(SessionSwitchReasonEx.SessionLogoff, -1);
|
|
break;
|
|
case SessionEndReasonsEx.SystemShutdown:
|
|
SessionInfo.StreamerState = StreamerState.WindowsShuttingDown | StreamerState.DisconnectExpected;
|
|
await _viewerHub.Clients.Clients(ViewerList).ShowMessage("Waiting for device to restart");
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
public Task NotifyViewersRelaunchedScreenCasterReady(string[] viewerIDs)
|
|
{
|
|
SessionInfo.DesktopConnectionId = Context.ConnectionId;
|
|
return _viewerHub.Clients
|
|
.Clients(viewerIDs)
|
|
.RelaunchedScreenCasterReady(
|
|
SessionInfo.UnattendedSessionId,
|
|
SessionInfo.AccessKey);
|
|
}
|
|
|
|
public override async Task OnConnectedAsync()
|
|
{
|
|
await base.OnConnectedAsync();
|
|
}
|
|
|
|
public override async Task OnDisconnectedAsync(Exception? exception)
|
|
{
|
|
_logger.LogDebug("Desktop app disconnected. Streamer State: {state}. Viewer Count: {count}",
|
|
SessionInfo.StreamerState,
|
|
ViewerList.Count);
|
|
|
|
SessionInfo.SetSessionReadyState(false);
|
|
|
|
if (SessionInfo.Mode == RemoteControlMode.Attended)
|
|
{
|
|
SessionInfo.StreamerState = StreamerState.Disconnected;
|
|
_ = _sessionCache.TryRemove(SessionInfo.AttendedSessionId, out _);
|
|
await _viewerHub.Clients.Clients(ViewerList).ScreenCasterDisconnected();
|
|
}
|
|
else if (
|
|
SessionInfo.Mode == RemoteControlMode.Unattended &&
|
|
!SessionInfo.StreamerState.HasFlag(StreamerState.DisconnectExpected))
|
|
{
|
|
// Don't restart if consent wasn't granted on the first request.
|
|
if (ViewerList.Count > 0 && SessionInfo.RequireConsent)
|
|
{
|
|
await RestartScreenCaster();
|
|
}
|
|
else
|
|
{
|
|
_ = _sessionCache.TryRemove($"{SessionInfo.UnattendedSessionId}", out _);
|
|
}
|
|
}
|
|
|
|
await base.OnDisconnectedAsync(exception);
|
|
}
|
|
|
|
public async Task<Result<string>> PingViewer(string viewerConnectionId)
|
|
{
|
|
try
|
|
{
|
|
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
|
var response = await _viewerHub.Clients.Client(viewerConnectionId).PingViewer(cts.Token);
|
|
return Result.Ok(response);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "Failed to ping viewer with connection ID {connectionId}.", viewerConnectionId);
|
|
return Result.Fail<string>("Failed to ping viewer.");
|
|
}
|
|
}
|
|
|
|
public Task ReceiveAttendedSessionInfo(string machineName)
|
|
{
|
|
SessionInfo.DesktopConnectionId = Context.ConnectionId;
|
|
SessionInfo.StartTime = DateTimeOffset.Now;
|
|
SessionInfo.MachineName = machineName;
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public Task<Result> ReceiveUnattendedSessionInfo(Guid unattendedSessionId, string accessKey, string machineName, string requesterName, string organizationName)
|
|
{
|
|
if (_sessionCache.TryGetValue($"{unattendedSessionId}", out var existingSession) &&
|
|
!string.IsNullOrWhiteSpace(existingSession.AccessKey) &&
|
|
accessKey != existingSession.AccessKey)
|
|
{
|
|
_logger.LogWarning(
|
|
"A desktop session tried to take over an existing session, " +
|
|
"but the access key didn't match.");
|
|
var result = Result.Fail("SessionId already exists on the server.");
|
|
return Task.FromResult(result);
|
|
}
|
|
|
|
SessionInfo = _sessionCache.GetOrAdd($"{unattendedSessionId}", (key) => SessionInfo);
|
|
|
|
SessionInfo.Mode = RemoteControlMode.Unattended;
|
|
SessionInfo.DesktopConnectionId = Context.ConnectionId;
|
|
SessionInfo.StartTime = DateTimeOffset.Now;
|
|
SessionInfo.UnattendedSessionId = unattendedSessionId;
|
|
SessionInfo.AccessKey = accessKey;
|
|
SessionInfo.MachineName = machineName;
|
|
SessionInfo.RequesterName = requesterName;
|
|
SessionInfo.OrganizationName = organizationName;
|
|
|
|
return Task.FromResult(Result.Ok());
|
|
}
|
|
|
|
public Task SendConnectionFailedToViewers(List<string> viewerIDs)
|
|
{
|
|
return _viewerHub.Clients.Clients(viewerIDs).ConnectionFailed();
|
|
}
|
|
|
|
public Task SendConnectionRequestDenied(string viewerID)
|
|
{
|
|
return _viewerHub.Clients.Client(viewerID).ConnectionRequestDenied();
|
|
}
|
|
|
|
public async Task SendDesktopStream(IAsyncEnumerable<byte[]> stream, Guid streamId)
|
|
{
|
|
using var signaler = _streamCache.GetOrAdd(streamId, key => new StreamSignaler(streamId));
|
|
signaler.DesktopConnectionId = Context.ConnectionId;
|
|
|
|
try
|
|
{
|
|
signaler.Stream = stream;
|
|
signaler.ReadySignal.Release();
|
|
|
|
// TODO: We can remove the timeout once we implement add a
|
|
// timeout for viewer idle (i.e. no input).
|
|
await signaler.EndSignal.WaitAsync(TimeSpan.FromHours(8));
|
|
}
|
|
finally
|
|
{
|
|
_ = _streamCache.TryRemove(signaler.StreamId, out _);
|
|
}
|
|
}
|
|
|
|
public Task SendDtoToViewer(byte[] dto, string viewerId)
|
|
{
|
|
return _viewerHub.Clients.Client(viewerId).SendDtoToViewer(dto);
|
|
}
|
|
|
|
public Task SendMessageToViewer(string viewerId, string message)
|
|
{
|
|
return _viewerHub.Clients.Client(viewerId).ShowMessage(message);
|
|
}
|
|
|
|
private async Task RestartScreenCaster()
|
|
{
|
|
SessionInfo.StreamerState = StreamerState.Reconnecting;
|
|
await _viewerHub.Clients.Clients(ViewerList).Reconnecting();
|
|
|
|
if (!_agentCache.TryGetConnectionId(SessionInfo.DeviceId, out var agentConnectionId))
|
|
{
|
|
await _viewerHub.Clients
|
|
.Clients(SessionInfo.ViewerList)
|
|
.ShowMessage("Waiting for agent to come online");
|
|
|
|
return;
|
|
}
|
|
|
|
SessionInfo.AgentConnectionId = agentConnectionId;
|
|
|
|
await _agentHub.Clients
|
|
.Client(SessionInfo.AgentConnectionId)
|
|
.RestartScreenCaster(
|
|
[.. SessionInfo.ViewerList],
|
|
$"{SessionInfo.UnattendedSessionId}",
|
|
SessionInfo.AccessKey,
|
|
SessionInfo.UserConnectionId,
|
|
SessionInfo.RequesterName,
|
|
SessionInfo.OrganizationName,
|
|
SessionInfo.OrganizationId);
|
|
}
|
|
private async Task NotifySessionChangedImpl(SessionSwitchReasonEx reason, int currentSessionId)
|
|
{
|
|
if (SessionInfo.RequireConsent)
|
|
{
|
|
// Don't restart if consent wasn't granted on the first request.
|
|
return;
|
|
}
|
|
|
|
_logger.LogDebug("Windows session changed during remote control. " +
|
|
"Reason: {reason}. " +
|
|
"Current Session ID: {sessionId}. " +
|
|
"Session Info: {@sessionInfo}",
|
|
reason,
|
|
currentSessionId,
|
|
SessionInfo);
|
|
|
|
await _agentHub.Clients
|
|
.Client(SessionInfo.AgentConnectionId)
|
|
.RestartScreenCaster(
|
|
[.. SessionInfo.ViewerList],
|
|
$"{SessionInfo.UnattendedSessionId}",
|
|
SessionInfo.AccessKey,
|
|
SessionInfo.UserConnectionId,
|
|
SessionInfo.RequesterUserName,
|
|
SessionInfo.OrganizationName,
|
|
SessionInfo.OrganizationId);
|
|
}
|
|
}
|