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()); // 请求日志中间件(用于调试) 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) { const targetUrl = customUrl || `http://${targetHost}/`; 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 流程) async function handleHomeAssistantLogin(config, credentials) { const { targetBaseUrl } = config; console.log('=== Home Assistant 登录 ==='); console.log(`目标URL: ${targetBaseUrl}`); console.log(`用户名: ${credentials.username}`); console.log(`密码: ${'*'.repeat(credentials.password.length)}`); // 创建 Cookie jar 来保持会话 const cookieJar = new CookieJar(); const client = wrapper(axios.create({ jar: cookieJar, withCredentials: true })); 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}/` }; // 先访问首页获取初始 Cookie(如果需要) let initialCookies = []; try { console.log('访问首页获取初始 Cookie...'); const homeResponse = await client.get(`${targetBaseUrl}/`, { headers: { 'User-Agent': baseHeaders['User-Agent'] }, maxRedirects: 5 }); initialCookies = homeResponse.headers['set-cookie'] || []; console.log(`获取到 ${initialCookies.length} 个初始 Cookie`); // 从 Cookie jar 中获取 Cookie const jarCookies = await cookieJar.getCookies(targetBaseUrl); console.log(`Cookie jar 中有 ${jarCookies.length} 个 Cookie`); } catch (error) { console.log('访问首页失败(可能不需要):', error.message); } // 步骤1: 创建登录流程 console.log('步骤1: 创建登录流程...'); let flowResponse; try { flowResponse = await client.post( `${targetBaseUrl}/auth/login_flow`, { client_id: `${targetBaseUrl}/`, handler: ['homeassistant', null], redirect_uri: `${targetBaseUrl}/` }, { headers: baseHeaders, maxRedirects: 0, validateStatus: function (status) { return status >= 200 && status < 500; } } ); } catch (error) { console.error('创建登录流程失败:', 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 }; } 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: 提交用户名和密码 console.log('步骤2: 提交用户名和密码...'); let loginResponse; try { loginResponse = await client.post( `${targetBaseUrl}/auth/login_flow/${flowId}`, { username: credentials.username, password: credentials.password, client_id: `${targetBaseUrl}/` }, { headers: baseHeaders, maxRedirects: 0, validateStatus: function (status) { return status >= 200 && status < 500; } } ); } catch (error) { console.error('提交登录信息失败:', 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 }; } console.log(`登录响应状态码: ${loginResponse.status}`); console.log(`登录响应数据:`, JSON.stringify(loginResponse.data, null, 2)); // 检查登录是否成功 const responseData = loginResponse.data || {}; const responseType = responseData.type; console.log(`响应类型: ${responseType}`); // Home Assistant 登录成功时,type 为 "create_entry" if (responseType === 'create_entry') { // 从 Cookie jar 中获取所有 Cookie console.log('从 Cookie jar 中获取 Cookie...'); const jarCookies = await cookieJar.getCookies(targetBaseUrl); console.log(`Cookie jar 中有 ${jarCookies.length} 个 Cookie`); // 合并所有请求的 Cookie(从响应头和 Cookie jar) const flowCookies = flowResponse.headers['set-cookie'] || []; const loginCookies = loginResponse.headers['set-cookie'] || []; const allCookies = [...initialCookies, ...flowCookies, ...loginCookies]; // 从 Cookie jar 转换为 Set-Cookie 格式 jarCookies.forEach(cookie => { let cookieStr = `${cookie.key}=${cookie.value}`; if (cookie.path) cookieStr += `; Path=${cookie.path}`; if (cookie.domain) cookieStr += `; Domain=${cookie.domain}`; if (cookie.expires) cookieStr += `; Expires=${cookie.expires.toUTCString()}`; if (cookie.maxAge) cookieStr += `; Max-Age=${cookie.maxAge}`; if (cookie.secure) cookieStr += `; Secure`; if (cookie.httpOnly) cookieStr += `; HttpOnly`; if (cookie.sameSite) cookieStr += `; SameSite=${cookie.sameSite}`; allCookies.push(cookieStr); }); // 去重 Cookie(保留最后一个) const cookieMap = new Map(); allCookies.forEach(cookie => { const name = cookie.split('=')[0]; cookieMap.set(name, cookie); }); let uniqueCookies = Array.from(cookieMap.values()); console.log(`登录成功!获取到 ${uniqueCookies.length} 个唯一 Cookie`); uniqueCookies.forEach((cookie, index) => { console.log(`Cookie ${index + 1}: ${cookie.substring(0, 100)}...`); }); // 步骤3: 处理 OAuth2 授权流程 // 登录成功后,Home Assistant 需要完成 OAuth2 授权才能访问主页面 console.log('步骤3: 处理 OAuth2 授权流程...'); try { // 构建 state 参数(base64 编码的 JSON) const stateData = { hassUrl: targetBaseUrl, clientId: `${targetBaseUrl}/` }; const state = Buffer.from(JSON.stringify(stateData)).toString('base64'); // 构建授权 URL const redirectUri = `${targetBaseUrl}/?auth_callback=1`; const clientId = `${targetBaseUrl}/`; const authorizeUrl = `${targetBaseUrl}/auth/authorize?response_type=code&redirect_uri=${encodeURIComponent(redirectUri)}&client_id=${encodeURIComponent(clientId)}&state=${encodeURIComponent(state)}`; console.log(`访问授权端点: ${authorizeUrl}`); // 使用 Cookie jar 的客户端访问授权端点,跟随重定向 const authorizeResponse = await client.get(authorizeUrl, { headers: { 'User-Agent': baseHeaders['User-Agent'], 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', 'Referer': `${targetBaseUrl}/` }, maxRedirects: 10, // 增加重定向次数 validateStatus: function (status) { return status >= 200 && status < 400; } }); console.log(`授权响应状态码: ${authorizeResponse.status}`); const finalUrl = authorizeResponse.request?.res?.responseUrl || authorizeResponse.config?.url || authorizeUrl; console.log(`授权最终 URL: ${finalUrl}`); // 检查是否是重定向到回调 URL if (finalUrl.includes('auth_callback=1') || finalUrl.includes('code=')) { console.log('检测到授权回调,访问回调 URL 获取 Cookie...'); try { const callbackResponse = await client.get(finalUrl, { headers: { 'User-Agent': baseHeaders['User-Agent'], 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', 'Referer': authorizeUrl }, maxRedirects: 5, validateStatus: function (status) { return status >= 200 && status < 400; } }); console.log(`回调响应状态码: ${callbackResponse.status}`); } catch (callbackError) { console.log('访问回调 URL 失败:', callbackError.message); } } // 尝试访问主页面来触发 Cookie 设置 console.log('访问主页面以触发 Cookie 设置...'); try { const homePageResponse = await client.get(`${targetBaseUrl}/`, { headers: { 'User-Agent': baseHeaders['User-Agent'], 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', 'Referer': finalUrl }, maxRedirects: 5, validateStatus: function (status) { return status >= 200 && status < 400; } }); console.log(`主页面响应状态码: ${homePageResponse.status}`); } catch (homeError) { console.log('访问主页面失败:', homeError.message); } // 从 Cookie jar 中获取授权后的 Cookie const authorizeJarCookies = await cookieJar.getCookies(targetBaseUrl); console.log(`授权后 Cookie jar 中有 ${authorizeJarCookies.length} 个 Cookie`); // 获取授权响应中的 Cookie const authorizeCookies = authorizeResponse.headers['set-cookie'] || []; console.log(`授权响应获取到 ${authorizeCookies.length} 个 Cookie`); // 从 Cookie jar 转换为 Set-Cookie 格式并合并 authorizeJarCookies.forEach(cookie => { let cookieStr = `${cookie.key}=${cookie.value}`; if (cookie.path) cookieStr += `; Path=${cookie.path}`; if (cookie.domain) cookieStr += `; Domain=${cookie.domain}`; if (cookie.expires) cookieStr += `; Expires=${cookie.expires.toUTCString()}`; if (cookie.maxAge) cookieStr += `; Max-Age=${cookie.maxAge}`; if (cookie.secure) cookieStr += `; Secure`; if (cookie.httpOnly) cookieStr += `; HttpOnly`; if (cookie.sameSite) cookieStr += `; SameSite=${cookie.sameSite}`; cookieMap.set(cookie.key, cookieStr); }); // 合并授权响应中的 Cookie authorizeCookies.forEach(cookie => { const name = cookie.split('=')[0]; cookieMap.set(name, cookie); }); uniqueCookies = Array.from(cookieMap.values()); console.log(`授权完成!最终获取到 ${uniqueCookies.length} 个唯一 Cookie`); // 如果还是没有 Cookie,尝试使用登录结果中的 token if (uniqueCookies.length === 0 && responseData.result) { console.log(`尝试使用登录结果中的 token: ${responseData.result}`); // 这里可以尝试使用 token 来获取访问令牌 // 但 Home Assistant 的 token 通常需要通过浏览器来设置 Cookie } } catch (error) { console.log('授权流程失败(可能不需要):', error.message); if (error.response) { console.log('授权响应状态:', error.response.status); console.log('授权响应 URL:', error.response.request?.res?.responseUrl || error.config?.url); } // 授权失败不影响登录,继续使用已有的 Cookie } return { success: true, cookies: uniqueCookies, response: loginResponse.data }; } else { 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 }; } } // 处理普通表单登录(未加密) 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; 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}] 登录成功!`); // 解析 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(添加更多调试信息) // 对于 Home Assistant,如果没有 Cookie,直接跳转到授权端点 let redirectUrl = `http://${config.targetHost}/`; if (config.loginMethod === 'home-assistant' && cookieData.length === 0) { // 构建授权 URL 让浏览器处理 const stateData = { hassUrl: config.targetBaseUrl, clientId: `${config.targetBaseUrl}/` }; const state = Buffer.from(JSON.stringify(stateData)).toString('base64'); const redirectUri = `${config.targetBaseUrl}/?auth_callback=1`; const clientId = `${config.targetBaseUrl}/`; redirectUrl = `${config.targetBaseUrl}/auth/authorize?response_type=code&redirect_uri=${encodeURIComponent(redirectUri)}&client_id=${encodeURIComponent(clientId)}&state=${encodeURIComponent(state)}`; console.log(`[${requestId}] Home Assistant 无 Cookie,跳转到授权端点: ${redirectUrl}`); } console.log(`[${requestId}] 生成跳转页面,目标: ${redirectUrl}`); const html = generateRedirectHTML( cookieData, config.targetHost, config.targetDomain, requestId, redirectUrl ); // 在响应头中设置 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 }); } }); // 获取所有配置的网站列表 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'); });