Move appsettings to DB.

This commit is contained in:
Jared Goodwin 2024-02-20 16:15:03 -08:00
parent 2af48bf663
commit 8afdd97640
48 changed files with 511 additions and 729 deletions

View File

@ -7,7 +7,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
<PublishProtocol>FileSystem</PublishProtocol>
<Configuration>Release</Configuration>
<Platform>x64</Platform>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net8.0-windows</TargetFramework>
<PublishDir>..\Server\wwwroot\Content\Win-x64\</PublishDir>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<SelfContained>true</SelfContained>

View File

@ -7,7 +7,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
<PublishProtocol>FileSystem</PublishProtocol>
<Configuration>Release</Configuration>
<Platform>x86</Platform>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net8.0-windows</TargetFramework>
<PublishDir>..\Server\wwwroot\Content\Win-x86\</PublishDir>
<RuntimeIdentifier>win-x86</RuntimeIdentifier>
<SelfContained>true</SelfContained>

View File

@ -7,7 +7,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
<PublishProtocol>FileSystem</PublishProtocol>
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net8.0-windows</TargetFramework>
<PublishDir>..\Agent\bin\publish\win-x64\Desktop</PublishDir>
<SelfContained>true</SelfContained>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>

View File

@ -7,7 +7,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
<PublishProtocol>FileSystem</PublishProtocol>
<Configuration>Release</Configuration>
<Platform>x64</Platform>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net8.0-windows</TargetFramework>
<PublishDir>..\Agent\bin\publish\win-x64\Desktop</PublishDir>
<SelfContained>true</SelfContained>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>

View File

@ -7,7 +7,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
<PublishProtocol>FileSystem</PublishProtocol>
<Configuration>Release</Configuration>
<Platform>x86</Platform>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net8.0-windows</TargetFramework>
<PublishDir>..\Agent\bin\publish\win-x86\Desktop</PublishDir>
<RuntimeIdentifier>win-x86</RuntimeIdentifier>
<SelfContained>true</SelfContained>

View File

@ -21,18 +21,18 @@ public class AgentUpdateController : ControllerBase
{
private readonly IHubContext<AgentHub, IAgentHubClient> _agentHubContext;
private readonly ILogger<AgentUpdateController> _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<AgentHub, IAgentHubClient> agentHubContext,
ILogger<AgentUpdateController> logger)
{
_hostEnv = hostingEnv;
_appConfig = appConfig;
_dataService = dataService;
_serviceSessionCache = serviceSessionCache;
_agentHubContext = agentHubContext;
_logger = logger;
@ -105,7 +105,8 @@ public class AgentUpdateController : ControllerBase
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);

View File

@ -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<ClientDownloadsController> logger)
{
_hostEnv = hostEnv;
_embeddedDataSearcher = embeddedDataSearcher;
_appConfig = appConfig;
_dataService = dataService;
_logger = logger;
}
@ -133,7 +133,8 @@ public class ClientDownloadsController : ControllerBase
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 @@ public class ClientDownloadsController : ControllerBase
private async Task<IActionResult> 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 @@ public class ClientDownloadsController : ControllerBase
private async Task<IActionResult> 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 @@ public class ClientDownloadsController : ControllerBase
{
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 @@ public class ClientDownloadsController : ControllerBase
}
}
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 @@ public class ClientDownloadsController : ControllerBase
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}.",

View File

@ -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> _desktopHub;
private readonly IRemoteControlSessionCache _remoteControlSessionCache;
@ -31,7 +30,6 @@ public class LoginController : ControllerBase
public LoginController(
SignInManager<RemotelyUser> signInManager,
IDataService dataService,
IApplicationConfig appConfig,
IHubContext<DesktopHub> casterHubContext,
IRemoteControlSessionCache remoteControlSessionCache,
IHubContext<ViewerHub> viewerHubContext,
@ -39,7 +37,6 @@ public class LoginController : ControllerBase
{
_signInManager = signInManager;
_dataService = dataService;
_appConfig = appConfig;
_desktopHub = casterHubContext;
_remoteControlSessionCache = remoteControlSessionCache;
_viewerHub = viewerHubContext;
@ -76,7 +73,8 @@ public class LoginController : ControllerBase
[HttpPost]
public async Task<IActionResult> Post([FromBody] ApiLogin login)
{
if (!_appConfig.AllowApiLogin)
var settings = await _dataService.GetSettings();
if (!settings.AllowApiLogin)
{
return NotFound();
}

View File

@ -27,10 +27,9 @@ public class RemoteControlController : ControllerBase
private readonly IHubContext<AgentHub, IAgentHubClient> _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<RemotelyUser> _signInManager;
private readonly ILogger<RemoteControlController> _logger;
@ -42,14 +41,12 @@ public class RemoteControlController : ControllerBase
IAgentHubSessionCache serviceSessionCache,
IOtpProvider otpProvider,
IHubEventHandler hubEvents,
IApplicationConfig appConfig,
ILogger<RemoteControlController> logger)
{
_dataService = dataService;
_agentHub = agentHub;
_remoteControlSessionCache = remoteControlSessionCache;
_serviceSessionCache = serviceSessionCache;
_appConfig = appConfig;
_otpProvider = otpProvider;
_hubEvents = hubEvents;
_signInManager = signInManager;
@ -72,7 +69,8 @@ public class RemoteControlController : ControllerBase
[Obsolete("This method is deprecated. Use the GET method along with API keys instead.")]
public async Task<IActionResult> Post([FromBody] RemoteControlRequest rcRequest)
{
if (!_appConfig.AllowApiLogin)
var settings = await _dataService.GetSettings();
if (!settings.AllowApiLogin)
{
return NotFound();
}
@ -145,7 +143,8 @@ public class RemoteControlController : ControllerBase
.OfType<RemoteControlSessionEx>()
.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.");
}

View File

@ -12,19 +12,18 @@ namespace Remotely.Server.Auth;
public class TwoFactorRequiredHandler : AuthorizationHandler<TwoFactorRequiredRequirement>
{
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);

View File

@ -16,13 +16,12 @@
@inject NavigationManager NavigationManager
@inject IdentityRedirectManager RedirectManager
@inject IDataService DataService
@inject IApplicationConfig AppConfig
@inject IWebHostEnvironment HostEnv
<PageTitle>Register</PageTitle>
<h1>Register</h1>
@if (!IsRegistrationEnabled())
@if (!_registrationEnabled)
{
<h2>Registration is disabled.</h2>
}
@ -69,6 +68,7 @@ else
@code {
private IEnumerable<IdentityError>? 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<RemotelyUser>)UserStore;
}
private bool IsRegistrationEnabled()
private async Task<bool> IsRegistrationEnabled()
{
return AppConfig.MaxOrganizationCount < 0 || _organizationCount < AppConfig.MaxOrganizationCount;
var settings = await DataService.GetSettings();
return settings.MaxOrganizationCount < 0 || _organizationCount < settings.MaxOrganizationCount;
}
private sealed class InputModel

View File

@ -1,6 +1,6 @@
@inject AuthenticationStateProvider AuthProvider
@inject IDataService DataService
@inject IApplicationConfig AppConfig
@inject IDataService DataService
@inject IThemeProvider ThemeProvider
<!DOCTYPE html>
@ -28,19 +28,6 @@
<HeadOutlet @rendermode="RenderModeForPage" />
</head>
<body>
<component type="typeof(App)" render-mode="ServerPrerendered" />
<div id="blazor-error-ui">
<environment include="Staging,Production">
An error has occurred. This application may no longer respond until reloaded.
</environment>
<environment include="Development">
An unhandled exception has occurred. See browser dev tools for details.
</environment>
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>
<Routes @rendermode="RenderModeForPage" />
<script src="_framework/blazor.web.js"></script>
</body>

View File

@ -2,13 +2,13 @@
@inherits AuthComponentBase
@inject NavigationManager NavManager
@inject IApplicationConfig AppConfig
@inject IDataService DataService
@inject SignInManager<RemotelyUser> SignInManager
@if (!string.IsNullOrWhiteSpace(AppConfig.MessageOfTheDay))
@if (!string.IsNullOrWhiteSpace(_settings?.MessageOfTheDay))
{
<div class="me-5">
<AlertBanner Message="@AppConfig.MessageOfTheDay" StatusClass="info" />
<AlertBanner Message="@_settings?.MessageOfTheDay" StatusClass="info" />
</div>
}
@ -17,11 +17,12 @@
<ChatFrame />
@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();

View File

@ -91,11 +91,6 @@ public partial class Terminal : AuthComponentBase, IDisposable
[Inject]
private IToastService ToastService { get; init; } = null!;
public void Dispose()
{
Messenger.Unregister<PowerShellCompletionsMessage, string>(this, CircuitConnection.ConnectionId);
GC.SuppressFinalize(this);
}
protected override Task OnAfterRenderAsync(bool firstRender)
{

View File

@ -34,6 +34,12 @@
</NotAuthorized>
</AuthorizeView>
<div id="blazor-error-ui">
An unhandled error has occurred.
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>
<ToastHarness />
<LoaderHarness />
<ModalHarness />

View File

@ -1,7 +1,7 @@
@implements IDisposable
@inject NavigationManager NavigationManager
@inject IAuthService AuthService
@inject IApplicationConfig AppConfig
@inject IDataService DataService
@inject IDataService DataService
<div class="ps-4 pb-1 pe-0 pt-0 navbar navbar-dark" style="background-color: rgba(0,0,0,0.3)">
@ -122,7 +122,7 @@
<li class="px-3 mt-3">
<a class="btn btn-link text-light" href="Account/Login">Log in</a>
</li>
@if (AppConfig.MaxOrganizationCount < 0 || DataService.GetOrganizationCount() < AppConfig.MaxOrganizationCount)
@if (_isRegistrationEnabled)
{
<li class="px-3">
<a class="btn btn-link text-light" href="Account/Register">Register</a>
@ -139,7 +139,7 @@
@code {
private bool collapseNavMenu = true;
private bool _isRegistrationEnabled;
private RemotelyUser? _user;
private Organization? _organization;
private string? _currentUrl;
@ -151,6 +151,9 @@
protected override async Task OnInitializedAsync()
{
var settings = await DataService.GetSettings();
_isRegistrationEnabled = settings.MaxOrganizationCount < 0 || DataService.GetOrganizationCount() < settings.MaxOrganizationCount;
await base.OnInitializedAsync();
_currentUrl = NavigationManager.ToBaseRelativePath(NavigationManager.Uri);

View File

@ -1,5 +1,4 @@
@page "/"
@inject IApplicationConfig AppConfig
@inject IDataService DataService
<AuthorizeView>
@ -13,7 +12,7 @@
<p class="lead">
<a class="btn btn-primary btn-lg me-2" href="Account/Login" role="button">Log In</a>
@if (AppConfig.MaxOrganizationCount < 0 || DataService.GetOrganizationCount() < AppConfig.MaxOrganizationCount)
@if (_isRegistrationEnabled)
{
<a class="btn btn-primary btn-lg" href="Account/Register" role="button">Register</a>
}
@ -24,3 +23,15 @@
<AuthorizedIndex />
</Authorized>
</AuthorizeView>
@code
{
private bool _isRegistrationEnabled;
protected override async Task OnInitializedAsync()
{
var settings = await DataService.GetSettings();
_isRegistrationEnabled = settings.MaxOrganizationCount < 0 || DataService.GetOrganizationCount() < settings.MaxOrganizationCount;
await base.OnInitializedAsync();
}
}

View File

@ -121,18 +121,6 @@
<br />
<ValidationMessage For="() => Input.DataRetentionInDays" />
</div>
<div class="form-group">
<label class="control-label">Database Provider</label>
<br />
<select class="form-control" @bind="Input.DBProvider">
@foreach (var provider in Enum.GetValues<DbProvider>())
{
<option @key="provider" value="@provider">@provider</option>
}
</select>
<br />
<ValidationMessage For="() => Input.DBProvider" />
</div>
<div class="form-group">
<label>Enable Remote Control Recording</label>
<br />
@ -357,34 +345,6 @@
<ValidationMessage For="() => Input.UseHttpLogging" />
</div>
<h4>Connection Strings</h4>
<div class="form-group">
<label class="control-label">PostgreSQL</label>
<br />
<InputText @bind-Value="ConnectionStrings.PostgreSQL" class="form-control" autocomplete="off" />
<br />
<ValidationMessage For="() => ConnectionStrings.PostgreSQL" />
</div>
<div class="form-group">
<label class="control-label">SQLite</label>
<br />
<InputText @bind-Value="ConnectionStrings.SQLite" class="form-control" autocomplete="off" />
<br />
<ValidationMessage For="() => ConnectionStrings.SQLite" />
</div>
<div class="form-group">
<label class="control-label">SQL Server</label>
<br />
<InputText @bind-Value="ConnectionStrings.SQLServer" class="form-control" autocomplete="off" />
<br />
<ValidationMessage For="() => ConnectionStrings.SQLServer" />
</div>
<div class="form-group mt-3">
<button type="button" class="btn btn-primary" @onclick="Save">Save</button>
</div>

View File

@ -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<string> 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<string> 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<string> 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<RemotelyUser> _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<RemotelyUser> _userList = new();
[Inject]
public required IHubContext<AgentHub, IAgentHubClient> AgentHubContext { get; init; }
[Inject]
private IHubContext<AgentHub, IAgentHubClient> AgentHubContext { get; init; } = null!;
public required ICircuitManager CircuitManager { get; init; }
[Inject]
private IConfiguration Configuration { get; init; } = null!;
private ConnectionStringsModel ConnectionStrings { get; } = new();
public required IDataService DataService { get; init; }
[Inject]
private IDataService DataService { get; init; } = null!;
public required IEmailSenderEx EmailSender { get; init; }
[Inject]
private IEmailSenderEx EmailSender { get; init; } = null!;
public required IWebHostEnvironment HostEnv { get; init; }
[Inject]
private IWebHostEnvironment HostEnv { get; init; } = null!;
public required ILogger<ServerConfig> Logger { get; init; }
[Inject]
private ILogger<ServerConfig> Logger { get; init; } = null!;
public required IModalService ModalService { get; init; }
[Inject]
private IAgentHubSessionCache ServiceSessionCache { get; init; } = null!;
private AppSettingsModel Input { get; } = new();
public required IAgentHubSessionCache ServiceSessionCache { get; init; }
[Inject]
private IModalService ModalService { get; init; } = null!;
public required IToastService ToastService { get; init; }
[Inject]
private IUpgradeService UpgradeService { get; init; } = null!;
public required IUpgradeService UpgradeService { get; init; }
[Inject]
private ICircuitManager CircuitManager { get; init; } = null!;
private SettingsModel Input { get; set; } = new();
private IEnumerable<string> OutdatedDevices => GetOutdatedDevices();
[Inject]
private IToastService ToastService { get; init; } = null!;
private int TotalDevices => DataService.GetTotalDevices();
private IEnumerable<RemotelyUser> UserList
@ -182,7 +67,7 @@ public partial class ServerConfig : AuthComponentBase
{
if (User is null)
{
return Enumerable.Empty<RemotelyUser>();
return [];
}
EnsureUserSet();
@ -202,8 +87,8 @@ public partial class ServerConfig : AuthComponentBase
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 @@ public partial class ServerConfig : AuthComponentBase
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 @@ public partial class ServerConfig : AuthComponentBase
}
}
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<IDictionary<string, object>>(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)

View File

@ -24,4 +24,5 @@
@using Remotely.Server.Components.Scripts
@using Remotely.Server.Components.TreeView
@using Remotely.Server.Auth
@using Remotely.Shared.Entities
@using Remotely.Shared.Entities
@using Remotely.Server.Models

View File

@ -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 class AppDb : IdentityDbContext
public DbSet<DeviceGroup> DeviceGroups { get; set; }
public DbSet<Device> Devices { get; set; }
public DbSet<InviteLink> InviteLinks { get; set; }
public DbSet<KeyValueRecord> KeyValueRecords { get; set; }
public DbSet<Organization> Organizations { get; set; }
public DbSet<SavedScript> SavedScripts { get; set; }
public DbSet<ScriptResult> ScriptResults { get; set; }
@ -45,7 +41,6 @@ public class AppDb : IdentityDbContext
public DbSet<SharedFile> SharedFiles { get; set; }
public new DbSet<RemotelyUser> Users { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder options)
{
options.ConfigureWarnings(x => x.Ignore(RelationalEventId.MultipleCollectionIncludeWarning));
@ -284,13 +279,13 @@ public class AppDb : IdentityDbContext
{
if (string.IsNullOrEmpty(value))
{
return Array.Empty<string>();
return [];
}
return JsonSerializer.Deserialize<string[]>(value, jsonOptions) ?? Array.Empty<string>();
return JsonSerializer.Deserialize<string[]>(value, jsonOptions) ?? [];
}
catch
{
return Array.Empty<string>();
return [];
}
}

View File

@ -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<AppDb>();
}
}

View File

@ -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
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
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 .
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
USER app
ENTRYPOINT ["dotnet", "Remotely_Server.dll"]
HEALTHCHECK --interval=5m --timeout=3s \

View File

@ -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"]

View File

@ -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<SettingsModel> 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<SettingsModel>(record.Value) ?? new();
}
}

View File

@ -23,9 +23,8 @@ namespace Remotely.Server.Hubs;
public class AgentHub : Hub<IAgentHubClient>
{
private readonly IApplicationConfig _appConfig;
private readonly ICircuitManager _circuitManager;
private readonly IDataService _dataService;
private readonly ICircuitManager _circuitManager;
private readonly IExpiringTokenService _expiringTokenService;
private readonly ILogger<AgentHub> _logger;
private readonly IMessenger _messenger;
@ -33,8 +32,8 @@ public class AgentHub : Hub<IAgentHubClient>
private readonly IAgentHubSessionCache _serviceSessionCache;
private readonly IHubContext<ViewerHub> _viewerHubContext;
public AgentHub(IDataService dataService,
IApplicationConfig appConfig,
public AgentHub(
IDataService dataService,
IAgentHubSessionCache serviceSessionCache,
IHubContext<ViewerHub> viewerHubContext,
ICircuitManager circuitManager,
@ -46,7 +45,6 @@ public class AgentHub : Hub<IAgentHubClient>
_dataService = dataService;
_serviceSessionCache = serviceSessionCache;
_viewerHubContext = viewerHubContext;
_appConfig = appConfig;
_circuitManager = circuitManager;
_expiringTokenService = expiringTokenService;
_remoteControlSessions = remoteControlSessionCache;
@ -288,9 +286,10 @@ public class AgentHub : Hub<IAgentHubClient>
return _messenger.Send(message, requesterId);
}
public string GetServerUrl()
public async Task<string> GetServerUrl()
{
return _appConfig.ServerUrl;
var settings = await _dataService.GetSettings();
return settings.ServerUrl;
}
public string GetServerVerificationToken()
@ -382,6 +381,7 @@ public class AgentHub : Hub<IAgentHubClient>
private async Task<bool> CheckForDeviceBan(params string[] deviceIdNameOrIPs)
{
var settings = await _dataService.GetSettings();
foreach (var device in deviceIdNameOrIPs)
{
if (string.IsNullOrWhiteSpace(device))
@ -389,7 +389,7 @@ public class AgentHub : Hub<IAgentHubClient>
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);

View File

@ -71,11 +71,10 @@ public interface ICircuitConnection
public class CircuitConnection : CircuitHandler, ICircuitConnection
{
private readonly IHubContext<AgentHub, IAgentHubClient> _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<CircuitConnection> _logger;
@ -89,7 +88,6 @@ public class CircuitConnection : CircuitHandler, ICircuitConnection
IDataService dataService,
ISelectedCardsStore cardStore,
IHubContext<AgentHub, IAgentHubClient> agentHubContext,
IApplicationConfig appConfig,
ICircuitManager circuitManager,
IToastService toastService,
IExpiringTokenService expiringTokenService,
@ -101,7 +99,6 @@ public class CircuitConnection : CircuitHandler, ICircuitConnection
_dataService = dataService;
_agentHubContext = agentHubContext;
_cardStore = cardStore;
_appConfig = appConfig;
_authService = authService;
_circuitManager = circuitManager;
_toastService = toastService;
@ -228,6 +225,8 @@ public class CircuitConnection : CircuitHandler, ICircuitConnection
public async Task<Result<RemoteControlSessionEx>> 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 class CircuitConnection : CircuitHandler, ICircuitConnection
.OfType<RemoteControlSessionEx>()
.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 class CircuitConnection : CircuitHandler, ICircuitConnection
DeviceId = deviceId,
ViewOnly = viewOnly,
OrganizationId = User.OrganizationID,
RequireConsent = _appConfig.EnforceAttendedAccess,
NotifyUserOnStart = _appConfig.RemoteControlNotifyUser
RequireConsent = settings.EnforceAttendedAccess,
NotifyUserOnStart = settings.RemoteControlNotifyUser
};
_remoteControlSessionCache.AddOrUpdate($"{sessionId}", session);

View File

@ -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<string> 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<string> 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<string> TrustedCorsOrigins { get; set; } = [];
public bool UseHsts { get; set; }
public bool UseHttpLogging { get; set; }
}

View File

@ -0,0 +1,7 @@
namespace Remotely.Server.Options;
public class ApplicationOptions
{
public const string SectionKey = "ApplicationOptions";
public string DbProvider { get; set; } = "SQLite";
}

View File

@ -23,6 +23,9 @@ using Immense.SimpleMessenger;
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 @@ var services = builder.Services;
configuration.AddEnvironmentVariables("Remotely_");
services.Configure<ApplicationOptions>(
configuration.GetSection(ApplicationOptions.SectionKey));
services
.AddRazorComponents()
.AddInteractiveServerComponents();
@ -41,21 +47,29 @@ services.AddScoped<IdentityUserAccessor>();
services.AddScoped<IdentityRedirectManager>();
services.AddScoped<AuthenticationStateProvider, IdentityRevalidatingAuthenticationStateProvider>();
ConfigureSerilog(builder);
var dbProvider = configuration["ApplicationOptions:DbProvider"]?.ToLower();
builder.Logging.AddConfiguration(builder.Configuration.GetSection("Logging"));
if (OperatingSystem.IsWindows() &&
bool.TryParse(builder.Configuration["ApplicationOptions:EnableWindowsEventLog"], out var enableEventLog) &&
enableEventLog)
switch (dbProvider)
{
builder.Logging.AddEventLog();
}
var dbProvider = configuration["ApplicationOptions:DBProvider"]?.ToLower();
if (string.IsNullOrWhiteSpace(dbProvider))
{
throw new InvalidOperationException("DBProvider is missing from appsettings.json.");
case "sqlite":
services.AddDbContext<AppDb, SqliteDbContext>(
contextLifetime: ServiceLifetime.Transient,
optionsLifetime: ServiceLifetime.Transient);
break;
case "sqlserver":
services.AddDbContext<AppDb, SqlServerDbContext>(
contextLifetime: ServiceLifetime.Transient,
optionsLifetime: ServiceLifetime.Transient);
break;
case "postgresql":
services.AddDbContext<AppDb, PostgreSqlDbContext>(
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 @@ else if (dbProvider == "postgresql")
services.AddDbContext<AppDb, PostgreSqlDbContext>();
}
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.AddAuthorization(options =>
services.AddDatabaseDeveloperPageExceptionFilter();
if (bool.TryParse(configuration["ApplicationOptions:UseHttpLogging"], out var useHttpLogging) &&
useHttpLogging)
if (settings.UseHttpLogging)
{
services.AddHttpLogging(options =>
{
@ -128,14 +161,12 @@ if (bool.TryParse(configuration["ApplicationOptions:UseHttpLogging"], out var us
});
}
var trustedOrigins = configuration.GetSection("ApplicationOptions:TrustedCorsOrigins").Get<string[]>();
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 @@ services.AddCors(options =>
});
var knownProxies = configuration.GetSection("ApplicationOptions:KnownProxies").Get<string[]>();
services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders = ForwardedHeaders.All;
@ -153,7 +183,7 @@ services.Configure<ForwardedHeadersOptions>(options =>
// 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 @@ services.AddRateLimiter(options =>
{
clOptions.QueueLimit = int.MaxValue;
var concurrentPermits = configuration.GetSection("ApplicationOptions:MaxConcurrentUpdates").Get<int>();
if (concurrentPermits <= 0)
{
concurrentPermits = 10;
}
clOptions.PermitLimit = concurrentPermits;
clOptions.PermitLimit =
settings.MaxConcurrentUpdates <= 0 ?
10 :
settings.MaxConcurrentUpdates;
});
});
services.AddHttpClient();
@ -202,7 +229,6 @@ else
}
services.AddScoped<IAppDbFactory, AppDbFactory>();
services.AddTransient<IDataService, DataService>();
services.AddSingleton<IApplicationConfig, ApplicationConfig>();
services.AddScoped<ApiAuthorizationFilter>();
services.AddScoped<LocalOnlyFilter>();
services.AddScoped<ExpiringTokenFilter>();
@ -246,9 +272,7 @@ var app = builder.Build();
app.UseRateLimiter();
var appConfig = app.Services.GetRequiredService<IApplicationConfig>();
if (appConfig.UseHttpLogging)
if (settings.UseHttpLogging)
{
app.UseHttpLogging();
}
@ -265,11 +289,11 @@ if (app.Environment.IsDevelopment())
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 @@ app.MapAdditionalIdentityEndpoints();
using (var scope = app.Services.CreateScope())
{
using var context = scope.ServiceProvider.GetRequiredService<AppDb>();
var dataService = scope.ServiceProvider.GetRequiredService<IDataService>();
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))
{
dataRetentionDays = retentionSetting;
}
var dataRetentionDays = settings.DataRetentionInDays;
if (dataRetentionDays <= 0)
{
dataRetentionDays = 7;
}
var logPath = LogsManager.DefaultLogsDirectory;
void ApplySharedLoggerConfig(LoggerConfiguration loggerConfiguration)
@ -366,8 +385,8 @@ void ConfigureSerilog(WebApplicationBuilder webAppBuilder)
.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);

View File

@ -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<string[]>() ?? System.Array.Empty<string>();
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<string[]>() ?? System.Array.Empty<string>();
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<Theme>(_config["ApplicationOptions:Theme"], out var result) ? result : Theme.Dark;
public string[] TrustedCorsOrigins => _config.GetSection("ApplicationOptions:TrustedCorsOrigins").Get<string[]>() ?? System.Array.Empty<string>();
public bool UseHsts => bool.TryParse(_config["ApplicationOptions:UseHsts"], out var result) && result;
public bool UseHttpLogging => bool.TryParse(_config["ApplicationOptions:UseHttpLogging"], out var result) && result;
}

View File

@ -19,17 +19,17 @@ public class DataCleanupService : BackgroundService, IDisposable
private readonly ILogger<DataCleanupService> _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<DataCleanupService> logger)
{
_scopeFactory = scopeFactory;
_systemTime = systemTime;
_appConfig = appConfig;
_dataService = dataService;
_logger = logger;
}
@ -76,14 +76,18 @@ public class DataCleanupService : BackgroundService, IDisposable
await dataService.CleanupOldRecords();
}
private Task RemoveExpiredRecordings()
private async Task RemoveExpiredRecordings()
{
using var scope = _scopeFactory.CreateScope();
var dataService = scope.ServiceProvider.GetRequiredService<IDataService>();
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 @@ public class DataCleanupService : BackgroundService, IDisposable
_logger.LogError(ex, "Error while deleting expired recording: {file}", file);
}
}
return Task.CompletedTask;
}
}

View File

@ -21,6 +21,7 @@ using Remotely.Shared.ViewModels;
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 @@ public interface IDataService
List<string> GetServerAdmins();
Task<SettingsModel> GetSettings();
Task<Result<SharedFile>> GetSharedFiled(string fileId);
int GetTotalDevices();
@ -193,6 +196,8 @@ public interface IDataService
Task ResetBranding(string organizationId);
Task SaveSettings(SettingsModel settings);
Task SetAllDevicesNotOnline();
Task SetDisplayName(RemotelyUser user, string displayName);
@ -224,18 +229,16 @@ public interface IDataService
public class DataService : IDataService
{
private readonly IApplicationConfig _appConfig;
private readonly IAppDbFactory _appDbFactory;
private readonly IHostEnvironment _hostEnvironment;
private readonly ILogger<DataService> _logger;
private readonly SemaphoreSlim _settingsLock = new(1, 1);
public DataService(
IApplicationConfig appConfig,
IHostEnvironment hostEnvironment,
IAppDbFactory appDbFactory,
ILogger<DataService> logger)
{
_appConfig = appConfig;
_hostEnvironment = hostEnvironment;
_appDbFactory = appDbFactory;
_logger = logger;
@ -639,14 +642,15 @@ public class DataService : IDataService
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 class DataService : IDataService
.ToList();
}
public async Task<SettingsModel> 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<Result<SharedFile>> GetSharedFiled(string fileId)
{
using var dbContext = _appDbFactory.GetContext();
@ -1930,6 +1953,35 @@ public class DataService : IDataService
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 class DataService : IDataService
}
private async Task<string> 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);

View File

@ -6,6 +6,7 @@ using Microsoft.Extensions.Logging;
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 class EmailSender : IEmailSender
public class EmailSenderEx : IEmailSenderEx
{
private readonly IApplicationConfig _appConfig;
private readonly IDataService _dataService;
private readonly ILogger<EmailSenderEx> _logger;
public EmailSenderEx(
IApplicationConfig appConfig,
IDataService dataService,
ILogger<EmailSenderEx> logger)
{
_appConfig = appConfig;
_dataService = dataService;
_logger = logger;
}
public async Task<bool> SendEmailAsync(
@ -54,8 +55,10 @@ public class EmailSenderEx : IEmailSenderEx
{
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 class EmailSenderEx : IEmailSenderEx
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 class EmailSenderEx : IEmailSenderEx
}
}
public Task<bool> SendEmailAsync(string email, string subject, string htmlMessage, string? organizationID = null)
public async Task<bool> 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<EmailSenderFake> _logger) : IEmailSenderEx

View File

@ -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<bool> IsAuthorized(AuthorizationFilterContext context)
public async Task<bool> 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;
}
}

View File

@ -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<RemoteControlViewerOptions> GetViewerOptions()
public async Task<RemoteControlViewerOptions> GetViewerOptions()
{
var settings = await _dataService.GetSettings();
var options = new RemoteControlViewerOptions()
{
ShouldRecordSession = _appConfig.EnableRemoteControlRecording
ShouldRecordSession = settings.EnableRemoteControlRecording
};
return Task.FromResult(options);
return options;
}
}

View File

@ -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<string> GetFaviconUrl(PageModel viewerModel)

View File

@ -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<Theme> 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;
}
}

View File

@ -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"
}
}
}

View File

@ -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; }
}

View File

@ -8,6 +8,7 @@ using Microsoft.Extensions.Logging;
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 class AgentHubTests
var circuitConnection = new Mock<ICircuitConnection>();
circuitManager.Setup(x => x.Connections).Returns(new[] { circuitConnection.Object });
circuitConnection.Setup(x => x.User).Returns(_testData.Org1Admin1);
var appConfig = new Mock<IApplicationConfig>();
var viewerHub = new Mock<IHubContext<ViewerHub>>();
var expiringTokenService = new Mock<IExpiringTokenService>();
var serviceSessionCache = new Mock<IAgentHubSessionCache>();
@ -40,11 +40,12 @@ public class AgentHubTests
var messenger = new Mock<IMessenger>();
var logger = new Mock<ILogger<AgentHub>>();
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 class AgentHubTests
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 class AgentHubTests
var circuitConnection = new Mock<ICircuitConnection>();
circuitManager.Setup(x => x.Connections).Returns(new[] { circuitConnection.Object });
circuitConnection.Setup(x => x.User).Returns(_testData.Org1Admin1);
var appConfig = new Mock<IApplicationConfig>();
var viewerHub = new Mock<IHubContext<ViewerHub>>();
var expiringTokenService = new Mock<IExpiringTokenService>();
var serviceSessionCache = new Mock<IAgentHubSessionCache>();
@ -81,11 +82,13 @@ public class AgentHubTests
var messenger = new Mock<IMessenger>();
var logger = new Mock<ILogger<AgentHub>>();
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 class AgentHubTests
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 class AgentHubTests
await _testData.Init();
_dataService = IoCActivator.ServiceProvider.GetRequiredService<IDataService>();
}
private class CallerContext : HubCallerContext
{
public override string ConnectionId => "test-id";
public override string? UserIdentifier => null;
public override ClaimsPrincipal? User => null;
public override IDictionary<object, object?> Items { get; } = new Dictionary<object, object?>();
public override IFeatureCollection Features { get; } = new FeatureCollection();
public override CancellationToken ConnectionAborted => CancellationToken.None;
public override void Abort()
{
}
}
}

View File

@ -24,7 +24,6 @@ public class CircuitConnectionTests
private Mock<IAuthService> _authService;
private Mock<ISelectedCardsStore> _clientAppState;
private HubContextFixture<AgentHub, IAgentHubClient> _agentHubContextFixture;
private Mock<IApplicationConfig> _appConfig;
private Mock<ICircuitManager> _circuitManager;
private Mock<IToastService> _toastService;
private Mock<IExpiringTokenService> _expiringTokenService;
@ -45,7 +44,6 @@ public class CircuitConnectionTests
_authService = new Mock<IAuthService>();
_clientAppState = new Mock<ISelectedCardsStore>();
_agentHubContextFixture = new HubContextFixture<AgentHub, IAgentHubClient>();
_appConfig = new Mock<IApplicationConfig>();
_circuitManager = new Mock<ICircuitManager>();
_toastService = new Mock<IToastService>();
_expiringTokenService = new Mock<IExpiringTokenService>();
@ -59,7 +57,6 @@ public class CircuitConnectionTests
_dataService,
_clientAppState.Object,
_agentHubContextFixture.HubContextMock.Object,
_appConfig.Object,
_circuitManager.Object,
_toastService.Object,
_expiringTokenService.Object,

View File

@ -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<Startup>()
.CaptureStartupErrors(true)
.ConfigureAppConfiguration(config =>
{
config.AddInMemoryCollection(new Dictionary<string, string?>()
{
["ApplicationOptions:DBProvider"] = "InMemory"
});
});
_builder.Build();
}
}
private static WebApplicationBuilder? _builder;
private static WebApplication? _webApp;
[AssemblyInitialize]
public static void AssemblyInit(TestContext context)
{
Activate();
_builder = WebApplication.CreateBuilder();
_builder.Services.AddDbContext<AppDb, TestingDbContext>(
contextLifetime: ServiceLifetime.Transient,
optionsLifetime: ServiceLifetime.Transient);
_builder.Services.
AddIdentity<RemotelyUser, IdentityRole>(options => options.Stores.MaxLengthForKeys = 128)
.AddEntityFrameworkStores<AppDb>()
.AddDefaultTokenProviders();
_builder.Services.AddTransient<IAppDbFactory, AppDbFactory>();
_builder.Services.AddTransient<IDataService, DataService>();
_builder.Services.AddTransient<IEmailSenderEx, EmailSenderEx>();
_webApp = _builder.Build();
ServiceProvider = _webApp.Services;
}
}
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<AppDb, TestingDbContext>();
services.AddIdentity<RemotelyUser, IdentityRole>(options => options.Stores.MaxLengthForKeys = 128)
.AddEntityFrameworkStores<AppDb>()
.AddDefaultTokenProviders();
services.AddTransient<IAppDbFactory, AppDbFactory>();
services.AddTransient<IDataService, DataService>();
services.AddTransient<IApplicationConfig, ApplicationConfig>();
services.AddTransient<IEmailSenderEx, EmailSenderEx>();
IoCActivator.ServiceProvider = services.BuildServiceProvider();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
}
}

View File

@ -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"

View File

@ -2,13 +2,13 @@
<Project ToolsVersion="15.0" Sdk="Microsoft.Docker.Sdk">
<PropertyGroup Label="Globals">
<ProjectVersion>2.1</ProjectVersion>
<DockerComposeProjectName>remotely</DockerComposeProjectName>
<DockerComposeProjectName>remotely</DockerComposeProjectName>
<DockerTargetOS>Linux</DockerTargetOS>
<DockerPublishLocally>False</DockerPublishLocally>
<ProjectGuid>90ec49b2-b56a-4ecd-8f63-2162dd140f7c</ProjectGuid>
<DockerLaunchAction>LaunchBrowser</DockerLaunchAction>
<DockerServiceUrl>{Scheme}://localhost:{ServicePort}</DockerServiceUrl>
<DockerServiceName>server</DockerServiceName>
<DockerServiceName>remotely</DockerServiceName>
</PropertyGroup>
<ItemGroup>
<None Include="docker-compose.override.yml">

View File

@ -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
- ${APPDATA}/ASP.NET/Https:/home/app/.aspnet/https:ro

View File

@ -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