162 lines
5.4 KiB
JavaScript
162 lines
5.4 KiB
JavaScript
// 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' });
|
||
|
|
});
|