| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| const express = require('express'); |
| const crypto = require('crypto'); |
|
|
| const PORT = parseInt(process.env.PORT || '3011'); |
| const CHALLENGE_URL = process.env.CHALLENGE_URL || 'https://cursor.com/cn/docs'; |
| const REFRESH_INTERVAL = parseInt(process.env.REFRESH_INTERVAL || '3000000'); |
| const CHALLENGE_WAIT = parseInt(process.env.CHALLENGE_WAIT || '55000'); |
|
|
| let browser, context, challengePage, workerPage; |
| let ready = false; |
| let startTime = Date.now(); |
| let challengeCount = 0; |
| let requestCount = 0; |
|
|
| const pendingRequests = new Map(); |
|
|
| |
|
|
| const fs = require('fs'); |
|
|
| |
| function findSystemChrome() { |
| const paths = [ |
| |
| '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', |
| |
| '/usr/bin/google-chrome', |
| '/usr/bin/google-chrome-stable', |
| '/usr/bin/chromium', |
| '/usr/bin/chromium-browser', |
| |
| 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe', |
| 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe', |
| ]; |
| for (const p of paths) { |
| if (fs.existsSync(p)) return p; |
| } |
| return null; |
| } |
|
|
| async function loadStealth() { |
| const { chromium } = require('playwright-extra'); |
| const stealth = require('puppeteer-extra-plugin-stealth'); |
| chromium.use(stealth()); |
| return chromium; |
| } |
|
|
| async function initBrowser() { |
| const chromium = await loadStealth(); |
| const chromePath = findSystemChrome(); |
|
|
| const launchOptions = { |
| headless: true, |
| args: [ |
| '--no-sandbox', |
| '--disable-setuid-sandbox', |
| '--disable-dev-shm-usage', |
| '--disable-gpu', |
| ], |
| }; |
|
|
| if (chromePath) { |
| launchOptions.executablePath = chromePath; |
| console.log(`[Stealth] Using system Chrome: ${chromePath}`); |
| } else { |
| console.log('[Stealth] System Chrome not found, using Playwright Chromium'); |
| } |
|
|
| console.log('[Stealth] Launching browser...'); |
| browser = await chromium.launch(launchOptions); |
|
|
| context = await browser.newContext({ |
| userAgent: |
| 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36', |
| locale: 'zh-CN', |
| viewport: { width: 1920, height: 1080 }, |
| }); |
|
|
| |
| challengePage = await context.newPage(); |
| console.log(`[Stealth] Passing Vercel challenge: ${CHALLENGE_URL}`); |
| await challengePage.goto(CHALLENGE_URL, { |
| waitUntil: 'domcontentloaded', |
| timeout: 60000, |
| }); |
|
|
| |
| await simulateHumanBehavior(challengePage); |
|
|
| |
| const ok = await waitForCookie(); |
| if (!ok) { |
| |
| console.log('[Stealth] First attempt failed, retrying challenge...'); |
| await challengePage.reload({ waitUntil: 'domcontentloaded', timeout: 60000 }); |
| await simulateHumanBehavior(challengePage); |
| const retryOk = await waitForCookie(); |
| if (!retryOk) { |
| throw new Error('Failed to obtain _vcrcs cookie after retry'); |
| } |
| } |
| challengeCount++; |
|
|
| |
| |
| workerPage = await context.newPage(); |
| await workerPage.goto(CHALLENGE_URL, { |
| waitUntil: 'domcontentloaded', |
| timeout: 60000, |
| }); |
|
|
| |
| await workerPage.exposeFunction( |
| '__proxyCallback', |
| (requestId, type, data) => { |
| const pending = pendingRequests.get(requestId); |
| if (!pending) return; |
|
|
| switch (type) { |
| case 'headers': { |
| const { status, contentType } = JSON.parse(data); |
| const headers = { |
| 'Cache-Control': 'no-cache', |
| Connection: 'keep-alive', |
| }; |
| if (contentType) headers['Content-Type'] = contentType; |
| pending.res.writeHead(status, headers); |
| break; |
| } |
| case 'chunk': |
| pending.res.write(data); |
| break; |
| case 'end': |
| pending.res.end(); |
| pending.resolve(); |
| break; |
| case 'error': |
| if (!pending.res.headersSent) { |
| pending.res.writeHead(502, { |
| 'Content-Type': 'application/json', |
| }); |
| } |
| pending.res.end( |
| JSON.stringify({ error: { message: data } }), |
| ); |
| pending.resolve(); |
| break; |
| } |
| }, |
| ); |
|
|
| ready = true; |
| console.log('[Stealth] Ready! Accepting proxy requests.'); |
| } |
|
|
| async function waitForCookie(maxWait) { |
| maxWait = maxWait || CHALLENGE_WAIT; |
| const start = Date.now(); |
| while (Date.now() - start < maxWait) { |
| const cookies = await context.cookies(); |
| const vcrcs = cookies.find((c) => c.name === '_vcrcs'); |
| if (vcrcs) { |
| console.log( |
| '[Stealth] _vcrcs obtained:', |
| vcrcs.value.substring(0, 40) + '...', |
| ); |
| return true; |
| } |
| await new Promise((r) => setTimeout(r, 2000)); |
| } |
| console.error('[Stealth] Failed to obtain _vcrcs within timeout'); |
| return false; |
| } |
|
|
| |
| async function simulateHumanBehavior(page) { |
| try { |
| |
| await new Promise(r => setTimeout(r, 500 + Math.random() * 1000)); |
|
|
| |
| const points = Array.from({ length: 5 }, () => ({ |
| x: 100 + Math.random() * 600, |
| y: 100 + Math.random() * 400, |
| })); |
| for (const p of points) { |
| await page.mouse.move(p.x, p.y, { steps: 5 + Math.floor(Math.random() * 10) }); |
| await new Promise(r => setTimeout(r, 100 + Math.random() * 300)); |
| } |
|
|
| |
| await page.mouse.wheel(0, 100 + Math.random() * 200); |
| await new Promise(r => setTimeout(r, 300 + Math.random() * 500)); |
| await page.mouse.wheel(0, -(50 + Math.random() * 100)); |
|
|
| |
| await page.mouse.click(300 + Math.random() * 400, 300 + Math.random() * 200); |
| await new Promise(r => setTimeout(r, 200 + Math.random() * 500)); |
|
|
| console.log('[Stealth] Human behavior simulation done'); |
| } catch (e) { |
| |
| console.log('[Stealth] Human simulation skipped:', e.message); |
| } |
| } |
|
|
| async function refreshChallenge() { |
| console.log('[Stealth] Refreshing challenge...'); |
| try { |
| await challengePage.goto(CHALLENGE_URL, { |
| waitUntil: 'networkidle', |
| timeout: 30000, |
| }); |
| const ok = await waitForCookie(); |
| if (ok) { |
| challengeCount++; |
| console.log( |
| `[Stealth] Challenge refreshed (total: ${challengeCount})`, |
| ); |
| } else { |
| console.error('[Stealth] Challenge refresh failed - cookie not obtained'); |
| } |
| } catch (e) { |
| console.error('[Stealth] Challenge refresh error:', e.message); |
| } |
| } |
|
|
| async function restartBrowser() { |
| console.log('[Stealth] Restarting browser...'); |
| ready = false; |
| try { |
| if (browser) await browser.close().catch(() => {}); |
| } catch (_) {} |
| browser = null; |
| context = null; |
| challengePage = null; |
| workerPage = null; |
| await initBrowser(); |
| } |
|
|
| |
|
|
| const app = express(); |
| app.use(express.json({ limit: '10mb' })); |
|
|
| |
| app.get('/health', async (_req, res) => { |
| let cookie = null; |
| if (context) { |
| const cookies = await context.cookies().catch(() => []); |
| const vcrcs = cookies.find((c) => c.name === '_vcrcs'); |
| if (vcrcs) cookie = vcrcs.value.substring(0, 40) + '...'; |
| } |
| res.json({ |
| status: ready ? 'ok' : 'initializing', |
| uptime: Math.floor((Date.now() - startTime) / 1000), |
| challengeCount, |
| requestCount, |
| cookie, |
| }); |
| }); |
|
|
| |
| app.post('/proxy/chat', async (req, res) => { |
| if (!ready) { |
| res.status(503).json({ |
| error: { message: 'Stealth proxy not ready, please wait' }, |
| }); |
| return; |
| } |
|
|
| const requestId = crypto.randomUUID(); |
| requestCount++; |
|
|
| |
| let aborted = false; |
| req.on('close', () => { |
| aborted = true; |
| }); |
|
|
| const promise = new Promise((resolve) => { |
| pendingRequests.set(requestId, { res, resolve }); |
| }); |
|
|
| |
| workerPage |
| .evaluate( |
| async ({ body, requestId }) => { |
| try { |
| const r = await fetch('/api/chat', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify(body), |
| }); |
|
|
| await window.__proxyCallback( |
| requestId, |
| 'headers', |
| JSON.stringify({ |
| status: r.status, |
| contentType: r.headers.get('content-type'), |
| }), |
| ); |
|
|
| if (!r.body) { |
| const text = await r.text(); |
| if (text) |
| await window.__proxyCallback( |
| requestId, |
| 'chunk', |
| text, |
| ); |
| await window.__proxyCallback(requestId, 'end', ''); |
| return; |
| } |
|
|
| const reader = r.body.getReader(); |
| const decoder = new TextDecoder(); |
| while (true) { |
| const { done, value } = await reader.read(); |
| if (done) break; |
| const chunk = decoder.decode(value, { stream: true }); |
| if (chunk) |
| await window.__proxyCallback( |
| requestId, |
| 'chunk', |
| chunk, |
| ); |
| } |
| await window.__proxyCallback(requestId, 'end', ''); |
| } catch (e) { |
| await window.__proxyCallback( |
| requestId, |
| 'error', |
| e.message || 'Browser fetch failed', |
| ); |
| } |
| }, |
| { body: req.body, requestId }, |
| ) |
| .catch((err) => { |
| const pending = pendingRequests.get(requestId); |
| if (pending && !pending.res.headersSent) { |
| pending.res.writeHead(502, { |
| 'Content-Type': 'application/json', |
| }); |
| pending.res.end( |
| JSON.stringify({ |
| error: { |
| message: 'Browser evaluate failed: ' + err.message, |
| }, |
| }), |
| ); |
| pending.resolve(); |
| } |
| }); |
|
|
| await promise; |
| pendingRequests.delete(requestId); |
| }); |
|
|
| |
|
|
| const MAX_INIT_RETRIES = parseInt(process.env.MAX_INIT_RETRIES || '5'); |
|
|
| (async () => { |
| |
| for (let attempt = 1; attempt <= MAX_INIT_RETRIES; attempt++) { |
| try { |
| console.log(`[Stealth] Initialization attempt ${attempt}/${MAX_INIT_RETRIES}...`); |
| await initBrowser(); |
| break; |
| } catch (e) { |
| console.error(`[Stealth] Attempt ${attempt} failed:`, e.message); |
| |
| if (browser) await browser.close().catch(() => {}); |
| browser = null; context = null; challengePage = null; workerPage = null; |
|
|
| if (attempt >= MAX_INIT_RETRIES) { |
| console.error(`[Stealth] All ${MAX_INIT_RETRIES} attempts failed, exiting.`); |
| process.exit(1); |
| } |
| const delay = attempt * 5; |
| console.log(`[Stealth] Retrying in ${delay}s...`); |
| await new Promise(r => setTimeout(r, delay * 1000)); |
| } |
| } |
|
|
| app.listen(PORT, '0.0.0.0', () => { |
| console.log(`[Stealth] Proxy listening on port ${PORT}`); |
| }); |
|
|
| |
| setInterval(refreshChallenge, REFRESH_INTERVAL); |
|
|
| |
| browser.on('disconnected', () => { |
| console.error('[Stealth] Browser disconnected! Restarting...'); |
| ready = false; |
| setTimeout(restartBrowser, 3000); |
| }); |
| })(); |
|
|
| |
| const shutdown = async () => { |
| console.log('[Stealth] Shutting down...'); |
| ready = false; |
| if (browser) await browser.close().catch(() => {}); |
| process.exit(0); |
| }; |
|
|
| process.on('SIGTERM', shutdown); |
| process.on('SIGINT', shutdown); |
|
|