const { app, BrowserWindow, WebContentsView, ipcMain, session } = require('electron'); const path = require('path'); const fs = require('fs'); const os = require('os'); 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 = [ // Google ecosystem (OAuth) 'google.com', 'accounts.google.com', 'googleapis.com', 'googleusercontent.com', 'gstatic.com', 'youtube.com', 'ytimg.com', 'googlevideo.com', // Yandex 'yandex.ru', 'yandex.com', 'passport.yandex.ru', 'passport.yandex.com', 'yastatic.net', // GitHub 'github.com', 'github.io', 'githubassets.com', 'githubusercontent.com', // VK / Mail.ru 'vk.com', 'vk.ru', 'vkuser.net', 'mail.ru', 'my.mail.ru', // Microsoft (login.live.com etc., некоторые сайты через них) 'live.com', 'microsoft.com', 'microsoftonline.com', 'office.com', // Apple 'apple.com', 'icloud.com', // Facebook (для соцлогина) 'facebook.com', 'fb.com', ]; const DEFAULT_CONFIG = { apps: [], proxy: { host: '127.0.0.1', port: '7890' }, trustedDomains: DEFAULT_TRUSTED_DOMAINS }; let blockerPromise = null; 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() { try { if (fs.existsSync(CONFIG_PATH)) { const cfg = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8')); if (Array.isArray(cfg.trustedDomains) && cfg.trustedDomains.length) { cachedTrustedDomains = cfg.trustedDomains; } } } catch (_) {} } function isTrustedDomain(hostname) { if (!hostname) return false; const h = hostname.toLowerCase(); return cachedTrustedDomains.some(d => { const dom = d.toLowerCase().replace(/^\./, ''); return h === dom || h.endsWith('.' + dom); }); } function getBlocker() { if (blockerPromise) return blockerPromise; blockerPromise = (async () => { // Load from cache first (avoids re-downloading on every startup) if (fs.existsSync(BLOCKER_CACHE_PATH)) { try { const data = fs.readFileSync(BLOCKER_CACHE_PATH); const b = ElectronBlocker.deserialize(new Uint8Array(data)); console.log('[adblock] loaded from cache'); return b; } catch (e) { console.warn('[adblock] cache invalid, re-downloading:', e.message); } } // Download filter lists (EasyList + EasyPrivacy + uBlock Origin + Russian ad networks) console.log('[adblock] downloading filter lists...'); const fetchFn = (url, opts) => getProxySession().fetch(url, opts); const russianLists = [ 'https://filters.adtidy.org/extension/ublock/filters/1.txt', // AdGuard Russian 'https://easylist-downloads.adblockplus.org/ruadlist+easylist.txt', // RuAdList ]; const b = await ElectronBlocker.fromLists(fetchFn, [...adsAndTrackingLists, ...russianLists]); // Whitelist domains that need ALL requests passed through unfiltered. // Tracking-list false positives on these break critical functionality: // • Google: OAuth/login integrity checks fail without gstatic + analytics endpoints // → "Возможно, этот браузер или приложение небезопасны" error // • Yandex/Mail/Microsoft/Apple: same OAuth-style integrity flows // • TMDB: movie search API and poster CDN const whitelist = [ '@@||api.themoviedb.org^', '@@||image.tmdb.org^', '@@||themoviedb.org^', '@@||google.com^', '@@||googleapis.com^', '@@||googleusercontent.com^', '@@||gstatic.com^', '@@||youtube.com^', '@@||ytimg.com^', '@@||googlevideo.com^', '@@||google-analytics.com^', '@@||googletagmanager.com^', '@@||yandex.ru^', '@@||yandex.com^', '@@||yastatic.net^', '@@||mc.yandex.ru^', '@@||github.com^', '@@||githubassets.com^', '@@||githubusercontent.com^', '@@||vk.com^', '@@||vk.ru^', '@@||vkuser.net^', '@@||mail.ru^', '@@||my.mail.ru^', '@@||imgsmail.ru^', '@@||microsoft.com^', '@@||microsoftonline.com^', '@@||live.com^', '@@||office.com^', '@@||apple.com^', '@@||icloud.com^', '@@||facebook.com^', '@@||fbcdn.net^', ]; b.updateFromDiff({ added: whitelist }); fs.writeFileSync(BLOCKER_CACHE_PATH, Buffer.from(b.serialize())); console.log('[adblock] filter lists downloaded and cached'); return b; })(); return blockerPromise; } function enableBlockingInSession(sess) { getBlocker() .then(b => { b.enableBlockingInSession(sess); // Remove the cliqz preload script that the blocker just registered on this // session. The preload injects inline