@prosopo/provider 5.3.0 → 5.3.1
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/.turbo/turbo-build$colon$cjs.log +3 -3
- package/.turbo/turbo-build$colon$tsc.log +25 -25
- package/.turbo/turbo-build.log +4 -4
- package/CHANGELOG.md +7 -0
- package/dist/api/ja4.d.ts.map +1 -1
- package/dist/api/ja4.js +14 -147
- package/dist/api/ja4.js.map +1 -1
- package/dist/cjs/api/ja4.cjs +13 -147
- package/package.json +3 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @prosopo/provider@5.3.
|
|
2
|
+
> @prosopo/provider@5.3.1 build:cjs
|
|
3
3
|
> NODE_ENV=${NODE_ENV:-development}; vite build --config vite.cjs.config.ts --mode $NODE_ENV
|
|
4
4
|
|
|
5
5
|
ViteCommonJSConfig: .
|
|
@@ -74,6 +74,7 @@ dist/cjs/utils/hashUserAgent.cjs 0
|
|
|
74
74
|
dist/cjs/utils/devicePlatform.cjs 0.76 kB │ gzip: 0.46 kB
|
|
75
75
|
dist/cjs/api/validateAddress.cjs 0.80 kB │ gzip: 0.36 kB
|
|
76
76
|
dist/cjs/tasks/frictionless/frictionlessTasksUtils.cjs 0.85 kB │ gzip: 0.43 kB
|
|
77
|
+
dist/cjs/api/ja4.cjs 0.85 kB │ gzip: 0.48 kB
|
|
77
78
|
dist/cjs/api/dnsEventUrl.cjs 0.85 kB │ gzip: 0.46 kB
|
|
78
79
|
dist/cjs/tasks/dataset/datasetTasks.cjs 0.92 kB │ gzip: 0.41 kB
|
|
79
80
|
dist/cjs/api/block.cjs 0.98 kB │ gzip: 0.38 kB
|
|
@@ -141,7 +142,6 @@ dist/cjs/api/captcha/getPoWCaptchaChallenge.cjs 6
|
|
|
141
142
|
dist/cjs/api/captcha/getImageCaptchaChallenge.cjs 6.44 kB │ gzip: 1.91 kB
|
|
142
143
|
dist/cjs/api/captcha/submitPoWCaptchaSolution.cjs 6.80 kB │ gzip: 2.19 kB
|
|
143
144
|
dist/cjs/api/captcha/getPuzzleCaptchaChallenge.cjs 6.90 kB │ gzip: 1.93 kB
|
|
144
|
-
dist/cjs/api/ja4.cjs 7.11 kB │ gzip: 2.25 kB
|
|
145
145
|
dist/cjs/api/metrics.cjs 7.51 kB │ gzip: 2.12 kB
|
|
146
146
|
dist/cjs/tasks/spam/checkTrafficFilter.cjs 7.65 kB │ gzip: 2.55 kB
|
|
147
147
|
dist/cjs/tasks/detection/bundlePool.cjs 8.72 kB │ gzip: 3.26 kB
|
|
@@ -162,7 +162,7 @@ dist/cjs/tasks/detection/decodeBehavior.cjs 32
|
|
|
162
162
|
dist/cjs/tasks/detection/decodeSimd.cjs 36.03 kB │ gzip: 11.96 kB
|
|
163
163
|
dist/cjs/tasks/detection/decodePayload.cjs 55.69 kB │ gzip: 18.47 kB
|
|
164
164
|
|
|
165
|
-
[32m✓ built in
|
|
165
|
+
[32m✓ built in 160ms[39m
|
|
166
166
|
[copy-plugin] copying /home/runner/work/captcha/captcha/packages/provider/src/tasks/detection/decodeBehavior.js to /home/runner/work/captcha/captcha/packages/provider/dist/cjs/tasks/detection/decodeBehavior.js
|
|
167
167
|
[copy-plugin] copying /home/runner/work/captcha/captcha/packages/provider/src/tasks/detection/decodePayload.js to /home/runner/work/captcha/captcha/packages/provider/dist/cjs/tasks/detection/decodePayload.js
|
|
168
168
|
[copy-plugin] copying /home/runner/work/captcha/captcha/packages/provider/src/tasks/detection/decodeSimd.js to /home/runner/work/captcha/captcha/packages/provider/dist/cjs/tasks/detection/decodeSimd.js
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
|
|
2
|
-
> @prosopo/provider@5.3.
|
|
2
|
+
> @prosopo/provider@5.3.1 build:tsc
|
|
3
3
|
> tsc --build --verbose
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
10:19:10 AM - Projects in this build:
|
|
6
6
|
* ../../dev/config/tsconfig.json
|
|
7
7
|
* ../util/tsconfig.json
|
|
8
8
|
* ../logger/tsconfig.json
|
|
@@ -26,49 +26,49 @@
|
|
|
26
26
|
* ../puzzle-assets/tsconfig.json
|
|
27
27
|
* tsconfig.json
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
10:19:10 AM - Project '../../dev/config/tsconfig.json' is up to date because newest input '../../dev/config/src/webpack/webpack.config.ts' is older than output '../../dev/config/tsconfig.tsbuildinfo'
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
10:19:10 AM - Project '../util/tsconfig.json' is up to date because newest input '../util/src/url.ts' is older than output '../util/tsconfig.tsbuildinfo'
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
10:19:10 AM - Project '../logger/tsconfig.json' is up to date because newest input '../logger/src/tests/loggerBrowser.unit.test.ts' is older than output '../logger/tsconfig.tsbuildinfo'
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
10:19:10 AM - Project '../api-route/tsconfig.json' is up to date because newest input '../api-route/src/apiRoutes.ts' is older than output '../api-route/tsconfig.tsbuildinfo'
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
10:19:10 AM - Project '../locale/tsconfig.json' is up to date because newest input '../locale/src/translationKey.ts' is older than output '../locale/tsconfig.tsbuildinfo'
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
10:19:10 AM - Project '../util-crypto/tsconfig.json' is up to date because newest input '../util-crypto/src/types.ts' is older than output '../util-crypto/tsconfig.tsbuildinfo'
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
10:19:10 AM - Project '../types/tsconfig.json' is up to date because newest input '../types/src/types.test-d.ts' is older than output '../types/tsconfig.tsbuildinfo'
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
10:19:10 AM - Project '../common/tsconfig.json' is up to date because newest input '../common/src/index.ts' is older than output '../common/tsconfig.tsbuildinfo'
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
10:19:10 AM - Project '../ipinfo/tsconfig.json' is up to date because newest input '../ipinfo/src/IpInfoService.ts' is older than output '../ipinfo/tsconfig.tsbuildinfo'
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
10:19:10 AM - Project '../redis-client/tsconfig.json' is up to date because newest input '../redis-client/src/index.ts' is older than output '../redis-client/tsconfig.tsbuildinfo'
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
10:19:10 AM - Project '../api/tsconfig.json' is up to date because newest input '../api/src/tests/apiHarness.ts' is older than output '../api/tsconfig.tsbuildinfo'
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
10:19:10 AM - Project '../user-access-policy/tsconfig.json' is up to date because newest input '../user-access-policy/src/ruleRecord.ts' is older than output '../user-access-policy/tsconfig.tsbuildinfo'
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
10:19:10 AM - Project '../types-database/tsconfig.json' is up to date because newest input '../types-database/src/index.ts' is older than output '../types-database/tsconfig.tsbuildinfo'
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
10:19:10 AM - Project '../database/tsconfig.json' is up to date because newest input '../database/src/tests/unit/redisCacheEscalation.unit.test.ts' is older than output '../database/tsconfig.tsbuildinfo'
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
10:19:10 AM - Project '../keyring/tsconfig.json' is up to date because newest input '../keyring/src/keyring/keyring.test.ts' is older than output '../keyring/tsconfig.tsbuildinfo'
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
10:19:10 AM - Project '../types-env/tsconfig.json' is up to date because newest input '../types-env/src/tests/typesEnv.test-d.ts' is older than output '../types-env/tsconfig.tsbuildinfo'
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
10:19:10 AM - Project '../env/tsconfig.json' is up to date because newest input '../env/src/index.ts' is older than output '../env/tsconfig.tsbuildinfo'
|
|
62
62
|
|
|
63
|
-
|
|
63
|
+
10:19:10 AM - Project '../api-express-router/tsconfig.json' is up to date because newest input '../api-express-router/src/tests/unit/apiExpressRouterFactory.unit.test.ts' is older than output '../api-express-router/tsconfig.tsbuildinfo'
|
|
64
64
|
|
|
65
|
-
|
|
65
|
+
10:19:10 AM - Project '../datasets/tsconfig.json' is up to date because newest input '../datasets/src/tests/mocks/data/captchas.ts' is older than output '../datasets/tsconfig.tsbuildinfo'
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
10:19:10 AM - Project '../load-balancer/tsconfig.json' is up to date because newest input '../load-balancer/src/balancer.ts' is older than output '../load-balancer/tsconfig.tsbuildinfo'
|
|
68
68
|
|
|
69
|
-
|
|
69
|
+
10:19:10 AM - Project '../puzzle-assets/tsconfig.json' is up to date because newest input '../puzzle-assets/src/background.ts' is older than output '../puzzle-assets/tsconfig.tsbuildinfo'
|
|
70
70
|
|
|
71
|
-
|
|
71
|
+
10:19:10 AM - Project 'tsconfig.json' is out of date because output file 'tsconfig.tsbuildinfo' does not exist
|
|
72
72
|
|
|
73
|
-
|
|
73
|
+
10:19:10 AM - Building project '/home/runner/work/captcha/captcha/packages/provider/tsconfig.json'...
|
|
74
74
|
|
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
|
|
2
|
-
> @prosopo/provider@5.3.
|
|
2
|
+
> @prosopo/provider@5.3.1 build
|
|
3
3
|
> npm run build:cross-env -- --mode ${NODE_ENV:-development}
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
> @prosopo/provider@5.3.
|
|
6
|
+
> @prosopo/provider@5.3.1 build:cross-env
|
|
7
7
|
> vite build --config vite.esm.config.ts --mode production
|
|
8
8
|
|
|
9
9
|
ViteEsmConfig: .
|
|
@@ -81,6 +81,7 @@ dist/utils/devicePlatform.js 0.74 k
|
|
|
81
81
|
dist/tasks/frictionless/frictionlessTasksUtils.js 0.79 kB │ gzip: 0.42 kB
|
|
82
82
|
dist/api/block.js 0.80 kB │ gzip: 0.36 kB
|
|
83
83
|
dist/api/dnsEventUrl.js 0.80 kB │ gzip: 0.44 kB
|
|
84
|
+
dist/api/ja4.js 0.80 kB │ gzip: 0.45 kB
|
|
84
85
|
dist/tasks/dataset/datasetTasks.js 0.85 kB │ gzip: 0.39 kB
|
|
85
86
|
dist/tasks/powCaptcha/powTasksUtils.js 0.89 kB │ gzip: 0.46 kB
|
|
86
87
|
dist/api/headerCheckMiddleware.js 0.91 kB │ gzip: 0.43 kB
|
|
@@ -146,7 +147,6 @@ dist/api/captcha/getImageCaptchaChallenge.js 5.86 k
|
|
|
146
147
|
dist/api/captcha/getPuzzleCaptchaChallenge.js 6.23 kB │ gzip: 1.87 kB
|
|
147
148
|
dist/api/captcha/submitPoWCaptchaSolution.js 6.47 kB │ gzip: 2.16 kB
|
|
148
149
|
dist/api/metrics.js 6.96 kB │ gzip: 2.09 kB
|
|
149
|
-
dist/api/ja4.js 7.06 kB │ gzip: 2.23 kB
|
|
150
150
|
dist/tasks/spam/checkTrafficFilter.js 7.32 kB │ gzip: 2.53 kB
|
|
151
151
|
dist/tasks/detection/bundlePool.js 8.30 kB │ gzip: 3.21 kB
|
|
152
152
|
dist/tasks/decisionMachine/decisionMachineRunner.js 9.40 kB │ gzip: 2.86 kB
|
|
@@ -166,7 +166,7 @@ dist/tasks/detection/decodeBehavior.js 32.26 k
|
|
|
166
166
|
dist/tasks/detection/decodeSimd.js 36.06 kB │ gzip: 12.00 kB
|
|
167
167
|
dist/tasks/detection/decodePayload.js 55.60 kB │ gzip: 18.45 kB
|
|
168
168
|
|
|
169
|
-
[32m✓ built in
|
|
169
|
+
[32m✓ built in 130ms[39m
|
|
170
170
|
[copy-plugin] copying /home/runner/work/captcha/captcha/packages/provider/src/tasks/detection/decodeBehavior.js to /home/runner/work/captcha/captcha/packages/provider/dist/tasks/detection/decodeBehavior.js
|
|
171
171
|
[copy-plugin] copying /home/runner/work/captcha/captcha/packages/provider/src/tasks/detection/decodePayload.js to /home/runner/work/captcha/captcha/packages/provider/dist/tasks/detection/decodePayload.js
|
|
172
172
|
[copy-plugin] copying /home/runner/work/captcha/captcha/packages/provider/src/tasks/detection/decodeSimd.js to /home/runner/work/captcha/captcha/packages/provider/dist/tasks/detection/decodeSimd.js
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# @prosopo/provider
|
|
2
2
|
|
|
3
|
+
## 5.3.1
|
|
4
|
+
### Patch Changes
|
|
5
|
+
|
|
6
|
+
- 721c5ba: Move JA4 TLS fingerprint computation to a Rust napi module (@prosopo/native-ja4). Provider-side JA4 middleware is ~2.7× faster on realistic ClientHellos. The cli bundle plugin now copies the .node binary next to the bundle so it works in the container.
|
|
7
|
+
- Updated dependencies [721c5ba]
|
|
8
|
+
- @prosopo/native-ja4@0.0.2
|
|
9
|
+
|
|
3
10
|
## 5.3.0
|
|
4
11
|
### Minor Changes
|
|
5
12
|
|
package/dist/api/ja4.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ja4.d.ts","sourceRoot":"","sources":["../../src/api/ja4.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ja4.d.ts","sourceRoot":"","sources":["../../src/api/ja4.ts"],"names":[],"mappings":"AAkCA,qBAAa,aAAc,SAAQ,KAAK;gBAC3B,OAAO,EAAE,MAAM;CAI3B;AAQD,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAMjD"}
|
package/dist/api/ja4.js
CHANGED
|
@@ -1,38 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
2
|
//#region src/api/ja4.ts
|
|
3
|
-
var
|
|
4
|
-
var
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
var EXT_ALPN = 16;
|
|
10
|
-
var EXT_SUPPORTED_VERSIONS = 43;
|
|
11
|
-
function isGrease(id) {
|
|
12
|
-
return (id & 3855) === 2570 && id >> 8 === (id & 255);
|
|
13
|
-
}
|
|
14
|
-
function hash12(input) {
|
|
15
|
-
return createHash("sha256").update(input).digest("hex").slice(0, 12);
|
|
16
|
-
}
|
|
17
|
-
function formatIdList(ids) {
|
|
18
|
-
return ids.map((id) => id.toString(16).padStart(4, "0")).join(",");
|
|
19
|
-
}
|
|
20
|
-
function alpnChar(b) {
|
|
21
|
-
if (b >= 48 && b <= 57 || b >= 65 && b <= 90 || b >= 97 && b <= 122) return String.fromCharCode(b);
|
|
22
|
-
return b.toString(16).padStart(2, "0");
|
|
23
|
-
}
|
|
24
|
-
function alpnFirstLast(protocol) {
|
|
25
|
-
return [protocol.length > 0 ? alpnChar(protocol.readUInt8(0)) : "0", protocol.length > 1 ? alpnChar(protocol.readUInt8(protocol.length - 1)) : "0"];
|
|
26
|
-
}
|
|
27
|
-
function tlsVersionStr(version) {
|
|
28
|
-
switch (version) {
|
|
29
|
-
case 769: return "10";
|
|
30
|
-
case 770: return "11";
|
|
31
|
-
case 771: return "12";
|
|
32
|
-
case 772: return "13";
|
|
33
|
-
default: return "00";
|
|
3
|
+
var req = createRequire(import.meta.url);
|
|
4
|
+
var nativeCalculateJa4 = (() => {
|
|
5
|
+
try {
|
|
6
|
+
return req("@prosopo/native-ja4");
|
|
7
|
+
} catch {
|
|
8
|
+
return req("./index.linux-x64-gnu.node");
|
|
34
9
|
}
|
|
35
|
-
}
|
|
10
|
+
})().calculateJa4;
|
|
36
11
|
var Ja4ParseError = class extends Error {
|
|
37
12
|
constructor(message) {
|
|
38
13
|
super(message);
|
|
@@ -42,123 +17,15 @@ var Ja4ParseError = class extends Error {
|
|
|
42
17
|
/**
|
|
43
18
|
* Compute a JA4 TLS fingerprint from raw ClientHello bytes.
|
|
44
19
|
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* 4-byte handshake header (msg_type + msg_len)
|
|
48
|
-
* ClientHello body
|
|
49
|
-
*
|
|
50
|
-
* Throws Ja4ParseError for structural failures in the TLS record or handshake
|
|
51
|
-
* header (wrong record type, wrong message type, declared lengths exceeding the
|
|
52
|
-
* buffer, odd cipher-suite length).
|
|
53
|
-
*
|
|
54
|
-
* Extension-level malformation (truncated extension body, malformed ALPN
|
|
55
|
-
* entries) is handled defensively: parsing stops at the first bad entry and
|
|
56
|
-
* a fingerprint is produced from whatever was successfully parsed. This matches
|
|
57
|
-
* the Rust reference implementation and avoids turning extension parse errors
|
|
58
|
-
* into connection-fingerprinting failures.
|
|
20
|
+
* Delegates to the Rust implementation in @prosopo/native-ja4. Structural
|
|
21
|
+
* TLS parse failures are rethrown as Ja4ParseError.
|
|
59
22
|
*/
|
|
60
23
|
function calculateJa4(data) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (recordType !== HANDSHAKE_RECORD_TYPE) throw new Ja4ParseError(`Invalid record type: 0x${recordType.toString(16)}`);
|
|
66
|
-
offset += 2;
|
|
67
|
-
const recordLen = data.readUInt16BE(offset);
|
|
68
|
-
offset += 2;
|
|
69
|
-
if (data.length < RECORD_HEADER_LENGTH + recordLen) throw new Ja4ParseError("Record length exceeds buffer");
|
|
70
|
-
data = data.subarray(0, RECORD_HEADER_LENGTH + recordLen);
|
|
71
|
-
if (data.length < RECORD_HEADER_LENGTH + HANDSHAKE_HEADER_LENGTH) throw new Ja4ParseError("Record too short for handshake header");
|
|
72
|
-
const msgType = data.readUInt8(offset);
|
|
73
|
-
offset += 1;
|
|
74
|
-
if (msgType !== HANDSHAKE_MESSAGE_TYPE) throw new Ja4ParseError(`Not a ClientHello: handshake type 0x${msgType.toString(16)}`);
|
|
75
|
-
const msgLen = data.readUIntBE(offset, 3);
|
|
76
|
-
offset += 3;
|
|
77
|
-
if (data.length - offset < msgLen) throw new Ja4ParseError("Handshake message length exceeds record");
|
|
78
|
-
data = data.subarray(0, offset + msgLen);
|
|
79
|
-
if (data.length - offset < 35) throw new Ja4ParseError("ClientHello body too short");
|
|
80
|
-
let clientVersion = data.readUInt16BE(offset);
|
|
81
|
-
offset += 2;
|
|
82
|
-
offset += 32;
|
|
83
|
-
const sessionIdLen = data.readUInt8(offset);
|
|
84
|
-
offset += 1;
|
|
85
|
-
if (data.length - offset < sessionIdLen) throw new Ja4ParseError("Truncated at session ID");
|
|
86
|
-
offset += sessionIdLen;
|
|
87
|
-
if (data.length - offset < 2) throw new Ja4ParseError("Truncated at cipher suites length");
|
|
88
|
-
const cipherSuitesLen = data.readUInt16BE(offset);
|
|
89
|
-
offset += 2;
|
|
90
|
-
if (data.length - offset < cipherSuitesLen) throw new Ja4ParseError("Truncated at cipher suites");
|
|
91
|
-
if (cipherSuitesLen % 2 !== 0) throw new Ja4ParseError("Cipher suites length is not a multiple of 2");
|
|
92
|
-
const cipherSuites = [];
|
|
93
|
-
for (let i = 0; i < cipherSuitesLen; i += 2) cipherSuites.push(data.readUInt16BE(offset + i));
|
|
94
|
-
offset += cipherSuitesLen;
|
|
95
|
-
if (data.length - offset < 1) throw new Ja4ParseError("Truncated at compression methods");
|
|
96
|
-
const compressionLen = data.readUInt8(offset);
|
|
97
|
-
offset += 1;
|
|
98
|
-
if (data.length - offset < compressionLen) throw new Ja4ParseError("Truncated at compression methods data");
|
|
99
|
-
offset += compressionLen;
|
|
100
|
-
if (data.length - offset < 2) throw new Ja4ParseError("Truncated at extensions length");
|
|
101
|
-
const extensionsLen = data.readUInt16BE(offset);
|
|
102
|
-
offset += 2;
|
|
103
|
-
if (data.length - offset < extensionsLen) throw new Ja4ParseError("Truncated at extensions");
|
|
104
|
-
const extIds = [];
|
|
105
|
-
const sigAlgorithms = [];
|
|
106
|
-
const alpnProtocols = [];
|
|
107
|
-
let extOffset = offset;
|
|
108
|
-
const extEnd = offset + extensionsLen;
|
|
109
|
-
while (extOffset + 4 <= extEnd) {
|
|
110
|
-
const extId = data.readUInt16BE(extOffset);
|
|
111
|
-
extOffset += 2;
|
|
112
|
-
const extLen = data.readUInt16BE(extOffset);
|
|
113
|
-
extOffset += 2;
|
|
114
|
-
if (extOffset + extLen > extEnd) break;
|
|
115
|
-
const extData = data.subarray(extOffset, extOffset + extLen);
|
|
116
|
-
extOffset += extLen;
|
|
117
|
-
extIds.push(extId);
|
|
118
|
-
if (extId === EXT_SUPPORTED_VERSIONS) {
|
|
119
|
-
if (extData.length >= 1) {
|
|
120
|
-
const listLen = extData.readUInt8(0);
|
|
121
|
-
let best = 0;
|
|
122
|
-
for (let i = 1; i + 1 < extData.length && i + 1 <= listLen; i += 2) {
|
|
123
|
-
const v = extData.readUInt16BE(i);
|
|
124
|
-
if (!isGrease(v) && v > best) best = v;
|
|
125
|
-
}
|
|
126
|
-
if (best > 0) clientVersion = best;
|
|
127
|
-
}
|
|
128
|
-
} else if (extId === EXT_SIGNATURE_ALGORITHMS) {
|
|
129
|
-
if (extData.length >= 2) {
|
|
130
|
-
const algLen = extData.readUInt16BE(0);
|
|
131
|
-
for (let i = 2; i + 1 < extData.length && i + 1 < algLen + 2; i += 2) {
|
|
132
|
-
const alg = extData.readUInt16BE(i);
|
|
133
|
-
if (!isGrease(alg)) sigAlgorithms.push(alg);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
} else if (extId === EXT_ALPN) {
|
|
137
|
-
if (extData.length >= 2) {
|
|
138
|
-
const listLen = extData.readUInt16BE(0);
|
|
139
|
-
const listEnd = Math.min(extData.length, 2 + listLen);
|
|
140
|
-
let alpnOff = 2;
|
|
141
|
-
while (alpnOff < listEnd) {
|
|
142
|
-
const protoLen = extData.readUInt8(alpnOff);
|
|
143
|
-
alpnOff += 1;
|
|
144
|
-
if (alpnOff + protoLen > listEnd) break;
|
|
145
|
-
alpnProtocols.push(extData.subarray(alpnOff, alpnOff + protoLen));
|
|
146
|
-
alpnOff += protoLen;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
}
|
|
24
|
+
try {
|
|
25
|
+
return nativeCalculateJa4(data);
|
|
26
|
+
} catch (err) {
|
|
27
|
+
throw new Ja4ParseError(err instanceof Error ? err.message : String(err));
|
|
150
28
|
}
|
|
151
|
-
const versionStr = tlsVersionStr(clientVersion);
|
|
152
|
-
const sni = extIds.includes(EXT_SERVER_NAME) ? "d" : "i";
|
|
153
|
-
const firstAlpn = alpnProtocols[0];
|
|
154
|
-
const [alpnFirst, alpnLast] = firstAlpn ? alpnFirstLast(firstAlpn) : ["0", "0"];
|
|
155
|
-
const nonGreaseCiphers = cipherSuites.filter((c) => !isGrease(c));
|
|
156
|
-
const nonGreaseExts = extIds.filter((e) => !isGrease(e));
|
|
157
|
-
const firstChunk = `t${versionStr}${sni}${Math.min(99, nonGreaseCiphers.length).toString().padStart(2, "0")}${Math.min(99, nonGreaseExts.length).toString().padStart(2, "0")}${alpnFirst}${alpnLast}`;
|
|
158
|
-
const cipherHash = hash12(formatIdList([...nonGreaseCiphers].sort((a, b) => a - b)));
|
|
159
|
-
const extStr = formatIdList([...nonGreaseExts.filter((e) => e !== EXT_SERVER_NAME && e !== EXT_ALPN)].sort((a, b) => a - b));
|
|
160
|
-
const sigAlgStr = formatIdList(sigAlgorithms);
|
|
161
|
-
return `${firstChunk}_${cipherHash}_${hash12(sigAlgStr.length > 0 ? `${extStr}_${sigAlgStr}` : extStr)}`;
|
|
162
29
|
}
|
|
163
30
|
//#endregion
|
|
164
31
|
export { Ja4ParseError, calculateJa4 };
|
package/dist/api/ja4.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ja4.js","sourceRoot":"","sources":["../../src/api/ja4.ts"],"names":[],"mappings":"AAcA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"ja4.js","sourceRoot":"","sources":["../../src/api/ja4.ts"],"names":[],"mappings":"AAcA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAU5C,MAAM,GAAG,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC3C,MAAM,eAAe,GAAoB,CAAC,GAAG,EAAE;IAC9C,IAAI,CAAC;QACJ,OAAO,GAAG,CAAC,qBAAqB,CAAoB,CAAC;IACtD,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,GAAG,CAAC,4BAA4B,CAAoB,CAAC;IAC7D,CAAC;AACF,CAAC,CAAC,EAAE,CAAC;AACL,MAAM,kBAAkB,GAAG,eAAe,CAAC,YAAY,CAAC;AAExD,MAAM,OAAO,aAAc,SAAQ,KAAK;IACvC,YAAY,OAAe;QAC1B,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,eAAe,CAAC;IAC7B,CAAC;CACD;AAQD,MAAM,UAAU,YAAY,CAAC,IAAY;IACxC,IAAI,CAAC;QACJ,OAAO,kBAAkB,CAAC,IAAI,CAAC,CAAC;IACjC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACd,MAAM,IAAI,aAAa,CAAC,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;IAC3E,CAAC;AACF,CAAC"}
|
package/dist/cjs/api/ja4.cjs
CHANGED
|
@@ -1,38 +1,12 @@
|
|
|
1
|
-
let node_crypto = require("node:crypto");
|
|
2
1
|
//#region src/api/ja4.ts
|
|
3
|
-
var
|
|
4
|
-
var
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
var EXT_ALPN = 16;
|
|
10
|
-
var EXT_SUPPORTED_VERSIONS = 43;
|
|
11
|
-
function isGrease(id) {
|
|
12
|
-
return (id & 3855) === 2570 && id >> 8 === (id & 255);
|
|
13
|
-
}
|
|
14
|
-
function hash12(input) {
|
|
15
|
-
return (0, node_crypto.createHash)("sha256").update(input).digest("hex").slice(0, 12);
|
|
16
|
-
}
|
|
17
|
-
function formatIdList(ids) {
|
|
18
|
-
return ids.map((id) => id.toString(16).padStart(4, "0")).join(",");
|
|
19
|
-
}
|
|
20
|
-
function alpnChar(b) {
|
|
21
|
-
if (b >= 48 && b <= 57 || b >= 65 && b <= 90 || b >= 97 && b <= 122) return String.fromCharCode(b);
|
|
22
|
-
return b.toString(16).padStart(2, "0");
|
|
23
|
-
}
|
|
24
|
-
function alpnFirstLast(protocol) {
|
|
25
|
-
return [protocol.length > 0 ? alpnChar(protocol.readUInt8(0)) : "0", protocol.length > 1 ? alpnChar(protocol.readUInt8(protocol.length - 1)) : "0"];
|
|
26
|
-
}
|
|
27
|
-
function tlsVersionStr(version) {
|
|
28
|
-
switch (version) {
|
|
29
|
-
case 769: return "10";
|
|
30
|
-
case 770: return "11";
|
|
31
|
-
case 771: return "12";
|
|
32
|
-
case 772: return "13";
|
|
33
|
-
default: return "00";
|
|
2
|
+
var req = (0, require("node:module").createRequire)(require("url").pathToFileURL(__filename).href);
|
|
3
|
+
var nativeCalculateJa4 = (() => {
|
|
4
|
+
try {
|
|
5
|
+
return req("@prosopo/native-ja4");
|
|
6
|
+
} catch {
|
|
7
|
+
return req("./index.linux-x64-gnu.node");
|
|
34
8
|
}
|
|
35
|
-
}
|
|
9
|
+
})().calculateJa4;
|
|
36
10
|
var Ja4ParseError = class extends Error {
|
|
37
11
|
constructor(message) {
|
|
38
12
|
super(message);
|
|
@@ -42,123 +16,15 @@ var Ja4ParseError = class extends Error {
|
|
|
42
16
|
/**
|
|
43
17
|
* Compute a JA4 TLS fingerprint from raw ClientHello bytes.
|
|
44
18
|
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* 4-byte handshake header (msg_type + msg_len)
|
|
48
|
-
* ClientHello body
|
|
49
|
-
*
|
|
50
|
-
* Throws Ja4ParseError for structural failures in the TLS record or handshake
|
|
51
|
-
* header (wrong record type, wrong message type, declared lengths exceeding the
|
|
52
|
-
* buffer, odd cipher-suite length).
|
|
53
|
-
*
|
|
54
|
-
* Extension-level malformation (truncated extension body, malformed ALPN
|
|
55
|
-
* entries) is handled defensively: parsing stops at the first bad entry and
|
|
56
|
-
* a fingerprint is produced from whatever was successfully parsed. This matches
|
|
57
|
-
* the Rust reference implementation and avoids turning extension parse errors
|
|
58
|
-
* into connection-fingerprinting failures.
|
|
19
|
+
* Delegates to the Rust implementation in @prosopo/native-ja4. Structural
|
|
20
|
+
* TLS parse failures are rethrown as Ja4ParseError.
|
|
59
21
|
*/
|
|
60
22
|
function calculateJa4(data) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (recordType !== HANDSHAKE_RECORD_TYPE) throw new Ja4ParseError(`Invalid record type: 0x${recordType.toString(16)}`);
|
|
66
|
-
offset += 2;
|
|
67
|
-
const recordLen = data.readUInt16BE(offset);
|
|
68
|
-
offset += 2;
|
|
69
|
-
if (data.length < RECORD_HEADER_LENGTH + recordLen) throw new Ja4ParseError("Record length exceeds buffer");
|
|
70
|
-
data = data.subarray(0, RECORD_HEADER_LENGTH + recordLen);
|
|
71
|
-
if (data.length < RECORD_HEADER_LENGTH + HANDSHAKE_HEADER_LENGTH) throw new Ja4ParseError("Record too short for handshake header");
|
|
72
|
-
const msgType = data.readUInt8(offset);
|
|
73
|
-
offset += 1;
|
|
74
|
-
if (msgType !== HANDSHAKE_MESSAGE_TYPE) throw new Ja4ParseError(`Not a ClientHello: handshake type 0x${msgType.toString(16)}`);
|
|
75
|
-
const msgLen = data.readUIntBE(offset, 3);
|
|
76
|
-
offset += 3;
|
|
77
|
-
if (data.length - offset < msgLen) throw new Ja4ParseError("Handshake message length exceeds record");
|
|
78
|
-
data = data.subarray(0, offset + msgLen);
|
|
79
|
-
if (data.length - offset < 35) throw new Ja4ParseError("ClientHello body too short");
|
|
80
|
-
let clientVersion = data.readUInt16BE(offset);
|
|
81
|
-
offset += 2;
|
|
82
|
-
offset += 32;
|
|
83
|
-
const sessionIdLen = data.readUInt8(offset);
|
|
84
|
-
offset += 1;
|
|
85
|
-
if (data.length - offset < sessionIdLen) throw new Ja4ParseError("Truncated at session ID");
|
|
86
|
-
offset += sessionIdLen;
|
|
87
|
-
if (data.length - offset < 2) throw new Ja4ParseError("Truncated at cipher suites length");
|
|
88
|
-
const cipherSuitesLen = data.readUInt16BE(offset);
|
|
89
|
-
offset += 2;
|
|
90
|
-
if (data.length - offset < cipherSuitesLen) throw new Ja4ParseError("Truncated at cipher suites");
|
|
91
|
-
if (cipherSuitesLen % 2 !== 0) throw new Ja4ParseError("Cipher suites length is not a multiple of 2");
|
|
92
|
-
const cipherSuites = [];
|
|
93
|
-
for (let i = 0; i < cipherSuitesLen; i += 2) cipherSuites.push(data.readUInt16BE(offset + i));
|
|
94
|
-
offset += cipherSuitesLen;
|
|
95
|
-
if (data.length - offset < 1) throw new Ja4ParseError("Truncated at compression methods");
|
|
96
|
-
const compressionLen = data.readUInt8(offset);
|
|
97
|
-
offset += 1;
|
|
98
|
-
if (data.length - offset < compressionLen) throw new Ja4ParseError("Truncated at compression methods data");
|
|
99
|
-
offset += compressionLen;
|
|
100
|
-
if (data.length - offset < 2) throw new Ja4ParseError("Truncated at extensions length");
|
|
101
|
-
const extensionsLen = data.readUInt16BE(offset);
|
|
102
|
-
offset += 2;
|
|
103
|
-
if (data.length - offset < extensionsLen) throw new Ja4ParseError("Truncated at extensions");
|
|
104
|
-
const extIds = [];
|
|
105
|
-
const sigAlgorithms = [];
|
|
106
|
-
const alpnProtocols = [];
|
|
107
|
-
let extOffset = offset;
|
|
108
|
-
const extEnd = offset + extensionsLen;
|
|
109
|
-
while (extOffset + 4 <= extEnd) {
|
|
110
|
-
const extId = data.readUInt16BE(extOffset);
|
|
111
|
-
extOffset += 2;
|
|
112
|
-
const extLen = data.readUInt16BE(extOffset);
|
|
113
|
-
extOffset += 2;
|
|
114
|
-
if (extOffset + extLen > extEnd) break;
|
|
115
|
-
const extData = data.subarray(extOffset, extOffset + extLen);
|
|
116
|
-
extOffset += extLen;
|
|
117
|
-
extIds.push(extId);
|
|
118
|
-
if (extId === EXT_SUPPORTED_VERSIONS) {
|
|
119
|
-
if (extData.length >= 1) {
|
|
120
|
-
const listLen = extData.readUInt8(0);
|
|
121
|
-
let best = 0;
|
|
122
|
-
for (let i = 1; i + 1 < extData.length && i + 1 <= listLen; i += 2) {
|
|
123
|
-
const v = extData.readUInt16BE(i);
|
|
124
|
-
if (!isGrease(v) && v > best) best = v;
|
|
125
|
-
}
|
|
126
|
-
if (best > 0) clientVersion = best;
|
|
127
|
-
}
|
|
128
|
-
} else if (extId === EXT_SIGNATURE_ALGORITHMS) {
|
|
129
|
-
if (extData.length >= 2) {
|
|
130
|
-
const algLen = extData.readUInt16BE(0);
|
|
131
|
-
for (let i = 2; i + 1 < extData.length && i + 1 < algLen + 2; i += 2) {
|
|
132
|
-
const alg = extData.readUInt16BE(i);
|
|
133
|
-
if (!isGrease(alg)) sigAlgorithms.push(alg);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
} else if (extId === EXT_ALPN) {
|
|
137
|
-
if (extData.length >= 2) {
|
|
138
|
-
const listLen = extData.readUInt16BE(0);
|
|
139
|
-
const listEnd = Math.min(extData.length, 2 + listLen);
|
|
140
|
-
let alpnOff = 2;
|
|
141
|
-
while (alpnOff < listEnd) {
|
|
142
|
-
const protoLen = extData.readUInt8(alpnOff);
|
|
143
|
-
alpnOff += 1;
|
|
144
|
-
if (alpnOff + protoLen > listEnd) break;
|
|
145
|
-
alpnProtocols.push(extData.subarray(alpnOff, alpnOff + protoLen));
|
|
146
|
-
alpnOff += protoLen;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
}
|
|
23
|
+
try {
|
|
24
|
+
return nativeCalculateJa4(data);
|
|
25
|
+
} catch (err) {
|
|
26
|
+
throw new Ja4ParseError(err instanceof Error ? err.message : String(err));
|
|
150
27
|
}
|
|
151
|
-
const versionStr = tlsVersionStr(clientVersion);
|
|
152
|
-
const sni = extIds.includes(EXT_SERVER_NAME) ? "d" : "i";
|
|
153
|
-
const firstAlpn = alpnProtocols[0];
|
|
154
|
-
const [alpnFirst, alpnLast] = firstAlpn ? alpnFirstLast(firstAlpn) : ["0", "0"];
|
|
155
|
-
const nonGreaseCiphers = cipherSuites.filter((c) => !isGrease(c));
|
|
156
|
-
const nonGreaseExts = extIds.filter((e) => !isGrease(e));
|
|
157
|
-
const firstChunk = `t${versionStr}${sni}${Math.min(99, nonGreaseCiphers.length).toString().padStart(2, "0")}${Math.min(99, nonGreaseExts.length).toString().padStart(2, "0")}${alpnFirst}${alpnLast}`;
|
|
158
|
-
const cipherHash = hash12(formatIdList([...nonGreaseCiphers].sort((a, b) => a - b)));
|
|
159
|
-
const extStr = formatIdList([...nonGreaseExts.filter((e) => e !== EXT_SERVER_NAME && e !== EXT_ALPN)].sort((a, b) => a - b));
|
|
160
|
-
const sigAlgStr = formatIdList(sigAlgorithms);
|
|
161
|
-
return `${firstChunk}_${cipherHash}_${hash12(sigAlgStr.length > 0 ? `${extStr}_${sigAlgStr}` : extStr)}`;
|
|
162
28
|
}
|
|
163
29
|
//#endregion
|
|
164
30
|
exports.Ja4ParseError = Ja4ParseError;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prosopo/provider",
|
|
3
|
-
"version": "5.3.
|
|
3
|
+
"version": "5.3.1",
|
|
4
4
|
"author": "PROSOPO LIMITED <info@prosopo.io>",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
"@prosopo/puzzle-assets": "0.1.1",
|
|
38
38
|
"@prosopo/redis-client": "1.0.32",
|
|
39
39
|
"@prosopo/locale": "3.3.0",
|
|
40
|
+
"@prosopo/native-ja4": "0.0.2",
|
|
40
41
|
"@prosopo/types": "5.2.1",
|
|
41
42
|
"@prosopo/types-database": "5.1.4",
|
|
42
43
|
"@prosopo/types-env": "2.10.29",
|
|
@@ -55,7 +56,7 @@
|
|
|
55
56
|
"zod": "3.23.8"
|
|
56
57
|
},
|
|
57
58
|
"devDependencies": {
|
|
58
|
-
"@prosopo/config": "3.3.
|
|
59
|
+
"@prosopo/config": "3.3.10",
|
|
59
60
|
"@types/node": "22.5.5",
|
|
60
61
|
"@types/pg": "8.16.0",
|
|
61
62
|
"@types/uuid": "10.0.0",
|