Merge branch 'captcha-workaround' into 'config'

Captcha workaround

See merge request leftypol/leftypol!9
This commit is contained in:
Zankaria Auxa 2024-10-28 12:36:37 +00:00
commit aa8525aa86
4 changed files with 121 additions and 82 deletions

View file

@ -301,9 +301,8 @@
'lock',
'raw',
'embed',
'g-recaptcha-response',
'h-captcha-response',
'cf-turnstile-response',
'captcha-response',
'captcha-form-id',
'spoiler',
'page',
'file_url',
@ -330,33 +329,40 @@
'answer' => '4'
);
*/
/**
* The captcha is dynamically injected on the client if the server replies with the `captcha-required` cookie set
* to 1.
* Use false to disable this configuration, otherwise, set the IP that vichan should check for captcha responses.
*/
$config['dynamic_captcha'] = false;
// Enable reCaptcha to make spam even harder. Rarely necessary.
$config['recaptcha'] = false;
// Public and private key pair from https://www.google.com/recaptcha/admin/create
$config['recaptcha_public'] = '6LcXTcUSAAAAAKBxyFWIt2SO8jwx4W7wcSMRoN3f';
$config['recaptcha_private'] = '6LcXTcUSAAAAAOGVbVdhmEM1_SyRF4xTKe8jbzf_';
// Enable hCaptcha.
$config['hcaptcha'] = false;
// Public and private key pair for using hCaptcha.
$config['hcaptcha_public'] = '7a4b21e0-dc53-46f2-a9f8-91d2e74b63a0';
$config['hcaptcha_private'] = '0x4e9A01bE637b51dC41a7Ea9865C3fDe4aB72Cf17';
// Enable Cloudflare's Turnstile captcha.
$config['turnstile'] = false;
// Public and private key pair for turnstile.
$config['turnstile_public'] = '';
$config['turnstile_private'] = '';
// Enable a captcha system to make spam even harder. Rarely necessary.
$config['captcha'] = [
/**
* Select the captcha backend, false to disable.
* Can be false, "recaptcha", "hcaptcha" or "turnstile".
*/
'mode' => false,
/**
* The captcha is dynamically injected on the client if the server replies with the `captcha-required` cookie set
* to 1.
* Use false to disable this configuration, otherwise, set the IP that vichan should check for captcha responses.
*/
'dynamic' => false,
// Require to be non-zero if you use js/ajax.js (preferably no more than a few seconds), otherwise weird errors might occur.
'passthrough_timeout' => 0,
// Configure Google reCAPTCHA.
'recaptcha' => [
// Public and private key pair from https://www.google.com/recaptcha/admin/create
'public' => '6LcXTcUSAAAAAKBxyFWIt2SO8jwx4W7wcSMRoN3f',
'private' => '6LcXTcUSAAAAAOGVbVdhmEM1_SyRF4xTKe8jbzf_',
],
// Configure hCaptcha.
'hcaptcha' => [
// Public and private key pair for using hCaptcha.
'public' => '7a4b21e0-dc53-46f2-a9f8-91d2e74b63a0',
'private' => '0x4e9A01bE637b51dC41a7Ea9865C3fDe4aB72Cf17',
],
// Configure Cloudflare Turnstile.
'turnstile' => [
// Public and private key pair for turnstile.
'public' => '',
'private' => '',
]
];
// Ability to lock a board for normal users and still allow mods to post. Could also be useful for making an archive board
$config['board_locked'] = false;

120
post.php
View file

@ -167,6 +167,68 @@ function check_turnstile($secret, $response, $remote_ip, $expected_action)
return $json_ret['success'] === true && $json_ret['action'] === $expected_action;
}
/**
* A "sophisticated" workaround to js/ajax.js calling post.php multiple times on error/ban.
*/
function check_captcha(array $captcha_config, string $form_id, string $board_uri, string $response, string $remote_ip, string $expected_action) {
$dynamic = $captcha_config['dynamic'];
if ($dynamic !== false && $remote_ip !== $dynamic) {
return true;
}
switch ($captcha_config['mode']) {
case 'recaptcha':
case 'hcaptcha':
case 'turnstile':
$mode = $captcha_config['mode'];
break;
case false:
return true;
default:
\error_log("Unknown captcha mode '{$captcha_config['mode']}'");
throw new \RuntimeException('Captcha configuration error');
}
$passthrough_timeout = $captcha_config['passthrough_timeout'];
if ($passthrough_timeout != 0) {
$pass = Cache::get("captcha_passthrough_{$remote_ip}_{$form_id}");
if ($pass !== false) {
$let_through = $pass['expires'] > time() && $pass['board_uri'] === $board_uri && $pass['captcha_response'] === $response;
if ($let_through) {
return true;
}
}
}
$remote_ip_send = $dynamic !== false ? null : $remote_ip;
$private_key = $captcha_config[$mode]['private'];
$public_key = $captcha_config[$mode]['public'];
switch ($mode) {
case 'recaptcha':
$ret = check_recaptcha($private_key, $response, $remote_ip_send);
break;
case 'hcaptcha':
$ret = check_hcaptcha($private_key, $response, $remote_ip_send, $public_key);
break;
case 'turnstile':
$ret = check_turnstile($private_key, $response, $remote_ip_send, $expected_action);
break;
}
if ($ret && $passthrough_timeout != 0) {
$pass = [
'expires' => time() + $passthrough_timeout,
'board_uri' => $board_uri,
'captcha_response' => $response
];
Cache::set("captcha_passthrough_{$remote_ip}_{$form_id}", $pass, $passthrough_timeout + 2);
}
return $ret;
}
/**
* Deletes the (single) captcha associated with the ip and code.
*
@ -765,56 +827,16 @@ function handle_post()
if (!$dropped_post) {
if ($config['dynamic_captcha'] !== false) {
if ($_SERVER['REMOTE_ADDR'] === $config['dynamic_captcha']) {
if ($config['recaptcha']) {
if (!isset($_POST['g-recaptcha-response'])) {
error($config['error']['bot']);
}
if (!check_recaptcha($config['recaptcha_private'], $_POST['g-recaptcha-response'], null)) {
error($config['error']['captcha']);
}
} elseif ($config['hcaptcha']) {
if (!isset($_POST['h-captcha-response'])) {
error($config['error']['bot']);
}
if (!check_hcaptcha($config['hcaptcha_private'], $_POST['h-captcha-response'], null, $config['hcaptcha_public'])) {
error($config['error']['captcha']);
}
} elseif ($config['turnstile']) {
if (!isset($_POST['cf-turnstile-response'])) {
error($config['error']['bot']);
}
$expected_action = $post['op'] ? 'post-thread' : 'post-reply';
if (!check_turnstile($config['turnstile_private'], $_POST['cf-turnstile-response'], null, $expected_action)) {
error($config['error']['captcha']);
}
}
// Check for CAPTCHA right after opening the board so the "return" link is in there.
if ($config['captcha']['mode'] !== false) {
if (!isset($_POST['captcha-response'], $_POST['captcha-form-id'])) {
error($config['error']['bot']);
}
} else {
// Check for CAPTCHA right after opening the board so the "return" link is in there.
if ($config['recaptcha']) {
if (!isset($_POST['g-recaptcha-response'])) {
error($config['error']['bot']);
}
if (!check_recaptcha($config['recaptcha_private'], $_POST['g-recaptcha-response'], $_SERVER['REMOTE_ADDR'])) {
error($config['error']['captcha']);
}
} elseif ($config['hcaptcha']) {
if (!isset($_POST['h-captcha-response'])) {
error($config['error']['bot']);
}
if (!check_hcaptcha($config['hcaptcha_private'], $_POST['h-captcha-response'], $_SERVER['REMOTE_ADDR'], $config['hcaptcha_public'])) {
error($config['error']['captcha']);
}
} elseif ($config['turnstile']) {
if (!isset($_POST['cf-turnstile-response'])) {
error($config['error']['bot']);
}
$expected_action = $post['op'] ? 'post-thread' : 'post-reply';
if (!check_turnstile($config['turnstile_private'], $_POST['cf-turnstile-response'], null, $expected_action)) {
error($config['error']['captcha']);
}
$expected_action = $post['op'] ? 'post-thread' : 'post-reply';
$ret = check_captcha($config['captcha'], $_POST['captcha-form-id'], $post['board'], $_POST['captcha-response'], $_SERVER['REMOTE_ADDR'], $expected_action);
if (!$ret) {
error($config['error']['captcha']);
}
}

View file

@ -273,8 +273,9 @@ var captcha_renderer = null;
function onCaptchaLoadHcaptcha() {
if (captcha_renderer === null && (active_page === 'index' || active_page === 'catalog' || active_page === 'thread')) {
let renderer = {
renderOn: (container) => hcaptcha.render(container, {
applyOn: (container, params) => hcaptcha.render(container, {
sitekey: "{{ config.hcaptcha_public }}",
callback: params['on-success'],
}),
remove: (widgetId) => { /* Not supported */ },
reset: (widgetId) => hcaptcha.reset(widgetId)
@ -300,10 +301,11 @@ window.onCaptchaLoadTurnstile_post_thread = function() {
function onCaptchaLoadTurnstile(action) {
if (captcha_renderer === null && (active_page === 'index' || active_page === 'catalog' || active_page === 'thread')) {
let renderer = {
renderOn: function(container) {
applyOn: function(container, params) {
let widgetId = turnstile.render('#' + container, {
sitekey: "{{ config.turnstile_public }}",
action: action,
callback: params['on-success'],
});
if (widgetId === undefined) {
return null;
@ -320,9 +322,16 @@ function onCaptchaLoadTurnstile(action) {
{% endif %} // End if turnstile
function onCaptchaLoad(renderer) {
// Initialize the form identifier with a random password.
document.getElementById('captcha-form-id').value = generatePassword();
captcha_renderer = renderer;
let widgetId = renderer.renderOn('captcha-container');
let widgetId = renderer.applyOn('captcha-container', {
'on-success': function(token) {
document.getElementById('captcha-response').value = token;
}
});
if (widgetId === null) {
console.error('Could not render captcha!');
}

View file

@ -118,6 +118,8 @@
</th>
<td>
<div id="captcha-container"></div>
<textarea id="captcha-response" name="captcha-response" style="display:none;"></textarea>
<textarea id="captcha-form-id" name="captcha-form-id" style="display:none;"></textarea>
{{ antibot.html() }}
</td>
</tr>