liuq 4 maanden geleden
bovenliggende
commit
741b966fb2
1 gewijzigde bestanden met toevoegingen van 92 en 291 verwijderingen
  1. 92 291
      server.js

+ 92 - 291
server.js

@@ -458,329 +458,136 @@ async function handleRSAEncryptedFormLogin(config, credentials) {
 async function handleHomeAssistantLogin(config, credentials) {
   const { targetBaseUrl } = config;
   
-  console.log('=== Home Assistant 登录 ===');
+  console.log('=== Home Assistant 登录 (OAuth2 模式) ===');
   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}/`
+    'Origin': 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(
+    // ==========================================
+    // 步骤1: 创建登录流程 (Init Flow)
+    // ==========================================
+    console.log('步骤1: 创建登录流程...');
+    const flowResponse = await axios.post(
       `${targetBaseUrl}/auth/login_flow`,
       {
-        client_id: `${targetBaseUrl}/`,
+        client_id: `${targetBaseUrl}/`, // clientId 必须和浏览器访问的一致
         handler: ['homeassistant', null],
-        redirect_uri: `${targetBaseUrl}/`
+        redirect_uri: `${targetBaseUrl}/?auth_callback=1`
       },
-      {
+      { 
         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));
+    
+    console.log(`流程创建响应状态码: ${flowResponse.status}`);
+    console.log(`流程创建响应数据:`, JSON.stringify(flowResponse.data, null, 2));
+    
+    if (flowResponse.status !== 200) {
       return {
         success: false,
-        message: `创建登录流程失败: ${error.response.status} - ${JSON.stringify(error.response.data)}`,
-        response: error.response.data
+        message: `创建登录流程失败,状态码: ${flowResponse.status}`,
+        response: flowResponse.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(
+    
+    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: 提交用户名和密码...');
+    const loginResponse = await axios.post(
       `${targetBaseUrl}/auth/login_flow/${flowId}`,
       {
         username: credentials.username,
         password: credentials.password,
-        client_id: `${targetBaseUrl}/`
+        client_id: `${targetBaseUrl}/` // 保持一致
       },
-      {
+      { 
         headers: baseHeaders,
-        maxRedirects: 0,
         validateStatus: function (status) {
           return status >= 200 && status < 500;
         }
       }
     );
+    
+    console.log(`登录响应状态码: ${loginResponse.status}`);
+    console.log(`登录响应数据:`, JSON.stringify(loginResponse.data, null, 2));
+    
+    // ==========================================
+    // 步骤3: 构造浏览器跳转 URL (关键修正)
+    // ==========================================
+    const responseData = loginResponse.data || {};
+    const responseType = responseData.type;
+    
+    console.log(`响应类型: ${responseType}`);
+    
+    // 如果登录成功,type 通常是 'create_entry'
+    if (responseData.result) {
+      const authCode = responseData.result;
+      console.log(`登录成功!获取到 Authorization Code: ${authCode}`);
+      
+      // 构造魔术链接,让浏览器去完成 Token 交换
+      // 格式: http://ha-url/?auth_callback=1&code=YOUR_CODE
+      const redirectUrl = `${targetBaseUrl}/?auth_callback=1&code=${encodeURIComponent(authCode)}`;
+      
+      console.log('生成自动登录跳转链接:', redirectUrl);
+      console.log('Home Assistant 前端将自动识别 URL 中的 code 并完成 Token 交换');
+      
+      // 返回跳转 URL,让浏览器直接跳转
+      return {
+        success: true,
+        redirectUrl: redirectUrl, // 告诉上层直接跳转这个 URL
+        cookies: [], // HA 不需要 Cookie
+        response: loginResponse.data
+      };
+    } else {
+      console.error('登录未返回 Authorization Code:', responseData);
+      return {
+        success: false,
+        message: '登录失败: 未返回授权码',
+        response: responseData
+      };
+    }
+    
   } catch (error) {
-    console.error('提交登录信息失败:', error.message);
+    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)}`,
+        message: `登录失败: ${error.response.status} - ${JSON.stringify(error.response.data)}`,
         response: error.response.data
       };
     }
     return {
       success: false,
-      message: `提交登录信息失败: ${error.message}`,
+      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
-    };
-  }
 }
 
 // 处理普通表单登录(未加密)
@@ -1027,6 +834,16 @@ app.get('/api/auto-login/:siteId', async (req, res) => {
     
     console.log(`[${requestId}] 登录成功!`);
     
+    // 对于 Home Assistant,如果返回了 redirectUrl,直接重定向
+    if (config.loginMethod === 'home-assistant' && loginResult.redirectUrl) {
+      console.log(`[${requestId}] Home Assistant 登录成功,直接重定向到: ${loginResult.redirectUrl}`);
+      console.log(`[${requestId}] 总耗时: ${Date.now() - startTime}ms`);
+      console.log('='.repeat(80) + '\n');
+      
+      // 直接重定向到带有 auth code 的 URL
+      return res.redirect(loginResult.redirectUrl);
+    }
+    
     // 解析 Cookie
     const cookieData = parseCookies(loginResult.cookies);
     console.log(`[${requestId}] 解析到 ${cookieData.length} 个 Cookie:`);
@@ -1034,24 +851,8 @@ app.get('/api/auto-login/:siteId', async (req, res) => {
       console.log(`[${requestId}]   Cookie ${index + 1}: ${cookie.name} = ${cookie.value.substring(0, 20)}...`);
     });
     
-    // 生成跳转 HTML(添加更多调试信息)
-    // 对于 Home Assistant,如果没有 Cookie,让浏览器端完成登录
+    // 生成跳转 HTML
     let redirectUrl = `http://${config.targetHost}/`;
-    let homeAssistantLoginData = null;
-    
-    if (config.loginMethod === 'home-assistant' && cookieData.length === 0) {
-      console.log(`[${requestId}] Home Assistant 无 Cookie,准备在浏览器端完成登录流程`);
-      // 传递登录信息给浏览器端
-      homeAssistantLoginData = {
-        targetBaseUrl: config.targetBaseUrl,
-        username: credentials.username,
-        password: credentials.password,
-        // 如果后端已获取到 result token,也传递过去
-        resultToken: loginResult.response?.result
-      };
-      redirectUrl = config.targetBaseUrl;
-      console.log(`[${requestId}] 将在浏览器端执行 Home Assistant 登录流程`);
-    }
     
     console.log(`[${requestId}] 生成跳转页面,目标: ${redirectUrl}`);
     const html = generateRedirectHTML(
@@ -1060,7 +861,7 @@ app.get('/api/auto-login/:siteId', async (req, res) => {
       config.targetDomain,
       requestId,
       redirectUrl,
-      homeAssistantLoginData
+      null
     );
     
     // 在响应头中设置 Cookie