2 Commits

Author SHA1 Message Date
103c2d1e09 feat(1.0.10): experimental chrome.* spoof for Google embedded-browser detection
Inject chrome.app, chrome.runtime, chrome.csi, chrome.loadTimes,
navigator.permissions.query wrapper via executeJavaScript on dom-ready
for every view. Goal: pass Google's JS-side embedded-browser detector
("Возможно, этот браузер небезопасны") by exposing the same chrome.*
shape real Chrome does.

Caveats acknowledged upfront:
- dom-ready fires AFTER <head> scripts, so detection scripts there have
  already seen the un-spoofed environment. Helps only if Google re-checks
  on form submit / later events.
- TLS fingerprint (JA3/JA4) is server-side. If Google flags us there,
  no client-side spoof works. This is a best-effort attempt.

No webPreferences changes — keeps contextIsolation:true and
sandbox-equivalent isolation intact. If this fails we lose nothing
architecturally and revert is trivial.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 22:45:49 +03:00
82f7fa7545 fix(1.0.9): revert OAuth popup BrowserWindow — Google detects embedded popups
1.0.1 ("trusted-domains OAuth popups") changed setWindowOpenHandler to
return action:'allow' with overrideBrowserWindowOptions for trusted
domains (Google, Yandex, etc.), opening a real Electron BrowserWindow
as popup. The reasoning was that OAuth flows need window.opener +
postMessage. That's correct for some flows but wrong for YouTube-style
login, which uses straight redirect.

Worse: Google specifically detects popup-style embedded browsers
(Electron BrowserWindow has distinct fingerprint vs real Chrome popup)
and blocks them with "Возможно, этот браузер небезопасны". The user
reported this stopped working after 1.0.0 — that's why.

Restore the 1.0.0 behavior for trusted domains: deny the popup and call
view.webContents.loadURL(newUrl) in the same view. The OAuth flow now
happens as a normal in-place navigation: YouTube → accounts.google.com
→ (user logs in) → redirect back to YouTube. No popup, no fingerprint
mismatch. The only UX loss is the popup window aesthetic; behavior is
functionally identical and matches what worked in 1.0.0.

Untrusted cross-domain still asks for confirmation, same-origin popups
still navigate in-place — unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 22:31:37 +03:00
2 changed files with 79 additions and 20 deletions

97
main.js
View File

@@ -40,6 +40,61 @@ const DEFAULT_CONFIG = { apps: [], proxy: { host: '127.0.0.1', port: '7890' }, t
let blockerPromise = null; let blockerPromise = null;
let cachedTrustedDomains = DEFAULT_TRUSTED_DOMAINS; let cachedTrustedDomains = DEFAULT_TRUSTED_DOMAINS;
// chrome.* spoof: injected via executeJavaScript on every page's dom-ready.
// Goal is to look like real Chrome to JS-based "embedded browser" detectors
// (Google login, etc.). Cannot fix TLS-fingerprint detection — that's server-side.
const CHROME_SPOOF_JS = `(function(){
try {
if (!window.chrome) window.chrome = {};
var c = window.chrome;
if (!c.app) c.app = {
isInstalled: false,
InstallState: { DISABLED: 'disabled', INSTALLED: 'installed', NOT_INSTALLED: 'not_installed' },
RunningState: { CANNOT_RUN: 'cannot_run', READY_TO_RUN: 'ready_to_run', RUNNING: 'running' },
getDetails: function(){ return null; },
getIsInstalled: function(){ return false; },
runningState: function(){ return 'cannot_run'; }
};
if (!c.runtime) c.runtime = {
PlatformOs: { MAC:'mac', WIN:'win', ANDROID:'android', CROS:'cros', LINUX:'linux', OPENBSD:'openbsd' },
PlatformArch: { ARM:'arm', X86_32:'x86-32', X86_64:'x86-64' },
PlatformNaclArch: { ARM:'arm', X86_32:'x86-32', X86_64:'x86-64' },
RequestUpdateCheckStatus: { NO_UPDATE:'no_update', THROTTLED:'throttled', UPDATE_AVAILABLE:'update_available' },
OnInstalledReason: { CHROME_UPDATE:'chrome_update', INSTALL:'install', SHARED_MODULE_UPDATE:'shared_module_update', UPDATE:'update' },
OnRestartRequiredReason: { APP_UPDATE:'app_update', OS_UPDATE:'os_update', PERIODIC:'periodic' },
sendMessage: function(){},
connect: function(){
return {
postMessage: function(){}, disconnect: function(){},
onDisconnect: { addListener: function(){}, removeListener: function(){} },
onMessage: { addListener: function(){}, removeListener: function(){} }
};
}
};
if (!c.csi) c.csi = function(){ return { startE: Date.now()-1000, onloadT: Date.now()-500, pageT: 1000, tran: 15 }; };
if (!c.loadTimes) c.loadTimes = function(){
var t = performance.timing;
return {
commitLoadTime: t.responseStart/1000, connectionInfo: 'http/1.1',
finishDocumentLoadTime: t.domContentLoadedEventEnd/1000,
finishLoadTime: (t.loadEventEnd/1000) || 0,
firstPaintAfterLoadTime: 0, firstPaintTime: t.responseEnd/1000,
navigationType: 'Other', npnNegotiatedProtocol: 'h2',
requestTime: t.requestStart/1000, startLoadTime: t.fetchStart/1000,
wasAlternateProtocolAvailable: false, wasFetchedViaSpdy: true, wasNpnNegotiated: true
};
};
// navigator.permissions.query: Notification permission must agree with Notification.permission
if (navigator.permissions && navigator.permissions.query) {
var origQuery = navigator.permissions.query.bind(navigator.permissions);
navigator.permissions.query = function(p){
if (p && p.name === 'notifications') return Promise.resolve({ state: Notification.permission, onchange: null });
return origQuery(p);
};
}
} catch (_) {}
})();`;
function loadTrustedDomainsFromDisk() { function loadTrustedDomainsFromDisk() {
try { try {
if (fs.existsSync(CONFIG_PATH)) { if (fs.existsSync(CONFIG_PATH)) {
@@ -608,6 +663,17 @@ ipcMain.on('create-view', async (_event, name, url, imageUrl, _zoom, useProxy) =
view.setBounds(getViewBounds()); view.setBounds(getViewBounds());
attachDevToolsShortcut(view.webContents); attachDevToolsShortcut(view.webContents);
// Experimental: spoof chrome.* JS objects on every page so Google's
// "embedded browser" detector sees a real-Chrome-shaped global. Runs on
// dom-ready which is AFTER <head> scripts, so detection scripts that ran
// there have already seen the un-spoofed environment — this fix only
// helps if Google's gate is re-checked on form submit / later events.
// TLS fingerprint (JA3) is server-side and unaffected; if Google flags us
// there, no client-side spoof helps. Best-effort attempt only.
view.webContents.on('dom-ready', () => {
view.webContents.executeJavaScript(CHROME_SPOOF_JS).catch(() => {});
});
view.webContents.on('did-finish-load', () => { view.webContents.on('did-finish-load', () => {
removeLoader(); removeLoader();
addChild(view); addChild(view);
@@ -659,37 +725,30 @@ ipcMain.on('create-view', async (_event, name, url, imageUrl, _zoom, useProxy) =
trackNavigation(newUrl); trackNavigation(newUrl);
}); });
view.webContents.on('will-redirect', (_e, u) => trackNavigation(u)); view.webContents.on('will-redirect', (_e, u) => trackNavigation(u));
view.webContents.setWindowOpenHandler(({ url: newUrl, frameName, features }) => { view.webContents.setWindowOpenHandler(({ url: newUrl }) => {
let newHostname = ''; let newHostname = '';
try { newHostname = new URL(newUrl).hostname; } catch (_) {} try { newHostname = new URL(newUrl).hostname; } catch (_) {}
// Trusted domain → open as real popup BrowserWindow with same session. // Trusted domain (Google, Yandex, etc.) → navigate IN-PLACE, no popup.
// This is what OAuth flows need: window.opener.postMessage() works, // 1.0.1 tried opening a real popup BrowserWindow here for OAuth postMessage
// popup can close itself when done, parent stays on the original page. // flows — turns out Google specifically detects popup-style embedded
// browsers and blocks OAuth ("Возможно, этот браузер небезопасны").
// YouTube-style login uses standard redirect flow, so in-place navigation
// works AND avoids the popup fingerprint. 1.0.0 behavior, restored.
if (newHostname && isTrustedDomain(newHostname)) { if (newHostname && isTrustedDomain(newHostname)) {
return { trackNavigation(newUrl);
action: 'allow', view.webContents.loadURL(newUrl);
overrideBrowserWindowOptions: { return { action: 'deny' };
width: 520, height: 640,
parent: mainWindow,
autoHideMenuBar: true,
webPreferences: {
session: view.webContents.session,
contextIsolation: true,
nodeIntegration: false,
},
},
};
} }
// Untrusted cross-domain → ask the user (original behavior). // Untrusted cross-domain → ask the user.
if (origHostname && newHostname && newHostname !== origHostname) { if (origHostname && newHostname && newHostname !== origHostname) {
pendingNavigate = { view, url: newUrl }; pendingNavigate = { view, url: newUrl };
setConfirm(`Перейти на "${newHostname}"?`, 'navigate-confirmed'); setConfirm(`Перейти на "${newHostname}"?`, 'navigate-confirmed');
return { action: 'deny' }; return { action: 'deny' };
} }
// Same-origin popup → just navigate the current view. // Same-origin popup → navigate the current view.
trackNavigation(newUrl); trackNavigation(newUrl);
view.webContents.loadURL(newUrl); view.webContents.loadURL(newUrl);
return { action: 'deny' }; return { action: 'deny' };

View File

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