95 lines
2.9 KiB
TypeScript
95 lines
2.9 KiB
TypeScript
import { Request, Response, NextFunction } from "express";
|
|
import { config } from "../config";
|
|
|
|
export const AGNAI_DOT_CHAT_IP = "157.230.249.32";
|
|
const RATE_LIMIT_ENABLED = Boolean(config.modelRateLimit);
|
|
const RATE_LIMIT = Math.max(1, config.modelRateLimit);
|
|
const ONE_MINUTE_MS = 60 * 1000;
|
|
|
|
const lastAttempts = new Map<string, number[]>();
|
|
|
|
const expireOldAttempts = (now: number) => (attempt: number) =>
|
|
attempt > now - ONE_MINUTE_MS;
|
|
|
|
const getTryAgainInMs = (ip: string) => {
|
|
const now = Date.now();
|
|
const attempts = lastAttempts.get(ip) || [];
|
|
const validAttempts = attempts.filter(expireOldAttempts(now));
|
|
|
|
if (validAttempts.length >= RATE_LIMIT) {
|
|
return validAttempts[0] - now + ONE_MINUTE_MS;
|
|
} else {
|
|
lastAttempts.set(ip, [...validAttempts, now]);
|
|
return 0;
|
|
}
|
|
};
|
|
|
|
const getStatus = (ip: string) => {
|
|
const now = Date.now();
|
|
const attempts = lastAttempts.get(ip) || [];
|
|
const validAttempts = attempts.filter(expireOldAttempts(now));
|
|
return {
|
|
remaining: Math.max(0, RATE_LIMIT - validAttempts.length),
|
|
reset: validAttempts.length > 0 ? validAttempts[0] + ONE_MINUTE_MS : now,
|
|
};
|
|
};
|
|
|
|
/** Prunes attempts and IPs that are no longer relevant after one minutes. */
|
|
const clearOldAttempts = () => {
|
|
const now = Date.now();
|
|
for (const [ip, attempts] of lastAttempts.entries()) {
|
|
const validAttempts = attempts.filter(expireOldAttempts(now));
|
|
if (validAttempts.length === 0) {
|
|
lastAttempts.delete(ip);
|
|
} else {
|
|
lastAttempts.set(ip, validAttempts);
|
|
}
|
|
}
|
|
};
|
|
setInterval(clearOldAttempts, 10 * 1000);
|
|
|
|
export const getUniqueIps = () => {
|
|
return lastAttempts.size;
|
|
};
|
|
|
|
export const ipLimiter = (req: Request, res: Response, next: NextFunction) => {
|
|
if (!RATE_LIMIT_ENABLED) {
|
|
next();
|
|
return;
|
|
}
|
|
|
|
// Exempt Agnai.chat from rate limiting since it's shared between a lot of
|
|
// users. Dunno how to prevent this from being abused without some sort of
|
|
// identifier sent from Agnaistic to identify specific users.
|
|
if (req.ip === AGNAI_DOT_CHAT_IP) {
|
|
next();
|
|
return;
|
|
}
|
|
|
|
// If user is authenticated, key rate limiting by their token. Otherwise, key
|
|
// rate limiting by their IP address. Mitigates key sharing.
|
|
const rateLimitKey = req.user?.token || req.ip;
|
|
|
|
const { remaining, reset } = getStatus(rateLimitKey);
|
|
res.set("X-RateLimit-Limit", config.modelRateLimit.toString());
|
|
res.set("X-RateLimit-Remaining", remaining.toString());
|
|
res.set("X-RateLimit-Reset", reset.toString());
|
|
|
|
const tryAgainInMs = getTryAgainInMs(rateLimitKey);
|
|
if (tryAgainInMs > 0) {
|
|
res.set("Retry-After", tryAgainInMs.toString());
|
|
res.status(429).json({
|
|
error: {
|
|
type: "proxy_rate_limited",
|
|
message: `This proxy is rate limited to ${
|
|
config.modelRateLimit
|
|
} model requests per minute. Please try again in ${Math.ceil(
|
|
tryAgainInMs / 1000
|
|
)} seconds.`,
|
|
},
|
|
});
|
|
} else {
|
|
next();
|
|
}
|
|
};
|