feat(fpagent): public installer and private-release proxy
Serve /fpagent/install.sh (also curl|bash) and proxy the private fingerprint-agent releases through /fpagent/releases/* using GITEA_RELEASE_INSTALL_KEY, so a field laptop needs no forge credentials. The installer picks the offline bundle for the host codename when present, verifies the published sha256, then hands off to the bundle's own installer.
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
// The /fpagent installer surface: a public script, and a proxy that reads the
|
||||
// private Gitea release with GITEA_RELEASE_INSTALL_KEY so the operator's
|
||||
// laptop never needs credentials.
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { createApp, FPAGENT_REPO } from '../server.js';
|
||||
import { startServer, stopServer, urlOf } from './helpers.js';
|
||||
|
||||
const TOKEN = 'release-install-secret';
|
||||
const TAG = 'v9.9.9';
|
||||
const ARCHIVE = 'fpagent-deadbee-linux-x86_64.tar.gz';
|
||||
const UBUNTU_ARCHIVE = 'fpagent-deadbee-ubuntu-jammy-x86_64.tar.gz';
|
||||
const RELEASE = {
|
||||
tag_name: TAG,
|
||||
assets: [
|
||||
{ name: ARCHIVE },
|
||||
{ name: `${ARCHIVE}.sha256` },
|
||||
{ name: UBUNTU_ARCHIVE },
|
||||
{ name: 'not a safe name' },
|
||||
{ name: '../escape' },
|
||||
],
|
||||
};
|
||||
|
||||
// Fake Gitea: refuses without the token, serves the release and its assets.
|
||||
async function startFakeGitea({ releaseStatus = 200, downloadStatus = 200 } = {}) {
|
||||
const calls = [];
|
||||
const server = await startServer((req, res) => {
|
||||
calls.push({ url: req.url, auth: req.headers.authorization });
|
||||
if (req.headers.authorization !== `token ${TOKEN}`) {
|
||||
res.writeHead(404);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
if (req.url === `/api/v1/repos/${FPAGENT_REPO}/releases/latest`) {
|
||||
res.writeHead(releaseStatus, { 'content-type': 'application/json' });
|
||||
res.end(JSON.stringify(RELEASE));
|
||||
return;
|
||||
}
|
||||
if (req.url.startsWith(`/${FPAGENT_REPO}/releases/download/`)) {
|
||||
res.writeHead(downloadStatus, { 'content-type': 'application/gzip' });
|
||||
res.end(Buffer.from('bundle-bytes'));
|
||||
return;
|
||||
}
|
||||
res.writeHead(404);
|
||||
res.end();
|
||||
});
|
||||
return { server, calls };
|
||||
}
|
||||
|
||||
async function bootFpagentApp(giteaUrl, overrides = {}) {
|
||||
const app = createApp({
|
||||
giteaUrl,
|
||||
fpagentRepo: FPAGENT_REPO,
|
||||
giteaToken: TOKEN,
|
||||
cropperClients: null,
|
||||
...overrides,
|
||||
});
|
||||
const server = await new Promise((resolve) => {
|
||||
const s = app.listen(0, '127.0.0.1', () => resolve(s));
|
||||
});
|
||||
return { server, base: urlOf(server) };
|
||||
}
|
||||
|
||||
test('latest release is proxied as tools-relative URLs, without the token', async () => {
|
||||
const gitea = await startFakeGitea();
|
||||
const { server, base } = await bootFpagentApp(urlOf(gitea.server));
|
||||
try {
|
||||
const r = await fetch(`${base}/fpagent/releases/latest`);
|
||||
assert.equal(r.status, 200);
|
||||
const body = await r.json();
|
||||
assert.equal(body.tag, TAG);
|
||||
// Unsafe asset names are dropped; the rest point back at this app.
|
||||
assert.deepEqual(
|
||||
body.assets.map((a) => a.name),
|
||||
[ARCHIVE, `${ARCHIVE}.sha256`, UBUNTU_ARCHIVE]
|
||||
);
|
||||
for (const a of body.assets) {
|
||||
assert.equal(a.url, `/fpagent/releases/download/${TAG}/${a.name}`);
|
||||
}
|
||||
assert.ok(!JSON.stringify(body).includes(TOKEN));
|
||||
assert.equal(gitea.calls[0].auth, `token ${TOKEN}`); // server authenticated upstream
|
||||
} finally {
|
||||
await stopServer(server);
|
||||
await stopServer(gitea.server);
|
||||
}
|
||||
});
|
||||
|
||||
test('bundles stream through this app; the client sends no credentials', async () => {
|
||||
const gitea = await startFakeGitea();
|
||||
const { server, base } = await bootFpagentApp(urlOf(gitea.server));
|
||||
try {
|
||||
const r = await fetch(`${base}/fpagent/releases/download/${TAG}/${ARCHIVE}`);
|
||||
assert.equal(r.status, 200);
|
||||
assert.equal(Buffer.from(await r.arrayBuffer()).toString(), 'bundle-bytes');
|
||||
const assetCall = gitea.calls.find((c) => c.url.includes('/releases/download/'));
|
||||
assert.equal(assetCall.auth, `token ${TOKEN}`);
|
||||
} finally {
|
||||
await stopServer(server);
|
||||
await stopServer(gitea.server);
|
||||
}
|
||||
});
|
||||
|
||||
test('unsafe asset names are rejected before reaching the forge', async () => {
|
||||
const gitea = await startFakeGitea();
|
||||
const { server, base } = await bootFpagentApp(urlOf(gitea.server));
|
||||
try {
|
||||
const r = await fetch(`${base}/fpagent/releases/download/${TAG}/-bad`);
|
||||
assert.equal(r.status, 400);
|
||||
assert.equal(gitea.calls.length, 0);
|
||||
} finally {
|
||||
await stopServer(server);
|
||||
await stopServer(gitea.server);
|
||||
}
|
||||
});
|
||||
|
||||
test('a missing bundle surfaces as 404', async () => {
|
||||
const gitea = await startFakeGitea({ downloadStatus: 404 });
|
||||
const { server, base } = await bootFpagentApp(urlOf(gitea.server));
|
||||
try {
|
||||
const r = await fetch(`${base}/fpagent/releases/download/${TAG}/${ARCHIVE}`);
|
||||
assert.equal(r.status, 404);
|
||||
} finally {
|
||||
await stopServer(server);
|
||||
await stopServer(gitea.server);
|
||||
}
|
||||
});
|
||||
|
||||
test('without GITEA_RELEASE_INSTALL_KEY the installer reports 503', async () => {
|
||||
const { server, base } = await bootFpagentApp('https://sources.yrka.app', { giteaToken: '' });
|
||||
try {
|
||||
const r = await fetch(`${base}/fpagent/releases/latest`);
|
||||
assert.equal(r.status, 503);
|
||||
} finally {
|
||||
await stopServer(server);
|
||||
}
|
||||
});
|
||||
|
||||
test('install.sh is served publicly at /fpagent/install.sh', async () => {
|
||||
const { server, base } = await bootFpagentApp('https://sources.yrka.app', { giteaToken: '' });
|
||||
try {
|
||||
const r = await fetch(`${base}/fpagent/install.sh`);
|
||||
assert.equal(r.status, 200);
|
||||
assert.match(await r.text(), /FPAGENT_BASE_URL/);
|
||||
} finally {
|
||||
await stopServer(server);
|
||||
}
|
||||
});
|
||||
|
||||
test('install.sh is valid bash', () => {
|
||||
const script = path.join(
|
||||
path.dirname(fileURLToPath(import.meta.url)),
|
||||
'..',
|
||||
'public',
|
||||
'fpagent',
|
||||
'install.sh'
|
||||
);
|
||||
execFileSync('bash', ['-n', script], { stdio: 'pipe' });
|
||||
});
|
||||
Reference in New Issue
Block a user