@@ -3,17 +3,21 @@ import * as crypto from 'crypto';
3
3
import * as tls from 'tls';
4
4
import * as net from 'net';
5
5
6
+ type ErrorWithConsumedData = Error & {
7
+ consumedData: Buffer
8
+ };
9
+
6
10
const collectBytes = (stream: stream.Readable, byteLength: number) => {
7
11
if (byteLength === 0) return Buffer.from([]);
8
12
9
13
return new Promise<Buffer>(async (resolve, reject) => {
10
14
const closeReject = () => reject(new Error('Stream closed before expected data could be read'));
11
15
16
+ const data: Buffer[] = [];
17
+
12
18
try {
13
19
stream.on('error', reject);
14
20
stream.on('close', closeReject);
15
-
16
- const data: Buffer[] = [];
17
21
let dataLength = 0;
18
22
let readNull = false;
19
23
do {
@@ -34,6 +38,7 @@ const collectBytes = (stream: stream.Readable, byteLength: number) => {
34
38
35
39
return resolve(Buffer.concat(data, byteLength));
36
40
} catch (e) {
41
+ Object.assign(e as ErrorWithConsumedData, { consumedData: data });
37
42
reject(e);
38
43
} finally {
39
44
stream.removeListener('error', reject);
@@ -57,33 +62,81 @@ export type TlsFingerprintData = [
57
62
curveFormats: number[]
58
63
];
59
64
65
+ /**
66
+ * Seperate error class. If you want to detect TLS parsing errors, but ignore TLS fingerprint
67
+ * issues from definitely-not-TLS traffic, you can ignore all instances of this error.
68
+ */
69
+ export class NonTlsError extends Error {
70
+ constructor(message: string) {
71
+ super(message);
72
+
73
+ // Fix prototypes (required for custom error types):
74
+ const actualProto = new.target.prototype;
75
+ Object.setPrototypeOf(this, actualProto);
76
+ }
77
+ }
78
+
79
+ async function extractTlsHello(inputStream: stream.Readable): Promise<Buffer> {
80
+ const consumedData = [];
81
+ try {
82
+ consumedData.push(await collectBytes(inputStream, 1));
83
+ const [recordType] = consumedData[0];
84
+ if (recordType !== 0x16) throw new Error("Can't calculate TLS fingerprint - not a TLS stream");
85
+
86
+ consumedData.push(await collectBytes(inputStream, 2));
87
+ const recordLengthBytes = await collectBytes(inputStream, 2);
88
+ consumedData.push(recordLengthBytes);
89
+ const recordLength = recordLengthBytes.readUint16BE();
90
+
91
+ consumedData.push(await collectBytes(inputStream, recordLength));
92
+
93
+ // Put all the bytes back, so that this stream can still be used to create a real TLS session
94
+ return Buffer.concat(consumedData);
95
+ } catch (error: any) {
96
+ if (error.consumedData) {
97
+ // This happens if there's an error inside collectBytes with a partial read.
98
+ (error.consumedData as ErrorWithConsumedData).consumedData = Buffer.concat([
99
+ ...consumedData,
100
+ error.consumedData as Buffer
101
+ ])
102
+ } else {
103
+ Object.assign(error, { consumedData: Buffer.concat(consumedData) });
104
+ }
105
+
106
+ throw error;
107
+ }
108
+ }
109
+
60
110
export async function getTlsFingerprintData(inputStream: stream.Readable): Promise<TlsFingerprintData> {
61
111
const wasFlowing = inputStream.readableFlowing;
62
112
if (wasFlowing) inputStream.pause(); // Pause other readers, so we have time to precisely get the data we need.
63
113
64
- const recordTypeBytes = await collectBytes(inputStream, 1);
65
- const [recordType] = recordTypeBytes;
66
- if (recordType !== 0x16) throw new Error("Can't calculate TLS fingerprint - not a TLS stream");
114
+ let clientHelloRecordData: Buffer;
115
+ try {
116
+ clientHelloRecordData = await extractTlsHello(inputStream);
117
+ } catch (error: any) {
118
+ if ('consumedData' in error) {
119
+ inputStream.unshift(error.consumedData as Buffer);
120
+ }
121
+ if (wasFlowing) inputStream.resume(); // If there were other readers, resume and let them continue
122
+ throw new NonTlsError(error.message);
123
+ }
67
124
68
- const tlsRecordVersion = await collectBytes(inputStream, 2);
69
- const recordLengthBytes = await collectBytes( inputStream, 2 );
70
- const recordLength = recordLengthBytes.readUint16BE ();
125
+ // Put all the bytes back, so that this stream can still be used to create a real TLS session
126
+ inputStream.unshift(clientHelloRecordData );
127
+ if (wasFlowing) inputStream.resume (); // If there were other readers, resume and let them continue
71
128
72
129
// Collect all the hello bytes, and then give us a stream of exactly only those bytes, so we can
73
130
// still process them step by step in order:
74
- const recordBytes = await collectBytes(inputStream, recordLength);
75
- const helloDataStream = stream.Readable.from(recordBytes, { objectMode: false });
76
-
77
- // Put all the bytes back, so that this stream can still be used to create a real TLS session
78
- inputStream.unshift(Buffer.concat([recordTypeBytes, tlsRecordVersion, recordLengthBytes, recordBytes]));
79
- if (wasFlowing) inputStream.resume(); // If there were other readers, resume and let them continue
131
+ const clientHello = clientHelloRecordData.slice(5); // Strip TLS record prefix
132
+ const helloDataStream = stream.Readable.from(clientHello, { objectMode: false });
80
133
81
134
const [helloType] = (await collectBytes(helloDataStream, 1));
82
135
if (helloType !== 0x1) throw new Error("Can't calculate TLS fingerprint - not a TLS client hello");
83
136
84
137
const helloLength = (await collectBytes(helloDataStream, 3)).readIntBE(0, 3);
85
- if (helloLength !== recordLength - 4) throw new Error(
86
- `Unexpected client hello length: ${helloLength} (or ${recordLength })`
138
+ if (helloLength !== clientHello.byteLength - 4) throw new Error(
139
+ `Unexpected client hello length: ${helloLength} (of ${clientHello.byteLength })`
87
140
);
88
141
89
142
const clientTlsVersion = await collectBytes(helloDataStream, 2);
@@ -220,9 +273,11 @@ export function enableFingerprinting(tlsServer: tls.Server) {
220
273
ja3: calculateJa3FromFingerprintData(fingerprintData)
221
274
};
222
275
} catch (e) {
223
- console.warn(`TLS fingerprint not available for TLS connection from ${
224
- socket.remoteAddress ?? 'unknown address'
225
- }: ${(e as Error).message ?? e}`);
276
+ if (!(e instanceof NonTlsError)) { // Ignore totally non-TLS traffic
277
+ console.warn(`TLS fingerprint not available for TLS connection from ${
278
+ socket.remoteAddress ?? 'unknown address'
279
+ }: ${(e as Error).message ?? e}`);
280
+ }
226
281
}
227
282
228
283
// Once we have a fingerprint, TLS handshakes can continue as normal:
0 commit comments