const { SESSION_TTL, APP, PLEX, TIMEOUTS, buildMediaAuth } = require('../../constants'); const { createCache, CACHE_CONFIGS } = require('../../cache-config'); /** * Auth session handlers routes factory * @param {Object} deps - Explicit dependencies * @param {Object} deps.authManager - Auth manager * @param {Object} deps.credentialManager - Credential manager * @param {Function} deps.asyncHandler - Async route handler wrapper * @param {Function} deps.errorResponse - Error response helper * @param {Object} deps.log - Logger instance * @param {Function} deps.fetchT - Timeout-wrapped fetch * @returns {{ getAppSession: Function, appSessionCache: Object }} */ module.exports = function({ authManager: _authManager, credentialManager: _credentialManager, asyncHandler: _asyncHandler, errorResponse: _errorResponse, log, fetchT }) { const ctx = { fetchT }; const appSessionCache = createCache(CACHE_CONFIGS.appSessions); async function getAppSession(serviceId, baseUrl, username, password) { const cached = appSessionCache.get(serviceId); if (cached && cached.exp > Date.now()) { if (cached.failed) return null; return cached.cookies; } let loginUrl, loginBody, contentType = 'application/x-www-form-urlencoded'; const extraHeaders = {}; let expectJsonToken = false; const formEncode = (s) => encodeURIComponent(s).replace(/\*/g, '%2A'); switch (serviceId) { case 'torrent': loginUrl = `${baseUrl}api/v2/auth/login`; loginBody = `username=${formEncode(username)}&password=${formEncode(password)}`; extraHeaders['Authorization'] = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`; break; case 'router': { const routerBody = `username=${formEncode(username)}&password=${formEncode(password)}&Continue=Continue`; try { const { spawnSync } = require('child_process'); const proc = spawnSync('wget', [ '-q', '-S', `--post-data=${routerBody}`, '-O', '/dev/null', `${baseUrl}/cgi-bin/login.ha` ], { timeout: 5000, encoding: 'utf8' }); const result = (proc.stderr || '').split('\n').slice(0, 2).join('\n'); const locationMatch = result.match(/Location:\s*(.+)/); const location = locationMatch ? locationMatch[1].trim() : ''; if (location && !location.includes('login')) { appSessionCache.set(serviceId, { cookies: '__ip_session=1', exp: Date.now() + SESSION_TTL.IP_SESSION }); log.info('auth', 'Router auto-login successful (IP-based session)', { serviceId }); return '__ip_session=1'; } log.warn('auth', 'Router auto-login failed', { serviceId }); } catch (e) { log.warn('auth', 'Router auto-login error', { serviceId, error: e.message?.substring(0, 100) }); } appSessionCache.set(serviceId, { failed: true, exp: Date.now() + SESSION_TTL.FAILED_LOGIN }); return null; } case 'sync': loginUrl = `${baseUrl}/rest/noauth/auth/password`; contentType = 'application/json'; loginBody = JSON.stringify({ username, password }); break; case 'chat': loginUrl = `${baseUrl}/api/v1/auths/signin`; contentType = 'application/json'; loginBody = JSON.stringify({ email: username, password }); expectJsonToken = true; break; case 'jellyfin': case 'emby': { const mediaAuth = buildMediaAuth(APP.DEVICE_IDS.SSO); try { const authResp = await fetchT(`${baseUrl}/Users/AuthenticateByName`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Emby-Authorization': mediaAuth }, body: JSON.stringify({ Username: username, Pw: password }), }, TIMEOUTS.HTTP_LONG); const authData = await authResp.json(); if (authData.AccessToken) { const tokenData = { token: authData.AccessToken, userId: authData.User?.Id, serverId: authData.ServerId, serverName: authData.User?.ServerName || serviceId, }; appSessionCache.set(serviceId, { cookies: `token=${authData.AccessToken}`, token: authData.AccessToken, tokenData, exp: Date.now() + SESSION_TTL.TOKEN_SESSION }); log.info('auth', 'Auto-login successful (token + userId obtained)', { serviceId }); return `token=${authData.AccessToken}`; } log.warn('auth', 'Auto-login failed', { serviceId, status: authResp.status }); } catch (e) { log.warn('auth', 'Auto-login error', { serviceId, error: e.message }); } appSessionCache.set(serviceId, { failed: true, exp: Date.now() + SESSION_TTL.FAILED_LOGIN }); return null; } case 'plex': { try { const plexResp = await fetchT(PLEX.AUTH_URL, { method: 'POST', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', 'Authorization': `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`, 'X-Plex-Client-Identifier': APP.DEVICE_IDS.SSO, 'X-Plex-Product': APP.NAME, 'X-Plex-Version': APP.VERSION, }, body: JSON.stringify({}), }, TIMEOUTS.HTTP_LONG); const plexData = await plexResp.json(); const token = plexData?.user?.authToken; if (token) { appSessionCache.set(serviceId, { cookies: `plexToken=${token}`, token, exp: Date.now() + SESSION_TTL.TOKEN_SESSION }); log.info('auth', 'Plex auto-login successful via plex.tv', { serviceId }); return `plexToken=${token}`; } log.warn('auth', 'Plex auto-login failed: no token in response', { serviceId, status: plexResp.status }); } catch (e) { log.warn('auth', 'Plex auto-login error', { serviceId, error: e.message }); } appSessionCache.set(serviceId, { failed: true, exp: Date.now() + SESSION_TTL.FAILED_LOGIN }); return null; } default: loginUrl = `${baseUrl}login`; loginBody = `username=${formEncode(username)}&password=${formEncode(password)}&rememberMe=on`; extraHeaders['Authorization'] = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`; break; } try { const resp = await fetchT(loginUrl, { method: 'POST', headers: { 'Content-Type': contentType, ...extraHeaders }, body: loginBody, redirect: 'manual', }, TIMEOUTS.HTTP_LONG); if (expectJsonToken) { try { const data = await resp.json(); if (data.token) { const cookies = `token=${data.token}`; appSessionCache.set(serviceId, { cookies, exp: Date.now() + SESSION_TTL.COOKIE_SESSION }); log.info('auth', 'Auto-login successful (JWT token cached)', { serviceId }); return cookies; } } catch (e) { /* JSON parse failed */ } log.warn('auth', 'Auto-login: no token in response', { serviceId, status: resp.status }); appSessionCache.set(serviceId, { failed: true, exp: Date.now() + SESSION_TTL.FAILED_LOGIN }); return null; } if (serviceId === 'torrent') { const text = await resp.text(); if (text.trim() !== 'Ok.') { log.warn('auth', 'Auto-login failed', { serviceId, response: text.trim() }); appSessionCache.set(serviceId, { failed: true, exp: Date.now() + SESSION_TTL.FAILED_LOGIN }); return null; } } const setCookies = resp.headers.getSetCookie?.() || []; if (setCookies.length > 0) { const cookies = setCookies.map(c => c.split(';')[0]).join('; '); appSessionCache.set(serviceId, { cookies, exp: Date.now() + SESSION_TTL.COOKIE_SESSION }); log.info('auth', 'Auto-login successful, session cached', { serviceId, cookieCount: setCookies.length }); return cookies; } const rawCookie = resp.headers.get('set-cookie'); if (rawCookie) { const cookies = rawCookie.split(/,(?=[^ ])/).map(c => c.split(';')[0].trim()).join('; '); appSessionCache.set(serviceId, { cookies, exp: Date.now() + SESSION_TTL.COOKIE_SESSION }); log.info('auth', 'Auto-login successful (fallback), session cached', { serviceId }); return cookies; } log.warn('auth', 'Auto-login: no cookies in response', { serviceId, status: resp.status }); appSessionCache.set(serviceId, { failed: true, exp: Date.now() + SESSION_TTL.FAILED_LOGIN }); } catch (e) { log.warn('auth', 'Auto-login error', { serviceId, error: e.message }); appSessionCache.set(serviceId, { failed: true, exp: Date.now() + SESSION_TTL.FAILED_LOGIN }); } return null; } // Expose both the function and the cache so sso-gate can use them return { getAppSession, appSessionCache }; };