@push.rocks/smartproxy 19.4.2 → 19.5.2
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.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/proxies/http-proxy/handlers/index.d.ts +1 -2
- package/dist_ts/proxies/http-proxy/handlers/index.js +3 -3
- package/dist_ts/proxies/smart-proxy/certificate-manager.js +30 -25
- package/dist_ts/proxies/smart-proxy/models/route-types.d.ts +9 -40
- package/dist_ts/proxies/smart-proxy/models/route-types.js +1 -1
- package/dist_ts/proxies/smart-proxy/route-connection-handler.d.ts +2 -10
- package/dist_ts/proxies/smart-proxy/route-connection-handler.js +69 -43
- package/dist_ts/proxies/smart-proxy/utils/index.d.ts +2 -2
- package/dist_ts/proxies/smart-proxy/utils/index.js +3 -3
- package/dist_ts/proxies/smart-proxy/utils/route-helpers.d.ts +61 -20
- package/dist_ts/proxies/smart-proxy/utils/route-helpers.js +240 -45
- package/dist_ts/proxies/smart-proxy/utils/route-patterns.d.ts +0 -18
- package/dist_ts/proxies/smart-proxy/utils/route-patterns.js +4 -43
- package/dist_ts/proxies/smart-proxy/utils/route-utils.js +14 -15
- package/dist_ts/proxies/smart-proxy/utils/route-validators.js +10 -31
- package/package.json +7 -7
- package/readme.hints.md +38 -1
- package/readme.plan.md +314 -382
- package/readme.plan2.md +764 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/proxies/http-proxy/handlers/index.ts +1 -2
- package/ts/proxies/smart-proxy/certificate-manager.ts +29 -23
- package/ts/proxies/smart-proxy/models/route-types.ts +12 -56
- package/ts/proxies/smart-proxy/route-connection-handler.ts +73 -60
- package/ts/proxies/smart-proxy/utils/index.ts +0 -2
- package/ts/proxies/smart-proxy/utils/route-helpers.ts +278 -61
- package/ts/proxies/smart-proxy/utils/route-patterns.ts +6 -56
- package/ts/proxies/smart-proxy/utils/route-utils.ts +12 -15
- package/ts/proxies/smart-proxy/utils/route-validators.ts +9 -31
- package/ts/proxies/http-proxy/handlers/redirect-handler.ts +0 -105
- package/ts/proxies/http-proxy/handlers/static-handler.ts +0 -261
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
* - HTTPS passthrough routes (createHttpsPassthroughRoute)
|
|
12
12
|
* - Complete HTTPS servers with redirects (createCompleteHttpsServer)
|
|
13
13
|
* - Load balancer routes (createLoadBalancerRoute)
|
|
14
|
-
* - Static file server routes (createStaticFileRoute)
|
|
15
14
|
* - API routes (createApiRoute)
|
|
16
15
|
* - WebSocket routes (createWebSocketRoute)
|
|
17
16
|
* - Port mapping routes (createPortMappingRoute, createOffsetPortMappingRoute)
|
|
@@ -19,6 +18,7 @@
|
|
|
19
18
|
* - NFTables routes (createNfTablesRoute, createNfTablesTerminateRoute)
|
|
20
19
|
*/
|
|
21
20
|
|
|
21
|
+
import * as plugins from '../../../plugins.js';
|
|
22
22
|
import type { IRouteConfig, IRouteMatch, IRouteAction, IRouteTarget, TPortRange, IRouteContext } from '../models/route-types.js';
|
|
23
23
|
|
|
24
24
|
/**
|
|
@@ -118,11 +118,8 @@ export function createHttpToHttpsRedirect(
|
|
|
118
118
|
|
|
119
119
|
// Create route action
|
|
120
120
|
const action: IRouteAction = {
|
|
121
|
-
type: '
|
|
122
|
-
|
|
123
|
-
to: `https://{domain}:${httpsPort}{path}`,
|
|
124
|
-
status: 301
|
|
125
|
-
}
|
|
121
|
+
type: 'socket-handler',
|
|
122
|
+
socketHandler: SocketHandlers.httpRedirect(`https://{domain}:${httpsPort}{path}`, 301)
|
|
126
123
|
};
|
|
127
124
|
|
|
128
125
|
// Create the route config
|
|
@@ -266,60 +263,6 @@ export function createLoadBalancerRoute(
|
|
|
266
263
|
};
|
|
267
264
|
}
|
|
268
265
|
|
|
269
|
-
/**
|
|
270
|
-
* Create a static file server route
|
|
271
|
-
* @param domains Domain(s) to match
|
|
272
|
-
* @param rootDir Root directory path for static files
|
|
273
|
-
* @param options Additional route options
|
|
274
|
-
* @returns Route configuration object
|
|
275
|
-
*/
|
|
276
|
-
export function createStaticFileRoute(
|
|
277
|
-
domains: string | string[],
|
|
278
|
-
rootDir: string,
|
|
279
|
-
options: {
|
|
280
|
-
indexFiles?: string[];
|
|
281
|
-
serveOnHttps?: boolean;
|
|
282
|
-
certificate?: 'auto' | { key: string; cert: string };
|
|
283
|
-
httpPort?: number | number[];
|
|
284
|
-
httpsPort?: number | number[];
|
|
285
|
-
name?: string;
|
|
286
|
-
[key: string]: any;
|
|
287
|
-
} = {}
|
|
288
|
-
): IRouteConfig {
|
|
289
|
-
// Create route match
|
|
290
|
-
const match: IRouteMatch = {
|
|
291
|
-
ports: options.serveOnHttps
|
|
292
|
-
? (options.httpsPort || 443)
|
|
293
|
-
: (options.httpPort || 80),
|
|
294
|
-
domains
|
|
295
|
-
};
|
|
296
|
-
|
|
297
|
-
// Create route action
|
|
298
|
-
const action: IRouteAction = {
|
|
299
|
-
type: 'static',
|
|
300
|
-
static: {
|
|
301
|
-
root: rootDir,
|
|
302
|
-
index: options.indexFiles || ['index.html', 'index.htm']
|
|
303
|
-
}
|
|
304
|
-
};
|
|
305
|
-
|
|
306
|
-
// Add TLS configuration if serving on HTTPS
|
|
307
|
-
if (options.serveOnHttps) {
|
|
308
|
-
action.tls = {
|
|
309
|
-
mode: 'terminate',
|
|
310
|
-
certificate: options.certificate || 'auto'
|
|
311
|
-
};
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// Create the route config
|
|
315
|
-
return {
|
|
316
|
-
match,
|
|
317
|
-
action,
|
|
318
|
-
name: options.name || `Static Files for ${Array.isArray(domains) ? domains.join(', ') : domains}`,
|
|
319
|
-
...options
|
|
320
|
-
};
|
|
321
|
-
}
|
|
322
|
-
|
|
323
266
|
/**
|
|
324
267
|
* Create an API route configuration
|
|
325
268
|
* @param domains Domain(s) to match
|
|
@@ -810,4 +753,278 @@ export function createCompleteNfTablesHttpsServer(
|
|
|
810
753
|
);
|
|
811
754
|
|
|
812
755
|
return [httpsRoute, httpRedirectRoute];
|
|
813
|
-
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* Create a socket handler route configuration
|
|
760
|
+
* @param domains Domain(s) to match
|
|
761
|
+
* @param ports Port(s) to listen on
|
|
762
|
+
* @param handler Socket handler function
|
|
763
|
+
* @param options Additional route options
|
|
764
|
+
* @returns Route configuration object
|
|
765
|
+
*/
|
|
766
|
+
export function createSocketHandlerRoute(
|
|
767
|
+
domains: string | string[],
|
|
768
|
+
ports: TPortRange,
|
|
769
|
+
handler: (socket: plugins.net.Socket) => void | Promise<void>,
|
|
770
|
+
options: {
|
|
771
|
+
name?: string;
|
|
772
|
+
priority?: number;
|
|
773
|
+
path?: string;
|
|
774
|
+
} = {}
|
|
775
|
+
): IRouteConfig {
|
|
776
|
+
return {
|
|
777
|
+
name: options.name || 'socket-handler-route',
|
|
778
|
+
priority: options.priority !== undefined ? options.priority : 50,
|
|
779
|
+
match: {
|
|
780
|
+
domains,
|
|
781
|
+
ports,
|
|
782
|
+
...(options.path && { path: options.path })
|
|
783
|
+
},
|
|
784
|
+
action: {
|
|
785
|
+
type: 'socket-handler',
|
|
786
|
+
socketHandler: handler
|
|
787
|
+
}
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/**
|
|
792
|
+
* Pre-built socket handlers for common use cases
|
|
793
|
+
*/
|
|
794
|
+
export const SocketHandlers = {
|
|
795
|
+
/**
|
|
796
|
+
* Simple echo server handler
|
|
797
|
+
*/
|
|
798
|
+
echo: (socket: plugins.net.Socket, context: IRouteContext) => {
|
|
799
|
+
socket.write('ECHO SERVER READY\n');
|
|
800
|
+
socket.on('data', data => socket.write(data));
|
|
801
|
+
},
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* TCP proxy handler
|
|
805
|
+
*/
|
|
806
|
+
proxy: (targetHost: string, targetPort: number) => (socket: plugins.net.Socket, context: IRouteContext) => {
|
|
807
|
+
const target = plugins.net.connect(targetPort, targetHost);
|
|
808
|
+
socket.pipe(target);
|
|
809
|
+
target.pipe(socket);
|
|
810
|
+
socket.on('close', () => target.destroy());
|
|
811
|
+
target.on('close', () => socket.destroy());
|
|
812
|
+
target.on('error', (err) => {
|
|
813
|
+
console.error('Proxy target error:', err);
|
|
814
|
+
socket.destroy();
|
|
815
|
+
});
|
|
816
|
+
},
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* Line-based protocol handler
|
|
820
|
+
*/
|
|
821
|
+
lineProtocol: (handler: (line: string, socket: plugins.net.Socket) => void) => (socket: plugins.net.Socket, context: IRouteContext) => {
|
|
822
|
+
let buffer = '';
|
|
823
|
+
socket.on('data', (data) => {
|
|
824
|
+
buffer += data.toString();
|
|
825
|
+
const lines = buffer.split('\n');
|
|
826
|
+
buffer = lines.pop() || '';
|
|
827
|
+
lines.forEach(line => {
|
|
828
|
+
if (line.trim()) {
|
|
829
|
+
handler(line.trim(), socket);
|
|
830
|
+
}
|
|
831
|
+
});
|
|
832
|
+
});
|
|
833
|
+
},
|
|
834
|
+
|
|
835
|
+
/**
|
|
836
|
+
* Simple HTTP response handler (for testing)
|
|
837
|
+
*/
|
|
838
|
+
httpResponse: (statusCode: number, body: string) => (socket: plugins.net.Socket, context: IRouteContext) => {
|
|
839
|
+
const response = [
|
|
840
|
+
`HTTP/1.1 ${statusCode} ${statusCode === 200 ? 'OK' : 'Error'}`,
|
|
841
|
+
'Content-Type: text/plain',
|
|
842
|
+
`Content-Length: ${body.length}`,
|
|
843
|
+
'Connection: close',
|
|
844
|
+
'',
|
|
845
|
+
body
|
|
846
|
+
].join('\r\n');
|
|
847
|
+
|
|
848
|
+
socket.write(response);
|
|
849
|
+
socket.end();
|
|
850
|
+
},
|
|
851
|
+
|
|
852
|
+
/**
|
|
853
|
+
* Block connection immediately
|
|
854
|
+
*/
|
|
855
|
+
block: (message?: string) => (socket: plugins.net.Socket, context: IRouteContext) => {
|
|
856
|
+
const finalMessage = message || `Connection blocked from ${context.clientIp}`;
|
|
857
|
+
if (finalMessage) {
|
|
858
|
+
socket.write(finalMessage);
|
|
859
|
+
}
|
|
860
|
+
socket.end();
|
|
861
|
+
},
|
|
862
|
+
|
|
863
|
+
/**
|
|
864
|
+
* HTTP block response
|
|
865
|
+
*/
|
|
866
|
+
httpBlock: (statusCode: number = 403, message?: string) => (socket: plugins.net.Socket, context: IRouteContext) => {
|
|
867
|
+
const defaultMessage = `Access forbidden for ${context.domain || context.clientIp}`;
|
|
868
|
+
const finalMessage = message || defaultMessage;
|
|
869
|
+
|
|
870
|
+
const response = [
|
|
871
|
+
`HTTP/1.1 ${statusCode} ${finalMessage}`,
|
|
872
|
+
'Content-Type: text/plain',
|
|
873
|
+
`Content-Length: ${finalMessage.length}`,
|
|
874
|
+
'Connection: close',
|
|
875
|
+
'',
|
|
876
|
+
finalMessage
|
|
877
|
+
].join('\r\n');
|
|
878
|
+
|
|
879
|
+
socket.write(response);
|
|
880
|
+
socket.end();
|
|
881
|
+
},
|
|
882
|
+
|
|
883
|
+
/**
|
|
884
|
+
* HTTP redirect handler
|
|
885
|
+
*/
|
|
886
|
+
httpRedirect: (locationTemplate: string, statusCode: number = 301) => (socket: plugins.net.Socket, context: IRouteContext) => {
|
|
887
|
+
let buffer = '';
|
|
888
|
+
|
|
889
|
+
socket.once('data', (data) => {
|
|
890
|
+
buffer += data.toString();
|
|
891
|
+
|
|
892
|
+
const lines = buffer.split('\r\n');
|
|
893
|
+
const requestLine = lines[0];
|
|
894
|
+
const [method, path] = requestLine.split(' ');
|
|
895
|
+
|
|
896
|
+
const domain = context.domain || 'localhost';
|
|
897
|
+
const port = context.port;
|
|
898
|
+
|
|
899
|
+
let finalLocation = locationTemplate
|
|
900
|
+
.replace('{domain}', domain)
|
|
901
|
+
.replace('{port}', String(port))
|
|
902
|
+
.replace('{path}', path)
|
|
903
|
+
.replace('{clientIp}', context.clientIp);
|
|
904
|
+
|
|
905
|
+
const message = `Redirecting to ${finalLocation}`;
|
|
906
|
+
const response = [
|
|
907
|
+
`HTTP/1.1 ${statusCode} ${statusCode === 301 ? 'Moved Permanently' : 'Found'}`,
|
|
908
|
+
`Location: ${finalLocation}`,
|
|
909
|
+
'Content-Type: text/plain',
|
|
910
|
+
`Content-Length: ${message.length}`,
|
|
911
|
+
'Connection: close',
|
|
912
|
+
'',
|
|
913
|
+
message
|
|
914
|
+
].join('\r\n');
|
|
915
|
+
|
|
916
|
+
socket.write(response);
|
|
917
|
+
socket.end();
|
|
918
|
+
});
|
|
919
|
+
},
|
|
920
|
+
|
|
921
|
+
/**
|
|
922
|
+
* HTTP server handler for ACME challenges and other HTTP needs
|
|
923
|
+
*/
|
|
924
|
+
httpServer: (handler: (req: { method: string; url: string; headers: Record<string, string>; body?: string }, res: { status: (code: number) => void; header: (name: string, value: string) => void; send: (data: string) => void; end: () => void }) => void) => (socket: plugins.net.Socket, context: IRouteContext) => {
|
|
925
|
+
let buffer = '';
|
|
926
|
+
let requestParsed = false;
|
|
927
|
+
|
|
928
|
+
socket.on('data', (data) => {
|
|
929
|
+
if (requestParsed) return; // Only handle the first request
|
|
930
|
+
|
|
931
|
+
buffer += data.toString();
|
|
932
|
+
|
|
933
|
+
// Check if we have a complete HTTP request
|
|
934
|
+
const headerEndIndex = buffer.indexOf('\r\n\r\n');
|
|
935
|
+
if (headerEndIndex === -1) return; // Need more data
|
|
936
|
+
|
|
937
|
+
requestParsed = true;
|
|
938
|
+
|
|
939
|
+
// Parse the HTTP request
|
|
940
|
+
const headerPart = buffer.substring(0, headerEndIndex);
|
|
941
|
+
const bodyPart = buffer.substring(headerEndIndex + 4);
|
|
942
|
+
|
|
943
|
+
const lines = headerPart.split('\r\n');
|
|
944
|
+
const [method, url] = lines[0].split(' ');
|
|
945
|
+
|
|
946
|
+
const headers: Record<string, string> = {};
|
|
947
|
+
for (let i = 1; i < lines.length; i++) {
|
|
948
|
+
const colonIndex = lines[i].indexOf(':');
|
|
949
|
+
if (colonIndex > 0) {
|
|
950
|
+
const name = lines[i].substring(0, colonIndex).trim().toLowerCase();
|
|
951
|
+
const value = lines[i].substring(colonIndex + 1).trim();
|
|
952
|
+
headers[name] = value;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// Create request object
|
|
957
|
+
const req = {
|
|
958
|
+
method: method || 'GET',
|
|
959
|
+
url: url || '/',
|
|
960
|
+
headers,
|
|
961
|
+
body: bodyPart
|
|
962
|
+
};
|
|
963
|
+
|
|
964
|
+
// Create response object
|
|
965
|
+
let statusCode = 200;
|
|
966
|
+
const responseHeaders: Record<string, string> = {};
|
|
967
|
+
let ended = false;
|
|
968
|
+
|
|
969
|
+
const res = {
|
|
970
|
+
status: (code: number) => {
|
|
971
|
+
statusCode = code;
|
|
972
|
+
},
|
|
973
|
+
header: (name: string, value: string) => {
|
|
974
|
+
responseHeaders[name] = value;
|
|
975
|
+
},
|
|
976
|
+
send: (data: string) => {
|
|
977
|
+
if (ended) return;
|
|
978
|
+
ended = true;
|
|
979
|
+
|
|
980
|
+
if (!responseHeaders['content-type']) {
|
|
981
|
+
responseHeaders['content-type'] = 'text/plain';
|
|
982
|
+
}
|
|
983
|
+
responseHeaders['content-length'] = String(data.length);
|
|
984
|
+
responseHeaders['connection'] = 'close';
|
|
985
|
+
|
|
986
|
+
const statusText = statusCode === 200 ? 'OK' :
|
|
987
|
+
statusCode === 404 ? 'Not Found' :
|
|
988
|
+
statusCode === 500 ? 'Internal Server Error' : 'Response';
|
|
989
|
+
|
|
990
|
+
let response = `HTTP/1.1 ${statusCode} ${statusText}\r\n`;
|
|
991
|
+
for (const [name, value] of Object.entries(responseHeaders)) {
|
|
992
|
+
response += `${name}: ${value}\r\n`;
|
|
993
|
+
}
|
|
994
|
+
response += '\r\n';
|
|
995
|
+
response += data;
|
|
996
|
+
|
|
997
|
+
socket.write(response);
|
|
998
|
+
socket.end();
|
|
999
|
+
},
|
|
1000
|
+
end: () => {
|
|
1001
|
+
if (ended) return;
|
|
1002
|
+
ended = true;
|
|
1003
|
+
socket.write('HTTP/1.1 200 OK\r\nContent-Length: 0\r\nConnection: close\r\n\r\n');
|
|
1004
|
+
socket.end();
|
|
1005
|
+
}
|
|
1006
|
+
};
|
|
1007
|
+
|
|
1008
|
+
try {
|
|
1009
|
+
handler(req, res);
|
|
1010
|
+
// Ensure response is sent even if handler doesn't call send()
|
|
1011
|
+
setTimeout(() => {
|
|
1012
|
+
if (!ended) {
|
|
1013
|
+
res.send('');
|
|
1014
|
+
}
|
|
1015
|
+
}, 1000);
|
|
1016
|
+
} catch (error) {
|
|
1017
|
+
if (!ended) {
|
|
1018
|
+
res.status(500);
|
|
1019
|
+
res.send('Internal Server Error');
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
});
|
|
1023
|
+
|
|
1024
|
+
socket.on('error', () => {
|
|
1025
|
+
if (!requestParsed) {
|
|
1026
|
+
socket.end();
|
|
1027
|
+
}
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
};
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import type { IRouteConfig, IRouteMatch, IRouteAction, IRouteTarget } from '../models/route-types.js';
|
|
9
9
|
import { mergeRouteConfigs } from './route-utils.js';
|
|
10
|
+
import { SocketHandlers } from './route-helpers.js';
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Create a basic HTTP route configuration
|
|
@@ -112,11 +113,11 @@ export function createHttpToHttpsRedirect(
|
|
|
112
113
|
ports: 80
|
|
113
114
|
},
|
|
114
115
|
action: {
|
|
115
|
-
type: '
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
116
|
+
type: 'socket-handler',
|
|
117
|
+
socketHandler: SocketHandlers.httpRedirect(
|
|
118
|
+
options.preservePath ? 'https://{domain}{path}' : 'https://{domain}',
|
|
119
|
+
options.redirectCode || 301
|
|
120
|
+
)
|
|
120
121
|
},
|
|
121
122
|
name: options.name || `HTTP to HTTPS redirect: ${Array.isArray(domains) ? domains.join(', ') : domains}`
|
|
122
123
|
};
|
|
@@ -214,57 +215,6 @@ export function createApiGatewayRoute(
|
|
|
214
215
|
return mergeRouteConfigs(baseRoute, apiRoute);
|
|
215
216
|
}
|
|
216
217
|
|
|
217
|
-
/**
|
|
218
|
-
* Create a static file server route pattern
|
|
219
|
-
* @param domains Domain(s) to match
|
|
220
|
-
* @param rootDirectory Root directory for static files
|
|
221
|
-
* @param options Additional route options
|
|
222
|
-
* @returns Static file server route configuration
|
|
223
|
-
*/
|
|
224
|
-
export function createStaticFileServerRoute(
|
|
225
|
-
domains: string | string[],
|
|
226
|
-
rootDirectory: string,
|
|
227
|
-
options: {
|
|
228
|
-
useTls?: boolean;
|
|
229
|
-
certificate?: 'auto' | { key: string; cert: string };
|
|
230
|
-
indexFiles?: string[];
|
|
231
|
-
cacheControl?: string;
|
|
232
|
-
path?: string;
|
|
233
|
-
[key: string]: any;
|
|
234
|
-
} = {}
|
|
235
|
-
): IRouteConfig {
|
|
236
|
-
// Create base route with static action
|
|
237
|
-
const baseRoute: IRouteConfig = {
|
|
238
|
-
match: {
|
|
239
|
-
domains,
|
|
240
|
-
ports: options.useTls ? 443 : 80,
|
|
241
|
-
path: options.path || '/'
|
|
242
|
-
},
|
|
243
|
-
action: {
|
|
244
|
-
type: 'static',
|
|
245
|
-
static: {
|
|
246
|
-
root: rootDirectory,
|
|
247
|
-
index: options.indexFiles || ['index.html', 'index.htm'],
|
|
248
|
-
headers: {
|
|
249
|
-
'Cache-Control': options.cacheControl || 'public, max-age=3600'
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
},
|
|
253
|
-
name: options.name || `Static Server: ${Array.isArray(domains) ? domains.join(', ') : domains}`,
|
|
254
|
-
priority: options.priority || 50
|
|
255
|
-
};
|
|
256
|
-
|
|
257
|
-
// Add TLS configuration if requested
|
|
258
|
-
if (options.useTls) {
|
|
259
|
-
baseRoute.action.tls = {
|
|
260
|
-
mode: 'terminate',
|
|
261
|
-
certificate: options.certificate || 'auto'
|
|
262
|
-
};
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
return baseRoute;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
218
|
/**
|
|
269
219
|
* Create a WebSocket route pattern
|
|
270
220
|
* @param domains Domain(s) to match
|
|
@@ -53,7 +53,15 @@ export function mergeRouteConfigs(
|
|
|
53
53
|
if (overrideRoute.action) {
|
|
54
54
|
// If action types are different, replace the entire action
|
|
55
55
|
if (overrideRoute.action.type && overrideRoute.action.type !== mergedRoute.action.type) {
|
|
56
|
-
|
|
56
|
+
// Handle socket handler specially since it's a function
|
|
57
|
+
if (overrideRoute.action.type === 'socket-handler' && overrideRoute.action.socketHandler) {
|
|
58
|
+
mergedRoute.action = {
|
|
59
|
+
type: 'socket-handler',
|
|
60
|
+
socketHandler: overrideRoute.action.socketHandler
|
|
61
|
+
};
|
|
62
|
+
} else {
|
|
63
|
+
mergedRoute.action = JSON.parse(JSON.stringify(overrideRoute.action));
|
|
64
|
+
}
|
|
57
65
|
} else {
|
|
58
66
|
// Otherwise merge the action properties
|
|
59
67
|
mergedRoute.action = { ...mergedRoute.action };
|
|
@@ -74,20 +82,9 @@ export function mergeRouteConfigs(
|
|
|
74
82
|
};
|
|
75
83
|
}
|
|
76
84
|
|
|
77
|
-
//
|
|
78
|
-
if (overrideRoute.action.
|
|
79
|
-
mergedRoute.action.
|
|
80
|
-
...mergedRoute.action.redirect,
|
|
81
|
-
...overrideRoute.action.redirect
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Merge static options
|
|
86
|
-
if (overrideRoute.action.static) {
|
|
87
|
-
mergedRoute.action.static = {
|
|
88
|
-
...mergedRoute.action.static,
|
|
89
|
-
...overrideRoute.action.static
|
|
90
|
-
};
|
|
85
|
+
// Handle socket handler update
|
|
86
|
+
if (overrideRoute.action.socketHandler) {
|
|
87
|
+
mergedRoute.action.socketHandler = overrideRoute.action.socketHandler;
|
|
91
88
|
}
|
|
92
89
|
}
|
|
93
90
|
}
|
|
@@ -98,7 +98,7 @@ export function validateRouteAction(action: IRouteAction): { valid: boolean; err
|
|
|
98
98
|
// Validate action type
|
|
99
99
|
if (!action.type) {
|
|
100
100
|
errors.push('Action type is required');
|
|
101
|
-
} else if (!['forward', '
|
|
101
|
+
} else if (!['forward', 'socket-handler'].includes(action.type)) {
|
|
102
102
|
errors.push(`Invalid action type: ${action.type}`);
|
|
103
103
|
}
|
|
104
104
|
|
|
@@ -143,30 +143,12 @@ export function validateRouteAction(action: IRouteAction): { valid: boolean; err
|
|
|
143
143
|
}
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
// Validate
|
|
147
|
-
if (action.type === '
|
|
148
|
-
if (!action.
|
|
149
|
-
errors.push('
|
|
150
|
-
} else {
|
|
151
|
-
|
|
152
|
-
errors.push('Redirect target (to) is required');
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
if (action.redirect.status &&
|
|
156
|
-
![301, 302, 303, 307, 308].includes(action.redirect.status)) {
|
|
157
|
-
errors.push('Invalid redirect status code');
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// Validate static file config for 'static' action
|
|
163
|
-
if (action.type === 'static') {
|
|
164
|
-
if (!action.static) {
|
|
165
|
-
errors.push('Static file configuration is required for static action');
|
|
166
|
-
} else {
|
|
167
|
-
if (!action.static.root) {
|
|
168
|
-
errors.push('Static file root directory is required');
|
|
169
|
-
}
|
|
146
|
+
// Validate socket handler for 'socket-handler' action
|
|
147
|
+
if (action.type === 'socket-handler') {
|
|
148
|
+
if (!action.socketHandler) {
|
|
149
|
+
errors.push('Socket handler function is required for socket-handler action');
|
|
150
|
+
} else if (typeof action.socketHandler !== 'function') {
|
|
151
|
+
errors.push('Socket handler must be a function');
|
|
170
152
|
}
|
|
171
153
|
}
|
|
172
154
|
|
|
@@ -261,12 +243,8 @@ export function hasRequiredPropertiesForAction(route: IRouteConfig, actionType:
|
|
|
261
243
|
switch (actionType) {
|
|
262
244
|
case 'forward':
|
|
263
245
|
return !!route.action.target && !!route.action.target.host && !!route.action.target.port;
|
|
264
|
-
case '
|
|
265
|
-
return !!route.action.
|
|
266
|
-
case 'static':
|
|
267
|
-
return !!route.action.static && !!route.action.static.root;
|
|
268
|
-
case 'block':
|
|
269
|
-
return true; // Block action doesn't require additional properties
|
|
246
|
+
case 'socket-handler':
|
|
247
|
+
return !!route.action.socketHandler && typeof route.action.socketHandler === 'function';
|
|
270
248
|
default:
|
|
271
249
|
return false;
|
|
272
250
|
}
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
import * as plugins from '../../../plugins.js';
|
|
2
|
-
import type { IRouteConfig } from '../../smart-proxy/models/route-types.js';
|
|
3
|
-
import type { IConnectionRecord } from '../../smart-proxy/models/interfaces.js';
|
|
4
|
-
import type { ILogger } from '../models/types.js';
|
|
5
|
-
import { createLogger } from '../models/types.js';
|
|
6
|
-
import { HttpStatus, getStatusText } from '../models/http-types.js';
|
|
7
|
-
|
|
8
|
-
export interface IRedirectHandlerContext {
|
|
9
|
-
connectionId: string;
|
|
10
|
-
connectionManager: any; // Avoid circular deps
|
|
11
|
-
settings: any;
|
|
12
|
-
logger?: ILogger;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Handles HTTP redirect routes
|
|
17
|
-
*/
|
|
18
|
-
export class RedirectHandler {
|
|
19
|
-
/**
|
|
20
|
-
* Handle redirect routes
|
|
21
|
-
*/
|
|
22
|
-
public static async handleRedirect(
|
|
23
|
-
socket: plugins.net.Socket,
|
|
24
|
-
route: IRouteConfig,
|
|
25
|
-
context: IRedirectHandlerContext
|
|
26
|
-
): Promise<void> {
|
|
27
|
-
const { connectionId, connectionManager, settings } = context;
|
|
28
|
-
const logger = context.logger || createLogger(settings.logLevel || 'info');
|
|
29
|
-
const action = route.action;
|
|
30
|
-
|
|
31
|
-
// We should have a redirect configuration
|
|
32
|
-
if (!action.redirect) {
|
|
33
|
-
logger.error(`[${connectionId}] Redirect action missing redirect configuration`);
|
|
34
|
-
socket.end();
|
|
35
|
-
connectionManager.cleanupConnection({ id: connectionId }, 'missing_redirect');
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// For TLS connections, we can't do redirects at the TCP level
|
|
40
|
-
// This check should be done before calling this handler
|
|
41
|
-
|
|
42
|
-
// Wait for the first HTTP request to perform the redirect
|
|
43
|
-
const dataListeners: ((chunk: Buffer) => void)[] = [];
|
|
44
|
-
|
|
45
|
-
const httpDataHandler = (chunk: Buffer) => {
|
|
46
|
-
// Remove all data listeners to avoid duplicated processing
|
|
47
|
-
for (const listener of dataListeners) {
|
|
48
|
-
socket.removeListener('data', listener);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Parse HTTP request to get path
|
|
52
|
-
try {
|
|
53
|
-
const headersEnd = chunk.indexOf('\r\n\r\n');
|
|
54
|
-
if (headersEnd === -1) {
|
|
55
|
-
// Not a complete HTTP request, need more data
|
|
56
|
-
socket.once('data', httpDataHandler);
|
|
57
|
-
dataListeners.push(httpDataHandler);
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const httpHeaders = chunk.slice(0, headersEnd).toString();
|
|
62
|
-
const requestLine = httpHeaders.split('\r\n')[0];
|
|
63
|
-
const [method, path] = requestLine.split(' ');
|
|
64
|
-
|
|
65
|
-
// Extract Host header
|
|
66
|
-
const hostMatch = httpHeaders.match(/Host: (.+?)(\r\n|\r|\n|$)/i);
|
|
67
|
-
const host = hostMatch ? hostMatch[1].trim() : '';
|
|
68
|
-
|
|
69
|
-
// Process the redirect URL with template variables
|
|
70
|
-
let redirectUrl = action.redirect.to;
|
|
71
|
-
redirectUrl = redirectUrl.replace(/\{domain\}/g, host);
|
|
72
|
-
redirectUrl = redirectUrl.replace(/\{path\}/g, path || '');
|
|
73
|
-
redirectUrl = redirectUrl.replace(/\{port\}/g, socket.localPort?.toString() || '80');
|
|
74
|
-
|
|
75
|
-
// Prepare the HTTP redirect response
|
|
76
|
-
const redirectResponse = [
|
|
77
|
-
`HTTP/1.1 ${action.redirect.status} Moved`,
|
|
78
|
-
`Location: ${redirectUrl}`,
|
|
79
|
-
'Connection: close',
|
|
80
|
-
'Content-Length: 0',
|
|
81
|
-
'',
|
|
82
|
-
'',
|
|
83
|
-
].join('\r\n');
|
|
84
|
-
|
|
85
|
-
if (settings.enableDetailedLogging) {
|
|
86
|
-
logger.info(
|
|
87
|
-
`[${connectionId}] Redirecting to ${redirectUrl} with status ${action.redirect.status}`
|
|
88
|
-
);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Send the redirect response
|
|
92
|
-
socket.end(redirectResponse);
|
|
93
|
-
connectionManager.initiateCleanupOnce({ id: connectionId }, 'redirect_complete');
|
|
94
|
-
} catch (err) {
|
|
95
|
-
logger.error(`[${connectionId}] Error processing HTTP redirect: ${err}`);
|
|
96
|
-
socket.end();
|
|
97
|
-
connectionManager.initiateCleanupOnce({ id: connectionId }, 'redirect_error');
|
|
98
|
-
}
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
// Setup the HTTP data handler
|
|
102
|
-
socket.once('data', httpDataHandler);
|
|
103
|
-
dataListeners.push(httpDataHandler);
|
|
104
|
-
}
|
|
105
|
-
}
|