Update server.js
Browse files
server.js
CHANGED
|
@@ -1,128 +1,195 @@
|
|
| 1 |
-
const express = require('express');
|
| 2 |
-
const
|
| 3 |
-
const
|
| 4 |
-
const
|
| 5 |
-
const
|
| 6 |
-
|
| 7 |
-
const
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
}
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
error: '无效的目标URL'
|
| 55 |
-
}
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
const
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
}
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
//
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
});
|
|
|
|
| 1 |
+
const express = require('express');
|
| 2 |
+
const http = require('http');
|
| 3 |
+
const https = require('https');
|
| 4 |
+
const zlib = require('zlib');
|
| 5 |
+
const path = require('path');
|
| 6 |
+
const crypto = require('crypto');
|
| 7 |
+
const fs = require('fs');
|
| 8 |
+
|
| 9 |
+
const app = express();
|
| 10 |
+
const PORT = process.env.PORT || 8080;
|
| 11 |
+
|
| 12 |
+
// 中间件:解析请求体
|
| 13 |
+
app.use(express.json());
|
| 14 |
+
app.use(express.urlencoded({ extended: true }));
|
| 15 |
+
|
| 16 |
+
// 中间件:处理HTML文件中的环境变量注入
|
| 17 |
+
app.use((req, res, next) => {
|
| 18 |
+
if (req.path.endsWith('.html') || req.path === '/' || req.path.endsWith('/')) {
|
| 19 |
+
const filePath = req.path === '/' || req.path.endsWith('/')
|
| 20 |
+
? path.join(__dirname, 'index.html')
|
| 21 |
+
: path.join(__dirname, req.path);
|
| 22 |
+
|
| 23 |
+
if (fs.existsSync(filePath)) {
|
| 24 |
+
let content = fs.readFileSync(filePath, 'utf8');
|
| 25 |
+
|
| 26 |
+
// 替换密码占位符
|
| 27 |
+
const password = process.env.PASSWORD || '';
|
| 28 |
+
let passwordHash = '';
|
| 29 |
+
|
| 30 |
+
if (password) {
|
| 31 |
+
const hash = crypto.createHash('sha256');
|
| 32 |
+
hash.update(password);
|
| 33 |
+
passwordHash = hash.digest('hex');
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
content = content.replace(
|
| 37 |
+
'window.__ENV__.PASSWORD = "{{PASSWORD}}";',
|
| 38 |
+
`window.__ENV__.PASSWORD = "${passwordHash}"; // SHA-256 hash`
|
| 39 |
+
);
|
| 40 |
+
|
| 41 |
+
res.setHeader('Content-Type', 'text/html');
|
| 42 |
+
return res.send(content);
|
| 43 |
+
}
|
| 44 |
+
}
|
| 45 |
+
next();
|
| 46 |
+
});
|
| 47 |
+
|
| 48 |
+
// 代理处理函数
|
| 49 |
+
app.use('/proxy/:url(*)', async (req, res) => {
|
| 50 |
+
try {
|
| 51 |
+
// 获取目标 URL
|
| 52 |
+
const targetUrl = decodeURIComponent(req.params.url);
|
| 53 |
+
if (!targetUrl || !targetUrl.match(/^https?:\/\/.+/i)) {
|
| 54 |
+
return res.status(400).json({ error: '无效的目标URL' });
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
console.log(`处理代理请求: ${targetUrl}`);
|
| 58 |
+
|
| 59 |
+
// 选择合适的协议
|
| 60 |
+
const protocol = targetUrl.startsWith('https') ? https : http;
|
| 61 |
+
|
| 62 |
+
// 设置请求选项
|
| 63 |
+
const options = {
|
| 64 |
+
headers: {
|
| 65 |
+
'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',
|
| 66 |
+
'Accept': req.headers.accept || '*/*',
|
| 67 |
+
'Accept-Language': req.headers['accept-language'] || 'zh-CN,zh;q=0.9',
|
| 68 |
+
'Accept-Encoding': 'gzip, deflate',
|
| 69 |
+
'Referer': req.headers.referer || new URL(targetUrl).origin
|
| 70 |
+
},
|
| 71 |
+
timeout: 10000 // 10秒超时
|
| 72 |
+
};
|
| 73 |
+
|
| 74 |
+
// 创建请求
|
| 75 |
+
const proxyReq = protocol.get(targetUrl, options, (proxyRes) => {
|
| 76 |
+
// 设置CORS头
|
| 77 |
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
| 78 |
+
res.setHeader('Access-Control-Allow-Methods', 'GET, HEAD, OPTIONS');
|
| 79 |
+
res.setHeader('Access-Control-Allow-Headers', '*');
|
| 80 |
+
|
| 81 |
+
// 设置缓存头
|
| 82 |
+
res.setHeader('Cache-Control', 'public, max-age=86400');
|
| 83 |
+
|
| 84 |
+
// 如果是JSON内容,确保设置正确的编码
|
| 85 |
+
if (proxyRes.headers['content-type'] &&
|
| 86 |
+
proxyRes.headers['content-type'].includes('application/json')) {
|
| 87 |
+
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
| 88 |
+
} else {
|
| 89 |
+
// 复制其他响应头
|
| 90 |
+
for (const [key, value] of Object.entries(proxyRes.headers)) {
|
| 91 |
+
if (!['content-length', 'transfer-encoding', 'content-encoding'].includes(key.toLowerCase())) {
|
| 92 |
+
res.setHeader(key, value);
|
| 93 |
+
}
|
| 94 |
+
}
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
// 设置状态码
|
| 98 |
+
res.status(proxyRes.statusCode);
|
| 99 |
+
|
| 100 |
+
// 处理内容编码
|
| 101 |
+
let responseStream = proxyRes;
|
| 102 |
+
const contentEncoding = proxyRes.headers['content-encoding']?.toLowerCase();
|
| 103 |
+
|
| 104 |
+
if (contentEncoding === 'gzip') {
|
| 105 |
+
responseStream = proxyRes.pipe(zlib.createGunzip());
|
| 106 |
+
} else if (contentEncoding === 'deflate') {
|
| 107 |
+
responseStream = proxyRes.pipe(zlib.createInflate());
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
// 收集响应数据
|
| 111 |
+
const chunks = [];
|
| 112 |
+
responseStream.on('data', (chunk) => chunks.push(chunk));
|
| 113 |
+
|
| 114 |
+
responseStream.on('end', () => {
|
| 115 |
+
try {
|
| 116 |
+
const buffer = Buffer.concat(chunks);
|
| 117 |
+
|
| 118 |
+
// 检查是否是M3U8内容
|
| 119 |
+
const isM3u8 = (proxyRes.headers['content-type'] &&
|
| 120 |
+
(proxyRes.headers['content-type'].includes('application/vnd.apple.mpegurl') ||
|
| 121 |
+
proxyRes.headers['content-type'].includes('application/x-mpegurl'))) ||
|
| 122 |
+
buffer.toString('utf8', 0, 7) === '#EXTM3U';
|
| 123 |
+
|
| 124 |
+
if (isM3u8) {
|
| 125 |
+
// 针对M3U8内容的特殊处理
|
| 126 |
+
const content = buffer.toString('utf8');
|
| 127 |
+
// 这里可以添加M3U8处理逻辑,如果需要的话
|
| 128 |
+
res.setHeader('Content-Type', 'application/vnd.apple.mpegurl');
|
| 129 |
+
res.end(content);
|
| 130 |
+
return;
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
// 二进制内容,如视频片段,直接传递
|
| 134 |
+
if (proxyRes.headers['content-type'] &&
|
| 135 |
+
(proxyRes.headers['content-type'].includes('video/') ||
|
| 136 |
+
proxyRes.headers['content-type'].includes('audio/') ||
|
| 137 |
+
proxyRes.headers['content-type'].includes('application/octet-stream'))) {
|
| 138 |
+
res.end(buffer);
|
| 139 |
+
return;
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
// 文本内容(包括JSON)
|
| 143 |
+
const body = buffer.toString('utf8');
|
| 144 |
+
res.end(body);
|
| 145 |
+
} catch (error) {
|
| 146 |
+
console.error('处理响应数据错误:', error);
|
| 147 |
+
res.status(502).json({ error: '处理API响应失败' });
|
| 148 |
+
}
|
| 149 |
+
});
|
| 150 |
+
});
|
| 151 |
+
|
| 152 |
+
// 处理请求错误
|
| 153 |
+
proxyReq.on('error', (err) => {
|
| 154 |
+
console.error(`代理请求错误: ${err.message} (${targetUrl})`);
|
| 155 |
+
res.status(502).json({ error: `代理请求失败: ${err.message}` });
|
| 156 |
+
});
|
| 157 |
+
|
| 158 |
+
// 处���超时
|
| 159 |
+
proxyReq.on('timeout', () => {
|
| 160 |
+
proxyReq.destroy();
|
| 161 |
+
console.error(`代理请求超时: ${targetUrl}`);
|
| 162 |
+
res.status(504).json({ error: '代理请求超时' });
|
| 163 |
+
});
|
| 164 |
+
|
| 165 |
+
} catch (error) {
|
| 166 |
+
console.error(`代理处理错误: ${error.message}`);
|
| 167 |
+
res.status(500).json({ error: `代理服务器错误: ${error.message}` });
|
| 168 |
+
}
|
| 169 |
+
});
|
| 170 |
+
|
| 171 |
+
// 处理OPTIONS请求
|
| 172 |
+
app.options('/proxy/:url(*)', (req, res) => {
|
| 173 |
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
| 174 |
+
res.setHeader('Access-Control-Allow-Methods', 'GET, HEAD, OPTIONS');
|
| 175 |
+
res.setHeader('Access-Control-Allow-Headers', '*');
|
| 176 |
+
res.setHeader('Access-Control-Max-Age', '86400');
|
| 177 |
+
res.status(204).end();
|
| 178 |
+
});
|
| 179 |
+
|
| 180 |
+
// 静态文件服务 - 所有其他请求
|
| 181 |
+
app.use(express.static(path.join(__dirname), {
|
| 182 |
+
maxAge: '1d'
|
| 183 |
+
}));
|
| 184 |
+
|
| 185 |
+
// 错误处理中间件
|
| 186 |
+
app.use((err, req, res, next) => {
|
| 187 |
+
console.error(`服务器错误: ${err.stack}`);
|
| 188 |
+
res.status(500).send('服务器内部错误');
|
| 189 |
+
});
|
| 190 |
+
|
| 191 |
+
// 启动服务器
|
| 192 |
+
app.listen(PORT, () => {
|
| 193 |
+
console.log(`LibreTV 服务器已启动,运行在 http://localhost:${PORT}`);
|
| 194 |
+
console.log(`代理服务可通过 http://localhost:${PORT}/proxy/{URL} 访问`);
|
| 195 |
});
|