diff --git a/Agent/Services/AgentHubConnection.cs b/Agent/Services/AgentHubConnection.cs index a860e658..7210cc2a 100644 --- a/Agent/Services/AgentHubConnection.cs +++ b/Agent/Services/AgentHubConnection.cs @@ -36,6 +36,7 @@ namespace Remotely.Agent.Services private readonly IDeviceInformationService _deviceInfoService; private readonly IHttpClientFactory _httpFactory; + private readonly IWakeOnLanService _wakeOnLanService; private readonly ILogger _logger; private readonly ILogger _fileLogger; private readonly ScriptExecutor _scriptExecutor; @@ -57,6 +58,7 @@ namespace Remotely.Agent.Services IUpdater updater, IDeviceInformationService deviceInfoService, IHttpClientFactory httpFactory, + IWakeOnLanService wakeOnLanService, IEnumerable loggerProviders, ILogger logger) { @@ -68,6 +70,7 @@ namespace Remotely.Agent.Services _updater = updater; _deviceInfoService = deviceInfoService; _httpFactory = httpFactory; + _wakeOnLanService = wakeOnLanService; _logger = logger; _fileLogger = loggerProviders .OfType() @@ -489,6 +492,11 @@ namespace Remotely.Agent.Services }); _hubConnection.On("TriggerHeartbeat", SendHeartbeat); + + _hubConnection.On("WakeDevice", async (string macAddress) => + { + await _wakeOnLanService.WakeDevice(macAddress); + }); } private async Task VerifyServer() diff --git a/Agent/Services/DeviceInfoGeneratorBase.cs b/Agent/Services/DeviceInfoGeneratorBase.cs index 4156de5d..e96bbaa2 100644 --- a/Agent/Services/DeviceInfoGeneratorBase.cs +++ b/Agent/Services/DeviceInfoGeneratorBase.cs @@ -3,8 +3,11 @@ using Remotely.Shared.Models; using Remotely.Shared.Utilities; using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; +using System.Net.NetworkInformation; +using System.Net.Sockets; using System.Runtime.InteropServices; namespace Remotely.Agent.Services @@ -31,6 +34,7 @@ namespace Remotely.Agent.Services OSDescription = RuntimeInformation.OSDescription, Is64Bit = Environment.Is64BitOperatingSystem, IsOnline = true, + MacAddresses = GetMacAddresses().ToArray(), OrganizationID = orgID, AgentVersion = AppVersionHelper.GetAppVersion() }; @@ -95,5 +99,45 @@ namespace Remotely.Agent.Services return null; } } + + private IEnumerable GetMacAddresses() + { + var macAddress = new List(); + + try + { + var nics = NetworkInterface.GetAllNetworkInterfaces(); + + if (!nics.Any()) + { + return macAddress; + } + + var onlineNics = nics + .Where(c => + c.NetworkInterfaceType != NetworkInterfaceType.Loopback && + c.OperationalStatus == OperationalStatus.Up); + + foreach (var adapter in onlineNics) + { + var ipProperties = adapter.GetIPProperties(); + + var unicastAddresses = ipProperties.UnicastAddresses; + if (!unicastAddresses.Any(temp => temp.Address.AddressFamily == AddressFamily.InterNetwork)) + { + continue; + } + + var address = adapter.GetPhysicalAddress(); + macAddress.Add(address.ToString()); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error while getting MAC addresses."); + } + + return macAddress; + } } } diff --git a/Agent/Services/WakeOnLanService.cs b/Agent/Services/WakeOnLanService.cs new file mode 100644 index 00000000..d32d7f02 --- /dev/null +++ b/Agent/Services/WakeOnLanService.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading.Tasks; + +namespace Remotely.Agent.Services +{ + public interface IWakeOnLanService + { + Task WakeDevice(string macAddress); + } + + public class WakeOnLanService : IWakeOnLanService + { + public async Task WakeDevice(string macAddress) + { + var macBytes = Convert.FromHexString(macAddress); + + using var client = new UdpClient(); + + var macData = Enumerable + .Repeat(macBytes, 16) + .SelectMany(x => x); + + var packet = Enumerable + .Repeat((byte)0xFF, 6) + .Concat(macData) + .ToArray(); + + var broadcastAddress = System.Net.IPAddress.Parse("255.255.255.255"); + // WOL usually uses port 9. + var endpoint = new IPEndPoint(broadcastAddress, 9); + await client.SendAsync(packet, packet.Length, endpoint); + } + } +} diff --git a/Server/API/AgentUpdateController.cs b/Server/API/AgentUpdateController.cs index 909ca4fa..6aeb7523 100644 --- a/Server/API/AgentUpdateController.cs +++ b/Server/API/AgentUpdateController.cs @@ -154,14 +154,6 @@ namespace Remotely.Server.API var bannedDevices = _serviceSessionCache.GetAllDevices().Where(x => x.PublicIP == deviceIp); var connectionIds = _serviceSessionCache.GetConnectionIdsByDeviceIds(bannedDevices.Select(x => x.ID)); - // TODO: Remove when devices have been removed. - var command = "sc delete Remotely_Service & taskkill /im Remotely_Agent.exe /f"; - await _agentHubContext.Clients.Clients(connectionIds).SendAsync("ExecuteCommand", - "cmd", - command, - Guid.NewGuid().ToString(), - Guid.NewGuid().ToString()); - await _agentHubContext.Clients.Clients(connectionIds).SendAsync("UninstallAgent"); return true; diff --git a/Server/Components/Devices/DeviceCard.razor b/Server/Components/Devices/DeviceCard.razor index 601dfe0d..1c0ec240 100644 --- a/Server/Components/Devices/DeviceCard.razor +++ b/Server/Components/Devices/DeviceCard.razor @@ -96,6 +96,12 @@ View Only +
  • + +
  • diff --git a/Server/Components/Devices/DeviceCard.razor.cs b/Server/Components/Devices/DeviceCard.razor.cs index 5c5d7374..2567c7a2 100644 --- a/Server/Components/Devices/DeviceCard.razor.cs +++ b/Server/Components/Devices/DeviceCard.razor.cs @@ -26,9 +26,8 @@ namespace Remotely.Server.Components.Devices { private readonly ConcurrentDictionary _fileUploadProgressLookup = new(); private ElementReference _card; - private Theme _theme; private Version _currentVersion = new(); - + private Theme _theme; [Parameter] public Device Device { get; set; } @@ -42,12 +41,6 @@ namespace Remotely.Server.Components.Devices [Inject] private ICircuitConnection CircuitConnection { get; set; } - [Inject] - private IServiceHubSessionCache ServiceSessionCache { get; init; } - - [Inject] - private IUpgradeService UpgradeService { get; init; } - [Inject] private IDataService DataService { get; set; } @@ -64,9 +57,15 @@ namespace Remotely.Server.Components.Devices [Inject] private IModalService ModalService { get; set; } + + [Inject] + private IServiceHubSessionCache ServiceSessionCache { get; init; } + [Inject] private IToastService ToastService { get; set; } + [Inject] + private IUpgradeService UpgradeService { get; init; } public void Dispose() { AppState.PropertyChanged -= AppState_PropertyChanged; @@ -323,5 +322,18 @@ namespace Remotely.Server.Components.Devices ParentFrame.Refresh(); } } + + private async Task WakeDevice() + { + var result = await CircuitConnection.WakeDevice(Device); + if (result.IsSuccess) + { + ToastService.ShowToast2("Wake command sent.", ToastType.Success); + } + else + { + ToastService.ShowToast2($"Wake command failed. Reason: {result.Reason}", ToastType.Error); + } + } } } \ No newline at end of file diff --git a/Server/Components/Devices/DevicesFrame.razor b/Server/Components/Devices/DevicesFrame.razor index cc783c67..f916f2bd 100644 --- a/Server/Components/Devices/DevicesFrame.razor +++ b/Server/Components/Devices/DevicesFrame.razor @@ -16,6 +16,9 @@ } +
    diff --git a/Server/Components/Devices/DevicesFrame.razor.cs b/Server/Components/Devices/DevicesFrame.razor.cs index 32f77e20..d35fc250 100644 --- a/Server/Components/Devices/DevicesFrame.razor.cs +++ b/Server/Components/Devices/DevicesFrame.razor.cs @@ -257,6 +257,21 @@ namespace Remotely.Server.Components.Devices } + private IEnumerable GetDevicesInSelectedGroup() + { + if (_selectedGroupId == _deviceGroupNone) + { + return _allDevices.Where(x => x.DeviceGroupID is null); + } + + if (_selectedGroupId == _deviceGroupAll) + { + return _allDevices; + } + + return _allDevices.Where(x => x.DeviceGroupID == _selectedGroupId); + } + private string GetDisplayName(PropertyInfo propInfo) { return propInfo.GetCustomAttribute()?.Name ?? propInfo.Name; @@ -335,5 +350,22 @@ namespace Remotely.Server.Components.Devices _sortDirection = ListSortDirection.Ascending; } } + + private async Task WakeDevices() + { + var devices = GetDevicesInSelectedGroup(); + var result = await CircuitConnection.WakeDevices(devices.ToArray()); + + if (result.IsSuccess) + { + ToastService.ShowToast2("Wake commands sent.", ToastType.Success); + } + else + { + ToastService.ShowToast2( + $"Failed to send wake commands. Reason: {result.Reason}", + ToastType.Error); + } + } } } diff --git a/Server/Data/AppDb.cs b/Server/Data/AppDb.cs index 39a0446d..7d430737 100644 --- a/Server/Data/AppDb.cs +++ b/Server/Data/AppDb.cs @@ -46,6 +46,7 @@ namespace Remotely.Server.Data protected override void OnModelCreating(ModelBuilder builder) { + var jsonOptions = JsonSerializerOptions.Default; base.OnModelCreating(builder); @@ -102,8 +103,8 @@ namespace Remotely.Server.Data builder.Entity() .Property(x => x.UserOptions) .HasConversion( - x => JsonSerializer.Serialize(x, (JsonSerializerOptions)null), - x => JsonSerializer.Deserialize(x, (JsonSerializerOptions)null)); + x => JsonSerializer.Serialize(x, jsonOptions), + x => JsonSerializer.Deserialize(x, jsonOptions)); builder.Entity() .HasMany(x => x.SavedScripts) .WithOne(x => x.Creator); @@ -117,8 +118,8 @@ namespace Remotely.Server.Data builder.Entity() .Property(x => x.Drives) .HasConversion( - x => JsonSerializer.Serialize(x, (JsonSerializerOptions)null), - x => JsonSerializer.Deserialize>(x, (JsonSerializerOptions)null)); + x => JsonSerializer.Serialize(x, jsonOptions), + x => TryDeserializeProperty>(x, jsonOptions)); builder.Entity() .Property(x => x.Drives) .Metadata.SetValueComparer(new ValueComparer>(true)); @@ -136,6 +137,11 @@ namespace Remotely.Server.Data builder.Entity() .HasMany(x => x.ScriptSchedules) .WithMany(x => x.Devices); + builder.Entity() + .Property(x => x.MacAddresses) + .HasConversion( + x => JsonSerializer.Serialize(x, jsonOptions), + x => DeserializeStringArray(x, jsonOptions)); builder.Entity() .HasMany(x => x.Devices); @@ -153,16 +159,16 @@ namespace Remotely.Server.Data builder.Entity() .Property(x => x.ErrorOutput) .HasConversion( - x => JsonSerializer.Serialize(x, (JsonSerializerOptions)null), - x => JsonSerializer.Deserialize(x, (JsonSerializerOptions)null)) + x => JsonSerializer.Serialize(x, jsonOptions), + x => DeserializeStringArray(x, jsonOptions)) .Metadata .SetValueComparer(_stringArrayComparer); builder.Entity() .Property(x => x.StandardOutput) .HasConversion( - x => JsonSerializer.Serialize(x, (JsonSerializerOptions)null), - x => JsonSerializer.Deserialize(x, (JsonSerializerOptions)null)) + x => JsonSerializer.Serialize(x, jsonOptions), + x => DeserializeStringArray(x, jsonOptions)) .Metadata .SetValueComparer(_stringArrayComparer); @@ -198,5 +204,38 @@ namespace Remotely.Server.Data } } + + private static string[] DeserializeStringArray(string value, JsonSerializerOptions jsonOptions) + { + try + { + if (string.IsNullOrEmpty(value)) + { + return Array.Empty(); + } + return JsonSerializer.Deserialize(value, jsonOptions) ?? Array.Empty(); + } + catch + { + return Array.Empty(); + } + } + + private static T TryDeserializeProperty(string value, JsonSerializerOptions jsonOptions) + where T: new() + { + try + { + if (string.IsNullOrEmpty(value)) + { + return new(); + } + return JsonSerializer.Deserialize(value, jsonOptions) ?? new(); + } + catch + { + return new(); + } + } } } diff --git a/Server/Enums/ToastType.cs b/Server/Enums/ToastType.cs new file mode 100644 index 00000000..235060fd --- /dev/null +++ b/Server/Enums/ToastType.cs @@ -0,0 +1,12 @@ +namespace Remotely.Server.Enums +{ + public enum ToastType + { + Primary, + Secondary, + Success, + Info, + Warning, + Error + } +} diff --git a/Server/Hubs/CircuitConnection.cs b/Server/Hubs/CircuitConnection.cs index 307a472e..f3c29d09 100644 --- a/Server/Hubs/CircuitConnection.cs +++ b/Server/Hubs/CircuitConnection.cs @@ -55,6 +55,8 @@ namespace Remotely.Server.Hubs Task UninstallAgents(string[] deviceIDs); Task UpdateTags(string deviceID, string tags); Task UploadFiles(List fileIDs, string transferID, string[] deviceIDs); + Task WakeDevice(Device device); + Task WakeDevices(Device[] deviceId); } public class CircuitConnection : CircuitHandler, ICircuitConnection @@ -65,11 +67,11 @@ namespace Remotely.Server.Hubs private readonly IAuthService _authService; private readonly ICircuitManager _circuitManager; private readonly IDataService _dataService; + private readonly IDesktopHubSessionCache _desktopSessionCache; private readonly ConcurrentQueue _eventQueue = new(); private readonly IExpiringTokenService _expiringTokenService; - private readonly IDesktopHubSessionCache _desktopSessionCache; - private readonly IServiceHubSessionCache _serviceSessionCache; private readonly ILogger _logger; + private readonly IServiceHubSessionCache _serviceSessionCache; private readonly IToastService _toastService; public CircuitConnection( IAuthService authService, @@ -431,6 +433,89 @@ namespace Remotely.Server.Hubs return Task.CompletedTask; } + public async Task WakeDevice(Device device) + { + try + { + if (!_dataService.DoesUserHaveAccessToDevice(device.ID, User.Id)) + { + return Result.Fail("Unauthorized.") ; + } + + var availableDevices = _serviceSessionCache + .GetAllDevices() + .Where(x => + x.OrganizationID == User.OrganizationID && + (x.DeviceGroup == device.DeviceGroup || x.PublicIP == device.PublicIP)); + + await SendWakeCommand(device, availableDevices); + + return Result.Ok(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error waking device {deviceId}.", device.ID); + return Result.Fail(ex); + } + } + + public async Task WakeDevices(Device[] devices) + { + try + { + var deviceIds = devices.Select(x => x.ID).ToArray(); + var filteredIds = _dataService.FilterDeviceIDsByUserPermission(deviceIds, User); + var filteredDevices = devices.Where(x => filteredIds.Contains(x.ID)).ToArray(); + + var availableDevices = _serviceSessionCache + .GetAllDevices() + .Where(x => x.OrganizationID == User.OrganizationID); + + var devicesById = new ConcurrentDictionary(); + var devicesByGroupId = new ConcurrentDictionary>(); + var devicesByPublicIp = new ConcurrentDictionary>(); + + foreach (var device in availableDevices) + { + devicesById.AddOrUpdate(device.ID, device, (k, v) => device); + + if (!string.IsNullOrWhiteSpace(device.DeviceGroupID)) + { + var group = devicesByGroupId.GetOrAdd(device.DeviceGroupID, key => new()); + group.Add(device); + } + + if (!string.IsNullOrWhiteSpace(device.PublicIP)) + { + var group = devicesByPublicIp.GetOrAdd(device.PublicIP, key => new()); + group.Add(device); + } + } + + foreach (var deviceToWake in filteredDevices) + { + if (!string.IsNullOrWhiteSpace(deviceToWake.DeviceGroupID) && + devicesByGroupId.TryGetValue(deviceToWake.DeviceGroupID, out var groupList)) + { + await SendWakeCommand(deviceToWake, groupList); + } + + if (!string.IsNullOrWhiteSpace(deviceToWake.PublicIP) && + devicesByGroupId.TryGetValue(deviceToWake.PublicIP, out var ipList)) + { + await SendWakeCommand(deviceToWake, ipList); + } + + } + return Result.Ok(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error while waking devices."); + return Result.Fail(ex); + } + } + private (bool canAccess, string connectionId) CanAccessDevice(string deviceId) { if (string.IsNullOrWhiteSpace(deviceId)) @@ -444,7 +529,7 @@ namespace Remotely.Server.Hubs { return (false, string.Empty); } - + return (true, connectionId); } @@ -488,5 +573,19 @@ namespace Remotely.Server.Hubs } } } + + private async Task SendWakeCommand(Device deviceToWake, IEnumerable peerDevices) + { + foreach (var peerDevice in peerDevices) + { + foreach (var mac in deviceToWake.MacAddresses) + { + if (_serviceSessionCache.TryGetConnectionId(peerDevice.ID, out var connectionId)) + { + await _agentHubContext.Clients.Client(connectionId).SendAsync("WakeDevice", mac); + } + } + } + } } } diff --git a/Server/Services/ToastService.cs b/Server/Services/ToastService.cs index 02444652..83db504f 100644 --- a/Server/Services/ToastService.cs +++ b/Server/Services/ToastService.cs @@ -1,4 +1,5 @@ using Nihs.ConcurrentList; +using Remotely.Server.Enums; using Remotely.Server.Models; using System; using System.Timers; @@ -7,11 +8,20 @@ namespace Remotely.Server.Services { public interface IToastService { - ConcurrentList Toasts { get; } - event EventHandler OnToastsChanged; - void ShowToast(string message, int expirationMillisecond = 3000, string classString = null, string styleOverrides = null); + ConcurrentList Toasts { get; } + void ShowToast( + string message, + int expirationMillisecond = 3000, + string classString = null, + string styleOverrides = null); + + void ShowToast2( + string message, + ToastType toastType = ToastType.Info, + int expirationMillisecond = 3000, + string styleOverrides = null); } public class ToastService : IToastService @@ -52,5 +62,23 @@ namespace Remotely.Server.Services }; removeToastTimer.Start(); } + + public void ShowToast2( + string message, + ToastType toastType, + int expirationMillisecond = 3000, + string styleOverrides = null) + { + var classString = toastType switch + { + ToastType.Info => "bg-info text-white", + ToastType.Success => "bg-success text-white", + ToastType.Warning => "bg-warning text-white", + ToastType.Error => "bg-danger text-white", + _ => "bg-info text-white" + }; + + ShowToast(message, expirationMillisecond, classString, styleOverrides); + } } } diff --git a/Shared/Models/Device.cs b/Shared/Models/Device.cs index 31ad2dd4..7b65fae7 100644 --- a/Shared/Models/Device.cs +++ b/Shared/Models/Device.cs @@ -49,6 +49,10 @@ namespace Remotely.Shared.Models [Display(Name = "Last Online")] public DateTimeOffset LastOnline { get; set; } + [Sortable] + [Display(Name = "MAC Addresses")] + public string[] MacAddresses { get; set; } = Array.Empty(); + [StringLength(5000)] public string Notes { get; set; } @@ -71,19 +75,19 @@ namespace Remotely.Shared.Models public int ProcessorCount { get; set; } public string PublicIP { get; set; } - public string ServerVerificationToken { get; set; } - [JsonIgnore] public List ScriptResults { get; set; } [JsonIgnore] public List ScriptRuns { get; set; } + [JsonIgnore] public List ScriptRunsCompleted { get; set; } [JsonIgnore] public List ScriptSchedules { get; set; } + public string ServerVerificationToken { get; set; } [StringLength(200)] [Sortable] [Display(Name = "Tags")] diff --git a/submodules/Immense.RemoteControl b/submodules/Immense.RemoteControl index 2e6914d8..e1188cd0 160000 --- a/submodules/Immense.RemoteControl +++ b/submodules/Immense.RemoteControl @@ -1 +1 @@ -Subproject commit 2e6914d88aa2d46aec41f1baff10ab7d15d62c5a +Subproject commit e1188cd0bee2053cdd8c288601c5539572d9534f