Skip to content

Commit 78bdec5

Browse files
authoredJan 9, 2025··
Inject CF-Connecting-IP from workerd clientIp (#7702)
* Inject CF-Connecting-IP from workerd clientIp * Create red-lamps-obey.md * Handle ipv6 * Skip tests on windows * More tests for windows * Add more comments * skip on windows
1 parent 65a3e35 commit 78bdec5

File tree

5 files changed

+118
-22
lines changed

5 files changed

+118
-22
lines changed
 

‎.changeset/red-lamps-obey.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"miniflare": minor
3+
---
4+
5+
Support the `CF-Connecting-IP` header, which will be available in your Worker to determine the IP address of the client that initiated a request.

‎packages/miniflare/src/http/server.ts

+2-9
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,9 @@ import fs from "fs/promises";
22
import { z } from "zod";
33
import { CORE_PLUGIN } from "../plugins";
44
import { HttpOptions, Socket_Https } from "../runtime";
5-
import { Awaitable, CoreHeaders } from "../workers";
5+
import { Awaitable } from "../workers";
66
import { CERT, KEY } from "./cert";
77

8-
export const ENTRY_SOCKET_HTTP_OPTIONS: HttpOptions = {
9-
// Even though we inject a `cf` object in the entry worker, allow it to
10-
// be customised via `dispatchFetch`
11-
cfBlobHeader: CoreHeaders.CF_BLOB,
12-
};
13-
148
export async function getEntrySocketHttpOptions(
159
coreOpts: z.infer<typeof CORE_PLUGIN.sharedOptions>
1610
): Promise<{ http: HttpOptions } | { https: Socket_Https }> {
@@ -34,7 +28,6 @@ export async function getEntrySocketHttpOptions(
3428
if (privateKey && certificateChain) {
3529
return {
3630
https: {
37-
options: ENTRY_SOCKET_HTTP_OPTIONS,
3831
tlsOptions: {
3932
keypair: {
4033
privateKey: privateKey,
@@ -44,7 +37,7 @@ export async function getEntrySocketHttpOptions(
4437
},
4538
};
4639
} else {
47-
return { http: ENTRY_SOCKET_HTTP_OPTIONS };
40+
return { http: {} };
4841
}
4942
}
5043

‎packages/miniflare/src/index.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ import {
2828
coupleWebSocket,
2929
DispatchFetch,
3030
DispatchFetchDispatcher,
31-
ENTRY_SOCKET_HTTP_OPTIONS,
3231
fetch,
3332
getAccessibleHosts,
3433
getEntrySocketHttpOptions,
@@ -1117,7 +1116,7 @@ export class Miniflare {
11171116
sockets.push({
11181117
name: SOCKET_ENTRY_LOCAL,
11191118
service: { name: SERVICE_ENTRY },
1120-
http: ENTRY_SOCKET_HTTP_OPTIONS,
1119+
http: {},
11211120
address: "127.0.0.1:0",
11221121
});
11231122
}

‎packages/miniflare/src/workers/core/entry.worker.ts

+31-11
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ const encoder = new TextEncoder();
3535

3636
function getUserRequest(
3737
request: Request<unknown, IncomingRequestCfProperties>,
38-
env: Env
38+
env: Env,
39+
clientIp: string | undefined
3940
) {
4041
// The ORIGINAL_URL header is added to outbound requests from Miniflare,
4142
// triggered either by calling Miniflare.#dispatchFetch(request),
@@ -89,15 +90,6 @@ function getUserRequest(
8990
// special handling to allow this if a `Request` instance is passed.
9091
// See https://github.com/cloudflare/workerd/issues/1122 for more details.
9192
request = new Request(url, request);
92-
if (request.cf === undefined) {
93-
const cf: IncomingRequestCfProperties = {
94-
...env[CoreBindings.JSON_CF_BLOB],
95-
// Defaulting to empty string to preserve undefined `Accept-Encoding`
96-
// through Wrangler's proxy worker.
97-
clientAcceptEncoding: request.headers.get("Accept-Encoding") ?? "",
98-
};
99-
request = new Request(request, { cf });
100-
}
10193

10294
// `Accept-Encoding` is always set to "br, gzip" in Workers:
10395
// https://developers.cloudflare.com/fundamentals/reference/http-request-headers/#accept-encoding
@@ -107,6 +99,18 @@ function getUserRequest(
10799
request.headers.set("Host", url.host);
108100
}
109101

102+
if (clientIp && !request.headers.get("CF-Connecting-IP")) {
103+
const ipv4Regex = /(?<ip>.*?):\d+/;
104+
const ipv6Regex = /\[(?<ip>.*?)\]:\d+/;
105+
const ip =
106+
clientIp.match(ipv6Regex)?.groups?.ip ??
107+
clientIp.match(ipv4Regex)?.groups?.ip;
108+
109+
if (ip) {
110+
request.headers.set("CF-Connecting-IP", ip);
111+
}
112+
}
113+
110114
request.headers.delete(CoreHeaders.PROXY_SHARED_SECRET);
111115
request.headers.delete(CoreHeaders.ORIGINAL_URL);
112116
request.headers.delete(CoreHeaders.DISABLE_PRETTY_ERROR);
@@ -343,6 +347,22 @@ export default <ExportedHandler<Env>>{
343347
async fetch(request, env, ctx) {
344348
const startTime = Date.now();
345349

350+
const clientIp = request.cf?.clientIp as string;
351+
352+
// Parse this manually (rather than using the `cfBlobHeader` config property in workerd to parse it into request.cf)
353+
// This is because we want to have access to the clientIp, which workerd puts in request.cf if no cfBlobHeader is provided
354+
const clientCfBlobHeader = request.headers.get(CoreHeaders.CF_BLOB);
355+
356+
const cf: IncomingRequestCfProperties = clientCfBlobHeader
357+
? JSON.parse(clientCfBlobHeader)
358+
: {
359+
...env[CoreBindings.JSON_CF_BLOB],
360+
// Defaulting to empty string to preserve undefined `Accept-Encoding`
361+
// through Wrangler's proxy worker.
362+
clientAcceptEncoding: request.headers.get("Accept-Encoding") ?? "",
363+
};
364+
request = new Request(request, { cf });
365+
346366
// The proxy client will always specify an operation
347367
const isProxy = request.headers.get(CoreHeaders.OP) !== null;
348368
if (isProxy) return handleProxy(request, env);
@@ -356,7 +376,7 @@ export default <ExportedHandler<Env>>{
356376
const clientAcceptEncoding = request.headers.get("Accept-Encoding");
357377

358378
try {
359-
request = getUserRequest(request, env);
379+
request = getUserRequest(request, env, clientIp);
360380
} catch (e) {
361381
if (e instanceof HttpError) {
362382
return e.toResponse();

‎packages/miniflare/test/index.spec.ts

+79
Original file line numberDiff line numberDiff line change
@@ -2610,6 +2610,85 @@ test("Miniflare: getCf() returns a user provided cf object", async (t) => {
26102610
t.deepEqual(cf, { myFakeField: "test" });
26112611
});
26122612

2613+
test("Miniflare: dispatchFetch() can override cf", async (t) => {
2614+
const mf = new Miniflare({
2615+
script:
2616+
"export default { fetch(request) { return Response.json(request.cf) } }",
2617+
modules: true,
2618+
cf: {
2619+
myFakeField: "test",
2620+
},
2621+
});
2622+
t.teardown(() => mf.dispose());
2623+
2624+
const cf = await mf.dispatchFetch("http://example.com/", {
2625+
cf: { myFakeField: "test2" },
2626+
});
2627+
const cfJson = (await cf.json()) as { myFakeField: string };
2628+
t.deepEqual(cfJson.myFakeField, "test2");
2629+
});
2630+
2631+
test("Miniflare: CF-Connecting-IP is injected", async (t) => {
2632+
const mf = new Miniflare({
2633+
script:
2634+
"export default { fetch(request) { return new Response(request.headers.get('CF-Connecting-IP')) } }",
2635+
modules: true,
2636+
cf: {
2637+
myFakeField: "test",
2638+
},
2639+
});
2640+
t.teardown(() => mf.dispose());
2641+
2642+
const ip = await mf.dispatchFetch("http://example.com/");
2643+
// Tracked in https://github.com/cloudflare/workerd/issues/3310
2644+
if (!isWindows) {
2645+
t.deepEqual(await ip.text(), "127.0.0.1");
2646+
} else {
2647+
t.deepEqual(await ip.text(), "");
2648+
}
2649+
});
2650+
2651+
test("Miniflare: CF-Connecting-IP is injected (ipv6)", async (t) => {
2652+
const mf = new Miniflare({
2653+
script:
2654+
"export default { fetch(request) { return new Response(request.headers.get('CF-Connecting-IP')) } }",
2655+
modules: true,
2656+
cf: {
2657+
myFakeField: "test",
2658+
},
2659+
host: "::1",
2660+
});
2661+
t.teardown(() => mf.dispose());
2662+
2663+
const ip = await mf.dispatchFetch("http://example.com/");
2664+
2665+
// Tracked in https://github.com/cloudflare/workerd/issues/3310
2666+
if (!isWindows) {
2667+
t.deepEqual(await ip.text(), "::1");
2668+
} else {
2669+
t.deepEqual(await ip.text(), "");
2670+
}
2671+
});
2672+
2673+
test("Miniflare: CF-Connecting-IP is preserved when present", async (t) => {
2674+
const mf = new Miniflare({
2675+
script:
2676+
"export default { fetch(request) { return new Response(request.headers.get('CF-Connecting-IP')) } }",
2677+
modules: true,
2678+
cf: {
2679+
myFakeField: "test",
2680+
},
2681+
});
2682+
t.teardown(() => mf.dispose());
2683+
2684+
const ip = await mf.dispatchFetch("http://example.com/", {
2685+
headers: {
2686+
"CF-Connecting-IP": "128.0.0.1",
2687+
},
2688+
});
2689+
t.deepEqual(await ip.text(), "128.0.0.1");
2690+
});
2691+
26132692
test("Miniflare: can use module fallback service", async (t) => {
26142693
const modulesRoot = "/";
26152694
const modules: Record<string, Omit<Worker_Module, "name">> = {

0 commit comments

Comments
 (0)
Please sign in to comment.