server.js 52 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390
  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, homeAssistantData = null) {
  94. const targetUrl = customUrl || `http://${targetHost}/`;
  95. const isHomeAssistant = homeAssistantData !== null;
  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. const homeAssistantData = ${homeAssistantData ? JSON.stringify(homeAssistantData) : 'null'};
  149. console.log('========================================');
  150. console.log('[浏览器端] 自动登录脚本开始执行');
  151. console.log('[浏览器端] 请求ID:', requestId);
  152. console.log('[浏览器端] 目标URL:', targetUrl);
  153. console.log('[浏览器端] 目标域名:', targetDomain);
  154. console.log('[浏览器端] Cookie 数量:', cookies.length);
  155. console.log('[浏览器端] Cookie 详情:', cookies);
  156. console.log('[浏览器端] 是否为 Home Assistant:', isHomeAssistant);
  157. console.log('[浏览器端] Home Assistant 数据:', homeAssistantData);
  158. // 方法1: 尝试直接设置 Cookie(可能因为跨域限制而失败)
  159. if (cookies.length > 0) {
  160. console.log('[浏览器端] 开始尝试设置 Cookie...');
  161. let successCount = 0;
  162. let failCount = 0;
  163. cookies.forEach(function(cookie) {
  164. try {
  165. // 构建 Cookie 字符串
  166. let cookieStr = cookie.name + '=' + cookie.value;
  167. cookieStr += '; path=' + (cookie.path || '/');
  168. if (cookie.maxAge) {
  169. cookieStr += '; max-age=' + cookie.maxAge;
  170. }
  171. if (cookie.expires) {
  172. cookieStr += '; expires=' + cookie.expires;
  173. }
  174. if (cookie.secure) {
  175. cookieStr += '; secure';
  176. }
  177. if (cookie.sameSite) {
  178. cookieStr += '; samesite=' + cookie.sameSite;
  179. }
  180. // 注意:Domain 属性无法通过 JavaScript 设置跨域 Cookie
  181. // 但我们可以尝试设置(浏览器会忽略跨域的 Domain)
  182. if (cookie.domain) {
  183. cookieStr += '; domain=' + cookie.domain;
  184. }
  185. document.cookie = cookieStr;
  186. console.log('[浏览器端] ✓ 尝试设置 Cookie:', cookie.name);
  187. successCount++;
  188. // 验证 Cookie 是否设置成功
  189. const allCookies = document.cookie;
  190. if (allCookies.indexOf(cookie.name + '=') !== -1) {
  191. console.log('[浏览器端] ✓ Cookie 设置成功:', cookie.name);
  192. } else {
  193. console.warn('[浏览器端] ⚠ Cookie 可能未设置成功:', cookie.name, '(可能是跨域限制)');
  194. }
  195. } catch(e) {
  196. console.error('[浏览器端] ✗ 设置 Cookie 失败:', cookie.name, e);
  197. failCount++;
  198. }
  199. });
  200. console.log('[浏览器端] Cookie 设置结果: 成功 ' + successCount + ', 失败 ' + failCount);
  201. } else {
  202. console.log('[浏览器端] 没有 Cookie 需要设置,直接跳转');
  203. }
  204. // 对于 Home Assistant,在浏览器端执行登录流程
  205. if (isHomeAssistant && homeAssistantData) {
  206. console.log('[浏览器端] Home Assistant 登录,在浏览器端执行登录流程');
  207. console.log('[浏览器端] 目标 URL:', homeAssistantData.targetBaseUrl);
  208. console.log('[浏览器端] 用户名:', homeAssistantData.username);
  209. // 异步执行登录流程(通过后端代理避免 CORS)
  210. async function loginHomeAssistant() {
  211. try {
  212. console.log('[浏览器端] 步骤1: 创建登录流程(通过代理)...');
  213. // 使用后端代理避免 CORS 问题
  214. const proxyBaseUrl = window.location.origin; // 后端服务器地址
  215. const flowResponse = await fetch(proxyBaseUrl + '/api/home-assistant-proxy/login-flow', {
  216. method: 'POST',
  217. headers: {
  218. 'Content-Type': 'application/json'
  219. },
  220. body: JSON.stringify({
  221. targetBaseUrl: homeAssistantData.targetBaseUrl
  222. })
  223. });
  224. if (!flowResponse.ok) {
  225. throw new Error('创建登录流程失败: ' + flowResponse.status);
  226. }
  227. const flowData = await flowResponse.json();
  228. console.log('[浏览器端] 流程创建响应:', flowData);
  229. if (!flowData.flow_id) {
  230. throw new Error('无法获取 flow_id');
  231. }
  232. console.log('[浏览器端] 步骤2: 提交用户名和密码(通过代理)...');
  233. const loginResponse = await fetch(proxyBaseUrl + '/api/home-assistant-proxy/login', {
  234. method: 'POST',
  235. headers: {
  236. 'Content-Type': 'application/json'
  237. },
  238. body: JSON.stringify({
  239. targetBaseUrl: homeAssistantData.targetBaseUrl,
  240. flowId: flowData.flow_id,
  241. username: homeAssistantData.username,
  242. password: homeAssistantData.password
  243. })
  244. });
  245. if (!loginResponse.ok) {
  246. throw new Error('登录失败: ' + loginResponse.status);
  247. }
  248. const loginData = await loginResponse.json();
  249. console.log('[浏览器端] 登录响应:', loginData);
  250. if (loginData.type === 'create_entry') {
  251. console.log('[浏览器端] 登录成功!准备跳转到授权端点...');
  252. // 构建授权 URL
  253. const stateData = {
  254. hassUrl: homeAssistantData.targetBaseUrl,
  255. clientId: homeAssistantData.targetBaseUrl + '/'
  256. };
  257. const state = btoa(JSON.stringify(stateData));
  258. const redirectUri = homeAssistantData.targetBaseUrl + '/?auth_callback=1';
  259. const clientId = homeAssistantData.targetBaseUrl + '/';
  260. const authorizeUrl = homeAssistantData.targetBaseUrl + '/auth/authorize?response_type=code&redirect_uri=' + encodeURIComponent(redirectUri) + '&client_id=' + encodeURIComponent(clientId) + '&state=' + encodeURIComponent(state);
  261. console.log('[浏览器端] 授权 URL:', authorizeUrl);
  262. console.log('[浏览器端] 跳转到授权端点...');
  263. console.log('========================================');
  264. window.location.href = authorizeUrl;
  265. } else {
  266. throw new Error('登录失败: ' + JSON.stringify(loginData));
  267. }
  268. } catch (error) {
  269. console.error('[浏览器端] 登录失败:', error);
  270. alert('自动登录失败: ' + error.message + '\\n\\n将跳转到登录页面,请手动登录。');
  271. window.location.href = targetUrl;
  272. }
  273. }
  274. // 执行登录
  275. loginHomeAssistant();
  276. return;
  277. }
  278. // 方法2: 使用隐藏的 iframe 加载目标站点,让服务器设置 Cookie
  279. // 然后跳转到目标站点
  280. console.log('[浏览器端] 创建隐藏 iframe 加载目标站点...');
  281. const iframe = document.getElementById('cookieFrame');
  282. iframe.onload = function() {
  283. console.log('[浏览器端] iframe 加载完成');
  284. };
  285. iframe.onerror = function(error) {
  286. console.error('[浏览器端] iframe 加载失败:', error);
  287. };
  288. iframe.src = targetUrl;
  289. // 延迟跳转,确保 iframe 加载完成
  290. setTimeout(function() {
  291. console.log('[浏览器端] 准备跳转到目标站点:', targetUrl);
  292. console.log('[浏览器端] 当前页面 Cookie:', document.cookie);
  293. console.log('========================================');
  294. window.location.href = targetUrl;
  295. }, 1500);
  296. })();
  297. </script>
  298. </body>
  299. </html>
  300. `;
  301. }
  302. // 处理 RSA 加密表单登录
  303. async function handleRSAEncryptedFormLogin(config, credentials) {
  304. const { targetBaseUrl, loginUrl, loginMethodConfig } = config;
  305. const { publicKey, usernameField, passwordField, captchaField, captchaRequired, contentType, successCode, successField } = loginMethodConfig;
  306. console.log('=== RSA 加密表单登录 ===');
  307. console.log(`目标URL: ${targetBaseUrl}${loginUrl}`);
  308. console.log(`用户名: ${credentials.username}`);
  309. console.log(`密码: ${'*'.repeat(credentials.password.length)}`);
  310. console.log(`内容类型: ${contentType}`);
  311. console.log(`成功标识字段: ${successField || 'code'}, 成功值: ${successCode}`);
  312. // 加密用户名和密码
  313. const usernameEncrypted = encryptWithRSA(credentials.username, publicKey);
  314. const passwordEncrypted = encryptWithRSA(credentials.password, publicKey);
  315. console.log('用户名和密码已加密');
  316. console.log(`加密后用户名长度: ${usernameEncrypted.length}`);
  317. console.log(`加密后密码长度: ${passwordEncrypted.length}`);
  318. // 构建请求数据
  319. const requestData = {
  320. [usernameField]: usernameEncrypted,
  321. [passwordField]: passwordEncrypted
  322. };
  323. if (captchaField) {
  324. requestData[captchaField] = captchaRequired ? '' : '';
  325. }
  326. // 发送登录请求
  327. const headers = {};
  328. let requestBody;
  329. if (contentType === 'application/x-www-form-urlencoded') {
  330. headers['Content-Type'] = 'application/x-www-form-urlencoded';
  331. requestBody = new URLSearchParams(requestData).toString();
  332. } else if (contentType === 'application/json') {
  333. headers['Content-Type'] = 'application/json';
  334. requestBody = JSON.stringify(requestData);
  335. } else {
  336. requestBody = requestData;
  337. }
  338. console.log(`发送登录请求到: ${targetBaseUrl}${loginUrl}`);
  339. // 添加可能需要的请求头(模拟浏览器请求)
  340. headers['Referer'] = `${targetBaseUrl}/`;
  341. headers['Origin'] = targetBaseUrl;
  342. 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';
  343. headers['Accept'] = 'application/json, text/javascript, */*; q=0.01';
  344. headers['Accept-Language'] = 'zh-CN,zh;q=0.9,en;q=0.8';
  345. headers['X-Requested-With'] = 'XMLHttpRequest';
  346. console.log(`请求头:`, JSON.stringify(headers, null, 2));
  347. console.log(`请求体长度: ${requestBody.length} 字符`);
  348. console.log(`请求体内容预览: ${requestBody.substring(0, 300)}...`);
  349. // 先访问登录页面获取可能的session cookie
  350. console.log('先访问登录页面获取session...');
  351. try {
  352. const loginPageResponse = await axios.get(`${targetBaseUrl}/`, {
  353. headers: {
  354. 'User-Agent': headers['User-Agent']
  355. },
  356. withCredentials: true,
  357. maxRedirects: 5
  358. });
  359. console.log('登录页面访问成功,获取到的Cookie:', loginPageResponse.headers['set-cookie'] || []);
  360. } catch (error) {
  361. console.log('访问登录页面失败(可能不需要):', error.message);
  362. }
  363. const loginResponse = await axios.post(
  364. `${targetBaseUrl}${loginUrl}`,
  365. requestBody,
  366. {
  367. headers,
  368. withCredentials: true,
  369. maxRedirects: 0,
  370. validateStatus: function (status) {
  371. return status >= 200 && status < 400;
  372. }
  373. }
  374. );
  375. console.log(`登录响应状态码: ${loginResponse.status}`);
  376. console.log(`响应头:`, JSON.stringify(loginResponse.headers, null, 2));
  377. console.log(`响应数据:`, JSON.stringify(loginResponse.data, null, 2));
  378. // 检查登录是否成功
  379. const responseData = loginResponse.data || {};
  380. const successValue = successField ? responseData[successField] : responseData.code;
  381. console.log(`成功标识值: ${successValue}, 期望值: ${successCode}`);
  382. if (successValue === successCode) {
  383. const cookies = loginResponse.headers['set-cookie'] || [];
  384. console.log(`登录成功!获取到 ${cookies.length} 个 Cookie`);
  385. cookies.forEach((cookie, index) => {
  386. console.log(`Cookie ${index + 1}: ${cookie.substring(0, 100)}...`);
  387. });
  388. return {
  389. success: true,
  390. cookies: cookies,
  391. response: loginResponse.data
  392. };
  393. } else {
  394. console.error(`登录失败!响应:`, responseData);
  395. return {
  396. success: false,
  397. message: responseData.msg || responseData.message || '登录失败',
  398. response: responseData
  399. };
  400. }
  401. }
  402. // 处理 Home Assistant 登录(OAuth2 流程 - 严格匹配 redirect_uri)
  403. async function handleHomeAssistantLogin(config, credentials) {
  404. const { targetBaseUrl } = config;
  405. console.log('=== Home Assistant 登录 (OAuth2 严格模式) ===');
  406. console.log(`目标URL: ${targetBaseUrl}`);
  407. console.log(`用户名: ${credentials.username}`);
  408. console.log(`密码: ${'*'.repeat(credentials.password.length)}`);
  409. // 基础请求头,伪装成浏览器
  410. const baseHeaders = {
  411. 'Content-Type': 'application/json',
  412. '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',
  413. 'Accept': 'application/json, text/plain, */*',
  414. 'Origin': targetBaseUrl,
  415. 'Referer': `${targetBaseUrl}/`
  416. };
  417. // 【关键】:OAuth2 协议要求 client_id 和 redirect_uri 在整个流程中完全一致
  418. const CLIENT_ID = `${targetBaseUrl}/`;
  419. const REDIRECT_URI = `${targetBaseUrl}/?auth_callback=1`;
  420. console.log('Client ID:', CLIENT_ID);
  421. console.log('Redirect URI:', REDIRECT_URI);
  422. try {
  423. // ==========================================
  424. // 步骤1: 创建登录流程 (Init Flow)
  425. // ==========================================
  426. console.log('[1/3] 初始化登录流程...');
  427. const flowResponse = await axios.post(
  428. `${targetBaseUrl}/auth/login_flow`,
  429. {
  430. client_id: CLIENT_ID,
  431. handler: ['homeassistant', null],
  432. redirect_uri: REDIRECT_URI // 【重要】:必须和最后跳转的地址完全一致
  433. },
  434. {
  435. headers: baseHeaders,
  436. validateStatus: function (status) {
  437. return status >= 200 && status < 500;
  438. }
  439. }
  440. );
  441. console.log(`流程创建响应状态码: ${flowResponse.status}`);
  442. console.log(`流程创建响应数据:`, JSON.stringify(flowResponse.data, null, 2));
  443. if (flowResponse.status !== 200) {
  444. return {
  445. success: false,
  446. message: `创建登录流程失败,状态码: ${flowResponse.status}`,
  447. response: flowResponse.data
  448. };
  449. }
  450. const flowId = flowResponse.data?.flow_id;
  451. if (!flowId) {
  452. console.error('无法获取 flow_id');
  453. return {
  454. success: false,
  455. message: '无法获取 flow_id',
  456. response: flowResponse.data
  457. };
  458. }
  459. console.log(`获取到 flow_id: ${flowId}`);
  460. // ==========================================
  461. // 步骤2: 提交用户名和密码 (Submit Credentials)
  462. // ==========================================
  463. console.log('[2/3] 提交用户名和密码...');
  464. const loginResponse = await axios.post(
  465. `${targetBaseUrl}/auth/login_flow/${flowId}`,
  466. {
  467. username: credentials.username,
  468. password: credentials.password,
  469. client_id: CLIENT_ID // 【重要】:必须和步骤1的 client_id 完全一致
  470. },
  471. {
  472. headers: baseHeaders,
  473. validateStatus: function (status) {
  474. return status >= 200 && status < 500;
  475. }
  476. }
  477. );
  478. console.log(`登录响应状态码: ${loginResponse.status}`);
  479. console.log(`登录响应数据:`, JSON.stringify(loginResponse.data, null, 2));
  480. // ==========================================
  481. // 步骤3: 换取 Token(全托管方案)
  482. // ==========================================
  483. const responseData = loginResponse.data || {};
  484. const responseType = responseData.type;
  485. console.log(`响应类型: ${responseType}`);
  486. // 如果登录成功,type 为 'create_entry',result 字段包含 Authorization Code
  487. if (responseData.result && responseType === 'create_entry') {
  488. const authCode = responseData.result;
  489. console.log('[3/4] 登录成功!获取到 Authorization Code:', authCode);
  490. console.log('[3/4] Node.js 将代替浏览器换取 Token...');
  491. try {
  492. // ==========================================
  493. // Node.js 直接换取 Token(避免前端路由抢跑问题)
  494. // ==========================================
  495. const tokenResponse = await axios.post(
  496. `${targetBaseUrl}/auth/token`,
  497. new URLSearchParams({
  498. grant_type: 'authorization_code',
  499. code: authCode,
  500. client_id: CLIENT_ID
  501. }).toString(),
  502. {
  503. headers: {
  504. 'Content-Type': 'application/x-www-form-urlencoded'
  505. }
  506. }
  507. );
  508. const tokens = tokenResponse.data;
  509. console.log('[4/4] ✅ Token 换取成功!');
  510. console.log(`Access Token: ${tokens.access_token.substring(0, 20)}...`);
  511. console.log(`Token 类型: ${tokens.token_type}`);
  512. console.log(`过期时间: ${tokens.expires_in}秒`);
  513. // OAuth2 跨端口方案:返回带有 code 的 URL,但使用增强的中间页面
  514. // 虽然获取了 Token,但由于跨端口限制,我们仍然使用 code 方式
  515. // 只是添加更好的处理逻辑
  516. const magicLink = `${REDIRECT_URI}&code=${encodeURIComponent(authCode)}`;
  517. return {
  518. success: true,
  519. useEnhancedRedirect: true, // 使用增强的重定向方案
  520. redirectUrl: magicLink,
  521. tokens: tokens, // 保留 Token 信息用于日志
  522. targetBaseUrl: targetBaseUrl,
  523. cookies: [],
  524. response: loginResponse.data
  525. };
  526. } catch (tokenError) {
  527. console.error('❌ Token 换取失败:', tokenError.message);
  528. if (tokenError.response) {
  529. console.error('Token 响应:', JSON.stringify(tokenError.response.data, null, 2));
  530. }
  531. // 如果 Token 换取失败,降级到传统方式
  532. console.log('⚠️ 降级到传统 redirect 方式...');
  533. const magicLink = `${REDIRECT_URI}&code=${encodeURIComponent(authCode)}`;
  534. return {
  535. success: true,
  536. redirectUrl: magicLink,
  537. cookies: [],
  538. response: loginResponse.data
  539. };
  540. }
  541. } else {
  542. console.error('❌ 登录失败!未返回 Authorization Code');
  543. console.error('响应数据:', responseData);
  544. // 提取错误信息
  545. const errorMessage = responseData.errors?.base?.[0]
  546. || responseData.errors?.username?.[0]
  547. || responseData.errors?.password?.[0]
  548. || responseData.message
  549. || `登录失败,响应类型: ${responseType}`;
  550. return {
  551. success: false,
  552. message: errorMessage,
  553. response: responseData
  554. };
  555. }
  556. } catch (error) {
  557. console.error('Home Assistant 登录流程异常:', error.message);
  558. if (error.response) {
  559. console.error('响应状态:', error.response.status);
  560. console.error('响应数据:', JSON.stringify(error.response.data, null, 2));
  561. return {
  562. success: false,
  563. message: `登录失败: ${error.response.status} - ${JSON.stringify(error.response.data)}`,
  564. response: error.response.data
  565. };
  566. }
  567. return {
  568. success: false,
  569. message: `登录失败: ${error.message}`,
  570. response: null
  571. };
  572. }
  573. }
  574. // 处理普通表单登录(未加密)
  575. async function handlePlainFormLogin(config, credentials) {
  576. const { targetBaseUrl, loginUrl, loginMethodConfig } = config;
  577. const { usernameField, passwordField, captchaField, contentType, successCode, successField } = loginMethodConfig;
  578. console.log('=== 普通表单登录 ===');
  579. console.log(`目标URL: ${targetBaseUrl}${loginUrl}`);
  580. console.log(`用户名: ${credentials.username}`);
  581. console.log(`密码: ${'*'.repeat(credentials.password.length)}`);
  582. console.log(`内容类型: ${contentType}`);
  583. console.log(`成功标识字段: ${successField || 'code'}, 成功值: ${successCode}`);
  584. // 构建请求数据
  585. const requestData = {
  586. [usernameField]: credentials.username,
  587. [passwordField]: credentials.password
  588. };
  589. if (captchaField) {
  590. requestData[captchaField] = '';
  591. }
  592. // 发送登录请求
  593. const headers = {};
  594. let requestBody;
  595. if (contentType === 'application/x-www-form-urlencoded') {
  596. headers['Content-Type'] = 'application/x-www-form-urlencoded';
  597. requestBody = new URLSearchParams(requestData).toString();
  598. } else if (contentType === 'application/json') {
  599. headers['Content-Type'] = 'application/json';
  600. requestBody = JSON.stringify(requestData);
  601. } else {
  602. requestBody = requestData;
  603. }
  604. console.log(`发送登录请求到: ${targetBaseUrl}${loginUrl}`);
  605. console.log(`请求头:`, JSON.stringify(headers, null, 2));
  606. console.log(`请求体:`, contentType === 'application/json' ? requestBody : requestBody.substring(0, 200) + '...');
  607. const loginResponse = await axios.post(
  608. `${targetBaseUrl}${loginUrl}`,
  609. requestBody,
  610. {
  611. headers,
  612. withCredentials: true,
  613. maxRedirects: 0,
  614. validateStatus: function (status) {
  615. return status >= 200 && status < 400;
  616. }
  617. }
  618. );
  619. console.log(`登录响应状态码: ${loginResponse.status}`);
  620. console.log(`响应数据:`, JSON.stringify(loginResponse.data, null, 2));
  621. // 检查登录是否成功
  622. const responseData = loginResponse.data || {};
  623. const successValue = successField ? responseData[successField] : responseData.code;
  624. console.log(`成功标识值: ${successValue}, 期望值: ${successCode}`);
  625. if (successValue === successCode) {
  626. const cookies = loginResponse.headers['set-cookie'] || [];
  627. console.log(`登录成功!获取到 ${cookies.length} 个 Cookie`);
  628. cookies.forEach((cookie, index) => {
  629. console.log(`Cookie ${index + 1}: ${cookie.substring(0, 100)}...`);
  630. });
  631. return {
  632. success: true,
  633. cookies: cookies,
  634. response: loginResponse.data
  635. };
  636. } else {
  637. console.error(`登录失败!响应:`, responseData);
  638. return {
  639. success: false,
  640. message: responseData.msg || responseData.message || '登录失败',
  641. response: responseData
  642. };
  643. }
  644. }
  645. // 通用的自动登录端点
  646. app.get('/api/auto-login/:siteId', async (req, res) => {
  647. const startTime = Date.now();
  648. const requestId = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
  649. // 立即输出日志,确认请求已到达
  650. console.log('\n' + '='.repeat(80));
  651. console.log(`[${requestId}] ⚡⚡⚡ 收到自动登录请求!⚡⚡⚡`);
  652. console.log(`[${requestId}] 时间: ${new Date().toISOString()}`);
  653. console.log(`[${requestId}] 请求路径: ${req.path}`);
  654. console.log(`[${requestId}] 请求方法: ${req.method}`);
  655. console.log(`[${requestId}] 完整URL: ${req.protocol}://${req.get('host')}${req.originalUrl}`);
  656. console.log(`[${requestId}] 客户端IP: ${req.ip || req.connection.remoteAddress || req.socket.remoteAddress}`);
  657. console.log(`[${requestId}] User-Agent: ${req.get('user-agent') || 'Unknown'}`);
  658. try {
  659. const { siteId } = req.params;
  660. console.log(`[${requestId}] 网站ID: ${siteId}`);
  661. // 获取网站配置
  662. const config = autoLoginConfig[siteId];
  663. if (!config) {
  664. console.error(`[${requestId}] 错误: 未找到网站ID "${siteId}" 的配置`);
  665. console.error(`[${requestId}] 可用的网站ID: ${Object.keys(autoLoginConfig).join(', ') || '无'}`);
  666. return res.status(404).json({
  667. success: false,
  668. message: `未找到网站ID "${siteId}" 的配置`,
  669. availableSites: Object.keys(autoLoginConfig)
  670. });
  671. }
  672. console.log(`[${requestId}] 网站名称: ${config.name}`);
  673. console.log(`[${requestId}] 目标地址: ${config.targetBaseUrl}`);
  674. console.log(`[${requestId}] 登录方法: ${config.loginMethod}`);
  675. // 获取登录凭据(优先使用环境变量)
  676. const envUsername = process.env[config.credentials.envUsername];
  677. const envPassword = process.env[config.credentials.envPassword];
  678. const credentials = {
  679. username: envUsername || config.credentials.username,
  680. password: envPassword || config.credentials.password
  681. };
  682. console.log(`[${requestId}] 凭据来源: ${envUsername ? '环境变量' : '配置文件'}`);
  683. console.log(`[${requestId}] 用户名: ${credentials.username}`);
  684. console.log(`[${requestId}] 密码: ${'*'.repeat(credentials.password.length)}`);
  685. if (!credentials.username || !credentials.password) {
  686. console.error(`[${requestId}] 错误: 登录凭据未配置`);
  687. return res.status(400).json({
  688. success: false,
  689. message: '登录凭据未配置'
  690. });
  691. }
  692. // 根据登录方法处理登录
  693. let loginResult;
  694. console.log(`[${requestId}] 开始执行登录...`);
  695. switch (config.loginMethod) {
  696. case 'rsa-encrypted-form':
  697. loginResult = await handleRSAEncryptedFormLogin(config, credentials);
  698. break;
  699. case 'plain-form':
  700. loginResult = await handlePlainFormLogin(config, credentials);
  701. break;
  702. case 'home-assistant':
  703. loginResult = await handleHomeAssistantLogin(config, credentials);
  704. break;
  705. default:
  706. console.error(`[${requestId}] 错误: 不支持的登录方法: ${config.loginMethod}`);
  707. return res.status(400).json({
  708. success: false,
  709. message: `不支持的登录方法: ${config.loginMethod}`
  710. });
  711. }
  712. if (!loginResult.success) {
  713. console.error(`[${requestId}] 登录失败:`, loginResult.message);
  714. console.error(`[${requestId}] 失败响应:`, JSON.stringify(loginResult.response, null, 2));
  715. const duration = Date.now() - startTime;
  716. console.log(`[${requestId}] 总耗时: ${duration}ms`);
  717. console.log('='.repeat(80) + '\n');
  718. // 返回错误页面而不是 JSON
  719. const errorHtml = `
  720. <!DOCTYPE html>
  721. <html lang="zh-CN">
  722. <head>
  723. <meta charset="UTF-8">
  724. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  725. <title>自动登录失败</title>
  726. <style>
  727. body {
  728. display: flex;
  729. justify-content: center;
  730. align-items: center;
  731. height: 100vh;
  732. margin: 0;
  733. font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  734. background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
  735. }
  736. .error-container {
  737. background: white;
  738. padding: 40px;
  739. border-radius: 12px;
  740. box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
  741. max-width: 600px;
  742. text-align: center;
  743. }
  744. .error-icon {
  745. font-size: 64px;
  746. margin-bottom: 20px;
  747. }
  748. .error-title {
  749. font-size: 24px;
  750. color: #e74c3c;
  751. margin-bottom: 15px;
  752. }
  753. .error-message {
  754. font-size: 16px;
  755. color: #666;
  756. margin-bottom: 20px;
  757. line-height: 1.6;
  758. }
  759. .error-details {
  760. background: #f8f9fa;
  761. padding: 15px;
  762. border-radius: 8px;
  763. margin-top: 20px;
  764. text-align: left;
  765. font-size: 14px;
  766. color: #555;
  767. }
  768. .error-details pre {
  769. margin: 0;
  770. white-space: pre-wrap;
  771. word-wrap: break-word;
  772. }
  773. </style>
  774. </head>
  775. <body>
  776. <div class="error-container">
  777. <div class="error-icon">❌</div>
  778. <div class="error-title">自动登录失败</div>
  779. <div class="error-message">${loginResult.message}</div>
  780. <div class="error-details">
  781. <strong>请求ID:</strong> ${requestId}<br>
  782. <strong>网站:</strong> ${config.name}<br>
  783. <strong>详细信息:</strong>
  784. <pre>${JSON.stringify(loginResult.response, null, 2)}</pre>
  785. </div>
  786. <button onclick="window.history.back()" style="margin-top: 20px; padding: 10px 20px; background: #3498db; color: white; border: none; border-radius: 6px; cursor: pointer;">返回</button>
  787. </div>
  788. </body>
  789. </html>
  790. `;
  791. return res.status(500).send(errorHtml);
  792. }
  793. console.log(`[${requestId}] 登录成功!`);
  794. // OAuth2 跨端口:调试页面方案
  795. if (config.loginMethod === 'home-assistant' && loginResult.useEnhancedRedirect) {
  796. console.log(`[${requestId}] 🚀 Home Assistant OAuth2 - 调试重定向方案`);
  797. console.log(`[${requestId}] Token 已获取: ${loginResult.tokens.access_token.substring(0, 30)}...`);
  798. console.log(`[${requestId}] Authorization Code: ${loginResult.redirectUrl.match(/code=([^&]+)/)?.[1]}`);
  799. console.log(`[${requestId}] 重定向 URL: ${loginResult.redirectUrl}`);
  800. const magicLink = loginResult.redirectUrl;
  801. const authCode = magicLink.match(/code=([^&]+)/)?.[1] || 'unknown';
  802. // 生成调试页面
  803. const debugHtml = `
  804. <!DOCTYPE html>
  805. <html lang="zh-CN">
  806. <head>
  807. <meta charset="UTF-8">
  808. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  809. <title>Home Assistant OAuth2 调试</title>
  810. <style>
  811. body {
  812. font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  813. background: #1e1e1e;
  814. color: #d4d4d4;
  815. padding: 20px;
  816. margin: 0;
  817. }
  818. .container {
  819. max-width: 1000px;
  820. margin: 0 auto;
  821. }
  822. h1 {
  823. color: #4ec9b0;
  824. border-bottom: 2px solid #4ec9b0;
  825. padding-bottom: 10px;
  826. }
  827. .section {
  828. background: #252526;
  829. border: 1px solid #3e3e42;
  830. border-radius: 8px;
  831. padding: 20px;
  832. margin: 20px 0;
  833. }
  834. .section h2 {
  835. color: #569cd6;
  836. margin-top: 0;
  837. }
  838. pre {
  839. background: #1e1e1e;
  840. border: 1px solid #3e3e42;
  841. border-radius: 4px;
  842. padding: 15px;
  843. overflow-x: auto;
  844. }
  845. .success { color: #4ec9b0; }
  846. .warning { color: #ce9178; }
  847. .error { color: #f48771; }
  848. .button {
  849. background: #0e639c;
  850. color: white;
  851. border: none;
  852. padding: 12px 24px;
  853. border-radius: 6px;
  854. font-size: 16px;
  855. cursor: pointer;
  856. margin: 10px 5px;
  857. }
  858. .button:hover { background: #1177bb; }
  859. .button.secondary {
  860. background: #3e3e42;
  861. }
  862. .button.secondary:hover { background: #505050; }
  863. #log {
  864. background: #1e1e1e;
  865. border: 1px solid #3e3e42;
  866. border-radius: 4px;
  867. padding: 15px;
  868. max-height: 300px;
  869. overflow-y: auto;
  870. font-family: 'Consolas', 'Monaco', monospace;
  871. font-size: 12px;
  872. }
  873. .log-entry {
  874. margin: 5px 0;
  875. padding: 5px;
  876. border-left: 3px solid #569cd6;
  877. padding-left: 10px;
  878. }
  879. </style>
  880. </head>
  881. <body>
  882. <div class="container">
  883. <h1>🔍 Home Assistant OAuth2 登录调试</h1>
  884. <div class="section">
  885. <h2>✅ 后端登录成功</h2>
  886. <p>Authorization Code 已获取,Token 已验证。</p>
  887. <p><strong class="success">Authorization Code:</strong> <code>${authCode.substring(0, 20)}...</code></p>
  888. </div>
  889. <div class="section">
  890. <h2>📋 OAuth2 跨端口问题分析</h2>
  891. <p class="warning">⚠️ 检测到跨端口场景:</p>
  892. <ul>
  893. <li>Node.js 后端:<code>222.243.138.146:8889</code></li>
  894. <li>Home Assistant:<code>222.243.138.146:8123</code></li>
  895. <li>localStorage 隔离:不同端口无法共享 Token</li>
  896. </ul>
  897. </div>
  898. <div class="section">
  899. <h2>🎯 手动测试步骤</h2>
  900. <p>请按以下步骤测试,帮助我们诊断问题:</p>
  901. <h3>测试 1:直接访问魔术链接</h3>
  902. <p>复制下面的 URL 到新标签页,看是否能登录:</p>
  903. <pre>${magicLink}</pre>
  904. <button class="button" onclick="window.open('${magicLink}', '_blank')">
  905. 🔗 在新标签页打开
  906. </button>
  907. <h3>测试 2:在当前标签页跳转</h3>
  908. <p>让当前页面跳转过去(可能有更好的效果):</p>
  909. <button class="button secondary" onclick="window.location.href='${magicLink}'">
  910. ➡️ 当前标签页跳转
  911. </button>
  912. <h3>测试 3:iframe 预加载然后跳转</h3>
  913. <p>使用 iframe 预加载,5秒后跳转:</p>
  914. <button class="button secondary" onclick="testIframeMethod()">
  915. 🔄 使用 iframe 方案
  916. </button>
  917. </div>
  918. <div class="section">
  919. <h2>📊 实时日志</h2>
  920. <div id="log"></div>
  921. </div>
  922. <div class="section">
  923. <h2>💡 建议</h2>
  924. <p>如果以上测试都失败,强烈建议使用 <strong class="success">Trusted Networks</strong> 方案:</p>
  925. <ul>
  926. <li>✅ 官方支持,100% 可靠</li>
  927. <li>✅ 无需复杂的 OAuth2 流程</li>
  928. <li>✅ 零延迟,直接登录</li>
  929. </ul>
  930. <button class="button" onclick="alert('请在 Home Assistant 的 configuration.yaml 中配置:\\n\\nhomeassistant:\\n auth_providers:\\n - type: trusted_networks\\n trusted_networks:\\n - 118.251.191.88/32\\n trusted_users:\\n 118.251.191.88/32: YOUR_USER_ID\\n allow_bypass_login: true\\n - type: homeassistant')">
  931. 📖 查看 Trusted Networks 配置
  932. </button>
  933. </div>
  934. </div>
  935. <iframe id="testFrame" style="display:none;"></iframe>
  936. <script>
  937. const logDiv = document.getElementById('log');
  938. const iframe = document.getElementById('testFrame');
  939. function addLog(msg, type = 'info') {
  940. const entry = document.createElement('div');
  941. entry.className = 'log-entry';
  942. entry.textContent = new Date().toLocaleTimeString() + ' - ' + msg;
  943. logDiv.appendChild(entry);
  944. logDiv.scrollTop = logDiv.scrollHeight;
  945. console.log('[调试] ' + msg);
  946. }
  947. function testIframeMethod() {
  948. addLog('开始 iframe 测试...');
  949. addLog('加载 URL: ${magicLink}');
  950. let loaded = false;
  951. iframe.onload = function() {
  952. if (!loaded) {
  953. loaded = true;
  954. addLog('✓ iframe 加载完成');
  955. addLog('等待 5 秒后跳转...');
  956. let countdown = 5;
  957. const timer = setInterval(function() {
  958. countdown--;
  959. addLog('倒计时: ' + countdown + '秒');
  960. if (countdown <= 0) {
  961. clearInterval(timer);
  962. addLog('正在跳转到 Home Assistant...');
  963. window.location.href = '${targetBaseUrl}';
  964. }
  965. }, 1000);
  966. }
  967. };
  968. iframe.onerror = function(e) {
  969. addLog('✗ iframe 加载失败: ' + e, 'error');
  970. };
  971. iframe.src = '${magicLink}';
  972. }
  973. addLog('后端 OAuth2 登录成功');
  974. addLog('Authorization Code: ${authCode.substring(0, 20)}...');
  975. addLog('请选择测试方法');
  976. </script>
  977. </body>
  978. </html>
  979. `;
  980. console.log(`[${requestId}] 返回调试页面,供手动测试`);
  981. console.log(`[${requestId}] 总耗时: ${Date.now() - startTime}ms`);
  982. console.log('='.repeat(80) + '\n');
  983. return res.send(debugHtml);
  984. }
  985. // 对于 Home Assistant,如果使用传统 redirect 方式(降级方案)
  986. if (config.loginMethod === 'home-assistant' && loginResult.redirectUrl) {
  987. console.log(`[${requestId}] Home Assistant 登录成功,使用传统 redirect 方式(降级)`);
  988. console.log(`[${requestId}] 重定向到: ${loginResult.redirectUrl}`);
  989. // 使用中间页面而不是直接 redirect
  990. // 这样可以添加延迟,让 HA 前端有时间处理 code
  991. const intermediateHtml = `
  992. <!DOCTYPE html>
  993. <html lang="zh-CN">
  994. <head>
  995. <meta charset="UTF-8">
  996. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  997. <title>正在登录 Home Assistant...</title>
  998. <style>
  999. body {
  1000. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  1001. color: white;
  1002. font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  1003. display: flex;
  1004. justify-content: center;
  1005. align-items: center;
  1006. height: 100vh;
  1007. margin: 0;
  1008. }
  1009. .container { text-align: center; }
  1010. .loader {
  1011. border: 4px solid rgba(255, 255, 255, 0.3);
  1012. border-top: 4px solid white;
  1013. border-radius: 50%;
  1014. width: 50px;
  1015. height: 50px;
  1016. animation: spin 1s linear infinite;
  1017. margin: 0 auto 20px;
  1018. }
  1019. @keyframes spin {
  1020. 0% { transform: rotate(0deg); }
  1021. 100% { transform: rotate(360deg); }
  1022. }
  1023. h2 { margin: 0 0 10px 0; }
  1024. p { margin: 5px 0; opacity: 0.9; font-size: 14px; }
  1025. </style>
  1026. </head>
  1027. <body>
  1028. <div class="container">
  1029. <div class="loader"></div>
  1030. <h2>正在登录...</h2>
  1031. <p>准备进入 Home Assistant</p>
  1032. </div>
  1033. <iframe id="authFrame" style="display:none;"></iframe>
  1034. <script>
  1035. // 使用 iframe 预加载带有 code 的 URL
  1036. // 让 HA 前端在后台完成 code → token 的交换
  1037. const authUrl = "${loginResult.redirectUrl}";
  1038. const targetUrl = "${config.targetBaseUrl}";
  1039. const iframe = document.getElementById('authFrame');
  1040. console.log('[降级方案] 使用 iframe 预加载:', authUrl);
  1041. // 设置 iframe 超时
  1042. let loaded = false;
  1043. iframe.onload = function() {
  1044. if (!loaded) {
  1045. loaded = true;
  1046. console.log('[降级方案] iframe 加载完成,等待 HA 处理 code...');
  1047. // 给 HA 足够时间处理 code
  1048. setTimeout(function() {
  1049. console.log('[降级方案] 跳转到主页');
  1050. window.location.href = targetUrl;
  1051. }, 2000);
  1052. }
  1053. };
  1054. // 加载带有 code 的 URL
  1055. iframe.src = authUrl;
  1056. // 保险起见,10秒后强制跳转
  1057. setTimeout(function() {
  1058. if (!loaded) {
  1059. console.log('[降级方案] 超时,强制跳转');
  1060. window.location.href = targetUrl;
  1061. }
  1062. }, 10000);
  1063. </script>
  1064. </body>
  1065. </html>
  1066. `;
  1067. console.log(`[${requestId}] 总耗时: ${Date.now() - startTime}ms`);
  1068. console.log('='.repeat(80) + '\n');
  1069. return res.send(intermediateHtml);
  1070. }
  1071. // 解析 Cookie
  1072. const cookieData = parseCookies(loginResult.cookies);
  1073. console.log(`[${requestId}] 解析到 ${cookieData.length} 个 Cookie:`);
  1074. cookieData.forEach((cookie, index) => {
  1075. console.log(`[${requestId}] Cookie ${index + 1}: ${cookie.name} = ${cookie.value.substring(0, 20)}...`);
  1076. });
  1077. // 生成跳转 HTML
  1078. let redirectUrl = `http://${config.targetHost}/`;
  1079. console.log(`[${requestId}] 生成跳转页面,目标: ${redirectUrl}`);
  1080. const html = generateRedirectHTML(
  1081. cookieData,
  1082. config.targetHost,
  1083. config.targetDomain,
  1084. requestId,
  1085. redirectUrl,
  1086. null
  1087. );
  1088. // 在响应头中设置 Cookie
  1089. console.log(`[${requestId}] 设置响应头 Cookie...`);
  1090. loginResult.cookies.forEach((cookie, index) => {
  1091. // 修改 Cookie 的 Domain,移除端口号
  1092. let modifiedCookie = cookie.replace(/Domain=[^;]+/i, `Domain=${config.targetDomain}`);
  1093. res.setHeader('Set-Cookie', modifiedCookie);
  1094. console.log(`[${requestId}] 设置 Cookie ${index + 1}: ${modifiedCookie.substring(0, 80)}...`);
  1095. });
  1096. const duration = Date.now() - startTime;
  1097. console.log(`[${requestId}] 总耗时: ${duration}ms`);
  1098. console.log(`[${requestId}] 返回跳转页面`);
  1099. console.log('='.repeat(80) + '\n');
  1100. res.send(html);
  1101. } catch (error) {
  1102. const duration = Date.now() - startTime;
  1103. console.error(`[${requestId}] 自动登录异常:`, error.message);
  1104. console.error(`[${requestId}] 错误堆栈:`, error.stack);
  1105. if (error.response) {
  1106. console.error(`[${requestId}] 响应状态:`, error.response.status);
  1107. console.error(`[${requestId}] 响应头:`, JSON.stringify(error.response.headers, null, 2));
  1108. console.error(`[${requestId}] 响应数据:`, JSON.stringify(error.response.data, null, 2));
  1109. }
  1110. if (error.request) {
  1111. console.error(`[${requestId}] 请求信息:`, {
  1112. url: error.config?.url,
  1113. method: error.config?.method,
  1114. headers: error.config?.headers
  1115. });
  1116. }
  1117. console.log(`[${requestId}] 总耗时: ${duration}ms`);
  1118. console.log('='.repeat(80) + '\n');
  1119. res.status(500).json({
  1120. success: false,
  1121. message: '自动登录失败: ' + error.message,
  1122. error: process.env.NODE_ENV === 'development' ? error.stack : undefined
  1123. });
  1124. }
  1125. });
  1126. // Home Assistant 登录代理端点(解决浏览器 CORS 问题)
  1127. app.post('/api/home-assistant-proxy/login-flow', async (req, res) => {
  1128. try {
  1129. const targetBaseUrl = req.body.targetBaseUrl;
  1130. console.log('[代理] 创建 Home Assistant 登录流程:', targetBaseUrl);
  1131. const response = await axios.post(
  1132. `${targetBaseUrl}/auth/login_flow`,
  1133. {
  1134. client_id: `${targetBaseUrl}/`,
  1135. handler: ['homeassistant', null],
  1136. redirect_uri: `${targetBaseUrl}/`
  1137. },
  1138. {
  1139. headers: {
  1140. 'Content-Type': 'application/json'
  1141. }
  1142. }
  1143. );
  1144. res.json(response.data);
  1145. } catch (error) {
  1146. console.error('[代理] 创建登录流程失败:', error.message);
  1147. res.status(500).json({ error: error.message });
  1148. }
  1149. });
  1150. app.post('/api/home-assistant-proxy/login', async (req, res) => {
  1151. try {
  1152. const { targetBaseUrl, flowId, username, password } = req.body;
  1153. console.log('[代理] 提交 Home Assistant 登录凭据:', targetBaseUrl, flowId);
  1154. const response = await axios.post(
  1155. `${targetBaseUrl}/auth/login_flow/${flowId}`,
  1156. {
  1157. username: username,
  1158. password: password,
  1159. client_id: `${targetBaseUrl}/`
  1160. },
  1161. {
  1162. headers: {
  1163. 'Content-Type': 'application/json'
  1164. }
  1165. }
  1166. );
  1167. res.json(response.data);
  1168. } catch (error) {
  1169. console.error('[代理] 登录失败:', error.message);
  1170. res.status(500).json({ error: error.message });
  1171. }
  1172. });
  1173. // 获取所有配置的网站列表
  1174. app.get('/api/auto-login', (req, res) => {
  1175. const sites = Object.keys(autoLoginConfig).map(siteId => ({
  1176. id: siteId,
  1177. name: autoLoginConfig[siteId].name,
  1178. endpoint: `/api/auto-login/${siteId}`
  1179. }));
  1180. res.json({ sites });
  1181. });
  1182. // 健康检查端点
  1183. app.get('/api/health', (req, res) => {
  1184. res.json({
  1185. status: 'ok',
  1186. timestamp: new Date().toISOString(),
  1187. port: PORT,
  1188. configuredSites: Object.keys(autoLoginConfig)
  1189. });
  1190. });
  1191. // 测试端点 - 用于验证配置
  1192. app.get('/api/test/:siteId', (req, res) => {
  1193. const { siteId } = req.params;
  1194. const config = autoLoginConfig[siteId];
  1195. if (!config) {
  1196. return res.json({
  1197. success: false,
  1198. message: `未找到网站ID "${siteId}" 的配置`,
  1199. availableSites: Object.keys(autoLoginConfig)
  1200. });
  1201. }
  1202. const envUsername = process.env[config.credentials.envUsername];
  1203. const envPassword = process.env[config.credentials.envPassword];
  1204. const credentials = {
  1205. username: envUsername || config.credentials.username,
  1206. password: envPassword || config.credentials.password
  1207. };
  1208. res.json({
  1209. success: true,
  1210. siteId,
  1211. config: {
  1212. name: config.name,
  1213. targetBaseUrl: config.targetBaseUrl,
  1214. loginMethod: config.loginMethod,
  1215. loginUrl: config.loginUrl,
  1216. hasCredentials: !!(credentials.username && credentials.password),
  1217. credentialsSource: envUsername ? '环境变量' : '配置文件',
  1218. username: credentials.username,
  1219. passwordLength: credentials.password ? credentials.password.length : 0
  1220. }
  1221. });
  1222. });
  1223. app.listen(PORT, '0.0.0.0', () => {
  1224. console.log('\n' + '='.repeat(80));
  1225. console.log('🚀 后端服务器启动成功!');
  1226. console.log('='.repeat(80));
  1227. console.log(`📍 本地地址: http://localhost:${PORT}`);
  1228. console.log(`📍 服务器地址: http://0.0.0.0:${PORT}`);
  1229. console.log(`📍 外部访问: http://222.243.138.146:${PORT} (通过防火墙端口映射)`);
  1230. console.log(`\n📋 已配置的自动登录网站: ${Object.keys(autoLoginConfig).join(', ') || '无'}`);
  1231. console.log(`\n🔗 可用端点:`);
  1232. console.log(` - 健康检查: http://localhost:${PORT}/api/health`);
  1233. console.log(` - 测试配置: http://localhost:${PORT}/api/test/:siteId`);
  1234. console.log(` - 自动登录: http://localhost:${PORT}/api/auto-login/:siteId`);
  1235. console.log(`\n💡 提示: 确保防火墙已配置端口映射 (前端:8888, 后端:8889 -> 外网)`);
  1236. console.log('='.repeat(80) + '\n');
  1237. });