mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-19 01:26:29 +03:00
fix: authenticate the live-server liveness probe
greptile-apps[bot] escalated the identity ladder to a pid AND port both coincidentally reused by different processes. The definitive terminator was available all along: the helper serves an authenticated endpoint and server.json records the token, so the probe now requires a 200 from /status?token=... over HTTP. Nothing but our helper can answer that, which closes the entire misidentification class rather than the next rung. The regression test hosts its responder in a child process (the probe is execFileSync, so a same-process responder can never accept while the parent's event loop is blocked; production helpers are always separate processes). This work was produced with AI assistance (Claude Code). Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Code
parent
9a3f5aa34b
commit
16a84bc390
@@ -300,35 +300,37 @@ function readPointerEntries(repoRoot) {
|
||||
function hasLiveServer(appRoot) {
|
||||
let pid;
|
||||
let port;
|
||||
let token;
|
||||
try {
|
||||
const info = JSON.parse(fs.readFileSync(path.join(appRoot, '.impeccable', 'live', 'server.json'), 'utf-8'));
|
||||
if (!info || typeof info.pid !== 'number') return false;
|
||||
pid = info.pid;
|
||||
port = Number(info.port);
|
||||
token = typeof info.token === 'string' ? info.token : null;
|
||||
process.kill(pid, 0);
|
||||
} catch (err) {
|
||||
// EPERM: the process exists but is not signalable by this user.
|
||||
if (err?.code !== 'EPERM') return false;
|
||||
}
|
||||
// Liveness alone misclassifies a REUSED pid (helper died without removing
|
||||
// server.json, the OS handed the pid to something else, even another node
|
||||
// process). The decisive signal is the recorded PORT: a real helper is
|
||||
// listening on it, a pid squatter is not. The probe is a spawned node
|
||||
// one-liner so it works identically on every platform (no bash, no ps).
|
||||
if (Number.isInteger(port) && port > 0) {
|
||||
// Liveness alone misclassifies a REUSED pid, and a bare TCP connect
|
||||
// misclassifies a coincidental listener on a reused port. The decisive
|
||||
// signal is IDENTITY: the helper answers its authenticated /status
|
||||
// endpoint with the token server.json records; nothing else on that port
|
||||
// can. The probe is a spawned node one-liner so it works identically on
|
||||
// every platform.
|
||||
if (Number.isInteger(port) && port > 0 && token) {
|
||||
try {
|
||||
execFileSync(process.execPath, ['-e', [
|
||||
"const s = require('node:net').connect({ host: '127.0.0.1', port: Number(process.argv[1]), timeout: 800 });",
|
||||
"s.on('connect', () => { s.destroy(); process.exit(0); });",
|
||||
"s.on('timeout', () => { s.destroy(); process.exit(1); });",
|
||||
"s.on('error', () => process.exit(1));",
|
||||
].join(''), String(port)], { timeout: 3000, stdio: 'ignore' });
|
||||
"const req = require('node:http').get({ host: '127.0.0.1', port: Number(process.argv[1]), path: '/status?token=' + encodeURIComponent(process.argv[2]), timeout: 1200 }, (res) => { res.resume(); process.exit(res.statusCode === 200 ? 0 : 1); });",
|
||||
"req.on('timeout', () => { req.destroy(); process.exit(1); });",
|
||||
"req.on('error', () => process.exit(1));",
|
||||
].join(''), String(port), token], { timeout: 4000, stdio: 'ignore' });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Legacy server.json without a port: best-effort process identity check.
|
||||
// Legacy server.json without a port/token: best-effort identity check.
|
||||
if (process.platform === 'win32') return true;
|
||||
try {
|
||||
const command = execFileSync('ps', ['-p', String(pid), '-o', 'command='], { encoding: 'utf-8' });
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { describe, it, beforeEach, afterEach } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { spawn, spawnSync } from 'node:child_process';
|
||||
import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { createServer } from 'node:net';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import {
|
||||
discoverAppCandidates,
|
||||
@@ -212,13 +211,25 @@ describe('review regressions: multi-app pointer', () => {
|
||||
writeRootsManifest(a);
|
||||
writeRootsManifest(b); // B booted last: a naive pointer now points at B
|
||||
|
||||
// A's helper server is the one alive: a real listener on a real port
|
||||
// (the liveness check probes the recorded port, so a bare pid is not
|
||||
// enough to count as running).
|
||||
const srv = createServer();
|
||||
await new Promise((resolve) => srv.listen(0, '127.0.0.1', resolve));
|
||||
// A's helper server is the one alive: an authenticated /status
|
||||
// responder on a real port, hosted in a CHILD process because the
|
||||
// probe is execFileSync and a same-process responder could never
|
||||
// accept while the event loop is blocked (production helpers are
|
||||
// always separate processes).
|
||||
const responder = spawn(process.execPath, ['-e', [
|
||||
"const s = require('node:http').createServer((q, r) => {",
|
||||
" const ok = q.url === '/status?token=t';",
|
||||
" r.writeHead(ok ? 200 : 401, { 'Content-Type': 'application/json' });",
|
||||
" r.end('{}');",
|
||||
"});",
|
||||
"s.listen(0, '127.0.0.1', () => console.log(s.address().port));",
|
||||
].join('\n')], { stdio: ['ignore', 'pipe', 'ignore'] });
|
||||
const livePort = await new Promise((resolve, reject) => {
|
||||
responder.stdout.once('data', (chunk) => resolve(Number(String(chunk).trim())));
|
||||
responder.once('error', reject);
|
||||
setTimeout(() => reject(new Error('responder never became ready')), 5000);
|
||||
});
|
||||
try {
|
||||
const livePort = srv.address().port;
|
||||
write(repo, 'siteA/.impeccable/live/server.json', JSON.stringify({ pid: process.pid, port: livePort, token: 't' }));
|
||||
write(repo, 'siteB/.impeccable/live/server.json', JSON.stringify({ pid: 999999999, port: 2, token: 't' }));
|
||||
|
||||
@@ -226,7 +237,7 @@ describe('review regressions: multi-app pointer', () => {
|
||||
assert.equal(resolved.source, 'pointer');
|
||||
assert.equal(resolved.manifest.appRoot, join(repo, 'siteA'));
|
||||
} finally {
|
||||
await new Promise((resolve) => srv.close(resolve));
|
||||
responder.kill();
|
||||
}
|
||||
} finally {
|
||||
rmSync(repo, { recursive: true, force: true });
|
||||
|
||||
Reference in New Issue
Block a user