1 Commits

Author SHA1 Message Date
1030622e19 fix(1.0.12): generalize OAuth popup for all providers, skip passive flows
Two issues in 1.0.11:

  1. YouTube auto-opens window.open on accounts.google.com/...?passive=true
     at page load to silently pick up an existing Google session via
     postMessage. Our setWindowOpenHandler routed these to a top-level popup
     where the postMessage parent context is missing → Google falls back to
     "JavaScript отключен". Active "Войти" clicks don't carry passive=true,
     so they still need the popup.

  2. Only accounts.google.com was intercepted. Yandex, Mail.ru, Microsoft,
     VK, Apple and GitHub login pages run similar embedded-browser checks;
     in-place WebContentsView navigation to them would likely trip the same
     detectors.

Replaced isGoogleLoginUrl/migrateGoogleCookies with a provider table
(OAUTH_PROVIDERS by host, OAUTH_PATH_HOSTS by host+path-prefix) so any
known login domain routes through the same clean popup + cookie-migration
flow. passive=true URLs are filtered out so window.open auto-launches
silently fail instead of popping a broken popup.

LOGIN_PARTITION renamed persist:google-login → persist:oauth-login since
it now holds login state for all providers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 01:11:29 +03:00
2 changed files with 82 additions and 31 deletions

111
main.js
View File

@@ -629,29 +629,75 @@ async function restoreSession() {
// Google's anti-abuse JS at /v3/signin/_/AccountsSignInUi/browserinfo rejects // Google's anti-abuse JS at /v3/signin/_/AccountsSignInUi/browserinfo rejects
// any session that has an *explicit* setProxy({ proxyRules }) applied — the way // any session that has an *explicit* setProxy({ proxyRules }) applied — the way
// Chromium routes through such a proxy (explicit CONNECT etc.) is fingerprintable. // Chromium routes through such a proxy (explicit CONNECT etc.) is fingerprintable.
// Sessions inheriting Windows' system proxy pass cleanly. So we open the OAuth // Sessions inheriting Windows' system proxy pass cleanly. So we open OAuth flows
// flow in a dedicated partition that we never call setProxy() on, then copy the // 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 // resulting auth cookies into the parent view's session so the user appears
// so the user appears logged-in there too. // logged-in there too.
// //
// The popup is a top-level BrowserWindow (not WebContentsView) — embedded view // 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 // shape is also a signal anti-abuse engines read. No preload, no chrome.* spoof,
// overlay: anything we touch on the DOM/globals could trip the detector. // no fade overlay: anything we touch on the DOM/globals could trip the detector.
const LOGIN_PARTITION = 'persist:google-login'; //
// 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 { try {
const h = new URL(u).hostname; const url = new URL(u);
return h === 'accounts.google.com' || h.endsWith('.accounts.google.com'); // YouTube and friends silently call window.open on accounts.google.com/
} catch (_) { return false; } // ...?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) { function isOAuthLoginUrl(u) { return oauthProviderFor(u) !== null; }
// Copy .google.com and .youtube.com cookies so the parent view sees the
// just-established login session. Domains must include both bare and async function migrateOAuthCookies(fromSess, toSess, cookieDomains) {
// dot-prefixed so subdomain cookies are picked up. for (const domain of cookieDomains) {
const domains = ['.google.com', 'accounts.google.com', '.youtube.com', 'www.youtube.com', '.googleusercontent.com'];
for (const domain of domains) {
let cookies = []; let cookies = [];
try { cookies = await fromSess.cookies.get({ domain }); } catch (_) { continue; } try { cookies = await fromSess.cookies.get({ domain }); } catch (_) { continue; }
for (const c of cookies) { for (const c of cookies) {
@@ -674,6 +720,8 @@ async function migrateGoogleCookies(fromSess, toSess) {
const activeLoginPopups = new Set(); const activeLoginPopups = new Set();
function openLoginPopup(parentView, url) { function openLoginPopup(parentView, url) {
const provider = oauthProviderFor(url);
if (!provider) return; // shouldn't be called for non-OAuth urls
for (const p of activeLoginPopups) { for (const p of activeLoginPopups) {
if (p.parentView === parentView && !p.window.isDestroyed()) { if (p.parentView === parentView && !p.window.isDestroyed()) {
p.window.focus(); p.window.focus();
@@ -687,7 +735,7 @@ function openLoginPopup(parentView, url) {
const popup = new BrowserWindow({ const popup = new BrowserWindow({
width: 600, width: 600,
height: 750, height: 750,
title: 'Вход в Google', title: 'Вход',
parent: mainWindow, parent: mainWindow,
modal: false, modal: false,
autoHideMenuBar: true, autoHideMenuBar: true,
@@ -703,11 +751,11 @@ function openLoginPopup(parentView, url) {
activeLoginPopups.add(entry); activeLoginPopups.add(entry);
let finalizing = false; let finalizing = false;
const finishLogin = async (newUrl) => { const finishLogin = async () => {
if (finalizing) return; if (finalizing) return;
finalizing = true; finalizing = true;
try { try {
await migrateGoogleCookies(loginSess, parentView.webContents.session); await migrateOAuthCookies(loginSess, parentView.webContents.session, provider.cookieDomains);
if (!parentView.webContents.isDestroyed()) parentView.webContents.reload(); if (!parentView.webContents.isDestroyed()) parentView.webContents.reload();
} finally { } finally {
if (!popup.isDestroyed()) popup.close(); if (!popup.isDestroyed()) popup.close();
@@ -715,13 +763,16 @@ function openLoginPopup(parentView, url) {
}; };
const checkRedirect = (newUrl) => { 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 { try {
const h = new URL(newUrl).hostname; const h = new URL(newUrl).hostname;
// Login flow handed control back to a non-Google host (youtube.com etc.) → success. // Login flow handed control back to a non-login host (the service we
if (h && !h.endsWith('.google.com') && h !== 'google.com') { // came from, e.g. youtube.com) → success.
finishLogin(newUrl); const loginHost = provider.host;
} const isStillOnProviderApex = h === loginHost || h.endsWith('.' + loginHost.replace(/^[^.]+\./, ''));
if (!isStillOnProviderApex) finishLogin();
} catch (_) {} } catch (_) {}
}; };
popup.webContents.on('will-redirect', (_e, u) => checkRedirect(u)); 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) => { view.webContents.on('will-navigate', (e, newUrl) => {
if (newUrl.startsWith('data:')) { trackNavigation(newUrl); return; } if (newUrl.startsWith('data:')) { trackNavigation(newUrl); return; }
// accounts.google.com → top-level BrowserWindow popup (see openLoginPopup). // OAuth login URL → top-level BrowserWindow popup (see openLoginPopup).
if (isGoogleLoginUrl(newUrl)) { e.preventDefault(); openLoginPopup(view, newUrl); return; } if (isOAuthLoginUrl(newUrl)) { e.preventDefault(); openLoginPopup(view, newUrl); return; }
let newHostname = ''; let newHostname = '';
try { newHostname = new URL(newUrl).hostname; } catch (_) { trackNavigation(newUrl); return; } try { newHostname = new URL(newUrl).hostname; } catch (_) { trackNavigation(newUrl); return; }
if (origHostname && newHostname && newHostname !== origHostname && !isTrustedDomain(newHostname)) { 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); trackNavigation(newUrl);
}); });
view.webContents.on('will-redirect', (e, u) => { 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); trackNavigation(u);
}); });
view.webContents.setWindowOpenHandler(({ url: newUrl }) => { view.webContents.setWindowOpenHandler(({ url: newUrl }) => {
// accounts.google.com → top-level BrowserWindow popup (see openLoginPopup). // OAuth login URL → top-level BrowserWindow popup (see openLoginPopup).
if (isGoogleLoginUrl(newUrl)) { openLoginPopup(view, newUrl); return { action: 'deny' }; } if (isOAuthLoginUrl(newUrl)) { openLoginPopup(view, newUrl); return { action: 'deny' }; }
let newHostname = ''; let newHostname = '';
try { newHostname = new URL(newUrl).hostname; } catch (_) {} try { newHostname = new URL(newUrl).hostname; } catch (_) {}

View File

@@ -1,6 +1,6 @@
{ {
"name": "ESH-Media", "name": "ESH-Media",
"version": "1.0.11", "version": "1.0.12",
"private": true, "private": true,
"main": "main.js", "main": "main.js",
"scripts": { "scripts": {