diff --git a/Server/API/AgentUpdateController.cs b/Server/API/AgentUpdateController.cs index 2ed3e877..909ca4fa 100644 --- a/Server/API/AgentUpdateController.cs +++ b/Server/API/AgentUpdateController.cs @@ -27,32 +27,28 @@ namespace Remotely.Server.API private readonly IHubContext _agentHubContext; - + private readonly ILogger _logger; private readonly IApplicationConfig _appConfig; - - private readonly IDataService _dataService; - private readonly IWebHostEnvironment _hostEnv; - private readonly IServiceHubSessionCache _serviceSessionCache; public AgentUpdateController(IWebHostEnvironment hostingEnv, - IDataService dataService, IApplicationConfig appConfig, IServiceHubSessionCache serviceSessionCache, - IHubContext agentHubContext) + IHubContext agentHubContext, + ILogger logger) { _hostEnv = hostingEnv; - _dataService = dataService; _appConfig = appConfig; _serviceSessionCache = serviceSessionCache; _agentHubContext = agentHubContext; + _logger = logger; } [HttpGet("[action]/{downloadId}")] public ActionResult ClearDownload(string downloadId) { - _dataService.WriteEvent($"Clearing download ID {downloadId}.", EventType.Debug, null); + _logger.LogDebug("Clearing download ID {downloadId}.", downloadId); _downloadingAgents.Remove(downloadId); return Ok(); } @@ -92,10 +88,16 @@ namespace Remotely.Server.API _downloadingAgents.Set(downloadId, string.Empty, cacheOptions); var waitTime = DateTimeOffset.Now - startWait; - _dataService.WriteEvent($"Download started after wait time of {waitTime}. " + - $"ID: {downloadId}. " + - $"IP: {remoteIp}. " + - $"Current Downloads: {_downloadingAgents.Count}. Max Allowed: {_appConfig.MaxConcurrentUpdates}", EventType.Debug, null); + _logger.LogDebug( + "Download started after wait time of {waitTime}. " + + "ID: {downloadId}. " + + "IP: {remoteIp}. " + + "Current Downloads: {_downloadingAgentsCount}. Max Allowed: {_appConfigMaxConcurrentUpdates}", + waitTime, + downloadId, + remoteIp, + _downloadingAgents.Count, + _appConfig.MaxConcurrentUpdates); string filePath; @@ -115,11 +117,13 @@ namespace Remotely.Server.API filePath = Path.Combine(_hostEnv.WebRootPath, "Content", "Remotely-MacOS-x64.zip"); break; default: - _dataService.WriteEvent($"Unknown platform requested in {nameof(AgentUpdateController)}. " + - $"Platform: {platform}. " + - $"IP: {remoteIp}.", - EventType.Warning, - null); + _logger.LogWarning( + "Unknown platform requested in {className}. " + + "Platform: {platform}. " + + "IP: {remoteIp}.", + nameof(AgentUpdateController), + platform, + remoteIp); return BadRequest(); } @@ -130,7 +134,7 @@ namespace Remotely.Server.API catch (Exception ex) { _downloadingAgents.Remove(downloadId); - _dataService.WriteEvent(ex, null); + _logger.LogError(ex, "Error while downloading package."); return StatusCode((int)HttpStatusCode.InternalServerError); } } @@ -144,7 +148,7 @@ namespace Remotely.Server.API if (_appConfig.BannedDevices.Contains(deviceIp)) { - _dataService.WriteEvent($"Device IP ({deviceIp}) is banned. Sending uninstall command.", null); + _logger.LogInformation("Device IP ({deviceIp}) is banned. Sending uninstall command.", deviceIp); var bannedDevices = _serviceSessionCache.GetAllDevices().Where(x => x.PublicIP == deviceIp); diff --git a/Server/API/AlertsController.cs b/Server/API/AlertsController.cs index 8bc3286f..bc5e1990 100644 --- a/Server/API/AlertsController.cs +++ b/Server/API/AlertsController.cs @@ -1,4 +1,6 @@ using Microsoft.AspNetCore.Mvc; +using Microsoft.Build.Framework; +using Microsoft.Extensions.Logging; using Remotely.Server.Auth; using Remotely.Server.Services; using Remotely.Shared.Models; @@ -20,12 +22,18 @@ namespace Remotely.Server.API private readonly IDataService _dataService; private readonly IEmailSenderEx _emailSender; private readonly IHttpClientFactory _httpClientFactory; + private readonly ILogger _logger; - public AlertsController(IDataService dataService, IEmailSenderEx emailSender, IHttpClientFactory httpClientFactory) + public AlertsController( + IDataService dataService, + IEmailSenderEx emailSender, + IHttpClientFactory httpClientFactory, + ILogger logger) { _dataService = dataService; _emailSender = emailSender; _httpClientFactory = httpClientFactory; + _logger = logger; } [HttpPost("Create")] @@ -33,7 +41,7 @@ namespace Remotely.Server.API { Request.Headers.TryGetValue("OrganizationID", out var orgID); - _dataService.WriteEvent("Alert created. Alert Options: " + JsonSerializer.Serialize(alertOptions), orgID); + _logger.LogInformation("Alert created. Alert Options: {options}", JsonSerializer.Serialize(alertOptions)); if (alertOptions.ShouldAlert) { @@ -43,7 +51,7 @@ namespace Remotely.Server.API } catch (Exception ex) { - _dataService.WriteEvent(ex, orgID); + _logger.LogError(ex, "Error while adding alert."); } } @@ -58,7 +66,7 @@ namespace Remotely.Server.API } catch (Exception ex) { - _dataService.WriteEvent(ex, orgID); + _logger.LogError(ex, "Error while sending email."); } } @@ -81,11 +89,11 @@ namespace Remotely.Server.API } using var response = await httpClient.SendAsync(request); - _dataService.WriteEvent($"Alert API Response Status: {response.StatusCode}.", orgID); + _logger.LogInformation("Alert API Response Status: {responseStatusCode}.", response.StatusCode); } catch (Exception ex) { - _dataService.WriteEvent(ex, orgID); + _logger.LogError(ex, "Error while sending alert API request."); } } diff --git a/Server/API/LoginController.cs b/Server/API/LoginController.cs index 07b8dec0..da4af862 100644 --- a/Server/API/LoginController.cs +++ b/Server/API/LoginController.cs @@ -3,6 +3,8 @@ using Immense.RemoteControl.Server.Services; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.SignalR; +using Microsoft.Build.Framework; +using Microsoft.Extensions.Logging; using Remotely.Server.Hubs; using Remotely.Server.Models; using Remotely.Server.Services; @@ -24,6 +26,7 @@ namespace Remotely.Server.API private readonly IDesktopHubSessionCache _desktopSessionCache; private readonly SignInManager _signInManager; private readonly IHubContext _viewerHub; + private readonly ILogger _logger; public LoginController( SignInManager signInManager, @@ -31,7 +34,8 @@ namespace Remotely.Server.API IApplicationConfig appConfig, IHubContext casterHubContext, IDesktopHubSessionCache desktopSessionCache, - IHubContext viewerHubContext) + IHubContext viewerHubContext, + ILogger logger) { _signInManager = signInManager; _dataService = dataService; @@ -39,6 +43,7 @@ namespace Remotely.Server.API _desktopHub = casterHubContext; _desktopSessionCache = desktopSessionCache; _viewerHub = viewerHubContext; + _logger = logger; } [HttpGet("Logout")] @@ -60,7 +65,7 @@ namespace Remotely.Server.API } } await _signInManager.SignOutAsync(); - _dataService.WriteEvent($"API logout successful for {HttpContext?.User?.Identity?.Name}.", orgId); + _logger.LogInformation("API logout successful for {userName}.", HttpContext?.User?.Identity?.Name); return Ok(); } @@ -77,20 +82,20 @@ namespace Remotely.Server.API var result = await _signInManager.PasswordSignInAsync(login.Email, login.Password, false, true); if (result.Succeeded) { - _dataService.WriteEvent($"API login successful for {login.Email}.", orgId); + _logger.LogInformation("API login successful for {loginEmail}.", login.Email); return Ok(); } else if (result.IsLockedOut) { - _dataService.WriteEvent($"API login unsuccessful due to lockout for {login.Email}.", orgId); + _logger.LogInformation("API login unsuccessful due to lockout for {loginEmail}.", login.Email); return Unauthorized("Account is locked."); } else if (result.RequiresTwoFactor) { - _dataService.WriteEvent($"API login unsuccessful due to 2FA for {login.Email}.", orgId); + _logger.LogInformation("API login unsuccessful due to 2FA for {loginEmail}.", login.Email); return Unauthorized("Account requires two-factor authentication."); } - _dataService.WriteEvent($"API login unsuccessful due to bad attempt for {login.Email}.", orgId); + _logger.LogInformation("API login unsuccessful due to bad attempt for {loginEmail}.", login.Email); return BadRequest(); } } diff --git a/Server/API/RemoteControlController.cs b/Server/API/RemoteControlController.cs index 1f82b5de..69e0e901 100644 --- a/Server/API/RemoteControlController.cs +++ b/Server/API/RemoteControlController.cs @@ -15,6 +15,8 @@ using Immense.RemoteControl.Server.Services; using Remotely.Server.Services.RcImplementations; using Immense.RemoteControl.Server.Abstractions; using Immense.RemoteControl.Shared.Helpers; +using Microsoft.Build.Framework; +using Microsoft.Extensions.Logging; // For more information on enabling Web API for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860 @@ -32,6 +34,7 @@ namespace Remotely.Server.API private readonly IHubEventHandler _hubEvents; private readonly IDataService _dataService; private readonly SignInManager _signInManager; + private readonly ILogger _logger; public RemoteControlController( SignInManager signInManager, @@ -41,7 +44,8 @@ namespace Remotely.Server.API IServiceHubSessionCache serviceSessionCache, IOtpProvider otpProvider, IHubEventHandler hubEvents, - IApplicationConfig appConfig) + IApplicationConfig appConfig, + ILogger logger) { _dataService = dataService; _serviceHub = serviceHub; @@ -51,6 +55,7 @@ namespace Remotely.Server.API _otpProvider = otpProvider; _hubEvents = hubEvents; _signInManager = signInManager; + _logger = logger; } [HttpGet("{deviceID}")] @@ -75,20 +80,20 @@ namespace Remotely.Server.API if (result.Succeeded && _dataService.DoesUserHaveAccessToDevice(rcRequest.DeviceID, _dataService.GetUserByNameWithOrg(rcRequest.Email))) { - _dataService.WriteEvent($"API login successful for {rcRequest.Email}.", orgId); + _logger.LogInformation("API login successful for {rcRequestEmail}.", rcRequest.Email); return await InitiateRemoteControl(rcRequest.DeviceID, orgId); } else if (result.IsLockedOut) { - _dataService.WriteEvent($"API login unsuccessful due to lockout for {rcRequest.Email}.", orgId); + _logger.LogInformation("API login successful for {rcRequestEmail}.", rcRequest.Email); return Unauthorized("Account is locked."); } else if (result.RequiresTwoFactor) { - _dataService.WriteEvent($"API login unsuccessful due to 2FA for {rcRequest.Email}.", orgId); + _logger.LogInformation("API login successful for {rcRequestEmail}.", rcRequest.Email); return Unauthorized("Account requires two-factor authentication."); } - _dataService.WriteEvent($"API login unsuccessful due to bad attempt for {rcRequest.Email}.", orgId); + _logger.LogInformation("API login unsuccessful due to bad attempt for {rcRequestEmail}.", rcRequest.Email); return BadRequest(); } diff --git a/Server/API/ServerLogsController.cs b/Server/API/ServerLogsController.cs index aec96757..f154096e 100644 --- a/Server/API/ServerLogsController.cs +++ b/Server/API/ServerLogsController.cs @@ -1,9 +1,12 @@ using Microsoft.AspNetCore.Mvc; using Remotely.Server.Auth; -using Remotely.Server.Services; using System.Text; using System.Text.Json; using System; +using Microsoft.Extensions.Logging; +using Remotely.Server.Services; +using System.IO; +using System.Threading.Tasks; namespace Remotely.Server.API { @@ -11,23 +14,33 @@ namespace Remotely.Server.API [ApiController] public class ServerLogsController : ControllerBase { - private readonly IDataService _dataService; - private readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions() { WriteIndented = true }; + private readonly ILogsManager _logsManager; + private readonly ILogger _logger; - public ServerLogsController(IDataService dataService) + public ServerLogsController( + ILogsManager logsManager, + ILogger logger) { - _dataService = dataService; + _logsManager = logsManager; + _logger = logger; } [ServiceFilter(typeof(ApiAuthorizationFilter))] [HttpGet("Download")] - public ActionResult Download() + public async Task Download() { - Request.Headers.TryGetValue("OrganizationID", out var orgId); + _logger.LogInformation( + "Downloading server logs. Remote IP: {ip}", + HttpContext.Connection.RemoteIpAddress); - var logs = _dataService.GetAllEventLogs(User.Identity?.Name, orgId); - var fileBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(logs, _jsonOptions)); - return File(fileBytes, "application/octet-stream", "ServerLogs.json"); + var zipFile = await _logsManager.ZipAllLogs(); + Response.OnCompleted(() => + { + Directory.Delete(zipFile.DirectoryName, true); + return Task.CompletedTask; + }); + + return File(zipFile.OpenRead(), "application/octet-stream", zipFile.Name); } } } diff --git a/Server/Areas/Identity/Pages/Account/ForgotPassword.cshtml.cs b/Server/Areas/Identity/Pages/Account/ForgotPassword.cshtml.cs index 48eb78c9..56ecc341 100644 --- a/Server/Areas/Identity/Pages/Account/ForgotPassword.cshtml.cs +++ b/Server/Areas/Identity/Pages/Account/ForgotPassword.cshtml.cs @@ -12,6 +12,7 @@ using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.AspNetCore.WebUtilities; using Remotely.Shared.Models; using Remotely.Server.Services; +using Microsoft.Extensions.Logging; namespace Remotely.Server.Areas.Identity.Pages.Account { @@ -21,14 +22,18 @@ namespace Remotely.Server.Areas.Identity.Pages.Account private readonly UserManager _userManager; private readonly IEmailSenderEx _emailSender; private readonly IDataService _dataService; + private readonly ILogger _logger; - public ForgotPasswordModel(UserManager userManager, + public ForgotPasswordModel( + UserManager userManager, IEmailSenderEx emailSender, - IDataService dataService) + IDataService dataService, + ILogger logger) { _userManager = userManager; _emailSender = emailSender; _dataService = dataService; + _logger = logger; } [BindProperty] @@ -62,7 +67,8 @@ namespace Remotely.Server.Areas.Identity.Pages.Account values: new { area = "Identity", code }, protocol: Request.Scheme); - _dataService.WriteEvent($"Sending password reset for user {user.UserName}. Reset URL: {callbackUrl}", user.OrganizationID); + _logger.LogInformation( + "Sending password reset for user {username}. Reset URL: {callbackUrl}", user.UserName, callbackUrl); var emailResult = await _emailSender.SendEmailAsync( Input.Email, diff --git a/Server/Components/LoaderHarness.razor b/Server/Components/LoaderHarness.razor new file mode 100644 index 00000000..efdd347d --- /dev/null +++ b/Server/Components/LoaderHarness.razor @@ -0,0 +1,29 @@ +@using Nihs.SimpleMessenger; +@using Remotely.Server.Models.Messages; +@inject IMessenger Messenger + +@if (_loaderShown) +{ + +} + +@code { + private bool _loaderShown; + private string _statusMessage = string.Empty; + + protected override Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + Messenger.Register(this, HandleShowLoaderMessage); + } + return base.OnAfterRenderAsync(firstRender); + } + + private async Task HandleShowLoaderMessage(ShowLoaderMessage message) + { + _loaderShown = message.IsShown; + _statusMessage = message.StatusMessage; + await InvokeAsync(StateHasChanged); + } +} diff --git a/Server/Components/LoadingSignal.razor b/Server/Components/LoadingSignal.razor index b3d6fe9a..a05168c1 100644 --- a/Server/Components/LoadingSignal.razor +++ b/Server/Components/LoadingSignal.razor @@ -1,21 +1,31 @@ @inject IClientAppState AppState - -@if (_theme == Theme.Dark) -{ -
-} -else if (_theme == Theme.Light) -{ -
-} +
+
+ @if (!string.IsNullOrEmpty(StatusMessage)) + { +

+ @StatusMessage +

+ } +
+
+
@code { private Theme _theme; + [Parameter] + public string StatusMessage { get; set; } = string.Empty; + protected override async Task OnInitializedAsync() { - await base.OnInitializedAsync(); _theme = await AppState.GetEffectiveTheme(); + await base.OnInitializedAsync(); + } + + private string GetSignalClass() + { + return _theme == Theme.Dark ? "signal-dark" : "signal-light"; } } \ No newline at end of file diff --git a/Server/Components/LoadingSignal.razor.css b/Server/Components/LoadingSignal.razor.css index 74692f8e..ee7452f0 100644 --- a/Server/Components/LoadingSignal.razor.css +++ b/Server/Components/LoadingSignal.razor.css @@ -1,18 +1,42 @@ -.signal { +.signal-background { position: fixed; - top: 45vh; - left: calc(50% - 25px); + top: 0; + left: 0; + height: 100vh; + width: 100vw; + background-color: rgba(0,0,0,0.5); + z-index: 1000; +} + +.signal-wrapper { + position: absolute; + display: inline-block; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} + +.signal { + display: inline-block; border-width: 8px; border-style: solid; + margin-top: 10px; border-radius: 100%; height: 50px; width: 50px; opacity: 0; - position: absolute; + transform-origin: center; animation: pulsate .85s ease-out; animation-iteration-count: infinite; } + .signal.signal-dark { + border-color: whitesmoke; + } + .signal.signal-light { + border-color: #333; + } + @keyframes pulsate { 0% { transform: scale(.1); diff --git a/Server/Components/ModalHarness.razor b/Server/Components/ModalHarness.razor index cb079d2b..910aa2f6 100644 --- a/Server/Components/ModalHarness.razor +++ b/Server/Components/ModalHarness.razor @@ -49,8 +49,8 @@ { _displayStyle = "block"; await InvokeAsync(StateHasChanged); - // The fade animation won't work without a delay here. - await Task.Delay(100); + // The fade animation won't work without a delay here. + await Task.Delay(100); _showClass = "show"; await InvokeAsync(StateHasChanged); }; diff --git a/Server/Data/AppDb.cs b/Server/Data/AppDb.cs index 210c5a6b..39a0446d 100644 --- a/Server/Data/AppDb.cs +++ b/Server/Data/AppDb.cs @@ -28,7 +28,6 @@ namespace Remotely.Server.Data public DbSet BrandingInfos { get; set; } public DbSet DeviceGroups { get; set; } public DbSet Devices { get; set; } - public DbSet EventLogs { get; set; } public DbSet InviteLinks { get; set; } public DbSet Organizations { get; set; } public DbSet ScriptRuns { get; set; } @@ -58,9 +57,6 @@ namespace Remotely.Server.Data builder.Entity() .HasMany(x => x.RemotelyUsers) .WithOne(x => x.Organization); - builder.Entity() - .HasMany(x => x.EventLogs) - .WithOne(x => x.Organization); builder.Entity() .HasMany(x => x.DeviceGroups) .WithOne(x => x.Organization); diff --git a/Server/Hubs/AgentHub.cs b/Server/Hubs/AgentHub.cs index 5c9a1b3a..0658a714 100644 --- a/Server/Hubs/AgentHub.cs +++ b/Server/Hubs/AgentHub.cs @@ -23,6 +23,7 @@ namespace Remotely.Server.Hubs private readonly ICircuitManager _circuitManager; private readonly IDataService _dataService; private readonly IExpiringTokenService _expiringTokenService; + private readonly ILogger _logger; private readonly IServiceHubSessionCache _serviceSessionCache; private readonly IHubContext _viewerHubContext; @@ -31,7 +32,8 @@ namespace Remotely.Server.Hubs IServiceHubSessionCache serviceSessionCache, IHubContext viewerHubContext, ICircuitManager circuitManager, - IExpiringTokenService expiringTokenService) + IExpiringTokenService expiringTokenService, + ILogger logger) { _dataService = dataService; _serviceSessionCache = serviceSessionCache; @@ -39,6 +41,7 @@ namespace Remotely.Server.Hubs _appConfig = appConfig; _circuitManager = circuitManager; _expiringTokenService = expiringTokenService; + _logger = logger; } // TODO: Replace with new invoke capability in .NET 7 in ScriptingController. @@ -133,7 +136,7 @@ namespace Remotely.Server.Hubs } catch (Exception ex) { - _dataService.WriteEvent(ex, device?.OrganizationID); + _logger.LogError(ex, "Error while setting device to online status."); } Context.Abort(); @@ -286,7 +289,7 @@ namespace Remotely.Server.Hubs if (_appConfig.BannedDevices.Any(x => !string.IsNullOrWhiteSpace(x) && x.Equals(device, StringComparison.OrdinalIgnoreCase))) { - _dataService.WriteEvent($"Device ID/name/IP ({device}) is banned. Sending uninstall command.", null); + _logger.LogWarning("Device ID/name/IP ({device}) is banned. Sending uninstall command.", device); _ = Clients.Caller.SendAsync("UninstallAgent"); return true; diff --git a/Server/Hubs/CircuitConnection.cs b/Server/Hubs/CircuitConnection.cs index 40f063f4..307a472e 100644 --- a/Server/Hubs/CircuitConnection.cs +++ b/Server/Hubs/CircuitConnection.cs @@ -222,7 +222,10 @@ namespace Remotely.Server.Hubs if (!_dataService.DoesUserHaveAccessToDevice(deviceId, User)) { var device = _dataService.GetDevice(targetDevice.ID); - _dataService.WriteEvent($"Remote control attempted by unauthorized user. Device ID: {deviceId}. User Name: {User.UserName}.", EventType.Warning, device?.OrganizationID); + _logger.LogWarning( + "Remote control attempted by unauthorized user. Device ID: {deviceId}. User Name: {userName}.", + deviceId, + User.UserName); return Result.Fail("Unauthorized."); } @@ -414,13 +417,11 @@ namespace Remotely.Server.Hubs public Task UploadFiles(List fileIDs, string transferID, string[] deviceIDs) { - _dataService.WriteEvent(new EventLog() - { - EventType = EventType.Info, - Message = $"File transfer started by {User.UserName}. File transfer IDs: {string.Join(", ", fileIDs)}.", - TimeStamp = Time.Now, - OrganizationID = User.OrganizationID - }); + _logger.LogInformation( + "File transfer started by {userName}. File transfer IDs: {fileIds}.", + User.UserName, + string.Join(", ", fileIDs)); + deviceIDs = _dataService.FilterDeviceIDsByUserPermission(deviceIDs, User); var connections = GetActiveConnectionsForUserOrg(deviceIDs); foreach (var connection in connections) diff --git a/Server/Models/Messages/ShowLoaderMessage.cs b/Server/Models/Messages/ShowLoaderMessage.cs new file mode 100644 index 00000000..94537209 --- /dev/null +++ b/Server/Models/Messages/ShowLoaderMessage.cs @@ -0,0 +1,13 @@ +namespace Remotely.Server.Models.Messages; + +public class ShowLoaderMessage +{ + public ShowLoaderMessage(bool isShown, string statusMessage) + { + IsShown = isShown; + StatusMessage = statusMessage; + } + + public bool IsShown { get; } + public string StatusMessage { get; } +} \ No newline at end of file diff --git a/Server/Pages/ServerLogs.razor b/Server/Pages/ServerLogs.razor index cd6b4187..5f6156e8 100644 --- a/Server/Pages/ServerLogs.razor +++ b/Server/Pages/ServerLogs.razor @@ -1,10 +1,13 @@ @page "/server-logs" +@using System.Collections.ObjectModel; @attribute [Authorize] @inherits AuthComponentBase @inject IDataService DataService @inject IToastService ToastService @inject IJsInterop JsInterop +@inject ILogsManager LogsManager +@inject ILoaderService LoaderService

Server Logs

@@ -34,9 +37,9 @@
Type:
- - @foreach (var eventType in Enum.GetValues(typeof(EventType))) + @foreach (var eventType in Enum.GetValues(typeof(LogLevel))) { } @@ -45,51 +48,23 @@
Filter:
- +
From:
- +
To:
- +
- - - - - - - - - - - - @foreach (var eventLog in FilteredLogs) - { - - - - - - - - } - -
TypeTimestampMessageSourceStack Trace
@eventLog.EventType@eventLog.TimeStamp - @if (!string.IsNullOrWhiteSpace(eventLog.Message)) - { - foreach (var line in eventLog.Message.Split("\n", StringSplitOptions.RemoveEmptyEntries)) - { -
@line
- } - } -
@eventLog.Source@eventLog.StackTrace
+ } else { @@ -97,34 +72,104 @@ else } @code { - private readonly List _filteredLogs = new(); - private EventType? _eventType; - private string _messageFilter; + private readonly string _messageDebounceKey = Guid.NewGuid().ToString(); + private readonly List _filteredLogs = new(); + private LogLevel? _logLevel; + private string _messageFilter = string.Empty; + private DateTimeOffset _fromDate = DateTimeOffset.Now.AddDays(-7); private DateTimeOffset _toDate = DateTimeOffset.Now; - - - private IEnumerable FilteredLogs + private DateTimeOffset FromDate { - get + get => _fromDate; + set { - return DataService.GetEventLogs(User.UserName, - _fromDate, - _toDate, - _eventType, - _messageFilter); + if (value > _toDate) + { + ToastService.ShowToast("Invalid date range.", classString: "bg-warning"); + return; + } + _fromDate = value; + _ = RefreshLogs(); } } + private LogLevel? LogLevel + { + get => _logLevel; + set + { + _logLevel = value; + _ = RefreshLogs(); + } + } + + private string MessageFilter + { + get => _messageFilter; + set + { + _messageFilter = value; + + Debouncer.Debounce( + TimeSpan.FromSeconds(1), + () => _ = RefreshLogs(), + _messageDebounceKey); + } + } + + private DateTimeOffset ToDate + { + get => _toDate; + set + { + if (value < _fromDate) + { + ToastService.ShowToast("Invalid date range.", classString: "bg-warning"); + return; + } + _toDate = value; + _ = RefreshLogs(); + } + } + + protected override async Task OnInitializedAsync() + { + await RefreshLogs(); + await base.OnInitializedAsync(); + } + private async Task ClearAllLogs() { - var result = await JsInterop.Confirm("Are you sure you want to delete all logs?"); + var result = await JsInterop.Confirm("Are you sure you want to delete all previous logs? Today's logs are retained."); if (result) { - await DataService.ClearLogs(User.UserName); + using (var _ = LoaderService.ShowLoader("Deleting logs")) + { + await LogsManager.DeleteLogs(); + } + await RefreshLogs(); ToastService.ShowToast("Logs deleted."); } } + private async Task RefreshLogs() + { + using var _ = await LoaderService.ShowLoader("Refreshing logs"); + _filteredLogs.Clear(); + + var logsAsync = LogsManager.GetLogs( + _fromDate, + _toDate, + _messageFilter, + _logLevel); + + await foreach (var line in logsAsync) + { + _filteredLogs.Add(line); + } + + await InvokeAsync(StateHasChanged); + } } diff --git a/Server/Pages/ServerLogs.razor.css b/Server/Pages/ServerLogs.razor.css index 2aa1e48b..e01c6ec1 100644 --- a/Server/Pages/ServerLogs.razor.css +++ b/Server/Pages/ServerLogs.razor.css @@ -4,13 +4,18 @@ grid-column-gap: 2em; } - .filters-row { display: grid; grid-template-columns: auto auto auto 1fr; grid-column-gap: 1em; } +.logs-content { + width: 100%; + white-space: pre; + height: 500px; +} + @media (max-width: 641px) { .buttons-row { diff --git a/Server/Program.cs b/Server/Program.cs index 2c4b9f24..b40e5095 100644 --- a/Server/Program.cs +++ b/Server/Program.cs @@ -35,15 +35,16 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using Remotely.Shared.Services; using System; using Immense.RemoteControl.Server.Services; +using Serilog; +using Nihs.SimpleMessenger; var builder = WebApplication.CreateBuilder(args); var configuration = builder.Configuration; var services = builder.Services; -builder.Logging.ClearProviders(); +ConfigureSerilog(builder); + builder.Logging.AddConfiguration(builder.Configuration.GetSection("Logging")); -builder.Logging.AddConsole(); -builder.Logging.AddDebug(); if (OperatingSystem.IsWindows() && bool.TryParse(builder.Configuration["ApplicationOptions:EnableWindowsEventLog"], out var enableEventLog) && @@ -111,16 +112,21 @@ services.AddRazorPages(); services.AddServerSideBlazor(); services.AddScoped>(); services.AddDatabaseDeveloperPageExceptionFilter(); -services.AddHttpLogging(options => + +if (bool.TryParse(configuration["ApplicationOptions:UseHttpLogging"], out var useHttpLogging) && + useHttpLogging) { - options.RequestHeaders.Add("X-Forwarded-For"); - options.RequestHeaders.Add("X-Forwarded-Proto"); - options.RequestHeaders.Add("X-Forwarded-Host"); - options.RequestHeaders.Add("X-Original-For"); - options.RequestHeaders.Add("X-Original-Proto"); - options.RequestHeaders.Add("X-Original-Host"); - options.RequestHeaders.Add("Host"); -}); + services.AddHttpLogging(options => + { + options.RequestHeaders.Add("X-Forwarded-For"); + options.RequestHeaders.Add("X-Forwarded-Proto"); + options.RequestHeaders.Add("X-Forwarded-Host"); + options.RequestHeaders.Add("X-Original-For"); + options.RequestHeaders.Add("X-Original-Proto"); + options.RequestHeaders.Add("X-Original-Host"); + options.RequestHeaders.Add("Host"); + }); +} var trustedOrigins = configuration.GetSection("ApplicationOptions:TrustedCorsOrigins").Get(); @@ -183,13 +189,14 @@ services.AddSingleton(); services.AddScoped(); services.AddScoped(); services.AddScoped(); -services.AddHostedService(); +services.AddHostedService(); services.AddHostedService(); services.AddSingleton(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); +services.AddScoped(); services.AddScoped(x => (CircuitHandler)x.GetRequiredService()); services.AddSingleton(); services.AddScoped(); @@ -198,6 +205,8 @@ services.AddScoped(); services.AddScoped(); services.AddSingleton(); services.AddSingleton(); +services.AddSingleton(); +services.AddSingleton(WeakReferenceMessenger.Default); services.AddRemoteControlServer(config => { @@ -263,7 +272,6 @@ using (var scope = app.Services.CreateScope()) { using var context = scope.ServiceProvider.GetRequiredService(); var dataService = scope.ServiceProvider.GetRequiredService(); - var loggerFactory = scope.ServiceProvider.GetRequiredService(); if (context.Database.IsRelational()) { @@ -272,8 +280,6 @@ using (var scope = app.Services.CreateScope()) await dataService.SetAllDevicesNotOnline(); dataService.CleanupOldRecords(); - - loggerFactory.AddProvider(new DbLoggerProvider(app.Environment, app.Services)); } await app.RunAsync(); @@ -313,4 +319,46 @@ void ConfigureStaticFiles() ServeUnknownFileTypes = true }); } +} + +void ConfigureSerilog(WebApplicationBuilder webAppBuilder) +{ + try + { + var dataRetentionDays = 7; + if (int.TryParse(webAppBuilder.Configuration["ApplicationOptions:DataRetentionInDays"], out var retentionSetting)) + { + dataRetentionDays = retentionSetting; + } + + var logPath = LogsManager.DefaultLogsDirectory; + + void ApplySharedLoggerConfig(LoggerConfiguration loggerConfiguration) + { + loggerConfiguration + .Enrich.FromLogContext() + .WriteTo.Console() + .WriteTo.File($"{logPath}/Remotely_Server.log", + rollingInterval: RollingInterval.Day, + retainedFileTimeLimit: TimeSpan.FromDays(dataRetentionDays), + shared: true); + } + + var loggerConfig = new LoggerConfiguration(); + ApplySharedLoggerConfig(loggerConfig); + Log.Logger = loggerConfig.CreateBootstrapLogger(); + + builder.Host.UseSerilog((context, services, configuration) => + { + configuration + .ReadFrom.Configuration(context.Configuration) + .ReadFrom.Services(services); + + ApplySharedLoggerConfig(configuration); + }); + } + catch (Exception ex) + { + Console.WriteLine($"Failed to configure Serilog file logging. Error: {ex.Message}"); + } } \ No newline at end of file diff --git a/Server/Server.csproj b/Server/Server.csproj index d1549c8f..df99d5f6 100644 --- a/Server/Server.csproj +++ b/Server/Server.csproj @@ -31,7 +31,9 @@ + + diff --git a/Server/Services/DataCleanupService.cs b/Server/Services/DataCleanupService.cs new file mode 100644 index 00000000..2cad7999 --- /dev/null +++ b/Server/Services/DataCleanupService.cs @@ -0,0 +1,62 @@ +using Microsoft.Build.Framework; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Remotely.Server.Services +{ + public class DataCleanupService : IHostedService, IDisposable + { + private readonly ILogger _logger; + + private readonly IServiceProvider _services; + + private System.Timers.Timer _cleanupTimer = new(TimeSpan.FromDays(1)); + + + public DataCleanupService( + IServiceProvider serviceProvider, + ILogger logger) + { + _services = serviceProvider; + _logger = logger; + + _cleanupTimer.Elapsed += CleanupTimer_Elapsed; + } + public void Dispose() + { + _cleanupTimer?.Dispose(); + GC.SuppressFinalize(this); + } + + public Task StartAsync(CancellationToken cancellationToken) + { + _cleanupTimer.Start(); + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + _cleanupTimer.Stop(); + _cleanupTimer.Dispose(); + return Task.CompletedTask; + } + + private void CleanupTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) + { + try + { + using var scope = _services.CreateScope(); + var dataService = scope.ServiceProvider.GetRequiredService(); + dataService.CleanupOldRecords(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error during data cleanup."); + } + } + } +} diff --git a/Server/Services/DataService.cs b/Server/Services/DataService.cs index 8c8c7f5a..7d24dbae 100644 --- a/Server/Services/DataService.cs +++ b/Server/Services/DataService.cs @@ -51,8 +51,6 @@ namespace Remotely.Server.Services void CleanupOldRecords(); - Task ClearLogs(string currentUserName); - Task CreateApiToken(string userName, string tokenName, string secretHash); Task CreateDevice(DeviceSetupOptions options); @@ -101,8 +99,6 @@ namespace Remotely.Server.Services Device[] GetAllDevices(string orgID); - EventLog[] GetAllEventLogs(string username, string orgId); - InviteLink[] GetAllInviteLinks(string organizationId); ScriptResult[] GetAllScriptResults(string orgId, string deviceId); @@ -136,8 +132,6 @@ namespace Remotely.Server.Services Device[] GetDevicesForUser(string userName); - EventLog[] GetEventLogs(string userName, DateTimeOffset from, DateTimeOffset to, EventType? type, string message); - Organization GetOrganizationById(string organizationID); Task GetOrganizationByUserName(string userName); @@ -222,16 +216,6 @@ namespace Remotely.Server.Services void UpdateUserOptions(string userName, RemotelyUserOptions options); bool ValidateApiKey(string keyId, string apiSecret, string requestPath, string remoteIP); - - void WriteEvent(EventLog eventLog); - - void WriteEvent(Exception ex, string organizationID); - - void WriteEvent(string message, EventType eventType, string organizationID); - - void WriteEvent(string message, string organizationID); - - void WriteLog(LogLevel logLevel, string category, EventId eventId, string state, Exception exception, string[] scopeStack); } public class DataService : IDataService @@ -239,15 +223,18 @@ namespace Remotely.Server.Services private readonly IApplicationConfig _appConfig; private readonly IHostEnvironment _hostEnvironment; private readonly IAppDbFactory _appDbFactory; + private readonly ILogger _logger; public DataService( IApplicationConfig appConfig, IHostEnvironment hostEnvironment, - IAppDbFactory appDbFactory) + IAppDbFactory appDbFactory, + ILogger logger) { _appConfig = appConfig; _hostEnvironment = hostEnvironment; _appDbFactory = appDbFactory; + _logger = logger; } public async Task AddAlert(string deviceId, string organizationID, string alertMessage, string details = null) @@ -375,13 +362,13 @@ namespace Remotely.Server.Services if (!dbContext.Organizations.Any(x => x.ID == device.OrganizationID)) { - WriteEvent(new EventLog() - { - EventType = EventType.Info, - Message = $"Unable to add device {device.DeviceName} because organization {device.OrganizationID}" + - $"does not exist. Device ID: {device.ID}.", - Source = "DataService.AddOrUpdateDevice" - }); + _logger.LogInformation( + "Unable to add device {deviceName} because organization {organizationID}" + + "does not exist. Device ID: {ID}.", + device.DeviceName, + device.OrganizationID, + device.ID); + return false; } dbContext.Devices.Add(device); @@ -622,11 +609,6 @@ namespace Remotely.Server.Services dbContext.RemoveRange(scriptRuns); - var eventLogs = dbContext.EventLogs - .Where(x => x.TimeStamp < expirationDate); - - dbContext.RemoveRange(eventLogs); - var commandResults = dbContext.ScriptResults .Where(x => x.TimeStamp < expirationDate); @@ -641,36 +623,6 @@ namespace Remotely.Server.Services } } - public async Task ClearLogs(string currentUserName) - { - using var dbContext = _appDbFactory.GetContext(); - - var currentUser = await dbContext.Users.FirstOrDefaultAsync(x => x.UserName == currentUserName); - if (currentUser is null) - { - return; - } - - try - { - if (currentUser.IsServerAdmin) - { - await dbContext.EventLogs.ExecuteDeleteAsync(); - } - else - { - var eventLogs = dbContext.EventLogs.Where(x => x.OrganizationID == currentUser.OrganizationID); - await eventLogs.ExecuteDeleteAsync(); - } - - await dbContext.SaveChangesAsync(); - } - catch (Exception ex) - { - WriteEvent(ex, currentUser.OrganizationID); - } - } - public async Task CreateApiToken(string userName, string tokenName, string secretHash) { using var dbContext = _appDbFactory.GetContext(); @@ -729,7 +681,7 @@ namespace Remotely.Server.Services } catch (Exception ex) { - WriteEvent(ex, options.OrganizationID); + _logger.LogError(ex, "error while creating device for organization {id}.", options.OrganizationID); return null; } } @@ -759,7 +711,7 @@ namespace Remotely.Server.Services } catch (Exception ex) { - WriteEvent(ex, organizationID); + _logger.LogError(ex, "Error while creating user for organization {id}.", organizationID); return false; } } @@ -1071,31 +1023,6 @@ namespace Remotely.Server.Services return dbContext.Devices.Where(x => x.OrganizationID == orgID).ToArray(); } - public EventLog[] GetAllEventLogs(string username, string orgId) - { - using var dbContext = _appDbFactory.GetContext(); - - var query = dbContext.EventLogs - .AsNoTracking() - .AsQueryable(); - - if (!string.IsNullOrWhiteSpace(username)) - { - var user = dbContext.Users.FirstOrDefault(x => x.UserName == username); - if (user?.IsAdministrator == true) - { - return query - .OrderByDescending(x => x.TimeStamp) - .ToArray(); - } - } - - return query - .Where(x => x.OrganizationID == orgId) - .OrderByDescending(x => x.TimeStamp) - .ToArray(); - } - public InviteLink[] GetAllInviteLinks(string organizationId) { using var dbContext = _appDbFactory.GetContext(); @@ -1334,45 +1261,6 @@ namespace Remotely.Server.Services .ToArray(); } - public EventLog[] GetEventLogs(string userName, DateTimeOffset from, DateTimeOffset to, EventType? type, string message) - { - using var dbContext = _appDbFactory.GetContext(); - - var user = dbContext.Users - .FirstOrDefault(x => x.UserName == userName); - - var query = dbContext.EventLogs - .AsNoTracking() - .AsQueryable(); - - var fromDate = from.Date; - var toDate = to.Date.AddDays(1); - - if (user.IsServerAdmin) - { - query = query.Where(x => x.TimeStamp >= fromDate && x.TimeStamp <= toDate) - .OrderByDescending(x => x.TimeStamp); - } - else - { - var orgID = user.OrganizationID; - query = query - .Where(x => x.OrganizationID == orgID && - x.TimeStamp >= fromDate && x.TimeStamp <= toDate) - .OrderByDescending(x => x.TimeStamp); - } - if (type != null) - { - query = query.Where(x => x.EventType == type); - } - if (!string.IsNullOrWhiteSpace(message)) - { - message = message.ToLower(); - query = query.Where(x => x.Message.ToLower().Contains(message)); - } - return query.ToArray(); - } - public Organization GetOrganizationById(string organizationID) { using var dbContext = _appDbFactory.GetContext(); @@ -1966,118 +1854,16 @@ namespace Remotely.Server.Services dbContext.SaveChanges(); } - WriteEvent($"API token used. Token: {keyId}. Path: {requestPath}. Validated: {isValid}. Remote IP: {remoteIP}", EventType.Info, token?.OrganizationID); + _logger.LogInformation( + "API token used. Token: {keyId}. Path: {requestPath}. Validated: {isValid}. Remote IP: {remoteIP}", + keyId, + requestPath, + isValid, + remoteIP); return isValid; } - public void WriteEvent(EventLog eventLog) - { - try - { - using var dbContext = _appDbFactory.GetContext(); - - dbContext.EventLogs.Add(eventLog); - dbContext.SaveChanges(); - } - catch { } - } - - public void WriteEvent(Exception ex, string organizationID) - { - try - { - using var dbContext = _appDbFactory.GetContext(); - - dbContext.EventLogs.Add(new EventLog() - { - EventType = EventType.Error, - Message = ex.Message, - Source = ex.Source, - StackTrace = ex.StackTrace, - TimeStamp = DateTimeOffset.Now, - OrganizationID = organizationID - }); - dbContext.SaveChanges(); - } - catch { } - } - - public void WriteEvent(string message, string organizationID) - { - WriteEvent(message, EventType.Info, organizationID); - } - - public void WriteEvent(string message, EventType eventType, string organizationID) - { - try - { - using var dbContext = _appDbFactory.GetContext(); - - dbContext.EventLogs.Add(new EventLog() - { - EventType = eventType, - Message = message, - TimeStamp = DateTimeOffset.Now, - OrganizationID = organizationID - }); - dbContext.SaveChanges(); - } - catch { } - } - - public void WriteLog(LogLevel logLevel, string category, EventId eventId, string state, Exception exception, string[] scopeStack) - { - // Prevent re-entrancy. - if (eventId.Name?.Contains("EntityFrameworkCore") == true) - { - return; - } - - try - { - // TODO: Refactor EventLog to resemble these params. Replace WriteEvent with ILogger. - using var dbContext = _appDbFactory.GetContext(); - - EventType eventType = EventType.Debug; - switch (logLevel) - { - case LogLevel.None: - case LogLevel.Trace: - case LogLevel.Debug: - eventType = EventType.Debug; - break; - - case LogLevel.Information: - eventType = EventType.Info; - break; - - case LogLevel.Warning: - eventType = EventType.Warning; - break; - - case LogLevel.Error: - case LogLevel.Critical: - eventType = EventType.Error; - break; - - default: - break; - } - - dbContext.EventLogs.Add(new EventLog() - { - StackTrace = exception?.StackTrace, - EventType = eventType, - Message = $"[{logLevel}] [{string.Join(" - ", scopeStack)} - {category}] | Message: {state} | Exception: {exception?.Message}", - TimeStamp = DateTimeOffset.Now - }); - - dbContext.SaveChanges(); - } - catch { } - } - private async Task AddSharedFileInternal( string fileName, byte[] fileContents, diff --git a/Server/Services/DbCleanupService.cs b/Server/Services/DbCleanupService.cs deleted file mode 100644 index c186fa84..00000000 --- a/Server/Services/DbCleanupService.cs +++ /dev/null @@ -1,48 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace Remotely.Server.Services -{ - public class DbCleanupService : IHostedService, IDisposable - { - public DbCleanupService(IServiceProvider serviceProvider) - { - Services = serviceProvider; - } - - private IServiceProvider Services { get; } - private System.Timers.Timer CleanupTimer { get; set; } - - public void Dispose() - { - CleanupTimer?.Dispose(); - GC.SuppressFinalize(this); - } - - public Task StartAsync(CancellationToken cancellationToken) - { - CleanupTimer?.Dispose(); - CleanupTimer = new System.Timers.Timer(TimeSpan.FromDays(1).TotalMilliseconds); - CleanupTimer.Elapsed += CleanupTimer_Elapsed; - CleanupTimer.Start(); - return Task.CompletedTask; - } - - public Task StopAsync(CancellationToken cancellationToken) - { - CleanupTimer?.Dispose(); - return Task.CompletedTask; - } - - private void CleanupTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) - { - using var scope = Services.CreateScope(); - var dataService = scope.ServiceProvider.GetRequiredService(); - - dataService.CleanupOldRecords(); - } - } -} diff --git a/Server/Services/DbLogger.cs b/Server/Services/DbLogger.cs deleted file mode 100644 index 5bc91a66..00000000 --- a/Server/Services/DbLogger.cs +++ /dev/null @@ -1,83 +0,0 @@ -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Threading; - -namespace Remotely.Server.Services -{ - public class DbLogger : ILogger - { - private readonly string _categoryName; - private readonly IWebHostEnvironment _hostEnvironment; - private readonly IServiceProvider _serviceProvider; - - protected static ConcurrentStack ScopeStack { get; } = new ConcurrentStack(); - - public DbLogger(string categoryName, IWebHostEnvironment hostEnvironment, IServiceProvider serviceProvider) - { - _categoryName = categoryName; - _hostEnvironment = hostEnvironment; - _serviceProvider = serviceProvider; - } - - public IDisposable BeginScope(TState state) - { - ScopeStack.Push(state.ToString()); - return new NoopDisposable(); - } - - public bool IsEnabled(LogLevel logLevel) - { - switch (logLevel) - { - case LogLevel.Trace: - break; - case LogLevel.Debug: - case LogLevel.Information: - if (_hostEnvironment.IsDevelopment()) - { - return true; - } - break; - case LogLevel.Warning: - case LogLevel.Error: - case LogLevel.Critical: - return true; - case LogLevel.None: - break; - default: - break; - } - return false; - } - - public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) - { - using var scope = _serviceProvider.CreateScope(); - var dataService = scope.ServiceProvider.GetRequiredService(); - - var scopeStack = ScopeStack.Any() ? - new string[] { ScopeStack.FirstOrDefault(), ScopeStack.LastOrDefault() } : - Array.Empty(); - - dataService.WriteLog(logLevel, _categoryName, eventId, state.ToString(), exception, scopeStack); - } - - - private class NoopDisposable : IDisposable - { - public void Dispose() - { - while (!ScopeStack.TryPop(out _)) - { - Thread.Sleep(100); - } - } - } - } -} diff --git a/Server/Services/DbLoggerProvider.cs b/Server/Services/DbLoggerProvider.cs deleted file mode 100644 index 739fa324..00000000 --- a/Server/Services/DbLoggerProvider.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Logging; -using System; - -namespace Remotely.Server.Services -{ - public class DbLoggerProvider : ILoggerProvider - { - private readonly IWebHostEnvironment _hostEnvironment; - private readonly IServiceProvider _serviceProvider; - - public DbLoggerProvider(IWebHostEnvironment hostEnvironment, IServiceProvider serviceProvider) - { - _hostEnvironment = hostEnvironment; - _serviceProvider = serviceProvider; - } - - - public ILogger CreateLogger(string categoryName) - { - return new DbLogger(categoryName, _hostEnvironment, _serviceProvider); - } - - public void Dispose() - { - GC.SuppressFinalize(this); - } - } -} diff --git a/Server/Services/EmailSender.cs b/Server/Services/EmailSender.cs index ed55d924..4417f061 100644 --- a/Server/Services/EmailSender.cs +++ b/Server/Services/EmailSender.cs @@ -1,6 +1,8 @@ using MailKit.Net.Smtp; using MailKit.Security; using Microsoft.AspNetCore.Identity.UI.Services; +using Microsoft.Build.Framework; +using Microsoft.Extensions.Logging; using MimeKit; using MimeKit.Text; using System; @@ -15,67 +17,6 @@ namespace Remotely.Server.Services Task SendEmailAsync(string email, string subject, string htmlMessage, string organizationID = null); } - public class EmailSenderEx : IEmailSenderEx - { - public EmailSenderEx(IApplicationConfig appConfig, IDataService dataService) - { - AppConfig = appConfig; - DataService = dataService; - } - - private IApplicationConfig AppConfig { get; } - private IDataService DataService { get; } - - public async Task SendEmailAsync(string toEmail, string replyTo, string subject, string htmlMessage, string organizationID = null) - { - try - { - var message = new MimeMessage(); - message.From.Add(new MailboxAddress(AppConfig.SmtpDisplayName, AppConfig.SmtpEmail)); - message.To.Add(MailboxAddress.Parse(toEmail)); - message.ReplyTo.Add(MailboxAddress.Parse(replyTo)); - message.Subject = subject; - message.Body = new TextPart(TextFormat.Html) - { - Text = htmlMessage - }; - - using var client = new SmtpClient(); - - if (!string.IsNullOrWhiteSpace(AppConfig.SmtpLocalDomain)) - { - client.LocalDomain = AppConfig.SmtpLocalDomain; - } - - client.CheckCertificateRevocation = AppConfig.SmtpCheckCertificateRevocation; - - await client.ConnectAsync(AppConfig.SmtpHost, AppConfig.SmtpPort); - - if (!string.IsNullOrWhiteSpace(AppConfig.SmtpUserName) && - !string.IsNullOrWhiteSpace(AppConfig.SmtpPassword)) - { - await client.AuthenticateAsync(AppConfig.SmtpUserName, AppConfig.SmtpPassword); - } - - await client.SendAsync(message); - await client.DisconnectAsync(true); - - DataService.WriteEvent($"Email successfully sent to {toEmail}. Subject: \"{subject}\".", organizationID); - - return true; - } - catch (Exception ex) - { - DataService.WriteEvent(ex, organizationID); - return false; - } - } - - public Task SendEmailAsync(string email, string subject, string htmlMessage, string organizationID = null) - { - return SendEmailAsync(email, AppConfig.SmtpEmail, subject, htmlMessage, organizationID); - } - } public class EmailSender : IEmailSender { public EmailSender(IEmailSenderEx emailSenderEx) @@ -91,4 +32,66 @@ namespace Remotely.Server.Services } } + public class EmailSenderEx : IEmailSenderEx + { + private readonly IApplicationConfig _appConfig; + private readonly ILogger _logger; + + public EmailSenderEx( + IApplicationConfig appConfig, + ILogger logger) + { + _appConfig = appConfig; + _logger = logger; + } + public async Task SendEmailAsync(string toEmail, string replyTo, string subject, string htmlMessage, string organizationID = null) + { + try + { + var message = new MimeMessage(); + message.From.Add(new MailboxAddress(_appConfig.SmtpDisplayName, _appConfig.SmtpEmail)); + message.To.Add(MailboxAddress.Parse(toEmail)); + message.ReplyTo.Add(MailboxAddress.Parse(replyTo)); + message.Subject = subject; + message.Body = new TextPart(TextFormat.Html) + { + Text = htmlMessage + }; + + using var client = new SmtpClient(); + + if (!string.IsNullOrWhiteSpace(_appConfig.SmtpLocalDomain)) + { + client.LocalDomain = _appConfig.SmtpLocalDomain; + } + + client.CheckCertificateRevocation = _appConfig.SmtpCheckCertificateRevocation; + + await client.ConnectAsync(_appConfig.SmtpHost, _appConfig.SmtpPort); + + if (!string.IsNullOrWhiteSpace(_appConfig.SmtpUserName) && + !string.IsNullOrWhiteSpace(_appConfig.SmtpPassword)) + { + await client.AuthenticateAsync(_appConfig.SmtpUserName, _appConfig.SmtpPassword); + } + + await client.SendAsync(message); + await client.DisconnectAsync(true); + + _logger.LogInformation("Email successfully sent to {toEmail}. Subject: \"{subject}\".", toEmail, subject); + + return true; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error while sending email."); + return false; + } + } + + public Task SendEmailAsync(string email, string subject, string htmlMessage, string organizationID = null) + { + return SendEmailAsync(email, _appConfig.SmtpEmail, subject, htmlMessage, organizationID); + } + } } diff --git a/Server/Services/LoaderService.cs b/Server/Services/LoaderService.cs new file mode 100644 index 00000000..37d87437 --- /dev/null +++ b/Server/Services/LoaderService.cs @@ -0,0 +1,34 @@ +using Immense.RemoteControl.Shared.Primitives; +using Nihs.SimpleMessenger; +using Remotely.Server.Models.Messages; +using System; +using System.Threading.Tasks; + +namespace Remotely.Server.Services; + +public interface ILoaderService +{ + Task ShowLoader(string statusMessage); + void HideLoader(); +} + +public class LoaderService : ILoaderService +{ + private readonly IMessenger _messenger; + + public LoaderService(IMessenger messenger) + { + _messenger = messenger; + } + + public async Task ShowLoader(string statusMessage) + { + await _messenger.Send(new ShowLoaderMessage(true, statusMessage)); + return new CallbackDisposable(HideLoader); + } + + public void HideLoader() + { + _messenger.Send(new ShowLoaderMessage(false, string.Empty)); + } +} \ No newline at end of file diff --git a/Server/Services/LogsManager.cs b/Server/Services/LogsManager.cs new file mode 100644 index 00000000..4dc0fba9 --- /dev/null +++ b/Server/Services/LogsManager.cs @@ -0,0 +1,209 @@ +using Immense.RemoteControl.Shared.Services; +using Microsoft.Extensions.Logging; +using Remotely.Shared.Extensions; +using Serilog; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Remotely.Server.Services +{ + public interface ILogsManager + { + Task DeleteLogs(); + + IAsyncEnumerable GetLogs( + DateTimeOffset startDate, + DateTimeOffset endDate, + string messageFilter, + LogLevel? logLevelFilter); + + string GetLogsDirectory(); + Task ZipAllLogs(); + } + + public class LogsManager : ILogsManager + { + private static readonly ReadOnlyDictionary _logLevelMap = new(new Dictionary() + { + ["[VRB]"] = LogLevel.Trace, + ["[DBG]"] = LogLevel.Debug, + ["[INF]"] = LogLevel.Information, + ["[WRN]"] = LogLevel.Warning, + ["[ERR]"] = LogLevel.Error, + ["[FTL]"] = LogLevel.Critical + }); + + public static string DefaultLogsDirectory + { + get + { + var logsDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "logs"); + if (Directory.Exists("/remotely-data")) + { + logsDir = "/remotely-data/logs"; + } + return logsDir; + } + } + + public async Task DeleteLogs() + { + var logsDir = GetLogsDirectory(); + + var files = Directory.GetFiles(logsDir); + + if (!files.Any()) + { + return; + } + + await foreach (var file in files.ToAsyncEnumerable()) + { + try + { + if (new FileInfo(file).LastWriteTime.Date == DateTime.Today) + { + continue; + } + File.Delete(file); + } + catch (Exception ex) + { + Console.WriteLine($"Failed to delete log file: {file}. Message: {ex.Message}"); + } + } + } + + public async IAsyncEnumerable GetLogs( + DateTimeOffset startDate, + DateTimeOffset endDate, + string messageFilter, + LogLevel? logLevelFilter) + { + var fromDate = startDate.UtcDateTime.Date; + var toDate = endDate.UtcDateTime.Date.AddDays(1); + + var result = new StringBuilder(); + var logsDir = GetLogsDirectory(); + + var files = Directory + .GetFiles(logsDir) + .Select(x => new FileInfo(x)) + .Where(x => + x.LastWriteTimeUtc >= fromDate && + x.LastWriteTimeUtc <= toDate) + .OrderBy(x => x.LastWriteTimeUtc); + + foreach (var file in files) + { + var linesAsync = GetLines(file, messageFilter, logLevelFilter); + await foreach (var line in linesAsync) + { + yield return line; + } + + } + } + + public string GetLogsDirectory() + { + return Directory.CreateDirectory(DefaultLogsDirectory).FullName; + } + + public async Task ZipAllLogs() + { + var logsDir = GetLogsDirectory(); + var baseDir = AppDomain.CurrentDomain.BaseDirectory; + var tempDir = Directory.CreateDirectory(Path.Combine(baseDir, "temp", Guid.NewGuid().ToString())).FullName; + var zipFilePath = Path.Combine( + tempDir, + $"Remotely_Logs-{DateTimeOffset.Now:yyyy-MM-dd-HH-mm-ss}.zip"); + + using var zipArchive = ZipFile.Open(zipFilePath, ZipArchiveMode.Update); + + var files = Directory.GetFiles(logsDir); + + foreach (var file in files) + { + var entry = zipArchive.CreateEntry(Path.GetFileName(file)); + using var entryStream = entry.Open(); + using var fs = File.Open(file, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + await fs.CopyToAsync(entryStream); + } + + return new FileInfo(zipFilePath); + } + private async IAsyncEnumerable GetLines( + FileInfo file, + string messageFilter, + LogLevel? logLevelFilter) + { + LogLevel? currentLogLevel = null; + + using var fs = File.Open(file.FullName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + using var sr = new StreamReader(fs); + + while (true) + { + var currentLine = await sr.ReadLineAsync(); + + if (currentLine is null) + { + break; + } + + if (logLevelFilter is not null) + { + if (TryGetLogLevel(currentLine, out var parsedLevel)) + { + currentLogLevel = parsedLevel; + } + + if (currentLogLevel != logLevelFilter) + { + continue; + } + } + + if (!string.IsNullOrWhiteSpace(messageFilter) && + !currentLine.Contains(messageFilter, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + yield return currentLine; + } + } + + private bool TryGetLogLevel( + string lineContent, + [NotNullWhen(true)] out LogLevel? logLevel) + { + try + { + var logLevelTag = lineContent[31..36]; + if (_logLevelMap.TryGetValue(logLevelTag, out var result)) + { + logLevel = result; + return true; + } + } + catch + { + // Ignored. + } + + logLevel = default; + return false; + } + } +} diff --git a/Server/Services/RcImplementations/HubEventHandler.cs b/Server/Services/RcImplementations/HubEventHandler.cs index e98381bb..456f14a1 100644 --- a/Server/Services/RcImplementations/HubEventHandler.cs +++ b/Server/Services/RcImplementations/HubEventHandler.cs @@ -70,32 +70,24 @@ namespace Remotely.Server.Services.RcImplementations return Task.CompletedTask; } - switch (reason) - { - case SessionSwitchReasonEx.ConsoleDisconnect: - case SessionSwitchReasonEx.RemoteConnect: - case SessionSwitchReasonEx.RemoteDisconnect: - case SessionSwitchReasonEx.SessionLogoff: - case SessionSwitchReasonEx.SessionLock: - case SessionSwitchReasonEx.SessionRemoteControl: - return _serviceHub.Clients - .Client(ex.AgentConnectionId) - .SendAsync("RestartScreenCaster", - ex.ViewerList, - ex.UnattendedSessionId, - ex.AccessKey, - ex.UserConnectionId, - ex.RequesterUserName, - ex.OrganizationName, - ex.OrganizationId); - case SessionSwitchReasonEx.ConsoleConnect: - case SessionSwitchReasonEx.SessionUnlock: - case SessionSwitchReasonEx.SessionLogon: - default: - break; - } + _logger.LogDebug("Windows session changed during remote control. " + + "Reason: {reason}. " + + "Current Session ID: {sessionId}. " + + "Session Info: {@sesisonInfo}", + reason, + currentSessionId, + session); - return Task.CompletedTask; + return _serviceHub.Clients + .Client(ex.AgentConnectionId) + .SendAsync("RestartScreenCaster", + ex.ViewerList, + ex.UnattendedSessionId, + ex.AccessKey, + ex.UserConnectionId, + ex.RequesterUserName, + ex.OrganizationName, + ex.OrganizationId); } public Task RestartScreenCaster(RemoteControlSession session, HashSet viewerList) diff --git a/Server/Services/ScriptScheduleDispatcher.cs b/Server/Services/ScriptScheduleDispatcher.cs index 3b71e450..7705820d 100644 --- a/Server/Services/ScriptScheduleDispatcher.cs +++ b/Server/Services/ScriptScheduleDispatcher.cs @@ -38,13 +38,13 @@ namespace Remotely.Server.Services { try { - _logger.LogInformation("Script Schedule Dispatcher started."); + _logger.LogDebug("Script Schedule Dispatcher started."); var schedules = await _dataService.GetScriptSchedulesDue(); if (schedules?.Any() != true) { - _logger.LogInformation("No schedules are due."); + _logger.LogDebug("No schedules are due."); return; } @@ -52,18 +52,18 @@ namespace Remotely.Server.Services { try { - _logger.LogInformation("Considering {scheduleName}. Interval: {interval}. Next Run: {nextRun}.", + _logger.LogDebug("Considering {scheduleName}. Interval: {interval}. Next Run: {nextRun}.", schedule.Name, schedule.Interval, schedule.NextRun); if (!AdvanceSchedule(schedule)) { - _logger.LogInformation("Schedule is not due."); + _logger.LogDebug("Schedule is not due."); continue; } - _logger.LogInformation("Creating script run for schedule {scheduleName}.", schedule.Name); + _logger.LogDebug("Creating script run for schedule {scheduleName}.", schedule.Name); var scriptRun = new ScriptRun() { @@ -100,7 +100,7 @@ namespace Remotely.Server.Services await _circuitConnection.RunScript(onlineDevices, schedule.SavedScriptId, scriptRun.Id, ScriptInputType.ScheduledScript, true); - _logger.LogInformation("Created script run for schedule {scheduleName}.", schedule.Name); + _logger.LogDebug("Created script run for schedule {scheduleName}.", schedule.Name); schedule.LastRun = Time.Now; await _dataService.AddOrUpdateScriptSchedule(schedule); diff --git a/Server/Shared/MainLayout.razor b/Server/Shared/MainLayout.razor index 9f69fb50..412c0663 100644 --- a/Server/Shared/MainLayout.razor +++ b/Server/Shared/MainLayout.razor @@ -16,7 +16,7 @@ - + diff --git a/Server/Shared/NavMenu.razor b/Server/Shared/NavMenu.razor index a14fe9a4..e56e0b4b 100644 --- a/Server/Shared/NavMenu.razor +++ b/Server/Shared/NavMenu.razor @@ -83,14 +83,14 @@ API Keys - @if (_user?.IsServerAdmin == true) { +