BREAKING CHANGE(redirect): Remove deprecated SSL redirect implementation and update exports to use the new redirect module
This commit is contained in:
		| @@ -1,5 +1,12 @@ | ||||
| # Changelog | ||||
|  | ||||
| ## 2025-04-04 - 7.0.0 - BREAKING CHANGE(redirect) | ||||
| Remove deprecated SSL redirect implementation and update exports to use the new redirect module | ||||
|  | ||||
| - Deleted ts/classes.sslredirect.ts which contained the old SSL redirect logic | ||||
| - Updated ts/index.ts to export 'redirect/classes.redirect.js' instead of the removed SSL redirect module | ||||
| - Adopted a new redirect implementation that provides enhanced features and a more consistent API | ||||
|  | ||||
| ## 2025-03-25 - 6.0.1 - fix(readme) | ||||
| Update README documentation: replace all outdated 'PortProxy' references with 'SmartProxy', adjust architecture diagrams, code examples, and configuration details (including correcting IPTables to NfTables) to reflect the new naming. | ||||
|  | ||||
|   | ||||
| @@ -3,6 +3,6 @@ | ||||
|  */ | ||||
| export const commitinfo = { | ||||
|   name: '@push.rocks/smartproxy', | ||||
|   version: '6.0.1', | ||||
|   version: '7.0.0', | ||||
|   description: 'A powerful proxy package that effectively handles high traffic, with features such as SSL/TLS support, port proxying, WebSocket handling, dynamic routing with authentication options, and automatic ACME certificate management.' | ||||
| } | ||||
|   | ||||
| @@ -1,32 +0,0 @@ | ||||
| import * as plugins from './plugins.js'; | ||||
|  | ||||
| export class SslRedirect { | ||||
|   httpServer: plugins.http.Server; | ||||
|   port: number; | ||||
|   constructor(portArg: number) { | ||||
|     this.port = portArg; | ||||
|   } | ||||
|  | ||||
|   public async start() { | ||||
|     this.httpServer = plugins.http.createServer((request, response) => { | ||||
|       const requestUrl = new URL(request.url, `http://${request.headers.host}`); | ||||
|       const completeUrlWithoutProtocol = `${requestUrl.host}${requestUrl.pathname}${requestUrl.search}`; | ||||
|       const redirectUrl = `https://${completeUrlWithoutProtocol}`; | ||||
|       console.log(`Got http request for http://${completeUrlWithoutProtocol}`); | ||||
|       console.log(`Redirecting to ${redirectUrl}`); | ||||
|       response.writeHead(302, { | ||||
|         Location: redirectUrl, | ||||
|       }); | ||||
|       response.end(); | ||||
|     }); | ||||
|     this.httpServer.listen(this.port); | ||||
|   } | ||||
|  | ||||
|   public async stop() { | ||||
|     const done = plugins.smartpromise.defer(); | ||||
|     this.httpServer.close(() => { | ||||
|       done.resolve(); | ||||
|     }); | ||||
|     await done.promise; | ||||
|   } | ||||
| } | ||||
| @@ -1,7 +1,7 @@ | ||||
| export * from './nfttablesproxy/classes.nftablesproxy.js'; | ||||
| export * from './networkproxy/classes.np.networkproxy.js'; | ||||
| export * from './port80handler/classes.port80handler.js'; | ||||
| export * from './classes.sslredirect.js'; | ||||
| export * from './redirect/classes.redirect.js'; | ||||
| export * from './smartproxy/classes.smartproxy.js'; | ||||
| export * from './smartproxy/classes.pp.snihandler.js'; | ||||
| export * from './smartproxy/classes.pp.interfaces.js'; | ||||
|   | ||||
							
								
								
									
										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