File size: 10,416 Bytes
49fc5e8
15f579f
49fc5e8
 
 
 
 
 
 
 
 
 
 
3df4061
49fc5e8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3df4061
 
 
 
 
 
 
 
 
240ee8f
3df4061
 
240ee8f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3df4061
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
240ee8f
 
 
 
 
3df4061
240ee8f
3df4061
 
 
 
 
 
 
15f579f
49fc5e8
 
 
 
 
 
15f579f
 
 
 
 
 
 
 
 
 
 
 
 
49fc5e8
15f579f
 
49fc5e8
15f579f
 
49fc5e8
15f579f
 
 
 
 
 
49fc5e8
15f579f
 
 
 
 
 
 
 
49fc5e8
 
 
15f579f
 
 
 
 
 
 
 
 
 
49fc5e8
15f579f
 
 
 
 
 
 
 
 
 
 
 
 
49fc5e8
15f579f
 
 
 
 
 
 
 
 
 
 
 
49fc5e8
15f579f
 
 
 
 
 
 
 
 
 
 
 
49fc5e8
 
15f579f
49fc5e8
15f579f
 
 
 
 
49fc5e8
15f579f
49fc5e8
15f579f
 
 
 
49fc5e8
 
 
 
 
 
 
 
15f579f
 
 
 
 
 
 
 
49fc5e8
 
15f579f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49fc5e8
 
 
 
 
 
 
 
 
 
 
 
dbbbf7e
1
2
3
4
5
6
7
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
55
56
57
58
59
60
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
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
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 = `
      <script>
        // 页面加载后立即执行
        document.addEventListener('DOMContentLoaded', function() {
          console.log('正在应用强制设置...');
          
          // 选择所有API源
          if (typeof API_SITES !== 'undefined') {
            // 获取所有API源的键(不包括自定义API和aggregated)
            window.selectedAPIs = Object.keys(API_SITES).filter(key => key !== 'aggregated' && key !== 'custom');
            localStorage.setItem('selectedAPIs', JSON.stringify(window.selectedAPIs));
            
            // 延迟执行,确保界面元素加载完成
            setTimeout(function() {
              // 选择所有API复选框
              const apiCheckboxes = document.querySelectorAll('#apiCheckboxes input[type="checkbox"]');
              apiCheckboxes.forEach(checkbox => {
                checkbox.checked = true;
              });
              
              // 选择所有自定义API复选框
              const customApiCheckboxes = document.querySelectorAll('#customApisList input[type="checkbox"]');
              customApiCheckboxes.forEach(checkbox => {
                checkbox.checked = true;
                
                // 获取自定义API索引并添加到selectedAPIs
                const customIndex = checkbox.dataset.customIndex;
                if (customIndex) {
                  const customApiId = 'custom_' + customIndex;
                  if (!window.selectedAPIs.includes(customApiId)) {
                    window.selectedAPIs.push(customApiId);
                  }
                }
              });
              
              // 更新selectedAPIs到localStorage
              localStorage.setItem('selectedAPIs', JSON.stringify(window.selectedAPIs));
              
              // 如果存在updateSelectedApiCount函数,更新API计数显示
              if (typeof updateSelectedApiCount === 'function') {
                updateSelectedApiCount();
              }
            }, 500);
          }
          
          // 默认关闭黄色内容过滤
          localStorage.setItem('yellowFilterEnabled', 'false');
          
          // 默认开启分片广告过滤
          localStorage.setItem('adFilteringEnabled', 'true');
          
          // 默认开启豆瓣热门推荐
          localStorage.setItem('doubanEnabled', 'true');
          
          // 如果页面上有相关元素,直接更新UI
          setTimeout(function() {
            // 更新黄色内容过滤开关
            const yellowFilterToggle = document.getElementById('yellowFilterToggle');
            if (yellowFilterToggle) {
              yellowFilterToggle.checked = false;
            }
            
            // 更新分片广告过滤开关
            const adFilterToggle = document.getElementById('adFilterToggle');
            if (adFilterToggle) {
              adFilterToggle.checked = true;
            }
            
            // 更新豆瓣热门开关
            const doubanToggle = document.getElementById('doubanToggle');
            if (doubanToggle) {
              doubanToggle.checked = true;
            }
            
            // 尝试更新豆瓣区域可见性
            if (typeof updateDoubanVisibility === 'function') {
              updateDoubanVisibility();
            } else if (window.updateDoubanVisibility) {
              window.updateDoubanVisibility();
            }
            
            // 尝试调用全选API的函数
            if (typeof selectAllAPIs === 'function') {
              selectAllAPIs(true);
            }
            
            console.log('已应用强制设置');
          }, 800);
        });
      </script>
      `;
      
      // 在</body>前插入脚本
      content = content.replace('</body>', forcedSettingsScript + '</body>');
      
      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} 访问`);
});