@push.rocks/smartproxy 3.34.0 → 3.37.1
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 +3 -3
- package/dist_ts/classes.networkproxy.d.ts +85 -0
- package/dist_ts/classes.networkproxy.js +385 -6
- package/dist_ts/classes.portproxy.d.ts +31 -0
- package/dist_ts/classes.portproxy.js +196 -189
- package/dist_ts/classes.snihandler.d.ts +45 -0
- package/dist_ts/classes.snihandler.js +274 -0
- package/dist_ts/index.d.ts +1 -0
- package/dist_ts/index.js +2 -1
- package/package.json +5 -5
- package/ts/00_commitinfo_data.ts +2 -2
- package/ts/classes.networkproxy.ts +461 -6
- package/ts/classes.portproxy.ts +232 -204
- package/ts/classes.snihandler.ts +331 -0
- package/ts/index.ts +1 -0
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import { Buffer } from 'buffer';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SNI (Server Name Indication) handler for TLS connections.
|
|
5
|
+
* Provides robust extraction of SNI values from TLS ClientHello messages.
|
|
6
|
+
*/
|
|
7
|
+
export class SniHandler {
|
|
8
|
+
// TLS record types and constants
|
|
9
|
+
private static readonly TLS_HANDSHAKE_RECORD_TYPE = 22;
|
|
10
|
+
private static readonly TLS_CLIENT_HELLO_HANDSHAKE_TYPE = 1;
|
|
11
|
+
private static readonly TLS_SNI_EXTENSION_TYPE = 0x0000;
|
|
12
|
+
private static readonly TLS_SESSION_TICKET_EXTENSION_TYPE = 0x0023;
|
|
13
|
+
private static readonly TLS_SNI_HOST_NAME_TYPE = 0;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Checks if a buffer contains a TLS handshake message (record type 22)
|
|
17
|
+
* @param buffer - The buffer to check
|
|
18
|
+
* @returns true if the buffer starts with a TLS handshake record type
|
|
19
|
+
*/
|
|
20
|
+
public static isTlsHandshake(buffer: Buffer): boolean {
|
|
21
|
+
return buffer.length > 0 && buffer[0] === this.TLS_HANDSHAKE_RECORD_TYPE;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Checks if a buffer contains a TLS ClientHello message
|
|
26
|
+
* @param buffer - The buffer to check
|
|
27
|
+
* @returns true if the buffer appears to be a ClientHello message
|
|
28
|
+
*/
|
|
29
|
+
public static isClientHello(buffer: Buffer): boolean {
|
|
30
|
+
// Minimum ClientHello size (TLS record header + handshake header)
|
|
31
|
+
if (buffer.length < 9) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Check record type (must be TLS_HANDSHAKE_RECORD_TYPE)
|
|
36
|
+
if (buffer[0] !== this.TLS_HANDSHAKE_RECORD_TYPE) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Skip version and length in TLS record header (5 bytes total)
|
|
41
|
+
// Check handshake type at byte 5 (must be CLIENT_HELLO)
|
|
42
|
+
return buffer[5] === this.TLS_CLIENT_HELLO_HANDSHAKE_TYPE;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Extracts the SNI (Server Name Indication) from a TLS ClientHello message.
|
|
47
|
+
* Implements robust parsing with support for session resumption edge cases.
|
|
48
|
+
*
|
|
49
|
+
* @param buffer - The buffer containing the TLS ClientHello message
|
|
50
|
+
* @param enableLogging - Whether to enable detailed debug logging
|
|
51
|
+
* @returns The extracted server name or undefined if not found
|
|
52
|
+
*/
|
|
53
|
+
public static extractSNI(buffer: Buffer, enableLogging: boolean = false): string | undefined {
|
|
54
|
+
// Logging helper
|
|
55
|
+
const log = (message: string) => {
|
|
56
|
+
if (enableLogging) {
|
|
57
|
+
console.log(`[SNI Extraction] ${message}`);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
// Buffer must be at least 5 bytes (TLS record header)
|
|
63
|
+
if (buffer.length < 5) {
|
|
64
|
+
log('Buffer too small for TLS record header');
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Check record type (must be TLS_HANDSHAKE_RECORD_TYPE = 22)
|
|
69
|
+
if (buffer[0] !== this.TLS_HANDSHAKE_RECORD_TYPE) {
|
|
70
|
+
log(`Not a TLS handshake record: ${buffer[0]}`);
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Check TLS version
|
|
75
|
+
const majorVersion = buffer[1];
|
|
76
|
+
const minorVersion = buffer[2];
|
|
77
|
+
log(`TLS version: ${majorVersion}.${minorVersion}`);
|
|
78
|
+
|
|
79
|
+
// Parse record length (bytes 3-4, big-endian)
|
|
80
|
+
const recordLength = (buffer[3] << 8) + buffer[4];
|
|
81
|
+
log(`Record length: ${recordLength}`);
|
|
82
|
+
|
|
83
|
+
// Validate record length against buffer size
|
|
84
|
+
if (buffer.length < recordLength + 5) {
|
|
85
|
+
log('Buffer smaller than expected record length');
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Start of handshake message in the buffer
|
|
90
|
+
let pos = 5;
|
|
91
|
+
|
|
92
|
+
// Check handshake type (must be CLIENT_HELLO = 1)
|
|
93
|
+
if (buffer[pos] !== this.TLS_CLIENT_HELLO_HANDSHAKE_TYPE) {
|
|
94
|
+
log(`Not a ClientHello message: ${buffer[pos]}`);
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Skip handshake type (1 byte)
|
|
99
|
+
pos += 1;
|
|
100
|
+
|
|
101
|
+
// Parse handshake length (3 bytes, big-endian)
|
|
102
|
+
const handshakeLength = (buffer[pos] << 16) + (buffer[pos + 1] << 8) + buffer[pos + 2];
|
|
103
|
+
log(`Handshake length: ${handshakeLength}`);
|
|
104
|
+
|
|
105
|
+
// Skip handshake length (3 bytes)
|
|
106
|
+
pos += 3;
|
|
107
|
+
|
|
108
|
+
// Check client version (2 bytes)
|
|
109
|
+
const clientMajorVersion = buffer[pos];
|
|
110
|
+
const clientMinorVersion = buffer[pos + 1];
|
|
111
|
+
log(`Client version: ${clientMajorVersion}.${clientMinorVersion}`);
|
|
112
|
+
|
|
113
|
+
// Skip client version (2 bytes)
|
|
114
|
+
pos += 2;
|
|
115
|
+
|
|
116
|
+
// Skip client random (32 bytes)
|
|
117
|
+
pos += 32;
|
|
118
|
+
|
|
119
|
+
// Parse session ID
|
|
120
|
+
if (pos + 1 > buffer.length) {
|
|
121
|
+
log('Buffer too small for session ID length');
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const sessionIdLength = buffer[pos];
|
|
126
|
+
log(`Session ID length: ${sessionIdLength}`);
|
|
127
|
+
|
|
128
|
+
// Skip session ID length (1 byte) and session ID
|
|
129
|
+
pos += 1 + sessionIdLength;
|
|
130
|
+
|
|
131
|
+
// Check if we have enough bytes left
|
|
132
|
+
if (pos + 2 > buffer.length) {
|
|
133
|
+
log('Buffer too small for cipher suites length');
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Parse cipher suites length (2 bytes, big-endian)
|
|
138
|
+
const cipherSuitesLength = (buffer[pos] << 8) + buffer[pos + 1];
|
|
139
|
+
log(`Cipher suites length: ${cipherSuitesLength}`);
|
|
140
|
+
|
|
141
|
+
// Skip cipher suites length (2 bytes) and cipher suites
|
|
142
|
+
pos += 2 + cipherSuitesLength;
|
|
143
|
+
|
|
144
|
+
// Check if we have enough bytes left
|
|
145
|
+
if (pos + 1 > buffer.length) {
|
|
146
|
+
log('Buffer too small for compression methods length');
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Parse compression methods length (1 byte)
|
|
151
|
+
const compressionMethodsLength = buffer[pos];
|
|
152
|
+
log(`Compression methods length: ${compressionMethodsLength}`);
|
|
153
|
+
|
|
154
|
+
// Skip compression methods length (1 byte) and compression methods
|
|
155
|
+
pos += 1 + compressionMethodsLength;
|
|
156
|
+
|
|
157
|
+
// Check if we have enough bytes for extensions length
|
|
158
|
+
if (pos + 2 > buffer.length) {
|
|
159
|
+
log('No extensions present or buffer too small');
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Parse extensions length (2 bytes, big-endian)
|
|
164
|
+
const extensionsLength = (buffer[pos] << 8) + buffer[pos + 1];
|
|
165
|
+
log(`Extensions length: ${extensionsLength}`);
|
|
166
|
+
|
|
167
|
+
// Skip extensions length (2 bytes)
|
|
168
|
+
pos += 2;
|
|
169
|
+
|
|
170
|
+
// Extensions end position
|
|
171
|
+
const extensionsEnd = pos + extensionsLength;
|
|
172
|
+
|
|
173
|
+
// Check if extensions length is valid
|
|
174
|
+
if (extensionsEnd > buffer.length) {
|
|
175
|
+
log('Extensions length exceeds buffer size');
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Track if we found session tickets (for improved resumption handling)
|
|
180
|
+
let hasSessionTicket = false;
|
|
181
|
+
|
|
182
|
+
// Iterate through extensions
|
|
183
|
+
while (pos + 4 <= extensionsEnd) {
|
|
184
|
+
// Parse extension type (2 bytes, big-endian)
|
|
185
|
+
const extensionType = (buffer[pos] << 8) + buffer[pos + 1];
|
|
186
|
+
log(`Extension type: 0x${extensionType.toString(16).padStart(4, '0')}`);
|
|
187
|
+
|
|
188
|
+
// Skip extension type (2 bytes)
|
|
189
|
+
pos += 2;
|
|
190
|
+
|
|
191
|
+
// Parse extension length (2 bytes, big-endian)
|
|
192
|
+
const extensionLength = (buffer[pos] << 8) + buffer[pos + 1];
|
|
193
|
+
log(`Extension length: ${extensionLength}`);
|
|
194
|
+
|
|
195
|
+
// Skip extension length (2 bytes)
|
|
196
|
+
pos += 2;
|
|
197
|
+
|
|
198
|
+
// Check if this is the SNI extension
|
|
199
|
+
if (extensionType === this.TLS_SNI_EXTENSION_TYPE) {
|
|
200
|
+
log('Found SNI extension');
|
|
201
|
+
|
|
202
|
+
// Ensure we have enough bytes for the server name list
|
|
203
|
+
if (pos + 2 > extensionsEnd) {
|
|
204
|
+
log('Extension too small for server name list length');
|
|
205
|
+
pos += extensionLength; // Skip this extension
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Parse server name list length (2 bytes, big-endian)
|
|
210
|
+
const serverNameListLength = (buffer[pos] << 8) + buffer[pos + 1];
|
|
211
|
+
log(`Server name list length: ${serverNameListLength}`);
|
|
212
|
+
|
|
213
|
+
// Skip server name list length (2 bytes)
|
|
214
|
+
pos += 2;
|
|
215
|
+
|
|
216
|
+
// Ensure server name list length is valid
|
|
217
|
+
if (pos + serverNameListLength > extensionsEnd) {
|
|
218
|
+
log('Server name list length exceeds extension size');
|
|
219
|
+
break; // Exit the loop, extension parsing is broken
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// End position of server name list
|
|
223
|
+
const serverNameListEnd = pos + serverNameListLength;
|
|
224
|
+
|
|
225
|
+
// Iterate through server names
|
|
226
|
+
while (pos + 3 <= serverNameListEnd) {
|
|
227
|
+
// Check name type (must be HOST_NAME_TYPE = 0 for hostname)
|
|
228
|
+
const nameType = buffer[pos];
|
|
229
|
+
log(`Name type: ${nameType}`);
|
|
230
|
+
|
|
231
|
+
if (nameType !== this.TLS_SNI_HOST_NAME_TYPE) {
|
|
232
|
+
log(`Unsupported name type: ${nameType}`);
|
|
233
|
+
pos += 1; // Skip name type (1 byte)
|
|
234
|
+
|
|
235
|
+
// Skip name length (2 bytes) and name data
|
|
236
|
+
if (pos + 2 <= serverNameListEnd) {
|
|
237
|
+
const nameLength = (buffer[pos] << 8) + buffer[pos + 1];
|
|
238
|
+
pos += 2 + nameLength;
|
|
239
|
+
} else {
|
|
240
|
+
log('Invalid server name entry');
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Skip name type (1 byte)
|
|
247
|
+
pos += 1;
|
|
248
|
+
|
|
249
|
+
// Ensure we have enough bytes for name length
|
|
250
|
+
if (pos + 2 > serverNameListEnd) {
|
|
251
|
+
log('Server name entry too small for name length');
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Parse name length (2 bytes, big-endian)
|
|
256
|
+
const nameLength = (buffer[pos] << 8) + buffer[pos + 1];
|
|
257
|
+
log(`Name length: ${nameLength}`);
|
|
258
|
+
|
|
259
|
+
// Skip name length (2 bytes)
|
|
260
|
+
pos += 2;
|
|
261
|
+
|
|
262
|
+
// Ensure we have enough bytes for the name
|
|
263
|
+
if (pos + nameLength > serverNameListEnd) {
|
|
264
|
+
log('Name length exceeds server name list size');
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Extract server name (hostname)
|
|
269
|
+
const serverName = buffer.slice(pos, pos + nameLength).toString('utf8');
|
|
270
|
+
log(`Extracted server name: ${serverName}`);
|
|
271
|
+
return serverName;
|
|
272
|
+
}
|
|
273
|
+
} else if (extensionType === this.TLS_SESSION_TICKET_EXTENSION_TYPE) {
|
|
274
|
+
// If we encounter a session ticket extension, mark it for later
|
|
275
|
+
log('Found session ticket extension');
|
|
276
|
+
hasSessionTicket = true;
|
|
277
|
+
pos += extensionLength; // Skip this extension
|
|
278
|
+
} else {
|
|
279
|
+
// Skip this extension
|
|
280
|
+
pos += extensionLength;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Log if we found a session ticket but no SNI
|
|
285
|
+
if (hasSessionTicket) {
|
|
286
|
+
log('Session ticket present but no SNI found - possible resumption scenario');
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
log('No SNI extension found in ClientHello');
|
|
290
|
+
return undefined;
|
|
291
|
+
} catch (error) {
|
|
292
|
+
log(`Error parsing SNI: ${error instanceof Error ? error.message : String(error)}`);
|
|
293
|
+
return undefined;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Attempts to extract SNI from an initial ClientHello packet and handles
|
|
299
|
+
* session resumption edge cases more robustly than the standard extraction.
|
|
300
|
+
*
|
|
301
|
+
* This method is specifically designed for Chrome and other browsers that
|
|
302
|
+
* may send different ClientHello formats during session resumption.
|
|
303
|
+
*
|
|
304
|
+
* @param buffer - The buffer containing the TLS ClientHello message
|
|
305
|
+
* @param enableLogging - Whether to enable detailed debug logging
|
|
306
|
+
* @returns The extracted server name or undefined if not found
|
|
307
|
+
*/
|
|
308
|
+
public static extractSNIWithResumptionSupport(
|
|
309
|
+
buffer: Buffer,
|
|
310
|
+
enableLogging: boolean = false
|
|
311
|
+
): string | undefined {
|
|
312
|
+
// First try the standard SNI extraction
|
|
313
|
+
const standardSni = this.extractSNI(buffer, enableLogging);
|
|
314
|
+
if (standardSni) {
|
|
315
|
+
return standardSni;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// If standard extraction failed and we have a valid ClientHello,
|
|
319
|
+
// this might be a session resumption with non-standard format
|
|
320
|
+
if (this.isClientHello(buffer)) {
|
|
321
|
+
if (enableLogging) {
|
|
322
|
+
console.log('[SNI Extraction] Detected ClientHello without standard SNI, possible session resumption');
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Additional handling could be implemented here for specific browser behaviors
|
|
326
|
+
// For now, this is a placeholder for future improvements
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return undefined;
|
|
330
|
+
}
|
|
331
|
+
}
|
package/ts/index.ts
CHANGED