rate limiting on get, more unit test config
This commit is contained in:
parent
9fcd8e5903
commit
1d824d5f07
@ -37,9 +37,13 @@ services:
|
|||||||
- DATABASE_URL=file:/database/db.sqlite
|
- DATABASE_URL=file:/database/db.sqlite
|
||||||
- FRONTEND_URL=http://localhost:5000
|
- FRONTEND_URL=http://localhost:5000
|
||||||
- CLEANUP_INTERVAL_SECONDS=600
|
- CLEANUP_INTERVAL_SECONDS=600
|
||||||
|
- NODE_ENV=production
|
||||||
|
# Rate limit for uploading notes
|
||||||
- POST_LIMIT_WINDOW_SECONDS=86400
|
- POST_LIMIT_WINDOW_SECONDS=86400
|
||||||
- POST_LIMIT=50
|
- POST_LIMIT=50
|
||||||
- NODE_ENV=production
|
# Rate limit for downloading notes
|
||||||
|
- GET_LIMIT_WINDOW_SECONDS=60
|
||||||
|
- GET_LIMIT=20
|
||||||
depends_on:
|
depends_on:
|
||||||
migrate:
|
migrate:
|
||||||
condition: service_completed_successfully
|
condition: service_completed_successfully
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
|
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
|
||||||
|
|
||||||
# If ENVIRONMENT=dev, CORS is enabled.
|
# If ENVIRONMENT=dev, CORS is enabled.
|
||||||
ENVIRONMENT=dev
|
ENVIRONMENT=test
|
||||||
|
|
||||||
# Port at which the server is hosted.
|
# Port at which the server is hosted.
|
||||||
PORT=8080
|
PORT=8080
|
||||||
@ -20,8 +20,10 @@ DATABASE_URL="file:./dev.sqlite"
|
|||||||
# and expired notes are deleted.
|
# and expired notes are deleted.
|
||||||
CLEANUP_INTERVAL_SECONDS=60
|
CLEANUP_INTERVAL_SECONDS=60
|
||||||
|
|
||||||
# Rate limit window after which post limit is reset
|
# Rate limit for uploading notes.
|
||||||
POST_LIMIT_WINDOW="86400 # 24 hours"
|
POST_LIMIT_WINDOW=86400
|
||||||
|
POST_LIMIT=50
|
||||||
|
|
||||||
# Max. posted notes within rate limit window
|
# Rate limit for downloading notes.
|
||||||
POST_LIMIT=50
|
GET_LIMIT_WINDOW=60
|
||||||
|
GET_LIMIT=20
|
@ -1,5 +1,7 @@
|
|||||||
DATABASE_URL="file:./test.sqlite"
|
DATABASE_URL="file:./test.sqlite"
|
||||||
|
|
||||||
POST_LIMIT=50
|
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
|
LOG_LEVEL=warn
|
@ -7,8 +7,8 @@
|
|||||||
"test": "run-s test:db:reset test:test",
|
"test": "run-s test:db:reset test:test",
|
||||||
"coverage": "run-s test:db:reset test:coverage",
|
"coverage": "run-s test:db:reset test:coverage",
|
||||||
"test-watch": "dotenv -e .env.test -- vitest unit --coverage",
|
"test-watch": "dotenv -e .env.test -- vitest unit --coverage",
|
||||||
"test:test": "dotenv -e .env.test -- vitest run ",
|
"test:test": "dotenv -e .env.test -- vitest run --no-threads",
|
||||||
"test:coverage": "dotenv -e .env.test -- vitest run --coverage",
|
"test:coverage": "dotenv -e .env.test -- vitest run --no-threads --coverage",
|
||||||
"test:db:reset": "dotenv -e .env.test -- npx prisma migrate reset -f",
|
"test:db:reset": "dotenv -e .env.test -- npx prisma migrate reset -f",
|
||||||
"build": "npx tsc",
|
"build": "npx tsc",
|
||||||
"dev": "npx nodemon ./server.ts | npx pino-colada"
|
"dev": "npx nodemon ./server.ts | npx pino-colada"
|
||||||
|
@ -31,13 +31,33 @@ describe("GET /api/note", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("responds 404 for invalid ID", async () => {
|
it("responds 404 for invalid ID", async () => {
|
||||||
// Insert a note
|
|
||||||
// Make get request
|
// Make get request
|
||||||
const res = await request(app).get(`/api/note/NaN`);
|
const res = await request(app).get(`/api/note/NaN`);
|
||||||
|
|
||||||
// Validate returned note
|
// Validate returned note
|
||||||
expect(res.statusCode).toBe(404);
|
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", () => {
|
describe("POST /api/note", () => {
|
||||||
@ -85,6 +105,15 @@ describe("POST /api/note", () => {
|
|||||||
expect(res.body.hmac).toEqual(testNote.hmac);
|
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 () => {
|
it("Applies rate limits to endpoint", async () => {
|
||||||
// make more requests than the post limit set in .env.test
|
// make more requests than the post limit set in .env.test
|
||||||
const requests = [];
|
const requests = [];
|
||||||
@ -96,14 +125,8 @@ describe("POST /api/note", () => {
|
|||||||
|
|
||||||
// at least one response should be 429
|
// at least one response should be 429
|
||||||
expect(responseCodes).toContain(429);
|
expect(responseCodes).toContain(429);
|
||||||
});
|
|
||||||
|
|
||||||
it("Applies upload limit to endpoint of 400kb", async () => {
|
// sleep for 100 ms to allow rate limiter to reset
|
||||||
const largeNote = {
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
ciphertext: "a".repeat(400 * 1024),
|
|
||||||
hmac: "sample_hmac",
|
|
||||||
};
|
|
||||||
const res = await request(app).post("/api/note").send(largeNote);
|
|
||||||
expect(res.statusCode).toBe(413);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -31,15 +31,37 @@ app.use(
|
|||||||
|
|
||||||
// Apply rate limiting
|
// Apply rate limiting
|
||||||
const postLimiter = rateLimit({
|
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
|
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
|
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
|
||||||
legacyHeaders: false, // Disable the `X-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
|
// Apply 400kB upload limit on POST
|
||||||
app.use(bodyParser.json({ limit: "400k" }));
|
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
|
// Post new encrypted note
|
||||||
app.post(
|
app.post(
|
||||||
"/api/note/",
|
"/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
|
// For testing purposes
|
||||||
app.get("/api/test", (req, res, next) => {
|
app.get("/api/test", (req, res, next) => {
|
||||||
res.status(200).send("Hello world!");
|
res.status(200).send("Hello world!");
|
||||||
|
Loading…
Reference in New Issue
Block a user