const express = require('express'); const { createProxyMiddleware } = require('http-proxy-middleware'); const path = require('path'); const crypto = require('crypto'); const fs = require('fs'); const app = express(); const PORT = process.env.PORT || 8080; // 中间件:解析请求体 app.use(express.json()); app.use(express.urlencoded({ extended: true })); // 中间件:处理HTML文件中的环境变量注入和强制设置脚本 app.use((req, res, next) => { if (req.path.endsWith('.html') || req.path === '/' || req.path.endsWith('/')) { const filePath = req.path === '/' || req.path.endsWith('/') ? path.join(__dirname, 'index.html') : path.join(__dirname, req.path); if (fs.existsSync(filePath)) { let content = fs.readFileSync(filePath, 'utf8'); // 替换密码占位符 const password = process.env.PASSWORD || ''; let passwordHash = ''; if (password) { const hash = crypto.createHash('sha256'); hash.update(password); passwordHash = hash.digest('hex'); } content = content.replace( 'window.__ENV__.PASSWORD = "{{PASSWORD}}";', `window.__ENV__.PASSWORD = "${passwordHash}"; // SHA-256 hash` ); // 注入强制设置脚本 const forcedSettingsScript = ` `; // 在前插入脚本 content = content.replace('', forcedSettingsScript + ''); res.setHeader('Content-Type', 'text/html; charset=utf-8'); return res.send(content); } } next(); }); // 创建代理中间件函数 function createDynamicProxy(req, res, next) { // 从URL参数获取目标URL const targetUrl = decodeURIComponent(req.params.url); if (!targetUrl || !targetUrl.match(/^https?:\/\/.+/i)) { return res.status(400).json({ success: false, error: '无效的目标URL' }); } // 提取主机和协议 try { const urlObj = new URL(targetUrl); const target = `${urlObj.protocol}//${urlObj.host}`; // 确保路径和查询参数包含的中文正确编码 let pathToProxy = urlObj.pathname; // 处理查询参数,确保中文字符被正确编码 if (urlObj.search) { // 分析查询参数 const searchParams = new URLSearchParams(urlObj.search); // 重新创建查询字符串,确保中文编码正确 const encodedParams = new URLSearchParams(); for (const [key, value] of searchParams.entries()) { // 对于特定参数(如wd=搜索词),确保正确编码 if (key === 'wd' || key === 'ids') { // 确保中文搜索词被正确编码 - 先解码确保不重复编码,再重新编码 const decodedValue = decodeURIComponent(value); encodedParams.append(key, decodedValue); } else { encodedParams.append(key, value); } } // 重建查询字符串 pathToProxy += `?${encodedParams.toString()}`; } // 创建代理 const proxy = createProxyMiddleware({ target, changeOrigin: true, pathRewrite: () => pathToProxy, secure: false, onProxyReq: (proxyReq, req, res) => { // 设置请求头 proxyReq.setHeader('User-Agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36'); proxyReq.setHeader('Accept', req.headers.accept || '*/*'); proxyReq.setHeader('Accept-Encoding', 'gzip, deflate'); proxyReq.setHeader('Accept-Language', 'zh-CN,zh;q=0.9,en;q=0.8'); proxyReq.setHeader('Referer', req.headers.referer || target); // 如果是API请求,明确指定内容类型和编码 if (req.url.includes('/api.php/provide/vod/')) { proxyReq.setHeader('Content-Type', 'application/json; charset=utf-8'); } }, onProxyRes: (proxyRes, req, res) => { // 设置CORS头 proxyRes.headers['access-control-allow-origin'] = '*'; proxyRes.headers['access-control-allow-methods'] = 'GET, HEAD, OPTIONS'; proxyRes.headers['access-control-allow-headers'] = '*'; // 设置缓存策略 proxyRes.headers['cache-control'] = 'public, max-age=86400'; // 确保API响应的内容类型正确包含编码 if (req.url.includes('/api.php/provide/vod/')) { proxyRes.headers['content-type'] = 'application/json; charset=utf-8'; } }, // 错误处理 onError: (err, req, res) => { console.error(`[代理错误] ${err.message}`); res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ error: `代理请求失败: ${err.message}` })); } }); proxy(req, res, next); } catch (error) { console.error(`代理错误: ${error.message}`); return res.status(500).json({ success: false, error: `代理请求失败: ${error.message}` }); } } // 设置代理路由 app.use('/proxy/:url(*)', createDynamicProxy); // OPTIONS请求处理 app.options('/proxy/:url(*)', (req, res) => { res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET, HEAD, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', '*'); res.setHeader('Access-Control-Max-Age', '86400'); res.status(204).end(); }); // 确保所有响应使用正确的编码 app.use((req, res, next) => { if (!res.headersSent && !req.path.startsWith('/proxy/')) { res.setHeader('Content-Type', 'text/html; charset=utf-8'); } next(); }); // 静态文件服务 - 所有其他请求 app.use(express.static(path.join(__dirname), { maxAge: '1d', setHeaders: (res, path) => { // 为HTML文件设置正确的编码 if (path.endsWith('.html')) { res.setHeader('Content-Type', 'text/html; charset=utf-8'); } // 为CSS文件设置正确的编码 else if (path.endsWith('.css')) { res.setHeader('Content-Type', 'text/css; charset=utf-8'); } // 为JS文件设置正确的编码 else if (path.endsWith('.js')) { res.setHeader('Content-Type', 'text/javascript; charset=utf-8'); } } })); // 错误处理中间件 app.use((err, req, res, next) => { console.error(`服务器错误: ${err.stack}`); res.status(500).send('服务器内部错误'); }); // 启动服务器 app.listen(PORT, () => { console.log(`LibreTV 服务器已启动,运行在 http://localhost:${PORT}`); console.log(`代理服务可通过 http://localhost:${PORT}/proxy/{URL} 访问`); });