diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a755f3f3..5eaaa852 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -24,7 +24,7 @@ jobs: - name: Start server run: | sudo npm start & - sleep 6 # Give server some time to start + sleep 8 # Give server some time to start - name: Check if server is running run: | curl --fail http://localhost:80 || exit 1 diff --git a/package-lock.json b/package-lock.json index 4c0b3553..085305f0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,21 +9,28 @@ "version": "0.0.1", "dependencies": { "chalk": "^4.1.2", + "compression": "^1.7.4", "express": "^4.18.2", + "express-rate-limit": "^7.1.5", + "express-slow-down": "^2.0.1", "express-validator": "^7.0.1", "helmet": "^7.1.0", "hpp": "^0.2.3", - "module-alias": "^2.2.3" + "module-alias": "^2.2.3", + "raw-body": "^2.5.2", + "toobusy-js": "^0.5.1" }, "devDependencies": { "@jest/globals": "^29.7.0", "@tsconfig/node20": "^20.1.2", "@types/bcrypt": "^5.0.2", + "@types/compression": "^1.7.5", "@types/dotenv": "^8.2.0", "@types/express": "^4.17.21", "@types/hpp": "^0.2.5", "@types/jest": "^29.5.11", "@types/node": "^20.10.6", + "@types/toobusy-js": "^0.5.4", "@typescript-eslint/eslint-plugin": "^6.18.1", "@typescript-eslint/parser": "^6.18.1", "axios": "^1.6.5", @@ -1513,6 +1520,15 @@ "@types/node": "*" } }, + "node_modules/@types/compression": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@types/compression/-/compression-1.7.5.tgz", + "integrity": "sha512-AAQvK5pxMpaT+nDvhHrsBhLSYG5yQdtkaJE1WYieSNY2mVFKAgmU4ks65rkZD5oqnGCFLyQpUr1CqI4DmUMyDg==", + "dev": true, + "dependencies": { + "@types/express": "*" + } + }, "node_modules/@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", @@ -1680,6 +1696,12 @@ "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", "dev": true }, + "node_modules/@types/toobusy-js": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@types/toobusy-js/-/toobusy-js-0.5.4.tgz", + "integrity": "sha512-hsKMbYiaL3ZWx7B3FYyN0rEJexw7I1HgKbNToX3ZZJv6373to954wlA7zrXR3/XoVwZnFwWqFguBs91sNzJGKQ==", + "dev": true + }, "node_modules/@types/yargs": { "version": "17.0.32", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", @@ -2306,6 +2328,20 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/body-parser/node_modules/raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -2595,6 +2631,47 @@ "node": ">= 0.8" } }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "dependencies": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/compression/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -3348,6 +3425,34 @@ "node": ">= 0.10.0" } }, + "node_modules/express-rate-limit": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.1.5.tgz", + "integrity": "sha512-/iVogxu7ueadrepw1bS0X0kaRC/U0afwiYRSLg68Ts+p4Dc85Q5QKsOnPS/QUjPMHvOJQtBDrZgvkOzf8ejUYw==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": "4 || 5 || ^5.0.0-beta.1" + } + }, + "node_modules/express-slow-down": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/express-slow-down/-/express-slow-down-2.0.1.tgz", + "integrity": "sha512-zRogSZhNXJYKDBekhgFfFXGrOngH7Fub7Mx2g8OQ4RUBwSJP/3TVEKMgSGR/WlneT0mJ6NBUnidHhIELGVPe3w==", + "dependencies": { + "express-rate-limit": "7" + }, + "engines": { + "node": ">= 16" + }, + "peerDependencies": { + "express": ">= 4" + } + }, "node_modules/express-validator": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.0.1.tgz", @@ -5122,6 +5227,14 @@ "node": ">= 0.8" } }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -5518,9 +5631,9 @@ } }, "node_modules/raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", @@ -6031,6 +6144,14 @@ "node": ">=0.6" } }, + "node_modules/toobusy-js": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/toobusy-js/-/toobusy-js-0.5.1.tgz", + "integrity": "sha512-GiCux/c8G2TV0FTDgtxnXOxmSAndaI/9b1YxT14CqyeBDtTZAcJLx9KlXT3qECi8D0XCc78T4sN/7gWtjRyCaA==", + "engines": { + "node": ">=0.9.1" + } + }, "node_modules/touch": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", diff --git a/package.json b/package.json index 7e450e91..8b597505 100644 --- a/package.json +++ b/package.json @@ -18,11 +18,13 @@ "@jest/globals": "^29.7.0", "@tsconfig/node20": "^20.1.2", "@types/bcrypt": "^5.0.2", + "@types/compression": "^1.7.5", "@types/dotenv": "^8.2.0", "@types/express": "^4.17.21", "@types/hpp": "^0.2.5", "@types/jest": "^29.5.11", "@types/node": "^20.10.6", + "@types/toobusy-js": "^0.5.4", "@typescript-eslint/eslint-plugin": "^6.18.1", "@typescript-eslint/parser": "^6.18.1", "axios": "^1.6.5", @@ -37,11 +39,16 @@ }, "dependencies": { "chalk": "^4.1.2", + "compression": "^1.7.4", "express": "^4.18.2", + "express-rate-limit": "^7.1.5", + "express-slow-down": "^2.0.1", "express-validator": "^7.0.1", "helmet": "^7.1.0", "hpp": "^0.2.3", - "module-alias": "^2.2.3" + "module-alias": "^2.2.3", + "raw-body": "^2.5.2", + "toobusy-js": "^0.5.1" }, "_moduleAliases": { "@src": "dist" diff --git a/src/app.ts b/src/app.ts index 0f5b50a1..1e59d969 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,58 +1,117 @@ require('module-alias/register'); import { config } from 'dotenv'; import express from 'express'; +import toobusy from 'toobusy-js'; +// import { rateLimit } from 'express-rate-limit'; +// import { slowDown } from 'express-slow-down'; +import compression from 'compression'; import helmet from 'helmet'; import hpp from 'hpp'; -import cache from './cache'; -import * as error from "./error"; +import getRawBody from 'raw-body'; +import cache from './middleware/cache'; +import * as error from "./middleware/error"; import writeRouter from '@src/controller/write'; import readRouter from '@src/controller/read'; -import path from 'path'; +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(); +config(); // dotenv + const app = express(); -app.use( - helmet({ - contentSecurityPolicy: { - directives: { - "default-src": "'self'", - "img-src": "*" - } - } - }) -); -app.use(hpp()); +app.use((req, res, next) => { // monitor eventloop to block requests if busy + if (toobusy()) { + res.status(503).set({ 'Cache-Control': 'no-cache, no-store, must-revalidate', 'Retry-After': '60' }).send("I'm busy right now, sorry."); + } 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(); + } else { + const message = "No IP provided" + logger.error(message); + res.status(400).send(message); + } + +}) + +// const slowDownLimiter = slowDown({ +// windowMs: 1 * 60 * 1000, +// delayAfter: 5, // Allow 5 requests per 15 minutes. +// delayMs: (used) => (used - 5) * 1000, // Add delay after delayAfter is reached +// }) + +// const rateLimiter = rateLimit({ +// windowMs: 1 * 60 * 1000, +// limit: 10, // Limit each IP per `window` +// standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers +// legacyHeaders: false, // Disable the `X-RateLimit-*` headers +// }) + +app.use(helmet({ contentSecurityPolicy: { directives: { "default-src": "'self'", "img-src": "*" } } })); app.use(cache); +app.use(compression()) +app.use(hpp()); +app.use(function (req, res, next) { // limit request size limit when recieving data + if (!['POST', 'PUT', 'DELETE'].includes(req.method)) { return next(); } + getRawBody(req, { length: req.headers['content-length'], limit: '1mb', encoding: true }, + function (err) { + if (err) { return next(err) } + next() + } + ) +}) // routes app.get('/', (req, res) => { - res.send('Hello World, via TypeScript and Node.js!'); + console.log(req.ip + " - " + res.locals.ip); + res.send('Hello World, via TypeScript and Node.js! ' + res.locals.ip); }); - app.use('/write', writeRouter); app.use('/read', readRouter); // use httpdocs as static folder app.use('/', express.static(path.join(__dirname, 'httpdocs'), { extensions: ['html', 'txt', "pdf"], - index: "start.html", -})) + index: ["start.html", "start.txt"], +})); // error handling app.use(error.notFound); app.use(error.handler); // init server -app.listen(80, () => { - logger.log(`Server running //localhost:80, ENV: ${process.env.NODE_ENV}`, true); +const server = app.listen(80, () => { + logger.log(`Server running //localhost:80, ENV: ${process.env.NODE_ENV}`, true); +}); + +// catching shutdowns +['SIGINT', 'SIGTERM', 'exit'].forEach((signal) => { + process.on(signal, () => { + function logAndExit() { + // calling .shutdown allows your process to exit normally + toobusy.shutdown(); + logger.log(`Server shutdown on signal: ${signal} //localhost:80`, true); + process.exit(); + } + if (signal != "exit") { // give the server time to shutdown before closing + server.close(logAndExit); + } else { + logger.log(`Server shutdown immediate: ${signal} //localhost:80`, true); + } + }); }); -process.on('uncaughtException', function(err) { +// last resort error handling +process.on('uncaughtException', function (err) { console.error('Caught exception:', err); logger.error(err); + server.close(); process.exit(1); }); \ No newline at end of file diff --git a/src/controller/read.ts b/src/controller/read.ts index f17c93c9..02294dc6 100644 --- a/src/controller/read.ts +++ b/src/controller/read.ts @@ -1,6 +1,6 @@ import express, { Request, Response, NextFunction } from 'express'; import * as file from '@src/scripts/file'; -import { create as createError } from '@src/error'; +import { create as createError } from '@src/middleware/error'; import { validationResult, query } from 'express-validator'; const router = express.Router(); diff --git a/src/controller/write.ts b/src/controller/write.ts index dc0658d2..7b405ffd 100644 --- a/src/controller/write.ts +++ b/src/controller/write.ts @@ -1,19 +1,25 @@ 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/middleware/error'; +import { baseSlowDown, errorRateLimiter } from '@src/middleware/limit'; // 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 +27,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('/', baseSlowDown, entry.validate, errorChecking, writeData); +router.head('/', baseSlowDown, entry.validate, errorChecking); export default router; \ No newline at end of file diff --git a/src/cache.ts b/src/middleware/cache.ts similarity index 100% rename from src/cache.ts rename to src/middleware/cache.ts diff --git a/src/error.ts b/src/middleware/error.ts similarity index 96% rename from src/error.ts rename to src/middleware/error.ts index 7b8d624d..2eaf6e8d 100644 --- a/src/error.ts +++ b/src/middleware/error.ts @@ -29,7 +29,7 @@ export function handler(err: Error, req: Request, res: Response message = err.message; } - const responseBody = { + const responseBody:Response.Error = { status: statusCode, name: err.name, message: message, diff --git a/src/middleware/limit.ts b/src/middleware/limit.ts new file mode 100644 index 00000000..a73e6c82 --- /dev/null +++ b/src/middleware/limit.ts @@ -0,0 +1,59 @@ +import { Request, Response, NextFunction } from 'express'; +import { rateLimit, Options as rateLimiterOptions } from 'express-rate-limit'; +import { slowDown, Options as slowDownOptions } from 'express-slow-down'; +import logger from '@src/scripts/logger'; + + +/* +** configurations +*/ + +const baseOptions: Partial = { + windowMs: 30 * 60 * 1000, + skip: (req, res) => (res.locals.ip == "127.0.0.1" || res.locals.ip == "::1") +} + +const baseSlowDownOptions: Partial = { + ...baseOptions, + delayAfter: 3, // no delay for amount of attempts + delayMs: (used: number) => (used - 3) * 125, // Add delay after delayAfter is reached +} + +const baseRateLimitOptions: 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 +} + + +/* +** cleanup +*/ +const ipsThatReachedLimit: RateLimit.obj = {}; // prevent logs from flooding +setInterval(() => { + const oneHourAgo = Date.now() - 60 * 60 * 1000; + for (const ip in ipsThatReachedLimit) { + if (ipsThatReachedLimit[ip].time < oneHourAgo) { + delete ipsThatReachedLimit[ip]; + } + } +}, 60 * 60 * 1000); + + +/* +** exported section +*/ +export const baseSlowDown = slowDown(baseSlowDownOptions); + +export const errorRateLimiter = rateLimit({ + ...baseRateLimitOptions, + 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, time: Date.now() }; + } + res.status(options.statusCode).send(options.message); + } +}); \ No newline at end of file diff --git a/src/models/entry.ts b/src/models/entry.ts index 4305fea3..1f510bf8 100644 --- a/src/models/entry.ts +++ b/src/models/entry.ts @@ -1,7 +1,7 @@ import { NextFunction, Request, Response } from 'express'; import { checkExact, query } from 'express-validator'; import { crypt } from '@src/scripts/crypt'; -import { create as createError } from '@src/error'; +import { create as createError } from '@src/middleware/error'; import * as file from '@src/scripts/file'; import { getTime } from '@src/scripts/time'; import { getSpeed } from '@src/scripts/speed'; diff --git a/src/scripts/file.ts b/src/scripts/file.ts index 5fdec3fd..a8b693b0 100644 --- a/src/scripts/file.ts +++ b/src/scripts/file.ts @@ -1,7 +1,7 @@ import fs from 'fs'; import path from 'path'; import { promisify } from 'util'; -import { create as createError } from '@src/error'; +import { create as createError } from '@src/middleware/error'; import { NextFunction, Response } from 'express'; import logger from '@src/scripts/logger'; diff --git a/src/scripts/logger.ts b/src/scripts/logger.ts index 0beeaea2..67effdd9 100644 --- a/src/scripts/logger.ts +++ b/src/scripts/logger.ts @@ -1,13 +1,20 @@ // primitive text logger -import fs from 'fs'; // typescript will compile to require -import path from 'path'; // typescript will compile to require -import chalk from "chalk"; // keep import syntax after compile +import fs from 'fs'; +import path from 'path'; +import chalk from "chalk"; -const logPath = path.resolve(__dirname, '../httpdocs', 'log.txt'); +const dirPath = path.resolve(__dirname, '../httpdocs/log'); +const logPath = path.resolve(dirPath, 'start.txt'); + +if (!fs.existsSync(dirPath)) { + fs.mkdirSync(dirPath, { recursive: true }); +} + +// const logPath = path.resolve(__dirname, '../httpdocs/log', 'start.txt'); const date = new Date().toLocaleString('de-DE', { hour12: false }); export default { - log: (message:string|JSON, showDateInConsole:boolean=false, showLogInTest=false) => { + log: (message: string | JSON, showDateInConsole: boolean = false, showLogInTest = false) => { message = JSON.stringify(message); fs.appendFileSync(logPath, `${date} \t|\t ${message} \n`); if (showDateInConsole) { @@ -17,8 +24,29 @@ export default { console.log(message); } }, - error: (message:string|JSON|Response.Error) => { - fs.appendFileSync(logPath, `${date} \t|\t ERROR: ${message} \n`); - console.error(message); + error: (content: string | Response.Error) => { + // logfile + const applyErrorPrefix = !/^\[\w+\]/.test(typeof content == "string" ? content : content.message); + const logMessageTemplate = `${date} \t|\t${applyErrorPrefix ? ' [ERROR]' : ''} ${typeof content == "string" ? content : JSON.stringify(content.message) } \n`; + fs.appendFileSync(logPath, logMessageTemplate); + if (process.env.NODE_ENV == "production") { return; } + + // console + if (typeof content != "string" && Object.hasOwnProperty.call(content, "message")) { + const messageAsString = JSON.stringify(content.message); + if (content.stack) { // replace redundant information + content.stack = content.stack.replace(messageAsString, ""); + } + const consoleMessage = structuredClone(content); // create clone so response output is not "further" affected + consoleMessage.message = messageAsString; // gitbash output improvement (w/o objects in arrays appear as [Object]) + content = consoleMessage; + } else if (typeof content == "string") { + const prefix = content.match(/^\[\w+\]/); + if (prefix?.length) { + content = content.replace(prefix[0], chalk.red(prefix[0])); + } + } + console.error(content); // log string right away or processed Object + } } diff --git a/src/tests/integration.test.ts b/src/tests/integration.test.ts index 2d40ced0..6133c1d1 100644 --- a/src/tests/integration.test.ts +++ b/src/tests/integration.test.ts @@ -129,7 +129,7 @@ describe("GET /write", () => { expect(entry.time.created).toBeGreaterThan(date.getTime()); expect(entry.time.diff).toBeGreaterThan(3.5); - expect(entry.time.diff).toBeLessThan(4); + expect(entry.time.diff).toBeLessThan(4.6); const germanDayPattern = "(Montag|Dienstag|Mittwoch|Donnerstag|Freitag|Samstag|Sonntag)"; diff --git a/types.d.ts b/types.d.ts index 7cb4c689..8931ce5b 100644 --- a/types.d.ts +++ b/types.d.ts @@ -1,6 +1,15 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ +namespace RateLimit { + interface obj { + [key: string]: { + limitReachedOnError: boolean, + time: number + } + } +} + namespace Response { interface Message { message: string;