feat(agent): ship a Node 12 executable for Windows Server 2008 R2
Machines that cannot install Node 22 had no way to run the agent at all, which
left them stuck on direct mode — and direct mode only works when the hub can
route to the VNC port, which it often cannot.
agent.js now resolves the two Node 22 globals it uses through fallbacks: `ws`
for the control and tunnel sockets, and http/https for the single enrolment
POST. Node 22 loads neither, since `globalThis.WebSocket || require('ws')`
short-circuits. The require and the http/https references are static so the
bundler can follow them.
A new Docker stage bundles that file with a Node 12 runtime — the last line
supporting Windows 7 and Server 2008 R2 — into one self-contained .exe, served
from /download/agent.exe and linked from the enrolment page. The route answers
503 rather than 404 in a dev checkout, where the build has not run.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+59
-12
@@ -10,13 +10,23 @@
|
||||
//
|
||||
// Deliberately dependency-free: Node 22 ships a global WebSocket, so this file
|
||||
// can be copied onto a machine and run with nothing but `node`.
|
||||
//
|
||||
// It also has to run on machines that cannot have Node 22 — Windows Server
|
||||
// 2008 R2 tops out at Node 13 — so the two modern globals it leans on are
|
||||
// resolved through fallbacks rather than used directly. On Node 22 nothing
|
||||
// extra is loaded; on an old runtime it needs the `ws` package alongside it.
|
||||
|
||||
const fs = require('fs');
|
||||
const http = require('http');
|
||||
const https = require('https');
|
||||
const net = require('net');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const { execFile } = require('child_process');
|
||||
|
||||
// Short-circuits before `require`, so Node 22 never looks for `ws` at all.
|
||||
const WebSocket = globalThis.WebSocket || require('ws');
|
||||
|
||||
const VERSION = '0.1.0';
|
||||
const DEFAULT_CONFIG = process.env.RCS_AGENT_CONFIG
|
||||
|| path.join(os.homedir(), '.rcs-agent.json');
|
||||
@@ -39,6 +49,47 @@ function writeConfig(cfg, file = DEFAULT_CONFIG) {
|
||||
fs.writeFileSync(file, JSON.stringify(cfg, null, 2) + '\n', { mode: 0o600 });
|
||||
}
|
||||
|
||||
/**
|
||||
* POSTs JSON and reads JSON back, the one HTTP call this agent makes. Uses
|
||||
* global `fetch` where it exists (Node 18+) and falls back to the http modules
|
||||
* otherwise. Resolves to `{ ok, status, body }` either way; a non-JSON or empty
|
||||
* response yields `body = {}` so callers can read `body.error` unguarded.
|
||||
*/
|
||||
function postJson(url, payload) {
|
||||
const data = Buffer.from(JSON.stringify(payload), 'utf8');
|
||||
const headers = { 'Content-Type': 'application/json' };
|
||||
|
||||
if (typeof fetch === 'function') {
|
||||
return fetch(url, { method: 'POST', headers, body: data })
|
||||
.then(async (res) => ({
|
||||
ok: res.ok,
|
||||
status: res.status,
|
||||
body: await res.json().catch(() => ({})),
|
||||
}));
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const target = new URL(url);
|
||||
// Statically referenced, not `require(expr)` — the bundler that builds the
|
||||
// legacy Windows executable can only follow literal requires.
|
||||
const lib = target.protocol === 'https:' ? https : http;
|
||||
const req = lib.request(target, {
|
||||
method: 'POST',
|
||||
headers: { ...headers, 'Content-Length': data.length },
|
||||
}, (res) => {
|
||||
const chunks = [];
|
||||
res.on('data', (chunk) => chunks.push(chunk));
|
||||
res.on('end', () => {
|
||||
let body = {};
|
||||
try { body = JSON.parse(Buffer.concat(chunks).toString('utf8')); } catch { /* not JSON */ }
|
||||
resolve({ ok: res.statusCode >= 200 && res.statusCode < 300, status: res.statusCode, body });
|
||||
});
|
||||
});
|
||||
req.on('error', reject);
|
||||
req.end(data);
|
||||
});
|
||||
}
|
||||
|
||||
/** Accepts either a full enrolment URL or a bare token. */
|
||||
function parseInvite(input) {
|
||||
const raw = String(input || '').trim();
|
||||
@@ -131,20 +182,16 @@ async function cmdEnroll(args) {
|
||||
const vncPort = Number(args['vnc-port'] || process.env.RCS_VNC_PORT || 5900);
|
||||
const vncHost = String(args['vnc-host'] || process.env.RCS_VNC_HOST || '127.0.0.1');
|
||||
|
||||
const res = await fetch(`${hub}/api/public/enroll`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
token,
|
||||
hostname: os.hostname(),
|
||||
os: `${os.type()} ${os.release()} (${os.arch()})`,
|
||||
agentVersion: VERSION,
|
||||
vncPort,
|
||||
name: args.name || undefined,
|
||||
}),
|
||||
const res = await postJson(`${hub}/api/public/enroll`, {
|
||||
token,
|
||||
hostname: os.hostname(),
|
||||
os: `${os.type()} ${os.release()} (${os.arch()})`,
|
||||
agentVersion: VERSION,
|
||||
vncPort,
|
||||
name: args.name || undefined,
|
||||
});
|
||||
|
||||
const body = await res.json().catch(() => ({}));
|
||||
const body = res.body;
|
||||
if (!res.ok) throw new Error(body.error || `enrolment failed (HTTP ${res.status})`);
|
||||
|
||||
const cfg = {
|
||||
|
||||
Reference in New Issue
Block a user