using MailKit.Search; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Mvc; using Microsoft.Build.Framework; using Microsoft.Extensions.Logging; using Remotely.Server.Auth; using Remotely.Server.Extensions; using Remotely.Server.Services; using Remotely.Shared; using Remotely.Shared.Models; using Remotely.Shared.Services; using Remotely.Shared.Utilities; using System; using System.Collections.Generic; using System.IO; using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; namespace Remotely.Server.API; [Route("api/[controller]")] [ApiController] public class ClientDownloadsController : ControllerBase { private readonly IApplicationConfig _appConfig; private readonly IEmbeddedServerDataSearcher _embeddedDataSearcher; private readonly SemaphoreSlim _fileLock = new(1, 1); private readonly IWebHostEnvironment _hostEnv; private readonly ILogger _logger; public ClientDownloadsController( IWebHostEnvironment hostEnv, IEmbeddedServerDataSearcher embeddedDataSearcher, IApplicationConfig appConfig, ILogger logger) { _hostEnv = hostEnv; _embeddedDataSearcher = embeddedDataSearcher; _appConfig = appConfig; _logger = logger; } [HttpGet("desktop/{platformID}")] public async Task GetDesktop(string platformID) { switch (platformID) { case "WindowsDesktop-x64": { var filePath = Path.Combine(_hostEnv.WebRootPath, "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"); return await GetDesktopFile(filePath); } case "UbuntuDesktop": { var filePath = Path.Combine(_hostEnv.WebRootPath, "Content", "Linux-x64", "Remotely_Desktop"); return await GetDesktopFile(filePath); } case "MacOS-x64": { var filePath = Path.Combine(_hostEnv.WebRootPath, "Content", "MacOS-x64", "Remotely_Desktop"); return await GetDesktopFile(filePath); } case "MacOS-arm64": { var filePath = Path.Combine(_hostEnv.WebRootPath, "Content", "MacOS-arm64", "Remotely_Desktop"); return await GetDesktopFile(filePath); } default: return NotFound(); } } [HttpGet("desktop/{platformId}/{organizationId}")] public async Task GetDesktop(string platformId, string organizationId) { switch (platformId) { case "WindowsDesktop-x64": { var filePath = Path.Combine(_hostEnv.WebRootPath, "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"); return await GetDesktopFile(filePath, organizationId); } case "UbuntuDesktop": { var filePath = Path.Combine(_hostEnv.WebRootPath, "Content", "Linux-x64", "Remotely_Desktop"); return await GetDesktopFile(filePath, organizationId); } case "MacOS-x64": { var filePath = Path.Combine(_hostEnv.WebRootPath, "Content", "MacOS-x64", "Remotely_Desktop"); return await GetDesktopFile(filePath); } case "MacOS-arm64": { var filePath = Path.Combine(_hostEnv.WebRootPath, "Content", "MacOS-arm64", "Remotely_Desktop"); return await GetDesktopFile(filePath); } default: return NotFound(); } } [ServiceFilter(typeof(ApiAuthorizationFilter))] [HttpGet("{platformID}")] public async Task GetInstaller(string platformID) { if (!Request.Headers.TryGetOrganizationId(out var orgId)) { return BadRequest("OrganizationID is required."); } return await GetInstallFile(orgId, platformID); } [HttpGet("{organizationID}/{platformID}")] public async Task GetInstaller(string organizationID, string platformID) { return await GetInstallFile(organizationID, platformID); } private async Task GetBashInstaller(string fileName, string organizationId) { var fileContents = new List(); fileContents.AddRange(await System.IO.File.ReadAllLinesAsync(Path.Combine(_hostEnv.WebRootPath, "Content", fileName))); var hostIndex = fileContents.IndexOf("HostName="); var orgIndex = fileContents.IndexOf("Organization="); var effectiveScheme = _appConfig.ForceClientHttps ? "https" : Request.Scheme; fileContents[hostIndex] = $"HostName=\"{effectiveScheme}://{Request.Host}\""; fileContents[orgIndex] = $"Organization=\"{organizationId}\""; var fileBytes = Encoding.UTF8.GetBytes(string.Join("\n", fileContents)); return File(fileBytes, "application/octet-stream", fileName); } private async Task GetDesktopFile(string filePath, string organizationId = null) { LogRequest(nameof(GetDesktopFile)); var effectiveScheme = _appConfig.ForceClientHttps ? "https" : Request.Scheme; var serverUrl = $"{effectiveScheme}://{Request.Host}"; var embeddedData = new EmbeddedServerData(new Uri(serverUrl), organizationId); var result = await _embeddedDataSearcher.GetRewrittenStream(filePath, embeddedData); if (!result.IsSuccess) { throw result.Exception; } return File(result.Value, "application/octet-stream", Path.GetFileName(filePath)); } private async Task GetInstallFile(string organizationId, string platformID) { LogRequest(nameof(GetInstallFile)); if (!await _fileLock.WaitAsync(TimeSpan.FromSeconds(15))) { return StatusCode(StatusCodes.Status408RequestTimeout); } try { switch (platformID) { case "WindowsInstaller": { var effectiveScheme = _appConfig.ForceClientHttps ? "https" : Request.Scheme; var serverUrl = $"{effectiveScheme}://{Request.Host}"; var filePath = Path.Combine(_hostEnv.WebRootPath, "Content", "Remotely_Installer.exe"); var embeddedData = new EmbeddedServerData(new Uri(serverUrl), organizationId); var result = await _embeddedDataSearcher.GetRewrittenStream(filePath, embeddedData); if (!result.IsSuccess) { throw result.Exception; } return File(result.Value, "application/octet-stream", "Remotely_Installer.exe"); } case "ManjaroInstaller-x64": { var fileName = "Install-Manjaro-x64.sh"; return await GetBashInstaller(fileName, organizationId); } case "UbuntuInstaller-x64": { var fileName = "Install-Ubuntu-x64.sh"; return await GetBashInstaller(fileName, organizationId); } case "MacOSInstaller-x64": { var fileName = "Install-MacOS-x64.sh"; return await GetBashInstaller(fileName, organizationId); } case "MacOSInstaller-arm64": { var fileName = "Install-MacOS-arm64.sh"; return await GetBashInstaller(fileName, organizationId); } default: return BadRequest(); } } finally { _fileLock.Release(); } } private void LogRequest(string methodName) { if (_appConfig.UseHttpLogging) { var ip = Request.HttpContext.Connection.RemoteIpAddress; if (ip?.IsIPv4MappedToIPv6 == true) { ip = ip.MapToIPv4(); } var effectiveScheme = _appConfig.ForceClientHttps ? "https" : Request.Scheme; _logger.LogInformation( "Started client download via {methodName}. Effective Scheme: {scheme}. Effective Host: {host}. Remote IP: {ip}.", methodName, effectiveScheme, Request.Host, $"{ip}"); } } }