Backport generic tool and preview improvements
This commit is contained in:
1 parent
80b9cc1547
commit
83a1049fe2
6 files changed
+236
-66
No files matched your search
+47
-1
@@ -64,7 +64,7 @@ const allowedOrigins = new Set(
|
||||
);
|
||||
|
||||
const SECURITY_HEADERS = {
|
||||
csp: "default-src 'self'; base-uri 'self'; form-action 'self'; img-src 'self' data:; font-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self'; connect-src 'self' ws: wss:; object-src 'none'; frame-ancestors 'none';",
|
||||
csp: "default-src 'self'; base-uri 'self'; form-action 'self'; img-src 'self' data: blob:; frame-src 'self' blob:; font-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self'; connect-src 'self' ws: wss:; object-src 'none'; frame-ancestors 'none';",
|
||||
hsts: "max-age=31536000; includeSubDomains; preload",
|
||||
referrerPolicy: "strict-origin-when-cross-origin",
|
||||
frameOptions: "DENY",
|
||||
@@ -389,6 +389,12 @@ function requireEditAuth(req: express.Request, res: express.Response, next: expr
|
||||
res.status(401).json({ ok: false, error: "unauthorized" });
|
||||
}
|
||||
|
||||
function requireHttpAuth(req: express.Request, res: express.Response, next: express.NextFunction) {
|
||||
if (!isAllowedWsOrigin(req.header("Origin"), req.header("Host"))) return res.status(403).type("text/plain").send("Forbidden origin");
|
||||
if (isValidToken(tokenFromRequest(req))) return next();
|
||||
res.status(401).type("text/plain").send("Unauthorized");
|
||||
}
|
||||
|
||||
function fileStamp(filePath: string): string {
|
||||
try {
|
||||
const stat = fs.statSync(filePath);
|
||||
@@ -1467,6 +1473,46 @@ app.delete("/api/skills/user/:id", requireEditAuth, async (req, res) => {
|
||||
|
||||
// Serve static client files in production after API routes.
|
||||
const clientDist = path.resolve(__dirname, "../dist");
|
||||
const TEXT_PREVIEW_EXTENSIONS = new Set([
|
||||
".txt", ".md", ".markdown", ".json", ".jsonl", ".csv", ".tsv", ".log", ".xml", ".html", ".htm", ".css", ".js", ".mjs", ".cjs", ".ts", ".tsx", ".jsx", ".py", ".php", ".sh", ".bash", ".zsh", ".yaml", ".yml", ".toml", ".ini", ".conf", ".sql",
|
||||
]);
|
||||
|
||||
async function resolveHomeFilePath(requested: string): Promise<string | undefined> {
|
||||
const trimmed = requested.trim();
|
||||
if (!trimmed) return undefined;
|
||||
const home = await fs.promises.realpath(os.homedir()).catch(() => undefined);
|
||||
const requestedPath = path.resolve(path.isAbsolute(trimmed) ? trimmed : path.join(os.homedir(), trimmed));
|
||||
const resolved = await fs.promises.realpath(requestedPath).catch(() => undefined);
|
||||
if (!home || !resolved || (resolved !== home && !resolved.startsWith(`${home}${path.sep}`))) return undefined;
|
||||
return resolved;
|
||||
}
|
||||
|
||||
function canPreviewTextFile(filePath: string, size: number): boolean {
|
||||
return size <= 2 * 1024 * 1024 && TEXT_PREVIEW_EXTENSIONS.has(path.extname(filePath).toLowerCase());
|
||||
}
|
||||
|
||||
app.get("/api/files", requireHttpAuth, async (req, res) => {
|
||||
const requested = typeof req.query.path === "string" ? req.query.path : "";
|
||||
const filePath = await resolveHomeFilePath(requested);
|
||||
if (!filePath) return res.status(403).type("text/plain").send("File is outside the workspace");
|
||||
const stats = await fs.promises.stat(filePath).catch(() => undefined);
|
||||
if (!stats?.isFile()) return res.status(404).type("text/plain").send("File not found");
|
||||
if (req.query.download === "1") return res.download(filePath, path.basename(filePath));
|
||||
if (req.query.inline === "1") {
|
||||
const inlineTypes: Record<string, string> = { ".pdf": "application/pdf", ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp" };
|
||||
const type = inlineTypes[path.extname(filePath).toLowerCase()];
|
||||
if (!type) return res.status(415).type("text/plain").send("File type is not previewable");
|
||||
res.type(type);
|
||||
return res.sendFile(filePath);
|
||||
}
|
||||
if (req.query.raw === "1") {
|
||||
if (!canPreviewTextFile(filePath, stats.size)) return res.status(415).type("text/plain").send("File type is not previewable as text");
|
||||
res.type("text/plain");
|
||||
return res.sendFile(filePath);
|
||||
}
|
||||
return res.status(400).type("text/plain").send("Use the file preview page");
|
||||
});
|
||||
|
||||
app.use(express.static(clientDist));
|
||||
app.get("/{*path}", (_req, res) => {
|
||||
res.sendFile(path.join(clientDist, "client", "index.html"));
|
||||
|
||||
Reference in new issue
Block a user