From d97be939b0bac9d96c11be50f9413af7030fc245 Mon Sep 17 00:00:00 2001 From: FireMario211 <17692105+FireMario211@users.noreply.github.com> Date: Sat, 2 Oct 2021 11:40:04 -0400 Subject: [PATCH] Added ratelimits to the API --- src/modules/api/announcements.ts | 10 +++++++--- src/modules/api/auth.ts | 4 ++++ src/modules/api/mail.ts | 4 ++++ src/modules/api/order.ts | 4 ++++ src/modules/api/register.ts | 4 ++++ src/modules/api/tickets.ts | 4 ++++ src/modules/api/user.ts | 4 ++++ src/modules/website.ts | 30 ++++++++++++++++++++++++------ src/types/endpoint.d.ts | 9 ++++++--- 9 files changed, 61 insertions(+), 12 deletions(-) diff --git a/src/modules/api/announcements.ts b/src/modules/api/announcements.ts index 7fd8de62..826911be 100644 --- a/src/modules/api/announcements.ts +++ b/src/modules/api/announcements.ts @@ -16,9 +16,13 @@ function decode_base64(str) { } export const prop = { - name: "announcements", - desc: "API for Announcements", - run: async (req: express.Request, res: express.Response): Promise => { + name: "announcements", + desc: "API for Announcements", + rateLimit: { + max: 10, + time: 30 * 1000 + }, + run: async (req: express.Request, res: express.Response): Promise => { const allowedMethods = ["GET", "POST", "DELETE"]; res.set("Allow", allowedMethods.join(", ")); // To give the method of whats allowed if (!allowedMethods.includes(req.method)) return res.sendStatus(405); diff --git a/src/modules/api/auth.ts b/src/modules/api/auth.ts index 20e7585e..6b97db9f 100644 --- a/src/modules/api/auth.ts +++ b/src/modules/api/auth.ts @@ -212,6 +212,10 @@ export const auth = { export const prop = { name: "auth", desc: "Authenticates a user (Logging in)", + rateLimit: { + max: 10, + time: 60 * 1000 + }, run: async (req: express.Request, res: express.Response): Promise => { res.set("Allow", "POST"); // To give the method of whats allowed if (req.method != "POST") return res.sendStatus(405) // If the request isn't POST then respond with Method not Allowed. diff --git a/src/modules/api/mail.ts b/src/modules/api/mail.ts index a91bfe8f..2fa4f81e 100644 --- a/src/modules/api/mail.ts +++ b/src/modules/api/mail.ts @@ -19,6 +19,10 @@ const emailRegex = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@ export const prop = { name: "mail", desc: "Sends a \"mail\" from /billing/mail to support@amethyst.host", + rateLimit: { + max: 3, + time: 60 * 1000 + }, run: async (req: express.Request, res: express.Response): Promise => { res.set("Allow", "POST"); // To give the method of whats allowed if (req.method != "POST") return res.sendStatus(405) // If the request isn't a POST request, then respond with Method not Allowed. diff --git a/src/modules/api/order.ts b/src/modules/api/order.ts index b78cea52..4bb1ca0d 100644 --- a/src/modules/api/order.ts +++ b/src/modules/api/order.ts @@ -11,6 +11,10 @@ const stripe = require('stripe')(process.env.STRIPE_SK_TEST); export const prop = { name: "order", desc: "API for ordering a service", + rateLimit: { + max: 1, + time: 30 * 1000 + }, run: async (req: express.Request, res: express.Response): Promise => { const allowedMethods = ["GET", "POST"]; res.set("Allow", allowedMethods.join(", ")); // To give the method of whats allowed diff --git a/src/modules/api/register.ts b/src/modules/api/register.ts index e52423fc..d48a8e12 100644 --- a/src/modules/api/register.ts +++ b/src/modules/api/register.ts @@ -9,6 +9,10 @@ import fetch from 'node-fetch'; export const prop = { name: "register", desc: "Creates an account (Only on Website)", + rateLimit: { + max: 3, + time: 2 * (60 * 1000) // 2 Minutes + }, run: async (req: express.Request, res: express.Response) => { res.set("Allow", "POST"); // To give the method of whats allowed if (req.method != "POST") return res.sendStatus(405) // If the request isn't POST then respond with Method not Allowed. diff --git a/src/modules/api/tickets.ts b/src/modules/api/tickets.ts index a8918e91..5e6c7105 100644 --- a/src/modules/api/tickets.ts +++ b/src/modules/api/tickets.ts @@ -50,6 +50,10 @@ function editContent(content, timestamp, ticket_id) { export const prop = { name: "tickets", desc: "Support Ticket System", + rateLimit: { + max: 20, + time: 10 * 1000 + }, run: async (req: express.Request, res: express.Response): Promise => { const allowedMethods = ["GET", "POST", "PATCH", "PUT", "DELETE"]; const params = req.params[0].split("/").slice(1); // Probably a better way to do this in website.ts via doing /api/:method/:optionalparam*? but it doesnt work for me. diff --git a/src/modules/api/user.ts b/src/modules/api/user.ts index c462bd0b..7ef6ab99 100644 --- a/src/modules/api/user.ts +++ b/src/modules/api/user.ts @@ -16,6 +16,10 @@ export const prop = { name: "user", desc: "API for users.", + rateLimit: { + max: 2, + time: 10 * 1000 + }, run: async (req: express.Request, res: express.Response) => { // Copy & Paste from tickets.ts hA const allowedMethods = ["GET", "POST", "PATCH", "PUT", "DELETE"]; diff --git a/src/modules/website.ts b/src/modules/website.ts index 6cc657c3..fd39b5af 100644 --- a/src/modules/website.ts +++ b/src/modules/website.ts @@ -12,8 +12,9 @@ import cookieParser from "cookie-parser" import { auth } from "./api/auth" import * as plans from "../plans.json" import { sql } from './sql' -import { Ticket } from "../types/billing/ticket"; +import { Ticket } from "../types/billing/ticket"; import { UserData } from "../types/billing/user"; +import rateLimit from "express-rate-limit"; //import cors from "cors" const app : express.Application = express(); const html: string = path.join(__dirname, "views", "html"); @@ -26,11 +27,27 @@ const files : Array = fs.readdirSync(`./dist/modules/api`) for (const f of files) { const ep: Endpoint = require(`./api/${f.replace(".js", "")}`); endpoints.set(ep.prop.name, ep); + if (ep.prop["rateLimit"]) { + app.use("/api/" + ep.prop.name + "*", rateLimit({ + windowMs: ep.prop["rateLimit"].time, + max: ep.prop["rateLimit"].max, + message: "You are sending too many API requests! Please try again later.", + })); + // There could be another solution for doing this. + } + } util.expressLog(`${endpoints.size} api endpoints loaded`); app.use(cookieParser()) +// 30 requests every 40 seconds +const apiLimiter = rateLimit({ + windowMs: 40 * 1000, // 40 seconds + max: 30, + message: "You are sending too many API requests! Please try again later." +}); + app.use(morgan("[express]\t:method :url :status :res[content-length] - :response-time ms")); // serve static files @@ -77,21 +94,22 @@ app.get("/", (r: express.Request, s: express.Response) => { }); // Probably another method to do this, but this is the best I can think of right now. -const apiMethod = function (r: express.Request, s: express.Response) { +const apiMethod = async function (r: express.Request, s: express.Response, next: express.NextFunction) { const ep: Endpoint = endpoints.get(r.params.method) if (ep) { // Prevent site from sending errors when the :method is not defined. - ep.prop.run(r, s); + + // Ratelimiter could be improved. + await ep.prop.run(r, s); } else { return s.status(404) .send("if you were searching for a 404.. you found it!!"); } } - /* amethyst.host/api/bill amethyst.host/api/auth and so on.. */ -app.all("/api/:method*", (r: express.Request, s: express.Response) => { - apiMethod(r, s); +app.all("/api/:method*", apiLimiter, (r: express.Request, s: express.Response, next: express.NextFunction) => { + apiMethod(r, s, next); }); // billing app.get("/billing", (r: express.Request, s: express.Response) => { diff --git a/src/types/endpoint.d.ts b/src/types/endpoint.d.ts index f7938a00..3eb4b996 100644 --- a/src/types/endpoint.d.ts +++ b/src/types/endpoint.d.ts @@ -3,9 +3,12 @@ import { Request, Response } from "express"; export interface Endpoint { prop: { - readonly name : string; - readonly desc?: string; - + readonly name : string; + readonly desc? : string; + readonly rateLimit?: { + readonly max : number, + readonly time : number + } run: (req: Request, res: Response) => void;