import express from 'express'; import cors from 'cors'; import axios from 'axios'; import nodeRSA from 'node-rsa'; import cookieParser from 'cookie-parser'; import { readFileSync } from 'fs'; import { fileURLToPath } from 'url'; import { dirname, join } from 'path'; import { CookieJar } from 'tough-cookie'; import { wrapper } from 'axios-cookiejar-support'; // node-rsa 是 CommonJS 模块,需要使用默认导入 const NodeRSA = nodeRSA; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const app = express(); const PORT = process.env.PORT || 8889; // 中间件 app.use(cors({ origin: true, credentials: true })); app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.use(cookieParser()); // 静态文件服务 - 提供 example_page 文件夹访问 app.use('/example_page', express.static(join(__dirname, 'example_page'))); // 请求日志中间件(用于调试) app.use((req, res, next) => { console.log(`[请求] ${req.method} ${req.path} - ${new Date().toISOString()}`); next(); }); // 加载自动登录配置 let autoLoginConfig = {}; try { const configPath = join(__dirname, 'auto-login-config.json'); console.log('正在加载自动登录配置文件:', configPath); const configData = readFileSync(configPath, 'utf-8'); autoLoginConfig = JSON.parse(configData); console.log('✓ 已加载自动登录配置'); console.log(' 配置的网站数量:', Object.keys(autoLoginConfig).length); console.log(' 网站列表:', Object.keys(autoLoginConfig).join(', ')); Object.keys(autoLoginConfig).forEach(siteId => { const site = autoLoginConfig[siteId]; console.log(` - ${siteId}: ${site.name} (${site.loginMethod})`); }); } catch (error) { console.error('✗ 加载自动登录配置失败:', error.message); console.error(' 错误堆栈:', error.stack); console.log('将使用默认配置'); } // RSA 加密函数 // 注意:JSEncrypt 使用 PKCS1 填充,需要匹配 function encryptWithRSA(text, publicKey) { try { const key = new NodeRSA(publicKey, 'public', { encryptionScheme: 'pkcs1' // 使用 PKCS1 填充,与 JSEncrypt 兼容 }); const encrypted = key.encrypt(text, 'base64'); console.log(`RSA加密: "${text}" -> 长度 ${encrypted.length}`); return encrypted; } catch (error) { console.error('RSA加密失败:', error.message); throw error; } } // 解析 Cookie function parseCookies(setCookieHeaders) { return setCookieHeaders.map(cookie => { const match = cookie.match(/^([^=]+)=([^;]+)/); if (match) { const name = match[1]; const value = match[2]; // 提取其他属性 const pathMatch = cookie.match(/Path=([^;]+)/); const expiresMatch = cookie.match(/Expires=([^;]+)/); const maxAgeMatch = cookie.match(/Max-Age=([^;]+)/); const httpOnlyMatch = cookie.match(/HttpOnly/); const secureMatch = cookie.match(/Secure/); const sameSiteMatch = cookie.match(/SameSite=([^;]+)/); return { name, value, path: pathMatch ? pathMatch[1] : '/', expires: expiresMatch ? expiresMatch[1] : null, maxAge: maxAgeMatch ? maxAgeMatch[1] : null, httpOnly: !!httpOnlyMatch, secure: !!secureMatch, sameSite: sameSiteMatch ? sameSiteMatch[1] : null }; } return null; }).filter(Boolean); } // 生成跳转 HTML function generateRedirectHTML(cookieData, targetHost, targetDomain, requestId = '', customUrl = null, homeAssistantData = null) { const targetUrl = customUrl || `http://${targetHost}/`; const isHomeAssistant = homeAssistantData !== null; return ` 自动登录中...
正在自动登录,请稍候...
`; } // 处理 RSA 加密表单登录 async function handleRSAEncryptedFormLogin(config, credentials) { const { targetBaseUrl, loginUrl, loginMethodConfig } = config; const { publicKey, usernameField, passwordField, captchaField, captchaRequired, contentType, successCode, successField } = loginMethodConfig; console.log('=== RSA 加密表单登录 ==='); console.log(`目标URL: ${targetBaseUrl}${loginUrl}`); console.log(`用户名: ${credentials.username}`); console.log(`密码: ${'*'.repeat(credentials.password.length)}`); console.log(`内容类型: ${contentType}`); console.log(`成功标识字段: ${successField || 'code'}, 成功值: ${successCode}`); // 加密用户名和密码 const usernameEncrypted = encryptWithRSA(credentials.username, publicKey); const passwordEncrypted = encryptWithRSA(credentials.password, publicKey); console.log('用户名和密码已加密'); console.log(`加密后用户名长度: ${usernameEncrypted.length}`); console.log(`加密后密码长度: ${passwordEncrypted.length}`); // 构建请求数据 const requestData = { [usernameField]: usernameEncrypted, [passwordField]: passwordEncrypted }; if (captchaField) { requestData[captchaField] = captchaRequired ? '' : ''; } // 发送登录请求 const headers = {}; let requestBody; if (contentType === 'application/x-www-form-urlencoded') { headers['Content-Type'] = 'application/x-www-form-urlencoded'; requestBody = new URLSearchParams(requestData).toString(); } else if (contentType === 'application/json') { headers['Content-Type'] = 'application/json'; requestBody = JSON.stringify(requestData); } else { requestBody = requestData; } console.log(`发送登录请求到: ${targetBaseUrl}${loginUrl}`); // 添加可能需要的请求头(模拟浏览器请求) headers['Referer'] = `${targetBaseUrl}/`; headers['Origin'] = targetBaseUrl; 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'; headers['Accept'] = 'application/json, text/javascript, */*; q=0.01'; headers['Accept-Language'] = 'zh-CN,zh;q=0.9,en;q=0.8'; headers['X-Requested-With'] = 'XMLHttpRequest'; console.log(`请求头:`, JSON.stringify(headers, null, 2)); console.log(`请求体长度: ${requestBody.length} 字符`); console.log(`请求体内容预览: ${requestBody.substring(0, 300)}...`); // 先访问登录页面获取可能的session cookie console.log('先访问登录页面获取session...'); try { const loginPageResponse = await axios.get(`${targetBaseUrl}/`, { headers: { 'User-Agent': headers['User-Agent'] }, withCredentials: true, maxRedirects: 5 }); console.log('登录页面访问成功,获取到的Cookie:', loginPageResponse.headers['set-cookie'] || []); } catch (error) { console.log('访问登录页面失败(可能不需要):', error.message); } const loginResponse = await axios.post( `${targetBaseUrl}${loginUrl}`, requestBody, { headers, withCredentials: true, maxRedirects: 0, validateStatus: function (status) { return status >= 200 && status < 400; } } ); console.log(`登录响应状态码: ${loginResponse.status}`); console.log(`响应头:`, JSON.stringify(loginResponse.headers, null, 2)); console.log(`响应数据:`, JSON.stringify(loginResponse.data, null, 2)); // 检查登录是否成功 const responseData = loginResponse.data || {}; const successValue = successField ? responseData[successField] : responseData.code; console.log(`成功标识值: ${successValue}, 期望值: ${successCode}`); if (successValue === successCode) { const cookies = loginResponse.headers['set-cookie'] || []; console.log(`登录成功!获取到 ${cookies.length} 个 Cookie`); cookies.forEach((cookie, index) => { console.log(`Cookie ${index + 1}: ${cookie.substring(0, 100)}...`); }); return { success: true, cookies: cookies, response: loginResponse.data }; } else { console.error(`登录失败!响应:`, responseData); return { success: false, message: responseData.msg || responseData.message || '登录失败', response: responseData }; } } // 处理 Home Assistant 登录(OAuth2 流程 - 严格匹配 redirect_uri) async function handleHomeAssistantLogin(config, credentials) { const { targetBaseUrl } = config; console.log('=== Home Assistant 登录 (OAuth2 严格模式) ==='); console.log(`目标URL: ${targetBaseUrl}`); console.log(`用户名: ${credentials.username}`); console.log(`密码: ${'*'.repeat(credentials.password.length)}`); // 基础请求头,伪装成浏览器 const baseHeaders = { 'Content-Type': 'application/json', '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', 'Accept': 'application/json, text/plain, */*', 'Origin': targetBaseUrl, 'Referer': `${targetBaseUrl}/` }; // 【关键】:OAuth2 协议要求 client_id 和 redirect_uri 在整个流程中完全一致 const CLIENT_ID = `${targetBaseUrl}/`; const REDIRECT_URI = `${targetBaseUrl}/?auth_callback=1`; console.log('Client ID:', CLIENT_ID); console.log('Redirect URI:', REDIRECT_URI); try { // ========================================== // 步骤1: 创建登录流程 (Init Flow) // ========================================== console.log('[1/3] 初始化登录流程...'); const flowResponse = await axios.post( `${targetBaseUrl}/auth/login_flow`, { client_id: CLIENT_ID, handler: ['homeassistant', null], redirect_uri: REDIRECT_URI // 【重要】:必须和最后跳转的地址完全一致 }, { headers: baseHeaders, validateStatus: function (status) { return status >= 200 && status < 500; } } ); console.log(`流程创建响应状态码: ${flowResponse.status}`); console.log(`流程创建响应数据:`, JSON.stringify(flowResponse.data, null, 2)); if (flowResponse.status !== 200) { return { success: false, message: `创建登录流程失败,状态码: ${flowResponse.status}`, response: flowResponse.data }; } const flowId = flowResponse.data?.flow_id; if (!flowId) { console.error('无法获取 flow_id'); return { success: false, message: '无法获取 flow_id', response: flowResponse.data }; } console.log(`获取到 flow_id: ${flowId}`); // ========================================== // 步骤2: 提交用户名和密码 (Submit Credentials) // ========================================== console.log('[2/3] 提交用户名和密码...'); const loginResponse = await axios.post( `${targetBaseUrl}/auth/login_flow/${flowId}`, { username: credentials.username, password: credentials.password, client_id: CLIENT_ID // 【重要】:必须和步骤1的 client_id 完全一致 }, { headers: baseHeaders, validateStatus: function (status) { return status >= 200 && status < 500; } } ); console.log(`登录响应状态码: ${loginResponse.status}`); console.log(`登录响应数据:`, JSON.stringify(loginResponse.data, null, 2)); // ========================================== // 步骤3: 换取 Token(全托管方案) // ========================================== const responseData = loginResponse.data || {}; const responseType = responseData.type; console.log(`响应类型: ${responseType}`); // 如果登录成功,type 为 'create_entry',result 字段包含 Authorization Code if (responseData.result && responseType === 'create_entry') { const authCode = responseData.result; console.log('[3/4] 登录成功!获取到 Authorization Code:', authCode); console.log('[3/4] Node.js 将代替浏览器换取 Token...'); try { // ========================================== // Node.js 直接换取 Token(避免前端路由抢跑问题) // ========================================== const tokenResponse = await axios.post( `${targetBaseUrl}/auth/token`, new URLSearchParams({ grant_type: 'authorization_code', code: authCode, client_id: CLIENT_ID }).toString(), { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } } ); const tokens = tokenResponse.data; console.log('[4/4] ✅ Token 换取成功!'); console.log(`Access Token: ${tokens.access_token.substring(0, 20)}...`); console.log(`Token 类型: ${tokens.token_type}`); console.log(`过期时间: ${tokens.expires_in}秒`); // OAuth2 跨端口方案:返回带有 code 的 URL,但使用增强的中间页面 // 虽然获取了 Token,但由于跨端口限制,我们仍然使用 code 方式 // 只是添加更好的处理逻辑 const magicLink = `${REDIRECT_URI}&code=${encodeURIComponent(authCode)}`; return { success: true, useEnhancedRedirect: true, // 使用增强的重定向方案 redirectUrl: magicLink, tokens: tokens, // 保留 Token 信息用于日志 targetBaseUrl: targetBaseUrl, cookies: [], response: loginResponse.data }; } catch (tokenError) { console.error('❌ Token 换取失败:', tokenError.message); if (tokenError.response) { console.error('Token 响应:', JSON.stringify(tokenError.response.data, null, 2)); } // 如果 Token 换取失败,降级到传统方式 console.log('⚠️ 降级到传统 redirect 方式...'); const magicLink = `${REDIRECT_URI}&code=${encodeURIComponent(authCode)}`; return { success: true, redirectUrl: magicLink, cookies: [], response: loginResponse.data }; } } else { console.error('❌ 登录失败!未返回 Authorization Code'); console.error('响应数据:', responseData); // 提取错误信息 const errorMessage = responseData.errors?.base?.[0] || responseData.errors?.username?.[0] || responseData.errors?.password?.[0] || responseData.message || `登录失败,响应类型: ${responseType}`; return { success: false, message: errorMessage, response: responseData }; } } catch (error) { console.error('Home Assistant 登录流程异常:', error.message); if (error.response) { console.error('响应状态:', error.response.status); console.error('响应数据:', JSON.stringify(error.response.data, null, 2)); return { success: false, message: `登录失败: ${error.response.status} - ${JSON.stringify(error.response.data)}`, response: error.response.data }; } return { success: false, message: `登录失败: ${error.message}`, response: null }; } } // 处理 GET 查询参数登录(OA系统等) async function handleGetQueryLogin(config, credentials) { const { targetBaseUrl, loginUrl, loginMethodConfig, successRedirectUrl } = config; const { usernameParam, passwordParam, entCode, saveCookie, isOnly, successResponse } = loginMethodConfig; console.log('=== GET 查询参数登录 ==='); console.log(`目标URL: ${targetBaseUrl}${loginUrl}`); console.log(`用户名参数名: ${usernameParam}`); console.log(`密码参数名: ${passwordParam}`); console.log(`用户名: ${credentials.username}`); console.log(`密码: ${'*'.repeat(credentials.password.length)}`); console.log(`企业代码: ${entCode}`); // 构建查询参数 - 确保参数名正确 const params = new URLSearchParams(); params.append(usernameParam, credentials.username); params.append(passwordParam, credentials.password); params.append('ent_code', entCode); params.append('code', 'undefined'); params.append('mySel', 'undefined'); params.append('saveCookie', saveCookie); params.append('isOnly', isOnly); params.append('_', Date.now().toString()); // 实时时间戳,防止缓存 const loginUrlWithParams = `${targetBaseUrl}${loginUrl}?${params.toString()}`; console.log(`发送登录请求到: ${loginUrlWithParams}`); try { const loginResponse = await axios.get(loginUrlWithParams, { 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', 'Accept': '*/*', 'Referer': `${targetBaseUrl}/` }, withCredentials: true, maxRedirects: 0, validateStatus: function (status) { return status >= 200 && status < 400; } }); console.log(`登录响应状态码: ${loginResponse.status}`); console.log(`响应数据: ${loginResponse.data}`); // 检查登录是否成功(响应内容为 "ok") const responseText = loginResponse.data?.toString().trim() || ''; const isSuccess = responseText.toLowerCase() === successResponse.toLowerCase(); console.log(`响应内容: "${responseText}"`); console.log(`成功标识: ${successResponse}, 匹配结果: ${isSuccess}`); if (isSuccess) { const cookies = loginResponse.headers['set-cookie'] || []; console.log(`登录成功!获取到 ${cookies.length} 个 Cookie`); cookies.forEach((cookie, index) => { console.log(`Cookie ${index + 1}: ${cookie.substring(0, 100)}...`); }); return { success: true, cookies: cookies, redirectUrl: successRedirectUrl ? `${targetBaseUrl}${successRedirectUrl}` : null, response: loginResponse.data }; } else { console.error(`登录失败!响应内容: "${responseText}"`); return { success: false, message: `登录失败,响应: ${responseText}`, response: loginResponse.data }; } } catch (error) { console.error('登录请求异常:', error.message); if (error.response) { console.error('响应状态:', error.response.status); console.error('响应数据:', error.response.data); return { success: false, message: `登录失败: ${error.response.status} - ${error.response.data}`, response: error.response.data }; } return { success: false, message: `登录失败: ${error.message}`, response: null }; } } // 处理普通表单登录(未加密) async function handlePlainFormLogin(config, credentials) { const { targetBaseUrl, loginUrl, loginMethodConfig } = config; const { usernameField, passwordField, captchaField, contentType, successCode, successField } = loginMethodConfig; console.log('=== 普通表单登录 ==='); console.log(`目标URL: ${targetBaseUrl}${loginUrl}`); console.log(`用户名: ${credentials.username}`); console.log(`密码: ${'*'.repeat(credentials.password.length)}`); console.log(`内容类型: ${contentType}`); console.log(`成功标识字段: ${successField || 'code'}, 成功值: ${successCode}`); // 构建请求数据 const requestData = { [usernameField]: credentials.username, [passwordField]: credentials.password }; if (captchaField) { requestData[captchaField] = ''; } // 发送登录请求 const headers = {}; let requestBody; if (contentType === 'application/x-www-form-urlencoded') { headers['Content-Type'] = 'application/x-www-form-urlencoded'; requestBody = new URLSearchParams(requestData).toString(); } else if (contentType === 'application/json') { headers['Content-Type'] = 'application/json'; requestBody = JSON.stringify(requestData); } else { requestBody = requestData; } console.log(`发送登录请求到: ${targetBaseUrl}${loginUrl}`); console.log(`请求头:`, JSON.stringify(headers, null, 2)); console.log(`请求体:`, contentType === 'application/json' ? requestBody : requestBody.substring(0, 200) + '...'); const loginResponse = await axios.post( `${targetBaseUrl}${loginUrl}`, requestBody, { headers, withCredentials: true, maxRedirects: 0, validateStatus: function (status) { return status >= 200 && status < 400; } } ); console.log(`登录响应状态码: ${loginResponse.status}`); console.log(`响应数据:`, JSON.stringify(loginResponse.data, null, 2)); // 检查登录是否成功 const responseData = loginResponse.data || {}; const successValue = successField ? responseData[successField] : responseData.code; console.log(`成功标识值: ${successValue}, 期望值: ${successCode}`); if (successValue === successCode) { const cookies = loginResponse.headers['set-cookie'] || []; console.log(`登录成功!获取到 ${cookies.length} 个 Cookie`); cookies.forEach((cookie, index) => { console.log(`Cookie ${index + 1}: ${cookie.substring(0, 100)}...`); }); return { success: true, cookies: cookies, response: loginResponse.data }; } else { console.error(`登录失败!响应:`, responseData); return { success: false, message: responseData.msg || responseData.message || '登录失败', response: responseData }; } } // 通用的自动登录端点 app.get('/api/auto-login/:siteId', async (req, res) => { const startTime = Date.now(); const requestId = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; // 立即输出日志,确认请求已到达 console.log('\n' + '='.repeat(80)); console.log(`[${requestId}] ⚡⚡⚡ 收到自动登录请求!⚡⚡⚡`); console.log(`[${requestId}] 时间: ${new Date().toISOString()}`); console.log(`[${requestId}] 请求路径: ${req.path}`); console.log(`[${requestId}] 请求方法: ${req.method}`); console.log(`[${requestId}] 完整URL: ${req.protocol}://${req.get('host')}${req.originalUrl}`); console.log(`[${requestId}] 客户端IP: ${req.ip || req.connection.remoteAddress || req.socket.remoteAddress}`); console.log(`[${requestId}] User-Agent: ${req.get('user-agent') || 'Unknown'}`); try { const { siteId } = req.params; console.log(`[${requestId}] 网站ID: ${siteId}`); // 获取网站配置 const config = autoLoginConfig[siteId]; if (!config) { console.error(`[${requestId}] 错误: 未找到网站ID "${siteId}" 的配置`); console.error(`[${requestId}] 可用的网站ID: ${Object.keys(autoLoginConfig).join(', ') || '无'}`); return res.status(404).json({ success: false, message: `未找到网站ID "${siteId}" 的配置`, availableSites: Object.keys(autoLoginConfig) }); } console.log(`[${requestId}] 网站名称: ${config.name}`); console.log(`[${requestId}] 目标地址: ${config.targetBaseUrl}`); console.log(`[${requestId}] 登录方法: ${config.loginMethod}`); // 获取登录凭据(优先使用环境变量) const envUsername = process.env[config.credentials.envUsername]; const envPassword = process.env[config.credentials.envPassword]; const credentials = { username: envUsername || config.credentials.username, password: envPassword || config.credentials.password }; console.log(`[${requestId}] 凭据来源: ${envUsername ? '环境变量' : '配置文件'}`); console.log(`[${requestId}] 用户名: ${credentials.username}`); console.log(`[${requestId}] 密码: ${'*'.repeat(credentials.password.length)}`); if (!credentials.username || !credentials.password) { console.error(`[${requestId}] 错误: 登录凭据未配置`); return res.status(400).json({ success: false, message: '登录凭据未配置' }); } // 根据登录方法处理登录 let loginResult; console.log(`[${requestId}] 开始执行登录...`); switch (config.loginMethod) { case 'rsa-encrypted-form': loginResult = await handleRSAEncryptedFormLogin(config, credentials); break; case 'plain-form': loginResult = await handlePlainFormLogin(config, credentials); break; case 'home-assistant': loginResult = await handleHomeAssistantLogin(config, credentials); break; case 'get-query-login': loginResult = await handleGetQueryLogin(config, credentials); break; default: console.error(`[${requestId}] 错误: 不支持的登录方法: ${config.loginMethod}`); return res.status(400).json({ success: false, message: `不支持的登录方法: ${config.loginMethod}` }); } if (!loginResult.success) { console.error(`[${requestId}] 登录失败:`, loginResult.message); console.error(`[${requestId}] 失败响应:`, JSON.stringify(loginResult.response, null, 2)); const duration = Date.now() - startTime; console.log(`[${requestId}] 总耗时: ${duration}ms`); console.log('='.repeat(80) + '\n'); // 返回错误页面而不是 JSON const errorHtml = ` 自动登录失败
自动登录失败
${loginResult.message}
请求ID: ${requestId}
网站: ${config.name}
详细信息:
${JSON.stringify(loginResult.response, null, 2)}
`; return res.status(500).send(errorHtml); } console.log(`[${requestId}] 登录成功!`); // OAuth2 跨端口:调试页面方案 if (config.loginMethod === 'home-assistant' && loginResult.useEnhancedRedirect) { console.log(`[${requestId}] 🚀 Home Assistant OAuth2 - 调试重定向方案`); console.log(`[${requestId}] Token 已获取: ${loginResult.tokens.access_token.substring(0, 30)}...`); console.log(`[${requestId}] Authorization Code: ${loginResult.redirectUrl.match(/code=([^&]+)/)?.[1]}`); console.log(`[${requestId}] 重定向 URL: ${loginResult.redirectUrl}`); const magicLink = loginResult.redirectUrl; const authCode = magicLink.match(/code=([^&]+)/)?.[1] || 'unknown'; const targetBaseUrl = loginResult.targetBaseUrl || config.targetBaseUrl; // 生成调试页面 const debugHtml = ` Home Assistant OAuth2 调试

🔍 Home Assistant OAuth2 登录调试

✅ 后端登录成功

Authorization Code 已获取,Token 已验证。

Authorization Code: ${authCode.substring(0, 20)}...

📋 OAuth2 跨端口问题分析

⚠️ 检测到跨端口场景:

🎯 手动测试步骤

请按以下步骤测试,帮助我们诊断问题:

测试 1:直接访问魔术链接

复制下面的 URL 到新标签页,看是否能登录:

${magicLink}

测试 2:在当前标签页跳转

让当前页面跳转过去(可能有更好的效果):

测试 3:iframe 预加载然后跳转

使用 iframe 预加载,5秒后跳转:

📊 实时日志

💡 建议

如果以上测试都失败,强烈建议使用 Trusted Networks 方案:

`; console.log(`[${requestId}] 返回调试页面,供手动测试`); console.log(`[${requestId}] 魔术链接: ${magicLink}`); console.log(`[${requestId}] 目标地址: ${targetBaseUrl}`); console.log(`[${requestId}] 总耗时: ${Date.now() - startTime}ms`); console.log('='.repeat(80) + '\n'); return res.send(debugHtml); } // 对于 Home Assistant,如果使用传统 redirect 方式(降级方案) if (config.loginMethod === 'home-assistant' && loginResult.redirectUrl) { console.log(`[${requestId}] Home Assistant 登录成功,使用传统 redirect 方式(降级)`); console.log(`[${requestId}] 重定向到: ${loginResult.redirectUrl}`); // 使用中间页面而不是直接 redirect // 这样可以添加延迟,让 HA 前端有时间处理 code const intermediateHtml = ` 正在登录 Home Assistant...

正在登录...

准备进入 Home Assistant

`; console.log(`[${requestId}] 总耗时: ${Date.now() - startTime}ms`); console.log('='.repeat(80) + '\n'); return res.send(intermediateHtml); } // 对于 GET 查询登录,创建一个代理端点,返回 HTML 页面自动处理登录和跳转 if (config.loginMethod === 'get-query-login' && loginResult.redirectUrl) { console.log(`[${requestId}] GET 查询登录成功,重定向到: ${loginResult.redirectUrl}`); // 解析 Cookie const cookieData = parseCookies(loginResult.cookies); console.log(`[${requestId}] 解析到 ${cookieData.length} 个 Cookie:`); cookieData.forEach((cookie, index) => { console.log(`[${requestId}] Cookie ${index + 1}: ${cookie.name} = ${cookie.value.substring(0, 20)}...`); }); // 重新构建登录 URL(带参数) const { targetBaseUrl, loginUrl, loginMethodConfig } = config; const { usernameParam, passwordParam, entCode, saveCookie, isOnly } = loginMethodConfig; const params = new URLSearchParams(); params.append(usernameParam, credentials.username); params.append(passwordParam, credentials.password); params.append('ent_code', entCode); params.append('code', 'undefined'); params.append('mySel', 'undefined'); params.append('saveCookie', saveCookie); params.append('isOnly', isOnly); params.append('_', Date.now().toString()); const loginUrlWithParams = `${targetBaseUrl}${loginUrl}?${params.toString()}`; // 生成 HTML:使用 window.open 在新窗口打开登录 URL,然后在新窗口跳转 // 这是最可靠的方法,因为新窗口可以正常设置 Cookie const html = ` 自动登录中...
正在自动登录,请稍候...
`; const duration = Date.now() - startTime; console.log(`[${requestId}] 总耗时: ${duration}ms`); console.log(`[${requestId}] 返回直接跳转登录页面`); console.log(`[${requestId}] 登录 URL: ${loginUrlWithParams}`); console.log(`[${requestId}] 目标 URL: ${loginResult.redirectUrl}`); console.log('='.repeat(80) + '\n'); return res.send(html); } // 解析 Cookie const cookieData = parseCookies(loginResult.cookies); console.log(`[${requestId}] 解析到 ${cookieData.length} 个 Cookie:`); cookieData.forEach((cookie, index) => { console.log(`[${requestId}] Cookie ${index + 1}: ${cookie.name} = ${cookie.value.substring(0, 20)}...`); }); // 生成跳转 HTML let redirectUrl = `http://${config.targetHost}/`; console.log(`[${requestId}] 生成跳转页面,目标: ${redirectUrl}`); const html = generateRedirectHTML( cookieData, config.targetHost, config.targetDomain, requestId, redirectUrl, null ); // 在响应头中设置 Cookie console.log(`[${requestId}] 设置响应头 Cookie...`); loginResult.cookies.forEach((cookie, index) => { // 修改 Cookie 的 Domain,移除端口号 let modifiedCookie = cookie.replace(/Domain=[^;]+/i, `Domain=${config.targetDomain}`); res.setHeader('Set-Cookie', modifiedCookie); console.log(`[${requestId}] 设置 Cookie ${index + 1}: ${modifiedCookie.substring(0, 80)}...`); }); const duration = Date.now() - startTime; console.log(`[${requestId}] 总耗时: ${duration}ms`); console.log(`[${requestId}] 返回跳转页面`); console.log('='.repeat(80) + '\n'); res.send(html); } catch (error) { const duration = Date.now() - startTime; console.error(`[${requestId}] 自动登录异常:`, error.message); console.error(`[${requestId}] 错误堆栈:`, error.stack); if (error.response) { console.error(`[${requestId}] 响应状态:`, error.response.status); console.error(`[${requestId}] 响应头:`, JSON.stringify(error.response.headers, null, 2)); console.error(`[${requestId}] 响应数据:`, JSON.stringify(error.response.data, null, 2)); } if (error.request) { console.error(`[${requestId}] 请求信息:`, { url: error.config?.url, method: error.config?.method, headers: error.config?.headers }); } console.log(`[${requestId}] 总耗时: ${duration}ms`); console.log('='.repeat(80) + '\n'); res.status(500).json({ success: false, message: '自动登录失败: ' + error.message, error: process.env.NODE_ENV === 'development' ? error.stack : undefined }); } }); // Home Assistant 登录代理端点(解决浏览器 CORS 问题) app.post('/api/home-assistant-proxy/login-flow', async (req, res) => { try { const targetBaseUrl = req.body.targetBaseUrl; console.log('[代理] 创建 Home Assistant 登录流程:', targetBaseUrl); const response = await axios.post( `${targetBaseUrl}/auth/login_flow`, { client_id: `${targetBaseUrl}/`, handler: ['homeassistant', null], redirect_uri: `${targetBaseUrl}/` }, { headers: { 'Content-Type': 'application/json' } } ); res.json(response.data); } catch (error) { console.error('[代理] 创建登录流程失败:', error.message); res.status(500).json({ error: error.message }); } }); app.post('/api/home-assistant-proxy/login', async (req, res) => { try { const { targetBaseUrl, flowId, username, password } = req.body; console.log('[代理] 提交 Home Assistant 登录凭据:', targetBaseUrl, flowId); const response = await axios.post( `${targetBaseUrl}/auth/login_flow/${flowId}`, { username: username, password: password, client_id: `${targetBaseUrl}/` }, { headers: { 'Content-Type': 'application/json' } } ); res.json(response.data); } catch (error) { console.error('[代理] 登录失败:', error.message); res.status(500).json({ error: error.message }); } }); // 获取所有配置的网站列表 app.get('/api/auto-login', (req, res) => { const sites = Object.keys(autoLoginConfig).map(siteId => ({ id: siteId, name: autoLoginConfig[siteId].name, endpoint: `/api/auto-login/${siteId}` })); res.json({ sites }); }); // 健康检查端点 app.get('/api/health', (req, res) => { res.json({ status: 'ok', timestamp: new Date().toISOString(), port: PORT, configuredSites: Object.keys(autoLoginConfig) }); }); // 测试端点 - 用于验证配置 app.get('/api/test/:siteId', (req, res) => { const { siteId } = req.params; const config = autoLoginConfig[siteId]; if (!config) { return res.json({ success: false, message: `未找到网站ID "${siteId}" 的配置`, availableSites: Object.keys(autoLoginConfig) }); } const envUsername = process.env[config.credentials.envUsername]; const envPassword = process.env[config.credentials.envPassword]; const credentials = { username: envUsername || config.credentials.username, password: envPassword || config.credentials.password }; res.json({ success: true, siteId, config: { name: config.name, targetBaseUrl: config.targetBaseUrl, loginMethod: config.loginMethod, loginUrl: config.loginUrl, hasCredentials: !!(credentials.username && credentials.password), credentialsSource: envUsername ? '环境变量' : '配置文件', username: credentials.username, passwordLength: credentials.password ? credentials.password.length : 0 } }); }); app.listen(PORT, '0.0.0.0', () => { console.log('\n' + '='.repeat(80)); console.log('🚀 后端服务器启动成功!'); console.log('='.repeat(80)); console.log(`📍 本地地址: http://localhost:${PORT}`); console.log(`📍 服务器地址: http://0.0.0.0:${PORT}`); console.log(`📍 外部访问: http://222.243.138.146:${PORT} (通过防火墙端口映射)`); console.log(`\n📋 已配置的自动登录网站: ${Object.keys(autoLoginConfig).join(', ') || '无'}`); console.log(`\n🔗 可用端点:`); console.log(` - 健康检查: http://localhost:${PORT}/api/health`); console.log(` - 测试配置: http://localhost:${PORT}/api/test/:siteId`); console.log(` - 自动登录: http://localhost:${PORT}/api/auto-login/:siteId`); console.log(`\n💡 提示: 确保防火墙已配置端口映射 (前端:8888, 后端:8889 -> 外网)`); console.log('='.repeat(80) + '\n'); });