Skip to content

Commit

Permalink
[Temp] #41 write route rateLImited
Browse files Browse the repository at this point in the history
temp: see Todos
  • Loading branch information
Type-Style committed Feb 10, 2024
1 parent 22f2896 commit c79dee8
Show file tree
Hide file tree
Showing 3 changed files with 100 additions and 17 deletions.
14 changes: 12 additions & 2 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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({
Expand Down
95 changes: 80 additions & 15 deletions src/controller/write.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,107 @@
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<rateLimiterOptions & slowDownOptions> = {
windowMs: 5 * 60 * 1000,
//skip: (req, res) => (res.locals.ip == process.env.LOCALHOST)
}

const baseSlowDown: Partial<slowDownOptions> = {
...baseOptions,
delayAfter: 3, // no delay for amount of attempts
delayMs: (used: number) => (used - 3) * 125, // Add delay after delayAfter is reached
}

const baseRateLimit: Partial<rateLimiterOptions> = {
...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&timestamp=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") {
res.status(200).end();
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;
8 changes: 8 additions & 0 deletions types.d.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down

0 comments on commit c79dee8

Please sign in to comment.