From 43fdf7157b94b6ed229fd847173b5f90faecd01b Mon Sep 17 00:00:00 2001 From: Blaine Bublitz Date: Thu, 8 Feb 2024 12:25:57 -0700 Subject: [PATCH] feat: Support cookies and query via the protocol --- arcjet-next/index.ts | 81 ++++++++++++++++++++++++++++++---- arcjet/index.ts | 2 + arcjet/test/index.node.test.ts | 40 +++++++++++++---- protocol/index.ts | 2 + 4 files changed, 109 insertions(+), 16 deletions(-) diff --git a/arcjet-next/index.ts b/arcjet-next/index.ts index db378c6bb..f2348b7e0 100644 --- a/arcjet-next/index.ts +++ b/arcjet-next/index.ts @@ -120,7 +120,44 @@ export interface ArcjetNextRequest { ip?: string; - nextUrl?: Partial<{ pathname: string; search: string }>; + nextUrl?: Partial<{ pathname: string; search: string; protocol: string }>; + + cookies?: + | { + [Symbol.iterator](): IterableIterator< + [string, { name: string; value: string }] + >; + } + | Partial<{ [key: string]: string }>; +} + +function isIterable(val: any): val is Iterable { + return typeof val?.[Symbol.iterator] === "function"; +} + +function cookiesToArray( + cookies?: ArcjetNextRequest["cookies"], +): { name: string; value: string }[] { + if (typeof cookies === "undefined") { + return []; + } + + if (isIterable(cookies)) { + return Array.from(cookies).map(([_, cookie]) => cookie); + } else { + return Object.entries(cookies).map(([name, value]) => ({ + name, + value: value ?? "", + })); + } +} + +function cookiesToString(cookies?: ArcjetNextRequest["cookies"]): string { + // This is essentially the implementation of `RequestCookies#toString` in + // Next.js but normalized for NextApiRequest cookies object + return cookiesToArray(cookies) + .map((v) => `${v.name}=${encodeURIComponent(v.value)}`) + .join("; "); } export interface ArcjetNext { @@ -180,16 +217,42 @@ export default function arcjetNext( const ip = findIP(request, headers); const method = request.method ?? ""; const host = headers.get("host") ?? ""; - let path; - // TODO(#36): nextUrl has formatting logic when you `toString` but we don't account for that here + let path = ""; + let query = ""; + let protocol = ""; + // TODO(#36): nextUrl has formatting logic when you `toString` but + // we don't account for that here if (typeof request.nextUrl !== "undefined") { - path = request.nextUrl.pathname; - if (request.nextUrl.search !== "") { - path += "?" + request.nextUrl.search; + path = request.nextUrl.pathname ?? ""; + if (typeof request.nextUrl.search !== "undefined") { + query = request.nextUrl.search; + } + if (typeof request.nextUrl.protocol !== "undefined") { + protocol = request.nextUrl.protocol } } else { - path = request.url ?? ""; + // Do some very simple validation, but also try/catch around URL parsing + if ( + typeof request.url !== "undefined" && + request.url !== "" && + host !== "" + ) { + try { + const url = new URL(request.url, `http://${host}`); + path = url.pathname; + query = url.search; + protocol = url.protocol; + } catch { + // If the parsing above fails, just set the path as whatever url we + // received. + // TODO: Maybe add some local logging + path = request.url ?? ""; + } + } else { + path = request.url ?? ""; + } } + const cookies = cookiesToString(request.cookies); const extra: { [key: string]: string } = {}; @@ -213,10 +276,12 @@ export default function arcjetNext( ...props, ip, method, - protocol: "", + protocol, host, path, headers, + cookies, + query, ...extra, // TODO(#220): The generic manipulations get really mad here, so we just cast it } as ArcjetRequest>); diff --git a/arcjet/index.ts b/arcjet/index.ts index a194cd6ff..72d3ea75a 100644 --- a/arcjet/index.ts +++ b/arcjet/index.ts @@ -311,6 +311,8 @@ export function createRemoteClient( host: details.host, path: details.path, headers: Object.fromEntries(details.headers.entries()), + cookies: details.cookies, + query: details.query, // TODO(#208): Re-add body // body: details.body, extra: extraProps(details), diff --git a/arcjet/test/index.node.test.ts b/arcjet/test/index.node.test.ts index 19d26b66f..e3ae8794d 100644 --- a/arcjet/test/index.node.test.ts +++ b/arcjet/test/index.node.test.ts @@ -1611,6 +1611,8 @@ describe("Primitive > detectBot", () => { host: "example.com", path: "/", headers: new Headers(), + cookies: "", + query: "", extra: {}, }; @@ -1658,6 +1660,8 @@ describe("Primitive > detectBot", () => { "Mozilla/5.0 (Macintosh; Intel Mac OS X 13_5_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Safari/605.1.15", ], ]), + cookies: "", + query: "", "extra-test": "extra-test-value", }; @@ -1708,6 +1712,8 @@ describe("Primitive > detectBot", () => { "Mozilla/5.0 (Macintosh; Intel Mac OS X 13_5_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Safari/605.1.15", ], ]), + cookies: "", + query: "", "extra-test": "extra-test-value", }; @@ -1758,6 +1764,8 @@ describe("Primitive > detectBot", () => { "Mozilla/5.0 (Macintosh; Intel Mac OS X 13_5_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Safari/605.1.15", ], ]), + cookies: "", + query: "", "extra-test": "extra-test-value", }; @@ -1795,6 +1803,8 @@ describe("Primitive > detectBot", () => { "Mozilla/5.0 (Macintosh; Intel Mac OS X 13_5_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Safari/605.1.15", ], ]), + cookies: "", + query: "", "extra-test": "extra-test-value", }; @@ -1849,6 +1859,8 @@ describe("Primitive > detectBot", () => { host: "example.com", path: "/", headers: new Headers([["User-Agent", "curl/8.1.2"]]), + cookies: "", + query: "", "extra-test": "extra-test-value", }; @@ -1898,6 +1910,8 @@ describe("Primitive > detectBot", () => { "Mozilla/5.0 (Macintosh; Intel Mac OS X 13_5_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Safari/605.1.15", ], ]), + cookies: "", + query: "", "extra-test": "extra-test-value", }; @@ -1936,6 +1950,8 @@ describe("Primitive > detectBot", () => { host: "example.com", path: "/", headers: new Headers([["User-Agent", "curl/8.1.2"]]), + cookies: "", + query: "", "extra-test": "extra-test-value", }; @@ -2783,7 +2799,8 @@ describe("Primitive > validateEmail", () => { host: "example.com", path: "/", headers: new Headers(), - extra: {}, + cookies: "", + query: "", email: "foobarbaz@example.com", }; @@ -2813,7 +2830,8 @@ describe("Primitive > validateEmail", () => { host: "example.com", path: "/", headers: new Headers(), - extra: {}, + cookies: "", + query: "", email: "foobarbaz", }; @@ -2843,7 +2861,8 @@ describe("Primitive > validateEmail", () => { host: "example.com", path: "/", headers: new Headers(), - extra: {}, + cookies: "", + query: "", email: "foobarbaz@localhost", }; @@ -2873,7 +2892,8 @@ describe("Primitive > validateEmail", () => { host: "example.com", path: "/", headers: new Headers(), - extra: {}, + cookies: "", + query: "", email: "foobarbaz@localhost", }; @@ -2905,7 +2925,8 @@ describe("Primitive > validateEmail", () => { host: "example.com", path: "/", headers: new Headers(), - extra: {}, + cookies: "", + query: "", email: "@example.com", }; @@ -2935,7 +2956,8 @@ describe("Primitive > validateEmail", () => { host: "example.com", path: "/", headers: new Headers(), - extra: {}, + cookies: "", + query: "", email: "foobarbaz@[127.0.0.1]", }; @@ -2965,7 +2987,8 @@ describe("Primitive > validateEmail", () => { host: "example.com", path: "/", headers: new Headers(), - extra: {}, + cookies: "", + query: "", email: "foobarbaz@localhost", }; @@ -2997,7 +3020,8 @@ describe("Primitive > validateEmail", () => { host: "example.com", path: "/", headers: new Headers(), - extra: {}, + cookies: "", + query: "", email: "foobarbaz@[127.0.0.1]", }; diff --git a/protocol/index.ts b/protocol/index.ts index cd0462610..cfdbc79dc 100644 --- a/protocol/index.ts +++ b/protocol/index.ts @@ -379,6 +379,8 @@ export interface ArcjetRequestDetails { path: string; // TODO(#215): Allow `Record` and `Record`? headers: Headers; + cookies: string; + query: string; } export type ArcjetRule = {