| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526 |
- 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, homeAssistantData = null) {
- const targetUrl = customUrl || `http://${targetHost}/`;
- const isHomeAssistant = homeAssistantData !== null;
-
- return `
- <!DOCTYPE html>
- <html lang="zh-CN">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>自动登录中...</title>
- <style>
- body {
- display: flex;
- justify-content: center;
- align-items: center;
- height: 100vh;
- margin: 0;
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
- background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
- }
- .loading {
- text-align: center;
- }
- .spinner {
- border: 4px solid #f3f3f3;
- border-top: 4px solid #3498db;
- border-radius: 50%;
- width: 50px;
- height: 50px;
- animation: spin 1s linear infinite;
- margin: 0 auto 20px;
- }
- @keyframes spin {
- 0% { transform: rotate(0deg); }
- 100% { transform: rotate(360deg); }
- }
- .message {
- color: #333;
- font-size: 18px;
- }
- </style>
- </head>
- <body>
- <div class="loading">
- <div class="spinner"></div>
- <div class="message">正在自动登录,请稍候...</div>
- </div>
- <iframe id="cookieFrame" style="display:none;"></iframe>
- <script>
- (function() {
- const requestId = '${requestId}';
- const cookies = ${JSON.stringify(cookieData)};
- const targetUrl = '${targetUrl}';
- const targetDomain = '${targetDomain}';
- const isHomeAssistant = ${isHomeAssistant};
- const homeAssistantData = ${homeAssistantData ? JSON.stringify(homeAssistantData) : 'null'};
-
- console.log('========================================');
- console.log('[浏览器端] 自动登录脚本开始执行');
- console.log('[浏览器端] 请求ID:', requestId);
- console.log('[浏览器端] 目标URL:', targetUrl);
- console.log('[浏览器端] 目标域名:', targetDomain);
- console.log('[浏览器端] Cookie 数量:', cookies.length);
- console.log('[浏览器端] Cookie 详情:', cookies);
- console.log('[浏览器端] 是否为 Home Assistant:', isHomeAssistant);
- console.log('[浏览器端] Home Assistant 数据:', homeAssistantData);
-
- // 方法1: 尝试直接设置 Cookie(可能因为跨域限制而失败)
- if (cookies.length > 0) {
- console.log('[浏览器端] 开始尝试设置 Cookie...');
- let successCount = 0;
- let failCount = 0;
-
- cookies.forEach(function(cookie) {
- try {
- // 构建 Cookie 字符串
- let cookieStr = cookie.name + '=' + cookie.value;
- cookieStr += '; path=' + (cookie.path || '/');
- if (cookie.maxAge) {
- cookieStr += '; max-age=' + cookie.maxAge;
- }
- if (cookie.expires) {
- cookieStr += '; expires=' + cookie.expires;
- }
- if (cookie.secure) {
- cookieStr += '; secure';
- }
- if (cookie.sameSite) {
- cookieStr += '; samesite=' + cookie.sameSite;
- }
- // 注意:Domain 属性无法通过 JavaScript 设置跨域 Cookie
- // 但我们可以尝试设置(浏览器会忽略跨域的 Domain)
- if (cookie.domain) {
- cookieStr += '; domain=' + cookie.domain;
- }
-
- document.cookie = cookieStr;
- console.log('[浏览器端] ✓ 尝试设置 Cookie:', cookie.name);
- successCount++;
-
- // 验证 Cookie 是否设置成功
- const allCookies = document.cookie;
- if (allCookies.indexOf(cookie.name + '=') !== -1) {
- console.log('[浏览器端] ✓ Cookie 设置成功:', cookie.name);
- } else {
- console.warn('[浏览器端] ⚠ Cookie 可能未设置成功:', cookie.name, '(可能是跨域限制)');
- }
- } catch(e) {
- console.error('[浏览器端] ✗ 设置 Cookie 失败:', cookie.name, e);
- failCount++;
- }
- });
-
- console.log('[浏览器端] Cookie 设置结果: 成功 ' + successCount + ', 失败 ' + failCount);
- } else {
- console.log('[浏览器端] 没有 Cookie 需要设置,直接跳转');
- }
-
- // 对于 Home Assistant,在浏览器端执行登录流程
- if (isHomeAssistant && homeAssistantData) {
- console.log('[浏览器端] Home Assistant 登录,在浏览器端执行登录流程');
- console.log('[浏览器端] 目标 URL:', homeAssistantData.targetBaseUrl);
- console.log('[浏览器端] 用户名:', homeAssistantData.username);
-
- // 异步执行登录流程(通过后端代理避免 CORS)
- async function loginHomeAssistant() {
- try {
- console.log('[浏览器端] 步骤1: 创建登录流程(通过代理)...');
- // 使用后端代理避免 CORS 问题
- const proxyBaseUrl = window.location.origin; // 后端服务器地址
-
- const flowResponse = await fetch(proxyBaseUrl + '/api/home-assistant-proxy/login-flow', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({
- targetBaseUrl: homeAssistantData.targetBaseUrl
- })
- });
-
- if (!flowResponse.ok) {
- throw new Error('创建登录流程失败: ' + flowResponse.status);
- }
-
- const flowData = await flowResponse.json();
- console.log('[浏览器端] 流程创建响应:', flowData);
-
- if (!flowData.flow_id) {
- throw new Error('无法获取 flow_id');
- }
-
- console.log('[浏览器端] 步骤2: 提交用户名和密码(通过代理)...');
- const loginResponse = await fetch(proxyBaseUrl + '/api/home-assistant-proxy/login', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({
- targetBaseUrl: homeAssistantData.targetBaseUrl,
- flowId: flowData.flow_id,
- username: homeAssistantData.username,
- password: homeAssistantData.password
- })
- });
-
- if (!loginResponse.ok) {
- throw new Error('登录失败: ' + loginResponse.status);
- }
-
- const loginData = await loginResponse.json();
- console.log('[浏览器端] 登录响应:', loginData);
-
- if (loginData.type === 'create_entry') {
- console.log('[浏览器端] 登录成功!准备跳转到授权端点...');
-
- // 构建授权 URL
- const stateData = {
- hassUrl: homeAssistantData.targetBaseUrl,
- clientId: homeAssistantData.targetBaseUrl + '/'
- };
- const state = btoa(JSON.stringify(stateData));
- const redirectUri = homeAssistantData.targetBaseUrl + '/?auth_callback=1';
- const clientId = homeAssistantData.targetBaseUrl + '/';
- const authorizeUrl = homeAssistantData.targetBaseUrl + '/auth/authorize?response_type=code&redirect_uri=' + encodeURIComponent(redirectUri) + '&client_id=' + encodeURIComponent(clientId) + '&state=' + encodeURIComponent(state);
-
- console.log('[浏览器端] 授权 URL:', authorizeUrl);
- console.log('[浏览器端] 跳转到授权端点...');
- console.log('========================================');
-
- window.location.href = authorizeUrl;
- } else {
- throw new Error('登录失败: ' + JSON.stringify(loginData));
- }
- } catch (error) {
- console.error('[浏览器端] 登录失败:', error);
- alert('自动登录失败: ' + error.message + '\\n\\n将跳转到登录页面,请手动登录。');
- window.location.href = targetUrl;
- }
- }
-
- // 执行登录
- loginHomeAssistant();
- return;
- }
-
- // 方法2: 使用隐藏的 iframe 加载目标站点,让服务器设置 Cookie
- // 然后跳转到目标站点
- console.log('[浏览器端] 创建隐藏 iframe 加载目标站点...');
- const iframe = document.getElementById('cookieFrame');
-
- iframe.onload = function() {
- console.log('[浏览器端] iframe 加载完成');
- };
-
- iframe.onerror = function(error) {
- console.error('[浏览器端] iframe 加载失败:', error);
- };
-
- iframe.src = targetUrl;
-
- // 延迟跳转,确保 iframe 加载完成
- setTimeout(function() {
- console.log('[浏览器端] 准备跳转到目标站点:', targetUrl);
- console.log('[浏览器端] 当前页面 Cookie:', document.cookie);
- console.log('========================================');
- window.location.href = targetUrl;
- }, 1500);
- })();
- </script>
- </body>
- </html>
- `;
- }
- // 处理 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 = `
- <!DOCTYPE html>
- <html lang="zh-CN">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>自动登录失败</title>
- <style>
- body {
- display: flex;
- justify-content: center;
- align-items: center;
- height: 100vh;
- margin: 0;
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
- background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
- }
- .error-container {
- background: white;
- padding: 40px;
- border-radius: 12px;
- box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
- max-width: 600px;
- text-align: center;
- }
- .error-icon {
- font-size: 64px;
- margin-bottom: 20px;
- }
- .error-title {
- font-size: 24px;
- color: #e74c3c;
- margin-bottom: 15px;
- }
- .error-message {
- font-size: 16px;
- color: #666;
- margin-bottom: 20px;
- line-height: 1.6;
- }
- .error-details {
- background: #f8f9fa;
- padding: 15px;
- border-radius: 8px;
- margin-top: 20px;
- text-align: left;
- font-size: 14px;
- color: #555;
- }
- .error-details pre {
- margin: 0;
- white-space: pre-wrap;
- word-wrap: break-word;
- }
- </style>
- </head>
- <body>
- <div class="error-container">
- <div class="error-icon">❌</div>
- <div class="error-title">自动登录失败</div>
- <div class="error-message">${loginResult.message}</div>
- <div class="error-details">
- <strong>请求ID:</strong> ${requestId}<br>
- <strong>网站:</strong> ${config.name}<br>
- <strong>详细信息:</strong>
- <pre>${JSON.stringify(loginResult.response, null, 2)}</pre>
- </div>
- <button onclick="window.history.back()" style="margin-top: 20px; padding: 10px 20px; background: #3498db; color: white; border: none; border-radius: 6px; cursor: pointer;">返回</button>
- </div>
- </body>
- </html>
- `;
- 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 = `
- <!DOCTYPE html>
- <html lang="zh-CN">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>Home Assistant OAuth2 调试</title>
- <style>
- body {
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
- background: #1e1e1e;
- color: #d4d4d4;
- padding: 20px;
- margin: 0;
- }
- .container {
- max-width: 1000px;
- margin: 0 auto;
- }
- h1 {
- color: #4ec9b0;
- border-bottom: 2px solid #4ec9b0;
- padding-bottom: 10px;
- }
- .section {
- background: #252526;
- border: 1px solid #3e3e42;
- border-radius: 8px;
- padding: 20px;
- margin: 20px 0;
- }
- .section h2 {
- color: #569cd6;
- margin-top: 0;
- }
- pre {
- background: #1e1e1e;
- border: 1px solid #3e3e42;
- border-radius: 4px;
- padding: 15px;
- overflow-x: auto;
- }
- .success { color: #4ec9b0; }
- .warning { color: #ce9178; }
- .error { color: #f48771; }
- .button {
- background: #0e639c;
- color: white;
- border: none;
- padding: 12px 24px;
- border-radius: 6px;
- font-size: 16px;
- cursor: pointer;
- margin: 10px 5px;
- }
- .button:hover { background: #1177bb; }
- .button.secondary {
- background: #3e3e42;
- }
- .button.secondary:hover { background: #505050; }
- #log {
- background: #1e1e1e;
- border: 1px solid #3e3e42;
- border-radius: 4px;
- padding: 15px;
- max-height: 300px;
- overflow-y: auto;
- font-family: 'Consolas', 'Monaco', monospace;
- font-size: 12px;
- }
- .log-entry {
- margin: 5px 0;
- padding: 5px;
- border-left: 3px solid #569cd6;
- padding-left: 10px;
- }
- </style>
- </head>
- <body>
- <div class="container">
- <h1>🔍 Home Assistant OAuth2 登录调试</h1>
-
- <div class="section">
- <h2>✅ 后端登录成功</h2>
- <p>Authorization Code 已获取,Token 已验证。</p>
- <p><strong class="success">Authorization Code:</strong> <code>${authCode.substring(0, 20)}...</code></p>
- </div>
-
- <div class="section">
- <h2>📋 OAuth2 跨端口问题分析</h2>
- <p class="warning">⚠️ 检测到跨端口场景:</p>
- <ul>
- <li>Node.js 后端:<code>222.243.138.146:8889</code></li>
- <li>Home Assistant:<code>222.243.138.146:8123</code></li>
- <li>localStorage 隔离:不同端口无法共享 Token</li>
- </ul>
- </div>
-
- <div class="section">
- <h2>🎯 手动测试步骤</h2>
- <p>请按以下步骤测试,帮助我们诊断问题:</p>
-
- <h3>测试 1:直接访问魔术链接</h3>
- <p>复制下面的 URL 到新标签页,看是否能登录:</p>
- <pre>${magicLink}</pre>
- <button class="button" onclick="window.open('${magicLink}', '_blank')">
- 🔗 在新标签页打开
- </button>
-
- <h3>测试 2:在当前标签页跳转</h3>
- <p>让当前页面跳转过去(可能有更好的效果):</p>
- <button class="button secondary" onclick="window.location.href='${magicLink}'">
- ➡️ 当前标签页跳转
- </button>
-
- <h3>测试 3:iframe 预加载然后跳转</h3>
- <p>使用 iframe 预加载,5秒后跳转:</p>
- <button class="button secondary" onclick="testIframeMethod()">
- 🔄 使用 iframe 方案
- </button>
- </div>
-
- <div class="section">
- <h2>📊 实时日志</h2>
- <div id="log"></div>
- </div>
-
- <div class="section">
- <h2>💡 建议</h2>
- <p>如果以上测试都失败,强烈建议使用 <strong class="success">Trusted Networks</strong> 方案:</p>
- <ul>
- <li>✅ 官方支持,100% 可靠</li>
- <li>✅ 无需复杂的 OAuth2 流程</li>
- <li>✅ 零延迟,直接登录</li>
- </ul>
- <button class="button" onclick="alert('请在 Home Assistant 的 configuration.yaml 中配置:\\n\\nhomeassistant:\\n auth_providers:\\n - type: trusted_networks\\n trusted_networks:\\n - 118.251.191.88/32\\n trusted_users:\\n 118.251.191.88/32: YOUR_USER_ID\\n allow_bypass_login: true\\n - type: homeassistant')">
- 📖 查看 Trusted Networks 配置
- </button>
- </div>
- </div>
-
- <iframe id="testFrame" style="display:none;"></iframe>
-
- <script>
- const logDiv = document.getElementById('log');
- const iframe = document.getElementById('testFrame');
-
- function addLog(msg, type = 'info') {
- const entry = document.createElement('div');
- entry.className = 'log-entry';
- entry.textContent = new Date().toLocaleTimeString() + ' - ' + msg;
- logDiv.appendChild(entry);
- logDiv.scrollTop = logDiv.scrollHeight;
- console.log('[调试] ' + msg);
- }
-
- function testIframeMethod() {
- addLog('开始 iframe 测试...');
- addLog('加载 URL: ${magicLink}');
-
- let loaded = false;
- iframe.onload = function() {
- if (!loaded) {
- loaded = true;
- addLog('✓ iframe 加载完成');
- addLog('等待 5 秒后跳转...');
-
- let countdown = 5;
- const timer = setInterval(function() {
- countdown--;
- addLog('倒计时: ' + countdown + '秒');
- if (countdown <= 0) {
- clearInterval(timer);
- addLog('正在跳转到 Home Assistant...');
- window.location.href = '${targetBaseUrl}';
- }
- }, 1000);
- }
- };
-
- iframe.onerror = function(e) {
- addLog('✗ iframe 加载失败: ' + e, 'error');
- };
-
- iframe.src = '${magicLink.replace(/'/g, "\\'")}';
- }
-
- addLog('后端 OAuth2 登录成功');
- addLog('Authorization Code: ${authCode.substring(0, 20)}...');
- addLog('请选择测试方法');
- </script>
- </body>
- </html>
- `;
-
- 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 = `
- <!DOCTYPE html>
- <html lang="zh-CN">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>正在登录 Home Assistant...</title>
- <style>
- body {
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- color: white;
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
- display: flex;
- justify-content: center;
- align-items: center;
- height: 100vh;
- margin: 0;
- }
- .container { text-align: center; }
- .loader {
- border: 4px solid rgba(255, 255, 255, 0.3);
- border-top: 4px solid white;
- border-radius: 50%;
- width: 50px;
- height: 50px;
- animation: spin 1s linear infinite;
- margin: 0 auto 20px;
- }
- @keyframes spin {
- 0% { transform: rotate(0deg); }
- 100% { transform: rotate(360deg); }
- }
- h2 { margin: 0 0 10px 0; }
- p { margin: 5px 0; opacity: 0.9; font-size: 14px; }
- </style>
- </head>
- <body>
- <div class="container">
- <div class="loader"></div>
- <h2>正在登录...</h2>
- <p>准备进入 Home Assistant</p>
- </div>
- <iframe id="authFrame" style="display:none;"></iframe>
- <script>
- // 使用 iframe 预加载带有 code 的 URL
- // 让 HA 前端在后台完成 code → token 的交换
- const authUrl = "${loginResult.redirectUrl}";
- const targetUrl = "${config.targetBaseUrl}";
- const iframe = document.getElementById('authFrame');
-
- console.log('[降级方案] 使用 iframe 预加载:', authUrl);
-
- // 设置 iframe 超时
- let loaded = false;
- iframe.onload = function() {
- if (!loaded) {
- loaded = true;
- console.log('[降级方案] iframe 加载完成,等待 HA 处理 code...');
- // 给 HA 足够时间处理 code
- setTimeout(function() {
- console.log('[降级方案] 跳转到主页');
- window.location.href = targetUrl;
- }, 2000);
- }
- };
-
- // 加载带有 code 的 URL
- iframe.src = authUrl;
-
- // 保险起见,10秒后强制跳转
- setTimeout(function() {
- if (!loaded) {
- console.log('[降级方案] 超时,强制跳转');
- window.location.href = targetUrl;
- }
- }, 10000);
- </script>
- </body>
- </html>
- `;
-
- console.log(`[${requestId}] 总耗时: ${Date.now() - startTime}ms`);
- console.log('='.repeat(80) + '\n');
-
- return res.send(intermediateHtml);
- }
-
- // 对于 GET 查询登录,如果有 redirectUrl,使用 HTML 页面设置 Cookie 后跳转
- 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)}...`);
- });
-
- // 生成跳转 HTML,确保 Cookie 正确设置
- const html = generateRedirectHTML(
- cookieData,
- config.targetHost,
- config.targetDomain,
- requestId,
- loginResult.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');
-
- 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');
- });
|