Skip to content

Commit 9390e8c

Browse files
feat: Add client fingerprint properties (tlsJA4, h2Fingerprint, ohFingerprint) (#1248)
Co-authored-by: Sy Brand <[email protected]>
1 parent 1c6cb73 commit 9390e8c

File tree

9 files changed

+245
-0
lines changed

9 files changed

+245
-0
lines changed

documentation/docs/globals/FetchEvent/FetchEvent.mdx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ It provides the [`event.respondWith()`](./prototype/respondWith.mdx) method, whi
3131
- : Either `null` or an ArrayBuffer containing the raw client certificate in the mutual TLS handshake message. It is in PEM format. Returns an empty ArrayBuffer if this is not mTLS or available.
3232
- `FetchEvent.client.tlsClientHello` _**readonly**_
3333
- : Either `null` or an ArrayBuffer containing the raw bytes sent by the client in the TLS ClientHello message.
34+
- `FetchEvent.client.tlsJA4` _**readonly**_
35+
- : Either `null` or a string representation of the JA4 fingerprint of the TLS ClientHello message.
36+
- `FetchEvent.client.h2Fingerprint` _**readonly**_
37+
- : Either `null` or a string representation of the HTTP/2 fingerprint for HTTP/2 connections. Returns `null` for HTTP/1.1 connections.
38+
- `FetchEvent.client.ohFingerprint` _**readonly**_
39+
- : Either `null` or a string representation of the Original Header fingerprint based on the order and presence of request headers.
3440
- `FetchEvent.server` _**readonly**_
3541
- : Information about the server receiving the request for the Fastly Compute service.
3642
- `FetchEvent.server.address` _**readonly**_

integration-tests/js-compute/fixtures/app/src/client.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,41 @@ routes.set('/client/tlsProtocol', (event) => {
7474
);
7575
}
7676
});
77+
78+
routes.set('/client/tlsJA4', (event) => {
79+
if (isRunningLocally()) {
80+
strictEqual(event.client.tlsJA4, null);
81+
} else {
82+
strictEqual(
83+
typeof event.client.tlsJA4,
84+
'string',
85+
'typeof event.client.tlsJA4',
86+
);
87+
}
88+
});
89+
90+
routes.set('/client/h2Fingerprint', (event) => {
91+
if (isRunningLocally()) {
92+
strictEqual(event.client.h2Fingerprint, null);
93+
} else {
94+
// h2Fingerprint may be null for HTTP/1.1 connections
95+
const fp = event.client.h2Fingerprint;
96+
strictEqual(
97+
fp === null || typeof fp === 'string',
98+
true,
99+
'event.client.h2Fingerprint is null or string',
100+
);
101+
}
102+
});
103+
104+
routes.set('/client/ohFingerprint', (event) => {
105+
if (isRunningLocally()) {
106+
strictEqual(event.client.ohFingerprint, null);
107+
} else {
108+
strictEqual(
109+
typeof event.client.ohFingerprint,
110+
'string',
111+
'typeof event.client.ohFingerprint',
112+
);
113+
}
114+
});

integration-tests/js-compute/fixtures/app/tests.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,9 @@
449449
"GET /client/tlsClientCertificate": {},
450450
"GET /client/tlsCipherOpensslName": {},
451451
"GET /client/tlsProtocol": {},
452+
"GET /client/tlsJA4": {},
453+
"GET /client/h2Fingerprint": {},
454+
"GET /client/ohFingerprint": {},
452455
"GET /config-store": {
453456
"flake": true
454457
},

runtime/fastly/builtins/fetch-event.cpp

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,18 @@ JSString *ja3(JSObject *obj) {
4848
JS::Value val = JS::GetReservedSlot(obj, static_cast<uint32_t>(ClientInfo::Slots::JA3));
4949
return val.isString() ? val.toString() : nullptr;
5050
}
51+
JSString *ja4(JSObject *obj) {
52+
JS::Value val = JS::GetReservedSlot(obj, static_cast<uint32_t>(ClientInfo::Slots::JA4));
53+
return val.isString() ? val.toString() : nullptr;
54+
}
55+
JSString *h2Fingerprint(JSObject *obj) {
56+
JS::Value val = JS::GetReservedSlot(obj, static_cast<uint32_t>(ClientInfo::Slots::H2Fingerprint));
57+
return val.isString() ? val.toString() : nullptr;
58+
}
59+
JSString *ohFingerprint(JSObject *obj) {
60+
JS::Value val = JS::GetReservedSlot(obj, static_cast<uint32_t>(ClientInfo::Slots::OHFingerprint));
61+
return val.isString() ? val.toString() : nullptr;
62+
}
5163
JSObject *clientHello(JSObject *obj) {
5264
JS::Value val = JS::GetReservedSlot(obj, static_cast<uint32_t>(ClientInfo::Slots::ClientHello));
5365
return val.isObject() ? val.toObjectOrNull() : nullptr;
@@ -208,6 +220,81 @@ bool ClientInfo::tls_ja3_md5_get(JSContext *cx, unsigned argc, JS::Value *vp) {
208220
return true;
209221
}
210222

223+
bool ClientInfo::tls_ja4_get(JSContext *cx, unsigned argc, JS::Value *vp) {
224+
METHOD_HEADER(0);
225+
226+
JS::RootedString result(cx, ja4(self));
227+
if (!result) {
228+
auto res = host_api::HttpReq::http_req_downstream_tls_ja4();
229+
if (auto *err = res.to_err()) {
230+
HANDLE_ERROR(cx, *err);
231+
return false;
232+
}
233+
234+
if (!res.unwrap().has_value()) {
235+
args.rval().setNull();
236+
return true;
237+
}
238+
239+
auto ja4_str = std::move(res.unwrap().value());
240+
result.set(JS_NewStringCopyN(cx, ja4_str.ptr.get(), ja4_str.len));
241+
JS::SetReservedSlot(self, static_cast<uint32_t>(ClientInfo::Slots::JA4),
242+
JS::StringValue(result));
243+
}
244+
args.rval().setString(result);
245+
return true;
246+
}
247+
248+
bool ClientInfo::h2_fingerprint_get(JSContext *cx, unsigned argc, JS::Value *vp) {
249+
METHOD_HEADER(0);
250+
251+
JS::RootedString result(cx, h2Fingerprint(self));
252+
if (!result) {
253+
auto res = host_api::HttpReq::http_req_downstream_client_h2_fingerprint();
254+
if (auto *err = res.to_err()) {
255+
HANDLE_ERROR(cx, *err);
256+
return false;
257+
}
258+
259+
if (!res.unwrap().has_value()) {
260+
args.rval().setNull();
261+
return true;
262+
}
263+
264+
auto h2fp_str = std::move(res.unwrap().value());
265+
result.set(JS_NewStringCopyN(cx, h2fp_str.ptr.get(), h2fp_str.len));
266+
JS::SetReservedSlot(self, static_cast<uint32_t>(ClientInfo::Slots::H2Fingerprint),
267+
JS::StringValue(result));
268+
}
269+
args.rval().setString(result);
270+
return true;
271+
}
272+
273+
bool ClientInfo::oh_fingerprint_get(JSContext *cx, unsigned argc, JS::Value *vp) {
274+
METHOD_HEADER(0);
275+
276+
JS::RootedString result(cx, ohFingerprint(self));
277+
if (!result) {
278+
auto res = host_api::HttpReq::http_req_downstream_client_oh_fingerprint();
279+
if (auto *err = res.to_err()) {
280+
HANDLE_ERROR(cx, *err);
281+
return false;
282+
}
283+
284+
if (!res.unwrap().has_value()) {
285+
args.rval().setNull();
286+
return true;
287+
}
288+
289+
auto ohfp_str = std::move(res.unwrap().value());
290+
result.set(JS_NewStringCopyN(cx, ohfp_str.ptr.get(), ohfp_str.len));
291+
JS::SetReservedSlot(self, static_cast<uint32_t>(ClientInfo::Slots::OHFingerprint),
292+
JS::StringValue(result));
293+
}
294+
args.rval().setString(result);
295+
return true;
296+
}
297+
211298
bool ClientInfo::tls_client_hello_get(JSContext *cx, unsigned argc, JS::Value *vp) {
212299
METHOD_HEADER(0);
213300

@@ -323,6 +410,9 @@ const JSPropertySpec ClientInfo::properties[] = {
323410
JS_PSG("tlsCipherOpensslName", tls_cipher_openssl_name_get, JSPROP_ENUMERATE),
324411
JS_PSG("tlsProtocol", tls_protocol_get, JSPROP_ENUMERATE),
325412
JS_PSG("tlsJA3MD5", tls_ja3_md5_get, JSPROP_ENUMERATE),
413+
JS_PSG("tlsJA4", tls_ja4_get, JSPROP_ENUMERATE),
414+
JS_PSG("h2Fingerprint", h2_fingerprint_get, JSPROP_ENUMERATE),
415+
JS_PSG("ohFingerprint", oh_fingerprint_get, JSPROP_ENUMERATE),
326416
JS_PSG("tlsClientCertificate", tls_client_certificate_get, JSPROP_ENUMERATE),
327417
JS_PSG("tlsClientHello", tls_client_hello_get, JSPROP_ENUMERATE),
328418
JS_PS_END,

runtime/fastly/builtins/fetch-event.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ class ClientInfo final : public builtins::BuiltinNoConstructor<ClientInfo> {
1515
static bool tls_protocol_get(JSContext *cx, unsigned argc, JS::Value *vp);
1616
static bool tls_client_hello_get(JSContext *cx, unsigned argc, JS::Value *vp);
1717
static bool tls_ja3_md5_get(JSContext *cx, unsigned argc, JS::Value *vp);
18+
static bool tls_ja4_get(JSContext *cx, unsigned argc, JS::Value *vp);
19+
static bool h2_fingerprint_get(JSContext *cx, unsigned argc, JS::Value *vp);
20+
static bool oh_fingerprint_get(JSContext *cx, unsigned argc, JS::Value *vp);
1821
static bool tls_client_certificate_get(JSContext *cx, unsigned argc, JS::Value *vp);
1922

2023
public:
@@ -27,6 +30,9 @@ class ClientInfo final : public builtins::BuiltinNoConstructor<ClientInfo> {
2730
Protocol,
2831
ClientHello,
2932
JA3,
33+
JA4,
34+
H2Fingerprint,
35+
OHFingerprint,
3036
ClientCert,
3137
Count,
3238
};

runtime/fastly/host-api/fastly.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -532,6 +532,15 @@ int req_downstream_tls_raw_client_certificate(uint8_t *ret, size_t ret_len, size
532532
WASM_IMPORT("fastly_http_req", "downstream_tls_ja3_md5")
533533
int req_downstream_tls_ja3_md5(uint8_t *ret, size_t *nwritten);
534534

535+
WASM_IMPORT("fastly_http_req", "downstream_tls_ja4")
536+
int req_downstream_tls_ja4(uint8_t *ret, size_t ret_len, size_t *nwritten);
537+
538+
WASM_IMPORT("fastly_http_req", "downstream_client_h2_fingerprint")
539+
int req_downstream_client_h2_fingerprint(uint8_t *ret, size_t ret_len, size_t *nwritten);
540+
541+
WASM_IMPORT("fastly_http_req", "downstream_client_oh_fingerprint")
542+
int req_downstream_client_oh_fingerprint(uint8_t *ret, size_t ret_len, size_t *nwritten);
543+
535544
WASM_IMPORT("fastly_http_req", "new")
536545
int req_new(uint32_t *req_handle_out);
537546

runtime/fastly/host-api/host_api.cpp

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1807,6 +1807,90 @@ Result<std::optional<HostBytes>> HttpReq::http_req_downstream_tls_ja3_md5() {
18071807
return res;
18081808
}
18091809

1810+
// http-req-downstream-tls-ja4: func() -> result<option<string>, error>
1811+
Result<std::optional<HostString>> HttpReq::http_req_downstream_tls_ja4() {
1812+
TRACE_CALL()
1813+
Result<std::optional<HostString>> res;
1814+
1815+
fastly::fastly_host_error err;
1816+
fastly::fastly_world_string ret;
1817+
auto default_size = 128;
1818+
ret.ptr = static_cast<uint8_t *>(cabi_malloc(default_size, 4));
1819+
auto status = fastly::req_downstream_tls_ja4(ret.ptr, default_size, &ret.len);
1820+
if (status == FASTLY_HOST_ERROR_BUFFER_LEN) {
1821+
ret.ptr = static_cast<uint8_t *>(cabi_realloc(ret.ptr, default_size, 4, ret.len));
1822+
status = fastly::req_downstream_tls_ja4(ret.ptr, ret.len, &ret.len);
1823+
}
1824+
if (!convert_result(status, &err)) {
1825+
cabi_free(ret.ptr);
1826+
if (error_is_optional_none(err)) {
1827+
res.emplace(std::nullopt);
1828+
} else {
1829+
res.emplace_err(err);
1830+
}
1831+
} else {
1832+
res.emplace(make_host_string(ret));
1833+
}
1834+
1835+
return res;
1836+
}
1837+
1838+
// http-req-downstream-client-h2-fingerprint: func() -> result<option<string>, error>
1839+
Result<std::optional<HostString>> HttpReq::http_req_downstream_client_h2_fingerprint() {
1840+
TRACE_CALL()
1841+
Result<std::optional<HostString>> res;
1842+
1843+
fastly::fastly_host_error err;
1844+
fastly::fastly_world_string ret;
1845+
auto default_size = 128;
1846+
ret.ptr = static_cast<uint8_t *>(cabi_malloc(default_size, 4));
1847+
auto status = fastly::req_downstream_client_h2_fingerprint(ret.ptr, default_size, &ret.len);
1848+
if (status == FASTLY_HOST_ERROR_BUFFER_LEN) {
1849+
ret.ptr = static_cast<uint8_t *>(cabi_realloc(ret.ptr, default_size, 4, ret.len));
1850+
status = fastly::req_downstream_client_h2_fingerprint(ret.ptr, ret.len, &ret.len);
1851+
}
1852+
if (!convert_result(status, &err)) {
1853+
cabi_free(ret.ptr);
1854+
if (error_is_optional_none(err)) {
1855+
res.emplace(std::nullopt);
1856+
} else {
1857+
res.emplace_err(err);
1858+
}
1859+
} else {
1860+
res.emplace(make_host_string(ret));
1861+
}
1862+
1863+
return res;
1864+
}
1865+
1866+
// http-req-downstream-client-oh-fingerprint: func() -> result<option<string>, error>
1867+
Result<std::optional<HostString>> HttpReq::http_req_downstream_client_oh_fingerprint() {
1868+
TRACE_CALL()
1869+
Result<std::optional<HostString>> res;
1870+
1871+
fastly::fastly_host_error err;
1872+
fastly::fastly_world_string ret;
1873+
auto default_size = 128;
1874+
ret.ptr = static_cast<uint8_t *>(cabi_malloc(default_size, 4));
1875+
auto status = fastly::req_downstream_client_oh_fingerprint(ret.ptr, default_size, &ret.len);
1876+
if (status == FASTLY_HOST_ERROR_BUFFER_LEN) {
1877+
ret.ptr = static_cast<uint8_t *>(cabi_realloc(ret.ptr, default_size, 4, ret.len));
1878+
status = fastly::req_downstream_client_oh_fingerprint(ret.ptr, ret.len, &ret.len);
1879+
}
1880+
if (!convert_result(status, &err)) {
1881+
cabi_free(ret.ptr);
1882+
if (error_is_optional_none(err)) {
1883+
res.emplace(std::nullopt);
1884+
} else {
1885+
res.emplace_err(err);
1886+
}
1887+
} else {
1888+
res.emplace(make_host_string(ret));
1889+
}
1890+
1891+
return res;
1892+
}
1893+
18101894
bool HttpReq::is_valid() const { return this->handle != HttpReq::invalid; }
18111895

18121896
Result<HttpVersion> HttpReq::get_version() const {

runtime/fastly/host-api/host_api_fastly.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -573,6 +573,12 @@ class HttpReq final : public HttpBase {
573573

574574
static Result<std::optional<HostBytes>> http_req_downstream_tls_ja3_md5();
575575

576+
static Result<std::optional<HostString>> http_req_downstream_tls_ja4();
577+
578+
static Result<std::optional<HostString>> http_req_downstream_client_h2_fingerprint();
579+
580+
static Result<std::optional<HostString>> http_req_downstream_client_oh_fingerprint();
581+
576582
Result<Void> auto_decompress_gzip();
577583

578584
/// Send this request synchronously, and wait for the response.

types/globals.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,9 @@ declare interface ClientInfo {
436436
readonly address: string;
437437
readonly geo: import('fastly:geolocation').Geolocation | null;
438438
readonly tlsJA3MD5: string | null;
439+
readonly tlsJA4: string | null;
440+
readonly h2Fingerprint: string | null;
441+
readonly ohFingerprint: string | null;
439442
readonly tlsCipherOpensslName: string | null;
440443
readonly tlsProtocol: string | null;
441444
readonly tlsClientCertificate: ArrayBuffer | null;

0 commit comments

Comments
 (0)