@push.rocks/smartproxy 20.0.0 → 21.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,189 +0,0 @@
1
- import type { IForwardConfig } from '../config/forwarding-types.js';
2
- import { ForwardingHandler } from '../handlers/base-handler.js';
3
- import { HttpForwardingHandler } from '../handlers/http-handler.js';
4
- import { HttpsPassthroughHandler } from '../handlers/https-passthrough-handler.js';
5
- import { HttpsTerminateToHttpHandler } from '../handlers/https-terminate-to-http-handler.js';
6
- import { HttpsTerminateToHttpsHandler } from '../handlers/https-terminate-to-https-handler.js';
7
-
8
- /**
9
- * Factory for creating forwarding handlers based on the configuration type
10
- */
11
- export class ForwardingHandlerFactory {
12
- /**
13
- * Create a forwarding handler based on the configuration
14
- * @param config The forwarding configuration
15
- * @returns The appropriate forwarding handler
16
- */
17
- public static createHandler(config: IForwardConfig): ForwardingHandler {
18
- // Create the appropriate handler based on the forwarding type
19
- switch (config.type) {
20
- case 'http-only':
21
- return new HttpForwardingHandler(config);
22
-
23
- case 'https-passthrough':
24
- return new HttpsPassthroughHandler(config);
25
-
26
- case 'https-terminate-to-http':
27
- return new HttpsTerminateToHttpHandler(config);
28
-
29
- case 'https-terminate-to-https':
30
- return new HttpsTerminateToHttpsHandler(config);
31
-
32
- default:
33
- // Type system should prevent this, but just in case:
34
- throw new Error(`Unknown forwarding type: ${(config as any).type}`);
35
- }
36
- }
37
-
38
- /**
39
- * Apply default values to a forwarding configuration based on its type
40
- * @param config The original forwarding configuration
41
- * @returns A configuration with defaults applied
42
- */
43
- public static applyDefaults(config: IForwardConfig): IForwardConfig {
44
- // Create a deep copy of the configuration
45
- const result: IForwardConfig = JSON.parse(JSON.stringify(config));
46
-
47
- // Apply defaults based on forwarding type
48
- switch (config.type) {
49
- case 'http-only':
50
- // Set defaults for HTTP-only mode
51
- result.http = {
52
- enabled: true,
53
- ...config.http
54
- };
55
- // Set default port and socket if not provided
56
- if (!result.port) {
57
- result.port = 80;
58
- }
59
- if (!result.socket) {
60
- result.socket = `/tmp/forwarding-${config.type}-${result.port}.sock`;
61
- }
62
- break;
63
-
64
- case 'https-passthrough':
65
- // Set defaults for HTTPS passthrough
66
- result.https = {
67
- forwardSni: true,
68
- ...config.https
69
- };
70
- // SNI forwarding doesn't do HTTP
71
- result.http = {
72
- enabled: false,
73
- ...config.http
74
- };
75
- // Set default port and socket if not provided
76
- if (!result.port) {
77
- result.port = 443;
78
- }
79
- if (!result.socket) {
80
- result.socket = `/tmp/forwarding-${config.type}-${result.port}.sock`;
81
- }
82
- break;
83
-
84
- case 'https-terminate-to-http':
85
- // Set defaults for HTTPS termination to HTTP
86
- result.https = {
87
- ...config.https
88
- };
89
- // Support HTTP access by default in this mode
90
- result.http = {
91
- enabled: true,
92
- redirectToHttps: true,
93
- ...config.http
94
- };
95
- // Enable ACME by default
96
- result.acme = {
97
- enabled: true,
98
- maintenance: true,
99
- ...config.acme
100
- };
101
- // Set default port and socket if not provided
102
- if (!result.port) {
103
- result.port = 443;
104
- }
105
- if (!result.socket) {
106
- result.socket = `/tmp/forwarding-${config.type}-${result.port}.sock`;
107
- }
108
- break;
109
-
110
- case 'https-terminate-to-https':
111
- // Similar to terminate-to-http but with different target handling
112
- result.https = {
113
- ...config.https
114
- };
115
- result.http = {
116
- enabled: true,
117
- redirectToHttps: true,
118
- ...config.http
119
- };
120
- result.acme = {
121
- enabled: true,
122
- maintenance: true,
123
- ...config.acme
124
- };
125
- // Set default port and socket if not provided
126
- if (!result.port) {
127
- result.port = 443;
128
- }
129
- if (!result.socket) {
130
- result.socket = `/tmp/forwarding-${config.type}-${result.port}.sock`;
131
- }
132
- break;
133
- }
134
-
135
- return result;
136
- }
137
-
138
- /**
139
- * Validate a forwarding configuration
140
- * @param config The configuration to validate
141
- * @throws Error if the configuration is invalid
142
- */
143
- public static validateConfig(config: IForwardConfig): void {
144
- // Validate common properties
145
- if (!config.target) {
146
- throw new Error('Forwarding configuration must include a target');
147
- }
148
-
149
- if (!config.target.host || (Array.isArray(config.target.host) && config.target.host.length === 0)) {
150
- throw new Error('Target must include a host or array of hosts');
151
- }
152
-
153
- // Validate port if it's a number
154
- if (typeof config.target.port === 'number') {
155
- if (config.target.port <= 0 || config.target.port > 65535) {
156
- throw new Error('Target must include a valid port (1-65535)');
157
- }
158
- } else if (config.target.port !== 'preserve' && typeof config.target.port !== 'function') {
159
- throw new Error('Target port must be a number, "preserve", or a function');
160
- }
161
-
162
- // Type-specific validation
163
- switch (config.type) {
164
- case 'http-only':
165
- // HTTP-only needs http.enabled to be true
166
- if (config.http?.enabled === false) {
167
- throw new Error('HTTP-only forwarding must have HTTP enabled');
168
- }
169
- break;
170
-
171
- case 'https-passthrough':
172
- // HTTPS passthrough doesn't support HTTP
173
- if (config.http?.enabled === true) {
174
- throw new Error('HTTPS passthrough does not support HTTP');
175
- }
176
-
177
- // HTTPS passthrough doesn't work with ACME
178
- if (config.acme?.enabled === true) {
179
- throw new Error('HTTPS passthrough does not support ACME');
180
- }
181
- break;
182
-
183
- case 'https-terminate-to-http':
184
- case 'https-terminate-to-https':
185
- // These modes support all options, nothing specific to validate
186
- break;
187
- }
188
- }
189
- }
@@ -1,5 +0,0 @@
1
- /**
2
- * Forwarding factory implementations
3
- */
4
-
5
- export { ForwardingHandlerFactory } from './forwarding-factory.js';
@@ -1,155 +0,0 @@
1
- import * as plugins from '../../plugins.js';
2
- import type {
3
- IForwardConfig,
4
- IForwardingHandler
5
- } from '../config/forwarding-types.js';
6
- import { ForwardingHandlerEvents } from '../config/forwarding-types.js';
7
-
8
- /**
9
- * Base class for all forwarding handlers
10
- */
11
- export abstract class ForwardingHandler extends plugins.EventEmitter implements IForwardingHandler {
12
- /**
13
- * Create a new ForwardingHandler
14
- * @param config The forwarding configuration
15
- */
16
- constructor(protected config: IForwardConfig) {
17
- super();
18
- }
19
-
20
- /**
21
- * Initialize the handler
22
- * Base implementation does nothing, subclasses should override as needed
23
- */
24
- public async initialize(): Promise<void> {
25
- // Base implementation - no initialization needed
26
- }
27
-
28
- /**
29
- * Handle a new socket connection
30
- * @param socket The incoming socket connection
31
- */
32
- public abstract handleConnection(socket: plugins.net.Socket): void;
33
-
34
- /**
35
- * Handle an HTTP request
36
- * @param req The HTTP request
37
- * @param res The HTTP response
38
- */
39
- public abstract handleHttpRequest(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse): void;
40
-
41
- /**
42
- * Get a target from the configuration, supporting round-robin selection
43
- * @param incomingPort Optional incoming port for 'preserve' mode
44
- * @returns A resolved target object with host and port
45
- */
46
- protected getTargetFromConfig(incomingPort: number = 80): { host: string, port: number } {
47
- const { target } = this.config;
48
-
49
- // Handle round-robin host selection
50
- if (Array.isArray(target.host)) {
51
- if (target.host.length === 0) {
52
- throw new Error('No target hosts specified');
53
- }
54
-
55
- // Simple round-robin selection
56
- const randomIndex = Math.floor(Math.random() * target.host.length);
57
- return {
58
- host: target.host[randomIndex],
59
- port: this.resolvePort(target.port, incomingPort)
60
- };
61
- }
62
-
63
- // Single host
64
- return {
65
- host: target.host,
66
- port: this.resolvePort(target.port, incomingPort)
67
- };
68
- }
69
-
70
- /**
71
- * Resolves a port value, handling 'preserve' and function ports
72
- * @param port The port value to resolve
73
- * @param incomingPort Optional incoming port to use for 'preserve' mode
74
- */
75
- protected resolvePort(
76
- port: number | 'preserve' | ((ctx: any) => number),
77
- incomingPort: number = 80
78
- ): number {
79
- if (typeof port === 'function') {
80
- try {
81
- // Create a minimal context for the function that includes the incoming port
82
- const ctx = { port: incomingPort };
83
- return port(ctx);
84
- } catch (err) {
85
- console.error('Error resolving port function:', err);
86
- return incomingPort; // Fall back to incoming port
87
- }
88
- } else if (port === 'preserve') {
89
- return incomingPort; // Use the actual incoming port for 'preserve'
90
- } else {
91
- return port;
92
- }
93
- }
94
-
95
- /**
96
- * Redirect an HTTP request to HTTPS
97
- * @param req The HTTP request
98
- * @param res The HTTP response
99
- */
100
- protected redirectToHttps(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse): void {
101
- const host = req.headers.host || '';
102
- const path = req.url || '/';
103
- const redirectUrl = `https://${host}${path}`;
104
-
105
- res.writeHead(301, {
106
- 'Location': redirectUrl,
107
- 'Cache-Control': 'no-cache'
108
- });
109
- res.end(`Redirecting to ${redirectUrl}`);
110
-
111
- this.emit(ForwardingHandlerEvents.HTTP_RESPONSE, {
112
- statusCode: 301,
113
- headers: { 'Location': redirectUrl },
114
- size: 0
115
- });
116
- }
117
-
118
- /**
119
- * Apply custom headers from configuration
120
- * @param headers The original headers
121
- * @param variables Variables to replace in the headers
122
- * @returns The headers with custom values applied
123
- */
124
- protected applyCustomHeaders(
125
- headers: Record<string, string | string[] | undefined>,
126
- variables: Record<string, string>
127
- ): Record<string, string | string[] | undefined> {
128
- const customHeaders = this.config.advanced?.headers || {};
129
- const result = { ...headers };
130
-
131
- // Apply custom headers with variable substitution
132
- for (const [key, value] of Object.entries(customHeaders)) {
133
- if (typeof value !== 'string') continue;
134
-
135
- let processedValue = value;
136
-
137
- // Replace variables in the header value
138
- for (const [varName, varValue] of Object.entries(variables)) {
139
- processedValue = processedValue.replace(`{${varName}}`, varValue);
140
- }
141
-
142
- result[key] = processedValue;
143
- }
144
-
145
- return result;
146
- }
147
-
148
- /**
149
- * Get the timeout for this connection from configuration
150
- * @returns Timeout in milliseconds
151
- */
152
- protected getTimeout(): number {
153
- return this.config.advanced?.timeout || 60000; // Default: 60 seconds
154
- }
155
- }
@@ -1,163 +0,0 @@
1
- import * as plugins from '../../plugins.js';
2
- import { ForwardingHandler } from './base-handler.js';
3
- import type { IForwardConfig } from '../config/forwarding-types.js';
4
- import { ForwardingHandlerEvents } from '../config/forwarding-types.js';
5
- import { setupSocketHandlers } from '../../core/utils/socket-utils.js';
6
-
7
- /**
8
- * Handler for HTTP-only forwarding
9
- */
10
- export class HttpForwardingHandler extends ForwardingHandler {
11
- /**
12
- * Create a new HTTP forwarding handler
13
- * @param config The forwarding configuration
14
- */
15
- constructor(config: IForwardConfig) {
16
- super(config);
17
-
18
- // Validate that this is an HTTP-only configuration
19
- if (config.type !== 'http-only') {
20
- throw new Error(`Invalid configuration type for HttpForwardingHandler: ${config.type}`);
21
- }
22
- }
23
-
24
- /**
25
- * Initialize the handler
26
- * HTTP handler doesn't need special initialization
27
- */
28
- public async initialize(): Promise<void> {
29
- // Basic initialization from parent class
30
- await super.initialize();
31
- }
32
-
33
- /**
34
- * Handle a raw socket connection
35
- * HTTP handler doesn't do much with raw sockets as it mainly processes
36
- * parsed HTTP requests
37
- */
38
- public handleConnection(socket: plugins.net.Socket): void {
39
- // For HTTP, we mainly handle parsed requests, but we can still set up
40
- // some basic connection tracking
41
- const remoteAddress = socket.remoteAddress || 'unknown';
42
- const localPort = socket.localPort || 80;
43
-
44
- // Set up socket handlers with proper cleanup
45
- const handleClose = (reason: string) => {
46
- this.emit(ForwardingHandlerEvents.DISCONNECTED, {
47
- remoteAddress,
48
- reason
49
- });
50
- };
51
-
52
- // Use custom timeout handler that doesn't close the socket
53
- setupSocketHandlers(socket, handleClose, () => {
54
- // For HTTP, we can be more aggressive with timeouts since connections are shorter
55
- // But still don't close immediately - let the connection finish naturally
56
- console.warn(`HTTP socket timeout from ${remoteAddress}`);
57
- }, 'http');
58
-
59
- socket.on('error', (error) => {
60
- this.emit(ForwardingHandlerEvents.ERROR, {
61
- remoteAddress,
62
- error: error.message
63
- });
64
- });
65
-
66
- this.emit(ForwardingHandlerEvents.CONNECTED, {
67
- remoteAddress,
68
- localPort
69
- });
70
- }
71
-
72
- /**
73
- * Handle an HTTP request
74
- * @param req The HTTP request
75
- * @param res The HTTP response
76
- */
77
- public handleHttpRequest(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse): void {
78
- // Get the local port from the request (for 'preserve' port handling)
79
- const localPort = req.socket.localPort || 80;
80
-
81
- // Get the target from configuration, passing the incoming port
82
- const target = this.getTargetFromConfig(localPort);
83
-
84
- // Create a custom headers object with variables for substitution
85
- const variables = {
86
- clientIp: req.socket.remoteAddress || 'unknown'
87
- };
88
-
89
- // Prepare headers, merging with any custom headers from config
90
- const headers = this.applyCustomHeaders(req.headers, variables);
91
-
92
- // Create the proxy request options
93
- const options = {
94
- hostname: target.host,
95
- port: target.port,
96
- path: req.url,
97
- method: req.method,
98
- headers
99
- };
100
-
101
- // Create the proxy request
102
- const proxyReq = plugins.http.request(options, (proxyRes) => {
103
- // Copy status code and headers from the proxied response
104
- res.writeHead(proxyRes.statusCode || 500, proxyRes.headers);
105
-
106
- // Pipe the proxy response to the client response
107
- proxyRes.pipe(res);
108
-
109
- // Track bytes for logging
110
- let responseSize = 0;
111
- proxyRes.on('data', (chunk) => {
112
- responseSize += chunk.length;
113
- });
114
-
115
- proxyRes.on('end', () => {
116
- this.emit(ForwardingHandlerEvents.HTTP_RESPONSE, {
117
- statusCode: proxyRes.statusCode,
118
- headers: proxyRes.headers,
119
- size: responseSize
120
- });
121
- });
122
- });
123
-
124
- // Handle errors in the proxy request
125
- proxyReq.on('error', (error) => {
126
- this.emit(ForwardingHandlerEvents.ERROR, {
127
- remoteAddress: req.socket.remoteAddress,
128
- error: `Proxy request error: ${error.message}`
129
- });
130
-
131
- // Send an error response if headers haven't been sent yet
132
- if (!res.headersSent) {
133
- res.writeHead(502, { 'Content-Type': 'text/plain' });
134
- res.end(`Error forwarding request: ${error.message}`);
135
- } else {
136
- // Just end the response if headers have already been sent
137
- res.end();
138
- }
139
- });
140
-
141
- // Track request details for logging
142
- let requestSize = 0;
143
- req.on('data', (chunk) => {
144
- requestSize += chunk.length;
145
- });
146
-
147
- // Log the request
148
- this.emit(ForwardingHandlerEvents.HTTP_REQUEST, {
149
- method: req.method,
150
- url: req.url,
151
- headers: req.headers,
152
- remoteAddress: req.socket.remoteAddress,
153
- target: `${target.host}:${target.port}`
154
- });
155
-
156
- // Pipe the client request to the proxy request
157
- if (req.readable) {
158
- req.pipe(proxyReq);
159
- } else {
160
- proxyReq.end();
161
- }
162
- }
163
- }
@@ -1,185 +0,0 @@
1
- import * as plugins from '../../plugins.js';
2
- import { ForwardingHandler } from './base-handler.js';
3
- import type { IForwardConfig } from '../config/forwarding-types.js';
4
- import { ForwardingHandlerEvents } from '../config/forwarding-types.js';
5
- import { createIndependentSocketHandlers, setupSocketHandlers, createSocketWithErrorHandler } from '../../core/utils/socket-utils.js';
6
-
7
- /**
8
- * Handler for HTTPS passthrough (SNI forwarding without termination)
9
- */
10
- export class HttpsPassthroughHandler extends ForwardingHandler {
11
- /**
12
- * Create a new HTTPS passthrough handler
13
- * @param config The forwarding configuration
14
- */
15
- constructor(config: IForwardConfig) {
16
- super(config);
17
-
18
- // Validate that this is an HTTPS passthrough configuration
19
- if (config.type !== 'https-passthrough') {
20
- throw new Error(`Invalid configuration type for HttpsPassthroughHandler: ${config.type}`);
21
- }
22
- }
23
-
24
- /**
25
- * Initialize the handler
26
- * HTTPS passthrough handler doesn't need special initialization
27
- */
28
- public async initialize(): Promise<void> {
29
- // Basic initialization from parent class
30
- await super.initialize();
31
- }
32
-
33
- /**
34
- * Handle a TLS/SSL socket connection by forwarding it without termination
35
- * @param clientSocket The incoming socket from the client
36
- */
37
- public handleConnection(clientSocket: plugins.net.Socket): void {
38
- // Get the target from configuration
39
- const target = this.getTargetFromConfig();
40
-
41
- // Log the connection
42
- const remoteAddress = clientSocket.remoteAddress || 'unknown';
43
- const remotePort = clientSocket.remotePort || 0;
44
-
45
- this.emit(ForwardingHandlerEvents.CONNECTED, {
46
- remoteAddress,
47
- remotePort,
48
- target: `${target.host}:${target.port}`
49
- });
50
-
51
- // Track data transfer for logging
52
- let bytesSent = 0;
53
- let bytesReceived = 0;
54
- let serverSocket: plugins.net.Socket | null = null;
55
- let cleanupClient: ((reason: string) => Promise<void>) | null = null;
56
- let cleanupServer: ((reason: string) => Promise<void>) | null = null;
57
-
58
- // Create a connection to the target server with immediate error handling
59
- serverSocket = createSocketWithErrorHandler({
60
- port: target.port,
61
- host: target.host,
62
- onError: async (error) => {
63
- // Server connection failed - clean up client socket immediately
64
- this.emit(ForwardingHandlerEvents.ERROR, {
65
- error: error.message,
66
- code: (error as any).code || 'UNKNOWN',
67
- remoteAddress,
68
- target: `${target.host}:${target.port}`
69
- });
70
-
71
- // Clean up the client socket since we can't forward
72
- if (!clientSocket.destroyed) {
73
- clientSocket.destroy();
74
- }
75
-
76
- this.emit(ForwardingHandlerEvents.DISCONNECTED, {
77
- remoteAddress,
78
- bytesSent: 0,
79
- bytesReceived: 0,
80
- reason: `server_connection_failed: ${error.message}`
81
- });
82
- },
83
- onConnect: () => {
84
- // Connection successful - set up forwarding handlers
85
- const handlers = createIndependentSocketHandlers(
86
- clientSocket,
87
- serverSocket!,
88
- (reason) => {
89
- this.emit(ForwardingHandlerEvents.DISCONNECTED, {
90
- remoteAddress,
91
- bytesSent,
92
- bytesReceived,
93
- reason
94
- });
95
- }
96
- );
97
-
98
- cleanupClient = handlers.cleanupClient;
99
- cleanupServer = handlers.cleanupServer;
100
-
101
- // Setup handlers with custom timeout handling that doesn't close connections
102
- const timeout = this.getTimeout();
103
-
104
- setupSocketHandlers(clientSocket, cleanupClient, (socket) => {
105
- // Just reset timeout, don't close
106
- socket.setTimeout(timeout);
107
- }, 'client');
108
-
109
- setupSocketHandlers(serverSocket!, cleanupServer, (socket) => {
110
- // Just reset timeout, don't close
111
- socket.setTimeout(timeout);
112
- }, 'server');
113
-
114
- // Forward data from client to server
115
- clientSocket.on('data', (data) => {
116
- bytesSent += data.length;
117
-
118
- // Check if server socket is writable
119
- if (serverSocket && serverSocket.writable) {
120
- const flushed = serverSocket.write(data);
121
-
122
- // Handle backpressure
123
- if (!flushed) {
124
- clientSocket.pause();
125
- serverSocket.once('drain', () => {
126
- clientSocket.resume();
127
- });
128
- }
129
- }
130
-
131
- this.emit(ForwardingHandlerEvents.DATA_FORWARDED, {
132
- direction: 'outbound',
133
- bytes: data.length,
134
- total: bytesSent
135
- });
136
- });
137
-
138
- // Forward data from server to client
139
- serverSocket!.on('data', (data) => {
140
- bytesReceived += data.length;
141
-
142
- // Check if client socket is writable
143
- if (clientSocket.writable) {
144
- const flushed = clientSocket.write(data);
145
-
146
- // Handle backpressure
147
- if (!flushed) {
148
- serverSocket!.pause();
149
- clientSocket.once('drain', () => {
150
- serverSocket!.resume();
151
- });
152
- }
153
- }
154
-
155
- this.emit(ForwardingHandlerEvents.DATA_FORWARDED, {
156
- direction: 'inbound',
157
- bytes: data.length,
158
- total: bytesReceived
159
- });
160
- });
161
-
162
- // Set initial timeouts - they will be reset on each timeout event
163
- clientSocket.setTimeout(timeout);
164
- serverSocket!.setTimeout(timeout);
165
- }
166
- });
167
- }
168
-
169
- /**
170
- * Handle an HTTP request - HTTPS passthrough doesn't support HTTP
171
- * @param req The HTTP request
172
- * @param res The HTTP response
173
- */
174
- public handleHttpRequest(_req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse): void {
175
- // HTTPS passthrough doesn't support HTTP requests
176
- res.writeHead(404, { 'Content-Type': 'text/plain' });
177
- res.end('HTTP not supported for this domain');
178
-
179
- this.emit(ForwardingHandlerEvents.HTTP_RESPONSE, {
180
- statusCode: 404,
181
- headers: { 'Content-Type': 'text/plain' },
182
- size: 'HTTP not supported for this domain'.length
183
- });
184
- }
185
- }