index.js
function onCaptcha (options, response, body) {
  const callback = options.callback;
  // UDF that has the responsibility of returning control back to cloudscraper
  const handler = options.onCaptcha;
  // The form data to send back to Cloudflare
  const payload = { /* s, g-re-captcha-response */ };

  let cause;
  let match;

  match = body.match(/<form(?: [^<>]*)? id=["']?challenge-form['"]?(?: [^<>]*)?>([\S\s]*?)<\/form>/);
  if (!match) {
    cause = 'Challenge form extraction failed';
    return callback(new ParserError(cause, options, response));
  }

  // Defining response.challengeForm for debugging purposes
  const form = response.challengeForm = match[1];

  match = form.match(/\/recaptcha\/api\/fallback\?k=([^\s"'<>]*)/);
  if (!match) {
    // The site key wasn't inside the form so search the entire document
    match = body.match(/data-sitekey=["']?([^\s"'<>]*)/);
    if (!match) {
      cause = 'Unable to find the reCAPTCHA site key';
      return callback(new ParserError(cause, options, response));
    }
  }

  // Everything that is needed to solve the reCAPTCHA
  response.captcha = {
    url: response.request.uri.href,
    siteKey: match[1],
    form: payload
  };

  // Adding formData
  match = form.match(/<input(?: [^<>]*)? name=[^<>]+>/g);
  if (!match) {
    cause = 'Challenge form is missing inputs';
    return callback(new ParserError(cause, options, response));
  }

  const inputs = match;
  // Only adding inputs that have both a name and value defined
  for (let name, value, i = 0; i < inputs.length; i++) {
    name = inputs[i].match(/name=["']?([^\s"'<>]*)/);
    if (name) {
      value = inputs[i].match(/value=["']?([^\s"'<>]*)/);
      if (value) {
        payload[name[1]] = value[1];
      }
    }
  }

  // Sanity check
  if (!payload['s']) {
    cause = 'Challenge form is missing secret input';
    return callback(new ParserError(cause, options, response));
  }

  // The callback used to green light form submission
  const submit = function (error) {
    if (error) {
      // Pass an user defined error back to the original request call
      return callback(new CaptchaError(error, options, response));
    }

    onSubmitCaptcha(options, response, body);
  };

  // This seems like an okay-ish API (fewer arguments to the handler)
  response.captcha.submit = submit;

  // We're handing control over to the user now.
  const thenable = handler(options, response, body);
  // Handle the case where the user returns a promise
  if (thenable && typeof thenable.then === 'function') {
    // eslint-disable-next-line promise/catch-or-return
    thenable.then(submit, function (error) {
      if (!error) {
        // The user broke their promise with a falsy error
        submit(new Error('Falsy error'));
      } else {
        submit(error);
      }
    });
  }
}
function onCaptcha (options, response, body) {
  const callback = options.callback;
  // UDF that has the responsibility of returning control back to cloudscraper
  const handler = options.onCaptcha;
  // The form data to send back to Cloudflare
  const payload = { /* s, g-re-captcha-response */ };

  let cause;
  let match;

  match = body.match(/<form(?: [^<>]*)? id=["']?challenge-form['"]?(?: [^<>]*)?>([\S\s]*?)<\/form>/);
  if (!match) {
    cause = 'Challenge form extraction failed';
    return callback(new ParserError(cause, options, response));
  }

  // Defining response.challengeForm for debugging purposes
  const form = response.challengeForm = match[1];

  match = form.match(/\/recaptcha\/api\/fallback\?k=([^\s"'<>]*)/);
  if (!match) {
    // The site key wasn't inside the form so search the entire document
    match = body.match(/data-sitekey=["']?([^\s"'<>]*)/);
    if (!match) {
      cause = 'Unable to find the reCAPTCHA site key';
      return callback(new ParserError(cause, options, response));
    }
  }

  // Everything that is needed to solve the reCAPTCHA
  response.captcha = {
    url: response.request.uri.href,
    siteKey: match[1],
    form: payload
  };

  // Adding formData
  match = form.match(/<input(?: [^<>]*)? name=[^<>]+>/g);
  if (!match) {
    cause = 'Challenge form is missing inputs';
    return callback(new ParserError(cause, options, response));
  }

  const inputs = match;
  // Only adding inputs that have both a name and value defined
  for (let name, value, i = 0; i < inputs.length; i++) {
    name = inputs[i].match(/name=["']?([^\s"'<>]*)/);
    if (name) {
      value = inputs[i].match(/value=["']?([^\s"'<>]*)/);
      if (value) {
        payload[name[1]] = value[1];
      }
    }
  }

  // Sanity check
  if (!payload['s']) {
    cause = 'Challenge form is missing secret input';
    return callback(new ParserError(cause, options, response));
  }

  // The callback used to green light form submission
  const submit = function (error) {
    if (error) {
      // Pass an user defined error back to the original request call
      return callback(new CaptchaError(error, options, response));
    }

    onSubmitCaptcha(options, response, body);
  };

  // This seems like an okay-ish API (fewer arguments to the handler)
  response.captcha.submit = submit;

  // We're handing control over to the user now.
  const thenable = handler(options, response, body);
  // Handle the case where the user returns a promise
  if (thenable && typeof thenable.then === 'function') {
    // eslint-disable-next-line promise/catch-or-return
    thenable.then(submit, function (error) {
      if (!error) {
        // The user broke their promise with a falsy error
        submit(new Error('Falsy error'));
      } else {
        submit(error);
      }
    });
  }
}
Parses the reCAPTCHA form and hands control over to the user