fix: improve getIP header resolution for CF → Vercel setup (#28152)
Co-Authored-By: unknown <> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
unknown <>
Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent
9855176948
commit
c9abc556ba
@@ -17,11 +17,7 @@ async function handler(req: NextRequest) {
|
||||
return NextResponse.json({ message: "email is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
let ip = getIP(req) ?? email.data;
|
||||
const forwardedFor = req.headers.get("x-forwarded-for") as string;
|
||||
if (!ip && forwardedFor) {
|
||||
ip = forwardedFor?.split(",").at(0) ?? email.data;
|
||||
}
|
||||
const ip = getIP(req) ?? email.data;
|
||||
|
||||
await checkRateLimitAndThrowError({
|
||||
rateLimitingType: "core",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import getIP from "@calcom/lib/getIP";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function GET(req: Request) {
|
||||
const requestorIp = getIP(req);
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import getIP, { parseIpFromHeaders } from "./getIP";
|
||||
|
||||
function buildRequest(headers: Record<string, string>): Request {
|
||||
return new Request("https://example.com", { headers });
|
||||
}
|
||||
|
||||
function buildNextApiRequest(headers: Record<string, string | string[]>): NextApiRequest {
|
||||
return { headers } as unknown as NextApiRequest;
|
||||
}
|
||||
|
||||
describe("parseIpFromHeaders", () => {
|
||||
it("returns the first IP from a comma-separated string", () => {
|
||||
expect(parseIpFromHeaders("1.2.3.4, 5.6.7.8")).toBe("1.2.3.4");
|
||||
});
|
||||
|
||||
it("returns the single IP when there is no comma", () => {
|
||||
expect(parseIpFromHeaders("1.2.3.4")).toBe("1.2.3.4");
|
||||
});
|
||||
|
||||
it("returns the first element when given an array", () => {
|
||||
expect(parseIpFromHeaders(["1.2.3.4", "5.6.7.8"])).toBe("1.2.3.4");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getIP", () => {
|
||||
describe("with Web Request", () => {
|
||||
it("returns cf-connecting-ip when present", () => {
|
||||
const req = buildRequest({ "cf-connecting-ip": "1.1.1.1" });
|
||||
expect(getIP(req)).toBe("1.1.1.1");
|
||||
});
|
||||
|
||||
it("prefers cf-connecting-ip over other headers", () => {
|
||||
const req = buildRequest({
|
||||
"cf-connecting-ip": "1.1.1.1",
|
||||
"true-client-ip": "2.2.2.2",
|
||||
"x-forwarded-for": "3.3.3.3",
|
||||
"x-real-ip": "4.4.4.4",
|
||||
});
|
||||
expect(getIP(req)).toBe("1.1.1.1");
|
||||
});
|
||||
|
||||
it("falls back to true-client-ip when cf-connecting-ip is absent", () => {
|
||||
const req = buildRequest({
|
||||
"true-client-ip": "2.2.2.2",
|
||||
"x-forwarded-for": "3.3.3.3",
|
||||
"x-real-ip": "4.4.4.4",
|
||||
});
|
||||
expect(getIP(req)).toBe("2.2.2.2");
|
||||
});
|
||||
|
||||
it("falls back to x-forwarded-for when cf and true-client headers are absent", () => {
|
||||
const req = buildRequest({
|
||||
"x-forwarded-for": "3.3.3.3, 10.0.0.1",
|
||||
"x-real-ip": "4.4.4.4",
|
||||
});
|
||||
expect(getIP(req)).toBe("3.3.3.3");
|
||||
});
|
||||
|
||||
it("falls back to x-real-ip as last resort", () => {
|
||||
const req = buildRequest({ "x-real-ip": "4.4.4.4" });
|
||||
expect(getIP(req)).toBe("4.4.4.4");
|
||||
});
|
||||
|
||||
it("returns 127.0.0.1 when no IP headers are present", () => {
|
||||
const req = buildRequest({});
|
||||
expect(getIP(req)).toBe("127.0.0.1");
|
||||
});
|
||||
|
||||
it("extracts first IP from comma-separated x-forwarded-for", () => {
|
||||
const req = buildRequest({ "x-forwarded-for": "9.9.9.9, 10.0.0.1, 172.16.0.1" });
|
||||
expect(getIP(req)).toBe("9.9.9.9");
|
||||
});
|
||||
});
|
||||
|
||||
describe("with NextApiRequest", () => {
|
||||
it("returns cf-connecting-ip when present", () => {
|
||||
const req = buildNextApiRequest({ "cf-connecting-ip": "1.1.1.1" });
|
||||
expect(getIP(req)).toBe("1.1.1.1");
|
||||
});
|
||||
|
||||
it("prefers cf-connecting-ip over other headers", () => {
|
||||
const req = buildNextApiRequest({
|
||||
"cf-connecting-ip": "1.1.1.1",
|
||||
"true-client-ip": "2.2.2.2",
|
||||
"x-forwarded-for": "3.3.3.3",
|
||||
"x-real-ip": "4.4.4.4",
|
||||
});
|
||||
expect(getIP(req)).toBe("1.1.1.1");
|
||||
});
|
||||
|
||||
it("falls back to true-client-ip when cf-connecting-ip is absent", () => {
|
||||
const req = buildNextApiRequest({
|
||||
"true-client-ip": "2.2.2.2",
|
||||
"x-real-ip": "4.4.4.4",
|
||||
});
|
||||
expect(getIP(req)).toBe("2.2.2.2");
|
||||
});
|
||||
|
||||
it("falls back to x-forwarded-for when cf and true-client headers are absent", () => {
|
||||
const req = buildNextApiRequest({
|
||||
"x-forwarded-for": "3.3.3.3, 10.0.0.1",
|
||||
"x-real-ip": "4.4.4.4",
|
||||
});
|
||||
expect(getIP(req)).toBe("3.3.3.3");
|
||||
});
|
||||
|
||||
it("falls back to x-real-ip as last resort", () => {
|
||||
const req = buildNextApiRequest({ "x-real-ip": "4.4.4.4" });
|
||||
expect(getIP(req)).toBe("4.4.4.4");
|
||||
});
|
||||
|
||||
it("returns 127.0.0.1 when no IP headers are present", () => {
|
||||
const req = buildNextApiRequest({});
|
||||
expect(getIP(req)).toBe("127.0.0.1");
|
||||
});
|
||||
|
||||
it("handles array header values (NextApiRequest)", () => {
|
||||
const req = buildNextApiRequest({ "x-forwarded-for": ["5.5.5.5", "6.6.6.6"] });
|
||||
expect(getIP(req)).toBe("5.5.5.5");
|
||||
});
|
||||
});
|
||||
});
|
||||
+17
-9
@@ -1,6 +1,6 @@
|
||||
import process from "node:process";
|
||||
import type { NextApiRequest } from "next";
|
||||
import z from "zod";
|
||||
|
||||
import logger from "./logger";
|
||||
|
||||
export function parseIpFromHeaders(value: string | string[]) {
|
||||
@@ -8,20 +8,28 @@ export function parseIpFromHeaders(value: string | string[]) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to extract IP address from a request
|
||||
* Tries to extract IP address from a request.
|
||||
*
|
||||
* Header priority (CF → Vercel setup):
|
||||
* 1. cf-connecting-ip – set by Cloudflare with the real client IP
|
||||
* 2. true-client-ip – set by Cloudflare (Enterprise / Managed Transforms)
|
||||
* 3. x-forwarded-for – first IP is the real client; survives the CF → Vercel hop
|
||||
* 4. x-real-ip – set by Vercel to the *connecting* IP (CF edge IP when
|
||||
* behind Cloudflare, so least reliable)
|
||||
*
|
||||
* @see https://github.com/vercel/examples/blob/main/edge-functions/ip-blocking/lib/get-ip.ts
|
||||
**/
|
||||
export default function getIP(request: Request | NextApiRequest) {
|
||||
let xff =
|
||||
request instanceof Request
|
||||
? request.headers.get("cf-connecting-ip")
|
||||
: request.headers["cf-connecting-ip"];
|
||||
const headers: readonly string[] = ["cf-connecting-ip", "true-client-ip", "x-forwarded-for", "x-real-ip"];
|
||||
|
||||
if (!xff) {
|
||||
xff = request instanceof Request ? request.headers.get("x-real-ip") : request.headers["x-real-ip"];
|
||||
for (const header of headers) {
|
||||
const value = request instanceof Request ? request.headers.get(header) : request.headers[header];
|
||||
if (value) {
|
||||
return parseIpFromHeaders(value);
|
||||
}
|
||||
}
|
||||
|
||||
return xff ? parseIpFromHeaders(xff) : "127.0.0.1";
|
||||
return "127.0.0.1";
|
||||
}
|
||||
|
||||
const banlistSchema = z.array(z.string());
|
||||
|
||||
Reference in New Issue
Block a user