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');
});