captcha-queries.php: refactor remote captcha queries to use DI and abstract the implementation better

This commit is contained in:
Zankaria 2024-08-15 15:11:32 +02:00
parent e825e7aac5
commit 933594194c
3 changed files with 105 additions and 62 deletions

View File

@ -3,6 +3,9 @@ namespace Vichan;
use RuntimeException; use RuntimeException;
use Vichan\Driver\{HttpDriver, HttpDrivers, Log, LogDrivers}; use Vichan\Driver\{HttpDriver, HttpDrivers, Log, LogDrivers};
use Vichan\Service\HCaptchaQuery;
use Vichan\Service\ReCaptchaQuery;
use Vichan\Service\RemoteCaptchaQuery;
defined('TINYBOARD') or exit; defined('TINYBOARD') or exit;
@ -53,6 +56,17 @@ function build_context(array $config): Context {
HttpDriver::class => function($c) { HttpDriver::class => function($c) {
$config = $c->get('config'); $config = $c->get('config');
return HttpDrivers::getHttpDriver($config['upload_by_url_timeout'], $config['max_filesize']); return HttpDrivers::getHttpDriver($config['upload_by_url_timeout'], $config['max_filesize']);
},
RemoteCaptchaQuery::class => function($c) {
$config = $c->get('config');
$http = $c->get(HttpDriver::class);
if ($config['recaptcha']) {
return new ReCaptchaQuery($http, $config['recaptcha_private']);
} elseif ($config['hcaptcha']) {
return new HCaptchaQuery($http, $config['hcaptcha_private'], $config['hcaptcha_public']);
} else {
throw new RuntimeException('No remote captcha service available');
}
} }
]); ]);
} }

View File

@ -6,68 +6,107 @@ use Vichan\Driver\HttpDriver;
defined('TINYBOARD') or exit; defined('TINYBOARD') or exit;
class RemoteCaptchaQuery { class ReCaptchaQuery implements RemoteCaptchaQuery {
private HttpDriver $http; private HttpDriver $http;
private string $secret; private string $secret;
private string $endpoint;
/** /**
* Creates a new CaptchaRemoteQueries instance using the google recaptcha service. * Creates a new ReCaptchaQuery using the google recaptcha service.
* *
* @param HttpDriver $http The http client. * @param HttpDriver $http The http client.
* @param string $secret Server side secret. * @param string $secret Server side secret.
* @return CaptchaRemoteQueries A new captcha query instance. * @return ReCaptchaQuery A new ReCaptchaQuery query instance.
*/ */
public static function withRecaptcha(HttpDriver $http, string $secret): RemoteCaptchaQuery { public function __construct(HttpDriver $http, string $secret) {
return new self($http, $secret, 'https://www.google.com/recaptcha/api/siteverify');
}
/**
* Creates a new CaptchaRemoteQueries instance using the hcaptcha service.
*
* @param HttpDriver $http The http client.
* @param string $secret Server side secret.
* @return CaptchaRemoteQueries A new captcha query instance.
*/
public static function withHCaptcha(HttpDriver $http, string $secret): RemoteCaptchaQuery {
return new self($http, $secret, 'https://hcaptcha.com/siteverify');
}
private function __construct(HttpDriver $http, string $secret, string $endpoint) {
$this->http = $http; $this->http = $http;
$this->secret = $secret; $this->secret = $secret;
$this->endpoint = $endpoint;
} }
/** public function responseField(): string {
* Checks if the user at the remote ip passed the captcha. return 'g-recaptcha-response';
* }
* @param string $response User provided response.
* @param string $remote_ip User ip.
* @return bool Returns true if the user passed the captcha.
* @throws RuntimeException|JsonException Throws on IO errors or if it fails to decode the answer.
*/
public function verify(string $response, string $remote_ip): bool {
$data = array(
'secret' => $this->secret,
'response' => $response,
'remoteip' => $remote_ip
);
$ret = $this->http->requestGet($this->endpoint, $data); public function verify(string $response, ?string $remote_ip): bool {
$data = [
'secret' => $this->secret,
'response' => $response
];
if ($remote_ip !== null) {
$data['remoteip'] = $remote_ip;
}
$ret = $this->http->requestGet('https://www.google.com/recaptcha/api/siteverify', $data);
$resp = json_decode($ret, true, 16, JSON_THROW_ON_ERROR); $resp = json_decode($ret, true, 16, JSON_THROW_ON_ERROR);
return isset($resp['success']) && $resp['success']; return isset($resp['success']) && $resp['success'];
} }
} }
class HCaptchaQuery implements RemoteCaptchaQuery {
private HttpDriver $http;
private string $secret;
private string $sitekey;
/**
* Creates a new HCaptchaQuery using the hCaptcha service.
*
* @param HttpDriver $http The http client.
* @param string $secret Server side secret.
* @return HCaptchaQuery A new hCaptcha query instance.
*/
public function __construct(HttpDriver $http, string $secret, string $sitekey) {
$this->http = $http;
$this->secret = $secret;
$this->sitekey = $sitekey;
}
public function responseField(): string {
return 'h-captcha-response';
}
public function verify(string $response, ?string $remote_ip): bool {
$data = [
'secret' => $this->secret,
'response' => $response,
'sitekey' => $this->sitekey
];
if ($remote_ip !== null) {
$data['remoteip'] = $remote_ip;
}
$ret = $this->http->requestGet('https://hcaptcha.com/siteverify', $data);
$resp = json_decode($ret, true, 16, JSON_THROW_ON_ERROR);
return isset($resp['success']) && $resp['success'];
}
}
interface RemoteCaptchaQuery {
/**
* Name of the response field in the form data expected by the implementation.
*
* @return string The name of the field.
*/
public function responseField(): string;
/**
* Checks if the user at the remote ip passed the captcha.
*
* @param string $response User provided response.
* @param ?string $remote_ip User ip. Leave to null to only check the response value.
* @return bool Returns true if the user passed the captcha.
* @throws RuntimeException|JsonException Throws on IO errors or if it fails to decode the answer.
*/
public function verify(string $response, ?string $remote_ip): bool;
}
class NativeCaptchaQuery { class NativeCaptchaQuery {
private HttpDriver $http; private HttpDriver $http;
private string $domain; private string $domain;
private string $provider_check; private string $provider_check;
/** /**
* @param HttpDriver $http The http client. * @param HttpDriver $http The http client.
* @param string $domain The server's domain. * @param string $domain The server's domain.
@ -89,12 +128,12 @@ class NativeCaptchaQuery {
* @throws RuntimeException Throws on IO errors. * @throws RuntimeException Throws on IO errors.
*/ */
public function verify(string $extra, string $user_text, string $user_cookie): bool { public function verify(string $extra, string $user_text, string $user_cookie): bool {
$data = array( $data = [
'mode' => 'check', 'mode' => 'check',
'text' => $user_text, 'text' => $user_text,
'extra' => $extra, 'extra' => $extra,
'cookie' => $user_cookie 'cookie' => $user_cookie
); ];
$ret = $this->http->requestGet($this->domain . '/' . $this->provider_check, $data); $ret = $this->http->requestGet($this->domain . '/' . $this->provider_check, $data);
return $ret === '1'; return $ret === '1';

View File

@ -648,31 +648,21 @@ if (isset($_POST['delete'])) {
} }
} }
// Remote 3rd party captchas. // Remote 3rd party captchas.
else if (!$config['dynamic_captcha'] || $config['dynamic_captcha'] === $_SERVER['REMOTE_ADDR']) { elseif (($config['recaptcha'] || $config['hcaptcha'])
// recaptcha && (!$config['dynamic_captcha'] || $config['dynamic_captcha'] === $_SERVER['REMOTE_ADDR'])) {
if ($config['recaptcha']) { $query = $content->get(RemoteCaptchaQuery::class);
if (!isset($_POST['g-recaptcha-response'])) { $field = $query->responseField();
error($config['error']['bot']);
} if (!isset($_POST[$field])) {
$response = $_POST['g-recaptcha-response']; error($config['error']['bot']);
$query = RemoteCaptchaQuery::withRecaptcha($context->get(HttpDriver::class), $config['recaptcha_private']); }
} $response = $_POST[$field];
// hCaptcha
elseif ($config['hcaptcha']) {
if (!isset($_POST['h-captcha-response'])) {
error($config['error']['bot']);
}
$response = $_POST['h-captcha-response'];
$query = RemoteCaptchaQuery::withHCaptcha($context->get(HttpDriver::class), $config['hcaptcha_private']);
}
if (isset($query, $response)) {
$success = $query->verify($response, $_SERVER['REMOTE_ADDR']); $success = $query->verify($response, $_SERVER['REMOTE_ADDR']);
if (!$success) { if (!$success) {
error($config['error']['captcha']); error($config['error']['captcha']);
} }
} }
}
} catch (RuntimeException $e) { } catch (RuntimeException $e) {
$context->getLog()->log(Log::ERROR, "Captcha IO exception: {$e->getMessage()}"); $context->getLog()->log(Log::ERROR, "Captcha IO exception: {$e->getMessage()}");
error($config['error']['remote_io_error']); error($config['error']['remote_io_error']);