181 lines
5.3 KiB
TypeScript
181 lines
5.3 KiB
TypeScript
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
|
import { SmartProxy } from '../ts/index.js';
|
|
import * as fs from 'node:fs';
|
|
import * as http2 from 'node:http2';
|
|
import * as https from 'node:https';
|
|
import * as path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { findFreePorts } from './helpers/port-allocator.js';
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = path.dirname(__filename);
|
|
|
|
const CERT_PEM = fs.readFileSync(path.join(__dirname, '..', 'assets', 'certs', 'cert.pem'), 'utf8');
|
|
const KEY_PEM = fs.readFileSync(path.join(__dirname, '..', 'assets', 'certs', 'key.pem'), 'utf8');
|
|
const TEST_DOMAIN = 'verdaccio.test';
|
|
|
|
let backendPort: number;
|
|
let proxyPort: number;
|
|
let unavailableH3Port: number;
|
|
let backendServer: https.Server;
|
|
let proxy: SmartProxy;
|
|
|
|
function httpsRequest(requestPath: string): Promise<{ status: number; body: string }> {
|
|
return new Promise((resolve, reject) => {
|
|
const req = https.request(
|
|
{
|
|
hostname: 'localhost',
|
|
port: proxyPort,
|
|
path: requestPath,
|
|
method: 'GET',
|
|
headers: {
|
|
Host: TEST_DOMAIN,
|
|
},
|
|
rejectUnauthorized: false,
|
|
servername: TEST_DOMAIN,
|
|
agent: new https.Agent({ keepAlive: false, rejectUnauthorized: false }),
|
|
},
|
|
(res) => {
|
|
let body = '';
|
|
res.on('data', (chunk) => {
|
|
body += chunk.toString();
|
|
});
|
|
res.on('end', () => resolve({ status: res.statusCode ?? 0, body }));
|
|
},
|
|
);
|
|
req.on('error', reject);
|
|
req.setTimeout(5000, () => req.destroy(new Error('https request timeout')));
|
|
req.end();
|
|
});
|
|
}
|
|
|
|
function http2Request(requestPath: string): Promise<{ status: number; body: string }> {
|
|
return new Promise((resolve, reject) => {
|
|
const session = http2.connect(`https://localhost:${proxyPort}`, {
|
|
rejectUnauthorized: false,
|
|
servername: TEST_DOMAIN,
|
|
});
|
|
|
|
const cleanup = () => {
|
|
if (!session.closed && !session.destroyed) {
|
|
session.close();
|
|
}
|
|
};
|
|
|
|
session.once('error', (error) => {
|
|
cleanup();
|
|
reject(error);
|
|
});
|
|
|
|
session.once('connect', () => {
|
|
const req = session.request({
|
|
':method': 'GET',
|
|
':path': requestPath,
|
|
':authority': TEST_DOMAIN,
|
|
});
|
|
let status = 0;
|
|
let body = '';
|
|
req.setEncoding('utf8');
|
|
req.on('response', (headers) => {
|
|
status = Number(headers[':status'] ?? 0);
|
|
});
|
|
req.on('data', (chunk) => {
|
|
body += chunk;
|
|
});
|
|
req.on('end', () => {
|
|
cleanup();
|
|
resolve({ status, body });
|
|
});
|
|
req.on('error', (error) => {
|
|
cleanup();
|
|
reject(error);
|
|
});
|
|
req.end();
|
|
});
|
|
|
|
setTimeout(() => {
|
|
cleanup();
|
|
reject(new Error('http2 request timeout'));
|
|
}, 5000).unref();
|
|
});
|
|
}
|
|
|
|
tap.test('setup - backend with Alt-Svc H3 hint and TLS proxy', async () => {
|
|
[backendPort, proxyPort, unavailableH3Port] = await findFreePorts(3);
|
|
|
|
backendServer = https.createServer({ key: KEY_PEM, cert: CERT_PEM }, (req, res) => {
|
|
const body = JSON.stringify({ ok: true, url: req.url, host: req.headers.host });
|
|
res.writeHead(200, {
|
|
'content-type': 'application/json',
|
|
'content-length': Buffer.byteLength(body),
|
|
'alt-svc': `h3=":${unavailableH3Port}"; ma=86400`,
|
|
});
|
|
res.end(body);
|
|
});
|
|
|
|
await new Promise<void>((resolve, reject) => {
|
|
backendServer.once('error', reject);
|
|
backendServer.listen(backendPort, () => resolve());
|
|
});
|
|
|
|
proxy = new SmartProxy({
|
|
routes: [
|
|
{
|
|
id: 'backend-protocol-fallback',
|
|
name: 'backend-protocol-fallback',
|
|
match: { ports: proxyPort, domains: TEST_DOMAIN },
|
|
action: {
|
|
type: 'forward',
|
|
tls: {
|
|
mode: 'terminate',
|
|
certificate: {
|
|
key: KEY_PEM,
|
|
cert: CERT_PEM,
|
|
},
|
|
},
|
|
targets: [
|
|
{
|
|
host: 'localhost',
|
|
port: backendPort,
|
|
tls: { mode: 'passthrough' },
|
|
},
|
|
],
|
|
options: { backendProtocol: 'auto' },
|
|
},
|
|
},
|
|
],
|
|
connectionTimeout: 500,
|
|
metrics: { enabled: true, sampleIntervalMs: 100, retentionSeconds: 30 },
|
|
});
|
|
|
|
await proxy.start();
|
|
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
});
|
|
|
|
tap.test('backend protocol auto: fresh HTTP/1.1 survives unavailable H3 hint', async (tools) => {
|
|
tools.timeout(10000);
|
|
|
|
const first = await httpsRequest('/@consent.software%2Fcatalog');
|
|
expect(first.status).toEqual(200);
|
|
expect(JSON.parse(first.body).ok).toEqual(true);
|
|
|
|
const second = await httpsRequest('/@consent.software%2Fcatalog?retry=1');
|
|
expect(second.status).toEqual(200);
|
|
expect(JSON.parse(second.body).url).toEqual('/@consent.software%2Fcatalog?retry=1');
|
|
});
|
|
|
|
tap.test('backend protocol auto: fresh HTTP/2 survives suppressed H3 hint', async (tools) => {
|
|
tools.timeout(10000);
|
|
|
|
const result = await http2Request('/@consent.software%2Fcatalog?frontend=h2');
|
|
expect(result.status).toEqual(200);
|
|
expect(JSON.parse(result.body).url).toEqual('/@consent.software%2Fcatalog?frontend=h2');
|
|
});
|
|
|
|
tap.test('cleanup - backend protocol fallback', async () => {
|
|
await proxy.stop();
|
|
await new Promise<void>((resolve) => backendServer.close(() => resolve()));
|
|
});
|
|
|
|
export default tap.start();
|