import * as plugins from '../plugins.js'; export interface RedirectRule { /** * Optional protocol to match (http or https). If not specified, matches both. */ fromProtocol?: 'http' | 'https'; /** * Optional hostname pattern to match. Can use * as wildcard. * If not specified, matches all hosts. */ fromHost?: string; /** * Optional path prefix to match. If not specified, matches all paths. */ fromPath?: string; /** * Target protocol for the redirect (http or https) */ toProtocol: 'http' | 'https'; /** * Target hostname for the redirect. Can use $1, $2, etc. to reference * captured groups from wildcard matches in fromHost. */ toHost: string; /** * Optional target path prefix. If not specified, keeps original path. * Can use $path to reference the original path. */ toPath?: string; /** * HTTP status code for the redirect (301 for permanent, 302 for temporary) */ statusCode?: 301 | 302 | 307 | 308; } export class Redirect { private httpServer?: plugins.http.Server; private httpsServer?: plugins.https.Server; private rules: RedirectRule[] = []; private httpPort: number = 80; private httpsPort: number = 443; private sslOptions?: { key: Buffer; cert: Buffer; }; /** * Create a new Redirect instance * @param options Configuration options */ constructor(options: { httpPort?: number; httpsPort?: number; sslOptions?: { key: Buffer; cert: Buffer; }; rules?: RedirectRule[]; } = {}) { if (options.httpPort) this.httpPort = options.httpPort; if (options.httpsPort) this.httpsPort = options.httpsPort; if (options.sslOptions) this.sslOptions = options.sslOptions; if (options.rules) this.rules = options.rules; } /** * Add a redirect rule */ public addRule(rule: RedirectRule): void { this.rules.push(rule); } /** * Remove all redirect rules */ public clearRules(): void { this.rules = []; } /** * Set SSL options for HTTPS redirects */ public setSslOptions(options: { key: Buffer; cert: Buffer }): void { this.sslOptions = options; } /** * Process a request according to the configured rules */ private handleRequest( request: plugins.http.IncomingMessage, response: plugins.http.ServerResponse, protocol: 'http' | 'https' ): void { const requestUrl = new URL( request.url || '/', `${protocol}://${request.headers.host || 'localhost'}` ); const host = requestUrl.hostname; const path = requestUrl.pathname + requestUrl.search; // Find matching rule const matchedRule = this.findMatchingRule(protocol, host, path); if (matchedRule) { const targetUrl = this.buildTargetUrl(matchedRule, host, path); console.log(`Redirecting ${protocol}://${host}${path} to ${targetUrl}`); response.writeHead(matchedRule.statusCode || 302, { Location: targetUrl, }); response.end(); } else { // No matching rule, send 404 response.writeHead(404, { 'Content-Type': 'text/plain' }); response.end('Not Found'); } } /** * Find a matching redirect rule for the given request */ private findMatchingRule( protocol: 'http' | 'https', host: string, path: string ): RedirectRule | undefined { return this.rules.find((rule) => { // Check protocol match if (rule.fromProtocol && rule.fromProtocol !== protocol) { return false; } // Check host match if (rule.fromHost) { const pattern = rule.fromHost.replace(/\*/g, '(.*)'); const regex = new RegExp(`^${pattern}$`); if (!regex.test(host)) { return false; } } // Check path match if (rule.fromPath && !path.startsWith(rule.fromPath)) { return false; } return true; }); } /** * Build the target URL for a redirect */ private buildTargetUrl(rule: RedirectRule, originalHost: string, originalPath: string): string { let targetHost = rule.toHost; // Replace wildcards in host if (rule.fromHost && rule.fromHost.includes('*')) { const pattern = rule.fromHost.replace(/\*/g, '(.*)'); const regex = new RegExp(`^${pattern}$`); const matches = originalHost.match(regex); if (matches) { for (let i = 1; i < matches.length; i++) { targetHost = targetHost.replace(`$${i}`, matches[i]); } } } // Build target path let targetPath = originalPath; if (rule.toPath) { if (rule.toPath.includes('$path')) { // Replace $path with original path, optionally removing the fromPath prefix const pathSuffix = rule.fromPath ? originalPath.substring(rule.fromPath.length) : originalPath; targetPath = rule.toPath.replace('$path', pathSuffix); } else { targetPath = rule.toPath; } } return `${rule.toProtocol}://${targetHost}${targetPath}`; } /** * Start the redirect server(s) */ public async start(): Promise { const tasks = []; // Create and start HTTP server if we have a port if (this.httpPort) { this.httpServer = plugins.http.createServer((req, res) => this.handleRequest(req, res, 'http') ); const httpStartPromise = new Promise((resolve) => { this.httpServer?.listen(this.httpPort, () => { console.log(`HTTP redirect server started on port ${this.httpPort}`); resolve(); }); }); tasks.push(httpStartPromise); } // Create and start HTTPS server if we have SSL options and a port if (this.httpsPort && this.sslOptions) { this.httpsServer = plugins.https.createServer(this.sslOptions, (req, res) => this.handleRequest(req, res, 'https') ); const httpsStartPromise = new Promise((resolve) => { this.httpsServer?.listen(this.httpsPort, () => { console.log(`HTTPS redirect server started on port ${this.httpsPort}`); resolve(); }); }); tasks.push(httpsStartPromise); } // Wait for all servers to start await Promise.all(tasks); } /** * Stop the redirect server(s) */ public async stop(): Promise { const tasks = []; if (this.httpServer) { const httpStopPromise = new Promise((resolve) => { this.httpServer?.close(() => { console.log('HTTP redirect server stopped'); resolve(); }); }); tasks.push(httpStopPromise); } if (this.httpsServer) { const httpsStopPromise = new Promise((resolve) => { this.httpsServer?.close(() => { console.log('HTTPS redirect server stopped'); resolve(); }); }); tasks.push(httpsStopPromise); } await Promise.all(tasks); } } // For backward compatibility export class SslRedirect { private redirect: Redirect; port: number; constructor(portArg: number) { this.port = portArg; this.redirect = new Redirect({ httpPort: portArg, rules: [{ fromProtocol: 'http', toProtocol: 'https', toHost: '$1', statusCode: 302 }] }); } public async start() { await this.redirect.start(); } public async stop() { await this.redirect.stop(); } }