UNPKG

@extra/recaptcha

Version:

A plugin for playwright & puppeteer to solve reCAPTCHAs and hCaptchas automatically.

239 lines 10 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.RecaptchaPlugin = exports.BuiltinSolutionProviders = void 0; const automation_extra_plugin_1 = require("automation-extra-plugin"); const content_hcaptcha_1 = require("./content-hcaptcha"); const content_recaptcha_1 = require("./content-recaptcha"); const TwoCaptcha = require("./provider/2captcha"); exports.BuiltinSolutionProviders = [ { id: TwoCaptcha.PROVIDER_ID, fn: TwoCaptcha.getSolutions, }, ]; /** * A plugin to automatically detect and solve reCAPTCHAs. * @noInheritDoc */ class RecaptchaPlugin extends automation_extra_plugin_1.AutomationExtraPlugin { constructor(opts) { super(opts); this.debug('Initialized', this.opts); } get defaults() { return { visualFeedback: true, throwOnError: false, }; } get contentScriptOpts() { const { visualFeedback } = this.opts; return { visualFeedback, }; } _generateContentScript(vendor, fn, data) { this.debug('_generateContentScript', vendor, fn, data); let scriptSource = content_recaptcha_1.RecaptchaContentScript.toString(); let scriptName = 'RecaptchaContentScript'; if (vendor === 'hcaptcha') { scriptSource = content_hcaptcha_1.HcaptchaContentScript.toString(); scriptName = 'HcaptchaContentScript'; } return `(async() => { const DATA = ${JSON.stringify(data || null)} const OPTS = ${JSON.stringify(this.contentScriptOpts)} ${scriptSource} const script = new ${scriptName}(OPTS, DATA) return script.${fn}() })()`; } async findRecaptchas(page) { this.debug('findRecaptchas'); // As this might be called very early while recaptcha is still loading // we add some extra waiting logic for developer convenience. const hasRecaptchaScriptTag = await page.$(`script[src*="/recaptcha/api.js"]`); this.debug('hasRecaptchaScriptTag', !!hasRecaptchaScriptTag); if (hasRecaptchaScriptTag) { this.debug('waitForRecaptchaClient - start', new Date()); // Testing for .clients is more robust then .count (some sites implement recaptcha in a broken way) await page.waitForFunction(` (function() { return Object.keys((window.___grecaptcha_cfg || {}).clients || {}).length })() `, { polling: 200, timeout: 10 * 1000 }); this.debug('waitForRecaptchaClient - end', new Date()); // used as timer } const hasHcaptchaScriptTag = await page.$(`script[src*="//hcaptcha.com/1/api.js"]`); this.debug('hasHcaptchaScriptTag', !!hasHcaptchaScriptTag); if (hasHcaptchaScriptTag) { this.debug('wait:hasHcaptchaScriptTag - start', new Date()); await page.waitForFunction(` (function() { return window.hcaptcha })() `, { polling: 200, timeout: 10 * 1000 }); this.debug('wait:hasHcaptchaScriptTag - end', new Date()); // used as timer } // Even without a recaptcha script tag we're trying, just in case. const resultRecaptcha = (await page.evaluate(this._generateContentScript('recaptcha', 'findRecaptchas'))); const resultHcaptcha = (await page.evaluate(this._generateContentScript('hcaptcha', 'findRecaptchas'))); const response = { captchas: [...resultRecaptcha.captchas, ...resultHcaptcha.captchas], error: resultRecaptcha.error || resultHcaptcha.error, }; this.debug('findRecaptchas', response); if (this.opts.throwOnError && response.error) { throw new Error(response.error); } return response; } async getRecaptchaSolutions(captchas, provider) { this.debug('getRecaptchaSolutions'); provider = provider || this.opts.provider; if (!provider || (!provider.token && !provider.fn) || (provider.token && provider.token === 'XXXXXXX' && !provider.fn)) { throw new Error('Please provide a solution provider to the plugin.'); } let fn = provider.fn; if (!fn) { const builtinProvider = exports.BuiltinSolutionProviders.find((p) => p.id === (provider || {}).id); if (!builtinProvider || !builtinProvider.fn) { throw new Error(`Cannot find builtin provider with id '${provider.id}'.`); } fn = builtinProvider.fn; } const response = await fn.call(this, captchas, provider.token); response.error = response.error || response.solutions.find((s) => !!s.error); this.debug('getRecaptchaSolutions', response); if (response && response.error) { console.warn('PuppeteerExtraPluginRecaptcha: An error occured during "getRecaptchaSolutions":', response.error); } if (this.opts.throwOnError && response.error) { throw new Error(response.error); } return response; } async enterRecaptchaSolutions(page, solutions) { this.debug('enterRecaptchaSolutions'); const hasRecaptcha = !!solutions.find((s) => s._vendor === 'recaptcha'); const solvedRecaptcha = hasRecaptcha ? (await page.evaluate(this._generateContentScript('recaptcha', 'enterRecaptchaSolutions', { solutions, }))) : { solved: [] }; const hasHcaptcha = !!solutions.find((s) => s._vendor === 'hcaptcha'); const solvedHcaptcha = hasHcaptcha ? (await page.evaluate(this._generateContentScript('hcaptcha', 'enterRecaptchaSolutions', { solutions, }))) : { solved: [] }; const response = { solved: [...solvedRecaptcha.solved, ...solvedHcaptcha.solved], error: solvedRecaptcha.error || solvedHcaptcha.error, }; response.error = response.error || response.solved.find((s) => !!s.error); this.debug('enterRecaptchaSolutions', response); if (this.opts.throwOnError && response.error) { throw new Error(response.error); } return response; } async solveRecaptchas(page) { this.debug('solveRecaptchas'); const response = { captchas: [], solutions: [], solved: [], error: null, }; try { // If `this.opts.throwOnError` is set any of the // following will throw and abort execution. const { captchas, error: captchasError } = await this.findRecaptchas(page); response.captchas = captchas; if (captchas.length) { const { solutions, error: solutionsError, } = await this.getRecaptchaSolutions(response.captchas); response.solutions = solutions; const { solved, error: solvedError, } = await this.enterRecaptchaSolutions(page, response.solutions); response.solved = solved; response.error = captchasError || solutionsError || solvedError; } } catch (error) { response.error = error.toString(); } this.debug('solveRecaptchas', response); const fatalError = response.error && response.error.includes('Please provide a solution provider to the plugin'); if ((this.opts.throwOnError || fatalError) && response.error) { throw new Error(response.error); } return response; } _addCustomMethods(prop) { this.debug('_addCustomMethods', prop.url()); prop.findRecaptchas = async () => this.findRecaptchas(prop); prop.getRecaptchaSolutions = async (captchas, provider) => this.getRecaptchaSolutions(captchas, provider); prop.enterRecaptchaSolutions = async (solutions) => this.enterRecaptchaSolutions(prop, solutions); // Add convenience methods that wraps all others prop.solveRecaptchas = async () => this.solveRecaptchas(prop); } async beforeContext(options, browser) { if (!this.env.isPuppeteerBrowser(browser)) { return; } options.bypassCSP = true; } async onPageCreated(page) { this.debug('onPageCreated', page.url()); if (this.env.isPuppeteerPage(page)) { // Make sure we can run our content script await page.setBypassCSP(true); } // Add custom page methods this._addCustomMethods(page); // Add custom methods to potential frames as well page.on('frameattached', (frame) => { if (!frame) return; this._addCustomMethods(frame); }); } _addCustomMethodsToPages(pages) { this.debug('_addCustomMethodsToPages', pages.length); for (const page of pages) { this._addCustomMethods(page); for (const frame of page.mainFrame().childFrames()) { this._addCustomMethods(frame); } } } /** Add additions to already existing pages and frames */ async onBrowser(browser) { if (this.env.isPuppeteerBrowser(browser)) { const pages = await browser.pages(); this._addCustomMethodsToPages(pages); return; } if (this.env.isPlaywrightBrowser(browser)) { const pages = []; for (const context of browser.contexts()) { context.pages().forEach((p) => pages.push(p)); } this._addCustomMethodsToPages(pages); return; } } } exports.RecaptchaPlugin = RecaptchaPlugin; RecaptchaPlugin.id = 'recaptcha'; /** Default export, RecaptchaPlugin */ const defaultExport = (options) => { return new RecaptchaPlugin(options || {}); }; exports.default = defaultExport; //# sourceMappingURL=index.js.map