lemon-tls 0.2.2 → 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.
- package/README.md +258 -203
- package/index.d.ts +145 -14
- package/index.js +12 -0
- package/package.json +1 -1
- package/src/compat.js +290 -31
- package/src/crypto.js +127 -7
- package/src/record.js +408 -61
- package/src/session/message.js +27 -2
- package/src/session/ticket.js +185 -0
- package/src/tls_session.js +780 -94
- package/src/tls_socket.js +815 -249
- package/src/wire.js +25 -0
package/src/tls_socket.js
CHANGED
|
@@ -1,19 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
|
|
4
|
-
function writeClientRandomKeyLog(clientRandom, masterSecret, filePath) {
|
|
5
|
-
function toHex(u8) {
|
|
6
|
-
return Array.from(u8)
|
|
7
|
-
.map(b => b.toString(16).padStart(2, '0'))
|
|
8
|
-
.join('');
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const line = `CLIENT_RANDOM ${toHex(clientRandom)} ${toHex(masterSecret)}\n`;
|
|
12
|
-
|
|
13
|
-
fs.appendFileSync(filePath, line, 'utf8');
|
|
14
|
-
console.log(`✅ Added CLIENT_RANDOM line to ${filePath}`);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
1
|
import TLSSession from './tls_session.js';
|
|
18
2
|
|
|
19
3
|
import crypto from 'node:crypto';
|
|
@@ -21,15 +5,20 @@ import { Duplex } from 'node:stream';
|
|
|
21
5
|
|
|
22
6
|
import { TLS_CIPHER_SUITES } from './crypto.js';
|
|
23
7
|
import { TLS_CONTENT_TYPE as CT, TLS_ALERT_LEVEL, TLS_ALERT } from './wire.js';
|
|
8
|
+
import { decrypt_session_blob } from './session/ticket.js';
|
|
24
9
|
|
|
25
10
|
import {
|
|
26
11
|
getAeadAlgo,
|
|
27
12
|
deriveKeys as tls_derive_from_tls_secrets,
|
|
28
13
|
getNonce as get_nonce,
|
|
14
|
+
getNonceInto as get_nonce_into, // writes nonce into provided buffer — no alloc per record
|
|
29
15
|
encryptRecord as encrypt_tls_record,
|
|
16
|
+
encryptCompleteRecord13, // fused encrypt + header for TLS 1.3 app-data hot path
|
|
30
17
|
decryptRecord as decrypt_tls_record,
|
|
18
|
+
decryptRecordWithAadView as decrypt_tls_record_with_aad, // zero-alloc AAD path
|
|
31
19
|
parseInnerPlaintext as parse_tls_inner_plaintext,
|
|
32
20
|
encrypt12 as encrypt_tls12_gcm_fragment,
|
|
21
|
+
encryptCompleteRecord12, // fused encrypt + header for TLS 1.2 app-data hot path
|
|
33
22
|
decrypt12 as decrypt_tls12_gcm_fragment,
|
|
34
23
|
deriveKeys12,
|
|
35
24
|
writeRecord as writeRawRecord,
|
|
@@ -38,6 +27,28 @@ import {
|
|
|
38
27
|
// legacy_record_version (TLS 1.3 uses 0x0303 in record header)
|
|
39
28
|
const REC_VERSION = 0x0303;
|
|
40
29
|
|
|
30
|
+
// TLS signature scheme codes → OpenSSL-style names (for getSharedSigalgs).
|
|
31
|
+
// Covers RFC 8446 §4.2.3 (TLS 1.3) and the common TLS 1.2 codes.
|
|
32
|
+
const SIGALG_NAMES = {
|
|
33
|
+
0x0401: 'rsa_pkcs1_sha256',
|
|
34
|
+
0x0501: 'rsa_pkcs1_sha384',
|
|
35
|
+
0x0601: 'rsa_pkcs1_sha512',
|
|
36
|
+
0x0403: 'ecdsa_secp256r1_sha256',
|
|
37
|
+
0x0503: 'ecdsa_secp384r1_sha384',
|
|
38
|
+
0x0603: 'ecdsa_secp521r1_sha512',
|
|
39
|
+
0x0804: 'rsa_pss_rsae_sha256',
|
|
40
|
+
0x0805: 'rsa_pss_rsae_sha384',
|
|
41
|
+
0x0806: 'rsa_pss_rsae_sha512',
|
|
42
|
+
0x0807: 'ed25519',
|
|
43
|
+
0x0808: 'ed448',
|
|
44
|
+
0x0809: 'rsa_pss_pss_sha256',
|
|
45
|
+
0x080a: 'rsa_pss_pss_sha384',
|
|
46
|
+
0x080b: 'rsa_pss_pss_sha512',
|
|
47
|
+
};
|
|
48
|
+
function sigalgCodeToName(code) {
|
|
49
|
+
return SIGALG_NAMES[code] || `0x${code.toString(16).padStart(4, '0')}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
41
52
|
// ==== עזרי המרה ====
|
|
42
53
|
function toBuf(u8){ return Buffer.isBuffer(u8) ? u8 : Buffer.from(u8 || []); }
|
|
43
54
|
function toU8(buf){ return (buf instanceof Uint8Array) ? buf : new Uint8Array(buf || []); }
|
|
@@ -68,7 +79,18 @@ function TLSSocket(duplex, options){
|
|
|
68
79
|
options = options || {};
|
|
69
80
|
|
|
70
81
|
// Inherit from Duplex stream
|
|
71
|
-
|
|
82
|
+
// highWaterMark: raised to 256KB from the Node default of 16KB. TLS records max
|
|
83
|
+
// at 16KB of plaintext, so the default meant "pause after one record" — terrible
|
|
84
|
+
// for bulk transfers where we want many records in flight. 256KB keeps ~16 records
|
|
85
|
+
// queuable and lets the kernel TCP stack see a continuous stream instead of many
|
|
86
|
+
// small bursts interrupted by drain events. Most impactful on the Download path
|
|
87
|
+
// where the sender is aggressive (tight-loop sock.write).
|
|
88
|
+
Duplex.call(this, {
|
|
89
|
+
allowHalfOpen: true,
|
|
90
|
+
readableObjectMode: false,
|
|
91
|
+
writableObjectMode: false,
|
|
92
|
+
highWaterMark: 256 * 1024,
|
|
93
|
+
});
|
|
72
94
|
const self = this;
|
|
73
95
|
|
|
74
96
|
let _ticketKeys = options.ticketKeys ? Buffer.from(options.ticketKeys) : crypto.randomBytes(48);
|
|
@@ -87,11 +109,12 @@ function TLSSocket(duplex, options){
|
|
|
87
109
|
ALPNProtocols: options.ALPNProtocols || null,
|
|
88
110
|
SNICallback: options.SNICallback || null,
|
|
89
111
|
ticketKeys: _ticketKeys,
|
|
112
|
+
ticketLifetime: options.ticketLifetime,
|
|
90
113
|
session: options.session || null,
|
|
91
114
|
psk: options.psk || null,
|
|
92
115
|
rejectUnauthorized: options.rejectUnauthorized,
|
|
93
116
|
ca: options.ca || null,
|
|
94
|
-
|
|
117
|
+
sessionTickets: options.sessionTickets,
|
|
95
118
|
maxHandshakeSize: options.maxHandshakeSize || 0,
|
|
96
119
|
customExtensions: options.customExtensions || [],
|
|
97
120
|
requestCert: !!options.requestCert,
|
|
@@ -124,6 +147,13 @@ function TLSSocket(duplex, options){
|
|
|
124
147
|
app_read_seq: 0,
|
|
125
148
|
app_read_aead: null,
|
|
126
149
|
|
|
150
|
+
// Reusable 12-byte nonce scratch buffers — one per direction. Populated
|
|
151
|
+
// in-place per record from (iv XOR seq) and handed to createCipheriv which
|
|
152
|
+
// copies it into OpenSSL state. Saves 12-byte allocation per record —
|
|
153
|
+
// ~7.7KB per 10MB transfer, less GC pressure.
|
|
154
|
+
_nonceEncScratch: new Uint8Array(12),
|
|
155
|
+
_nonceDecScratch: new Uint8Array(12),
|
|
156
|
+
|
|
127
157
|
using_app_keys: false,
|
|
128
158
|
|
|
129
159
|
remote_ccs_seen: false,
|
|
@@ -131,8 +161,28 @@ function TLSSocket(duplex, options){
|
|
|
131
161
|
|
|
132
162
|
tls12_read_seq: 0,
|
|
133
163
|
|
|
134
|
-
// Buffers and queues
|
|
135
|
-
|
|
164
|
+
// Buffers and queues.
|
|
165
|
+
//
|
|
166
|
+
// readBuffer is a growable receive buffer with two offsets:
|
|
167
|
+
// - readStart: next byte to be parsed (advanced as records are consumed)
|
|
168
|
+
// - readEnd: next byte to be written by an incoming chunk
|
|
169
|
+
//
|
|
170
|
+
// When readStart === readEnd the buffer is fully drained; both reset to 0 and
|
|
171
|
+
// the underlying Buffer is reused (no reallocation). When a new chunk can't fit
|
|
172
|
+
// at the end we compact by moving the unread portion back to 0. Only when that
|
|
173
|
+
// still isn't enough do we double the buffer capacity.
|
|
174
|
+
//
|
|
175
|
+
// This gives O(N) total copy cost regardless of how data fragments across TCP
|
|
176
|
+
// chunks — versus the quadratic cost of `readBuffer = Buffer.concat([...])`.
|
|
177
|
+
// Initial 64KB capacity — fits 4 full TLS records (16KB each) or the entire
|
|
178
|
+
// handshake transcript for most certs. Avoids the first ~3 grow-and-copy
|
|
179
|
+
// cycles when readBuffer starts at 0 and an inbound 16KB chunk forces
|
|
180
|
+
// immediate doubling from 0→64KB across 3 reallocs. For short-lived
|
|
181
|
+
// connections (HTTP request/response), this single upfront allocation
|
|
182
|
+
// commonly means ZERO readBuffer resizes during the connection's lifetime.
|
|
183
|
+
readBuffer: Buffer.allocUnsafe(65536),
|
|
184
|
+
readStart: 0,
|
|
185
|
+
readEnd: 0,
|
|
136
186
|
appWriteQueue: [],
|
|
137
187
|
pendingHandshake: [],
|
|
138
188
|
|
|
@@ -146,7 +196,7 @@ function TLSSocket(duplex, options){
|
|
|
146
196
|
|
|
147
197
|
// Advanced options
|
|
148
198
|
maxRecordSize: options.maxRecordSize || 16384,
|
|
149
|
-
|
|
199
|
+
sessionTickets: options.sessionTickets !== false,
|
|
150
200
|
pins: options.pins || null, // ['sha256/AAAA...'] certificate pinning
|
|
151
201
|
handshakeTimeout: options.handshakeTimeout || 0, // ms, 0 = no timeout
|
|
152
202
|
allowedCipherSuites: options.allowedCipherSuites || null, // [0x1301, ...] whitelist
|
|
@@ -161,205 +211,356 @@ function TLSSocket(duplex, options){
|
|
|
161
211
|
|
|
162
212
|
|
|
163
213
|
// === Record Layer ===
|
|
214
|
+
// writeRecord returns the transport's backpressure signal (true = ready for more,
|
|
215
|
+
// false = transport buffer full, wait for 'drain' before writing more).
|
|
164
216
|
function writeRecord(type, payload){
|
|
165
217
|
if (!context.transport) throw new Error('No transport attached to TLSSocket');
|
|
166
|
-
if (context.destroyed || context.transport.destroyed || context.transport.writableEnded) return;
|
|
167
|
-
try { writeRawRecord(context.transport, type, payload, context.rec_version); }
|
|
168
|
-
catch(e){ self.emit('error', e); }
|
|
218
|
+
if (context.destroyed || context.transport.destroyed || context.transport.writableEnded) return false;
|
|
219
|
+
try { return writeRawRecord(context.transport, type, payload, context.rec_version); }
|
|
220
|
+
catch(e){ self.emit('error', e); return false; }
|
|
169
221
|
}
|
|
170
222
|
|
|
171
223
|
const MAX_RECORD_PLAINTEXT = 16384; // TLS max record size (2^14)
|
|
172
224
|
|
|
225
|
+
// writeAppData / writeAppDataSingle return the transport's backpressure signal:
|
|
226
|
+
// true → transport is ready for more writes
|
|
227
|
+
// false → transport's internal buffer is full; caller should wait for 'drain'
|
|
228
|
+
// on context.transport before writing more. Propagated up to _write so
|
|
229
|
+
// user sock.write() returns false and Node's stream flow control kicks in.
|
|
173
230
|
function writeAppData(plain){
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
if (
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
231
|
+
const total = plain.length;
|
|
232
|
+
const maxSize = context.maxRecordSize || MAX_RECORD_PLAINTEXT;
|
|
233
|
+
if (total > maxSize) {
|
|
234
|
+
// Fragment across multiple records. Cork+uncork lets Node batch the
|
|
235
|
+
// individual writes into a single TCP send (fewer syscalls, less overhead).
|
|
236
|
+
const t = context.transport;
|
|
237
|
+
const corkable = t && typeof t.cork === 'function' && typeof t.uncork === 'function';
|
|
238
|
+
if (corkable) t.cork();
|
|
239
|
+
let lastOk = true;
|
|
240
|
+
try {
|
|
241
|
+
for (let off = 0; off < total; off += maxSize) {
|
|
242
|
+
const endOff = off + maxSize > total ? total : off + maxSize;
|
|
243
|
+
if (!writeAppDataSingle(plain.subarray(off, endOff))) lastOk = false;
|
|
244
|
+
}
|
|
245
|
+
} finally {
|
|
246
|
+
if (corkable) t.uncork();
|
|
180
247
|
}
|
|
181
|
-
return;
|
|
248
|
+
return lastOk;
|
|
182
249
|
}
|
|
183
|
-
writeAppDataSingle(plain);
|
|
250
|
+
return writeAppDataSingle(plain);
|
|
184
251
|
}
|
|
185
252
|
|
|
186
253
|
function writeAppDataSingle(plain){
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
254
|
+
// Hot path — single-allocation fused encrypt+frame.
|
|
255
|
+
//
|
|
256
|
+
// encryptCompleteRecord13/12 produce a complete TLS record (header +
|
|
257
|
+
// encrypted body + tag) in ONE Buffer, which we hand directly to the
|
|
258
|
+
// transport. This skips:
|
|
259
|
+
// - a separate AAD buffer (5 bytes for TLS 1.3, 13 for TLS 1.2)
|
|
260
|
+
// - Buffer.concat of ct+tag (which itself allocates + 3 copies)
|
|
261
|
+
// - writeRecord's rec allocation and rec.set(payload, 5) copy
|
|
262
|
+
//
|
|
263
|
+
// Net savings per record: 2 allocations + 1 × plaintext-sized copy.
|
|
264
|
+
// For a 10MB transfer at 16KB records (640 records), that's ~10MB of
|
|
265
|
+
// avoided copies and ~1300 fewer allocations → less GC pressure, higher
|
|
266
|
+
// sustained throughput.
|
|
267
|
+
|
|
268
|
+
if (context.destroyed || !context.transport) return false;
|
|
269
|
+
const t = context.transport;
|
|
270
|
+
if (t.destroyed || t.writableEnded) return false;
|
|
271
|
+
|
|
272
|
+
if (context.isTls13) {
|
|
273
|
+
if (context.app_write_key === null) {
|
|
274
|
+
const ts = session.getTrafficSecrets();
|
|
275
|
+
if (ts.localAppSecret === null) return true; // keys not ready — silently drop
|
|
276
|
+
const d = tls_derive_from_tls_secrets(ts.localAppSecret, ts.cipher);
|
|
277
|
+
context.app_write_key = d.key;
|
|
278
|
+
context.app_write_iv = d.iv;
|
|
199
279
|
}
|
|
200
280
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
context.app_write_seq++;
|
|
204
|
-
|
|
281
|
+
const algo = context.aeadAlgo || getAeadAlgo(session.getCipher());
|
|
205
282
|
try {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
283
|
+
const rec = encryptCompleteRecord13(
|
|
284
|
+
CT.APPLICATION_DATA, plain,
|
|
285
|
+
context.app_write_key,
|
|
286
|
+
get_nonce_into(context._nonceEncScratch, context.app_write_iv, context.app_write_seq),
|
|
287
|
+
algo,
|
|
288
|
+
context.rec_version
|
|
289
|
+
);
|
|
290
|
+
context.app_write_seq++;
|
|
291
|
+
return t.write(rec);
|
|
292
|
+
} catch(e) {
|
|
293
|
+
self.emit('error', e);
|
|
294
|
+
return false;
|
|
209
295
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
let d12 = deriveKeys12(session.getTrafficSecrets().masterSecret, session.getTrafficSecrets().localRandom, session.getTrafficSecrets().remoteRandom, session.getCipher(), session.isServer);
|
|
296
|
+
} else {
|
|
297
|
+
if (context.app_write_key === null) {
|
|
298
|
+
const ts = session.getTrafficSecrets();
|
|
299
|
+
const d12 = deriveKeys12(ts.masterSecret, ts.localRandom, ts.remoteRandom, ts.cipher, ts.isServer);
|
|
215
300
|
context.app_write_key = d12.writeKey;
|
|
216
301
|
context.app_write_iv = d12.writeIv;
|
|
217
302
|
}
|
|
218
303
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
304
|
+
try {
|
|
305
|
+
const rec = encryptCompleteRecord12(
|
|
306
|
+
plain,
|
|
307
|
+
context.app_write_key,
|
|
308
|
+
context.app_write_iv,
|
|
309
|
+
context.app_write_seq,
|
|
310
|
+
CT.APPLICATION_DATA,
|
|
311
|
+
context.rec_version
|
|
312
|
+
);
|
|
313
|
+
context.app_write_seq++;
|
|
314
|
+
return t.write(rec);
|
|
315
|
+
} catch(e) {
|
|
316
|
+
self.emit('error', e);
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
230
319
|
}
|
|
231
|
-
|
|
232
320
|
}
|
|
233
321
|
|
|
234
|
-
function processCiphertext(body){
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
322
|
+
function processCiphertext(body, header){
|
|
323
|
+
// Hot path — same optimizations as writeAppDataSingle:
|
|
324
|
+
// - cached context.isTls13 instead of per-record session.getVersion()
|
|
325
|
+
// - cached context.aeadAlgo instead of per-record getAeadAlgo()
|
|
326
|
+
// - getTrafficSecrets() called once per key derivation (not per decrypt)
|
|
327
|
+
// - For TLS 1.3 app-data: header view is used directly as AAD, avoiding
|
|
328
|
+
// the 5-byte AAD allocation that decryptRecord would otherwise do
|
|
329
|
+
let out = null;
|
|
330
|
+
const isTls13 = context.isTls13 !== undefined
|
|
331
|
+
? context.isTls13
|
|
332
|
+
: session.getVersion() === 0x0304; // early records (pre-secureConnect)
|
|
333
|
+
|
|
334
|
+
if (!isTls13 && context.remote_ccs_seen === true) {
|
|
240
335
|
// TLS 1.2: decrypt with key_block derived keys
|
|
241
|
-
if(context.app_read_key
|
|
336
|
+
if (context.app_read_key === null) {
|
|
337
|
+
const ts = session.getTrafficSecrets();
|
|
242
338
|
|
|
243
|
-
|
|
339
|
+
// Guard: if master_secret isn't derived yet, we can't compute keys.
|
|
340
|
+
if (!ts.masterSecret) {
|
|
341
|
+
self.emit('error', new Error('Received encrypted record before master_secret derived'));
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const d12 = deriveKeys12(ts.masterSecret, ts.localRandom, ts.remoteRandom, ts.cipher, ts.isServer);
|
|
244
346
|
context.app_read_key = d12.readKey;
|
|
245
347
|
context.app_read_iv = d12.readIv;
|
|
246
348
|
context.app_write_key = d12.writeKey;
|
|
247
349
|
context.app_write_iv = d12.writeIv;
|
|
248
350
|
}
|
|
249
351
|
|
|
250
|
-
|
|
251
|
-
out = decrypt_tls12_gcm_fragment(
|
|
352
|
+
const recordType = context.using_app_keys ? 0x17 : 0x16;
|
|
353
|
+
out = decrypt_tls12_gcm_fragment(body, context.app_read_key, context.app_read_iv, context.tls12_read_seq, recordType);
|
|
252
354
|
|
|
253
|
-
}else if(isTls13 && context.using_app_keys
|
|
355
|
+
} else if (isTls13 && context.using_app_keys === true) {
|
|
254
356
|
// TLS 1.3: decrypt app data with app traffic secret
|
|
255
|
-
if(
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
357
|
+
if (context.app_read_key === null) {
|
|
358
|
+
const ts = session.getTrafficSecrets();
|
|
359
|
+
if (ts.remoteAppSecret === null) return;
|
|
360
|
+
const d = tls_derive_from_tls_secrets(ts.remoteAppSecret, ts.cipher);
|
|
361
|
+
context.app_read_key = d.key;
|
|
362
|
+
context.app_read_iv = d.iv;
|
|
363
|
+
}
|
|
261
364
|
|
|
262
|
-
|
|
263
|
-
|
|
365
|
+
const algo = context.aeadAlgo || getAeadAlgo(session.getCipher());
|
|
366
|
+
// Use header-view AAD path when header is available (parseRecordsAndDispatch).
|
|
367
|
+
// Saves a 5-byte allocation per record — small individually but meaningful
|
|
368
|
+
// cumulatively on the Download hot path (many records per second).
|
|
369
|
+
// get_nonce_into also reuses context._nonceDecScratch to avoid alloc per record.
|
|
370
|
+
const nonce = get_nonce_into(context._nonceDecScratch, context.app_read_iv, context.app_read_seq);
|
|
371
|
+
if (header !== undefined) {
|
|
372
|
+
out = decrypt_tls_record_with_aad(header, body, context.app_read_key, nonce, algo);
|
|
373
|
+
} else {
|
|
374
|
+
out = decrypt_tls_record(body, context.app_read_key, nonce, algo);
|
|
264
375
|
}
|
|
265
|
-
|
|
376
|
+
context.app_read_seq++;
|
|
377
|
+
|
|
378
|
+
} else if (isTls13) {
|
|
266
379
|
// TLS 1.3: decrypt handshake with handshake traffic secret
|
|
267
|
-
if(
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
380
|
+
if (context.handshake_read_key === null) {
|
|
381
|
+
const hs = session.getHandshakeSecrets();
|
|
382
|
+
if (hs.remoteSecret === null || hs.cipher === null) return;
|
|
383
|
+
const d = tls_derive_from_tls_secrets(hs.remoteSecret, hs.cipher);
|
|
384
|
+
context.handshake_read_key = d.key;
|
|
385
|
+
context.handshake_read_iv = d.iv;
|
|
386
|
+
}
|
|
273
387
|
|
|
274
|
-
|
|
275
|
-
|
|
388
|
+
const algo = context.aeadAlgo || getAeadAlgo(session.getCipher());
|
|
389
|
+
const nonce = get_nonce_into(context._nonceDecScratch, context.handshake_read_iv, context.handshake_read_seq);
|
|
390
|
+
if (header !== undefined) {
|
|
391
|
+
out = decrypt_tls_record_with_aad(header, body, context.handshake_read_key, nonce, algo);
|
|
392
|
+
} else {
|
|
393
|
+
out = decrypt_tls_record(body, context.handshake_read_key, nonce, algo);
|
|
276
394
|
}
|
|
395
|
+
context.handshake_read_seq++;
|
|
277
396
|
}
|
|
278
397
|
|
|
279
398
|
|
|
280
|
-
if(out!==null){
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
399
|
+
if (out !== null) {
|
|
400
|
+
// Reuse cached isTls13 computed at the top of this function
|
|
401
|
+
if (isTls13) {
|
|
402
|
+
// Inlined parseInnerPlaintext — parse_tls_inner_plaintext would
|
|
403
|
+
// allocate a {type, content} object per record. With 640 records
|
|
404
|
+
// per 10MB download, that's 640 short-lived objects just for
|
|
405
|
+
// destructuring — enough GC pressure to matter. We do the scan
|
|
406
|
+
// and dispatch directly here.
|
|
407
|
+
let j = out.length - 1;
|
|
408
|
+
while (j >= 0 && out[j] === 0) j--;
|
|
409
|
+
if (j < 0) {
|
|
410
|
+
self.emit('error', new Error('Malformed TLSInnerPlaintext'));
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
const content_type = out[j];
|
|
414
|
+
|
|
415
|
+
if (content_type === CT.APPLICATION_DATA) {
|
|
416
|
+
// zero-copy view into the decrypted plaintext
|
|
417
|
+
self.push(out.subarray(0, j));
|
|
418
|
+
} else if (content_type === CT.HANDSHAKE) {
|
|
419
|
+
session.message(out.subarray(0, j));
|
|
420
|
+
} else if (content_type === CT.ALERT) {
|
|
293
421
|
// TODO: handle alert
|
|
294
422
|
}
|
|
295
|
-
}else{
|
|
296
|
-
// TLS 1.2: no inner plaintext wrapping
|
|
297
|
-
if(context.using_app_keys
|
|
298
|
-
self.push(
|
|
299
|
-
}else{
|
|
300
|
-
session.message(
|
|
423
|
+
} else {
|
|
424
|
+
// TLS 1.2: no inner plaintext wrapping — `out` is the Buffer from decrypt12
|
|
425
|
+
if (context.using_app_keys === true) {
|
|
426
|
+
self.push(out);
|
|
427
|
+
} else {
|
|
428
|
+
session.message(out);
|
|
301
429
|
}
|
|
302
430
|
}
|
|
303
431
|
}
|
|
304
|
-
|
|
432
|
+
}
|
|
305
433
|
|
|
306
434
|
|
|
307
|
-
|
|
435
|
+
// ============================================================================
|
|
436
|
+
// Read buffer management
|
|
437
|
+
// ============================================================================
|
|
438
|
+
// The incoming TLS record queue is stored in a single growable Buffer with two
|
|
439
|
+
// offsets: readStart (next byte to read) and readEnd (next byte to write).
|
|
440
|
+
//
|
|
441
|
+
// Design rationale:
|
|
442
|
+
// - Chunks arriving from TCP are copied in-place at readEnd (single copy).
|
|
443
|
+
// - Records are parsed as zero-copy subarray views between readStart and readEnd.
|
|
444
|
+
// - When all data is consumed, both offsets reset to 0 (buffer is reused).
|
|
445
|
+
// - If the tail doesn't fit at the end, we compact (move unread to start).
|
|
446
|
+
// - Only if compaction isn't enough, we grow the buffer (doubling).
|
|
447
|
+
//
|
|
448
|
+
// Why this beats `readBuffer = Buffer.concat([readBuffer, chunk])`:
|
|
449
|
+
// The naive approach is O(N²) because each concat re-copies the entire
|
|
450
|
+
// growing buffer. With a 16KB record arriving in 1.4KB MTU chunks
|
|
451
|
+
// (~12 chunks), the naive approach performs ~110KB of copy work for 16KB
|
|
452
|
+
// of data. This approach performs 16KB.
|
|
453
|
+
function _appendChunk(chunk) {
|
|
454
|
+
const rb = context.readBuffer;
|
|
455
|
+
const readStart = context.readStart;
|
|
456
|
+
const readEnd = context.readEnd;
|
|
457
|
+
const chunkLen = chunk.length;
|
|
458
|
+
|
|
459
|
+
// Fast path: fits directly at the end of the existing buffer
|
|
460
|
+
if (readEnd + chunkLen <= rb.length) {
|
|
461
|
+
chunk.copy(rb, readEnd);
|
|
462
|
+
context.readEnd = readEnd + chunkLen;
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
308
465
|
|
|
466
|
+
const unread = readEnd - readStart;
|
|
467
|
+
const needed = unread + chunkLen;
|
|
468
|
+
|
|
469
|
+
// Compact path: unread data + new chunk fit in existing buffer, but not at the end
|
|
470
|
+
if (needed <= rb.length) {
|
|
471
|
+
if (readStart > 0 && unread > 0) rb.copy(rb, 0, readStart, readEnd);
|
|
472
|
+
chunk.copy(rb, unread);
|
|
473
|
+
context.readStart = 0;
|
|
474
|
+
context.readEnd = needed;
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Grow path: allocate a bigger buffer (doubling, with a minimum of 8KB)
|
|
479
|
+
let newSize = rb.length > 0 ? rb.length * 2 : 8192;
|
|
480
|
+
while (newSize < needed) newSize *= 2;
|
|
481
|
+
const newBuf = Buffer.allocUnsafe(newSize);
|
|
482
|
+
if (unread > 0) rb.copy(newBuf, 0, readStart, readEnd);
|
|
483
|
+
chunk.copy(newBuf, unread);
|
|
484
|
+
context.readBuffer = newBuf;
|
|
485
|
+
context.readStart = 0;
|
|
486
|
+
context.readEnd = needed;
|
|
487
|
+
}
|
|
309
488
|
|
|
310
489
|
function parseRecordsAndDispatch(){
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
490
|
+
// Hot path — zero-copy parsing:
|
|
491
|
+
// - Work directly on readBuffer with moving readStart offset (no slicing)
|
|
492
|
+
// - Body is a subarray view (no copy) passed down to crypto
|
|
493
|
+
// - The 5-byte header view is also passed — TLS 1.3 uses it as AAD
|
|
494
|
+
// directly, avoiding a 5-byte allocation per decrypted record
|
|
495
|
+
// - Only tracks and advances offsets; buffer stays the same reference
|
|
496
|
+
const rb = context.readBuffer;
|
|
497
|
+
let off = context.readStart;
|
|
498
|
+
const end = context.readEnd;
|
|
499
|
+
|
|
500
|
+
while (end - off >= 5) {
|
|
501
|
+
const type = rb[off];
|
|
502
|
+
// Version at rb[off+1..off+3] — currently unused (legacy 0x0303 always)
|
|
503
|
+
const len = (rb[off + 3] << 8) | rb[off + 4];
|
|
504
|
+
|
|
505
|
+
if (end - off < 5 + len) break; // record not fully received
|
|
506
|
+
|
|
507
|
+
// Zero-copy views: header (5 bytes) + body
|
|
508
|
+
const header = rb.subarray(off, off + 5);
|
|
509
|
+
const body = rb.subarray(off + 5, off + 5 + len);
|
|
510
|
+
off += 5 + len;
|
|
322
511
|
|
|
512
|
+
if (type === CT.APPLICATION_DATA) {
|
|
513
|
+
processCiphertext(body, header);
|
|
323
514
|
context.tls12_read_seq++;
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
515
|
+
} else if (type === CT.HANDSHAKE) {
|
|
516
|
+
if (context.remote_ccs_seen === true) {
|
|
517
|
+
// Encrypted handshake (TLS 1.2 Finished post-CCS). decrypt output is
|
|
518
|
+
// a FRESH Buffer per record — safe to pass views of it downstream.
|
|
519
|
+
processCiphertext(body, header);
|
|
520
|
+
} else {
|
|
521
|
+
// Plaintext handshake (ClientHello, ServerHello, Certificate, ...).
|
|
522
|
+
// `body` is a zero-copy view into readBuffer. session.message()
|
|
523
|
+
// stashes the raw bytes into the handshake transcript (used later
|
|
524
|
+
// for Finished MAC verification and TLS 1.3 key derivation), and
|
|
525
|
+
// also extracts fields like remote_random as subarray views.
|
|
526
|
+
//
|
|
527
|
+
// When the NEXT incoming chunk triggers readBuffer compact/reuse,
|
|
528
|
+
// those stashed views would point to overwritten bytes — causing
|
|
529
|
+
// bad key derivations and GCM tag failures much later. We must hand
|
|
530
|
+
// the session an owned Buffer so its transcript survives.
|
|
531
|
+
session.message(Buffer.from(body));
|
|
330
532
|
}
|
|
331
|
-
|
|
332
533
|
context.tls12_read_seq++;
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
context.remote_ccs_seen=true;
|
|
338
|
-
|
|
339
|
-
}else if(type === CT.ALERT ){
|
|
534
|
+
} else if (type === CT.CHANGE_CIPHER_SPEC) {
|
|
535
|
+
context.tls12_read_seq = 0;
|
|
536
|
+
context.remote_ccs_seen = true;
|
|
537
|
+
} else if (type === CT.ALERT) {
|
|
340
538
|
// Alert: 2 bytes — level (1=warning, 2=fatal), description
|
|
341
|
-
if(body.length >= 2){
|
|
342
|
-
|
|
343
|
-
|
|
539
|
+
if (body.length >= 2) {
|
|
540
|
+
const level = body[0];
|
|
541
|
+
const desc = body[1];
|
|
344
542
|
self.emit('alert', { level: level, description: desc });
|
|
345
|
-
if(desc === TLS_ALERT.CLOSE_NOTIFY){
|
|
346
|
-
// Peer is closing — send close_notify back and close
|
|
543
|
+
if (desc === TLS_ALERT.CLOSE_NOTIFY) {
|
|
347
544
|
session.close();
|
|
348
|
-
if(context.transport && typeof context.transport.end === 'function'){
|
|
545
|
+
if (context.transport && typeof context.transport.end === 'function') {
|
|
349
546
|
context.transport.end();
|
|
350
547
|
}
|
|
351
548
|
}
|
|
352
|
-
if(level === TLS_ALERT_LEVEL.FATAL){
|
|
353
|
-
|
|
354
|
-
if(context.transport && typeof context.transport.destroy === 'function'){
|
|
549
|
+
if (level === TLS_ALERT_LEVEL.FATAL) {
|
|
550
|
+
if (context.transport && typeof context.transport.destroy === 'function') {
|
|
355
551
|
context.transport.destroy();
|
|
356
552
|
}
|
|
357
553
|
}
|
|
358
554
|
}
|
|
359
555
|
}
|
|
556
|
+
}
|
|
360
557
|
|
|
361
|
-
|
|
362
|
-
|
|
558
|
+
// Reset offsets when buffer is fully drained (enables fast path on next chunk)
|
|
559
|
+
if (off >= end) {
|
|
560
|
+
context.readStart = 0;
|
|
561
|
+
context.readEnd = 0;
|
|
562
|
+
} else {
|
|
563
|
+
context.readStart = off;
|
|
363
564
|
}
|
|
364
565
|
}
|
|
365
566
|
|
|
@@ -367,7 +568,7 @@ function TLSSocket(duplex, options){
|
|
|
367
568
|
function bindTransport(){
|
|
368
569
|
if (!context.transport) return;
|
|
369
570
|
context.transport.on('data', function(chunk){
|
|
370
|
-
|
|
571
|
+
_appendChunk(chunk);
|
|
371
572
|
parseRecordsAndDispatch();
|
|
372
573
|
});
|
|
373
574
|
context.transport.on('error', function(err){ self.emit('error', err); });
|
|
@@ -375,18 +576,55 @@ function TLSSocket(duplex, options){
|
|
|
375
576
|
}
|
|
376
577
|
|
|
377
578
|
session.on('message', function(epoch, seq, type, data){
|
|
378
|
-
|
|
579
|
+
const buf = toBuf(data || []);
|
|
580
|
+
|
|
581
|
+
// Resolve TLS version: use cached isTls13 if set (post-secureConnect); otherwise query.
|
|
582
|
+
// During handshake we may not yet have context.isTls13 set — only after secureConnect.
|
|
583
|
+
const isTls13 = context.isTls13 !== undefined
|
|
584
|
+
? context.isTls13
|
|
585
|
+
: session.getVersion() === 0x0304;
|
|
379
586
|
|
|
380
|
-
// Alert messages —
|
|
587
|
+
// Alert messages — encrypt based on current epoch (0=plaintext, 1=handshake, 2=app)
|
|
381
588
|
if (type === 'alert') {
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
// TLS 1.3: post-handshake alerts are encrypted
|
|
385
|
-
let enc = encrypt_tls_record(CT.ALERT, buf, context.app_write_key, get_nonce(context.app_write_iv, context.app_write_seq), context.aeadAlgo || getAeadAlgo(session.getCipher()));
|
|
386
|
-
context.app_write_seq++;
|
|
387
|
-
writeRecord(CT.APPLICATION_DATA, Buffer.from(enc));
|
|
388
|
-
} else {
|
|
589
|
+
if (epoch === 0) {
|
|
590
|
+
// Pre-handshake: plaintext alert
|
|
389
591
|
writeRecord(CT.ALERT, buf);
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
if (isTls13) {
|
|
596
|
+
// TLS 1.3: wrap alert as APPLICATION_DATA (inner type is ALERT)
|
|
597
|
+
const writeKey = (epoch === 2) ? context.app_write_key : context.handshake_write_key;
|
|
598
|
+
const writeIv = (epoch === 2) ? context.app_write_iv : context.handshake_write_iv;
|
|
599
|
+
const writeSeq = (epoch === 2) ? context.app_write_seq : context.handshake_write_seq;
|
|
600
|
+
|
|
601
|
+
if (!writeKey) {
|
|
602
|
+
// Keys not ready — fall back to plaintext (defensive)
|
|
603
|
+
writeRecord(CT.ALERT, buf);
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
const algo = context.aeadAlgo || getAeadAlgo(session.getCipher());
|
|
608
|
+
const enc = encrypt_tls_record(CT.ALERT, buf, writeKey, get_nonce(writeIv, writeSeq), algo);
|
|
609
|
+
|
|
610
|
+
if (epoch === 2) context.app_write_seq++;
|
|
611
|
+
else context.handshake_write_seq++;
|
|
612
|
+
|
|
613
|
+
writeRecord(CT.APPLICATION_DATA, enc);
|
|
614
|
+
} else {
|
|
615
|
+
// TLS 1.2: post-CCS alerts are encrypted with app keys (AES-GCM).
|
|
616
|
+
// Outer record type remains ALERT (21), body is encrypted.
|
|
617
|
+
if (!context.app_write_key) {
|
|
618
|
+
writeRecord(CT.ALERT, buf);
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
const fragment = encrypt_tls12_gcm_fragment(
|
|
623
|
+
buf, context.app_write_key, context.app_write_iv,
|
|
624
|
+
context.app_write_seq, CT.ALERT
|
|
625
|
+
);
|
|
626
|
+
context.app_write_seq++;
|
|
627
|
+
writeRecord(CT.ALERT, fragment);
|
|
390
628
|
}
|
|
391
629
|
return;
|
|
392
630
|
}
|
|
@@ -402,82 +640,97 @@ function TLSSocket(duplex, options){
|
|
|
402
640
|
}
|
|
403
641
|
|
|
404
642
|
if (epoch === 1) {
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
if(isTls13){
|
|
643
|
+
if (isTls13) {
|
|
408
644
|
// TLS 1.3: encrypt handshake messages with handshake traffic secret
|
|
409
|
-
if(
|
|
410
|
-
|
|
411
|
-
if(
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
context.handshake_write_key=d.key;
|
|
415
|
-
context.handshake_write_iv=d.iv;
|
|
645
|
+
if (context.handshake_write_key === null) {
|
|
646
|
+
const hs = session.getHandshakeSecrets();
|
|
647
|
+
if (hs.localSecret === null) {
|
|
648
|
+
self.emit('error', new Error('Missing handshake write keys'));
|
|
649
|
+
return;
|
|
416
650
|
}
|
|
651
|
+
const d = tls_derive_from_tls_secrets(hs.localSecret, hs.cipher);
|
|
652
|
+
context.handshake_write_key = d.key;
|
|
653
|
+
context.handshake_write_iv = d.iv;
|
|
654
|
+
}
|
|
417
655
|
|
|
418
|
-
|
|
419
|
-
|
|
656
|
+
const algo = context.aeadAlgo || getAeadAlgo(session.getCipher());
|
|
657
|
+
try {
|
|
658
|
+
// Fused encrypt+frame — produces a complete TLS record buffer
|
|
659
|
+
// ready for transport.write. Same optimization as writeAppDataSingle.
|
|
660
|
+
const rec = encryptCompleteRecord13(
|
|
661
|
+
CT.HANDSHAKE, buf,
|
|
662
|
+
context.handshake_write_key,
|
|
663
|
+
get_nonce_into(context._nonceEncScratch, context.handshake_write_iv, context.handshake_write_seq),
|
|
664
|
+
algo,
|
|
665
|
+
context.rec_version
|
|
666
|
+
);
|
|
420
667
|
context.handshake_write_seq++;
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
writeRecord(CT.APPLICATION_DATA, Buffer.from(enc1));
|
|
424
|
-
} catch(e){
|
|
425
|
-
self.emit('error', e);
|
|
668
|
+
if (context.transport && !context.transport.destroyed && !context.transport.writableEnded) {
|
|
669
|
+
context.transport.write(rec);
|
|
426
670
|
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
self.emit('error', new Error('Missing handshake write keys'));
|
|
671
|
+
} catch(e) {
|
|
672
|
+
self.emit('error', e);
|
|
430
673
|
}
|
|
431
|
-
|
|
432
|
-
}else{
|
|
674
|
+
} else {
|
|
433
675
|
// TLS 1.2: send CCS first, then encrypt Finished with key_block keys
|
|
434
|
-
if(context.local_ccs_sent
|
|
676
|
+
if (context.local_ccs_sent === false) {
|
|
435
677
|
writeRecord(CT.CHANGE_CIPHER_SPEC, Buffer.from([0x01]));
|
|
436
|
-
context.local_ccs_sent=true;
|
|
437
|
-
|
|
438
|
-
context.app_write_seq=0;
|
|
678
|
+
context.local_ccs_sent = true;
|
|
679
|
+
context.app_write_seq = 0;
|
|
439
680
|
}
|
|
440
681
|
|
|
441
|
-
// Derive keys
|
|
442
|
-
if(context.app_write_key
|
|
443
|
-
|
|
682
|
+
// Derive keys once per direction
|
|
683
|
+
if (context.app_write_key === null) {
|
|
684
|
+
const ts = session.getTrafficSecrets();
|
|
685
|
+
const d12 = deriveKeys12(ts.masterSecret, ts.localRandom, ts.remoteRandom, ts.cipher, ts.isServer);
|
|
444
686
|
context.app_read_key = d12.readKey;
|
|
445
687
|
context.app_read_iv = d12.readIv;
|
|
446
688
|
context.app_write_key = d12.writeKey;
|
|
447
689
|
context.app_write_iv = d12.writeIv;
|
|
448
690
|
}
|
|
449
691
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
692
|
+
try {
|
|
693
|
+
const rec = encryptCompleteRecord12(
|
|
694
|
+
buf, context.app_write_key, context.app_write_iv,
|
|
695
|
+
context.app_write_seq, CT.HANDSHAKE, context.rec_version
|
|
696
|
+
);
|
|
697
|
+
context.app_write_seq++;
|
|
698
|
+
if (context.transport && !context.transport.destroyed && !context.transport.writableEnded) {
|
|
699
|
+
context.transport.write(rec);
|
|
700
|
+
}
|
|
701
|
+
} catch(e) {
|
|
702
|
+
self.emit('error', e);
|
|
703
|
+
}
|
|
461
704
|
}
|
|
462
|
-
|
|
463
705
|
}
|
|
464
706
|
|
|
465
707
|
if (epoch === 2) {
|
|
466
708
|
// Post-handshake message encrypted with app keys (e.g. NewSessionTicket)
|
|
467
709
|
if (!context.app_write_key) {
|
|
468
|
-
|
|
469
|
-
let ts = session.getTrafficSecrets();
|
|
710
|
+
const ts = session.getTrafficSecrets();
|
|
470
711
|
if (ts.localAppSecret) {
|
|
471
|
-
|
|
712
|
+
const d = tls_derive_from_tls_secrets(ts.localAppSecret, ts.cipher);
|
|
472
713
|
context.app_write_key = d.key;
|
|
473
714
|
context.app_write_iv = d.iv;
|
|
474
715
|
}
|
|
475
716
|
}
|
|
476
717
|
if (context.app_write_key) {
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
718
|
+
const algo = context.aeadAlgo || getAeadAlgo(session.getCipher());
|
|
719
|
+
try {
|
|
720
|
+
const rec = encryptCompleteRecord13(
|
|
721
|
+
CT.HANDSHAKE, buf,
|
|
722
|
+
context.app_write_key,
|
|
723
|
+
get_nonce_into(context._nonceEncScratch, context.app_write_iv, context.app_write_seq),
|
|
724
|
+
algo,
|
|
725
|
+
context.rec_version
|
|
726
|
+
);
|
|
727
|
+
context.app_write_seq++;
|
|
728
|
+
if (context.transport && !context.transport.destroyed && !context.transport.writableEnded) {
|
|
729
|
+
context.transport.write(rec);
|
|
730
|
+
}
|
|
731
|
+
} catch(e) {
|
|
732
|
+
self.emit('error', e);
|
|
733
|
+
}
|
|
481
734
|
}
|
|
482
735
|
return;
|
|
483
736
|
}
|
|
@@ -553,14 +806,19 @@ function TLSSocket(duplex, options){
|
|
|
553
806
|
local_supported_signature_algorithms: sigalgs,
|
|
554
807
|
});
|
|
555
808
|
|
|
556
|
-
// Resolve AEAD algorithm once cipher
|
|
809
|
+
// Resolve AEAD algorithm + isTls13 flag once cipher/version are negotiated.
|
|
810
|
+
// For client, both are known after receiving ServerHello (this event fires after).
|
|
811
|
+
// For server, these are cached again at secureConnect as a fallback.
|
|
557
812
|
if (session.getCipher()) context.aeadAlgo = getAeadAlgo(session.getCipher());
|
|
813
|
+
if (session.getVersion()) context.isTls13 = (session.getVersion() === 0x0304);
|
|
558
814
|
});
|
|
559
815
|
|
|
560
816
|
session.on('secureConnect', function(){
|
|
561
817
|
context.using_app_keys=true;
|
|
562
818
|
context.secureEstablished=true;
|
|
563
819
|
context.aeadAlgo = getAeadAlgo(session.getCipher());
|
|
820
|
+
// Cache isTls13 for hot-path decisions (avoids session.getVersion() calls per record)
|
|
821
|
+
context.isTls13 = session.getVersion() === 0x0304;
|
|
564
822
|
|
|
565
823
|
// Clear handshake timeout
|
|
566
824
|
if (context.handshakeTimer) { clearTimeout(context.handshakeTimer); context.handshakeTimer = null; }
|
|
@@ -599,16 +857,31 @@ function TLSSocket(duplex, options){
|
|
|
599
857
|
}
|
|
600
858
|
} catch(e) { /* keylog is best-effort */ }
|
|
601
859
|
|
|
602
|
-
// Flush any queued writes
|
|
603
|
-
|
|
604
|
-
|
|
860
|
+
// Flush any queued writes that arrived before the handshake completed.
|
|
861
|
+
// Perf: cork the transport so all flushed fragments go out as a single TCP send.
|
|
862
|
+
// For apps that .write() immediately after tls.connect() (before secureConnect),
|
|
863
|
+
// this turns N flushed records + N syscalls into 1 syscall.
|
|
864
|
+
if (context.appWriteQueue.length > 0) {
|
|
865
|
+
const t = context.transport;
|
|
866
|
+
const corkable = t && typeof t.cork === 'function' && typeof t.uncork === 'function';
|
|
867
|
+
if (corkable) t.cork();
|
|
868
|
+
try {
|
|
869
|
+
const q = context.appWriteQueue;
|
|
870
|
+
context.appWriteQueue = []; // swap out so re-entrant writes go to fresh queue
|
|
871
|
+
for (let i = 0; i < q.length; i++) writeAppData(q[i]);
|
|
872
|
+
} finally {
|
|
873
|
+
if (corkable) t.uncork();
|
|
874
|
+
}
|
|
605
875
|
}
|
|
606
876
|
|
|
607
877
|
self.emit('secureConnect');
|
|
608
878
|
});
|
|
609
879
|
|
|
610
|
-
// Forward session ticket event (Node.js compatible)
|
|
880
|
+
// Forward session ticket event (Node.js compatible).
|
|
881
|
+
// Also save the last session buffer so getSession() can return it (Node.js semantics).
|
|
882
|
+
let _lastSessionBuffer; // undefined until first 'session' event (matches Node's getSession())
|
|
611
883
|
session.on('session', function(ticketData) {
|
|
884
|
+
_lastSessionBuffer = Buffer.isBuffer(ticketData) ? ticketData : Buffer.from(ticketData);
|
|
612
885
|
self.emit('session', ticketData);
|
|
613
886
|
});
|
|
614
887
|
|
|
@@ -617,6 +890,11 @@ function TLSSocket(duplex, options){
|
|
|
617
890
|
self.emit('handshakeMessage', type, raw, parsed);
|
|
618
891
|
});
|
|
619
892
|
|
|
893
|
+
// Forward keylog event (Node.js compat — NSS SSLKEYLOGFILE format for Wireshark)
|
|
894
|
+
session.on('keylog', function(line) {
|
|
895
|
+
self.emit('keylog', line);
|
|
896
|
+
});
|
|
897
|
+
|
|
620
898
|
// Forward raw clienthello event (server-side, for JA3/inspection)
|
|
621
899
|
session.on('clienthello', function(raw, parsed) {
|
|
622
900
|
self.emit('clienthello', raw, parsed);
|
|
@@ -677,60 +955,154 @@ function TLSSocket(duplex, options){
|
|
|
677
955
|
self.emit('certificateRequest', msg);
|
|
678
956
|
});
|
|
679
957
|
|
|
680
|
-
// Server: automatic PSK handler (decrypts tickets with ticketKeys)
|
|
958
|
+
// Server: automatic PSK handler (decrypts TLS 1.3 tickets with ticketKeys).
|
|
959
|
+
// The new 'psk' event payload is { identity, obfuscatedAge } (object), not raw identity.
|
|
681
960
|
if (options.isServer) {
|
|
682
|
-
session.on('psk', function(
|
|
683
|
-
// First check if user has a custom handler
|
|
961
|
+
session.on('psk', function(info, callback) {
|
|
962
|
+
// First check if user has a custom handler on the socket
|
|
684
963
|
if (self.listenerCount('psk') > 0) {
|
|
685
|
-
// Let user handle it
|
|
686
|
-
self.emit('psk',
|
|
964
|
+
// Let user handle it — pass through the same payload
|
|
965
|
+
self.emit('psk', info, callback);
|
|
687
966
|
return;
|
|
688
967
|
}
|
|
689
968
|
|
|
690
|
-
// Auto-decrypt ticket using ticketKeys
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
let iv = id.slice(0, 12);
|
|
698
|
-
let tag = id.slice(id.length - 16);
|
|
699
|
-
let ct = id.slice(12, id.length - 16);
|
|
700
|
-
|
|
701
|
-
let decipher = crypto.createDecipheriv('aes-256-gcm', ticket_enc_key, iv);
|
|
702
|
-
decipher.setAuthTag(tag);
|
|
703
|
-
let pt = decipher.update(ct);
|
|
704
|
-
decipher.final();
|
|
705
|
-
|
|
706
|
-
let data = JSON.parse(pt.toString());
|
|
707
|
-
callback({
|
|
708
|
-
psk: new Uint8Array(Buffer.from(data.psk, 'base64')),
|
|
709
|
-
cipher: data.cipher,
|
|
710
|
-
});
|
|
711
|
-
} catch(e) {
|
|
712
|
-
callback(null); // Decryption failed → full handshake
|
|
969
|
+
// Auto-decrypt ticket using ticketKeys (unified format)
|
|
970
|
+
let state = decrypt_session_blob(info.identity, context.ticketKeys);
|
|
971
|
+
if (state && state.v === 13 && state.psk && state.cipher) {
|
|
972
|
+
callback({ psk: state.psk, cipher: state.cipher });
|
|
973
|
+
} else {
|
|
974
|
+
callback(null); // Decryption failed / not our ticket → full handshake
|
|
713
975
|
}
|
|
714
976
|
});
|
|
715
977
|
}
|
|
716
978
|
|
|
979
|
+
// Forward TLS 1.2 session resumption events to the socket level.
|
|
980
|
+
// User can listen on socket.on('newSession') / socket.on('resumeSession') —
|
|
981
|
+
// if no listener, the session still has a default (full handshake).
|
|
982
|
+
session.on('newSession', function(sessionId, sessionData, cb) {
|
|
983
|
+
if (self.listenerCount('newSession') > 0) {
|
|
984
|
+
self.emit('newSession', sessionId, sessionData, cb);
|
|
985
|
+
} else {
|
|
986
|
+
// No listener — no-op (session won't be resumable via Session ID unless user stores it)
|
|
987
|
+
cb();
|
|
988
|
+
}
|
|
989
|
+
});
|
|
990
|
+
|
|
991
|
+
session.on('resumeSession', function(sessionId, cb) {
|
|
992
|
+
if (self.listenerCount('resumeSession') > 0) {
|
|
993
|
+
self.emit('resumeSession', sessionId, cb);
|
|
994
|
+
} else {
|
|
995
|
+
// No listener — fall through to full handshake
|
|
996
|
+
cb(null, null);
|
|
997
|
+
}
|
|
998
|
+
});
|
|
999
|
+
|
|
717
1000
|
// If duplex was passed in constructor, start reading
|
|
718
1001
|
if (context.transport) {
|
|
719
1002
|
bindTransport();
|
|
720
1003
|
}
|
|
721
1004
|
|
|
722
1005
|
// === Duplex stream implementation ===
|
|
1006
|
+
//
|
|
1007
|
+
// Two behaviors the callback scheduling controls:
|
|
1008
|
+
//
|
|
1009
|
+
// 1. Coalescing: Node's Writable only batches writes into _writev while the
|
|
1010
|
+
// current _write/_writev is "in progress" (callback pending). If we call
|
|
1011
|
+
// the callback synchronously, each stream.write() completes immediately
|
|
1012
|
+
// and the next one goes through _write alone — no _writev, no batching.
|
|
1013
|
+
// By deferring the callback to process.nextTick, writes issued in the
|
|
1014
|
+
// same synchronous tick queue up, and Node delivers them to _writev in
|
|
1015
|
+
// one batch — letting us pack them into a single TLS record.
|
|
1016
|
+
//
|
|
1017
|
+
// 2. Backpressure: if transport.write returns false, its internal buffer is
|
|
1018
|
+
// full and we must wait for 'drain' before acknowledging the write. That
|
|
1019
|
+
// makes stream.write() return false to the user, letting Node's flow
|
|
1020
|
+
// control kick in (pauses pipes, emits 'drain' on our socket). Without
|
|
1021
|
+
// this, bulk senders flood the transport and can hit memory exhaustion.
|
|
1022
|
+
|
|
1023
|
+
// Wait for the transport to emit 'drain', or deliver an error if the
|
|
1024
|
+
// transport errors/closes before draining. Prevents callback leaks.
|
|
1025
|
+
function _awaitTransportDrain(callback) {
|
|
1026
|
+
const t = context.transport;
|
|
1027
|
+
if (!t) { process.nextTick(() => callback(new Error('No transport'))); return; }
|
|
1028
|
+
const cleanup = () => {
|
|
1029
|
+
t.removeListener('drain', onDrain);
|
|
1030
|
+
t.removeListener('error', onError);
|
|
1031
|
+
t.removeListener('close', onClose);
|
|
1032
|
+
};
|
|
1033
|
+
const onDrain = () => { cleanup(); callback(); };
|
|
1034
|
+
const onError = (err) => { cleanup(); callback(err); };
|
|
1035
|
+
const onClose = () => { cleanup(); callback(new Error('Transport closed before drain')); };
|
|
1036
|
+
t.once('drain', onDrain);
|
|
1037
|
+
t.once('error', onError);
|
|
1038
|
+
t.once('close', onClose);
|
|
1039
|
+
}
|
|
723
1040
|
|
|
724
1041
|
/** Duplex _write — called by stream.write() */
|
|
725
1042
|
self._write = function(chunk, encoding, callback) {
|
|
726
1043
|
if (context.destroyed) { callback(new Error('Socket destroyed')); return; }
|
|
727
|
-
|
|
1044
|
+
const buf = toBuf(chunk);
|
|
1045
|
+
|
|
728
1046
|
if (!context.using_app_keys) {
|
|
1047
|
+
// Pre-handshake: queue for flush at secureConnect.
|
|
729
1048
|
context.appWriteQueue.push(buf);
|
|
1049
|
+
process.nextTick(callback);
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
const transportOk = writeAppData(buf);
|
|
1054
|
+
if (transportOk !== false) {
|
|
1055
|
+
// Defer so Node's Writable can batch subsequent in-tick writes into _writev.
|
|
1056
|
+
process.nextTick(callback);
|
|
730
1057
|
} else {
|
|
731
|
-
|
|
1058
|
+
// Transport buffer is full — wait for 'drain' before completing.
|
|
1059
|
+
_awaitTransportDrain(callback);
|
|
1060
|
+
}
|
|
1061
|
+
};
|
|
1062
|
+
|
|
1063
|
+
/** Duplex _writev — vectored write, called when multiple writes are pending.
|
|
1064
|
+
*
|
|
1065
|
+
* Perf: coalesces N pending writes into a single buffer handed to writeAppData,
|
|
1066
|
+
* which then packs them into as few TLS records as possible (16KB max per record).
|
|
1067
|
+
* Without this, N calls to stream.write() produced N separate TLS records with
|
|
1068
|
+
* N × 21 bytes of framing+GCM overhead. With this, a burst of small writes
|
|
1069
|
+
* becomes a single record — the biggest throughput win for many-small-writes
|
|
1070
|
+
* workloads (HTTP request bodies, WebSocket frames, RPC pipelines).
|
|
1071
|
+
*/
|
|
1072
|
+
self._writev = function(chunks, callback) {
|
|
1073
|
+
if (context.destroyed) { callback(new Error('Socket destroyed')); return; }
|
|
1074
|
+
|
|
1075
|
+
let combined;
|
|
1076
|
+
if (chunks.length === 1) {
|
|
1077
|
+
combined = toBuf(chunks[0].chunk);
|
|
1078
|
+
} else {
|
|
1079
|
+
// Sum lengths once, then do a single allocation + N copies.
|
|
1080
|
+
let totalLen = 0;
|
|
1081
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
1082
|
+
const c = chunks[i].chunk;
|
|
1083
|
+
totalLen += Buffer.isBuffer(c) ? c.length : (c.byteLength || Buffer.byteLength(c));
|
|
1084
|
+
}
|
|
1085
|
+
combined = Buffer.allocUnsafe(totalLen);
|
|
1086
|
+
let off = 0;
|
|
1087
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
1088
|
+
const buf = toBuf(chunks[i].chunk);
|
|
1089
|
+
buf.copy(combined, off);
|
|
1090
|
+
off += buf.length;
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
if (!context.using_app_keys) {
|
|
1095
|
+
context.appWriteQueue.push(combined);
|
|
1096
|
+
process.nextTick(callback);
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
const transportOk = writeAppData(combined);
|
|
1101
|
+
if (transportOk !== false) {
|
|
1102
|
+
process.nextTick(callback);
|
|
1103
|
+
} else {
|
|
1104
|
+
_awaitTransportDrain(callback);
|
|
732
1105
|
}
|
|
733
|
-
callback();
|
|
734
1106
|
};
|
|
735
1107
|
|
|
736
1108
|
/** Duplex _read — data is pushed when received, no pull needed */
|
|
@@ -769,8 +1141,14 @@ function TLSSocket(duplex, options){
|
|
|
769
1141
|
};
|
|
770
1142
|
})(self.destroy);
|
|
771
1143
|
|
|
772
|
-
/**
|
|
773
|
-
|
|
1144
|
+
/** Node.js compat: returns the serialized session as a Buffer, or null.
|
|
1145
|
+
* Matches Node's TLSSocket.getSession() — the returned Buffer can be passed
|
|
1146
|
+
* back as the `session` option on the next tls.connect() to resume. */
|
|
1147
|
+
self.getSession = function(){ return _lastSessionBuffer; };
|
|
1148
|
+
|
|
1149
|
+
/** Internal: returns the underlying TLSSession object (LemonTLS-specific).
|
|
1150
|
+
* Used by compat.js and advanced consumers. Not part of the Node.js API. */
|
|
1151
|
+
self._getTLSSession = function(){ return session; };
|
|
774
1152
|
|
|
775
1153
|
/** Whether this connection used PSK resumption (true after secureConnect). */
|
|
776
1154
|
Object.defineProperty(self, 'isResumed', { get: function(){ return session.isResumed; } });
|
|
@@ -804,6 +1182,25 @@ function TLSSocket(duplex, options){
|
|
|
804
1182
|
enumerable: true
|
|
805
1183
|
});
|
|
806
1184
|
|
|
1185
|
+
/** Returns the SNI servername. On the server side this is the name the
|
|
1186
|
+
* client sent in its ClientHello SNI extension (string). On the client
|
|
1187
|
+
* side this is the name we sent ourselves. Returns false if not
|
|
1188
|
+
* available (no SNI extension present).
|
|
1189
|
+
*
|
|
1190
|
+
* Per Node docs: tlsSocket.servername — string | false. */
|
|
1191
|
+
Object.defineProperty(self, 'servername', {
|
|
1192
|
+
get: function(){
|
|
1193
|
+
// context.remote_sni is populated on the server from the client's SNI;
|
|
1194
|
+
// on the client side session.context.remote_sni will be null, so fall
|
|
1195
|
+
// back to options.servername (the name we sent).
|
|
1196
|
+
let name = null;
|
|
1197
|
+
try { name = session.context && session.context.remote_sni; } catch {}
|
|
1198
|
+
if (!name && options && options.servername) name = options.servername;
|
|
1199
|
+
return name || false;
|
|
1200
|
+
},
|
|
1201
|
+
enumerable: true
|
|
1202
|
+
});
|
|
1203
|
+
|
|
807
1204
|
/** Whether the peer certificate was validated. */
|
|
808
1205
|
Object.defineProperty(self, 'authorized', {
|
|
809
1206
|
get: function(){ return session.authorized; },
|
|
@@ -844,6 +1241,175 @@ function TLSSocket(duplex, options){
|
|
|
844
1241
|
}
|
|
845
1242
|
};
|
|
846
1243
|
|
|
1244
|
+
/** Node-compat: return the peer's leaf cert as a native X509Certificate
|
|
1245
|
+
* object (introduced in Node 15.9). Apps prefer this over the legacy
|
|
1246
|
+
* plain-object getPeerCertificate() for modern cert operations. */
|
|
1247
|
+
self.getPeerX509Certificate = function(){
|
|
1248
|
+
let chain = session.getPeerCertificate();
|
|
1249
|
+
if (!chain || chain.length === 0) return undefined;
|
|
1250
|
+
try { return new crypto.X509Certificate(chain[0].cert); }
|
|
1251
|
+
catch(e) { return undefined; }
|
|
1252
|
+
};
|
|
1253
|
+
|
|
1254
|
+
/** Node-compat: return our LOCAL cert (what we presented) as an
|
|
1255
|
+
* X509Certificate object. Returns undefined if we didn't send a cert
|
|
1256
|
+
* (e.g. client without client-cert auth). */
|
|
1257
|
+
self.getX509Certificate = function(){
|
|
1258
|
+
let sctx = session.context;
|
|
1259
|
+
let localChain = sctx && sctx.local_cert_chain;
|
|
1260
|
+
if (!localChain || localChain.length === 0) return undefined;
|
|
1261
|
+
try {
|
|
1262
|
+
let der = localChain[0].cert || localChain[0];
|
|
1263
|
+
return new crypto.X509Certificate(der);
|
|
1264
|
+
} catch(e) { return undefined; }
|
|
1265
|
+
};
|
|
1266
|
+
|
|
1267
|
+
/** Node-compat: return info about our LOCAL cert as a legacy plain
|
|
1268
|
+
* object (mirror of getPeerCertificate). Empty object {} if we didn't
|
|
1269
|
+
* present a cert — this matches Node's observed behavior. */
|
|
1270
|
+
self.getCertificate = function(){
|
|
1271
|
+
let sctx = session.context;
|
|
1272
|
+
let localChain = sctx && sctx.local_cert_chain;
|
|
1273
|
+
if (!localChain || localChain.length === 0) return {};
|
|
1274
|
+
try {
|
|
1275
|
+
let der = localChain[0].cert || localChain[0];
|
|
1276
|
+
let x509 = new crypto.X509Certificate(der);
|
|
1277
|
+
return {
|
|
1278
|
+
subject: x509.subject,
|
|
1279
|
+
issuer: x509.issuer,
|
|
1280
|
+
subjectaltname: x509.subjectAltName,
|
|
1281
|
+
valid_from: x509.validFrom,
|
|
1282
|
+
valid_to: x509.validTo,
|
|
1283
|
+
fingerprint: x509.fingerprint,
|
|
1284
|
+
fingerprint256: x509.fingerprint256,
|
|
1285
|
+
serialNumber: x509.serialNumber,
|
|
1286
|
+
raw: der
|
|
1287
|
+
};
|
|
1288
|
+
} catch(e) {
|
|
1289
|
+
return {};
|
|
1290
|
+
}
|
|
1291
|
+
};
|
|
1292
|
+
|
|
1293
|
+
/** Node-compat: return the TLS 1.2 session ticket as a Buffer, or undefined.
|
|
1294
|
+
* For a client, this is the ticket the server sent in NewSessionTicket.
|
|
1295
|
+
* For TLS 1.3 this returns undefined — use getSession() instead (1.3 uses
|
|
1296
|
+
* opaque PSK identities via NewSessionTicket, not ticket-encrypted state). */
|
|
1297
|
+
self.getTLSTicket = function(){
|
|
1298
|
+
let sctx = session.context;
|
|
1299
|
+
let t = sctx && sctx.tls12_received_ticket;
|
|
1300
|
+
if (!t || t.length === 0) return undefined;
|
|
1301
|
+
return Buffer.isBuffer(t) ? t : Buffer.from(t);
|
|
1302
|
+
};
|
|
1303
|
+
|
|
1304
|
+
/** Node-compat: return signature algorithms shared between client and server
|
|
1305
|
+
* (intersection of the two lists), as an array of lowercase OpenSSL-style
|
|
1306
|
+
* names. Server populates both sides during ClientHello processing. */
|
|
1307
|
+
self.getSharedSigalgs = function(){
|
|
1308
|
+
let sctx = session.context;
|
|
1309
|
+
let local = (sctx && sctx.local_supported_signature_algorithms) || [];
|
|
1310
|
+
let remote = (sctx && sctx.remote_supported_signature_algorithms) || [];
|
|
1311
|
+
if (!local.length || !remote.length) return [];
|
|
1312
|
+
// Intersection preserves LOCAL preference order (matches Node behavior)
|
|
1313
|
+
let set = new Set(remote);
|
|
1314
|
+
let out = [];
|
|
1315
|
+
for (let code of local) {
|
|
1316
|
+
if (set.has(code)) out.push(sigalgCodeToName(code));
|
|
1317
|
+
}
|
|
1318
|
+
return out;
|
|
1319
|
+
};
|
|
1320
|
+
|
|
1321
|
+
/** Node-compat: set the maximum plaintext fragment size for outgoing records.
|
|
1322
|
+
* Node/OpenSSL accepts values in [512, 16384]. We store it and let the
|
|
1323
|
+
* encrypt path chunk on this boundary; values outside the range throw. */
|
|
1324
|
+
self.setMaxSendFragment = function(size){
|
|
1325
|
+
size = Number(size);
|
|
1326
|
+
if (!Number.isInteger(size) || size < 512 || size > 16384) {
|
|
1327
|
+
throw new RangeError('setMaxSendFragment: size must be an integer in [512, 16384]');
|
|
1328
|
+
}
|
|
1329
|
+
self._maxSendFragment = size;
|
|
1330
|
+
return true;
|
|
1331
|
+
};
|
|
1332
|
+
|
|
1333
|
+
/** Node-compat: enable OpenSSL handshake tracing to stderr. Node forwards
|
|
1334
|
+
* this to SSL_CTX_set_msg_callback in OpenSSL. We don't have an equivalent
|
|
1335
|
+
* backend, so this is a no-op — kept for API surface parity. Apps wanting
|
|
1336
|
+
* handshake insight should use the 'keylog' and 'handshakeMessage' events. */
|
|
1337
|
+
self.enableTrace = function(){ /* no-op, see docstring */ };
|
|
1338
|
+
|
|
1339
|
+
// === Node.js net.Socket compat — delegate to underlying transport ===
|
|
1340
|
+
// These getters/methods delegate to the wrapped TCP socket so that TLSSocket
|
|
1341
|
+
// exposes the same surface as Node's TLSSocket (which inherits from net.Socket).
|
|
1342
|
+
|
|
1343
|
+
Object.defineProperty(self, 'remoteAddress', {
|
|
1344
|
+
get: function(){ return context.transport ? context.transport.remoteAddress : undefined; },
|
|
1345
|
+
enumerable: true
|
|
1346
|
+
});
|
|
1347
|
+
Object.defineProperty(self, 'remotePort', {
|
|
1348
|
+
get: function(){ return context.transport ? context.transport.remotePort : undefined; },
|
|
1349
|
+
enumerable: true
|
|
1350
|
+
});
|
|
1351
|
+
Object.defineProperty(self, 'remoteFamily', {
|
|
1352
|
+
get: function(){ return context.transport ? context.transport.remoteFamily : undefined; },
|
|
1353
|
+
enumerable: true
|
|
1354
|
+
});
|
|
1355
|
+
Object.defineProperty(self, 'localAddress', {
|
|
1356
|
+
get: function(){ return context.transport ? context.transport.localAddress : undefined; },
|
|
1357
|
+
enumerable: true
|
|
1358
|
+
});
|
|
1359
|
+
Object.defineProperty(self, 'localPort', {
|
|
1360
|
+
get: function(){ return context.transport ? context.transport.localPort : undefined; },
|
|
1361
|
+
enumerable: true
|
|
1362
|
+
});
|
|
1363
|
+
Object.defineProperty(self, 'localFamily', {
|
|
1364
|
+
get: function(){ return context.transport ? context.transport.localFamily : undefined; },
|
|
1365
|
+
enumerable: true
|
|
1366
|
+
});
|
|
1367
|
+
/** Bytes read from the underlying transport. */
|
|
1368
|
+
Object.defineProperty(self, 'bytesRead', {
|
|
1369
|
+
get: function(){ return context.transport ? context.transport.bytesRead : 0; },
|
|
1370
|
+
enumerable: true
|
|
1371
|
+
});
|
|
1372
|
+
/** Bytes written to the underlying transport. */
|
|
1373
|
+
Object.defineProperty(self, 'bytesWritten', {
|
|
1374
|
+
get: function(){ return context.transport ? context.transport.bytesWritten : 0; },
|
|
1375
|
+
enumerable: true
|
|
1376
|
+
});
|
|
1377
|
+
|
|
1378
|
+
/** Delegate setNoDelay() to underlying TCP socket. */
|
|
1379
|
+
self.setNoDelay = function(noDelay){
|
|
1380
|
+
if (context.transport && typeof context.transport.setNoDelay === 'function') {
|
|
1381
|
+
context.transport.setNoDelay(noDelay);
|
|
1382
|
+
}
|
|
1383
|
+
return self;
|
|
1384
|
+
};
|
|
1385
|
+
/** Delegate setKeepAlive() to underlying TCP socket. */
|
|
1386
|
+
self.setKeepAlive = function(enable, initialDelay){
|
|
1387
|
+
if (context.transport && typeof context.transport.setKeepAlive === 'function') {
|
|
1388
|
+
context.transport.setKeepAlive(enable, initialDelay);
|
|
1389
|
+
}
|
|
1390
|
+
return self;
|
|
1391
|
+
};
|
|
1392
|
+
/** Delegate setTimeout() to underlying TCP socket (overrides Duplex default). */
|
|
1393
|
+
self.setTimeout = function(msecs, callback){
|
|
1394
|
+
if (context.transport && typeof context.transport.setTimeout === 'function') {
|
|
1395
|
+
context.transport.setTimeout(msecs, callback);
|
|
1396
|
+
}
|
|
1397
|
+
return self;
|
|
1398
|
+
};
|
|
1399
|
+
/** Delegate ref/unref to underlying TCP socket for event-loop control. */
|
|
1400
|
+
self.ref = function(){
|
|
1401
|
+
if (context.transport && typeof context.transport.ref === 'function') {
|
|
1402
|
+
context.transport.ref();
|
|
1403
|
+
}
|
|
1404
|
+
return self;
|
|
1405
|
+
};
|
|
1406
|
+
self.unref = function(){
|
|
1407
|
+
if (context.transport && typeof context.transport.unref === 'function') {
|
|
1408
|
+
context.transport.unref();
|
|
1409
|
+
}
|
|
1410
|
+
return self;
|
|
1411
|
+
};
|
|
1412
|
+
|
|
847
1413
|
// === LemonTLS-only extensions (not in Node.js tls) ===
|
|
848
1414
|
|
|
849
1415
|
/** Handshake duration in ms, or null if not completed. */
|