mirror of
https://github.com/immense/Remotely.git
synced 2025-10-26 11:27:15 +00:00
Merge pull request #649 from immense/jaredg-respect-httplogging-setting
Replace DbLogger in the server with Serilog.
This commit is contained in:
commit
03862957be
@ -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);
|
||||
|
||||
@ -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.");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
29
Server/Components/LoaderHarness.razor
Normal file
29
Server/Components/LoaderHarness.razor
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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";
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
};
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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)
|
||||
|
||||
13
Server/Models/Messages/ShowLoaderMessage.cs
Normal file
13
Server/Models/Messages/ShowLoaderMessage.cs
Normal 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; }
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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}");
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
|
||||
|
||||
62
Server/Services/DataCleanupService.cs
Normal file
62
Server/Services/DataCleanupService.cs
Normal 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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
34
Server/Services/LoaderService.cs
Normal file
34
Server/Services/LoaderService.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
209
Server/Services/LogsManager.cs
Normal file
209
Server/Services/LogsManager.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
</div>
|
||||
|
||||
<ToastHarness />
|
||||
|
||||
<LoaderHarness />
|
||||
<ModalHarness />
|
||||
</div>
|
||||
</Authorized>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -10,6 +10,15 @@
|
||||
"Default": "Information"
|
||||
}
|
||||
},
|
||||
"Serilog": {
|
||||
"MinimumLevel": {
|
||||
"Default": "Information",
|
||||
"Override": {
|
||||
"Microsoft": "Warning",
|
||||
"System": "Warning"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ApplicationOptions": {
|
||||
"AllowApiLogin": false,
|
||||
"BannedDevices": [],
|
||||
|
||||
20
Shared/Extensions/IEnumerableExtensions.cs
Normal file
20
Shared/Extensions/IEnumerableExtensions.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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; }
|
||||
}
|
||||
}
|
||||
@ -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; }
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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
|
||||
Loading…
Reference in New Issue
Block a user