diff --git a/src/app.ts b/src/app.ts index e9d1228..11f75e1 100644 --- a/src/app.ts +++ b/src/app.ts @@ -15,6 +15,9 @@ import readRouter from '@src/controller/read'; import path from 'path'; import logger from '@src/scripts/logger'; +// console.log({ "status": 403, "name": "Error", "message": { "errors": [{ "type": "field", "msg": "Invalid value", "path": "user", "location": "query" }, { "type": "field", "msg": "is required", "path": "lat", "location": "query" }]}}); +// console.log(JSON.stringify({ "status": 403, "name": "Error", "message": { "errors": [{ "type": "field", "msg": "Invalid value", "path": "user", "location": "query" }, { "type": "field", "msg": "is required", "path": "lat", "location": "query" }]}}, null, 2)); + // configurations config(); // dotenv @@ -26,8 +29,15 @@ app.use((req, res, next) => { // monitor eventloop to block requests if busy } else { next(); } }); app.use((req, res, next) => { // clean up IPv6 Addresses - if (req.ip) { res.locals.ip = req.ip.startsWith('::ffff:') ? req.ip.substring(7) : req.ip; } - next(); + if (req.ip) { + res.locals.ip = req.ip.startsWith('::ffff:') ? req.ip.substring(7) : req.ip; + next(); + } else { + const message = "No IP provided" + logger.error(message); + res.status(400).send(message); + } + }) // const slowDownLimiter = slowDown({ diff --git a/src/controller/write.ts b/src/controller/write.ts index dc0658d..47f7d6b 100644 --- a/src/controller/write.ts +++ b/src/controller/write.ts @@ -1,19 +1,83 @@ import express, { Request, Response, NextFunction } from 'express'; import { entry } from '@src/models/entry'; import { validationResult } from 'express-validator'; -import { create as createError } from '@src/error'; +import { create as createError } from '@src/error'; +import { slowDown, Options as slowDownOptions } from 'express-slow-down'; +import { rateLimit, Options as rateLimiterOptions } from 'express-rate-limit'; +import logger from '@src/scripts/logger'; + +// TODO clean up after 1 day? +// TODO move rateLimit to own file +const ipsThatReachedLimit: RateLimit.obj = {}; + +const baseOptions: Partial = { + windowMs: 5 * 60 * 1000, + //skip: (req, res) => (res.locals.ip == process.env.LOCALHOST) +} + +const baseSlowDown: Partial = { + ...baseOptions, + delayAfter: 3, // no delay for amount of attempts + delayMs: (used: number) => (used - 3) * 125, // Add delay after delayAfter is reached +} + +const baseRateLimit: Partial = { + ...baseOptions, + limit: 10, // Limit each IP per window + standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers + legacyHeaders: false, // Disable the `X-RateLimit-*` headers +} + +const errorRateLimiter = rateLimit({ + ...baseRateLimit, + message: 'Too many requests with errors', + handler: (req: Request, res: Response, next: NextFunction, options: rateLimiterOptions) => { + if (!Object.prototype.hasOwnProperty.call(ipsThatReachedLimit, res.locals.ip)) { + logger.error(`[RateLimit] for invalid requests reached ${res.locals.ip}, ${req.get('User-Agent')}`); + ipsThatReachedLimit[res.locals.ip] = { limitReachedOnError: true }; + } + res.status(options.statusCode).send(options.message); + } +}); + +// function customRateLimit(req: Request, res: Response, next: NextFunction) { +// console.count("customRateLimit"); +// if (!validationResult(req).isEmpty()) { + +// } else { +// rateLimit({ +// ...baseRateLimit, +// limit: 20, +// handler: (req: Request, res: Response, next: NextFunction, options: rateLimiterOptions) => { +// if (!Object.prototype.hasOwnProperty.call(ipsThatReachedLimit, res.locals.ip)) { +// logger.error(`[RateLimit] for valid requests reached ${res.locals.ip}, ${req.get('User-Agent')}`); +// ipsThatReachedLimit[res.locals.ip] = { limitReachedOnError: false }; +// } +// res.status(options.statusCode).send(options.message); +// } +// }) +// } +// } + // example call: /write?user=xx&lat=00.000&lon=00.000×tamp=1704063600000&hdop=0.0&altitude=0.000&speed=0.000&heading=000.0 -async function errorChecking (req:Request, res:Response, next:NextFunction) { + +function errorChecking(req: Request, res: Response, next: NextFunction) { const errors = validationResult(req); if (!errors.isEmpty()) { - const errorAsJson = { errors: errors.array()}; - const errorAsString = JSON.stringify(errorAsJson); - const hasKeyErrors = errors.array().some(error => error.msg.includes("Key")); - - // send forbidden or unprocessable content - return createError(res, hasKeyErrors ? 403 : 422, errorAsString, next) + + // if errors happend, then rateLimit to prevent key bruteforcing + errorRateLimiter(req, res, () => { + const errorAsJson = { errors: errors.array() }; + const errorAsString = JSON.stringify(errorAsJson); + const hasKeyErrors = errors.array().some(error => error.msg.includes("Key")); + + // send forbidden or unprocessable content + return createError(res, hasKeyErrors ? 403 : 422, errorAsString, next) + }); + + return; } if (req.method == "HEAD") { @@ -21,22 +85,23 @@ async function errorChecking (req:Request, res:Response, next:NextFunction) { return; } + next(); +} + +async function writeData(req: Request, res: Response, next: NextFunction) { // Regular Save logic from here await entry.create(req, res, next); - if (!res.locals.error) { + if (!res.locals.error) { res.send(req.query); - } else { - /* at this point error handling already happend, - * or the request has already been send - * therefor there is no need for it again (only middleware to follow at this point) */ + } else { next(); } } const router = express.Router(); -router.get('/', entry.validate, errorChecking); -router.head('/', entry.validate, errorChecking); +router.get('/', slowDown(baseSlowDown), entry.validate, errorChecking, writeData); +router.head('/', slowDown(baseSlowDown), entry.validate, errorChecking); export default router; \ No newline at end of file diff --git a/types.d.ts b/types.d.ts index 7cb4c68..064db1f 100644 --- a/types.d.ts +++ b/types.d.ts @@ -1,6 +1,14 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ +namespace RateLimit { + interface obj { + [key: string]: { + limitReachedOnError: boolean + } + } +} + namespace Response { interface Message { message: string;