Skip to content

Instantly share code, notes, and snippets.

@prescience-data
Created April 15, 2021 09:12
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save prescience-data/c697b74b4b6b34d2c2aabdc76b39d468 to your computer and use it in GitHub Desktop.
Save prescience-data/c697b74b4b6b34d2c2aabdc76b39d468 to your computer and use it in GitHub Desktop.
HCaptcha Solver
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