Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1030622e19 |
111
main.js
111
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 (_) {}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ESH-Media",
|
||||
"version": "1.0.11",
|
||||
"version": "1.0.12",
|
||||
"private": true,
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
|
||||
Reference in New Issue
Block a user