server.js 37 KB

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