429 lines
13 KiB
TypeScript
Raw Normal View History

2024-10-19 05:25:26 -06:00
import cors from "cors"
import dotenv from "dotenv"
import { Sandbox } from "e2b"
2024-10-19 05:25:26 -06:00
import express, { Express } from "express"
import fs from "fs"
import { createServer } from "http"
import { Server } from "socket.io"
import { z } from "zod"
2024-10-19 15:43:18 -06:00
import { AIWorker } from "./AIWorker"
import { CONTAINER_TIMEOUT } from "./constants"
import { DokkuClient } from "./DokkuClient"
import { FileManager, SandboxFiles } from "./FileManager"
2024-04-30 01:56:43 -04:00
import {
2024-05-05 12:55:34 -07:00
createFileRL,
2024-05-11 18:03:42 -07:00
createFolderRL,
2024-05-05 12:55:34 -07:00
deleteFileRL,
renameFileRL,
saveFileRL,
2024-10-19 05:25:26 -06:00
} from "./ratelimit"
import { SecureGitClient } from "./SecureGitClient"
import { handleCloseTerminal, handleCreateFile, handleCreateFolder, handleCreateTerminal, handleDeleteFile, handleDeleteFolder, handleDeploy, handleDisconnect, handleGenerateCode, handleGetFile, handleGetFolder, handleHeartbeat, handleListApps, handleMoveFile, handleRenameFile, handleResizeTerminal, handleSaveFile, handleTerminalData } from "./SocketHandlers"
import { TerminalManager } from "./TerminalManager"
2024-10-24 15:59:21 -06:00
import { DokkuResponse, User } from "./types"
import { LockManager } from "./utils"
2024-04-18 16:40:08 -04:00
2024-10-19 15:16:24 -06:00
// Handle uncaught exceptions
2024-10-19 05:25:26 -06:00
process.on("uncaughtException", (error) => {
console.error("Uncaught Exception:", error)
// Do not exit the process
// You can add additional logging or recovery logic here
2024-10-19 05:25:26 -06:00
})
2024-10-19 15:16:24 -06:00
// Handle unhandled promise rejections
2024-10-19 05:25:26 -06:00
process.on("unhandledRejection", (reason, promise) => {
console.error("Unhandled Rejection at:", promise, "reason:", reason)
// Do not exit the process
// You can also handle the rejected promise here if needed
2024-10-19 05:25:26 -06:00
})
2024-10-19 15:16:24 -06:00
// Load environment variables
2024-10-19 05:25:26 -06:00
dotenv.config()
2024-04-18 16:40:08 -04:00
2024-10-19 15:16:24 -06:00
// Initialize Express app and create HTTP server
2024-10-19 05:25:26 -06:00
const app: Express = express()
const port = process.env.PORT || 4000
app.use(cors())
const httpServer = createServer(app)
2024-04-18 16:40:08 -04:00
const io = new Server(httpServer, {
cors: {
origin: "*",
},
2024-10-19 05:25:26 -06:00
})
2024-04-18 16:40:08 -04:00
2024-10-19 15:16:24 -06:00
// Check if the sandbox owner is connected
function isOwnerConnected(sandboxId: string): boolean {
return (connections[sandboxId] ?? 0) > 0
}
2024-10-19 15:16:24 -06:00
// Initialize containers and managers
2024-10-19 05:25:26 -06:00
const containers: Record<string, Sandbox> = {}
const connections: Record<string, number> = {}
const fileManagers: Record<string, FileManager> = {}
const terminalManagers: Record<string, TerminalManager> = {}
2024-04-29 00:50:25 -04:00
2024-10-19 15:16:24 -06:00
// Middleware for socket authentication
2024-04-18 16:40:08 -04:00
io.use(async (socket, next) => {
2024-10-19 15:16:24 -06:00
// Define the schema for handshake query validation
2024-05-25 20:13:31 -07:00
const handshakeSchema = z.object({
userId: z.string(),
sandboxId: z.string(),
EIO: z.string(),
transport: z.string(),
2024-10-19 05:25:26 -06:00
})
2024-05-25 20:13:31 -07:00
2024-10-19 05:25:26 -06:00
const q = socket.handshake.query
const parseQuery = handshakeSchema.safeParse(q)
2024-04-21 22:55:49 -04:00
2024-10-19 15:16:24 -06:00
// Check if the query is valid according to the schema
2024-04-21 22:55:49 -04:00
if (!parseQuery.success) {
2024-10-19 05:25:26 -06:00
next(new Error("Invalid request."))
return
2024-04-21 22:55:49 -04:00
}
2024-10-19 05:25:26 -06:00
const { sandboxId, userId } = parseQuery.data
2024-10-19 15:16:24 -06:00
// Fetch user data from the database
2024-05-13 23:22:06 -07:00
const dbUser = await fetch(
2024-05-26 18:37:36 -07:00
`${process.env.DATABASE_WORKER_URL}/api/user?id=${userId}`,
{
headers: {
Authorization: `${process.env.WORKERS_KEY}`,
},
}
2024-10-19 05:25:26 -06:00
)
const dbUserJSON = (await dbUser.json()) as User
2024-04-21 22:55:49 -04:00
2024-10-19 15:16:24 -06:00
// Check if user data was retrieved successfully
2024-04-21 22:55:49 -04:00
if (!dbUserJSON) {
2024-10-19 05:25:26 -06:00
next(new Error("DB error."))
return
2024-04-18 16:40:08 -04:00
}
2024-10-19 15:16:24 -06:00
// Check if the user owns the sandbox or has shared access
2024-10-19 05:25:26 -06:00
const sandbox = dbUserJSON.sandbox.find((s) => s.id === sandboxId)
2024-05-03 14:58:56 -07:00
const sharedSandboxes = dbUserJSON.usersToSandboxes.find(
(uts) => uts.sandboxId === sandboxId
2024-10-19 05:25:26 -06:00
)
2024-04-18 16:40:08 -04:00
2024-10-19 15:16:24 -06:00
// If user doesn't own or have shared access to the sandbox, deny access
2024-05-03 14:58:56 -07:00
if (!sandbox && !sharedSandboxes) {
2024-10-19 05:25:26 -06:00
next(new Error("Invalid credentials."))
return
2024-04-21 22:55:49 -04:00
}
2024-10-19 15:16:24 -06:00
// Set socket data with user information
2024-04-26 02:10:37 -04:00
socket.data = {
userId,
sandboxId: sandboxId,
isOwner: sandbox !== undefined,
2024-10-19 05:25:26 -06:00
}
2024-04-18 16:40:08 -04:00
2024-10-19 15:16:24 -06:00
// Allow the connection
2024-10-19 05:25:26 -06:00
next()
})
2024-04-18 16:40:08 -04:00
2024-10-19 15:16:24 -06:00
// Initialize lock manager
2024-10-19 05:25:26 -06:00
const lockManager = new LockManager()
2024-10-19 15:16:24 -06:00
// Check for required environment variables
2024-10-19 05:25:26 -06:00
if (!process.env.DOKKU_HOST)
console.error("Environment variable DOKKU_HOST is not defined")
if (!process.env.DOKKU_USERNAME)
console.error("Environment variable DOKKU_USERNAME is not defined")
if (!process.env.DOKKU_KEY)
console.error("Environment variable DOKKU_KEY is not defined")
2024-10-19 15:16:24 -06:00
// Initialize Dokku client
const client =
process.env.DOKKU_HOST && process.env.DOKKU_KEY && process.env.DOKKU_USERNAME
? new DokkuClient({
host: process.env.DOKKU_HOST,
username: process.env.DOKKU_USERNAME,
privateKey: fs.readFileSync(process.env.DOKKU_KEY),
})
2024-10-19 05:25:26 -06:00
: null
client?.connect()
2024-10-19 15:16:24 -06:00
// Initialize Git client used to deploy Dokku apps
2024-10-19 05:25:26 -06:00
const git =
process.env.DOKKU_HOST && process.env.DOKKU_KEY
? new SecureGitClient(
`dokku@${process.env.DOKKU_HOST}`,
process.env.DOKKU_KEY
)
2024-10-19 05:25:26 -06:00
: null
2024-10-19 15:43:18 -06:00
// Add this near the top of the file, after other initializations
const aiWorker = new AIWorker(
process.env.AI_WORKER_URL!,
process.env.CF_AI_KEY!,
process.env.DATABASE_WORKER_URL!,
process.env.WORKERS_KEY!
)
// Handle a client connecting to the server
2024-04-18 16:40:08 -04:00
io.on("connection", async (socket) => {
try {
const data = socket.data as {
2024-10-19 05:25:26 -06:00
userId: string
sandboxId: string
isOwner: boolean
}
2024-04-21 22:55:49 -04:00
2024-10-19 15:16:24 -06:00
// Handle connection based on user type (owner or not)
if (data.isOwner) {
2024-10-19 05:25:26 -06:00
connections[data.sandboxId] = (connections[data.sandboxId] ?? 0) + 1
} else {
if (!isOwnerConnected(data.sandboxId)) {
2024-10-19 05:25:26 -06:00
socket.emit("disableAccess", "The sandbox owner is not connected.")
return
}
}
2024-10-19 15:16:24 -06:00
// Create or retrieve container
2024-10-19 05:25:26 -06:00
const createdContainer = await lockManager.acquireLock(
data.sandboxId,
async () => {
try {
// Start a new container if the container doesn't exist or it timed out.
if (
!containers[data.sandboxId] ||
!(await containers[data.sandboxId].isRunning())
) {
containers[data.sandboxId] = await Sandbox.create({
timeoutMs: CONTAINER_TIMEOUT,
})
console.log("Created container ", data.sandboxId)
return true
}
} catch (e: any) {
console.error(`Error creating container ${data.sandboxId}:`, e)
socket.emit("error", `Error: container creation. ${e.message ?? e}`)
}
}
2024-10-19 05:25:26 -06:00
)
2024-04-27 19:12:25 -04:00
2024-10-19 15:16:24 -06:00
// Function to send loaded event
const sendLoadedEvent = (files: SandboxFiles) => {
socket.emit("loaded", files.files)
2024-10-19 05:25:26 -06:00
}
2024-10-19 15:16:24 -06:00
// Initialize file and terminal managers if container was created
2024-09-29 20:54:09 -07:00
if (createdContainer) {
fileManagers[data.sandboxId] = new FileManager(
data.sandboxId,
containers[data.sandboxId],
sendLoadedEvent
)
terminalManagers[data.sandboxId] = new TerminalManager(
containers[data.sandboxId]
)
2024-10-23 11:55:38 +01:00
console.log(`terminal manager set up for ${data.sandboxId}`)
await fileManagers[data.sandboxId].initialize()
2024-10-19 05:25:26 -06:00
}
2024-04-27 19:12:25 -04:00
const fileManager = fileManagers[data.sandboxId]
const terminalManager = terminalManagers[data.sandboxId]
2024-10-19 05:25:26 -06:00
// Load file list from the file manager into the editor
sendLoadedEvent(fileManager.sandboxFiles)
2024-05-11 17:23:45 -07:00
2024-10-19 15:16:24 -06:00
// Handle various socket events (heartbeat, file operations, terminal operations, etc.)
socket.on("heartbeat", async () => {
try {
handleHeartbeat(socket, data, containers)
} catch (e: any) {
2024-10-19 05:25:26 -06:00
console.error("Error setting timeout:", e)
socket.emit("error", `Error: set timeout. ${e.message ?? e}`)
}
2024-10-19 05:25:26 -06:00
})
socket.on("getFile", async (fileId: string, callback) => {
try {
callback(await handleGetFile(fileManager, fileId))
} catch (e: any) {
2024-10-19 05:25:26 -06:00
console.error("Error getting file:", e)
socket.emit("error", `Error: get file. ${e.message ?? e}`)
2024-05-05 12:58:45 -07:00
}
2024-10-19 05:25:26 -06:00
})
2024-05-05 12:58:45 -07:00
socket.on("getFolder", async (folderId: string, callback) => {
try {
callback(await handleGetFolder(fileManager, folderId))
} catch (e: any) {
2024-10-19 05:25:26 -06:00
console.error("Error getting folder:", e)
socket.emit("error", `Error: get folder. ${e.message ?? e}`)
}
2024-10-19 05:25:26 -06:00
})
2024-05-05 12:55:34 -07:00
socket.on("saveFile", async (fileId: string, body: string) => {
try {
await saveFileRL.consume(data.userId, 1)
await handleSaveFile(fileManager, fileId, body)
} catch (e: any) {
2024-10-19 05:25:26 -06:00
console.error("Error saving file:", e)
socket.emit("error", `Error: file saving. ${e.message ?? e}`)
}
2024-10-19 05:25:26 -06:00
})
2024-05-10 00:12:41 -07:00
socket.on("moveFile", async (fileId: string, folderId: string, callback) => {
try {
callback(await handleMoveFile(fileManager, fileId, folderId))
} catch (e: any) {
console.error("Error moving file:", e)
socket.emit("error", `Error: file moving. ${e.message ?? e}`)
2024-05-10 00:12:41 -07:00
}
})
2024-05-10 00:12:41 -07:00
2024-10-24 15:59:21 -06:00
socket.on("list", async (callback: (response: DokkuResponse) => void) => {
console.log("Retrieving apps list...")
try {
callback(await handleListApps(client))
2024-10-24 15:59:21 -06:00
} catch (error) {
callback({
success: false,
message: "Failed to retrieve apps list",
})
2024-07-21 14:58:38 -04:00
}
2024-10-24 15:59:21 -06:00
})
2024-07-21 14:58:38 -04:00
2024-10-24 15:59:21 -06:00
socket.on("deploy", async (callback: (response: DokkuResponse) => void) => {
try {
console.log("Deploying project ${data.sandboxId}...")
callback(await handleDeploy(git, fileManager, data.sandboxId))
2024-10-24 15:59:21 -06:00
} catch (error) {
callback({
success: false,
message: "Failed to deploy project: " + error,
})
}
2024-10-24 15:59:21 -06:00
})
socket.on("createFile", async (name: string, callback) => {
try {
await createFileRL.consume(data.userId, 1)
callback({ success: await handleCreateFile(fileManager, name) })
} catch (e: any) {
2024-10-19 05:25:26 -06:00
console.error("Error creating file:", e)
socket.emit("error", `Error: file creation. ${e.message ?? e}`)
}
2024-10-19 05:25:26 -06:00
})
2024-05-09 22:32:21 -07:00
socket.on("createFolder", async (name: string, callback) => {
try {
await createFolderRL.consume(data.userId, 1)
await handleCreateFolder(fileManager, name)
2024-10-19 05:25:26 -06:00
callback()
} catch (e: any) {
2024-10-19 05:25:26 -06:00
console.error("Error creating folder:", e)
socket.emit("error", `Error: folder creation. ${e.message ?? e}`)
}
2024-10-19 05:25:26 -06:00
})
2024-05-11 18:03:42 -07:00
socket.on("renameFile", async (fileId: string, newName: string) => {
try {
await renameFileRL.consume(data.userId, 1)
await handleRenameFile(fileManager, fileId, newName)
} catch (e: any) {
console.error("Error renaming file:", e)
socket.emit("error", `Error: file renaming. ${e.message ?? e}`)
}
2024-10-19 05:25:26 -06:00
})
2024-05-05 12:55:34 -07:00
socket.on("deleteFile", async (fileId: string, callback) => {
try {
await deleteFileRL.consume(data.userId, 1)
callback(await handleDeleteFile(fileManager, fileId))
} catch (e: any) {
2024-10-19 05:25:26 -06:00
console.error("Error deleting file:", e)
socket.emit("error", `Error: file deletion. ${e.message ?? e}`)
}
2024-10-19 05:25:26 -06:00
})
2024-05-11 17:23:45 -07:00
socket.on("deleteFolder", async (folderId: string, callback) => {
try {
callback(await handleDeleteFolder(fileManager, folderId))
} catch (e: any) {
2024-10-19 05:25:26 -06:00
console.error("Error deleting folder:", e)
socket.emit("error", `Error: folder deletion. ${e.message ?? e}`)
}
2024-10-19 05:25:26 -06:00
})
2024-04-29 02:19:27 -04:00
socket.on("createTerminal", async (id: string, callback) => {
try {
await handleCreateTerminal(lockManager, terminalManager, id, socket, containers, data)
2024-10-19 05:25:26 -06:00
callback()
} catch (e: any) {
2024-10-19 05:25:26 -06:00
console.error(`Error creating terminal ${id}:`, e)
socket.emit("error", `Error: terminal creation. ${e.message ?? e}`)
}
2024-10-19 05:25:26 -06:00
})
2024-04-30 22:48:36 -04:00
socket.on("resizeTerminal", (dimensions: { cols: number; rows: number }) => {
try {
handleResizeTerminal(terminalManager, dimensions)
} catch (e: any) {
console.error("Error resizing terminal:", e)
socket.emit("error", `Error: terminal resizing. ${e.message ?? e}`)
}
})
2024-05-06 23:34:45 -07:00
2024-09-05 12:32:32 -07:00
socket.on("terminalData", async (id: string, data: string) => {
try {
await handleTerminalData(terminalManager, id, data)
} catch (e: any) {
2024-10-19 05:25:26 -06:00
console.error("Error writing to terminal:", e)
socket.emit("error", `Error: writing to terminal. ${e.message ?? e}`)
}
2024-10-19 05:25:26 -06:00
})
2024-04-28 20:06:47 -04:00
socket.on("closeTerminal", async (id: string, callback) => {
try {
await handleCloseTerminal(terminalManager, id)
2024-10-19 05:25:26 -06:00
callback()
} catch (e: any) {
2024-10-19 05:25:26 -06:00
console.error("Error closing terminal:", e)
socket.emit("error", `Error: closing terminal. ${e.message ?? e}`)
}
2024-10-19 05:25:26 -06:00
})
2024-05-06 23:34:45 -07:00
socket.on("generateCode", async (fileName: string, code: string, line: number, instructions: string, callback) => {
try {
callback(await handleGenerateCode(aiWorker, data.userId, fileName, code, line, instructions))
} catch (e: any) {
console.error("Error generating code:", e)
socket.emit("error", `Error: code generation. ${e.message ?? e}`)
}
})
2024-05-13 23:22:06 -07:00
socket.on("disconnect", async () => {
try {
if (data.isOwner) {
connections[data.sandboxId]--
}
await terminalManager.closeAllTerminals()
await fileManager.closeWatchers()
if (data.isOwner && connections[data.sandboxId] <= 0) {
socket.broadcast.emit(
"disableAccess",
"The sandbox owner has disconnected."
)
}
} catch (e: any) {
2024-10-19 05:25:26 -06:00
console.log("Error disconnecting:", e)
socket.emit("error", `Error: disconnecting. ${e.message ?? e}`)
}
2024-10-19 05:25:26 -06:00
})
} catch (e: any) {
2024-10-19 05:25:26 -06:00
console.error("Error connecting:", e)
socket.emit("error", `Error: connection. ${e.message ?? e}`)
}
2024-10-19 05:25:26 -06:00
})
2024-04-18 16:40:08 -04:00
2024-10-19 15:16:24 -06:00
// Start the server
2024-04-18 16:40:08 -04:00
httpServer.listen(port, () => {
2024-10-19 05:25:26 -06:00
console.log(`Server running on port ${port}`)
})