Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions library/agent/addRouteParam.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import * as t from "tap";
import { wrap } from "../helpers/wrap";
import { addRouteParam, getRegisteredRouteParams } from "./addRouteParam";

let logs: string[] = [];
wrap(console, "warn", function warn() {
return function warn(message: string) {
logs.push(message);
};
});

t.beforeEach(() => {
logs = [];
});

t.test("it warns if slash is included", async (t) => {
addRouteParam("prefix/{digits}");
t.same(logs[0], "addRouteParam(...) expects a pattern without slashes.");
});

t.test("it warns if no curly braces are included", async (t) => {
addRouteParam("prefix-digits");
t.same(
logs[0],
"addRouteParam(...) expects a pattern that includes {digits} or {alpha}."
);
});

t.test("addRouteParam adds valid patterns", async (t) => {
addRouteParam("prefix-{digits}");
t.same(getRegisteredRouteParams().length, 1);
t.same(getRegisteredRouteParams()[0].test("prefix-12345"), true);

addRouteParam("prefix-{digits}");
t.same(getRegisteredRouteParams().length, 1);

addRouteParam("prefix-{alpha}");
t.same(getRegisteredRouteParams().length, 2);
t.same(getRegisteredRouteParams()[1].test("prefix-abcde"), true);

addRouteParam("prefix-{alpha}");
t.same(getRegisteredRouteParams().length, 2);
});
38 changes: 38 additions & 0 deletions library/agent/addRouteParam.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// oxlint-disable no-console
import { compileCustomPattern } from "../helpers/buildRouteFromURL";

const registeredPatterns: RegExp[] = [];
const registeredPatternsSet: Set<string> = new Set();

export function addRouteParam(pattern: string) {
if (!pattern.includes("{") || !pattern.includes("}")) {
console.warn(
"addRouteParam(...) expects a pattern that includes {digits} or {alpha}."
);
return;
}

if (pattern.includes("/")) {
console.warn("addRouteParam(...) expects a pattern without slashes.");
return;
}

const regex = compileCustomPattern(pattern);
if (!regex) {
console.warn(
"addRouteParam(...) could not compile the provided pattern into a valid regular expression."
);
return;
}

if (registeredPatternsSet.has(pattern)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check before compiling?

return;
}

registeredPatternsSet.add(pattern);
registeredPatterns.push(regex);
}

export function getRegisteredRouteParams(): RegExp[] {
return registeredPatterns;
}
138 changes: 86 additions & 52 deletions library/helpers/buildRouteFromURL.test.ts
Original file line number Diff line number Diff line change
@@ -1,173 +1,191 @@
import * as t from "tap";
import { buildRouteFromURL } from "./buildRouteFromURL";
import { buildRouteFromURL, compileCustomPattern } from "./buildRouteFromURL";
import * as ObjectID from "bson-objectid";
import { createHash } from "crypto";

t.test("it returns undefined for invalid URLs", async () => {
t.same(buildRouteFromURL(""), undefined);
t.same(buildRouteFromURL("http"), undefined);
t.same(buildRouteFromURL("", []), undefined);
t.same(buildRouteFromURL("http", []), undefined);
});

t.test("it returns / for root URLs", async () => {
t.same(buildRouteFromURL("/"), "/");
t.same(buildRouteFromURL("http://localhost/"), "/");
t.same(buildRouteFromURL("/", []), "/");
t.same(buildRouteFromURL("http://localhost/", []), "/");
});

t.test("it replaces numbers", async () => {
t.same(buildRouteFromURL("/posts/3"), "/posts/:number");
t.same(buildRouteFromURL("http://localhost/posts/3"), "/posts/:number");
t.same(buildRouteFromURL("http://localhost/posts/3/"), "/posts/:number");
t.same(buildRouteFromURL("/posts/3", []), "/posts/:number");
t.same(buildRouteFromURL("http://localhost/posts/3", []), "/posts/:number");
t.same(buildRouteFromURL("http://localhost/posts/3/", []), "/posts/:number");
t.same(
buildRouteFromURL("http://localhost/posts/3/comments/10"),
buildRouteFromURL("http://localhost/posts/3/comments/10", []),
"/posts/:number/comments/:number"
);
t.same(
buildRouteFromURL("/blog/2023/05/great-article"),
buildRouteFromURL("/blog/2023/05/great-article", []),
"/blog/:number/:number/great-article"
);
});

t.test("it replaces dates", async () => {
t.same(buildRouteFromURL("/posts/2023-05-01"), "/posts/:date");
t.same(buildRouteFromURL("/posts/2023-05-01/"), "/posts/:date");
t.same(buildRouteFromURL("/posts/2023-05-01", []), "/posts/:date");
t.same(buildRouteFromURL("/posts/2023-05-01/", []), "/posts/:date");
t.same(
buildRouteFromURL("/posts/2023-05-01/comments/2023-05-01"),
buildRouteFromURL("/posts/2023-05-01/comments/2023-05-01", []),
"/posts/:date/comments/:date"
);
t.same(buildRouteFromURL("/posts/01-05-2023"), "/posts/:date");
t.same(buildRouteFromURL("/posts/01-05-2023", []), "/posts/:date");
});

t.test("it ignores API version numbers", async () => {
t.same(buildRouteFromURL("/v1/posts/3"), "/v1/posts/:number");
t.same(buildRouteFromURL("/v1/posts/3", []), "/v1/posts/:number");
});

t.test("it replaces UUIDs v1", async () => {
t.same(
buildRouteFromURL("/posts/d9428888-122b-11e1-b85c-61cd3cbb3210"),
buildRouteFromURL("/posts/d9428888-122b-11e1-b85c-61cd3cbb3210", []),
"/posts/:uuid"
);
});

t.test("it replaces UUIDs v2", async () => {
t.same(
buildRouteFromURL("/posts/000003e8-2363-21ef-b200-325096b39f47"),
buildRouteFromURL("/posts/000003e8-2363-21ef-b200-325096b39f47", []),
"/posts/:uuid"
);
});

t.test("it replaces UUIDs v3", async () => {
t.same(
buildRouteFromURL("/posts/a981a0c2-68b1-35dc-bcfc-296e52ab01ec"),
buildRouteFromURL("/posts/a981a0c2-68b1-35dc-bcfc-296e52ab01ec", []),
"/posts/:uuid"
);
});

t.test("it replaces UUIDs v4", async () => {
t.same(
buildRouteFromURL("/posts/109156be-c4fb-41ea-b1b4-efe1671c5836"),
buildRouteFromURL("/posts/109156be-c4fb-41ea-b1b4-efe1671c5836", []),
"/posts/:uuid"
);
});

t.test("it replaces UUIDs v5", async () => {
t.same(
buildRouteFromURL("/posts/90123e1c-7512-523e-bb28-76fab9f2f73d"),
buildRouteFromURL("/posts/90123e1c-7512-523e-bb28-76fab9f2f73d", []),
"/posts/:uuid"
);
});

t.test("it replaces UUIDs v6", async () => {
t.same(
buildRouteFromURL("/posts/1ef21d2f-1207-6660-8c4f-419efbd44d48"),
buildRouteFromURL("/posts/1ef21d2f-1207-6660-8c4f-419efbd44d48", []),
"/posts/:uuid"
);
});

t.test("it replaces UUIDs v7", async () => {
t.same(
buildRouteFromURL("/posts/017f22e2-79b0-7cc3-98c4-dc0c0c07398f"),
buildRouteFromURL("/posts/017f22e2-79b0-7cc3-98c4-dc0c0c07398f", []),
"/posts/:uuid"
);
});

t.test("it replaces UUIDs v8", async () => {
t.same(
buildRouteFromURL("/posts/0d8f23a0-697f-83ae-802e-48f3756dd581"),
buildRouteFromURL("/posts/0d8f23a0-697f-83ae-802e-48f3756dd581", []),
"/posts/:uuid"
);
});

t.test("it ignores invalid UUIDs", async () => {
t.same(
buildRouteFromURL("/posts/00000000-0000-1000-6000-000000000000"),
buildRouteFromURL("/posts/00000000-0000-1000-6000-000000000000", []),
"/posts/00000000-0000-1000-6000-000000000000"
);
});

t.test("it ignores strings", async () => {
t.same(buildRouteFromURL("/posts/abc"), "/posts/abc");
t.same(buildRouteFromURL("/posts/abc", []), "/posts/abc");
});

t.test("it replaces email addresses", async () => {
t.same(buildRouteFromURL("/login/[email protected]"), "/login/:email");
t.same(buildRouteFromURL("/login/[email protected]"), "/login/:email");
t.same(buildRouteFromURL("/login/[email protected]", []), "/login/:email");
t.same(
buildRouteFromURL("/login/[email protected]", []),
"/login/:email"
);
});

t.test("it replaces IP addresses", async () => {
t.same(buildRouteFromURL("/block/1.2.3.4"), "/block/:ip");
t.same(buildRouteFromURL("/block/1.2.3.4", []), "/block/:ip");
t.same(
buildRouteFromURL("/block/2001:2:ffff:ffff:ffff:ffff:ffff:ffff", []),
"/block/:ip"
);
t.same(
buildRouteFromURL("/block/2001:2:ffff:ffff:ffff:ffff:ffff:ffff"),
buildRouteFromURL("/block/64:ff9a::255.255.255.255", []),
"/block/:ip"
);
t.same(buildRouteFromURL("/block/64:ff9a::255.255.255.255"), "/block/:ip");
t.same(buildRouteFromURL("/block/100::"), "/block/:ip");
t.same(buildRouteFromURL("/block/fec0::"), "/block/:ip");
t.same(buildRouteFromURL("/block/227.202.96.196"), "/block/:ip");
t.same(buildRouteFromURL("/block/100::", []), "/block/:ip");
t.same(buildRouteFromURL("/block/fec0::", []), "/block/:ip");
t.same(buildRouteFromURL("/block/227.202.96.196", []), "/block/:ip");
});

function generateHash(type: string) {
return createHash(type).update("test").digest("hex");
}

t.test("it replaces hashes", async () => {
t.same(buildRouteFromURL(`/files/${generateHash("md5")}`), "/files/:hash");
t.same(buildRouteFromURL(`/files/${generateHash("sha1")}`), "/files/:hash");
t.same(buildRouteFromURL(`/files/${generateHash("sha256")}`), "/files/:hash");
t.same(buildRouteFromURL(`/files/${generateHash("sha512")}`), "/files/:hash");
t.same(
buildRouteFromURL(`/files/${generateHash("md5")}`, []),
"/files/:hash"
);
t.same(
buildRouteFromURL(`/files/${generateHash("sha1")}`, []),
"/files/:hash"
);
t.same(
buildRouteFromURL(`/files/${generateHash("sha256")}`, []),
"/files/:hash"
);
t.same(
buildRouteFromURL(`/files/${generateHash("sha512")}`, []),
"/files/:hash"
);
});

t.test("it replaces secrets", async () => {
t.same(
buildRouteFromURL("/confirm/CnJ4DunhYfv2db6T1FRfciRBHtlNKOYrjoz"),
buildRouteFromURL("/confirm/CnJ4DunhYfv2db6T1FRfciRBHtlNKOYrjoz", []),
"/confirm/:secret"
);
});

t.test("it replaces BSON ObjectIDs", async () => {
t.same(
// @ts-expect-error It says that the expression isn't callable
buildRouteFromURL(`/posts/${ObjectID().toHexString()}`),
buildRouteFromURL(`/posts/${ObjectID().toHexString()}`, []),
"/posts/:objectId"
);
t.same(
buildRouteFromURL(`/posts/66ec29159d00113616fc7184`),
buildRouteFromURL(`/posts/66ec29159d00113616fc7184`, []),
"/posts/:objectId"
);
});

t.test("it replaces ULID strings", async () => {
t.same(
buildRouteFromURL("/posts/01ARZ3NDEKTSV4RRFFQ69G5FAV"),
buildRouteFromURL("/posts/01ARZ3NDEKTSV4RRFFQ69G5FAV", []),
"/posts/:ulid"
);
t.same(
buildRouteFromURL("/posts/01arz3ndektsv4rrffq69g5fav"),
buildRouteFromURL("/posts/01arz3ndektsv4rrffq69g5fav", []),
"/posts/:ulid"
);
});

t.test("test_ratelimiting_1 is not a secret", async () => {
t.same(buildRouteFromURL("/test_ratelimiting_1"), "/test_ratelimiting_1");
t.same(buildRouteFromURL("/test_ratelimiting_1", []), "/test_ratelimiting_1");
});

t.test("it does not detect static files as secrets", async () => {
Expand All @@ -180,21 +198,37 @@ t.test("it does not detect static files as secrets", async () => {
];

for (const file of files) {
t.same(buildRouteFromURL(`/assets/${file}`), `/assets/${file}`);
t.same(buildRouteFromURL(`/assets/${file}`, []), `/assets/${file}`);
}
});

t.test("it detects numeric comma separated arrays", async (t) => {
t.same(buildRouteFromURL("/users/1,2"), "/users/:array(number)");
t.same(buildRouteFromURL("/users/1,2,3,4,5"), "/users/:array(number)");
t.same(buildRouteFromURL("/users/1,2", []), "/users/:array(number)");
t.same(buildRouteFromURL("/users/1,2,3,4,5", []), "/users/:array(number)");
t.same(
buildRouteFromURL("/users/100,200,3000000,40000000,500000000"),
buildRouteFromURL("/users/100,200,3000000,40000000,500000000", []),
"/users/:array(number)"
);

t.same(buildRouteFromURL("/users/1,2,3,4,"), "/users/1,2,3,4,");
t.same(buildRouteFromURL("/users/1,"), "/users/1,");
t.same(buildRouteFromURL("/users/,1,2"), "/users/,1,2");
t.same(buildRouteFromURL("/users/1,2,3_"), "/users/1,2,3_");
t.same(buildRouteFromURL("/users/1,2,3a"), "/users/1,2,3a");
t.same(buildRouteFromURL("/users/1,2,3,4,", []), "/users/1,2,3,4,");
t.same(buildRouteFromURL("/users/1,", []), "/users/1,");
t.same(buildRouteFromURL("/users/,1,2", []), "/users/,1,2");
t.same(buildRouteFromURL("/users/1,2,3_", []), "/users/1,2,3_");
t.same(buildRouteFromURL("/users/1,2,3a", []), "/users/1,2,3a");
});

t.test("it supports custom patterns", async () => {
t.same(
buildRouteFromURL("/prefix-103799/api/dashboard", [
compileCustomPattern("prefix-{digits}")!,
]),
"/:custom/api/dashboard"
);

t.same(
buildRouteFromURL("/blog/01-31513/slug", [
compileCustomPattern("{digits}-{digits}")!,
]),
"/blog/:custom/slug"
);
});
Loading
Loading