server.js 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972
  1. import express from 'express';
  2. import cors from 'cors';
  3. import axios from 'axios';
  4. import nodeRSA from 'node-rsa';
  5. import cookieParser from 'cookie-parser';
  6. import { readFileSync } from 'fs';
  7. import { fileURLToPath } from 'url';
  8. import { dirname, join } from 'path';
  9. // node-rsa 是 CommonJS 模块,需要使用默认导入
  10. const NodeRSA = nodeRSA;
  11. const __filename = fileURLToPath(import.meta.url);
  12. const __dirname = dirname(__filename);
  13. const app = express();
  14. const PORT = process.env.PORT || 8889;
  15. // 中间件
  16. app.use(cors({
  17. origin: true,
  18. credentials: true
  19. }));
  20. app.use(express.json());
  21. app.use(express.urlencoded({ extended: true }));
  22. app.use(cookieParser());
  23. // 请求日志中间件(用于调试)
  24. app.use((req, res, next) => {
  25. console.log(`[请求] ${req.method} ${req.path} - ${new Date().toISOString()}`);
  26. next();
  27. });
  28. // 加载自动登录配置
  29. let autoLoginConfig = {};
  30. try {
  31. const configPath = join(__dirname, 'auto-login-config.json');
  32. console.log('正在加载自动登录配置文件:', configPath);
  33. const configData = readFileSync(configPath, 'utf-8');
  34. autoLoginConfig = JSON.parse(configData);
  35. console.log('✓ 已加载自动登录配置');
  36. console.log(' 配置的网站数量:', Object.keys(autoLoginConfig).length);
  37. console.log(' 网站列表:', Object.keys(autoLoginConfig).join(', '));
  38. Object.keys(autoLoginConfig).forEach(siteId => {
  39. const site = autoLoginConfig[siteId];
  40. console.log(` - ${siteId}: ${site.name} (${site.loginMethod})`);
  41. });
  42. } catch (error) {
  43. console.error('✗ 加载自动登录配置失败:', error.message);
  44. console.error(' 错误堆栈:', error.stack);
  45. console.log('将使用默认配置');
  46. }
  47. // RSA 加密函数
  48. // 注意:JSEncrypt 使用 PKCS1 填充,需要匹配
  49. function encryptWithRSA(text, publicKey) {
  50. try {
  51. const key = new NodeRSA(publicKey, 'public', {
  52. encryptionScheme: 'pkcs1' // 使用 PKCS1 填充,与 JSEncrypt 兼容
  53. });
  54. const encrypted = key.encrypt(text, 'base64');
  55. console.log(`RSA加密: "${text}" -> 长度 ${encrypted.length}`);
  56. return encrypted;
  57. } catch (error) {
  58. console.error('RSA加密失败:', error.message);
  59. throw error;
  60. }
  61. }
  62. // 解析 Cookie
  63. function parseCookies(setCookieHeaders) {
  64. return setCookieHeaders.map(cookie => {
  65. const match = cookie.match(/^([^=]+)=([^;]+)/);
  66. if (match) {
  67. const name = match[1];
  68. const value = match[2];
  69. // 提取其他属性
  70. const pathMatch = cookie.match(/Path=([^;]+)/);
  71. const expiresMatch = cookie.match(/Expires=([^;]+)/);
  72. const maxAgeMatch = cookie.match(/Max-Age=([^;]+)/);
  73. const httpOnlyMatch = cookie.match(/HttpOnly/);
  74. const secureMatch = cookie.match(/Secure/);
  75. const sameSiteMatch = cookie.match(/SameSite=([^;]+)/);
  76. return {
  77. name,
  78. value,
  79. path: pathMatch ? pathMatch[1] : '/',
  80. expires: expiresMatch ? expiresMatch[1] : null,
  81. maxAge: maxAgeMatch ? maxAgeMatch[1] : null,
  82. httpOnly: !!httpOnlyMatch,
  83. secure: !!secureMatch,
  84. sameSite: sameSiteMatch ? sameSiteMatch[1] : null
  85. };
  86. }
  87. return null;
  88. }).filter(Boolean);
  89. }
  90. // 生成跳转 HTML
  91. function generateRedirectHTML(cookieData, targetHost, targetDomain, requestId = '') {
  92. return `
  93. <!DOCTYPE html>
  94. <html lang="zh-CN">
  95. <head>
  96. <meta charset="UTF-8">
  97. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  98. <title>自动登录中...</title>
  99. <style>
  100. body {
  101. display: flex;
  102. justify-content: center;
  103. align-items: center;
  104. height: 100vh;
  105. margin: 0;
  106. font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  107. background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
  108. }
  109. .loading {
  110. text-align: center;
  111. }
  112. .spinner {
  113. border: 4px solid #f3f3f3;
  114. border-top: 4px solid #3498db;
  115. border-radius: 50%;
  116. width: 50px;
  117. height: 50px;
  118. animation: spin 1s linear infinite;
  119. margin: 0 auto 20px;
  120. }
  121. @keyframes spin {
  122. 0% { transform: rotate(0deg); }
  123. 100% { transform: rotate(360deg); }
  124. }
  125. .message {
  126. color: #333;
  127. font-size: 18px;
  128. }
  129. </style>
  130. </head>
  131. <body>
  132. <div class="loading">
  133. <div class="spinner"></div>
  134. <div class="message">正在自动登录,请稍候...</div>
  135. </div>
  136. <iframe id="cookieFrame" style="display:none;"></iframe>
  137. <script>
  138. (function() {
  139. const requestId = '${requestId}';
  140. const cookies = ${JSON.stringify(cookieData)};
  141. const targetUrl = 'http://${targetHost}/';
  142. const targetDomain = '${targetDomain}';
  143. console.log('========================================');
  144. console.log('[浏览器端] 自动登录脚本开始执行');
  145. console.log('[浏览器端] 请求ID:', requestId);
  146. console.log('[浏览器端] 目标URL:', targetUrl);
  147. console.log('[浏览器端] 目标域名:', targetDomain);
  148. console.log('[浏览器端] Cookie 数量:', cookies.length);
  149. console.log('[浏览器端] Cookie 详情:', cookies);
  150. // 方法1: 尝试直接设置 Cookie(可能因为跨域限制而失败)
  151. console.log('[浏览器端] 开始尝试设置 Cookie...');
  152. let successCount = 0;
  153. let failCount = 0;
  154. cookies.forEach(function(cookie) {
  155. try {
  156. // 构建 Cookie 字符串
  157. let cookieStr = cookie.name + '=' + cookie.value;
  158. cookieStr += '; path=' + cookie.path;
  159. if (cookie.maxAge) {
  160. cookieStr += '; max-age=' + cookie.maxAge;
  161. }
  162. if (cookie.expires) {
  163. cookieStr += '; expires=' + cookie.expires;
  164. }
  165. if (cookie.secure) {
  166. cookieStr += '; secure';
  167. }
  168. if (cookie.sameSite) {
  169. cookieStr += '; samesite=' + cookie.sameSite;
  170. }
  171. // 注意:Domain 属性无法通过 JavaScript 设置跨域 Cookie
  172. // 但我们可以尝试设置(浏览器会忽略跨域的 Domain)
  173. cookieStr += '; domain=' + targetDomain;
  174. document.cookie = cookieStr;
  175. console.log('[浏览器端] ✓ 尝试设置 Cookie:', cookie.name);
  176. successCount++;
  177. // 验证 Cookie 是否设置成功
  178. const allCookies = document.cookie;
  179. if (allCookies.indexOf(cookie.name + '=') !== -1) {
  180. console.log('[浏览器端] ✓ Cookie 设置成功:', cookie.name);
  181. } else {
  182. console.warn('[浏览器端] ⚠ Cookie 可能未设置成功:', cookie.name, '(可能是跨域限制)');
  183. }
  184. } catch(e) {
  185. console.error('[浏览器端] ✗ 设置 Cookie 失败:', cookie.name, e);
  186. failCount++;
  187. }
  188. });
  189. console.log('[浏览器端] Cookie 设置结果: 成功 ' + successCount + ', 失败 ' + failCount);
  190. // 方法2: 使用隐藏的 iframe 加载目标站点,让服务器设置 Cookie
  191. // 然后跳转到目标站点
  192. console.log('[浏览器端] 创建隐藏 iframe 加载目标站点...');
  193. const iframe = document.getElementById('cookieFrame');
  194. iframe.onload = function() {
  195. console.log('[浏览器端] iframe 加载完成');
  196. };
  197. iframe.onerror = function(error) {
  198. console.error('[浏览器端] iframe 加载失败:', error);
  199. };
  200. iframe.src = targetUrl;
  201. // 延迟跳转,确保 iframe 加载完成
  202. setTimeout(function() {
  203. console.log('[浏览器端] 准备跳转到目标站点:', targetUrl);
  204. console.log('[浏览器端] 当前页面 Cookie:', document.cookie);
  205. console.log('========================================');
  206. window.location.href = targetUrl;
  207. }, 1500);
  208. })();
  209. </script>
  210. </body>
  211. </html>
  212. `;
  213. }
  214. // 处理 RSA 加密表单登录
  215. async function handleRSAEncryptedFormLogin(config, credentials) {
  216. const { targetBaseUrl, loginUrl, loginMethodConfig } = config;
  217. const { publicKey, usernameField, passwordField, captchaField, captchaRequired, contentType, successCode, successField } = loginMethodConfig;
  218. console.log('=== RSA 加密表单登录 ===');
  219. console.log(`目标URL: ${targetBaseUrl}${loginUrl}`);
  220. console.log(`用户名: ${credentials.username}`);
  221. console.log(`密码: ${'*'.repeat(credentials.password.length)}`);
  222. console.log(`内容类型: ${contentType}`);
  223. console.log(`成功标识字段: ${successField || 'code'}, 成功值: ${successCode}`);
  224. // 加密用户名和密码
  225. const usernameEncrypted = encryptWithRSA(credentials.username, publicKey);
  226. const passwordEncrypted = encryptWithRSA(credentials.password, publicKey);
  227. console.log('用户名和密码已加密');
  228. console.log(`加密后用户名长度: ${usernameEncrypted.length}`);
  229. console.log(`加密后密码长度: ${passwordEncrypted.length}`);
  230. // 构建请求数据
  231. const requestData = {
  232. [usernameField]: usernameEncrypted,
  233. [passwordField]: passwordEncrypted
  234. };
  235. if (captchaField) {
  236. requestData[captchaField] = captchaRequired ? '' : '';
  237. }
  238. // 发送登录请求
  239. const headers = {};
  240. let requestBody;
  241. if (contentType === 'application/x-www-form-urlencoded') {
  242. headers['Content-Type'] = 'application/x-www-form-urlencoded';
  243. requestBody = new URLSearchParams(requestData).toString();
  244. } else if (contentType === 'application/json') {
  245. headers['Content-Type'] = 'application/json';
  246. requestBody = JSON.stringify(requestData);
  247. } else {
  248. requestBody = requestData;
  249. }
  250. console.log(`发送登录请求到: ${targetBaseUrl}${loginUrl}`);
  251. // 添加可能需要的请求头(模拟浏览器请求)
  252. headers['Referer'] = `${targetBaseUrl}/`;
  253. headers['Origin'] = targetBaseUrl;
  254. headers['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
  255. headers['Accept'] = 'application/json, text/javascript, */*; q=0.01';
  256. headers['Accept-Language'] = 'zh-CN,zh;q=0.9,en;q=0.8';
  257. headers['X-Requested-With'] = 'XMLHttpRequest';
  258. console.log(`请求头:`, JSON.stringify(headers, null, 2));
  259. console.log(`请求体长度: ${requestBody.length} 字符`);
  260. console.log(`请求体内容预览: ${requestBody.substring(0, 300)}...`);
  261. // 先访问登录页面获取可能的session cookie
  262. console.log('先访问登录页面获取session...');
  263. try {
  264. const loginPageResponse = await axios.get(`${targetBaseUrl}/`, {
  265. headers: {
  266. 'User-Agent': headers['User-Agent']
  267. },
  268. withCredentials: true,
  269. maxRedirects: 5
  270. });
  271. console.log('登录页面访问成功,获取到的Cookie:', loginPageResponse.headers['set-cookie'] || []);
  272. } catch (error) {
  273. console.log('访问登录页面失败(可能不需要):', error.message);
  274. }
  275. const loginResponse = await axios.post(
  276. `${targetBaseUrl}${loginUrl}`,
  277. requestBody,
  278. {
  279. headers,
  280. withCredentials: true,
  281. maxRedirects: 0,
  282. validateStatus: function (status) {
  283. return status >= 200 && status < 400;
  284. }
  285. }
  286. );
  287. console.log(`登录响应状态码: ${loginResponse.status}`);
  288. console.log(`响应头:`, JSON.stringify(loginResponse.headers, null, 2));
  289. console.log(`响应数据:`, JSON.stringify(loginResponse.data, null, 2));
  290. // 检查登录是否成功
  291. const responseData = loginResponse.data || {};
  292. const successValue = successField ? responseData[successField] : responseData.code;
  293. console.log(`成功标识值: ${successValue}, 期望值: ${successCode}`);
  294. if (successValue === successCode) {
  295. const cookies = loginResponse.headers['set-cookie'] || [];
  296. console.log(`登录成功!获取到 ${cookies.length} 个 Cookie`);
  297. cookies.forEach((cookie, index) => {
  298. console.log(`Cookie ${index + 1}: ${cookie.substring(0, 100)}...`);
  299. });
  300. return {
  301. success: true,
  302. cookies: cookies,
  303. response: loginResponse.data
  304. };
  305. } else {
  306. console.error(`登录失败!响应:`, responseData);
  307. return {
  308. success: false,
  309. message: responseData.msg || responseData.message || '登录失败',
  310. response: responseData
  311. };
  312. }
  313. }
  314. // 处理 Home Assistant 登录(OAuth2 流程)
  315. async function handleHomeAssistantLogin(config, credentials) {
  316. const { targetBaseUrl } = config;
  317. console.log('=== Home Assistant 登录 ===');
  318. console.log(`目标URL: ${targetBaseUrl}`);
  319. console.log(`用户名: ${credentials.username}`);
  320. console.log(`密码: ${'*'.repeat(credentials.password.length)}`);
  321. const baseHeaders = {
  322. 'Content-Type': 'application/json',
  323. 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
  324. 'Accept': 'application/json, text/plain, */*',
  325. 'Origin': targetBaseUrl,
  326. 'Referer': `${targetBaseUrl}/`
  327. };
  328. // 先访问首页获取初始 Cookie(如果需要)
  329. let initialCookies = [];
  330. try {
  331. console.log('访问首页获取初始 Cookie...');
  332. const homeResponse = await axios.get(`${targetBaseUrl}/`, {
  333. headers: {
  334. 'User-Agent': baseHeaders['User-Agent']
  335. },
  336. withCredentials: true,
  337. maxRedirects: 5
  338. });
  339. initialCookies = homeResponse.headers['set-cookie'] || [];
  340. console.log(`获取到 ${initialCookies.length} 个初始 Cookie`);
  341. } catch (error) {
  342. console.log('访问首页失败(可能不需要):', error.message);
  343. }
  344. // 步骤1: 创建登录流程
  345. console.log('步骤1: 创建登录流程...');
  346. let flowResponse;
  347. try {
  348. flowResponse = await axios.post(
  349. `${targetBaseUrl}/auth/login_flow`,
  350. {
  351. client_id: `${targetBaseUrl}/`,
  352. handler: ['homeassistant', null],
  353. redirect_uri: `${targetBaseUrl}/`
  354. },
  355. {
  356. headers: baseHeaders,
  357. withCredentials: true,
  358. maxRedirects: 0,
  359. validateStatus: function (status) {
  360. return status >= 200 && status < 500;
  361. }
  362. }
  363. );
  364. } catch (error) {
  365. console.error('创建登录流程失败:', error.message);
  366. if (error.response) {
  367. console.error('响应状态:', error.response.status);
  368. console.error('响应数据:', JSON.stringify(error.response.data, null, 2));
  369. return {
  370. success: false,
  371. message: `创建登录流程失败: ${error.response.status} - ${JSON.stringify(error.response.data)}`,
  372. response: error.response.data
  373. };
  374. }
  375. return {
  376. success: false,
  377. message: `创建登录流程失败: ${error.message}`,
  378. response: null
  379. };
  380. }
  381. console.log(`流程创建响应状态码: ${flowResponse.status}`);
  382. console.log(`流程创建响应数据:`, JSON.stringify(flowResponse.data, null, 2));
  383. if (flowResponse.status !== 200) {
  384. return {
  385. success: false,
  386. message: `创建登录流程失败,状态码: ${flowResponse.status}`,
  387. response: flowResponse.data
  388. };
  389. }
  390. const flowId = flowResponse.data?.flow_id;
  391. if (!flowId) {
  392. console.error('无法获取 flow_id');
  393. return {
  394. success: false,
  395. message: '无法获取 flow_id',
  396. response: flowResponse.data
  397. };
  398. }
  399. console.log(`获取到 flow_id: ${flowId}`);
  400. // 步骤2: 提交用户名和密码
  401. console.log('步骤2: 提交用户名和密码...');
  402. let loginResponse;
  403. try {
  404. loginResponse = await axios.post(
  405. `${targetBaseUrl}/auth/login_flow/${flowId}`,
  406. {
  407. username: credentials.username,
  408. password: credentials.password,
  409. client_id: `${targetBaseUrl}/`
  410. },
  411. {
  412. headers: baseHeaders,
  413. withCredentials: true,
  414. maxRedirects: 0,
  415. validateStatus: function (status) {
  416. return status >= 200 && status < 500;
  417. }
  418. }
  419. );
  420. } catch (error) {
  421. console.error('提交登录信息失败:', error.message);
  422. if (error.response) {
  423. console.error('响应状态:', error.response.status);
  424. console.error('响应数据:', JSON.stringify(error.response.data, null, 2));
  425. return {
  426. success: false,
  427. message: `提交登录信息失败: ${error.response.status} - ${JSON.stringify(error.response.data)}`,
  428. response: error.response.data
  429. };
  430. }
  431. return {
  432. success: false,
  433. message: `提交登录信息失败: ${error.message}`,
  434. response: null
  435. };
  436. }
  437. console.log(`登录响应状态码: ${loginResponse.status}`);
  438. console.log(`登录响应数据:`, JSON.stringify(loginResponse.data, null, 2));
  439. // 检查登录是否成功
  440. const responseData = loginResponse.data || {};
  441. const responseType = responseData.type;
  442. console.log(`响应类型: ${responseType}`);
  443. // Home Assistant 登录成功时,type 为 "create_entry"
  444. if (responseType === 'create_entry') {
  445. // 合并所有请求的 Cookie
  446. const flowCookies = flowResponse.headers['set-cookie'] || [];
  447. const loginCookies = loginResponse.headers['set-cookie'] || [];
  448. const allCookies = [...initialCookies, ...flowCookies, ...loginCookies];
  449. // 去重 Cookie(保留最后一个)
  450. const cookieMap = new Map();
  451. allCookies.forEach(cookie => {
  452. const name = cookie.split('=')[0];
  453. cookieMap.set(name, cookie);
  454. });
  455. let uniqueCookies = Array.from(cookieMap.values());
  456. console.log(`登录成功!获取到 ${uniqueCookies.length} 个唯一 Cookie`);
  457. uniqueCookies.forEach((cookie, index) => {
  458. console.log(`Cookie ${index + 1}: ${cookie.substring(0, 100)}...`);
  459. });
  460. // 步骤3: 处理 OAuth2 授权流程
  461. // 登录成功后,Home Assistant 需要完成 OAuth2 授权才能访问主页面
  462. console.log('步骤3: 处理 OAuth2 授权流程...');
  463. try {
  464. // 构建 state 参数(base64 编码的 JSON)
  465. const stateData = {
  466. hassUrl: targetBaseUrl,
  467. clientId: `${targetBaseUrl}/`
  468. };
  469. const state = Buffer.from(JSON.stringify(stateData)).toString('base64');
  470. // 构建授权 URL
  471. const redirectUri = `${targetBaseUrl}/?auth_callback=1`;
  472. const clientId = `${targetBaseUrl}/`;
  473. const authorizeUrl = `${targetBaseUrl}/auth/authorize?response_type=code&redirect_uri=${encodeURIComponent(redirectUri)}&client_id=${encodeURIComponent(clientId)}&state=${encodeURIComponent(state)}`;
  474. console.log(`访问授权端点: ${authorizeUrl}`);
  475. // 构建 Cookie 字符串用于授权请求
  476. const cookieHeader = uniqueCookies.map(cookie => {
  477. const cookieStr = cookie.split(';')[0]; // 只取 name=value 部分
  478. return cookieStr;
  479. }).join('; ');
  480. console.log(`使用 Cookie 头: ${cookieHeader.substring(0, 100)}...`);
  481. const authorizeResponse = await axios.get(authorizeUrl, {
  482. headers: {
  483. 'User-Agent': baseHeaders['User-Agent'],
  484. 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
  485. 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
  486. 'Referer': `${targetBaseUrl}/`,
  487. 'Cookie': cookieHeader
  488. },
  489. withCredentials: true,
  490. maxRedirects: 5,
  491. validateStatus: function (status) {
  492. return status >= 200 && status < 400;
  493. }
  494. });
  495. console.log(`授权响应状态码: ${authorizeResponse.status}`);
  496. console.log(`授权响应 URL: ${authorizeResponse.request?.res?.responseUrl || authorizeResponse.config?.url}`);
  497. // 获取授权响应中的 Cookie
  498. const authorizeCookies = authorizeResponse.headers['set-cookie'] || [];
  499. console.log(`授权响应获取到 ${authorizeCookies.length} 个 Cookie`);
  500. // 合并授权 Cookie
  501. authorizeCookies.forEach(cookie => {
  502. const name = cookie.split('=')[0];
  503. cookieMap.set(name, cookie);
  504. });
  505. uniqueCookies = Array.from(cookieMap.values());
  506. console.log(`授权完成!最终获取到 ${uniqueCookies.length} 个唯一 Cookie`);
  507. } catch (error) {
  508. console.log('授权流程失败(可能不需要):', error.message);
  509. if (error.response) {
  510. console.log('授权响应状态:', error.response.status);
  511. console.log('授权响应 URL:', error.response.request?.res?.responseUrl || error.config?.url);
  512. }
  513. // 授权失败不影响登录,继续使用已有的 Cookie
  514. }
  515. return {
  516. success: true,
  517. cookies: uniqueCookies,
  518. response: loginResponse.data
  519. };
  520. } else {
  521. console.error(`登录失败!响应:`, responseData);
  522. const errorMessage = responseData.errors?.base?.[0]
  523. || responseData.errors?.username?.[0]
  524. || responseData.errors?.password?.[0]
  525. || responseData.message
  526. || `登录失败,响应类型: ${responseType}`;
  527. return {
  528. success: false,
  529. message: errorMessage,
  530. response: responseData
  531. };
  532. }
  533. }
  534. // 处理普通表单登录(未加密)
  535. async function handlePlainFormLogin(config, credentials) {
  536. const { targetBaseUrl, loginUrl, loginMethodConfig } = config;
  537. const { usernameField, passwordField, captchaField, contentType, successCode, successField } = loginMethodConfig;
  538. console.log('=== 普通表单登录 ===');
  539. console.log(`目标URL: ${targetBaseUrl}${loginUrl}`);
  540. console.log(`用户名: ${credentials.username}`);
  541. console.log(`密码: ${'*'.repeat(credentials.password.length)}`);
  542. console.log(`内容类型: ${contentType}`);
  543. console.log(`成功标识字段: ${successField || 'code'}, 成功值: ${successCode}`);
  544. // 构建请求数据
  545. const requestData = {
  546. [usernameField]: credentials.username,
  547. [passwordField]: credentials.password
  548. };
  549. if (captchaField) {
  550. requestData[captchaField] = '';
  551. }
  552. // 发送登录请求
  553. const headers = {};
  554. let requestBody;
  555. if (contentType === 'application/x-www-form-urlencoded') {
  556. headers['Content-Type'] = 'application/x-www-form-urlencoded';
  557. requestBody = new URLSearchParams(requestData).toString();
  558. } else if (contentType === 'application/json') {
  559. headers['Content-Type'] = 'application/json';
  560. requestBody = JSON.stringify(requestData);
  561. } else {
  562. requestBody = requestData;
  563. }
  564. console.log(`发送登录请求到: ${targetBaseUrl}${loginUrl}`);
  565. console.log(`请求头:`, JSON.stringify(headers, null, 2));
  566. console.log(`请求体:`, contentType === 'application/json' ? requestBody : requestBody.substring(0, 200) + '...');
  567. const loginResponse = await axios.post(
  568. `${targetBaseUrl}${loginUrl}`,
  569. requestBody,
  570. {
  571. headers,
  572. withCredentials: true,
  573. maxRedirects: 0,
  574. validateStatus: function (status) {
  575. return status >= 200 && status < 400;
  576. }
  577. }
  578. );
  579. console.log(`登录响应状态码: ${loginResponse.status}`);
  580. console.log(`响应数据:`, JSON.stringify(loginResponse.data, null, 2));
  581. // 检查登录是否成功
  582. const responseData = loginResponse.data || {};
  583. const successValue = successField ? responseData[successField] : responseData.code;
  584. console.log(`成功标识值: ${successValue}, 期望值: ${successCode}`);
  585. if (successValue === successCode) {
  586. const cookies = loginResponse.headers['set-cookie'] || [];
  587. console.log(`登录成功!获取到 ${cookies.length} 个 Cookie`);
  588. cookies.forEach((cookie, index) => {
  589. console.log(`Cookie ${index + 1}: ${cookie.substring(0, 100)}...`);
  590. });
  591. return {
  592. success: true,
  593. cookies: cookies,
  594. response: loginResponse.data
  595. };
  596. } else {
  597. console.error(`登录失败!响应:`, responseData);
  598. return {
  599. success: false,
  600. message: responseData.msg || responseData.message || '登录失败',
  601. response: responseData
  602. };
  603. }
  604. }
  605. // 通用的自动登录端点
  606. app.get('/api/auto-login/:siteId', async (req, res) => {
  607. const startTime = Date.now();
  608. const requestId = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
  609. // 立即输出日志,确认请求已到达
  610. console.log('\n' + '='.repeat(80));
  611. console.log(`[${requestId}] ⚡⚡⚡ 收到自动登录请求!⚡⚡⚡`);
  612. console.log(`[${requestId}] 时间: ${new Date().toISOString()}`);
  613. console.log(`[${requestId}] 请求路径: ${req.path}`);
  614. console.log(`[${requestId}] 请求方法: ${req.method}`);
  615. console.log(`[${requestId}] 完整URL: ${req.protocol}://${req.get('host')}${req.originalUrl}`);
  616. console.log(`[${requestId}] 客户端IP: ${req.ip || req.connection.remoteAddress || req.socket.remoteAddress}`);
  617. console.log(`[${requestId}] User-Agent: ${req.get('user-agent') || 'Unknown'}`);
  618. try {
  619. const { siteId } = req.params;
  620. console.log(`[${requestId}] 网站ID: ${siteId}`);
  621. // 获取网站配置
  622. const config = autoLoginConfig[siteId];
  623. if (!config) {
  624. console.error(`[${requestId}] 错误: 未找到网站ID "${siteId}" 的配置`);
  625. console.error(`[${requestId}] 可用的网站ID: ${Object.keys(autoLoginConfig).join(', ') || '无'}`);
  626. return res.status(404).json({
  627. success: false,
  628. message: `未找到网站ID "${siteId}" 的配置`,
  629. availableSites: Object.keys(autoLoginConfig)
  630. });
  631. }
  632. console.log(`[${requestId}] 网站名称: ${config.name}`);
  633. console.log(`[${requestId}] 目标地址: ${config.targetBaseUrl}`);
  634. console.log(`[${requestId}] 登录方法: ${config.loginMethod}`);
  635. // 获取登录凭据(优先使用环境变量)
  636. const envUsername = process.env[config.credentials.envUsername];
  637. const envPassword = process.env[config.credentials.envPassword];
  638. const credentials = {
  639. username: envUsername || config.credentials.username,
  640. password: envPassword || config.credentials.password
  641. };
  642. console.log(`[${requestId}] 凭据来源: ${envUsername ? '环境变量' : '配置文件'}`);
  643. console.log(`[${requestId}] 用户名: ${credentials.username}`);
  644. console.log(`[${requestId}] 密码: ${'*'.repeat(credentials.password.length)}`);
  645. if (!credentials.username || !credentials.password) {
  646. console.error(`[${requestId}] 错误: 登录凭据未配置`);
  647. return res.status(400).json({
  648. success: false,
  649. message: '登录凭据未配置'
  650. });
  651. }
  652. // 根据登录方法处理登录
  653. let loginResult;
  654. console.log(`[${requestId}] 开始执行登录...`);
  655. switch (config.loginMethod) {
  656. case 'rsa-encrypted-form':
  657. loginResult = await handleRSAEncryptedFormLogin(config, credentials);
  658. break;
  659. case 'plain-form':
  660. loginResult = await handlePlainFormLogin(config, credentials);
  661. break;
  662. case 'home-assistant':
  663. loginResult = await handleHomeAssistantLogin(config, credentials);
  664. break;
  665. default:
  666. console.error(`[${requestId}] 错误: 不支持的登录方法: ${config.loginMethod}`);
  667. return res.status(400).json({
  668. success: false,
  669. message: `不支持的登录方法: ${config.loginMethod}`
  670. });
  671. }
  672. if (!loginResult.success) {
  673. console.error(`[${requestId}] 登录失败:`, loginResult.message);
  674. console.error(`[${requestId}] 失败响应:`, JSON.stringify(loginResult.response, null, 2));
  675. const duration = Date.now() - startTime;
  676. console.log(`[${requestId}] 总耗时: ${duration}ms`);
  677. console.log('='.repeat(80) + '\n');
  678. // 返回错误页面而不是 JSON
  679. const errorHtml = `
  680. <!DOCTYPE html>
  681. <html lang="zh-CN">
  682. <head>
  683. <meta charset="UTF-8">
  684. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  685. <title>自动登录失败</title>
  686. <style>
  687. body {
  688. display: flex;
  689. justify-content: center;
  690. align-items: center;
  691. height: 100vh;
  692. margin: 0;
  693. font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  694. background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
  695. }
  696. .error-container {
  697. background: white;
  698. padding: 40px;
  699. border-radius: 12px;
  700. box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
  701. max-width: 600px;
  702. text-align: center;
  703. }
  704. .error-icon {
  705. font-size: 64px;
  706. margin-bottom: 20px;
  707. }
  708. .error-title {
  709. font-size: 24px;
  710. color: #e74c3c;
  711. margin-bottom: 15px;
  712. }
  713. .error-message {
  714. font-size: 16px;
  715. color: #666;
  716. margin-bottom: 20px;
  717. line-height: 1.6;
  718. }
  719. .error-details {
  720. background: #f8f9fa;
  721. padding: 15px;
  722. border-radius: 8px;
  723. margin-top: 20px;
  724. text-align: left;
  725. font-size: 14px;
  726. color: #555;
  727. }
  728. .error-details pre {
  729. margin: 0;
  730. white-space: pre-wrap;
  731. word-wrap: break-word;
  732. }
  733. </style>
  734. </head>
  735. <body>
  736. <div class="error-container">
  737. <div class="error-icon">❌</div>
  738. <div class="error-title">自动登录失败</div>
  739. <div class="error-message">${loginResult.message}</div>
  740. <div class="error-details">
  741. <strong>请求ID:</strong> ${requestId}<br>
  742. <strong>网站:</strong> ${config.name}<br>
  743. <strong>详细信息:</strong>
  744. <pre>${JSON.stringify(loginResult.response, null, 2)}</pre>
  745. </div>
  746. <button onclick="window.history.back()" style="margin-top: 20px; padding: 10px 20px; background: #3498db; color: white; border: none; border-radius: 6px; cursor: pointer;">返回</button>
  747. </div>
  748. </body>
  749. </html>
  750. `;
  751. return res.status(500).send(errorHtml);
  752. }
  753. console.log(`[${requestId}] 登录成功!`);
  754. // 解析 Cookie
  755. const cookieData = parseCookies(loginResult.cookies);
  756. console.log(`[${requestId}] 解析到 ${cookieData.length} 个 Cookie:`);
  757. cookieData.forEach((cookie, index) => {
  758. console.log(`[${requestId}] Cookie ${index + 1}: ${cookie.name} = ${cookie.value.substring(0, 20)}...`);
  759. });
  760. // 生成跳转 HTML(添加更多调试信息)
  761. console.log(`[${requestId}] 生成跳转页面,目标: http://${config.targetHost}/`);
  762. const html = generateRedirectHTML(
  763. cookieData,
  764. config.targetHost,
  765. config.targetDomain,
  766. requestId
  767. );
  768. // 在响应头中设置 Cookie
  769. console.log(`[${requestId}] 设置响应头 Cookie...`);
  770. loginResult.cookies.forEach((cookie, index) => {
  771. // 修改 Cookie 的 Domain,移除端口号
  772. let modifiedCookie = cookie.replace(/Domain=[^;]+/i, `Domain=${config.targetDomain}`);
  773. res.setHeader('Set-Cookie', modifiedCookie);
  774. console.log(`[${requestId}] 设置 Cookie ${index + 1}: ${modifiedCookie.substring(0, 80)}...`);
  775. });
  776. const duration = Date.now() - startTime;
  777. console.log(`[${requestId}] 总耗时: ${duration}ms`);
  778. console.log(`[${requestId}] 返回跳转页面`);
  779. console.log('='.repeat(80) + '\n');
  780. res.send(html);
  781. } catch (error) {
  782. const duration = Date.now() - startTime;
  783. console.error(`[${requestId}] 自动登录异常:`, error.message);
  784. console.error(`[${requestId}] 错误堆栈:`, error.stack);
  785. if (error.response) {
  786. console.error(`[${requestId}] 响应状态:`, error.response.status);
  787. console.error(`[${requestId}] 响应头:`, JSON.stringify(error.response.headers, null, 2));
  788. console.error(`[${requestId}] 响应数据:`, JSON.stringify(error.response.data, null, 2));
  789. }
  790. if (error.request) {
  791. console.error(`[${requestId}] 请求信息:`, {
  792. url: error.config?.url,
  793. method: error.config?.method,
  794. headers: error.config?.headers
  795. });
  796. }
  797. console.log(`[${requestId}] 总耗时: ${duration}ms`);
  798. console.log('='.repeat(80) + '\n');
  799. res.status(500).json({
  800. success: false,
  801. message: '自动登录失败: ' + error.message,
  802. error: process.env.NODE_ENV === 'development' ? error.stack : undefined
  803. });
  804. }
  805. });
  806. // 获取所有配置的网站列表
  807. app.get('/api/auto-login', (req, res) => {
  808. const sites = Object.keys(autoLoginConfig).map(siteId => ({
  809. id: siteId,
  810. name: autoLoginConfig[siteId].name,
  811. endpoint: `/api/auto-login/${siteId}`
  812. }));
  813. res.json({ sites });
  814. });
  815. // 健康检查端点
  816. app.get('/api/health', (req, res) => {
  817. res.json({
  818. status: 'ok',
  819. timestamp: new Date().toISOString(),
  820. port: PORT,
  821. configuredSites: Object.keys(autoLoginConfig)
  822. });
  823. });
  824. // 测试端点 - 用于验证配置
  825. app.get('/api/test/:siteId', (req, res) => {
  826. const { siteId } = req.params;
  827. const config = autoLoginConfig[siteId];
  828. if (!config) {
  829. return res.json({
  830. success: false,
  831. message: `未找到网站ID "${siteId}" 的配置`,
  832. availableSites: Object.keys(autoLoginConfig)
  833. });
  834. }
  835. const envUsername = process.env[config.credentials.envUsername];
  836. const envPassword = process.env[config.credentials.envPassword];
  837. const credentials = {
  838. username: envUsername || config.credentials.username,
  839. password: envPassword || config.credentials.password
  840. };
  841. res.json({
  842. success: true,
  843. siteId,
  844. config: {
  845. name: config.name,
  846. targetBaseUrl: config.targetBaseUrl,
  847. loginMethod: config.loginMethod,
  848. loginUrl: config.loginUrl,
  849. hasCredentials: !!(credentials.username && credentials.password),
  850. credentialsSource: envUsername ? '环境变量' : '配置文件',
  851. username: credentials.username,
  852. passwordLength: credentials.password ? credentials.password.length : 0
  853. }
  854. });
  855. });
  856. app.listen(PORT, '0.0.0.0', () => {
  857. console.log('\n' + '='.repeat(80));
  858. console.log('🚀 后端服务器启动成功!');
  859. console.log('='.repeat(80));
  860. console.log(`📍 本地地址: http://localhost:${PORT}`);
  861. console.log(`📍 服务器地址: http://0.0.0.0:${PORT}`);
  862. console.log(`📍 外部访问: http://222.243.138.146:${PORT} (通过防火墙端口映射)`);
  863. console.log(`\n📋 已配置的自动登录网站: ${Object.keys(autoLoginConfig).join(', ') || '无'}`);
  864. console.log(`\n🔗 可用端点:`);
  865. console.log(` - 健康检查: http://localhost:${PORT}/api/health`);
  866. console.log(` - 测试配置: http://localhost:${PORT}/api/test/:siteId`);
  867. console.log(` - 自动登录: http://localhost:${PORT}/api/auto-login/:siteId`);
  868. console.log(`\n💡 提示: 确保防火墙已配置端口映射 (前端:8888, 后端:8889 -> 外网)`);
  869. console.log('='.repeat(80) + '\n');
  870. });