Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bb42187ba8 |
+28
-39
@@ -5,6 +5,7 @@ import path from "path";
|
|||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import { createServer } from "http";
|
import { createServer } from "http";
|
||||||
import { Server } from "socket.io";
|
import { Server } from "socket.io";
|
||||||
|
import { Worker } from "worker_threads";
|
||||||
import multer from "multer";
|
import multer from "multer";
|
||||||
import archiver from "archiver";
|
import archiver from "archiver";
|
||||||
import Database from "better-sqlite3";
|
import Database from "better-sqlite3";
|
||||||
@@ -77,13 +78,7 @@ const prisma = new PrismaClient();
|
|||||||
const PORT = process.env.PORT || 8000;
|
const PORT = process.env.PORT || 8000;
|
||||||
|
|
||||||
// Multer setup for file uploads
|
// Multer setup for file uploads
|
||||||
const upload = multer({
|
const upload = multer({ dest: uploadDir });
|
||||||
dest: uploadDir,
|
|
||||||
limits: {
|
|
||||||
fileSize: 50 * 1024 * 1024, // 50MB in bytes
|
|
||||||
files: 1, // Only one file per upload
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
app.use(
|
app.use(
|
||||||
cors({
|
cors({
|
||||||
@@ -94,22 +89,6 @@ app.use(
|
|||||||
app.use(express.json({ limit: "50mb" }));
|
app.use(express.json({ limit: "50mb" }));
|
||||||
app.use(express.urlencoded({ extended: true, limit: "50mb" }));
|
app.use(express.urlencoded({ extended: true, limit: "50mb" }));
|
||||||
|
|
||||||
// Log large requests for monitoring and debugging
|
|
||||||
app.use((req, res, next) => {
|
|
||||||
const contentLength = req.headers["content-length"];
|
|
||||||
if (contentLength) {
|
|
||||||
const sizeInMB = parseInt(contentLength) / 1024 / 1024;
|
|
||||||
if (sizeInMB > 10) {
|
|
||||||
console.log(
|
|
||||||
`[LARGE REQUEST] ${req.method} ${req.path} - ${sizeInMB.toFixed(
|
|
||||||
2
|
|
||||||
)}MB - Content-Length: ${contentLength} bytes`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
|
|
||||||
const elementsSchema = z.array(z.object({}).passthrough());
|
const elementsSchema = z.array(z.object({}).passthrough());
|
||||||
|
|
||||||
const appStateSchema = z.object({}).passthrough();
|
const appStateSchema = z.object({}).passthrough();
|
||||||
@@ -147,21 +126,31 @@ const respondWithValidationErrors = (
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const runIntegrityCheck = (filePath: string): boolean => {
|
// Non-blocking CPU check using worker threads
|
||||||
let dbInstance: Database.Database | undefined;
|
const verifyDatabaseIntegrityAsync = (filePath: string): Promise<boolean> => {
|
||||||
try {
|
return new Promise((resolve) => {
|
||||||
dbInstance = new Database(filePath, {
|
const worker = new Worker(
|
||||||
readonly: true,
|
path.resolve(__dirname, "./workers/db-verify.js"),
|
||||||
fileMustExist: true,
|
{
|
||||||
});
|
workerData: { filePath },
|
||||||
const result = dbInstance.prepare("PRAGMA integrity_check;").get();
|
|
||||||
return result?.integrity_check === "ok";
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Integrity check failed:", error);
|
|
||||||
return false;
|
|
||||||
} finally {
|
|
||||||
dbInstance?.close();
|
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
worker.on("message", (isValid: boolean) => resolve(isValid));
|
||||||
|
worker.on("error", (err) => {
|
||||||
|
console.error("Worker error:", err);
|
||||||
|
resolve(false);
|
||||||
|
});
|
||||||
|
worker.on("exit", (code) => {
|
||||||
|
if (code !== 0) resolve(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Kill worker if it takes too long (DoS protection)
|
||||||
|
setTimeout(() => {
|
||||||
|
worker.terminate();
|
||||||
|
resolve(false);
|
||||||
|
}, 10000); // 10 second timeout
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeFileIfExists = (filePath?: string) => {
|
const removeFileIfExists = (filePath?: string) => {
|
||||||
@@ -667,7 +656,7 @@ app.post("/import/sqlite/verify", upload.single("db"), async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const stagedPath = req.file.path;
|
const stagedPath = req.file.path;
|
||||||
const isValid = runIntegrityCheck(stagedPath);
|
const isValid = await verifyDatabaseIntegrityAsync(stagedPath);
|
||||||
removeFileIfExists(stagedPath);
|
removeFileIfExists(stagedPath);
|
||||||
|
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
@@ -706,7 +695,7 @@ app.post("/import/sqlite", upload.single("db"), async (req, res) => {
|
|||||||
return res.status(500).json({ error: "Failed to stage uploaded file" });
|
return res.status(500).json({ error: "Failed to stage uploaded file" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const isValid = runIntegrityCheck(stagedPath);
|
const isValid = await verifyDatabaseIntegrityAsync(stagedPath);
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
removeFileIfExists(stagedPath);
|
removeFileIfExists(stagedPath);
|
||||||
return res
|
return res
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
const { parentPort, workerData } = require('worker_threads');
|
||||||
|
const Database = require('better-sqlite3');
|
||||||
|
|
||||||
|
if (!parentPort) throw new Error("Must be run in a worker thread");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { filePath } = workerData;
|
||||||
|
const db = new Database(filePath, { readonly: true, fileMustExist: true });
|
||||||
|
|
||||||
|
// This is the CPU-heavy operation
|
||||||
|
const result = db.prepare("PRAGMA integrity_check;").get();
|
||||||
|
|
||||||
|
db.close();
|
||||||
|
parentPort.postMessage(result.integrity_check === "ok");
|
||||||
|
} catch (error) {
|
||||||
|
// Any error means invalid or corrupt DB
|
||||||
|
parentPort.postMessage(false);
|
||||||
|
}
|
||||||
@@ -12,9 +12,6 @@ http {
|
|||||||
gzip_vary on;
|
gzip_vary on;
|
||||||
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
|
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
|
||||||
|
|
||||||
# Set maximum request body size to 50MB to handle large drawings with embedded images
|
|
||||||
client_max_body_size 50M;
|
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name localhost;
|
server_name localhost;
|
||||||
@@ -32,18 +29,6 @@ http {
|
|||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
# Buffer and timeout settings for large payloads
|
|
||||||
proxy_buffering on;
|
|
||||||
proxy_buffer_size 4k;
|
|
||||||
proxy_buffers 8 4k;
|
|
||||||
proxy_busy_buffers_size 8k;
|
|
||||||
client_body_buffer_size 128k;
|
|
||||||
|
|
||||||
# Timeouts for large uploads (300 seconds)
|
|
||||||
proxy_connect_timeout 300s;
|
|
||||||
proxy_send_timeout 300s;
|
|
||||||
proxy_read_timeout 300s;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# WebSocket proxy for Socket.IO
|
# WebSocket proxy for Socket.IO
|
||||||
|
|||||||
Reference in New Issue
Block a user