add basic ratelimiting
This commit is contained in:
parent
5ae5c226ba
commit
7fba908b7c
@ -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);
|
||||||
|
6
backend/server/package-lock.json
generated
6
backend/server/package-lock.json
generated
@ -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",
|
||||||
|
@ -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"
|
||||||
},
|
},
|
||||||
|
@ -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,
|
||||||
|
21
backend/server/src/ratelimit.ts
Normal file
21
backend/server/src/ratelimit.ts
Normal 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,
|
||||||
|
})
|
@ -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" />
|
||||||
|
@ -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`}
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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: "" }
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user