MiniSearch / server /verifyTokenAndRateLimit.ts
github-actions[bot]
Sync from https://github.com/felladrin/MiniSearch
caa8784
Raw
History Blame Contribute Delete
2.98 kB
import type { IncomingMessage } from "node:http";
import { isIP } from "node:net";
import { argon2Verify } from "hash-wasm";
import { RateLimiterMemory } from "rate-limiter-flexible";
import { getSearchToken } from "./searchToken.ts";
import { addVerifiedToken, isVerifiedToken } from "./verifiedTokens.ts";
const rateLimiter = new RateLimiterMemory({
points: 10,
duration: 10,
});
/** Whether to trust proxy-set forwarding headers. Off unless `TRUST_PROXY` is `true`/`1`. */
function isProxyTrusted(): boolean {
const value = process.env.TRUST_PROXY?.trim().toLowerCase();
return value === "true" || value === "1";
}
/**
* Resolves the client IP used as the rate-limit key.
*
* `X-Forwarded-For` / `X-Real-IP` are only honored when `TRUST_PROXY` is
* enabled. On a directly-exposed instance those headers are fully
* client-controlled, so trusting them would let a caller forge a fresh IP per
* request and evade rate limiting entirely. When `TRUST_PROXY` is off (the
* default) we use the real TCP peer address, which cannot be spoofed.
*
* Enable `TRUST_PROXY` only when MiniSearch runs behind a reverse proxy that
* sets the rightmost `X-Forwarded-For` entry (e.g. nginx's
* `$proxy_add_x_forwarded_for`).
*/
export function getClientIp(request: IncomingMessage): string {
if (isProxyTrusted()) {
const forwarded = request.headers["x-forwarded-for"];
const xff = Array.isArray(forwarded) ? forwarded.join(",") : forwarded;
if (typeof xff === "string" && xff.length > 0) {
const parts = xff
.split(",")
.map((p) => p.trim())
.filter(Boolean);
const ip = parts[parts.length - 1];
if (ip && isIP(ip)) {
return ip;
}
}
const realIp = request.headers["x-real-ip"];
if (typeof realIp === "string" && realIp.length > 0 && isIP(realIp)) {
return realIp;
}
}
return request.socket.remoteAddress || "unknown";
}
export async function verifyTokenAndRateLimit(
token: string | null,
request?: IncomingMessage,
): Promise<{
isAuthorized: boolean;
statusCode?: number;
error?: string;
}> {
if (!token) {
return {
isAuthorized: false,
statusCode: 400,
error: "Missing token.",
};
}
if (!isVerifiedToken(token)) {
let isValidToken = false;
try {
isValidToken = await argon2Verify({
password: getSearchToken(),
hash: token,
});
} catch (error) {
void error;
}
if (!isValidToken) {
return {
isAuthorized: false,
statusCode: 401,
error: "Invalid token.",
};
}
}
// Records a new session or refreshes an active one's last-seen time.
addVerifiedToken(token);
const rateLimitKey = request ? getClientIp(request) : token;
try {
await rateLimiter.consume(rateLimitKey);
} catch {
return {
isAuthorized: false,
statusCode: 429,
error: "Too many requests.",
};
}
return { isAuthorized: true };
}