diff --git a/.github/workflows/label.yml b/.github/workflows/label.yml index 4cd034a6e..849c074b2 100644 --- a/.github/workflows/label.yml +++ b/.github/workflows/label.yml @@ -5,15 +5,16 @@ # file with configuration. For more information, see: # https://github.com/actions/labeler -name: "Pull Request Labeler" +name: 'Pull Request Labeler' on: -- pull_request_target + - pull_request_target jobs: triage: runs-on: ubuntu-latest steps: - - uses: actions/labeler@main - with: - repo-token: "${{ secrets.GITHUB_TOKEN }}" - sync-labels: true \ No newline at end of file + - uses: actions/labeler@main + with: + repo-token: '${{ secrets.GITHUB_TOKEN }}' + sync-labels: true + diff --git a/packages/puppeteer-extra-plugin-recaptcha/src/provider/capMonster-api.ts b/packages/puppeteer-extra-plugin-recaptcha/src/provider/capMonster-api.ts new file mode 100644 index 000000000..9ae793b7c --- /dev/null +++ b/packages/puppeteer-extra-plugin-recaptcha/src/provider/capMonster-api.ts @@ -0,0 +1,185 @@ +// TODO: Create our own API wrapper + +var https = require('https') +var url = require('url') + +var apiKey +var apiUrl = 'https://api.capmonster.cloud' +var apiInUrl = 'http://api.capmonster.cloud/in.php' +//var apigetTaskUrl = 'https://api.capmonster.cloud/getTaskResult' +// var apiMethod = 'base64' +// var SOFT_ID = '2589' + +var defaultOptions = { + pollingInterval: 40000, + retries: 4, +} + +function pollCaptcha(captchaId, options, invalid, callback) { + invalid = invalid.bind({ options: options, captchaId: captchaId }) + var intervalId = setInterval(function () { + // var httpsRequestOptions = url.parse( + // apiResUrl + + // '?action=get&soft_id=' + + // SOFT_ID + + // '&key=' + + // apiKey + + // '&id=' + + // captchaId + // ) + + var httpsRequestOptions = { + method: 'POST', + hostname: apiUrl, + path: '/getTaskResult', + headers: { + clientKey: apiKey, + taskId: captchaId, + nocache: 1, + }, + } + var request = https.request(httpsRequestOptions, function (response) { + var body = '' + + response.on('data', function (chunk) { + body += chunk + }) + + response.on('end', function () { + const res = JSON.parse(body) + if (res.status === 'processing') { + return + } + + clearInterval(intervalId) + + // var result = body // .split('|') + if (res.status !== 'ready') { + callback(res) // error + } else { + callback( + null, + { + id: captchaId, + text: res.solution.gRecaptchaResponse, + }, + invalid + ) + } + callback = function () {} // prevent the callback from being called more than once, if multiple https requests are open at the same time. + }) + }) + request.on('error', function (e) { + request.destroy() + callback(e) + }) + request.end() + }, options.pollingInterval || defaultOptions.pollingInterval) +} + +export const setApiKey = function (key) { + apiKey = key +} + +export const decodeReCaptcha = function ( + captchaMethod, + captcha, + pageUrl, + extraData, + options, + callback +) { + if (!callback) { + callback = options + options = defaultOptions + } + var httpsRequestOptions = url.parse(apiInUrl) + httpsRequestOptions.method = 'POST' + + var postData = { + key: apiKey, + method: captchaMethod, + pageURL: pageUrl, + ...extraData, + } + if (captchaMethod === 'userrecaptcha') { + postData.googlekey = captcha + } + if (captchaMethod === 'hcaptcha') { + postData.sitekey = captcha + } + postData.nocache = 1 + + var request = https.request(httpsRequestOptions, function (response) { + var body = '' + + response.on('data', function (chunk) { + body += chunk + }) + + response.on('end', function () { + var result = JSON.parse(body) + if (result.errorId !== 0) { + return callback(result.errorCode) + } + + pollCaptcha( + result.taskId, + options, + function (error) { + var callbackToInitialCallback = callback + + report(this.captchaId) + + if (error) { + return callbackToInitialCallback('CAPTCHA_FAILED') + } + + if (!this.options.retries) { + this.options.retries = defaultOptions.retries + } + if (this.options.retries > 1) { + this.options.retries = this.options.retries - 1 + decodeReCaptcha( + captchaMethod, + captcha, + pageUrl, + extraData, + this.options, + callback + ) + } else { + callbackToInitialCallback('CAPTCHA_FAILED_TOO_MANY_TIMES') + } + }, + callback + ) + }) + }) + request.on('error', function (e) { + request.destroy() + callback(e) + }) + request.write(postData) + request.end() +} + +export const report = function (captchaId) { + var reportUrl = + apiInUrl + + '?action=reportbad&soft_id=' + + '&key=' + + apiKey + + '&id=' + + captchaId + var options = url.parse(reportUrl) + + var request = https.request(options, function (response) { + // var body = '' + // response.on('data', function(chunk) { + // body += chunk + // }) + // response.on('end', function() {}) + }) + request.end() +} diff --git a/packages/puppeteer-extra-plugin-recaptcha/src/provider/capmonster.ts b/packages/puppeteer-extra-plugin-recaptcha/src/provider/capmonster.ts new file mode 100644 index 000000000..a6adf21b8 --- /dev/null +++ b/packages/puppeteer-extra-plugin-recaptcha/src/provider/capmonster.ts @@ -0,0 +1,133 @@ +export const PROVIDER_ID = 'capmonster' + +import * as types from '../types' + +export interface CapmonsterProviderOpts { + url: string + token: string + pollingInterval?: number +} + +import Debug from 'debug' +const debug = Debug(`puppeteer-extra-plugin:recaptcha:${PROVIDER_ID}`) + +import * as solver from './capMonster-api' + +const secondsBetweenDates = (before: Date, after: Date) => + (after.getTime() - before.getTime()) / 1000 + +export interface DecodeRecaptchaAsyncResult { + err?: any + result?: any + invalid?: any +} + +export interface TwoCaptchaProviderOpts { + useEnterpriseFlag?: boolean + useActionValue?: boolean +} + +const providerOptsDefaults: TwoCaptchaProviderOpts = { + useEnterpriseFlag: false, // Seems to make solving chance worse? + useActionValue: true, +} + +async function decodeRecaptchaAsync( + token: string, + vendor: types.CaptchaVendor, + sitekey: string, + url: string, + extraData: any, + opts = { pollingInterval: 2000 } +): Promise { + return new Promise((resolve) => { + const cb = (err: any, result: any, invalid: any) => + resolve({ err, result, invalid }) + try { + solver.setApiKey(token) + + let method = 'userrecaptcha' + if (vendor === 'hcaptcha') { + method = 'hcaptcha' + } + solver.decodeReCaptcha(method, sitekey, url, extraData, opts, cb) + } catch (error) { + return resolve({ err: error }) + } + }) +} + +export async function getSolutions( + captchas: types.CaptchaInfo[] = [], + token: string = '', + opts: TwoCaptchaProviderOpts = {} +): Promise { + opts = { ...providerOptsDefaults, ...opts } + const solutions = await Promise.all( + captchas.map((c) => getSolution(c, token, opts)) + ) + return { solutions, error: solutions.find((s) => !!s.error) } +} + +async function getSolution( + captcha: types.CaptchaInfo, + token: string, + opts: TwoCaptchaProviderOpts +): Promise { + const solution: types.CaptchaSolution = { + _vendor: captcha._vendor, + provider: PROVIDER_ID, + } + try { + if (!captcha || !captcha.sitekey || !captcha.url || !captcha.id) { + throw new Error('Missing data in captcha') + } + solution.id = captcha.id + solution.requestAt = new Date() + debug('Requesting solution..', solution) + const extraData = {} + if (captcha.s) { + extraData['data-s'] = captcha.s // google site specific property + } + if (opts.useActionValue && captcha.action) { + extraData['action'] = captcha.action // Optional v3/enterprise action + } + if (opts.useEnterpriseFlag && captcha.isEnterprise) { + extraData['enterprise'] = 1 + } + + if ( + process.env['CAPMONSTER_PROXY_TYPE'] && + process.env['CAPMONSTER_PROXY_ADDRESS'] + ) { + extraData['proxytype'] = + process.env['CAPMONSTER_PROXY_TYPE'].toUpperCase() + extraData['proxy'] = process.env['CAPMONSTER_PROXY_ADDRESS'] + } + + const { err, result, invalid } = await decodeRecaptchaAsync( + token, + captcha._vendor, + captcha.sitekey, + captcha.url, + extraData + ) + debug('Got response', { err, result, invalid }) + if (err) throw new Error(`${PROVIDER_ID} error: ${err}`) + if (!result || !result.text || !result.id) { + throw new Error(`${PROVIDER_ID} error: Missing response data: ${result}`) + } + solution.providerCaptchaId = result.id + solution.text = result.text + solution.responseAt = new Date() + solution.hasSolution = !!solution.text + solution.duration = secondsBetweenDates( + solution.requestAt, + solution.responseAt + ) + } catch (error) { + debug('Error', error) + solution.error = error.toString() + } + return solution +}