server.js 41 KB

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