Compare commits
3
Commits
cecaf74a0a
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6895167964 | ||
|
|
1518211a17 | ||
|
|
cfda6b19b7 |
@@ -28,6 +28,7 @@ RUN pnpm install --prod --frozen-lockfile
|
|||||||
COPY server ./server
|
COPY server ./server
|
||||||
COPY public ./public
|
COPY public ./public
|
||||||
COPY agent ./agent
|
COPY agent ./agent
|
||||||
|
COPY scripts ./scripts
|
||||||
COPY --from=agent-exe /build/dist/rcs-agent.exe ./dist/rcs-agent.exe
|
COPY --from=agent-exe /build/dist/rcs-agent.exe ./dist/rcs-agent.exe
|
||||||
|
|
||||||
VOLUME ["/data"]
|
VOLUME ["/data"]
|
||||||
|
|||||||
@@ -90,15 +90,61 @@ LAN as the hub.
|
|||||||
|
|
||||||
### Agent (the machine dials out)
|
### Agent (the machine dials out)
|
||||||
|
|
||||||
**Invite a machine** produces a link. Open it on the target machine and it shows a
|
**Invite a machine** produces a link. Open it on the target machine and it shows the
|
||||||
one-liner:
|
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
|
```bash
|
||||||
curl -fsSL https://your-hub/download/agent.js -o rcs-agent.js \
|
curl -fsSL https://support.freakma.com/install.sh?token=TOKEN | sh
|
||||||
&& node rcs-agent.js enroll https://your-hub/enroll/TOKEN \
|
|
||||||
&& node rcs-agent.js run
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**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`:
|
The agent needs Node 22+ and a VNC server listening on `127.0.0.1:5900`:
|
||||||
|
|
||||||
| OS | VNC server |
|
| OS | VNC server |
|
||||||
@@ -233,6 +279,9 @@ Everything is environment variables — see [.env.example](.env.example).
|
|||||||
| `POST` | `/api/sessions/:id/kill` | admin |
|
| `POST` | `/api/sessions/:id/kill` | admin |
|
||||||
| `GET` | `/api/sessions/history` | |
|
| `GET` | `/api/sessions/history` | |
|
||||||
| `GET` | `/api/sessions/audit` | admin |
|
| `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/enroll` | no auth, enrolment token |
|
||||||
| `POST` | `/api/public/session/:token` | no auth, support link |
|
| `POST` | `/api/public/session/:token` | no auth, support link |
|
||||||
| WS | `/ws/vnc?ticket=` | browser session |
|
| WS | `/ws/vnc?ticket=` | browser session |
|
||||||
|
|||||||
@@ -0,0 +1,346 @@
|
|||||||
|
<!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; }
|
||||||
|
/* The button floats over the block, so keep the long one-liners from
|
||||||
|
scrolling underneath it. */
|
||||||
|
.copy-wrap pre.code { padding-right: 76px; }
|
||||||
|
</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>
|
||||||
|
// Substituted by the server with this hub's public address. These commands run
|
||||||
|
// on a different machine, so location.origin would be wrong whenever the reader
|
||||||
|
// reached this page by IP or over a tunnel. Falls back only if served unmodified.
|
||||||
|
const stamped = '__HUB__';
|
||||||
|
const origin = stamped.indexOf('__') === 0 ? location.origin : stamped;
|
||||||
|
|
||||||
|
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>
|
||||||
+96
-32
@@ -36,47 +36,81 @@
|
|||||||
until you stop the agent.
|
until you stop the agent.
|
||||||
</p>
|
</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>
|
<li>
|
||||||
<strong>Make sure a VNC server is running</strong> on this machine, listening on
|
<strong>Turn on Screen Sharing</strong> — System Settings → General → Sharing.
|
||||||
<span class="mono">127.0.0.1:5900</span>.
|
|
||||||
<div class="faint" style="margin-top:6px">
|
<div class="faint" style="margin-top:6px">
|
||||||
macOS: System Settings → General → Sharing → Screen Sharing.<br>
|
Under Computer Settings, allow VNC viewers and set a password.
|
||||||
Windows: install TightVNC or UltraVNC.<br>
|
|
||||||
Linux: <span class="mono">x11vnc -localhost -rfbport 5900</span>.
|
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<strong>Install <a href="https://nodejs.org" target="_blank" rel="noopener">Node.js 22+</a></strong> if it is not already there.
|
<strong>Install <a href="https://nodejs.org" target="_blank" rel="noopener">Node.js 22+</a></strong> if it is not already there.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<strong>Run this</strong> in a terminal:
|
<strong>Paste this into Terminal</strong>:
|
||||||
<pre class="code" id="oneliner"></pre>
|
<pre class="code" id="cmd-macos"></pre>
|
||||||
<div class="row wrap" style="margin-top:10px">
|
<div class="row wrap" style="margin-top:10px">
|
||||||
<button class="small" id="copy-unix">Copy for macOS / Linux</button>
|
<button class="small" data-copy="cmd-macos">Copy</button>
|
||||||
<button class="small" id="copy-win">Copy for Windows</button>
|
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
|
|
||||||
<div class="notice" style="margin-top:14px">
|
<ol class="steps os-panel" data-os="windows" hidden>
|
||||||
<strong>On Windows 7 or Server 2008 R2?</strong>
|
<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">
|
<div class="faint" style="margin-top:6px">
|
||||||
Node.js will not install on those — the last version that supported them is long
|
Those have PowerShell 2.0 and no TLS 1.2, so the command above fails. Run this from
|
||||||
out of date. Use the standalone build instead: one file, nothing to install.
|
<span class="mono">cmd.exe</span> instead:
|
||||||
</div>
|
</div>
|
||||||
|
<pre class="code" id="cmd-windows-legacy"></pre>
|
||||||
<div class="row wrap" style="margin-top:10px">
|
<div class="row wrap" style="margin-top:10px">
|
||||||
<a class="small" href="/download/agent.exe" download="rcs-agent.exe">Download rcs-agent.exe</a>
|
<button class="small" data-copy="cmd-windows-legacy">Copy</button>
|
||||||
</div>
|
<a class="btn small" href="/download/agent.exe" download="rcs-agent.exe">Download the agent instead</a>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="login-foot">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -85,13 +119,27 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
const token = location.pathname.split('/').filter(Boolean).pop();
|
const token = location.pathname.split('/').filter(Boolean).pop();
|
||||||
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`;
|
// Substituted by the server with this hub's public address — the commands below
|
||||||
const win = `iwr ${origin}/download/agent.js -OutFile rcs-agent.js; `
|
// are pasted into a terminal on this machine and have to reach the hub by a name
|
||||||
+ `node rcs-agent.js enroll ${link}; node rcs-agent.js run`;
|
// it can resolve, not by whatever host the reader used to open this page.
|
||||||
const exe = `rcs-agent.exe enroll ${link}\r\nrcs-agent.exe run`;
|
const stamped = '__HUB__';
|
||||||
|
const origin = stamped.indexOf('__') === 0 ? location.origin : stamped;
|
||||||
|
|
||||||
|
// 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) {
|
function copy(text, button) {
|
||||||
navigator.clipboard.writeText(text).then(() => {
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
@@ -101,11 +149,27 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('oneliner').textContent = unix;
|
document.querySelectorAll('[data-copy]').forEach((button) => {
|
||||||
document.getElementById('copy-unix').onclick = (e) => copy(unix, e.currentTarget);
|
button.onclick = () => copy(commands[button.dataset.copy], button);
|
||||||
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);
|
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)}`)
|
fetch(`/api/public/invite/${encodeURIComponent(token)}`)
|
||||||
.then((r) => r.json().then((body) => ({ ok: r.ok, body })))
|
.then((r) => r.json().then((body) => ({ ok: r.ok, body })))
|
||||||
|
|||||||
@@ -87,6 +87,7 @@
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="spacer"></div>
|
<div class="spacer"></div>
|
||||||
|
<a class="btn ghost small" href="/docs" target="_blank" rel="noopener">Setup guide</a>
|
||||||
<span class="faint" id="whoami"></span>
|
<span class="faint" id="whoami"></span>
|
||||||
<button class="ghost small" id="logout">Sign out</button>
|
<button class="ghost small" id="logout">Sign out</button>
|
||||||
</header>
|
</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; }
|
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 */
|
/* ------------------------------------------------------------------ forms */
|
||||||
|
|
||||||
input, select, textarea {
|
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',
|
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 = config;
|
||||||
|
module.exports.baseUrl = baseUrl;
|
||||||
|
|||||||
+53
-1
@@ -82,11 +82,63 @@ app.get('/download/agent.exe', (_req, res) => {
|
|||||||
res.download(AGENT_EXE, 'rcs-agent.exe');
|
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('/novnc', express.static(NOVNC_DIR, { maxAge: '7d', immutable: true }));
|
||||||
app.use(express.static(PUBLIC_DIR));
|
app.use(express.static(PUBLIC_DIR));
|
||||||
|
|
||||||
|
// The setup pages print commands that get run on *another* machine, so they must
|
||||||
|
// name this hub's public address — not whatever host the reader happens to have
|
||||||
|
// typed. Reached over the LAN IP, `location.origin` would hand out a URL the
|
||||||
|
// target machine may not be able to resolve at all.
|
||||||
|
function sendPageWithHub(file) {
|
||||||
|
return (req, res) => {
|
||||||
|
let html;
|
||||||
|
try {
|
||||||
|
html = fs.readFileSync(path.join(PUBLIC_DIR, file), 'utf8');
|
||||||
|
} catch {
|
||||||
|
return res.status(500).type('text/plain').send(`${file} missing from this deployment`);
|
||||||
|
}
|
||||||
|
res.type('html').send(html.split('__HUB__').join(config.baseUrl(req)));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
app.get('/viewer', (_req, res) => res.sendFile(path.join(PUBLIC_DIR, 'viewer.html')));
|
app.get('/viewer', (_req, res) => res.sendFile(path.join(PUBLIC_DIR, 'viewer.html')));
|
||||||
app.get('/enroll/:token', (_req, res) => res.sendFile(path.join(PUBLIC_DIR, 'enroll.html')));
|
app.get('/docs', sendPageWithHub('docs.html'));
|
||||||
|
app.get('/enroll/:token', sendPageWithHub('enroll.html'));
|
||||||
app.get('/s/:token', (_req, res) => res.sendFile(path.join(PUBLIC_DIR, 'share.html')));
|
app.get('/s/:token', (_req, res) => res.sendFile(path.join(PUBLIC_DIR, 'share.html')));
|
||||||
|
|
||||||
app.use((req, res) => {
|
app.use((req, res) => {
|
||||||
|
|||||||
@@ -8,11 +8,7 @@ const { requireAuth, requireAdmin } = require('../auth');
|
|||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
function baseUrl(req) {
|
const { baseUrl } = config;
|
||||||
if (config.publicUrl) return config.publicUrl;
|
|
||||||
const proto = req.headers['x-forwarded-proto'] || req.protocol;
|
|
||||||
return `${proto}://${req.get('host')}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function inviteLink(req, kind, token) {
|
function inviteLink(req, kind, token) {
|
||||||
return `${baseUrl(req)}/${kind === 'enroll' ? 'enroll' : 's'}/${token}`;
|
return `${baseUrl(req)}/${kind === 'enroll' ? 'enroll' : 's'}/${token}`;
|
||||||
|
|||||||
Reference in New Issue
Block a user