All checks were successful
Docker Cloud Build / Build & Publish Image (push) Successful in 11m11s
- Add express-rate-limit: login limiter (10 req/15m) and global limiter (100 req/m) - Add helmet: secure HTTP headers with custom CSP configuration - Remove manual header settings in favor of helmet
124 lines
3.1 KiB
JavaScript
124 lines
3.1 KiB
JavaScript
import bodyParser from "body-parser";
|
|
import compression from "compression";
|
|
import express from "express";
|
|
import fileUpload from "express-fileupload";
|
|
import helmet from "helmet";
|
|
import { rateLimit } from "express-rate-limit";
|
|
import { isDebugMode } from "./lib/config.js";
|
|
import cors from "./lib/express/cors.js";
|
|
import jwt from "./lib/express/jwt.js";
|
|
import { debug, express as logger } from "./logger.js";
|
|
import mainRoutes from "./routes/main.js";
|
|
import wgPublicRoutes from "./routes/wg_public.js";
|
|
|
|
/**
|
|
* App
|
|
*/
|
|
const app = express();
|
|
app.use(fileUpload());
|
|
app.use(bodyParser.json());
|
|
app.use(bodyParser.urlencoded({ extended: true }));
|
|
|
|
// Gzip
|
|
app.use(compression());
|
|
|
|
/**
|
|
* General Logging, BEFORE routes
|
|
*/
|
|
|
|
app.disable("x-powered-by");
|
|
app.enable("trust proxy", ["loopback", "linklocal", "uniquelocal"]);
|
|
app.enable("strict routing");
|
|
|
|
// pretty print JSON when not live
|
|
if (isDebugMode()) {
|
|
app.set("json spaces", 2);
|
|
}
|
|
|
|
// CORS for everything
|
|
app.use(cors);
|
|
|
|
/**
|
|
* Global Rate Limiter: 100 requests per minute per IP
|
|
*/
|
|
const globalLimiter = rateLimit({
|
|
windowMs: 1 * 60 * 1000, // 1 minute
|
|
max: 100,
|
|
standardHeaders: true,
|
|
legacyHeaders: false,
|
|
message: { error: { message: "Too many requests, please try again later." } },
|
|
});
|
|
|
|
/**
|
|
* Login Rate Limiter: 10 requests per 15 minutes per IP
|
|
*/
|
|
const loginLimiter = rateLimit({
|
|
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
max: 10,
|
|
standardHeaders: true,
|
|
legacyHeaders: false,
|
|
message: { error: { message: "Too many login attempts, please try again in 15 minutes." } },
|
|
});
|
|
|
|
/**
|
|
* Helmet Security Headers
|
|
*/
|
|
app.use(
|
|
helmet({
|
|
contentSecurityPolicy: {
|
|
directives: {
|
|
defaultSrc: ["'self'"],
|
|
scriptSrc: ["'self'", "'unsafe-inline'", "'unsafe-eval'"], // unsafe-inline/eval required by some React libs
|
|
styleSrc: ["'self'", "'unsafe-inline'"],
|
|
imgSrc: ["'self'", "data:", "https://www.gravatar.com"],
|
|
connectSrc: ["'self'"],
|
|
frameAncestors: [process.env.X_FRAME_OPTIONS === "ALLOWALL" ? "*" : "'none'"],
|
|
},
|
|
},
|
|
crossOriginResourcePolicy: { policy: "cross-origin" },
|
|
}),
|
|
);
|
|
|
|
// Apply rate limiting
|
|
app.use("/api/tokens", loginLimiter);
|
|
app.use("/api/", globalLimiter);
|
|
|
|
// Bypass JWT for public authenticated requests mapped by WireGuard IP
|
|
app.use("/wg-public", wgPublicRoutes);
|
|
|
|
app.use(jwt());
|
|
app.use("/", mainRoutes);
|
|
|
|
// production error handler
|
|
// no stacktraces leaked to user
|
|
app.use((err, req, res, _) => {
|
|
const payload = {
|
|
error: {
|
|
code: err.status,
|
|
message: err.public ? err.message : "Internal Error",
|
|
},
|
|
};
|
|
|
|
if (typeof err.message_i18n !== "undefined") {
|
|
payload.error.message_i18n = err.message_i18n;
|
|
}
|
|
|
|
if (isDebugMode() || (req.baseUrl + req.path).includes("nginx/certificates")) {
|
|
payload.debug = {
|
|
stack: typeof err.stack !== "undefined" && err.stack ? err.stack.split("\n") : null,
|
|
previous: err.previous,
|
|
};
|
|
}
|
|
|
|
// Not every error is worth logging - but this is good for now until it gets annoying.
|
|
if (typeof err.stack !== "undefined" && err.stack) {
|
|
debug(logger, err.stack);
|
|
if (typeof err.public === "undefined" || !err.public) {
|
|
logger.warn(err.message);
|
|
}
|
|
}
|
|
|
|
res.status(err.status || 500).send(payload);
|
|
});
|
|
|
|
export default app;
|