From 7fba908b7cde3997cec5ef39d0794829b348e6f6 Mon Sep 17 00:00:00 2001 From: Ishaan Dey Date: Sun, 5 May 2024 12:55:34 -0700 Subject: [PATCH] add basic ratelimiting --- backend/database/src/index.ts | 6 + backend/server/package-lock.json | 6 + backend/server/package.json | 1 + backend/server/src/index.ts | 128 ++++++++++++++------- backend/server/src/ratelimit.ts | 21 ++++ frontend/components/dashboard/index.tsx | 9 +- frontend/components/editor/index.tsx | 11 +- frontend/components/editor/sidebar/new.tsx | 12 +- frontend/lib/utils.ts | 9 +- 9 files changed, 148 insertions(+), 55 deletions(-) create mode 100644 backend/server/src/ratelimit.ts diff --git a/backend/database/src/index.ts b/backend/database/src/index.ts index 993ce41..4162019 100644 --- a/backend/database/src/index.ts +++ b/backend/database/src/index.ts @@ -9,6 +9,7 @@ import { and, eq } from "drizzle-orm"; export interface Env { DB: D1Database; + RL: any; } // https://github.com/drizzle-team/drizzle-orm/tree/main/examples/cloudflare-d1 @@ -78,6 +79,11 @@ export default { const body = await request.json(); const { type, name, userId, visibility } = initSchema.parse(body); + const allSandboxes = await db.select().from(sandbox).all(); + if (allSandboxes.length >= 8) { + return new Response("You reached the maximum # of sandboxes.", { status: 400 }); + } + const sb = await db.insert(sandbox).values({ type, name, userId, visibility }).returning().get(); // console.log("sb:", sb); diff --git a/backend/server/package-lock.json b/backend/server/package-lock.json index c412099..051c87b 100644 --- a/backend/server/package-lock.json +++ b/backend/server/package-lock.json @@ -13,6 +13,7 @@ "dotenv": "^16.4.5", "express": "^4.19.2", "node-pty": "^1.0.0", + "rate-limiter-flexible": "^5.0.3", "socket.io": "^4.7.5", "zod": "^3.22.4" }, @@ -1167,6 +1168,11 @@ "node": ">= 0.6" } }, + "node_modules/rate-limiter-flexible": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/rate-limiter-flexible/-/rate-limiter-flexible-5.0.3.tgz", + "integrity": "sha512-lWx2y8NBVlTOLPyqs+6y7dxfEpT6YFqKy3MzWbCy95sTTOhOuxufP2QvRyOHpfXpB9OUJPbVLybw3z3AVAS5fA==" + }, "node_modules/raw-body": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", diff --git a/backend/server/package.json b/backend/server/package.json index 82a6650..efa7f52 100644 --- a/backend/server/package.json +++ b/backend/server/package.json @@ -15,6 +15,7 @@ "dotenv": "^16.4.5", "express": "^4.19.2", "node-pty": "^1.0.0", + "rate-limiter-flexible": "^5.0.3", "socket.io": "^4.7.5", "zod": "^3.22.4" }, diff --git a/backend/server/src/index.ts b/backend/server/src/index.ts index 039a261..9b7bf69 100644 --- a/backend/server/src/index.ts +++ b/backend/server/src/index.ts @@ -16,6 +16,12 @@ import { saveFile, } from "./utils" import { IDisposable, IPty, spawn } from "node-pty" +import { + createFileRL, + deleteFileRL, + renameFileRL, + saveFileRL, +} from "./ratelimit" dotenv.config() @@ -107,71 +113,107 @@ io.on("connection", async (socket) => { // todo: send diffs + debounce for efficiency socket.on("saveFile", async (fileId: string, body: string) => { - const file = sandboxFiles.fileData.find((f) => f.id === fileId) - if (!file) return - file.data = body + try { + await saveFileRL.consume(data.userId, 1) - fs.writeFile(path.join(dirName, file.id), body, function (err) { - if (err) throw err - }) - await saveFile(fileId, body) + const file = sandboxFiles.fileData.find((f) => f.id === fileId) + if (!file) return + file.data = body + + fs.writeFile(path.join(dirName, file.id), body, function (err) { + if (err) throw err + }) + await saveFile(fileId, body) + } catch (e) { + socket.emit("rateLimit", "Rate limited: file saving. Please slow down.") + } }) socket.on("createFile", async (name: string) => { - const id = `projects/${data.id}/${name}` + try { + await createFileRL.consume(data.userId, 1) - fs.writeFile(path.join(dirName, id), "", function (err) { - if (err) throw err - }) + const id = `projects/${data.id}/${name}` - sandboxFiles.files.push({ - id, - name, - type: "file", - }) + fs.writeFile(path.join(dirName, id), "", function (err) { + if (err) throw err + }) - sandboxFiles.fileData.push({ - id, - data: "", - }) + sandboxFiles.files.push({ + id, + name, + type: "file", + }) - await createFile(id) + sandboxFiles.fileData.push({ + id, + data: "", + }) + + await createFile(id) + } catch (e) { + socket.emit("rateLimit", "Rate limited: file creation. Please slow down.") + } }) socket.on("renameFile", async (fileId: string, newName: string) => { - const file = sandboxFiles.fileData.find((f) => f.id === fileId) - if (!file) return - file.id = newName + try { + await renameFileRL.consume(data.userId, 1) - const parts = fileId.split("/") - const newFileId = parts.slice(0, parts.length - 1).join("/") + "/" + newName + const file = sandboxFiles.fileData.find((f) => f.id === fileId) + if (!file) return + file.id = newName - fs.rename( - path.join(dirName, fileId), - path.join(dirName, newFileId), - function (err) { - if (err) throw err - } - ) - await renameFile(fileId, newFileId, file.data) + const parts = fileId.split("/") + const newFileId = + parts.slice(0, parts.length - 1).join("/") + "/" + newName + + fs.rename( + path.join(dirName, fileId), + path.join(dirName, newFileId), + function (err) { + if (err) throw err + } + ) + await renameFile(fileId, newFileId, file.data) + } catch (e) { + socket.emit("rateLimit", "Rate limited: file renaming. Please slow down.") + return + } }) socket.on("deleteFile", async (fileId: string, callback) => { - const file = sandboxFiles.fileData.find((f) => f.id === fileId) - if (!file) return + try { + await deleteFileRL.consume(data.userId, 1) + const file = sandboxFiles.fileData.find((f) => f.id === fileId) + if (!file) return - fs.unlink(path.join(dirName, fileId), function (err) { - if (err) throw err - }) - sandboxFiles.fileData = sandboxFiles.fileData.filter((f) => f.id !== fileId) + fs.unlink(path.join(dirName, fileId), function (err) { + if (err) throw err + }) + sandboxFiles.fileData = sandboxFiles.fileData.filter( + (f) => f.id !== fileId + ) - await deleteFile(fileId) + await deleteFile(fileId) - const newFiles = await getSandboxFiles(data.id) - callback(newFiles.files) + const newFiles = await getSandboxFiles(data.id) + callback(newFiles.files) + } catch (e) { + socket.emit("rateLimit", "Rate limited: file deletion. Please slow down.") + } }) socket.on("createTerminal", ({ id }: { id: string }) => { + if (terminals[id]) { + console.log("Terminal already exists.") + return + } + if (Object.keys(terminals).length >= 4) { + console.log("Too many terminals.") + return + } + const pty = spawn(os.platform() === "win32" ? "cmd.exe" : "bash", [], { name: "xterm", cols: 100, diff --git a/backend/server/src/ratelimit.ts b/backend/server/src/ratelimit.ts new file mode 100644 index 0000000..7fadd34 --- /dev/null +++ b/backend/server/src/ratelimit.ts @@ -0,0 +1,21 @@ +import { RateLimiterMemory } from "rate-limiter-flexible" + +export const saveFileRL = new RateLimiterMemory({ + points: 3, + duration: 1, +}) + +export const createFileRL = new RateLimiterMemory({ + points: 3, + duration: 1, +}) + +export const renameFileRL = new RateLimiterMemory({ + points: 3, + duration: 1, +}) + +export const deleteFileRL = new RateLimiterMemory({ + points: 3, + duration: 1, +}) diff --git a/frontend/components/dashboard/index.tsx b/frontend/components/dashboard/index.tsx index b3086e6..32b7f86 100644 --- a/frontend/components/dashboard/index.tsx +++ b/frontend/components/dashboard/index.tsx @@ -18,6 +18,7 @@ import NewProjectModal from "./newProject" import Link from "next/link" import { useSearchParams } from "next/navigation" import AboutModal from "./about" +import { toast } from "sonner" type TScreen = "projects" | "shared" | "settings" | "search" @@ -58,7 +59,13 @@ export default function Dashboard({
setNewProjectModalOpen(true)} + onClick={() => { + if (sandboxes.length >= 8) { + toast.error("You reached the maximum # of sandboxes.") + return + } + setNewProjectModalOpen(true) + }} className="mb-4" > diff --git a/frontend/components/editor/index.tsx b/frontend/components/editor/index.tsx index 391c40d..3bf7b7b 100644 --- a/frontend/components/editor/index.tsx +++ b/frontend/components/editor/index.tsx @@ -372,6 +372,7 @@ export default function CodeEditor({ const selectFile = (tab: TTab) => { if (tab.id === activeId) return const exists = tabs.find((t) => t.id === tab.id) + setTabs((prev) => { if (exists) { setActiveId(exists.id) @@ -420,8 +421,9 @@ export default function CodeEditor({ oldName: string, type: "file" | "folder" ) => { - if (!validateName(newName, oldName, type)) { - toast.error("Invalid file name.") + const valid = validateName(newName, oldName, type) + if (!valid.status) { + if (valid.message) toast.error("Invalid file name.") return false } @@ -633,6 +635,11 @@ export default function CodeEditor({ Shell