spotify / solver.js
alolipop's picture
Upload 5 files
d0ffca4 verified
Raw History Blame Contribute Delete
16.1 kB
'use strict';
const { addExtra } = require('puppeteer-extra');
const rebrowser = require('rebrowser-puppeteer');
const StealthPlugin = require('puppeteer-extra-plugin-stealth');
const puppeteer = addExtra(rebrowser);
puppeteer.use(StealthPlugin());
const log = {
info : (...a) => console.log('[CF-SOLVER]', ...a),
warn : (...a) => console.warn('[CF-SOLVER][WARN]', ...a),
error: (...a) => console.error('[CF-SOLVER][ERR]', ...a),
};
let _browser = null;
let _browserProxy = null;
const CHROME_ARGS = [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-blink-features=AutomationControlled',
'--disable-infobars',
'--disable-notifications',
'--disable-popup-blocking',
'--disable-background-networking',
'--disable-backgrounding-occluded-windows',
'--disable-renderer-backgrounding',
'--disable-ipc-flooding-protection',
'--disable-hang-monitor',
'--disable-prompt-on-repost',
'--disable-sync',
'--disable-translate',
'--disable-client-side-phishing-detection',
'--disable-component-update',
'--disable-domain-reliability',
'--disable-features=AudioServiceOutOfProcess,IsolateOrigins,site-per-process',
'--metrics-recording-only',
'--no-first-run',
'--no-default-browser-check',
'--password-store=basic',
'--use-mock-keychain',
'--window-size=1366,768',
'--hide-scrollbars',
'--mute-audio',
];
async function getBrowser(proxy) {
if (_browser && _browser.isConnected() && _browserProxy === (proxy || null)) {
return _browser;
}
if (_browser) {
try { await _browser.close(); } catch {}
_browser = null;
}
const args = [...CHROME_ARGS];
if (proxy) args.push(`--proxy-server=${proxy}`);
log.info('Launching browser...');
_browser = await puppeteer.launch({
headless : true,
args,
executablePath : process.env.CHROME_PATH || undefined,
ignoreHTTPSErrors: true,
defaultViewport : null,
pipe : true,
});
_browserProxy = proxy || null;
_browser.on('disconnected', () => {
log.warn('Browser disconnected');
_browser = null;
_browserProxy = null;
});
return _browser;
}
const sleep = ms => new Promise(r => setTimeout(r, ms));
const randInt = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;
const humanDelay = () => sleep(randInt(800, 2000));
const CHROME_VERSIONS = [121, 122, 123, 124];
const FINGERPRINT_PRESETS = [
{
label : 'win10-intel',
osToken : 'Windows NT 10.0; Win64; x64',
navigatorPlatform : 'Win32',
secChUaPlatform : 'Windows',
webglVendor : 'Google Inc. (Intel)',
webglRenderer : 'ANGLE (Intel, Intel(R) UHD Graphics 620 Direct3D11 vs_5_0 ps_5_0, D3D11)',
hwConcurrencyPool : [4, 8],
deviceMemoryPool : [8, 16],
},
{
label : 'linux-mesa',
osToken : 'X11; Linux x86_64',
navigatorPlatform : 'Linux x86_64',
secChUaPlatform : 'Linux',
webglVendor : 'Mesa/X.org',
webglRenderer : 'Mesa Intel(R) UHD Graphics 620 (KBL GT2)',
hwConcurrencyPool : [4, 8],
deviceMemoryPool : [8, 16],
},
];
function pickFingerprint() {
const base = FINGERPRINT_PRESETS[randInt(0, FINGERPRINT_PRESETS.length - 1)];
const cv = CHROME_VERSIONS[randInt(0, CHROME_VERSIONS.length - 1)];
return {
...base,
chromeVersion: cv,
ua : `Mozilla/5.0 (${base.osToken}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${cv}.0.0.0 Safari/537.36`,
languages : ['en-US', 'en'],
hwConcurrency: base.hwConcurrencyPool[randInt(0, base.hwConcurrencyPool.length - 1)],
deviceMemory : base.deviceMemoryPool[randInt(0, base.deviceMemoryPool.length - 1)],
};
}
async function simulateMouse(page) {
try {
const vp = page.viewport() || { width: 1366, height: 768 };
const steps = randInt(4, 8);
for (let i = 0; i < steps; i++) {
await page.mouse.move(
randInt(100, vp.width - 100),
randInt(100, vp.height - 100),
{ steps: randInt(5, 12) }
);
await sleep(randInt(80, 250));
}
} catch {}
}
async function hardenFingerprint(page, fp) {
await page.evaluateOnNewDocument((fp) => {
const overrides = {
webdriver : false,
languages : fp.languages,
platform : fp.navigatorPlatform,
hardwareConcurrency : fp.hwConcurrency,
deviceMemory : fp.deviceMemory,
maxTouchPoints : 0,
appVersion : fp.ua.replace('Mozilla/', ''),
userAgent : fp.ua,
};
for (const [k, v] of Object.entries(overrides)) {
try {
Object.defineProperty(navigator, k, { get: () => v, configurable: true });
} catch {}
}
const origQuery = window.Permissions?.prototype?.query;
if (origQuery) {
window.Permissions.prototype.query = function(params) {
if (['notifications', 'push', 'midi', 'camera', 'microphone', 'geolocation'].includes(params?.name)) {
return Promise.resolve({ state: 'prompt', onchange: null });
}
return origQuery.call(this, params);
};
}
const seed = Math.floor(Math.random() * 1e9);
let rngState = seed;
const seededRand = () => {
rngState = (rngState * 1103515245 + 12345) & 0x7fffffff;
return rngState / 0x7fffffff;
};
const origToDataURL = HTMLCanvasElement.prototype.toDataURL;
HTMLCanvasElement.prototype.toDataURL = function(type) {
const ctx = this.getContext('2d');
if (ctx) {
try {
const pixel = ctx.getImageData(0, 0, 1, 1).data;
if (pixel[3] !== 0) {
ctx.fillStyle = `rgba(${Math.floor(seededRand()*2)},${Math.floor(seededRand()*2)},${Math.floor(seededRand()*2)},0.003)`;
ctx.fillRect(seededRand() * this.width, seededRand() * this.height, 1, 1);
}
} catch {}
}
return origToDataURL.apply(this, arguments);
};
const VENDOR = fp.webglVendor;
const RENDERER = fp.webglRenderer;
const patchWebGL = (proto) => {
const orig = proto.getParameter;
proto.getParameter = function(param) {
if (param === 37445) return VENDOR;
if (param === 37446) return RENDERER;
return orig.call(this, param);
};
};
if (typeof WebGLRenderingContext !== 'undefined') patchWebGL(WebGLRenderingContext.prototype);
if (typeof WebGL2RenderingContext !== 'undefined') patchWebGL(WebGL2RenderingContext.prototype);
const origCreateAnalyser = AudioContext.prototype?.createAnalyser;
if (origCreateAnalyser) {
AudioContext.prototype.createAnalyser = function() {
const analyser = origCreateAnalyser.call(this);
const origGetFloatFreq = analyser.getFloatFrequencyData.bind(analyser);
analyser.getFloatFrequencyData = function(array) {
origGetFloatFreq(array);
for (let i = 0; i < array.length; i++) {
array[i] += (seededRand() + seededRand() - 1) * 0.00005;
}
};
return analyser;
};
}
try {
Object.defineProperty(navigator, 'connection', {
get: () => ({
effectiveType: '4g',
rtt: 50 + Math.floor(seededRand() * 100),
downlink: 5 + seededRand() * 10,
saveData: false,
onchange: null,
}),
configurable: true,
});
} catch {}
delete window.cdc_adoQpoasnfa76pfcZLmcfl_Array;
delete window.cdc_adoQpoasnfa76pfcZLmcfl_Promise;
delete window.cdc_adoQpoasnfa76pfcZLmcfl_Symbol;
}, fp);
}
async function setHeaders(page, fp, referer) {
const cv = fp.chromeVersion;
await page.setExtraHTTPHeaders({
'Accept' : 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
'Accept-Language' : 'en-US,en;q=0.9',
'Accept-Encoding' : 'gzip, deflate, br',
'Cache-Control' : 'no-cache',
'Pragma' : 'no-cache',
'Sec-Ch-Ua' : `"Chromium";v="${cv}", "Not A(Brand";v="24", "Google Chrome";v="${cv}"`,
'Sec-Ch-Ua-Mobile' : '?0',
'Sec-Ch-Ua-Platform' : `"${fp.secChUaPlatform}"`,
'Sec-Fetch-Dest' : 'document',
'Sec-Fetch-Mode' : 'navigate',
'Sec-Fetch-Site' : referer ? 'same-origin' : 'none',
'Sec-Fetch-User' : '?1',
'Upgrade-Insecure-Requests': '1',
...(referer ? { 'Referer': referer } : {}),
});
}
async function isChallenging(page) {
try {
const title = await page.title();
const url = page.url();
const bodyText = await page.evaluate(() => document.body?.innerText?.slice(0, 500) || '').catch(() => '');
const cfSignals = [
title.includes('Just a moment'),
title.includes('Checking your browser'),
title.includes('Please Wait'),
title.includes('Attention Required'),
url.includes('/cdn-cgi/challenge-platform'),
url.includes('challenge'),
bodyText.includes('Checking if the site connection is secure'),
bodyText.includes('Please enable JS and disable any ad blocker'),
bodyText.includes('DDoS protection by'),
];
return cfSignals.some(Boolean);
} catch {
return false;
}
}
async function waitCFClear(page, timeout = 35000) {
const deadline = Date.now() + timeout;
let checks = 0;
while (Date.now() < deadline) {
checks++;
const challenging = await isChallenging(page);
if (!challenging) {
log.info(`CF cleared after ${checks} checks`);
await sleep(1500);
return true;
}
if (checks % 3 === 0) await simulateMouse(page);
await sleep(1200);
}
throw new Error(`CF challenge timeout after ${timeout}ms`);
}
function extractCFCookies(cookies) {
return {
cf_clearance: cookies.find(c => c.name === 'cf_clearance'),
};
}
async function navigateWithRetry(page, url, opts = {}, maxRetry = 2) {
const waitUntil = opts.waitUntil || 'domcontentloaded';
const timeout = opts.timeout || 35000;
for (let attempt = 1; attempt <= maxRetry; attempt++) {
try {
log.info(`Navigation attempt ${attempt}/${maxRetry}: ${url}`);
const resp = await page.goto(url, { waitUntil, timeout });
const status = resp?.status();
if (status === 403 && attempt < maxRetry) {
log.warn(`Got 403, retry with referer injection...`);
const origin = new URL(url).origin;
await page.setExtraHTTPHeaders({ 'Referer': origin + '/' });
await sleep(randInt(1500, 2500));
continue;
}
return resp;
} catch (e) {
if (attempt === maxRetry) throw e;
log.warn(`Navigation failed (${e.message}), retry in 2s...`);
await sleep(2000);
}
}
}
async function extractTurnstile(page) {
return page.evaluate(() => {
const input = document.querySelector('[name="cf-turnstile-response"]');
if (input?.value) return input.value;
if (window.__turnstileToken) return window.__turnstileToken;
return null;
}).catch(() => null);
}
async function solve({ url, mode, timeout, proxy }) {
timeout = timeout || 35000;
mode = mode || 'full';
log.info(`Solving: ${url} [mode=${mode}]`);
const browser = await getBrowser(proxy);
const fp = pickFingerprint();
const context = await browser.createBrowserContext();
const page = await context.newPage();
page.setDefaultNavigationTimeout(timeout);
page.setDefaultTimeout(timeout);
try {
await page.setViewport({ width: 1366, height: 768, deviceScaleFactor: 1 });
await page.setUserAgent(fp.ua);
await hardenFingerprint(page, fp);
await setHeaders(page, fp);
if (mode === 'turnstile-min') {
await page.evaluateOnNewDocument(() => {
window.__turnstileToken = '';
const hookTurnstile = () => {
if (!window.turnstile || window.turnstile.__hooked) return;
const origRender = window.turnstile.render.bind(window.turnstile);
window.turnstile.render = function(container, params) {
if (params && typeof params.callback === 'function') {
const origCb = params.callback;
params.callback = function(token) {
window.__turnstileToken = token;
return origCb(token);
};
}
return origRender(container, params);
};
window.turnstile.__hooked = true;
};
Object.defineProperty(window, 'turnstile', {
configurable: true,
set(v) { this.__ts = v; setTimeout(hookTurnstile, 0); },
get() { return this.__ts; }
});
});
await navigateWithRetry(page, url, { waitUntil: 'networkidle2', timeout });
await waitCFClear(page, timeout);
await humanDelay();
let turnstileToken = null;
try {
await page.waitForFunction(() => !!window.__turnstileToken, { timeout: 30000 });
turnstileToken = await page.evaluate(() => window.__turnstileToken);
} catch {}
if (!turnstileToken) {
turnstileToken = await page.evaluate(() =>
document.querySelector('#turnstile_response')?.value
|| document.querySelector('[name="turnstile"]')?.value
|| ''
).catch(() => '');
}
if (!turnstileToken) throw new Error('Turnstile token tidak ditemukan');
const liveUA = await page.evaluate(() => navigator.userAgent).catch(() => fp.ua);
return { token: turnstileToken, user_agent: liveUA, fingerprint: fp.label };
}
if (mode === 'screenshot') {
await navigateWithRetry(page, url, { waitUntil: 'domcontentloaded', timeout });
await sleep(2000);
const screenshot = await page.screenshot({ encoding: 'base64', fullPage: false });
const cookies = await page.cookies();
const liveUA = await page.evaluate(() => navigator.userAgent).catch(() => fp.ua);
return { screenshot, cookies, user_agent: liveUA, fingerprint: fp.label };
}
// cf_clearance + full
await navigateWithRetry(page, url, { waitUntil: 'domcontentloaded', timeout });
await simulateMouse(page);
await waitCFClear(page, timeout);
await sleep(randInt(1000, 2000));
await simulateMouse(page);
let cookies = await page.cookies();
let { cf_clearance } = extractCFCookies(cookies);
if (!cf_clearance) {
log.warn('cf_clearance belum ada, tunggu...');
await sleep(2500);
await simulateMouse(page);
cookies = await page.cookies();
cf_clearance = extractCFCookies(cookies).cf_clearance;
}
if (mode === 'cf_clearance') {
if (!cf_clearance || !cf_clearance.value) {
throw new Error('cf_clearance cookie tidak ditemukan');
}
const liveUA = await page.evaluate(() => navigator.userAgent).catch(() => fp.ua);
return {
cf_clearance: cf_clearance.value,
user_agent : liveUA,
fingerprint : fp.label,
};
}
const liveUA = await page.evaluate(() => navigator.userAgent).catch(() => fp.ua);
const cookieStr = cookies.map(c => `${c.name}=${c.value}`).join('; ');
return {
cookies,
cookie : cookieStr,
user_agent : liveUA,
cf_clearance: cf_clearance?.value || null,
headers : {
'user-agent' : liveUA,
'cookie' : cookieStr,
'cf-clearance': cf_clearance?.value || '',
},
fingerprint : fp.label,
};
} catch (err) {
log.error(`Solve failed: ${err.message}`);
if (_browser && !_browser.isConnected()) {
_browser = null;
_browserProxy = null;
}
throw err;
} finally {
try { await page.close(); } catch {}
try { await context.close(); } catch {}
}
}
async function closeBrowser() {
if (_browser) {
try { await _browser.close(); } catch {}
_browser = null;
_browserProxy = null;
}
}
module.exports = { solve, closeBrowser };