react-native-nitro-net 0.2.0 → 0.3.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.
Files changed (70) hide show
  1. package/README.md +70 -12
  2. package/android/libs/arm64-v8a/librust_c_net.so +0 -0
  3. package/android/libs/armeabi-v7a/librust_c_net.so +0 -0
  4. package/android/libs/x86/librust_c_net.so +0 -0
  5. package/android/libs/x86_64/librust_c_net.so +0 -0
  6. package/cpp/HybridHttpParser.hpp +67 -0
  7. package/cpp/HybridNetDriver.hpp +6 -0
  8. package/cpp/HybridNetServerDriver.hpp +7 -0
  9. package/cpp/HybridNetSocketDriver.hpp +27 -0
  10. package/cpp/NetBindings.hpp +15 -0
  11. package/ios/Frameworks/RustCNet.xcframework/Info.plist +5 -5
  12. package/ios/Frameworks/RustCNet.xcframework/ios-arm64/RustCNet.framework/RustCNet +0 -0
  13. package/ios/Frameworks/RustCNet.xcframework/ios-arm64_x86_64-simulator/RustCNet.framework/RustCNet +0 -0
  14. package/lib/Net.nitro.d.ts +19 -0
  15. package/lib/http.d.ts +203 -0
  16. package/lib/http.js +1138 -0
  17. package/lib/https.d.ts +24 -0
  18. package/lib/https.js +144 -0
  19. package/lib/index.d.ts +46 -8
  20. package/lib/index.js +133 -26
  21. package/lib/tls.d.ts +21 -0
  22. package/lib/tls.js +74 -4
  23. package/nitrogen/generated/android/RustCNet+autolinking.cmake +2 -0
  24. package/nitrogen/generated/android/RustCNetOnLoad.cpp +2 -0
  25. package/nitrogen/generated/android/c++/JHybridHttpParserSpec.cpp +54 -0
  26. package/nitrogen/generated/android/c++/JHybridHttpParserSpec.hpp +65 -0
  27. package/nitrogen/generated/android/c++/JHybridNetDriverSpec.cpp +9 -0
  28. package/nitrogen/generated/android/c++/JHybridNetDriverSpec.hpp +1 -0
  29. package/nitrogen/generated/android/c++/JHybridNetServerDriverSpec.cpp +4 -0
  30. package/nitrogen/generated/android/c++/JHybridNetServerDriverSpec.hpp +1 -0
  31. package/nitrogen/generated/android/c++/JHybridNetSocketDriverSpec.cpp +9 -0
  32. package/nitrogen/generated/android/c++/JHybridNetSocketDriverSpec.hpp +2 -0
  33. package/nitrogen/generated/android/c++/JNetConfig.hpp +7 -3
  34. package/nitrogen/generated/android/kotlin/com/margelo/nitro/net/HybridHttpParserSpec.kt +58 -0
  35. package/nitrogen/generated/android/kotlin/com/margelo/nitro/net/HybridNetDriverSpec.kt +4 -0
  36. package/nitrogen/generated/android/kotlin/com/margelo/nitro/net/HybridNetServerDriverSpec.kt +4 -0
  37. package/nitrogen/generated/android/kotlin/com/margelo/nitro/net/HybridNetSocketDriverSpec.kt +8 -0
  38. package/nitrogen/generated/android/kotlin/com/margelo/nitro/net/NetConfig.kt +6 -3
  39. package/nitrogen/generated/ios/RustCNet-Swift-Cxx-Bridge.cpp +17 -0
  40. package/nitrogen/generated/ios/RustCNet-Swift-Cxx-Bridge.hpp +26 -0
  41. package/nitrogen/generated/ios/RustCNet-Swift-Cxx-Umbrella.hpp +5 -0
  42. package/nitrogen/generated/ios/c++/HybridHttpParserSpecSwift.cpp +11 -0
  43. package/nitrogen/generated/ios/c++/HybridHttpParserSpecSwift.hpp +79 -0
  44. package/nitrogen/generated/ios/c++/HybridNetDriverSpecSwift.hpp +11 -0
  45. package/nitrogen/generated/ios/c++/HybridNetServerDriverSpecSwift.hpp +6 -0
  46. package/nitrogen/generated/ios/c++/HybridNetSocketDriverSpecSwift.hpp +14 -0
  47. package/nitrogen/generated/ios/swift/HybridHttpParserSpec.swift +56 -0
  48. package/nitrogen/generated/ios/swift/HybridHttpParserSpec_cxx.swift +131 -0
  49. package/nitrogen/generated/ios/swift/HybridNetDriverSpec.swift +1 -0
  50. package/nitrogen/generated/ios/swift/HybridNetDriverSpec_cxx.swift +15 -0
  51. package/nitrogen/generated/ios/swift/HybridNetServerDriverSpec.swift +1 -0
  52. package/nitrogen/generated/ios/swift/HybridNetServerDriverSpec_cxx.swift +11 -0
  53. package/nitrogen/generated/ios/swift/HybridNetSocketDriverSpec.swift +2 -0
  54. package/nitrogen/generated/ios/swift/HybridNetSocketDriverSpec_cxx.swift +36 -0
  55. package/nitrogen/generated/ios/swift/NetConfig.swift +19 -1
  56. package/nitrogen/generated/shared/c++/HybridHttpParserSpec.cpp +21 -0
  57. package/nitrogen/generated/shared/c++/HybridHttpParserSpec.hpp +63 -0
  58. package/nitrogen/generated/shared/c++/HybridNetDriverSpec.cpp +1 -0
  59. package/nitrogen/generated/shared/c++/HybridNetDriverSpec.hpp +4 -0
  60. package/nitrogen/generated/shared/c++/HybridNetServerDriverSpec.cpp +1 -0
  61. package/nitrogen/generated/shared/c++/HybridNetServerDriverSpec.hpp +1 -0
  62. package/nitrogen/generated/shared/c++/HybridNetSocketDriverSpec.cpp +2 -0
  63. package/nitrogen/generated/shared/c++/HybridNetSocketDriverSpec.hpp +2 -0
  64. package/nitrogen/generated/shared/c++/NetConfig.hpp +6 -2
  65. package/package.json +3 -3
  66. package/src/Net.nitro.ts +17 -0
  67. package/src/http.ts +1304 -0
  68. package/src/https.ts +127 -0
  69. package/src/index.ts +149 -18
  70. package/src/tls.ts +82 -6
package/src/https.ts ADDED
@@ -0,0 +1,127 @@
1
+ import * as http from './http'
2
+ import * as tls from './tls'
3
+ import { Driver } from './Driver'
4
+ import { Buffer } from 'react-native-nitro-buffer'
5
+ import { IncomingMessage } from './http'
6
+
7
+ // ========== Server ==========
8
+
9
+ export class Server extends tls.Server {
10
+ private _httpConnections = new Set<any>();
11
+ public maxHeaderSize: number = 16384;
12
+ public maxRequestsPerSocket: number = 0;
13
+ public headersTimeout: number = 60000;
14
+ public requestTimeout: number = 300000;
15
+ public keepAliveTimeout: number = 5000;
16
+
17
+ constructor(options?: any, requestListener?: (req: http.IncomingMessage, res: http.ServerResponse) => void) {
18
+ if (typeof options === 'function') {
19
+ requestListener = options;
20
+ options = {};
21
+ }
22
+ super(options);
23
+
24
+ if (requestListener) {
25
+ this.on('request', requestListener);
26
+ }
27
+
28
+ // Initialize HTTP connection setup for secure connections
29
+ this.on('secureConnection', (socket: any) => {
30
+ // @ts-ignore - access internal http logic
31
+ (http.Server.prototype as any)._setupHttpConnection.call(this, socket);
32
+ });
33
+ }
34
+
35
+ public setTimeout(ms: number, callback?: () => void): this {
36
+ // @ts-ignore - access netServer via super's internal or cast
37
+ (this as any)._netServer.setTimeout(ms, callback);
38
+ return this;
39
+ }
40
+ }
41
+
42
+ export function createServer(options?: any, requestListener?: (req: http.IncomingMessage, res: http.ServerResponse) => void): Server {
43
+ return new Server(options, requestListener);
44
+ }
45
+
46
+ // ========== ClientRequest ==========
47
+
48
+ export class ClientRequest extends http.ClientRequest {
49
+ constructor(options: any, callback?: (res: http.IncomingMessage) => void) {
50
+ if (typeof options === 'string') {
51
+ options = new URL(options);
52
+ }
53
+ if (options instanceof URL) {
54
+ options = {
55
+ protocol: options.protocol,
56
+ hostname: options.hostname,
57
+ path: options.pathname + options.search,
58
+ port: options.port ? parseInt(options.port) : 443
59
+ };
60
+ }
61
+ options.protocol = 'https:';
62
+ super(options, callback);
63
+ }
64
+ }
65
+
66
+ export function request(
67
+ urlOrOptions: string | URL | http.RequestOptions,
68
+ optionsOrCallback?: http.RequestOptions | ((res: http.IncomingMessage) => void),
69
+ callback?: (res: http.IncomingMessage) => void
70
+ ): ClientRequest {
71
+ let opts: http.RequestOptions = {};
72
+ let cb: ((res: http.IncomingMessage) => void) | undefined = callback;
73
+
74
+ if (typeof urlOrOptions === 'string') {
75
+ const url = new URL(urlOrOptions);
76
+ opts = {
77
+ protocol: url.protocol,
78
+ hostname: url.hostname,
79
+ path: url.pathname + url.search,
80
+ port: url.port ? parseInt(url.port) : 443
81
+ };
82
+ } else if (urlOrOptions instanceof URL) {
83
+ opts = {
84
+ protocol: urlOrOptions.protocol,
85
+ hostname: urlOrOptions.hostname,
86
+ path: urlOrOptions.pathname + urlOrOptions.search,
87
+ port: urlOrOptions.port ? parseInt(urlOrOptions.port) : 443
88
+ };
89
+ } else {
90
+ opts = { ...urlOrOptions };
91
+ }
92
+
93
+ if (typeof optionsOrCallback === 'function') {
94
+ cb = optionsOrCallback;
95
+ } else if (optionsOrCallback) {
96
+ opts = { ...opts, ...optionsOrCallback };
97
+ }
98
+
99
+ opts.protocol = 'https:';
100
+ return new ClientRequest(opts, cb);
101
+ }
102
+
103
+ export function get(
104
+ urlOrOptions: string | URL | http.RequestOptions,
105
+ optionsOrCallback?: http.RequestOptions | ((res: http.IncomingMessage) => void),
106
+ callback?: (res: http.IncomingMessage) => void
107
+ ): ClientRequest {
108
+ const req = request(urlOrOptions, optionsOrCallback, callback);
109
+ req.end();
110
+ return req;
111
+ }
112
+
113
+ // ========== Agent ==========
114
+
115
+ export class Agent extends http.Agent {
116
+ constructor(options?: any) {
117
+ super(options);
118
+ }
119
+ }
120
+
121
+ export const globalAgent = new Agent({
122
+ keepAlive: true,
123
+ scheduling: 'lifo',
124
+ timeout: 5000,
125
+ });
126
+
127
+ export { IncomingMessage };
package/src/index.ts CHANGED
@@ -33,16 +33,21 @@ let _autoSelectFamilyDefault = 4; // Node default is usually 4/6 independent, bu
33
33
  let _isVerbose = false;
34
34
  let _isInitialized = false;
35
35
 
36
- function debugLog(message: string) {
37
- if (_isVerbose) {
38
- console.log(`[NET DEBUG] ${message}`);
39
- }
36
+ function isVerbose(): boolean {
37
+ return _isVerbose;
40
38
  }
41
39
 
42
40
  function setVerbose(enabled: boolean): void {
43
41
  _isVerbose = enabled;
44
42
  }
45
43
 
44
+ function debugLog(message: string) {
45
+ if (_isVerbose) {
46
+ const timestamp = new Date().toISOString().split('T')[1].split('Z')[0];
47
+ console.log(`[NET DEBUG ${timestamp}] ${message}`);
48
+ }
49
+ }
50
+
46
51
  function getDefaultAutoSelectFamily(): number {
47
52
  return _autoSelectFamilyDefault;
48
53
  }
@@ -79,6 +84,9 @@ function ensureInitialized(): void {
79
84
  */
80
85
  function initWithConfig(config: NetConfig): void {
81
86
  _isInitialized = true;
87
+ if (config.debug !== undefined) {
88
+ setVerbose(config.debug);
89
+ }
82
90
  Driver.initWithConfig(config);
83
91
  }
84
92
 
@@ -89,17 +97,69 @@ function initWithConfig(config: NetConfig): void {
89
97
  // SocketAddress
90
98
  // -----------------------------------------------------------------------------
91
99
 
100
+ export interface SocketAddressOptions {
101
+ address?: string;
102
+ family?: 'ipv4' | 'ipv6';
103
+ port?: number;
104
+ flowlabel?: number;
105
+ }
106
+
92
107
  export class SocketAddress {
93
108
  readonly address: string;
94
109
  readonly family: 'ipv4' | 'ipv6';
95
110
  readonly port: number;
96
111
  readonly flowlabel: number;
97
112
 
98
- constructor(options: { address: string, family?: 'ipv4' | 'ipv6', port: number, flowlabel?: number }) {
99
- this.address = options.address;
100
- this.family = options.family || (isIPv6(options.address) ? 'ipv6' : 'ipv4');
101
- this.port = options.port;
102
- this.flowlabel = options.flowlabel || 0;
113
+ constructor(options: SocketAddressOptions = {}) {
114
+ this.address = options.address ?? (options.family === 'ipv6' ? '::' : '127.0.0.1');
115
+ this.family = options.family || (isIPv6(this.address) ? 'ipv6' : 'ipv4');
116
+ this.port = options.port ?? 0;
117
+ this.flowlabel = options.flowlabel ?? 0;
118
+ }
119
+
120
+ /**
121
+ * Attempts to parse a string containing a socket address.
122
+ * Returns a SocketAddress if successful, or undefined if not.
123
+ *
124
+ * Supported formats:
125
+ * - `ip:port` (e.g., `127.0.0.1:8080`, `[::1]:8080`)
126
+ * - `ip` only (port defaults to 0)
127
+ */
128
+ static parse(input: string): SocketAddress | undefined {
129
+ if (!input || typeof input !== 'string') return undefined;
130
+ let address: string;
131
+ let port = 0;
132
+
133
+ // Handle IPv6 bracket notation: [::1]:port
134
+ const ipv6Match = input.match(/^\[([^\]]+)\]:?(\d*)$/);
135
+ if (ipv6Match) {
136
+ address = ipv6Match[1];
137
+ port = ipv6Match[2] ? parseInt(ipv6Match[2], 10) : 0;
138
+ if (!isIPv6(address)) return undefined;
139
+ return new SocketAddress({ address, port, family: 'ipv6' });
140
+ }
141
+
142
+ // Handle IPv4 or IPv6 without brackets
143
+ const lastColon = input.lastIndexOf(':');
144
+ if (lastColon === -1) {
145
+ // No port, just IP
146
+ address = input;
147
+ } else {
148
+ // Determine if the colon is a port separator or part of IPv6
149
+ const potentialPort = input.slice(lastColon + 1);
150
+ const potentialAddr = input.slice(0, lastColon);
151
+ if (/^\d+$/.test(potentialPort) && (isIPv4(potentialAddr) || isIPv6(potentialAddr))) {
152
+ address = potentialAddr;
153
+ port = parseInt(potentialPort, 10);
154
+ } else {
155
+ // It's an IPv6 address without port
156
+ address = input;
157
+ }
158
+ }
159
+
160
+ const family = isIPv6(address) ? 'ipv6' : (isIPv4(address) ? 'ipv4' : undefined);
161
+ if (!family) return undefined;
162
+ return new SocketAddress({ address, port, family });
103
163
  }
104
164
  }
105
165
 
@@ -107,9 +167,31 @@ export class SocketAddress {
107
167
  // BlockList
108
168
  // -----------------------------------------------------------------------------
109
169
 
170
+ export interface BlockListRule {
171
+ type: 'address' | 'range' | 'subnet';
172
+ address?: string;
173
+ start?: string;
174
+ end?: string;
175
+ prefix?: number;
176
+ family: 'ipv4' | 'ipv6';
177
+ }
178
+
110
179
  export class BlockList {
111
180
  private _rules: Array<{ type: 'address' | 'range' | 'subnet', data: any }> = [];
112
181
 
182
+ /** Returns an array of rules added to the blocklist. */
183
+ get rules(): BlockListRule[] {
184
+ return this._rules.map(r => {
185
+ if (r.type === 'address') {
186
+ return { type: 'address' as const, address: r.data.address, family: r.data.family };
187
+ } else if (r.type === 'range') {
188
+ return { type: 'range' as const, start: r.data.start, end: r.data.end, family: r.data.family };
189
+ } else {
190
+ return { type: 'subnet' as const, address: r.data.net, prefix: r.data.prefix, family: r.data.family };
191
+ }
192
+ });
193
+ }
194
+
113
195
  addAddress(address: string, family?: 'ipv4' | 'ipv6'): void {
114
196
  this._rules.push({ type: 'address', data: { address, family: family || (isIPv6(address) ? 'ipv6' : 'ipv4') } });
115
197
  }
@@ -143,6 +225,37 @@ export class BlockList {
143
225
  }
144
226
  return false;
145
227
  }
228
+
229
+ /**
230
+ * Serializes the BlockList to a JSON-compatible format.
231
+ */
232
+ toJSON(): BlockListRule[] {
233
+ return this.rules;
234
+ }
235
+
236
+ /**
237
+ * Creates a BlockList from a JSON array of rules.
238
+ */
239
+ static fromJSON(json: BlockListRule[]): BlockList {
240
+ const list = new BlockList();
241
+ for (const rule of json) {
242
+ if (rule.type === 'address' && rule.address) {
243
+ list.addAddress(rule.address, rule.family);
244
+ } else if (rule.type === 'range' && rule.start && rule.end) {
245
+ list.addRange(rule.start, rule.end, rule.family);
246
+ } else if (rule.type === 'subnet' && rule.address && rule.prefix !== undefined) {
247
+ list.addSubnet(rule.address, rule.prefix, rule.family);
248
+ }
249
+ }
250
+ return list;
251
+ }
252
+
253
+ /**
254
+ * Checks if a given value is a BlockList instance.
255
+ */
256
+ static isBlockList(value: unknown): value is BlockList {
257
+ return value instanceof BlockList;
258
+ }
146
259
  }
147
260
 
148
261
  function ipv4ToLong(ip: string): number {
@@ -178,6 +291,7 @@ export class Socket extends Duplex {
178
291
  public bytesWritten: number = 0;
179
292
  public autoSelectFamilyAttemptedAddresses: string[] = [];
180
293
  private _autoSelectFamily: boolean = false;
294
+ private _timeout: number = 0;
181
295
 
182
296
  get localFamily(): string {
183
297
  return this.localAddress && this.localAddress.includes(':') ? 'IPv6' : 'IPv4';
@@ -203,7 +317,9 @@ export class Socket extends Duplex {
203
317
  super({
204
318
  allowHalfOpen: options?.allowHalfOpen ?? false,
205
319
  readable: options?.readable ?? true,
206
- writable: options?.writable ?? true
320
+ writable: options?.writable ?? true,
321
+ // @ts-ignore
322
+ autoDestroy: false
207
323
  });
208
324
 
209
325
  if (options?.socketDriver) {
@@ -211,6 +327,8 @@ export class Socket extends Duplex {
211
327
  this._driver = options.socketDriver;
212
328
  this._connected = true;
213
329
  this._setupEvents();
330
+ // Enable noDelay by default
331
+ this._driver.setNoDelay(true);
214
332
  // Resume the socket since it starts paused on server-accept
215
333
  this.resume();
216
334
  // Emit connect for server-side socket? No, it's already connected.
@@ -219,8 +337,10 @@ export class Socket extends Duplex {
219
337
  ensureInitialized();
220
338
  this._driver = Driver.createSocket();
221
339
  this._setupEvents();
222
- // Also resume client socket initially so it's ready to receive
223
- this.resume();
340
+ // Enable noDelay by default to match Node.js and reduce latency for small writes
341
+ this._driver.setNoDelay(true);
342
+ // Do NOT resume here - socket is not connected yet!
343
+ // resume() will be called after 'connect' event in _connect()
224
344
  }
225
345
 
226
346
  this.on('finish', () => {
@@ -261,6 +381,8 @@ export class Socket extends Duplex {
261
381
  this.connecting = false;
262
382
  this._connected = true;
263
383
  this._updateAddresses();
384
+ // Now that we're connected, start receiving data
385
+ this.resume();
264
386
  this.emit('connect');
265
387
  this.emit('ready');
266
388
  break;
@@ -409,6 +531,7 @@ export class Socket extends Duplex {
409
531
  this._autoSelectFamily = true;
410
532
  }
411
533
 
534
+ debugLog(`Socket.connect: target=${host}:${port}, autoSelectFamily=${this._autoSelectFamily}`);
412
535
  return this._connect(port, host, connectionListener, options.signal);
413
536
  }
414
537
 
@@ -431,6 +554,7 @@ export class Socket extends Duplex {
431
554
  this.once('close', () => signal.removeEventListener('abort', abortHandler));
432
555
  }
433
556
 
557
+ debugLog(`Socket._connect: Calling driver.connect(${host}, ${port})`);
434
558
  this._driver?.connect(host, port);
435
559
  return this;
436
560
  }
@@ -511,6 +635,7 @@ export class Socket extends Duplex {
511
635
 
512
636
  // Standard net.Socket methods
513
637
  setTimeout(msecs: number, callback?: () => void): this {
638
+ this._timeout = msecs;
514
639
  if (this._driver) {
515
640
  this._driver.setTimeout(msecs);
516
641
  }
@@ -534,12 +659,13 @@ export class Socket extends Duplex {
534
659
  * Resume reading after a call to pause().
535
660
  */
536
661
  resume(): this {
537
- const id = (this._driver as any)?.id;
538
- debugLog(`Socket.resume() called, id: ${id}`);
662
+ const driver = this._driver as any;
663
+ const id = driver?.id;
664
+ debugLog(`Socket.resume() called, id: ${id === undefined ? 'none' : id}, destroyed: ${this.destroyed}`);
539
665
  super.resume();
540
- if (this._driver) {
666
+ if (driver) {
541
667
  debugLog(`Socket.resume() calling driver.resume(), id: ${id}`);
542
- this._driver.resume();
668
+ driver.resume();
543
669
  }
544
670
  return this;
545
671
  }
@@ -569,8 +695,8 @@ export class Socket extends Duplex {
569
695
  return this;
570
696
  }
571
697
 
572
- get timeout(): number | undefined {
573
- return undefined; // Not tracked strictly as a property yet
698
+ get timeout(): number {
699
+ return this._timeout;
574
700
  }
575
701
 
576
702
  get bufferSize(): number {
@@ -848,6 +974,8 @@ export function createServer(options?: any, connectionListener?: (socket: Socket
848
974
  }
849
975
 
850
976
  export * as tls from './tls'
977
+ export * as http from './http'
978
+ export * as https from './https'
851
979
 
852
980
  export {
853
981
  isIP,
@@ -855,6 +983,7 @@ export {
855
983
  isIPv6,
856
984
  getDefaultAutoSelectFamily,
857
985
  setDefaultAutoSelectFamily,
986
+ isVerbose,
858
987
  setVerbose,
859
988
  initWithConfig,
860
989
  };
@@ -876,4 +1005,6 @@ export default {
876
1005
  setDefaultAutoSelectFamily,
877
1006
  setVerbose,
878
1007
  initWithConfig,
1008
+ http: require('./http'),
1009
+ https: require('./https'),
879
1010
  };
package/src/tls.ts CHANGED
@@ -1,7 +1,14 @@
1
- import { Socket, Server as NetServer, SocketOptions } from './index'
1
+ import { Socket, Server as NetServer, SocketOptions, isVerbose } from './index'
2
2
  import { Driver } from './Driver'
3
3
  import { NetSocketDriver } from './Net.nitro'
4
4
 
5
+ function debugLog(message: string) {
6
+ if (isVerbose()) {
7
+ const timestamp = new Date().toISOString().split('T')[1].split('Z')[0];
8
+ console.log(`[NET DEBUG ${timestamp}] ${message}`);
9
+ }
10
+ }
11
+
5
12
  export interface PeerCertificate {
6
13
  subject: { [key: string]: string }
7
14
  issuer: { [key: string]: string }
@@ -26,6 +33,13 @@ export interface ConnectionOptions extends SocketOptions {
26
33
  pfx?: string | ArrayBuffer
27
34
  passphrase?: string
28
35
  keylog?: boolean // Enable keylogging (SSLKEYLOGFILE format)
36
+ /**
37
+ * Custom hostname verification function.
38
+ * If provided, it will be called after the TLS handshake to verify the peer certificate.
39
+ * Return `undefined` if valid, or an `Error` if invalid.
40
+ * If not provided, the default `checkServerIdentity` is used.
41
+ */
42
+ checkServerIdentity?: (hostname: string, cert: PeerCertificate) => Error | undefined
29
43
  }
30
44
 
31
45
  export interface SecureContextOptions {
@@ -179,7 +193,11 @@ export class TLSSocket extends Socket {
179
193
 
180
194
  renegotiate(options: any, callback: (err: Error | null) => void): boolean {
181
195
  if (callback) {
182
- process.nextTick(() => callback(new Error('Renegotiation is not supported by rustls')));
196
+ setTimeout(() => {
197
+ const err = new Error('Renegotiation is not supported by rustls');
198
+ (err as any).code = 'ERR_TLS_RENEGOTIATION_DISABLED';
199
+ callback(err);
200
+ }, 0);
183
201
  }
184
202
  return false;
185
203
  }
@@ -188,6 +206,39 @@ export class TLSSocket extends Socket {
188
206
  // No-op, already effectively disabled
189
207
  }
190
208
 
209
+ /**
210
+ * Enables trace output for this socket.
211
+ */
212
+ enableTrace(): void {
213
+ const driver = (this as any)._driver as NetSocketDriver
214
+ if (driver) {
215
+ driver.enableTrace()
216
+ }
217
+ }
218
+
219
+ /**
220
+ * Exports keying material for use by external protocols.
221
+ *
222
+ * @param length The number of bytes to return.
223
+ * @param label A label identifying the keying material.
224
+ * @param context An optional context.
225
+ * @returns Buffer containing keying material.
226
+ * @throws Error if export fails (e.g., TLS not connected).
227
+ */
228
+ exportKeyingMaterial(length: number, label: string, context?: Buffer): Buffer {
229
+ const driver = (this as any)._driver as NetSocketDriver
230
+ if (driver) {
231
+ const ctx = context ? new Uint8Array(context).buffer as ArrayBuffer : undefined
232
+ const result = driver.exportKeyingMaterial(length, label, ctx)
233
+ if (result) {
234
+ return Buffer.from(result)
235
+ }
236
+ }
237
+ const err = new Error('exportKeyingMaterial failed: TLS connection may not be established')
238
+ ; (err as any).code = 'ERR_TLS_EXPORT_KEYING_MATERIAL'
239
+ throw err
240
+ }
241
+
191
242
  constructor(socket: Socket, options?: ConnectionOptions)
192
243
  constructor(options: ConnectionOptions)
193
244
  constructor(socketOrOptions: Socket | ConnectionOptions, options?: ConnectionOptions) {
@@ -230,6 +281,21 @@ export class TLSSocket extends Socket {
230
281
  if (connectionListener) this.once('secureConnect', connectionListener);
231
282
 
232
283
  this.once('connect', () => {
284
+ // After the native TLS handshake, perform hostname verification
285
+ if (rejectUnauthorized !== false) {
286
+ const cert = this.getPeerCertificate() as PeerCertificate;
287
+ if (cert && Object.keys(cert).length > 0) {
288
+ const verifyFn = (typeof options === 'object' && options.checkServerIdentity)
289
+ ? options.checkServerIdentity
290
+ : checkServerIdentity;
291
+ const verifyErr = verifyFn(servername, cert);
292
+ if (verifyErr) {
293
+ this.emit('error', verifyErr);
294
+ this.destroy(verifyErr);
295
+ return;
296
+ }
297
+ }
298
+ }
233
299
  this.emit('secureConnect')
234
300
  })
235
301
 
@@ -255,14 +321,18 @@ export class TLSSocket extends Socket {
255
321
 
256
322
  if (path) {
257
323
  if (secureContextId !== undefined) {
324
+ debugLog(`TLSSocket.connect: Calling driver.connectUnixTLSWithContext(${path}, ${servername}, ctx=${secureContextId})`);
258
325
  driver.connectUnixTLSWithContext(path, servername, rejectUnauthorized, secureContextId)
259
326
  } else {
327
+ debugLog(`TLSSocket.connect: Calling driver.connectUnixTLS(${path}, ${servername})`);
260
328
  driver.connectUnixTLS(path, servername, rejectUnauthorized)
261
329
  }
262
330
  } else {
263
331
  if (secureContextId !== undefined) {
332
+ debugLog(`TLSSocket.connect: Calling driver.connectTLSWithContext(${host}, ${port}, ${servername}, ctx=${secureContextId})`);
264
333
  driver.connectTLSWithContext(host, port, servername, rejectUnauthorized, secureContextId)
265
334
  } else {
335
+ debugLog(`TLSSocket.connect: Calling driver.connectTLS(${host}, ${port}, ${servername})`);
266
336
  driver.connectTLS(host, port, servername, rejectUnauthorized)
267
337
  }
268
338
  }
@@ -323,6 +393,9 @@ export class Server extends NetServer {
323
393
  key: options.key,
324
394
  ca: options.ca
325
395
  }).id;
396
+ } else {
397
+ // Create empty secure context to allow late configuration (addContext)
398
+ this._secureContextId = createSecureContext().id;
326
399
  }
327
400
 
328
401
  this.on('connection', (socket: Socket) => {
@@ -408,12 +481,15 @@ export class Server extends NetServer {
408
481
 
409
482
  const driver = (this as any)._driver;
410
483
 
411
- if (handle || _path) {
412
- console.warn("TLS over Unix sockets/handles not fully implemented yet");
484
+ if (_path) {
485
+ driver.listenTLSUnix(_path, this._secureContextId, _backlog);
486
+ } else if (handle) {
487
+ console.warn("TLS over handles not fully implemented yet");
488
+ driver.listenTLS(_port || 0, this._secureContextId, _backlog, ipv6Only, reusePort);
489
+ } else {
490
+ driver.listenTLS(_port || 0, this._secureContextId, _backlog, ipv6Only, reusePort);
413
491
  }
414
492
 
415
- driver.listenTLS(_port || 0, this._secureContextId, _backlog, ipv6Only, reusePort);
416
-
417
493
  return this;
418
494
  }
419
495
  }