Solve captchas through the 2captcha API, not its extension
The two Tradeify automations clicked the 2captcha *extension's* solve button and waited for it to go green. That needed a second extension installed and signed in, put a real mouse click on a button whose markup is 2captcha's to change, and gave a run no way to tell "still solving" from "not installed". Replaced with one `solveCaptcha` step. No mouse: the extension reads the widget's sitekey out of the page, the dashboard buys a token from 2captcha, and the extension puts it in the response field and fires the page's own callback. captcha-hook.js is the part that earns its keep. A widget rendered programmatically puts nothing in the DOM — no sitekey, and no callback. Most pages only enable submit from that callback, so filling the hidden field alone leaves the page looking unsolved. The hook wraps turnstile/grecaptcha/hcaptcha `render` at document_start to hold on to what the page passed. Solves go through 2captcha's residential proxy. Its API rejects host names in proxyAddress, so na.proxy.2captcha.com is resolved to an IP first (cached); the gateway was confirmed to work addressed that way. Covers reCAPTCHA v2 (incl. invisible and Enterprise), hCaptcha and Turnstile. NOT reCAPTCHA v3: a v3 page carries an anchor iframe with k=, so it is detected as v2 and would be solved with the wrong task type. Everything else unsupported fails loudly instead. The API key and proxy credentials are hardcoded in the captcha route on request. Unlike the sqlite database, that file is in the repo, so they travel with it. TWOCAPTCHA_API_KEY / TWOCAPTCHA_PROXY and the settings rows still override; neither has a field on the Settings page. Runner VERSION and RUNNER_EXPECTED_VERSION both to 0.17.0 — they are a pair, and a mismatch shows as a red "restart your runner" banner that blames the wrong process. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
25289cb9b3
commit
159389be35
@@ -0,0 +1,69 @@
|
||||
/* AutoFirmer — captcha render hook.
|
||||
*
|
||||
* Runs in the page's MAIN world at document_start, before any captcha script
|
||||
* loads. It wraps `turnstile.render` / `grecaptcha.render` / `hcaptcha.render`
|
||||
* and records what the page passed to them on `window.__autofirmerCaptcha`.
|
||||
*
|
||||
* Why bother, when the sitekey is usually a `data-sitekey` attribute: a widget
|
||||
* rendered programmatically puts NOTHING in the DOM — no sitekey, and more
|
||||
* importantly no callback. The callback is the whole point. Filling the hidden
|
||||
* response field with a solved token does not by itself tell the page a token
|
||||
* arrived; the page only reacts when its own callback runs with it. Turnstile in
|
||||
* particular hands that callback straight to render() and never exposes it
|
||||
* anywhere else, so a hook installed before the widget renders is the only way
|
||||
* to hold on to it.
|
||||
*
|
||||
* Reads nothing and sends nothing. It only remembers arguments, in the page's
|
||||
* own world, for background.js to use when a token comes back from 2captcha.
|
||||
*/
|
||||
|
||||
(() => {
|
||||
const registry = (window.__autofirmerCaptcha = window.__autofirmerCaptcha || { widgets: [] });
|
||||
|
||||
/** Wrap `render` on a captcha API object, once. */
|
||||
function wrap(kind, api) {
|
||||
if (!api || typeof api !== 'object' || api.__autofirmerWrapped) return api;
|
||||
|
||||
if (typeof api.render === 'function') {
|
||||
const original = api.render.bind(api);
|
||||
api.render = function (container, params) {
|
||||
const widgetId = original(container, params);
|
||||
// `params` holds the sitekey, the callback, and for Turnstile the
|
||||
// action / cData / chlPageData that a solve has to be asked for.
|
||||
registry.widgets.push({ kind, params: params || {}, widgetId });
|
||||
return widgetId;
|
||||
};
|
||||
try {
|
||||
Object.defineProperty(api, '__autofirmerWrapped', { value: true });
|
||||
} catch { /* frozen API object — wrapping still took */ }
|
||||
}
|
||||
|
||||
// reCAPTCHA Enterprise is a second render() hanging off the same object.
|
||||
if (api.enterprise) wrap(kind, api.enterprise);
|
||||
return api;
|
||||
}
|
||||
|
||||
/** Wrap whatever the page eventually assigns to `window[name]`.
|
||||
*
|
||||
* The script that defines it loads after this one, so there is nothing to
|
||||
* wrap yet — the accessor waits for the assignment instead. It also catches
|
||||
* the reCAPTCHA pattern of replacing a `{ready}` placeholder later on. */
|
||||
function intercept(name, kind) {
|
||||
let current = wrap(kind, window[name]);
|
||||
try {
|
||||
Object.defineProperty(window, name, {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
get: () => current,
|
||||
set: (value) => { current = wrap(kind, value); },
|
||||
});
|
||||
} catch {
|
||||
// The page made the property non-configurable. Detection falls back
|
||||
// to the DOM, which covers every declaratively rendered widget.
|
||||
}
|
||||
}
|
||||
|
||||
intercept('turnstile', 'turnstile');
|
||||
intercept('grecaptcha', 'recaptcha');
|
||||
intercept('hcaptcha', 'hcaptcha');
|
||||
})();
|
||||
Reference in New Issue
Block a user