UNPKG

pear3

Version:

Undetectable Chrome automation with a fully automated Chromium using extension APIs. 😎

341 lines (296 loc) • 12.6 kB
const ws = require('ws'); const { exec } = require("child_process"); const installer = require('./installer.js'); const fs = require('fs'); const path = require('path'); const http = require('http'); const os = require('os'); const crypto = require('crypto'); module.exports = async function (app) { if (!app.browserPath) { if (!fs.existsSync(path.join(__dirname, 'chrome'))) await installer() const file = fs.readdirSync(path.join(__dirname, 'chrome'))[0] if (file == "chrome-win64") { app.browserPath = path.join(__dirname, 'chrome', file, 'chrome.exe') } else { app.browserPath = path.join(__dirname, 'chrome', file, 'chrome') } } const args = [ `--load-extension="${path.join(__dirname, 'extension')}"`, "--no-first-run", "--no-default-browser-check", "--disable-features=Translate", '--disable-translate', "--disable-infobars", "--disable-notifications", "--disable-popup-blocking", "--start-maximized", ...(app.args ? app.args : []) ] const settings = JSON.parse(fs.readFileSync(path.join(__dirname, 'extension/settings.json'), 'utf-8') || '{}'); if (app.cookies) { if (app.cookies.includes(".json")) app.cookies = JSON.parse(fs.readFileSync(app.cookies, 'utf-8')); else if (typeof app.cookies === 'string') app.cookies = JSON.parse(app.cookies); } else app.cookies = []; settings.cookies = app.cookies; fs.writeFileSync(path.join(__dirname, 'extension/settings.json'), JSON.stringify(settings, null, 2)); if (app.userAgent) args.push(`--user-agent="${app.userAgent}"`); if (app.viewport) { if (typeof app.viewport === 'string') { const [width, height] = app.viewport.split('x').map(Number); args.push(`--window-size=${width},${height}`); } else if (typeof app.viewport === 'object') { const { width, height } = app.viewport; args.push(`--window-size=${width},${height}`); } } if (app.incognito) args.push('--incognito'); if (app.headless) args.push('--headless'); if (app.disableGpu) args.push('--disable-gpu'); if (app.nosandbox) args.push('--no-sandbox'); if (app.proxy) args.push(`--proxy-server=${app.proxy}`); if (!app.profileDir) { app.profileDir = path.join(os.tmpdir(), 'chrome-profile-' + crypto.randomBytes(8).toString('hex')); } if (app.profileDir) { if (app.profileDir == true) return; app.profileDir = path.join(process.cwd(), app.profileDir); if (!fs.existsSync(app.profileDir)) fs.mkdirSync(app.profileDir, { recursive: true }); args.push(`--user-data-dir="${app.profileDir}"`); } if (app.muteaudio) args.push('--mute-audio'); app.fullEndpoint = `${app.endpoint || "http://localhost"}:${app.port || 8191}`; args.push(app.fullEndpoint); const file = fs.readFileSync(path.join(__dirname, 'extension/Template_content.js'), 'utf-8'); fs.writeFileSync(path.join(__dirname, 'extension/index.js'), file.replace(/__PEARSYSTEM_ENDPOINT__/g, app.fullEndpoint)); const indexHtml = fs.readFileSync(path.join(__dirname, 'index.html'), 'utf-8'); const SettingsJson = fs.readFileSync(path.join(__dirname, 'extension/settings.json'), 'utf-8'); const server = http.createServer((req, res) => { if (req.url === "/") { res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(indexHtml); } else if (req.url === "/settings.json") { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(SettingsJson); } }); const wss = new ws.Server({ server }); var firstConnection; var connections = {} wss.on('connection', (socket) => { if (!firstConnection) { if (app.debug) console.log('\x1b[32m%s\x1b[0m', 'PearSystem started'); firstConnection = socket; } socket.on('message', (message) => { if (globalThis.___PearDebug) console.log('Received message:', Buffer(message).toString()); const data = JSON.parse(message); if (data.action === false) return; if (data.action === 'init') { connections[data.newid] = socket; } AsyncPromieses[data?.id]?.resolve(data); delete AsyncPromieses[data?.id]; }); }); server.listen(app.port || 8191, () => { if (app.debug) console.log('\x1b[33m%s\x1b[0m', `Starting PearSystem`); }); exec(`"${app.browserPath}" ${args.join(' ')}`, (error, stdout, stderr) => { if (error) { console.error(`Error executing browser: ${error.message}`); return; } if (stderr) { wss.close(); server.close(); return; } }); app.webserver = server; app.wss = wss; let id = 0; const AsyncPromieses = {}; async function asyncSystem(session, command, options = {}) { if (!command) command = session, session = null; return new Promise((resolve, reject) => { if (!options.goto) command.id = id++; else command.id = session; if (!session) firstConnection.send(JSON.stringify(command)); else if (connections[session]) { command.session = session; connections[session].send(JSON.stringify(command)); } AsyncPromieses[command.id] = { resolve, reject }; }) } // Generic method creator for standard operations const createMethod = (type) => (session) => async (...args) => { const command = { type }; let result; switch (type) { case 'goto': command.url = args[0]; result = await asyncSystem(session, command, { goto: true }); break; case 'setTimeout': result = new Promise((resolve) => setTimeout(resolve, args[0])); break; case 'click': command[0] = args[0]; // CSS selector for legacy click result = await asyncSystem(session, command); break; // Keyboard events case 'keypress': case 'keydown': case 'keyup': command.key = args[0]; command.selector = args[1]; // optional selector command.options = args[2] || {}; // optional options result = await asyncSystem(session, command); break; // Mouse events case 'leftclick': case 'rightclick': case 'middleclick': case 'dblclick': case 'mousedown': case 'mouseup': case 'mousemove': command.selector = args[0]; // CSS selector command.options = args[1] || {}; // optional options (x, y, etc.) result = await asyncSystem(session, command); break; case 'scroll': command.selector = args[0]; // optional selector (if null, scrolls window) command.options = args[1] || {}; // { x, y } result = await asyncSystem(session, command); break; case 'type': case 'directType': command.selector = args[0]; // CSS selector command.text = args[1]; // Text to type result = await asyncSystem(session, command); break; case 'waitForSelector': command.selector = args[0]; // CSS selector const options = args[1] || {}; // Options object command.timeout = options.timeout !== undefined ? options.timeout : 30000; command.checkInterval = options.checkInterval || 100; result = await asyncSystem(session, command); break; case 'uploadFile': command.selector = args[0]; // File input selector command.filePath = args[1]; // File path to upload result = await asyncSystem(session, command); break; case 'getAttribute': command.selector = args[0]; // CSS selector command.attribute = args[1]; // Attribute name result = await asyncSystem(session, command); break; case 'querySelector': command.selector = args[0]; // CSS selector result = await asyncSystem(session, command); break; case 'getText': command.selector = args[0]; // CSS selector result = await asyncSystem(session, command); break; case 'shadowClick': command.selector = args[0]; command.shadowSelector = args[1]; result = await asyncSystem(session, command); break; case 'shadowQuerySelector': command.selector = args[0]; command.shadowSelector = args[1]; result = await asyncSystem(session, command); break; case 'shadowGetAttribute': command.selector = args[0]; command.shadowSelector = args[1]; command.attribute = args[2]; result = await asyncSystem(session, command); break; case 'evaluate': command.func = args[0]; command.args = args[1] || []; result = await asyncSystem(session, command); break; } if (type === "screenshot" && result.screenshot) { return Buffer.from(result.screenshot.split(',').pop(), 'base64'); } if (result[type] !== undefined) { return result[type]; } return result; }; app.newPage = async function () { const newTabData = (await asyncSystem({ action: 'newTab' })); const id = newTabData.newid; return { id, // Navigation goto: createMethod('goto')(id), url: createMethod('url')(id), reload: createMethod('reload')(id), close: createMethod('close')(id), // Content & Screenshots screenshot: createMethod('screenshot')(id), content: createMethod('content')(id), // Legacy click (maintained for compatibility) click: createMethod('click')(id), // Keyboard events keypress: createMethod('keypress')(id), keydown: createMethod('keydown')(id), keyup: createMethod('keyup')(id), // Mouse events leftclick: createMethod('leftclick')(id), rightclick: createMethod('rightclick')(id), middleclick: createMethod('middleclick')(id), dblclick: createMethod('dblclick')(id), mousedown: createMethod('mousedown')(id), mouseup: createMethod('mouseup')(id), mousemove: createMethod('mousemove')(id), // Scroll scroll: createMethod('scroll')(id), // Text input type: createMethod('type')(id), directType: createMethod('directType')(id), // Utility methods waitForSelector: createMethod('waitForSelector')(id), uploadFile: createMethod('uploadFile')(id), getAttribute: createMethod('getAttribute')(id), getText: createMethod('getText')(id), querySelector: createMethod('querySelector')(id), shadowClick: createMethod('shadowClick')(id), shadowQuerySelector: createMethod('shadowQuerySelector')(id), shadowGetAttribute: createMethod('shadowGetAttribute')(id), evaluate: createMethod('evaluate')(id), setTimeout: createMethod('setTimeout')(id), } } app.newTab = app.newPage app.setTimeout = (x) => new Promise((resolve) => setTimeout(resolve, x)); app.close = function () { wss.close(); server.close(); } await new Promise(resolve => { const interval = setInterval(() => { if (firstConnection) { clearInterval(interval); resolve(); } }, 100); }); return app }