diff --git a/main.js b/main.js index 3da5e9b..f891e9d 100644 --- a/main.js +++ b/main.js @@ -629,29 +629,75 @@ async function restoreSession() { // Google's anti-abuse JS at /v3/signin/_/AccountsSignInUi/browserinfo rejects // any session that has an *explicit* setProxy({ proxyRules }) applied — the way // Chromium routes through such a proxy (explicit CONNECT etc.) is fingerprintable. -// Sessions inheriting Windows' system proxy pass cleanly. So we open the OAuth -// flow in a dedicated partition that we never call setProxy() on, then copy the -// resulting .google.com / .youtube.com cookies into the parent view's session -// so the user appears logged-in there too. +// Sessions inheriting Windows' system proxy pass cleanly. So we open OAuth flows +// in a dedicated partition that we never call setProxy() on, then copy the +// resulting auth cookies into the parent view's session so the user appears +// logged-in there too. // // The popup is a top-level BrowserWindow (not WebContentsView) — embedded view -// shape is also a signal Google reads. No preload, no chrome.* spoof, no fade -// overlay: anything we touch on the DOM/globals could trip the detector. -const LOGIN_PARTITION = 'persist:google-login'; +// shape is also a signal anti-abuse engines read. No preload, no chrome.* spoof, +// no fade overlay: anything we touch on the DOM/globals could trip the detector. +// +// Yandex, Mail.ru, Microsoft Live, VK, Facebook, Apple, GitHub all run similar +// embedded-browser checks on their login pages (some more aggressive than +// Google's), so the popup is opened for every known OAuth provider host. +const LOGIN_PARTITION = 'persist:oauth-login'; -function isGoogleLoginUrl(u) { +// Hostname → list of cookie-domain suffixes to migrate back to the parent +// session after a successful login. Migrating extra domains is harmless, so +// each provider lists everything the auth handshake might set cookies on. +const OAUTH_PROVIDERS = { + 'accounts.google.com': ['.google.com', '.youtube.com', '.googleusercontent.com'], + 'passport.yandex.ru': ['.yandex.ru', '.yandex.com', '.passport.yandex.ru'], + 'passport.yandex.com': ['.yandex.ru', '.yandex.com', '.passport.yandex.com'], + 'oauth.yandex.ru': ['.yandex.ru', '.yandex.com'], + 'login.live.com': ['.live.com', '.microsoft.com', '.microsoftonline.com'], + 'login.microsoftonline.com': ['.microsoftonline.com', '.microsoft.com', '.live.com'], + 'login.microsoft.com': ['.microsoft.com', '.live.com'], + 'auth.mail.ru': ['.mail.ru'], + 'login.mail.ru': ['.mail.ru'], + 'oauth.mail.ru': ['.mail.ru'], + 'oauth.vk.com': ['.vk.com', '.vk.ru'], + 'login.vk.com': ['.vk.com', '.vk.ru'], + 'oauth.vk.ru': ['.vk.ru', '.vk.com'], + 'login.vk.ru': ['.vk.ru', '.vk.com'], + 'appleid.apple.com': ['.apple.com', '.icloud.com'], + 'idmsa.apple.com': ['.apple.com', '.icloud.com'], +}; + +// Hosts where the login subpath signals an OAuth flow (the rest of the host +// is regular browsing). Listed separately because the bare host is not a +// login page, only specific paths are. +const OAUTH_PATH_HOSTS = { + 'github.com': { prefix: '/login', cookies: ['.github.com', '.githubusercontent.com'] }, + 'www.facebook.com': { prefix: '/login', cookies: ['.facebook.com'] }, + 'm.facebook.com': { prefix: '/login', cookies: ['.facebook.com'] }, +}; + +function oauthProviderFor(u) { try { - const h = new URL(u).hostname; - return h === 'accounts.google.com' || h.endsWith('.accounts.google.com'); - } catch (_) { return false; } + const url = new URL(u); + // YouTube and friends silently call window.open on accounts.google.com/ + // ...?passive=true&... at page load to pick up an existing session via + // postMessage. That flow doesn't work in a top-level popup (no parent + // context → "JavaScript отключен" fallback). Skip popups for them; an + // active login click never has passive=true. + if (url.searchParams.get('passive') === 'true') return null; + if (OAUTH_PROVIDERS[url.hostname]) { + return { host: url.hostname, cookieDomains: OAUTH_PROVIDERS[url.hostname] }; + } + const pathRule = OAUTH_PATH_HOSTS[url.hostname]; + if (pathRule && url.pathname.startsWith(pathRule.prefix)) { + return { host: url.hostname, cookieDomains: pathRule.cookies }; + } + return null; + } catch (_) { return null; } } -async function migrateGoogleCookies(fromSess, toSess) { - // Copy .google.com and .youtube.com cookies so the parent view sees the - // just-established login session. Domains must include both bare and - // dot-prefixed so subdomain cookies are picked up. - const domains = ['.google.com', 'accounts.google.com', '.youtube.com', 'www.youtube.com', '.googleusercontent.com']; - for (const domain of domains) { +function isOAuthLoginUrl(u) { return oauthProviderFor(u) !== null; } + +async function migrateOAuthCookies(fromSess, toSess, cookieDomains) { + for (const domain of cookieDomains) { let cookies = []; try { cookies = await fromSess.cookies.get({ domain }); } catch (_) { continue; } for (const c of cookies) { @@ -674,6 +720,8 @@ async function migrateGoogleCookies(fromSess, toSess) { const activeLoginPopups = new Set(); function openLoginPopup(parentView, url) { + const provider = oauthProviderFor(url); + if (!provider) return; // shouldn't be called for non-OAuth urls for (const p of activeLoginPopups) { if (p.parentView === parentView && !p.window.isDestroyed()) { p.window.focus(); @@ -687,7 +735,7 @@ function openLoginPopup(parentView, url) { const popup = new BrowserWindow({ width: 600, height: 750, - title: 'Вход в Google', + title: 'Вход', parent: mainWindow, modal: false, autoHideMenuBar: true, @@ -703,11 +751,11 @@ function openLoginPopup(parentView, url) { activeLoginPopups.add(entry); let finalizing = false; - const finishLogin = async (newUrl) => { + const finishLogin = async () => { if (finalizing) return; finalizing = true; try { - await migrateGoogleCookies(loginSess, parentView.webContents.session); + await migrateOAuthCookies(loginSess, parentView.webContents.session, provider.cookieDomains); if (!parentView.webContents.isDestroyed()) parentView.webContents.reload(); } finally { if (!popup.isDestroyed()) popup.close(); @@ -715,13 +763,16 @@ function openLoginPopup(parentView, url) { }; const checkRedirect = (newUrl) => { - if (!newUrl || isGoogleLoginUrl(newUrl)) return; + if (!newUrl) return; + // Still on a login host (Google→Yandex→… cross-redirects are rare but allowed): stay. + if (isOAuthLoginUrl(newUrl)) return; try { const h = new URL(newUrl).hostname; - // Login flow handed control back to a non-Google host (youtube.com etc.) → success. - if (h && !h.endsWith('.google.com') && h !== 'google.com') { - finishLogin(newUrl); - } + // Login flow handed control back to a non-login host (the service we + // came from, e.g. youtube.com) → success. + const loginHost = provider.host; + const isStillOnProviderApex = h === loginHost || h.endsWith('.' + loginHost.replace(/^[^.]+\./, '')); + if (!isStillOnProviderApex) finishLogin(); } catch (_) {} }; popup.webContents.on('will-redirect', (_e, u) => checkRedirect(u)); @@ -814,8 +865,8 @@ ipcMain.on('create-view', async (_event, name, url, imageUrl, _zoom, useProxy, b view.webContents.on('will-navigate', (e, newUrl) => { if (newUrl.startsWith('data:')) { trackNavigation(newUrl); return; } - // accounts.google.com → top-level BrowserWindow popup (see openLoginPopup). - if (isGoogleLoginUrl(newUrl)) { e.preventDefault(); openLoginPopup(view, newUrl); return; } + // OAuth login URL → top-level BrowserWindow popup (see openLoginPopup). + if (isOAuthLoginUrl(newUrl)) { e.preventDefault(); openLoginPopup(view, newUrl); return; } let newHostname = ''; try { newHostname = new URL(newUrl).hostname; } catch (_) { trackNavigation(newUrl); return; } if (origHostname && newHostname && newHostname !== origHostname && !isTrustedDomain(newHostname)) { @@ -827,12 +878,12 @@ ipcMain.on('create-view', async (_event, name, url, imageUrl, _zoom, useProxy, b trackNavigation(newUrl); }); view.webContents.on('will-redirect', (e, u) => { - if (isGoogleLoginUrl(u)) { e.preventDefault(); openLoginPopup(view, u); return; } + if (isOAuthLoginUrl(u)) { e.preventDefault(); openLoginPopup(view, u); return; } trackNavigation(u); }); view.webContents.setWindowOpenHandler(({ url: newUrl }) => { - // accounts.google.com → top-level BrowserWindow popup (see openLoginPopup). - if (isGoogleLoginUrl(newUrl)) { openLoginPopup(view, newUrl); return { action: 'deny' }; } + // OAuth login URL → top-level BrowserWindow popup (see openLoginPopup). + if (isOAuthLoginUrl(newUrl)) { openLoginPopup(view, newUrl); return { action: 'deny' }; } let newHostname = ''; try { newHostname = new URL(newUrl).hostname; } catch (_) {} diff --git a/package.json b/package.json index f661737..a98ba42 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ESH-Media", - "version": "1.0.11", + "version": "1.0.12", "private": true, "main": "main.js", "scripts": {