diff --git a/.editorconfig b/.editorconfig index e68a8806..066d40e3 100644 --- a/.editorconfig +++ b/.editorconfig @@ -2,3 +2,6 @@ # CA1416: Validate platform compatibility dotnet_diagnostic.CA1416.severity = none + +# CS1591: Missing XML comment for publicly visible type or member +dotnet_diagnostic.CS1591.severity = none \ No newline at end of file diff --git a/.gitignore b/.gitignore index 56000bf2..75b00b93 100644 --- a/.gitignore +++ b/.gitignore @@ -289,4 +289,5 @@ Server/wwwroot/Content/Win-x86/* .vscode/launch.json .vscode/tasks.json Server/.config/dotnet-tools.json -/Server/Properties/ServiceDependencies/* \ No newline at end of file +/Server/Properties/ServiceDependencies/* +Server.Installer/Properties/launchSettings.json diff --git a/Desktop.Linux/ViewModels/MainWindowViewModel.cs b/Desktop.Linux/ViewModels/MainWindowViewModel.cs index ab1b85f9..21db6e9a 100644 --- a/Desktop.Linux/ViewModels/MainWindowViewModel.cs +++ b/Desktop.Linux/ViewModels/MainWindowViewModel.cs @@ -141,7 +141,7 @@ namespace Remotely.Desktop.Linux.ViewModels { try { - if (Libc.geteuid() != 0) + if (!EnvironmentHelper.IsDebug && Libc.geteuid() != 0) { await MessageBox.Show("Please run with sudo.", "Sudo Required", MessageBoxType.OK); Environment.Exit(0); diff --git a/README.md b/README.md index 24ea691b..9a0f6184 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # Remotely -A remote control and remote scripting solution, built with .NET Core, SignalR Core, and WebRTC. +A remote control and remote scripting solution, built with .NET 5, Blazor, SignalR Core, and WebRTC. [![Build Status](https://dev.azure.com/translucency/Remotely/_apis/build/status/Remotely-ReleaseBuild?branchName=master)](https://dev.azure.com/translucency/Remotely/_build/latest?definitionId=17&branchName=master) ![GitHub Build](https://github.com/lucent-sea/Remotely/workflows/GitHub%20Build/badge.svg) @@ -26,29 +26,29 @@ Subreddit: https://www.reddit.com/r/remotely_app/ ## Disclaimer -Hosting a Remotely server requires building and running an ASP.NET Core web app behind IIS (Windows), Nginx (Ubuntu), or Caddy Server (any OS). It's expected that the person deploying and maintaining the server is familiar with this process. +Hosting a Remotely server requires running an ASP.NET Core web app behind IIS (Windows), Nginx (Ubuntu), or Caddy Server (any OS). It's expected that the person deploying and maintaining the server is familiar with this process. Since this is a hobby project that I develop in between working full time and raising a family, there simply isn't time available to provide support in this capacity. -It's *highly* encouraged that you get comfortable building and deploying from source. This allows you to hard-code your server's hostname into the desktop client and the installer, which makes for a better experience for the end user. I've tried to make this as easy as possible using the GitHub Actions workflows mentioned below. You can begin using these immediately, or use them as a reference for creating your own customized build process. You can also use Azure Pipelines for free (which I personally use). +## Build Instructions +GitHub Actions allows you to build and deploy Remotely for free from their cloud servers. Since the Windows agent can only be built on Windows, and the Mac agent can only be built on Mac, using a build platform like GitHub Actions or Azure Pipelines is the only reasonable way to build the whole project. The definitions for the build processes are located in `/.github/workflows/` folder. -## Build Instructions (GitHub) -GitHub Actions allows you to build and deploy Remotely for free from their cloud servers. The definitions for the build processes are located in `/.github/workflows/` folder. +I've created a cross-platform command line tool that will leverage the GitHub Actions REST API to build the project and install it on your private server. This process will also embed your server's URL into the clients, so that they won't need to prompt the end user to enter it. -After forking the repo, follow the instructions in the workflow YML file. The easiest workflow to use is the Build.yml worfklow, and I'd recommend starting with that one. It will produce a build artifact (ZIP package) identical to what was on the Releases page, only the clients will have your server URL hard-coded. - -### Instructions for using the Build workflow: +### Instructions for using the Remotely_Server_Installer CLI tool: - Fork the repo if you haven't already. - If you've already forked the repo, you need to keep your repo updated with mine. This doesn't happen automatically. + - This can be done via the command line if you've cloned your repo locally. Refer to [GitHub's docs](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/syncing-a-fork) on how to do this. Otherwise, see below for how to do it completely through the GitHub website. - On the GitHub page for your repo, you'll see a message that says, "This branch is ## commits behind lucent-sea:master". - Click the "Pull request" link next to it. - On the next page, click the "switching the base" link. Now it's pulling from my repo into yours. - Create and complete the pull request to update your repo. -- Now go to the Actions tab. -- Click the "Build" workflow. -- Click "Run workflow". -- Enter the Server URL where your Remotely app will be running (e.g. https://app.remotely.one). -- If you're going to host on Windows, change the Server Runtime Identifier to `win-x64`. -- Click "Run workflow". -- When it's finished, there will be a build artifact for download that contains the server and clients. +- Create a Personal Access Token that the installer will use to authenticate with GitHub. + - Located here: https://github.com/settings/tokens + - Save the PAT when it's displayed. It will only be shown once. +- On your server, download the latest server installer executable (Linux or Windows) from [my releases page](https://github.com/lucent-sea/Remotely/releases). +- Run the app with elevation (e.g. sudo or "Run as admin"). +- Follow the prompts to build and install the server. +- Use `--help` argument to see all the command line arguments. + - If values are provided for all arguments, it will run non-interactive. ## Hosting a Server (Windows) diff --git a/Remotely.sln b/Remotely.sln index 75ad1369..f4603e8f 100644 --- a/Remotely.sln +++ b/Remotely.sln @@ -7,15 +7,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Agent", "Agent\Agent.csproj EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Utilities", "Utilities", "{2CAC9A2B-1402-465F-83F8-958B4E081CA3}" ProjectSection(SolutionItems) = preProject - Utilities\CentOS_Server_Install.sh = Utilities\CentOS_Server_Install.sh Utilities\Example_Apache_Config.txt = Utilities\Example_Apache_Config.txt Example_Nginx_Config.txt = Example_Nginx_Config.txt Utilities\Get-PSCommands.ps1 = Utilities\Get-PSCommands.ps1 Utilities\Get-WindowsCommands.ps1 = Utilities\Get-WindowsCommands.ps1 - Utilities\Install-RemotelyServer.ps1 = Utilities\Install-RemotelyServer.ps1 Utilities\Publish.ps1 = Utilities\Publish.ps1 Utilities\signtool.exe = Utilities\signtool.exe - Utilities\Ubuntu_Server_Install.sh = Utilities\Ubuntu_Server_Install.sh EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Assets", "Assets", "{D96B47F6-EF3E-4AF6-A1BE-006D531DDBA4}" @@ -49,6 +46,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Server", "Server\Server.csproj", "{75EC5DCD-DC76-4799-8623-206B1F73CA95}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Server.Installer", "Server.Installer\Server.Installer.csproj", "{BE7799D6-204C-4D35-83E7-7FB24DEBAE94}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -167,6 +166,18 @@ Global {75EC5DCD-DC76-4799-8623-206B1F73CA95}.Release|x64.Build.0 = Release|Any CPU {75EC5DCD-DC76-4799-8623-206B1F73CA95}.Release|x86.ActiveCfg = Release|Any CPU {75EC5DCD-DC76-4799-8623-206B1F73CA95}.Release|x86.Build.0 = Release|Any CPU + {BE7799D6-204C-4D35-83E7-7FB24DEBAE94}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BE7799D6-204C-4D35-83E7-7FB24DEBAE94}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BE7799D6-204C-4D35-83E7-7FB24DEBAE94}.Debug|x64.ActiveCfg = Debug|Any CPU + {BE7799D6-204C-4D35-83E7-7FB24DEBAE94}.Debug|x64.Build.0 = Debug|Any CPU + {BE7799D6-204C-4D35-83E7-7FB24DEBAE94}.Debug|x86.ActiveCfg = Debug|Any CPU + {BE7799D6-204C-4D35-83E7-7FB24DEBAE94}.Debug|x86.Build.0 = Debug|Any CPU + {BE7799D6-204C-4D35-83E7-7FB24DEBAE94}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BE7799D6-204C-4D35-83E7-7FB24DEBAE94}.Release|Any CPU.Build.0 = Release|Any CPU + {BE7799D6-204C-4D35-83E7-7FB24DEBAE94}.Release|x64.ActiveCfg = Release|Any CPU + {BE7799D6-204C-4D35-83E7-7FB24DEBAE94}.Release|x64.Build.0 = Release|Any CPU + {BE7799D6-204C-4D35-83E7-7FB24DEBAE94}.Release|x86.ActiveCfg = Release|Any CPU + {BE7799D6-204C-4D35-83E7-7FB24DEBAE94}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Server.Installer/Models/CliParams.cs b/Server.Installer/Models/CliParams.cs new file mode 100644 index 00000000..c9db2b45 --- /dev/null +++ b/Server.Installer/Models/CliParams.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Server.Installer.Models +{ + public class CliParams + { + public bool? CreateNew { get; set; } + public string GitHubPat { get; set; } + public string GitHubUsername { get; set; } + public string InstallDirectory { get; set; } + public string Reference { get; set; } + public Uri ServerUrl { get; set; } + public WebServerType? WebServer { get; set; } + } +} diff --git a/Server.Installer/Models/GitHubArtifactsResponsePayload.cs b/Server.Installer/Models/GitHubArtifactsResponsePayload.cs new file mode 100644 index 00000000..5ba21554 --- /dev/null +++ b/Server.Installer/Models/GitHubArtifactsResponsePayload.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Server.Installer.Models +{ + public class ArtifactsResponsePayload + { + public int total_count { get; set; } + public Artifact[] artifacts { get; set; } + } + + public class Artifact + { + public int id { get; set; } + public string node_id { get; set; } + public string name { get; set; } + public int size_in_bytes { get; set; } + public string url { get; set; } + public string archive_download_url { get; set; } + public bool expired { get; set; } + public DateTime created_at { get; set; } + public DateTime expires_at { get; set; } + public DateTime updated_at { get; set; } + } +} diff --git a/Server.Installer/Models/WebServerType.cs b/Server.Installer/Models/WebServerType.cs new file mode 100644 index 00000000..e7ad4db3 --- /dev/null +++ b/Server.Installer/Models/WebServerType.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Runtime.Serialization; +using System.Text; +using System.Threading.Tasks; + +namespace Server.Installer.Models +{ + public enum WebServerType + { + UbuntuCaddy, + UbuntuNginx, + CentOsCaddy, + CentOsNginx, + IisWindows + } +} diff --git a/Server.Installer/Program.cs b/Server.Installer/Program.cs new file mode 100644 index 00000000..865e13ec --- /dev/null +++ b/Server.Installer/Program.cs @@ -0,0 +1,254 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Remotely.Shared.Enums; +using Remotely.Shared.Services; +using Remotely.Shared.Utilities; +using Server.Installer.Models; +using Server.Installer.Services; +using System; +using System.IO; +using System.Threading.Tasks; + +namespace Server.Installer +{ + + public class Program + { + public static IServiceProvider Services { get; set; } + + public static async Task Main(string[] args) + { + Console.ForegroundColor = ConsoleColor.Cyan; + Console.WriteLine(AppConstants.RemotelyAscii); + Console.ForegroundColor = ConsoleColor.Gray; + Console.WriteLine("(https://remotely.one)"); + Console.WriteLine(); + Console.WriteLine(); + + + if (!ParseCliParams(args, out var cliParams)) + { + ShowHelpText(); + return; + } + + if (EnvironmentHelper.Platform != Platform.Windows && + EnvironmentHelper.Platform != Platform.Linux) + { + ConsoleHelper.WriteError("Remotely Server can only be installed on Linux or Windows."); + return; + } + + BuildServices(); + + var elevationDetector = Services.GetRequiredService(); + + if (!elevationDetector.IsElevated()) + { + ConsoleHelper.WriteError("The installer process be elevated. On Linux, run with sudo. " + + "On Windows, run from a command that was opened with \"Run as admin\"."); + ConsoleHelper.ReadLine("Press any key to exit."); + return; + } + + ConsoleHelper.WriteLine("Thank you for trying Remotely! This installer will use your " + + "GitHub credentials to build a customized Remotely package and install it on this server."); + + ConsoleHelper.WriteLine("You will need to enter a GitHub Personal Access Token, which will " + + "allow this app to access fork of the Remotely repo. You can generate a PAT at " + + "https://github.com/settings/tokens. You need to give it the \"repo\" scope."); + + ConsoleHelper.WriteLine("Be sure to retain your GitHub Personal Access Token if you want to re-use it " + + "for upgrading in the future. The installer does not save it locally."); + + + while (string.IsNullOrWhiteSpace(cliParams.GitHubUsername)) + { + cliParams.GitHubUsername = ConsoleHelper.ReadLine("Enter your GitHub username").Trim(); + } + + while (string.IsNullOrWhiteSpace(cliParams.GitHubPat)) + { + cliParams.GitHubPat = ConsoleHelper.ReadLine("Enter your GitHub Personal Access Token").Trim(); + } + + while (string.IsNullOrWhiteSpace(cliParams.Reference)) + { + ConsoleHelper.WriteLine("Enter the GitHub branch or tag name from which to build. For example, you can enter " + + " \"master\" to build the latest changes from the default branch. Or you can enter a release tag like \"v2021.04.13.1604\"."); + cliParams.Reference = ConsoleHelper.ReadLine("Input Reference").Trim(); + } + + while (string.IsNullOrWhiteSpace(cliParams.InstallDirectory)) + { + cliParams.InstallDirectory = ConsoleHelper.ReadLine("Enter the directory path where the server files should be extracted to (e.g. /var/www/remotely/)").Trim(); + } + + while (cliParams.ServerUrl is null) + { + var url = ConsoleHelper.ReadLine("Enter your server's public URL (e.g. https://app.remotely.one)").Trim(); + if (Uri.TryCreate(url, UriKind.Absolute, out var serverUrl)) + { + cliParams.ServerUrl = serverUrl; + } + + } + + while (cliParams.CreateNew is null) + { + ConsoleHelper.WriteLine("Create new build? True/false. If false, the latest existing build artifact on GitHub will be used."); + + var createNew = ConsoleHelper.ReadLine("Selection").Trim(); + if (bool.TryParse(createNew, out var result)) + { + cliParams.CreateNew = result; + } + } + + while (cliParams.WebServer is null) + { + ConsoleHelper.WriteLine("Which reverse proxy will be used?"); + ConsoleHelper.WriteLine(" [0] - Caddy on Ubuntu"); + ConsoleHelper.WriteLine(" [1] - Nginx on Ubuntu"); + ConsoleHelper.WriteLine(" [2] - Caddy on CentOS"); + ConsoleHelper.WriteLine(" [3] - Nginx on CentOS"); + ConsoleHelper.WriteLine(" [4] - IIS on Windows Server 2016+"); + + var webServerType = ConsoleHelper.ReadLine("Selection").Trim(); + if (Enum.TryParse(webServerType, out var result)) + { + cliParams.WebServer = result; + } + } + + ConsoleHelper.WriteLine($"Performing server install. GitHub User: {cliParams.GitHubUsername}. " + + $"Server URL: {cliParams.ServerUrl}. Installation Directory: {cliParams.InstallDirectory}"); + + var serverInstaller = Services.GetRequiredService(); + await serverInstaller.PerformInstall(cliParams); + + ConsoleHelper.WriteLine("Installation completed."); + } + + private static void BuildServices() + { + var services = new ServiceCollection(); + + services.AddSingleton(); + services.AddSingleton(); + + if (EnvironmentHelper.IsWindows) + { + services.AddSingleton(); + } + else if (EnvironmentHelper.IsLinux) + { + services.AddSingleton(); + } + else + { + throw new NotSupportedException("Operating system not supported."); + } + + Services = services.BuildServiceProvider(); + } + + private static bool ParseCliParams(string[] args, out CliParams cliParams) + { + cliParams = new CliParams(); + + for (var i = 0; i < args.Length; i += 2) + { + try + { + var key = args[i].Trim(); + var value = args[i + 1].Trim(); + + switch (key) + { + case "--github-username": + case "-u": + cliParams.GitHubUsername = value; + continue; + case "--github-pat": + case "-p": + cliParams.GitHubPat = value; + continue; + case "--server-url": + case "-s": + { + if (Uri.TryCreate(value, UriKind.Absolute, out var result)) + { + cliParams.ServerUrl = result; + continue; + } + return false; + } + case "--install-directory": + case "-i": + cliParams.InstallDirectory = value; + continue; + case "--reference": + case "-r": + cliParams.Reference = value; + continue; + case "--create-new": + case "-c": + { + if (bool.TryParse(value, out var result)) + { + cliParams.CreateNew = result; + } + return false; + } + default: + return false; + } + } + catch (Exception ex) + { + ConsoleHelper.WriteError($"Error while parsing command line arguments: {ex.Message}"); + return false; + } + + } + return true; + } + + private static void ShowHelpText() + { + ConsoleHelper.WriteLine("Remotely Server Installer", 0, ConsoleColor.Cyan); + ConsoleHelper.WriteLine("Builds a customized Remotely server using GitHub actions " + + "and installs the server on the local machine.", 1); + + ConsoleHelper.WriteLine("Usage:"); + + ConsoleHelper.WriteLine("\tNo Parameters - Run the installer interactively.", 2); + + ConsoleHelper.WriteLine("\t--github-username, -u Your GitHub username, where the forked Remotely repo exists.", 1); + + ConsoleHelper.WriteLine("\t--github-pat, -p The GitHub Personal Access Token to use for authentication. " + + "Create one at ttps://github.com/settings/tokens.", 1); + + ConsoleHelper.WriteLine("\t--server-url, -s The public URL where your Remotely server will be accessed (e.g. https://app.remotely.one).", 1); + + ConsoleHelper.WriteLine("\t--install-directory, -i The directory path where the server files will be installed (e.g. /var/www/remotely/).", 1); + + ConsoleHelper.WriteLine("Enter the GitHub branch or tag name from which to build. For example, you can enter " + + " \"master\" to build the latest changes from the default branch. Or you can enter a release tag like \"v2021.04.13.1604\".", 1); + + ConsoleHelper.WriteLine("\t--reference, -r The name of the branch or tag from which to build. For example, you can enter " + + " \"master\" to build the latest changes from the default branch. Or you can enter a release tag like \"v2021.04.13.1604\".", 1); + + ConsoleHelper.WriteLine("\t--create-new, -c True/false. Whether to run a new build. If false, the latest existing build artifact will be used.", 1); + + ConsoleHelper.WriteLine("\t--reverse-proxy, -r Number. The reverse proxy that will be used to forward requests to the Remotely server. " + + "Select the appropriate option for your operating system and web server. " + + "0 = Caddy on Ubuntu. 1 = Nginx on Ubuntu. 2 = Caddy on CentOS. 3 = Nginx on CentOS. 4 = IIS on Windows Server 2016+.", 1); + + ConsoleHelper.WriteLine("Example: sudo ./Remotely_Server_Installer -u lucent-sea -p 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 -s https://app.remotely.one -i /var/www/remotely/ -r master -c true -r 0"); + + ConsoleHelper.ReadLine("Press any key to exit"); + } + } +} diff --git a/Server.Installer/Remotely_Icon.ico b/Server.Installer/Remotely_Icon.ico new file mode 100644 index 00000000..55aaa8ca Binary files /dev/null and b/Server.Installer/Remotely_Icon.ico differ diff --git a/Server.Installer/Resources/CentOS_Caddy_Install.sh b/Server.Installer/Resources/CentOS_Caddy_Install.sh new file mode 100644 index 00000000..e9021446 --- /dev/null +++ b/Server.Installer/Resources/CentOS_Caddy_Install.sh @@ -0,0 +1,100 @@ +#!/bin/bash +echo "Thanks for trying Remotely!" +echo + + +Args=( "$@" ) +ArgLength=${#Args[@]} + +for (( i=0; i<${ArgLength}; i+=2 )); +do + if [ "${Args[$i]}" = "--host" ]; then + HostName="${Args[$i+1]}" + elif [ "${Args[$i]}" = "--approot" ]; then + AppRoot="${Args[$i+1]}" + fi +done + +if [ -z "$AppRoot" ]; then + read -p "Enter path where the Remotely server files should be installed (typically /var/www/remotely): " AppRoot + if [ -z "$AppRoot" ]; then + AppRoot="/var/www/remotely" + fi +fi + +if [ -z "$HostName" ]; then + read -p "Enter server host (e.g. remotely.yourdomainname.com): " HostName +fi + +chmod +x "$AppRoot/Remotely_Server" + +echo "Using $AppRoot as the Remotely website's content directory." + +yum update + +# Install .NET Core Runtime. +sudo rpm -Uvh https://packages.microsoft.com/config/centos/7/packages-microsoft-prod.rpm + +yum -y install apt-transport-https +yum -y update +yum -y install aspnetcore-runtime-5.0 + + + # Install other prerequisites. +yum -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm +yum -y install yum-utils +yum-config-manager --enable rhui-REGION-rhel-server-extras rhui-REGION-rhel-server-optional +yum -y install unzip +yum -y install acl +yum -y install libc6-dev +yum -y install libgdiplus + + +# Install Caddy +yum install yum-plugin-copr +yum copr enable @caddy/caddy +yum install caddy + + +# Configure Caddy +caddyConfig=" + $HostName { + reverse_proxy 127.0.0.1:5000 + } +" + +echo "$caddyConfig" > /etc/caddy/Caddyfile + + +# Create service. + +serviceConfig="[Unit] +Description=Remotely Server + +[Service] +WorkingDirectory=$AppRoot +ExecStart=$AppRoot/Remotely_Server +Restart=always +# Restart service after 10 seconds if the dotnet service crashes: +RestartSec=10 +SyslogIdentifier=remotely +Environment=ASPNETCORE_ENVIRONMENT=Production +Environment=DOTNET_PRINT_TELEMETRY_MESSAGE=false + +[Install] +WantedBy=multi-user.target" + +echo "$serviceConfig" > /etc/systemd/system/remotely.service + + +# Enable service. +systemctl enable remotely.service +# Start service. +systemctl start remotely.service + +firewall-cmd --permanent --zone=public --add-service=http +firewall-cmd --permanent --zone=public --add-service=https +firewall-cmd --reload + +# Restart caddy +systemctl restart caddy \ No newline at end of file diff --git a/Server.Installer/Resources/CentOS_Nginx_Install.sh b/Server.Installer/Resources/CentOS_Nginx_Install.sh new file mode 100644 index 00000000..3b4c403c --- /dev/null +++ b/Server.Installer/Resources/CentOS_Nginx_Install.sh @@ -0,0 +1,154 @@ +#!/bin/bash +echo "Thanks for trying Remotely!" +echo + +Args=( "$@" ) +ArgLength=${#Args[@]} + +for (( i=0; i<${ArgLength}; i+=2 )); +do + if [ "${Args[$i]}" = "--host" ]; then + HostName="${Args[$i+1]}" + elif [ "${Args[$i]}" = "--approot" ]; then + AppRoot="${Args[$i+1]}" + fi +done + +if [ -z "$AppRoot" ]; then + read -p "Enter path where the Remotely server files should be installed (typically /var/www/remotely): " AppRoot + if [ -z "$AppRoot" ]; then + AppRoot="/var/www/remotely" + fi +fi + +if [ -z "$HostName" ]; then + read -p "Enter server host (e.g. remotely.yourdomainname.com): " HostName +fi + +echo "Using $AppRoot as the Remotely website's content directory." + +yum update + +# Install .NET Core Runtime. +sudo rpm -Uvh https://packages.microsoft.com/config/centos/7/packages-microsoft-prod.rpm + +yum -y install apt-transport-https +yum -y update +yum -y install aspnetcore-runtime-5.0 + + + # Install other prerequisites. +yum -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm +yum -y install yum-utils +yum-config-manager --enable rhui-REGION-rhel-server-extras rhui-REGION-rhel-server-optional +yum -y install unzip +yum -y install acl +yum -y install libc6-dev +yum -y install libgdiplus + + +# Set permissions on Remotely files. +setfacl -R -m u:apache:rwx $AppRoot +chown -R apache:apache $AppRoot +chmod +x "$AppRoot/Remotely_Server" + + +# Install Nginx +yum -y install nginx + +systemctl start nginx + + +# Configure Nginx +nginxConfig="server { + listen 80; + server_name $HostName *.$HostName; + location / { + proxy_pass http://localhost:5000; + proxy_http_version 1.1; + proxy_set_header Upgrade \$http_upgrade; + proxy_set_header Connection close; + proxy_set_header Host \$host; + proxy_cache_bypass \$http_upgrade; + proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto \$scheme; + } + + location /_blazor { + proxy_pass http://localhost:5000; + proxy_http_version 1.1; + proxy_set_header Upgrade \$http_upgrade; + proxy_set_header Connection \"Upgrade\"; + proxy_set_header Host \$host; + proxy_cache_bypass \$http_upgrade; + } + location /AgentHub { + proxy_pass http://localhost:5000; + proxy_http_version 1.1; + proxy_set_header Upgrade \$http_upgrade; + proxy_set_header Connection \"Upgrade\"; + proxy_set_header Host \$host; + proxy_cache_bypass \$http_upgrade; + } + + location /ViewerHub { + proxy_pass http://localhost:5000; + proxy_http_version 1.1; + proxy_set_header Upgrade \$http_upgrade; + proxy_set_header Connection \"Upgrade\"; + proxy_set_header Host \$host; + proxy_cache_bypass \$http_upgrade; + } + location /CasterHub { + proxy_pass http://localhost:5000; + proxy_http_version 1.1; + proxy_set_header Upgrade \$http_upgrade; + proxy_set_header Connection \"Upgrade\"; + proxy_set_header Host \$host; + proxy_cache_bypass \$http_upgrade; + } +}" + +echo "$nginxConfig" > /etc/nginx/conf.d/remotely.conf + +# Test config. +nginx -t + +# Reload. +nginx -s reload + + +# Create service. + +serviceConfig="[Unit] +Description=Remotely Server + +[Service] +WorkingDirectory=$AppRoot +ExecStart=$AppRoot/Remotely_Server +Restart=always +# Restart service after 10 seconds if the dotnet service crashes: +RestartSec=10 +SyslogIdentifier=remotely +Environment=ASPNETCORE_ENVIRONMENT=Production +Environment=DOTNET_PRINT_TELEMETRY_MESSAGE=false + +[Install] +WantedBy=multi-user.target" + +echo "$serviceConfig" > /etc/systemd/system/remotely.service + + +# Enable service. +systemctl enable remotely.service +# Start service. +systemctl start remotely.service + +firewall-cmd --permanent --zone=public --add-service=http +firewall-cmd --permanent --zone=public --add-service=https +firewall-cmd --reload + +# Install Certbot and get SSL cert. +yum -y install certbot python3-certbot-nginx + +certbot --nginx \ No newline at end of file diff --git a/Server.Installer/Resources/IIS_Windows_Install.ps1 b/Server.Installer/Resources/IIS_Windows_Install.ps1 new file mode 100644 index 00000000..106b4dee --- /dev/null +++ b/Server.Installer/Resources/IIS_Windows_Install.ps1 @@ -0,0 +1,398 @@ +<# +.SYNOPSIS + Configures IIS and installs the Remotely server. +.COPYRIGHT + Copyright © 2020 Translucency Software. All rights reserved. +#> +param ( + # The name to use for the IIS Application Pool for the Remotely site. + [Parameter(Mandatory=$True)] + [string]$AppPoolName, + # The name to use for the IIS site. + [Parameter(Mandatory=$True)] + [string]$SiteName, + # The folder path where the Remotely server files are located. + [Parameter(Mandatory=$True)] + [string]$SitePath, + # Whether to run the script without any prompts. + [switch]$Quiet, + # The path to Windows ACME Simple (wacs.exe) to use for automatically obtaining and + # installing a Let's Encrypt certificate. + # (Project and downloads: https://github.com/win-acme/win-acme) + [string]$WacsPath, + # The email address to use when registering the certificate with WACS. + [string]$EmailAddress +) + +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + +$ErrorActionPreference = "Stop" +$Host.UI.RawUI.WindowTitle = "Remotely Setup" +Clear-Host + +#region Variables +$ServerCmdlets = $false +$FirewallSet = $false +$CopyErrors = $false +if ($PSScriptRoot -eq ""){ + $PSScriptRoot = (Get-Location) +} + +$Root = (Get-Item -Path $PSScriptRoot).Parent.FullName +$BindingHostname = "" +#endregion + +#region Functions +function Do-Pause { + if (!$Quiet){ + pause + } +} +function Wrap-Host +{ + [CmdletBinding()] + [Alias()] + [OutputType([int])] + Param + ( + # The text to write. + [Parameter(Mandatory=$false, + ValueFromPipelineByPropertyName=$true, + Position=0)] + [String] + $Text, + + # Param2 help description + [Parameter(Mandatory=$false, + ValueFromPipelineByPropertyName=$true, + Position=1)] + [ConsoleColor] + $ForegroundColor + ) + + Begin + { + } + Process + { + if (!$Text){ + Write-Host + return + } + $Width = $Host.UI.RawUI.BufferSize.Width + $SB = New-Object System.Text.StringBuilder + while ($Text.Length -gt $Width) { + [int]$LastSpace = $Text.Substring(0, $Width).LastIndexOf(" ") + $SB.AppendLine($Text.Substring(0, $LastSpace).Trim()) | Out-Null + $Text = $Text.Substring(($LastSpace), $Text.Length - $LastSpace).Trim() + } + $SB.Append($Text) | Out-Null + if ($ForegroundColor) + { + Write-Host $SB.ToString() -ForegroundColor $ForegroundColor + } + else + { + Write-Host $SB.ToString() + } + + } + End + { + } +} + +#endregion + + +#region Prerequisite Tests +### Test if process is elevated. ### +$User = [Security.Principal.WindowsIdentity]::GetCurrent() +if ((New-Object Security.Principal.WindowsPrincipal $User).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator) -eq $false) { + Wrap-Host + Wrap-Host "Error: This installation needs to be run from an elevated process (Run as Administrator)." -ForegroundColor Red + Do-Pause + return +} +### Check PS version. ### +if ((Get-Host).Version.Major -lt 5) { + Wrap-Host + Wrap-Host "Error: PowerShell 5 is required. Please install it via the Windows Management Framework 5.1 download from Microsoft." -ForegroundColor Red + Do-Pause + return +} +### Check Script Root ### +if (!$PSScriptRoot) { + Wrap-Host + Wrap-Host "Error: Unable to determine working directory. Please make sure you're running the full script and not just a section." -ForegroundColor Red + Do-Pause + return +} + +### Check OS version. ### +$OS = Get-WmiObject -Class Win32_OperatingSystem +if ($OS.Name.ToLower().Contains("home") -or $OS.Caption.ToLower().Contains("home")) { + Wrap-Host + Wrap-Host "Error: Windows Home version does not have the necessary features to run Remotely." -ForegroundColor Red + Do-Pause + return +} +### Test if Windows Feature cmdlets are available. ### +if ((Get-Command -Name "Add-WindowsFeature" -ErrorAction Ignore) -eq $null) { + $ServerCmdlets = $false +} +else { + $ServerCmdlets = $true +} +### Check if PostgreSQL is installed. ### +##if ((Get-Package -Name "*PostgreSQL*" -ErrorAction SilentlyContinue) -eq $null){ +## Wrap-Host +## Wrap-Host "ERROR: PostgreSQL was not found. Please install it from https://postgresql.org." -ForegroundColor Red +## Wrap-Host +## Do-Pause +## return +##} + +$HostingPackage = Get-Package -Name "*.NET*Windows Server Hosting*" -ErrorAction SilentlyContinue +$Result = Invoke-WebRequest -Uri "https://dotnet.microsoft.com/download/dotnet/current/runtime" -UseBasicParsing +$BundleLink = $Result.Links | Where-Object {$_.outerHTML -like "*Download Hosting Bundle*"} +$Result = Invoke-WebRequest -Uri ("https://dotnet.microsoft.com$($BundleLink.href)") -UseBasicParsing +$DownloadLink = $Result.Links | Where-Object {$_.outerHTML -like "*Click here to download manually*"} +$Parts = $DownloadLink.href.Split("-") +$RemoteVersionString = $Parts[$Parts.Length - 2] + +if ($HostingPackage -ne $null) { + $LocalVersion = [System.Version]::Parse($HostingPackage.Version) + +} +$RemoteVersion = [System.Version]::Parse($RemoteVersionString) + +if ($HostingPackage -eq $null -or $RemoteVersion -gt $LocalVersion) { + Wrap-Host "Downloading .NET Core Runtime and Hosting Bundle..." + $ProgressPreference = "SilentlyContinue" + Invoke-WebRequest -Uri $DownloadLink.href -OutFile "$env:TEMP\dotnet-hosting-win.exe" -UseBasicParsing + $ProgressPreference = "Continue" + Start-Process -FilePath "$env:TEMP\dotnet-hosting-win.exe" -ArgumentList "/install /quiet /norestart" -Wait + Wrap-Host + Wrap-Host ".NET Runtime installation completed." +} +else { + Wrap-Host ".NET Hosting Bundle is up-to-date." +} + +#endregion + +### Hosting Requirements ### +Wrap-Host +Wrap-Host "**********************************" +Wrap-Host " IMPORTANT" -ForegroundColor Green +Wrap-Host "**********************************" +Wrap-Host +Wrap-Host "You must have the .NET Core Runtime installed, which includes the IIS hosting module. The SDK doesn't have this." +Wrap-Host +Wrap-Host "If you have not already done so, please download and install it from the following link:" +Wrap-Host +Wrap-Host "https://dotnet.microsoft.com/download" +Wrap-Host +Do-Pause +Clear-Host + + +### Intro ### +Clear-Host +Wrap-Host +Wrap-Host "**********************************" +Wrap-Host " Remotely Setup" -ForegroundColor Cyan +Wrap-Host "**********************************" +Wrap-Host +Wrap-Host "Hello, and thank you for trying out Remotely!" -ForegroundColor Green +Wrap-Host +Wrap-Host "This setup script will create an IIS site and install Remotely on this machine." -ForegroundColor Green +Wrap-Host +Do-Pause +Clear-Host + + +### Automatic IIS Setup ### +if ($ServerCmdlets) { + $RebootRequired = $false + Wrap-Host + Wrap-Host "Installing IIS components..." -ForegroundColor Green + Write-Progress -Activity "IIS Component Installation" -Status "Installing web server" -PercentComplete (1/7*100) + $Result = Add-WindowsFeature Web-Server + if ($Result.RestartNeeded -like "Yes") + { + $RebootRequired = $true + } + #Write-Progress -Activity "IIS Component Installation" -Status "Installing ASP.NET" -PercentComplete (2/7*100) + #$Result = Add-WindowsFeature Web-Asp-Net + #if ($Result.RestartNeeded -like "Yes") + #{ + # $RebootRequired = $true + #} + #Write-Progress -Activity "IIS Component Installation" -Status "Installing ASP.NET 4.5" -PercentComplete (3/7*100) + #$Result = Add-WindowsFeature Web-Asp-Net45 + #if ($Result.RestartNeeded -like "Yes") + #{ + # $RebootRequired = $true + #} + #Write-Progress -Activity "IIS Component Installation" -Status "Installing web sockets" -PercentComplete (4/7*100) + #$Result = Add-WindowsFeature Web-WebSockets + #if ($Result.RestartNeeded -like "Yes") + #{ + # $RebootRequired = $true + #} + Write-Progress -Activity "IIS Component Installation" -Status "Installing IIS management tools" -PercentComplete (5/7*100) + $Result = Add-WindowsFeature Web-Mgmt-Tools + if ($Result.RestartNeeded -like "Yes") + { + $RebootRequired = $true + } + #Write-Progress -Activity "IIS Component Installation" -Status "Installing web filtering" -PercentComplete (6/7*100) + #$Result = Add-WindowsFeature Web-Filtering + #if ($Result.RestartNeeded -like "Yes") + #{ + # $RebootRequired = $true + #} + + Write-Progress -Activity "IIS Component Installation" -Status "IIS setup completed" -PercentComplete (7/7*100) -Completed + Start-Sleep 2 +} +else +{ + Wrap-Host + Wrap-Host "Installing IIS components..." -ForegroundColor Green + Write-Progress -Activity "IIS Component Installation" -Status "Installing web server" -PercentComplete (1/7*100) + DISM /Online /Enable-Feature /FeatureName:IIS-WebServer /All /Quiet + + #Write-Progress -Activity "IIS Component Installation" -Status "Installing ASP.NET" -PercentComplete (2/7*100) + #DISM /Online /Enable-Feature /FeatureName:IIS-ASPNET /All /Quiet + + #Write-Progress -Activity "IIS Component Installation" -Status "Installing ASP.NET 4.5" -PercentComplete (3/7*100) + #DISM /Online /Enable-Feature /FeatureName:IIS-ASPNET45 /All /Quiet + + #Write-Progress -Activity "IIS Component Installation" -Status "Installing web sockets" -PercentComplete (4/7*100) + #DISM /Online /Enable-Feature /FeatureName:IIS-WebSockets /All /Quiet + + Write-Progress -Activity "IIS Component Installation" -Status "Installing IIS management tools" -PercentComplete (5/7*100) + DISM /Online /Enable-Feature /FeatureName:IIS-ManagementConsole /All /Quiet + + #Write-Progress -Activity "IIS Component Installation" -Status "Installing web filtering" -PercentComplete (6/7*100) + #DISM /Online /Enable-Feature /FeatureName:IIS-RequestFiltering /All /Quiet + + Write-Progress -Activity "IIS Component Installation" -Status "IIS setup completed" -PercentComplete (7/7*100) -Completed + Start-Sleep 2 + +} + +Clear-Host +### Create IIS Site ## + +if ((Get-IISAppPool -Name $AppPoolName) -eq $null) { + New-WebAppPool -Name $AppPoolName + Set-ItemProperty -Path "IIS:\AppPools\$AppPoolName" -name processModel.identityType -Value 4 + Set-ItemProperty -Path "IIS:\AppPools\$AppPoolName" -name processModel.loadUserProfile -Value $true +} + +if ((Get-Website -Name $SiteName) -eq $null) { + New-Website -Name $SiteName -PhysicalPath $SitePath -HostHeader $BindingHostname -ApplicationPool $AppPoolName +} + + +Wrap-Host +Wrap-Host "This will DELETE ALL FILES in the selected website and install Remotely Server. If this is not your intention, close this window now and create a new website where Remotely Server will be installed." -ForegroundColor Red +Wrap-Host +Do-Pause + +# Stop site. +Clear-Host +Wrap-Host +Wrap-Host "Stopping website..." -ForegroundColor Green +Stop-Website -Name $SiteName + + +### File Cleanup ### +Wrap-Host +Wrap-Host "Cleaning up existing files..." -ForegroundColor Green +$Success = $false + +while ($Success -eq $false) { + try { + Get-ChildItem -Path "$SitePath" -Recurse | Remove-Item -Force -Recurse + $Success = $true + } + catch { + Start-Sleep -Seconds 1 + } +} + +### Set ACL on website folders and files ### +Wrap-Host +Wrap-Host "Setting ACLs..." -ForegroundColor Green +$Acl = Get-Acl -Path $SitePath +$Rule = New-Object System.Security.AccessControl.FileSystemAccessRule("BUILTIN\IIS_IUSRS", "Modify", "ContainerInherit,ObjectInherit", "None", "Allow") +$Acl.AddAccessRule($Rule) +$Acl.SetOwner((New-Object System.Security.Principal.NTAccount("Administrators"))) +Set-Acl -Path $SitePath -AclObject $Acl +Get-ChildItem -Path $SitePath -Recurse | ForEach-Object { + Set-Acl -Path $_.FullName -AclObject $Acl +} + +### Firewall Rules ### +Wrap-Host +Wrap-Host "Checking firewall rules for HTTP/HTTPS..." -ForegroundColor Green +try +{ + Enable-NetFirewallRule -Name "IIS-WebServerRole-HTTP-In-TCP" + Enable-NetFirewallRule -Name "IIS-WebServerRole-HTTPS-In-TCP" + if ((Get-NetFirewallRule -Name "IIS-WebServerRole-HTTP-In-TCP").Enabled -like "False" -or (Get-NetFirewallRule -Name "IIS-WebServerRole-HTTP-In-TCP").Enabled -like "False") + { + $FirewallSet = $false + } + else + { + $FirewallSet = $true + } +} +catch +{ + $FirewallSet = $false +} + +# Start website. +Start-Website -Name $SiteName + + +### SSL certificate installation. ### +if ($WacsPath) { + if (Test-Path -Path $WacsPath) { + &"$WacsPath" --target iis --siteid (Get-Website -Name $SiteName).ID --installation iis --emailaddress $EmailAddress --accepttos + } +} + +Wrap-Host +Wrap-Host +Wrap-Host +Wrap-Host "**********************************" +Wrap-Host " Server setup complete!" -ForegroundColor Green +Wrap-Host "**********************************" +Wrap-Host +Wrap-Host "If a path to Win-Acme exe path (WacsPath) wasn't provided, SSL/TLS needs to be set up in IIS. I recommend checking out Let's Encrypt for free, automated SSL certificates." -ForegroundColor Green + +if ($RebootRequired) { + Wrap-Host + Wrap-Host "A reboot is required for the new IIS components to work properly. Please reboot your computer at your earliest convenience." -ForegroundColor Red +} +if ($FirewallSet -eq $false) +{ + Wrap-Host + Wrap-Host "Firewall rules were not properly set. Please ensure that ports 80 (HTTP) and 443 (HTTPS) are open. Windows Firewall has predefined rules for these called ""World Wide Web Services (HTTP(S) Traffic-In)""." -ForegroundColor Red +} + +if ($CopyErrors) +{ + Wrap-Host + Wrap-Host "There were errors copying some of the server files. Please try deleting all files in the website directory and trying again." -ForegroundColor Red +} +Wrap-Host +Do-Pause \ No newline at end of file diff --git a/Server.Installer/Resources/Ubuntu_Caddy_Install.sh b/Server.Installer/Resources/Ubuntu_Caddy_Install.sh new file mode 100644 index 00000000..d2cfeb81 --- /dev/null +++ b/Server.Installer/Resources/Ubuntu_Caddy_Install.sh @@ -0,0 +1,100 @@ +#!/bin/bash +echo "Thanks for trying Remotely!" +echo + +Args=( "$@" ) +ArgLength=${#Args[@]} + +for (( i=0; i<${ArgLength}; i+=2 )); +do + if [ "${Args[$i]}" = "--host" ]; then + HostName="${Args[$i+1]}" + elif [ "${Args[$i]}" = "--approot" ]; then + AppRoot="${Args[$i+1]}" + fi +done + +if [ -z "$AppRoot" ]; then + read -p "Enter path where the Remotely server files should be installed (typically /var/www/remotely): " AppRoot + if [ -z "$AppRoot" ]; then + AppRoot="/var/www/remotely" + fi +fi + +if [ -z "$HostName" ]; then + read -p "Enter server host (e.g. remotely.yourdomainname.com): " HostName +fi + +chmod +x "$AppRoot/Remotely_Server" + +echo "Using $AppRoot as the Remotely website's content directory." + +UbuntuVersion=$(lsb_release -r -s) + + +# Install .NET Core Runtime. +wget -q https://packages.microsoft.com/config/ubuntu/$UbuntuVersion/packages-microsoft-prod.deb +dpkg -i packages-microsoft-prod.deb +add-apt-repository universe +apt-get update +apt-get -y install apt-transport-https +apt-get -y install aspnetcore-runtime-5.0 +rm packages-microsoft-prod.deb + + + # Install other prerequisites. +apt-get -y install unzip +apt-get -y install acl +apt-get -y install libc6-dev +apt-get -y install libgdiplus + + +# Install Caddy +apt install -y debian-keyring debian-archive-keyring apt-transport-https +curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo apt-key add - +curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee -a /etc/apt/sources.list.d/caddy-stable.list +apt update +apt install caddy + + +# Configure Caddy +caddyConfig=" + $HostName { + reverse_proxy 127.0.0.1:5000 + } +" + +echo "$caddyConfig" > /etc/caddy/Caddyfile + + +# Create Remotely service. + +serviceConfig="[Unit] +Description=Remotely Server + +[Service] +WorkingDirectory=$AppRoot +ExecStart=/usr/bin/dotnet $AppRoot/Remotely_Server.dll +Restart=always +# Restart service after 10 seconds if the dotnet service crashes: +RestartSec=10 +SyslogIdentifier=remotely +Environment=ASPNETCORE_ENVIRONMENT=Production +Environment=DOTNET_PRINT_TELEMETRY_MESSAGE=false + +[Install] +WantedBy=multi-user.target" + +echo "$serviceConfig" > /etc/systemd/system/remotely.service + + +# Enable service. +systemctl enable remotely.service +# Start service. +systemctl restart remotely.service + + +# Restart caddy +systemctl restart caddy + +echo "Installation completed." \ No newline at end of file diff --git a/Server.Installer/Resources/Ubuntu_Nginx_Install.sh b/Server.Installer/Resources/Ubuntu_Nginx_Install.sh new file mode 100644 index 00000000..e283bc2f --- /dev/null +++ b/Server.Installer/Resources/Ubuntu_Nginx_Install.sh @@ -0,0 +1,168 @@ +#!/bin/bash +echo "Thanks for trying Remotely!" +echo + +Args=( "$@" ) +ArgLength=${#Args[@]} + +for (( i=0; i<${ArgLength}; i+=2 )); +do + if [ "${Args[$i]}" = "--host" ]; then + HostName="${Args[$i+1]}" + elif [ "${Args[$i]}" = "--approot" ]; then + AppRoot="${Args[$i+1]}" + fi +done + +if [ -z "$AppRoot" ]; then + read -p "Enter path where the Remotely server files should be installed (typically /var/www/remotely): " AppRoot + if [ -z "$AppRoot" ]; then + AppRoot="/var/www/remotely" + fi +fi + +if [ -z "$HostName" ]; then + read -p "Enter server host (e.g. remotely.yourdomainname.com): " HostName +fi + +chmod +x "$AppRoot/Remotely_Server" + +echo "Using $AppRoot as the Remotely website's content directory." + +UbuntuVersion=$(lsb_release -r -s) + + +# Install .NET Core Runtime. +wget -q https://packages.microsoft.com/config/ubuntu/$UbuntuVersion/packages-microsoft-prod.deb +dpkg -i packages-microsoft-prod.deb +add-apt-repository universe +apt-get update +apt-get -y install apt-transport-https +apt-get -y install aspnetcore-runtime-5.0 +rm packages-microsoft-prod.deb + + + # Install other prerequisites. +apt-get -y install unzip +apt-get -y install acl +apt-get -y install libc6-dev +apt-get -y install libgdiplus + + +# Set permissions on Remotely files. +setfacl -R -m u:www-data:rwx $AppRoot +chown -R "$USER":www-data $AppRoot +chmod +x "$AppRoot/Remotely_Server" + + +# Install Nginx +apt-get update +apt-get -y install nginx + +systemctl start nginx + + +# Configure Nginx +nginxConfig=" + +server { + listen 80; + server_name $HostName *.$HostName; + location / { + proxy_pass http://localhost:5000; + proxy_http_version 1.1; + proxy_set_header Upgrade \$http_upgrade; + proxy_set_header Connection keep-alive; + proxy_set_header Host \$host; + proxy_cache_bypass \$http_upgrade; + proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto \$scheme; + } + + location /_blazor { + proxy_pass http://localhost:5000; + proxy_http_version 1.1; + proxy_set_header Upgrade \$http_upgrade; + proxy_set_header Connection \"upgrade\"; + proxy_set_header Host \$host; + proxy_cache_bypass \$http_upgrade; + proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto \$scheme; + } + location /AgentHub { + proxy_pass http://localhost:5000; + proxy_http_version 1.1; + proxy_set_header Upgrade \$http_upgrade; + proxy_set_header Connection \"upgrade\"; + proxy_set_header Host \$host; + proxy_cache_bypass \$http_upgrade; + proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto \$scheme; + } + location /ViewerHub { + proxy_pass http://localhost:5000; + proxy_http_version 1.1; + proxy_set_header Upgrade \$http_upgrade; + proxy_set_header Connection \"upgrade\"; + proxy_set_header Host \$host; + proxy_cache_bypass \$http_upgrade; + proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto \$scheme; + } + location /CasterHub { + proxy_pass http://localhost:5000; + proxy_http_version 1.1; + proxy_set_header Upgrade \$http_upgrade; + proxy_set_header Connection \"upgrade\"; + proxy_set_header Host \$host; + proxy_cache_bypass \$http_upgrade; + proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto \$scheme; + } +}" + +echo "$nginxConfig" > /etc/nginx/sites-available/remotely + +ln -s /etc/nginx/sites-available/remotely /etc/nginx/sites-enabled/remotely + +# Test config. +nginx -t + +# Reload. +nginx -s reload + + + + +# Create service. + +serviceConfig="[Unit] +Description=Remotely Server + +[Service] +WorkingDirectory=$AppRoot +ExecStart=/usr/bin/dotnet $AppRoot/Remotely_Server.dll +Restart=always +# Restart service after 10 seconds if the dotnet service crashes: +RestartSec=10 +SyslogIdentifier=remotely +User=www-data +Environment=ASPNETCORE_ENVIRONMENT=Production +Environment=DOTNET_PRINT_TELEMETRY_MESSAGE=false + +[Install] +WantedBy=multi-user.target" + +echo "$serviceConfig" > /etc/systemd/system/remotely.service + + +# Enable service. +systemctl enable remotely.service +# Start service. +systemctl restart remotely.service + + +# Install Certbot and get SSL cert. +apt-get -y install certbot python3-certbot-nginx + +certbot --nginx \ No newline at end of file diff --git a/Server.Installer/Server.Installer.csproj b/Server.Installer/Server.Installer.csproj new file mode 100644 index 00000000..73c26b3f --- /dev/null +++ b/Server.Installer/Server.Installer.csproj @@ -0,0 +1,35 @@ + + + + Exe + net5.0 + Remotely_Server_Installer + Remotely_Icon.ico + Remotely.Server.Installer + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Server.Installer/Services/GitHubApi.cs b/Server.Installer/Services/GitHubApi.cs new file mode 100644 index 00000000..ad891700 --- /dev/null +++ b/Server.Installer/Services/GitHubApi.cs @@ -0,0 +1,157 @@ +using Remotely.Shared.Utilities; +using Server.Installer.Models; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Net.Http.Json; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; + +namespace Server.Installer.Services +{ + public interface IGitHubApi : IDisposable + { + Task TriggerDispatch(CliParams cliParams); + Task GetLatestBuildArtifact(CliParams cliParams); + Task DownloadArtifact(CliParams cliParams, string artifactDownloadUrl, string downloadToPath); + } + public class GitHubApi : IGitHubApi + { + private readonly HttpClient _httpClient; + private readonly string _apiHost = "https://api.github.com"; + + public GitHubApi() + { + _httpClient = new HttpClient(); + _httpClient.DefaultRequestHeaders.Add("User-Agent", "Remotely Server Installer"); + } + + public void Dispose() + { + _httpClient?.Dispose(); + GC.SuppressFinalize(this); + } + + public async Task DownloadArtifact(CliParams cliParams, string artifactDownloadUrl, string downloadToPath) + { + try + { + ConsoleHelper.WriteLine("Downloading build artifact."); + + var message = GetHttpRequestMessage(HttpMethod.Get, artifactDownloadUrl, cliParams); + + var response = await _httpClient.SendAsync(message); + + ConsoleHelper.WriteLine($"Download artifact response status code: {response.StatusCode}"); + + if (!response.IsSuccessStatusCode) + { + ConsoleHelper.WriteError("GitHub API call to download build artifact failed. Please check your input parameters."); + Environment.Exit(1); + } + + using var responseStream = await response.Content.ReadAsStreamAsync(); + using var fileStream = new FileStream(downloadToPath, FileMode.Create); + + await responseStream.CopyToAsync(fileStream); + return true; + } + catch (Exception ex) + { + ConsoleHelper.WriteError($"Error while downloading artifact. Message: {ex.Message}"); + return false; + } + } + + public async Task GetLatestBuildArtifact(CliParams cliParams) + { + try + { + var message = GetHttpRequestMessage(HttpMethod.Get, + $"{_apiHost}/repos/{cliParams.GitHubUsername}/Remotely/actions/artifacts", + cliParams); + + var response = await _httpClient.SendAsync(message); + + ConsoleHelper.WriteLine($"Get artifacts response status code: {response.StatusCode}"); + + if (!response.IsSuccessStatusCode) + { + ConsoleHelper.WriteError("GitHub API call to get build artifacts failed. Please check your input parameters."); + Environment.Exit(1); + } + + var payload = await response.Content.ReadFromJsonAsync(); + if (payload?.artifacts?.Any() != true) + { + return null; + } + + return payload.artifacts.OrderByDescending(x => x.created_at).First(); + } + catch (Exception ex) + { + ConsoleHelper.WriteError("Error while trying to retrieve build artifacts." + + $"Error: {ex.Message}"); + Environment.Exit(1); + } + + return null; + } + + public async Task TriggerDispatch(CliParams cliParams) + { + try + { + ConsoleHelper.WriteLine("Trigger GitHub Actions build."); + + + var message = GetHttpRequestMessage( + HttpMethod.Post, + $"{_apiHost}/repos/{cliParams.GitHubUsername}/Remotely/actions/workflows/build.yml/dispatches", + cliParams); + + var rid = EnvironmentHelper.IsLinux ? + "linux-x64" : + "win-x64"; + + var body = new + { + @ref = cliParams.Reference, + inputs = new + { + serverUrl = cliParams.ServerUrl.ToString(), + rid = rid + } + }; + message.Content = new StringContent(JsonSerializer.Serialize(body)); + + var response = await _httpClient.SendAsync(message); + + ConsoleHelper.WriteLine($"Dispatch response status code: {response.StatusCode}"); + + return response.IsSuccessStatusCode; + } + catch (Exception ex) + { + ConsoleHelper.WriteError($"Error: {ex.Message}"); + } + return false; + } + + private HttpRequestMessage GetHttpRequestMessage(HttpMethod method, string url, CliParams cliParams) + { + var message = new HttpRequestMessage(method, url); + + var base64Auth = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{cliParams.GitHubUsername}:{cliParams.GitHubPat}")); + message.Headers.Authorization = new AuthenticationHeaderValue("Basic", base64Auth); + message.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.github.v3+json")); + + return message; + } + } +} diff --git a/Server.Installer/Services/ServerInstaller.cs b/Server.Installer/Services/ServerInstaller.cs new file mode 100644 index 00000000..03346f6c --- /dev/null +++ b/Server.Installer/Services/ServerInstaller.cs @@ -0,0 +1,138 @@ +using Remotely.Shared.Utilities; +using Server.Installer.Models; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace Server.Installer.Services +{ + public interface IServerInstaller + { + Task PerformInstall(CliParams cliParams); + } + + public class ServerInstaller : IServerInstaller + { + private readonly IGitHubApi _githubApi; + + public ServerInstaller(IGitHubApi githubApi) + { + _githubApi = githubApi; + } + + public async Task PerformInstall(CliParams cliParams) + { + var latestBuild = await _githubApi.GetLatestBuildArtifact(cliParams); + var existingBuildTimestamp = latestBuild?.created_at; + + if (cliParams.CreateNew == true) + { + var dispatchResult = await _githubApi.TriggerDispatch(cliParams); + + if (!dispatchResult) + { + ConsoleHelper.WriteError("GitHub API call to trigger build action failed. Please check your input parameters."); + return; + } + + ConsoleHelper.WriteLine("Build action triggered successfully. Waiting for build completion."); + + while (latestBuild is null || + latestBuild.created_at <= existingBuildTimestamp.Value) + { + await Task.Delay(TimeSpan.FromMinutes(1)); + ConsoleHelper.WriteLine("Waiting for GitHub build completion."); + latestBuild = await _githubApi.GetLatestBuildArtifact(cliParams); + } + } + else if (latestBuild is null) + { + ConsoleHelper.WriteError("There are no existing build artifacts, and --create-new was not specified. Exiting."); + return; + } + + var filePath = Path.Combine(Path.GetTempPath(), "Remotely_Artifact.zip"); + + var downloadResult = await _githubApi.DownloadArtifact(cliParams, latestBuild.archive_download_url, filePath); + + if (!downloadResult) + { + ConsoleHelper.WriteError("Downloading the build artifact was not successful."); + return; + } + + + ConsoleHelper.WriteLine("Extracting artifact files."); + if (Directory.Exists(cliParams.InstallDirectory)) + { + Directory.Delete(cliParams.InstallDirectory, true); + } + Directory.CreateDirectory(cliParams.InstallDirectory); + ZipFile.ExtractToDirectory(filePath, cliParams.InstallDirectory); + + await LaunchExternalInstaller(cliParams); + } + + private async Task LaunchExternalInstaller(CliParams cliParams) + { + ConsoleHelper.WriteLine("Launching install script for selected reverse proxy type."); + var resourcePath = "Remotely.Server.Installer.Resources."; + + resourcePath += cliParams.WebServer.Value switch + { + WebServerType.UbuntuCaddy => "Ubuntu_Caddy_Install.sh", + WebServerType.UbuntuNginx => "Ubuntu_Nginx_Install.sh", + WebServerType.CentOsCaddy => "CentOS_Caddy_Install.sh", + WebServerType.CentOsNginx => "CentOS_Nginx_Install.sh", + WebServerType.IisWindows => "IIS_Windows_Install.ps1", + _ => throw new Exception("Unrecognized reverse proxy type."), + }; + var fileName = resourcePath.Split(".").Last(); + var filePath = Path.Combine(Path.GetTempPath(), fileName); + + using (var mrs = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourcePath)) + using (var fileStream = new FileStream(filePath, FileMode.Create)) + { + await mrs.CopyToAsync(fileStream); + } + + var scriptResult = false; + ProcessStartInfo psi; + + if (cliParams.WebServer.Value == WebServerType.IisWindows) + { + psi = new ProcessStartInfo("powershell.exe") + { + Arguments = $"-f \"{filePath}\" -AppPoolName Remotely -SiteName Remotely " + + $"-SitePath \"{cliParams.InstallDirectory}\" -Quiet", + WorkingDirectory = cliParams.InstallDirectory + }; + } + else + { + Process.Start("sudo", $"chmod +x {filePath}").WaitForExit(); + psi = new ProcessStartInfo("sudo") + { + Arguments = $"\"{filePath}\" --host {cliParams.ServerUrl.Authority} --approot {cliParams.InstallDirectory}", + WorkingDirectory = cliParams.InstallDirectory + }; + } + + var proc = Process.Start(psi); + + scriptResult = await Task.Run(() => proc.WaitForExit((int)TimeSpan.FromMinutes(30).TotalMilliseconds)); + + if (!scriptResult) + { + ConsoleHelper.WriteError("Installer script is taking longer than expected."); + } + } + } +} diff --git a/Shared/Services/ElevationDetectorLinux.cs b/Shared/Services/ElevationDetectorLinux.cs new file mode 100644 index 00000000..3a247a35 --- /dev/null +++ b/Shared/Services/ElevationDetectorLinux.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace Remotely.Shared.Services +{ + public class ElevationDetectorLinux : IElevationDetector + { + [DllImport("libc", SetLastError = true)] + private static extern uint geteuid(); + + public bool IsElevated() + { + return geteuid() == 0; + } + } +} diff --git a/Shared/Services/ElevationDetectorWin.cs b/Shared/Services/ElevationDetectorWin.cs new file mode 100644 index 00000000..6a4cba10 --- /dev/null +++ b/Shared/Services/ElevationDetectorWin.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Security.Principal; +using System.Text; +using System.Threading.Tasks; + +namespace Remotely.Shared.Services +{ + public class ElevationDetectorWin : IElevationDetector + { + public bool IsElevated() + { + return new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator); + } + } +} diff --git a/Shared/Services/IElevationDetector.cs b/Shared/Services/IElevationDetector.cs new file mode 100644 index 00000000..c0b88c6e --- /dev/null +++ b/Shared/Services/IElevationDetector.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Remotely.Shared.Services +{ + public interface IElevationDetector + { + bool IsElevated(); + } +} diff --git a/Shared/Shared.csproj b/Shared/Shared.csproj index 877a8edd..05a4dbbe 100644 --- a/Shared/Shared.csproj +++ b/Shared/Shared.csproj @@ -12,6 +12,7 @@ + diff --git a/Shared/Utilities/AppConstants.cs b/Shared/Utilities/AppConstants.cs index 57097249..c3fe9743 100644 --- a/Shared/Utilities/AppConstants.cs +++ b/Shared/Utilities/AppConstants.cs @@ -13,5 +13,15 @@ namespace Remotely.Shared.Utilities public const long MaxUploadFileSize = 100_000_000; public const int RelayCodeLength = 4; public const double ScriptRunExpirationMinutes = 30; + + public const string RemotelyAscii = @" + _____ _ _ + | __ \ | | | | + | |__) |___ _ __ ___ ___ | |_ ___| |_ _ + | _ // _ \ '_ ` _ \ / _ \| __/ _ \ | | | | + | | \ \ __/ | | | | | (_) | || __/ | |_| | + |_| \_\___|_| |_| |_|\___/ \__\___|_|\__, | + __/ | + |___/ "; } } diff --git a/Shared/Utilities/ConsoleHelper.cs b/Shared/Utilities/ConsoleHelper.cs new file mode 100644 index 00000000..a4be24f6 --- /dev/null +++ b/Shared/Utilities/ConsoleHelper.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading.Tasks; + +namespace Remotely.Shared.Utilities +{ + public static class ConsoleHelper + { + public static string ReadLine(string prompt, ConsoleColor promptColor = ConsoleColor.Cyan) + { + Console.ForegroundColor = promptColor; + Console.Write($"{prompt.TrimEnd(':')}: "); + Console.ForegroundColor = ConsoleColor.Gray; + + var response = Console.ReadLine(); + Console.WriteLine(); + + return response; + } + + public static void WriteLine(string message, int extraEmptyLines = 0, ConsoleColor foreground = ConsoleColor.Gray, string callerName = "") + { + if (!string.IsNullOrWhiteSpace(callerName)) + { + message = $"[{callerName}] {message}"; + } + Console.ForegroundColor = foreground; + for (var i = 0; i < message.Length;) + { + var lineCount = 0; + + var line = new string(message.Skip(i).TakeWhile(x => { + i++; + return lineCount++ < 50 || !char.IsWhiteSpace(x); + }).ToArray()); + + Console.WriteLine(line); + } + Console.ForegroundColor = ConsoleColor.Gray; + Console.WriteLine(); + for (var i = 0; i < extraEmptyLines; i++) + { + Console.WriteLine(); + } + } + + public static void WriteError(string message, int extraEmptyLines = 0, [CallerMemberName] string callerName = "") + { + WriteLine(message, extraEmptyLines, ConsoleColor.Red, callerName); + } + } +} diff --git a/Shared/Win32/Win32Interop.cs b/Shared/Win32/Win32Interop.cs index 8f36c08f..c9934089 100644 --- a/Shared/Win32/Win32Interop.cs +++ b/Shared/Win32/Win32Interop.cs @@ -10,6 +10,7 @@ using static Remotely.Shared.Win32.User32; namespace Remotely.Shared.Win32 { + // TODO: Use https://github.com/dotnet/pinvoke for all p/invokes. Remove signatures from this project. public class Win32Interop { private static IntPtr _lastInputDesktop; diff --git a/Utilities/Publish.ps1 b/Utilities/Publish.ps1 index d329ac8b..61ef20e9 100644 --- a/Utilities/Publish.ps1 +++ b/Utilities/Publish.ps1 @@ -196,15 +196,6 @@ if ($RID.Length -gt 0 -and $OutDir.Length -gt 0) { } dotnet publish /p:Version=$CurrentVersion /p:FileVersion=$CurrentVersion --runtime $RID --configuration Release --output $OutDir "$Root\Server\" - - Copy-Item -Path "$Root\Utilities\Ubuntu_Server_Install.sh" -Destination "$OutDir\Ubuntu_Server_Install.sh" -Force - Replace-LineInFile -FilePath "$OutDir\Ubuntu_Server_Install.sh" -MatchPattern "HostName=`"`"" -ReplaceLineWith "HostName=`"$($HostNameUri.Host)`"" - - Copy-Item -Path "$Root\Utilities\CentOS_Server_Install.sh" -Destination "$OutDir\CentOS_Server_Install.sh" -Force - Replace-LineInFile -FilePath "$OutDir\CentOS_Server_Install.sh" -MatchPattern "HostName=`"`"" -ReplaceLineWith "HostName=`"$($HostNameUri.Host)`"" - - Copy-Item -Path "$Root\Utilities\Install-RemotelyServer.ps1" -Destination "$OutDir\Install-RemotelyServer.ps1" -Force - Replace-LineInFile -FilePath "$OutDir\CentOS_Server_Install.sh" -MatchPattern "`$BindingHostname = `"`"" -ReplaceLineWith "`$BindingHostname = `"$($HostNameUri.Host)`"" } else { Write-Host "`nSkipping server deployment. Params -outdir and -rid not specified." -ForegroundColor DarkYellow