BREAKING CHANGE(redirect): Remove deprecated SSL redirect implementation and update exports to use the new redirect module
This commit is contained in:
		
							
								
								
									
										295
									
								
								ts/redirect/classes.redirect.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										295
									
								
								ts/redirect/classes.redirect.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,295 @@ | ||||
| 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(); | ||||
|   } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user