Skip to content

Commit bf9c114

Browse files
committed
Read ALPN protocols from the client hello
1 parent ef57c6f commit bf9c114

File tree

3 files changed

+60
-1
lines changed

3 files changed

+60
-1
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ If parsing fails, this method will throw an error, but will still ensure all dat
2323
The returned promise resolves to an object, containing:
2424

2525
* `serverName` - The server name requested in the client hello (or undefined if SNI was not used)
26+
* `alpnProtocols` - A list of ALPN protcol names requested in the client hello (or undefined if ALPN was not used)
2627
* `fingerprintData` - The raw components used for JA3 TLS fingerprinting (see the next section)
2728

2829
### TLS fingerprinting

src/index.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ const isGREASE = (value: number) => (value & 0x0f0f) == 0x0a0a;
5656

5757
export type TlsHelloData = {
5858
serverName: string | undefined;
59+
alpnProtocols: string[] | undefined;
5960
fingerprintData: TlsFingerprintData;
6061
};
6162

@@ -138,6 +139,26 @@ function parseSniData(data: Buffer) {
138139
return undefined;
139140
}
140141

142+
function parseAlpnData(data: Buffer) {
143+
const protocols: string[] = [];
144+
145+
const listLength = data.readUInt16BE();
146+
if (listLength !== data.byteLength - 2) {
147+
throw new Error('Invalid length for ALPN list');
148+
}
149+
150+
let offset = 2;
151+
while (offset < data.byteLength) {
152+
const nameLength = data[offset];
153+
offset += 1;
154+
const name = data.slice(offset, offset + nameLength).toString('ascii');
155+
offset += nameLength;
156+
protocols.push(name);
157+
}
158+
159+
return protocols;
160+
}
161+
141162
export async function readTlsClientHello(inputStream: stream.Readable): Promise<TlsHelloData> {
142163
const wasFlowing = inputStream.readableFlowing;
143164
if (wasFlowing) inputStream.pause(); // Pause other readers, so we have time to precisely get the data we need.
@@ -237,13 +258,18 @@ export async function readTlsClientHello(inputStream: stream.Readable): Promise<
237258

238259
// And capture other client hello data that might be interesting:
239260
const sniExtensionData = extensions.find(({ id }) => id.equals(Buffer.from([0x0, 0x0])))?.data;
240-
241261
const serverName = sniExtensionData
242262
? parseSniData(sniExtensionData)
243263
: undefined;
244264

265+
const alpnExtensionData = extensions.find(({ id }) => id.equals(Buffer.from([0x0, 0x10])))?.data;
266+
const alpnProtocols = alpnExtensionData
267+
? parseAlpnData(alpnExtensionData)
268+
: undefined;
269+
245270
return {
246271
serverName,
272+
alpnProtocols,
247273
fingerprintData
248274
};
249275
}

test/test.spec.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,28 @@ describe("Read-TLS-Client-Hello", () => {
7676
expect(curveFormats).to.deep.equal([0, 1, 2]);
7777
});
7878

79+
it("can read Node's client hello data", async () => {
80+
server = makeDestroyable(new net.Server());
81+
82+
server.listen();
83+
await new Promise((resolve) => server.on('listening', resolve));
84+
85+
let incomingSocketPromise = getDeferred<net.Socket>();
86+
server.on('connection', (socket) => incomingSocketPromise.resolve(socket));
87+
88+
const port = (server.address() as net.AddressInfo).port;
89+
tls.connect({
90+
host: 'localhost',
91+
port
92+
}).on('error', () => {}); // Socket will fail, since server never responds, that's OK
93+
94+
const incomingSocket = await incomingSocketPromise;
95+
const { serverName, alpnProtocols } = await readTlsClientHello(incomingSocket);
96+
97+
expect(serverName).to.equal(undefined); // No SNI set for pure TLS like this
98+
expect(alpnProtocols).to.equal(undefined); // No SNI set for pure TLS like this
99+
});
100+
79101
it("can read Node's JA3 fingerprint", async () => {
80102
server = makeDestroyable(new net.Server());
81103

@@ -144,6 +166,16 @@ describe("Read-TLS-Client-Hello", () => {
144166
expect(serverName).to.equal('localhost');
145167
});
146168

169+
it("can capture ALPN protocols from a Chrome request", async () => {
170+
const incomingData = fs.createReadStream(path.join(__dirname, 'fixtures', 'chrome-tls-connect.bin'));
171+
172+
const { alpnProtocols } = await readTlsClientHello(incomingData);
173+
expect(alpnProtocols).to.deep.equal([
174+
'h2',
175+
'http/1.1'
176+
]);
177+
});
178+
147179
it("can calculate the correct TLS fingerprint from a Chrome request", async () => {
148180
const incomingData = fs.createReadStream(path.join(__dirname, 'fixtures', 'chrome-tls-connect.bin'));
149181

0 commit comments

Comments
 (0)