const express = require("express"); // from npm registry const https = require("https"); // built-in const Limiter = require("./limiter.js"); const Logger = require("./logger.js"); const Brute = require("./brute.js"); const api = express(); if (!process.env.NODE_ENV) { console.error("must be started with 'npm run start'"); process.exit(1); } // env-based config const dotenv = require("lazy-universal-dotenv"); const [nodeEnv, buildTarget] = [process.env.NODE_ENV, process.env.BUILD_TARGET]; const conf = dotenv.getEnvironment({ nodeEnv, buildTarget }).raw; Object.assign(process.env, conf); // override if (process.env.PORT === undefined) { console.error("Please run this package with `npm start`"); process.exit(1); } // config common to all routers: api.locals = Object.assign({ cooldown: Limiter.cooldown, mailer: { from: process.env.MAILER__FROM, }, logger: Logger, brute: Brute, }, api.locals); /******************************* BEGIN MIDDLEWARES ********************************/ const checkCaptcha = (req, res, next) => { const token = String(req.get("X-CAPTCHA-TOKEN") || ""); if (!token.match(/^[a-zA-Z0-9-_]{20,800}$/)) { res.status(403).json({ status: "error", error: "no token sent" }); req.app.locals.cooldown(req, 300000); return false; } if (process.env.NODE_ENV === "development") { // local development: no challenge check next(); return; } https.get(`https://www.google.com/recaptcha/api/siteverify?secret=${process.env.RECAPTCHA__SECRET}&response=${token}`, re => { re.setEncoding("utf8"); re.on("data", response => { const data = JSON.parse(response); if (!Reflect.has(data, "success") || data.success !== true) { if (Reflect.has(data, "error-codes")) { const error_codes = data["error-codes"].toString(); if (error_codes !== "invalid-input-response") { console.error("reCAPTCHA returned an error: %s", error_codes); } } res.status(403).json({ status: "error", error: "captcha validation failed" }); req.app.locals.cooldown(req, 300000); return false; } next(); // challenge passed, so process the request }); }).on("error", error => { console.error(error); res.status(403).json({ status: "error", error: "reCAPTCHA couldn't be reached" }); console.warn("reCAPTCHA couldn't be reached"); return false; }) }; // enables rapid local prototyping api.use((req, res, next) => { if (process.env.NODE_ENV === "development") { res.append("Access-Control-Allow-Origin", "*"); if (req.method === "OPTIONS") { res.append("Access-Control-Allow-Methods", "*"); res.append("Access-Control-Allow-Headers", "*"); res.status(200).json({}); return; } } next(); }); // always check rate limiting api.use(Limiter.check); /******************************* END MIDDLEWARES ********************************/ /******************************* BEGIN ROUTERS ********************************/ const global_router = express.Router(["caseSensitive", "strict"]); const tmwa_router = new (require("./routers/tmwa"))({ timezone: process.env.TZ, name: process.env.TMWA__NAME, url: process.env.TMWA__URI, root: process.env.TMWA__ROOT, home: process.env.TMWA__HOME, reset: process.env.TMWA__RESET, }, api, checkCaptcha); const vault = new (require("./routers/vault")) (api, checkCaptcha); global_router.use("/tmwa", tmwa_router); vault.init().then(() => { global_router.use("/vault", vault.router); }) api.use("/api", global_router); /******************************* END ROUTERS ********************************/ // default endpoint: api.use((req, res, next) => { res.status(404).json({ status: "error", error: "unknown endpoint" }); }); api.set("trust proxy", "loopback"); // only allow localhost to communicate with the API api.disable("x-powered-by"); // we don't need this header console.log(`Running in ${process.env.NODE_ENV} mode`); api.listen(process.env.PORT, () => console.info(`Listening on port ${process.env.PORT}`));