{ }Cloudflare ConnectProgrammable Gateway

The forwarding Worker

A few lines. The full context.

Gateway invokes GatewayMiddleware.handle(request, context, next) before the request reaches its destination.

View plain source ↗
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",
];