= {
...baseOptions,
- limit: 10, // Limit each IP per window
+ limit: 50, // Limit each IP per window
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
+ handler: function rateHandler(req: Request, res: Response, next: NextFunction, options: rateLimiterOptions) {
+ if (!Object.prototype.hasOwnProperty.call(ipsThatReachedLimit, res.locals.ip)) {
+ logger.error(`[RateLimit] reached ${req.originalUrl}, ${res.locals.ip}, ${req.get('User-Agent')}`);
+ ipsThatReachedLimit[res.locals.ip] = { limitReachedOnError: true, time: Date.now() };
+ }
+ res.status(options.statusCode).send(options.message);
+ },
+ message: "Too many requests"
}
@@ -46,14 +52,21 @@ setInterval(() => {
*/
export const baseSlowDown = slowDown(baseSlowDownOptions);
+export const loginSlowDown = slowDown({
+ ...baseSlowDownOptions,
+ delayAfter: 1, // no delay for amount of attempts
+ delayMs: (used: number) => (used - 1) * 250, // Add delay after delayAfter is reached
+ });
+
+export const baseRateLimiter = rateLimit(baseRateLimitOptions);
+
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);
- }
+});
+
+export const loginLimiter = rateLimit({
+ ...baseRateLimitOptions,
+ limit: 3,
+ message: 'Too many attempts without valid login',
});
\ No newline at end of file
diff --git a/src/models/entry.ts b/src/models/entry.ts
index 1f510bf..e7fc107 100644
--- a/src/models/entry.ts
+++ b/src/models/entry.ts
@@ -1,6 +1,6 @@
import { NextFunction, Request, Response } from 'express';
import { checkExact, query } from 'express-validator';
-import { crypt } from '@src/scripts/crypt';
+import { compare } from '@src/scripts/crypt';
import { create as createError } from '@src/middleware/error';
import * as file from '@src/scripts/file';
import { getTime } from '@src/scripts/time';
@@ -51,7 +51,7 @@ export const entry = {
}
} else {
entries.push(entry);
- }
+ }
file.write(res, fileObj, next);
@@ -102,10 +102,6 @@ export function checkTime(value: string) {
throw new Error('Timestamp should represent a valid date');
}
- if (process.env.NODE_ENV == "development") {
- return true; // dev testing convenience
- }
-
const now = new Date();
const difference = now.getTime() - date.getTime();
const oneDayInMilliseconds = 24 * 60 * 60 * 1000;
@@ -117,23 +113,16 @@ export function checkTime(value: string) {
}
-function checkKey(value: string) {
+async function checkKey(value: string) {
+ if (!value) { throw new Error('Key required'); }
+ if (!process.env.KEYB) { throw new Error('Configuration wrong'); }
if (process.env.NODE_ENV != "production" && value == "test") {
return true; // dev testing convenience
}
- if (!value) {
- throw new Error('Key required');
- }
-
- value = decodeURIComponent(value);
+ const result = await compare(decodeURIComponent(value), process.env.KEYB);
- const hash = crypt(value);
-
- if (process.env.KEYB != hash) {
- if (process.env.NODE_ENV == "development") {
- console.log(hash);
- }
+ if (!result) {
throw new Error('Key does not match');
}
diff --git a/src/scripts/crypt.ts b/src/scripts/crypt.ts
index b14ef42..1928c52 100644
--- a/src/scripts/crypt.ts
+++ b/src/scripts/crypt.ts
@@ -1,9 +1,20 @@
-import * as crypto from 'crypto';
+import * as bcrypt from "bcrypt";
+import crypto from "crypto";
-export const crypt = function (value:string) {
+export const crypt = async function (password: string, quick = false) {
+ const extendedPassword = pepper(password);
+ return await bcrypt.hash(extendedPassword, quick ? 8 : 16);
+};
+
+export const compare = async function (password: string, hash: string) {
+ const extendedPassword = pepper(password);
+ return await bcrypt.compare(extendedPassword, hash)
+}
+
+function pepper(password: string) {
const key = process.env.KEYA;
- if (!key) {
- throw new Error('KEYA is not defined in the environment variables');
- }
- return crypto.createHmac('sha256', key).update(value).digest("base64");
-};
\ No newline at end of file
+ if (!key) { throw new Error('KEYA is not defined in the environment variables'); }
+ return password + crypto.createHmac('sha256', key).digest("base64");
+}
+
+
diff --git a/src/scripts/logger.ts b/src/scripts/logger.ts
index 67effdd..ddd608a 100644
--- a/src/scripts/logger.ts
+++ b/src/scripts/logger.ts
@@ -10,17 +10,16 @@ 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) => {
message = JSON.stringify(message);
fs.appendFileSync(logPath, `${date} \t|\t ${message} \n`);
if (showDateInConsole) {
message = `${chalk.dim(date + ":")} ${message}`;
}
- if (process.env.NODE_ENV == "development" || showLogInTest && process.env.NODE_ENV == "test") {
+ if (process.env.NODE_ENV != "production") {
console.log(message);
}
},
@@ -46,7 +45,7 @@ export default {
content = content.replace(prefix[0], chalk.red(prefix[0]));
}
}
- console.error(content); // log string right away or processed Object
+ console.error(content);
}
}
diff --git a/src/tests/app.test.ts b/src/tests/app.test.ts
index b7c01c2..44ba08d 100644
--- a/src/tests/app.test.ts
+++ b/src/tests/app.test.ts
@@ -1,4 +1,10 @@
import axios from 'axios';
+import qs from 'qs';
+
+// random data of 0.75Kb pre GZIP
+const randomData = qs.stringify({
+ randomData: 'zIakHvSaXDdLtaPaL02LhGr4Fk6hzXF7tELeR733YZyyye1fnjNzrSlHgqcHU8BKqvE5Mi4B7iHIEdqjTelpoWyaqXqX8l6LzOvROAkTF4lrLXLD1oMHwDL9hnjR0P7g0BB2DqagKkoEYD4TmXeAXT9PbevbirWnOEzmIgSv65SlsNTRFYhmzWl93twXEBNclHTCTnZpf6diWoo8FsXZR49pe9v8J1paalh2LlbNF4ZUxMxNpSvSTRHxvkYo0TMpd0NqUSSLduLIWcE1jhCWnmHhsbohDZjFfMhVS8IFvCiu7rxfuWgwMPqD9FcBR79eqJBy2tjDMqA9S1k9k50AkbOQ6USVfEuqOtocqXonTvC3Jml90KYSs0gX4SSTFHofpMtbWIdkuKqZbitQjsPSBpTx27dhFZd8zT4erdE1ltHnq83pjEj9hQYqatmdzQGYnOyh9YDt8i1IJpk4DX83DLzw3QhaFPgZFq98SOj4ILytmBMIqOtD464aF8PKGq6g7dVqYOtyF2FwyY0xgA7LjGaFzaCDjnGEcPIMRc2tcorsuRPKUI0zcde1gYPsn4WKaKUp87hJd1YtorzCXPfvivfGGL5v1XaSzApc9BbZpbxcpTOi4Pgvx7hNafUcaCr6kcjp4JVYSktnnGCwEplgGEF8uCELsEBUi9LNhgsnwgoRh55TaJfcaFfGfYLokXYEgiyOwYhhdEY3kfjHZWAyFS4owCR6nMJGOGMHrQi1fBefdp28PQGwgELix5Vf8j6P'
+});
describe('Server Status', () => {
it('The server is running', async () => {
@@ -12,4 +18,25 @@ describe('Server Status', () => {
expect(serverStatus).toBe(200);
})
-})
\ No newline at end of file
+
+ it('server is ignoring body on GET requests', async () => {
+ let serverStatus;
+ try {
+ const response = await axios.request({
+ url: 'http://localhost:80/',
+ method: 'GET',
+ data: randomData,
+ });
+ serverStatus = response.status;
+ } catch (error) {
+ console.error(error);
+ }
+
+ expect(serverStatus).toBe(200);
+ })
+
+})
+
+
+
+
diff --git a/src/tests/integration.test.ts b/src/tests/integration.test.ts
index 6133c1d..2e189e0 100644
--- a/src/tests/integration.test.ts
+++ b/src/tests/integration.test.ts
@@ -1,4 +1,5 @@
import axios, { AxiosError } from 'axios';
+import qs from 'qs';
import fs from "fs";
import path from "path";
@@ -9,6 +10,7 @@ async function callServer(timestamp = new Date().getTime(), query: string, expec
params.set("timestamp", timestamp.toString());
url.search = params.toString();
+
let response;
if (expectStatus == 200) {
if (method == "GET") {
@@ -41,44 +43,67 @@ function isInRange(actual: string | number, expected: number, range: number) {
return Math.abs(Number(actual) - expected) <= range;
}
+async function verifiedRequest(url: string, token: string) {
+ const response = await axios({
+ method: 'get',
+ url: url,
+ headers: {
+ 'Authorization': `Bearer ${token}`,
+ }
+ });
+ return response;
+}
+
+
+
describe('HEAD /write', () => {
+ // eslint-disable-next-line jest/expect-expect
it('with all parameters correctly set it should succeed', async () => {
await callServer(undefined, "user=xx&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0&key=test", 200);
});
+ // eslint-disable-next-line jest/expect-expect
it('without key it sends 403', async () => {
await callServer(undefined, "user=xx&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0", 403);
});
+ // eslint-disable-next-line jest/expect-expect
it('with user length not equal to 2 it sends 422', async () => {
await callServer(undefined, "user=x&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0&key=test", 422);
});
+ // eslint-disable-next-line jest/expect-expect
it('with lat not between -90 and 90 it sends 422', async () => {
await callServer(undefined, "user=xx&lat=91.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0&key=test", 422);
});
+ // eslint-disable-next-line jest/expect-expect
it('with lon not between -180 and 180 it sends 422', async () => {
await callServer(undefined, "user=xx&lat=45.000&lon=181.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0&key=test", 422);
});
+ // eslint-disable-next-line jest/expect-expect
it('with timestamp to old sends 422', async () => {
const timestamp = new Date().getTime() - 24 * 60 * 60 * 1000 * 2; // two days ago
await callServer(timestamp, "user=xx&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=150.000&heading=180.0&key=test", 422);
})
+ // eslint-disable-next-line jest/expect-expect
it('with hdop not between 0 and 100 it sends 422', async () => {
await callServer(undefined, "user=xx&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=101.0&altitude=5000.000&speed=150.000&heading=180.0&key=test", 422);
});
+ // eslint-disable-next-line jest/expect-expect
it('with altitude not between 0 and 10000 it sends 422', async () => {
await callServer(undefined, "user=xx&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=10001.000&speed=150.000&heading=180.0&key=test", 422);
});
+ // eslint-disable-next-line jest/expect-expect
it('with speed not between 0 and 300 it sends 422', async () => {
await callServer(undefined, "user=xx&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=301.000&heading=180.0&key=test", 422);
});
+ // eslint-disable-next-line jest/expect-expect
it('with heading not between 0 and 360 it sends 422', async () => {
await callServer(undefined, "user=xx&lat=45.000&lon=90.000×tamp=R3Pl4C3&hdop=50.0&altitude=5000.000&speed=150.000&heading=361.0&key=test", 422);
});
@@ -92,7 +117,7 @@ describe("GET /write", () => {
const filePath = path.resolve(dirPath, `data-${formattedDate}.json`);
it('there should a file of the current date', async () => {
- await await callServer(undefined, "user=xx&lat=52.51451&lon=13.35105×tamp=R3Pl4C3&hdop=20.0&altitude=5000.000&speed=150.000&heading=180.0&key=test", 200, "GET");
+ await callServer(undefined, "user=xx&lat=52.51451&lon=13.35105×tamp=R3Pl4C3&hdop=20.0&altitude=5000.000&speed=150.000&heading=180.0&key=test", 200, "GET");
fs.access(filePath, fs.constants.F_OK, (err) => {
expect(err).toBeFalsy();
@@ -176,13 +201,14 @@ describe("GET /write", () => {
expect(entry.ignore).toBe(false); // current one to be false allways
expect(lastEntry.ignore).toBe(true); // last one to high hdop to be true
- await await callServer(undefined, "user=xx&lat=52.51627&lon=13.37770×tamp=R3Pl4C3&hdop=50&altitude=4000.000&speed=150.000&heading=180.0&key=test", 200, "GET");
+ await callServer(undefined, "user=xx&lat=52.51627&lon=13.37770×tamp=R3Pl4C3&hdop=50&altitude=4000.000&speed=150.000&heading=180.0&key=test", 200, "GET");
jsonData = getData(filePath);
entry = jsonData.entries[1]; // same data point, but not last now therefore ignore true
expect(entry.ignore).toBe(true);
});
});
+
describe('API calls', () => {
test(`1000 api calls`, async () => {
for (let i = 0; i < 1000; i++) {
@@ -203,15 +229,57 @@ describe('API calls', () => {
});
-describe('/read', () => {
- test(`returns json`, async () => {
- const response = await axios.get("http://localhost:80/read?index=0");
+describe('read and login', () => {
+ let token = "";
+ const testData = qs.stringify({
+ user: "TEST",
+ password: "test",
+ });
+ test(`redirect without logged in`, async () => {
+ try {
+ await axios.get("http://localhost:80/read/");
+ } catch (error) {
+ const axiosError = error as AxiosError;
+ if (axiosError.response) {
+ expect(axiosError.response.status).toBe(401);
+ } else {
+ console.error(axiosError);
+ }
+ }
+ });
+
+ it('test user can login', async () => {
+ const response = await axios.post('http://localhost:80/read/login', testData);
+
+ expect(response.status).toBe(200);
+ expect(response.headers['content-type']).toEqual(expect.stringContaining('application/json'));
+ expect(response).toHaveProperty('data.token');
+ expect(response.data.token).not.toBeNull();
+ token = response.data.token;
+ })
+
+ test('wrong token get error', async () => {
+ try {
+ await verifiedRequest("http://localhost:80/read?index=0", "justWrongValue");
+ } catch (error) {
+ const axiosError = error as AxiosError;
+ if (axiosError.response) {
+ expect(axiosError.response.status).toBe(403);
+ } else {
+ console.error(axiosError);
+ }
+ }
+ });
+
+ test('verified request returns json', async () => {
+ const response = await verifiedRequest("http://localhost:80/read?index=0", token);
expect(response.status).toBe(200);
expect(response.headers['content-type']).toEqual(expect.stringContaining('application/json'));
});
+
test(`index parameter to long`, async () => {
try {
- await axios.get("http://localhost:80/read?index=1234");
+ await verifiedRequest("http://localhost:80/read?index=1234", token);
} catch (error) {
const axiosError = error as AxiosError;
if (axiosError.response) {
@@ -221,9 +289,10 @@ describe('/read', () => {
}
}
});
+
test(`index parameter to be a number`, async () => {
try {
- await axios.get("http://localhost:80/read?index=a9");
+ await verifiedRequest("http://localhost:80/read?index=a9", token);
} catch (error) {
const axiosError = error as AxiosError;
if (axiosError.response) {
@@ -234,7 +303,8 @@ describe('/read', () => {
}
});
test(`index parameter reduces length of json`, async () => {
- const response = await axios.get("http://localhost:80/read?index=999");
+ const response = await verifiedRequest("http://localhost:80/read?index=999", token);
+ expect(response.status).toBe(200);
expect(response.data.entries.length).toBe(1);
});
});
\ No newline at end of file
diff --git a/src/tests/login.test.ts b/src/tests/login.test.ts
new file mode 100644
index 0000000..3fefe2d
--- /dev/null
+++ b/src/tests/login.test.ts
@@ -0,0 +1,58 @@
+import axios, { AxiosError } from 'axios';
+import qs from 'qs';
+
+const userDataLarge = qs.stringify({
+ user: "user",
+ password: "pass",
+ kilobyte: 'BPSwVu5vcvhWB17HcfIdyQK83mHJZKChv7zDihBJoifWK9EJFzK7VYf3kUgIqkc0io8DnSdewzc9U0GpzodQUFz0KLMaogsJruEbNSKvxnzUxS5UqSR64lLOmGumoPcn2InC0Ebpqfdiw90HFVZVlE3AY6Lhgbx8ILHi55RvpuGefDjBsePgow8Jh9sc8uVMCDglLmHQ0zk3PumMj0KlOszbMmX9fG0pPUsvLLc40biPBv9t97K3BFjYd3fGriRAQ3bFhGHBz2wzGbNQfHjKFDHuSvXOw8KReM7Wwd4Cl02QQ3RnDJVwH6cayh4BqFRXlP3i6uXw0l9qxdTv0q1CtV9rJho6zwo04gkGLvsS3AoYJQtHnOtUDdHPExu7l3nMKnPoRUwl7K2ePfHRuppFGqa43Q49bI04VjEhrB9k5S2uZJoxZdm63rIUrydmkZWdvBLVVZUIXwwIRnwLmoa26htKOz9FPKwWIPOM0NZj4jAoPhKqLDJwziNZn5UupzxBXoUM3BIyEk3K8GXs7eBduH9GCK2z2HPF0fJNtGiHASe7jCOC2mhSC5zGf9k0Yu1Ey63oQQZUtT7L57lp7UzPE2p6wzKDlbJZOn0Ho5OUfq3hE2C8fQRO1M6jDvRTiUIKhhxSHYd75Pvh4SG9lD8w5OHASusLDxmzKBUuG4GrGrQYpd0awJkqnKp5lk7psLD22YTtjTuDgI500tQLXSslxI1kIuB8RnN1LsxHyRQMVtXmNFOKKZV2U2frWpImIz2wSHCYrwRGygwDtiFfwtVwTapjhQqUMyb1vrWWi3EL1Y50fDCjDDHlvLI4N2tr2DULFf3a9m2SYWSoE6CYP4og5YyqjhqFQFm9urREInyZi9L0iQoMYxEqxTjGiVJfKmaSChSd0kQz6z2OdsxFbkMWJ2CAHOL1XNK8iFFSp93fIspaNMIonRVDCj4ZIP1LaPHDmIYcYTNU4k3Uz6VBHSIc1VjiG3sc2MZpKw9An0tJVlWbtVSk2RGYWIANAYyr5pQS'
+});
+const userData = qs.stringify({
+ user: "user",
+ password: "pass"
+});
+
+describe('Login', () => {
+ it('form available', async () => {
+ let serverStatus = {};
+ let response = { data: "", status: "" };
+ try {
+ response = await axios.get('http://localhost:80/read/login');
+ serverStatus = response.status;
+ } catch (error) {
+ console.error(error);
+ }
+
+ expect(serverStatus).toBe(200);
+ expect(response.data).toContain('
+
+
+
\ No newline at end of file