diff --git a/Desktop.Win/Properties/PublishProfiles/desktop-win-x64.pubxml b/Desktop.Win/Properties/PublishProfiles/desktop-win-x64.pubxml index fa4ce51eb..ace0a3e8b 100644 --- a/Desktop.Win/Properties/PublishProfiles/desktop-win-x64.pubxml +++ b/Desktop.Win/Properties/PublishProfiles/desktop-win-x64.pubxml @@ -7,7 +7,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121. FileSystem Release x64 - net8.0 + net8.0-windows ..\Server\wwwroot\Content\Win-x64\ win-x64 true diff --git a/Desktop.Win/Properties/PublishProfiles/desktop-win-x86.pubxml b/Desktop.Win/Properties/PublishProfiles/desktop-win-x86.pubxml index cdccaccf3..4c2fc2653 100644 --- a/Desktop.Win/Properties/PublishProfiles/desktop-win-x86.pubxml +++ b/Desktop.Win/Properties/PublishProfiles/desktop-win-x86.pubxml @@ -7,7 +7,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121. FileSystem Release x86 - net8.0 + net8.0-windows ..\Server\wwwroot\Content\Win-x86\ win-x86 true diff --git a/Desktop.Win/Properties/PublishProfiles/packaged-win-x64-debug.pubxml b/Desktop.Win/Properties/PublishProfiles/packaged-win-x64-debug.pubxml index 62bd00ece..d90708483 100644 --- a/Desktop.Win/Properties/PublishProfiles/packaged-win-x64-debug.pubxml +++ b/Desktop.Win/Properties/PublishProfiles/packaged-win-x64-debug.pubxml @@ -7,7 +7,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121. FileSystem Debug x64 - net8.0 + net8.0-windows ..\Agent\bin\publish\win-x64\Desktop true win-x64 diff --git a/Desktop.Win/Properties/PublishProfiles/packaged-win-x64.pubxml b/Desktop.Win/Properties/PublishProfiles/packaged-win-x64.pubxml index 0bfa9e5bb..9772a4f7c 100644 --- a/Desktop.Win/Properties/PublishProfiles/packaged-win-x64.pubxml +++ b/Desktop.Win/Properties/PublishProfiles/packaged-win-x64.pubxml @@ -7,7 +7,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121. FileSystem Release x64 - net8.0 + net8.0-windows ..\Agent\bin\publish\win-x64\Desktop true win-x64 diff --git a/Desktop.Win/Properties/PublishProfiles/packaged-win-x86.pubxml b/Desktop.Win/Properties/PublishProfiles/packaged-win-x86.pubxml index 1841a4d93..0fb235b33 100644 --- a/Desktop.Win/Properties/PublishProfiles/packaged-win-x86.pubxml +++ b/Desktop.Win/Properties/PublishProfiles/packaged-win-x86.pubxml @@ -7,7 +7,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121. FileSystem Release x86 - net8.0 + net8.0-windows ..\Agent\bin\publish\win-x86\Desktop win-x86 true diff --git a/Server/API/AgentUpdateController.cs b/Server/API/AgentUpdateController.cs index 38c39355c..08963e1cc 100644 --- a/Server/API/AgentUpdateController.cs +++ b/Server/API/AgentUpdateController.cs @@ -21,18 +21,18 @@ public class AgentUpdateController : ControllerBase { private readonly IHubContext _agentHubContext; private readonly ILogger _logger; - private readonly IApplicationConfig _appConfig; + private readonly IDataService _dataService; private readonly IWebHostEnvironment _hostEnv; private readonly IAgentHubSessionCache _serviceSessionCache; public AgentUpdateController(IWebHostEnvironment hostingEnv, - IApplicationConfig appConfig, + IDataService dataService, IAgentHubSessionCache serviceSessionCache, IHubContext agentHubContext, ILogger logger) { _hostEnv = hostingEnv; - _appConfig = appConfig; + _dataService = dataService; _serviceSessionCache = serviceSessionCache; _agentHubContext = agentHubContext; _logger = logger; @@ -105,7 +105,8 @@ private async Task CheckForDeviceBan(string deviceIp) return false; } - if (_appConfig.BannedDevices.Contains(deviceIp)) + var settings = await _dataService.GetSettings(); + if (settings.BannedDevices.Contains(deviceIp)) { _logger.LogInformation("Device IP ({deviceIp}) is banned. Sending uninstall command.", deviceIp); diff --git a/Server/API/ClientDownloadsController.cs b/Server/API/ClientDownloadsController.cs index 332379c1f..6d1dddc9e 100644 --- a/Server/API/ClientDownloadsController.cs +++ b/Server/API/ClientDownloadsController.cs @@ -20,7 +20,7 @@ namespace Remotely.Server.API; [ApiController] public class ClientDownloadsController : ControllerBase { - private readonly IApplicationConfig _appConfig; + private readonly IDataService _dataService; private readonly IEmbeddedServerDataSearcher _embeddedDataSearcher; private readonly SemaphoreSlim _fileLock = new(1, 1); private readonly IWebHostEnvironment _hostEnv; @@ -28,12 +28,12 @@ public class ClientDownloadsController : ControllerBase public ClientDownloadsController( IWebHostEnvironment hostEnv, IEmbeddedServerDataSearcher embeddedDataSearcher, - IApplicationConfig appConfig, + IDataService dataService, ILogger logger) { _hostEnv = hostEnv; _embeddedDataSearcher = embeddedDataSearcher; - _appConfig = appConfig; + _dataService = dataService; _logger = logger; } @@ -133,7 +133,8 @@ private async Task GetBashInstaller(string fileName, string organ var hostIndex = fileContents.IndexOf("HostName="); var orgIndex = fileContents.IndexOf("Organization="); - var effectiveScheme = _appConfig.ForceClientHttps ? "https" : Request.Scheme; + var settings = await _dataService.GetSettings(); + var effectiveScheme = settings.ForceClientHttps ? "https" : Request.Scheme; fileContents[hostIndex] = $"HostName=\"{effectiveScheme}://{Request.Host}\""; fileContents[orgIndex] = $"Organization=\"{organizationId}\""; @@ -143,9 +144,10 @@ private async Task GetBashInstaller(string fileName, string organ private async Task GetDesktopFile(string filePath, string? organizationId = null) { - LogRequest(nameof(GetDesktopFile)); + var settings = await _dataService.GetSettings(); + await LogRequest(nameof(GetDesktopFile)); - var effectiveScheme = _appConfig.ForceClientHttps ? "https" : Request.Scheme; + var effectiveScheme = settings.ForceClientHttps ? "https" : Request.Scheme; var serverUrl = $"{effectiveScheme}://{Request.Host}"; var embeddedData = new EmbeddedServerData(new Uri(serverUrl), organizationId); var result = await _embeddedDataSearcher.GetRewrittenStream(filePath, embeddedData); @@ -160,7 +162,8 @@ private async Task GetDesktopFile(string filePath, string? organi private async Task GetInstallFile(string organizationId, string platformID) { - LogRequest(nameof(GetInstallFile)); + var settings = await _dataService.GetSettings(); + await LogRequest(nameof(GetInstallFile)); if (!await _fileLock.WaitAsync(TimeSpan.FromSeconds(15))) { @@ -173,7 +176,7 @@ private async Task GetInstallFile(string organizationId, string p { case "WindowsInstaller": { - var effectiveScheme = _appConfig.ForceClientHttps ? "https" : Request.Scheme; + var effectiveScheme = settings.ForceClientHttps ? "https" : Request.Scheme; var serverUrl = $"{effectiveScheme}://{Request.Host}"; var filePath = Path.Combine(_hostEnv.WebRootPath, "Content", "Remotely_Installer.exe"); var embeddedData = new EmbeddedServerData(new Uri(serverUrl), organizationId); @@ -220,9 +223,10 @@ private async Task GetInstallFile(string organizationId, string p } } - private void LogRequest(string methodName) + private async Task LogRequest(string methodName) { - if (_appConfig.UseHttpLogging) + var settings = await _dataService.GetSettings(); + if (settings.UseHttpLogging) { var ip = Request.HttpContext.Connection.RemoteIpAddress; if (ip?.IsIPv4MappedToIPv6 == true) @@ -230,7 +234,7 @@ private void LogRequest(string methodName) ip = ip.MapToIPv4(); } - var effectiveScheme = _appConfig.ForceClientHttps ? "https" : Request.Scheme; + var effectiveScheme = settings.ForceClientHttps ? "https" : Request.Scheme; _logger.LogInformation( "Started client download via {methodName}. Effective Scheme: {scheme}. Effective Host: {host}. Remote IP: {ip}.", diff --git a/Server/API/LoginController.cs b/Server/API/LoginController.cs index 3268bfc9a..e497f1b70 100644 --- a/Server/API/LoginController.cs +++ b/Server/API/LoginController.cs @@ -20,7 +20,6 @@ namespace Remotely.Server.API; [Obsolete("This controller is here only for legacy purposes. For new integrations, use API tokens.")] public class LoginController : ControllerBase { - private readonly IApplicationConfig _appConfig; private readonly IDataService _dataService; private readonly IHubContext _desktopHub; private readonly IRemoteControlSessionCache _remoteControlSessionCache; @@ -31,7 +30,6 @@ public class LoginController : ControllerBase public LoginController( SignInManager signInManager, IDataService dataService, - IApplicationConfig appConfig, IHubContext casterHubContext, IRemoteControlSessionCache remoteControlSessionCache, IHubContext viewerHubContext, @@ -39,7 +37,6 @@ public LoginController( { _signInManager = signInManager; _dataService = dataService; - _appConfig = appConfig; _desktopHub = casterHubContext; _remoteControlSessionCache = remoteControlSessionCache; _viewerHub = viewerHubContext; @@ -76,7 +73,8 @@ public async Task Logout() [HttpPost] public async Task Post([FromBody] ApiLogin login) { - if (!_appConfig.AllowApiLogin) + var settings = await _dataService.GetSettings(); + if (!settings.AllowApiLogin) { return NotFound(); } diff --git a/Server/API/RemoteControlController.cs b/Server/API/RemoteControlController.cs index fe339e07f..8cd83ca16 100644 --- a/Server/API/RemoteControlController.cs +++ b/Server/API/RemoteControlController.cs @@ -27,10 +27,9 @@ public class RemoteControlController : ControllerBase private readonly IHubContext _agentHub; private readonly IRemoteControlSessionCache _remoteControlSessionCache; private readonly IAgentHubSessionCache _serviceSessionCache; - private readonly IApplicationConfig _appConfig; + private readonly IDataService _dataService; private readonly IOtpProvider _otpProvider; private readonly IHubEventHandler _hubEvents; - private readonly IDataService _dataService; private readonly SignInManager _signInManager; private readonly ILogger _logger; @@ -42,14 +41,12 @@ public RemoteControlController( IAgentHubSessionCache serviceSessionCache, IOtpProvider otpProvider, IHubEventHandler hubEvents, - IApplicationConfig appConfig, ILogger logger) { _dataService = dataService; _agentHub = agentHub; _remoteControlSessionCache = remoteControlSessionCache; _serviceSessionCache = serviceSessionCache; - _appConfig = appConfig; _otpProvider = otpProvider; _hubEvents = hubEvents; _signInManager = signInManager; @@ -72,7 +69,8 @@ public async Task Get(string deviceID) [Obsolete("This method is deprecated. Use the GET method along with API keys instead.")] public async Task Post([FromBody] RemoteControlRequest rcRequest) { - if (!_appConfig.AllowApiLogin) + var settings = await _dataService.GetSettings(); + if (!settings.AllowApiLogin) { return NotFound(); } @@ -145,7 +143,8 @@ private async Task InitiateRemoteControl(string deviceID, string .OfType() .Count(x => x.OrganizationId == orgId); - if (sessionCount > _appConfig.RemoteControlSessionLimit) + 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."); } diff --git a/Server/Auth/TwoFactorRequiredHandler.cs b/Server/Auth/TwoFactorRequiredHandler.cs index 73aa8d659..0d62fb79a 100644 --- a/Server/Auth/TwoFactorRequiredHandler.cs +++ b/Server/Auth/TwoFactorRequiredHandler.cs @@ -12,19 +12,18 @@ namespace Remotely.Server.Auth; public class TwoFactorRequiredHandler : AuthorizationHandler { private readonly IDataService _dataService; - private readonly IApplicationConfig _appConfig; - public TwoFactorRequiredHandler(IDataService dataService, IApplicationConfig appConfig) + public TwoFactorRequiredHandler(IDataService dataService) { _dataService = dataService; - _appConfig = appConfig; } protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, TwoFactorRequiredRequirement requirement) { + var settings = await _dataService.GetSettings(); if (context.User.Identity?.IsAuthenticated == true && context.User.Identity.Name is not null && - _appConfig.Require2FA) + settings.Require2FA) { var userResult = await _dataService.GetUserByName(context.User.Identity.Name); diff --git a/Server/Components/Account/Pages/Register.razor b/Server/Components/Account/Pages/Register.razor index c9a69429d..77c9ceb8b 100644 --- a/Server/Components/Account/Pages/Register.razor +++ b/Server/Components/Account/Pages/Register.razor @@ -16,13 +16,12 @@ @inject NavigationManager NavigationManager @inject IdentityRedirectManager RedirectManager @inject IDataService DataService -@inject IApplicationConfig AppConfig @inject IWebHostEnvironment HostEnv Register

Register

-@if (!IsRegistrationEnabled()) +@if (!_registrationEnabled) {

Registration is disabled.

} @@ -69,6 +68,7 @@ else @code { private IEnumerable? identityErrors; private int _organizationCount; + private bool _registrationEnabled; [SupplyParameterFromForm] private InputModel Input { get; set; } = new(); @@ -78,12 +78,13 @@ else private string? Message => identityErrors is null ? null : $"Error: {string.Join(", ", identityErrors.Select(error => error.Description))}"; - protected override void OnInitialized() + protected override async Task OnInitializedAsync() { + _registrationEnabled = await IsRegistrationEnabled(); _organizationCount = DataService.GetOrganizationCount(); - base.OnInitialized(); + await base.OnInitializedAsync(); } - + public async Task RegisterUser(EditContext editContext) { var user = CreateUser(); @@ -155,9 +156,10 @@ else return (IUserEmailStore)UserStore; } - private bool IsRegistrationEnabled() + private async Task IsRegistrationEnabled() { - return AppConfig.MaxOrganizationCount < 0 || _organizationCount < AppConfig.MaxOrganizationCount; + var settings = await DataService.GetSettings(); + return settings.MaxOrganizationCount < 0 || _organizationCount < settings.MaxOrganizationCount; } private sealed class InputModel diff --git a/Server/Components/App.razor b/Server/Components/App.razor index 74162a94a..8d385b5eb 100644 --- a/Server/Components/App.razor +++ b/Server/Components/App.razor @@ -1,6 +1,6 @@ @inject AuthenticationStateProvider AuthProvider @inject IDataService DataService -@inject IApplicationConfig AppConfig +@inject IDataService DataService @inject IThemeProvider ThemeProvider @@ -28,19 +28,6 @@ - - -
- - An error has occurred. This application may no longer respond until reloaded. - - - An unhandled exception has occurred. See browser dev tools for details. - - Reload - 🗙 -
- diff --git a/Server/Components/AuthorizedIndex.razor b/Server/Components/AuthorizedIndex.razor index 7b6096098..2c8a1f363 100644 --- a/Server/Components/AuthorizedIndex.razor +++ b/Server/Components/AuthorizedIndex.razor @@ -2,13 +2,13 @@ @inherits AuthComponentBase @inject NavigationManager NavManager -@inject IApplicationConfig AppConfig +@inject IDataService DataService @inject SignInManager SignInManager -@if (!string.IsNullOrWhiteSpace(AppConfig.MessageOfTheDay)) +@if (!string.IsNullOrWhiteSpace(_settings?.MessageOfTheDay)) {
- +
} @@ -17,11 +17,12 @@ @code { + private SettingsModel? _settings; protected override async Task OnAfterRenderAsync(bool firstRender) { if (User is not null && - AppConfig.Require2FA && + _settings?.Require2FA == true && !User.TwoFactorEnabled) { NavManager.NavigateTo("/TwoFactorRequired"); @@ -31,6 +32,7 @@ protected override async Task OnInitializedAsync() { + _settings = await DataService.GetSettings(); await base.OnInitializedAsync(); var isAuthenticated = await AuthService.IsAuthenticated(); diff --git a/Server/Components/Devices/Terminal.razor.cs b/Server/Components/Devices/Terminal.razor.cs index a1cd1db3d..199046092 100644 --- a/Server/Components/Devices/Terminal.razor.cs +++ b/Server/Components/Devices/Terminal.razor.cs @@ -91,11 +91,6 @@ private string InputText [Inject] private IToastService ToastService { get; init; } = null!; - public void Dispose() - { - Messenger.Unregister(this, CircuitConnection.ConnectionId); - GC.SuppressFinalize(this); - } protected override Task OnAfterRenderAsync(bool firstRender) { diff --git a/Server/Components/Layout/MainLayout.razor b/Server/Components/Layout/MainLayout.razor index 82c4dbcde..c8a88b432 100644 --- a/Server/Components/Layout/MainLayout.razor +++ b/Server/Components/Layout/MainLayout.razor @@ -34,6 +34,12 @@ +
+ An unhandled error has occurred. + Reload + 🗙 +
+ \ No newline at end of file diff --git a/Server/Components/Layout/NavMenu.razor b/Server/Components/Layout/NavMenu.razor index 773a95a86..3c016ea0e 100644 --- a/Server/Components/Layout/NavMenu.razor +++ b/Server/Components/Layout/NavMenu.razor @@ -1,7 +1,7 @@ @implements IDisposable @inject NavigationManager NavigationManager @inject IAuthService AuthService -@inject IApplicationConfig AppConfig +@inject IDataService DataService @inject IDataService DataService -
- -
- -
- -

@@ -357,34 +345,6 @@
-

Connection Strings

- -
- -
- -
- -
- -
- -
- -
- -
- - -
- -
- -
- -
- -
diff --git a/Server/Components/Pages/ServerConfig.razor.cs b/Server/Components/Pages/ServerConfig.razor.cs index 8429037dc..86078ca42 100644 --- a/Server/Components/Pages/ServerConfig.razor.cs +++ b/Server/Components/Pages/ServerConfig.razor.cs @@ -2,123 +2,18 @@ using Microsoft.AspNetCore.Components.Forms; using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.SignalR; -using Remotely.Server.Components; using Remotely.Server.Hubs; +using Remotely.Server.Models; using Remotely.Server.Services; using Remotely.Shared.Entities; -using Remotely.Shared.Enums; using Remotely.Shared.Interfaces; -using System.ComponentModel.DataAnnotations; using System.Text.Json; -using System.Text.Json.Serialization; namespace Remotely.Server.Components.Pages; -public class AppSettingsModel -{ - [Display(Name = "Allow API Login")] - public bool AllowApiLogin { get; set; } - - [Display(Name = "Banned Devices")] - public List BannedDevices { get; set; } = new(); - - [Display(Name = "Data Retention (days)")] - public double DataRetentionInDays { get; set; } - - [Display(Name = "Database Provider")] - [JsonConverter(typeof(JsonStringEnumConverter))] - public DbProvider DBProvider { get; set; } - - [Display(Name = "Enable Remote Control Recording")] - public bool EnableRemoteControlRecording { get; set; } - - [Display(Name = "Enable Windows Event Log")] - public bool EnableWindowsEventLog { get; set; } - - [Display(Name = "Enforce Attended Access")] - public bool EnforceAttendedAccess { get; set; } - - [Display(Name = "Force Client HTTPS")] - public bool ForceClientHttps { get; set; } - - [Display(Name = "Known Proxies")] - public List KnownProxies { get; set; } = new(); - - [Display(Name = "Max Concurrent Updates")] - public int MaxConcurrentUpdates { get; set; } - - [Display(Name = "Max Organizations")] - public int MaxOrganizationCount { get; set; } - [Display(Name = "Message of the Day")] - public string? MessageOfTheDay { get; set; } - - [Display(Name = "Redirect To HTTPS")] - public bool RedirectToHttps { get; set; } - - [Display(Name = "Remote Control Notify User")] - public bool RemoteControlNotifyUser { get; set; } - - [Display(Name = "Remote Control Requires Authentication")] - public bool RemoteControlRequiresAuthentication { get; set; } - - [Display(Name = "Remote Control Session Limit")] - public double RemoteControlSessionLimit { get; set; } - - [Display(Name = "Require 2FA")] - public bool Require2FA { get; set; } - - [Display(Name = "SMTP Display Name")] - public string? SmtpDisplayName { get; set; } - - [Display(Name = "SMTP Email")] - [EmailAddress] - public string? SmtpEmail { get; set; } - - [Display(Name = "SMTP Host")] - public string? SmtpHost { get; set; } - [Display(Name = "SMTP Local Domain")] - public string? SmtpLocalDomain { get; set; } - - [Display(Name = "SMTP Check Certificate Revocation")] - public bool SmtpCheckCertificateRevocation { get; set; } - - [Display(Name = "SMTP Password")] - public string? SmtpPassword { get; set; } - - [Display(Name = "SMTP Port")] - public int SmtpPort { get; set; } - - [Display(Name = "SMTP Username")] - public string? SmtpUserName { get; set; } - - [Display(Name = "Theme")] - [JsonConverter(typeof(JsonStringEnumConverter))] - public Theme Theme { get; set; } - - [Display(Name = "Trusted CORS Origins")] - public List TrustedCorsOrigins { get; set; } = new(); - - [Display(Name = "Use HSTS")] - public bool UseHsts { get; set; } - - [Display(Name = "Use HTTP Logging")] - public bool UseHttpLogging { get; set; } -} - -public class ConnectionStringsModel -{ - [Display(Name = "PostgreSQL")] - public string? PostgreSQL { get; set; } - - [Display(Name = "SQLite")] - public string? SQLite { get; set; } - - [Display(Name = "SQL Server")] - public string? SQLServer { get; set; } -} - public partial class ServerConfig : AuthComponentBase { + private readonly List _userList = new(); private string? _alertMessage; private string? _bannedDeviceSelected; private string? _bannedDeviceToAdd; @@ -126,54 +21,44 @@ public partial class ServerConfig : AuthComponentBase private string? _knownProxySelected; private string? _knownProxyToAdd; - private bool _showMyOrgAdminsOnly = true; private bool _showAdminsOnly; - + private bool _showMyOrgAdminsOnly = true; private string? _trustedCorsOriginSelected; private string? _trustedCorsOriginToAdd; - private readonly List _userList = new(); - - [Inject] - private IHubContext AgentHubContext { get; init; } = null!; + public required IHubContext AgentHubContext { get; init; } [Inject] - private IConfiguration Configuration { get; init; } = null!; - - private ConnectionStringsModel ConnectionStrings { get; } = new(); + public required ICircuitManager CircuitManager { get; init; } [Inject] - private IDataService DataService { get; init; } = null!; + public required IDataService DataService { get; init; } [Inject] - private IEmailSenderEx EmailSender { get; init; } = null!; + public required IEmailSenderEx EmailSender { get; init; } [Inject] - private IWebHostEnvironment HostEnv { get; init; } = null!; + public required IWebHostEnvironment HostEnv { get; init; } [Inject] - private ILogger Logger { get; init; } = null!; + public required ILogger Logger { get; init; } [Inject] - private IAgentHubSessionCache ServiceSessionCache { get; init; } = null!; - - private AppSettingsModel Input { get; } = new(); + public required IModalService ModalService { get; init; } [Inject] - private IModalService ModalService { get; init; } = null!; + public required IAgentHubSessionCache ServiceSessionCache { get; init; } [Inject] - private IUpgradeService UpgradeService { get; init; } = null!; + public required IToastService ToastService { get; init; } [Inject] - private ICircuitManager CircuitManager { get; init; } = null!; + public required IUpgradeService UpgradeService { get; init; } - private IEnumerable OutdatedDevices => GetOutdatedDevices(); - - [Inject] - private IToastService ToastService { get; init; } = null!; + private SettingsModel Input { get; set; } = new(); + private IEnumerable OutdatedDevices => GetOutdatedDevices(); private int TotalDevices => DataService.GetTotalDevices(); private IEnumerable UserList @@ -182,7 +67,7 @@ private IEnumerable UserList { if (User is null) { - return Enumerable.Empty(); + return []; } EnsureUserSet(); @@ -202,8 +87,8 @@ protected override async Task OnInitializedAsync() return; } - Configuration.Bind("ApplicationOptions", Input); - Configuration.Bind("ConnectionStrings", ConnectionStrings); + Input = await DataService.GetSettings(); + _userList.AddRange(DataService.GetAllUsersForServer().OrderBy(x => x.UserName)); } @@ -320,25 +205,18 @@ private void RemoveTrustedCorsOrigin() private async Task Save() { - var resetEvent = new ManualResetEventSlim(); - - Configuration.GetReloadToken().RegisterChangeCallback((e) => - { - resetEvent.Set(); - }, null); - - await SaveInputToAppSettings(); - - resetEvent.Wait(5_000); + + await DataService.SaveSettings(Input); ToastService.ShowToast("Configuration saved."); - _alertMessage = "Configuration saved."; } private async Task SaveAndTestSmtpSettings() { EnsureUserSet(); - await SaveInputToAppSettings(); + + await DataService.SaveSettings(Input); + if (string.IsNullOrWhiteSpace(User.Email)) { ToastService.ShowToast2("User email is not set.", Enums.ToastType.Warning); @@ -358,58 +236,6 @@ private async Task SaveAndTestSmtpSettings() } } - private async Task SaveInputToAppSettings() - { - string savePath; - var prodSettings = HostEnv.ContentRootFileProvider.GetFileInfo("appsettings.Production.json"); - var stagingSettings = HostEnv.ContentRootFileProvider.GetFileInfo("appsettings.Staging.json"); - var devSettings = HostEnv.ContentRootFileProvider.GetFileInfo("appsettings.Development.json"); - var settings = HostEnv.ContentRootFileProvider.GetFileInfo("appsettings.json"); - - if (HostEnv.IsProduction() - && prodSettings.Exists && - !string.IsNullOrWhiteSpace(prodSettings.PhysicalPath)) - { - savePath = prodSettings.PhysicalPath; - } - else if ( - HostEnv.IsStaging() && - stagingSettings.Exists && - !string.IsNullOrWhiteSpace(stagingSettings.PhysicalPath)) - { - savePath = stagingSettings.PhysicalPath; - } - else if ( - HostEnv.IsDevelopment() && - devSettings.Exists && - !string.IsNullOrWhiteSpace(devSettings.PhysicalPath)) - { - savePath = devSettings.PhysicalPath; - } - else if (settings.Exists && !string.IsNullOrWhiteSpace(settings.PhysicalPath)) - { - savePath = settings.PhysicalPath; - } - else - { - return; - } - - var settingsJson = JsonSerializer.Deserialize>(await File.ReadAllTextAsync(savePath)); - if (settingsJson is null) - { - return; - } - settingsJson["ApplicationOptions"] = Input; - settingsJson["ConnectionStrings"] = ConnectionStrings; - - await File.WriteAllTextAsync(savePath, JsonSerializer.Serialize(settingsJson, new JsonSerializerOptions() { WriteIndented = true })); - - if (Configuration is IConfigurationRoot root) - { - root.Reload(); - } - } private void SetIsServerAdmin(ChangeEventArgs ev, RemotelyUser user) { if (ev.Value is not bool isAdmin) diff --git a/Server/Components/_Imports.razor b/Server/Components/_Imports.razor index 37615a39f..bbc5eac2e 100644 --- a/Server/Components/_Imports.razor +++ b/Server/Components/_Imports.razor @@ -24,4 +24,5 @@ @using Remotely.Server.Components.Scripts @using Remotely.Server.Components.TreeView @using Remotely.Server.Auth -@using Remotely.Shared.Entities \ No newline at end of file +@using Remotely.Shared.Entities +@using Remotely.Server.Models \ No newline at end of file diff --git a/Server/Data/AppDb.cs b/Server/Data/AppDb.cs index 866b182b8..93b60adfa 100644 --- a/Server/Data/AppDb.cs +++ b/Server/Data/AppDb.cs @@ -1,17 +1,12 @@ -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Microsoft.Extensions.Hosting; using Remotely.Server.Converters; using Remotely.Shared.Entities; using Remotely.Shared.Models; -using System; -using System.Collections.Generic; -using System.Linq; using System.Text.Json; namespace Remotely.Server.Data; @@ -37,6 +32,7 @@ public AppDb(IWebHostEnvironment hostEnvironment) public DbSet DeviceGroups { get; set; } public DbSet Devices { get; set; } public DbSet InviteLinks { get; set; } + public DbSet KeyValueRecords { get; set; } public DbSet Organizations { get; set; } public DbSet SavedScripts { get; set; } public DbSet ScriptResults { get; set; } @@ -45,7 +41,6 @@ public AppDb(IWebHostEnvironment hostEnvironment) public DbSet SharedFiles { get; set; } public new DbSet Users { get; set; } - protected override void OnConfiguring(DbContextOptionsBuilder options) { options.ConfigureWarnings(x => x.Ignore(RelationalEventId.MultipleCollectionIncludeWarning)); @@ -284,13 +279,13 @@ private static string[] DeserializeStringArray(string value, JsonSerializerOptio { if (string.IsNullOrEmpty(value)) { - return Array.Empty(); + return []; } - return JsonSerializer.Deserialize(value, jsonOptions) ?? Array.Empty(); + return JsonSerializer.Deserialize(value, jsonOptions) ?? []; } catch { - return Array.Empty(); + return []; } } diff --git a/Server/Data/AppDbFactory.cs b/Server/Data/AppDbFactory.cs index 372987769..9d036de44 100644 --- a/Server/Data/AppDbFactory.cs +++ b/Server/Data/AppDbFactory.cs @@ -10,31 +10,10 @@ public interface IAppDbFactory AppDb GetContext(); } -public class AppDbFactory : IAppDbFactory +public class AppDbFactory(IServiceProvider _services) : IAppDbFactory { - private readonly IApplicationConfig _appConfig; - private readonly IConfiguration _configuration; - private readonly IWebHostEnvironment _hostEnv; - - public AppDbFactory( - IApplicationConfig appConfig, - IConfiguration configuration, - IWebHostEnvironment hostEnv) - { - _appConfig = appConfig; - _configuration = configuration; - _hostEnv = hostEnv; - } - public AppDb GetContext() { - return _appConfig.DBProvider.ToLower() switch - { - "sqlite" => new SqliteDbContext(_configuration, _hostEnv), - "sqlserver" => new SqlServerDbContext(_configuration, _hostEnv), - "postgresql" => new PostgreSqlDbContext(_configuration, _hostEnv), - "inmemory" => new TestingDbContext(_hostEnv), - _ => throw new ArgumentException("Unknown DB provider."), - }; + return _services.GetRequiredService(); } } diff --git a/Server/Dockerfile b/Server/Dockerfile index d3f93b486..4c72e5dea 100644 --- a/Server/Dockerfile +++ b/Server/Dockerfile @@ -1,22 +1,38 @@ -FROM mcr.microsoft.com/dotnet/aspnet:8.0-jammy +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base -USER app - -SHELL ["/bin/bash", "-c"] +RUN apt -y update && apt -y install curl +RUN mkdir -p /app/AppData +RUN chown app:app -R /app/AppData +WORKDIR /app EXPOSE 5000 EXPOSE 5001 -COPY /_immense.Remotely/Server/linux-x64/Server /app -WORKDIR /app +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +COPY ["Directory.Build.props", "."] +COPY ["Server/Server.csproj", "Server/"] +COPY ["Shared/Shared.csproj", "Shared/"] +COPY ["submodules/Immense.RemoteControl/Immense.RemoteControl.Shared/Immense.RemoteControl.Shared.csproj", "submodules/Immense.RemoteControl/Immense.RemoteControl.Shared/"] +COPY ["submodules/Immense.RemoteControl/Immense.RemoteControl.Server/Immense.RemoteControl.Server.csproj", "submodules/Immense.RemoteControl/Immense.RemoteControl.Server/"] +RUN dotnet restore "./Server/./Server.csproj" +COPY . . +WORKDIR "/src/Server" + +RUN dotnet build "./Server.csproj" -c $BUILD_CONFIGURATION -o /app/build -RUN \ - apt-get -y update && \ - apt-get -y install curl && \ - mkdir -p /remotely-data && \ - sed -i 's/DataSource=Remotely.db/DataSource=\/app\/AppData\/Remotely.db/' /app/appsettings.json +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "./Server.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . + +USER app ENTRYPOINT ["dotnet", "Remotely_Server.dll"] HEALTHCHECK --interval=5m --timeout=3s \ diff --git a/Server/Dockerfile.local b/Server/Dockerfile.pipelines similarity index 77% rename from Server/Dockerfile.local rename to Server/Dockerfile.pipelines index 72287a035..f26befcc4 100644 --- a/Server/Dockerfile.local +++ b/Server/Dockerfile.pipelines @@ -7,15 +7,15 @@ SHELL ["/bin/bash", "-c"] EXPOSE 5000 EXPOSE 5001 -COPY ./bin/publish /app +COPY /_immense.Remotely/Server/linux-x64/Server /app WORKDIR /app RUN \ apt-get -y update && \ apt-get -y install curl && \ - mkdir -p /remotely-data && \ - sed -i 's/DataSource=Remotely.db/DataSource=\/app\/AppData\/Remotely.db/' ./appsettings.json + mkdir -p /app/AppData && \ + sed -i 's/DataSource=Remotely.db/DataSource=\/app\/AppData\/Remotely.db/' /app/appsettings.json ENTRYPOINT ["dotnet", "Remotely_Server.dll"] diff --git a/Server/Extensions/AppDbExtensions.cs b/Server/Extensions/AppDbExtensions.cs new file mode 100644 index 000000000..2ced10982 --- /dev/null +++ b/Server/Extensions/AppDbExtensions.cs @@ -0,0 +1,31 @@ +using Remotely.Server.Data; +using Remotely.Server.Models; +using System.Text.Json; + +namespace Remotely.Server.Extensions; + +public static class AppDbExtensions +{ + public static async Task GetAppSettings(this AppDb dbContext) + { + var record = await dbContext.KeyValueRecords.FindAsync(SettingsModel.DbKey); + if (record is null) + { + record = new() + { + Key = SettingsModel.DbKey, + }; + await dbContext.KeyValueRecords.AddAsync(record); + await dbContext.SaveChangesAsync(); + } + + if (string.IsNullOrWhiteSpace(record.Value)) + { + var settings = new SettingsModel(); + record.Value = JsonSerializer.Serialize(settings); + await dbContext.SaveChangesAsync(); + } + + return JsonSerializer.Deserialize(record.Value) ?? new(); + } +} diff --git a/Server/Hubs/AgentHub.cs b/Server/Hubs/AgentHub.cs index 32ee48604..25d862c26 100644 --- a/Server/Hubs/AgentHub.cs +++ b/Server/Hubs/AgentHub.cs @@ -23,9 +23,8 @@ namespace Remotely.Server.Hubs; public class AgentHub : Hub { - private readonly IApplicationConfig _appConfig; - private readonly ICircuitManager _circuitManager; private readonly IDataService _dataService; + private readonly ICircuitManager _circuitManager; private readonly IExpiringTokenService _expiringTokenService; private readonly ILogger _logger; private readonly IMessenger _messenger; @@ -33,8 +32,8 @@ public class AgentHub : Hub private readonly IAgentHubSessionCache _serviceSessionCache; private readonly IHubContext _viewerHubContext; - public AgentHub(IDataService dataService, - IApplicationConfig appConfig, + public AgentHub( + IDataService dataService, IAgentHubSessionCache serviceSessionCache, IHubContext viewerHubContext, ICircuitManager circuitManager, @@ -46,7 +45,6 @@ public AgentHub(IDataService dataService, _dataService = dataService; _serviceSessionCache = serviceSessionCache; _viewerHubContext = viewerHubContext; - _appConfig = appConfig; _circuitManager = circuitManager; _expiringTokenService = expiringTokenService; _remoteControlSessions = remoteControlSessionCache; @@ -288,9 +286,10 @@ public Task DownloadFileProgress(int progressPercent, string requesterId) return _messenger.Send(message, requesterId); } - public string GetServerUrl() + public async Task GetServerUrl() { - return _appConfig.ServerUrl; + var settings = await _dataService.GetSettings(); + return settings.ServerUrl; } public string GetServerVerificationToken() @@ -382,6 +381,7 @@ public Task TransferCompleted(string transferId, string requesterId) private async Task CheckForDeviceBan(params string[] deviceIdNameOrIPs) { + var settings = await _dataService.GetSettings(); foreach (var device in deviceIdNameOrIPs) { if (string.IsNullOrWhiteSpace(device)) @@ -389,7 +389,7 @@ private async Task CheckForDeviceBan(params string[] deviceIdNameOrIPs) continue; } - if (_appConfig.BannedDevices.Any(x => !string.IsNullOrWhiteSpace(x) && + if (settings.BannedDevices.Any(x => !string.IsNullOrWhiteSpace(x) && x.Equals(device, StringComparison.OrdinalIgnoreCase))) { _logger.LogWarning("Device ID/name/IP ({device}) is banned. Sending uninstall command.", device); diff --git a/Server/Hubs/CircuitConnection.cs b/Server/Hubs/CircuitConnection.cs index da55cb5be..6afd104d1 100644 --- a/Server/Hubs/CircuitConnection.cs +++ b/Server/Hubs/CircuitConnection.cs @@ -71,11 +71,10 @@ public interface ICircuitConnection public class CircuitConnection : CircuitHandler, ICircuitConnection { private readonly IHubContext _agentHubContext; - private readonly IApplicationConfig _appConfig; + private readonly IDataService _dataService; private readonly ISelectedCardsStore _cardStore; private readonly IAuthService _authService; private readonly ICircuitManager _circuitManager; - private readonly IDataService _dataService; private readonly IRemoteControlSessionCache _remoteControlSessionCache; private readonly IExpiringTokenService _expiringTokenService; private readonly ILogger _logger; @@ -89,7 +88,6 @@ public CircuitConnection( IDataService dataService, ISelectedCardsStore cardStore, IHubContext agentHubContext, - IApplicationConfig appConfig, ICircuitManager circuitManager, IToastService toastService, IExpiringTokenService expiringTokenService, @@ -101,7 +99,6 @@ public CircuitConnection( _dataService = dataService; _agentHubContext = agentHubContext; _cardStore = cardStore; - _appConfig = appConfig; _authService = authService; _circuitManager = circuitManager; _toastService = toastService; @@ -228,6 +225,8 @@ public async Task ReinstallAgents(string[] deviceIDs) public async Task> RemoteControl(string deviceId, bool viewOnly) { + var settings = await _dataService.GetSettings(); + if (!_agentSessionCache.TryGetByDeviceId(deviceId, out var targetDevice)) { var message = new DisplayNotificationMessage( @@ -256,7 +255,7 @@ public async Task> RemoteControl(string deviceId, .OfType() .Count(x => x.OrganizationId == User.OrganizationID); - if (sessionCount >= _appConfig.RemoteControlSessionLimit) + if (sessionCount >= settings.RemoteControlSessionLimit) { var message = new DisplayNotificationMessage( "There are already the maximum amount of active remote control sessions for your organization.", @@ -290,8 +289,8 @@ public async Task> RemoteControl(string deviceId, DeviceId = deviceId, ViewOnly = viewOnly, OrganizationId = User.OrganizationID, - RequireConsent = _appConfig.EnforceAttendedAccess, - NotifyUserOnStart = _appConfig.RemoteControlNotifyUser + RequireConsent = settings.EnforceAttendedAccess, + NotifyUserOnStart = settings.RemoteControlNotifyUser }; _remoteControlSessionCache.AddOrUpdate($"{sessionId}", session); diff --git a/Server/Models/SettingsModel.cs b/Server/Models/SettingsModel.cs new file mode 100644 index 000000000..78546dbae --- /dev/null +++ b/Server/Models/SettingsModel.cs @@ -0,0 +1,39 @@ +using Remotely.Shared.Enums; + +namespace Remotely.Server.Models; + +public class SettingsModel +{ + public static Guid DbKey { get; } = Guid.Parse("a35d6212-c0b7-49b2-89e1-7ba497f94a35"); + + public bool AllowApiLogin { get; set; } + public List BannedDevices { get; set; } = []; + public double DataRetentionInDays { get; set; } = 90; + public string DbProvider { get; set; } = "SQLite"; + public bool EnableRemoteControlRecording { get; set; } + public bool EnableWindowsEventLog { get; set; } + public bool EnforceAttendedAccess { get; set; } + public bool ForceClientHttps { get; set; } + public List KnownProxies { get; set; } = []; + public int MaxConcurrentUpdates { get; set; } = 10; + public int MaxOrganizationCount { get; set; } = 1; + public string MessageOfTheDay { get; set; } = string.Empty; + 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; + public string SmtpDisplayName { get; set; } = string.Empty; + public string SmtpEmail { get; set; } = string.Empty; + public string SmtpHost { get; set; } = string.Empty; + public string SmtpLocalDomain { get; set; } = string.Empty; + public string SmtpPassword { get; set; } = string.Empty; + public int SmtpPort { get; set; } = 587; + public string SmtpUserName { get; set; } = string.Empty; + public Theme Theme { get; set; } = Theme.Dark; + public List TrustedCorsOrigins { get; set; } = []; + public bool UseHsts { get; set; } + public bool UseHttpLogging { get; set; } +} diff --git a/Server/Options/ApplicationOptions.cs b/Server/Options/ApplicationOptions.cs new file mode 100644 index 000000000..1a11cc3b8 --- /dev/null +++ b/Server/Options/ApplicationOptions.cs @@ -0,0 +1,7 @@ +namespace Remotely.Server.Options; + +public class ApplicationOptions +{ + public const string SectionKey = "ApplicationOptions"; + public string DbProvider { get; set; } = "SQLite"; +} diff --git a/Server/Program.cs b/Server/Program.cs index 239139837..3627d352c 100644 --- a/Server/Program.cs +++ b/Server/Program.cs @@ -23,6 +23,9 @@ using Remotely.Server.Services.Stores; using Remotely.Server.Components.Account; using Remotely.Server.Components; +using Remotely.Server.Options; +using Remotely.Server.Extensions; +using Remotely.Server.Models; var builder = WebApplication.CreateBuilder(args); var configuration = builder.Configuration; @@ -30,6 +33,9 @@ configuration.AddEnvironmentVariables("Remotely_"); +services.Configure( + configuration.GetSection(ApplicationOptions.SectionKey)); + services .AddRazorComponents() .AddInteractiveServerComponents(); @@ -41,21 +47,29 @@ services.AddScoped(); services.AddScoped(); -ConfigureSerilog(builder); - -builder.Logging.AddConfiguration(builder.Configuration.GetSection("Logging")); - -if (OperatingSystem.IsWindows() && - bool.TryParse(builder.Configuration["ApplicationOptions:EnableWindowsEventLog"], out var enableEventLog) && - enableEventLog) -{ - builder.Logging.AddEventLog(); -} +var dbProvider = configuration["ApplicationOptions:DbProvider"]?.ToLower(); -var dbProvider = configuration["ApplicationOptions:DBProvider"]?.ToLower(); -if (string.IsNullOrWhiteSpace(dbProvider)) +switch (dbProvider) { - throw new InvalidOperationException("DBProvider is missing from appsettings.json."); + case "sqlite": + services.AddDbContext( + contextLifetime: ServiceLifetime.Transient, + optionsLifetime: ServiceLifetime.Transient); + break; + case "sqlserver": + services.AddDbContext( + contextLifetime: ServiceLifetime.Transient, + optionsLifetime: ServiceLifetime.Transient); + break; + case "postgresql": + services.AddDbContext( + contextLifetime: ServiceLifetime.Transient, + optionsLifetime: ServiceLifetime.Transient); + break; + default: + throw new InvalidOperationException( + $"Invalid DBProvider: {dbProvider}. Ensure a valid value " + + $"is set in appsettings.json or environment variables."); } if (dbProvider == "sqlite") @@ -71,6 +85,26 @@ services.AddDbContext(); } +using AppDb appDb = dbProvider switch +{ + "sqlite" => new SqliteDbContext(builder.Configuration, builder.Environment), + "sqlserver" => new SqlServerDbContext(builder.Configuration, builder.Environment), + "postgresql" => new PostgreSqlDbContext(builder.Configuration, builder.Environment), + _ => throw new InvalidOperationException($"Invalid DBProvider: {dbProvider}") +}; + +await appDb.Database.MigrateAsync(); +var settings = await appDb.GetAppSettings(); + +ConfigureSerilog(builder, settings); + +builder.Logging.AddConfiguration(builder.Configuration.GetSection("Logging")); + +if (OperatingSystem.IsWindows() && settings.EnableWindowsEventLog) +{ + builder.Logging.AddEventLog(); +} + builder.Services.AddAuthentication(options => { options.DefaultScheme = IdentityConstants.ApplicationScheme; @@ -113,8 +147,7 @@ services.AddDatabaseDeveloperPageExceptionFilter(); -if (bool.TryParse(configuration["ApplicationOptions:UseHttpLogging"], out var useHttpLogging) && - useHttpLogging) +if (settings.UseHttpLogging) { services.AddHttpLogging(options => { @@ -128,14 +161,12 @@ }); } -var trustedOrigins = configuration.GetSection("ApplicationOptions:TrustedCorsOrigins").Get(); - services.AddCors(options => { - if (trustedOrigins != null) + if (settings.TrustedCorsOrigins is { Count: > 0} trustedOrigins) { options.AddPolicy("TrustedOriginPolicy", builder => builder - .WithOrigins(trustedOrigins) + .WithOrigins(trustedOrigins.ToArray()) .AllowAnyHeader() .AllowAnyMethod() .AllowCredentials() @@ -144,7 +175,6 @@ }); -var knownProxies = configuration.GetSection("ApplicationOptions:KnownProxies").Get(); services.Configure(options => { options.ForwardedHeaders = ForwardedHeaders.All; @@ -153,7 +183,7 @@ // Default Docker host. We want to allow forwarded headers from this address. options.KnownProxies.Add(IPAddress.Parse("172.17.0.1")); - if (knownProxies?.Any() == true) + if (settings.KnownProxies is { Count: >0 } knownProxies) { foreach (var proxy in knownProxies) { @@ -180,13 +210,10 @@ { clOptions.QueueLimit = int.MaxValue; - var concurrentPermits = configuration.GetSection("ApplicationOptions:MaxConcurrentUpdates").Get(); - if (concurrentPermits <= 0) - { - concurrentPermits = 10; - } - - clOptions.PermitLimit = concurrentPermits; + clOptions.PermitLimit = + settings.MaxConcurrentUpdates <= 0 ? + 10 : + settings.MaxConcurrentUpdates; }); }); services.AddHttpClient(); @@ -202,7 +229,6 @@ } services.AddScoped(); services.AddTransient(); -services.AddSingleton(); services.AddScoped(); services.AddScoped(); services.AddScoped(); @@ -246,9 +272,7 @@ app.UseRateLimiter(); -var appConfig = app.Services.GetRequiredService(); - -if (appConfig.UseHttpLogging) +if (settings.UseHttpLogging) { app.UseHttpLogging(); } @@ -265,11 +289,11 @@ else { app.UseExceptionHandler("/Error"); - if (bool.TryParse(app.Configuration["ApplicationOptions:UseHsts"], out var hsts) && hsts) + if (settings.UseHsts) { app.UseHsts(); } - if (bool.TryParse(app.Configuration["ApplicationOptions:RedirectToHttps"], out var redirect) && redirect) + if (settings.RedirectToHttps) { app.UseHttpsRedirection(); } @@ -297,14 +321,8 @@ using (var scope = app.Services.CreateScope()) { - using var context = scope.ServiceProvider.GetRequiredService(); var dataService = scope.ServiceProvider.GetRequiredService(); - if (context.Database.IsRelational()) - { - await context.Database.MigrateAsync(); - } - await dataService.SetAllDevicesNotOnline(); await dataService.CleanupOldRecords(); } @@ -348,16 +366,17 @@ void ConfigureStaticFiles() } } -void ConfigureSerilog(WebApplicationBuilder webAppBuilder) +void ConfigureSerilog(WebApplicationBuilder webAppBuilder, SettingsModel settings) { try { - var dataRetentionDays = 7; - if (int.TryParse(webAppBuilder.Configuration["ApplicationOptions:DataRetentionInDays"], out var retentionSetting)) + + var dataRetentionDays = settings.DataRetentionInDays; + if (dataRetentionDays <= 0) { - dataRetentionDays = retentionSetting; + dataRetentionDays = 7; } - + var logPath = LogsManager.DefaultLogsDirectory; void ApplySharedLoggerConfig(LoggerConfiguration loggerConfiguration) @@ -366,8 +385,8 @@ void ApplySharedLoggerConfig(LoggerConfiguration loggerConfiguration) .Enrich.FromLogContext() .Enrich.WithThreadId() .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} {Properties}{NewLine}{Exception}") - .WriteTo.File($"{logPath}/Remotely_Server.log", - rollingInterval: RollingInterval.Day, + .WriteTo.File($"{logPath}/Remotely_Server.log", + rollingInterval: RollingInterval.Day, retainedFileTimeLimit: TimeSpan.FromDays(dataRetentionDays), outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj} {Properties}{NewLine}{Exception}", shared: true); diff --git a/Server/Services/ApplicationConfig.cs b/Server/Services/ApplicationConfig.cs deleted file mode 100644 index 9fd583882..000000000 --- a/Server/Services/ApplicationConfig.cs +++ /dev/null @@ -1,81 +0,0 @@ -using Microsoft.Extensions.Configuration; -using Remotely.Shared.Enums; -using Remotely.Shared.Models; -using System; - -namespace Remotely.Server.Services; - -public interface IApplicationConfig -{ - bool AllowApiLogin { get; } - string[] BannedDevices { get; } - double DataRetentionInDays { get; } - string DBProvider { get; } - bool EnableRemoteControlRecording { get; } - bool EnableWindowsEventLog { get; } - bool EnforceAttendedAccess { get; } - bool ForceClientHttps { get; } - string[] KnownProxies { get; } - int MaxConcurrentUpdates { get; } - int MaxOrganizationCount { get; } - string MessageOfTheDay { get; } - bool RedirectToHttps { get; } - bool RemoteControlNotifyUser { get; } - bool RemoteControlRequiresAuthentication { get; } - int RemoteControlSessionLimit { get; } - bool Require2FA { get; } - string ServerUrl { get; } - bool SmtpCheckCertificateRevocation { get; } - string SmtpDisplayName { get; } - string SmtpEmail { get; } - string SmtpHost { get; } - string SmtpLocalDomain { get; } - string SmtpPassword { get; } - int SmtpPort { get; } - string SmtpUserName { get; } - Theme Theme { get; } - string[] TrustedCorsOrigins { get; } - bool UseHsts { get; } - bool UseHttpLogging { get; } -} - -public class ApplicationConfig : IApplicationConfig -{ - private readonly IConfiguration _config; - - public ApplicationConfig(IConfiguration config) - { - _config = config; - } - - public bool AllowApiLogin => bool.TryParse(_config["ApplicationOptions:AllowApiLogin"], out var result) && result; - public string[] BannedDevices => _config.GetSection("ApplicationOptions:BannedDevices").Get() ?? System.Array.Empty(); - public double DataRetentionInDays => double.TryParse(_config["ApplicationOptions:DataRetentionInDays"], out var result) ? result : 30; - public string DBProvider => _config["ApplicationOptions:DBProvider"] ?? "SQLite"; - public bool EnableRemoteControlRecording => bool.TryParse(_config["ApplicationOptions:EnableRemoteControlRecording"], out var result) && result; - public bool EnableWindowsEventLog => bool.TryParse(_config["ApplicationOptions:EnableWindowsEventLog"], out var result) && result; - public bool EnforceAttendedAccess => bool.TryParse(_config["ApplicationOptions:EnforceAttendedAccess"], out var result) && result; - public bool ForceClientHttps => bool.TryParse(_config["ApplicationOptions:ForceClientHttps"], out var result) && result; - public string[] KnownProxies => _config.GetSection("ApplicationOptions:KnownProxies").Get() ?? System.Array.Empty(); - public int MaxConcurrentUpdates => int.TryParse(_config["ApplicationOptions:MaxConcurrentUpdates"], out var result) ? result : 10; - public int MaxOrganizationCount => int.TryParse(_config["ApplicationOptions:MaxOrganizationCount"], out var result) ? result : 1; - public string MessageOfTheDay => _config["ApplicationOptions:MessageOfTheDay"] ?? string.Empty; - public bool RedirectToHttps => bool.TryParse(_config["ApplicationOptions:RedirectToHttps"], out var result) && result; - public bool RemoteControlNotifyUser => bool.TryParse(_config["ApplicationOptions:RemoteControlNotifyUser"], out var result) && result; - public bool RemoteControlRequiresAuthentication => bool.TryParse(_config["ApplicationOptions:RemoteControlRequiresAuthentication"], out var result) && result; - public int RemoteControlSessionLimit => int.TryParse(_config["ApplicationOptions:RemoteControlSessionLimit"], out var result) ? result : 3; - public bool Require2FA => bool.TryParse(_config["ApplicationOptions:Require2FA"], out var result) && result; - public string ServerUrl => _config["ApplicationOptions:ServerUrl"] ?? string.Empty; - public bool SmtpCheckCertificateRevocation => !bool.TryParse(_config["ApplicationOptions:SmtpCheckCertificateRevocation"], out var result) || result; - public string SmtpDisplayName => _config["ApplicationOptions:SmtpDisplayName"] ?? string.Empty; - public string SmtpEmail => _config["ApplicationOptions:SmtpEmail"] ?? string.Empty; - public string SmtpHost => _config["ApplicationOptions:SmtpHost"] ?? string.Empty; - public string SmtpLocalDomain => _config["ApplicationOptions:SmtpLocalDomain"] ?? string.Empty; - public string SmtpPassword => _config["ApplicationOptions:SmtpPassword"] ?? string.Empty; - public int SmtpPort => int.TryParse(_config["ApplicationOptions:SmtpPort"], out var result) ? result : 25; - public string SmtpUserName => _config["ApplicationOptions:SmtpUserName"] ?? string.Empty; - public Theme Theme => Enum.TryParse(_config["ApplicationOptions:Theme"], out var result) ? result : Theme.Dark; - public string[] TrustedCorsOrigins => _config.GetSection("ApplicationOptions:TrustedCorsOrigins").Get() ?? System.Array.Empty(); - public bool UseHsts => bool.TryParse(_config["ApplicationOptions:UseHsts"], out var result) && result; - public bool UseHttpLogging => bool.TryParse(_config["ApplicationOptions:UseHttpLogging"], out var result) && result; -} diff --git a/Server/Services/DataCleanupService.cs b/Server/Services/DataCleanupService.cs index 6ef1cf658..47a5bc41b 100644 --- a/Server/Services/DataCleanupService.cs +++ b/Server/Services/DataCleanupService.cs @@ -19,17 +19,17 @@ public class DataCleanupService : BackgroundService, IDisposable private readonly ILogger _logger; private readonly IServiceScopeFactory _scopeFactory; private readonly ISystemTime _systemTime; - private readonly IApplicationConfig _appConfig; + private readonly IDataService _dataService; public DataCleanupService( IServiceScopeFactory scopeFactory, ISystemTime systemTime, - IApplicationConfig appConfig, + IDataService dataService, ILogger logger) { _scopeFactory = scopeFactory; _systemTime = systemTime; - _appConfig = appConfig; + _dataService = dataService; _logger = logger; } @@ -76,14 +76,18 @@ private async Task RemoveExpiredDbRecords() await dataService.CleanupOldRecords(); } - private Task RemoveExpiredRecordings() + private async Task RemoveExpiredRecordings() { + using var scope = _scopeFactory.CreateScope(); + var dataService = scope.ServiceProvider.GetRequiredService(); + var settings = await dataService.GetSettings(); + if (!Directory.Exists(SessionRecordingSink.RecordingsDirectory)) { - return Task.CompletedTask; + return; } - var expirationDate = _systemTime.Now.UtcDateTime - TimeSpan.FromDays(_appConfig.DataRetentionInDays); + var expirationDate = _systemTime.Now.UtcDateTime - TimeSpan.FromDays(settings.DataRetentionInDays); var files = Directory .GetFiles( @@ -106,7 +110,5 @@ private Task RemoveExpiredRecordings() _logger.LogError(ex, "Error while deleting expired recording: {file}", file); } } - - return Task.CompletedTask; } } diff --git a/Server/Services/DataService.cs b/Server/Services/DataService.cs index 1ad7d7c7d..9afc921a5 100644 --- a/Server/Services/DataService.cs +++ b/Server/Services/DataService.cs @@ -21,6 +21,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.Json; using System.Threading.Tasks; namespace Remotely.Server.Services; @@ -171,6 +172,8 @@ Task> GetDeviceGroup( List GetServerAdmins(); + Task GetSettings(); + Task> GetSharedFiled(string fileId); int GetTotalDevices(); @@ -193,6 +196,8 @@ Task> GetUserByName( Task ResetBranding(string organizationId); + Task SaveSettings(SettingsModel settings); + Task SetAllDevicesNotOnline(); Task SetDisplayName(RemotelyUser user, string displayName); @@ -224,18 +229,16 @@ Task UpdateBrandingInfo( public class DataService : IDataService { - private readonly IApplicationConfig _appConfig; private readonly IAppDbFactory _appDbFactory; private readonly IHostEnvironment _hostEnvironment; private readonly ILogger _logger; + private readonly SemaphoreSlim _settingsLock = new(1, 1); public DataService( - IApplicationConfig appConfig, IHostEnvironment hostEnvironment, IAppDbFactory appDbFactory, ILogger logger) { - _appConfig = appConfig; _hostEnvironment = hostEnvironment; _appDbFactory = appDbFactory; _logger = logger; @@ -639,14 +642,15 @@ public async Task ChangeUserIsAdmin(string organizationId, string targetUserId, public async Task CleanupOldRecords() { + var settings = await GetSettings(); using var dbContext = _appDbFactory.GetContext(); - if (_appConfig.DataRetentionInDays < 0) + if (settings.DataRetentionInDays < 0) { return; } - var expirationDate = DateTimeOffset.Now - TimeSpan.FromDays(_appConfig.DataRetentionInDays); + var expirationDate = DateTimeOffset.Now - TimeSpan.FromDays(settings.DataRetentionInDays); var scriptRuns = await dbContext.ScriptRuns .Include(x => x.Results) @@ -1711,6 +1715,25 @@ public List GetServerAdmins() .ToList(); } + public async Task GetSettings() + { + await _settingsLock.WaitAsync(); + try + { + using var dbContext = _appDbFactory.GetContext(); + return await dbContext.GetAppSettings(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error while getting settings from database."); + return new(); + } + finally + { + _settingsLock.Release(); + } + } + public async Task> GetSharedFiled(string fileId) { using var dbContext = _appDbFactory.GetContext(); @@ -1930,6 +1953,35 @@ public async Task ResetBranding(string organizationId) await dbContext.SaveChangesAsync(); } + public async Task SaveSettings(SettingsModel settings) + { + await _settingsLock.WaitAsync(); + try + { + using var dbContext = _appDbFactory.GetContext(); + var record = await dbContext.KeyValueRecords.FindAsync(SettingsModel.DbKey); + if (record is null) + { + record = new() + { + Key = SettingsModel.DbKey, + }; + await dbContext.KeyValueRecords.AddAsync(record); + await dbContext.SaveChangesAsync(); + } + record.Value = JsonSerializer.Serialize(settings); + await dbContext.SaveChangesAsync(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error while saving settings to database."); + } + finally + { + _settingsLock.Release(); + } + } + public async Task SetAllDevicesNotOnline() { using var dbContext = _appDbFactory.GetContext(); @@ -2188,14 +2240,15 @@ public async Task ValidateApiKey(string keyId, string apiSecret, string re } private async Task AddSharedFileImpl( - string fileName, + string fileName, byte[] fileContents, string contentType, string organizationId) { + var settings = await GetSettings(); using var dbContext = _appDbFactory.GetContext(); - var expirationDate = DateTimeOffset.Now.AddDays(-_appConfig.DataRetentionInDays); + var expirationDate = DateTimeOffset.Now.AddDays(-settings.DataRetentionInDays); var expiredFiles = dbContext.SharedFiles.Where(x => x.Timestamp < expirationDate); dbContext.RemoveRange(expiredFiles); diff --git a/Server/Services/EmailSender.cs b/Server/Services/EmailSender.cs index c2a5a1662..55d9d1b26 100644 --- a/Server/Services/EmailSender.cs +++ b/Server/Services/EmailSender.cs @@ -6,6 +6,7 @@ using Microsoft.Identity.Client; using MimeKit; using MimeKit.Text; +using NuGet.Configuration; using System; using System.Net; using System.Threading.Tasks; @@ -35,14 +36,14 @@ public Task SendEmailAsync(string email, string subject, string htmlMessage) public class EmailSenderEx : IEmailSenderEx { - private readonly IApplicationConfig _appConfig; + private readonly IDataService _dataService; private readonly ILogger _logger; public EmailSenderEx( - IApplicationConfig appConfig, + IDataService dataService, ILogger logger) { - _appConfig = appConfig; + _dataService = dataService; _logger = logger; } public async Task SendEmailAsync( @@ -54,8 +55,10 @@ public async Task SendEmailAsync( { try { + var settings = await _dataService.GetSettings(); + var message = new MimeMessage(); - message.From.Add(new MailboxAddress(_appConfig.SmtpDisplayName, _appConfig.SmtpEmail)); + message.From.Add(new MailboxAddress(settings.SmtpDisplayName, settings.SmtpEmail)); message.To.Add(MailboxAddress.Parse(toEmail)); message.ReplyTo.Add(MailboxAddress.Parse(replyTo)); message.Subject = subject; @@ -66,19 +69,19 @@ public async Task SendEmailAsync( using var client = new SmtpClient(); - if (!string.IsNullOrWhiteSpace(_appConfig.SmtpLocalDomain)) + if (!string.IsNullOrWhiteSpace(settings.SmtpLocalDomain)) { - client.LocalDomain = _appConfig.SmtpLocalDomain; + client.LocalDomain = settings.SmtpLocalDomain; } - client.CheckCertificateRevocation = _appConfig.SmtpCheckCertificateRevocation; + client.CheckCertificateRevocation = settings.SmtpCheckCertificateRevocation; - await client.ConnectAsync(_appConfig.SmtpHost, _appConfig.SmtpPort); + await client.ConnectAsync(settings.SmtpHost, settings.SmtpPort); - if (!string.IsNullOrWhiteSpace(_appConfig.SmtpUserName) && - !string.IsNullOrWhiteSpace(_appConfig.SmtpPassword)) + if (!string.IsNullOrWhiteSpace(settings.SmtpUserName) && + !string.IsNullOrWhiteSpace(settings.SmtpPassword)) { - await client.AuthenticateAsync(_appConfig.SmtpUserName, _appConfig.SmtpPassword); + await client.AuthenticateAsync(settings.SmtpUserName, settings.SmtpPassword); } await client.SendAsync(message); @@ -95,9 +98,10 @@ public async Task SendEmailAsync( } } - public Task SendEmailAsync(string email, string subject, string htmlMessage, string? organizationID = null) + public async Task SendEmailAsync(string email, string subject, string htmlMessage, string? organizationID = null) { - return SendEmailAsync(email, _appConfig.SmtpEmail, subject, htmlMessage, organizationID); + var settings = await _dataService.GetSettings(); + return await SendEmailAsync(email, settings.SmtpEmail, subject, htmlMessage, organizationID); } } public class EmailSenderFake(ILogger _logger) : IEmailSenderEx diff --git a/Server/Services/RcImplementations/ViewerAuthorizer.cs b/Server/Services/RcImplementations/ViewerAuthorizer.cs index e4efc6124..5f664e2ef 100644 --- a/Server/Services/RcImplementations/ViewerAuthorizer.cs +++ b/Server/Services/RcImplementations/ViewerAuthorizer.cs @@ -10,35 +10,36 @@ namespace Remotely.Server.Services.RcImplementations; public class ViewerAuthorizer : IViewerAuthorizer { - private readonly IApplicationConfig _appConfig; + private readonly IDataService _dataService; private readonly IOtpProvider _otpProvider; - public ViewerAuthorizer(IApplicationConfig appConfig, IOtpProvider otpProvider) + public ViewerAuthorizer(IDataService dataService, IOtpProvider otpProvider) { - _appConfig = appConfig; + _dataService = dataService; _otpProvider = otpProvider; } public string UnauthorizedRedirectUrl { get; } = "/Account/Login"; - public Task IsAuthorized(AuthorizationFilterContext context) + public async Task IsAuthorized(AuthorizationFilterContext context) { - if (!_appConfig.RemoteControlRequiresAuthentication) + var settings = await _dataService.GetSettings(); + if (!settings.RemoteControlRequiresAuthentication) { - return Task.FromResult(true); + return true; } if (context.HttpContext.User.Identity?.IsAuthenticated == true) { - return Task.FromResult(true); + return true; } if (context.HttpContext.Request.Query.TryGetValue("otp", out var otp) && _otpProvider.Exists($"{otp}")) { - return Task.FromResult(true); + return true; } - return Task.FromResult(false); + return false; } } diff --git a/Server/Services/RcImplementations/ViewerOptionsProvider.cs b/Server/Services/RcImplementations/ViewerOptionsProvider.cs index ac78424ab..3e7e35d99 100644 --- a/Server/Services/RcImplementations/ViewerOptionsProvider.cs +++ b/Server/Services/RcImplementations/ViewerOptionsProvider.cs @@ -8,18 +8,19 @@ namespace Remotely.Server.Services.RcImplementations; public class ViewerOptionsProvider : IViewerOptionsProvider { - private readonly IApplicationConfig _appConfig; + private readonly IDataService _dataService; - public ViewerOptionsProvider(IApplicationConfig appConfig) + public ViewerOptionsProvider(IDataService dataService) { - _appConfig = appConfig; + _dataService = dataService; } - public Task GetViewerOptions() + public async Task GetViewerOptions() { + var settings = await _dataService.GetSettings(); var options = new RemoteControlViewerOptions() { - ShouldRecordSession = _appConfig.EnableRemoteControlRecording + ShouldRecordSession = settings.EnableRemoteControlRecording }; - return Task.FromResult(options); + return options; } } diff --git a/Server/Services/RcImplementations/ViewerPageDataProvider.cs b/Server/Services/RcImplementations/ViewerPageDataProvider.cs index 31df87996..7368b1925 100644 --- a/Server/Services/RcImplementations/ViewerPageDataProvider.cs +++ b/Server/Services/RcImplementations/ViewerPageDataProvider.cs @@ -12,12 +12,10 @@ namespace Remotely.Server.Services.RcImplementations; public class ViewerPageDataProvider : IViewerPageDataProvider { - private readonly IApplicationConfig _appConfig; private readonly IDataService _dataService; - public ViewerPageDataProvider(IDataService dataService, IApplicationConfig appConfig) + public ViewerPageDataProvider(IDataService dataService) { _dataService = dataService; - _appConfig = appConfig; } public Task GetFaviconUrl(PageModel viewerModel) diff --git a/Server/Services/ThemeProvider.cs b/Server/Services/ThemeProvider.cs index 3e9106197..c58c2df55 100644 --- a/Server/Services/ThemeProvider.cs +++ b/Server/Services/ThemeProvider.cs @@ -12,25 +12,26 @@ public interface IThemeProvider public class ThemeProvider : IThemeProvider { private readonly IAuthService _authService; - private readonly IApplicationConfig _appConfig; + private readonly IDataService _dataService; - public ThemeProvider(IAuthService authService, IApplicationConfig appConfig) + public ThemeProvider(IAuthService authService, IDataService dataService) { _authService = authService; - _appConfig = appConfig; + _dataService = dataService; } public async Task GetEffectiveTheme() { + var settings = await _dataService.GetSettings(); if (await _authService.IsAuthenticated()) { var userResult = await _authService.GetUser(); if (userResult.IsSuccess) { - return userResult.Value.UserOptions?.Theme ?? _appConfig.Theme; + return userResult.Value.UserOptions?.Theme ?? settings.Theme; } } - return _appConfig.Theme; + return settings.Theme; } } diff --git a/Server/appsettings.json b/Server/appsettings.json index da6d238e5..97d0af750 100644 --- a/Server/appsettings.json +++ b/Server/appsettings.json @@ -20,34 +20,6 @@ } }, "ApplicationOptions": { - "AllowApiLogin": false, - "BannedDevices": [], - "DataRetentionInDays": 90, - "DBProvider": "SQLite", - "EnableRemoteControlRecording": false, - "EnableWindowsEventLog": false, - "EnforceAttendedAccess": false, - "ForceClientHttps": false, - "KnownProxies": [], - "MaxConcurrentUpdates": 10, - "MaxOrganizationCount": 1, - "MessageOfTheDay": "", - "RedirectToHttps": true, - "RemoteControlNotifyUser": true, - "RemoteControlRequiresAuthentication": true, - "RemoteControlSessionLimit": 3, - "Require2FA": false, - "SmtpDisplayName": "", - "SmtpEmail": "", - "SmtpHost": "", - "SmtpLocalDomain": "", - "SmtpCheckCertificateRevocation": true, - "SmtpPassword": "", - "SmtpPort": 587, - "SmtpUserName": "", - "Theme": "Dark", - "TrustedCorsOrigins": [], - "UseHsts": false, - "UseHttpLogging": false + "DBProvider": "SQLite" } -} +} \ No newline at end of file diff --git a/Shared/Entities/KeyValueRecord.cs b/Shared/Entities/KeyValueRecord.cs new file mode 100644 index 000000000..8c261f10a --- /dev/null +++ b/Shared/Entities/KeyValueRecord.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Remotely.Shared.Entities; + +public class KeyValueRecord +{ + [Key] + public Guid Key { get; set; } + public string? Value { get; set; } +} diff --git a/Tests/Server.Tests/AgentHubTests.cs b/Tests/Server.Tests/AgentHubTests.cs index b1646af7f..772001a44 100644 --- a/Tests/Server.Tests/AgentHubTests.cs +++ b/Tests/Server.Tests/AgentHubTests.cs @@ -8,6 +8,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; using Remotely.Server.Hubs; +using Remotely.Server.Models; using Remotely.Server.Services; using Remotely.Shared.Extensions; using Remotely.Shared.Interfaces; @@ -32,7 +33,6 @@ public async Task DeviceCameOnline_BannedByName() var circuitConnection = new Mock(); circuitManager.Setup(x => x.Connections).Returns(new[] { circuitConnection.Object }); circuitConnection.Setup(x => x.User).Returns(_testData.Org1Admin1); - var appConfig = new Mock(); var viewerHub = new Mock>(); var expiringTokenService = new Mock(); var serviceSessionCache = new Mock(); @@ -40,11 +40,12 @@ public async Task DeviceCameOnline_BannedByName() var messenger = new Mock(); var logger = new Mock>(); - appConfig.Setup(x => x.BannedDevices).Returns(new string[] { $"{_testData.Org1Device1.DeviceName}" }); + var settings = await _dataService.GetSettings(); + settings.BannedDevices = [_testData.Org1Device1.DeviceName!]; + await _dataService.SaveSettings(settings); var hub = new AgentHub( _dataService, - appConfig.Object, serviceSessionCache.Object, viewerHub.Object, circuitManager.Object, @@ -58,7 +59,8 @@ public async Task DeviceCameOnline_BannedByName() hubClients.Setup(x => x.Caller).Returns(caller.Object); hub.Clients = hubClients.Object; - Assert.IsFalse(await hub.DeviceCameOnline(_testData.Org1Device1.ToDto())); + var result = await hub.DeviceCameOnline(_testData.Org1Device1.ToDto()); + Assert.IsFalse(result); hubClients.Verify(x => x.Caller, Times.Once); caller.Verify(x => x.UninstallAgent(), Times.Once); } @@ -73,7 +75,6 @@ public async Task DeviceCameOnline_BannedById() var circuitConnection = new Mock(); circuitManager.Setup(x => x.Connections).Returns(new[] { circuitConnection.Object }); circuitConnection.Setup(x => x.User).Returns(_testData.Org1Admin1); - var appConfig = new Mock(); var viewerHub = new Mock>(); var expiringTokenService = new Mock(); var serviceSessionCache = new Mock(); @@ -81,11 +82,13 @@ public async Task DeviceCameOnline_BannedById() var messenger = new Mock(); var logger = new Mock>(); - appConfig.Setup(x => x.BannedDevices).Returns(new string[] { _testData.Org1Device1.ID }); + + var settings = await _dataService.GetSettings(); + settings.BannedDevices = [$"{_testData.Org1Device1.ID}"]; + await _dataService.SaveSettings(settings); var hub = new AgentHub( _dataService, - appConfig.Object, serviceSessionCache.Object, viewerHub.Object, circuitManager.Object, @@ -99,7 +102,8 @@ public async Task DeviceCameOnline_BannedById() hubClients.Setup(x => x.Caller).Returns(caller.Object); hub.Clients = hubClients.Object; - Assert.IsFalse(await hub.DeviceCameOnline(_testData.Org1Device1.ToDto())); + var result = await hub.DeviceCameOnline(_testData.Org1Device1.ToDto()); + Assert.IsFalse(result); hubClients.Verify(x => x.Caller, Times.Once); caller.Verify(x => x.UninstallAgent(), Times.Once); } @@ -117,23 +121,4 @@ public async Task TestInit() await _testData.Init(); _dataService = IoCActivator.ServiceProvider.GetRequiredService(); } - - private class CallerContext : HubCallerContext - { - public override string ConnectionId => "test-id"; - - public override string? UserIdentifier => null; - public override ClaimsPrincipal? User => null; - - public override IDictionary Items { get; } = new Dictionary(); - - public override IFeatureCollection Features { get; } = new FeatureCollection(); - - public override CancellationToken ConnectionAborted => CancellationToken.None; - - public override void Abort() - { - - } - } } diff --git a/Tests/Server.Tests/CircuitConnectionTests.cs b/Tests/Server.Tests/CircuitConnectionTests.cs index a462a3a15..fdd6d6951 100644 --- a/Tests/Server.Tests/CircuitConnectionTests.cs +++ b/Tests/Server.Tests/CircuitConnectionTests.cs @@ -24,7 +24,6 @@ public class CircuitConnectionTests private Mock _authService; private Mock _clientAppState; private HubContextFixture _agentHubContextFixture; - private Mock _appConfig; private Mock _circuitManager; private Mock _toastService; private Mock _expiringTokenService; @@ -45,7 +44,6 @@ public async Task Init() _authService = new Mock(); _clientAppState = new Mock(); _agentHubContextFixture = new HubContextFixture(); - _appConfig = new Mock(); _circuitManager = new Mock(); _toastService = new Mock(); _expiringTokenService = new Mock(); @@ -59,7 +57,6 @@ public async Task Init() _dataService, _clientAppState.Object, _agentHubContextFixture.HubContextMock.Object, - _appConfig.Object, _circuitManager.Object, _toastService.Object, _expiringTokenService.Object, diff --git a/Tests/Server.Tests/IoCActivator.cs b/Tests/Server.Tests/IoCActivator.cs index 6a3466fb3..e905efb14 100644 --- a/Tests/Server.Tests/IoCActivator.cs +++ b/Tests/Server.Tests/IoCActivator.cs @@ -22,54 +22,29 @@ namespace Remotely.Server.Tests; public class IoCActivator { public static IServiceProvider ServiceProvider { get; set; } = null!; - private static IWebHostBuilder? _builder; - - public static void Activate() - { - if (_builder is null) - { - _builder = WebHost.CreateDefaultBuilder() - .UseStartup() - .CaptureStartupErrors(true) - .ConfigureAppConfiguration(config => - { - config.AddInMemoryCollection(new Dictionary() - { - ["ApplicationOptions:DBProvider"] = "InMemory" - }); - }); - - _builder.Build(); - } - } - + private static WebApplicationBuilder? _builder; + private static WebApplication? _webApp; [AssemblyInitialize] public static void AssemblyInit(TestContext context) { - Activate(); - } -} + _builder = WebApplication.CreateBuilder(); -public class Startup -{ - public void ConfigureServices(IServiceCollection services) - { - services.AddDbContext(); - - services.AddIdentity(options => options.Stores.MaxLengthForKeys = 128) - .AddEntityFrameworkStores() - .AddDefaultTokenProviders(); + _builder.Services.AddDbContext( + contextLifetime: ServiceLifetime.Transient, + optionsLifetime: ServiceLifetime.Transient); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); + _builder.Services. + AddIdentity(options => options.Stores.MaxLengthForKeys = 128) + .AddEntityFrameworkStores() + .AddDefaultTokenProviders(); - IoCActivator.ServiceProvider = services.BuildServiceProvider(); - } + _builder.Services.AddTransient(); + _builder.Services.AddTransient(); + _builder.Services.AddTransient(); - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { + _webApp = _builder.Build(); + ServiceProvider = _webApp.Services; } } + diff --git a/Utilities/Publish.ps1 b/Utilities/Publish.ps1 index e4eb184af..75c8ba726 100644 --- a/Utilities/Publish.ps1 +++ b/Utilities/Publish.ps1 @@ -103,7 +103,7 @@ if ((Test-Path -Path "$Root\Agent\bin\publish\linux-x64") -eq $true) { } -# Publish Core clients. +# Publish agents. dotnet publish /p:Version=$CurrentVersion /p:FileVersion=$CurrentVersion --runtime win-x64 --self-contained --configuration Release --output "$Root\Agent\bin\publish\win-x64" "$Root\Agent" dotnet publish /p:Version=$CurrentVersion /p:FileVersion=$CurrentVersion --runtime linux-x64 --self-contained --configuration Release --output "$Root\Agent\bin\publish\linux-x64" "$Root\Agent" dotnet publish /p:Version=$CurrentVersion /p:FileVersion=$CurrentVersion --runtime win-x86 --self-contained --configuration Release --output "$Root\Agent\bin\publish\win-x86" "$Root\Agent" diff --git a/.dockerignore b/docker-compose/.dockerignore similarity index 100% rename from .dockerignore rename to docker-compose/.dockerignore diff --git a/docker-compose/docker-compose.dcproj b/docker-compose/docker-compose.dcproj index 6be91d1de..cfbe7b521 100644 --- a/docker-compose/docker-compose.dcproj +++ b/docker-compose/docker-compose.dcproj @@ -2,13 +2,13 @@ 2.1 - remotely + remotely Linux False 90ec49b2-b56a-4ecd-8f63-2162dd140f7c LaunchBrowser {Scheme}://localhost:{ServicePort} - server + remotely diff --git a/docker-compose/docker-compose.override.yml b/docker-compose/docker-compose.override.yml index 0cde41cf8..f9d111eab 100644 --- a/docker-compose/docker-compose.override.yml +++ b/docker-compose/docker-compose.override.yml @@ -1,51 +1,14 @@ version: '3.4' services: - server: - build: - context: ../Server - dockerfile: Dockerfile.local + remotely: environment: - ASPNETCORE_ENVIRONMENT=Development - ASPNETCORE_HTTP_PORTS=5000 - ASPNETCORE_HTTPS_PORTS=5001 - - Remotely_ApplicationOptions__AllowApiLogin=false, - #- Remotely_ApplicationOptions__BannedDevices__0=, - - Remotely_ApplicationOptions__DataRetentionInDays=90, - - Remotely_ApplicationOptions__DBProvider=SQLite, - - Remotely_ApplicationOptions__EnableRemoteControlRecording=false, - - Remotely_ApplicationOptions__EnableWindowsEventLog=false, - - Remotely_ApplicationOptions__EnforceAttendedAccess=false, - - Remotely_ApplicationOptions__ForceClientHttps=false, - #- Remotely_ApplicationOptions__KnownProxies__0=, - - Remotely_ApplicationOptions__MaxConcurrentUpdates=10, - - Remotely_ApplicationOptions__MaxOrganizationCount=1, - - Remotely_ApplicationOptions__MessageOfTheDay=, - - Remotely_ApplicationOptions__RedirectToHttps=true, - - Remotely_ApplicationOptions__RemoteControlNotifyUser=true, - - Remotely_ApplicationOptions__RemoteControlRequiresAuthentication=true, - - Remotely_ApplicationOptions__RemoteControlSessionLimit=3, - - Remotely_ApplicationOptions__Require2FA=false, - - Remotely_ApplicationOptions__SmtpDisplayName=, - - Remotely_ApplicationOptions__SmtpEmail=, - - Remotely_ApplicationOptions__SmtpHost=, - - Remotely_ApplicationOptions__SmtpLocalDomain=, - - Remotely_ApplicationOptions__SmtpCheckCertificateRevocation=true, - - Remotely_ApplicationOptions__SmtpPassword=, - - Remotely_ApplicationOptions__SmtpPort=587, - - Remotely_ApplicationOptions__SmtpUserName=, - - Remotely_ApplicationOptions__Theme=Dark, - #- Remotely_ApplicationOptions__TrustedCorsOrigins__0=, - - Remotely_ApplicationOptions__UseHsts=false, - - Remotely_ApplicationOptions__UseHttpLogging=false ports: - - "5000" - - "5001" + - "5000:5000" + - "5001:5001" volumes: - ${APPDATA}/Microsoft/UserSecrets:/home/app/.microsoft/usersecrets:ro - - ${APPDATA}/ASP.NET/Https:/home/app/.aspnet/https:ro - - remotely-data:/app/AppData - -volumes: - remotely-data: - name: remotely-data \ No newline at end of file + - ${APPDATA}/ASP.NET/Https:/home/app/.aspnet/https:ro \ No newline at end of file diff --git a/docker-compose/docker-compose.yml b/docker-compose/docker-compose.yml index 5ce84f900..be573190a 100644 --- a/docker-compose/docker-compose.yml +++ b/docker-compose/docker-compose.yml @@ -1,45 +1,62 @@ version: '3.4' +volumes: + remotely-data: + name: remotely-data + services: remotely: - image: immybot/remotely + image: immybot/remotely:latest volumes: - - /remotely-data:/app/AppData + - remotely-data:/app/AppData build: - context: ../Server - dockerfile: Dockerfile + context: ../ + dockerfile: Server/Dockerfile ports: - "5000:5000" + - "5001:5001" environment: - ASPNETCORE_ENVIRONMENT=Production - ASPNETCORE_HTTP_PORTS=5000 - ASPNETCORE_HTTPS_PORTS=5001 - - Remotely_ApplicationOptions__AllowApiLogin=false, - #- Remotely_ApplicationOptions__BannedDevices__0=, - - Remotely_ApplicationOptions__DataRetentionInDays=90, - - Remotely_ApplicationOptions__DBProvider=SQLite, - - Remotely_ApplicationOptions__EnableRemoteControlRecording=false, - - Remotely_ApplicationOptions__EnableWindowsEventLog=false, - - Remotely_ApplicationOptions__EnforceAttendedAccess=false, - - Remotely_ApplicationOptions__ForceClientHttps=false, - #- Remotely_ApplicationOptions__KnownProxies__0=, - - Remotely_ApplicationOptions__MaxConcurrentUpdates=10, - - Remotely_ApplicationOptions__MaxOrganizationCount=1, - - Remotely_ApplicationOptions__MessageOfTheDay=, - - Remotely_ApplicationOptions__RedirectToHttps=true, - - Remotely_ApplicationOptions__RemoteControlNotifyUser=true, - - Remotely_ApplicationOptions__RemoteControlRequiresAuthentication=true, - - Remotely_ApplicationOptions__RemoteControlSessionLimit=3, - - Remotely_ApplicationOptions__Require2FA=false, - - Remotely_ApplicationOptions__SmtpDisplayName=, - - Remotely_ApplicationOptions__SmtpEmail=, - - Remotely_ApplicationOptions__SmtpHost=, - - Remotely_ApplicationOptions__SmtpLocalDomain=, - - Remotely_ApplicationOptions__SmtpCheckCertificateRevocation=true, - - Remotely_ApplicationOptions__SmtpPassword=, - - Remotely_ApplicationOptions__SmtpPort=587, - - Remotely_ApplicationOptions__SmtpUserName=, - - Remotely_ApplicationOptions__Theme=Dark, - #- Remotely_ApplicationOptions__TrustedCorsOrigins__0=, - - Remotely_ApplicationOptions__UseHsts=false, + # Other ASP.NET Core configurations can be overridden here, such as Logging. + # See https://learn.microsoft.com/en-us/aspnet/core/fundamentals/configuration/?view=aspnetcore-8.0 + + # Values for DbProvider are SQLite, SQLServer, and PostgreSQL. + - Remotely_ApplicationOptions__DbProvider=SQLite + # This path shouldn't be changed. It points to the Docker volume. + - Remotely_ConnectionStrings__SQLite=Data Source=/app/AppData/Remotely.db + # If using SQL Server, change the connection string to point to your SQL Server instance. + - Remotely_ConnectionStrings__SQLServer=Server=(localdb)\\mssqllocaldb;Database=Remotely-Server-53bc9b9d-9d6a-45d4-8429-2a2761773502;Trusted_Connection=True;MultipleActiveResultSets=true + # If using PostgreSQL, change the connection string to point to your PostgreSQL instance. + - Remotely_ConnectionStrings__PostgreSQL=Server=Host=localhost;Database=Remotely;Username=postgres; + + - Remotely_ApplicationOptions__AllowApiLogin=false + #- Remotely_ApplicationOptions__BannedDevices__0= + - Remotely_ApplicationOptions__DataRetentionInDays=90 + - Remotely_ApplicationOptions__DBProvider=SQLite + - Remotely_ApplicationOptions__EnableRemoteControlRecording=false + - Remotely_ApplicationOptions__EnableWindowsEventLog=false + - Remotely_ApplicationOptions__EnforceAttendedAccess=false + - Remotely_ApplicationOptions__ForceClientHttps=false + #- Remotely_ApplicationOptions__KnownProxies__0= + - Remotely_ApplicationOptions__MaxConcurrentUpdates=10 + - Remotely_ApplicationOptions__MaxOrganizationCount=1 + - Remotely_ApplicationOptions__MessageOfTheDay= + - Remotely_ApplicationOptions__RedirectToHttps=true + - Remotely_ApplicationOptions__RemoteControlNotifyUser=true + - Remotely_ApplicationOptions__RemoteControlRequiresAuthentication=true + - Remotely_ApplicationOptions__RemoteControlSessionLimit=3 + - Remotely_ApplicationOptions__Require2FA=false + - Remotely_ApplicationOptions__SmtpDisplayName= + - Remotely_ApplicationOptions__SmtpEmail= + - Remotely_ApplicationOptions__SmtpHost= + - Remotely_ApplicationOptions__SmtpLocalDomain= + - Remotely_ApplicationOptions__SmtpCheckCertificateRevocation=true + - Remotely_ApplicationOptions__SmtpPassword= + - Remotely_ApplicationOptions__SmtpPort=587 + - Remotely_ApplicationOptions__SmtpUserName= + - Remotely_ApplicationOptions__Theme=Dark + #- Remotely_ApplicationOptions__TrustedCorsOrigins__0= + - Remotely_ApplicationOptions__UseHsts=false - Remotely_ApplicationOptions__UseHttpLogging=false \ No newline at end of file