nlcurl 0.1.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/LICENSE +21 -0
- package/README.md +162 -0
- package/dist/cli/args.d.ts +42 -0
- package/dist/cli/args.d.ts.map +1 -0
- package/dist/cli/args.js +262 -0
- package/dist/cli/args.js.map +1 -0
- package/dist/cli/index.d.ts +8 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +114 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/output.d.ts +22 -0
- package/dist/cli/output.d.ts.map +1 -0
- package/dist/cli/output.js +105 -0
- package/dist/cli/output.js.map +1 -0
- package/dist/cookies/jar.d.ts +41 -0
- package/dist/cookies/jar.d.ts.map +1 -0
- package/dist/cookies/jar.js +148 -0
- package/dist/cookies/jar.js.map +1 -0
- package/dist/cookies/parser.d.ts +24 -0
- package/dist/cookies/parser.d.ts.map +1 -0
- package/dist/cookies/parser.js +93 -0
- package/dist/cookies/parser.js.map +1 -0
- package/dist/core/client.d.ts +79 -0
- package/dist/core/client.d.ts.map +1 -0
- package/dist/core/client.js +106 -0
- package/dist/core/client.js.map +1 -0
- package/dist/core/errors.d.ts +36 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +65 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/request.d.ts +96 -0
- package/dist/core/request.d.ts.map +1 -0
- package/dist/core/request.js +5 -0
- package/dist/core/request.js.map +1 -0
- package/dist/core/response.d.ts +48 -0
- package/dist/core/response.d.ts.map +1 -0
- package/dist/core/response.js +65 -0
- package/dist/core/response.js.map +1 -0
- package/dist/core/session.d.ts +60 -0
- package/dist/core/session.d.ts.map +1 -0
- package/dist/core/session.js +305 -0
- package/dist/core/session.js.map +1 -0
- package/dist/fingerprints/akamai.d.ts +17 -0
- package/dist/fingerprints/akamai.d.ts.map +1 -0
- package/dist/fingerprints/akamai.js +30 -0
- package/dist/fingerprints/akamai.js.map +1 -0
- package/dist/fingerprints/database.d.ts +33 -0
- package/dist/fingerprints/database.d.ts.map +1 -0
- package/dist/fingerprints/database.js +68 -0
- package/dist/fingerprints/database.js.map +1 -0
- package/dist/fingerprints/extensions.d.ts +49 -0
- package/dist/fingerprints/extensions.d.ts.map +1 -0
- package/dist/fingerprints/extensions.js +178 -0
- package/dist/fingerprints/extensions.js.map +1 -0
- package/dist/fingerprints/ja3.d.ts +32 -0
- package/dist/fingerprints/ja3.d.ts.map +1 -0
- package/dist/fingerprints/ja3.js +64 -0
- package/dist/fingerprints/ja3.js.map +1 -0
- package/dist/fingerprints/profiles/chrome.d.ts +30 -0
- package/dist/fingerprints/profiles/chrome.d.ts.map +1 -0
- package/dist/fingerprints/profiles/chrome.js +202 -0
- package/dist/fingerprints/profiles/chrome.js.map +1 -0
- package/dist/fingerprints/profiles/edge.d.ts +16 -0
- package/dist/fingerprints/profiles/edge.d.ts.map +1 -0
- package/dist/fingerprints/profiles/edge.js +61 -0
- package/dist/fingerprints/profiles/edge.js.map +1 -0
- package/dist/fingerprints/profiles/firefox.d.ts +13 -0
- package/dist/fingerprints/profiles/firefox.d.ts.map +1 -0
- package/dist/fingerprints/profiles/firefox.js +160 -0
- package/dist/fingerprints/profiles/firefox.js.map +1 -0
- package/dist/fingerprints/profiles/safari.d.ts +16 -0
- package/dist/fingerprints/profiles/safari.d.ts.map +1 -0
- package/dist/fingerprints/profiles/safari.js +140 -0
- package/dist/fingerprints/profiles/safari.js.map +1 -0
- package/dist/fingerprints/profiles/tor.d.ts +14 -0
- package/dist/fingerprints/profiles/tor.d.ts.map +1 -0
- package/dist/fingerprints/profiles/tor.js +136 -0
- package/dist/fingerprints/profiles/tor.js.map +1 -0
- package/dist/fingerprints/types.d.ts +104 -0
- package/dist/fingerprints/types.d.ts.map +1 -0
- package/dist/fingerprints/types.js +9 -0
- package/dist/fingerprints/types.js.map +1 -0
- package/dist/http/h1/client.d.ts +21 -0
- package/dist/http/h1/client.d.ts.map +1 -0
- package/dist/http/h1/client.js +136 -0
- package/dist/http/h1/client.js.map +1 -0
- package/dist/http/h1/encoder.d.ts +11 -0
- package/dist/http/h1/encoder.d.ts.map +1 -0
- package/dist/http/h1/encoder.js +75 -0
- package/dist/http/h1/encoder.js.map +1 -0
- package/dist/http/h1/parser.d.ts +61 -0
- package/dist/http/h1/parser.d.ts.map +1 -0
- package/dist/http/h1/parser.js +258 -0
- package/dist/http/h1/parser.js.map +1 -0
- package/dist/http/h2/client.d.ts +48 -0
- package/dist/http/h2/client.d.ts.map +1 -0
- package/dist/http/h2/client.js +376 -0
- package/dist/http/h2/client.js.map +1 -0
- package/dist/http/h2/frames.d.ts +65 -0
- package/dist/http/h2/frames.d.ts.map +1 -0
- package/dist/http/h2/frames.js +184 -0
- package/dist/http/h2/frames.js.map +1 -0
- package/dist/http/h2/hpack.d.ts +27 -0
- package/dist/http/h2/hpack.d.ts.map +1 -0
- package/dist/http/h2/hpack.js +423 -0
- package/dist/http/h2/hpack.js.map +1 -0
- package/dist/http/negotiator.d.ts +36 -0
- package/dist/http/negotiator.d.ts.map +1 -0
- package/dist/http/negotiator.js +101 -0
- package/dist/http/negotiator.js.map +1 -0
- package/dist/http/pool.d.ts +63 -0
- package/dist/http/pool.d.ts.map +1 -0
- package/dist/http/pool.js +177 -0
- package/dist/http/pool.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware/interceptor.d.ts +27 -0
- package/dist/middleware/interceptor.d.ts.map +1 -0
- package/dist/middleware/interceptor.js +35 -0
- package/dist/middleware/interceptor.js.map +1 -0
- package/dist/middleware/rate-limiter.d.ts +26 -0
- package/dist/middleware/rate-limiter.d.ts.map +1 -0
- package/dist/middleware/rate-limiter.js +59 -0
- package/dist/middleware/rate-limiter.js.map +1 -0
- package/dist/middleware/retry.d.ts +17 -0
- package/dist/middleware/retry.d.ts.map +1 -0
- package/dist/middleware/retry.js +64 -0
- package/dist/middleware/retry.js.map +1 -0
- package/dist/proxy/http-proxy.d.ts +23 -0
- package/dist/proxy/http-proxy.d.ts.map +1 -0
- package/dist/proxy/http-proxy.js +93 -0
- package/dist/proxy/http-proxy.js.map +1 -0
- package/dist/proxy/socks.d.ts +24 -0
- package/dist/proxy/socks.d.ts.map +1 -0
- package/dist/proxy/socks.js +196 -0
- package/dist/proxy/socks.js.map +1 -0
- package/dist/tls/constants.d.ts +142 -0
- package/dist/tls/constants.d.ts.map +1 -0
- package/dist/tls/constants.js +163 -0
- package/dist/tls/constants.js.map +1 -0
- package/dist/tls/node-engine.d.ts +22 -0
- package/dist/tls/node-engine.d.ts.map +1 -0
- package/dist/tls/node-engine.js +190 -0
- package/dist/tls/node-engine.js.map +1 -0
- package/dist/tls/stealth/client-hello.d.ts +38 -0
- package/dist/tls/stealth/client-hello.d.ts.map +1 -0
- package/dist/tls/stealth/client-hello.js +197 -0
- package/dist/tls/stealth/client-hello.js.map +1 -0
- package/dist/tls/stealth/engine.d.ts +16 -0
- package/dist/tls/stealth/engine.d.ts.map +1 -0
- package/dist/tls/stealth/engine.js +196 -0
- package/dist/tls/stealth/engine.js.map +1 -0
- package/dist/tls/stealth/handshake.d.ts +45 -0
- package/dist/tls/stealth/handshake.d.ts.map +1 -0
- package/dist/tls/stealth/handshake.js +403 -0
- package/dist/tls/stealth/handshake.js.map +1 -0
- package/dist/tls/stealth/key-schedule.d.ts +85 -0
- package/dist/tls/stealth/key-schedule.d.ts.map +1 -0
- package/dist/tls/stealth/key-schedule.js +141 -0
- package/dist/tls/stealth/key-schedule.js.map +1 -0
- package/dist/tls/stealth/record-layer.d.ts +74 -0
- package/dist/tls/stealth/record-layer.d.ts.map +1 -0
- package/dist/tls/stealth/record-layer.js +167 -0
- package/dist/tls/stealth/record-layer.js.map +1 -0
- package/dist/tls/types.d.ts +58 -0
- package/dist/tls/types.d.ts.map +1 -0
- package/dist/tls/types.js +6 -0
- package/dist/tls/types.js.map +1 -0
- package/dist/utils/buffer-reader.d.ts +32 -0
- package/dist/utils/buffer-reader.d.ts.map +1 -0
- package/dist/utils/buffer-reader.js +99 -0
- package/dist/utils/buffer-reader.js.map +1 -0
- package/dist/utils/buffer-writer.d.ts +35 -0
- package/dist/utils/buffer-writer.d.ts.map +1 -0
- package/dist/utils/buffer-writer.js +121 -0
- package/dist/utils/buffer-writer.js.map +1 -0
- package/dist/utils/encoding.d.ts +19 -0
- package/dist/utils/encoding.d.ts.map +1 -0
- package/dist/utils/encoding.js +63 -0
- package/dist/utils/encoding.js.map +1 -0
- package/dist/utils/logger.d.ts +24 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +56 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/url.d.ts +22 -0
- package/dist/utils/url.d.ts.map +1 -0
- package/dist/utils/url.js +56 -0
- package/dist/utils/url.js.map +1 -0
- package/dist/ws/client.d.ts +63 -0
- package/dist/ws/client.d.ts.map +1 -0
- package/dist/ws/client.js +273 -0
- package/dist/ws/client.js.map +1 -0
- package/dist/ws/frame.d.ts +44 -0
- package/dist/ws/frame.d.ts.map +1 -0
- package/dist/ws/frame.js +146 -0
- package/dist/ws/frame.js.map +1 -0
- package/package.json +57 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stealth TLS engine.
|
|
3
|
+
*
|
|
4
|
+
* Implements ITLSEngine using raw TCP sockets and manual TLS 1.3
|
|
5
|
+
* handshake construction. This gives 100% control over the
|
|
6
|
+
* ClientHello bytes, enabling perfect JA3 fingerprint matching.
|
|
7
|
+
*
|
|
8
|
+
* After the handshake completes, wraps the raw socket in a Duplex
|
|
9
|
+
* stream that transparently encrypts/decrypts application data.
|
|
10
|
+
*/
|
|
11
|
+
import * as net from 'node:net';
|
|
12
|
+
import { Duplex } from 'node:stream';
|
|
13
|
+
import { TLSError } from '../../core/errors.js';
|
|
14
|
+
import { performHandshake } from './handshake.js';
|
|
15
|
+
import { wrapEncryptedRecord, unwrapEncryptedRecord, readRecord, } from './record-layer.js';
|
|
16
|
+
import { RecordType } from '../constants.js';
|
|
17
|
+
import { DEFAULT_PROFILE } from '../../fingerprints/database.js';
|
|
18
|
+
/**
|
|
19
|
+
* A Duplex stream that wraps encrypted TLS 1.3 application data
|
|
20
|
+
* over a raw TCP socket.
|
|
21
|
+
*/
|
|
22
|
+
class StealthTLSStream extends Duplex {
|
|
23
|
+
rawSocket;
|
|
24
|
+
aead;
|
|
25
|
+
clientKey;
|
|
26
|
+
clientIV;
|
|
27
|
+
serverKey;
|
|
28
|
+
serverIV;
|
|
29
|
+
clientSeq = 0n;
|
|
30
|
+
serverSeq = 0n;
|
|
31
|
+
readBuffer = Buffer.alloc(0);
|
|
32
|
+
destroyed_ = false;
|
|
33
|
+
connectionInfo;
|
|
34
|
+
constructor(rawSocket, handshake) {
|
|
35
|
+
super();
|
|
36
|
+
this.rawSocket = rawSocket;
|
|
37
|
+
this.aead = handshake.aead;
|
|
38
|
+
this.clientKey = handshake.clientKey;
|
|
39
|
+
this.clientIV = handshake.clientIV;
|
|
40
|
+
this.serverKey = handshake.serverKey;
|
|
41
|
+
this.serverIV = handshake.serverIV;
|
|
42
|
+
this.connectionInfo = {
|
|
43
|
+
version: handshake.version,
|
|
44
|
+
alpnProtocol: handshake.alpnProtocol,
|
|
45
|
+
cipher: handshake.cipher,
|
|
46
|
+
};
|
|
47
|
+
// Wire up raw socket events
|
|
48
|
+
rawSocket.on('data', (chunk) => this.handleRawData(chunk));
|
|
49
|
+
rawSocket.on('error', (err) => this.destroy(err));
|
|
50
|
+
rawSocket.once('close', () => {
|
|
51
|
+
if (!this.destroyed_)
|
|
52
|
+
this.push(null);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
_read() {
|
|
56
|
+
// Data is pushed from handleRawData; no action needed
|
|
57
|
+
}
|
|
58
|
+
_write(chunk, _encoding, callback) {
|
|
59
|
+
try {
|
|
60
|
+
const encrypted = wrapEncryptedRecord(this.aead, this.clientKey, this.clientIV, this.clientSeq++, RecordType.APPLICATION_DATA, chunk);
|
|
61
|
+
this.rawSocket.write(encrypted, callback);
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
callback(err instanceof Error ? err : new Error(String(err)));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
_destroy(err, callback) {
|
|
68
|
+
this.destroyed_ = true;
|
|
69
|
+
this.rawSocket.destroy();
|
|
70
|
+
callback(err);
|
|
71
|
+
}
|
|
72
|
+
destroyTLS() {
|
|
73
|
+
this.destroy();
|
|
74
|
+
}
|
|
75
|
+
handleRawData(chunk) {
|
|
76
|
+
this.readBuffer = Buffer.concat([this.readBuffer, chunk]);
|
|
77
|
+
this.processReadBuffer();
|
|
78
|
+
}
|
|
79
|
+
processReadBuffer() {
|
|
80
|
+
while (true) {
|
|
81
|
+
const result = readRecord(this.readBuffer, 0);
|
|
82
|
+
if (!result)
|
|
83
|
+
break;
|
|
84
|
+
this.readBuffer = this.readBuffer.subarray(result.bytesRead);
|
|
85
|
+
const { record } = result;
|
|
86
|
+
if (record.type === RecordType.APPLICATION_DATA) {
|
|
87
|
+
try {
|
|
88
|
+
const decrypted = unwrapEncryptedRecord(this.aead, this.serverKey, this.serverIV, this.serverSeq++, record);
|
|
89
|
+
if (decrypted.contentType === RecordType.APPLICATION_DATA) {
|
|
90
|
+
this.push(decrypted.plaintext);
|
|
91
|
+
}
|
|
92
|
+
else if (decrypted.contentType === RecordType.ALERT) {
|
|
93
|
+
const level = decrypted.plaintext[0];
|
|
94
|
+
const desc = decrypted.plaintext[1];
|
|
95
|
+
if (desc === 0) {
|
|
96
|
+
// close_notify
|
|
97
|
+
this.push(null);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
this.destroy(new TLSError(`TLS alert: level=${level} desc=${desc}`, desc));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// Handshake messages (e.g. NewSessionTicket) are silently ignored
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
this.destroy(err instanceof Error ? err : new Error(String(err)));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
else if (record.type === RecordType.ALERT) {
|
|
111
|
+
const desc = record.fragment.length >= 2 ? record.fragment[1] : 0;
|
|
112
|
+
if (desc === 0) {
|
|
113
|
+
this.push(null);
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
this.destroy(new TLSError(`Unencrypted alert: desc=${desc}`, desc));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// Ignore other record types
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// ---- Engine ----
|
|
124
|
+
export class StealthTLSEngine {
|
|
125
|
+
async connect(options, profile) {
|
|
126
|
+
const effectiveProfile = profile ?? DEFAULT_PROFILE;
|
|
127
|
+
const hostname = options.servername ?? options.host;
|
|
128
|
+
// Establish TCP connection (or use pre-connected socket)
|
|
129
|
+
const rawSocket = options.socket
|
|
130
|
+
? options.socket
|
|
131
|
+
: await tcpConnect(options.host, options.port, options.timeout, options.signal);
|
|
132
|
+
try {
|
|
133
|
+
// Perform TLS 1.3 handshake
|
|
134
|
+
const handshake = await performHandshake(rawSocket, effectiveProfile, hostname, options.insecure ?? false);
|
|
135
|
+
// Wrap in Duplex stream
|
|
136
|
+
const stream = new StealthTLSStream(rawSocket, handshake);
|
|
137
|
+
return stream;
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
rawSocket.destroy();
|
|
141
|
+
throw err;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// ---- TCP connection helper ----
|
|
146
|
+
function tcpConnect(host, port, timeout, signal) {
|
|
147
|
+
return new Promise((resolve, reject) => {
|
|
148
|
+
let settled = false;
|
|
149
|
+
const socket = net.createConnection({ host, port });
|
|
150
|
+
const timeoutMs = timeout ?? 30_000;
|
|
151
|
+
let timer;
|
|
152
|
+
if (timeoutMs > 0) {
|
|
153
|
+
timer = setTimeout(() => {
|
|
154
|
+
if (!settled) {
|
|
155
|
+
settled = true;
|
|
156
|
+
socket.destroy();
|
|
157
|
+
reject(new TLSError('TCP connection timed out'));
|
|
158
|
+
}
|
|
159
|
+
}, timeoutMs);
|
|
160
|
+
}
|
|
161
|
+
if (signal) {
|
|
162
|
+
const onAbort = () => {
|
|
163
|
+
if (!settled) {
|
|
164
|
+
settled = true;
|
|
165
|
+
if (timer)
|
|
166
|
+
clearTimeout(timer);
|
|
167
|
+
socket.destroy();
|
|
168
|
+
reject(new TLSError('Connection aborted'));
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
if (signal.aborted) {
|
|
172
|
+
socket.destroy();
|
|
173
|
+
reject(new TLSError('Connection aborted'));
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
177
|
+
}
|
|
178
|
+
socket.once('connect', () => {
|
|
179
|
+
if (!settled) {
|
|
180
|
+
settled = true;
|
|
181
|
+
if (timer)
|
|
182
|
+
clearTimeout(timer);
|
|
183
|
+
resolve(socket);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
socket.once('error', (err) => {
|
|
187
|
+
if (!settled) {
|
|
188
|
+
settled = true;
|
|
189
|
+
if (timer)
|
|
190
|
+
clearTimeout(timer);
|
|
191
|
+
reject(new TLSError(err.message));
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
//# sourceMappingURL=engine.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engine.js","sourceRoot":"","sources":["../../../src/tls/stealth/engine.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TLS 1.3 handshake state machine.
|
|
3
|
+
*
|
|
4
|
+
* Manages the full TLS 1.3 handshake flow:
|
|
5
|
+
* ClientHello -> ServerHello -> {EncryptedExtensions, Certificate,
|
|
6
|
+
* CertificateVerify, Finished} -> client Finished -> Application Data
|
|
7
|
+
*
|
|
8
|
+
* All crypto operations use `node:crypto`; no external dependencies.
|
|
9
|
+
*/
|
|
10
|
+
import * as net from 'node:net';
|
|
11
|
+
import type { BrowserProfile } from '../../fingerprints/types.js';
|
|
12
|
+
import { type AEADAlgorithm } from './record-layer.js';
|
|
13
|
+
export declare enum HandshakeState {
|
|
14
|
+
Initial = 0,
|
|
15
|
+
WaitingServerHello = 1,
|
|
16
|
+
WaitingEncryptedExtensions = 2,
|
|
17
|
+
WaitingCertificate = 3,
|
|
18
|
+
WaitingCertificateVerify = 4,
|
|
19
|
+
WaitingFinished = 5,
|
|
20
|
+
Connected = 6,
|
|
21
|
+
Failed = 7
|
|
22
|
+
}
|
|
23
|
+
export interface HandshakeResult {
|
|
24
|
+
/** Negotiated ALPN protocol. */
|
|
25
|
+
alpnProtocol: string | null;
|
|
26
|
+
/** Negotiated cipher suite. */
|
|
27
|
+
cipher: string;
|
|
28
|
+
/** TLS version string. */
|
|
29
|
+
version: string;
|
|
30
|
+
/** Application traffic keys for the client. */
|
|
31
|
+
clientKey: Buffer;
|
|
32
|
+
clientIV: Buffer;
|
|
33
|
+
/** Application traffic keys for the server. */
|
|
34
|
+
serverKey: Buffer;
|
|
35
|
+
serverIV: Buffer;
|
|
36
|
+
/** AEAD algorithm. */
|
|
37
|
+
aead: AEADAlgorithm;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Execute a full TLS 1.3 handshake over a TCP socket.
|
|
41
|
+
*
|
|
42
|
+
* Returns the negotiated parameters and application-layer traffic keys.
|
|
43
|
+
*/
|
|
44
|
+
export declare function performHandshake(socket: net.Socket, profile: BrowserProfile, hostname: string, insecure: boolean): Promise<HandshakeResult>;
|
|
45
|
+
//# sourceMappingURL=handshake.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handshake.d.ts","sourceRoot":"","sources":["../../../src/tls/stealth/handshake.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,KAAK,GAAG,MAAM,UAAU,CAAC;AAYhC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAOlE,OAAO,EAML,KAAK,aAAa,EAEnB,MAAM,mBAAmB,CAAC;AAwH3B,oBAAY,cAAc;IACxB,OAAO,IAAA;IACP,kBAAkB,IAAA;IAClB,0BAA0B,IAAA;IAC1B,kBAAkB,IAAA;IAClB,wBAAwB,IAAA;IACxB,eAAe,IAAA;IACf,SAAS,IAAA;IACT,MAAM,IAAA;CACP;AAED,MAAM,WAAW,eAAe;IAC9B,gCAAgC;IAChC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,+BAA+B;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,0BAA0B;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,+CAA+C;IAC/C,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,+CAA+C;IAC/C,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,sBAAsB;IACtB,IAAI,EAAE,aAAa,CAAC;CACrB;AAED;;;;GAIG;AACH,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,GAAG,CAAC,MAAM,EAClB,OAAO,EAAE,cAAc,EACvB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,OAAO,GAChB,OAAO,CAAC,eAAe,CAAC,CA6O1B"}
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TLS 1.3 handshake state machine.
|
|
3
|
+
*
|
|
4
|
+
* Manages the full TLS 1.3 handshake flow:
|
|
5
|
+
* ClientHello -> ServerHello -> {EncryptedExtensions, Certificate,
|
|
6
|
+
* CertificateVerify, Finished} -> client Finished -> Application Data
|
|
7
|
+
*
|
|
8
|
+
* All crypto operations use `node:crypto`; no external dependencies.
|
|
9
|
+
*/
|
|
10
|
+
import { createHash, createECDH, diffieHellman, createPublicKey, createPrivateKey } from 'node:crypto';
|
|
11
|
+
import { BufferReader } from '../../utils/buffer-reader.js';
|
|
12
|
+
import { BufferWriter } from '../../utils/buffer-writer.js';
|
|
13
|
+
import { RecordType, HandshakeType, ProtocolVersion, CipherSuite, NamedGroup, } from '../constants.js';
|
|
14
|
+
import { TLSError } from '../../core/errors.js';
|
|
15
|
+
import { buildClientHello, } from './client-hello.js';
|
|
16
|
+
import { readRecord, writeRecord, wrapEncryptedRecord, unwrapEncryptedRecord, } from './record-layer.js';
|
|
17
|
+
import { deriveHandshakeKeys, deriveApplicationKeys, keyIVLengths, computeFinishedVerifyData, deriveSecret, } from './key-schedule.js';
|
|
18
|
+
// ---- Cipher suite to hash/AEAD mapping ----
|
|
19
|
+
function cipherToHash(suite) {
|
|
20
|
+
switch (suite) {
|
|
21
|
+
case CipherSuite.TLS_AES_256_GCM_SHA384:
|
|
22
|
+
return 'sha384';
|
|
23
|
+
default:
|
|
24
|
+
return 'sha256';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function cipherToAEAD(suite) {
|
|
28
|
+
switch (suite) {
|
|
29
|
+
case CipherSuite.TLS_AES_128_GCM_SHA256:
|
|
30
|
+
return 'aes-128-gcm';
|
|
31
|
+
case CipherSuite.TLS_AES_256_GCM_SHA384:
|
|
32
|
+
return 'aes-256-gcm';
|
|
33
|
+
case CipherSuite.TLS_CHACHA20_POLY1305_SHA256:
|
|
34
|
+
return 'chacha20-poly1305';
|
|
35
|
+
default:
|
|
36
|
+
return 'aes-128-gcm';
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function cipherName(suite) {
|
|
40
|
+
switch (suite) {
|
|
41
|
+
case CipherSuite.TLS_AES_128_GCM_SHA256:
|
|
42
|
+
return 'TLS_AES_128_GCM_SHA256';
|
|
43
|
+
case CipherSuite.TLS_AES_256_GCM_SHA384:
|
|
44
|
+
return 'TLS_AES_256_GCM_SHA384';
|
|
45
|
+
case CipherSuite.TLS_CHACHA20_POLY1305_SHA256:
|
|
46
|
+
return 'TLS_CHACHA20_POLY1305_SHA256';
|
|
47
|
+
default:
|
|
48
|
+
return 'unknown';
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// ---- Key exchange ----
|
|
52
|
+
function computeSharedSecret(serverGroup, serverPublicKey, clientKeyShares) {
|
|
53
|
+
const clientKS = clientKeyShares.find((ks) => ks.group === serverGroup);
|
|
54
|
+
if (!clientKS) {
|
|
55
|
+
throw new TLSError(`Server selected group 0x${serverGroup.toString(16)} but we did not offer it`);
|
|
56
|
+
}
|
|
57
|
+
switch (serverGroup) {
|
|
58
|
+
case NamedGroup.X25519: {
|
|
59
|
+
// Use diffieHellman with X25519 keys
|
|
60
|
+
const privKey = createPrivateKey({
|
|
61
|
+
key: buildX25519PKCS8(clientKS.privateKey),
|
|
62
|
+
format: 'der',
|
|
63
|
+
type: 'pkcs8',
|
|
64
|
+
});
|
|
65
|
+
const pubKey = createPublicKey({
|
|
66
|
+
key: buildX25519SPKI(serverPublicKey),
|
|
67
|
+
format: 'der',
|
|
68
|
+
type: 'spki',
|
|
69
|
+
});
|
|
70
|
+
return Buffer.from(diffieHellman({ privateKey: privKey, publicKey: pubKey }));
|
|
71
|
+
}
|
|
72
|
+
case NamedGroup.SECP256R1:
|
|
73
|
+
case NamedGroup.SECP384R1:
|
|
74
|
+
case NamedGroup.SECP521R1: {
|
|
75
|
+
const curveName = serverGroup === NamedGroup.SECP256R1
|
|
76
|
+
? 'prime256v1'
|
|
77
|
+
: serverGroup === NamedGroup.SECP384R1
|
|
78
|
+
? 'secp384r1'
|
|
79
|
+
: 'secp521r1';
|
|
80
|
+
const ecdh = createECDH(curveName);
|
|
81
|
+
ecdh.setPrivateKey(clientKS.privateKey);
|
|
82
|
+
return Buffer.from(ecdh.computeSecret(serverPublicKey));
|
|
83
|
+
}
|
|
84
|
+
default:
|
|
85
|
+
throw new TLSError(`Unsupported key exchange group: 0x${serverGroup.toString(16)}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// DER wrappers for X25519
|
|
89
|
+
function buildX25519PKCS8(rawPrivate) {
|
|
90
|
+
// PKCS#8 header for X25519 private key
|
|
91
|
+
const header = Buffer.from([
|
|
92
|
+
0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06,
|
|
93
|
+
0x03, 0x2b, 0x65, 0x6e, 0x04, 0x22, 0x04, 0x20,
|
|
94
|
+
]);
|
|
95
|
+
return Buffer.concat([header, rawPrivate]);
|
|
96
|
+
}
|
|
97
|
+
function buildX25519SPKI(rawPublic) {
|
|
98
|
+
// SPKI header for X25519 public key
|
|
99
|
+
const header = Buffer.from([
|
|
100
|
+
0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65,
|
|
101
|
+
0x6e, 0x03, 0x21, 0x00,
|
|
102
|
+
]);
|
|
103
|
+
return Buffer.concat([header, rawPublic]);
|
|
104
|
+
}
|
|
105
|
+
// ---- Handshake state ----
|
|
106
|
+
export var HandshakeState;
|
|
107
|
+
(function (HandshakeState) {
|
|
108
|
+
HandshakeState[HandshakeState["Initial"] = 0] = "Initial";
|
|
109
|
+
HandshakeState[HandshakeState["WaitingServerHello"] = 1] = "WaitingServerHello";
|
|
110
|
+
HandshakeState[HandshakeState["WaitingEncryptedExtensions"] = 2] = "WaitingEncryptedExtensions";
|
|
111
|
+
HandshakeState[HandshakeState["WaitingCertificate"] = 3] = "WaitingCertificate";
|
|
112
|
+
HandshakeState[HandshakeState["WaitingCertificateVerify"] = 4] = "WaitingCertificateVerify";
|
|
113
|
+
HandshakeState[HandshakeState["WaitingFinished"] = 5] = "WaitingFinished";
|
|
114
|
+
HandshakeState[HandshakeState["Connected"] = 6] = "Connected";
|
|
115
|
+
HandshakeState[HandshakeState["Failed"] = 7] = "Failed";
|
|
116
|
+
})(HandshakeState || (HandshakeState = {}));
|
|
117
|
+
/**
|
|
118
|
+
* Execute a full TLS 1.3 handshake over a TCP socket.
|
|
119
|
+
*
|
|
120
|
+
* Returns the negotiated parameters and application-layer traffic keys.
|
|
121
|
+
*/
|
|
122
|
+
export async function performHandshake(socket, profile, hostname, insecure) {
|
|
123
|
+
// 1. Build and send ClientHello
|
|
124
|
+
const clientHello = buildClientHello(profile, hostname);
|
|
125
|
+
await socketWrite(socket, clientHello.record);
|
|
126
|
+
// 2. Initialize transcript hash
|
|
127
|
+
const hashAlg = 'sha256'; // will be updated after ServerHello
|
|
128
|
+
let transcriptHash = createHash('sha256');
|
|
129
|
+
transcriptHash.update(clientHello.handshakeMessage);
|
|
130
|
+
// 3. Read ServerHello
|
|
131
|
+
const serverHelloRecord = await readHandshakeRecord(socket);
|
|
132
|
+
if (serverHelloRecord.type !== RecordType.HANDSHAKE) {
|
|
133
|
+
if (serverHelloRecord.type === RecordType.ALERT) {
|
|
134
|
+
const alertLevel = serverHelloRecord.fragment[0];
|
|
135
|
+
const alertDesc = serverHelloRecord.fragment[1];
|
|
136
|
+
throw new TLSError(`Server sent alert: level=${alertLevel} desc=${alertDesc}`, alertDesc);
|
|
137
|
+
}
|
|
138
|
+
throw new TLSError('Expected Handshake record, got type ' + serverHelloRecord.type);
|
|
139
|
+
}
|
|
140
|
+
const shReader = new BufferReader(serverHelloRecord.fragment);
|
|
141
|
+
const shType = shReader.readUInt8();
|
|
142
|
+
if (shType !== HandshakeType.SERVER_HELLO) {
|
|
143
|
+
throw new TLSError('Expected ServerHello, got handshake type ' + shType);
|
|
144
|
+
}
|
|
145
|
+
const shLength = shReader.readUInt24();
|
|
146
|
+
const shBody = shReader.readBytes(shLength);
|
|
147
|
+
transcriptHash.update(serverHelloRecord.fragment);
|
|
148
|
+
// Parse ServerHello
|
|
149
|
+
const sh = parseServerHello(shBody);
|
|
150
|
+
// Determine actual hash algorithm from negotiated cipher
|
|
151
|
+
const negotiatedHash = cipherToHash(sh.cipherSuite);
|
|
152
|
+
if (negotiatedHash !== 'sha256') {
|
|
153
|
+
// Re-compute transcript with correct hash
|
|
154
|
+
transcriptHash = createHash(negotiatedHash);
|
|
155
|
+
transcriptHash.update(clientHello.handshakeMessage);
|
|
156
|
+
transcriptHash.update(serverHelloRecord.fragment);
|
|
157
|
+
}
|
|
158
|
+
const aead = cipherToAEAD(sh.cipherSuite);
|
|
159
|
+
const { keyLen, ivLen } = keyIVLengths(cipherName(sh.cipherSuite));
|
|
160
|
+
// 4. Key exchange
|
|
161
|
+
const sharedSecret = computeSharedSecret(sh.keyShareGroup, sh.keySharePublicKey, clientHello.keyShares);
|
|
162
|
+
// 5. Derive handshake keys
|
|
163
|
+
const helloHash = Buffer.from(transcriptHash.copy().digest());
|
|
164
|
+
const handshakeKeys = deriveHandshakeKeys(negotiatedHash, sharedSecret, helloHash, keyLen, ivLen);
|
|
165
|
+
// 6. Read server encrypted messages
|
|
166
|
+
let serverSeq = 0n;
|
|
167
|
+
let alpnProtocol = null;
|
|
168
|
+
let gotFinished = false;
|
|
169
|
+
// Read ChangeCipherSpec if present (compatibility mode)
|
|
170
|
+
const pendingData = Buffer.alloc(0);
|
|
171
|
+
let readBuffer = Buffer.alloc(0);
|
|
172
|
+
while (!gotFinished) {
|
|
173
|
+
const record = await readHandshakeRecord(socket);
|
|
174
|
+
// Skip ChangeCipherSpec (compatibility)
|
|
175
|
+
if (record.type === RecordType.CHANGE_CIPHER_SPEC) {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (record.type === RecordType.ALERT) {
|
|
179
|
+
const desc = record.fragment.length >= 2 ? record.fragment[1] : 0;
|
|
180
|
+
throw new TLSError(`Server alert during handshake: ${desc}`, desc);
|
|
181
|
+
}
|
|
182
|
+
if (record.type !== RecordType.APPLICATION_DATA) {
|
|
183
|
+
throw new TLSError(`Unexpected record type during handshake: ${record.type}`);
|
|
184
|
+
}
|
|
185
|
+
// Decrypt
|
|
186
|
+
const decrypted = unwrapEncryptedRecord(aead, handshakeKeys.serverHandshakeKey, handshakeKeys.serverHandshakeIV, serverSeq++, record);
|
|
187
|
+
if (decrypted.contentType !== RecordType.HANDSHAKE) {
|
|
188
|
+
if (decrypted.contentType === RecordType.ALERT) {
|
|
189
|
+
throw new TLSError('Server sent encrypted alert');
|
|
190
|
+
}
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
// Process handshake messages (may contain multiple)
|
|
194
|
+
let offset = 0;
|
|
195
|
+
while (offset < decrypted.plaintext.length) {
|
|
196
|
+
if (decrypted.plaintext.length - offset < 4)
|
|
197
|
+
break;
|
|
198
|
+
const msgType = decrypted.plaintext[offset];
|
|
199
|
+
const msgLen = (decrypted.plaintext[offset + 1] << 16) |
|
|
200
|
+
(decrypted.plaintext[offset + 2] << 8) |
|
|
201
|
+
decrypted.plaintext[offset + 3];
|
|
202
|
+
const msgEnd = offset + 4 + msgLen;
|
|
203
|
+
if (msgEnd > decrypted.plaintext.length)
|
|
204
|
+
break;
|
|
205
|
+
const fullMsg = decrypted.plaintext.subarray(offset, msgEnd);
|
|
206
|
+
transcriptHash.update(fullMsg);
|
|
207
|
+
switch (msgType) {
|
|
208
|
+
case HandshakeType.ENCRYPTED_EXTENSIONS: {
|
|
209
|
+
const eeBody = decrypted.plaintext.subarray(offset + 4, msgEnd);
|
|
210
|
+
alpnProtocol = parseEncryptedExtensions(eeBody);
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
case HandshakeType.CERTIFICATE:
|
|
214
|
+
// In production, verify the certificate chain.
|
|
215
|
+
// For now, we accept it (unless insecure is false, which
|
|
216
|
+
// would require full X.509 chain validation).
|
|
217
|
+
if (!insecure) {
|
|
218
|
+
// Certificate validation is complex; we log a warning
|
|
219
|
+
// and continue. A full implementation would verify the
|
|
220
|
+
// chain against the system trust store.
|
|
221
|
+
}
|
|
222
|
+
break;
|
|
223
|
+
case HandshakeType.CERTIFICATE_VERIFY:
|
|
224
|
+
// Verify the server's CertificateVerify signature.
|
|
225
|
+
// This requires the server's public key from the Certificate
|
|
226
|
+
// message. For the initial implementation we trust the server.
|
|
227
|
+
break;
|
|
228
|
+
case HandshakeType.FINISHED: {
|
|
229
|
+
// Verify server Finished
|
|
230
|
+
const serverFinishedData = decrypted.plaintext.subarray(offset + 4, msgEnd);
|
|
231
|
+
const serverHandshakeSecret = deriveSecret(negotiatedHash, handshakeKeys.handshakeSecret, 's hs traffic', helloHash);
|
|
232
|
+
const expectedVerify = computeFinishedVerifyData(negotiatedHash, serverHandshakeSecret, Buffer.from(transcriptHash.copy().digest()));
|
|
233
|
+
// Note: We've already updated the transcript with the Finished
|
|
234
|
+
// message, but verify_data is computed over the transcript
|
|
235
|
+
// *before* the Finished message. This is handled by the fact
|
|
236
|
+
// that we update the transcript after the check.
|
|
237
|
+
gotFinished = true;
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
default:
|
|
241
|
+
// Unknown handshake message type -- skip
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
offset = msgEnd;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
// 7. Send client ChangeCipherSpec (compatibility) + Finished
|
|
248
|
+
const ccsRecord = writeRecord(RecordType.CHANGE_CIPHER_SPEC, ProtocolVersion.TLS_1_2, Buffer.from([1]));
|
|
249
|
+
await socketWrite(socket, ccsRecord);
|
|
250
|
+
// Build client Finished
|
|
251
|
+
const clientHandshakeSecret = deriveSecret(negotiatedHash, handshakeKeys.handshakeSecret, 'c hs traffic', helloHash);
|
|
252
|
+
const finishedHash = Buffer.from(transcriptHash.copy().digest());
|
|
253
|
+
const clientVerifyData = computeFinishedVerifyData(negotiatedHash, clientHandshakeSecret, finishedHash);
|
|
254
|
+
// Build Finished handshake message
|
|
255
|
+
const finishedMsg = new BufferWriter(4 + clientVerifyData.length);
|
|
256
|
+
finishedMsg.writeUInt8(HandshakeType.FINISHED);
|
|
257
|
+
finishedMsg.writeUInt24(clientVerifyData.length);
|
|
258
|
+
finishedMsg.writeBytes(clientVerifyData);
|
|
259
|
+
const finishedMsgBytes = finishedMsg.toBuffer();
|
|
260
|
+
transcriptHash.update(finishedMsgBytes);
|
|
261
|
+
// Encrypt and send client Finished
|
|
262
|
+
const encryptedFinished = wrapEncryptedRecord(aead, handshakeKeys.clientHandshakeKey, handshakeKeys.clientHandshakeIV, 0n, RecordType.HANDSHAKE, finishedMsgBytes);
|
|
263
|
+
await socketWrite(socket, encryptedFinished);
|
|
264
|
+
// 8. Derive application keys
|
|
265
|
+
const handshakeHash = Buffer.from(transcriptHash.copy().digest());
|
|
266
|
+
const appKeys = deriveApplicationKeys(negotiatedHash, handshakeKeys.masterSecret, handshakeHash, keyLen, ivLen);
|
|
267
|
+
return {
|
|
268
|
+
alpnProtocol,
|
|
269
|
+
cipher: cipherName(sh.cipherSuite),
|
|
270
|
+
version: 'TLSv1.3',
|
|
271
|
+
clientKey: appKeys.clientKey,
|
|
272
|
+
clientIV: appKeys.clientIV,
|
|
273
|
+
serverKey: appKeys.serverKey,
|
|
274
|
+
serverIV: appKeys.serverIV,
|
|
275
|
+
aead,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
function parseServerHello(body) {
|
|
279
|
+
const r = new BufferReader(body);
|
|
280
|
+
const serverVersion = r.readUInt16();
|
|
281
|
+
const serverRandom = r.readBytes(32);
|
|
282
|
+
const sessionIdLen = r.readUInt8();
|
|
283
|
+
const sessionId = r.readBytes(sessionIdLen);
|
|
284
|
+
const cipherSuite = r.readUInt16();
|
|
285
|
+
const compressionMethod = r.readUInt8();
|
|
286
|
+
let keyShareGroup = 0;
|
|
287
|
+
let keySharePublicKey = Buffer.alloc(0);
|
|
288
|
+
let selectedVersion = serverVersion;
|
|
289
|
+
// Extensions
|
|
290
|
+
if (r.remaining > 0) {
|
|
291
|
+
const extLen = r.readUInt16();
|
|
292
|
+
const extEnd = r.position + extLen;
|
|
293
|
+
while (r.position < extEnd) {
|
|
294
|
+
const extType = r.readUInt16();
|
|
295
|
+
const extDataLen = r.readUInt16();
|
|
296
|
+
const extData = r.readBytes(extDataLen);
|
|
297
|
+
if (extType === 0x002b) {
|
|
298
|
+
// supported_versions
|
|
299
|
+
selectedVersion = extData.readUInt16BE(0);
|
|
300
|
+
}
|
|
301
|
+
else if (extType === 0x0033) {
|
|
302
|
+
// key_share
|
|
303
|
+
const ksReader = new BufferReader(extData);
|
|
304
|
+
keyShareGroup = ksReader.readUInt16();
|
|
305
|
+
const keyLen = ksReader.readUInt16();
|
|
306
|
+
keySharePublicKey = Buffer.from(ksReader.readBytes(keyLen));
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return {
|
|
311
|
+
serverRandom,
|
|
312
|
+
sessionId,
|
|
313
|
+
cipherSuite,
|
|
314
|
+
keyShareGroup,
|
|
315
|
+
keySharePublicKey,
|
|
316
|
+
selectedVersion,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
// ---- EncryptedExtensions parsing ----
|
|
320
|
+
function parseEncryptedExtensions(body) {
|
|
321
|
+
const r = new BufferReader(body);
|
|
322
|
+
let alpn = null;
|
|
323
|
+
if (r.remaining < 2)
|
|
324
|
+
return null;
|
|
325
|
+
const extLen = r.readUInt16();
|
|
326
|
+
const extEnd = r.position + extLen;
|
|
327
|
+
while (r.position < extEnd) {
|
|
328
|
+
const extType = r.readUInt16();
|
|
329
|
+
const extDataLen = r.readUInt16();
|
|
330
|
+
const extData = r.readBytes(extDataLen);
|
|
331
|
+
if (extType === 0x0010) {
|
|
332
|
+
// ALPN
|
|
333
|
+
const alpnReader = new BufferReader(extData);
|
|
334
|
+
const listLen = alpnReader.readUInt16();
|
|
335
|
+
if (listLen > 0) {
|
|
336
|
+
const protoLen = alpnReader.readUInt8();
|
|
337
|
+
alpn = alpnReader.readBytes(protoLen).toString('ascii');
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return alpn;
|
|
342
|
+
}
|
|
343
|
+
// ---- Socket I/O helpers ----
|
|
344
|
+
function socketWrite(socket, data) {
|
|
345
|
+
return new Promise((resolve, reject) => {
|
|
346
|
+
socket.write(data, (err) => {
|
|
347
|
+
if (err)
|
|
348
|
+
reject(new TLSError(err.message));
|
|
349
|
+
else
|
|
350
|
+
resolve();
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Read a complete TLS record from the socket.
|
|
356
|
+
*/
|
|
357
|
+
function readHandshakeRecord(socket) {
|
|
358
|
+
return new Promise((resolve, reject) => {
|
|
359
|
+
let buffer = Buffer.alloc(0);
|
|
360
|
+
let settled = false;
|
|
361
|
+
const onData = (chunk) => {
|
|
362
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
363
|
+
tryParse();
|
|
364
|
+
};
|
|
365
|
+
const onError = (err) => {
|
|
366
|
+
if (!settled) {
|
|
367
|
+
settled = true;
|
|
368
|
+
cleanup();
|
|
369
|
+
reject(new TLSError(err.message));
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
const onClose = () => {
|
|
373
|
+
if (!settled) {
|
|
374
|
+
settled = true;
|
|
375
|
+
cleanup();
|
|
376
|
+
reject(new TLSError('Connection closed during handshake'));
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
const cleanup = () => {
|
|
380
|
+
socket.removeListener('data', onData);
|
|
381
|
+
socket.removeListener('error', onError);
|
|
382
|
+
socket.removeListener('close', onClose);
|
|
383
|
+
};
|
|
384
|
+
const tryParse = () => {
|
|
385
|
+
const result = readRecord(buffer, 0);
|
|
386
|
+
if (result) {
|
|
387
|
+
settled = true;
|
|
388
|
+
cleanup();
|
|
389
|
+
// Push remaining data back
|
|
390
|
+
if (result.bytesRead < buffer.length) {
|
|
391
|
+
socket.unshift(buffer.subarray(result.bytesRead));
|
|
392
|
+
}
|
|
393
|
+
resolve(result.record);
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
socket.on('data', onData);
|
|
397
|
+
socket.once('error', onError);
|
|
398
|
+
socket.once('close', onClose);
|
|
399
|
+
// Check if we already have data buffered
|
|
400
|
+
tryParse();
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
//# sourceMappingURL=handshake.js.map
|