4chan/lib/captcha.php
2025-04-17 14:46:47 -05:00

686 lines
16 KiB
PHP

<?
require_once "lib/rpc.php";
require_once 'lib/ini.php';
load_ini_file('captcha_config.ini');
$recaptcha_public_key = RECAPTCHA_API_KEY_PUBLIC;
$recaptcha_private_key = RECAPTCHA_API_KEY_PRIVATE;
$hcaptcha_public_key = HCAPTCHA_API_KEY_PUBLIC;
$hcaptcha_private_key = HCAPTCHA_API_KEY_PRIVATE;
// Parameter formats and other checks must much the formatting in /captcha
function is_twister_captcha_valid($memcached, $ip, $userpwd, $board = '!', $thread_id = 0, &$unsolved_count = null) {
if (!$memcached) {
return false;
}
if (defined('TEST_BOARD') && TEST_BOARD) {
require_once 'lib/twister_captcha-test.php';
}
else {
require_once 'lib/twister_captcha.php';
}
if (!isset($_POST['t-challenge']) || !$_POST['t-challenge']) {
return false;
}
if (!isset($_POST['t-response']) || !$_POST['t-response']) {
return false;
}
if (strlen($_POST['t-response']) > 24) {
return false;
}
if (strlen($_POST['t-challenge']) > 255) {
return false;
}
// User agent
if (!isset($_SERVER['HTTP_USER_AGENT'])) {
$user_agent = '!';
}
else {
$user_agent = md5($_SERVER['HTTP_USER_AGENT']);
}
// Password
$password = '!';
if ($userpwd && !$userpwd->isNew()) {
$password = $userpwd->getPwd();
}
// ---
list($uniq_id, $challenge_hash) = explode('.', $_POST['t-challenge']);
$long_ip = ip2long($ip);
if (!$uniq_id || !$challenge_hash || !$long_ip) {
return false;
}
$challenge_key = "ch$long_ip";
$params = [$ip, $password, $user_agent, $board, $thread_id];
$response = TwisterCaptcha::normalizeReponseStr($_POST['t-response']);
$is_valid = TwisterCaptcha::verifyChallengeHash($challenge_hash, $uniq_id, $response, $params);
if ($is_valid) {
$active_uniq_id = $memcached->get($challenge_key);
// Delete challenge
$memcached->delete($challenge_key);
if (!$active_uniq_id || $uniq_id !== $active_uniq_id) {
return false;
}
// Return and decrement the unsolved session count
$us = decrement_twister_captcha_session($memcached, $ip, $unsolved_count !== null);
if ($unsolved_count !== null) {
$unsolved_count = $us;
}
return true;
}
// Delete challenge
$memcached->delete($challenge_key);
return false;
}
// Decrements the unsolved count by 2 and returns the old count
function decrement_twister_captcha_session($memcached, $ip, $return_old = true) {
if (!$memcached) {
return false;
}
$long_ip = ip2long($ip);
if (!$long_ip) {
return false;
}
$key = "us$long_ip";
if ($return_old) {
$val = $memcached->get($key);
if ($val === false) {
$val = 0;
}
}
else {
$val = 0;
}
$memcached->decrement($key, 2);
return $val;
}
// FIXME: The IP arg isn't used for now
function set_twister_captcha_credits($memcached, $ip, $userpwd, $current_time) {
if (!$memcached || !$userpwd) {
return false;
}
$current_time = (int)$current_time;
if ($current_time <= 0) {
return false;
}
//$long_ip = ip2long($ip);
//if (!$long_ip) {
//return false;
//}
$credits = 0;
// Config
// Stage 1 should match the check in use_twister_captcha_credit()
// and captcha.php for optimisation purposes
// Stage 1
$noop_known_ttl_1 = 4320; // required user lifetime (3 days, in minutes)
$noop_post_count_1 = 5; // required post count
$noop_credits_1 = 1; // credits given
$noop_duration_1 = 3600; // duration of the credits (1 hour, in seconds)
// Stage 2
$noop_known_ttl_2 = 21600; // 15 days
$noop_post_count_2 = 20;
$noop_credits_2 = 2;
$noop_duration_2 = 7200; // 2 hours
// Stage 3
$noop_known_ttl_3 = 129600; // 90 days
$noop_post_count_3 = 100;
$noop_credits_3 = 3;
$noop_duration_3 = 10800; // 3 hours
// ---
// The IP changed too recently
if ($userpwd->ipLifetime() < 60) {
return false;
}
// Stage 3
if ($userpwd->isUserKnown($noop_known_ttl_3) && $userpwd->postCount() >= $noop_post_count_3) {
$credits = $noop_credits_3;
$duration = $noop_duration_3;
}
// Stage 2
else if ($userpwd->isUserKnown($noop_known_ttl_2) && $userpwd->postCount() >= $noop_post_count_2) {
$credits = $noop_credits_2;
$duration = $noop_duration_2;
}
// Stage 1
else if ($userpwd->isUserKnown($noop_known_ttl_1) && $userpwd->postCount() >= $noop_post_count_1) {
$credits = $noop_credits_1;
$duration = $noop_duration_1;
}
else {
return false;
}
if (!$credits || $credits > 3) {
return false;
}
$expiration_ts = $current_time + $duration;
// Require no more than 5 actions in the past 8 minutes
/*
$query = <<<SQL
SELECT SQL_NO_CACHE COUNT(*) FROM user_actions
WHERE ip = $long_ip
AND time >= DATE_SUB(NOW(), INTERVAL 8 MINUTE)
SQL;
$res = mysql_global_call($query);
if (!$res) {
return false;
}
$count = (int)mysql_fetch_row($res)[0];
if ($count > 5) {
return false;
}
*/
// Set credits
$pwd = $userpwd->getPwd();
if (!$pwd) {
return false;
}
$key = "cr-$pwd";
$val = "$credits.$expiration_ts";
$res = $memcached->replace($key, $val, $expiration_ts);
if ($res === false) {
if ($memcached->getResultCode() === Memcached::RES_NOTSTORED) {
return $memcached->set($key, $val, $expiration_ts);
}
else {
return false;
}
}
return true;
}
// FIXME: The IP arg isn't used for now
function use_twister_captcha_credit($memcached, $ip, $userpwd) {
if (!$memcached || !$userpwd) {
return false;
}
//$long_ip = ip2long($ip);
//if (!$long_ip) {
//return false;
//}
// Must match the check in set_twister_captcha_credits()
$noop_known_ttl_1 = 4320; // required user lifetime (3 days, in minutes)
$noop_post_count_1 = 5; // required post count
if (!$userpwd->isUserKnown($noop_known_ttl_1) || $userpwd->postCount() < $noop_post_count_1) {
return false;
}
$pwd = $userpwd->getPwd();
if (!$pwd) {
return false;
}
$key = "cr-$pwd";
$credits = $memcached->get($key);
if ($credits === false) {
return false;
}
list($count, $ts) = explode('.', $credits);
$count = (int)$count;
$ts = (int)$ts;
// No credits left
if ($count <= 0 || $ts <= 0) {
$memcached->delete($key);
return false;
}
$count -= 1;
$res = $memcached->replace($key, "$count.$ts", $ts);
if ($res === false && $memcached->getResultCode() !== Memcached::RES_NOTSTORED) {
return false;
}
return true;
}
function twister_captcha_form() {
return '<div id="t-root"></div>';
}
function log_failed_captcha($ip, $userpwd, $board, $thread_id, $is_quiet, $meta = null) {
$data = [
'board' => $board,
'thread_id' => $thread_id,
];
if ($userpwd) {
$data['arg_num'] = $userpwd->pwdLifetime();
$data['pwd'] = $userpwd->getPwd();
}
if ($meta) {
$data['meta'] = $meta;
}
if ($is_quiet) {
$type = 'failed_captcha_quiet';
}
else {
$type = 'failed_captcha';
}
write_to_event_log($type, $ip, $data);
}
function h_captcha_form($autoload = false, $cb = 'onRecaptchaLoaded', $dark = false) {
global $hcaptcha_public_key;
$js_tag = '<script src="https://hcaptcha.com/1/api.js'
. (!$autoload ? "?onload=$cb&amp;render=explicit" : '') . '" async defer></script>';
if ($autoload) {
$attrs = ' class="h-captcha" data-sitekey="' . $hcaptcha_public_key . '"';
if ($dark) {
$attrs .= ' data-theme="dark"';
}
}
else {
$attrs = '';
}
$container_tag = '<script>window.hcaptchaKey = "' . $hcaptcha_public_key
. '";</script><div id="g-recaptcha"'
. $attrs . '></div>';
return $js_tag.$container_tag;
}
// Moves css out of the form for html validation
function captcha_form($autoload = false, $cb = 'onRecaptchaLoaded', $dark = false) {
global $recaptcha_public_key;
$js_tag = '<script src="https://www.google.com/recaptcha/api.js'
. (!$autoload ? "?onload=$cb&amp;render=explicit" : '') . '" async defer></script>';
if ($autoload) {
$attrs = ' class="g-recaptcha" data-sitekey="' . $recaptcha_public_key . '"';
if ($dark) {
$attrs .= ' data-theme="dark"';
}
}
else {
$attrs = '';
}
$container_tag = '<script>window.recaptchaKey = "' . $recaptcha_public_key
. '";</script><div id="g-recaptcha"'
. $attrs . '></div>';
$noscript_tag =<<<HTML
<noscript>
<div style="width: 302px;">
<div style="width: 302px; position: relative;">
<div style="width: 302px; height: 422px;">
<iframe src="https://www.google.com/recaptcha/api/fallback?k=$recaptcha_public_key" frameborder="0" scrolling="no" style="width: 302px; height:422px; border-style: none;"></iframe>
</div>
<div style="width: 300px; height: 60px; border-style: none;bottom: 12px; left: 25px; margin: 0px; padding: 0px; right: 25px;background: #f9f9f9; border: 1px solid #c1c1c1; border-radius: 3px;">
<textarea id="g-recaptcha-response" name="g-recaptcha-response" class="g-recaptcha-response" style="width: 250px; height: 40px; border: 1px solid #c1c1c1;margin: 10px 25px; padding: 0px; resize: none;"></textarea>
</div>
</div>
</div>
</noscript>
HTML;
if (defined('NOSCRIPT_CAPTCHA_ONLY') && NOSCRIPT_CAPTCHA_ONLY == 1) {
return $container_tag.$noscript_tag;
}
return $js_tag.$container_tag.$noscript_tag;
}
// Legacy captcha
// Uses recaptcha v2 for noscript captcha as the v1 seems to be broken currently.
function captcha_form_alt() {
global $recaptcha_public_key;
$html = <<<HTML
<div id="captchaContainerAlt"></div>
<script>
function onAltCaptchaClick() {
Recaptcha.reload('t');
}
function onAltCaptchaReady() {
var el;
if (el = document.getElementById('recaptcha_image')) {
el.title = 'Reload';
el.addEventListener('click', onAltCaptchaClick, false);
}
}
if (!window.passEnabled) {
var el = document.createElement('script');
el.type = 'text/javascript';
el.src = '//www.google.com/recaptcha/api/js/recaptcha_ajax.js';
el.onload = function() {
Recaptcha.create('$recaptcha_public_key',
'captchaContainerAlt',
{
theme: 'clean',
tabindex: 3,
callback: onAltCaptchaReady
}
);
}
document.head.appendChild(el);
}</script>
<noscript>
<div style="width: 302px;">
<div style="width: 302px; position: relative;">
<div style="width: 302px; height: 422px;">
<iframe src="https://www.google.com/recaptcha/api/fallback?k=$recaptcha_public_key" frameborder="0" scrolling="no" style="width: 302px; height:422px; border-style: none;"></iframe>
</div>
<div style="width: 300px; height: 60px; border-style: none;bottom: 12px; left: 25px; margin: 0px; padding: 0px; right: 25px;background: #f9f9f9; border: 1px solid #c1c1c1; border-radius: 3px;">
<textarea id="g-recaptcha-response" name="g-recaptcha-response" class="g-recaptcha-response" style="width: 250px; height: 40px; border: 1px solid #c1c1c1;margin: 10px 25px; padding: 0px; resize: none;"></textarea>
</div>
</div>
</div>
</noscript>
HTML;
return $html;
}
function recaptcha_ban($n, $time, $return_error = 0, $length = 1)
{
auto_ban_poster($name, $length, 1, "failed verification $n times per $time", "Possible spambot; repeatedly sent incorrect CAPTCHA verification.");
if( $return_error == 1 ) {
return S_GENERICERROR;
}
error(S_GENERICERROR);
}
/**
* Works for both recaptcha and hcaptcha
*/
function recaptcha_bad_captcha($return_error = false, $codes = null) {
$error = S_BADCAPTCHA;
if (is_array($codes)) {
if (in_array('missing-input-response', $codes)) {
$error = S_NOCAPTCHA;
}
if ($return_error) {
return $error;
}
else {
error($error);
}
}
else {
if ($return_error) {
return $error;
}
else {
error($error);
}
}
}
// -----------
// hCaptcha
// -----------
function start_hcaptcha_verify($return_error = false) {
global $hcaptcha_private_key, $hcaptcha_ch;
$response = $_POST["h-captcha-response"];
if (!$response) {
if ($return_error == false) {
error(S_NOCAPTCHA);
}
return S_NOCAPTCHA;
}
$response = urlencode($response);
$rlen = strlen($response);
if ($rlen > 32768) {
return recaptcha_bad_captcha($return_error);
}
$api_url = 'https://hcaptcha.com/siteverify';
$post = array(
'secret' => $hcaptcha_private_key,
'response' => $response
);
$hcaptcha_ch = rpc_start_captcha_request($api_url, $post, null, false);
}
function end_hcaptcha_verify($return_error = false) {
global $hcaptcha_ch;
if (!$hcaptcha_ch) {
return;
}
$ret = rpc_finish_request($hcaptcha_ch, $error, $httperror);
// BAD
// 413 Request Too Large is bad; it was caused intentionally by the user.
if ($httperror == 413) {
return recaptcha_bad_captcha($return_error);
}
// BAD
if ($ret == null) {
return recaptcha_bad_captcha($return_error);
}
$resp = json_decode($ret, true);
// BAD
// Malformed JSON response from Google
if (json_last_error() !== JSON_ERROR_NONE) {
return recaptcha_bad_captcha($return_error);
}
// GOOD
if ($resp['success']) {
return $resp;
}
// BAD
return recaptcha_bad_captcha($return_error, $resp['error-codes']);
}
// -----------
// reCaptcha V2
// -----------
// FIXME $challenge_field is no longer used
function start_recaptcha_verify($return_error = false, $challenge_field = '') {
global $recaptcha_private_key, $recaptcha_ch;
$response = $_POST["g-recaptcha-response"];
if (!$response) {
if ($return_error == false) {
error(S_NOCAPTCHA);
}
return S_NOCAPTCHA;
}
$response = urlencode($response);
$rlen = strlen($response);
if ($rlen > 4096) {
return recaptcha_bad_captcha($return_error);
}
$api_url = 'https://www.google.com/recaptcha/api/siteverify';
$post = array(
'secret' => $recaptcha_private_key,
'response' => $response
);
$recaptcha_ch = rpc_start_captcha_request($api_url, $post, null, false);
}
function end_recaptcha_verify($return_error = false) {
global $recaptcha_ch;
if (!$recaptcha_ch) {
return;
}
$ret = rpc_finish_request($recaptcha_ch, $error, $httperror);
// BAD
// 413 Request Too Large is bad; it was caused intentionally by the user.
if ($httperror == 413) {
return recaptcha_bad_captcha($return_error);
}
// BAD
if ($ret == null) {
return recaptcha_bad_captcha($return_error);
}
$resp = json_decode($ret, true);
// BAD
// Malformed JSON response from Google
if (json_last_error() !== JSON_ERROR_NONE) {
return recaptcha_bad_captcha($return_error);
}
// GOOD
if ($resp['success']) {
return $resp;
}
// BAD
return recaptcha_bad_captcha($return_error, $resp['error-codes']);
}
// -----------
// reCaptcha V1
// -----------
function start_recaptcha_verify_alt($return_error = false, $challenge_field = '') {
global $recaptcha_private_key, $recaptcha_ch;
$challenge = ( $challenge_field == '' ) ? $_POST["recaptcha_challenge_field"] : $challenge_field;
$response = $_POST["recaptcha_response_field"];
if (!$challenge || !$response) {
if( $return_error == false ) {
error(S_NOCAPTCHA);
}
return S_NOCAPTCHA;
}
$num_words = 1 + preg_match_all('/\\s/', $response);
$rlen = strlen($response);
if ($num_words > 3 || $rlen > 128) {
return recaptcha_bad_captcha($return_error);
}
$post = array(
"privatekey" => $recaptcha_private_key,
"challenge" => $challenge,
"remoteip" => $_SERVER["REMOTE_ADDR"],
"response" => $response
);
$recaptcha_ch = rpc_start_request("https://www.google.com/recaptcha/api/verify", $post, null, false);
}
function end_recaptcha_verify_alt($return_error = false) {
global $recaptcha_ch;
if (!$recaptcha_ch) return;
$ret = rpc_finish_request($recaptcha_ch, $error, $httperror);
if ($httperror == 413) {
return recaptcha_bad_captcha($return_error);
}
if ($ret) {
$lines = explode("\n", $ret);
if ($lines[0] === "true") {
// GOOD
return;
}
}
// BAD
return recaptcha_bad_captcha($return_error);
}
?>