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 | # 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) | ## 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. | 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 = { | export const commitinfo = { | ||||||
|   name: '@push.rocks/smartproxy', |   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.' |   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 './nfttablesproxy/classes.nftablesproxy.js'; | ||||||
| export * from './networkproxy/classes.np.networkproxy.js'; | export * from './networkproxy/classes.np.networkproxy.js'; | ||||||
| export * from './port80handler/classes.port80handler.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.smartproxy.js'; | ||||||
| export * from './smartproxy/classes.pp.snihandler.js'; | export * from './smartproxy/classes.pp.snihandler.js'; | ||||||
| export * from './smartproxy/classes.pp.interfaces.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