rate limiting on get, more unit test config

This commit is contained in:
Maxime Cannoodt 2022-07-10 23:00:32 +02:00
parent 9fcd8e5903
commit 1d824d5f07
6 changed files with 72 additions and 34 deletions

View File

@ -37,9 +37,13 @@ services:
- DATABASE_URL=file:/database/db.sqlite
- FRONTEND_URL=http://localhost:5000
- CLEANUP_INTERVAL_SECONDS=600
- NODE_ENV=production
# Rate limit for uploading notes
- POST_LIMIT_WINDOW_SECONDS=86400
- POST_LIMIT=50
- NODE_ENV=production
# Rate limit for downloading notes
- GET_LIMIT_WINDOW_SECONDS=60
- GET_LIMIT=20
depends_on:
migrate:
condition: service_completed_successfully

View File

@ -5,7 +5,7 @@
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
# If ENVIRONMENT=dev, CORS is enabled.
ENVIRONMENT=dev
ENVIRONMENT=test
# Port at which the server is hosted.
PORT=8080
@ -20,8 +20,10 @@ DATABASE_URL="file:./dev.sqlite"
# and expired notes are deleted.
CLEANUP_INTERVAL_SECONDS=60
# Rate limit window after which post limit is reset
POST_LIMIT_WINDOW="86400 # 24 hours"
# Rate limit for uploading notes.
POST_LIMIT_WINDOW=86400
POST_LIMIT=50
# Max. posted notes within rate limit window
POST_LIMIT=50
# Rate limit for downloading notes.
GET_LIMIT_WINDOW=60
GET_LIMIT=20

View File

@ -1,5 +1,7 @@
DATABASE_URL="file:./test.sqlite"
POST_LIMIT=50
POST_LIMIT_WINDOW=1
POST_LIMIT_WINDOW_SECONDS=0.1
GET_LIMIT=20
GET_LIMIT_WINDOW_SECONDS=0.1
LOG_LEVEL=warn

View File

@ -7,8 +7,8 @@
"test": "run-s test:db:reset test:test",
"coverage": "run-s test:db:reset test:coverage",
"test-watch": "dotenv -e .env.test -- vitest unit --coverage",
"test:test": "dotenv -e .env.test -- vitest run ",
"test:coverage": "dotenv -e .env.test -- vitest run --coverage",
"test:test": "dotenv -e .env.test -- vitest run --no-threads",
"test:coverage": "dotenv -e .env.test -- vitest run --no-threads --coverage",
"test:db:reset": "dotenv -e .env.test -- npx prisma migrate reset -f",
"build": "npx tsc",
"dev": "npx nodemon ./server.ts | npx pino-colada"

View File

@ -31,13 +31,33 @@ describe("GET /api/note", () => {
});
it("responds 404 for invalid ID", async () => {
// Insert a note
// Make get request
const res = await request(app).get(`/api/note/NaN`);
// Validate returned note
expect(res.statusCode).toBe(404);
});
it("Applies rate limits to endpoint", async () => {
// Insert a note
const { id } = await prisma.encryptedNote.create({
data: testNote,
});
// Make get requests
const requests = [];
for (let i = 0; i < 51; i++) {
requests.push(request(app).get(`/api/note/${id}`));
}
const responses = await Promise.all(requests);
const responseCodes = responses.map((res) => res.statusCode);
// at least one response should be 429
expect(responseCodes).toContain(429);
// sleep for 100 ms to allow rate limiter to reset
await new Promise((resolve) => setTimeout(resolve, 100));
});
});
describe("POST /api/note", () => {
@ -85,6 +105,15 @@ describe("POST /api/note", () => {
expect(res.body.hmac).toEqual(testNote.hmac);
});
it("Applies upload limit to endpoint of 400kb", async () => {
const largeNote = {
ciphertext: "a".repeat(400 * 1024),
hmac: "sample_hmac",
};
const res = await request(app).post("/api/note").send(largeNote);
expect(res.statusCode).toBe(413);
});
it("Applies rate limits to endpoint", async () => {
// make more requests than the post limit set in .env.test
const requests = [];
@ -96,14 +125,8 @@ describe("POST /api/note", () => {
// at least one response should be 429
expect(responseCodes).toContain(429);
});
it("Applies upload limit to endpoint of 400kb", async () => {
const largeNote = {
ciphertext: "a".repeat(400 * 1024),
hmac: "sample_hmac",
};
const res = await request(app).post("/api/note").send(largeNote);
expect(res.statusCode).toBe(413);
// sleep for 100 ms to allow rate limiter to reset
await new Promise((resolve) => setTimeout(resolve, 100));
});
});

View File

@ -31,15 +31,37 @@ app.use(
// Apply rate limiting
const postLimiter = rateLimit({
windowMs: parseInt(process.env.POST_LIMIT_WINDOW_SECONDS as string) * 1000,
windowMs: parseFloat(process.env.POST_LIMIT_WINDOW_SECONDS as string) * 1000,
max: parseInt(process.env.POST_LIMIT as string), // Limit each IP to X requests per window
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
});
const getLimiter = rateLimit({
windowMs: parseFloat(process.env.GET_LIMIT_WINDOW_SECONDS as string) * 1000,
max: parseInt(process.env.GET_LIMIT as string), // Limit each IP to X requests per window
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
});
// Apply 400kB upload limit on POST
app.use(bodyParser.json({ limit: "400k" }));
// Get encrypted note
app.get("/api/note/:id", getLimiter, (req, res, next) => {
prisma.encryptedNote
.findUnique({
where: { id: req.params.id },
})
.then((note) => {
if (note != null) {
res.send(note);
}
res.status(404).send();
})
.catch(next);
});
// Post new encrypted note
app.post(
"/api/note/",
@ -60,21 +82,6 @@ app.post(
}
);
// Get encrypted note
app.get("/api/note/:id", (req, res, next) => {
prisma.encryptedNote
.findUnique({
where: { id: req.params.id },
})
.then((note) => {
if (note != null) {
res.send(note);
}
res.status(404).send();
})
.catch(next);
});
// For testing purposes
app.get("/api/test", (req, res, next) => {
res.status(200).send("Hello world!");