Add Server.Installer project.

This commit is contained in:
Jared 2021-04-13 18:17:50 -07:00 committed by Jared Goodwin
parent 56b30537cf
commit 50ff25277f
26 changed files with 1723 additions and 29 deletions

View File

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

3
.gitignore vendored
View File

@ -289,4 +289,5 @@ Server/wwwroot/Content/Win-x86/*
.vscode/launch.json
.vscode/tasks.json
Server/.config/dotnet-tools.json
/Server/Properties/ServiceDependencies/*
/Server/Properties/ServiceDependencies/*
Server.Installer/Properties/launchSettings.json

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

254
Server.Installer/Program.cs Normal file
View File

@ -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<IElevationDetector>();
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>(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<IServerInstaller>();
await serverInstaller.PerformInstall(cliParams);
ConsoleHelper.WriteLine("Installation completed.");
}
private static void BuildServices()
{
var services = new ServiceCollection();
services.AddSingleton<IGitHubApi, GitHubApi>();
services.AddSingleton<IServerInstaller, ServerInstaller>();
if (EnvironmentHelper.IsWindows)
{
services.AddSingleton<IElevationDetector, ElevationDetectorWin>();
}
else if (EnvironmentHelper.IsLinux)
{
services.AddSingleton<IElevationDetector, ElevationDetectorLinux>();
}
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");
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,35 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
<AssemblyName>Remotely_Server_Installer</AssemblyName>
<ApplicationIcon>Remotely_Icon.ico</ApplicationIcon>
<RootNamespace>Remotely.Server.Installer</RootNamespace>
</PropertyGroup>
<ItemGroup>
<None Remove="Resources\CentOS_Caddy_Install.sh" />
<None Remove="Resources\CentOS_Nginx_Install.sh" />
<None Remove="Resources\IIS_Windows_Install.ps1" />
<None Remove="Resources\Ubuntu_Caddy_Install.sh" />
<None Remove="Resources\Ubuntu_Nginx_Install.sh" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Resources\CentOS_Nginx_Install.sh" />
<EmbeddedResource Include="Resources\CentOS_Caddy_Install.sh" />
<EmbeddedResource Include="Resources\IIS_Windows_Install.ps1" />
<EmbeddedResource Include="Resources\Ubuntu_Nginx_Install.sh" />
<EmbeddedResource Include="Resources\Ubuntu_Caddy_Install.sh" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Shared\Shared.csproj" />
</ItemGroup>
</Project>

View File

@ -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<bool> TriggerDispatch(CliParams cliParams);
Task<Artifact> GetLatestBuildArtifact(CliParams cliParams);
Task<bool> 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<bool> 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<Artifact> 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<ArtifactsResponsePayload>();
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<bool> 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;
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -12,6 +12,7 @@
<PackageReference Include="ConcurrentList" Version="1.4.0" />
<PackageReference Include="Microsoft.Extensions.Identity.Stores" Version="5.0.5" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
<PackageReference Include="System.Security.Principal.Windows" Version="5.0.0" />
<PackageReference Include="System.Text.Json" Version="5.0.2" />
</ItemGroup>

View File

@ -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 = @"
_____ _ _
| __ \ | | | |
| |__) |___ _ __ ___ ___ | |_ ___| |_ _
| _ // _ \ '_ ` _ \ / _ \| __/ _ \ | | | |
| | \ \ __/ | | | | | (_) | || __/ | |_| |
|_| \_\___|_| |_| |_|\___/ \__\___|_|\__, |
__/ |
|___/ ";
}
}

View File

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

View File

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

View File

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