The forwarding Worker
A few lines. The full context.
Gateway invokes GatewayMiddleware.handle(request, context, next) before the request reaches its destination.
src/forward.jsDeployed source · JavaScript
import { WorkerEntrypoint } from "cloudflare:workers";
import { captureContext } from "./capture.js";
import { CAPTURE_HEADER, DEMO_EMAIL, DEMO_HOST } from "./config.js";
export class GatewayMiddleware extends WorkerEntrypoint {
async handle(request, context, next) {
const headers = new Headers(request.headers);
headers.delete(CAPTURE_HEADER);
const url = new URL(request.url);
if (url.hostname === DEMO_HOST && request.method === "GET" &&
(url.pathname === "/" || url.pathname === "/context")) {
// Gateway supplies the context, including lazy identity and posture.
const snapshot = await captureContext(request, context);
if (snapshot.identity.value?.email === DEMO_EMAIL) {
// Save privately before continuing the original request to the site.
const ticket = await this.env.CONTEXT_VIEWER.capture(snapshot);
headers.set(CAPTURE_HEADER, ticket);
}
}
// Continue to the original destination using Gateway routing and egress.
return next(new Request(request, { headers }));
}
}
export default {
fetch() {
return new Response("Gateway RPC entrypoint only.", { status: 403 });
},
};01 / Receive
Read the original request metadata and resolve identity and device posture.
02 / Save
Save this user's capture privately and attach a signed, single-use reference.
03 / Continue
Call next(request) once. Gateway sends the request to its original destination.
Context capture helper src/capture.js
import { CAPTURE_HEADER, CONTEXT_FIELDS } from "./config.js";
const SECRET_KEY = /(?:authorization|cookie|password|passwd|secret|token|api[-_]?key|credential|assertion|jwt)/i;
const SAFE_HEADER = /^(?:accept|accept-encoding|accept-language|cache-control|content-length|content-type|host|origin|sec-ch-ua(?:-mobile|-platform)?|sec-fetch-(?:dest|mode|site|user)|upgrade-insecure-requests|user-agent|cf-ray|cf-connecting-ip|x-forwarded-proto)$/i;
export function safeUrl(value) {
try {
const url = new URL(value);
url.username = "";
url.password = "";
url.hash = "";
for (const key of [...url.searchParams.keys()]) {
if (SECRET_KEY.test(key) || /^(?:code|state|session|sig|signature)$/i.test(key)) {
url.searchParams.set(key, "[redacted]");
}
}
return url.href;
} catch {
return "[invalid URL]";
}
}
export function safeValue(value, key = "", ancestors = new Set()) {
if (SECRET_KEY.test(key)) return "[redacted]";
if (typeof value === "function" || value === undefined || typeof value === "symbol") return undefined;
if (typeof value === "string") {
return /^https?:\/\//i.test(value) ? safeUrl(value) : value;
}
if (typeof value === "bigint") return String(value);
if (value === null || typeof value !== "object") return value;
if (ancestors.has(value)) return "[circular reference]";
const nextAncestors = new Set(ancestors).add(value);
if (Array.isArray(value)) return value.map((item) => safeValue(item, "", nextAncestors));
return Object.fromEntries(Object.entries(value)
.filter(([, item]) => typeof item !== "function" && item !== undefined)
.map(([name, item]) => [name, safeValue(item, name, nextAncestors)]));
}
async function resolveContext(context, method) {
if (typeof context?.[method] !== "function") return { status: "unavailable" };
try {
const value = await context[method]();
return value === undefined
? { status: "not provided" }
: { status: "available", value: safeValue(value) };
} catch {
return { status: "unavailable" };
}
}
export async function captureContext(request, context) {
const [identity, devicePosture] = await Promise.all([
resolveContext(context, "identity"),
resolveContext(context, "devicePosture"),
]);
const metadata = {};
for (const field of new Set([...Object.keys(context ?? {}), ...CONTEXT_FIELDS])) {
if (field === "identity" || field === "devicePosture") continue;
const value = safeValue(context?.[field], field);
if (value !== undefined) Object.defineProperty(metadata, field, { value, enumerable: true });
}
return {
capturedAt: new Date().toISOString(),
context: metadata,
identity,
devicePosture,
request: {
method: request.method,
url: safeUrl(request.url),
headers: Object.fromEntries([...request.headers]
.filter(([name]) => name !== CAPTURE_HEADER)
.map(([name, value]) => [name, SAFE_HEADER.test(name) ? value : "[not captured]"])),
...(request.cf ? { cf: safeValue(request.cf) } : {}),
},
};
}Demo configuration src/config.js
export const DEMO_HOST = "context.grstnhbr.com";
export const DEMO_EMAIL = "albertjason.g@gmail.com";
export const CAPTURE_HEADER = "x-connect-context-capture";
export const CAPTURE_TTL_MS = 15 * 60 * 1000;
export const CONTEXT_FIELDS = [
"srcIp", "srcPort", "dstIp", "dstPort", "protocol", "detectedProtocol",
"sni", "onrampType", "vnetId", "proxyEndpoint", "internalSrcIp", "accountTag",
"policyId", "privateAppAud", "infraAppAuds", "host", "uri",
"embeddedProtocols", "domainCategories",
];