Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions documentation/docs/globals/FetchEvent/FetchEvent.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@ It provides the [`event.respondWith()`](./prototype/respondWith.mdx) method, whi
- : 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.
- `FetchEvent.client.tlsClientHello` _**readonly**_
- : Either `null` or an ArrayBuffer containing the raw bytes sent by the client in the TLS ClientHello message.
- `FetchEvent.client.tlsJA4` _**readonly**_
- : Either `null` or a string representation of the JA4 fingerprint of the TLS ClientHello message.
- `FetchEvent.client.h2Fingerprint` _**readonly**_
- : Either `null` or a string representation of the HTTP/2 fingerprint for HTTP/2 connections. Returns `null` for HTTP/1.1 connections.
- `FetchEvent.client.ohFingerprint` _**readonly**_
- : Either `null` or a string representation of the Original Header fingerprint based on the order and presence of request headers.
- `FetchEvent.client.tlsClientServername` _**readonly**_
- : Either `null` or a string representation of the SNI (Server Name Indication) hostname from the TLS handshake.
- `FetchEvent.server` _**readonly**_
- : Information about the server receiving the request for the Fastly Compute service.
- `FetchEvent.server.address` _**readonly**_
Expand Down
50 changes: 50 additions & 0 deletions integration-tests/js-compute/fixtures/app/src/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,53 @@ routes.set('/client/tlsProtocol', (event) => {
);
}
});

routes.set('/client/tlsJA4', (event) => {
if (isRunningLocally()) {
strictEqual(event.client.tlsJA4, null);
} else {
strictEqual(
typeof event.client.tlsJA4,
'string',
'typeof event.client.tlsJA4',
);
}
});

routes.set('/client/h2Fingerprint', (event) => {
if (isRunningLocally()) {
strictEqual(event.client.h2Fingerprint, null);
} else {
// h2Fingerprint may be null for HTTP/1.1 connections
const fp = event.client.h2Fingerprint;
strictEqual(
fp === null || typeof fp === 'string',
true,
'event.client.h2Fingerprint is null or string',
);
}
});

routes.set('/client/ohFingerprint', (event) => {
if (isRunningLocally()) {
strictEqual(event.client.ohFingerprint, null);
} else {
strictEqual(
typeof event.client.ohFingerprint,
'string',
'typeof event.client.ohFingerprint',
);
}
});

routes.set('/client/tlsClientServername', (event) => {
if (isRunningLocally()) {
strictEqual(event.client.tlsClientServername, null);
} else {
strictEqual(
typeof event.client.tlsClientServername,
'string',
'typeof event.client.tlsClientServername',
);
}
});
4 changes: 4 additions & 0 deletions integration-tests/js-compute/fixtures/app/tests.json
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,10 @@
"GET /client/tlsClientCertificate": {},
"GET /client/tlsCipherOpensslName": {},
"GET /client/tlsProtocol": {},
"GET /client/tlsJA4": {},
"GET /client/h2Fingerprint": {},
"GET /client/ohFingerprint": {},
"GET /client/tlsClientServername": {},
"GET /config-store": {
"flake": true
},
Expand Down
184 changes: 171 additions & 13 deletions runtime/fastly/builtins/fetch-event.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,22 @@ JSString *ja3(JSObject *obj) {
JS::Value val = JS::GetReservedSlot(obj, static_cast<uint32_t>(ClientInfo::Slots::JA3));
return val.isString() ? val.toString() : nullptr;
}
JSString *ja4(JSObject *obj) {
JS::Value val = JS::GetReservedSlot(obj, static_cast<uint32_t>(ClientInfo::Slots::JA4));
return val.isString() ? val.toString() : nullptr;
}
JSString *h2Fingerprint(JSObject *obj) {
JS::Value val = JS::GetReservedSlot(obj, static_cast<uint32_t>(ClientInfo::Slots::H2Fingerprint));
return val.isString() ? val.toString() : nullptr;
}
JSString *ohFingerprint(JSObject *obj) {
JS::Value val = JS::GetReservedSlot(obj, static_cast<uint32_t>(ClientInfo::Slots::OHFingerprint));
return val.isString() ? val.toString() : nullptr;
}
JSString *servername(JSObject *obj) {
JS::Value val = JS::GetReservedSlot(obj, static_cast<uint32_t>(ClientInfo::Slots::Servername));
return val.isString() ? val.toString() : nullptr;
}
JSObject *clientHello(JSObject *obj) {
JS::Value val = JS::GetReservedSlot(obj, static_cast<uint32_t>(ClientInfo::Slots::ClientHello));
return val.isObject() ? val.toObjectOrNull() : nullptr;
Expand All @@ -61,8 +77,14 @@ JSString *protocol(JSObject *obj) {
return val.isString() ? val.toString() : nullptr;
}

host_api::HttpReq get_request_handle(JSObject *obj) {
JS::Value val = JS::GetReservedSlot(obj, static_cast<uint32_t>(ClientInfo::Slots::RequestHandle));
return host_api::HttpReq{static_cast<uint32_t>(val.toInt32())};
}

static JSString *retrieve_client_address(JSContext *cx, JS::HandleObject self) {
auto res = host_api::HttpReq::downstream_client_ip_addr();
auto req = get_request_handle(self);
auto res = req.downstream_client_ip_addr();
if (auto *err = res.to_err()) {
HANDLE_ERROR(cx, *err);
return nullptr;
Expand Down Expand Up @@ -159,7 +181,8 @@ bool ClientInfo::tls_cipher_openssl_name_get(JSContext *cx, unsigned argc, JS::V

JS::RootedString result(cx, cipher(self));
if (!result) {
auto res = host_api::HttpReq::http_req_downstream_tls_cipher_openssl_name();
auto req = get_request_handle(self);
auto res = req.downstream_tls_cipher_openssl_name();
if (auto *err = res.to_err()) {
HANDLE_ERROR(cx, *err);
return false;
Expand All @@ -185,7 +208,8 @@ bool ClientInfo::tls_ja3_md5_get(JSContext *cx, unsigned argc, JS::Value *vp) {

JS::RootedString result(cx, ja3(self));
if (!result) {
auto res = host_api::HttpReq::http_req_downstream_tls_ja3_md5();
auto req = get_request_handle(self);
auto res = req.downstream_tls_ja3_md5();
if (auto *err = res.to_err()) {
HANDLE_ERROR(cx, *err);
return false;
Expand All @@ -208,12 +232,91 @@ bool ClientInfo::tls_ja3_md5_get(JSContext *cx, unsigned argc, JS::Value *vp) {
return true;
}

bool ClientInfo::tls_ja4_get(JSContext *cx, unsigned argc, JS::Value *vp) {
METHOD_HEADER(0);

JS::RootedString result(cx, ja4(self));
if (!result) {
auto req = get_request_handle(self);
auto res = req.downstream_tls_ja4();
if (auto *err = res.to_err()) {
HANDLE_ERROR(cx, *err);
return false;
}

if (!res.unwrap().has_value()) {
args.rval().setNull();
return true;
}

auto ja4_str = std::move(res.unwrap().value());
result.set(JS_NewStringCopyN(cx, ja4_str.ptr.get(), ja4_str.len));
JS::SetReservedSlot(self, static_cast<uint32_t>(ClientInfo::Slots::JA4),
JS::StringValue(result));
}
args.rval().setString(result);
return true;
}

bool ClientInfo::h2_fingerprint_get(JSContext *cx, unsigned argc, JS::Value *vp) {
METHOD_HEADER(0);

JS::RootedString result(cx, h2Fingerprint(self));
if (!result) {
auto req = get_request_handle(self);
auto res = req.downstream_client_h2_fingerprint();
if (auto *err = res.to_err()) {
HANDLE_ERROR(cx, *err);
return false;
}

if (!res.unwrap().has_value()) {
args.rval().setNull();
return true;
}

auto h2fp_str = std::move(res.unwrap().value());
result.set(JS_NewStringCopyN(cx, h2fp_str.ptr.get(), h2fp_str.len));
JS::SetReservedSlot(self, static_cast<uint32_t>(ClientInfo::Slots::H2Fingerprint),
JS::StringValue(result));
}
args.rval().setString(result);
return true;
}

bool ClientInfo::oh_fingerprint_get(JSContext *cx, unsigned argc, JS::Value *vp) {
METHOD_HEADER(0);

JS::RootedString result(cx, ohFingerprint(self));
if (!result) {
auto req = get_request_handle(self);
auto res = req.downstream_client_oh_fingerprint();
if (auto *err = res.to_err()) {
HANDLE_ERROR(cx, *err);
return false;
}

if (!res.unwrap().has_value()) {
args.rval().setNull();
return true;
}

auto ohfp_str = std::move(res.unwrap().value());
result.set(JS_NewStringCopyN(cx, ohfp_str.ptr.get(), ohfp_str.len));
JS::SetReservedSlot(self, static_cast<uint32_t>(ClientInfo::Slots::OHFingerprint),
JS::StringValue(result));
}
args.rval().setString(result);
return true;
}

bool ClientInfo::tls_client_hello_get(JSContext *cx, unsigned argc, JS::Value *vp) {
METHOD_HEADER(0);

JS::RootedObject buffer(cx, clientHello(self));
if (!buffer) {
auto res = host_api::HttpReq::http_req_downstream_tls_client_hello();
auto req = get_request_handle(self);
auto res = req.downstream_tls_client_hello();
if (auto *err = res.to_err()) {
HANDLE_ERROR(cx, *err);
return false;
Expand Down Expand Up @@ -248,7 +351,8 @@ bool ClientInfo::tls_client_certificate_get(JSContext *cx, unsigned argc, JS::Va

JS::RootedObject buffer(cx, clientCert(self));
if (!buffer) {
auto res = host_api::HttpReq::http_req_downstream_tls_raw_client_certificate();
auto req = get_request_handle(self);
auto res = req.downstream_tls_raw_client_certificate();
if (auto *err = res.to_err()) {
HANDLE_ERROR(cx, *err);
return false;
Expand Down Expand Up @@ -284,7 +388,8 @@ bool ClientInfo::tls_protocol_get(JSContext *cx, unsigned argc, JS::Value *vp) {

JS::RootedString result(cx, protocol(self));
if (!result) {
auto res = host_api::HttpReq::http_req_downstream_tls_protocol();
auto req = get_request_handle(self);
auto res = req.downstream_tls_protocol();
if (auto *err = res.to_err()) {
HANDLE_ERROR(cx, *err);
return false;
Expand All @@ -305,6 +410,33 @@ bool ClientInfo::tls_protocol_get(JSContext *cx, unsigned argc, JS::Value *vp) {
return true;
}

bool ClientInfo::tls_client_servername_get(JSContext *cx, unsigned argc, JS::Value *vp) {
METHOD_HEADER(0);

JS::RootedString result(cx, servername(self));
if (!result) {
auto req = get_request_handle(self);
auto res = req.downstream_tls_client_servername();
if (auto *err = res.to_err()) {
HANDLE_ERROR(cx, *err);
return false;
}

if (!res.unwrap().has_value()) {
args.rval().setNull();
return true;
}

auto sni = std::move(res.unwrap().value());
result.set(JS_NewStringCopyN(cx, sni.ptr.get(), sni.len));
JS::SetReservedSlot(self, static_cast<uint32_t>(ClientInfo::Slots::Servername),
JS::StringValue(result));
}

args.rval().setString(result);
return true;
}

const JSFunctionSpec ClientInfo::static_methods[] = {
JS_FS_END,
};
Expand All @@ -322,14 +454,23 @@ const JSPropertySpec ClientInfo::properties[] = {
JS_PSG("geo", geo_get, JSPROP_ENUMERATE),
JS_PSG("tlsCipherOpensslName", tls_cipher_openssl_name_get, JSPROP_ENUMERATE),
JS_PSG("tlsProtocol", tls_protocol_get, JSPROP_ENUMERATE),
JS_PSG("tlsClientServername", tls_client_servername_get, JSPROP_ENUMERATE),
JS_PSG("tlsJA3MD5", tls_ja3_md5_get, JSPROP_ENUMERATE),
JS_PSG("tlsJA4", tls_ja4_get, JSPROP_ENUMERATE),
JS_PSG("h2Fingerprint", h2_fingerprint_get, JSPROP_ENUMERATE),
JS_PSG("ohFingerprint", oh_fingerprint_get, JSPROP_ENUMERATE),
JS_PSG("tlsClientCertificate", tls_client_certificate_get, JSPROP_ENUMERATE),
JS_PSG("tlsClientHello", tls_client_hello_get, JSPROP_ENUMERATE),
JS_PS_END,
};

JSObject *ClientInfo::create(JSContext *cx) {
return JS_NewObjectWithGivenProto(cx, &class_, proto_obj);
JSObject *ClientInfo::create(JSContext *cx, uint32_t req_handle) {
JS::RootedObject obj(cx, JS_NewObjectWithGivenProto(cx, &class_, proto_obj));
if (!obj)
return nullptr;
JS::SetReservedSlot(obj, static_cast<uint32_t>(Slots::RequestHandle),
JS::Int32Value(req_handle));
return obj;
}

namespace {
Expand All @@ -339,8 +480,14 @@ JSString *server_address(JSObject *obj) {
return val.isString() ? val.toString() : nullptr;
}

host_api::HttpReq get_server_request_handle(JSObject *obj) {
JS::Value val = JS::GetReservedSlot(obj, static_cast<uint32_t>(ServerInfo::Slots::RequestHandle));
return host_api::HttpReq{static_cast<uint32_t>(val.toInt32())};
}

static JSString *retrieve_server_address(JSContext *cx, JS::HandleObject self) {
auto res = host_api::HttpReq::downstream_server_ip_addr();
auto req = get_server_request_handle(self);
auto res = req.downstream_server_ip_addr();
if (auto *err = res.to_err()) {
HANDLE_ERROR(cx, *err);
return nullptr;
Expand Down Expand Up @@ -389,8 +536,13 @@ const JSPropertySpec ServerInfo::properties[] = {
JS_PS_END,
};

JSObject *ServerInfo::create(JSContext *cx) {
return JS_NewObjectWithGivenProto(cx, &class_, proto_obj);
JSObject *ServerInfo::create(JSContext *cx, uint32_t req_handle) {
JS::RootedObject obj(cx, JS_NewObjectWithGivenProto(cx, &class_, proto_obj));
if (!obj)
return nullptr;
JS::SetReservedSlot(obj, static_cast<uint32_t>(Slots::RequestHandle),
JS::Int32Value(req_handle));
return obj;
}

namespace {
Expand Down Expand Up @@ -452,7 +604,10 @@ bool FetchEvent::client_get(JSContext *cx, unsigned argc, JS::Value *vp) {
JS::GetReservedSlot(self, static_cast<uint32_t>(Slots::ClientInfo)));

if (clientInfo.isUndefined()) {
JS::RootedObject obj(cx, ClientInfo::create(cx));
JS::RootedObject request(
cx, &JS::GetReservedSlot(self, static_cast<uint32_t>(Slots::Request)).toObject());
uint32_t req_handle = Request::request_handle(request).handle;
JS::RootedObject obj(cx, ClientInfo::create(cx, req_handle));
if (!obj)
return false;
clientInfo.setObject(*obj);
Expand All @@ -470,7 +625,10 @@ bool FetchEvent::server_get(JSContext *cx, unsigned argc, JS::Value *vp) {
JS::GetReservedSlot(self, static_cast<uint32_t>(Slots::ServerInfo)));

if (serverInfo.isUndefined()) {
JS::RootedObject obj(cx, ServerInfo::create(cx));
JS::RootedObject request(
cx, &JS::GetReservedSlot(self, static_cast<uint32_t>(Slots::Request)).toObject());
uint32_t req_handle = Request::request_handle(request).handle;
JS::RootedObject obj(cx, ServerInfo::create(cx, req_handle));
if (!obj) {
return false;
}
Expand Down
Loading
Loading