diff --git a/.azure-pipelines/Release Build.yml b/.azure-pipelines/Release Build.yml index e6eb2ff44..abc064506 100644 --- a/.azure-pipelines/Release Build.yml +++ b/.azure-pipelines/Release Build.yml @@ -5,8 +5,8 @@ pr: - master jobs: -- job: Build - displayName: Build +- job: Windows_Build + displayName: Windows Build timeoutInMinutes: 360 pool: vmImage: windows-latest @@ -44,12 +44,6 @@ jobs: - task: NuGetToolInstaller@1 displayName: 'Use NuGet' - - task: NuGetCommand@2 - displayName: NuGet restore Agent.Installer.Win - inputs: - solution: Agent.Installer.Win/packages.config - packagesDirectory: $(Build.SourcesDirectory)\packages - - task: UseDotNet@2 displayName: Use .NET SDK inputs: diff --git a/.gitignore b/.gitignore index 243775cf0..93d822baf 100644 --- a/.gitignore +++ b/.gitignore @@ -296,3 +296,4 @@ Server.Installer/Properties/launchSettings.json !/.vscode/launch.json !/.vscode/tasks.json /Server/appsettings.Development.json +/Server/AppData diff --git a/Agent.Installer.Win/ViewModels/MainWindowViewModel.cs b/Agent.Installer.Win/ViewModels/MainWindowViewModel.cs index 2f887031b..86b4c1b89 100644 --- a/Agent.Installer.Win/ViewModels/MainWindowViewModel.cs +++ b/Agent.Installer.Win/ViewModels/MainWindowViewModel.cs @@ -317,7 +317,7 @@ private bool CheckParams() (serverUri.Scheme != Uri.UriSchemeHttp && serverUri.Scheme != Uri.UriSchemeHttps)) { Logger.Write("ServerUrl is not valid."); - MessageBoxEx.Show("Server URL must be a valid Uri (e.g. https://app.remotely.one).", "Invalid Server URL", MessageBoxButton.OK, MessageBoxImage.Error); + MessageBoxEx.Show("Server URL must be a valid Uri (e.g. https://app.example.com).", "Invalid Server URL", MessageBoxButton.OK, MessageBoxImage.Error); return false; } diff --git a/Agent/Agent.csproj b/Agent/Agent.csproj index 4d5b68f83..a754785f5 100644 --- a/Agent/Agent.csproj +++ b/Agent/Agent.csproj @@ -11,7 +11,6 @@ Remotely Agent Immense Networks 1.0.0.0 - https://remotely.one AnyCPU;x86;x64 Remotely_Agent Remotely.Agent @@ -24,18 +23,18 @@ - - + + - - - - - + + + + + diff --git a/Agent/Program.cs b/Agent/Program.cs index 82b8800b2..370400920 100644 --- a/Agent/Program.cs +++ b/Agent/Program.cs @@ -2,13 +2,10 @@ using Microsoft.Extensions.Logging; using Remotely.Agent.Interfaces; using Remotely.Agent.Services; -using Remotely.Shared.Enums; using Remotely.Shared.Utilities; using Remotely.Shared.Services; using System; -using System.Diagnostics; using System.IO; -using System.ServiceProcess; using System.Threading.Tasks; using System.Runtime.Versioning; using Remotely.Agent.Services.Linux; diff --git a/Desktop.Linux/Desktop.Linux.csproj b/Desktop.Linux/Desktop.Linux.csproj index 7faab16c2..15014a8a5 100644 --- a/Desktop.Linux/Desktop.Linux.csproj +++ b/Desktop.Linux/Desktop.Linux.csproj @@ -30,7 +30,6 @@ Remotely Desktop Desktop client for allowing your IT admin to provide remote support. Copyright © 2023 Immense Networks - https://remotely.one enable diff --git a/Desktop.Linux/Program.cs b/Desktop.Linux/Program.cs index 10972d166..72ee7948f 100644 --- a/Desktop.Linux/Program.cs +++ b/Desktop.Linux/Program.cs @@ -33,7 +33,7 @@ public static async Task Main(string[] args) var logger = new FileLogger("Remotely_Desktop", version, "Program.cs"); var filePath = Environment.ProcessPath ?? Environment.GetCommandLineArgs().First(); var serverUrl = Debugger.IsAttached ? "http://localhost:5000" : string.Empty; - var getEmbeddedResult = await EmbeddedServerDataSearcher.Instance.TryGetEmbeddedData(filePath); + var getEmbeddedResult = EmbeddedServerDataProvider.Instance.TryGetEmbeddedData(filePath); if (getEmbeddedResult.IsSuccess) { serverUrl = getEmbeddedResult.Value.ServerUrl.AbsoluteUri; @@ -46,7 +46,7 @@ public static async Task Main(string[] args) var services = new ServiceCollection(); services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); services.AddRemoteControlLinux( config => diff --git a/Desktop.Shared/Services/BrandingProvider.cs b/Desktop.Shared/Services/BrandingProvider.cs index 1e128a966..113cac48a 100644 --- a/Desktop.Shared/Services/BrandingProvider.cs +++ b/Desktop.Shared/Services/BrandingProvider.cs @@ -13,7 +13,7 @@ namespace Desktop.Shared.Services; public class BrandingProvider : IBrandingProvider { private readonly IAppState _appState; - private readonly IEmbeddedServerDataSearcher _embeddedDataSearcher; + private readonly IEmbeddedServerDataProvider _embeddedDataSearcher; private readonly ILogger _logger; private readonly IOrganizationIdProvider _orgIdProvider; private BrandingInfoBase? _brandingInfo; @@ -22,7 +22,7 @@ public class BrandingProvider : IBrandingProvider public BrandingProvider( IAppState appState, IOrganizationIdProvider orgIdProvider, - IEmbeddedServerDataSearcher embeddedServerDataSearcher, + IEmbeddedServerDataProvider embeddedServerDataSearcher, ILogger logger) { _appState = appState; @@ -85,7 +85,7 @@ private async Task> TryGetBrandingInfo() return Result.Fail("Failed to retrieve executing file name."); } - var result = await _embeddedDataSearcher.TryGetEmbeddedData(filePath); + var result = _embeddedDataSearcher.TryGetEmbeddedData(filePath); if (!result.IsSuccess) { diff --git a/Desktop.Win/Desktop.Win.csproj b/Desktop.Win/Desktop.Win.csproj index e5532159a..bf4a04b5f 100644 --- a/Desktop.Win/Desktop.Win.csproj +++ b/Desktop.Win/Desktop.Win.csproj @@ -12,7 +12,6 @@ Jared Goodwin Immense Networks Remotely Desktop - https://remotely.one AnyCPU;x86;x64 True diff --git a/Desktop.Win/Program.cs b/Desktop.Win/Program.cs index 1b3653185..42e63e994 100644 --- a/Desktop.Win/Program.cs +++ b/Desktop.Win/Program.cs @@ -35,7 +35,7 @@ public static async Task Main(string[] args) var logger = new FileLogger("Remotely_Desktop", version, "Program.cs"); var filePath = Environment.ProcessPath ?? Environment.GetCommandLineArgs().First(); var serverUrl = Debugger.IsAttached ? "https://localhost:5001" : string.Empty; - var getEmbeddedResult = await EmbeddedServerDataSearcher.Instance.TryGetEmbeddedData(filePath); + var getEmbeddedResult = EmbeddedServerDataProvider.Instance.TryGetEmbeddedData(filePath); if (getEmbeddedResult.IsSuccess) { serverUrl = getEmbeddedResult.Value.ServerUrl.AbsoluteUri; @@ -47,7 +47,7 @@ public static async Task Main(string[] args) var services = new ServiceCollection(); services.AddSingleton(); - services.AddSingleton(EmbeddedServerDataSearcher.Instance); + services.AddSingleton(EmbeddedServerDataProvider.Instance); services.AddRemoteControlWindows( config => diff --git a/Remotely.slnLaunch b/Remotely.slnLaunch new file mode 100644 index 000000000..8732c505f --- /dev/null +++ b/Remotely.slnLaunch @@ -0,0 +1,37 @@ +[ + { + "Name": "Agent \u002B Server \u002B Desktop", + "Projects": [ + { + "Path": "Agent\\Agent.csproj", + "Action": "Start", + "DebugTarget": "Agent" + }, + { + "Path": "Desktop.Win\\Desktop.Win.csproj", + "Action": "Start", + "DebugTarget": "Desktop.Win" + }, + { + "Path": "Server\\Server.csproj", + "Action": "Start", + "DebugTarget": "Server" + } + ] + }, + { + "Name": "Agent \u002B Server", + "Projects": [ + { + "Path": "Agent\\Agent.csproj", + "Action": "Start", + "DebugTarget": "Agent" + }, + { + "Path": "Server\\Server.csproj", + "Action": "Start", + "DebugTarget": "Server" + } + ] + } +] \ No newline at end of file diff --git a/Server/API/ClientDownloadsController.cs b/Server/API/ClientDownloadsController.cs index 8638492ef..e3944608b 100644 --- a/Server/API/ClientDownloadsController.cs +++ b/Server/API/ClientDownloadsController.cs @@ -15,14 +15,14 @@ namespace Remotely.Server.API; public class ClientDownloadsController : ControllerBase { private readonly IDataService _dataService; - private readonly IEmbeddedServerDataSearcher _embeddedDataSearcher; + private readonly IEmbeddedServerDataProvider _embeddedDataSearcher; private readonly SemaphoreSlim _fileLock = new(1, 1); private readonly IWebHostEnvironment _hostEnv; private readonly ILogger _logger; public ClientDownloadsController( IWebHostEnvironment hostEnv, - IEmbeddedServerDataSearcher embeddedDataSearcher, + IEmbeddedServerDataProvider embeddedDataSearcher, IDataService dataService, ILogger logger) { @@ -39,27 +39,27 @@ public async Task GetDesktop(string platformID) { case "WindowsDesktop-x64": { - var filePath = Path.Combine(_hostEnv.WebRootPath, "Content", "Win-x64", "Remotely_Desktop.exe"); + var filePath = Path.Combine("Content", "Win-x64", "Remotely_Desktop.exe"); return await GetDesktopFile(filePath); } case "WindowsDesktop-x86": { - var filePath = Path.Combine(_hostEnv.WebRootPath, "Content", "Win-x86", "Remotely_Desktop.exe"); + var filePath = Path.Combine("Content", "Win-x86", "Remotely_Desktop.exe"); return await GetDesktopFile(filePath); } case "UbuntuDesktop": { - var filePath = Path.Combine(_hostEnv.WebRootPath, "Content", "Linux-x64", "Remotely_Desktop"); + var filePath = Path.Combine("Content", "Linux-x64", "Remotely_Desktop"); return await GetDesktopFile(filePath); } case "MacOS-x64": { - var filePath = Path.Combine(_hostEnv.WebRootPath, "Content", "MacOS-x64", "Remotely_Desktop"); + var filePath = Path.Combine("Content", "MacOS-x64", "Remotely_Desktop"); return await GetDesktopFile(filePath); } case "MacOS-arm64": { - var filePath = Path.Combine(_hostEnv.WebRootPath, "Content", "MacOS-arm64", "Remotely_Desktop"); + var filePath = Path.Combine("Content", "MacOS-arm64", "Remotely_Desktop"); return await GetDesktopFile(filePath); } default: @@ -75,27 +75,27 @@ public async Task GetDesktop(string platformId, string organizati { case "WindowsDesktop-x64": { - var filePath = Path.Combine(_hostEnv.WebRootPath, "Content", "Win-x64", "Remotely_Desktop.exe"); + var filePath = Path.Combine("Content", "Win-x64", "Remotely_Desktop.exe"); return await GetDesktopFile(filePath, organizationId); } case "WindowsDesktop-x86": { - var filePath = Path.Combine(_hostEnv.WebRootPath, "Content", "Win-x86", "Remotely_Desktop.exe"); + var filePath = Path.Combine("Content", "Win-x86", "Remotely_Desktop.exe"); return await GetDesktopFile(filePath, organizationId); } case "UbuntuDesktop": { - var filePath = Path.Combine(_hostEnv.WebRootPath, "Content", "Linux-x64", "Remotely_Desktop"); + var filePath = Path.Combine("Content", "Linux-x64", "Remotely_Desktop"); return await GetDesktopFile(filePath, organizationId); } case "MacOS-x64": { - var filePath = Path.Combine(_hostEnv.WebRootPath, "Content", "MacOS-x64", "Remotely_Desktop"); + var filePath = Path.Combine("Content", "MacOS-x64", "Remotely_Desktop"); return await GetDesktopFile(filePath); } case "MacOS-arm64": { - var filePath = Path.Combine(_hostEnv.WebRootPath, "Content", "MacOS-arm64", "Remotely_Desktop"); + var filePath = Path.Combine("Content", "MacOS-arm64", "Remotely_Desktop"); return await GetDesktopFile(filePath); } default: @@ -137,22 +137,26 @@ private async Task GetBashInstaller(string fileName, string organ return File(fileBytes, "application/octet-stream", fileName); } - private async Task GetDesktopFile(string filePath, string? organizationId = null) + private async Task GetDesktopFile(string relativeFilePath, string? organizationId = null) { - var settings = await _dataService.GetSettings(); await LogRequest(nameof(GetDesktopFile)); + var defaultOrg = await _dataService.GetDefaultOrganization(); - var effectiveScheme = settings.ForceClientHttps ? "https" : Request.Scheme; - var serverUrl = $"{effectiveScheme}://{Request.Host}"; - var embeddedData = new EmbeddedServerData(new Uri(serverUrl), organizationId); - var result = await _embeddedDataSearcher.GetAppendedStream(filePath, embeddedData); - - if (!result.IsSuccess) + // The default org will be used if unspecified, so might as well save the + // space in the file name. + if (defaultOrg.IsSuccess && + defaultOrg.Value.ID.Equals(organizationId, StringComparison.OrdinalIgnoreCase)) { - throw result.Exception ?? new Exception(result.Reason); + organizationId = null; } - return File(result.Value, "application/octet-stream", Path.GetFileName(filePath)); + var settings = await _dataService.GetSettings(); + var effectiveScheme = settings.ForceClientHttps ? "https" : Request.Scheme; + var serverUrl = $"{effectiveScheme}://{Request.Host}"; + + var embeddedData = new EmbeddedServerData(new Uri(serverUrl), organizationId); + var fileName = _embeddedDataSearcher.GetEncodedFileName(relativeFilePath, embeddedData); + return File(relativeFilePath, "application/octet-stream", fileName); } private async Task GetInstallFile(string organizationId, string platformID) diff --git a/Server/API/CustomBinariesController.cs b/Server/API/CustomBinariesController.cs new file mode 100644 index 000000000..cd12ff4e5 --- /dev/null +++ b/Server/API/CustomBinariesController.cs @@ -0,0 +1,52 @@ +using Microsoft.AspNetCore.Mvc; +using Remotely.Server.Services; +using Remotely.Shared.Models; +using Remotely.Shared.Services; + +namespace Remotely.Server.API; + +[Route("api/custom-binaries")] +[ApiController] +public class CustomBinariesController( + IDataService _dataService, + IWebHostEnvironment _hostingEnvironment, + IEmbeddedServerDataProvider _embeddedData) : ControllerBase +{ + [HttpGet("win-x86/desktop/{organizationId}")] + public async Task GetWinX86Desktop(string organizationId) + { + var embeddedData = await GetEmbeddedData(organizationId); + var filePath = Path.Combine(_hostingEnvironment.ContentRootPath, "AppData", "Win-x86", "Remotely_Desktop.exe"); + var fileName = _embeddedData.GetEncodedFileName(filePath, embeddedData); + var rs = System.IO.File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.Read); + return File(rs, "application/octet-stream", fileName); + } + + [HttpGet("win-x64/desktop/{organizationId}")] + public async Task GetWinX64Desktop(string organizationId) + { + var embeddedData = await GetEmbeddedData(organizationId); + var filePath = Path.Combine(_hostingEnvironment.ContentRootPath, "AppData", "Win-x64", "Remotely_Desktop.exe"); + var fileName = _embeddedData.GetEncodedFileName(filePath, embeddedData); + var rs = System.IO.File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.Read); + return File(rs, "application/octet-stream", fileName); + } + + private async Task GetEmbeddedData(string? organizationId) + { + var defaultOrg = await _dataService.GetDefaultOrganization(); + + // The default org will be used if unspecified, so might as well save the + // space in the file name. + if (defaultOrg.IsSuccess && + defaultOrg.Value.ID.Equals(organizationId, StringComparison.OrdinalIgnoreCase)) + { + organizationId = null; + } + + var settings = await _dataService.GetSettings(); + var effectiveScheme = settings.ForceClientHttps ? "https" : Request.Scheme; + var serverUrl = $"{effectiveScheme}://{Request.Host}"; + return new EmbeddedServerData(new Uri(serverUrl), organizationId); + } +} diff --git a/Server/Auth/TwoFactorRequiredHandler.cs b/Server/Auth/TwoFactorRequiredHandler.cs index 0d62fb79a..47fb2dfef 100644 --- a/Server/Auth/TwoFactorRequiredHandler.cs +++ b/Server/Auth/TwoFactorRequiredHandler.cs @@ -1,31 +1,21 @@ using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Identity; +using Remotely.Server.Models; using Remotely.Server.Services; -using Remotely.Shared.Entities; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; +using System.Security.Principal; namespace Remotely.Server.Auth; -public class TwoFactorRequiredHandler : AuthorizationHandler +public class TwoFactorRequiredHandler( + IHttpContextAccessor _contextAccessor, + IDataService _dataService) : AuthorizationHandler { - private readonly IDataService _dataService; - - public TwoFactorRequiredHandler(IDataService dataService) - { - _dataService = dataService; - } - protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, TwoFactorRequiredRequirement requirement) { var settings = await _dataService.GetSettings(); - if (context.User.Identity?.IsAuthenticated == true && - context.User.Identity.Name is not null && - settings.Require2FA) + if (context.User?.Identity is { } identity && + IsTwoFactorRequired(identity, settings)) { - var userResult = await _dataService.GetUserByName(context.User.Identity.Name); + var userResult = await _dataService.GetUserByName(identity.Name!); if (!userResult.IsSuccess || !userResult.Value.TwoFactorEnabled) @@ -36,4 +26,20 @@ context.User.Identity.Name is not null && } context.Succeed(requirement); } + + private bool IsTwoFactorRequired(IIdentity identity, SettingsModel settings) + { + // Account management pages are exempt since they're required + // to set up 2FA. + var path = _contextAccessor.HttpContext?.Request.Path ?? ""; + if (path.StartsWithSegments("/Account/Manage")) + { + return false; + } + + return + settings.Require2FA && + identity.IsAuthenticated && + identity.Name is not null; + } } diff --git a/Server/Components/AuthorizedIndex.razor b/Server/Components/AuthorizedIndex.razor index 2c8a1f363..20eb4ed9f 100644 --- a/Server/Components/AuthorizedIndex.razor +++ b/Server/Components/AuthorizedIndex.razor @@ -19,17 +19,6 @@ @code { private SettingsModel? _settings; - protected override async Task OnAfterRenderAsync(bool firstRender) - { - if (User is not null && - _settings?.Require2FA == true && - !User.TwoFactorEnabled) - { - NavManager.NavigateTo("/TwoFactorRequired"); - } - await base.OnAfterRenderAsync(firstRender); - } - protected override async Task OnInitializedAsync() { _settings = await DataService.GetSettings(); diff --git a/Server/Components/Devices/DeviceCard.razor b/Server/Components/Devices/DeviceCard.razor index 69a61ae26..c4a18746b 100644 --- a/Server/Components/Devices/DeviceCard.razor +++ b/Server/Components/Devices/DeviceCard.razor @@ -59,7 +59,9 @@ { foreach (var kvp in _fileUploadProgressLookup) { - +
+ @(GetProgressMessage(kvp.Key)) +
} }
diff --git a/Server/Components/Devices/DeviceCard.razor.cs b/Server/Components/Devices/DeviceCard.razor.cs index ca4a4fb55..df5a66913 100644 --- a/Server/Components/Devices/DeviceCard.razor.cs +++ b/Server/Components/Devices/DeviceCard.razor.cs @@ -7,6 +7,7 @@ using Remotely.Server.Models.Messages; using Remotely.Server.Services; using Remotely.Server.Services.Stores; +using Remotely.Shared; using Remotely.Shared.Entities; using Remotely.Shared.Enums; using Remotely.Shared.Utilities; @@ -36,19 +37,22 @@ public partial class DeviceCard : AuthComponentBase [Inject] - private ISelectedCardsStore SelectedCards { get; init; } = null!; + public required ISelectedCardsStore SelectedCards { get; init; } [Inject] - private IThemeProvider ThemeProvider { get; init; } = null!; + public required IThemeProvider ThemeProvider { get; init; } [Inject] - private ICircuitConnection CircuitConnection { get; init; } = null!; + public required ICircuitConnection CircuitConnection { get; init; } [Inject] - private IDataService DataService { get; init; } = null!; + public required IDataService DataService { get; init; } [Inject] - private IChatSessionStore ChatCache { get; init; } = null!; + public required IChatSessionStore ChatCache { get; init; } + + [Inject] + public required ILogger Logger { get; init; } private bool IsExpanded => _state == DeviceCardState.Expanded; @@ -220,23 +224,44 @@ await DataService.UpdateDevice(Device.ID, private async Task OnFileInputChanged(InputFileChangeEventArgs args) { - EnsureUserSet(); - - ToastService.ShowToast("File upload started."); - - var fileId = await DataService.AddSharedFile(args.File, User.OrganizationID, OnFileInputProgress); + try + { + EnsureUserSet(); - var transferId = Guid.NewGuid().ToString(); + ToastService.ShowToast("File upload started."); - var result = await CircuitConnection.TransferFileFromBrowserToAgent(Device.ID, transferId, new[] { fileId }); + if (args.File.Size > AppConstants.MaxUploadFileSize) + { + var maxFileSize = AppConstants.MaxUploadFileSize / 1000 / 1000; + ToastService.ShowToast2($"File size exceeds the maximum allowed size of {maxFileSize}MB.", ToastType.Warning); + return; + } + + var fileId = await DataService.AddSharedFile(args.File, User.OrganizationID, OnFileInputProgress); + var transferId = Guid.NewGuid().ToString(); + var result = await CircuitConnection.TransferFileFromBrowserToAgent(Device.ID, transferId, [fileId]); - if (!result) + if (!result) + { + ToastService.ShowToast("Device not found.", classString: "bg-warning"); + } + else + { + ToastService.ShowToast("File upload completed."); + } + } + catch (Exception ex) { - ToastService.ShowToast("Device not found.", classString: "bg-warning"); + Logger.LogError(ex, "Error while uploading file to device."); + ToastService.ShowToast2("Failed to upload file", ToastType.Error); } - else + finally { - ToastService.ShowToast("File upload completed."); + if (args.File.Name is not null) + { + _ = _fileUploadProgressLookup.TryRemove(args.File.Name, out _); + await InvokeAsync(StateHasChanged); + } } } @@ -252,6 +277,7 @@ private void OnFileInputProgress(double percentComplete, string fileName) _fileUploadProgressLookup.AddOrUpdate(fileName, percentComplete, (k, v) => percentComplete); InvokeAsync(StateHasChanged); } + private void OpenDeviceDetails() { JsInterop.OpenWindow($"/device-details/{Device.ID}", "_blank"); diff --git a/Server/Components/Layout/MainLayout.razor b/Server/Components/Layout/MainLayout.razor index c8a88b432..c4acb02f1 100644 --- a/Server/Components/Layout/MainLayout.razor +++ b/Server/Components/Layout/MainLayout.razor @@ -1,6 +1,7 @@ @using Remotely.Server.Components @using Remotely.Server.Auth @inherits LayoutComponentBase +@inject NavigationManager NavMan @@ -25,7 +26,7 @@

Two-factor authentication is required. Click the button below to set up your authenticator app.

- Enable 2FA +

@@ -42,4 +43,11 @@ - \ No newline at end of file + + +@code { + private void NavigateToTwoFactor() + { + NavMan.NavigateTo("/Account/Manage/TwoFactorAuthentication", true); + } +} \ No newline at end of file diff --git a/Server/Components/Layout/NavMenu.razor b/Server/Components/Layout/NavMenu.razor index 3c016ea0e..585ab1352 100644 --- a/Server/Components/Layout/NavMenu.razor +++ b/Server/Components/Layout/NavMenu.razor @@ -25,26 +25,34 @@

Resident Agents

+

Installable background agents that provide unattended access and remote scripting.

@@ -72,7 +81,7 @@

Example Quiet Install:
- + powershell.exe -ExecutionPolicy Bypass -F {path}\Install-Remotely.ps1 -install @@ -88,7 +97,7 @@

Example Local Install:
- + powershell.exe -ExecutionPolicy Bypass -F {path}\Install-Remotely.ps1 -install @@ -100,7 +109,7 @@

All Override Options:
- + powershell.exe -ExecutionPolicy Bypass -F {path}\Install-Remotely.ps1 -install -quiet -supportshortcut @@ -124,12 +133,12 @@

Example Install:
- + sudo [path]/Install-Ubuntu-x64.sh

Example Local Install:
- + sudo [path]/Install-Ubuntu-x64.sh --path [path]/Remotely-Linux.zip

@@ -149,17 +158,17 @@

Example Install:
- + sudo [path]/Install-MacOS-x64.sh

Example Local Install:
- + sudo [path]/Install-MacOS-x64.sh --path [path]/Remotely-MacOS-x64.zip

Example Uninstall:
- + sudo [path]/Install-MacOS-x64.sh --uninstall

diff --git a/Server/Components/Pages/ServerConfig.razor b/Server/Components/Pages/ServerConfig.razor index 6c322e460..fefe8dc20 100644 --- a/Server/Components/Pages/ServerConfig.razor +++ b/Server/Components/Pages/ServerConfig.razor @@ -345,9 +345,7 @@ -
- -
+ \ No newline at end of file diff --git a/Server/Components/Pages/ServerConfig.razor.css b/Server/Components/Pages/ServerConfig.razor.css index 6a2d3fcb7..0bdd2c908 100644 --- a/Server/Components/Pages/ServerConfig.razor.css +++ b/Server/Components/Pages/ServerConfig.razor.css @@ -6,4 +6,10 @@ overflow-x: hidden; white-space: nowrap; border-radius: 5px; +} + +#saveButton { + position: fixed; + right: 40px; + bottom: 20px; } \ No newline at end of file diff --git a/Server/Components/Scripts/SavedScripts.razor b/Server/Components/Scripts/SavedScripts.razor index 39bfbc126..a768c94a5 100644 --- a/Server/Components/Scripts/SavedScripts.razor +++ b/Server/Components/Scripts/SavedScripts.razor @@ -122,7 +122,7 @@
Server URL
-
The URL of the server that the device connects to (e.g. https://app.remotely.one).
+
The URL of the server that the device connects to (e.g. https://app.example.com).
diff --git a/Server/Components/_Imports.razor b/Server/Components/_Imports.razor index bbc5eac2e..9b1d1830c 100644 --- a/Server/Components/_Imports.razor +++ b/Server/Components/_Imports.razor @@ -25,4 +25,5 @@ @using Remotely.Server.Components.TreeView @using Remotely.Server.Auth @using Remotely.Shared.Entities -@using Remotely.Server.Models \ No newline at end of file +@using Remotely.Server.Models +@using Remotely.Shared.Services; \ No newline at end of file diff --git a/Server/Dockerfile b/Server/Dockerfile index cdb3fc2a1..e42ad611a 100644 --- a/Server/Dockerfile +++ b/Server/Dockerfile @@ -9,5 +9,5 @@ WORKDIR /app ENTRYPOINT ["dotnet", "Remotely_Server.dll"] -HEALTHCHECK --interval=5m --timeout=3s \ +HEALTHCHECK \ CMD curl -f http://localhost:${ASPNETCORE_HTTP_PORTS}/api/healthcheck || exit 1 \ No newline at end of file diff --git a/Server/Dockerfile.pipelines b/Server/Dockerfile.pipelines index 8590ecbd8..3db38ebb2 100644 --- a/Server/Dockerfile.pipelines +++ b/Server/Dockerfile.pipelines @@ -9,5 +9,5 @@ WORKDIR /app ENTRYPOINT ["dotnet", "Remotely_Server.dll"] -HEALTHCHECK --interval=5m --timeout=3s \ +HEALTHCHECK \ CMD curl -f http://localhost:${ASPNETCORE_HTTP_PORTS}/api/healthcheck || exit 1 \ No newline at end of file diff --git a/Server/Program.cs b/Server/Program.cs index 8d43320fa..9104f0113 100644 --- a/Server/Program.cs +++ b/Server/Program.cs @@ -193,7 +193,6 @@ services.AddSignalR(options => { options.EnableDetailedErrors = builder.Environment.IsDevelopment(); - options.MaximumParallelInvocationsPerClient = 5; options.MaximumReceiveMessageSize = 100_000; }) .AddJsonProtocol(options => @@ -246,7 +245,7 @@ services.AddScoped(); services.AddScoped(); services.AddSingleton(); -services.AddSingleton(); +services.AddSingleton(); services.AddSingleton(); services.AddScoped(); services.AddScoped(); diff --git a/Server/Server.csproj b/Server/Server.csproj index f5d1e7111..e77691750 100644 --- a/Server/Server.csproj +++ b/Server/Server.csproj @@ -14,25 +14,25 @@ - - - - - - - + + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - + + + + - + diff --git a/Server/Services/DataService.cs b/Server/Services/DataService.cs index 65c4d98b1..ed7b8bc3e 100644 --- a/Server/Services/DataService.cs +++ b/Server/Services/DataService.cs @@ -15,6 +15,7 @@ using Remotely.Shared.Dtos; using Remotely.Shared.Entities; using Remotely.Shared.Enums; +using Remotely.Shared.Extensions; using Remotely.Shared.Models; using Remotely.Shared.Utilities; using Remotely.Shared.ViewModels; @@ -552,18 +553,26 @@ public async Task AddScriptRun(ScriptRun scriptRun) public async Task AddSharedFile(IBrowserFile file, string organizationId, Action progressCallback) { - var fileContents = new byte[file.Size]; - using var stream = file.OpenReadStream(AppConstants.MaxUploadFileSize); + var fileSize = file.Size; + var fileName = file.Name; - for (var i = 0; i < file.Size; i += 5_000) - { - var readSize = (int)Math.Min(5_000, file.Size - i); - await stream.ReadAsync(fileContents.AsMemory(i, readSize)); + var fileContents = new byte[fileSize]; + var stream = file.OpenReadStream(AppConstants.MaxUploadFileSize); - progressCallback.Invoke((double)stream.Position / stream.Length, file.Name); + var bytesRead = 0; + while (bytesRead < fileSize) + { + var segmentEnd = Math.Min(50_000, fileSize - bytesRead); + var read = await stream.ReadAsync(fileContents.AsMemory(bytesRead, (int)segmentEnd)); + if (read == 0) + { + break; + } + bytesRead += read; + progressCallback.Invoke((double)bytesRead / fileSize, fileName); } - progressCallback.Invoke(1, file.Name); + progressCallback.Invoke(1, fileName); return await AddSharedFileImpl(file.Name, fileContents, file.ContentType, organizationId); } diff --git a/Server/Services/JsInterop.cs b/Server/Services/JsInterop.cs index d9e3ba6a5..be63cecd3 100644 --- a/Server/Services/JsInterop.cs +++ b/Server/Services/JsInterop.cs @@ -6,6 +6,8 @@ namespace Remotely.Server.Services; public interface IJsInterop { + void AddBeforeUnloadHandler(); + void AddClassName(ElementReference element, string className); ValueTask Alert(string message); @@ -13,20 +15,25 @@ public interface IJsInterop ValueTask Confirm(string message); + ValueTask GetCursorIndex(ElementReference terminalInput); + void InvokeClick(string elementId); + void OpenWindow(string url, string target); + + void PreventTabOut(ElementReference terminalInput); + ValueTask Prompt(string message); - void SetStyleProperty(ElementReference element, string propertyName, string value); - void StartDraggingY(ElementReference element, double clientY); + void ScrollToElement(ElementReference element); void ScrollToEnd(ElementReference element); - void AddBeforeUnloadHandler(); - void OpenWindow(string url, string target); - void ScrollToElement(ElementReference element); - ValueTask GetCursorIndex(ElementReference terminalInput); - void PreventTabOut(ElementReference terminalInput); + ValueTask SetClipboardText(string text); + + void SetStyleProperty(ElementReference element, string propertyName, string value); + void StartDraggingY(ElementReference element, double clientY); } + public class JsInterop : IJsInterop { private readonly IJSRuntime _jsRuntime; @@ -95,6 +102,12 @@ public void ScrollToEnd(ElementReference element) { _jsRuntime.InvokeVoidAsync("scrollToEnd", element); } + + public async ValueTask SetClipboardText(string text) + { + return await _jsRuntime.InvokeAsync("setClipboardText", text); + } + public void SetStyleProperty(ElementReference element, string propertyName, string value) { _jsRuntime.InvokeVoidAsync("setStyleProperty", element, propertyName, value); diff --git a/Server/wwwroot/interop.js b/Server/wwwroot/interop.js index 9f1ec9a12..626ea2131 100644 --- a/Server/wwwroot/interop.js +++ b/Server/wwwroot/interop.js @@ -42,6 +42,22 @@ window.scrollToElement = (element) => { }, 200); } + +/** + * @param {string} text + * @returns {Promise} + */ +window.setClipboardText = async (text) => { + try { + await navigator.clipboard.writeText(text); + return true; + } + catch (ex) { + console.error(ex); + return false; + } +} + window.setStyleProperty = (element, propertyName, value) => { element.style[propertyName] = value; } diff --git a/Shared/Services/EmbeddedServerDataProvider.cs b/Shared/Services/EmbeddedServerDataProvider.cs new file mode 100644 index 000000000..e42ed109b --- /dev/null +++ b/Shared/Services/EmbeddedServerDataProvider.cs @@ -0,0 +1,54 @@ +#nullable enable + +using Immense.RemoteControl.Shared; +using MessagePack; +using Remotely.Shared.Models; +using System; +using System.IO; + +namespace Remotely.Shared.Services; + +public interface IEmbeddedServerDataProvider +{ + string GetEncodedFileName(string filePath, EmbeddedServerData serverData); + Result TryGetEmbeddedData(string filePath); +} + +public class EmbeddedServerDataProvider : IEmbeddedServerDataProvider +{ + public static EmbeddedServerDataProvider Instance { get; } = new(); + + public string GetEncodedFileName(string filePath, EmbeddedServerData serverData) + { + var fileName = Path.GetFileNameWithoutExtension(filePath); + var ext = Path.GetExtension(filePath); + + // Make the base64 string safe file paths and URIs. + var encodedData = Convert + .ToBase64String(MessagePackSerializer.Serialize(serverData)) + .Replace("/", "_") + .Replace("+", "-"); + + return $"{fileName}[{encodedData}]{ext}"; + } + + public Result TryGetEmbeddedData(string filePath) + { + try + { + var fileName = Path.GetFileNameWithoutExtension(filePath); + var start = fileName.LastIndexOf('[') + 1; + var end = fileName.LastIndexOf(']'); + var base64 = fileName[start..end] + .Replace("_", "/") + .Replace("-", "+"); + + var embeddedData = MessagePackSerializer.Deserialize(Convert.FromBase64String(base64)); + return Result.Ok(embeddedData); + } + catch (Exception ex) + { + return Result.Fail(ex); + } + } +} diff --git a/Shared/Services/EmbeddedServerDataSearcher.cs b/Shared/Services/EmbeddedServerDataSearcher.cs deleted file mode 100644 index 72b948b7b..000000000 --- a/Shared/Services/EmbeddedServerDataSearcher.cs +++ /dev/null @@ -1,82 +0,0 @@ -#nullable enable - -using Immense.RemoteControl.Shared; -using MessagePack; -using Microsoft.Extensions.Logging; -using Remotely.Shared.Entities; -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.Text; -using System.Text.Json; -using System.Threading.Tasks; - -namespace Remotely.Shared.Services; - -public interface IEmbeddedServerDataSearcher -{ - Task> GetAppendedStream(string filePath, EmbeddedServerData serverData); - Task> TryGetEmbeddedData(string filePath); -} - -public class EmbeddedServerDataSearcher() : IEmbeddedServerDataSearcher -{ - public static EmbeddedServerDataSearcher Instance { get; } = new(); - - public async Task> TryGetEmbeddedData(string filePath) - { - try - { - if (!File.Exists(filePath)) - { - return Result.Fail($"File path does not exist: {filePath}"); - } - - using var fs = File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.Read); - using var br = new BinaryReader(fs); - using var sr = new StreamReader(fs); - - fs.Seek(-4, SeekOrigin.End); - var dataSize = br.ReadInt32(); - fs.Seek(-dataSize - 4, SeekOrigin.End); - - var buffer = new byte[dataSize]; - await fs.ReadExactlyAsync(buffer); - var json = Encoding.UTF8.GetString(buffer); - - var embeddedData = JsonSerializer.Deserialize(json); - - if (embeddedData is null) - { - return Result.Fail("Embedded data is empty."); - } - - return Result.Ok(embeddedData); - } - catch (Exception ex) - { - return Result.Fail(ex); - } - } - - public Task> GetAppendedStream(string filePath, EmbeddedServerData serverData) - { - try - { - var json = JsonSerializer.Serialize(serverData); - var jsonBytes = Encoding.UTF8.GetBytes(json); - byte[] appendPayload = [.. jsonBytes, .. BitConverter.GetBytes(jsonBytes.Length)]; - var fs = File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.Read); - var appendableStream = new AppendableStream(fs, appendPayload); - return Task.FromResult(Result.Ok(appendableStream)); - } - catch (Exception ex) - { - return Task.FromResult(Result.Fail(ex)); - } - } -} diff --git a/Shared/Shared.csproj b/Shared/Shared.csproj index 66ace65bc..1774f3c8f 100644 --- a/Shared/Shared.csproj +++ b/Shared/Shared.csproj @@ -10,10 +10,10 @@ - + - + diff --git a/Tests/LoadTester/LoadTester.csproj b/Tests/LoadTester/LoadTester.csproj index fed703384..a8401d86e 100644 --- a/Tests/LoadTester/LoadTester.csproj +++ b/Tests/LoadTester/LoadTester.csproj @@ -8,7 +8,7 @@ - + diff --git a/Tests/Server.Tests/Server.Tests.csproj b/Tests/Server.Tests/Server.Tests.csproj index 13a5c7116..e1031ed25 100644 --- a/Tests/Server.Tests/Server.Tests.csproj +++ b/Tests/Server.Tests/Server.Tests.csproj @@ -13,12 +13,12 @@ - - - + + + - - + + diff --git a/Tests/Shared.Tests/Shared.Tests.csproj b/Tests/Shared.Tests/Shared.Tests.csproj index 03dc5490a..0bcf9dbca 100644 --- a/Tests/Shared.Tests/Shared.Tests.csproj +++ b/Tests/Shared.Tests/Shared.Tests.csproj @@ -13,10 +13,10 @@ - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Utilities/Example_Nginx_Config.txt b/Utilities/Example_Nginx_Config.txt index 2db1bb9c3..0b82a5c86 100644 --- a/Utilities/Example_Nginx_Config.txt +++ b/Utilities/Example_Nginx_Config.txt @@ -1,6 +1,6 @@ server { listen 80; - server_name app.remotely.one *.app.remotely.one; + server_name app.example.com *.app.example.com; location / { proxy_pass http://localhost:5000; proxy_http_version 1.1; diff --git a/submodules/Immense.RemoteControl b/submodules/Immense.RemoteControl index da865e7ca..4a7d9edde 160000 --- a/submodules/Immense.RemoteControl +++ b/submodules/Immense.RemoteControl @@ -1 +1 @@ -Subproject commit da865e7ca2de8a9c7eb27e81b60c37e08affdc21 +Subproject commit 4a7d9eddedb097654ad992d74ae6f521e4ed2ef2