184 lines
5.7 KiB
JavaScript
184 lines
5.7 KiB
JavaScript
import { access, readFile, writeFile } from "node:fs/promises";
|
|
|
|
const candidates = [
|
|
"node_modules/@indiekit/indiekit/lib/routes.js",
|
|
];
|
|
|
|
const patchMarker = "const sessionLimit = rateLimit({";
|
|
|
|
const oldLimitBlock = `const router = express.Router();
|
|
const limit = rateLimit({
|
|
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
max: 250,
|
|
standardHeaders: true,
|
|
legacyHeaders: false,
|
|
validate: false,
|
|
});`;
|
|
|
|
const newLimitBlock = `const router = express.Router();
|
|
|
|
// Strict rate limiter for session/auth routes (brute force protection)
|
|
const sessionLimit = rateLimit({
|
|
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
max: 50,
|
|
standardHeaders: true,
|
|
legacyHeaders: false,
|
|
validate: false,
|
|
});
|
|
|
|
// Generous rate limiter for public API endpoints (read-only data)
|
|
const apiLimit = rateLimit({
|
|
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
max: 1000,
|
|
standardHeaders: true,
|
|
legacyHeaders: false,
|
|
validate: false,
|
|
});`;
|
|
|
|
const replacements = [
|
|
{
|
|
label: "session login rate limit",
|
|
from: ' router.get("/session/login", limit, sessionController.login);',
|
|
to: ' router.get("/session/login", sessionLimit, sessionController.login);',
|
|
},
|
|
{
|
|
label: "session post rate limit",
|
|
from: ' router.post("/session/login", limit, indieauth.login());',
|
|
to: ' router.post("/session/login", sessionLimit, indieauth.login());',
|
|
},
|
|
{
|
|
label: "session auth rate limit",
|
|
from: ' router.get("/session/auth", limit, indieauth.authorize());',
|
|
to: ' router.get("/session/auth", sessionLimit, indieauth.authorize());',
|
|
},
|
|
{
|
|
label: "public _routes rate limit",
|
|
from: " router.use(endpoint.mountPath, limit, endpoint._routes(Indiekit));",
|
|
to: " router.use(endpoint.mountPath, apiLimit, endpoint._routes(Indiekit));",
|
|
},
|
|
{
|
|
label: "public routesPublic rate limit",
|
|
from: ` if (endpoint.mountPath && endpoint.routesPublic) {
|
|
router.use(endpoint.mountPath, limit, endpoint.routesPublic);
|
|
}`,
|
|
to: ` if (endpoint.mountPath && endpoint.routesPublic) {
|
|
// Skip rate limiting for root-mounted endpoints (mountPath "/") because
|
|
// router.use("/", apiLimit, ...) matches ALL routes, applying the rate
|
|
// limiter globally.
|
|
if (endpoint.mountPath === "/") {
|
|
router.use(endpoint.mountPath, endpoint.routesPublic);
|
|
} else {
|
|
router.use(endpoint.mountPath, apiLimit, endpoint.routesPublic);
|
|
}
|
|
}`,
|
|
},
|
|
{
|
|
label: "well-known rate limit",
|
|
from: ' router.use("/.well-known/", limit, endpoint.routesWellKnown);',
|
|
to: ' router.use("/.well-known/", apiLimit, endpoint.routesWellKnown);',
|
|
},
|
|
{
|
|
label: "content negotiation routes",
|
|
from: " // Authenticate subsequent requests",
|
|
to: ` // Content negotiation routes - serves ActivityPub JSON-LD for post URLs
|
|
// and handles NodeInfo data at /nodeinfo/2.1. Mounted at root before auth
|
|
// so unauthenticated AP clients can fetch post representations.
|
|
for (const endpoint of endpoints) {
|
|
if (endpoint.contentNegotiationRoutes) {
|
|
router.use("/", endpoint.contentNegotiationRoutes);
|
|
}
|
|
}
|
|
|
|
// Authenticate subsequent requests`,
|
|
},
|
|
{
|
|
label: "plugin list limit removal",
|
|
from: ' router.get("/plugins", limit, pluginController.list);',
|
|
to: ' router.get("/plugins", pluginController.list);',
|
|
},
|
|
{
|
|
label: "plugin view limit removal",
|
|
from: ' router.get("/plugins/:pluginId", limit, pluginController.view);',
|
|
to: ' router.get("/plugins/:pluginId", pluginController.view);',
|
|
},
|
|
{
|
|
label: "status limit removal",
|
|
from: ' router.get("/status", limit, statusController.viewStatus);',
|
|
to: ' router.get("/status", statusController.viewStatus);',
|
|
},
|
|
{
|
|
label: "authenticated endpoint limit removal",
|
|
from: " router.use(endpoint.mountPath, limit, endpoint.routes);",
|
|
to: " router.use(endpoint.mountPath, endpoint.routes);",
|
|
},
|
|
];
|
|
|
|
async function exists(filePath) {
|
|
try {
|
|
await access(filePath);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
let checked = 0;
|
|
let patched = 0;
|
|
|
|
for (const filePath of candidates) {
|
|
if (!(await exists(filePath))) {
|
|
continue;
|
|
}
|
|
|
|
checked += 1;
|
|
const source = await readFile(filePath, "utf8");
|
|
|
|
if (source.includes(patchMarker)) {
|
|
continue;
|
|
}
|
|
|
|
if (!source.includes(oldLimitBlock)) {
|
|
console.warn(
|
|
`[postinstall] Skipping routes patch for ${filePath}: upstream format changed`,
|
|
);
|
|
continue;
|
|
}
|
|
|
|
let updated = source.replace(oldLimitBlock, newLimitBlock);
|
|
|
|
for (const replacement of replacements) {
|
|
if (updated.includes(replacement.from)) {
|
|
updated = updated.replace(replacement.from, replacement.to);
|
|
} else {
|
|
console.warn(
|
|
`[postinstall] routes patch skipped section (${replacement.label}) in ${filePath}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
const looksPatched =
|
|
updated.includes("const sessionLimit = rateLimit({") &&
|
|
updated.includes("const apiLimit = rateLimit({") &&
|
|
updated.includes('router.get("/session/login", sessionLimit, sessionController.login);') &&
|
|
updated.includes("router.use(endpoint.mountPath, endpoint.routes);") &&
|
|
!updated.includes('router.get("/session/login", limit, sessionController.login);') &&
|
|
!updated.includes("router.use(endpoint.mountPath, limit, endpoint.routes);");
|
|
|
|
if (!looksPatched) {
|
|
console.warn(
|
|
`[postinstall] Skipping routes patch for ${filePath}: patch validation failed`,
|
|
);
|
|
continue;
|
|
}
|
|
|
|
await writeFile(filePath, updated, "utf8");
|
|
patched += 1;
|
|
}
|
|
|
|
if (checked === 0) {
|
|
console.log("[postinstall] No indiekit routes files found");
|
|
} else if (patched === 0) {
|
|
console.log("[postinstall] indiekit routes rate-limit patch already applied");
|
|
} else {
|
|
console.log(`[postinstall] Patched indiekit routes rate limits in ${patched} file(s)`);
|
|
} |