Merge pull request #649 from immense/jaredg-respect-httplogging-setting

Replace DbLogger in the server with Serilog.
This commit is contained in:
Jared Goodwin 2023-05-23 14:55:27 -07:00 committed by GitHub
commit 03862957be
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 837 additions and 712 deletions

View File

@ -27,32 +27,28 @@ namespace Remotely.Server.API
private readonly IHubContext<AgentHub> _agentHubContext;
private readonly ILogger<AgentUpdateController> _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<AgentHub> agentHubContext)
IHubContext<AgentHub> agentHubContext,
ILogger<AgentUpdateController> 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);

View File

@ -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<AlertsController> _logger;
public AlertsController(IDataService dataService, IEmailSenderEx emailSender, IHttpClientFactory httpClientFactory)
public AlertsController(
IDataService dataService,
IEmailSenderEx emailSender,
IHttpClientFactory httpClientFactory,
ILogger<AlertsController> 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.");
}
}

View File

@ -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<RemotelyUser> _signInManager;
private readonly IHubContext<ViewerHub> _viewerHub;
private readonly ILogger<LoginController> _logger;
public LoginController(
SignInManager<RemotelyUser> signInManager,
@ -31,7 +34,8 @@ namespace Remotely.Server.API
IApplicationConfig appConfig,
IHubContext<DesktopHub> casterHubContext,
IDesktopHubSessionCache desktopSessionCache,
IHubContext<ViewerHub> viewerHubContext)
IHubContext<ViewerHub> viewerHubContext,
ILogger<LoginController> 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();
}
}

View File

@ -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<RemotelyUser> _signInManager;
private readonly ILogger<RemoteControlController> _logger;
public RemoteControlController(
SignInManager<RemotelyUser> signInManager,
@ -41,7 +44,8 @@ namespace Remotely.Server.API
IServiceHubSessionCache serviceSessionCache,
IOtpProvider otpProvider,
IHubEventHandler hubEvents,
IApplicationConfig appConfig)
IApplicationConfig appConfig,
ILogger<RemoteControlController> 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();
}

View File

@ -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<ServerLogsController> _logger;
public ServerLogsController(IDataService dataService)
public ServerLogsController(
ILogsManager logsManager,
ILogger<ServerLogsController> logger)
{
_dataService = dataService;
_logsManager = logsManager;
_logger = logger;
}
[ServiceFilter(typeof(ApiAuthorizationFilter))]
[HttpGet("Download")]
public ActionResult Download()
public async Task<IActionResult> 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);
}
}
}

View File

@ -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<RemotelyUser> _userManager;
private readonly IEmailSenderEx _emailSender;
private readonly IDataService _dataService;
private readonly ILogger<ForgotPasswordModel> _logger;
public ForgotPasswordModel(UserManager<RemotelyUser> userManager,
public ForgotPasswordModel(
UserManager<RemotelyUser> userManager,
IEmailSenderEx emailSender,
IDataService dataService)
IDataService dataService,
ILogger<ForgotPasswordModel> 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,

View File

@ -0,0 +1,29 @@
@using Nihs.SimpleMessenger;
@using Remotely.Server.Models.Messages;
@inject IMessenger Messenger
@if (_loaderShown)
{
<LoadingSignal StatusMessage="@_statusMessage" />
}
@code {
private bool _loaderShown;
private string _statusMessage = string.Empty;
protected override Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
Messenger.Register<ShowLoaderMessage>(this, HandleShowLoaderMessage);
}
return base.OnAfterRenderAsync(firstRender);
}
private async Task HandleShowLoaderMessage(ShowLoaderMessage message)
{
_loaderShown = message.IsShown;
_statusMessage = message.StatusMessage;
await InvokeAsync(StateHasChanged);
}
}

View File

@ -1,21 +1,31 @@
@inject IClientAppState AppState
@if (_theme == Theme.Dark)
{
<div class="signal" style="border-color: whitesmoke"></div>
}
else if (_theme == Theme.Light)
{
<div class="signal" style="border-color: #333"></div>
}
<div class="signal-background">
<div class="signal-wrapper text-center">
@if (!string.IsNullOrEmpty(StatusMessage))
{
<h4>
@StatusMessage
</h4>
}
<div class="signal @(GetSignalClass())"></div>
</div>
</div>
@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";
}
}

View File

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

View File

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

View File

@ -28,7 +28,6 @@ namespace Remotely.Server.Data
public DbSet<BrandingInfo> BrandingInfos { get; set; }
public DbSet<DeviceGroup> DeviceGroups { get; set; }
public DbSet<Device> Devices { get; set; }
public DbSet<EventLog> EventLogs { get; set; }
public DbSet<InviteLink> InviteLinks { get; set; }
public DbSet<Organization> Organizations { get; set; }
public DbSet<ScriptRun> ScriptRuns { get; set; }
@ -58,9 +57,6 @@ namespace Remotely.Server.Data
builder.Entity<Organization>()
.HasMany(x => x.RemotelyUsers)
.WithOne(x => x.Organization);
builder.Entity<Organization>()
.HasMany(x => x.EventLogs)
.WithOne(x => x.Organization);
builder.Entity<Organization>()
.HasMany(x => x.DeviceGroups)
.WithOne(x => x.Organization);

View File

@ -23,6 +23,7 @@ namespace Remotely.Server.Hubs
private readonly ICircuitManager _circuitManager;
private readonly IDataService _dataService;
private readonly IExpiringTokenService _expiringTokenService;
private readonly ILogger<AgentHub> _logger;
private readonly IServiceHubSessionCache _serviceSessionCache;
private readonly IHubContext<ViewerHub> _viewerHubContext;
@ -31,7 +32,8 @@ namespace Remotely.Server.Hubs
IServiceHubSessionCache serviceSessionCache,
IHubContext<ViewerHub> viewerHubContext,
ICircuitManager circuitManager,
IExpiringTokenService expiringTokenService)
IExpiringTokenService expiringTokenService,
ILogger<AgentHub> 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;

View File

@ -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<RemoteControlSessionEx>("Unauthorized.");
}
@ -414,13 +417,11 @@ namespace Remotely.Server.Hubs
public Task UploadFiles(List<string> 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)

View File

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

View File

@ -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
<h3 class="mb-3">Server Logs</h3>
@ -34,9 +37,9 @@
<div style="display:inline-block">
<strong>Type:</strong>
<br />
<select class="form-control-sm" @bind="_eventType">
<select class="form-control-sm" @bind="LogLevel">
<option value="">All</option>
@foreach (var eventType in Enum.GetValues(typeof(EventType)))
@foreach (var eventType in Enum.GetValues(typeof(LogLevel)))
{
<option @key="eventType" value="@eventType">@eventType</option>
}
@ -45,51 +48,23 @@
<div style="display:inline-block">
<strong>Filter:</strong>
<br />
<input type="text" @bind="_messageFilter" />
<input type="text" @bind="MessageFilter" @bind:event="oninput" />
</div>
<div style="display:inline-block">
<strong>From:</strong>
<br />
<input type="date" @bind="_fromDate" />
<input type="date" @bind="FromDate" />
</div>
<div style="display:inline-block">
<strong>To:</strong>
<br />
<input type="date" @bind-value="_toDate" />
<input type="date" @bind="ToDate" />
</div>
</div>
<table class="table table-hover table-striped">
<thead>
<tr>
<th>Type</th>
<th>Timestamp</th>
<th>Message</th>
<th>Source</th>
<th>Stack Trace</th>
</tr>
</thead>
<tbody>
@foreach (var eventLog in FilteredLogs)
{
<tr @key="eventLog">
<td>@eventLog.EventType</td>
<td>@eventLog.TimeStamp</td>
<td>
@if (!string.IsNullOrWhiteSpace(eventLog.Message))
{
foreach (var line in eventLog.Message.Split("\n", StringSplitOptions.RemoveEmptyEntries))
{
<div>@line</div>
}
}
</td>
<td>@eventLog.Source</td>
<td>@eventLog.StackTrace</td>
</tr>
}
</tbody>
</table>
<textarea readonly class="logs-content bg-dark text-white">
@(string.Join(Environment.NewLine, _filteredLogs))
</textarea>
}
else
{
@ -97,34 +72,104 @@ else
}
@code {
private readonly List<EventLog> _filteredLogs = new();
private EventType? _eventType;
private string _messageFilter;
private readonly string _messageDebounceKey = Guid.NewGuid().ToString();
private readonly List<string> _filteredLogs = new();
private LogLevel? _logLevel;
private string _messageFilter = string.Empty;
private DateTimeOffset _fromDate = DateTimeOffset.Now.AddDays(-7);
private DateTimeOffset _toDate = DateTimeOffset.Now;
private IEnumerable<EventLog> 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);
}
}

View File

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

View File

@ -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<AuthenticationStateProvider, RevalidatingIdentityAuthenticationStateProvider<RemotelyUser>>();
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<string[]>();
@ -183,13 +189,14 @@ services.AddSingleton<IApplicationConfig, ApplicationConfig>();
services.AddScoped<ApiAuthorizationFilter>();
services.AddScoped<LocalOnlyFilter>();
services.AddScoped<ExpiringTokenFilter>();
services.AddHostedService<DbCleanupService>();
services.AddHostedService<DataCleanupService>();
services.AddHostedService<ScriptScheduler>();
services.AddSingleton<IUpgradeService, UpgradeService>();
services.AddScoped<IToastService, ToastService>();
services.AddScoped<IModalService, ModalService>();
services.AddScoped<IJsInterop, JsInterop>();
services.AddScoped<ICircuitConnection, CircuitConnection>();
services.AddScoped<ILoaderService, LoaderService>();
services.AddScoped(x => (CircuitHandler)x.GetRequiredService<ICircuitConnection>());
services.AddSingleton<ICircuitManager, CircuitManager>();
services.AddScoped<IAuthService, AuthService>();
@ -198,6 +205,8 @@ services.AddScoped<IExpiringTokenService, ExpiringTokenService>();
services.AddScoped<IScriptScheduleDispatcher, ScriptScheduleDispatcher>();
services.AddSingleton<IOtpProvider, OtpProvider>();
services.AddSingleton<IEmbeddedServerDataSearcher, EmbeddedServerDataSearcher>();
services.AddSingleton<ILogsManager, LogsManager>();
services.AddSingleton(WeakReferenceMessenger.Default);
services.AddRemoteControlServer(config =>
{
@ -263,7 +272,6 @@ using (var scope = app.Services.CreateScope())
{
using var context = scope.ServiceProvider.GetRequiredService<AppDb>();
var dataService = scope.ServiceProvider.GetRequiredService<IDataService>();
var loggerFactory = scope.ServiceProvider.GetRequiredService<ILoggerFactory>();
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}");
}
}

View File

@ -31,7 +31,9 @@
<PackageReference Include="Microsoft.Extensions.Logging.EventLog" Version="7.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.18.1" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="7.0.6" />
<PackageReference Include="Nihs.SimpleMessenger" Version="1.0.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="7.0.4" />
<PackageReference Include="Serilog.AspNetCore" Version="7.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
</ItemGroup>

View File

@ -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<DataCleanupService> _logger;
private readonly IServiceProvider _services;
private System.Timers.Timer _cleanupTimer = new(TimeSpan.FromDays(1));
public DataCleanupService(
IServiceProvider serviceProvider,
ILogger<DataCleanupService> 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<IDataService>();
dataService.CleanupOldRecords();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error during data cleanup.");
}
}
}
}

View File

@ -51,8 +51,6 @@ namespace Remotely.Server.Services
void CleanupOldRecords();
Task ClearLogs(string currentUserName);
Task<ApiToken> CreateApiToken(string userName, string tokenName, string secretHash);
Task<Device> 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<Organization> 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<DataService> _logger;
public DataService(
IApplicationConfig appConfig,
IHostEnvironment hostEnvironment,
IAppDbFactory appDbFactory)
IAppDbFactory appDbFactory,
ILogger<DataService> 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<ApiToken> 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<T>.
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<string> AddSharedFileInternal(
string fileName,
byte[] fileContents,

View File

@ -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<IDataService>();
dataService.CleanupOldRecords();
}
}
}

View File

@ -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<string> ScopeStack { get; } = new ConcurrentStack<string>();
public DbLogger(string categoryName, IWebHostEnvironment hostEnvironment, IServiceProvider serviceProvider)
{
_categoryName = categoryName;
_hostEnvironment = hostEnvironment;
_serviceProvider = serviceProvider;
}
public IDisposable BeginScope<TState>(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<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
using var scope = _serviceProvider.CreateScope();
var dataService = scope.ServiceProvider.GetRequiredService<IDataService>();
var scopeStack = ScopeStack.Any() ?
new string[] { ScopeStack.FirstOrDefault(), ScopeStack.LastOrDefault() } :
Array.Empty<string>();
dataService.WriteLog(logLevel, _categoryName, eventId, state.ToString(), exception, scopeStack);
}
private class NoopDisposable : IDisposable
{
public void Dispose()
{
while (!ScopeStack.TryPop(out _))
{
Thread.Sleep(100);
}
}
}
}
}

View File

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

View File

@ -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<bool> 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<bool> 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<bool> 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<EmailSenderEx> _logger;
public EmailSenderEx(
IApplicationConfig appConfig,
ILogger<EmailSenderEx> logger)
{
_appConfig = appConfig;
_logger = logger;
}
public async Task<bool> 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<bool> SendEmailAsync(string email, string subject, string htmlMessage, string organizationID = null)
{
return SendEmailAsync(email, _appConfig.SmtpEmail, subject, htmlMessage, organizationID);
}
}
}

View File

@ -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<IDisposable> ShowLoader(string statusMessage);
void HideLoader();
}
public class LoaderService : ILoaderService
{
private readonly IMessenger _messenger;
public LoaderService(IMessenger messenger)
{
_messenger = messenger;
}
public async Task<IDisposable> ShowLoader(string statusMessage)
{
await _messenger.Send(new ShowLoaderMessage(true, statusMessage));
return new CallbackDisposable(HideLoader);
}
public void HideLoader()
{
_messenger.Send(new ShowLoaderMessage(false, string.Empty));
}
}

View File

@ -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<string> GetLogs(
DateTimeOffset startDate,
DateTimeOffset endDate,
string messageFilter,
LogLevel? logLevelFilter);
string GetLogsDirectory();
Task<FileInfo> ZipAllLogs();
}
public class LogsManager : ILogsManager
{
private static readonly ReadOnlyDictionary<string, LogLevel> _logLevelMap = new(new Dictionary<string, LogLevel>()
{
["[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<string> 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<FileInfo> 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<string> 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;
}
}
}

View File

@ -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<string> viewerList)

View File

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

View File

@ -16,7 +16,7 @@
</div>
<ToastHarness />
<LoaderHarness />
<ModalHarness />
</div>
</Authorized>

View File

@ -83,14 +83,14 @@
<span class="oi oi-key" aria-hidden="true"></span> API Keys
</NavLink>
</li>
<li class="nav-item px-3">
<NavLink class="nav-link" href="server-logs">
<span class="oi oi-document" aria-hidden="true"></span> Server Logs
</NavLink>
</li>
@if (_user?.IsServerAdmin == true)
{
<li class="nav-item px-3">
<NavLink class="nav-link" href="server-logs">
<span class="oi oi-document" aria-hidden="true"></span> Server Logs
</NavLink>
</li>
<li class="nav-item px-3">
<NavLink class="nav-link" href="server-config">
<span class="oi oi-wrench" aria-hidden="true"></span> Server Config

View File

@ -10,6 +10,15 @@
"Default": "Information"
}
},
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Warning",
"System": "Warning"
}
}
},
"ApplicationOptions": {
"AllowApiLogin": false,
"BannedDevices": [],

View File

@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Remotely.Shared.Extensions
{
public static class IEnumerableExtensions
{
public static async IAsyncEnumerable<T> ToAsyncEnumerable<T>(this IEnumerable<T> source)
{
foreach (var item in source)
{
yield return item;
await Task.Yield();
}
}
}
}

View File

@ -1,23 +0,0 @@
using Remotely.Shared.Enums;
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
namespace Remotely.Shared.Models
{
public class EventLog
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public string ID { get; set; }
public EventType EventType { get; set; }
public string Message { get; set; }
public string Source { get; set; }
public string StackTrace { get; set; }
public string OrganizationID { get; set; }
public DateTimeOffset TimeStamp { get; set; } = DateTimeOffset.Now;
[JsonIgnore]
public Organization Organization { get; set; }
}
}

View File

@ -26,8 +26,6 @@ namespace Remotely.Shared.Models
public ICollection<Device> Devices { get; set; }
public ICollection<EventLog> EventLogs { get; set; }
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public string ID { get; set; }

View File

@ -188,6 +188,11 @@ namespace Remotely.Shared.Services
$"[Thread ID: {Environment.CurrentManagedThreadId}]\t" +
$"[{DateTimeOffset.Now:yyyy-MM-dd HH:mm:ss.fff}]\t";
if (exception is not null)
{
entry += $"[Exception: {exception.GetType().Name}]\t";
}
entry += scopeStack.Any() ?
$"[{categoryName} => {string.Join(" => ", scopeStack)}]\t" :
$"[{categoryName}]\t";

View File

@ -39,10 +39,18 @@ namespace Remotely.Tests
var viewerHub = new Mock<IHubContext<ViewerHub>>();
var expiringTokenService = new Mock<IExpiringTokenService>();
var serviceSessionCache = new Mock<IServiceHubSessionCache>();
var logger = new Mock<ILogger<AgentHub>>();
appConfig.Setup(x => x.BannedDevices).Returns(new string[] { _testData.Device1.DeviceName });
var hub = new AgentHub(DataService, appConfig.Object, serviceSessionCache.Object, viewerHub.Object, circuitManager.Object, expiringTokenService.Object);
var hub = new AgentHub(
DataService,
appConfig.Object,
serviceSessionCache.Object,
viewerHub.Object,
circuitManager.Object,
expiringTokenService.Object,
logger.Object);
var hubClients = new Mock<IHubCallerClients>();
var caller = new Mock<ISingleClientProxy>();
@ -54,6 +62,8 @@ namespace Remotely.Tests
caller.Verify(x => x.SendCoreAsync("UninstallAgent", It.IsAny<object[]>(), It.IsAny<CancellationToken>()), Times.Once);
}
// TODO: Checking of device ban should be pulled out into
// a separate service that's better testable.
[TestMethod]
[DoNotParallelize]
public async Task DeviceCameOnline_BannedById()
@ -66,10 +76,18 @@ namespace Remotely.Tests
var viewerHub = new Mock<IHubContext<ViewerHub>>();
var expiringTokenService = new Mock<IExpiringTokenService>();
var serviceSessionCache = new Mock<IServiceHubSessionCache>();
var logger = new Mock<ILogger<AgentHub>>();
appConfig.Setup(x => x.BannedDevices).Returns(new string[] { _testData.Device1.ID });
var hub = new AgentHub(DataService, appConfig.Object, serviceSessionCache.Object, viewerHub.Object, circuitManager.Object, expiringTokenService.Object);
var hub = new AgentHub(
DataService,
appConfig.Object,
serviceSessionCache.Object,
viewerHub.Object,
circuitManager.Object,
expiringTokenService.Object,
logger.Object);
var hubClients = new Mock<IHubCallerClients>();
var caller = new Mock<ISingleClientProxy>();
@ -81,51 +99,6 @@ namespace Remotely.Tests
caller.Verify(x => x.SendCoreAsync("UninstallAgent", It.IsAny<object[]>(), It.IsAny<CancellationToken>()), Times.Once);
}
[TestMethod]
[DoNotParallelize]
public async Task DeviceCameOnline_NotBanned()
{
var appConfig = new Mock<IApplicationConfig>();
var circuitManager = new Mock<ICircuitManager>();
var circuitConnection = new Mock<ICircuitConnection>();
circuitManager.Setup(x => x.Connections).Returns(new[] { circuitConnection.Object });
circuitConnection.Setup(x => x.User).Returns(_testData.Admin1);
var browserHubClients = new Mock<IHubClients>();
var expiringTokenService = new Mock<IExpiringTokenService>();
var serviceSessionCache = new Mock<IServiceHubSessionCache>();
var viewerHub = new Mock<IHubContext<ViewerHub>>();
appConfig.Setup(x => x.BannedDevices).Returns(Array.Empty<string>());
var hub = new AgentHub(DataService, appConfig.Object, serviceSessionCache.Object, viewerHub.Object, circuitManager.Object, expiringTokenService.Object)
{
Context = new CallerContext()
};
var agentHubClients = new Mock<IHubCallerClients>();
var agentHubCaller = new Mock<ISingleClientProxy>();
var agentClientsProxy = new Mock<IClientProxy>();
agentHubClients.Setup(x => x.Caller).Returns(agentHubCaller.Object);
agentHubClients.Setup(x => x.Clients(It.IsAny<IReadOnlyList<string>>())).Returns(agentClientsProxy.Object);
hub.Clients = agentHubClients.Object;
var result = await hub.DeviceCameOnline(_testData.Device1);
Assert.IsTrue(result);
agentHubClients.Verify(x => x.Caller, Times.Never);
agentHubCaller.Verify(x => x.SendCoreAsync("UninstallAgent", It.IsAny<object[]>(), It.IsAny<CancellationToken>()), Times.Never);
circuitConnection.Verify(x => x.InvokeCircuitEvent(
CircuitEventName.DeviceUpdate,
It.Is<Device>(x => x.ID == _testData.Device1.ID)),
Times.Once);
}
[TestCleanup]
public void TestCleanup()
{

@ -1 +1 @@
Subproject commit 51063504e723f2ee5a8fe4e672855d905a3f0eca
Subproject commit f7083733b34b4ad1a207f1103a4e35c0376411d1