tunnelfetch 1.0.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 (96) hide show
  1. package/LICENSE +28 -0
  2. package/README.md +617 -0
  3. package/README.zh-CN.md +470 -0
  4. package/package.json +74 -0
  5. package/src/client/cookies.js +429 -0
  6. package/src/client/decode.js +346 -0
  7. package/src/client/redirect.js +249 -0
  8. package/src/client.js +704 -0
  9. package/src/errors.js +181 -0
  10. package/src/http1/chunked.js +289 -0
  11. package/src/http1/index.js +10 -0
  12. package/src/http1/request.js +143 -0
  13. package/src/http1/response.js +493 -0
  14. package/src/http2/connection.js +1170 -0
  15. package/src/http2/constants.js +129 -0
  16. package/src/http2/frames.js +291 -0
  17. package/src/http2/hpack.js +420 -0
  18. package/src/http2/huffman.js +203 -0
  19. package/src/http2/index.js +21 -0
  20. package/src/index.js +46 -0
  21. package/src/pool.js +256 -0
  22. package/src/proxy/direct.js +62 -0
  23. package/src/proxy/http-connect.js +206 -0
  24. package/src/proxy/index.js +197 -0
  25. package/src/proxy/socks5.js +344 -0
  26. package/src/tls/aead.js +263 -0
  27. package/src/tls/connect.js +407 -0
  28. package/src/tls/constants.js +334 -0
  29. package/src/tls/extensions.js +376 -0
  30. package/src/tls/handshake-messages.js +901 -0
  31. package/src/tls/handshake.js +568 -0
  32. package/src/tls/handshake12.js +507 -0
  33. package/src/tls/index.js +44 -0
  34. package/src/tls/keyschedule.js +473 -0
  35. package/src/tls/record.js +872 -0
  36. package/src/tls/tickets.js +145 -0
  37. package/src/tls/transcript.js +101 -0
  38. package/src/tls/wire.js +224 -0
  39. package/src/transport.js +296 -0
  40. package/src/trust/der.js +551 -0
  41. package/src/trust/index.js +375 -0
  42. package/src/trust/name.js +235 -0
  43. package/src/trust/ocsp.js +759 -0
  44. package/src/trust/path.js +595 -0
  45. package/src/trust/roots.js +454 -0
  46. package/src/trust/x509.js +902 -0
  47. package/src/util/bytes.js +470 -0
  48. package/src/util/deadline.js +266 -0
  49. package/src/warmup-fixture.js +85 -0
  50. package/src/warmup.js +243 -0
  51. package/types/client/cookies.d.ts +159 -0
  52. package/types/client/decode.d.ts +54 -0
  53. package/types/client/redirect.d.ts +96 -0
  54. package/types/client.d.ts +323 -0
  55. package/types/errors.d.ts +141 -0
  56. package/types/http1/chunked.d.ts +48 -0
  57. package/types/http1/index.d.ts +3 -0
  58. package/types/http1/request.d.ts +44 -0
  59. package/types/http1/response.d.ts +183 -0
  60. package/types/http2/connection.d.ts +282 -0
  61. package/types/http2/constants.d.ts +95 -0
  62. package/types/http2/frames.d.ts +116 -0
  63. package/types/http2/hpack.d.ts +99 -0
  64. package/types/http2/huffman.d.ts +21 -0
  65. package/types/http2/index.d.ts +5 -0
  66. package/types/index.d.ts +17 -0
  67. package/types/pool.d.ts +135 -0
  68. package/types/proxy/direct.d.ts +26 -0
  69. package/types/proxy/http-connect.d.ts +37 -0
  70. package/types/proxy/index.d.ts +62 -0
  71. package/types/proxy/socks5.d.ts +47 -0
  72. package/types/tls/aead.d.ts +67 -0
  73. package/types/tls/connect.d.ts +280 -0
  74. package/types/tls/constants.d.ts +275 -0
  75. package/types/tls/extensions.d.ts +195 -0
  76. package/types/tls/handshake-messages.d.ts +430 -0
  77. package/types/tls/handshake.d.ts +90 -0
  78. package/types/tls/handshake12.d.ts +35 -0
  79. package/types/tls/index.d.ts +9 -0
  80. package/types/tls/keyschedule.d.ts +272 -0
  81. package/types/tls/record.d.ts +361 -0
  82. package/types/tls/tickets.d.ts +66 -0
  83. package/types/tls/transcript.d.ts +52 -0
  84. package/types/tls/wire.d.ts +106 -0
  85. package/types/transport.d.ts +222 -0
  86. package/types/trust/der.d.ts +239 -0
  87. package/types/trust/index.d.ts +194 -0
  88. package/types/trust/name.d.ts +33 -0
  89. package/types/trust/ocsp.d.ts +138 -0
  90. package/types/trust/path.d.ts +139 -0
  91. package/types/trust/roots.d.ts +36 -0
  92. package/types/trust/x509.d.ts +401 -0
  93. package/types/util/bytes.d.ts +183 -0
  94. package/types/util/deadline.d.ts +133 -0
  95. package/types/warmup-fixture.d.ts +11 -0
  96. package/types/warmup.d.ts +45 -0
@@ -0,0 +1,872 @@
1
+ // The record layer: framing, fragmentation, reassembly, alerts, key changes, KeyUpdate.
2
+ //
3
+ // This class sits between a ciphertext duplex ({readable, writable} of raw transport bytes)
4
+ // and the handshake driver / application data. It owns everything RFC 8446 s5 owns:
5
+ //
6
+ // * record parsing that is provably independent of transport chunking (ByteReader),
7
+ // * per-direction cipher state with sequence numbers that reset on every key change,
8
+ // * handshake message reassembly — one message may span records, one record may carry many
9
+ // messages, and a message may never span a key change,
10
+ // * TLS 1.3 compatibility-mode change_cipher_spec handling,
11
+ // * alert decode/encode, close_notify lifecycle, and post-handshake KeyUpdate.
12
+ //
13
+ // The failure discipline is: any protocol violation makes the layer permanently unusable (the
14
+ // first error is sticky and rethrown), a best-effort fatal alert is sent so the peer is not
15
+ // left hanging, and nothing decrypted after a violation is ever surfaced.
16
+
17
+ import {
18
+ ByteReader, ByteWriter, UnexpectedEofError, concat, u8, u16, readU16, readU24,
19
+ } from '../util/bytes.js';
20
+ import { TlsError, TlsUnsupportedError, codes, hex8, hex16 } from '../errors.js';
21
+ import {
22
+ RECORD_TYPE, HANDSHAKE_TYPE, ALERT_DESC, ALERT_LEVEL, MAX_PLAINTEXT, MAX_CIPHERTEXT,
23
+ LEGACY_VERSION, TLS12, TLS13, CIPHER_PARAMS,
24
+ } from './constants.js';
25
+ import { createAead } from './aead.js';
26
+ import { trafficKeys, nextTrafficSecret } from './keyschedule.js';
27
+
28
+ /** Reverse alert map (name -> number), derived so constants.js stays the single source. */
29
+ const ALERT = Object.fromEntries(Object.entries(ALERT_DESC).map(([n, name]) => [name, +n]));
30
+
31
+ // Flood caps. All are counts, not clocks, so the layer stays deterministic with no injected
32
+ // time source. A compliant peer sends at most one compatibility CCS and has no reason to
33
+ // stream warning alerts or empty records; the caps only bound how long we humour a broken or
34
+ // hostile one.
35
+ const MAX_IGNORED_CCS = 8;
36
+ const MAX_IGNORED_ALERTS = 4;
37
+ const MAX_CONSECUTIVE_EMPTY = 32;
38
+
39
+ const KEY_UPDATE_NOT_REQUESTED = Uint8Array.from([HANDSHAKE_TYPE.key_update, 0, 0, 1, 0]);
40
+ const KEY_UPDATE_REQUESTED = Uint8Array.from([HANDSHAKE_TYPE.key_update, 0, 0, 1, 1]);
41
+
42
+ const typeName = (t) =>
43
+ Object.entries(RECORD_TYPE).find(([, v]) => v === t)?.[0] ?? hex8(t);
44
+
45
+ /**
46
+ * Await `promise`, but give up after `ms` and resolve anyway.
47
+ *
48
+ * Used only for shutdown courtesies, where abandoning the wait is strictly better than blocking:
49
+ * the peer either received the alert or has stopped reading, and no third outcome is worth waiting
50
+ * on. The timer is always cleared, so a fast path leaves nothing pending.
51
+ */
52
+ async function withGrace(promise, ms) {
53
+ if (!(ms > 0)) {
54
+ promise.catch(() => {});
55
+ return;
56
+ }
57
+ let timer;
58
+ try {
59
+ await Promise.race([
60
+ promise,
61
+ new Promise((resolve) => {
62
+ timer = setTimeout(resolve, ms);
63
+ }),
64
+ ]);
65
+ } catch {
66
+ /* the transport is already unusable; there is nothing further to attempt */
67
+ } finally {
68
+ if (timer !== undefined) clearTimeout(timer);
69
+ }
70
+ }
71
+
72
+ /**
73
+ * A complete handshake message off the wire. `raw` includes the 4-byte header, which is the
74
+ * form the transcript hash consumes.
75
+ * @typedef {object} HandshakeMessage
76
+ * @property {number} type
77
+ * @property {Uint8Array} body
78
+ * @property {Uint8Array} raw
79
+ */
80
+
81
+ /**
82
+ * Keys for one direction: a TLS 1.3 traffic `secret` (key and IV derived per RFC 8446 s7.3,
83
+ * KeyUpdate rotation possible) or raw TLS 1.2 key_block slices (no forward rotation).
84
+ * @typedef {{ cipher: number, secret: Uint8Array }
85
+ * | { cipher: number, key: Uint8Array, iv: Uint8Array }} DirectionKeys
86
+ */
87
+
88
+ /**
89
+ * @typedef {object} RecordLayerOptions
90
+ * @property {number} [maxHandshakeMessage] per-message cap; certificate chains dominate sizing
91
+ * @property {number} [maxKeyUpdates] received KeyUpdates before we call it a flood
92
+ * @property {number} [shutdownGraceMs] how long a courtesy close_notify or fatal alert may
93
+ * block shutdown before being abandoned, default 2000; see _shutdown()
94
+ * @property {null | ((type: number, length: number) => number)} [padding] extra zero bytes per
95
+ * record, TLS 1.3 only
96
+ * @property {null | ((msg: HandshakeMessage) => void | Promise<void>)} [onPostHandshake]
97
+ * NewSessionTicket consumer; default is to discard
98
+ */
99
+
100
+ export class RecordLayer {
101
+ /**
102
+ * @param {import('./connect.js').ByteDuplex} duplex ciphertext transport
103
+ * @param {RecordLayerOptions} [opts]
104
+ */
105
+ constructor({ readable, writable }, opts = {}) {
106
+ this._r = new ByteReader(readable);
107
+ this._w = new ByteWriter(writable);
108
+ this._maxHandshakeMessage = opts.maxHandshakeMessage ?? 1 << 17;
109
+ this._maxKeyUpdates = opts.maxKeyUpdates ?? 32;
110
+ // How long a courtesy close_notify or fatal alert may block shutdown. See _shutdown().
111
+ this._shutdownGraceMs = opts.shutdownGraceMs ?? 2000;
112
+ this._padding = opts.padding ?? null;
113
+ this._onPostHandshake = opts.onPostHandshake ?? null;
114
+
115
+ this._version = TLS13; // semantics selector; setVersion() pins it when negotiated
116
+ /** @typedef {null | { aead: import('./aead.js').Aead, seq: bigint, cipher: number,
117
+ * hash: import('./keyschedule.js').ScheduleHash,
118
+ * secret: Uint8Array | null }} DirectionState */
119
+ /** @type {DirectionState} */
120
+ this._send = null;
121
+ /** @type {DirectionState} */
122
+ this._recv = null;
123
+
124
+ /** Handshake reassembly. Chunk list, not one growing buffer, so a peer drip-feeding a
125
+ * message in tiny records costs O(records), not O(records^2).
126
+ * @type {Uint8Array[]} */
127
+ this._hsChunks = [];
128
+ this._hsLen = 0;
129
+ /** @type {{ type: number, len: number } | null} header of the message being assembled */
130
+ this._hsHeader = null;
131
+
132
+ this._handshakeComplete = false;
133
+ this._closedByPeer = false; // close_notify received; everything after is ignored
134
+ this._closedLocally = false; // close_notify or fatal alert sent; no further writes
135
+ /** @type {TlsError | null} first protocol error; sticky */
136
+ this._fatal = null;
137
+ this._anyRecordWritten = false;
138
+ this._reading = false;
139
+
140
+ /** All wire emission is funnelled through this chain. Two interleaved writers would
141
+ * reorder — or worse, reuse — AEAD sequence numbers, and with GCM a single reused nonce
142
+ * forfeits the key, so serialization here is a security control, not a convenience. */
143
+ this._writeChain = Promise.resolve();
144
+
145
+ this._ignoredCcs = 0;
146
+ this._ignoredAlerts = 0;
147
+ this._emptyStreak = 0;
148
+ this._keyUpdatesReceived = 0;
149
+ }
150
+
151
+ // ------------------------------------------------------------------ configuration
152
+
153
+ /**
154
+ * Pin the negotiated version. Chooses CCS semantics (1.3: compatibility noise to ignore;
155
+ * 1.2: a real key-change signal surfaced as `{ ccs: true }`), alert strictness, and AEAD
156
+ * framing. Must happen before any keys are installed.
157
+ * @param {number} version `TLS12` (0x0303) or `TLS13` (0x0304); anything else throws
158
+ */
159
+ setVersion(version) {
160
+ if (version !== TLS12 && version !== TLS13) {
161
+ throw new TlsUnsupportedError(codes.TLS_VERSION_UNSUPPORTED,
162
+ `record layer cannot speak version ${hex16(version)}`, { version });
163
+ }
164
+ if (this._send || this._recv) {
165
+ throw new TlsError(codes.CONFIG_INVALID, 'version cannot change after keys are installed');
166
+ }
167
+ this._version = version;
168
+ }
169
+
170
+ get version() {
171
+ return this._version;
172
+ }
173
+
174
+ get handshakeComplete() {
175
+ return this._handshakeComplete;
176
+ }
177
+
178
+ /** The handshake driver calls this after the Finished exchange. Gates CCS and KeyUpdate. */
179
+ markHandshakeComplete() {
180
+ this._handshakeComplete = true;
181
+ }
182
+
183
+ /**
184
+ * Install (or replace) the NewSessionTicket consumer after construction. Exists because the
185
+ * consumer needs secrets that do not exist when the record layer is built — the resumption
186
+ * master secret is derived from the transcript through the client Finished — so the driver
187
+ * wires it in at handshake completion. An exception it throws surfaces on the read path and
188
+ * fails the connection, which is the correct fate for a peer whose post-handshake messages do
189
+ * not parse.
190
+ * @param {null | ((msg: HandshakeMessage) => void | Promise<void>)} fn
191
+ */
192
+ setPostHandshake(fn) {
193
+ this._onPostHandshake = fn;
194
+ }
195
+
196
+ // ------------------------------------------------------------------ key management
197
+
198
+ /**
199
+ * Install send-direction protection. Pass `secret` (a TLS 1.3 traffic secret; key and IV are
200
+ * derived per RFC 8446 s7.3, and KeyUpdate rotation becomes possible) or raw `key`+`iv`
201
+ * (TLS 1.2 key_block slices, which have no forward rotation). Sequence numbers reset.
202
+ * @param {DirectionKeys} keys
203
+ * @returns {Promise<void>}
204
+ */
205
+ async setSendKeys({ cipher, secret, key, iv }) {
206
+ this._send = await this._makeState({ cipher, secret, key, iv });
207
+ }
208
+
209
+ /**
210
+ * Install receive-direction protection. Refuses if a partially reassembled handshake
211
+ * message is pending: RFC 8446 s5.1 forbids a handshake message from spanning a key change,
212
+ * and enforcing it here — at the only point where the receive cipher can change — covers
213
+ * both driver-installed keys and KeyUpdate rotation with a single check.
214
+ * @param {DirectionKeys} keys
215
+ * @returns {Promise<void>}
216
+ */
217
+ async setReceiveKeys({ cipher, secret, key, iv }) {
218
+ if (this._hsLen > 0) {
219
+ this._fail(codes.TLS_RECORD,
220
+ `a handshake message (${this._hsLen} bytes pending) spans a key change`,
221
+ { pending: this._hsLen }, ALERT.unexpected_message);
222
+ }
223
+ this._recv = await this._makeState({ cipher, secret, key, iv });
224
+ }
225
+
226
+ /**
227
+ * @param {{ cipher: number, secret?: Uint8Array, key?: Uint8Array, iv?: Uint8Array }} keys
228
+ * @returns {Promise<NonNullable<DirectionState>>}
229
+ */
230
+ async _makeState({ cipher, secret, key, iv }) {
231
+ const params = CIPHER_PARAMS[cipher];
232
+ if (!params) {
233
+ throw new TlsUnsupportedError(codes.TLS_CIPHER_UNSUPPORTED,
234
+ `cipher suite ${hex16(cipher)} has no parameters`, { cipher });
235
+ }
236
+ if (secret) {
237
+ if (this._version !== TLS13) {
238
+ throw new TlsError(codes.CONFIG_INVALID,
239
+ 'traffic secrets are TLS 1.3; TLS 1.2 installs raw key_block slices');
240
+ }
241
+ ({ key, iv } = await trafficKeys(params.hash, secret, params.keyLen, params.ivLen));
242
+ } else if (!key || !iv) {
243
+ throw new TlsError(codes.CONFIG_INVALID, 'keys need either a traffic secret or key+iv');
244
+ }
245
+ const aead = await createAead({ version: this._version, cipher, key, iv });
246
+ return { aead, seq: 0n, cipher, hash: params.hash, secret: secret ?? null };
247
+ }
248
+
249
+ // ------------------------------------------------------------------ read side
250
+
251
+ /**
252
+ * Next handshake message during the handshake phase.
253
+ * Returns `{ type, body, raw }` (raw includes the 4-byte header, ready for the transcript),
254
+ * `{ ccs: true }` in TLS 1.2 mode when the peer's change_cipher_spec arrives, or `null` if
255
+ * the peer closed cleanly (which mid-handshake the driver should treat as failure).
256
+ * @returns {Promise<HandshakeMessage | { ccs: true } | null>}
257
+ */
258
+ async nextHandshakeMessage() {
259
+ return this._guardedRead(async () => {
260
+ const ev = await this._nextEvent();
261
+ if (ev.kind === 'close') return null;
262
+ if (ev.kind === 'ccs') return { ccs: true };
263
+ if (ev.kind === 'data') {
264
+ this._fail(codes.TLS_RECORD, 'application data received during the handshake',
265
+ { length: ev.bytes.byteLength }, ALERT.unexpected_message);
266
+ }
267
+ return { type: ev.msgType, body: ev.body, raw: ev.raw };
268
+ });
269
+ }
270
+
271
+ /**
272
+ * Next application data chunk, or null at clean close_notify EOF. Post-handshake handshake
273
+ * messages (KeyUpdate, NewSessionTicket) are consumed transparently here.
274
+ * @returns {Promise<Uint8Array | null>}
275
+ */
276
+ async readAppData() {
277
+ if (!this._handshakeComplete) {
278
+ throw new TlsError(codes.CONFIG_INVALID, 'readAppData before the handshake completed');
279
+ }
280
+ return this._guardedRead(async () => {
281
+ for (;;) {
282
+ const ev = await this._nextEvent();
283
+ if (ev.kind === 'close') return null;
284
+ if (ev.kind === 'data') return ev.bytes;
285
+ if (ev.kind === 'handshake') {
286
+ await this._postHandshakeMessage(ev);
287
+ continue;
288
+ }
289
+ // 'ccs' is unreachable here: both versions reject CCS once keys/handshake are done.
290
+ this._fail(codes.TLS_RECORD, 'unexpected change_cipher_spec after the handshake', {},
291
+ ALERT.unexpected_message);
292
+ }
293
+ });
294
+ }
295
+
296
+ /**
297
+ * @template T
298
+ * @param {() => Promise<T>} fn
299
+ * @returns {Promise<T>}
300
+ */
301
+ async _guardedRead(fn) {
302
+ if (this._fatal) throw this._fatal;
303
+ if (this._reading) {
304
+ throw new TlsError(codes.CONFIG_INVALID, 'concurrent reads on one record layer');
305
+ }
306
+ this._reading = true;
307
+ try {
308
+ return await fn();
309
+ } finally {
310
+ this._reading = false;
311
+ }
312
+ }
313
+
314
+ /**
315
+ * One protocol event: a complete handshake message, an app-data chunk, a 1.2 CCS, or close.
316
+ * All the "ignore and keep reading" cases (compat CCS, warning alerts, empty app records)
317
+ * loop in here, each behind a flood cap.
318
+ * @returns {Promise<{ kind: 'close' } | { kind: 'ccs' } | { kind: 'data', bytes: Uint8Array }
319
+ * | { kind: 'handshake', msgType: number, body: Uint8Array, raw: Uint8Array }>}
320
+ */
321
+ async _nextEvent() {
322
+ for (;;) {
323
+ const msg = this._takeHandshakeMessage();
324
+ if (msg) return msg;
325
+ const rec = await this._nextPlaintextRecord();
326
+ if (rec === null) return { kind: 'close' };
327
+ const { type, data } = rec;
328
+ if (type === RECORD_TYPE.handshake) {
329
+ if (data.byteLength === 0) {
330
+ // RFC 8446 s5.1: zero-length handshake fragments MUST NOT be sent. Tolerating them
331
+ // would allow infinite record streams that never assemble a message.
332
+ this._fail(codes.TLS_RECORD, 'zero-length handshake record', {},
333
+ ALERT.unexpected_message);
334
+ }
335
+ this._hsChunks.push(data.slice()); // copy: record bodies may alias transport buffers
336
+ this._hsLen += data.byteLength;
337
+ continue;
338
+ }
339
+ if (type === RECORD_TYPE.alert) {
340
+ if (this._handleAlert(data) === 'close') return { kind: 'close' };
341
+ continue;
342
+ }
343
+ if (type === RECORD_TYPE.application_data) {
344
+ if (data.byteLength === 0) {
345
+ if (++this._emptyStreak > MAX_CONSECUTIVE_EMPTY) {
346
+ this._fail(codes.TLS_RECORD,
347
+ `${this._emptyStreak} consecutive empty application_data records`, {},
348
+ ALERT.unexpected_message);
349
+ }
350
+ continue;
351
+ }
352
+ this._emptyStreak = 0;
353
+ return { kind: 'data', bytes: data };
354
+ }
355
+ return { kind: 'ccs' }; // TLS 1.2 only; 1.3 CCS never escapes _nextPlaintextRecord
356
+ }
357
+ }
358
+
359
+ /**
360
+ * Complete message off the reassembly buffer, if one is there.
361
+ * @returns {{ kind: 'handshake', msgType: number, body: Uint8Array, raw: Uint8Array } | null}
362
+ */
363
+ _takeHandshakeMessage() {
364
+ if (this._hsLen < 4) return null;
365
+ if (!this._hsHeader) {
366
+ const four = new Uint8Array(4);
367
+ let o = 0;
368
+ for (const c of this._hsChunks) {
369
+ const n = Math.min(4 - o, c.byteLength);
370
+ four.set(c.subarray(0, n), o);
371
+ o += n;
372
+ if (o === 4) break;
373
+ }
374
+ const len = readU24(four, 1);
375
+ if (len > this._maxHandshakeMessage) {
376
+ this._fail(codes.TLS_HANDSHAKE,
377
+ `handshake message type ${four[0]} declares ${len} bytes, over the ` +
378
+ `${this._maxHandshakeMessage} byte cap`,
379
+ { type: four[0], length: len, limit: this._maxHandshakeMessage },
380
+ ALERT.unexpected_message);
381
+ }
382
+ this._hsHeader = { type: four[0], len };
383
+ }
384
+ const total = 4 + this._hsHeader.len;
385
+ if (this._hsLen < total) return null;
386
+ const all = concat(this._hsChunks, this._hsLen);
387
+ const rest = all.subarray(total);
388
+ this._hsChunks = rest.byteLength ? [rest] : [];
389
+ this._hsLen = rest.byteLength;
390
+ const { type } = this._hsHeader;
391
+ this._hsHeader = null;
392
+ return {
393
+ kind: 'handshake', msgType: type, body: all.subarray(4, total), raw: all.subarray(0, total),
394
+ };
395
+ }
396
+
397
+ /**
398
+ * Read one record off the wire and reduce it to (inner type, plaintext), or null once the
399
+ * peer has said close_notify. Handles decryption, the plaintext/ciphertext legality rules,
400
+ * and TLS 1.3 compatibility CCS.
401
+ * @returns {Promise<{ type: number, data: Uint8Array } | null>}
402
+ */
403
+ async _nextPlaintextRecord() {
404
+ for (;;) {
405
+ if (this._closedByPeer) return null; // RFC 8446 s6.1: ignore everything after close_notify
406
+ let header;
407
+ try {
408
+ header = await this._r.readExactly(5, 'record header');
409
+ } catch (e) {
410
+ if (e instanceof UnexpectedEofError) {
411
+ // EOF without close_notify is indistinguishable from an attacker cutting the
412
+ // stream at a record boundary — a truncation attack. Fail, never "clean end".
413
+ this._fail(codes.TLS_TRUNCATED, e.detail?.got === 0
414
+ ? 'peer closed the transport without close_notify'
415
+ : `transport ended mid record header (${e.detail?.got} of 5 bytes)`,
416
+ { got: e.detail?.got });
417
+ }
418
+ throw e;
419
+ }
420
+ const type = header[0];
421
+ const length = readU16(header, 3);
422
+ // header[1..2] is legacy_record_version: deliberately unchecked. RFC 8446 s5.1 requires
423
+ // ignoring it, and real middleboxes emit historical values there.
424
+ if (type !== RECORD_TYPE.change_cipher_spec && type !== RECORD_TYPE.alert &&
425
+ type !== RECORD_TYPE.handshake && type !== RECORD_TYPE.application_data) {
426
+ this._fail(codes.TLS_RECORD, `unknown record type ${hex8(type)}`, { type },
427
+ ALERT.unexpected_message);
428
+ }
429
+ // 2^14 for plaintext records, +256 for AEAD expansion once keys are on. The 1.3 bound is
430
+ // also imposed on 1.2 peers: RFC 5246 nominally allows 2^14+2048, but the only 1.2
431
+ // suites we speak are GCM with 24 bytes of overhead, so a compliant peer stays far under.
432
+ const limit = this._recv ? MAX_CIPHERTEXT : MAX_PLAINTEXT;
433
+ if (length > limit) {
434
+ this._fail(codes.TLS_RECORD, `record length ${length} exceeds ${limit}`,
435
+ { type, length, limit }, ALERT.record_overflow);
436
+ }
437
+ let body;
438
+ try {
439
+ body = await this._r.readExactly(length, `${typeName(type)} record body`);
440
+ } catch (e) {
441
+ if (e instanceof UnexpectedEofError) {
442
+ this._fail(codes.TLS_TRUNCATED,
443
+ `transport ended mid record: got ${e.detail?.got} of ${length} body bytes`,
444
+ { type, length, got: e.detail?.got });
445
+ }
446
+ throw e;
447
+ }
448
+
449
+ if (type === RECORD_TYPE.change_cipher_spec) {
450
+ if (length !== 1 || body[0] !== 0x01) {
451
+ this._fail(codes.TLS_RECORD,
452
+ `change_cipher_spec must be exactly one 0x01 byte, got ${length} byte(s)` +
453
+ (length >= 1 ? ` starting ${hex8(body[0])}` : ''),
454
+ { length }, ALERT.unexpected_message);
455
+ }
456
+ if (this._version === TLS12) {
457
+ if (this._recv) {
458
+ // A second CCS would signal renegotiation, which this package refuses to speak.
459
+ this._fail(codes.TLS_RECORD, 'change_cipher_spec after keys were installed', {},
460
+ ALERT.unexpected_message);
461
+ }
462
+ return { type, data: body };
463
+ }
464
+ // TLS 1.3 compatibility mode (RFC 8446 s5): drop, but only mid-handshake, and not
465
+ // in unlimited quantity.
466
+ if (this._handshakeComplete) {
467
+ this._fail(codes.TLS_RECORD, 'change_cipher_spec after handshake completion', {},
468
+ ALERT.unexpected_message);
469
+ }
470
+ if (++this._ignoredCcs > MAX_IGNORED_CCS) {
471
+ this._fail(codes.TLS_RECORD, `${this._ignoredCcs} change_cipher_spec records`, {},
472
+ ALERT.unexpected_message);
473
+ }
474
+ continue;
475
+ }
476
+
477
+ if (!this._recv) return { type, data: body };
478
+
479
+ if (this._version === TLS13 && type !== RECORD_TYPE.application_data) {
480
+ // Once the peer encrypts, a plaintext alert/handshake record can only be injected or
481
+ // a peer bug; surfacing it would hand an attacker a plaintext channel.
482
+ this._fail(codes.TLS_RECORD,
483
+ `plaintext ${typeName(type)} record after encryption started`, { type },
484
+ ALERT.unexpected_message);
485
+ }
486
+ let opened;
487
+ try {
488
+ opened = await this._recv.aead.decrypt(this._recv.seq, body, header);
489
+ } catch (e) {
490
+ if (e instanceof TlsError && !this._fatal) {
491
+ this._fatal = e;
492
+ this._sendAlertBestEffort(ALERT.bad_record_mac);
493
+ }
494
+ throw e;
495
+ }
496
+ this._recv.seq++;
497
+ if (this._version === TLS13) {
498
+ const t = opened.type;
499
+ if (t !== RECORD_TYPE.alert && t !== RECORD_TYPE.handshake &&
500
+ t !== RECORD_TYPE.application_data) {
501
+ // Includes protected change_cipher_spec, which RFC 8446 s5 singles out as fatal.
502
+ this._fail(codes.TLS_RECORD, `forbidden inner content type ${hex8(t)}`, { type: t },
503
+ ALERT.unexpected_message);
504
+ }
505
+ return { type: t, data: opened.plaintext };
506
+ }
507
+ return { type, data: opened.plaintext };
508
+ }
509
+ }
510
+
511
+ /**
512
+ * @param {Uint8Array} data
513
+ * @returns {'close' | 'ignored'} or throws for fatal alerts
514
+ */
515
+ _handleAlert(data) {
516
+ if (data.byteLength !== 2) {
517
+ this._fail(codes.TLS_RECORD, `alert record of ${data.byteLength} bytes (must be exactly 2)`,
518
+ { length: data.byteLength }, ALERT.decode_error);
519
+ }
520
+ const [level, desc] = data;
521
+ const name = ALERT_DESC[desc] ?? `unknown_${desc}`;
522
+ if (desc === ALERT.close_notify) {
523
+ this._closedByPeer = true;
524
+ return 'close';
525
+ }
526
+ // TLS 1.3 (s6): every alert except close_notify/user_canceled is an error regardless of
527
+ // its claimed level. TLS 1.2 peers legitimately send warnings (unrecognized_name, most
528
+ // famously) that a client must survive.
529
+ const ignorable = this._version === TLS13
530
+ ? desc === ALERT.user_canceled
531
+ : level === ALERT_LEVEL.warning;
532
+ if (ignorable) {
533
+ if (++this._ignoredAlerts > MAX_IGNORED_ALERTS) {
534
+ this._fail(codes.TLS_RECORD, `peer sent ${this._ignoredAlerts} warning alerts`, {},
535
+ ALERT.unexpected_message);
536
+ }
537
+ return 'ignored';
538
+ }
539
+ const err = new TlsError(codes.TLS_ALERT, `peer sent fatal alert: ${name} (${desc})`,
540
+ { level, description: desc, name });
541
+ this._fatal = err;
542
+ throw err;
543
+ }
544
+
545
+ /**
546
+ * KeyUpdate and NewSessionTicket arriving under application keys.
547
+ * @param {{ msgType: number, body: Uint8Array, raw: Uint8Array }} msg
548
+ */
549
+ async _postHandshakeMessage({ msgType, body, raw }) {
550
+ if (this._version === TLS13 && msgType === HANDSHAKE_TYPE.key_update) {
551
+ if (++this._keyUpdatesReceived > this._maxKeyUpdates) {
552
+ this._fail(codes.TLS_RECORD,
553
+ `peer sent ${this._keyUpdatesReceived} KeyUpdates, over the ` +
554
+ `${this._maxKeyUpdates} cap`, { count: this._keyUpdatesReceived },
555
+ ALERT.unexpected_message);
556
+ }
557
+ if (body.byteLength !== 1 || body[0] > 1) {
558
+ this._fail(codes.TLS_HANDSHAKE,
559
+ `malformed KeyUpdate: ${body.byteLength} byte body` +
560
+ (body.byteLength >= 1 ? `, request ${hex8(body[0])}` : ''),
561
+ {}, ALERT.illegal_parameter);
562
+ }
563
+ // The peer already switched its send keys; rotate our receive state first so the very
564
+ // next record decrypts. setReceiveKeys re-checks the spans-a-key-change rule, which
565
+ // also catches a peer that put more handshake bytes after KeyUpdate in the same record.
566
+ const r = this._recv;
567
+ if (!r?.secret) {
568
+ this._fail(codes.TLS_RECORD, 'KeyUpdate but receive keys are not rotatable', {},
569
+ ALERT.unexpected_message);
570
+ }
571
+ await this.setReceiveKeys({
572
+ cipher: r.cipher, secret: await nextTrafficSecret(r.hash, r.secret),
573
+ });
574
+ if (body[0] === 1) {
575
+ // update_requested: answer with update_not_requested under the OLD send keys, then
576
+ // rotate our send side (RFC 8446 s4.6.3 ordering). One chained task, so a concurrent
577
+ // application write cannot slip between the response and the rotation. Deliberately
578
+ // NOT awaited: this runs on the read path, and blocking reads until the peer drains
579
+ // our writes deadlocks two zero-buffer endpoints (each waiting on the other's read
580
+ // loop). The chain still orders it before any later write of ours; if the transport
581
+ // is dead the very next operation will surface that, so the error adds nothing here.
582
+ this._enqueueWrite(async () => {
583
+ await this._emit(RECORD_TYPE.handshake, KEY_UPDATE_NOT_REQUESTED);
584
+ await this._rotateSend();
585
+ }).catch(() => {});
586
+ }
587
+ return;
588
+ }
589
+ if (this._version === TLS13 && msgType === HANDSHAKE_TYPE.new_session_ticket) {
590
+ // Tickets are optional to use and safe to drop (RFC 8446 s4.6.1).
591
+ if (this._onPostHandshake) await this._onPostHandshake({ type: msgType, body, raw });
592
+ return;
593
+ }
594
+ this._fail(codes.TLS_HANDSHAKE, `unexpected post-handshake message type ${msgType}`,
595
+ { type: msgType }, ALERT.unexpected_message);
596
+ }
597
+
598
+ // ------------------------------------------------------------------ write side
599
+
600
+ /**
601
+ * Write one or more complete handshake messages, coalescing them into as few records as
602
+ * possible (the ClientHello flight and the 1.2 client second flight benefit) and
603
+ * fragmenting anything over 2^14.
604
+ * @param {Uint8Array | Uint8Array[]} messages
605
+ */
606
+ async writeHandshake(messages) {
607
+ const bytes = Array.isArray(messages) ? concat(messages) : messages;
608
+ if (bytes.byteLength === 0) {
609
+ throw new TlsError(codes.CONFIG_INVALID, 'refusing to write a zero-length handshake record');
610
+ }
611
+ await this._writeFragmented(RECORD_TYPE.handshake, bytes);
612
+ }
613
+
614
+ /**
615
+ * Write application data, fragmented to the record size limit.
616
+ * @param {Uint8Array} bytes
617
+ * @returns {Promise<void>}
618
+ */
619
+ async writeAppData(bytes) {
620
+ if (!this._send) {
621
+ throw new TlsError(codes.CONFIG_INVALID, 'writeAppData before send keys were installed');
622
+ }
623
+ await this._writeFragmented(RECORD_TYPE.application_data, bytes);
624
+ }
625
+
626
+ /** The one-byte compatibility (1.3) or key-change (1.2) CCS record. Always plaintext. */
627
+ async writeChangeCipherSpec() {
628
+ this._assertWritable();
629
+ await this._enqueueWrite(() =>
630
+ this._writeRecord(RECORD_TYPE.change_cipher_spec, Uint8Array.from([0x01])));
631
+ }
632
+
633
+ /**
634
+ * Post-handshake KeyUpdate initiated by us: send under current keys, then rotate our send
635
+ * chain. With `requestPeer` the peer must answer and rotate its own send keys too.
636
+ * @param {{ requestPeer?: boolean }} [opts]
637
+ * @returns {Promise<void>}
638
+ */
639
+ async updateKeys({ requestPeer = false } = {}) {
640
+ if (this._version !== TLS13 || !this._handshakeComplete || !this._send?.secret) {
641
+ throw new TlsError(codes.CONFIG_INVALID,
642
+ 'KeyUpdate needs a completed TLS 1.3 handshake with secret-based keys');
643
+ }
644
+ this._assertWritable();
645
+ await this._enqueueWrite(async () => {
646
+ await this._emit(RECORD_TYPE.handshake,
647
+ requestPeer ? KEY_UPDATE_REQUESTED : KEY_UPDATE_NOT_REQUESTED);
648
+ await this._rotateSend();
649
+ });
650
+ }
651
+
652
+ /**
653
+ * @param {number} level `ALERT_LEVEL.warning` (1) or `ALERT_LEVEL.fatal` (2)
654
+ * @param {number} desc alert description byte, per `ALERT_DESC`
655
+ * @returns {Promise<void>}
656
+ */
657
+ async sendAlert(level, desc) {
658
+ this._assertWritable();
659
+ await this._enqueueWrite(() =>
660
+ this._emit(RECORD_TYPE.alert, Uint8Array.from([level & 0xff, desc & 0xff])));
661
+ }
662
+
663
+ /**
664
+ * Clean shutdown: close_notify, then close the transport write side.
665
+ * @returns {Promise<void>}
666
+ */
667
+ async close() {
668
+ await this._shutdown(ALERT_LEVEL.warning, ALERT.close_notify);
669
+ }
670
+
671
+ /**
672
+ * Shutdown, bounded.
673
+ *
674
+ * The alert is a courtesy: a peer that has stopped reading will never see it, and its write can
675
+ * therefore never complete once the transport's buffer fills. Awaiting that without a bound
676
+ * hangs close() forever on the ordinary path, and hangs abort() on the failure path — where it
677
+ * also swallows the error the caller was about to be given, turning a diagnosable failure into a
678
+ * request that simply never returns. So the courtesy gets a deadline and is then abandoned.
679
+ *
680
+ * Alert and FIN are one chained task so a single deadline covers both; queueing them separately
681
+ * would let a stalled alert consume one budget and the FIN another.
682
+ * @param {number} level
683
+ * @param {number} desc
684
+ */
685
+ async _shutdown(level, desc) {
686
+ const sendAlert = !this._closedLocally;
687
+ this._closedLocally = true;
688
+ const task = this._enqueueWrite(async () => {
689
+ if (sendAlert) {
690
+ try {
691
+ await this._emit(RECORD_TYPE.alert, Uint8Array.from([level & 0xff, desc & 0xff]));
692
+ } catch {
693
+ /* peer may already be gone; the alert is best-effort by nature */
694
+ }
695
+ }
696
+ await this._w.close();
697
+ });
698
+ await withGrace(task, this._shutdownGraceMs);
699
+ }
700
+
701
+ /**
702
+ * Abort: send a fatal alert naming why, then close. A peer left to time out on a dead
703
+ * connection is an interop bug of ours, not a neutral choice.
704
+ * @param {number} [desc] alert description byte, default internal_error
705
+ * @returns {Promise<void>}
706
+ */
707
+ async abort(desc = ALERT.internal_error) {
708
+ await this._shutdown(ALERT_LEVEL.fatal, desc);
709
+ }
710
+
711
+ /**
712
+ * Application-data face of the connection as a {readable, writable} pair, for stacking the
713
+ * HTTP layer on top exactly like it would stack on a raw socket.
714
+ * @returns {import('./connect.js').ByteDuplex}
715
+ */
716
+ plaintextDuplex() {
717
+ const self = this;
718
+ return {
719
+ // highWaterMark 0: pull only when a consumer actually reads. The default (1) makes the
720
+ // stream pull EAGERLY at construction, which parks a readAppData() — and therefore a
721
+ // transport read — on every connection before the request has even been written, and
722
+ // steals the first post-handshake record into the stream's queue for any caller that
723
+ // reads the record layer directly. On-demand is strictly lazier with identical delivery.
724
+ readable: new ReadableStream({
725
+ async pull(controller) {
726
+ const bytes = await self.readAppData();
727
+ if (bytes === null) controller.close();
728
+ else controller.enqueue(bytes);
729
+ },
730
+ cancel() {
731
+ return self._r.cancel();
732
+ },
733
+ }, { highWaterMark: 0 }),
734
+ writable: new WritableStream({
735
+ write(chunk) {
736
+ return self.writeAppData(chunk);
737
+ },
738
+ close() {
739
+ return self.close();
740
+ },
741
+ abort() {
742
+ return self.abort(ALERT.internal_error);
743
+ },
744
+ }),
745
+ };
746
+ }
747
+
748
+ // ------------------------------------------------------------------ write internals
749
+
750
+ _assertWritable() {
751
+ if (this._closedLocally) {
752
+ throw new TlsError(codes.CONFIG_INVALID, 'write after close_notify or a fatal alert');
753
+ }
754
+ }
755
+
756
+ /**
757
+ * Serialize a wire-writing task behind every previously enqueued one.
758
+ * @template T
759
+ * @param {() => Promise<T>} fn
760
+ * @returns {Promise<T>}
761
+ */
762
+ _enqueueWrite(fn) {
763
+ const task = this._writeChain.then(fn);
764
+ this._writeChain = task.then(() => undefined, () => undefined);
765
+ return task;
766
+ }
767
+
768
+ /**
769
+ * @param {number} type
770
+ * @param {Uint8Array} bytes
771
+ */
772
+ async _writeFragmented(type, bytes) {
773
+ this._assertWritable();
774
+ // The whole call is one chained task so fragments of two concurrent writes cannot
775
+ // interleave (which would corrupt the plaintext order even though each record decrypts).
776
+ await this._enqueueWrite(async () => {
777
+ if (bytes.byteLength === 0) {
778
+ await this._emit(type, bytes); // an empty app-data record is a legal keepalive
779
+ return;
780
+ }
781
+ for (let o = 0; o < bytes.byteLength; o += MAX_PLAINTEXT) {
782
+ await this._emit(type, bytes.subarray(o, Math.min(o + MAX_PLAINTEXT, bytes.byteLength)));
783
+ }
784
+ });
785
+ }
786
+
787
+ /**
788
+ * Encrypt-and-frame one fragment. Only ever runs inside the write chain.
789
+ * @param {number} type
790
+ * @param {Uint8Array} chunk
791
+ */
792
+ async _emit(type, chunk) {
793
+ if (!this._send) {
794
+ await this._writeRecord(type, chunk);
795
+ return;
796
+ }
797
+ let padding = 0;
798
+ if (this._padding && this._version === TLS13) {
799
+ padding = this._padding(type, chunk.byteLength);
800
+ if (!Number.isInteger(padding) || padding < 0) {
801
+ throw new TlsError(codes.CONFIG_INVALID,
802
+ `padding policy returned ${padding}, not a non-negative integer`);
803
+ }
804
+ }
805
+ // Claim the sequence number synchronously, before any await, so a bug that lets two
806
+ // emits race can only skip a number — never reuse one.
807
+ const state = this._send;
808
+ const seq = state.seq;
809
+ state.seq = seq + 1n;
810
+ const body = await state.aead.encrypt(seq, type, chunk, { padding });
811
+ const outer = this._version === TLS13 ? RECORD_TYPE.application_data : type;
812
+ await this._writeRecord(outer, body);
813
+ }
814
+
815
+ /**
816
+ * @param {number} type
817
+ * @param {Uint8Array} body
818
+ */
819
+ async _writeRecord(type, body) {
820
+ // RFC 8446 s5.1: the first plaintext ClientHello record MAY say 0x0301, and doing so is
821
+ // the compatibility choice (RFC 8448's traces do the same); everything else says 0x0303.
822
+ const version = !this._anyRecordWritten && type === RECORD_TYPE.handshake && !this._send
823
+ ? 0x0301
824
+ : LEGACY_VERSION;
825
+ this._anyRecordWritten = true;
826
+ await this._w.writeAll([u8(type), u16(version), u16(body.byteLength), body]);
827
+ }
828
+
829
+ /** Derive application_traffic_secret_N+1 and swap the send state. Runs inside the chain. */
830
+ async _rotateSend() {
831
+ const s = this._send;
832
+ const secret = await nextTrafficSecret(s.hash, s.secret);
833
+ const params = CIPHER_PARAMS[s.cipher];
834
+ const { key, iv } = await trafficKeys(params.hash, secret, params.keyLen, params.ivLen);
835
+ const aead = await createAead({ version: this._version, cipher: s.cipher, key, iv });
836
+ this._send = { aead, seq: 0n, cipher: s.cipher, hash: s.hash, secret };
837
+ }
838
+
839
+ // ------------------------------------------------------------------ failure plumbing
840
+
841
+ /**
842
+ * Record the first fatal error, tell the peer (best-effort), throw. Never returns.
843
+ * @param {string} code
844
+ * @param {string} message
845
+ * @param {Record<string, unknown>} [detail]
846
+ * @param {number} [alertDesc]
847
+ * @returns {never}
848
+ */
849
+ _fail(code, message, detail, alertDesc) {
850
+ const err = new TlsError(code, message, detail);
851
+ if (!this._fatal) {
852
+ this._fatal = err;
853
+ if (alertDesc !== undefined) this._sendAlertBestEffort(alertDesc);
854
+ }
855
+ throw err;
856
+ }
857
+
858
+ /**
859
+ * Fire-and-forget: awaiting a write here could park the failure path behind transport
860
+ * backpressure, and the error must reach our caller no matter what the peer does. The
861
+ * write chain still orders it before the transport close.
862
+ * @param {number} desc
863
+ */
864
+ _sendAlertBestEffort(desc) {
865
+ if (this._closedLocally) return;
866
+ this._closedLocally = true;
867
+ this._enqueueWrite(async () => {
868
+ await this._emit(RECORD_TYPE.alert, Uint8Array.from([ALERT_LEVEL.fatal, desc]));
869
+ await this._w.close();
870
+ }).catch(() => {});
871
+ }
872
+ }