| 32 | * 用于解决 file:// 协议下第三方 iframe CSP 限制问题 |
| 33 | */ |
| 34 | export async function startInternalPluginServer(): Promise<number> { |
| 35 | if (!app.isPackaged) return 0 |
| 36 | |
| 37 | const basePath = path.join(process.resourcesPath, 'app.asar.unpacked', 'internal-plugins') |
| 38 | |
| 39 | if (!fs.existsSync(basePath)) { |
| 40 | console.warn('[InternalPluginServer] 内置插件目录不存在:', basePath) |
| 41 | return 0 |
| 42 | } |
| 43 | |
| 44 | server = createServer((req, res) => { |
| 45 | if (!req.url || req.method !== 'GET') { |
| 46 | res.writeHead(405) |
| 47 | res.end() |
| 48 | return |
| 49 | } |
| 50 | |
| 51 | // 解析 URL,去掉 query string |
| 52 | const urlPath = decodeURIComponent(req.url.split('?')[0]) |
| 53 | |
| 54 | // 映射到文件系统路径 |
| 55 | const filePath = path.resolve(path.join(basePath, urlPath)) |
| 56 | |
| 57 | // 安全检查:防止目录穿越 |
| 58 | if (!filePath.startsWith(basePath)) { |
| 59 | res.writeHead(403) |
| 60 | res.end() |
| 61 | return |
| 62 | } |
| 63 | |
| 64 | // 检查文件是否存在 |
| 65 | if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) { |
| 66 | res.writeHead(404) |
| 67 | res.end() |
| 68 | return |
| 69 | } |
| 70 | |
| 71 | // 确定 MIME 类型 |
| 72 | const ext = path.extname(filePath).toLowerCase() |
| 73 | const contentType = MIME_TYPES[ext] || 'application/octet-stream' |
| 74 | |
| 75 | try { |
| 76 | const content = fs.readFileSync(filePath) |
| 77 | res.writeHead(200, { 'Content-Type': contentType }) |
| 78 | res.end(content) |
| 79 | } catch { |
| 80 | res.writeHead(500) |
| 81 | res.end() |
| 82 | } |
| 83 | }) |
| 84 | |
| 85 | return new Promise((resolve, reject) => { |
| 86 | server!.listen(0, '127.0.0.1', () => { |
| 87 | const addr = server!.address() |
| 88 | if (addr && typeof addr === 'object') { |
| 89 | serverPort = addr.port |
| 90 | console.log(`[InternalPluginServer] 已启动: http://127.0.0.1:${serverPort}`) |
| 91 | resolve(serverPort) |