Skip to content
Open
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
22 changes: 1 addition & 21 deletions standalone/src/cap.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
import { isLoaded as ipdbIsLoaded, lookup as ipLookup } from "./ipdb.js";
import valkeyRateLimit from "./ratelimit.js";
import { checkCorsOrigin, getFiltering, getHeaders, getRatelimit } from "./settings-cache.js";
import { getClientIp } from "./ip_extraction.js";

function hourlyBucket() {
return String(Math.floor(Date.now() / 1000 / 3600) * 3600);
Expand All @@ -34,28 +35,7 @@ function parseUA(ua) {
return { platform, os };
}

const DEFAULT_IP_HEADERS = ["X-Forwarded-For", "X-Real-IP", "CF-Connecting-IP"];

function getClientIp(request, srv) {
const cachedHeaders = getHeaders();
const headerName = cachedHeaders?.ipHeader || process.env.RATELIMIT_IP_HEADER;
if (headerName) {
const ip = request.headers.get(headerName) || request.headers.get(headerName.toLowerCase());
if (ip) {
const parts = ip.split(",").filter((e) => !!e.trim());
return parts[0].trim();
}
}

for (const h of DEFAULT_IP_HEADERS) {
const val = request.headers.get(h);
if (val) {
const parts = val.split(",").filter((e) => !!e.trim());
return parts[0].trim();
}
}
return srv?.requestIP(request)?.address || null;
}

const CHALLENGE_TTL_MS = 15 * 60 * 1000; // 15min
const TOKEN_TTL_MS = 2 * 60 * 60 * 1000; // 2h
Expand Down
24 changes: 24 additions & 0 deletions standalone/src/ip_extraction.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { getHeaders } from "./settings-cache.js";

const DEFAULT_IP_HEADERS = ["X-Forwarded-For", "X-Real-IP", "CF-Connecting-IP"];

export function getClientIp(request, srv) {
const cachedHeaders = getHeaders();
const headerName = cachedHeaders?.ipHeader || process.env.RATELIMIT_IP_HEADER;
if (headerName) {
const ip = request.headers.get(headerName) || request.headers.get(headerName.toLowerCase());
if (ip) {
const parts = ip.split(",").filter((e) => !!e.trim());
return parts[0].trim();
}
}

for (const h of DEFAULT_IP_HEADERS) {
const val = request.headers.get(h);
if (val) {
const parts = val.split(",").filter((e) => !!e.trim());
return parts[0].trim();
}
}
return srv?.requestIP(request)?.address || null;
}
57 changes: 10 additions & 47 deletions standalone/src/ratelimit.js
Original file line number Diff line number Diff line change
@@ -1,53 +1,9 @@
import { Elysia } from "elysia";
import { db } from "./db.js";
import { getHeaders } from "./settings-cache.js";
import { getClientIp } from "./ip_extraction.js";

let scopeCounter = 0;

const DEFAULT_IP_HEADERS = ["X-Forwarded-For", "X-Real-IP", "CF-Connecting-IP"];

const generator = (req, server) => {
const cachedHeaders = getHeaders();
const headerFromSettings = cachedHeaders?.ipHeader;
const headerName = headerFromSettings || process.env.RATELIMIT_IP_HEADER;
if (headerName) {
const header = headerName;
const ip = req.headers.get(header) || req.headers.get(header.toLowerCase());

if (ip) {
const parts = ip.split(",").filter((e) => !!e.trim());
return parts[0].trim();
}

console.error(
`⚠️ [ratelimit] Unable to find the IP in the header "${header}". Make sure to set the RATELIMIT_IP_HEADER env variable \n to a header which returns the user's IP.`,
);
return "";
}

for (const h of DEFAULT_IP_HEADERS) {
const val = req.headers.get(h);
if (val) {
const parts = val.split(",").filter((e) => !!e.trim());
return parts[0].trim();
}
}

const ip = server?.requestIP(req)?.address;

if (!server || !req || !ip) {
if (process.env.HIDE_RATELIMIT_IP_WARNING !== "true") {
console.warn(
`⚠️ [ratelimit] Unable to determine client IP, rate limiting disabled. If you're running locally, it should be safe \n to ignore this warning. Otherwise, make sure to set the RATELIMIT_IP_HEADER env variable to a header \n which returns the user's IP. Hide this warning with env.HIDE_RATELIMIT_IP_WARNING=true`,
);
}

return "";
}

return ip ?? "";
};

export default function valkeyRateLimit({
max: defaultMax = 30,
duration: defaultDuration = 5000,
Expand All @@ -58,8 +14,15 @@ export default function valkeyRateLimit({

return new Elysia({ name: `cap-ratelimit-${scope}`, scoped: true }).onBeforeHandle(
async ({ request, set, server: srv, params }) => {
const ip = generator(request, srv);
if (!ip) return;
const ip = getClientIp(request, srv);
if (!ip) {
if (process.env.HIDE_RATELIMIT_IP_WARNING !== "true") {
console.warn(
`⚠️ [ratelimit] Unable to determine client IP, rate limiting disabled. If you're running locally, it should be safe \n to ignore this warning. Otherwise, make sure to set the RATELIMIT_IP_HEADER env variable to a header \n which returns the user's IP. Hide this warning with env.HIDE_RATELIMIT_IP_WARNING=true`,
);
}
return;
}

let max = defaultMax;
let duration = defaultDuration;
Expand Down