server.js 39 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031
  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 登录 (OAuth2 模式) ===');
  406. console.log(`目标URL: ${targetBaseUrl}`);
  407. console.log(`用户名: ${credentials.username}`);
  408. console.log(`密码: ${'*'.repeat(credentials.password.length)}`);
  409. const baseHeaders = {
  410. 'Content-Type': 'application/json',
  411. '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',
  412. 'Accept': 'application/json, text/plain, */*',
  413. 'Origin': targetBaseUrl
  414. };
  415. try {
  416. // ==========================================
  417. // 步骤1: 创建登录流程 (Init Flow)
  418. // ==========================================
  419. console.log('步骤1: 创建登录流程...');
  420. const flowResponse = await axios.post(
  421. `${targetBaseUrl}/auth/login_flow`,
  422. {
  423. client_id: `${targetBaseUrl}/`, // clientId 必须和浏览器访问的一致
  424. handler: ['homeassistant', null],
  425. redirect_uri: `${targetBaseUrl}/?auth_callback=1`
  426. },
  427. {
  428. headers: baseHeaders,
  429. validateStatus: function (status) {
  430. return status >= 200 && status < 500;
  431. }
  432. }
  433. );
  434. console.log(`流程创建响应状态码: ${flowResponse.status}`);
  435. console.log(`流程创建响应数据:`, JSON.stringify(flowResponse.data, null, 2));
  436. if (flowResponse.status !== 200) {
  437. return {
  438. success: false,
  439. message: `创建登录流程失败,状态码: ${flowResponse.status}`,
  440. response: flowResponse.data
  441. };
  442. }
  443. const flowId = flowResponse.data?.flow_id;
  444. if (!flowId) {
  445. console.error('无法获取 flow_id');
  446. return {
  447. success: false,
  448. message: '无法获取 flow_id',
  449. response: flowResponse.data
  450. };
  451. }
  452. console.log(`获取到 flow_id: ${flowId}`);
  453. // ==========================================
  454. // 步骤2: 提交用户名和密码 (Submit Credentials)
  455. // ==========================================
  456. console.log('步骤2: 提交用户名和密码...');
  457. const loginResponse = await axios.post(
  458. `${targetBaseUrl}/auth/login_flow/${flowId}`,
  459. {
  460. username: credentials.username,
  461. password: credentials.password,
  462. client_id: `${targetBaseUrl}/` // 保持一致
  463. },
  464. {
  465. headers: baseHeaders,
  466. validateStatus: function (status) {
  467. return status >= 200 && status < 500;
  468. }
  469. }
  470. );
  471. console.log(`登录响应状态码: ${loginResponse.status}`);
  472. console.log(`登录响应数据:`, JSON.stringify(loginResponse.data, null, 2));
  473. // ==========================================
  474. // 步骤3: 构造浏览器跳转 URL (关键修正)
  475. // ==========================================
  476. const responseData = loginResponse.data || {};
  477. const responseType = responseData.type;
  478. console.log(`响应类型: ${responseType}`);
  479. // 如果登录成功,type 通常是 'create_entry'
  480. if (responseData.result) {
  481. const authCode = responseData.result;
  482. console.log(`登录成功!获取到 Authorization Code: ${authCode}`);
  483. // 构造魔术链接,让浏览器去完成 Token 交换
  484. // 格式: http://ha-url/?auth_callback=1&code=YOUR_CODE
  485. const redirectUrl = `${targetBaseUrl}/?auth_callback=1&code=${encodeURIComponent(authCode)}`;
  486. console.log('生成自动登录跳转链接:', redirectUrl);
  487. console.log('Home Assistant 前端将自动识别 URL 中的 code 并完成 Token 交换');
  488. // 返回跳转 URL,让浏览器直接跳转
  489. return {
  490. success: true,
  491. redirectUrl: redirectUrl, // 告诉上层直接跳转这个 URL
  492. cookies: [], // HA 不需要 Cookie
  493. response: loginResponse.data
  494. };
  495. } else {
  496. console.error('登录未返回 Authorization Code:', responseData);
  497. return {
  498. success: false,
  499. message: '登录失败: 未返回授权码',
  500. response: responseData
  501. };
  502. }
  503. } catch (error) {
  504. console.error('Home Assistant 登录流程异常:', error.message);
  505. if (error.response) {
  506. console.error('响应状态:', error.response.status);
  507. console.error('响应数据:', JSON.stringify(error.response.data, null, 2));
  508. return {
  509. success: false,
  510. message: `登录失败: ${error.response.status} - ${JSON.stringify(error.response.data)}`,
  511. response: error.response.data
  512. };
  513. }
  514. return {
  515. success: false,
  516. message: `登录失败: ${error.message}`,
  517. response: null
  518. };
  519. }
  520. }
  521. // 处理普通表单登录(未加密)
  522. async function handlePlainFormLogin(config, credentials) {
  523. const { targetBaseUrl, loginUrl, loginMethodConfig } = config;
  524. const { usernameField, passwordField, captchaField, contentType, successCode, successField } = loginMethodConfig;
  525. console.log('=== 普通表单登录 ===');
  526. console.log(`目标URL: ${targetBaseUrl}${loginUrl}`);
  527. console.log(`用户名: ${credentials.username}`);
  528. console.log(`密码: ${'*'.repeat(credentials.password.length)}`);
  529. console.log(`内容类型: ${contentType}`);
  530. console.log(`成功标识字段: ${successField || 'code'}, 成功值: ${successCode}`);
  531. // 构建请求数据
  532. const requestData = {
  533. [usernameField]: credentials.username,
  534. [passwordField]: credentials.password
  535. };
  536. if (captchaField) {
  537. requestData[captchaField] = '';
  538. }
  539. // 发送登录请求
  540. const headers = {};
  541. let requestBody;
  542. if (contentType === 'application/x-www-form-urlencoded') {
  543. headers['Content-Type'] = 'application/x-www-form-urlencoded';
  544. requestBody = new URLSearchParams(requestData).toString();
  545. } else if (contentType === 'application/json') {
  546. headers['Content-Type'] = 'application/json';
  547. requestBody = JSON.stringify(requestData);
  548. } else {
  549. requestBody = requestData;
  550. }
  551. console.log(`发送登录请求到: ${targetBaseUrl}${loginUrl}`);
  552. console.log(`请求头:`, JSON.stringify(headers, null, 2));
  553. console.log(`请求体:`, contentType === 'application/json' ? requestBody : requestBody.substring(0, 200) + '...');
  554. const loginResponse = await axios.post(
  555. `${targetBaseUrl}${loginUrl}`,
  556. requestBody,
  557. {
  558. headers,
  559. withCredentials: true,
  560. maxRedirects: 0,
  561. validateStatus: function (status) {
  562. return status >= 200 && status < 400;
  563. }
  564. }
  565. );
  566. console.log(`登录响应状态码: ${loginResponse.status}`);
  567. console.log(`响应数据:`, JSON.stringify(loginResponse.data, null, 2));
  568. // 检查登录是否成功
  569. const responseData = loginResponse.data || {};
  570. const successValue = successField ? responseData[successField] : responseData.code;
  571. console.log(`成功标识值: ${successValue}, 期望值: ${successCode}`);
  572. if (successValue === successCode) {
  573. const cookies = loginResponse.headers['set-cookie'] || [];
  574. console.log(`登录成功!获取到 ${cookies.length} 个 Cookie`);
  575. cookies.forEach((cookie, index) => {
  576. console.log(`Cookie ${index + 1}: ${cookie.substring(0, 100)}...`);
  577. });
  578. return {
  579. success: true,
  580. cookies: cookies,
  581. response: loginResponse.data
  582. };
  583. } else {
  584. console.error(`登录失败!响应:`, responseData);
  585. return {
  586. success: false,
  587. message: responseData.msg || responseData.message || '登录失败',
  588. response: responseData
  589. };
  590. }
  591. }
  592. // 通用的自动登录端点
  593. app.get('/api/auto-login/:siteId', async (req, res) => {
  594. const startTime = Date.now();
  595. const requestId = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
  596. // 立即输出日志,确认请求已到达
  597. console.log('\n' + '='.repeat(80));
  598. console.log(`[${requestId}] ⚡⚡⚡ 收到自动登录请求!⚡⚡⚡`);
  599. console.log(`[${requestId}] 时间: ${new Date().toISOString()}`);
  600. console.log(`[${requestId}] 请求路径: ${req.path}`);
  601. console.log(`[${requestId}] 请求方法: ${req.method}`);
  602. console.log(`[${requestId}] 完整URL: ${req.protocol}://${req.get('host')}${req.originalUrl}`);
  603. console.log(`[${requestId}] 客户端IP: ${req.ip || req.connection.remoteAddress || req.socket.remoteAddress}`);
  604. console.log(`[${requestId}] User-Agent: ${req.get('user-agent') || 'Unknown'}`);
  605. try {
  606. const { siteId } = req.params;
  607. console.log(`[${requestId}] 网站ID: ${siteId}`);
  608. // 获取网站配置
  609. const config = autoLoginConfig[siteId];
  610. if (!config) {
  611. console.error(`[${requestId}] 错误: 未找到网站ID "${siteId}" 的配置`);
  612. console.error(`[${requestId}] 可用的网站ID: ${Object.keys(autoLoginConfig).join(', ') || '无'}`);
  613. return res.status(404).json({
  614. success: false,
  615. message: `未找到网站ID "${siteId}" 的配置`,
  616. availableSites: Object.keys(autoLoginConfig)
  617. });
  618. }
  619. console.log(`[${requestId}] 网站名称: ${config.name}`);
  620. console.log(`[${requestId}] 目标地址: ${config.targetBaseUrl}`);
  621. console.log(`[${requestId}] 登录方法: ${config.loginMethod}`);
  622. // 获取登录凭据(优先使用环境变量)
  623. const envUsername = process.env[config.credentials.envUsername];
  624. const envPassword = process.env[config.credentials.envPassword];
  625. const credentials = {
  626. username: envUsername || config.credentials.username,
  627. password: envPassword || config.credentials.password
  628. };
  629. console.log(`[${requestId}] 凭据来源: ${envUsername ? '环境变量' : '配置文件'}`);
  630. console.log(`[${requestId}] 用户名: ${credentials.username}`);
  631. console.log(`[${requestId}] 密码: ${'*'.repeat(credentials.password.length)}`);
  632. if (!credentials.username || !credentials.password) {
  633. console.error(`[${requestId}] 错误: 登录凭据未配置`);
  634. return res.status(400).json({
  635. success: false,
  636. message: '登录凭据未配置'
  637. });
  638. }
  639. // 根据登录方法处理登录
  640. let loginResult;
  641. console.log(`[${requestId}] 开始执行登录...`);
  642. switch (config.loginMethod) {
  643. case 'rsa-encrypted-form':
  644. loginResult = await handleRSAEncryptedFormLogin(config, credentials);
  645. break;
  646. case 'plain-form':
  647. loginResult = await handlePlainFormLogin(config, credentials);
  648. break;
  649. case 'home-assistant':
  650. loginResult = await handleHomeAssistantLogin(config, credentials);
  651. break;
  652. default:
  653. console.error(`[${requestId}] 错误: 不支持的登录方法: ${config.loginMethod}`);
  654. return res.status(400).json({
  655. success: false,
  656. message: `不支持的登录方法: ${config.loginMethod}`
  657. });
  658. }
  659. if (!loginResult.success) {
  660. console.error(`[${requestId}] 登录失败:`, loginResult.message);
  661. console.error(`[${requestId}] 失败响应:`, JSON.stringify(loginResult.response, null, 2));
  662. const duration = Date.now() - startTime;
  663. console.log(`[${requestId}] 总耗时: ${duration}ms`);
  664. console.log('='.repeat(80) + '\n');
  665. // 返回错误页面而不是 JSON
  666. const errorHtml = `
  667. <!DOCTYPE html>
  668. <html lang="zh-CN">
  669. <head>
  670. <meta charset="UTF-8">
  671. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  672. <title>自动登录失败</title>
  673. <style>
  674. body {
  675. display: flex;
  676. justify-content: center;
  677. align-items: center;
  678. height: 100vh;
  679. margin: 0;
  680. font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  681. background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
  682. }
  683. .error-container {
  684. background: white;
  685. padding: 40px;
  686. border-radius: 12px;
  687. box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
  688. max-width: 600px;
  689. text-align: center;
  690. }
  691. .error-icon {
  692. font-size: 64px;
  693. margin-bottom: 20px;
  694. }
  695. .error-title {
  696. font-size: 24px;
  697. color: #e74c3c;
  698. margin-bottom: 15px;
  699. }
  700. .error-message {
  701. font-size: 16px;
  702. color: #666;
  703. margin-bottom: 20px;
  704. line-height: 1.6;
  705. }
  706. .error-details {
  707. background: #f8f9fa;
  708. padding: 15px;
  709. border-radius: 8px;
  710. margin-top: 20px;
  711. text-align: left;
  712. font-size: 14px;
  713. color: #555;
  714. }
  715. .error-details pre {
  716. margin: 0;
  717. white-space: pre-wrap;
  718. word-wrap: break-word;
  719. }
  720. </style>
  721. </head>
  722. <body>
  723. <div class="error-container">
  724. <div class="error-icon">❌</div>
  725. <div class="error-title">自动登录失败</div>
  726. <div class="error-message">${loginResult.message}</div>
  727. <div class="error-details">
  728. <strong>请求ID:</strong> ${requestId}<br>
  729. <strong>网站:</strong> ${config.name}<br>
  730. <strong>详细信息:</strong>
  731. <pre>${JSON.stringify(loginResult.response, null, 2)}</pre>
  732. </div>
  733. <button onclick="window.history.back()" style="margin-top: 20px; padding: 10px 20px; background: #3498db; color: white; border: none; border-radius: 6px; cursor: pointer;">返回</button>
  734. </div>
  735. </body>
  736. </html>
  737. `;
  738. return res.status(500).send(errorHtml);
  739. }
  740. console.log(`[${requestId}] 登录成功!`);
  741. // 对于 Home Assistant,如果返回了 redirectUrl,直接重定向
  742. if (config.loginMethod === 'home-assistant' && loginResult.redirectUrl) {
  743. console.log(`[${requestId}] Home Assistant 登录成功,直接重定向到: ${loginResult.redirectUrl}`);
  744. console.log(`[${requestId}] 总耗时: ${Date.now() - startTime}ms`);
  745. console.log('='.repeat(80) + '\n');
  746. // 直接重定向到带有 auth code 的 URL
  747. return res.redirect(loginResult.redirectUrl);
  748. }
  749. // 解析 Cookie
  750. const cookieData = parseCookies(loginResult.cookies);
  751. console.log(`[${requestId}] 解析到 ${cookieData.length} 个 Cookie:`);
  752. cookieData.forEach((cookie, index) => {
  753. console.log(`[${requestId}] Cookie ${index + 1}: ${cookie.name} = ${cookie.value.substring(0, 20)}...`);
  754. });
  755. // 生成跳转 HTML
  756. let redirectUrl = `http://${config.targetHost}/`;
  757. console.log(`[${requestId}] 生成跳转页面,目标: ${redirectUrl}`);
  758. const html = generateRedirectHTML(
  759. cookieData,
  760. config.targetHost,
  761. config.targetDomain,
  762. requestId,
  763. redirectUrl,
  764. null
  765. );
  766. // 在响应头中设置 Cookie
  767. console.log(`[${requestId}] 设置响应头 Cookie...`);
  768. loginResult.cookies.forEach((cookie, index) => {
  769. // 修改 Cookie 的 Domain,移除端口号
  770. let modifiedCookie = cookie.replace(/Domain=[^;]+/i, `Domain=${config.targetDomain}`);
  771. res.setHeader('Set-Cookie', modifiedCookie);
  772. console.log(`[${requestId}] 设置 Cookie ${index + 1}: ${modifiedCookie.substring(0, 80)}...`);
  773. });
  774. const duration = Date.now() - startTime;
  775. console.log(`[${requestId}] 总耗时: ${duration}ms`);
  776. console.log(`[${requestId}] 返回跳转页面`);
  777. console.log('='.repeat(80) + '\n');
  778. res.send(html);
  779. } catch (error) {
  780. const duration = Date.now() - startTime;
  781. console.error(`[${requestId}] 自动登录异常:`, error.message);
  782. console.error(`[${requestId}] 错误堆栈:`, error.stack);
  783. if (error.response) {
  784. console.error(`[${requestId}] 响应状态:`, error.response.status);
  785. console.error(`[${requestId}] 响应头:`, JSON.stringify(error.response.headers, null, 2));
  786. console.error(`[${requestId}] 响应数据:`, JSON.stringify(error.response.data, null, 2));
  787. }
  788. if (error.request) {
  789. console.error(`[${requestId}] 请求信息:`, {
  790. url: error.config?.url,
  791. method: error.config?.method,
  792. headers: error.config?.headers
  793. });
  794. }
  795. console.log(`[${requestId}] 总耗时: ${duration}ms`);
  796. console.log('='.repeat(80) + '\n');
  797. res.status(500).json({
  798. success: false,
  799. message: '自动登录失败: ' + error.message,
  800. error: process.env.NODE_ENV === 'development' ? error.stack : undefined
  801. });
  802. }
  803. });
  804. // Home Assistant 登录代理端点(解决浏览器 CORS 问题)
  805. app.post('/api/home-assistant-proxy/login-flow', async (req, res) => {
  806. try {
  807. const targetBaseUrl = req.body.targetBaseUrl;
  808. console.log('[代理] 创建 Home Assistant 登录流程:', targetBaseUrl);
  809. const response = await axios.post(
  810. `${targetBaseUrl}/auth/login_flow`,
  811. {
  812. client_id: `${targetBaseUrl}/`,
  813. handler: ['homeassistant', null],
  814. redirect_uri: `${targetBaseUrl}/`
  815. },
  816. {
  817. headers: {
  818. 'Content-Type': 'application/json'
  819. }
  820. }
  821. );
  822. res.json(response.data);
  823. } catch (error) {
  824. console.error('[代理] 创建登录流程失败:', error.message);
  825. res.status(500).json({ error: error.message });
  826. }
  827. });
  828. app.post('/api/home-assistant-proxy/login', async (req, res) => {
  829. try {
  830. const { targetBaseUrl, flowId, username, password } = req.body;
  831. console.log('[代理] 提交 Home Assistant 登录凭据:', targetBaseUrl, flowId);
  832. const response = await axios.post(
  833. `${targetBaseUrl}/auth/login_flow/${flowId}`,
  834. {
  835. username: username,
  836. password: password,
  837. client_id: `${targetBaseUrl}/`
  838. },
  839. {
  840. headers: {
  841. 'Content-Type': 'application/json'
  842. }
  843. }
  844. );
  845. res.json(response.data);
  846. } catch (error) {
  847. console.error('[代理] 登录失败:', error.message);
  848. res.status(500).json({ error: error.message });
  849. }
  850. });
  851. // 获取所有配置的网站列表
  852. app.get('/api/auto-login', (req, res) => {
  853. const sites = Object.keys(autoLoginConfig).map(siteId => ({
  854. id: siteId,
  855. name: autoLoginConfig[siteId].name,
  856. endpoint: `/api/auto-login/${siteId}`
  857. }));
  858. res.json({ sites });
  859. });
  860. // 健康检查端点
  861. app.get('/api/health', (req, res) => {
  862. res.json({
  863. status: 'ok',
  864. timestamp: new Date().toISOString(),
  865. port: PORT,
  866. configuredSites: Object.keys(autoLoginConfig)
  867. });
  868. });
  869. // 测试端点 - 用于验证配置
  870. app.get('/api/test/:siteId', (req, res) => {
  871. const { siteId } = req.params;
  872. const config = autoLoginConfig[siteId];
  873. if (!config) {
  874. return res.json({
  875. success: false,
  876. message: `未找到网站ID "${siteId}" 的配置`,
  877. availableSites: Object.keys(autoLoginConfig)
  878. });
  879. }
  880. const envUsername = process.env[config.credentials.envUsername];
  881. const envPassword = process.env[config.credentials.envPassword];
  882. const credentials = {
  883. username: envUsername || config.credentials.username,
  884. password: envPassword || config.credentials.password
  885. };
  886. res.json({
  887. success: true,
  888. siteId,
  889. config: {
  890. name: config.name,
  891. targetBaseUrl: config.targetBaseUrl,
  892. loginMethod: config.loginMethod,
  893. loginUrl: config.loginUrl,
  894. hasCredentials: !!(credentials.username && credentials.password),
  895. credentialsSource: envUsername ? '环境变量' : '配置文件',
  896. username: credentials.username,
  897. passwordLength: credentials.password ? credentials.password.length : 0
  898. }
  899. });
  900. });
  901. app.listen(PORT, '0.0.0.0', () => {
  902. console.log('\n' + '='.repeat(80));
  903. console.log('🚀 后端服务器启动成功!');
  904. console.log('='.repeat(80));
  905. console.log(`📍 本地地址: http://localhost:${PORT}`);
  906. console.log(`📍 服务器地址: http://0.0.0.0:${PORT}`);
  907. console.log(`📍 外部访问: http://222.243.138.146:${PORT} (通过防火墙端口映射)`);
  908. console.log(`\n📋 已配置的自动登录网站: ${Object.keys(autoLoginConfig).join(', ') || '无'}`);
  909. console.log(`\n🔗 可用端点:`);
  910. console.log(` - 健康检查: http://localhost:${PORT}/api/health`);
  911. console.log(` - 测试配置: http://localhost:${PORT}/api/test/:siteId`);
  912. console.log(` - 自动登录: http://localhost:${PORT}/api/auto-login/:siteId`);
  913. console.log(`\n💡 提示: 确保防火墙已配置端口映射 (前端:8888, 后端:8889 -> 外网)`);
  914. console.log('='.repeat(80) + '\n');
  915. });