Implement wake-on-lan.

This commit is contained in:
Jared Goodwin 2023-06-22 07:22:17 -07:00
parent e6cc771b00
commit 1794059822
14 changed files with 351 additions and 33 deletions

View File

@ -36,6 +36,7 @@ namespace Remotely.Agent.Services
private readonly IDeviceInformationService _deviceInfoService;
private readonly IHttpClientFactory _httpFactory;
private readonly IWakeOnLanService _wakeOnLanService;
private readonly ILogger<AgentHubConnection> _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<ILoggerProvider> loggerProviders,
ILogger<AgentHubConnection> logger)
{
@ -68,6 +70,7 @@ namespace Remotely.Agent.Services
_updater = updater;
_deviceInfoService = deviceInfoService;
_httpFactory = httpFactory;
_wakeOnLanService = wakeOnLanService;
_logger = logger;
_fileLogger = loggerProviders
.OfType<FileLoggerProvider>()
@ -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<bool> VerifyServer()

View File

@ -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<string> GetMacAddresses()
{
var macAddress = new List<string>();
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;
}
}
}

View File

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

View File

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

View File

@ -96,6 +96,12 @@
<span class="ml-2">View Only</span>
</button>
</li>
<li>
<button class="dropdown-item" @onclick="() => WakeDevice()">
<i class="oi oi-eye" title="Wake Device"></i>
<span class="ml-2">Wake</span>
</button>
</li>
<li>
<FileInputButton ClassNames="dropdown-item btn btn-primary"
OnChanged="OnFileInputChanged">

View File

@ -26,9 +26,8 @@ namespace Remotely.Server.Components.Devices
{
private readonly ConcurrentDictionary<string, double> _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);
}
}
}
}

View File

@ -16,6 +16,9 @@
<option @key="group.ID" value="@group.ID">@group.Name</option>
}
</select>
<button class="btn btn-sm btn-secondary" title="Wake Devices" style="width: 40px; margin-left: 5px;" @onclick="WakeDevices">
<span class="oi oi-eye"></span>
</button>
</div>
<div>

View File

@ -257,6 +257,21 @@ namespace Remotely.Server.Components.Devices
}
private IEnumerable<Device> 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<DisplayAttribute>()?.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);
}
}
}
}

View File

@ -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<RemotelyUser>()
.Property(x => x.UserOptions)
.HasConversion(
x => JsonSerializer.Serialize(x, (JsonSerializerOptions)null),
x => JsonSerializer.Deserialize<RemotelyUserOptions>(x, (JsonSerializerOptions)null));
x => JsonSerializer.Serialize(x, jsonOptions),
x => JsonSerializer.Deserialize<RemotelyUserOptions>(x, jsonOptions));
builder.Entity<RemotelyUser>()
.HasMany(x => x.SavedScripts)
.WithOne(x => x.Creator);
@ -117,8 +118,8 @@ namespace Remotely.Server.Data
builder.Entity<Device>()
.Property(x => x.Drives)
.HasConversion(
x => JsonSerializer.Serialize(x, (JsonSerializerOptions)null),
x => JsonSerializer.Deserialize<List<Drive>>(x, (JsonSerializerOptions)null));
x => JsonSerializer.Serialize(x, jsonOptions),
x => TryDeserializeProperty<List<Drive>>(x, jsonOptions));
builder.Entity<Device>()
.Property(x => x.Drives)
.Metadata.SetValueComparer(new ValueComparer<List<Drive>>(true));
@ -136,6 +137,11 @@ namespace Remotely.Server.Data
builder.Entity<Device>()
.HasMany(x => x.ScriptSchedules)
.WithMany(x => x.Devices);
builder.Entity<Device>()
.Property(x => x.MacAddresses)
.HasConversion(
x => JsonSerializer.Serialize(x, jsonOptions),
x => DeserializeStringArray(x, jsonOptions));
builder.Entity<DeviceGroup>()
.HasMany(x => x.Devices);
@ -153,16 +159,16 @@ namespace Remotely.Server.Data
builder.Entity<ScriptResult>()
.Property(x => x.ErrorOutput)
.HasConversion(
x => JsonSerializer.Serialize(x, (JsonSerializerOptions)null),
x => JsonSerializer.Deserialize<string[]>(x, (JsonSerializerOptions)null))
x => JsonSerializer.Serialize(x, jsonOptions),
x => DeserializeStringArray(x, jsonOptions))
.Metadata
.SetValueComparer(_stringArrayComparer);
builder.Entity<ScriptResult>()
.Property(x => x.StandardOutput)
.HasConversion(
x => JsonSerializer.Serialize(x, (JsonSerializerOptions)null),
x => JsonSerializer.Deserialize<string[]>(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<string>();
}
return JsonSerializer.Deserialize<string[]>(value, jsonOptions) ?? Array.Empty<string>();
}
catch
{
return Array.Empty<string>();
}
}
private static T TryDeserializeProperty<T>(string value, JsonSerializerOptions jsonOptions)
where T: new()
{
try
{
if (string.IsNullOrEmpty(value))
{
return new();
}
return JsonSerializer.Deserialize<T>(value, jsonOptions) ?? new();
}
catch
{
return new();
}
}
}
}

12
Server/Enums/ToastType.cs Normal file
View File

@ -0,0 +1,12 @@
namespace Remotely.Server.Enums
{
public enum ToastType
{
Primary,
Secondary,
Success,
Info,
Warning,
Error
}
}

View File

@ -55,6 +55,8 @@ namespace Remotely.Server.Hubs
Task UninstallAgents(string[] deviceIDs);
Task UpdateTags(string deviceID, string tags);
Task UploadFiles(List<string> fileIDs, string transferID, string[] deviceIDs);
Task<Result> WakeDevice(Device device);
Task<Result> 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<CircuitEvent> _eventQueue = new();
private readonly IExpiringTokenService _expiringTokenService;
private readonly IDesktopHubSessionCache _desktopSessionCache;
private readonly IServiceHubSessionCache _serviceSessionCache;
private readonly ILogger<CircuitConnection> _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<Result> 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<Result> 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<string, Device>();
var devicesByGroupId = new ConcurrentDictionary<string, List<Device>>();
var devicesByPublicIp = new ConcurrentDictionary<string, List<Device>>();
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<Device> 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);
}
}
}
}
}
}

View File

@ -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<Toast> Toasts { get; }
event EventHandler OnToastsChanged;
void ShowToast(string message, int expirationMillisecond = 3000, string classString = null, string styleOverrides = null);
ConcurrentList<Toast> 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);
}
}
}

View File

@ -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<string>();
[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<ScriptResult> ScriptResults { get; set; }
[JsonIgnore]
public List<ScriptRun> ScriptRuns { get; set; }
[JsonIgnore]
public List<ScriptRun> ScriptRunsCompleted { get; set; }
[JsonIgnore]
public List<ScriptSchedule> ScriptSchedules { get; set; }
public string ServerVerificationToken { get; set; }
[StringLength(200)]
[Sortable]
[Display(Name = "Tags")]

@ -1 +1 @@
Subproject commit 2e6914d88aa2d46aec41f1baff10ab7d15d62c5a
Subproject commit e1188cd0bee2053cdd8c288601c5539572d9534f