@extra/recaptcha
Version:
A plugin for playwright & puppeteer to solve reCAPTCHAs and hCaptchas automatically.
369 lines (366 loc) • 13.7 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.RecaptchaContentScript = exports.ContentScriptDefaultData = exports.ContentScriptDefaultOpts = void 0;
exports.ContentScriptDefaultOpts = {
visualFeedback: true,
};
exports.ContentScriptDefaultData = {
solutions: [],
};
/**
* Content script for Recaptcha handling (runs in browser context)
* @note External modules are not supported here (due to content script isolation)
*/
class RecaptchaContentScript {
constructor(opts = exports.ContentScriptDefaultOpts, data = exports.ContentScriptDefaultData) {
// Poor mans _.pluck
this._pick = (props) => (o) => props.reduce((a, e) => (Object.assign(Object.assign({}, a), { [e]: o[e] })), {});
// make sure the element is visible - this is equivalent to jquery's is(':visible')
this._isVisible = (elem) => !!(elem.offsetWidth ||
elem.offsetHeight ||
(typeof elem.getClientRects === 'function' &&
elem.getClientRects().length));
this.opts = opts;
this.data = data;
}
// Recaptcha client is a nested, circular object with object keys that seem generated
// We flatten that object a couple of levels deep for easy access to certain keys we're interested in.
_flattenObject(item, levels = 2, ignoreHTML = true) {
const isObject = (x) => x && typeof x === 'object';
const isHTML = (x) => x && x instanceof HTMLElement;
let newObj = {};
for (let i = 0; i < levels; i++) {
item = Object.keys(newObj).length ? newObj : item;
Object.keys(item).forEach((key) => {
if (ignoreHTML && isHTML(item[key]))
return;
if (isObject(item[key])) {
Object.keys(item[key]).forEach((innerKey) => {
if (ignoreHTML && isHTML(item[key][innerKey]))
return;
const keyName = isObject(item[key][innerKey])
? `obj_${key}_${innerKey}`
: `${innerKey}`;
newObj[keyName] = item[key][innerKey];
});
}
else {
newObj[key] = item[key];
}
});
}
return newObj;
}
// Helper function to return an object based on a well known value
_getKeyByValue(object, value) {
return Object.keys(object).find((key) => object[key] === value);
}
async _waitUntilDocumentReady() {
return new Promise(function (resolve) {
if (!document || !window)
return resolve(null);
const loadedAlready = /^loaded|^i|^c/.test(document.readyState);
if (loadedAlready)
return resolve(null);
function onReady() {
resolve(null);
document.removeEventListener('DOMContentLoaded', onReady);
window.removeEventListener('load', onReady);
}
document.addEventListener('DOMContentLoaded', onReady);
window.addEventListener('load', onReady);
});
}
_paintCaptchaBusy($iframe) {
try {
if (this.opts.visualFeedback) {
$iframe.style.filter = `opacity(60%) hue-rotate(400deg)`; // violet
}
}
catch (error) {
// noop
}
return $iframe;
}
_paintCaptchaSolved($iframe) {
try {
if (this.opts.visualFeedback) {
$iframe.style.filter = `opacity(60%) hue-rotate(230deg)`; // green
}
}
catch (error) {
// noop
}
return $iframe;
}
_findVisibleIframeNodes() {
return Array.from(document.querySelectorAll(`iframe[src^='https://www.google.com/recaptcha/api2/anchor'][name^="a-"]` +
', ' +
`iframe[src^='https://www.google.com/recaptcha/enterprise/anchor'][name^="a-"]`));
}
_findVisibleIframeNodeById(id) {
return document.querySelector(`iframe[src^='https://www.google.com/recaptcha/api2/anchor'][name^="a-${id || ''}"]` +
', ' +
`iframe[src^='https://www.google.com/recaptcha/enterprise/anchor'][name^="a-${id || ''}"]`);
}
_hideChallengeWindowIfPresent(id) {
let frame = document.querySelector(`iframe[src^='https://www.google.com/recaptcha/api2/bframe'][name^="c-${id || ''}"]` +
', ' +
`iframe[src^='https://www.google.com/recaptcha/enterprise/bframe'][name^="c-${id || ''}"]`);
if (!frame) {
return;
}
while (frame &&
frame.parentElement &&
frame.parentElement !== document.body) {
frame = frame.parentElement;
}
if (frame) {
frame.style.visibility = 'hidden';
}
}
getClients() {
// Bail out early if there's no indication of recaptchas
if (!window || !window.__google_recaptcha_client)
return;
if (!window.___grecaptcha_cfg || !window.___grecaptcha_cfg.clients) {
return;
}
if (!Object.keys(window.___grecaptcha_cfg.clients).length)
return;
return window.___grecaptcha_cfg.clients;
}
getVisibleIframesIds() {
// Find all regular visible recaptcha boxes through their iframes
return this._findVisibleIframeNodes()
.filter(($f) => this._isVisible($f))
.map(($f) => this._paintCaptchaBusy($f))
.filter(($f) => $f && $f.getAttribute('name'))
.map(($f) => $f.getAttribute('name') || '') // a-841543e13666
.map((rawId) => rawId.split('-').slice(-1)[0] // a-841543e13666 => 841543e13666
)
.filter((id) => id);
}
getInvisibleIframesIds() {
// Find all invisible recaptcha boxes through their iframes (only the ones with an active challenge window)
return this._findVisibleIframeNodes()
.filter(($f) => $f && $f.getAttribute('name'))
.map(($f) => $f.getAttribute('name') || '') // a-841543e13666
.map((rawId) => rawId.split('-').slice(-1)[0] // a-841543e13666 => 841543e13666
)
.filter((id) => id)
.filter((id) => document.querySelectorAll(`iframe[src^='https://www.google.com/recaptcha/api2/bframe'][name^="c-${id || ''}"]` +
', ' +
`iframe[src^='https://www.google.com/recaptcha/enterprise/bframe'][name^="c-${id || ''}"]`).length);
}
getIframesIds() {
// Find all recaptcha boxes through their iframes, check for invisible ones as fallback
const results = [
...this.getVisibleIframesIds(),
...this.getInvisibleIframesIds(),
];
// Deduplicate results by using the unique id as key
return [...new Map(results.map((x) => [x.id, x])).values()];
}
getResponseInputById(id) {
if (!id)
return;
const $iframe = this._findVisibleIframeNodeById(id);
if (!$iframe)
return;
const $parentForm = $iframe.closest(`form`);
if ($parentForm) {
return $parentForm.querySelector(`[name='g-recaptcha-response']`);
}
// Not all reCAPTCHAs are in forms
// https://github.com/berstend/puppeteer-extra/issues/57
if (document && document.body) {
return document.body.querySelector(`[name='g-recaptcha-response']`);
}
}
getClientById(id) {
if (!id)
return;
const clients = this.getClients();
// Lookup captcha "client" info using extracted id
let client = Object.values(clients || {})
.filter((obj) => this._getKeyByValue(obj, id))
.shift(); // returns first entry in array or undefined
if (!client)
return;
client = this._flattenObject(client);
client.widgetId = client.id;
client.id = id;
return client;
}
extractInfoFromClient(client) {
if (!client)
return;
const info = this._pick(['sitekey', 'callback'])(client);
if (!info.sitekey)
return;
info._vendor = 'recaptcha';
info.id = client.id;
info.s = client.s; // google site specific
info.widgetId = client.widgetId;
info.display = this._pick([
'size',
'top',
'left',
'width',
'height',
'theme',
])(client);
// callbacks can be strings or funtion refs
if (info.callback && typeof info.callback === 'function') {
info.callback = info.callback.name || 'anonymous';
}
if (document && document.location)
info.url = document.location.href;
return info;
}
async findRecaptchas() {
const result = {
captchas: [],
error: null,
};
try {
await this._waitUntilDocumentReady();
const clients = this.getClients();
if (!clients)
return result;
result.captchas = this.getIframesIds()
.map((id) => this.getClientById(id))
.map((client) => this.extractInfoFromClient(client))
.map((info) => {
if (!info)
return;
const $input = this.getResponseInputById(info.id);
info.hasResponseElement = !!$input;
return info;
})
.filter((info) => info);
}
catch (error) {
result.error = error;
return result;
}
return result;
}
async enterRecaptchaSolutions() {
const result = {
solved: [],
error: null,
};
try {
await this._waitUntilDocumentReady();
const clients = this.getClients();
if (!clients) {
result.error = 'No recaptchas found';
return result;
}
const solutions = this.data.solutions;
if (!solutions || !solutions.length) {
result.error = 'No solutions provided';
return result;
}
result.solved = this.getIframesIds()
.map((id) => this.getClientById(id))
.map((client) => {
const solved = {
_vendor: 'recaptcha',
id: client.id,
responseElement: false,
responseCallback: false,
};
const $iframe = this._findVisibleIframeNodeById(solved.id);
if (!$iframe) {
solved.error = `Iframe not found for id '${solved.id}'`;
return solved;
}
const solution = solutions.find((s) => s.id === solved.id);
if (!solution || !solution.text) {
solved.error = `Solution not found for id '${solved.id}'`;
return solved;
}
// Hide if present challenge window
this._hideChallengeWindowIfPresent(solved.id);
// Enter solution in response textarea
const $input = this.getResponseInputById(solved.id);
if ($input) {
$input.innerHTML = solution.text;
solved.responseElement = true;
}
// Enter solution in optional callback
if (client.callback) {
try {
if (typeof client.callback === 'function') {
client.callback.call(window, solution.text);
}
else {
eval(client.callback).call(window, solution.text); // tslint:disable-line
}
solved.responseCallback = true;
}
catch (error) {
solved.error = error;
}
}
// Finishing up
solved.isSolved = solved.responseCallback || solved.responseElement;
solved.solvedAt = new Date();
this._paintCaptchaSolved($iframe);
return solved;
});
}
catch (error) {
result.error = error;
return result;
}
return result;
}
}
exports.RecaptchaContentScript = RecaptchaContentScript;
/*
// Example data
{
"captchas": [{
"sitekey": "6LdAUwoUAAAAAH44X453L0tUWOvx11XXXXXXXX",
"id": "lnfy52r0cccc",
"widgetId": 0,
"display": {
"size": null,
"top": 23,
"left": 13,
"width": 28,
"height": 28,
"theme": null
},
"url": "https://example.com",
"hasResponseElement": true
}],
"error": null
}
{
"solutions": [{
"id": "lnfy52r0cccc",
"provider": "2captcha",
"providerCaptchaId": "61109548000",
"text": "03AF6jDqVSOVODT-wLKZ47U0UXz...",
"requestAt": "2019-02-09T18:30:43.587Z",
"responseAt": "2019-02-09T18:30:57.937Z"
}]
"error": null
}
{
"solved": [{
"id": "lnfy52r0cccc",
"responseElement": true,
"responseCallback": false,
"isSolved": true,
"solvedAt": {}
}]
"error": null
}
*/
//# sourceMappingURL=content-recaptcha.js.map