116 lines
4.7 KiB
JavaScript
116 lines
4.7 KiB
JavaScript
'use strict';
|
|
|
|
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const { PassThrough, Readable } = require('node:stream');
|
|
|
|
const {
|
|
BridgeAuth,
|
|
BridgeAuthError,
|
|
generatePairingSecret,
|
|
hashPairingSecret,
|
|
verifyPairingSecret,
|
|
readJsonBody,
|
|
createRequestLimiter
|
|
} = require('../src/bridge-auth');
|
|
|
|
const EXTENSION_ORIGIN = 'chrome-extension://onbkfpbggekakjddomjjnboippimlmch';
|
|
|
|
test('pairing secrets are random, URL-safe, and stored as a hash envelope', () => {
|
|
const first = generatePairingSecret();
|
|
const second = generatePairingSecret();
|
|
assert.match(first, /^[A-Za-z0-9_-]{43}$/);
|
|
assert.notEqual(first, second);
|
|
|
|
const envelope = hashPairingSecret(first);
|
|
assert.equal(envelope.version, 1);
|
|
assert.equal(envelope.algorithm, 'scrypt');
|
|
assert.equal(Object.values(envelope).includes(first), false);
|
|
assert.equal(verifyPairingSecret(first, envelope), true);
|
|
assert.equal(verifyPairingSecret(second, envelope), false);
|
|
assert.equal(verifyPairingSecret('', envelope), false);
|
|
assert.equal(verifyPairingSecret(null, envelope), false);
|
|
});
|
|
|
|
test('auth rejects missing/wrong secrets and unknown or malformed origins', () => {
|
|
const auth = new BridgeAuth({ expectedOrigin: EXTENSION_ORIGIN });
|
|
const { secret } = auth.rotate();
|
|
|
|
assert.equal(auth.authenticate({ origin: EXTENSION_ORIGIN, secret }), true);
|
|
assert.equal(auth.authenticate({ origin: EXTENSION_ORIGIN }), false);
|
|
assert.equal(auth.authenticate({ origin: EXTENSION_ORIGIN, secret: 'wrong' }), false);
|
|
assert.equal(auth.authenticate({ origin: 'chrome-extension://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', secret }), false);
|
|
assert.equal(auth.authenticate({ origin: `${EXTENSION_ORIGIN}/`, secret }), false);
|
|
assert.equal(auth.authenticate({ origin: `${EXTENSION_ORIGIN}\r\nX-Evil: yes`, secret }), false);
|
|
assert.equal(auth.authenticate({ origin: EXTENSION_ORIGIN, secret: `${secret}\r\n` }), false);
|
|
});
|
|
|
|
test('rotation invalidates the previous secret and revoke invalidates the current secret', () => {
|
|
const auth = new BridgeAuth({ expectedOrigin: EXTENSION_ORIGIN });
|
|
const first = auth.rotate().secret;
|
|
const second = auth.rotate().secret;
|
|
|
|
assert.equal(auth.authenticate({ origin: EXTENSION_ORIGIN, secret: first }), false);
|
|
assert.equal(auth.authenticate({ origin: EXTENSION_ORIGIN, secret: second }), true);
|
|
auth.revoke();
|
|
assert.equal(auth.authenticate({ origin: EXTENSION_ORIGIN, secret: second }), false);
|
|
assert.equal(auth.envelope, null);
|
|
});
|
|
|
|
test('an envelope can be safely persisted and loaded by a future desktop wiring', () => {
|
|
const first = new BridgeAuth({ expectedOrigin: EXTENSION_ORIGIN });
|
|
const { secret, envelope } = first.rotate();
|
|
const persisted = JSON.parse(JSON.stringify(envelope));
|
|
const restored = new BridgeAuth({ expectedOrigin: EXTENSION_ORIGIN, envelope: persisted });
|
|
|
|
assert.equal(restored.authenticate({ origin: EXTENSION_ORIGIN, secret }), true);
|
|
assert.throws(
|
|
() => new BridgeAuth({ expectedOrigin: 'https://example.test' }),
|
|
(error) => error instanceof BridgeAuthError && error.code === 'INVALID_EXTENSION_ORIGIN'
|
|
);
|
|
assert.throws(
|
|
() => new BridgeAuth({ expectedOrigin: 'chrome-extension://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' }),
|
|
(error) => error instanceof BridgeAuthError && error.code === 'INVALID_EXTENSION_ORIGIN'
|
|
);
|
|
});
|
|
|
|
test('readJsonBody accepts a bounded JSON object and rejects malformed or oversized input', async () => {
|
|
assert.deepEqual(
|
|
await readJsonBody(Readable.from(['{"ok":true}']), { maxBytes: 64 }),
|
|
{ ok: true }
|
|
);
|
|
|
|
await assert.rejects(
|
|
readJsonBody(Readable.from(['{"nope"']), { maxBytes: 64 }),
|
|
(error) => error.code === 'MALFORMED_JSON'
|
|
);
|
|
await assert.rejects(
|
|
readJsonBody(Readable.from(['{"value":"', 'x'.repeat(100), '"}']), { maxBytes: 32 }),
|
|
(error) => error.code === 'BODY_TOO_LARGE'
|
|
);
|
|
await assert.rejects(
|
|
readJsonBody(Readable.from(['[]']), { maxBytes: 64 }),
|
|
(error) => error.code === 'INVALID_JSON_BODY'
|
|
);
|
|
});
|
|
|
|
test('readJsonBody aborts a slow body at its deadline', async () => {
|
|
const stream = new PassThrough();
|
|
const pending = readJsonBody(stream, { maxBytes: 64, deadlineMs: 15 });
|
|
stream.write('{');
|
|
await assert.rejects(pending, (error) => error.code === 'BODY_DEADLINE_EXCEEDED');
|
|
});
|
|
|
|
test('request limiter rejects excess concurrent bodies before work begins', async () => {
|
|
const limiter = createRequestLimiter({ maxConcurrent: 1 });
|
|
let release;
|
|
const active = limiter.run(() => new Promise((resolve) => { release = resolve; }));
|
|
await assert.rejects(
|
|
limiter.run(async () => 'never'),
|
|
(error) => error.code === 'TOO_MANY_REQUESTS'
|
|
);
|
|
release('done');
|
|
assert.equal(await active, 'done');
|
|
assert.equal(limiter.active, 0);
|
|
});
|