@push.rocks/smartproxy 3.37.0 → 3.37.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.
@@ -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
@@ -3,3 +3,4 @@ export * from './classes.networkproxy.js';
3
3
  export * from './classes.portproxy.js';
4
4
  export * from './classes.port80handler.js';
5
5
  export * from './classes.sslredirect.js';
6
+ export * from './classes.snihandler.js';