295 lines
7.3 KiB
TypeScript
295 lines
7.3 KiB
TypeScript
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<void> {
|
|
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<void>((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<void>((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<void> {
|
|
const tasks = [];
|
|
|
|
if (this.httpServer) {
|
|
const httpStopPromise = new Promise<void>((resolve) => {
|
|
this.httpServer?.close(() => {
|
|
console.log('HTTP redirect server stopped');
|
|
resolve();
|
|
});
|
|
});
|
|
tasks.push(httpStopPromise);
|
|
}
|
|
|
|
if (this.httpsServer) {
|
|
const httpsStopPromise = new Promise<void>((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();
|
|
}
|
|
} |