feat(install): one-command device registration for macOS, Windows and Linux
Registering a machine meant reading a multi-step page, installing Node, and running three commands in the right order. Now it is one line per platform. The hub serves scripts/install.sh and scripts/install.ps1 with its own address and the enrolment token substituted in, so the published command carries everything and there is nothing to fill in: curl -fsSL https://support.freakma.com/install.sh?token=TOKEN | sh irm https://support.freakma.com/install.ps1?token=TOKEN | iex That output is piped straight into a shell, so the token — the only untrusted value in either file — is refused unless it matches the base64url shape that randomToken produces. Each script checks for a usable runtime and stops with instructions rather than guessing, warns when nothing is serving RFB on the loopback, installs per-user with no root or administrator, and registers a login-scoped service: launchd on macOS, a lingering systemd user service on Linux, a logon task on Windows. The Windows script uses Node 22 when it is present and falls back to the bundled executable otherwise, which is what lets one command cover both Windows 11 and Server 2008 R2. It registers a logon task rather than a service on purpose: services run in session 0 and cannot draw on the interactive desktop, so the "ask first" consent prompt would never appear. Also adds /docs — a per-OS setup guide with service management and a troubleshooting table — and reworks the enrolment page into OS tabs that open on whichever platform the reader is sitting at. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -28,6 +28,7 @@ RUN pnpm install --prod --frozen-lockfile
|
||||
COPY server ./server
|
||||
COPY public ./public
|
||||
COPY agent ./agent
|
||||
COPY scripts ./scripts
|
||||
COPY --from=agent-exe /build/dist/rcs-agent.exe ./dist/rcs-agent.exe
|
||||
|
||||
VOLUME ["/data"]
|
||||
|
||||
@@ -90,15 +90,61 @@ LAN as the hub.
|
||||
|
||||
### Agent (the machine dials out)
|
||||
|
||||
**Invite a machine** produces a link. Open it on the target machine and it shows a
|
||||
one-liner:
|
||||
**Invite a machine** produces a link. Open it on the target machine and it shows the
|
||||
command with the token already in it. The full walkthrough — per OS, with service
|
||||
management and troubleshooting — is served at
|
||||
[`/docs`](https://support.freakma.com/docs).
|
||||
|
||||
## Registering a device
|
||||
|
||||
One command per platform. The hub substitutes its own address and the enrolment token
|
||||
into the script before serving it, so there is nothing to fill in but `TOKEN`.
|
||||
|
||||
**macOS and Linux**
|
||||
|
||||
```bash
|
||||
curl -fsSL https://your-hub/download/agent.js -o rcs-agent.js \
|
||||
&& node rcs-agent.js enroll https://your-hub/enroll/TOKEN \
|
||||
&& node rcs-agent.js run
|
||||
curl -fsSL https://support.freakma.com/install.sh?token=TOKEN | sh
|
||||
```
|
||||
|
||||
**Windows**
|
||||
|
||||
```powershell
|
||||
irm https://support.freakma.com/install.ps1?token=TOKEN | iex
|
||||
```
|
||||
|
||||
**Windows 7 / Server 2008 R2** — PowerShell 2.0 has no `irm`, and .NET 3.5 does not
|
||||
negotiate TLS 1.2 without [KB3154518](https://support.microsoft.com/kb/3154518), so
|
||||
the short form fails twice over. From `cmd.exe`:
|
||||
|
||||
```bat
|
||||
powershell -c "[Net.ServicePointManager]::SecurityProtocol=3072; (New-Object Net.WebClient).DownloadString('https://support.freakma.com/install.ps1?token=TOKEN') | iex"
|
||||
```
|
||||
|
||||
What the scripts do, in order: check for a usable runtime and stop with instructions
|
||||
rather than guessing; warn if nothing is listening on `127.0.0.1:5900`; download the
|
||||
agent into a per-user directory; enrol, exchanging the one-time token for an agent key
|
||||
written `0600`; and register a login-scoped service.
|
||||
|
||||
| Platform | Installs to | Kept alive by |
|
||||
|---|---|---|
|
||||
| macOS | `~/.rcs-agent` | launchd agent, `com.freakma.rcs-agent` |
|
||||
| Linux | `~/.rcs-agent` | systemd **user** service `rcs-agent`, with lingering enabled |
|
||||
| Windows | `%LOCALAPPDATA%\RemoteControlSupport` | logon task `RemoteControlSupportAgent` |
|
||||
|
||||
Nothing needs root or administrator. On Windows the installer uses Node 22 if it is
|
||||
already there and falls back to the self-contained `rcs-agent.exe` otherwise, so the
|
||||
same command works on a fresh Windows 11 box and on Server 2008 R2.
|
||||
|
||||
The Windows installer deliberately registers a **logon task, not a service**: services
|
||||
run in session 0 and cannot draw on the interactive desktop, so the *ask first* consent
|
||||
prompt would never appear and every session would time out waiting for an answer.
|
||||
|
||||
Both scripts are plain text and worth reading before piping them into a shell —
|
||||
[`/install.sh`](https://support.freakma.com/install.sh) and
|
||||
[`/install.ps1`](https://support.freakma.com/install.ps1) are exactly what runs.
|
||||
|
||||
### Doing it by hand
|
||||
|
||||
The agent needs Node 22+ and a VNC server listening on `127.0.0.1:5900`:
|
||||
|
||||
| OS | VNC server |
|
||||
@@ -233,6 +279,9 @@ Everything is environment variables — see [.env.example](.env.example).
|
||||
| `POST` | `/api/sessions/:id/kill` | admin |
|
||||
| `GET` | `/api/sessions/history` | |
|
||||
| `GET` | `/api/sessions/audit` | admin |
|
||||
| `GET` | `/docs` | setup guide, no auth |
|
||||
| `GET` | `/download/agent.js` · `/download/agent.exe` | no auth |
|
||||
| `GET` | `/install.sh` · `/install.ps1` | no auth, `?token=` is stamped in |
|
||||
| `POST` | `/api/public/enroll` | no auth, enrolment token |
|
||||
| `POST` | `/api/public/session/:token` | no auth, support link |
|
||||
| WS | `/ws/vnc?ticket=` | browser session |
|
||||
|
||||
@@ -0,0 +1,339 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="color-scheme" content="dark">
|
||||
<title>Registering a device — Remote Support</title>
|
||||
<link rel="stylesheet" href="/styles.css">
|
||||
<style>
|
||||
.docs-shell { max-width: 860px; margin: 0 auto; padding: 28px 20px 80px; position: relative; z-index: 1; }
|
||||
.docs-shell h2 { margin: 34px 0 10px; font-size: 17px; }
|
||||
.docs-shell h3 { margin: 22px 0 8px; font-size: 14px; color: var(--text); }
|
||||
.docs-shell p { margin: 8px 0; font-size: 14px; line-height: 1.65; color: var(--text-dim, var(--text)); }
|
||||
.docs-shell ul { margin: 8px 0 8px 18px; font-size: 14px; line-height: 1.7; }
|
||||
.docs-shell table { width: 100%; border-collapse: collapse; margin: 12px 0; font-size: 13.5px; }
|
||||
.docs-shell th, .docs-shell td { text-align: left; padding: 8px 10px; border-bottom: 1px solid var(--border, #2a2a2a); vertical-align: top; }
|
||||
.docs-shell th { color: var(--text-faint); font-weight: 600; font-size: 12px; text-transform: uppercase; letter-spacing: .04em; }
|
||||
.os-panel[hidden] { display: none; }
|
||||
.copy-wrap { position: relative; }
|
||||
.copy-wrap button { position: absolute; top: 8px; right: 8px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="bg-field" id="bg-field" aria-hidden="true"></div>
|
||||
|
||||
<div class="docs-shell">
|
||||
|
||||
<div class="card-header" style="padding-left:0;border:0">
|
||||
<div class="brand-mark">RC</div>
|
||||
<div>
|
||||
<strong style="font-size:14px">Remote Support</strong>
|
||||
<span class="brand-sub">Documentation</span>
|
||||
</div>
|
||||
<div style="margin-left:auto"><a class="btn ghost small" href="/">Back to console</a></div>
|
||||
</div>
|
||||
|
||||
<span class="card-kicker">Setup guide</span>
|
||||
<h1 style="margin:6px 0 4px">Registering a device</h1>
|
||||
<p class="faint">
|
||||
How to put a machine under remote support, on macOS, Windows and Linux — what to install,
|
||||
what the one-command installer actually does, and how to keep it running.
|
||||
</p>
|
||||
|
||||
<h2>Before you start</h2>
|
||||
<p>
|
||||
Every registered machine needs two things: a <strong>VNC server</strong> serving its screen on
|
||||
<span class="mono">127.0.0.1:5900</span>, and the <strong>agent</strong>, which dials out to this
|
||||
hub and forwards that port over an authenticated WebSocket. The agent is what makes NAT and
|
||||
firewalls stop mattering — nothing inbound is ever opened on the machine.
|
||||
</p>
|
||||
<p>
|
||||
You also need an <strong>enrollment link</strong>. In the console, open
|
||||
<strong>Machines → Invite a machine</strong>. The link carries a one-time token and expires;
|
||||
open it on the machine being registered and it shows the exact command with the token already
|
||||
filled in.
|
||||
</p>
|
||||
|
||||
<div class="notice">
|
||||
<strong>The alternative is direct mode</strong>, where this hub connects straight to the
|
||||
machine's VNC port. That only works when the hub can route to it. If the machine is on another
|
||||
subnet, behind NAT, or reachable only through a VPN or SSH tunnel, use the agent — a direct
|
||||
entry will just time out.
|
||||
</div>
|
||||
|
||||
<h2>Pick a platform</h2>
|
||||
<nav class="tabs" id="os-tabs" style="margin-bottom:16px">
|
||||
<button data-os="macos" class="active">macOS</button>
|
||||
<button data-os="windows">Windows</button>
|
||||
<button data-os="linux">Linux</button>
|
||||
</nav>
|
||||
|
||||
<!-- ------------------------------------------------------------ macOS -->
|
||||
<section class="os-panel" data-os="macos">
|
||||
<h3>1. Turn on Screen Sharing</h3>
|
||||
<p>
|
||||
<strong>System Settings → General → Sharing → Screen Sharing</strong>. Under
|
||||
<em>Computer Settings</em>, enable <em>VNC viewers may control screen with password</em> and
|
||||
set a password — the hub authenticates with it and never passes it to the browser.
|
||||
</p>
|
||||
<p class="faint">
|
||||
macOS serves VNC on port 5900 on all interfaces. The agent only ever connects to it over
|
||||
loopback, so you can firewall the port off from the network.
|
||||
</p>
|
||||
|
||||
<h3>2. Install Node.js 22 or newer</h3>
|
||||
<p>
|
||||
<span class="mono">brew install node</span>, or the installer from
|
||||
<a href="https://nodejs.org" target="_blank" rel="noopener">nodejs.org</a>. Check with
|
||||
<span class="mono">node -v</span>.
|
||||
</p>
|
||||
|
||||
<h3>3. Run the installer</h3>
|
||||
<p>Paste this in Terminal, replacing <span class="mono">TOKEN</span> with the one from your enrollment link:</p>
|
||||
<div class="copy-wrap">
|
||||
<pre class="code" id="cmd-macos"></pre>
|
||||
<button class="small" data-copy="cmd-macos">Copy</button>
|
||||
</div>
|
||||
<p>
|
||||
It installs to <span class="mono">~/.rcs-agent</span>, enrolls the machine, and loads a
|
||||
<strong>launchd</strong> agent so it starts at login and restarts if it dies.
|
||||
</p>
|
||||
|
||||
<h3>Managing it</h3>
|
||||
<table>
|
||||
<tr><th>Do this</th><th>Command</th></tr>
|
||||
<tr><td>Stop</td><td class="mono">launchctl unload ~/Library/LaunchAgents/com.freakma.rcs-agent.plist</td></tr>
|
||||
<tr><td>Start</td><td class="mono">launchctl load ~/Library/LaunchAgents/com.freakma.rcs-agent.plist</td></tr>
|
||||
<tr><td>Watch the log</td><td class="mono">tail -f ~/.rcs-agent/agent.log</td></tr>
|
||||
<tr><td>Remove</td><td class="mono">launchctl unload …plist && rm -rf ~/.rcs-agent</td></tr>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<!-- ---------------------------------------------------------- Windows -->
|
||||
<section class="os-panel" data-os="windows" hidden>
|
||||
<h3>1. Install a VNC server</h3>
|
||||
<p>
|
||||
Windows has none built in. <a href="https://www.tightvnc.com" target="_blank" rel="noopener">TightVNC</a>
|
||||
or UltraVNC both work. During setup, register it as a system service so it survives reboots,
|
||||
and set a password.
|
||||
</p>
|
||||
|
||||
<h3>2. Run the installer</h3>
|
||||
<p>In PowerShell, with <span class="mono">TOKEN</span> from your enrollment link:</p>
|
||||
<div class="copy-wrap">
|
||||
<pre class="code" id="cmd-windows"></pre>
|
||||
<button class="small" data-copy="cmd-windows">Copy</button>
|
||||
</div>
|
||||
<p>
|
||||
No prerequisites. If Node.js 22+ happens to be installed the script uses it; otherwise it
|
||||
downloads <span class="mono">rcs-agent.exe</span>, a self-contained build with its own
|
||||
runtime inside. Everything lands in
|
||||
<span class="mono">%LOCALAPPDATA%\RemoteControlSupport</span> and no administrator rights
|
||||
are needed.
|
||||
</p>
|
||||
|
||||
<h3>Windows 7 and Server 2008 R2</h3>
|
||||
<p>
|
||||
Those ship PowerShell 2.0, which has no <span class="mono">irm</span>, and .NET 3.5, which
|
||||
does not negotiate TLS 1.2 by default — the short command fails on both counts. Use this
|
||||
instead, from <strong>cmd.exe</strong>:
|
||||
</p>
|
||||
<div class="copy-wrap">
|
||||
<pre class="code" id="cmd-windows-legacy"></pre>
|
||||
<button class="small" data-copy="cmd-windows-legacy">Copy</button>
|
||||
</div>
|
||||
<p class="faint">
|
||||
If it still cannot connect, the machine is missing
|
||||
<a href="https://support.microsoft.com/kb/3154518" target="_blank" rel="noopener">KB3154518</a>,
|
||||
which adds TLS 1.2 to .NET 3.5. As a last resort,
|
||||
<a href="/download/agent.exe" download="rcs-agent.exe">download the agent</a> on another
|
||||
machine, copy it across, and run
|
||||
<span class="mono">rcs-agent.exe enroll <link></span> then
|
||||
<span class="mono">rcs-agent.exe run</span>.
|
||||
</p>
|
||||
|
||||
<h3>Managing it</h3>
|
||||
<table>
|
||||
<tr><th>Do this</th><th>Command</th></tr>
|
||||
<tr><td>Check it exists</td><td class="mono">schtasks /query /tn RemoteControlSupportAgent</td></tr>
|
||||
<tr><td>Start</td><td class="mono">schtasks /run /tn RemoteControlSupportAgent</td></tr>
|
||||
<tr><td>Stop</td><td class="mono">schtasks /end /tn RemoteControlSupportAgent</td></tr>
|
||||
<tr><td>Remove</td><td class="mono">schtasks /delete /tn RemoteControlSupportAgent /f</td></tr>
|
||||
</table>
|
||||
|
||||
<div class="notice">
|
||||
<strong>Do not install the agent as a Windows service</strong> on a machine where
|
||||
<em>ask first</em> is enabled. Services run in session 0 and cannot draw on the interactive
|
||||
desktop, so the consent prompt would never appear and every session would time out waiting
|
||||
for an answer. The installer registers a <em>logon task</em> precisely to avoid this — it
|
||||
runs in the console session, where the prompt is visible.
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ------------------------------------------------------------ Linux -->
|
||||
<section class="os-panel" data-os="linux" hidden>
|
||||
<h3>1. Start a VNC server on the loopback</h3>
|
||||
<p>For an existing X session — what you want for support, since it shows the real screen:</p>
|
||||
<div class="copy-wrap">
|
||||
<pre class="code" id="cmd-linux-x11vnc"></pre>
|
||||
<button class="small" data-copy="cmd-linux-x11vnc">Copy</button>
|
||||
</div>
|
||||
<p class="faint">
|
||||
On Wayland, <span class="mono">x11vnc</span> cannot see the screen. Either log in on an Xorg
|
||||
session, or use a Wayland-native server such as <span class="mono">wayvnc</span>.
|
||||
</p>
|
||||
|
||||
<h3>2. Install Node.js 22 or newer</h3>
|
||||
<table>
|
||||
<tr><th>Distro</th><th>Command</th></tr>
|
||||
<tr><td>Debian / Ubuntu</td><td class="mono">curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - && sudo apt install -y nodejs</td></tr>
|
||||
<tr><td>Fedora / RHEL</td><td class="mono">sudo dnf install nodejs</td></tr>
|
||||
<tr><td>Arch</td><td class="mono">sudo pacman -S nodejs</td></tr>
|
||||
</table>
|
||||
|
||||
<h3>3. Run the installer</h3>
|
||||
<div class="copy-wrap">
|
||||
<pre class="code" id="cmd-linux"></pre>
|
||||
<button class="small" data-copy="cmd-linux">Copy</button>
|
||||
</div>
|
||||
<p>
|
||||
It installs to <span class="mono">~/.rcs-agent</span> and registers a
|
||||
<strong>systemd user service</strong>. It also enables lingering, so the agent keeps running
|
||||
when nobody is logged in — without that, the machine drops off the console at logout, which
|
||||
is exactly when you tend to need it.
|
||||
</p>
|
||||
|
||||
<h3>Managing it</h3>
|
||||
<table>
|
||||
<tr><th>Do this</th><th>Command</th></tr>
|
||||
<tr><td>Status</td><td class="mono">systemctl --user status rcs-agent</td></tr>
|
||||
<tr><td>Logs</td><td class="mono">journalctl --user -u rcs-agent -f</td></tr>
|
||||
<tr><td>Restart</td><td class="mono">systemctl --user restart rcs-agent</td></tr>
|
||||
<tr><td>Remove</td><td class="mono">systemctl --user disable --now rcs-agent && rm -rf ~/.rcs-agent</td></tr>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<h2>Registering a machine that cannot run the agent</h2>
|
||||
<p>
|
||||
Printers, appliances, locked-down boxes, anything too old for a current runtime — the agent
|
||||
does not have to run <em>on</em> the machine it serves. Run it on any other machine on the same
|
||||
LAN and point it across:
|
||||
</p>
|
||||
<div class="copy-wrap">
|
||||
<pre class="code" id="cmd-proxy"></pre>
|
||||
<button class="small" data-copy="cmd-proxy">Copy</button>
|
||||
</div>
|
||||
<p class="faint">
|
||||
That host now needs to reach the target's VNC port, and it becomes the machine whose uptime
|
||||
determines whether the target is reachable.
|
||||
</p>
|
||||
|
||||
<h2>Checking it worked</h2>
|
||||
<ul>
|
||||
<li>The machine appears in <strong>Machines</strong> in the console, marked online.</li>
|
||||
<li>Its hostname and OS show up — the agent reports both when it registers.</li>
|
||||
<li>Clicking <strong>Connect</strong> opens the screen.</li>
|
||||
</ul>
|
||||
|
||||
<h2>When it does not</h2>
|
||||
<table>
|
||||
<tr><th>Symptom</th><th>Cause</th></tr>
|
||||
<tr>
|
||||
<td class="mono">timed out connecting to …:5900</td>
|
||||
<td>This hub cannot route to that address. A direct-mode entry on a subnet the hub cannot
|
||||
reach — register it with the agent instead.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="mono">cannot reach …:5900 (ECONNREFUSED)</td>
|
||||
<td>Routing is fine, nothing is listening. The VNC server is not running.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="mono">VNC authentication failed</td>
|
||||
<td>Wrong stored password. Note that VNC authentication only uses the first 8 characters.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Enrollment says the link expired</td>
|
||||
<td>Enrollment tokens are one-time and time-limited. Mint a new one.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Machine shows offline right after install</td>
|
||||
<td>The agent is not running. Check the service commands for your platform above.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Sessions hang on "waiting for consent"</td>
|
||||
<td><em>Ask first</em> is on and the agent cannot show a dialog — it is running headless, as
|
||||
a Windows service, or on Linux without <span class="mono">zenity</span>/<span class="mono">kdialog</span>.</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h2>What the installer does</h2>
|
||||
<p>
|
||||
Both scripts are served by this hub with its address and your token already substituted in, so
|
||||
there is nothing to fill in. They are worth reading before you pipe them into a shell —
|
||||
<a id="raw-sh" href="/install.sh">install.sh</a> and
|
||||
<a id="raw-ps1" href="/install.ps1">install.ps1</a> are the exact text that runs. In order they:
|
||||
</p>
|
||||
<ul>
|
||||
<li>check for a usable runtime, and stop with instructions rather than guessing;</li>
|
||||
<li>warn if nothing is listening on <span class="mono">127.0.0.1:5900</span>;</li>
|
||||
<li>download the agent into a per-user directory — no root, no administrator;</li>
|
||||
<li>enroll, which exchanges the one-time token for an agent key stored with <span class="mono">0600</span> permissions;</li>
|
||||
<li>register a login-scoped service so it comes back after a reboot.</li>
|
||||
</ul>
|
||||
|
||||
<p class="login-foot" style="margin-top:30px">
|
||||
Access control, support links and session policy are covered in the project README.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<script src="/particles.js"></script>
|
||||
<script>
|
||||
const origin = location.origin;
|
||||
|
||||
const commands = {
|
||||
'cmd-macos': `curl -fsSL ${origin}/install.sh?token=TOKEN | sh`,
|
||||
'cmd-linux': `curl -fsSL ${origin}/install.sh?token=TOKEN | sh`,
|
||||
'cmd-windows': `irm ${origin}/install.ps1?token=TOKEN | iex`,
|
||||
'cmd-windows-legacy': `powershell -c "[Net.ServicePointManager]::SecurityProtocol=3072; (New-Object Net.WebClient).DownloadString('${origin}/install.ps1?token=TOKEN') | iex"`,
|
||||
'cmd-linux-x11vnc': 'x11vnc -localhost -rfbport 5900 -forever -shared -passwd YOURPASSWORD',
|
||||
'cmd-proxy': `curl -fsSL ${origin}/download/agent.js -o rcs-agent.js\n`
|
||||
+ `node rcs-agent.js enroll ${origin}/enroll/TOKEN --vnc-host 192.168.1.50 --name "old-box"\n`
|
||||
+ 'node rcs-agent.js run',
|
||||
};
|
||||
|
||||
for (const [id, text] of Object.entries(commands)) {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.textContent = text;
|
||||
}
|
||||
|
||||
document.querySelectorAll('[data-copy]').forEach((button) => {
|
||||
button.onclick = () => {
|
||||
navigator.clipboard.writeText(commands[button.dataset.copy]).then(() => {
|
||||
const old = button.textContent;
|
||||
button.textContent = 'Copied';
|
||||
setTimeout(() => { button.textContent = old; }, 1500);
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
const tabs = document.getElementById('os-tabs');
|
||||
tabs.onclick = (event) => {
|
||||
const button = event.target.closest('button[data-os]');
|
||||
if (!button) return;
|
||||
tabs.querySelectorAll('button').forEach((b) => b.classList.toggle('active', b === button));
|
||||
document.querySelectorAll('.os-panel').forEach((panel) => {
|
||||
panel.hidden = panel.dataset.os !== button.dataset.os;
|
||||
});
|
||||
};
|
||||
|
||||
// Land on the tab for whatever the reader is sitting at.
|
||||
const guess = /Mac/i.test(navigator.platform) ? 'macos'
|
||||
: /Win/i.test(navigator.platform) ? 'windows'
|
||||
: /Linux/i.test(navigator.platform) ? 'linux' : null;
|
||||
if (guess) tabs.querySelector(`button[data-os="${guess}"]`)?.click();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
+90
-30
@@ -36,47 +36,81 @@
|
||||
until you stop the agent.
|
||||
</p>
|
||||
|
||||
<ol class="steps">
|
||||
<nav class="tabs" id="os-tabs" style="margin:16px 0 4px">
|
||||
<button data-os="macos" class="active">macOS</button>
|
||||
<button data-os="windows">Windows</button>
|
||||
<button data-os="linux">Linux</button>
|
||||
</nav>
|
||||
|
||||
<ol class="steps os-panel" data-os="macos">
|
||||
<li>
|
||||
<strong>Make sure a VNC server is running</strong> on this machine, listening on
|
||||
<span class="mono">127.0.0.1:5900</span>.
|
||||
<strong>Turn on Screen Sharing</strong> — System Settings → General → Sharing.
|
||||
<div class="faint" style="margin-top:6px">
|
||||
macOS: System Settings → General → Sharing → Screen Sharing.<br>
|
||||
Windows: install TightVNC or UltraVNC.<br>
|
||||
Linux: <span class="mono">x11vnc -localhost -rfbport 5900</span>.
|
||||
Under Computer Settings, allow VNC viewers and set a password.
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Install <a href="https://nodejs.org" target="_blank" rel="noopener">Node.js 22+</a></strong> if it is not already there.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Run this</strong> in a terminal:
|
||||
<pre class="code" id="oneliner"></pre>
|
||||
<strong>Paste this into Terminal</strong>:
|
||||
<pre class="code" id="cmd-macos"></pre>
|
||||
<div class="row wrap" style="margin-top:10px">
|
||||
<button class="small" id="copy-unix">Copy for macOS / Linux</button>
|
||||
<button class="small" id="copy-win">Copy for Windows</button>
|
||||
<button class="small" data-copy="cmd-macos">Copy</button>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<div class="notice" style="margin-top:14px">
|
||||
<strong>On Windows 7 or Server 2008 R2?</strong>
|
||||
<ol class="steps os-panel" data-os="windows" hidden>
|
||||
<li>
|
||||
<strong>Install TightVNC or UltraVNC</strong>, set a password, and let it register as a
|
||||
service so it survives a reboot.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Paste this into PowerShell</strong>:
|
||||
<pre class="code" id="cmd-windows"></pre>
|
||||
<div class="row wrap" style="margin-top:10px">
|
||||
<button class="small" data-copy="cmd-windows">Copy</button>
|
||||
</div>
|
||||
<div class="faint" style="margin-top:8px">
|
||||
Nothing else to install — if Node.js is missing the script fetches a self-contained
|
||||
agent instead.
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<ol class="steps os-panel" data-os="linux" hidden>
|
||||
<li>
|
||||
<strong>Start a VNC server on the loopback</strong>:
|
||||
<pre class="code" id="cmd-linux-x11vnc"></pre>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Install <a href="https://nodejs.org" target="_blank" rel="noopener">Node.js 22+</a></strong> if it is not already there.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Paste this into a terminal</strong>:
|
||||
<pre class="code" id="cmd-linux"></pre>
|
||||
<div class="row wrap" style="margin-top:10px">
|
||||
<button class="small" data-copy="cmd-linux">Copy</button>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<div class="notice hidden" id="legacy-windows" style="margin-top:14px">
|
||||
<strong>Windows 7 or Server 2008 R2?</strong>
|
||||
<div class="faint" style="margin-top:6px">
|
||||
Node.js will not install on those — the last version that supported them is long
|
||||
out of date. Use the standalone build instead: one file, nothing to install.
|
||||
Those have PowerShell 2.0 and no TLS 1.2, so the command above fails. Run this from
|
||||
<span class="mono">cmd.exe</span> instead:
|
||||
</div>
|
||||
<pre class="code" id="cmd-windows-legacy"></pre>
|
||||
<div class="row wrap" style="margin-top:10px">
|
||||
<a class="small" href="/download/agent.exe" download="rcs-agent.exe">Download rcs-agent.exe</a>
|
||||
</div>
|
||||
<div class="faint" style="margin-top:10px">Then in a Command Prompt, in the folder you saved it to:</div>
|
||||
<pre class="code" id="exe-oneliner"></pre>
|
||||
<div class="row wrap" style="margin-top:10px">
|
||||
<button class="small" id="copy-exe">Copy</button>
|
||||
<button class="small" data-copy="cmd-windows-legacy">Copy</button>
|
||||
<a class="btn small" href="/download/agent.exe" download="rcs-agent.exe">Download the agent instead</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="login-foot">
|
||||
Link expires <span id="expiry"></span> · once enrolled, this machine appears in the operator console
|
||||
Link expires <span id="expiry"></span> · <a href="/docs" target="_blank" rel="noopener">full setup guide</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -88,10 +122,20 @@
|
||||
const origin = location.origin;
|
||||
const link = `${origin}/enroll/${token}`;
|
||||
|
||||
const unix = `curl -fsSL ${origin}/download/agent.js -o rcs-agent.js \\\n && node rcs-agent.js enroll ${link} \\\n && node rcs-agent.js run`;
|
||||
const win = `iwr ${origin}/download/agent.js -OutFile rcs-agent.js; `
|
||||
+ `node rcs-agent.js enroll ${link}; node rcs-agent.js run`;
|
||||
const exe = `rcs-agent.exe enroll ${link}\r\nrcs-agent.exe run`;
|
||||
// The hub stamps its own address and this token into the install scripts, so
|
||||
// each of these really is the whole command — nothing to fill in by hand.
|
||||
const commands = {
|
||||
'cmd-macos': `curl -fsSL ${origin}/install.sh?token=${token} | sh`,
|
||||
'cmd-linux': `curl -fsSL ${origin}/install.sh?token=${token} | sh`,
|
||||
'cmd-windows': `irm ${origin}/install.ps1?token=${token} | iex`,
|
||||
'cmd-windows-legacy': `powershell -c "[Net.ServicePointManager]::SecurityProtocol=3072; (New-Object Net.WebClient).DownloadString('${origin}/install.ps1?token=${token}') | iex"`,
|
||||
'cmd-linux-x11vnc': 'x11vnc -localhost -rfbport 5900 -forever -shared -passwd YOURPASSWORD',
|
||||
};
|
||||
|
||||
for (const [id, text] of Object.entries(commands)) {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.textContent = text;
|
||||
}
|
||||
|
||||
function copy(text, button) {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
@@ -101,11 +145,27 @@
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('oneliner').textContent = unix;
|
||||
document.getElementById('copy-unix').onclick = (e) => copy(unix, e.currentTarget);
|
||||
document.getElementById('copy-win').onclick = (e) => copy(win, e.currentTarget);
|
||||
document.getElementById('exe-oneliner').textContent = exe;
|
||||
document.getElementById('copy-exe').onclick = (e) => copy(exe, e.currentTarget);
|
||||
document.querySelectorAll('[data-copy]').forEach((button) => {
|
||||
button.onclick = () => copy(commands[button.dataset.copy], button);
|
||||
});
|
||||
|
||||
const tabs = document.getElementById('os-tabs');
|
||||
const legacyNotice = document.getElementById('legacy-windows');
|
||||
tabs.onclick = (event) => {
|
||||
const button = event.target.closest('button[data-os]');
|
||||
if (!button) return;
|
||||
tabs.querySelectorAll('button').forEach((b) => b.classList.toggle('active', b === button));
|
||||
document.querySelectorAll('.os-panel').forEach((panel) => {
|
||||
panel.hidden = panel.dataset.os !== button.dataset.os;
|
||||
});
|
||||
legacyNotice.classList.toggle('hidden', button.dataset.os !== 'windows');
|
||||
};
|
||||
|
||||
// Open on the tab for whatever this machine is — it is the machine being set up.
|
||||
const guess = /Mac/i.test(navigator.platform) ? 'macos'
|
||||
: /Win/i.test(navigator.platform) ? 'windows'
|
||||
: /Linux/i.test(navigator.platform) ? 'linux' : null;
|
||||
if (guess) tabs.querySelector(`button[data-os="${guess}"]`).click();
|
||||
|
||||
fetch(`/api/public/invite/${encodeURIComponent(token)}`)
|
||||
.then((r) => r.json().then((body) => ({ ok: r.ok, body })))
|
||||
|
||||
@@ -87,6 +87,7 @@
|
||||
</nav>
|
||||
|
||||
<div class="spacer"></div>
|
||||
<a class="btn ghost small" href="/docs" target="_blank" rel="noopener">Setup guide</a>
|
||||
<span class="faint" id="whoami"></span>
|
||||
<button class="ghost small" id="logout">Sign out</button>
|
||||
</header>
|
||||
|
||||
@@ -205,6 +205,36 @@ button.ghost.danger:hover:not(:disabled) { background: rgba(244,98,111,.12); }
|
||||
|
||||
button.small { padding: 5px 11px; font-size: 12.5px; border-radius: 6px; }
|
||||
|
||||
/* Links that have to read as controls: downloads, and anything that navigates
|
||||
rather than acts. Same surface as a button, minus the states a link cannot
|
||||
have. Keep the modifiers in sync with the button rules above. */
|
||||
a.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
font: 500 13.5px/1.2 var(--sans);
|
||||
letter-spacing: .005em;
|
||||
padding: 8px 14px;
|
||||
border-radius: var(--r-sm);
|
||||
border: 1px solid var(--line-strong);
|
||||
background: var(--surface-2);
|
||||
color: var(--text);
|
||||
white-space: nowrap;
|
||||
transition: border-color var(--t-fast), background var(--t-fast),
|
||||
color var(--t-fast), box-shadow var(--t-fast);
|
||||
}
|
||||
a.btn:hover { background: var(--surface-3); border-color: var(--line-hover); }
|
||||
a.btn.small { padding: 5px 11px; font-size: 12.5px; border-radius: 6px; }
|
||||
a.btn.ghost { background: transparent; border-color: transparent; color: var(--text-dim); }
|
||||
a.btn.ghost:hover { background: var(--surface-2); color: var(--text); }
|
||||
a.btn.primary {
|
||||
background: var(--mint);
|
||||
border-color: var(--mint);
|
||||
color: var(--mint-ink);
|
||||
font-weight: 650;
|
||||
}
|
||||
a.btn.primary:hover { background: var(--mint-bright); border-color: var(--mint-bright); }
|
||||
|
||||
/* ------------------------------------------------------------------ forms */
|
||||
|
||||
input, select, textarea {
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
# Remote Control Support - agent installer for Windows.
|
||||
#
|
||||
# Served from the hub, which substitutes HUB and TOKEN below before sending it:
|
||||
#
|
||||
# irm https://support.freakma.com/install.ps1?token=TOKEN | iex
|
||||
#
|
||||
# Windows 7 / Server 2008 R2 ship PowerShell 2.0, which has no Invoke-RestMethod
|
||||
# and no TLS 1.2, so those machines need the longer form instead:
|
||||
#
|
||||
# powershell -c "[Net.ServicePointManager]::SecurityProtocol=3072; (New-Object Net.WebClient).DownloadString('https://support.freakma.com/install.ps1?token=TOKEN') | iex"
|
||||
#
|
||||
# Everything is written under %LOCALAPPDATA%. No administrator rights needed.
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$Hub = '__HUB__'
|
||||
$Token = '__TOKEN__'
|
||||
|
||||
# .NET 3.5 has no Tls12 enum member, so the numeric value is the portable way.
|
||||
# Harmless on modern Windows, essential on 2008 R2 (which also needs KB3154518).
|
||||
try { [Net.ServicePointManager]::SecurityProtocol = 3072 } catch { }
|
||||
|
||||
function Write-Step { param($m) Write-Host "> $m" -ForegroundColor Cyan }
|
||||
function Write-Warn { param($m) Write-Host "! $m" -ForegroundColor Yellow }
|
||||
function Write-Die { param($m) Write-Host "x $m" -ForegroundColor Red; exit 1 }
|
||||
|
||||
if (-not $Token) {
|
||||
Write-Die "no enrolment token. Open the invite link on this machine to get the full command."
|
||||
}
|
||||
|
||||
$InstallDir = Join-Path $env:LOCALAPPDATA 'RemoteControlSupport'
|
||||
$Config = Join-Path $InstallDir 'config.json'
|
||||
$TaskName = 'RemoteControlSupportAgent'
|
||||
|
||||
if (-not (Test-Path $InstallDir)) { New-Item -ItemType Directory -Path $InstallDir | Out-Null }
|
||||
|
||||
$client = New-Object Net.WebClient
|
||||
|
||||
# ------------------------------------------------------------------ runtime
|
||||
|
||||
# Node 22 is preferred where it exists: smaller download, current runtime. Where
|
||||
# it does not - which includes every Windows 7 and Server 2008 R2 box, since
|
||||
# Node 14 dropped them - the self-contained executable carries its own Node 12.
|
||||
$NodeMajor = 0
|
||||
try { $NodeMajor = [int]((& node -p 'process.versions.node.split(".")[0]') 2>$null) } catch { }
|
||||
|
||||
if ($NodeMajor -ge 22) {
|
||||
Write-Step "using the Node.js $NodeMajor already installed here"
|
||||
$AgentPath = Join-Path $InstallDir 'agent.js'
|
||||
$client.DownloadFile("$Hub/download/agent.js", $AgentPath)
|
||||
$Exe = 'node'
|
||||
$Args = @($AgentPath)
|
||||
} else {
|
||||
Write-Step "no Node.js 22+ here - downloading the self-contained agent (about 30 MB)"
|
||||
$AgentPath = Join-Path $InstallDir 'rcs-agent.exe'
|
||||
$client.DownloadFile("$Hub/download/agent.exe", $AgentPath)
|
||||
$Exe = $AgentPath
|
||||
$Args = @()
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------- vnc
|
||||
|
||||
# Advisory: the agent enrols fine without it, but a session cannot start until
|
||||
# something is actually serving RFB on the loopback port.
|
||||
$vncUp = $false
|
||||
try {
|
||||
$probe = New-Object Net.Sockets.TcpClient
|
||||
$probe.Connect('127.0.0.1', 5900)
|
||||
$vncUp = $probe.Connected
|
||||
$probe.Close()
|
||||
} catch { }
|
||||
if (-not $vncUp) {
|
||||
Write-Warn "nothing is listening on 127.0.0.1:5900 - install TightVNC or UltraVNC and let it start on boot"
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------ enrol
|
||||
|
||||
Write-Step "enrolling with $Hub"
|
||||
& $Exe @($Args + @('enroll', "$Hub/enroll/$Token", '--config', $Config))
|
||||
if ($LASTEXITCODE -ne 0) { Write-Die "enrolment failed - the link may have expired" }
|
||||
|
||||
# --------------------------------------------------------------- scheduled
|
||||
|
||||
# A Windows *service* runs in session 0 and cannot draw on the interactive
|
||||
# desktop, so the "ask first" consent dialog would never appear. A logon task
|
||||
# runs in the console session, where it can. schtasks.exe is used rather than
|
||||
# Register-ScheduledTask because PowerShell 2.0 does not have that cmdlet.
|
||||
Write-Step "registering a logon task so it starts with Windows"
|
||||
|
||||
if ($Args.Count -gt 0) {
|
||||
$run = '"' + $Exe + '" "' + $Args[0] + '" run --config "' + $Config + '"'
|
||||
} else {
|
||||
$run = '"' + $Exe + '" run --config "' + $Config + '"'
|
||||
}
|
||||
|
||||
& schtasks.exe /create /tn $TaskName /tr $run /sc onlogon /f | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Warn "could not register the logon task - start the agent by hand with: $run"
|
||||
} else {
|
||||
& schtasks.exe /run /tn $TaskName | Out-Null
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Done. This machine is registered and should now appear in the console."
|
||||
Write-Host "Installed in: $InstallDir"
|
||||
Write-Host "Remove with: schtasks /delete /tn $TaskName /f"
|
||||
@@ -0,0 +1,167 @@
|
||||
#!/bin/sh
|
||||
# Remote Control Support — agent installer for macOS and Linux.
|
||||
#
|
||||
# Served from the hub, which substitutes HUB and TOKEN below before sending it,
|
||||
# so the published one-liner carries the enrolment token in its URL:
|
||||
#
|
||||
# curl -fsSL https://support.freakma.com/install.sh?token=TOKEN | sh
|
||||
#
|
||||
# Fetched without a token it still works, taking one as an argument instead:
|
||||
#
|
||||
# curl -fsSL https://support.freakma.com/install.sh | sh -s -- TOKEN
|
||||
#
|
||||
# Everything lands under ~/.rcs-agent. Nothing here needs root.
|
||||
|
||||
set -eu
|
||||
|
||||
HUB="__HUB__"
|
||||
TOKEN="__TOKEN__"
|
||||
|
||||
# Written long-hand rather than `[ $# -gt 0 ] && TOKEN="$1"`: under `set -e` the
|
||||
# short-circuit form leaves the script's exit status at 1 when no argument was
|
||||
# given, which is the normal case for the piped one-liner.
|
||||
if [ $# -gt 0 ]; then TOKEN="$1"; fi
|
||||
|
||||
INSTALL_DIR="$HOME/.rcs-agent"
|
||||
AGENT="$INSTALL_DIR/agent.js"
|
||||
CONFIG="$INSTALL_DIR/config.json"
|
||||
SERVICE_NAME="rcs-agent"
|
||||
|
||||
say() { printf '%s\n' "$*"; }
|
||||
warn() { printf '\033[33m!\033[0m %s\n' "$*" >&2; }
|
||||
die() { printf '\033[31mx\033[0m %s\n' "$*" >&2; exit 1; }
|
||||
step() { printf '\033[36m>\033[0m %s\n' "$*"; }
|
||||
|
||||
[ -n "$TOKEN" ] || die "no enrolment token. Open the invite link on this machine, or pass one:
|
||||
curl -fsSL $HUB/install.sh | sh -s -- YOUR_TOKEN"
|
||||
|
||||
case "$(uname -s)" in
|
||||
Darwin) PLATFORM=macos ;;
|
||||
Linux) PLATFORM=linux ;;
|
||||
*) die "$(uname -s) is not supported by this script — see $HUB/docs" ;;
|
||||
esac
|
||||
|
||||
# ------------------------------------------------------------------ node
|
||||
|
||||
# The agent uses the global WebSocket, which is only on by default from Node 22.
|
||||
command -v node >/dev/null 2>&1 || die "Node.js 22 or newer is required and was not found.
|
||||
macOS: brew install node (or https://nodejs.org)
|
||||
Debian: curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - && sudo apt install -y nodejs
|
||||
Fedora: sudo dnf install nodejs"
|
||||
|
||||
NODE_MAJOR=$(node -p 'process.versions.node.split(".")[0]' 2>/dev/null || echo 0)
|
||||
[ "$NODE_MAJOR" -ge 22 ] 2>/dev/null || die "Node.js 22 or newer is required — this machine has $(node -v).
|
||||
Upgrade it, or run the agent on another machine on this LAN and point it here:
|
||||
node agent.js enroll <link> --vnc-host $(hostname)"
|
||||
|
||||
# ------------------------------------------------------------------- vnc
|
||||
|
||||
# Advisory only: the VNC server can be started after enrolment, and on a fresh
|
||||
# macOS the Screen Sharing toggle is usually the last thing someone does.
|
||||
vnc_listening() {
|
||||
if command -v nc >/dev/null 2>&1; then nc -z -w 2 127.0.0.1 5900 >/dev/null 2>&1; return $?; fi
|
||||
return 1
|
||||
}
|
||||
|
||||
if ! vnc_listening; then
|
||||
case "$PLATFORM" in
|
||||
macos) warn "nothing is listening on 127.0.0.1:5900 — turn on System Settings > General > Sharing > Screen Sharing" ;;
|
||||
linux) warn "nothing is listening on 127.0.0.1:5900 — start one, e.g. x11vnc -localhost -rfbport 5900" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# --------------------------------------------------------------- install
|
||||
|
||||
step "installing to $INSTALL_DIR"
|
||||
mkdir -p "$INSTALL_DIR"
|
||||
curl -fsSL "$HUB/download/agent.js" -o "$AGENT" || die "could not download the agent from $HUB"
|
||||
|
||||
step "enrolling with $HUB"
|
||||
node "$AGENT" enroll "$HUB/enroll/$TOKEN" --config "$CONFIG"
|
||||
|
||||
# --------------------------------------------------------------- service
|
||||
|
||||
NODE_BIN=$(command -v node)
|
||||
|
||||
install_launchd() {
|
||||
PLIST="$HOME/Library/LaunchAgents/com.freakma.rcs-agent.plist"
|
||||
mkdir -p "$HOME/Library/LaunchAgents"
|
||||
cat > "$PLIST" <<PLIST_EOF
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key><string>com.freakma.rcs-agent</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>$NODE_BIN</string>
|
||||
<string>$AGENT</string>
|
||||
<string>run</string>
|
||||
<string>--config</string>
|
||||
<string>$CONFIG</string>
|
||||
</array>
|
||||
<key>RunAtLoad</key><true/>
|
||||
<key>KeepAlive</key><true/>
|
||||
<key>StandardOutPath</key><string>$INSTALL_DIR/agent.log</string>
|
||||
<key>StandardErrorPath</key><string>$INSTALL_DIR/agent.log</string>
|
||||
</dict>
|
||||
</plist>
|
||||
PLIST_EOF
|
||||
launchctl unload "$PLIST" >/dev/null 2>&1 || true
|
||||
launchctl load "$PLIST"
|
||||
say ""
|
||||
say "Running as a launchd agent. Useful commands:"
|
||||
say " launchctl unload $PLIST # stop"
|
||||
say " launchctl load $PLIST # start"
|
||||
say " tail -f $INSTALL_DIR/agent.log"
|
||||
}
|
||||
|
||||
install_systemd_user() {
|
||||
UNIT_DIR="$HOME/.config/systemd/user"
|
||||
mkdir -p "$UNIT_DIR"
|
||||
cat > "$UNIT_DIR/$SERVICE_NAME.service" <<UNIT_EOF
|
||||
[Unit]
|
||||
Description=Remote Control Support agent
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
ExecStart=$NODE_BIN $AGENT run --config $CONFIG
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
UNIT_EOF
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user enable --now "$SERVICE_NAME"
|
||||
say ""
|
||||
say "Running as a systemd user service. Useful commands:"
|
||||
say " systemctl --user status $SERVICE_NAME"
|
||||
say " journalctl --user -u $SERVICE_NAME -f"
|
||||
# Without lingering the service dies at logout, which is exactly when remote
|
||||
# support is most likely to be needed.
|
||||
loginctl enable-linger "$(id -un)" >/dev/null 2>&1 \
|
||||
|| warn "run 'sudo loginctl enable-linger $(id -un)' so the agent survives logout"
|
||||
}
|
||||
|
||||
install_manual() {
|
||||
say ""
|
||||
say "No supported service manager found. Start the agent by hand:"
|
||||
say " node $AGENT run --config $CONFIG"
|
||||
}
|
||||
|
||||
step "setting it to start automatically"
|
||||
case "$PLATFORM" in
|
||||
macos) install_launchd ;;
|
||||
linux)
|
||||
if command -v systemctl >/dev/null 2>&1 && systemctl --user show-environment >/dev/null 2>&1; then
|
||||
install_systemd_user
|
||||
else
|
||||
install_manual
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
say ""
|
||||
say "Done. This machine is registered and should now appear in the console."
|
||||
say "Config: $CONFIG"
|
||||
@@ -48,4 +48,17 @@ const config = {
|
||||
logLevel: process.env.LOG_LEVEL || 'info',
|
||||
};
|
||||
|
||||
/**
|
||||
* Public base URL for links this hub hands out — invite URLs, and the hub
|
||||
* address baked into the install scripts. `PUBLIC_URL` wins; without it the
|
||||
* request's own host is used, which is right behind a proxy that sets
|
||||
* `X-Forwarded-Proto` and wrong when someone hits the container directly.
|
||||
*/
|
||||
function baseUrl(req) {
|
||||
if (config.publicUrl) return config.publicUrl;
|
||||
const proto = req.headers['x-forwarded-proto'] || req.protocol;
|
||||
return `${proto}://${req.get('host')}`;
|
||||
}
|
||||
|
||||
module.exports = config;
|
||||
module.exports.baseUrl = baseUrl;
|
||||
|
||||
@@ -82,10 +82,46 @@ app.get('/download/agent.exe', (_req, res) => {
|
||||
res.download(AGENT_EXE, 'rcs-agent.exe');
|
||||
});
|
||||
|
||||
/* ------------------------------------------------------ install scripts */
|
||||
|
||||
// One-command registration: the hub stamps its own address and the enrolment
|
||||
// token into the script before serving it, so the whole thing is a single
|
||||
// copy-paste with nothing to fill in.
|
||||
//
|
||||
// curl -fsSL https://host/install.sh?token=TOKEN | sh
|
||||
// irm https://host/install.ps1?token=TOKEN | iex
|
||||
const SCRIPTS_DIR = path.join(__dirname, '..', 'scripts');
|
||||
|
||||
// Tokens are base64url (see randomToken). This output is piped straight into a
|
||||
// shell, so anything that is not shaped like a token is refused rather than
|
||||
// interpolated — a token is the only untrusted value in these files.
|
||||
const TOKEN_PATTERN = /^[A-Za-z0-9_-]{16,128}$/;
|
||||
|
||||
function serveInstallScript(file, contentType) {
|
||||
return (req, res) => {
|
||||
const token = String(req.query.token || '');
|
||||
if (token && !TOKEN_PATTERN.test(token)) {
|
||||
return res.status(400).type('text/plain').send('that is not a valid enrolment token');
|
||||
}
|
||||
let body;
|
||||
try {
|
||||
body = fs.readFileSync(path.join(SCRIPTS_DIR, file), 'utf8');
|
||||
} catch {
|
||||
return res.status(500).type('text/plain').send('install script missing from this deployment');
|
||||
}
|
||||
body = body.split('__HUB__').join(config.baseUrl(req)).split('__TOKEN__').join(token);
|
||||
res.type(contentType).send(body);
|
||||
};
|
||||
}
|
||||
|
||||
app.get('/install.sh', serveInstallScript('install.sh', 'text/x-shellscript; charset=utf-8'));
|
||||
app.get('/install.ps1', serveInstallScript('install.ps1', 'text/plain; charset=utf-8'));
|
||||
|
||||
app.use('/novnc', express.static(NOVNC_DIR, { maxAge: '7d', immutable: true }));
|
||||
app.use(express.static(PUBLIC_DIR));
|
||||
|
||||
app.get('/viewer', (_req, res) => res.sendFile(path.join(PUBLIC_DIR, 'viewer.html')));
|
||||
app.get('/docs', (_req, res) => res.sendFile(path.join(PUBLIC_DIR, 'docs.html')));
|
||||
app.get('/enroll/:token', (_req, res) => res.sendFile(path.join(PUBLIC_DIR, 'enroll.html')));
|
||||
app.get('/s/:token', (_req, res) => res.sendFile(path.join(PUBLIC_DIR, 'share.html')));
|
||||
|
||||
|
||||
@@ -8,11 +8,7 @@ const { requireAuth, requireAdmin } = require('../auth');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
function baseUrl(req) {
|
||||
if (config.publicUrl) return config.publicUrl;
|
||||
const proto = req.headers['x-forwarded-proto'] || req.protocol;
|
||||
return `${proto}://${req.get('host')}`;
|
||||
}
|
||||
const { baseUrl } = config;
|
||||
|
||||
function inviteLink(req, kind, token) {
|
||||
return `${baseUrl(req)}/${kind === 'enroll' ? 'enroll' : 's'}/${token}`;
|
||||
|
||||
Reference in New Issue
Block a user