Merge in abstractions in Server project. Remove unneeded RemoteControlSessionLimit.

This commit is contained in:
Jared Goodwin 2024-07-16 10:39:16 -07:00
parent 81641afe06
commit 9e055af473
33 changed files with 286 additions and 880 deletions

View File

@ -121,7 +121,6 @@ All other configuration is done in the Server Config page once you're logged in.
- Set this to -1 or increase it to a specific number to allow multi-tenancy.
- RedirectToHttps: Whether ASP.NET Core will redirect all traffic from HTTP to HTTPS. This is independent of Caddy, Nginx, and IIS configurations that do the same.
- RemoteControlNotifyUsers: Whether to show a notification to the end user when an unattended remote control session starts.
- RemoteControlSessionLimit: How many concurrent remote control sessions are allowed per organization.
- RemoteControlRequiresAuthentication: Whether the remote control page requires authentication to establish a connection.
- Require2FA: Require users to set up 2FA before they can use the main app.
- Smpt-: SMTP settings for auto-generated system emails (such as registration and password reset).

View File

@ -5,7 +5,6 @@ using Remotely.Server.Hubs;
using Remotely.Server.Models;
using Remotely.Server.Services;
using Remotely.Server.Auth;
using Remotely.Server.Abstractions;
using Remotely.Shared.Helpers;
using Remotely.Server.Extensions;
using Remotely.Shared.Entities;
@ -24,7 +23,6 @@ public class RemoteControlController : ControllerBase
private readonly IAgentHubSessionCache _serviceSessionCache;
private readonly IDataService _dataService;
private readonly IOtpProvider _otpProvider;
private readonly IHubEventHandler _hubEvents;
private readonly SignInManager<RemotelyUser> _signInManager;
private readonly ILogger<RemoteControlController> _logger;
@ -35,7 +33,6 @@ public class RemoteControlController : ControllerBase
IHubContext<AgentHub, IAgentHubClient> agentHub,
IAgentHubSessionCache serviceSessionCache,
IOtpProvider otpProvider,
IHubEventHandler hubEvents,
ILogger<RemoteControlController> logger)
{
_dataService = dataService;
@ -43,7 +40,6 @@ public class RemoteControlController : ControllerBase
_remoteControlSessionCache = remoteControlSessionCache;
_serviceSessionCache = serviceSessionCache;
_otpProvider = otpProvider;
_hubEvents = hubEvents;
_signInManager = signInManager;
_logger = logger;
}
@ -134,20 +130,12 @@ public class RemoteControlController : ControllerBase
}
}
var sessionCount = _remoteControlSessionCache.Sessions
.OfType<RemoteControlSessionEx>()
.Count(x => x.OrganizationId == orgId);
var settings = await _dataService.GetSettings();
if (sessionCount > settings.RemoteControlSessionLimit)
{
return BadRequest("There are already the maximum amount of active remote control sessions for your organization.");
}
var sessionCount = _remoteControlSessionCache.Sessions.Count(x => x.OrganizationId == orgId);
var sessionId = Guid.NewGuid();
var accessKey = RandomGenerator.GenerateAccessKey();
var session = new RemoteControlSessionEx()
var session = new RemoteControlSession()
{
UnattendedSessionId = sessionId,
UserConnectionId = HttpContext.Connection.Id,
@ -158,13 +146,8 @@ public class RemoteControlController : ControllerBase
_remoteControlSessionCache.AddOrUpdate($"{sessionId}", session, (k, v) =>
{
if (v is RemoteControlSessionEx ex)
{
ex.AgentConnectionId = HttpContext.Connection.Id;
return ex;
}
v.Dispose();
return session;
v.AgentConnectionId = HttpContext.Connection.Id;
return v;
});
var orgNameResult = await _dataService.GetOrganizationNameById(orgId);

View File

@ -1,94 +0,0 @@
using Remotely.Server.Extensions;
using Remotely.Server.Models;
using Remotely.Server.Services;
using Remotely.Shared.Enums;
namespace Remotely.Server.Abstractions;
/// <summary>
/// Contains functionality that needs to be implemented outside of the remote control process.
/// This service will be registered as a singleton within <see cref="RemoteControlServerBuilder"/>.
/// </summary>
public interface IHubEventHandler
{
/// <summary>
/// This is called when a viewer has selected a different Windows session. A new remote control
/// process should be started in that session. The viewer's connection ID should be passed into the
/// new process using the --viewers argument, and they'll be automatically signaled when the new
/// session is ready.
/// </summary>
/// <param name="session"></param>
/// <param name="viewerConnectionId"></param>
/// <param name="targetWindowsSession"></param>
/// <returns></returns>
Task ChangeWindowsSession(RemoteControlSession session, string viewerConnectionId, int targetWindowsSession);
/// <summary>
/// <para>
/// This is called when the viewer invokes "Ctrl+Alt+Del". On the desktop end, it calls the
/// native function SendSAS (https://docs.microsoft.com/en-us/windows/win32/api/sas/nf-sas-sendsas).
/// </para>
/// <para>
/// This can (usually?) only be called from a Windows service, so this event can be routed to one.
/// The desktop process will also try to call it. I don't believe there are any side effects from
/// calling it multiple times, even if they both succeed.
/// </para>
/// </summary>
/// <param name="session"></param>
/// <param name="viewerConnectionId"></param>
/// <returns></returns>
Task InvokeCtrlAltDel(RemoteControlSession session, string viewerConnectionId);
/// <summary>
/// This is called when a new session is added to the <see cref="IRemoteControlSessionCache"/>.
/// </summary>
/// <param name="sessionInfo"></param>
/// <returns></returns>
Task NotifyDesktopSessionAdded(RemoteControlSession sessionInfo);
/// <summary>
/// This is called when all viewers have left a remote control session
/// and the session is removed from the <see cref="IRemoteControlSessionCache"/>.
/// </summary>
/// <param name="sessionInfo"></param>
/// <returns></returns>
Task NotifyDesktopSessionRemoved(RemoteControlSession sessionInfo);
/// <summary>
/// This is called when a remote control session ends. This event may occur
/// multiple times per session if additional viewers are invited to the same session.
/// </summary>
/// <param name="sessionInfo"></param>
/// <returns></returns>
Task NotifyRemoteControlEnded(RemoteControlSession sessionInfo);
/// <summary>
/// This is called when a remote control session starts. This event may occur
/// multiple times per session if additional viewers are invited to the same session.
/// </summary>
/// <param name="sessionInfo"></param>
/// <returns></returns>
Task NotifyRemoteControlStarted(RemoteControlSession sessionInfo);
/// <summary>
/// This is called when the Windows session has changed for an active remote control session.
/// </summary>
/// <param name="sessionInfo"></param>
/// <param name="reason">The type of change that's occurring.</param>
/// <param name="currentSessionId">The current session ID of the remote control process.</param>
/// <returns></returns>
Task NotifySessionChanged(RemoteControlSession session, SessionSwitchReasonEx reason, int currentSessionId);
/// <summary>
/// This is called when the remote control session ends unexpectedly from the desktop
/// side, and the viewer is expecting it to restart automatically.
/// </summary>
/// <param name="sessionInfo"></param>
/// <param name="viewerList">
/// This is the list of viewer SignalR connection IDs. These should be comma-delimited
/// and passed into the new remote control process with the --viewer param, and they will
/// be signaled to automatically reconnect when the new session is ready.
/// </param>
/// <returns></returns>
Task RestartScreenCaster(RemoteControlSession session);
}

View File

@ -1,26 +0,0 @@
using Remotely.Server.Models;
using Microsoft.AspNetCore.SignalR;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Remotely.Server.Abstractions;
/// <summary>
/// The service is responsible for storing session recordings.
/// </summary>
public interface ISessionRecordingSink
{
/// <summary>
/// Sink a live webm stream to persistent storage.
/// </summary>
/// <param name="webmStream"></param>
/// <param name="hubCallerContext"></param>
/// <param name="session"></param>
/// <returns></returns>
Task SinkWebmStream(
IAsyncEnumerable<byte[]> webmStream,
RemoteControlSession session);
}

View File

@ -1,26 +0,0 @@
using Microsoft.AspNetCore.Mvc.Filters;
using Remotely.Server.Extensions;
namespace Remotely.Server.Abstractions;
/// <summary>
/// This service is used to determine if the current user is authorized
/// to view the remote control page. It gets registered as a scoped service
/// within <see cref="RemoteControlServerBuilder"/>.
/// </summary>
public interface IViewerAuthorizer
{
/// <summary>
/// Where the browser should be redirected if IsAuthorized returns false.
/// Example: "/Account/Login"
/// </summary>
string UnauthorizedRedirectUrl { get; }
/// <summary>
/// Whether the current user is authorized to view the remote control page.
/// Note: This does not inherently give access to any devices or resources.
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
Task<bool> IsAuthorized(AuthorizationFilterContext context);
}

View File

@ -1,18 +0,0 @@
using Remotely.Server.Models;
using Remotely.Shared.Models;
using Microsoft.AspNetCore.SignalR;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Remotely.Server.Abstractions;
/// <summary>
/// Provides options related to how the viewer front-end should behave.
/// </summary>
public interface IViewerOptionsProvider
{
Task<RemoteControlViewerOptions> GetViewerOptions();
}

View File

@ -1,20 +0,0 @@
using Remotely.Server.Areas.RemoteControl.Pages;
using Remotely.Server.Models;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Remotely.Server.Extensions;
namespace Remotely.Server.Abstractions;
/// <summary>
/// This service is used to provider UI data to the remote control page.
/// It gets registered as a scoped service within <see cref="RemoteControlServerBuilder"/>.
/// </summary>
public interface IViewerPageDataProvider
{
Task<ViewerPageTheme> GetTheme(PageModel pageModel);
Task<string> GetUserDisplayName(PageModel pageModel);
Task<string> GetPageTitle(PageModel pageModel);
Task<string> GetPageDescription(PageModel pageModel);
Task<string> GetFaviconUrl(PageModel pageModel);
Task<string> GetLogoUrl(PageModel pageModel);
}

View File

@ -1,9 +1,6 @@
using Remotely.Server.Abstractions;
using Bitbound.SimpleMessenger;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.Extensions.Logging;
using Remotely.Server.Components.ModalContents;
using Remotely.Server.Hubs;
using Remotely.Server.Models.Messages;
@ -13,10 +10,6 @@ using Remotely.Shared.Entities;
using Remotely.Shared.Enums;
using Remotely.Shared.Models;
using Remotely.Shared.Utilities;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Remotely.Server.Components.Devices;
@ -122,7 +115,7 @@ public partial class Terminal : AuthComponentBase, IDisposable
var match = completion.CompletionMatches[completion.CurrentMatchIndex];
var replacementText = string.Concat(
_lastCompletionInput.Substring(0, completion.ReplacementIndex),
_lastCompletionInput[..completion.ReplacementIndex],
match.CompletionText,
_lastCompletionInput[(completion.ReplacementIndex + completion.ReplacementLength)..]);
@ -170,7 +163,7 @@ public partial class Terminal : AuthComponentBase, IDisposable
}
var devices = CardStore.SelectedDevices.ToArray();
if (!devices.Any())
if (devices.Length == 0)
{
ToastService.ShowToast("You must select at least one device.", classString: "bg-warning");
return;
@ -281,7 +274,7 @@ public partial class Terminal : AuthComponentBase, IDisposable
EnsureUserSet();
var quickScripts = await DataService.GetQuickScripts(User.Id);
if (quickScripts?.Any() != true)
if (quickScripts.Count == 0)
{
ToastService.ShowToast("No quick scripts saved.", classString: "bg-warning");
return;
@ -307,8 +300,8 @@ public partial class Terminal : AuthComponentBase, IDisposable
private void ShowTerminalHelp()
{
ModalService.ShowModal("Terminal Help", new[]
{
ModalService.ShowModal("Terminal Help",
[
"Enter terminal commands that will execute on all selected devices.",
"Tab completion is available for PowerShell Core (PSCore) and Windows PowerShell (WinPS). Tab and Shift + Tab " +
@ -324,7 +317,7 @@ public partial class Terminal : AuthComponentBase, IDisposable
"Note: The first PS Core command or tab completion takes a few moments while the service is " +
"starting on the remote device."
});
]);
}
private async void TerminalStore_TerminalLinesChanged(object? sender, EventArgs e)

View File

@ -37,9 +37,6 @@
</div>
@code {
private string? _showClass;
private string? _displayStyle;
private IJSObjectReference? _module;
private ElementReference _modalRef;

View File

@ -215,13 +215,6 @@
<br />
<ValidationMessage For="() => Input.RemoteControlRequiresAuthentication" />
</div>
<div class="form-group">
<label class="control-label">Remote Control Session Limit</label>
<br />
<InputNumber @bind-Value="Input.RemoteControlSessionLimit" class="form-control" autocomplete="off" />
<br />
<ValidationMessage For="() => Input.RemoteControlSessionLimit" />
</div>
<div class="form-group">
<label>Require 2FA</label>
<br />

View File

@ -1,20 +1,11 @@
using Remotely.Server.Abstractions;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.SignalR;
using Remotely.Server.Components.Pages;
using Remotely.Server.Hubs;
using Remotely.Server.Migrations.PostgreSql;
using Remotely.Server.Migrations.Sqlite;
using Remotely.Server.Migrations.SqlServer;
using Remotely.Server.Services;
using Remotely.Shared.Entities;
using Remotely.Shared.Enums;
using Remotely.Shared.Utilities;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Remotely.Server.Components.Scripts;

View File

@ -1,107 +0,0 @@
using Remotely.Server.Abstractions;
using Microsoft.Extensions.DependencyInjection;
namespace Remotely.Server.Extensions;
public interface IRemoteControlServerBuilder
{
/// <summary>
/// Adds your implementation of <see cref="IHubEventHandler"/> to the
/// DI container as a singleton.
/// </summary>
/// <typeparam name="T"></typeparam>
void AddHubEventHandler<T>()
where T : class, IHubEventHandler;
/// <summary>
/// Adds your implementation of <see cref="ISessionRecordingSink"/> to the
/// DI container as a singleton.
/// </summary>
/// <typeparam name="T"></typeparam>
void AddSessionRecordingSink<T>()
where T : class, ISessionRecordingSink;
/// <summary>
/// Adds your implementation of <see cref="IViewerAuthorizer"/> to the
/// DI container as a scoped service.
/// </summary>
/// <typeparam name="T"></typeparam>
void AddViewerAuthorizer<T>()
where T : class, IViewerAuthorizer;
/// <summary>
/// Adds your implementation of <see cref="IViewerOptionsProvider"/> to the
/// DI container as a singleton.
/// </summary>
/// <typeparam name="T"></typeparam>
void AddViewerOptionsProvider<T>()
where T : class, IViewerOptionsProvider;
/// <summary>
/// Adds your implementation of <see cref="IViewerPageDataProvider"/> to the
/// DI container as a scoped service.
/// </summary>
/// <typeparam name="T"></typeparam>
void AddViewerPageDataProvider<T>()
where T : class, IViewerPageDataProvider;
}
internal class RemoteControlServerBuilder : IRemoteControlServerBuilder
{
private readonly IServiceCollection _services;
public RemoteControlServerBuilder(IServiceCollection services)
{
_services = services;
}
public void AddHubEventHandler<T>()
where T : class, IHubEventHandler
{
_services.AddSingleton<IHubEventHandler, T>();
}
public void AddSessionRecordingSink<T>()
where T : class, ISessionRecordingSink
{
_services.AddSingleton<ISessionRecordingSink, T>();
}
public void AddViewerAuthorizer<T>()
where T : class, IViewerAuthorizer
{
_services.AddScoped<IViewerAuthorizer, T>();
}
public void AddViewerOptionsProvider<T>()
where T : class, IViewerOptionsProvider
{
_services.AddSingleton<IViewerOptionsProvider, T>();
}
public void AddViewerPageDataProvider<T>()
where T : class, IViewerPageDataProvider
{
_services.AddScoped<IViewerPageDataProvider, T>();
}
internal void Validate()
{
var serviceTypes = new[]
{
typeof(IHubEventHandler),
typeof(IViewerAuthorizer),
typeof(IViewerPageDataProvider),
typeof(ISessionRecordingSink),
typeof(IViewerOptionsProvider)
};
foreach (var type in serviceTypes)
{
if (!_services.Any(x => x.ServiceType == type))
{
throw new Exception($"Missing service registration for type {type.Name}.");
}
}
}
}

View File

@ -1,39 +0,0 @@
using Remotely.Server.Filters;
using Remotely.Server.Services;
using Remotely.Shared.Services;
using Microsoft.Extensions.DependencyInjection;
namespace Remotely.Server.Extensions;
public static class ServiceCollectionExtensions
{
/// <summary>
/// Adds remote control services to an ASP.NET Core web app. Remember to call
/// <see cref="IApplicationBuilderExtensions.UseRemoteControlServer(Microsoft.AspNetCore.Builder.WebApplication)"/>
/// after the WebApplication has been built.
/// </summary>
/// <param name="services"></param>
/// <param name="configure">Provides methods for adding required service implementations.</param>
/// <returns></returns>
public static IServiceCollection AddRemoteControlServer(
this IServiceCollection services,
Action<IRemoteControlServerBuilder> configure)
{
var builder = new RemoteControlServerBuilder(services);
configure(builder);
builder.Validate();
services
.AddSignalR()
.AddMessagePackProtocol();
services.AddHostedService<RemoteControlSessionCleaner>();
services.AddHostedService<RemoteControlSessionReconnector>();
services.AddSingleton<IDesktopStreamCache, DesktopStreamCache>();
services.AddSingleton<IRemoteControlSessionCache, RemoteControlSessionCache>();
services.AddSingleton<ISystemTime, SystemTime>();
services.AddScoped<ViewerAuthorizationFilter>();
return services;
}
}

View File

@ -1,25 +1,42 @@
using Remotely.Server.Abstractions;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Remotely.Server.Services;
namespace Remotely.Server.Filters;
internal class ViewerAuthorizationFilter : IAsyncAuthorizationFilter
internal class ViewerAuthorizationFilter(
IDataService _dataService,
IOtpProvider _otpProvider): IAsyncAuthorizationFilter
{
private readonly IViewerAuthorizer _authorizer;
public ViewerAuthorizationFilter(IViewerAuthorizer authorizer)
{
_authorizer = authorizer;
}
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
if (await _authorizer.IsAuthorized(context))
if (await IsAuthorized(context))
{
return;
}
context.Result = new RedirectResult(_authorizer.UnauthorizedRedirectUrl);
context.Result = new RedirectResult("/Account/Login");
}
private async Task<bool> IsAuthorized(AuthorizationFilterContext context)
{
var settings = await _dataService.GetSettings();
if (!settings.RemoteControlRequiresAuthentication)
{
return true;
}
if (context.HttpContext.User.Identity?.IsAuthenticated == true)
{
return true;
}
if (context.HttpContext.Request.Query.TryGetValue("otp", out var otp) &&
_otpProvider.Exists($"{otp}"))
{
return true;
}
return false;
}
}

View File

@ -104,7 +104,6 @@ public class AgentHub : Hub<IAgentHubClient>
var waitingSessions = _remoteControlSessions
.Sessions
.OfType<RemoteControlSessionEx>()
.Where(x => x.DeviceId == Device.ID);
foreach (var session in waitingSessions)

View File

@ -31,7 +31,7 @@ public interface ICircuitConnection
Task ReinstallAgents(string[] deviceIDs);
Task<Result<RemoteControlSessionEx>> RemoteControl(string deviceID, bool viewOnly);
Task<Result<RemoteControlSession>> RemoteControl(string deviceID, bool viewOnly);
Task RemoveDevices(string[] deviceIDs);
@ -215,7 +215,7 @@ public class CircuitConnection : CircuitHandler, ICircuitConnection
_dataService.RemoveDevices(deviceIDs);
}
public async Task<Result<RemoteControlSessionEx>> RemoteControl(string deviceId, bool viewOnly)
public async Task<Result<RemoteControlSession>> RemoteControl(string deviceId, bool viewOnly)
{
var settings = await _dataService.GetSettings();
@ -228,7 +228,7 @@ public class CircuitConnection : CircuitHandler, ICircuitConnection
await _messenger.Send(message, ConnectionId);
return Result.Fail<RemoteControlSessionEx>("Device is not online.");
return Result.Fail<RemoteControlSession>("Device is not online.");
}
@ -239,26 +239,10 @@ public class CircuitConnection : CircuitHandler, ICircuitConnection
"Remote control attempted by unauthorized user. Device ID: {deviceId}. User Name: {userName}.",
deviceId,
User.UserName);
return Result.Fail<RemoteControlSessionEx>("Unauthorized.");
return Result.Fail<RemoteControlSession>("Unauthorized.");
}
var sessionCount = _remoteControlSessionCache.Sessions
.OfType<RemoteControlSessionEx>()
.Count(x => x.OrganizationId == User.OrganizationID);
if (sessionCount >= settings.RemoteControlSessionLimit)
{
var message = new DisplayNotificationMessage(
"There are already the maximum amount of active remote control sessions for your organization.",
"Max number of concurrent sessions reached.",
"bg-warning");
await _messenger.Send(message, ConnectionId);
return Result.Fail<RemoteControlSessionEx>("Max number of concurrent sessions reached.");
}
if (!_agentSessionCache.TryGetConnectionId(targetDevice.ID, out var serviceConnectionId))
{
var message = new DisplayNotificationMessage(
@ -267,13 +251,13 @@ public class CircuitConnection : CircuitHandler, ICircuitConnection
"bg-warning");
await _messenger.Send(message, ConnectionId);
return Result.Fail<RemoteControlSessionEx>("Service connection not found.");
return Result.Fail<RemoteControlSession>("Service connection not found.");
}
var sessionId = Guid.NewGuid();
var accessKey = RandomGenerator.GenerateAccessKey();
var session = new RemoteControlSessionEx()
var session = new RemoteControlSession()
{
UnattendedSessionId = sessionId,
UserConnectionId = ConnectionId,
@ -292,7 +276,7 @@ public class CircuitConnection : CircuitHandler, ICircuitConnection
if (!orgResult.IsSuccess)
{
_toastService.ShowToast2(orgResult.Reason, Enums.ToastType.Warning);
return Result.Fail<RemoteControlSessionEx>(orgResult.Reason);
return Result.Fail<RemoteControlSession>(orgResult.Reason);
}
await _agentHubContext.Clients.Client(serviceConnectionId).RemoteControl(

View File

@ -1,4 +1,3 @@
using Remotely.Server.Abstractions;
using Remotely.Server.Enums;
using Remotely.Server.Models;
using Remotely.Server.Services;
@ -10,23 +9,26 @@ namespace Remotely.Server.Hubs;
public class DesktopHub : Hub<IDesktopHubClient>
{
private readonly IHubEventHandler _hubEvents;
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,
IHubContext<ViewerHub, IViewerHubClient> viewerHubContext,
IHubEventHandler hubEvents,
IAgentHubSessionCache agentCache,
IHubContext<AgentHub, IAgentHubClient> agentHub,
IHubContext<ViewerHub, IViewerHubClient> viewerHub,
ILogger<DesktopHub> logger)
{
_sessionCache = sessionCache;
_agentCache = agentCache;
_streamCache = streamCache;
_viewerHub = viewerHubContext;
_hubEvents = hubEvents;
_viewerHub = viewerHub;
_agentHub = agentHub;
_logger = logger;
}
@ -109,7 +111,7 @@ public class DesktopHub : Hub<IDesktopHubClient>
{
SessionInfo.StreamerState = StreamerState.ChangingSessions | StreamerState.DisconnectExpected;
await _viewerHub.Clients.Clients(ViewerList).ShowMessage("Changing sessions");
await _hubEvents.NotifySessionChanged(SessionInfo, reason, currentSessionId);
await NotifySessionChangedImpl(reason, currentSessionId);
}
public async Task NotifySessionEnding(SessionEndReasonsEx reason)
@ -119,7 +121,7 @@ public class DesktopHub : Hub<IDesktopHubClient>
case SessionEndReasonsEx.Logoff:
SessionInfo.StreamerState = StreamerState.WindowsLoggingOff | StreamerState.DisconnectExpected;
await _viewerHub.Clients.Clients(ViewerList).ShowMessage("Windows session ending");
await _hubEvents.NotifySessionChanged(SessionInfo, SessionSwitchReasonEx.SessionLogoff, -1);
await NotifySessionChangedImpl(SessionSwitchReasonEx.SessionLogoff, -1);
break;
case SessionEndReasonsEx.SystemShutdown:
SessionInfo.StreamerState = StreamerState.WindowsShuttingDown | StreamerState.DisconnectExpected;
@ -163,11 +165,10 @@ public class DesktopHub : Hub<IDesktopHubClient>
SessionInfo.Mode == RemoteControlMode.Unattended &&
!SessionInfo.StreamerState.HasFlag(StreamerState.DisconnectExpected))
{
if (ViewerList.Count > 0)
// Don't restart if consent wasn't granted on the first request.
if (ViewerList.Count > 0 && SessionInfo.RequireConsent)
{
SessionInfo.StreamerState = StreamerState.Reconnecting;
await _viewerHub.Clients.Clients(ViewerList).Reconnecting();
await _hubEvents.RestartScreenCaster(SessionInfo);
await RestartScreenCaster();
}
else
{
@ -228,6 +229,7 @@ public class DesktopHub : Hub<IDesktopHubClient>
return Task.FromResult(Result.Ok());
}
public Task SendConnectionFailedToViewers(List<string> viewerIDs)
{
return _viewerHub.Clients.Clients(viewerIDs).ConnectionFailed();
@ -248,11 +250,9 @@ public class DesktopHub : Hub<IDesktopHubClient>
signaler.Stream = stream;
signaler.ReadySignal.Release();
await _hubEvents.NotifyRemoteControlStarted(SessionInfo);
// 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));
await _hubEvents.NotifyRemoteControlEnded(SessionInfo);
}
finally
{
@ -269,4 +269,59 @@ public class DesktopHub : Hub<IDesktopHubClient>
{
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);
}
}

View File

@ -1,14 +1,11 @@
using Remotely.Server.Abstractions;
using Remotely.Server.Enums;
using Remotely.Server.Filters;
using Remotely.Server.Models;
using Remotely.Server.Services;
using Remotely.Shared;
using Remotely.Shared.Interfaces;
using Remotely.Shared.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Logging;
namespace Remotely.Server.Hubs;
@ -16,27 +13,27 @@ namespace Remotely.Server.Hubs;
public class ViewerHub : Hub<IViewerHubClient>
{
private readonly IHubContext<DesktopHub, IDesktopHubClient> _desktopHub;
private readonly IViewerOptionsProvider _viewerOptionsProvider;
private readonly IHubContext<AgentHub, IAgentHubClient> _agentHub;
private readonly IDataService _dataService;
private readonly ISessionRecordingSink _sessionRecordingSink;
private readonly IRemoteControlSessionCache _desktopSessionCache;
private readonly IHubEventHandler _hubEvents;
private readonly ILogger<ViewerHub> _logger;
private readonly IDesktopStreamCache _streamCache;
public ViewerHub(
IHubEventHandler hubEvents,
IRemoteControlSessionCache desktopSessionCache,
IDesktopStreamCache streamCache,
IHubContext<AgentHub, IAgentHubClient> agentHub,
IHubContext<DesktopHub, IDesktopHubClient> desktopHub,
IViewerOptionsProvider viewerOptionsProvider,
ISessionRecordingSink sessionRecordingSink,
IDataService dataService,
ILogger<ViewerHub> logger)
{
_hubEvents = hubEvents;
_desktopSessionCache = desktopSessionCache;
_streamCache = streamCache;
_desktopHub = desktopHub;
_viewerOptionsProvider = viewerOptionsProvider;
_agentHub = agentHub;
_dataService = dataService;
_sessionRecordingSink = sessionRecordingSink;
_logger = logger;
}
@ -79,21 +76,40 @@ public class ViewerHub : Hub<IViewerHubClient>
}
public async Task<Result> ChangeWindowsSession(int targetWindowsSession)
{
if (SessionInfo.Mode != RemoteControlMode.Unattended)
try
{
return Result.Fail("Only available in unattended mode.");
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.");
}
SessionInfo.ViewerList.Remove(Context.ConnectionId);
await _desktopHub.Clients
.Client(SessionInfo.DesktopConnectionId)
.ViewerDisconnected(Context.ConnectionId);
SessionInfo = SessionInfo.CreateNew();
_desktopSessionCache.AddOrUpdate($"{SessionInfo.UnattendedSessionId}", SessionInfo);
await _hubEvents.ChangeWindowsSession(SessionInfo, Context.ConnectionId, targetWindowsSession);
return Result.Ok();
}
public async IAsyncEnumerable<byte[]> GetDesktopStream()
@ -136,12 +152,23 @@ public class ViewerHub : Hub<IViewerHubClient>
public async Task<RemoteControlViewerOptions> GetViewerOptions()
{
return await _viewerOptionsProvider.GetViewerOptions();
var settings = await _dataService.GetSettings();
return new RemoteControlViewerOptions()
{
ShouldRecordSession = settings.EnableRemoteControlRecording
};
}
public Task InvokeCtrlAltDel()
public async Task InvokeCtrlAltDel()
{
return _hubEvents.InvokeCtrlAltDel(SessionInfo, Context.ConnectionId);
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)
@ -274,4 +301,5 @@ public class ViewerHub : Hub<IViewerHubClient>
_logger.LogError(ex, "Error while storing session recording for stream {streamId}.", SessionInfo.StreamId);
}
}
}

View File

@ -20,12 +20,12 @@ public class RemoteControlSession : IDisposable
{
Created = DateTimeOffset.Now;
}
public string AccessKey { get; internal set; } = string.Empty;
public string AgentConnectionId { get; set; } = string.Empty;
public string AttendedSessionId { get; set; } = string.Empty;
public DateTimeOffset Created { get; internal set; }
public string DesktopConnectionId { get; internal set; } = string.Empty;
public string DeviceId { get; set; } = string.Empty;
public DateTimeOffset LastStateChange { get; private set; } = DateTimeOffset.Now;
public string MachineName { get; internal set; } = string.Empty;
public RemoteControlMode Mode { get; internal set; }
@ -35,6 +35,7 @@ public class RemoteControlSession : IDisposable
/// </summary>
public bool NotifyUserOnStart { get; set; } = true;
public string OrganizationId { get; set; } = string.Empty;
public string OrganizationName { get; internal set; } = string.Empty;
public string RelativeAccessUri => $"/RemoteControl/Viewer?mode=Unattended&sessionId={UnattendedSessionId}&accessKey={AccessKey}&viewonly=False";
public string RequesterName { get; set; } = string.Empty;

View File

@ -1,10 +0,0 @@
using Remotely.Server.Models;
using System;
namespace Remotely.Server.Models;
public class RemoteControlSessionEx : RemoteControlSession
{
public string DeviceId { get; set; } = string.Empty;
public string OrganizationId { get; set; } = string.Empty;
}

View File

@ -21,7 +21,6 @@ public class SettingsModel
public bool RedirectToHttps { get; set; } = true;
public bool RemoteControlNotifyUser { get; set; } = true;
public bool RemoteControlRequiresAuthentication { get; set; } = true;
public int RemoteControlSessionLimit { get; set; } = 5;
public bool Require2FA { get; set; }
public string ServerUrl { get; set; } = string.Empty;
public bool SmtpCheckCertificateRevocation { get; set; } = true;

View File

@ -1,30 +1,24 @@
using Remotely.Server.Abstractions;
using Remotely.Server.Filters;
using Remotely.Server.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Remotely.Server.Services;
namespace Remotely.Server.Areas.RemoteControl.Pages;
[ServiceFilter(typeof(ViewerAuthorizationFilter))]
public class ViewerModel : PageModel
public class ViewerModel(IDataService _dataService) : PageModel
{
private readonly IViewerPageDataProvider _viewerDataProvider;
public ViewerModel(IViewerPageDataProvider viewerDataProvider)
{
_viewerDataProvider = viewerDataProvider;
}
public string FaviconUrl { get; private set; } = string.Empty;
public string LogoUrl { get; private set; } = string.Empty;
public string PageDescription { get; private set; } = string.Empty;
public string PageTitle { get; private set; } = string.Empty;
public string FaviconUrl { get; } = "favicon.ico";
public string LogoUrl { get; set; } = string.Empty;
public string PageDescription { get; } = "Open-source remote support tools.";
public string PageTitle { get; } = "Remotely Remote Control";
public string ThemeUrl { get; private set; } = string.Empty;
public string UserDisplayName { get; private set; } = string.Empty;
public async Task OnGet()
{
var theme = await _viewerDataProvider.GetTheme(this);
var theme = await GetTheme();
ThemeUrl = theme switch
{
@ -32,10 +26,59 @@ public class ViewerModel : PageModel
ViewerPageTheme.Light => "/_content/Remotely.Server/css/remote-control-light.css",
_ => "/_content/Remotely.Server/css/remote-control-dark.css"
};
UserDisplayName = await _viewerDataProvider.GetUserDisplayName(this);
PageTitle = await _viewerDataProvider.GetPageTitle(this);
PageDescription = await _viewerDataProvider.GetPageDescription(this);
FaviconUrl = await _viewerDataProvider.GetFaviconUrl(this);
LogoUrl = await _viewerDataProvider.GetLogoUrl(this);
UserDisplayName = await GetUserDisplayName();
LogoUrl = await GetLogoUrl();
}
private async Task<string> GetLogoUrl()
{
return await GetTheme() == ViewerPageTheme.Dark ?
"/images/viewer/remotely-logo-dark.svg" :
"/images/viewer/remotely-logo-light.svg";
}
private Task<ViewerPageTheme> GetTheme()
{
// TODO: Implement light theme in new viewer design.
return Task.FromResult(ViewerPageTheme.Dark);
//if (User.Identity.IsAuthenticated)
//{
// var user = _dataService.GetUserByNameWithOrg(User.Identity.Name);
// var userTheme = user.UserOptions.Theme switch
// {
// Theme.Light => ViewerPageTheme.Light,
// Theme.Dark => ViewerPageTheme.Dark,
// _ => ViewerPageTheme.Dark
// };
// return Task.FromResult(userTheme);
//}
//var appTheme = _appConfig.Theme switch
//{
// Theme.Light => ViewerPageTheme.Light,
// Theme.Dark => ViewerPageTheme.Dark,
// _ => ViewerPageTheme.Dark
//};
//return Task.FromResult(appTheme);
}
private async Task<string> GetUserDisplayName()
{
if (string.IsNullOrWhiteSpace(User?.Identity?.Name))
{
return string.Empty;
}
var userResult = await _dataService.GetUserByName(User.Identity.Name);
if (!userResult.IsSuccess)
{
return string.Empty;
}
var user = userResult.Value;
var displayName = user.UserOptions?.DisplayName ?? user.UserName ?? string.Empty;
return displayName;
}
}

View File

@ -17,13 +17,13 @@ using Remotely.Server.Hubs;
using Remotely.Server.Models;
using Remotely.Server.Options;
using Remotely.Server.Services;
using Remotely.Server.Services.RcImplementations;
using Remotely.Server.Services.Stores;
using Remotely.Shared.Entities;
using Remotely.Shared.Services;
using Serilog;
using System.Net;
using RatePolicyNames = Remotely.Server.RateLimiting.PolicyNames;
using Remotely.Server.Filters;
var builder = WebApplication.CreateBuilder(args);
var configuration = builder.Configuration;
@ -116,7 +116,7 @@ services.AddIdentityCore<RemotelyUser>(options =>
services.AddScoped<IAuthorizationHandler, TwoFactorRequiredHandler>();
services.AddScoped<IAuthorizationHandler, OrganizationAdminRequirementHandler>();
services.AddScoped<IAuthorizationHandler, ServerAdminRequirementHandler>();
builder.Services.AddSingleton<IEmailSender<RemotelyUser>, IdentityNoOpEmailSender>();
services.AddSingleton<IEmailSender<RemotelyUser>, IdentityNoOpEmailSender>();
services.AddAuthorization(options =>
{
@ -248,18 +248,15 @@ services.AddSingleton<ILogsManager, LogsManager>();
services.AddScoped<IThemeProvider, ThemeProvider>();
services.AddScoped<IChatSessionStore, ChatSessionStore>();
services.AddScoped<ITerminalStore, TerminalStore>();
services.AddScoped<ViewerAuthorizationFilter>();
services.AddSingleton(WeakReferenceMessenger.Default);
services.AddRemoteControlServer(config =>
{
config.AddHubEventHandler<HubEventHandler>();
config.AddViewerAuthorizer<ViewerAuthorizer>();
config.AddViewerPageDataProvider<ViewerPageDataProvider>();
config.AddViewerOptionsProvider<ViewerOptionsProvider>();
config.AddSessionRecordingSink<SessionRecordingSink>();
});
services.AddSingleton<ISessionRecordingSink, SessionRecordingSink>();
services.AddSingleton<IDesktopStreamCache, DesktopStreamCache>();
services.AddSingleton<IRemoteControlSessionCache, RemoteControlSessionCache>();
services.AddSingleton<ISystemTime, SystemTime>();
services.AddSingleton<IAgentHubSessionCache, AgentHubSessionCache>();
services.AddHostedService<RemoteControlSessionCleaner>();
services.AddHostedService<RemoteControlSessionReconnector>();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

View File

@ -4,7 +4,6 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Identity.Client;
using Remotely.Server.Services.RcImplementations;
using System;
using System.IO;
using System.Linq;

View File

@ -1,157 +0,0 @@
using Remotely.Server.Abstractions;
using Remotely.Server.Hubs;
using Remotely.Server.Models;
using Remotely.Shared.Enums;
using Remotely.Shared.Interfaces;
using Microsoft.AspNetCore.SignalR;
namespace Remotely.Server.Services.RcImplementations;
public class HubEventHandler : IHubEventHandler
{
private readonly IHubContext<AgentHub, IAgentHubClient> _serviceHub;
private readonly IAgentHubSessionCache _agentCache;
private readonly IHubContext<ViewerHub, IViewerHubClient> _viewerHub;
private readonly ILogger<HubEventHandler> _logger;
public HubEventHandler(
IHubContext<AgentHub, IAgentHubClient> agentHub,
IHubContext<ViewerHub, IViewerHubClient> viewerHub,
IAgentHubSessionCache agentHubSessionCache,
ILogger<HubEventHandler> logger)
{
_serviceHub = agentHub;
_agentCache = agentHubSessionCache;
_viewerHub = viewerHub;
_logger = logger;
}
public Task ChangeWindowsSession(RemoteControlSession session, string viewerConnectionId, int targetWindowsSession)
{
if (session is not RemoteControlSessionEx ex)
{
_logger.LogError("Event should have been for RemoteControlSessionEx.");
return Task.CompletedTask;
}
return _serviceHub.Clients
.Client(ex.AgentConnectionId)
.ChangeWindowsSession(
viewerConnectionId,
$"{ex.UnattendedSessionId}",
ex.AccessKey,
ex.UserConnectionId,
ex.RequesterUserName,
ex.OrganizationName,
ex.OrganizationId,
targetWindowsSession);
}
public Task InvokeCtrlAltDel(RemoteControlSession session, string viewerConnectionId)
{
if (session is not RemoteControlSessionEx ex)
{
_logger.LogError("Event should have been for RemoteControlSessionEx.");
return Task.CompletedTask;
}
return _serviceHub.Clients.Client(ex.AgentConnectionId).InvokeCtrlAltDel();
}
public Task NotifyDesktopSessionAdded(RemoteControlSession sessionInfo)
{
return Task.CompletedTask;
}
public Task NotifyDesktopSessionDisposed(RemoteControlSession sessionInfo)
{
return Task.CompletedTask;
}
public Task NotifyDesktopSessionRemoved(RemoteControlSession sessionInfo)
{
return Task.CompletedTask;
}
public Task NotifyRemoteControlEnded(RemoteControlSession sessionInfo)
{
return Task.CompletedTask;
}
public Task NotifyRemoteControlStarted(RemoteControlSession sessionInfo)
{
return Task.CompletedTask;
}
public Task NotifySessionChanged(RemoteControlSession session, SessionSwitchReasonEx reason, int currentSessionId)
{
if (session is not RemoteControlSessionEx ex)
{
_logger.LogError("Event should have been for RemoteControlSessionEx.");
return Task.CompletedTask;
}
if (ex.RequireConsent)
{
// Don't restart if consent wasn't granted on the first request.
return Task.CompletedTask;
}
_logger.LogDebug("Windows session changed during remote control. " +
"Reason: {reason}. " +
"Current Session ID: {sessionId}. " +
"Session Info: {@sessionInfo}",
reason,
currentSessionId,
session);
return _serviceHub.Clients
.Client(ex.AgentConnectionId)
.RestartScreenCaster(
ex.ViewerList.ToArray(),
$"{ex.UnattendedSessionId}",
ex.AccessKey,
ex.UserConnectionId,
ex.RequesterUserName,
ex.OrganizationName,
ex.OrganizationId);
}
public async Task RestartScreenCaster(RemoteControlSession session)
{
if (session is not RemoteControlSessionEx sessionEx)
{
_logger.LogError("Event should have been for RemoteControlSessionEx.");
return;
}
if (sessionEx.RequireConsent)
{
// Don't restart if consent wasn't granted on the first request.
return;
}
if (!_agentCache.TryGetConnectionId(sessionEx.DeviceId, out var agentConnectionId))
{
await _viewerHub.Clients
.Clients(sessionEx.ViewerList)
.ShowMessage("Waiting for agent to come online");
return;
}
sessionEx.AgentConnectionId = agentConnectionId;
await _serviceHub.Clients
.Client(sessionEx.AgentConnectionId)
.RestartScreenCaster(
session.ViewerList.ToArray(),
$"{sessionEx.UnattendedSessionId}",
sessionEx.AccessKey,
sessionEx.UserConnectionId,
sessionEx.RequesterName,
sessionEx.OrganizationName,
sessionEx.OrganizationId);
}
}

View File

@ -1,45 +0,0 @@
using Remotely.Server.Abstractions;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Caching.Memory;
using Remotely.Shared.Utilities;
using System;
using System.Threading.Tasks;
namespace Remotely.Server.Services.RcImplementations;
public class ViewerAuthorizer : IViewerAuthorizer
{
private readonly IDataService _dataService;
private readonly IOtpProvider _otpProvider;
public ViewerAuthorizer(IDataService dataService, IOtpProvider otpProvider)
{
_dataService = dataService;
_otpProvider = otpProvider;
}
public string UnauthorizedRedirectUrl { get; } = "/Account/Login";
public async Task<bool> IsAuthorized(AuthorizationFilterContext context)
{
var settings = await _dataService.GetSettings();
if (!settings.RemoteControlRequiresAuthentication)
{
return true;
}
if (context.HttpContext.User.Identity?.IsAuthenticated == true)
{
return true;
}
if (context.HttpContext.Request.Query.TryGetValue("otp", out var otp) &&
_otpProvider.Exists($"{otp}"))
{
return true;
}
return false;
}
}

View File

@ -1,26 +0,0 @@
using Remotely.Server.Abstractions;
using Remotely.Server.Models;
using Remotely.Shared.Models;
using Microsoft.AspNetCore.SignalR;
using System.Threading.Tasks;
namespace Remotely.Server.Services.RcImplementations;
public class ViewerOptionsProvider : IViewerOptionsProvider
{
private readonly IDataService _dataService;
public ViewerOptionsProvider(IDataService dataService)
{
_dataService = dataService;
}
public async Task<RemoteControlViewerOptions> GetViewerOptions()
{
var settings = await _dataService.GetSettings();
var options = new RemoteControlViewerOptions()
{
ShouldRecordSession = settings.EnableRemoteControlRecording
};
return options;
}
}

View File

@ -1,87 +0,0 @@
using Remotely.Server.Abstractions;
using Remotely.Server.Areas.RemoteControl.Pages;
using Remotely.Server.Models;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Org.BouncyCastle.Ocsp;
using Remotely.Shared.Enums;
using Remotely.Shared.Models;
using System;
using System.Threading.Tasks;
namespace Remotely.Server.Services.RcImplementations;
public class ViewerPageDataProvider : IViewerPageDataProvider
{
private readonly IDataService _dataService;
public ViewerPageDataProvider(IDataService dataService)
{
_dataService = dataService;
}
public Task<string> GetFaviconUrl(PageModel viewerModel)
{
return Task.FromResult("/_content/Remotely.Server/favicon.ico");
}
public async Task<string> GetLogoUrl(PageModel viewerModel)
{
return await GetTheme(viewerModel) == ViewerPageTheme.Dark ?
"/images/viewer/remotely-logo-dark.svg" :
"/images/viewer/remotely-logo-light.svg";
}
public Task<string> GetPageDescription(PageModel viewerModel)
{
return Task.FromResult("Open-source remote support tools.");
}
public Task<string> GetPageTitle(PageModel pageModel)
{
return Task.FromResult("Remotely Remote Control");
}
public Task<ViewerPageTheme> GetTheme(PageModel pageModel)
{
// TODO: Implement light theme in new viewer design.
return Task.FromResult(ViewerPageTheme.Dark);
//if (pageModel.User.Identity.IsAuthenticated)
//{
// var user = _dataService.GetUserByNameWithOrg(pageModel.User.Identity.Name);
// var userTheme = user.UserOptions.Theme switch
// {
// Theme.Light => ViewerPageTheme.Light,
// Theme.Dark => ViewerPageTheme.Dark,
// _ => ViewerPageTheme.Dark
// };
// return Task.FromResult(userTheme);
//}
//var appTheme = _appConfig.Theme switch
//{
// Theme.Light => ViewerPageTheme.Light,
// Theme.Dark => ViewerPageTheme.Dark,
// _ => ViewerPageTheme.Dark
//};
//return Task.FromResult(appTheme);
}
public async Task<string> GetUserDisplayName(PageModel pageModel)
{
if (string.IsNullOrWhiteSpace(pageModel?.User?.Identity?.Name))
{
return string.Empty;
}
var userResult = await _dataService.GetUserByName(pageModel.User.Identity.Name);
if (!userResult.IsSuccess)
{
return string.Empty;
}
var user = userResult.Value;
var displayName = user.UserOptions?.DisplayName ?? user.UserName ?? string.Empty;
return displayName;
}
}

View File

@ -1,7 +1,5 @@
using Remotely.Server.Abstractions;
using Remotely.Server.Models;
using Remotely.Shared.Services;
using Microsoft.Extensions.Logging;
using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
@ -35,17 +33,14 @@ internal class RemoteControlSessionCache : IRemoteControlSessionCache
// so we need to use an outer lock.
private readonly object _sessionsLock = new();
private readonly IHubEventHandler _hubEventHandler;
private readonly ILogger<RemoteControlSessionCache> _logger;
private readonly ISystemTime _systemTime;
public RemoteControlSessionCache(
ISystemTime systemTime,
IHubEventHandler hubEventHandler,
ILogger<RemoteControlSessionCache> logger)
{
_systemTime = systemTime;
_hubEventHandler = hubEventHandler;
_logger = logger;
}
@ -77,7 +72,6 @@ internal class RemoteControlSessionCache : IRemoteControlSessionCache
}
_sessions[sessionId] = session;
NotifySessionAdded(session);
return session;
}
}
@ -88,9 +82,7 @@ internal class RemoteControlSessionCache : IRemoteControlSessionCache
{
return _sessions.GetOrAdd(sessionId, (key) =>
{
var session = valueFactory(key);
NotifySessionAdded(session);
return session;
return valueFactory(key);
});
}
}
@ -102,13 +94,12 @@ internal class RemoteControlSessionCache : IRemoteControlSessionCache
foreach (var session in _sessions)
{
if (session.Value.Mode is RemoteControlMode.Unattended or RemoteControlMode.Unknown &&
!session.Value.ViewerList.Any() &&
session.Value.ViewerList.Count == 0 &&
session.Value.Created < _systemTime.Now.AddMinutes(-1))
{
_logger.LogWarning("Removing expired session: {session}", JsonSerializer.Serialize(session.Value));
if (_sessions.TryRemove(session.Key, out var expiredSession))
{
NotifySessionRemoved(expiredSession);
expiredSession.Dispose();
}
}
@ -121,13 +112,7 @@ internal class RemoteControlSessionCache : IRemoteControlSessionCache
{
lock (_sessionsLock)
{
if (_sessions.TryAdd(sessionId, session))
{
NotifySessionAdded(session);
return true;
}
return false;
return _sessions.TryAdd(sessionId, session);
}
}
@ -147,7 +132,6 @@ internal class RemoteControlSessionCache : IRemoteControlSessionCache
{
try
{
NotifySessionRemoved(session);
session.Dispose();
}
catch (Exception ex)
@ -161,20 +145,4 @@ internal class RemoteControlSessionCache : IRemoteControlSessionCache
return false;
}
private void NotifySessionAdded(RemoteControlSession session)
{
try
{
_ = _hubEventHandler.NotifyDesktopSessionAdded(session);
}
catch { } // Ignore errors thrown by consumer.
}
private void NotifySessionRemoved(RemoteControlSession session)
{
try
{
_ = _hubEventHandler.NotifyDesktopSessionRemoved(session);
}
catch { } // Ignore errors thrown by consumer.
}
}

View File

@ -1,30 +1,27 @@
using Remotely.Server.Abstractions;
using Remotely.Server.Enums;
using Remotely.Server.Enums;
using Remotely.Server.Hubs;
using Remotely.Shared.Helpers;
using Remotely.Shared.Interfaces;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace Remotely.Server.Services;
internal class RemoteControlSessionReconnector : BackgroundService
{
private readonly IRemoteControlSessionCache _sessionCache;
private readonly IHubEventHandler _hubEvents;
private readonly IHubContext<ViewerHub, IViewerHubClient> _viewerHub;
private readonly IHubContext<AgentHub, IAgentHubClient> _agentHub;
private readonly ILogger<RemoteControlSessionCleaner> _logger;
public RemoteControlSessionReconnector(
IRemoteControlSessionCache sessionCache,
IHubEventHandler hubEvents,
IHubContext<ViewerHub, IViewerHubClient> viewerHub,
IHubContext<AgentHub, IAgentHubClient> agentHub,
ILogger<RemoteControlSessionCleaner> logger)
{
_sessionCache = sessionCache;
_hubEvents = hubEvents;
_viewerHub = viewerHub;
_agentHub = agentHub;
_logger = logger;
}
@ -64,7 +61,16 @@ internal class RemoteControlSessionReconnector : BackgroundService
await RateLimiter.Throttle(async () =>
{
await _viewerHub.Clients.Clients(session.ViewerList).Reconnecting();
await _hubEvents.RestartScreenCaster(session);
await _agentHub.Clients
.Client(session.AgentConnectionId)
.RestartScreenCaster(
[.. session.ViewerList],
$"{session.UnattendedSessionId}",
session.AccessKey,
session.UserConnectionId,
session.RequesterName,
session.OrganizationName,
session.OrganizationId);
},
TimeSpan.FromSeconds(10),
key: $"{session.UnattendedSessionId}",

View File

@ -1,13 +1,7 @@
using Remotely.Server.Abstractions;
using Microsoft.Extensions.Logging;
using Remotely.Server.Hubs;
using Remotely.Server.Hubs;
using Remotely.Shared.Entities;
using Remotely.Shared.Enums;
using Remotely.Shared.Utilities;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Remotely.Server.Services;

View File

@ -1,7 +1,25 @@
using Remotely.Server.Abstractions;
using Remotely.Server.Models;
using Remotely.Server.Models;
namespace Remotely.Server.Services;
/// <summary>
/// The service is responsible for storing session recordings.
/// </summary>
public interface ISessionRecordingSink
{
/// <summary>
/// Sink a live webm stream to persistent storage.
/// </summary>
/// <param name="webmStream"></param>
/// <param name="hubCallerContext"></param>
/// <param name="session"></param>
/// <returns></returns>
Task SinkWebmStream(
IAsyncEnumerable<byte[]> webmStream,
RemoteControlSession session);
}
namespace Remotely.Server.Services.RcImplementations;
public class SessionRecordingSink : ISessionRecordingSink
{
@ -17,8 +35,8 @@ public class SessionRecordingSink : ISessionRecordingSink
get
{
return Path.Combine(
AppDomain.CurrentDomain.BaseDirectory,
"AppData",
AppDomain.CurrentDomain.BaseDirectory,
"AppData",
"recordings");
}
}
@ -31,7 +49,7 @@ public class SessionRecordingSink : ISessionRecordingSink
_ = Directory.CreateDirectory(targetDir);
var viewerName = !string.IsNullOrWhiteSpace(session.RequesterName) ?
session.RequesterName :
session.RequesterName :
"AnonymousUser";
var fileName = $"{viewerName}_{DateTime.Now:yyyyMMdd_HHmmssfff}.webm";
@ -42,7 +60,7 @@ public class SessionRecordingSink : ISessionRecordingSink
{
await fs.WriteAsync(chunk);
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error while sinking webm stream.");

View File

@ -1,6 +1,4 @@
using Castle.Core.Logging;
using Remotely.Server.Abstractions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using Remotely.Server.Hubs;
@ -11,7 +9,6 @@ using Remotely.Shared.Utilities;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Remotely.Server.Tests;