-
-
Save voxxit/40875206a5f15abf0641b4727b724af3 to your computer and use it in GitHub Desktop.
HCaptcha Solver
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { IncomingMessage, RequestListener, ServerResponse } from "http" | |
import { createServer, Server } from "https" | |
import puppeteer, { | |
Browser, | |
BrowserLaunchArgumentOptions, | |
Protocol | |
} from "puppeteer-core" | |
import { Page } from "./types" | |
import Cookie = Protocol.Network.Cookie | |
import CookieParam = Protocol.Network.CookieParam | |
export type Repeater<R> = () => Promise<R> | |
export interface AbstractAccount { | |
email: string | |
password: string | |
} | |
/** | |
* Define the primary hcaptcha login url. | |
* @type {string} | |
*/ | |
const HCAPTCHA_ENDPOINT: string = "https://dashboard.hcaptcha.com/login" | |
/** | |
* Typed errors. | |
*/ | |
export class AccountLoginError extends Error { | |
public constructor(err?: Error) { | |
super(`Failed to log into account. ${err?.message}`.trim()) | |
} | |
} | |
export class StatusTimeoutError extends Error { | |
public constructor() { | |
super(`Timeout while waiting for status.`) | |
} | |
} | |
export class MaxAttemptsError extends Error { | |
public constructor(attempts: number) { | |
super(`Reached max attempts (${attempts}) while waiting for response.`) | |
} | |
} | |
export class CookieStatusError extends Error { | |
public constructor(cookieStatus: string) { | |
super(`Error while resolving cookies: ${cookieStatus}`) | |
} | |
} | |
export class ReceivedChallengeError extends Error { | |
public constructor() { | |
super(`Challenge shown!`) | |
} | |
} | |
export class InvalidRangeError extends Error { | |
public constructor(min: number, max: number) { | |
super( | |
`Received invalid numerical range. Minimum value "${min}" was greater than maximum value "${max}".` | |
) | |
} | |
} | |
/** | |
* Provides a MersenneTwister pseudo-random generator. | |
* @type {MersenneTwister} | |
*/ | |
export const generator: MersenneTwister = new MersenneTwister() | |
/** | |
* Simple function repeater. | |
* | |
* @param {Repeater<R>} fn | |
* @param {number} count | |
* @return {Promise<void>} | |
*/ | |
export const repeat = async <R = unknown>( | |
fn: Repeater<R>, | |
count: number | |
): Promise<void> => { | |
if (count > 0) { | |
await fn() | |
await repeat(fn, count--) | |
} | |
} | |
/** | |
* Simple random number generator. | |
* | |
* @param {number} min | |
* @param {number} max | |
* @return {number} | |
*/ | |
export const rand = (min: number, max: number): number => { | |
if (min > max) { | |
throw new InvalidRangeError(min, max) | |
} | |
return Math.floor(generator.random() * (max - min + 1) + min) | |
} | |
/** | |
* Attempt to retrieve HCaptcha cookies. | |
* | |
* @param {Page} page | |
* @param {AbstractAccount} account | |
* @return {Promise<Protocol.Network.Cookie | undefined>} | |
*/ | |
export const getHCaptchaCookie = async ( | |
page: Page, | |
account: AbstractAccount | |
): Promise<Cookie | undefined> => { | |
await page.goto(HCAPTCHA_ENDPOINT) | |
try { | |
await page.waitForSelector(`[data-cy="input-email"]`, { timeout: 5000 }) | |
await repeat(() => page.keyboard.press("Tab", { delay: rand(20, 100) }), 5) | |
await page.keyboard.type(account.email) | |
await page.keyboard.press("Tab") | |
await page.keyboard.type(account.password, { delay: rand(5, 15) }) | |
await repeat(() => page.keyboard.press("Tab", { delay: rand(20, 100) }), 2) | |
await page.keyboard.press("Enter") | |
await page.waitForSelector(`[data-cy="setAccessibilityCookie"]`, { | |
timeout: 10000 | |
}) | |
} catch (err) { | |
throw new AccountLoginError(err) | |
} | |
await page.waitForTimeout(rand(3000, 3500)) | |
await page.click(`[data-cy="setAccessibilityCookie"]`) | |
try { | |
await page.waitForSelector(`[data-cy="fetchStatus"]`, { timeout: 10000 }) | |
} catch (e) { | |
throw new StatusTimeoutError() | |
} | |
const cookieStatus: string = await page.$eval( | |
`[data-cy="fetchStatus"]`, | |
({ textContent }) => { | |
return textContent || `` | |
} | |
) | |
if (cookieStatus !== "Cookie set.") { | |
throw new CookieStatusError(cookieStatus) | |
} | |
return (await page.cookies()).find( | |
(c: Cookie) => (c.name = "hc_accessibility") | |
) | |
} | |
/** | |
* Recursively wait for a response from the captcha solver. | |
* | |
* @param {Page} page | |
* @param {number} maxAttempts | |
* @return {Promise<string>} | |
*/ | |
export const waitForResponse = async ( | |
page: Page, | |
maxAttempts: number = 20 | |
): Promise<string> => { | |
const response: string = await page.$eval( | |
"[name=h-captcha-response]", | |
({ nodeValue }) => nodeValue || `` | |
) | |
const opacity: string = await page.evaluate(() => { | |
return Array.from(document.querySelectorAll("div"))[1].style.opacity | |
}) | |
if (opacity === "1") { | |
throw new ReceivedChallengeError() | |
} | |
if (response) { | |
return response | |
} | |
await page.waitForTimeout(rand(1000, 1500)) | |
if (maxAttempts > 0) { | |
return waitForResponse(page, maxAttempts--) | |
} else { | |
throw new MaxAttemptsError(maxAttempts) | |
} | |
} | |
/** | |
* Solve captchas on a specified account. | |
* | |
* @param {string} url | |
* @return {(page: Page, cookie: Protocol.Network.CookieParam) => Promise<string>} | |
*/ | |
export const accSolveHCaptcha = (url: string) => async ( | |
page: Page, | |
cookie: CookieParam | |
): Promise<string> => { | |
await page.setCookie(cookie) | |
await page.goto(url) | |
await page.waitForTimeout(rand(1000, 1200)) | |
await page.keyboard.press("Tab") | |
await page.waitForTimeout(rand(100, 300)) | |
await page.keyboard.press("Enter") | |
await page.waitForTimeout(rand(100, 300)) | |
await page.keyboard.press("Enter") | |
return waitForResponse(page) | |
} | |
/** | |
* Captcha server factory. | |
* | |
* @param {number} port | |
* @return {Server} | |
*/ | |
export const bootCaptchaServer = (port: number = 21337): Server => { | |
const onRequest: RequestListener = ( | |
req: IncomingMessage, | |
res: ServerResponse | |
) => { | |
console.log(req.url) | |
res.write(`<html> | |
<head> | |
<script src="https://hcaptcha.com/1/api.js" async defer></script> | |
</head> | |
<body> | |
<div class="h-captcha" data-sitekey="${process.env.HCAPTCHA_SITEKEY}"></div> | |
</body> | |
</html> | |
`) | |
} | |
const server: Server = createServer(onRequest) | |
server.listen(port) | |
return server | |
} | |
/** | |
* Entrypoint | |
* | |
* @param {string} url | |
* @return {Promise<void>} | |
*/ | |
export const run = async (url: string): Promise<void> => { | |
await bootCaptchaServer() | |
const launchOptions: BrowserLaunchArgumentOptions = { | |
headless: false, | |
args: [`--host-rules=MAP ${url} 127.0.0.1:21337`] | |
} | |
const browser: Browser = await puppeteer.launch(launchOptions) | |
// Do stuff... | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment