Skip to content

Commit 3ec180e

Browse files
committed
Provide a convenient helper for setup on an existing TLS/HTTPS server
1 parent 4ac1dfd commit 3ec180e

File tree

2 files changed

+115
-2
lines changed

2 files changed

+115
-2
lines changed

src/index.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import * as stream from 'stream';
22
import * as crypto from 'crypto';
3+
import * as tls from 'tls';
4+
import * as net from 'net';
35

46
const collectBytes = (stream: stream.Readable, byteLength: number) => {
57
if (byteLength === 0) return Buffer.from([]);
@@ -164,4 +166,76 @@ export function calculateJa3FromFingerprintData(fingerprintData: TlsFingerprintD
164166

165167
export async function getTlsFingerprintAsJa3(rawStream: stream.Readable) {
166168
return calculateJa3FromFingerprintData(await getTlsFingerprintData(rawStream));
169+
}
170+
171+
interface FingerprintedSocket extends net.Socket {
172+
tlsFingerprint?: {
173+
data: TlsFingerprintData;
174+
ja3: string;
175+
}
176+
}
177+
178+
declare module 'tls' {
179+
interface TLSSocket {
180+
/**
181+
* This module extends the global TLS types so that all TLS sockets may include
182+
* TLS fingerprint data.
183+
*
184+
* This is only set if the socket came from a TLS server where fingerprinting
185+
* has been enabled with `enableFingerprinting`.
186+
*/
187+
tlsFingerprint?: {
188+
data: TlsFingerprintData;
189+
ja3: string;
190+
}
191+
}
192+
}
193+
194+
/**
195+
* Modify a TLS server, so that the TLS fingerprint is always parsed and attached to all
196+
* sockets at the point when the 'secureConnection' event fires.
197+
*
198+
* This method mutates and returns the TLS server provided. TLS fingerprint data is
199+
* available from all TLS sockets afterwards in the `socket.tlsFingerprint` property.
200+
*
201+
* This will work for all standard uses of a TLS server or similar (e.g. an HTTPS server)
202+
* but may behave unpredictably for advanced use cases, e.g. if you are already
203+
* manually injecting connections, hooking methods or events or otherwise doing something
204+
* funky & complicated. In those cases you probably want to use the fingerprint
205+
* calculation methods directly inside your funky logic instead.
206+
*/
207+
export function enableFingerprinting(tlsServer: tls.Server) {
208+
// Disable the normal TLS 'connection' event listener that triggers TLS setup:
209+
const tlsConnectionListener = tlsServer.listeners('connection')[0] as (socket: net.Socket) => {};
210+
if (!tlsConnectionListener) throw new Error('TLS server is not listening for connection events');
211+
tlsServer.removeListener('connection', tlsConnectionListener);
212+
213+
// Listen ourselves for connections, get the fingerprint first, then let TLS setup resume:
214+
tlsServer.on('connection', async (socket: FingerprintedSocket) => {
215+
try {
216+
const fingerprintData = await getTlsFingerprintData(socket);
217+
218+
socket.tlsFingerprint = {
219+
data: fingerprintData,
220+
ja3: calculateJa3FromFingerprintData(fingerprintData)
221+
};
222+
} catch (e) {
223+
console.warn(`TLS fingerprint not available for TLS connection from ${
224+
socket.remoteAddress ?? 'unknown address'
225+
}: ${(e as Error).message ?? e}`);
226+
}
227+
228+
// Once we have a fingerprint, TLS handshakes can continue as normal:
229+
tlsConnectionListener.call(tlsServer, socket);
230+
});
231+
232+
tlsServer.prependListener('secureConnection', (tlsSocket: tls.TLSSocket) => {
233+
const fingerprint = (tlsSocket as unknown as {
234+
_parent?: FingerprintedSocket, // Private TLS socket field which points to the source
235+
})._parent?.tlsFingerprint;
236+
237+
tlsSocket.tlsFingerprint = fingerprint;
238+
});
239+
240+
return tlsServer;
167241
}

test/test.spec.ts

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as path from 'path';
22
import * as fs from 'fs';
33
import * as net from 'net';
44
import * as tls from 'tls';
5+
import * as http from 'http';
56
import * as https from 'https';
67
import { makeDestroyable, DestroyableServer } from 'destroyable-server';
78

@@ -16,7 +17,9 @@ import {
1617
import {
1718
getTlsFingerprintData,
1819
getTlsFingerprintAsJa3,
19-
calculateJa3FromFingerprintData
20+
calculateJa3FromFingerprintData,
21+
enableFingerprinting,
22+
2023
} from '../src/index';
2124

2225
const nodeMajorVersion = parseInt(process.version.slice(1).split('.')[0], 10);
@@ -202,11 +205,47 @@ describe("Read-TLS-Fingerprint", () => {
202205
});
203206

204207
const tlsSocket = await tlsSocketPromise;
205-
const fingerprint = (tlsSocket as any).tlsFingerprint;
208+
const fingerprint = tlsSocket.tlsFingerprint;
206209
expect(fingerprint).to.be.oneOf([
207210
'76cd17e0dc73c98badbb6ee3752dcf4c', // Node 12 - 16
208211
'6521bd74aad3476cdb3daa827288ec35' // Node 17+
209212
]);
210213
});
211214

215+
it("can be calculated automatically with the provided helper", async () => {
216+
const httpsServer = makeDestroyable(
217+
enableFingerprinting(
218+
https.createServer({ key: testKey, cert: testCert })
219+
)
220+
);
221+
server = httpsServer;
222+
223+
const tlsSocketPromise = new Promise<tls.TLSSocket>((resolve) =>
224+
httpsServer.on('request', (request: http.IncomingMessage) =>
225+
resolve(request.socket as tls.TLSSocket)
226+
)
227+
);
228+
229+
httpsServer.listen();
230+
await new Promise((resolve) => httpsServer.on('listening', resolve));
231+
232+
const port = (httpsServer.address() as net.AddressInfo).port;
233+
https.get({
234+
host: 'localhost',
235+
ca: [testCert],
236+
port
237+
}).on('error', () => {}); // No response, we don't care
238+
239+
const tlsSocket = await tlsSocketPromise;
240+
const fingerprint = tlsSocket.tlsFingerprint;
241+
242+
expect(fingerprint!.data[0]).to.equal(771); // Is definitely a TLS 1.2+ fingerprint
243+
expect(fingerprint!.data.length).to.equal(5); // Full data is checked in other tests
244+
245+
expect(fingerprint!.ja3).to.be.oneOf([
246+
'398430069e0a8ecfbc8db0778d658d77', // Node 12 - 16
247+
'0cce74b0d9b7f8528fb2181588d23793' // Node 17+
248+
]);
249+
});
250+
212251
});

0 commit comments

Comments
 (0)