const http = require("http"); const fs = require("fs"); const path = require("path"); const url = require("url"); // 从 Docker Secrets 读取密码的函数 function getDockerSecret(secretName) { try { const secretPath = `/run/secrets/${secretName}`; if (fs.existsSync(secretPath)) { return fs.readFileSync(secretPath, "utf8").trim(); } } catch (err) { console.warn(`Failed to read Docker secret '${secretName}':`, err.message); } return null; } // 配置信息 const UPLOAD_DIR = path.resolve("./uploads"); // 使用绝对路径 const PORT = process.env.PORT || 8080; const SERVER_ENABLED = process.env.ENABLE_HTTP_SERVER === "true"; // 新增:控制服务器是否启用 // 优先从 Docker Secrets 读取密码,如果没有则回退到环境变量 const SERVER_PASSWORD_FILE = process.env.SERVER_PASSWORD_FILE || "my_secret"; const SERVER_PASSWORD = getDockerSecret(SERVER_PASSWORD_FILE) || process.env.SERVER_PASSWORD || "securePass123"; const VALID_CREDENTIALS = { username: process.env.SERVER_USERNAME || "admin", password: SERVER_PASSWORD, }; // 验证密码来源 if (getDockerSecret(SERVER_PASSWORD_FILE)) { console.log("Using password from Docker Secret"); } else if (process.env.SERVER_PASSWORD) { console.warn("Using password from environment variable (less secure)"); } else { console.warn( "Warning: Using default password. Set Docker Secret or SERVER_PASSWORD environment variable for production." ); } if (!process.env.SERVER_USERNAME) { console.warn( "Warning: Using default username. Set SERVER_USERNAME environment variable for production." ); } // 检查服务器是否启用 if (!SERVER_ENABLED) { console.log( "HTTP Server is disabled. Set ENABLE_HTTP_SERVER=true to enable it." ); process.exit(0); } // 创建上传目录(如果不存在) if (!fs.existsSync(UPLOAD_DIR)) { fs.mkdirSync(UPLOAD_DIR, { recursive: true }); } const server = http.createServer((req, res) => { // 设置CORS头部 res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS"); res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization"); res.setHeader("Access-Control-Allow-Credentials", "true"); // 处理预检请求 if (req.method === "OPTIONS") { res.writeHead(200); return res.end(); } // 认证检查 if (!authenticate(req)) { res.writeHead(401, { "WWW-Authenticate": 'Basic realm="Secure File Server"', "Content-Type": "text/plain", }); return res.end("Unauthorized"); } // 路由处理 const parsedUrl = url.parse(req.url, true); if (req.method === "POST" && parsedUrl.pathname === "/upload") { handleUpload(req, res, parsedUrl.query.dir || ""); } else if (req.method === "GET" && parsedUrl.pathname === "/download") { handleDownload(req, res, parsedUrl.query.dir || ""); } else if (req.method === "DELETE" && parsedUrl.pathname === "/delete") { handleDelete(req, res, parsedUrl.query.dir || ""); } else if (req.method === "GET" && parsedUrl.pathname === "/list") { handleList(req, res, parsedUrl.query.dir || ""); } else { res.writeHead(404, { "Content-Type": "text/plain" }); res.end("Not Found"); } }); // 基本认证验证 function authenticate(req) { const authHeader = req.headers["authorization"]; if (!authHeader) return false; const [scheme, credentials] = authHeader.split(" "); if (scheme !== "Basic") return false; const [username, password] = Buffer.from(credentials, "base64") .toString() .split(":"); return ( username === VALID_CREDENTIALS.username && password === VALID_CREDENTIALS.password ); } // 安全处理文件名 function sanitizeFilename(originalName) { // 移除路径部分,只保留文件名 const base = path.basename(originalName); // 替换非法字符(Windows文件系统非法字符) return base.replace(/[\\/:*?"<>|]/g, "_"); } // 安全路径解析(防止目录遍历攻击) function resolveSafePath(...pathSegments) { const targetPath = path.resolve(UPLOAD_DIR, ...pathSegments); const relativePath = path.relative(UPLOAD_DIR, targetPath); // 检查路径是否试图访问UPLOAD_DIR之外 if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) { throw new Error("Invalid path"); } return targetPath; } // 文件上传处理 function handleUpload(req, res, dirParam) { const contentType = req.headers["content-type"]; if (!contentType || !contentType.includes("multipart/form-data")) { res.writeHead(400, { "Content-Type": "text/plain" }); return res.end("Invalid Content-Type. Expected multipart/form-data"); } const boundaryMatch = contentType.match(/boundary=(.+)$/); if (!boundaryMatch) { res.writeHead(400, { "Content-Type": "text/plain" }); return res.end("Missing boundary in Content-Type"); } const boundary = boundaryMatch[1]; let body = []; req.on("data", (chunk) => body.push(chunk)); req.on("end", () => { try { const buffer = Buffer.concat(body); const parts = parseMultipart(buffer, boundary); if (!parts.file || !parts.filename) { console.log("Parsed parts:", Object.keys(parts)); // 调试信息 throw new Error("No valid file uploaded"); } // 安全处理文件名 const safeFilename = sanitizeFilename(parts.filename); // 验证文件名 if (!safeFilename || safeFilename === "." || safeFilename === "..") { res.writeHead(400, { "Content-Type": "text/plain" }); return res.end("Invalid filename"); } // 解析并验证目标路径 const targetDir = resolveSafePath(dirParam); const filePath = resolveSafePath(dirParam, safeFilename); // 确保目标目录存在 if (!fs.existsSync(targetDir)) { fs.mkdirSync(targetDir, { recursive: true }); } fs.writeFile(filePath, parts.file, (err) => { if (err) { console.error("File write error:", err); res.writeHead(500, { "Content-Type": "text/plain" }); return res.end("Internal Server Error"); } res.writeHead(200, { "Content-Type": "application/json" }); res.end( JSON.stringify({ success: true, filename: safeFilename, directory: dirParam, message: "File uploaded successfully", }) ); }); } catch (err) { console.error("Upload error:", err); res.writeHead(400, { "Content-Type": "text/plain" }); res.end(err.message); } }); } // 解析multipart数据 - 改进版本 function parseMultipart(buffer, boundary) { const result = {}; const boundaryBuffer = Buffer.from(`--${boundary}`); const parts = []; let start = 0; let end = buffer.indexOf(boundaryBuffer, start); while (end !== -1) { if (start !== 0) { // 跳过第一个边界前的内容 parts.push(buffer.slice(start, end)); } start = end + boundaryBuffer.length; end = buffer.indexOf(boundaryBuffer, start); } for (const part of parts) { if (part.length === 0) continue; // 查找头部结束位置 const headerEndIndex = part.indexOf("\r\n\r\n"); if (headerEndIndex === -1) continue; const headers = part.slice(0, headerEndIndex).toString(); const content = part.slice(headerEndIndex + 4); // 移除结尾的 \r\n const actualContent = content.slice(0, content.length - 2); const nameMatch = headers.match(/name="([^"]+)"/); const filenameMatch = headers.match(/filename="([^"]+)"/); if (nameMatch) { const name = nameMatch[1]; if (filenameMatch && filenameMatch[1]) { result.filename = filenameMatch[1]; result.file = actualContent; console.log( `Found file: ${result.filename}, size: ${actualContent.length} bytes` ); // 调试信息 } else { result[name] = actualContent.toString(); } } } return result; } // 文件下载处理 function handleDownload(req, res, dirParam) { try { const parsedUrl = url.parse(req.url, true); const filename = parsedUrl.query.filename; if (!filename) { res.writeHead(400, { "Content-Type": "text/plain" }); return res.end("Missing filename parameter"); } // 安全处理文件名 const safeFilename = sanitizeFilename(filename); // 验证文件名 if (!safeFilename || safeFilename === "." || safeFilename === "..") { res.writeHead(400, { "Content-Type": "text/plain" }); return res.end("Invalid filename"); } // 解析并验证文件路径 const filePath = resolveSafePath(dirParam, safeFilename); // 检查文件是否存在 if (!fs.existsSync(filePath)) { res.writeHead(404, { "Content-Type": "text/plain" }); return res.end("File not found"); } const stat = fs.statSync(filePath); // 设置下载文件名 const encodedFilename = encodeURIComponent(safeFilename); res.writeHead(200, { "Content-Type": "application/octet-stream", "Content-Length": stat.size, "Content-Disposition": `attachment; filename="${encodedFilename}"; filename*=UTF-8''${encodedFilename}`, }); fs.createReadStream(filePath).pipe(res); } catch (err) { console.error("Download error:", err); res.writeHead(400, { "Content-Type": "text/plain" }); res.end(err.message); } } function handleDelete(req, res, dirParam) { try { const parsedUrl = url.parse(req.url, true); const filename = parsedUrl.query.filename; if (!filename) { res.writeHead(400, { "Content-Type": "text/plain" }); return res.end("Missing filename parameter"); } // 安全处理文件名 const safeFilename = sanitizeFilename(filename); // 验证文件名 if (!safeFilename || safeFilename === "." || safeFilename === "..") { res.writeHead(400, { "Content-Type": "text/plain" }); return res.end("Invalid filename"); } // 解析并验证文件路径 const filePath = resolveSafePath(dirParam, safeFilename); // 检查文件是否存在 if (!fs.existsSync(filePath)) { res.writeHead(404, { "Content-Type": "text/plain" }); return res.end("File not found"); } // 检查是否为文件(非目录) const stat = fs.statSync(filePath); if (!stat.isFile()) { res.writeHead(400, { "Content-Type": "text/plain" }); return res.end("Target is not a file"); } // 删除文件 fs.unlink(filePath, (err) => { if (err) { console.error("File delete error:", err); res.writeHead(500, { "Content-Type": "text/plain" }); return res.end("Internal Server Error"); } res.writeHead(200, { "Content-Type": "application/json" }); res.end( JSON.stringify({ success: true, filename: safeFilename, directory: dirParam, message: "File deleted successfully", }) ); }); } catch (err) { console.error("Delete error:", err); res.writeHead(400, { "Content-Type": "text/plain" }); res.end(err.message); } } // 目录列表处理 function handleList(req, res, dirParam) { try { // 解析并验证目录路径 const targetDir = resolveSafePath(dirParam); // 检查目录是否存在 if (!fs.existsSync(targetDir)) { res.writeHead(404, { "Content-Type": "text/plain" }); return res.end("Directory not found"); } // 检查是否为目录 const stat = fs.statSync(targetDir); if (!stat.isDirectory()) { res.writeHead(400, { "Content-Type": "text/plain" }); return res.end("Target is not a directory"); } // 读取目录内容 fs.readdir(targetDir, { withFileTypes: true }, (err, entries) => { if (err) { console.error("Directory read error:", err); res.writeHead(500, { "Content-Type": "text/plain" }); return res.end("Internal Server Error"); } const fileList = entries.map((entry) => { const stat = fs.statSync(path.join(targetDir, entry.name)); return { name: entry.name, type: entry.isDirectory() ? "directory" : "file", size: entry.isFile() ? stat.size : null, modifiedTime: stat.mtime.toISOString(), createdTime: stat.birthtime.toISOString(), }; }); // 按类型和名称排序(目录在前,然后按名称排序) fileList.sort((a, b) => { if (a.type !== b.type) { return a.type === "directory" ? -1 : 1; } return a.name.localeCompare(b.name); }); res.writeHead(200, { "Content-Type": "application/json" }); res.end( JSON.stringify({ success: true, directory: dirParam, files: fileList, totalCount: fileList.length, }) ); }); } catch (err) { console.error("List error:", err); res.writeHead(400, { "Content-Type": "text/plain" }); res.end(err.message); } } // 启动服务器 server.listen(PORT, () => { console.log(`Secure File Server running at http://localhost:${PORT}`); console.log(`Username: ${VALID_CREDENTIALS.username}`); console.log("Password: [HIDDEN FOR SECURITY]"); });