The setup pages build commands that get pasted into a terminal on a *different* machine, but they built them from location.origin. Open the console by LAN IP or over a tunnel and the enrolment one-liner told the target machine to curl an address it may not resolve at all. /docs and /enroll/:token are now served through a template that stamps in config.baseUrl — PUBLIC_URL where set, the proxied request host otherwise — with location.origin left as a fallback for the unsubstituted file. The remaining 127.0.0.1 references are the loopback VNC port on the machine being registered, which is meant to be literal. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
199 lines
7.8 KiB
HTML
199 lines
7.8 KiB
HTML
<!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>Set up remote support</title>
|
|
<link rel="stylesheet" href="/styles.css">
|
|
</head>
|
|
<body>
|
|
|
|
<div class="bg-field" id="bg-field" aria-hidden="true"></div>
|
|
|
|
<div class="center-shell">
|
|
<div class="center-card">
|
|
|
|
<div class="card-header">
|
|
<div class="brand-mark">RC</div>
|
|
<div>
|
|
<strong style="font-size:14px">Remote Support</strong>
|
|
<span class="brand-sub">Machine enrollment</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card-body">
|
|
<span class="card-kicker">One-time setup</span>
|
|
<h1>Set up remote support</h1>
|
|
|
|
<div id="state-loading" class="faint">Checking this link…</div>
|
|
|
|
<div id="state-invalid" class="notice error hidden"></div>
|
|
|
|
<div id="state-ok" class="hidden">
|
|
<p class="faint">
|
|
This registers this computer so it can be supported remotely. It stays connected
|
|
until you stop the agent.
|
|
</p>
|
|
|
|
<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>Turn on Screen Sharing</strong> — System Settings → General → Sharing.
|
|
<div class="faint" style="margin-top:6px">
|
|
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>Paste this into Terminal</strong>:
|
|
<pre class="code" id="cmd-macos"></pre>
|
|
<div class="row wrap" style="margin-top:10px">
|
|
<button class="small" data-copy="cmd-macos">Copy</button>
|
|
</div>
|
|
</li>
|
|
</ol>
|
|
|
|
<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">
|
|
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">
|
|
<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> · <a href="/docs" target="_blank" rel="noopener">full setup guide</a>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const token = location.pathname.split('/').filter(Boolean).pop();
|
|
|
|
// Substituted by the server with this hub's public address — the commands below
|
|
// are pasted into a terminal on this machine and have to reach the hub by a name
|
|
// it can resolve, not by whatever host the reader used to open this page.
|
|
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) {
|
|
navigator.clipboard.writeText(text).then(() => {
|
|
const old = button.textContent;
|
|
button.textContent = 'Copied';
|
|
setTimeout(() => { button.textContent = old; }, 1500);
|
|
});
|
|
}
|
|
|
|
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 })))
|
|
.then(({ ok, body }) => {
|
|
document.getElementById('state-loading').classList.add('hidden');
|
|
if (!ok || !body.usable || body.kind !== 'enroll') {
|
|
const box = document.getElementById('state-invalid');
|
|
box.textContent = body.error || `This enrollment link is ${body.reason || 'not valid'}.`;
|
|
box.classList.remove('hidden');
|
|
return;
|
|
}
|
|
const expiry = document.getElementById('expiry');
|
|
expiry.textContent = body.expiresAt ? new Date(body.expiresAt).toLocaleString() : 'never';
|
|
document.getElementById('state-ok').classList.remove('hidden');
|
|
})
|
|
.catch(() => {
|
|
document.getElementById('state-loading').textContent = 'Could not reach the server.';
|
|
});
|
|
</script>
|
|
|
|
<script type="module">
|
|
import { mountParticles } from '/particles.js';
|
|
mountParticles(document.getElementById('bg-field'));
|
|
</script>
|
|
</body>
|
|
</html>
|