From cd3057dac1223ea48729de983655c2533a997589 Mon Sep 17 00:00:00 2001 From: Jared Goodwin Date: Tue, 4 Jun 2024 12:34:20 -0700 Subject: [PATCH] Add ability to upload custom versions of the attended support client. --- .azure-pipelines/Release Build.yml | 132 +++++------ .gitignore | 1 + Desktop.Linux/Program.cs | 4 +- Desktop.Shared/Services/BrandingProvider.cs | 4 +- Desktop.Win/Program.cs | 4 +- Server/API/ClientDownloadsController.cs | 41 ++-- Server/API/CustomBinariesController.cs | 52 ++++ Server/Components/Devices/DeviceCard.razor | 4 +- Server/Components/Devices/DeviceCard.razor.cs | 64 +++-- Server/Components/Pages/Deploy.razor | 222 +++++++++++++++--- Server/Components/_Imports.razor | 3 +- Server/Program.cs | 3 +- Server/Services/DataService.cs | 25 +- ...rcher.cs => EmbeddedServerDataProvider.cs} | 15 +- submodules/Immense.RemoteControl | 2 +- 15 files changed, 412 insertions(+), 164 deletions(-) create mode 100644 Server/API/CustomBinariesController.cs rename Shared/Services/{EmbeddedServerDataSearcher.cs => EmbeddedServerDataProvider.cs} (77%) diff --git a/.azure-pipelines/Release Build.yml b/.azure-pipelines/Release Build.yml index b2424efa..37d462eb 100644 --- a/.azure-pipelines/Release Build.yml +++ b/.azure-pipelines/Release Build.yml @@ -5,71 +5,71 @@ pr: - master jobs: -#- job: Mac_Build -# displayName: Mac Build -# timeoutInMinutes: 360 -# pool: -# vmImage: macos-latest -# steps: -# -# - task: InstallSSHKey@0 -# inputs: -# knownHostsEntry: | -# github.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl -# github.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ== -# github.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmKSENjQEezOmxkZMy7opKgwFB9nkt5YRrYMjNuG5N87uRgg6CLrbo5wAdT/y6v0mKV0U2w0WZ2YB/++Tpockg= -# sshKeySecureFile: 'pipelines_rsa' -# -# - checkout: self -# submodules: recursive -# clean: true -# fetchTags: false -# -# - task: PowerShell@2 -# displayName: Add CurrentVersion Variable -# inputs: -# targetType: inline -# script: | -# $VersionString = git show -s --format=%ci $(Build.SourceVersion) -# $VersionDate = [DateTimeOffset]::Parse($VersionString) -# $CurrentVersion = $VersionDate.ToString("yyyy.MM.dd.HHmm") -# -# [System.Console]::WriteLine("##vso[task.setvariable variable=CurrentVersion]$CurrentVersion") -# -# Write-Host "Setting current version to $CurrentVersion." -# -# - task: UseDotNet@2 -# displayName: Use .NET SDK -# inputs: -# version: 8.x -# -# - task: DotNetCoreCLI@2 -# displayName: dotnet publish x64 -# inputs: -# command: publish -# publishWebProjects: false -# projects: '**/Agent.csproj' -# arguments: -c $(BuildConfiguration) -r osx-x64 -o "$(Build.SourcesDirectory)/Agent/bin/publish" /p:Version=$(CurrentVersion) /p:FileVersion=$(CurrentVersion) -# zipAfterPublish: false -# modifyOutputPath: false -# -# - task: PowerShell@2 -# displayName: PowerShell Script -# inputs: -# targetType: inline -# script: | -# Compress-Archive -Path "$(Build.SourcesDirectory)/Agent/bin/publish/*" -DestinationPath "$(Build.SourcesDirectory)/Agent/bin/Remotely-MacOS-x64.zip" -Force -# -# - task: PublishPipelineArtifact@1 -# displayName: Publish macOS x64 Agent -# inputs: -# path: $(Build.SourcesDirectory)/Agent/bin/Remotely-MacOS-x64.zip -# artifactName: Mac-x64-Agent +- job: Mac_Build + displayName: Mac Build + timeoutInMinutes: 360 + pool: + vmImage: macos-latest + steps: + + - task: InstallSSHKey@0 + inputs: + knownHostsEntry: | + github.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl + github.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ== + github.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmKSENjQEezOmxkZMy7opKgwFB9nkt5YRrYMjNuG5N87uRgg6CLrbo5wAdT/y6v0mKV0U2w0WZ2YB/++Tpockg= + sshKeySecureFile: 'pipelines_rsa' + + - checkout: self + submodules: recursive + clean: true + fetchTags: false + + - task: PowerShell@2 + displayName: Add CurrentVersion Variable + inputs: + targetType: inline + script: | + $VersionString = git show -s --format=%ci $(Build.SourceVersion) + $VersionDate = [DateTimeOffset]::Parse($VersionString) + $CurrentVersion = $VersionDate.ToString("yyyy.MM.dd.HHmm") + + [System.Console]::WriteLine("##vso[task.setvariable variable=CurrentVersion]$CurrentVersion") + + Write-Host "Setting current version to $CurrentVersion." + + - task: UseDotNet@2 + displayName: Use .NET SDK + inputs: + version: 8.x + + - task: DotNetCoreCLI@2 + displayName: dotnet publish x64 + inputs: + command: publish + publishWebProjects: false + projects: '**/Agent.csproj' + arguments: -c $(BuildConfiguration) -r osx-x64 -o "$(Build.SourcesDirectory)/Agent/bin/publish" /p:Version=$(CurrentVersion) /p:FileVersion=$(CurrentVersion) + zipAfterPublish: false + modifyOutputPath: false + + - task: PowerShell@2 + displayName: PowerShell Script + inputs: + targetType: inline + script: | + Compress-Archive -Path "$(Build.SourcesDirectory)/Agent/bin/publish/*" -DestinationPath "$(Build.SourcesDirectory)/Agent/bin/Remotely-MacOS-x64.zip" -Force + + - task: PublishPipelineArtifact@1 + displayName: Publish macOS x64 Agent + inputs: + path: $(Build.SourcesDirectory)/Agent/bin/Remotely-MacOS-x64.zip + artifactName: Mac-x64-Agent - job: Windows_Build displayName: Windows Build timeoutInMinutes: 360 - #dependsOn: Mac_Build + dependsOn: Mac_Build pool: vmImage: windows-latest @@ -90,11 +90,11 @@ jobs: - task: VisualStudioTestPlatformInstaller@1 displayName: Visual Studio Test Platform Installer -# - task: DownloadPipelineArtifact@2 -# displayName: Download macOS x64 Agent -# inputs: -# artifact: Mac-x64-Agent -# path: $(Build.SourcesDirectory)\Server\wwwroot\Content\ + - task: DownloadPipelineArtifact@2 + displayName: Download macOS x64 Agent + inputs: + artifact: Mac-x64-Agent + path: $(Build.SourcesDirectory)\Server\wwwroot\Content\ - task: PowerShell@2 displayName: Add CurrentVersion Variable diff --git a/.gitignore b/.gitignore index 243775cf..93d822ba 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/Desktop.Linux/Program.cs b/Desktop.Linux/Program.cs index 00db19f0..72ee7948 100644 --- a/Desktop.Linux/Program.cs +++ b/Desktop.Linux/Program.cs @@ -33,7 +33,7 @@ public class Program 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 = EmbeddedServerDataSearcher.Instance.TryGetEmbeddedData(filePath); + var getEmbeddedResult = EmbeddedServerDataProvider.Instance.TryGetEmbeddedData(filePath); if (getEmbeddedResult.IsSuccess) { serverUrl = getEmbeddedResult.Value.ServerUrl.AbsoluteUri; @@ -46,7 +46,7 @@ public class Program 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 b57bbbd3..113cac48 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; diff --git a/Desktop.Win/Program.cs b/Desktop.Win/Program.cs index c677c90f..42e63e99 100644 --- a/Desktop.Win/Program.cs +++ b/Desktop.Win/Program.cs @@ -35,7 +35,7 @@ public class Program 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 = EmbeddedServerDataSearcher.Instance.TryGetEmbeddedData(filePath); + var getEmbeddedResult = EmbeddedServerDataProvider.Instance.TryGetEmbeddedData(filePath); if (getEmbeddedResult.IsSuccess) { serverUrl = getEmbeddedResult.Value.ServerUrl.AbsoluteUri; @@ -47,7 +47,7 @@ public class Program var services = new ServiceCollection(); services.AddSingleton(); - services.AddSingleton(EmbeddedServerDataSearcher.Instance); + services.AddSingleton(EmbeddedServerDataProvider.Instance); services.AddRemoteControlWindows( config => diff --git a/Server/API/ClientDownloadsController.cs b/Server/API/ClientDownloadsController.cs index 1cd1aa95..e3944608 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 class ClientDownloadsController : ControllerBase { 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 class ClientDownloadsController : ControllerBase { 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,14 +137,9 @@ public class ClientDownloadsController : ControllerBase 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 effectiveScheme = settings.ForceClientHttps ? "https" : Request.Scheme; - var serverUrl = $"{effectiveScheme}://{Request.Host}"; - var defaultOrg = await _dataService.GetDefaultOrganization(); // The default org will be used if unspecified, so might as well save the @@ -154,10 +149,14 @@ public class ClientDownloadsController : ControllerBase { organizationId = null; } + + 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(filePath, embeddedData); - var fileInfo = _hostEnv.WebRootFileProvider.GetFileInfo(filePath.Replace(_hostEnv.WebRootPath, string.Empty)); - return File(fileInfo.CreateReadStream(), "application/octet-stream", fileName); + 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 00000000..cd12ff4e --- /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/Components/Devices/DeviceCard.razor b/Server/Components/Devices/DeviceCard.razor index 69a61ae2..c4a18746 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 ca4a4fb5..df5a6691 100644 --- a/Server/Components/Devices/DeviceCard.razor.cs +++ b/Server/Components/Devices/DeviceCard.razor.cs @@ -7,6 +7,7 @@ using Remotely.Server.Hubs; 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 @@ public partial class DeviceCard : AuthComponentBase private async Task OnFileInputChanged(InputFileChangeEventArgs args) { - EnsureUserSet(); - - ToastService.ShowToast("File upload started."); - - var fileId = await DataService.AddSharedFile(args.File, User.OrganizationID, OnFileInputProgress); - - var transferId = Guid.NewGuid().ToString(); - - var result = await CircuitConnection.TransferFileFromBrowserToAgent(Device.ID, transferId, new[] { fileId }); - - if (!result) + try { - ToastService.ShowToast("Device not found.", classString: "bg-warning"); + EnsureUserSet(); + + ToastService.ShowToast("File upload started."); + + 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) + { + ToastService.ShowToast("Device not found.", classString: "bg-warning"); + } + else + { + ToastService.ShowToast("File upload completed."); + } } - else + catch (Exception ex) { - ToastService.ShowToast("File upload completed."); + Logger.LogError(ex, "Error while uploading file to device."); + ToastService.ShowToast2("Failed to upload file", ToastType.Error); + } + finally + { + if (args.File.Name is not null) + { + _ = _fileUploadProgressLookup.TryRemove(args.File.Name, out _); + await InvokeAsync(StateHasChanged); + } } } @@ -252,6 +277,7 @@ public partial class DeviceCard : AuthComponentBase _fileUploadProgressLookup.AddOrUpdate(fileName, percentComplete, (k, v) => percentComplete); InvokeAsync(StateHasChanged); } + private void OpenDeviceDetails() { JsInterop.OpenWindow($"/device-details/{Device.ID}", "_blank"); diff --git a/Server/Components/Pages/Deploy.razor b/Server/Components/Pages/Deploy.razor index 11af24b8..d15de186 100644 --- a/Server/Components/Pages/Deploy.razor +++ b/Server/Components/Pages/Deploy.razor @@ -1,18 +1,29 @@ @page "/deploy" +@using Microsoft.Extensions.FileProviders +@using System.Diagnostics @attribute [Authorize] @inject NavigationManager NavMan @inject IAuthService Auth @inject IDataService DataService @inject IJsInterop JsInterop @inject IToastService Toasts +@inject IWebHostEnvironment HostEnv @inject ILogger Logger -

Deploy Scripts

-

+@if (_isLoading) +{ + +} + +

+ Persistent Agents +

+

Copy and paste on a remote computer to install the agent.

+
Windows 10/11 (32-Bit and 64-Bit) @@ -53,33 +64,125 @@
+

+ Attended Support +

+

+ You can upload custom versions of the attended support client (i.e. "Remotely_Desktop.exe") + and send the download link to end-users when they need support. For example, you could sign + the EXE with a commercial certificate so the end-user (hopefully) doesn't see a SmartScreen + warning about the file. +

+

+ NOTE: These binaries must be manually updated each time a new Docker image is pulled. +

+ +
+
+
+ Windows 10/11 (64-Bit) +
+ + @if (_winX64File?.Exists != true) + { +
+ Note: A custom binary for this file hasn't been uploaded yet. +
+ } + else if (_winX64File?.PhysicalPath is not null) + { +
+ Created Date: @(_winX64CreatedDate) UTC +
+ } + +
+ + + + Upload File + + +
+ +
+ + +
+ +
+ +
+
+ Windows 10/11 (32-Bit) +
+ + @if(_winX86File?.Exists != true) + { +
+ Note: A custom binary for this file hasn't been uploaded yet. +
+ } + else if (_winX86File?.PhysicalPath is not null) + { +
+ Created Date: @(_winX86CreatedDate) UTC +
+ } + +
+ + + + Upload File + + +
+ +
+ + +
+
+
+ @code { - private string? _organizationId; - private bool _isAuthenticated; + private static readonly SemaphoreSlim _writeLock = new(1, 1); + private string _organizationId = string.Empty; private string _windowsScript = string.Empty; private string _ubuntuScript = string.Empty; private string _manjaroScript = string.Empty; + private string _appDataDir = string.Empty; + private IFileInfo? _winX64File; + private IFileInfo? _winX86File; + private string _winX64Uri = string.Empty; + private string _winX86Uri = string.Empty; + private bool _isLoading = false; + private string _loadingMessage = string.Empty; + private DateTime _winX64CreatedDate; + private DateTime _winX86CreatedDate; protected override async Task OnInitializedAsync() { - _isAuthenticated = await Auth.IsAuthenticated(); + var userResult = await Auth.GetUser(); + if (!userResult.IsSuccess) + { + NavMan.NavigateTo("/", false); + return; + } - if (_isAuthenticated) - { - var userResult = await Auth.GetUser(); - if (userResult.IsSuccess) - { - _organizationId = userResult.Value.OrganizationID; - } - } - else - { - var orgResult = await DataService.GetDefaultOrganization(); - if (orgResult.IsSuccess) - { - _organizationId = orgResult.Value.ID; - } - } + _organizationId = userResult.Value.OrganizationID; + _appDataDir = Path.Combine(HostEnv.ContentRootPath, "AppData"); + _winX64Uri = $"{NavMan.BaseUri}api/custom-binaries/win-x64/desktop/{_organizationId}"; + _winX86Uri = $"{NavMan.BaseUri}api/custom-binaries/win-x86/desktop/{_organizationId}"; + + SetFileInfos(); SetScriptContent(); @@ -104,6 +207,44 @@ Toasts.ShowToast2("Failed to set clipboard content", ToastType.Error); } + + private string GetLinuxScript(string platformId) + { + return + $"sudo rm -f /tmp/Install-Remotely.sh && " + + $"sudo wget -q -O /tmp/Install-Remotely.sh {NavMan.BaseUri}api/ClientDownloads/{platformId}/{_organizationId} && " + + $"sudo chmod +x /tmp/Install-Remotely.sh && " + + $"sudo /tmp/Install-Remotely.sh"; + } + + + private async Task HandleWin64FileChanged(InputFileChangeEventArgs ev) + { + await TrySaveFile(_winX64File, ev); + } + + private async Task HandleWin86FileChanged(InputFileChangeEventArgs ev) + { + await TrySaveFile(_winX86File, ev); + } + + private void SetFileInfos() + { + _winX64File = HostEnv.ContentRootFileProvider.GetFileInfo("AppData/Win-x64/Remotely_Desktop.exe"); + if (_winX64File?.Exists == true && _winX64File?.PhysicalPath is not null) + { + var fileInfo = new FileInfo(_winX64File.PhysicalPath); + _winX64CreatedDate = fileInfo.CreationTimeUtc; + } + + _winX86File = HostEnv.ContentRootFileProvider.GetFileInfo("AppData/Win-x86/Remotely_Desktop.exe"); + if (_winX86File?.Exists == true && _winX86File?.PhysicalPath is not null) + { + var fileInfo = new FileInfo(_winX86File.PhysicalPath); + _winX86CreatedDate = fileInfo.CreationTimeUtc; + } + } + private void SetScriptContent() { _windowsScript = @@ -115,12 +256,39 @@ _manjaroScript = GetLinuxScript("ManjaroInstaller-x64"); } - private string GetLinuxScript(string platformId) + private async Task TrySaveFile(IFileInfo? fileInfo, InputFileChangeEventArgs ev) { - return - $"sudo rm -f /tmp/Install-Remotely.sh && " + - $"sudo wget -q -O /tmp/Install-Remotely.sh {NavMan.BaseUri}api/ClientDownloads/{platformId}/{_organizationId} && " + - $"sudo chmod +x /tmp/Install-Remotely.sh && " + - $"sudo /tmp/Install-Remotely.sh"; + await _writeLock.WaitAsync(); + try + { + if (fileInfo?.PhysicalPath is null) + { + Toasts.ShowToast2("Unable to find save path", ToastType.Error); + return; + } + + _loadingMessage = "Uploading file"; + _isLoading = true; + await InvokeAsync(StateHasChanged); + + _ = Directory.CreateDirectory(Path.GetDirectoryName(fileInfo.PhysicalPath)!); + await using var rs = ev.File.OpenReadStream(500_000_000); + await using var fs = File.Open(fileInfo.PhysicalPath, FileMode.Create); + await rs.CopyToAsync(fs); + Toasts.ShowToast2("Custom binary uploaded successfully", ToastType.Success); + SetFileInfos(); + } + catch (Exception ex) + { + Logger.LogError(ex, "Error while uploading custom binary."); + Toasts.ShowToast2("Failed to upload custom binary", ToastType.Error); + } + finally + { + _writeLock.Release(); + _isLoading = false; + await InvokeAsync(StateHasChanged); + } } + } \ No newline at end of file diff --git a/Server/Components/_Imports.razor b/Server/Components/_Imports.razor index bbc5eac2..9b1d1830 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/Program.cs b/Server/Program.cs index 8d43320f..9104f011 100644 --- a/Server/Program.cs +++ b/Server/Program.cs @@ -193,7 +193,6 @@ services.Configure(options => 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.AddScoped(); services.AddSingleton(); -services.AddSingleton(); +services.AddSingleton(); services.AddSingleton(); services.AddScoped(); services.AddScoped(); diff --git a/Server/Services/DataService.cs b/Server/Services/DataService.cs index 65c4d98b..ed7b8bc3 100644 --- a/Server/Services/DataService.cs +++ b/Server/Services/DataService.cs @@ -15,6 +15,7 @@ using Remotely.Shared; 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 class DataService : IDataService 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 fileContents = new byte[fileSize]; + var stream = file.OpenReadStream(AppConstants.MaxUploadFileSize); + + var bytesRead = 0; + while (bytesRead < fileSize) { - var readSize = (int)Math.Min(5_000, file.Size - i); - await stream.ReadAsync(fileContents.AsMemory(i, readSize)); - - progressCallback.Invoke((double)stream.Position / stream.Length, file.Name); + 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/Shared/Services/EmbeddedServerDataSearcher.cs b/Shared/Services/EmbeddedServerDataProvider.cs similarity index 77% rename from Shared/Services/EmbeddedServerDataSearcher.cs rename to Shared/Services/EmbeddedServerDataProvider.cs index 297e3493..e42ed109 100644 --- a/Shared/Services/EmbeddedServerDataSearcher.cs +++ b/Shared/Services/EmbeddedServerDataProvider.cs @@ -2,30 +2,21 @@ 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 +public interface IEmbeddedServerDataProvider { string GetEncodedFileName(string filePath, EmbeddedServerData serverData); Result TryGetEmbeddedData(string filePath); } -public class EmbeddedServerDataSearcher() : IEmbeddedServerDataSearcher +public class EmbeddedServerDataProvider : IEmbeddedServerDataProvider { - public static EmbeddedServerDataSearcher Instance { get; } = new(); + public static EmbeddedServerDataProvider Instance { get; } = new(); public string GetEncodedFileName(string filePath, EmbeddedServerData serverData) { diff --git a/submodules/Immense.RemoteControl b/submodules/Immense.RemoteControl index 6c77d32d..9151b753 160000 --- a/submodules/Immense.RemoteControl +++ b/submodules/Immense.RemoteControl @@ -1 +1 @@ -Subproject commit 6c77d32dd6d307ad60d9acd6bbe2c98f2b9b50a9 +Subproject commit 9151b7534e56a7fa7f2233fa5a415a14afc9005c