88 lines
4.9 KiB
JavaScript
88 lines
4.9 KiB
JavaScript
// B1/#1827 r2: rejecting an unusable key must leave both complete trees unchanged.
|
|
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import http from 'node:http';
|
|
import { sha256, validManagementKey } from '../src/common.mjs';
|
|
import { initializeProxy, installPaths } from '../src/settings.mjs';
|
|
import { ManagementClient } from '../src/client.mjs';
|
|
|
|
const pin = { version: '8.0.42', binary: { sha256: sha256('fixture binary') }, panel: { sha256: sha256('fixture panel') } };
|
|
function fixture(t) {
|
|
const root = fs.mkdtempSync(join(tmpdir(), 'p1-encoding-'));
|
|
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
|
const configDir = join(root, 'config'), dataRoot = join(root, 'data');
|
|
fs.mkdirSync(join(configDir, 'secrets'), { recursive: true, mode: 0o700 });
|
|
fs.mkdirSync(join(dataRoot, 'existing'), { recursive: true, mode: 0o700 });
|
|
fs.writeFileSync(join(configDir, 'config.json'), '{"fixture":"unchanged"}\n', { mode: 0o600 });
|
|
fs.writeFileSync(join(configDir, 'secrets', 'existing'), 'fixture-existing', { mode: 0o600 });
|
|
fs.writeFileSync(join(dataRoot, 'existing', 'marker'), 'fixture-data', { mode: 0o600 });
|
|
const installed = installPaths(dataRoot, pin);
|
|
fs.mkdirSync(join(installed.dir, 'static'), { recursive: true, mode: 0o700 });
|
|
fs.writeFileSync(installed.binary, 'fixture binary', { mode: 0o755 });
|
|
fs.writeFileSync(installed.panel, 'fixture panel', { mode: 0o600 });
|
|
return { configDir, dataRoot };
|
|
}
|
|
function inventory(root) {
|
|
const entries = [];
|
|
function walk(file, relative) {
|
|
const s = fs.lstatSync(file, { bigint: true });
|
|
const entry = {
|
|
path: relative, kind: s.isDirectory() ? 'directory' : s.isFile() ? 'file' : s.isSymbolicLink() ? 'symlink' : 'other',
|
|
mode: (s.mode & 0o7777n).toString(8), uid: String(s.uid), gid: String(s.gid), inode: String(s.ino), device: String(s.dev),
|
|
modified: String(s.mtimeNs), changed: String(s.ctimeNs),
|
|
};
|
|
// Read-only inventory may update atime; it must not alter identity/content/mode or mtime/ctime.
|
|
if (s.isFile()) { entry.size = String(s.size); entry.sha256 = sha256(fs.readFileSync(file)); }
|
|
if (s.isSymbolicLink()) entry.target = fs.readlinkSync(file);
|
|
entries.push(entry);
|
|
if (s.isDirectory()) for (const child of fs.readdirSync(file).sort()) walk(join(file, child), relative === '.' ? child : relative + '/' + child);
|
|
}
|
|
walk(root, '.');
|
|
return entries;
|
|
}
|
|
const inventories = f => ({ config: inventory(f.configDir), data: inventory(f.dataRoot) });
|
|
for (const [label, key] of [
|
|
['Latin-1', 'fx329-caf\u00e9'],
|
|
['leading space', ' fx329-ascii'],
|
|
['trailing space', 'fx329-ascii '],
|
|
['internal space', 'fx329 ascii'],
|
|
]) {
|
|
test(`proxy-key-invalid: ${label} leaves complete config/data inventories unchanged`, async t => {
|
|
const f = fixture(t), before = inventories(f);
|
|
const outcome = await initializeProxy({ ...f, pin, key }).then(() => null, error => error.code);
|
|
assert.deepEqual({ refusal: outcome, trees: inventories(f) }, { refusal: 'proxy-key-invalid', trees: before });
|
|
assert.equal(validManagementKey(key), false);
|
|
});
|
|
}
|
|
test('printable non-space ASCII control initializes and authenticates with unchanged existing files', async t => {
|
|
const f = fixture(t), key = 'fx329-Az09!_~', before = inventories(f);
|
|
assert.equal(validManagementKey(key), true);
|
|
const settings = await initializeProxy({ ...f, pin, key });
|
|
const after = inventories(f);
|
|
for (const area of ['config', 'data']) {
|
|
for (const old of before[area].filter(entry => entry.kind === 'file'))
|
|
assert.deepEqual(after[area].find(entry => entry.path === old.path), old);
|
|
}
|
|
const added = area => after[area].filter(entry => !before[area].some(old => old.path === entry.path)).map(entry => entry.path).sort();
|
|
assert.deepEqual(added('config'), ['proxy.json', 'secrets/proxy-management.key']);
|
|
assert.deepEqual(added('data'), ['proxy', 'proxy/auth', 'proxy/config.yaml', 'proxy/logs']);
|
|
assert.equal(fs.readFileSync(join(f.configDir, 'secrets/proxy-management.key'), 'utf8'), key);
|
|
const config = JSON.parse(fs.readFileSync(join(f.dataRoot, 'proxy/config.yaml'), 'utf8'));
|
|
const server = http.createServer((request, response) => {
|
|
response.statusCode = request.headers.authorization === `Bearer ${key}` ? 200 : 401;
|
|
response.setHeader('Content-Type', 'application/json');
|
|
response.end(JSON.stringify(response.statusCode === 200 ? config.access['api-keys'] : { error: 'invalid' }));
|
|
});
|
|
await new Promise(resolve => server.listen(settings.port, '127.0.0.1', resolve));
|
|
try {
|
|
const client = new ManagementClient({ baseUrl: `http://127.0.0.1:${settings.port}`, key });
|
|
assert.deepEqual(await client.getKeys(), config.access['api-keys']);
|
|
} finally {
|
|
server.closeAllConnections();
|
|
await new Promise(resolve => server.close(resolve));
|
|
}
|
|
});
|