233 lines
7.3 KiB
TypeScript
233 lines
7.3 KiB
TypeScript
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
|
import { SmartProxy } from '@push.rocks/smartproxy';
|
|
import * as http from 'node:http';
|
|
import * as net from 'node:net';
|
|
import {
|
|
deriveHttpRedirectConfiguration,
|
|
deriveHttpRedirects,
|
|
} from '../ts/config/helpers.http-redirects.js';
|
|
|
|
async function getFreePort(): Promise<number> {
|
|
return await new Promise<number>((resolve, reject) => {
|
|
const server = net.createServer();
|
|
server.once('error', reject);
|
|
server.listen(0, '127.0.0.1', () => {
|
|
const address = server.address();
|
|
const port = typeof address === 'object' && address ? address.port : 0;
|
|
server.close(() => resolve(port));
|
|
});
|
|
});
|
|
}
|
|
|
|
async function requestHeaders(
|
|
port: number,
|
|
path: string,
|
|
headers?: Record<string, string>,
|
|
): Promise<http.IncomingMessage> {
|
|
return await new Promise<http.IncomingMessage>((resolve, reject) => {
|
|
const request = http.get({ host: '127.0.0.1', port, path, headers, agent: false }, resolve);
|
|
request.once('error', reject);
|
|
});
|
|
}
|
|
|
|
tap.test('deriveHttpRedirectConfiguration creates active runtime redirects from HTTPS routes', async () => {
|
|
const result = deriveHttpRedirectConfiguration([
|
|
{
|
|
id: 'route-1',
|
|
name: 'app-route',
|
|
match: { ports: 443, domains: 'app.example.com' },
|
|
action: {
|
|
type: 'forward',
|
|
targets: [{ host: '127.0.0.1', port: 8080 }],
|
|
tls: { mode: 'terminate', certificate: 'auto' },
|
|
},
|
|
remoteIngress: {
|
|
enabled: true,
|
|
edgeFilter: ['edge-a'],
|
|
},
|
|
} as any,
|
|
]);
|
|
|
|
expect(result.redirects.length).toEqual(1);
|
|
expect(result.redirects[0].status).toEqual('active');
|
|
expect(result.redirects[0].domainPattern).toEqual('app.example.com');
|
|
expect(result.redirects[0].remoteIngress).toEqual(true);
|
|
expect(result.runtimeRoutes.length).toEqual(1);
|
|
expect(result.runtimeRoutes[0].match.ports).toEqual(80);
|
|
expect(result.runtimeRoutes[0].match.domains).toEqual('app.example.com');
|
|
expect(result.runtimeRoutes[0].priority).toEqual(0);
|
|
expect(result.runtimeRoutes[0].remoteIngress).toEqual({ enabled: true, edgeFilter: ['edge-a'] });
|
|
expect(typeof result.runtimeRoutes[0].action.socketHandler).toEqual('function');
|
|
});
|
|
|
|
tap.test('deriveHttpRedirectConfiguration deduplicates identical redirect scopes', async () => {
|
|
const redirects = deriveHttpRedirects([
|
|
{
|
|
id: 'route-1',
|
|
name: 'first-route',
|
|
match: { ports: [443], domains: ['app.example.com'] },
|
|
action: {
|
|
type: 'forward',
|
|
targets: [{ host: '127.0.0.1', port: 8080 }],
|
|
tls: { mode: 'terminate', certificate: 'auto' },
|
|
},
|
|
} as any,
|
|
{
|
|
id: 'route-2',
|
|
name: 'second-route',
|
|
match: { ports: [443], domains: ['app.example.com'] },
|
|
action: {
|
|
type: 'forward',
|
|
targets: [{ host: '127.0.0.1', port: 8081 }],
|
|
tls: { mode: 'terminate', certificate: 'auto' },
|
|
},
|
|
} as any,
|
|
]);
|
|
|
|
expect(redirects.length).toEqual(1);
|
|
expect(redirects[0].sourceRouteNames).toEqual(['first-route', 'second-route']);
|
|
});
|
|
|
|
tap.test('deriveHttpRedirectConfiguration treats broad explicit HTTP routes as covered', async () => {
|
|
const result = deriveHttpRedirectConfiguration([
|
|
{
|
|
name: 'https-route',
|
|
match: { ports: 443, domains: 'app.example.com' },
|
|
action: {
|
|
type: 'forward',
|
|
targets: [{ host: '127.0.0.1', port: 8080 }],
|
|
tls: { mode: 'terminate', certificate: 'auto' },
|
|
},
|
|
} as any,
|
|
{
|
|
name: 'existing-http-route',
|
|
match: { ports: 80, domains: 'app.example.com' },
|
|
action: {
|
|
type: 'forward',
|
|
targets: [{ host: '127.0.0.1', port: 8080 }],
|
|
},
|
|
} as any,
|
|
]);
|
|
|
|
expect(result.redirects.length).toEqual(1);
|
|
expect(result.redirects[0].status).toEqual('covered');
|
|
expect(result.redirects[0].coveredByRouteNames).toEqual(['existing-http-route']);
|
|
expect(result.runtimeRoutes.length).toEqual(0);
|
|
});
|
|
|
|
tap.test('deriveHttpRedirectConfiguration skips broad redirects that overlap path-specific HTTP routes', async () => {
|
|
const result = deriveHttpRedirectConfiguration([
|
|
{
|
|
name: 'https-route',
|
|
match: { ports: 443, domains: 'app.example.com' },
|
|
action: {
|
|
type: 'forward',
|
|
targets: [{ host: '127.0.0.1', port: 8080 }],
|
|
tls: { mode: 'terminate', certificate: 'auto' },
|
|
},
|
|
} as any,
|
|
{
|
|
name: 'existing-http-health-route',
|
|
match: { ports: 80, domains: 'app.example.com', path: '/health' },
|
|
action: {
|
|
type: 'forward',
|
|
targets: [{ host: '127.0.0.1', port: 8080 }],
|
|
},
|
|
} as any,
|
|
]);
|
|
|
|
expect(result.redirects[0].status).toEqual('skipped');
|
|
expect(result.runtimeRoutes.length).toEqual(0);
|
|
});
|
|
|
|
tap.test('deriveHttpRedirectConfiguration skips wildcard redirects that overlap explicit HTTP domains', async () => {
|
|
const result = deriveHttpRedirectConfiguration([
|
|
{
|
|
name: 'wildcard-https-route',
|
|
match: { ports: 443, domains: '*.example.com' },
|
|
action: {
|
|
type: 'forward',
|
|
targets: [{ host: '127.0.0.1', port: 8080 }],
|
|
tls: { mode: 'terminate', certificate: 'auto' },
|
|
},
|
|
} as any,
|
|
{
|
|
name: 'explicit-http-app-route',
|
|
match: { ports: 80, domains: 'app.example.com' },
|
|
action: {
|
|
type: 'forward',
|
|
targets: [{ host: '127.0.0.1', port: 8080 }],
|
|
},
|
|
} as any,
|
|
]);
|
|
|
|
expect(result.redirects[0].status).toEqual('skipped');
|
|
expect(result.runtimeRoutes.length).toEqual(0);
|
|
});
|
|
|
|
tap.test('deriveHttpRedirectConfiguration ignores non-web or narrowed HTTPS routes', async () => {
|
|
const redirects = deriveHttpRedirects([
|
|
{
|
|
name: 'udp-route',
|
|
match: { ports: 443, domains: 'udp.example.com', transport: 'udp' },
|
|
action: {
|
|
type: 'forward',
|
|
targets: [{ host: '127.0.0.1', port: 443 }],
|
|
tls: { mode: 'passthrough' },
|
|
},
|
|
} as any,
|
|
{
|
|
name: 'header-route',
|
|
match: { ports: 443, domains: 'header.example.com', headers: { 'x-test': 'yes' } },
|
|
action: {
|
|
type: 'forward',
|
|
targets: [{ host: '127.0.0.1', port: 8080 }],
|
|
tls: { mode: 'terminate', certificate: 'auto' },
|
|
},
|
|
} as any,
|
|
{
|
|
name: 'socket-handler-route',
|
|
match: { ports: 443, domains: 'handler.example.com' },
|
|
action: {
|
|
type: 'socket-handler',
|
|
socketHandler: () => {},
|
|
},
|
|
} as any,
|
|
]);
|
|
|
|
expect(redirects.length).toEqual(0);
|
|
});
|
|
|
|
tap.test('generated runtime redirect preserves host and path', async () => {
|
|
const proxyPort = await getFreePort();
|
|
const redirectRoute = deriveHttpRedirectConfiguration([
|
|
{
|
|
name: 'https-route',
|
|
match: { ports: 443, domains: 'app.example.com' },
|
|
action: {
|
|
type: 'forward',
|
|
targets: [{ host: '127.0.0.1', port: 8080 }],
|
|
tls: { mode: 'terminate', certificate: 'auto' },
|
|
},
|
|
} as any,
|
|
]).runtimeRoutes[0] as any;
|
|
redirectRoute.match = { ...redirectRoute.match, ports: proxyPort };
|
|
|
|
const proxy = new SmartProxy({
|
|
connectionRateLimitPerMinute: 1000,
|
|
routes: [redirectRoute],
|
|
});
|
|
|
|
try {
|
|
await proxy.start();
|
|
const response = await requestHeaders(proxyPort, '/some/path?x=1', { host: 'app.example.com' });
|
|
expect(response.statusCode).toEqual(301);
|
|
expect(response.headers.location).toEqual('https://app.example.com/some/path?x=1');
|
|
response.destroy();
|
|
} finally {
|
|
await proxy.stop();
|
|
}
|
|
});
|
|
|
|
export default tap.start();
|