add basic ratelimiting

This commit is contained in:
Ishaan Dey 2024-05-05 12:55:34 -07:00
parent 5ae5c226ba
commit 7fba908b7c
9 changed files with 148 additions and 55 deletions

View File

@ -9,6 +9,7 @@ import { and, eq } from "drizzle-orm";
export interface Env { export interface Env {
DB: D1Database; DB: D1Database;
RL: any;
} }
// https://github.com/drizzle-team/drizzle-orm/tree/main/examples/cloudflare-d1 // https://github.com/drizzle-team/drizzle-orm/tree/main/examples/cloudflare-d1
@ -78,6 +79,11 @@ export default {
const body = await request.json(); const body = await request.json();
const { type, name, userId, visibility } = initSchema.parse(body); 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(); const sb = await db.insert(sandbox).values({ type, name, userId, visibility }).returning().get();
// console.log("sb:", sb); // console.log("sb:", sb);

View File

@ -13,6 +13,7 @@
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"express": "^4.19.2", "express": "^4.19.2",
"node-pty": "^1.0.0", "node-pty": "^1.0.0",
"rate-limiter-flexible": "^5.0.3",
"socket.io": "^4.7.5", "socket.io": "^4.7.5",
"zod": "^3.22.4" "zod": "^3.22.4"
}, },
@ -1167,6 +1168,11 @@
"node": ">= 0.6" "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": { "node_modules/raw-body": {
"version": "2.5.2", "version": "2.5.2",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",

View File

@ -15,6 +15,7 @@
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"express": "^4.19.2", "express": "^4.19.2",
"node-pty": "^1.0.0", "node-pty": "^1.0.0",
"rate-limiter-flexible": "^5.0.3",
"socket.io": "^4.7.5", "socket.io": "^4.7.5",
"zod": "^3.22.4" "zod": "^3.22.4"
}, },

View File

@ -16,6 +16,12 @@ import {
saveFile, saveFile,
} from "./utils" } from "./utils"
import { IDisposable, IPty, spawn } from "node-pty" import { IDisposable, IPty, spawn } from "node-pty"
import {
createFileRL,
deleteFileRL,
renameFileRL,
saveFileRL,
} from "./ratelimit"
dotenv.config() dotenv.config()
@ -107,6 +113,9 @@ io.on("connection", async (socket) => {
// todo: send diffs + debounce for efficiency // todo: send diffs + debounce for efficiency
socket.on("saveFile", async (fileId: string, body: string) => { socket.on("saveFile", async (fileId: string, body: string) => {
try {
await saveFileRL.consume(data.userId, 1)
const file = sandboxFiles.fileData.find((f) => f.id === fileId) const file = sandboxFiles.fileData.find((f) => f.id === fileId)
if (!file) return if (!file) return
file.data = body file.data = body
@ -115,9 +124,15 @@ io.on("connection", async (socket) => {
if (err) throw err if (err) throw err
}) })
await saveFile(fileId, body) await saveFile(fileId, body)
} catch (e) {
socket.emit("rateLimit", "Rate limited: file saving. Please slow down.")
}
}) })
socket.on("createFile", async (name: string) => { socket.on("createFile", async (name: string) => {
try {
await createFileRL.consume(data.userId, 1)
const id = `projects/${data.id}/${name}` const id = `projects/${data.id}/${name}`
fs.writeFile(path.join(dirName, id), "", function (err) { fs.writeFile(path.join(dirName, id), "", function (err) {
@ -136,15 +151,22 @@ io.on("connection", async (socket) => {
}) })
await createFile(id) await createFile(id)
} catch (e) {
socket.emit("rateLimit", "Rate limited: file creation. Please slow down.")
}
}) })
socket.on("renameFile", async (fileId: string, newName: string) => { socket.on("renameFile", async (fileId: string, newName: string) => {
try {
await renameFileRL.consume(data.userId, 1)
const file = sandboxFiles.fileData.find((f) => f.id === fileId) const file = sandboxFiles.fileData.find((f) => f.id === fileId)
if (!file) return if (!file) return
file.id = newName file.id = newName
const parts = fileId.split("/") const parts = fileId.split("/")
const newFileId = parts.slice(0, parts.length - 1).join("/") + "/" + newName const newFileId =
parts.slice(0, parts.length - 1).join("/") + "/" + newName
fs.rename( fs.rename(
path.join(dirName, fileId), path.join(dirName, fileId),
@ -154,24 +176,44 @@ io.on("connection", async (socket) => {
} }
) )
await renameFile(fileId, newFileId, file.data) 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) => { socket.on("deleteFile", async (fileId: string, callback) => {
try {
await deleteFileRL.consume(data.userId, 1)
const file = sandboxFiles.fileData.find((f) => f.id === fileId) const file = sandboxFiles.fileData.find((f) => f.id === fileId)
if (!file) return if (!file) return
fs.unlink(path.join(dirName, fileId), function (err) { fs.unlink(path.join(dirName, fileId), function (err) {
if (err) throw err if (err) throw err
}) })
sandboxFiles.fileData = sandboxFiles.fileData.filter((f) => f.id !== fileId) sandboxFiles.fileData = sandboxFiles.fileData.filter(
(f) => f.id !== fileId
)
await deleteFile(fileId) await deleteFile(fileId)
const newFiles = await getSandboxFiles(data.id) const newFiles = await getSandboxFiles(data.id)
callback(newFiles.files) callback(newFiles.files)
} catch (e) {
socket.emit("rateLimit", "Rate limited: file deletion. Please slow down.")
}
}) })
socket.on("createTerminal", ({ id }: { id: string }) => { 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", [], { const pty = spawn(os.platform() === "win32" ? "cmd.exe" : "bash", [], {
name: "xterm", name: "xterm",
cols: 100, cols: 100,

View File

@ -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,
})

View File

@ -18,6 +18,7 @@ import NewProjectModal from "./newProject"
import Link from "next/link" import Link from "next/link"
import { useSearchParams } from "next/navigation" import { useSearchParams } from "next/navigation"
import AboutModal from "./about" import AboutModal from "./about"
import { toast } from "sonner"
type TScreen = "projects" | "shared" | "settings" | "search" type TScreen = "projects" | "shared" | "settings" | "search"
@ -58,7 +59,13 @@ export default function Dashboard({
<div className="w-56 shrink-0 border-r border-border p-4 justify-between flex flex-col"> <div className="w-56 shrink-0 border-r border-border p-4 justify-between flex flex-col">
<div className="flex flex-col"> <div className="flex flex-col">
<CustomButton <CustomButton
onClick={() => setNewProjectModalOpen(true)} onClick={() => {
if (sandboxes.length >= 8) {
toast.error("You reached the maximum # of sandboxes.")
return
}
setNewProjectModalOpen(true)
}}
className="mb-4" className="mb-4"
> >
<Plus className="w-4 h-4 mr-2" /> <Plus className="w-4 h-4 mr-2" />

View File

@ -372,6 +372,7 @@ export default function CodeEditor({
const selectFile = (tab: TTab) => { const selectFile = (tab: TTab) => {
if (tab.id === activeId) return if (tab.id === activeId) return
const exists = tabs.find((t) => t.id === tab.id) const exists = tabs.find((t) => t.id === tab.id)
setTabs((prev) => { setTabs((prev) => {
if (exists) { if (exists) {
setActiveId(exists.id) setActiveId(exists.id)
@ -420,8 +421,9 @@ export default function CodeEditor({
oldName: string, oldName: string,
type: "file" | "folder" type: "file" | "folder"
) => { ) => {
if (!validateName(newName, oldName, type)) { const valid = validateName(newName, oldName, type)
toast.error("Invalid file name.") if (!valid.status) {
if (valid.message) toast.error("Invalid file name.")
return false return false
} }
@ -633,6 +635,11 @@ export default function CodeEditor({
Shell Shell
</Tab> </Tab>
<Button <Button
onClick={() => {
if (terminals.length >= 4) {
toast.error("You reached the maximum # of terminals.")
}
}}
size="smIcon" size="smIcon"
variant={"secondary"} variant={"secondary"}
className={`font-normal select-none text-muted-foreground`} className={`font-normal select-none text-muted-foreground`}

View File

@ -20,14 +20,16 @@ export default function New({
const createNew = () => { const createNew = () => {
const name = inputRef.current?.value const name = inputRef.current?.value
// console.log("Create:", name, type)
if (name && validateName(name, "", type)) { if (name) {
const valid = validateName(name, "", type)
if (valid.status) {
if (type === "file") { if (type === "file") {
socket.emit("createFile", name) socket.emit("createFile", name)
} }
addNew(name, type) addNew(name, type)
} }
}
stopEditing() stopEditing()
} }

View File

@ -21,16 +21,17 @@ export function validateName(
oldName: string, oldName: string,
type: "file" | "folder" type: "file" | "folder"
) { ) {
if (newName === oldName || newName.length === 0) {
return { status: false, message: "" }
}
if ( if (
newName === oldName ||
newName.length === 0 ||
newName.includes("/") || newName.includes("/") ||
newName.includes("\\") || newName.includes("\\") ||
newName.includes(" ") || newName.includes(" ") ||
(type === "file" && !newName.includes(".")) || (type === "file" && !newName.includes(".")) ||
(type === "folder" && newName.includes(".")) (type === "folder" && newName.includes("."))
) { ) {
return false return { status: false, message: "Invalid file name." }
} }
return true return { status: true, message: "" }
} }