Add ability to upload custom versions of the attended support client.

This commit is contained in:
Jared Goodwin 2024-06-04 12:34:20 -07:00
parent f46567fec8
commit cd3057dac1
15 changed files with 412 additions and 164 deletions

View File

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

1
.gitignore vendored
View File

@ -296,3 +296,4 @@ Server.Installer/Properties/launchSettings.json
!/.vscode/launch.json
!/.vscode/tasks.json
/Server/appsettings.Development.json
/Server/AppData

View File

@ -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<IOrganizationIdProvider, OrganizationIdProvider>();
services.AddSingleton<IEmbeddedServerDataSearcher, EmbeddedServerDataSearcher>();
services.AddSingleton<IEmbeddedServerDataProvider, EmbeddedServerDataProvider>();
services.AddRemoteControlLinux(
config =>

View File

@ -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<BrandingProvider> _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<BrandingProvider> logger)
{
_appState = appState;

View File

@ -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<IOrganizationIdProvider, OrganizationIdProvider>();
services.AddSingleton<IEmbeddedServerDataSearcher>(EmbeddedServerDataSearcher.Instance);
services.AddSingleton<IEmbeddedServerDataProvider>(EmbeddedServerDataProvider.Instance);
services.AddRemoteControlWindows(
config =>

View File

@ -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<ClientDownloadsController> _logger;
public ClientDownloadsController(
IWebHostEnvironment hostEnv,
IEmbeddedServerDataSearcher embeddedDataSearcher,
IEmbeddedServerDataProvider embeddedDataSearcher,
IDataService dataService,
ILogger<ClientDownloadsController> 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<IActionResult> GetDesktopFile(string filePath, string? organizationId = null)
private async Task<IActionResult> 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<IActionResult> GetInstallFile(string organizationId, string platformID)

View File

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

View File

@ -59,7 +59,9 @@
{
foreach (var kvp in _fileUploadProgressLookup)
{
<AlertBanner Message="@GetProgressMessage(kvp.Key)" />
<div class="alert alert-info">
@(GetProgressMessage(kvp.Key))
</div>
}
}
<div>

View File

@ -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<DeviceCard> 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");

View File

@ -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<Deploy> Logger
<h3>Deploy Scripts</h3>
<p class="text-info col-sm-12 mb-4">
@if (_isLoading)
{
<LoadingSignal StatusMessage="@_loadingMessage" />
}
<h4 class="text-success mt-3">
Persistent Agents
</h4>
<p class="text-info mb-3">
Copy and paste on a remote computer to install the agent.
</p>
<div class="row">
<div class="col-sm-12 mb-3">
<div class="mb-1">
<strong>Windows 10/11 (32-Bit and 64-Bit)</strong>
@ -53,33 +64,125 @@
</div>
</div>
<h4 class="text-success mt-3">
Attended Support
</h4>
<p class="text-info">
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.
</p>
<p class="mb-3 text-warning">
NOTE: These binaries must be manually updated each time a new Docker image is pulled.
</p>
<div class="row">
<div class="col-sm-12 mb-4">
<div class="mb-1">
<strong>Windows 10/11 (64-Bit)</strong>
</div>
@if (_winX64File?.Exists != true)
{
<div class="text-danger mb-2">
Note: A custom binary for this file hasn't been uploaded yet.
</div>
}
else if (_winX64File?.PhysicalPath is not null)
{
<div class="text-primary mb-2">
Created Date: @(_winX64CreatedDate) UTC
</div>
}
<div class="mb-2">
<FileInputButton ClassNames="btn btn-primary"
OnChanged="HandleWin64FileChanged">
<ButtonContent>
<i class="oi oi-data-transfer-upload" title="Upload File"></i>
<span class="ms-2">Upload File</span>
</ButtonContent>
</FileInputButton>
</div>
<div class="input-group">
<input type="text" class="form-control" readonly value="@_winX64Uri" />
<button class="btn btn-primary" type="button" id="button-addon2" @onclick="() => CopyToClipboard(_winX64Uri)">
<i class="oi oi-clipboard"></i>
</button>
</div>
</div>
<div class="col-sm-12 mb-4">
<div class="mb-1">
<strong>Windows 10/11 (32-Bit)</strong>
</div>
@if(_winX86File?.Exists != true)
{
<div class="text-danger mb-2">
Note: A custom binary for this file hasn't been uploaded yet.
</div>
}
else if (_winX86File?.PhysicalPath is not null)
{
<div class="text-primary mb-2">
Created Date: @(_winX86CreatedDate) UTC
</div>
}
<div class="mb-2">
<FileInputButton ClassNames="btn btn-primary"
OnChanged="HandleWin86FileChanged">
<ButtonContent>
<i class="oi oi-data-transfer-upload" title="Upload File"></i>
<span class="ms-2">Upload File</span>
</ButtonContent>
</FileInputButton>
</div>
<div class="input-group">
<input type="text" class="form-control" readonly value="@_winX86Uri" />
<button class="btn btn-primary" type="button" id="button-addon2" @onclick="() => CopyToClipboard(_winX86Uri)">
<i class="oi oi-clipboard"></i>
</button>
</div>
</div>
</div>
@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);
}
}
}

View File

@ -25,4 +25,5 @@
@using Remotely.Server.Components.TreeView
@using Remotely.Server.Auth
@using Remotely.Shared.Entities
@using Remotely.Server.Models
@using Remotely.Server.Models
@using Remotely.Shared.Services;

View File

@ -193,7 +193,6 @@ services.Configure<ForwardedHeadersOptions>(options =>
services.AddSignalR(options =>
{
options.EnableDetailedErrors = builder.Environment.IsDevelopment();
options.MaximumParallelInvocationsPerClient = 5;
options.MaximumReceiveMessageSize = 100_000;
})
.AddJsonProtocol(options =>
@ -246,7 +245,7 @@ services.AddScoped<ISelectedCardsStore, SelectedCardsStore>();
services.AddScoped<IExpiringTokenService, ExpiringTokenService>();
services.AddScoped<IScriptScheduleDispatcher, ScriptScheduleDispatcher>();
services.AddSingleton<IOtpProvider, OtpProvider>();
services.AddSingleton<IEmbeddedServerDataSearcher, EmbeddedServerDataSearcher>();
services.AddSingleton<IEmbeddedServerDataProvider, EmbeddedServerDataProvider>();
services.AddSingleton<ILogsManager, LogsManager>();
services.AddScoped<IThemeProvider, ThemeProvider>();
services.AddScoped<IChatSessionStore, ChatSessionStore>();

View File

@ -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<string> AddSharedFile(IBrowserFile file, string organizationId, Action<double, string> 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);
}

View File

@ -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<EmbeddedServerData> 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)
{

@ -1 +1 @@
Subproject commit 6c77d32dd6d307ad60d9acd6bbe2c98f2b9b50a9
Subproject commit 9151b7534e56a7fa7f2233fa5a415a14afc9005c