server.js 47 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230
  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 流程)
  403. async function handleHomeAssistantLogin(config, credentials) {
  404. const { targetBaseUrl } = config;
  405. console.log('=== Home Assistant 登录 ===');
  406. console.log(`目标URL: ${targetBaseUrl}`);
  407. console.log(`用户名: ${credentials.username}`);
  408. console.log(`密码: ${'*'.repeat(credentials.password.length)}`);
  409. // 创建 Cookie jar 来保持会话
  410. const cookieJar = new CookieJar();
  411. const client = wrapper(axios.create({
  412. jar: cookieJar,
  413. withCredentials: true
  414. }));
  415. const baseHeaders = {
  416. 'Content-Type': 'application/json',
  417. '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',
  418. 'Accept': 'application/json, text/plain, */*',
  419. 'Origin': targetBaseUrl,
  420. 'Referer': `${targetBaseUrl}/`
  421. };
  422. // 先访问首页获取初始 Cookie(如果需要)
  423. let initialCookies = [];
  424. try {
  425. console.log('访问首页获取初始 Cookie...');
  426. const homeResponse = await client.get(`${targetBaseUrl}/`, {
  427. headers: {
  428. 'User-Agent': baseHeaders['User-Agent']
  429. },
  430. maxRedirects: 5
  431. });
  432. initialCookies = homeResponse.headers['set-cookie'] || [];
  433. console.log(`获取到 ${initialCookies.length} 个初始 Cookie`);
  434. // 从 Cookie jar 中获取 Cookie
  435. const jarCookies = await cookieJar.getCookies(targetBaseUrl);
  436. console.log(`Cookie jar 中有 ${jarCookies.length} 个 Cookie`);
  437. } catch (error) {
  438. console.log('访问首页失败(可能不需要):', error.message);
  439. }
  440. // 步骤1: 创建登录流程
  441. console.log('步骤1: 创建登录流程...');
  442. let flowResponse;
  443. try {
  444. flowResponse = await client.post(
  445. `${targetBaseUrl}/auth/login_flow`,
  446. {
  447. client_id: `${targetBaseUrl}/`,
  448. handler: ['homeassistant', null],
  449. redirect_uri: `${targetBaseUrl}/`
  450. },
  451. {
  452. headers: baseHeaders,
  453. maxRedirects: 0,
  454. validateStatus: function (status) {
  455. return status >= 200 && status < 500;
  456. }
  457. }
  458. );
  459. } catch (error) {
  460. console.error('创建登录流程失败:', error.message);
  461. if (error.response) {
  462. console.error('响应状态:', error.response.status);
  463. console.error('响应数据:', JSON.stringify(error.response.data, null, 2));
  464. return {
  465. success: false,
  466. message: `创建登录流程失败: ${error.response.status} - ${JSON.stringify(error.response.data)}`,
  467. response: error.response.data
  468. };
  469. }
  470. return {
  471. success: false,
  472. message: `创建登录流程失败: ${error.message}`,
  473. response: null
  474. };
  475. }
  476. console.log(`流程创建响应状态码: ${flowResponse.status}`);
  477. console.log(`流程创建响应数据:`, JSON.stringify(flowResponse.data, null, 2));
  478. if (flowResponse.status !== 200) {
  479. return {
  480. success: false,
  481. message: `创建登录流程失败,状态码: ${flowResponse.status}`,
  482. response: flowResponse.data
  483. };
  484. }
  485. const flowId = flowResponse.data?.flow_id;
  486. if (!flowId) {
  487. console.error('无法获取 flow_id');
  488. return {
  489. success: false,
  490. message: '无法获取 flow_id',
  491. response: flowResponse.data
  492. };
  493. }
  494. console.log(`获取到 flow_id: ${flowId}`);
  495. // 步骤2: 提交用户名和密码
  496. console.log('步骤2: 提交用户名和密码...');
  497. let loginResponse;
  498. try {
  499. loginResponse = await client.post(
  500. `${targetBaseUrl}/auth/login_flow/${flowId}`,
  501. {
  502. username: credentials.username,
  503. password: credentials.password,
  504. client_id: `${targetBaseUrl}/`
  505. },
  506. {
  507. headers: baseHeaders,
  508. maxRedirects: 0,
  509. validateStatus: function (status) {
  510. return status >= 200 && status < 500;
  511. }
  512. }
  513. );
  514. } catch (error) {
  515. console.error('提交登录信息失败:', error.message);
  516. if (error.response) {
  517. console.error('响应状态:', error.response.status);
  518. console.error('响应数据:', JSON.stringify(error.response.data, null, 2));
  519. return {
  520. success: false,
  521. message: `提交登录信息失败: ${error.response.status} - ${JSON.stringify(error.response.data)}`,
  522. response: error.response.data
  523. };
  524. }
  525. return {
  526. success: false,
  527. message: `提交登录信息失败: ${error.message}`,
  528. response: null
  529. };
  530. }
  531. console.log(`登录响应状态码: ${loginResponse.status}`);
  532. console.log(`登录响应数据:`, JSON.stringify(loginResponse.data, null, 2));
  533. // 检查登录是否成功
  534. const responseData = loginResponse.data || {};
  535. const responseType = responseData.type;
  536. console.log(`响应类型: ${responseType}`);
  537. // Home Assistant 登录成功时,type 为 "create_entry"
  538. if (responseType === 'create_entry') {
  539. // 从 Cookie jar 中获取所有 Cookie
  540. console.log('从 Cookie jar 中获取 Cookie...');
  541. const jarCookies = await cookieJar.getCookies(targetBaseUrl);
  542. console.log(`Cookie jar 中有 ${jarCookies.length} 个 Cookie`);
  543. // 合并所有请求的 Cookie(从响应头和 Cookie jar)
  544. const flowCookies = flowResponse.headers['set-cookie'] || [];
  545. const loginCookies = loginResponse.headers['set-cookie'] || [];
  546. const allCookies = [...initialCookies, ...flowCookies, ...loginCookies];
  547. // 从 Cookie jar 转换为 Set-Cookie 格式
  548. jarCookies.forEach(cookie => {
  549. let cookieStr = `${cookie.key}=${cookie.value}`;
  550. if (cookie.path) cookieStr += `; Path=${cookie.path}`;
  551. if (cookie.domain) cookieStr += `; Domain=${cookie.domain}`;
  552. if (cookie.expires) cookieStr += `; Expires=${cookie.expires.toUTCString()}`;
  553. if (cookie.maxAge) cookieStr += `; Max-Age=${cookie.maxAge}`;
  554. if (cookie.secure) cookieStr += `; Secure`;
  555. if (cookie.httpOnly) cookieStr += `; HttpOnly`;
  556. if (cookie.sameSite) cookieStr += `; SameSite=${cookie.sameSite}`;
  557. allCookies.push(cookieStr);
  558. });
  559. // 去重 Cookie(保留最后一个)
  560. const cookieMap = new Map();
  561. allCookies.forEach(cookie => {
  562. const name = cookie.split('=')[0];
  563. cookieMap.set(name, cookie);
  564. });
  565. let uniqueCookies = Array.from(cookieMap.values());
  566. console.log(`登录成功!获取到 ${uniqueCookies.length} 个唯一 Cookie`);
  567. uniqueCookies.forEach((cookie, index) => {
  568. console.log(`Cookie ${index + 1}: ${cookie.substring(0, 100)}...`);
  569. });
  570. // 步骤3: 处理 OAuth2 授权流程
  571. // 登录成功后,Home Assistant 需要完成 OAuth2 授权才能访问主页面
  572. console.log('步骤3: 处理 OAuth2 授权流程...');
  573. try {
  574. // 构建 state 参数(base64 编码的 JSON)
  575. const stateData = {
  576. hassUrl: targetBaseUrl,
  577. clientId: `${targetBaseUrl}/`
  578. };
  579. const state = Buffer.from(JSON.stringify(stateData)).toString('base64');
  580. // 构建授权 URL
  581. const redirectUri = `${targetBaseUrl}/?auth_callback=1`;
  582. const clientId = `${targetBaseUrl}/`;
  583. const authorizeUrl = `${targetBaseUrl}/auth/authorize?response_type=code&redirect_uri=${encodeURIComponent(redirectUri)}&client_id=${encodeURIComponent(clientId)}&state=${encodeURIComponent(state)}`;
  584. console.log(`访问授权端点: ${authorizeUrl}`);
  585. // 使用 Cookie jar 的客户端访问授权端点,跟随重定向
  586. const authorizeResponse = await client.get(authorizeUrl, {
  587. headers: {
  588. 'User-Agent': baseHeaders['User-Agent'],
  589. 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
  590. 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
  591. 'Referer': `${targetBaseUrl}/`
  592. },
  593. maxRedirects: 10, // 增加重定向次数
  594. validateStatus: function (status) {
  595. return status >= 200 && status < 400;
  596. }
  597. });
  598. console.log(`授权响应状态码: ${authorizeResponse.status}`);
  599. const finalUrl = authorizeResponse.request?.res?.responseUrl || authorizeResponse.config?.url || authorizeUrl;
  600. console.log(`授权最终 URL: ${finalUrl}`);
  601. // 检查是否是重定向到回调 URL
  602. if (finalUrl.includes('auth_callback=1') || finalUrl.includes('code=')) {
  603. console.log('检测到授权回调,访问回调 URL 获取 Cookie...');
  604. try {
  605. const callbackResponse = await client.get(finalUrl, {
  606. headers: {
  607. 'User-Agent': baseHeaders['User-Agent'],
  608. 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
  609. 'Referer': authorizeUrl
  610. },
  611. maxRedirects: 5,
  612. validateStatus: function (status) {
  613. return status >= 200 && status < 400;
  614. }
  615. });
  616. console.log(`回调响应状态码: ${callbackResponse.status}`);
  617. } catch (callbackError) {
  618. console.log('访问回调 URL 失败:', callbackError.message);
  619. }
  620. }
  621. // 尝试访问主页面来触发 Cookie 设置
  622. console.log('访问主页面以触发 Cookie 设置...');
  623. try {
  624. const homePageResponse = await client.get(`${targetBaseUrl}/`, {
  625. headers: {
  626. 'User-Agent': baseHeaders['User-Agent'],
  627. 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
  628. 'Referer': finalUrl
  629. },
  630. maxRedirects: 5,
  631. validateStatus: function (status) {
  632. return status >= 200 && status < 400;
  633. }
  634. });
  635. console.log(`主页面响应状态码: ${homePageResponse.status}`);
  636. } catch (homeError) {
  637. console.log('访问主页面失败:', homeError.message);
  638. }
  639. // 从 Cookie jar 中获取授权后的 Cookie
  640. const authorizeJarCookies = await cookieJar.getCookies(targetBaseUrl);
  641. console.log(`授权后 Cookie jar 中有 ${authorizeJarCookies.length} 个 Cookie`);
  642. // 获取授权响应中的 Cookie
  643. const authorizeCookies = authorizeResponse.headers['set-cookie'] || [];
  644. console.log(`授权响应获取到 ${authorizeCookies.length} 个 Cookie`);
  645. // 从 Cookie jar 转换为 Set-Cookie 格式并合并
  646. authorizeJarCookies.forEach(cookie => {
  647. let cookieStr = `${cookie.key}=${cookie.value}`;
  648. if (cookie.path) cookieStr += `; Path=${cookie.path}`;
  649. if (cookie.domain) cookieStr += `; Domain=${cookie.domain}`;
  650. if (cookie.expires) cookieStr += `; Expires=${cookie.expires.toUTCString()}`;
  651. if (cookie.maxAge) cookieStr += `; Max-Age=${cookie.maxAge}`;
  652. if (cookie.secure) cookieStr += `; Secure`;
  653. if (cookie.httpOnly) cookieStr += `; HttpOnly`;
  654. if (cookie.sameSite) cookieStr += `; SameSite=${cookie.sameSite}`;
  655. cookieMap.set(cookie.key, cookieStr);
  656. });
  657. // 合并授权响应中的 Cookie
  658. authorizeCookies.forEach(cookie => {
  659. const name = cookie.split('=')[0];
  660. cookieMap.set(name, cookie);
  661. });
  662. uniqueCookies = Array.from(cookieMap.values());
  663. console.log(`授权完成!最终获取到 ${uniqueCookies.length} 个唯一 Cookie`);
  664. // 如果还是没有 Cookie,尝试使用登录结果中的 token
  665. if (uniqueCookies.length === 0 && responseData.result) {
  666. console.log(`尝试使用登录结果中的 token: ${responseData.result}`);
  667. // 这里可以尝试使用 token 来获取访问令牌
  668. // 但 Home Assistant 的 token 通常需要通过浏览器来设置 Cookie
  669. }
  670. } catch (error) {
  671. console.log('授权流程失败(可能不需要):', error.message);
  672. if (error.response) {
  673. console.log('授权响应状态:', error.response.status);
  674. console.log('授权响应 URL:', error.response.request?.res?.responseUrl || error.config?.url);
  675. }
  676. // 授权失败不影响登录,继续使用已有的 Cookie
  677. }
  678. return {
  679. success: true,
  680. cookies: uniqueCookies,
  681. response: loginResponse.data
  682. };
  683. } else {
  684. console.error(`登录失败!响应:`, responseData);
  685. const errorMessage = responseData.errors?.base?.[0]
  686. || responseData.errors?.username?.[0]
  687. || responseData.errors?.password?.[0]
  688. || responseData.message
  689. || `登录失败,响应类型: ${responseType}`;
  690. return {
  691. success: false,
  692. message: errorMessage,
  693. response: responseData
  694. };
  695. }
  696. }
  697. // 处理普通表单登录(未加密)
  698. async function handlePlainFormLogin(config, credentials) {
  699. const { targetBaseUrl, loginUrl, loginMethodConfig } = config;
  700. const { usernameField, passwordField, captchaField, contentType, successCode, successField } = loginMethodConfig;
  701. console.log('=== 普通表单登录 ===');
  702. console.log(`目标URL: ${targetBaseUrl}${loginUrl}`);
  703. console.log(`用户名: ${credentials.username}`);
  704. console.log(`密码: ${'*'.repeat(credentials.password.length)}`);
  705. console.log(`内容类型: ${contentType}`);
  706. console.log(`成功标识字段: ${successField || 'code'}, 成功值: ${successCode}`);
  707. // 构建请求数据
  708. const requestData = {
  709. [usernameField]: credentials.username,
  710. [passwordField]: credentials.password
  711. };
  712. if (captchaField) {
  713. requestData[captchaField] = '';
  714. }
  715. // 发送登录请求
  716. const headers = {};
  717. let requestBody;
  718. if (contentType === 'application/x-www-form-urlencoded') {
  719. headers['Content-Type'] = 'application/x-www-form-urlencoded';
  720. requestBody = new URLSearchParams(requestData).toString();
  721. } else if (contentType === 'application/json') {
  722. headers['Content-Type'] = 'application/json';
  723. requestBody = JSON.stringify(requestData);
  724. } else {
  725. requestBody = requestData;
  726. }
  727. console.log(`发送登录请求到: ${targetBaseUrl}${loginUrl}`);
  728. console.log(`请求头:`, JSON.stringify(headers, null, 2));
  729. console.log(`请求体:`, contentType === 'application/json' ? requestBody : requestBody.substring(0, 200) + '...');
  730. const loginResponse = await axios.post(
  731. `${targetBaseUrl}${loginUrl}`,
  732. requestBody,
  733. {
  734. headers,
  735. withCredentials: true,
  736. maxRedirects: 0,
  737. validateStatus: function (status) {
  738. return status >= 200 && status < 400;
  739. }
  740. }
  741. );
  742. console.log(`登录响应状态码: ${loginResponse.status}`);
  743. console.log(`响应数据:`, JSON.stringify(loginResponse.data, null, 2));
  744. // 检查登录是否成功
  745. const responseData = loginResponse.data || {};
  746. const successValue = successField ? responseData[successField] : responseData.code;
  747. console.log(`成功标识值: ${successValue}, 期望值: ${successCode}`);
  748. if (successValue === successCode) {
  749. const cookies = loginResponse.headers['set-cookie'] || [];
  750. console.log(`登录成功!获取到 ${cookies.length} 个 Cookie`);
  751. cookies.forEach((cookie, index) => {
  752. console.log(`Cookie ${index + 1}: ${cookie.substring(0, 100)}...`);
  753. });
  754. return {
  755. success: true,
  756. cookies: cookies,
  757. response: loginResponse.data
  758. };
  759. } else {
  760. console.error(`登录失败!响应:`, responseData);
  761. return {
  762. success: false,
  763. message: responseData.msg || responseData.message || '登录失败',
  764. response: responseData
  765. };
  766. }
  767. }
  768. // 通用的自动登录端点
  769. app.get('/api/auto-login/:siteId', async (req, res) => {
  770. const startTime = Date.now();
  771. const requestId = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
  772. // 立即输出日志,确认请求已到达
  773. console.log('\n' + '='.repeat(80));
  774. console.log(`[${requestId}] ⚡⚡⚡ 收到自动登录请求!⚡⚡⚡`);
  775. console.log(`[${requestId}] 时间: ${new Date().toISOString()}`);
  776. console.log(`[${requestId}] 请求路径: ${req.path}`);
  777. console.log(`[${requestId}] 请求方法: ${req.method}`);
  778. console.log(`[${requestId}] 完整URL: ${req.protocol}://${req.get('host')}${req.originalUrl}`);
  779. console.log(`[${requestId}] 客户端IP: ${req.ip || req.connection.remoteAddress || req.socket.remoteAddress}`);
  780. console.log(`[${requestId}] User-Agent: ${req.get('user-agent') || 'Unknown'}`);
  781. try {
  782. const { siteId } = req.params;
  783. console.log(`[${requestId}] 网站ID: ${siteId}`);
  784. // 获取网站配置
  785. const config = autoLoginConfig[siteId];
  786. if (!config) {
  787. console.error(`[${requestId}] 错误: 未找到网站ID "${siteId}" 的配置`);
  788. console.error(`[${requestId}] 可用的网站ID: ${Object.keys(autoLoginConfig).join(', ') || '无'}`);
  789. return res.status(404).json({
  790. success: false,
  791. message: `未找到网站ID "${siteId}" 的配置`,
  792. availableSites: Object.keys(autoLoginConfig)
  793. });
  794. }
  795. console.log(`[${requestId}] 网站名称: ${config.name}`);
  796. console.log(`[${requestId}] 目标地址: ${config.targetBaseUrl}`);
  797. console.log(`[${requestId}] 登录方法: ${config.loginMethod}`);
  798. // 获取登录凭据(优先使用环境变量)
  799. const envUsername = process.env[config.credentials.envUsername];
  800. const envPassword = process.env[config.credentials.envPassword];
  801. const credentials = {
  802. username: envUsername || config.credentials.username,
  803. password: envPassword || config.credentials.password
  804. };
  805. console.log(`[${requestId}] 凭据来源: ${envUsername ? '环境变量' : '配置文件'}`);
  806. console.log(`[${requestId}] 用户名: ${credentials.username}`);
  807. console.log(`[${requestId}] 密码: ${'*'.repeat(credentials.password.length)}`);
  808. if (!credentials.username || !credentials.password) {
  809. console.error(`[${requestId}] 错误: 登录凭据未配置`);
  810. return res.status(400).json({
  811. success: false,
  812. message: '登录凭据未配置'
  813. });
  814. }
  815. // 根据登录方法处理登录
  816. let loginResult;
  817. console.log(`[${requestId}] 开始执行登录...`);
  818. switch (config.loginMethod) {
  819. case 'rsa-encrypted-form':
  820. loginResult = await handleRSAEncryptedFormLogin(config, credentials);
  821. break;
  822. case 'plain-form':
  823. loginResult = await handlePlainFormLogin(config, credentials);
  824. break;
  825. case 'home-assistant':
  826. loginResult = await handleHomeAssistantLogin(config, credentials);
  827. break;
  828. default:
  829. console.error(`[${requestId}] 错误: 不支持的登录方法: ${config.loginMethod}`);
  830. return res.status(400).json({
  831. success: false,
  832. message: `不支持的登录方法: ${config.loginMethod}`
  833. });
  834. }
  835. if (!loginResult.success) {
  836. console.error(`[${requestId}] 登录失败:`, loginResult.message);
  837. console.error(`[${requestId}] 失败响应:`, JSON.stringify(loginResult.response, null, 2));
  838. const duration = Date.now() - startTime;
  839. console.log(`[${requestId}] 总耗时: ${duration}ms`);
  840. console.log('='.repeat(80) + '\n');
  841. // 返回错误页面而不是 JSON
  842. const errorHtml = `
  843. <!DOCTYPE html>
  844. <html lang="zh-CN">
  845. <head>
  846. <meta charset="UTF-8">
  847. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  848. <title>自动登录失败</title>
  849. <style>
  850. body {
  851. display: flex;
  852. justify-content: center;
  853. align-items: center;
  854. height: 100vh;
  855. margin: 0;
  856. font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  857. background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
  858. }
  859. .error-container {
  860. background: white;
  861. padding: 40px;
  862. border-radius: 12px;
  863. box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
  864. max-width: 600px;
  865. text-align: center;
  866. }
  867. .error-icon {
  868. font-size: 64px;
  869. margin-bottom: 20px;
  870. }
  871. .error-title {
  872. font-size: 24px;
  873. color: #e74c3c;
  874. margin-bottom: 15px;
  875. }
  876. .error-message {
  877. font-size: 16px;
  878. color: #666;
  879. margin-bottom: 20px;
  880. line-height: 1.6;
  881. }
  882. .error-details {
  883. background: #f8f9fa;
  884. padding: 15px;
  885. border-radius: 8px;
  886. margin-top: 20px;
  887. text-align: left;
  888. font-size: 14px;
  889. color: #555;
  890. }
  891. .error-details pre {
  892. margin: 0;
  893. white-space: pre-wrap;
  894. word-wrap: break-word;
  895. }
  896. </style>
  897. </head>
  898. <body>
  899. <div class="error-container">
  900. <div class="error-icon">❌</div>
  901. <div class="error-title">自动登录失败</div>
  902. <div class="error-message">${loginResult.message}</div>
  903. <div class="error-details">
  904. <strong>请求ID:</strong> ${requestId}<br>
  905. <strong>网站:</strong> ${config.name}<br>
  906. <strong>详细信息:</strong>
  907. <pre>${JSON.stringify(loginResult.response, null, 2)}</pre>
  908. </div>
  909. <button onclick="window.history.back()" style="margin-top: 20px; padding: 10px 20px; background: #3498db; color: white; border: none; border-radius: 6px; cursor: pointer;">返回</button>
  910. </div>
  911. </body>
  912. </html>
  913. `;
  914. return res.status(500).send(errorHtml);
  915. }
  916. console.log(`[${requestId}] 登录成功!`);
  917. // 解析 Cookie
  918. const cookieData = parseCookies(loginResult.cookies);
  919. console.log(`[${requestId}] 解析到 ${cookieData.length} 个 Cookie:`);
  920. cookieData.forEach((cookie, index) => {
  921. console.log(`[${requestId}] Cookie ${index + 1}: ${cookie.name} = ${cookie.value.substring(0, 20)}...`);
  922. });
  923. // 生成跳转 HTML(添加更多调试信息)
  924. // 对于 Home Assistant,如果没有 Cookie,让浏览器端完成登录
  925. let redirectUrl = `http://${config.targetHost}/`;
  926. let homeAssistantLoginData = null;
  927. if (config.loginMethod === 'home-assistant' && cookieData.length === 0) {
  928. console.log(`[${requestId}] Home Assistant 无 Cookie,准备在浏览器端完成登录流程`);
  929. // 传递登录信息给浏览器端
  930. homeAssistantLoginData = {
  931. targetBaseUrl: config.targetBaseUrl,
  932. username: credentials.username,
  933. password: credentials.password,
  934. // 如果后端已获取到 result token,也传递过去
  935. resultToken: loginResult.response?.result
  936. };
  937. redirectUrl = config.targetBaseUrl;
  938. console.log(`[${requestId}] 将在浏览器端执行 Home Assistant 登录流程`);
  939. }
  940. console.log(`[${requestId}] 生成跳转页面,目标: ${redirectUrl}`);
  941. const html = generateRedirectHTML(
  942. cookieData,
  943. config.targetHost,
  944. config.targetDomain,
  945. requestId,
  946. redirectUrl,
  947. homeAssistantLoginData
  948. );
  949. // 在响应头中设置 Cookie
  950. console.log(`[${requestId}] 设置响应头 Cookie...`);
  951. loginResult.cookies.forEach((cookie, index) => {
  952. // 修改 Cookie 的 Domain,移除端口号
  953. let modifiedCookie = cookie.replace(/Domain=[^;]+/i, `Domain=${config.targetDomain}`);
  954. res.setHeader('Set-Cookie', modifiedCookie);
  955. console.log(`[${requestId}] 设置 Cookie ${index + 1}: ${modifiedCookie.substring(0, 80)}...`);
  956. });
  957. const duration = Date.now() - startTime;
  958. console.log(`[${requestId}] 总耗时: ${duration}ms`);
  959. console.log(`[${requestId}] 返回跳转页面`);
  960. console.log('='.repeat(80) + '\n');
  961. res.send(html);
  962. } catch (error) {
  963. const duration = Date.now() - startTime;
  964. console.error(`[${requestId}] 自动登录异常:`, error.message);
  965. console.error(`[${requestId}] 错误堆栈:`, error.stack);
  966. if (error.response) {
  967. console.error(`[${requestId}] 响应状态:`, error.response.status);
  968. console.error(`[${requestId}] 响应头:`, JSON.stringify(error.response.headers, null, 2));
  969. console.error(`[${requestId}] 响应数据:`, JSON.stringify(error.response.data, null, 2));
  970. }
  971. if (error.request) {
  972. console.error(`[${requestId}] 请求信息:`, {
  973. url: error.config?.url,
  974. method: error.config?.method,
  975. headers: error.config?.headers
  976. });
  977. }
  978. console.log(`[${requestId}] 总耗时: ${duration}ms`);
  979. console.log('='.repeat(80) + '\n');
  980. res.status(500).json({
  981. success: false,
  982. message: '自动登录失败: ' + error.message,
  983. error: process.env.NODE_ENV === 'development' ? error.stack : undefined
  984. });
  985. }
  986. });
  987. // Home Assistant 登录代理端点(解决浏览器 CORS 问题)
  988. app.post('/api/home-assistant-proxy/login-flow', async (req, res) => {
  989. try {
  990. const targetBaseUrl = req.body.targetBaseUrl;
  991. console.log('[代理] 创建 Home Assistant 登录流程:', targetBaseUrl);
  992. const response = await axios.post(
  993. `${targetBaseUrl}/auth/login_flow`,
  994. {
  995. client_id: `${targetBaseUrl}/`,
  996. handler: ['homeassistant', null],
  997. redirect_uri: `${targetBaseUrl}/`
  998. },
  999. {
  1000. headers: {
  1001. 'Content-Type': 'application/json'
  1002. }
  1003. }
  1004. );
  1005. res.json(response.data);
  1006. } catch (error) {
  1007. console.error('[代理] 创建登录流程失败:', error.message);
  1008. res.status(500).json({ error: error.message });
  1009. }
  1010. });
  1011. app.post('/api/home-assistant-proxy/login', async (req, res) => {
  1012. try {
  1013. const { targetBaseUrl, flowId, username, password } = req.body;
  1014. console.log('[代理] 提交 Home Assistant 登录凭据:', targetBaseUrl, flowId);
  1015. const response = await axios.post(
  1016. `${targetBaseUrl}/auth/login_flow/${flowId}`,
  1017. {
  1018. username: username,
  1019. password: password,
  1020. client_id: `${targetBaseUrl}/`
  1021. },
  1022. {
  1023. headers: {
  1024. 'Content-Type': 'application/json'
  1025. }
  1026. }
  1027. );
  1028. res.json(response.data);
  1029. } catch (error) {
  1030. console.error('[代理] 登录失败:', error.message);
  1031. res.status(500).json({ error: error.message });
  1032. }
  1033. });
  1034. // 获取所有配置的网站列表
  1035. app.get('/api/auto-login', (req, res) => {
  1036. const sites = Object.keys(autoLoginConfig).map(siteId => ({
  1037. id: siteId,
  1038. name: autoLoginConfig[siteId].name,
  1039. endpoint: `/api/auto-login/${siteId}`
  1040. }));
  1041. res.json({ sites });
  1042. });
  1043. // 健康检查端点
  1044. app.get('/api/health', (req, res) => {
  1045. res.json({
  1046. status: 'ok',
  1047. timestamp: new Date().toISOString(),
  1048. port: PORT,
  1049. configuredSites: Object.keys(autoLoginConfig)
  1050. });
  1051. });
  1052. // 测试端点 - 用于验证配置
  1053. app.get('/api/test/:siteId', (req, res) => {
  1054. const { siteId } = req.params;
  1055. const config = autoLoginConfig[siteId];
  1056. if (!config) {
  1057. return res.json({
  1058. success: false,
  1059. message: `未找到网站ID "${siteId}" 的配置`,
  1060. availableSites: Object.keys(autoLoginConfig)
  1061. });
  1062. }
  1063. const envUsername = process.env[config.credentials.envUsername];
  1064. const envPassword = process.env[config.credentials.envPassword];
  1065. const credentials = {
  1066. username: envUsername || config.credentials.username,
  1067. password: envPassword || config.credentials.password
  1068. };
  1069. res.json({
  1070. success: true,
  1071. siteId,
  1072. config: {
  1073. name: config.name,
  1074. targetBaseUrl: config.targetBaseUrl,
  1075. loginMethod: config.loginMethod,
  1076. loginUrl: config.loginUrl,
  1077. hasCredentials: !!(credentials.username && credentials.password),
  1078. credentialsSource: envUsername ? '环境变量' : '配置文件',
  1079. username: credentials.username,
  1080. passwordLength: credentials.password ? credentials.password.length : 0
  1081. }
  1082. });
  1083. });
  1084. app.listen(PORT, '0.0.0.0', () => {
  1085. console.log('\n' + '='.repeat(80));
  1086. console.log('🚀 后端服务器启动成功!');
  1087. console.log('='.repeat(80));
  1088. console.log(`📍 本地地址: http://localhost:${PORT}`);
  1089. console.log(`📍 服务器地址: http://0.0.0.0:${PORT}`);
  1090. console.log(`📍 外部访问: http://222.243.138.146:${PORT} (通过防火墙端口映射)`);
  1091. console.log(`\n📋 已配置的自动登录网站: ${Object.keys(autoLoginConfig).join(', ') || '无'}`);
  1092. console.log(`\n🔗 可用端点:`);
  1093. console.log(` - 健康检查: http://localhost:${PORT}/api/health`);
  1094. console.log(` - 测试配置: http://localhost:${PORT}/api/test/:siteId`);
  1095. console.log(` - 自动登录: http://localhost:${PORT}/api/auto-login/:siteId`);
  1096. console.log(`\n💡 提示: 确保防火墙已配置端口映射 (前端:8888, 后端:8889 -> 外网)`);
  1097. console.log('='.repeat(80) + '\n');
  1098. });