3 Commits

Author SHA1 Message Date
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
b5e1296a7a fix(1.0.8): strip cliqz adblocker preload — breaks CSP-strict sites
cliqz/adblocker-electron registers its preload at session level via
session.setPreloads([...preloads, PRELOAD_PATH]) inside
enableBlockingInSession. That preload injects inline <script> elements
via doc.createElement('script') + script.appendChild(textNode) +
parent.appendChild(script). On modern strict-CSP sites this breaks:

- Trusted Types (YouTube, Gmail): "An HTMLScriptElement was directly
  modified and will not be executed" — 52+ console errors.
- Nonce-required CSP (kinogo via Cloudflare): "Refused to execute inline
  script ... script-src 'nonce-...'" — competing with Cloudflare's
  challenge JS, likely the proximate cause of the 403 we see on kinogo
  (CF treats the broken page as bot).

Remove the cliqz preload from each session immediately after
enableBlockingInSession. The network/CSP/blockers attached via
webRequest hooks remain active — only the script-injection layer for
anti-anti-adblock scriptlets is lost, which is a niche feature that
breaks more sites than it fixes.

The 1.0.7 Blink TrustedDOMTypes disable stays (defense in depth, no
cost). The 1.0.6 CSP-header strip stays removed (adblocker overwrites
the webRequest listener anyway).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 22:24:47 +03:00
e80704c534 fix(1.0.7): disable Trusted Types engine-wide via Blink feature flag
The 1.0.6 fix (strip require-trusted-types-for from CSP via
onHeadersReceived) didn't take effect: cliqz/adblocker calls
session.webRequest.onHeadersReceived during enableBlockingInSession,
overwriting our hook (Electron permits only one listener per session).

Replace with engine-level kill switch:
  app.commandLine.appendSwitch('disable-blink-features', 'TrustedDOMTypes')

Makes the entire Trusted Types runtime feature inert, so
require-trusted-types-for CSP becomes a no-op site-wide. Safe in this
kiosk/single-user context; only relaxes one security boundary that
sites use to harden against XSS via adblocker-style script injection —
which is exactly what we need to neutralize for cliqz's anti-anti-adblock
scriptlets on YouTube.

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

99
main.js
View File

@@ -6,6 +6,16 @@ const cheerio = require('cheerio');
const { ElectronBlocker, adsAndTrackingLists } = require('@cliqz/adblocker-electron');
const { autoUpdater } = require('electron-updater');
// Disable Trusted Types CSP enforcement engine-wide.
// YouTube sends `Content-Security-Policy: require-trusted-types-for 'script'`,
// which blocks the cliqz adblocker's scriptlet injection (it uses plain
// `script.text = ...`) → 52+ console errors and broken anti-adblock neutralizers.
// Stripping the CSP header via webRequest doesn't work — the adblocker's own
// onHeadersReceived hook overwrites ours (Electron allows only one listener
// per session). Disabling the Blink feature is the cleanest fix; safe in a
// kiosk single-user context.
app.commandLine.appendSwitch('disable-blink-features', 'TrustedDOMTypes');
const CONFIG_PATH = path.join(os.homedir(), '.ESH-Media.json');
const BLOCKER_CACHE_PATH = path.join(os.homedir(), '.ESH-Media-adblock-v3.bin');
const DEFAULT_TRUSTED_DOMAINS = [
@@ -101,7 +111,27 @@ function getBlocker() {
function enableBlockingInSession(sess) {
getBlocker()
.then(b => { b.enableBlockingInSession(sess); console.log('[adblock] enabled for session'); })
.then(b => {
b.enableBlockingInSession(sess);
// Remove the cliqz preload script that the blocker just registered on this
// session. The preload injects inline <script> elements (via createTextNode +
// appendChild) to neutralize anti-adblock scripts, but:
// • Strict-CSP sites (kinogo via Cloudflare, etc.) reject inline scripts
// without a matching nonce → "Refused to execute inline script".
// • Trusted-Types sites (YouTube, Gmail) reject `script.appendChild(text)`
// → "HTMLScriptElement was directly modified" (52 errors).
// We keep the adblocker's network blocking and CSP filtering (via the still-
// attached webRequest hooks), losing only the niche scriptlet/cosmetic-DOM
// injection layer that breaks more sites than it helps.
const before = sess.getPreloads();
const after = before.filter(p => !/adblocker-electron-preload/i.test(p));
if (after.length !== before.length) {
sess.setPreloads(after);
console.log('[adblock] enabled for session (preload script disabled)');
} else {
console.log('[adblock] enabled for session');
}
})
.catch(e => console.warn('[adblock] failed to enable:', e.message));
}
@@ -629,37 +659,30 @@ ipcMain.on('create-view', async (_event, name, url, imageUrl, _zoom, useProxy) =
trackNavigation(newUrl);
});
view.webContents.on('will-redirect', (_e, u) => trackNavigation(u));
view.webContents.setWindowOpenHandler(({ url: newUrl, frameName, features }) => {
view.webContents.setWindowOpenHandler(({ url: newUrl }) => {
let newHostname = '';
try { newHostname = new URL(newUrl).hostname; } catch (_) {}
// Trusted domain → open as real popup BrowserWindow with same session.
// This is what OAuth flows need: window.opener.postMessage() works,
// popup can close itself when done, parent stays on the original page.
// Trusted domain (Google, Yandex, etc.) → navigate IN-PLACE, no popup.
// 1.0.1 tried opening a real popup BrowserWindow here for OAuth postMessage
// 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)) {
return {
action: 'allow',
overrideBrowserWindowOptions: {
width: 520, height: 640,
parent: mainWindow,
autoHideMenuBar: true,
webPreferences: {
session: view.webContents.session,
contextIsolation: true,
nodeIntegration: false,
},
},
};
trackNavigation(newUrl);
view.webContents.loadURL(newUrl);
return { action: 'deny' };
}
// Untrusted cross-domain → ask the user (original behavior).
// Untrusted cross-domain → ask the user.
if (origHostname && newHostname && newHostname !== origHostname) {
pendingNavigate = { view, url: newUrl };
setConfirm(`Перейти на "${newHostname}"?`, 'navigate-confirmed');
return { action: 'deny' };
}
// Same-origin popup → just navigate the current view.
// Same-origin popup → navigate the current view.
trackNavigation(newUrl);
view.webContents.loadURL(newUrl);
return { action: 'deny' };
@@ -1228,39 +1251,9 @@ app.whenReady().then(async () => {
}
);
// Strip Trusted Types directives from CSP for sites that enforce them
// (YouTube, Gmail, etc.). The cliqz adblocker injects inline scriptlets to
// neutralize anti-adblock tricks; those injections use plain script.text
// assignment which TT blocks → "An HTMLScriptElement was directly modified
// and will not be executed" (52+ console errors on YouTube). Without TT
// the adblocker's scripts run and YouTube works normally.
const TT_STRIP_HOSTS = [
'youtube.com', 'youtu.be', 'youtubekids.com',
'google.com', 'gmail.com', 'mail.google.com',
];
const stripTrustedTypes = (sess) => {
sess.webRequest.onHeadersReceived(
{ urls: ['https://*/*'] },
(details, callback) => {
let host = '';
try { host = new URL(details.url).hostname; } catch {}
const match = TT_STRIP_HOSTS.some(d => host === d || host.endsWith('.' + d));
const headers = details.responseHeaders;
if (!match || !headers) return callback({});
for (const k of Object.keys(headers)) {
if (/^content-security-policy(-report-only)?$/i.test(k)) {
headers[k] = headers[k].map(v => v
.replace(/require-trusted-types-for[^;]*;?\s*/gi, '')
.replace(/trusted-types[^;]*;?\s*/gi, ''));
}
}
callback({ responseHeaders: headers });
}
);
};
stripTrustedTypes(session.defaultSession);
stripTrustedTypes(getProxySession());
stripTrustedTypes(getDirectSession());
// (Trusted Types now handled engine-wide via --disable-blink-features
// command-line switch at file top. webRequest.onHeadersReceived strip
// was tried in 1.0.6 but the cliqz adblocker overwrites the listener.)
// Apply proxy from config before blocker tries to download filter lists
loadTrustedDomainsFromDisk();

View File

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