121 lines
5.0 KiB
TypeScript
121 lines
5.0 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { ForwardingState } from '../src/forwarding/state.ts';
|
|
import { parseCapability, previewOrigin } from '../src/forwarding/security.ts';
|
|
|
|
const origin = 'https://preview.other.test';
|
|
const sessionId = 'a'.repeat(64);
|
|
|
|
function fakeSession(initialReady = true) {
|
|
let ready = initialReady;
|
|
return {
|
|
isForwardReady: () => ready,
|
|
openForward: async () => ({ close: async () => undefined }),
|
|
close: () => { ready = false; },
|
|
};
|
|
}
|
|
|
|
async function prepared() {
|
|
const session = fakeSession();
|
|
const state = new ForwardingState();
|
|
const response = await state.create(session as never, 8080, origin, sessionId);
|
|
const url = (await response.json() as { url: string }).url;
|
|
const capability = parseCapability(new URL(url).hash.slice(1));
|
|
assert.ok(capability);
|
|
return { state, session, launchToken: capability.token };
|
|
}
|
|
|
|
test('trusted grants use the main origin path and do not accept isolated preview tokens', async () => {
|
|
const session = fakeSession();
|
|
const state = new ForwardingState();
|
|
const response = await state.create(session as never, 8080, 'https://main.test', sessionId, 'trusted');
|
|
const body = await response.json() as { url: string };
|
|
assert.equal(body.url, `https://main.test/_forward/${sessionId}/`);
|
|
assert.equal((await state.preview(new Request('https://internal/preview'))).status, 410);
|
|
state.clear();
|
|
});
|
|
|
|
test('launch token is single-use and returns preview token', async () => {
|
|
const { state, session, launchToken } = await prepared();
|
|
try {
|
|
const request = () => new Request('https://internal/preview-launch', {
|
|
method: 'POST', headers: { 'x-preview-origin': origin, 'x-preview-token': launchToken },
|
|
});
|
|
const first = await state.preview(request());
|
|
assert.equal(first.status, 200);
|
|
const body = await first.json() as { token: string };
|
|
assert.match(body.token, /^[a-f0-9-]{36}$/);
|
|
assert.equal((await state.preview(request())).status, 401);
|
|
} finally { state.clear(); }
|
|
assert.equal(session.isForwardReady(), true);
|
|
});
|
|
|
|
test('cleared grant rejects the launch token with 410', async () => {
|
|
const { state, launchToken } = await prepared();
|
|
try {
|
|
state.clear();
|
|
const response = await state.preview(new Request('https://internal/preview-launch', {
|
|
method: 'POST', headers: { 'x-preview-origin': origin, 'x-preview-token': launchToken },
|
|
}));
|
|
assert.equal(response.status, 410);
|
|
} finally { state.clear(); }
|
|
});
|
|
|
|
test('invalid preview token returns 401', async () => {
|
|
const { state } = await prepared();
|
|
try {
|
|
const response = await state.preview(new Request('https://internal/preview', {
|
|
headers: { 'x-preview-origin': origin, 'x-preview-token': 'bad' },
|
|
}));
|
|
assert.equal(response.status, 401);
|
|
} finally { state.clear(); }
|
|
});
|
|
|
|
test('cross-site and missing-origin writes are rejected with 403', async () => {
|
|
const { state, launchToken } = await prepared();
|
|
try {
|
|
const launch = await state.preview(new Request('https://internal/preview-launch', {
|
|
method: 'POST', headers: { 'x-preview-origin': origin, 'x-preview-token': launchToken },
|
|
}));
|
|
const token = (await launch.json() as { token: string }).token;
|
|
const crossSite = await state.preview(new Request('https://internal/preview', {
|
|
method: 'POST', headers: { 'x-preview-origin': origin, 'x-preview-token': token, Origin: 'https://evil.test' },
|
|
}));
|
|
assert.equal(crossSite.status, 403);
|
|
const missingOrigin = await state.preview(new Request('https://internal/preview', {
|
|
method: 'POST', headers: { 'x-preview-origin': origin, 'x-preview-token': token },
|
|
}));
|
|
assert.equal(missingOrigin.status, 403);
|
|
} finally { state.clear(); }
|
|
});
|
|
|
|
test('unready SSH session returns 410', async () => {
|
|
const { state, session, launchToken } = await prepared();
|
|
try {
|
|
session.close();
|
|
const response = await state.preview(new Request('https://internal/preview-launch', {
|
|
method: 'POST', headers: { 'x-preview-origin': origin, 'x-preview-token': launchToken },
|
|
}));
|
|
assert.equal(response.status, 410);
|
|
} finally { state.clear(); }
|
|
});
|
|
|
|
test('stop closes the SSH session', async () => {
|
|
const { state, session } = await prepared();
|
|
state.stop();
|
|
assert.equal(session.isForwardReady(), false);
|
|
state.clear();
|
|
});
|
|
|
|
test('previewOrigin rejects missing, same-parent, and same-account workers.dev domains', () => {
|
|
assert.throws(() => previewOrigin(undefined, 'https://ssh.example.com'));
|
|
assert.throws(() => previewOrigin('https://preview.ssh.example.com', 'https://ssh.example.com'));
|
|
assert.throws(() => previewOrigin('https://preview.account.workers.dev', 'https://ssh.account.workers.dev'));
|
|
});
|
|
|
|
test('previewOrigin allows independent custom and workers.dev domains', () => {
|
|
assert.equal(previewOrigin('preview.other.test', 'https://ssh.example.com'), origin);
|
|
assert.equal(previewOrigin('https://preview.account.workers.dev', 'https://ssh.example.com'),
|
|
'https://preview.account.workers.dev');
|
|
});
|