Skip to content

Instantly share code, notes, and snippets.

@prescience-data
Created April 15, 2021 09:12
Show Gist options
  • Select an option

  • Save prescience-data/c697b74b4b6b34d2c2aabdc76b39d468 to your computer and use it in GitHub Desktop.

Select an option

Save prescience-data/c697b74b4b6b34d2c2aabdc76b39d468 to your computer and use it in GitHub Desktop.

Revisions

  1. prescience-data created this gist Apr 15, 2021.
    247 changes: 247 additions & 0 deletions hcaptcha.ts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,247 @@
    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...
    }