mirror of
https://github.com/immense/Remotely.git
synced 2025-10-26 11:27:15 +00:00
Merge in abstractions in Server project. Remove unneeded RemoteControlSessionLimit.
This commit is contained in:
parent
81641afe06
commit
9e055af473
@ -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).
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -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)
|
||||
|
||||
@ -37,9 +37,6 @@
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private string? _showClass;
|
||||
private string? _displayStyle;
|
||||
|
||||
private IJSObjectReference? _module;
|
||||
private ElementReference _modalRef;
|
||||
|
||||
|
||||
@ -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 />
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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.
|
||||
}
|
||||
}
|
||||
|
||||
@ -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}",
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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.");
|
||||
@ -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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user