mirror of
https://github.com/lkddi/nexusphp.git
synced 2026-04-03 14:10:57 +08:00
feat: Refine captcha configuration and drivers
Introduce a configurable captcha manager with drivers for image, Cloudflare Turnstile, and Google reCAPTCHA, including fallback behaviour. Refactor login, signup, complain, and related flows to use the new abstraction while simplifying the legacy image endpoint. Document captcha environment options and restore classic defaults in .env.example. Signed-off-by: Qi HU <github@spcsky.com>
This commit is contained in:
16
.env.example
16
.env.example
@@ -96,6 +96,22 @@ CHANNEL_NAME_SETTING=channel_setting
|
||||
CHANNEL_NAME_MODEL_EVENT=channel_model_event
|
||||
FORCE_SCHEME=
|
||||
|
||||
# Captcha settings
|
||||
# Available drivers: image, cloudflare_turnstile, google_recaptcha_v2
|
||||
CAPTCHA_DRIVER=image
|
||||
|
||||
# Cloudflare Turnstile keys (used when CAPTCHA_DRIVER=cloudflare_turnstile)
|
||||
TURNSTILE_SITE_KEY=
|
||||
TURNSTILE_SECRET_KEY=
|
||||
TURNSTILE_THEME=light
|
||||
TURNSTILE_SIZE=flexible
|
||||
|
||||
# Google reCAPTCHA v2 keys (used when CAPTCHA_DRIVER=google_recaptcha_v2)
|
||||
RECAPTCHA_SITE_KEY=
|
||||
RECAPTCHA_SECRET_KEY=
|
||||
RECAPTCHA_THEME=light
|
||||
RECAPTCHA_SIZE=normal
|
||||
|
||||
CROWDIN_ACCESS_TOKEN=
|
||||
CROWDIN_PROJECT_ID=
|
||||
|
||||
|
||||
19
app/Services/Captcha/CaptchaDriverInterface.php
Normal file
19
app/Services/Captcha/CaptchaDriverInterface.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Captcha;
|
||||
|
||||
interface CaptchaDriverInterface
|
||||
{
|
||||
public function isEnabled(): bool;
|
||||
|
||||
/**
|
||||
* Render the captcha markup for HTML forms.
|
||||
*/
|
||||
public function render(array $context = []): string;
|
||||
|
||||
/**
|
||||
* Verify the captcha response.
|
||||
*/
|
||||
public function verify(array $payload, array $context = []): bool;
|
||||
}
|
||||
|
||||
120
app/Services/Captcha/CaptchaManager.php
Normal file
120
app/Services/Captcha/CaptchaManager.php
Normal file
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Captcha;
|
||||
|
||||
use App\Services\Captcha\Exceptions\CaptchaValidationException;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class CaptchaManager
|
||||
{
|
||||
/** @var array<string, CaptchaDriverInterface> */
|
||||
protected array $drivers = [];
|
||||
|
||||
protected ?array $config = null;
|
||||
|
||||
public function driver(?string $name = null): CaptchaDriverInterface
|
||||
{
|
||||
$name = $name ?? $this->getDefaultDriver();
|
||||
|
||||
$driver = $this->getDriverInstance($name);
|
||||
|
||||
if ($name !== 'image' && !$driver->isEnabled()) {
|
||||
return $this->driver('image');
|
||||
}
|
||||
|
||||
return $driver;
|
||||
}
|
||||
|
||||
public function render(array $context = []): string
|
||||
{
|
||||
return $this->driver()->render($context);
|
||||
}
|
||||
|
||||
public function verify(array $payload, array $context = []): bool
|
||||
{
|
||||
try {
|
||||
return $this->driver()->verify($payload, $context);
|
||||
} catch (CaptchaValidationException $exception) {
|
||||
throw $exception;
|
||||
}
|
||||
}
|
||||
|
||||
public function isEnabled(): bool
|
||||
{
|
||||
return $this->driver()->isEnabled();
|
||||
}
|
||||
|
||||
protected function getDriverInstance(string $name): CaptchaDriverInterface
|
||||
{
|
||||
if (!isset($this->drivers[$name])) {
|
||||
try {
|
||||
$this->drivers[$name] = $this->resolveDriver($name);
|
||||
} catch (\InvalidArgumentException $exception) {
|
||||
if ($name !== 'image') {
|
||||
return $this->getDriverInstance('image');
|
||||
}
|
||||
throw $exception;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->drivers[$name];
|
||||
}
|
||||
|
||||
protected function resolveDriver(string $name): CaptchaDriverInterface
|
||||
{
|
||||
$config = $this->getConfigValue("drivers.$name", []);
|
||||
|
||||
if (!is_array($config) || empty($config)) {
|
||||
throw new \InvalidArgumentException("Captcha driver [$name] is not defined.");
|
||||
}
|
||||
|
||||
$driverClass = Arr::get($config, 'class');
|
||||
|
||||
if (!$driverClass || !class_exists($driverClass)) {
|
||||
throw new \InvalidArgumentException("Captcha driver class for [$name] is invalid.");
|
||||
}
|
||||
|
||||
$driver = new $driverClass($config);
|
||||
|
||||
if (!$driver instanceof CaptchaDriverInterface) {
|
||||
throw new \InvalidArgumentException("Captcha driver [$name] must implement " . CaptchaDriverInterface::class);
|
||||
}
|
||||
|
||||
return $driver;
|
||||
}
|
||||
|
||||
protected function getDefaultDriver(): string
|
||||
{
|
||||
return (string) $this->getConfigValue('default', 'image');
|
||||
}
|
||||
|
||||
protected function getConfigValue(string $key, $default = null)
|
||||
{
|
||||
if ($this->config === null) {
|
||||
$config = null;
|
||||
if (function_exists('app')) {
|
||||
try {
|
||||
$repository = app('config');
|
||||
if ($repository) {
|
||||
$config = $repository->get('captcha');
|
||||
}
|
||||
} catch (\Throwable $exception) {
|
||||
$config = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_array($config) && function_exists('nexus_config')) {
|
||||
$config = nexus_config('captcha', []);
|
||||
}
|
||||
|
||||
if (!is_array($config)) {
|
||||
$path = (defined('ROOT_PATH') ? ROOT_PATH : dirname(__DIR__, 3) . DIRECTORY_SEPARATOR) . 'config/captcha.php';
|
||||
$config = is_file($path) ? require $path : [];
|
||||
}
|
||||
|
||||
$this->config = is_array($config) ? $config : [];
|
||||
}
|
||||
|
||||
return Arr::get($this->config, $key, $default);
|
||||
}
|
||||
}
|
||||
156
app/Services/Captcha/Drivers/ImageCaptchaDriver.php
Normal file
156
app/Services/Captcha/Drivers/ImageCaptchaDriver.php
Normal file
@@ -0,0 +1,156 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Captcha\Drivers;
|
||||
|
||||
use App\Services\Captcha\CaptchaDriverInterface;
|
||||
use App\Services\Captcha\Exceptions\CaptchaValidationException;
|
||||
|
||||
class ImageCaptchaDriver implements CaptchaDriverInterface
|
||||
{
|
||||
protected array $config;
|
||||
|
||||
public function __construct(array $config = [])
|
||||
{
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
public function isEnabled(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function render(array $context = []): string
|
||||
{
|
||||
$labels = $context['labels'] ?? [];
|
||||
$imageLabel = $labels['image'] ?? 'Security Image';
|
||||
$codeLabel = $labels['code'] ?? 'Security Code';
|
||||
$secret = $context['secret'] ?? '';
|
||||
|
||||
$imagehash = $this->issue();
|
||||
$imageUrl = htmlspecialchars(sprintf('image.php?action=regimage&imagehash=%s&secret=%s', $imagehash, $secret ?? ''), ENT_QUOTES, 'UTF-8');
|
||||
|
||||
return implode("\n", [
|
||||
sprintf('<tr><td class="rowhead">%s</td><td align="left"><img src="%s" border="0" alt="CAPTCHA" /></td></tr>', htmlspecialchars($imageLabel, ENT_QUOTES, 'UTF-8'), $imageUrl),
|
||||
sprintf('<tr><td class="rowhead">%s</td><td align="left"><input type="text" autocomplete="off" style="width: 180px; border: 1px solid gray" name="imagestring" value="" /><input type="hidden" name="imagehash" value="%s" /></td></tr>', htmlspecialchars($codeLabel, ENT_QUOTES, 'UTF-8'), htmlspecialchars($imagehash, ENT_QUOTES, 'UTF-8')),
|
||||
]);
|
||||
}
|
||||
|
||||
public function verify(array $payload, array $context = []): bool
|
||||
{
|
||||
$imagehash = trim((string) ($payload['imagehash'] ?? ''));
|
||||
$imagestring = trim((string) ($payload['imagestring'] ?? ''));
|
||||
|
||||
if ($imagehash === '' || $imagestring === '') {
|
||||
throw new CaptchaValidationException('Missing captcha parameters.');
|
||||
}
|
||||
|
||||
$query = sprintf(
|
||||
"SELECT dateline FROM regimages WHERE imagehash='%s' AND imagestring='%s'",
|
||||
mysql_real_escape_string($imagehash),
|
||||
mysql_real_escape_string($imagestring)
|
||||
);
|
||||
|
||||
$sql = sql_query($query);
|
||||
$imgcheck = mysql_fetch_array($sql);
|
||||
|
||||
$this->deleteByHash($imagehash);
|
||||
|
||||
if (empty($imgcheck['dateline'])) {
|
||||
throw new CaptchaValidationException('Invalid captcha response.');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function issue(): string
|
||||
{
|
||||
$random = random_str();
|
||||
$imagehash = md5($random);
|
||||
$dateline = time();
|
||||
|
||||
$sql = sprintf(
|
||||
"INSERT INTO `regimages` (`imagehash`, `imagestring`, `dateline`) VALUES ('%s', '%s', '%s')",
|
||||
mysql_real_escape_string($imagehash),
|
||||
mysql_real_escape_string($random),
|
||||
mysql_real_escape_string((string) $dateline)
|
||||
);
|
||||
|
||||
sql_query($sql);
|
||||
|
||||
return $imagehash;
|
||||
}
|
||||
|
||||
public function outputImage(string $imagehash): void
|
||||
{
|
||||
$query = sprintf(
|
||||
"SELECT imagestring FROM regimages WHERE imagehash=%s",
|
||||
sqlesc($imagehash)
|
||||
);
|
||||
|
||||
$sql = sql_query($query);
|
||||
$regimage = mysql_fetch_array($sql);
|
||||
$imagestring = $regimage['imagestring'] ?? '';
|
||||
|
||||
if ($imagestring === '') {
|
||||
$this->renderFallback();
|
||||
return;
|
||||
}
|
||||
|
||||
$characters = implode(' ', str_split($imagestring));
|
||||
|
||||
if (!function_exists('imagecreatefrompng')) {
|
||||
$this->renderFallback();
|
||||
return;
|
||||
}
|
||||
|
||||
$fontwidth = imageFontWidth(5);
|
||||
$fontheight = imageFontHeight(5);
|
||||
$textwidth = $fontwidth * strlen($characters);
|
||||
$textheight = $fontheight;
|
||||
|
||||
$randimg = rand(1, 5);
|
||||
$imagePath = ROOT_PATH . "public/pic/regimages/reg{$randimg}.png";
|
||||
|
||||
if (!is_file($imagePath)) {
|
||||
$this->renderFallback();
|
||||
return;
|
||||
}
|
||||
|
||||
$im = imagecreatefrompng($imagePath);
|
||||
$imgheight = imagesy($im);
|
||||
$imgwidth = imagesx($im);
|
||||
$textposh = (int) floor(($imgwidth - $textwidth) / 2);
|
||||
$textposv = (int) floor(($imgheight - $textheight) / 2);
|
||||
|
||||
$dots = (int) floor($imgheight * $imgwidth / 35);
|
||||
for ($i = 1; $i <= $dots; $i++) {
|
||||
imagesetpixel($im, rand(0, $imgwidth - 1), rand(0, $imgheight - 1), imagecolorallocate($im, rand(0, 255), rand(0, 255), rand(0, 255)));
|
||||
}
|
||||
|
||||
$textcolor = imagecolorallocate($im, 0, 0, 0);
|
||||
imagestring($im, 5, $textposh, $textposv, $characters, $textcolor);
|
||||
|
||||
header('Content-type: image/png');
|
||||
imagepng($im);
|
||||
imagedestroy($im);
|
||||
}
|
||||
|
||||
protected function deleteByHash(string $imagehash): void
|
||||
{
|
||||
if ($imagehash === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
$delete = sprintf(
|
||||
"DELETE FROM regimages WHERE imagehash='%s'",
|
||||
mysql_real_escape_string($imagehash)
|
||||
);
|
||||
|
||||
sql_query($delete);
|
||||
}
|
||||
|
||||
protected function renderFallback(): void
|
||||
{
|
||||
http_response_code(404);
|
||||
}
|
||||
}
|
||||
134
app/Services/Captcha/Drivers/RecaptchaV2CaptchaDriver.php
Normal file
134
app/Services/Captcha/Drivers/RecaptchaV2CaptchaDriver.php
Normal file
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Captcha\Drivers;
|
||||
|
||||
use App\Services\Captcha\CaptchaDriverInterface;
|
||||
use App\Services\Captcha\Exceptions\CaptchaValidationException;
|
||||
|
||||
class RecaptchaV2CaptchaDriver implements CaptchaDriverInterface
|
||||
{
|
||||
protected array $config;
|
||||
|
||||
public function __construct(array $config = [])
|
||||
{
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
public function isEnabled(): bool
|
||||
{
|
||||
return !empty($this->config['site_key']) && !empty($this->config['secret_key']);
|
||||
}
|
||||
|
||||
public function render(array $context = []): string
|
||||
{
|
||||
if (!$this->isEnabled()) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$labels = $context['labels'] ?? [];
|
||||
$label = $labels['image'] ?? $labels['code'] ?? 'Security Check';
|
||||
$theme = $this->config['theme'] ?? 'light';
|
||||
$size = $this->config['size'] ?? 'normal';
|
||||
$validSizes = ['compact', 'normal'];
|
||||
if (!in_array($size, $validSizes, true)) {
|
||||
$size = 'normal';
|
||||
}
|
||||
|
||||
$attributes = sprintf(
|
||||
'class="g-recaptcha" data-sitekey="%s" data-theme="%s" data-size="%s"',
|
||||
htmlspecialchars($this->config['site_key'], ENT_QUOTES, 'UTF-8'),
|
||||
htmlspecialchars($theme, ENT_QUOTES, 'UTF-8'),
|
||||
htmlspecialchars($size, ENT_QUOTES, 'UTF-8')
|
||||
);
|
||||
|
||||
return sprintf(
|
||||
'<tr><td class="rowhead">%s</td><td align="left"><div %s></div>%s</td></tr>',
|
||||
htmlspecialchars($label, ENT_QUOTES, 'UTF-8'),
|
||||
$attributes,
|
||||
'<script src="https://www.google.com/recaptcha/api.js" async defer></script>'
|
||||
);
|
||||
}
|
||||
|
||||
public function verify(array $payload, array $context = []): bool
|
||||
{
|
||||
$token = trim((string) ($payload['request']['g-recaptcha-response'] ?? ''));
|
||||
|
||||
if ($token === '') {
|
||||
throw new CaptchaValidationException('Captcha verification token is missing.');
|
||||
}
|
||||
|
||||
$secret = $this->config['secret_key'] ?? '';
|
||||
|
||||
if ($secret === '') {
|
||||
throw new CaptchaValidationException('Captcha secret key is not configured.');
|
||||
}
|
||||
|
||||
$data = [
|
||||
'secret' => $secret,
|
||||
'response' => $token,
|
||||
];
|
||||
|
||||
$remoteIp = $context['ip'] ?? null;
|
||||
|
||||
if (!empty($remoteIp)) {
|
||||
$data['remoteip'] = $remoteIp;
|
||||
}
|
||||
|
||||
$result = $this->sendVerificationRequest('https://www.google.com/recaptcha/api/siteverify', $data);
|
||||
|
||||
if (!($result['success'] ?? false)) {
|
||||
throw new CaptchaValidationException('Captcha verification failed.');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function sendVerificationRequest(string $url, array $data): array
|
||||
{
|
||||
$payload = http_build_query($data);
|
||||
|
||||
if (function_exists('curl_init')) {
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => $payload,
|
||||
CURLOPT_TIMEOUT => 10,
|
||||
CURLOPT_SSL_VERIFYPEER => true,
|
||||
]);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
|
||||
if ($response === false) {
|
||||
$error = curl_error($ch);
|
||||
curl_close($ch);
|
||||
throw new CaptchaValidationException('Captcha verification request failed: ' . $error);
|
||||
}
|
||||
|
||||
curl_close($ch);
|
||||
} else {
|
||||
$context = stream_context_create([
|
||||
'http' => [
|
||||
'method' => 'POST',
|
||||
'header' => "Content-type: application/x-www-form-urlencoded\r\n",
|
||||
'content' => $payload,
|
||||
'timeout' => 10,
|
||||
],
|
||||
]);
|
||||
|
||||
$response = file_get_contents($url, false, $context);
|
||||
|
||||
if ($response === false) {
|
||||
throw new CaptchaValidationException('Captcha verification request failed.');
|
||||
}
|
||||
}
|
||||
|
||||
$decoded = json_decode($response, true);
|
||||
|
||||
if (!is_array($decoded)) {
|
||||
throw new CaptchaValidationException('Unexpected captcha verification response.');
|
||||
}
|
||||
|
||||
return $decoded;
|
||||
}
|
||||
}
|
||||
143
app/Services/Captcha/Drivers/TurnstileCaptchaDriver.php
Normal file
143
app/Services/Captcha/Drivers/TurnstileCaptchaDriver.php
Normal file
@@ -0,0 +1,143 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Captcha\Drivers;
|
||||
|
||||
use App\Services\Captcha\CaptchaDriverInterface;
|
||||
use App\Services\Captcha\Exceptions\CaptchaValidationException;
|
||||
|
||||
class TurnstileCaptchaDriver implements CaptchaDriverInterface
|
||||
{
|
||||
protected static bool $scriptInjected = false;
|
||||
|
||||
protected array $config;
|
||||
|
||||
public function __construct(array $config = [])
|
||||
{
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
public function isEnabled(): bool
|
||||
{
|
||||
return !empty($this->config['site_key']) && !empty($this->config['secret_key']);
|
||||
}
|
||||
|
||||
public function render(array $context = []): string
|
||||
{
|
||||
if (!$this->isEnabled()) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$labels = $context['labels'] ?? [];
|
||||
$label = $labels['image'] ?? $labels['code'] ?? 'Security Check';
|
||||
$theme = $this->config['theme'] ?? 'light';
|
||||
$size = $this->config['size'] ?? 'auto';
|
||||
if (is_string($size)) {
|
||||
$size = strtolower($size);
|
||||
}
|
||||
$validSizes = ['compact', 'normal', 'flexible'];
|
||||
if (!in_array($size, $validSizes, true)) {
|
||||
$size = 'auto';
|
||||
}
|
||||
|
||||
$attributes = sprintf(
|
||||
'class="cf-turnstile" data-sitekey="%s" data-theme="%s"%s',
|
||||
htmlspecialchars($this->config['site_key'], ENT_QUOTES, 'UTF-8'),
|
||||
htmlspecialchars($theme, ENT_QUOTES, 'UTF-8'),
|
||||
$size === 'auto' ? '' : sprintf(' data-size="%s"', htmlspecialchars($size, ENT_QUOTES, 'UTF-8'))
|
||||
);
|
||||
|
||||
$markup = sprintf(
|
||||
'<tr><td class="rowhead">%s</td><td align="left"><div %s></div>%s</td></tr>',
|
||||
htmlspecialchars($label, ENT_QUOTES, 'UTF-8'),
|
||||
$attributes,
|
||||
self::$scriptInjected ? '' : '<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>'
|
||||
);
|
||||
|
||||
self::$scriptInjected = true;
|
||||
|
||||
return $markup;
|
||||
}
|
||||
|
||||
public function verify(array $payload, array $context = []): bool
|
||||
{
|
||||
$token = trim((string) ($payload['request']['cf-turnstile-response'] ?? ''));
|
||||
|
||||
if ($token === '') {
|
||||
throw new CaptchaValidationException('Captcha verification token is missing.');
|
||||
}
|
||||
|
||||
$secret = $this->config['secret_key'] ?? '';
|
||||
|
||||
if ($secret === '') {
|
||||
throw new CaptchaValidationException('Captcha secret key is not configured.');
|
||||
}
|
||||
|
||||
$data = [
|
||||
'secret' => $secret,
|
||||
'response' => $token,
|
||||
];
|
||||
|
||||
$remoteIp = $context['ip'] ?? null;
|
||||
|
||||
if (!empty($remoteIp)) {
|
||||
$data['remoteip'] = $remoteIp;
|
||||
}
|
||||
|
||||
$result = $this->sendVerificationRequest('https://challenges.cloudflare.com/turnstile/v0/siteverify', $data);
|
||||
|
||||
if (!($result['success'] ?? false)) {
|
||||
throw new CaptchaValidationException('Captcha verification failed.');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function sendVerificationRequest(string $url, array $data): array
|
||||
{
|
||||
$payload = http_build_query($data);
|
||||
|
||||
if (function_exists('curl_init')) {
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => $payload,
|
||||
CURLOPT_TIMEOUT => 10,
|
||||
CURLOPT_SSL_VERIFYPEER => true,
|
||||
]);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
|
||||
if ($response === false) {
|
||||
$error = curl_error($ch);
|
||||
curl_close($ch);
|
||||
throw new CaptchaValidationException('Captcha verification request failed: ' . $error);
|
||||
}
|
||||
|
||||
curl_close($ch);
|
||||
} else {
|
||||
$context = stream_context_create([
|
||||
'http' => [
|
||||
'method' => 'POST',
|
||||
'header' => "Content-type: application/x-www-form-urlencoded\r\n",
|
||||
'content' => $payload,
|
||||
'timeout' => 10,
|
||||
],
|
||||
]);
|
||||
|
||||
$response = file_get_contents($url, false, $context);
|
||||
|
||||
if ($response === false) {
|
||||
throw new CaptchaValidationException('Captcha verification request failed.');
|
||||
}
|
||||
}
|
||||
|
||||
$decoded = json_decode($response, true);
|
||||
|
||||
if (!is_array($decoded)) {
|
||||
throw new CaptchaValidationException('Unexpected captcha verification response.');
|
||||
}
|
||||
|
||||
return $decoded;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Captcha\Exceptions;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class CaptchaValidationException extends RuntimeException
|
||||
{
|
||||
}
|
||||
|
||||
27
config/captcha.php
Normal file
27
config/captcha.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'default' => nexus_env('CAPTCHA_DRIVER', 'image'),
|
||||
|
||||
'drivers' => [
|
||||
'image' => [
|
||||
'class' => \App\Services\Captcha\Drivers\ImageCaptchaDriver::class,
|
||||
],
|
||||
|
||||
'cloudflare_turnstile' => [
|
||||
'class' => \App\Services\Captcha\Drivers\TurnstileCaptchaDriver::class,
|
||||
'site_key' => nexus_env('TURNSTILE_SITE_KEY'),
|
||||
'secret_key' => nexus_env('TURNSTILE_SECRET_KEY'),
|
||||
'theme' => nexus_env('TURNSTILE_THEME', 'auto'),
|
||||
'size' => nexus_env('TURNSTILE_SIZE', 'auto'),
|
||||
],
|
||||
|
||||
'google_recaptcha_v2' => [
|
||||
'class' => \App\Services\Captcha\Drivers\RecaptchaV2CaptchaDriver::class,
|
||||
'site_key' => nexus_env('RECAPTCHA_SITE_KEY'),
|
||||
'secret_key' => nexus_env('RECAPTCHA_SECRET_KEY'),
|
||||
'theme' => nexus_env('RECAPTCHA_THEME', 'light'),
|
||||
'size' => nexus_env('RECAPTCHA_SIZE', 'normal'),
|
||||
],
|
||||
],
|
||||
];
|
||||
@@ -1767,56 +1767,102 @@ function random_str($length="6")
|
||||
}
|
||||
return $str;
|
||||
}
|
||||
function image_code () {
|
||||
$randomstr = random_str();
|
||||
$imagehash = md5($randomstr);
|
||||
$dateline = time();
|
||||
$sql = 'INSERT INTO `regimages` (`imagehash`, `imagestring`, `dateline`) VALUES (\''.$imagehash.'\', \''.$randomstr.'\', \''.$dateline.'\');';
|
||||
sql_query($sql);
|
||||
return $imagehash;
|
||||
function captcha_manager(): \App\Services\Captcha\CaptchaManager
|
||||
{
|
||||
static $manager;
|
||||
|
||||
if (!$manager) {
|
||||
$manager = new \App\Services\Captcha\CaptchaManager();
|
||||
}
|
||||
|
||||
return $manager;
|
||||
}
|
||||
|
||||
function check_code ($imagehash, $imagestring, $where = 'signup.php',$maxattemptlog=false,$head=true) {
|
||||
global $lang_functions;
|
||||
function image_code () {
|
||||
$driver = captcha_manager()->driver('image');
|
||||
|
||||
if (!method_exists($driver, 'issue')) {
|
||||
throw new \RuntimeException('Image captcha driver is unavailable.');
|
||||
}
|
||||
|
||||
return $driver->issue();
|
||||
}
|
||||
|
||||
function check_code ($imagehash, $imagestring, $where = 'signup.php', $maxattemptlog = false, $head = true) {
|
||||
global $lang_functions;
|
||||
global $iv;
|
||||
|
||||
if ($iv !== 'yes') {
|
||||
return true;
|
||||
}
|
||||
$query = sprintf("SELECT * FROM regimages WHERE imagehash='%s' AND imagestring='%s'",
|
||||
mysql_real_escape_string((string)$imagehash),
|
||||
mysql_real_escape_string((string)$imagestring)
|
||||
);
|
||||
$sql = sql_query($query);
|
||||
$imgcheck = mysql_fetch_array($sql);
|
||||
if(!$imgcheck['dateline']) {
|
||||
$delete = sprintf("DELETE FROM regimages WHERE imagehash='%s'",
|
||||
mysql_real_escape_string((string)$imagehash)
|
||||
);
|
||||
sql_query($delete);
|
||||
if (!$maxattemptlog)
|
||||
stderr('Error',$lang_functions['std_invalid_image_code']."<a href=\"".htmlspecialchars($where)."\">".$lang_functions['std_here_to_request_new'], false);
|
||||
else
|
||||
failedlogins($lang_functions['std_invalid_image_code']."<a href=\"".htmlspecialchars($where)."\">".$lang_functions['std_here_to_request_new'],true,$head);
|
||||
}else{
|
||||
$delete = sprintf("DELETE FROM regimages WHERE imagehash='%s'",
|
||||
mysql_real_escape_string((string)$imagehash)
|
||||
);
|
||||
sql_query($delete);
|
||||
return true;
|
||||
}
|
||||
|
||||
$manager = captcha_manager();
|
||||
|
||||
if (!$manager->isEnabled()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'imagehash' => $imagehash,
|
||||
'imagestring' => $imagestring,
|
||||
'request' => array_merge($_POST ?? [], $_GET ?? []),
|
||||
];
|
||||
|
||||
$context = [
|
||||
'where' => $where,
|
||||
'maxattemptlog' => $maxattemptlog,
|
||||
'head' => $head,
|
||||
'ip' => getip(),
|
||||
];
|
||||
|
||||
try {
|
||||
if ($manager->verify($payload, $context)) {
|
||||
return true;
|
||||
}
|
||||
} catch (\App\Services\Captcha\Exceptions\CaptchaValidationException $exception) {
|
||||
$message = $exception->getMessage();
|
||||
|
||||
$defaultMessage = $lang_functions['std_invalid_image_code'] . "<a href=\"" . htmlspecialchars($where) . "\">" . $lang_functions['std_here_to_request_new'];
|
||||
|
||||
if ($message === '' || $message === 'Invalid captcha response.' || $message === 'Missing captcha parameters.') {
|
||||
$message = $defaultMessage;
|
||||
}
|
||||
|
||||
if (!$maxattemptlog) {
|
||||
stderr('Error', $message, false);
|
||||
} else {
|
||||
failedlogins($message, true, $head);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function show_image_code () {
|
||||
global $lang_functions;
|
||||
global $iv;
|
||||
if ($iv == "yes") {
|
||||
unset($imagehash);
|
||||
$imagehash = image_code () ;
|
||||
print ("<tr><td class=\"rowhead\">".$lang_functions['row_security_image']."</td>");
|
||||
print ("<td align=\"left\"><img src=\"".htmlspecialchars("image.php?action=regimage&imagehash=".$imagehash."&secret=".($_GET['secret'] ?? ''))."\" border=\"0\" alt=\"CAPTCHA\" /></td></tr>");
|
||||
print ("<tr><td class=\"rowhead\">".$lang_functions['row_security_code']."</td><td align=\"left\">");
|
||||
print("<input type=\"text\" autocomplete=\"off\" style=\"width: 180px; border: 1px solid gray\" name=\"imagestring\" value=\"\" />");
|
||||
print("<input type=\"hidden\" name=\"imagehash\" value=\"$imagehash\" /></td></tr>");
|
||||
}
|
||||
global $lang_functions;
|
||||
global $iv;
|
||||
|
||||
if ($iv !== 'yes') {
|
||||
return;
|
||||
}
|
||||
|
||||
$manager = captcha_manager();
|
||||
|
||||
if (!$manager->isEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$markup = $manager->render([
|
||||
'labels' => [
|
||||
'image' => $lang_functions['row_security_image'],
|
||||
'code' => $lang_functions['row_security_code'],
|
||||
],
|
||||
'secret' => $_GET['secret'] ?? '',
|
||||
]);
|
||||
|
||||
if ($markup !== '') {
|
||||
echo $markup;
|
||||
}
|
||||
}
|
||||
|
||||
function get_ip_location($ip)
|
||||
|
||||
@@ -307,6 +307,7 @@ function nexus_config($key, $default = null)
|
||||
$files = [
|
||||
ROOT_PATH . 'config/nexus.php',
|
||||
ROOT_PATH . 'config/emoji.php',
|
||||
ROOT_PATH . 'config/captcha.php',
|
||||
];
|
||||
foreach ($files as $file) {
|
||||
$basename = basename($file);
|
||||
|
||||
@@ -18,7 +18,7 @@ if($_SERVER['REQUEST_METHOD'] === 'POST'){
|
||||
switch($action = filter_input(INPUT_POST, 'action', FILTER_SANITIZE_FULL_SPECIAL_CHARS)){
|
||||
case 'new':
|
||||
cur_user_check();
|
||||
check_code ($_POST['imagehash'], $_POST['imagestring'],'complains.php');
|
||||
check_code ($_POST['imagehash'] ?? null, $_POST['imagestring'] ?? null,'complains.php');
|
||||
\Nexus\Database\NexusLock::lockOrFail("complains:lock:" . getip(), 10);
|
||||
$email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
|
||||
\Nexus\Database\NexusLock::lockOrFail("complains:lock:" . $email, 600);
|
||||
|
||||
@@ -29,7 +29,7 @@ bark($lang_confirm_resend['std_need_admin_verification']);
|
||||
if ($_SERVER["REQUEST_METHOD"] == "POST")
|
||||
{
|
||||
if ($iv == "yes")
|
||||
check_code ($_POST['imagehash'], $_POST['imagestring'],"confirm_resend.php",true);
|
||||
check_code ($_POST['imagehash'] ?? null, $_POST['imagestring'] ?? null,"confirm_resend.php",true);
|
||||
$email = unesc(htmlspecialchars(trim($_POST["email"] ?? '')));
|
||||
$wantpassword = unesc(htmlspecialchars(trim($_POST["wantpassword"])));
|
||||
$passagain = unesc(htmlspecialchars(trim($_POST["passagain"])));
|
||||
|
||||
@@ -1,60 +1,22 @@
|
||||
<?php
|
||||
require_once("../include/bittorrent.php");
|
||||
dbconn();
|
||||
$action = $_GET['action'];
|
||||
$imagehash = $_GET['imagehash'];
|
||||
if($action == "regimage")
|
||||
{
|
||||
$query = "SELECT * FROM regimages WHERE imagehash= ".sqlesc($imagehash);
|
||||
$sql = sql_query($query);
|
||||
$regimage = mysql_fetch_array($sql);
|
||||
$imagestring = $regimage['imagestring'];
|
||||
$space = $newstring = '';
|
||||
for($i=0;$i<strlen($imagestring);$i++)
|
||||
{
|
||||
$newstring .= $space.$imagestring[$i];
|
||||
$space = " ";
|
||||
}
|
||||
$imagestring = $newstring;
|
||||
|
||||
if(function_exists("imagecreatefrompng"))
|
||||
{
|
||||
$fontwidth = imageFontWidth(5);
|
||||
$fontheight = imageFontHeight(5);
|
||||
$textwidth = $fontwidth*strlen($imagestring);
|
||||
$textheight = $fontheight;
|
||||
$action = $_GET['action'] ?? '';
|
||||
$imagehash = $_GET['imagehash'] ?? '';
|
||||
|
||||
$randimg = rand(1, 5);
|
||||
$im = imagecreatefrompng("pic/regimages/reg".$randimg.".png");
|
||||
|
||||
$imgheight = 40;
|
||||
$imgwidth = 150;
|
||||
$textposh = floor(($imgwidth-$textwidth)/2);
|
||||
$textposv = floor(($imgheight-$textheight)/2);
|
||||
|
||||
$dots = $imgheight*$imgwidth/35;
|
||||
$gd = imagecreatetruecolor($imgwidth, $imgheight);
|
||||
for($i=1;$i<=$dots;$i++)
|
||||
{
|
||||
imagesetpixel($im, rand(0, $imgwidth), rand(0, $imgheight), imagecolorallocate($gd, rand(0, 255), rand(0, 255), rand(0, 255)));
|
||||
}
|
||||
|
||||
$textcolor = imagecolorallocate($im, 0, 0, 0);
|
||||
imagestring($im, 5, $textposh, $textposv, $imagestring, $textcolor);
|
||||
|
||||
// output the image
|
||||
header("Content-type: image/png");
|
||||
imagepng($im);
|
||||
imagedestroy($im);
|
||||
exit;
|
||||
}
|
||||
else
|
||||
{
|
||||
header("Location: pic/clear.gif");
|
||||
}
|
||||
if ($action !== 'regimage') {
|
||||
http_response_code(404);
|
||||
exit('Invalid captcha action');
|
||||
}
|
||||
else
|
||||
{
|
||||
die('invalid action');
|
||||
|
||||
$driver = captcha_manager()->driver('image');
|
||||
|
||||
if (!method_exists($driver, 'outputImage')) {
|
||||
http_response_code(404);
|
||||
exit('Captcha driver does not support image rendering');
|
||||
}
|
||||
|
||||
$driver->outputImage($imagehash);
|
||||
|
||||
?>
|
||||
|
||||
@@ -28,7 +28,7 @@ $mailTwoFour = sprintf($lang_recover['mail_two_four'], $siteName);
|
||||
if ($_SERVER["REQUEST_METHOD"] == "POST")
|
||||
{
|
||||
if ($iv == "yes")
|
||||
check_code ($_POST['imagehash'], $_POST['imagestring'],"recover.php",true);
|
||||
check_code ($_POST['imagehash'] ?? null, $_POST['imagestring'] ?? null,"recover.php",true);
|
||||
$email = unesc(htmlspecialchars(trim($_POST["email"] ?? '')));
|
||||
$email = safe_email($email);
|
||||
if (!$email)
|
||||
|
||||
@@ -15,7 +15,7 @@ function bark($text = "")
|
||||
stderr($lang_takelogin['std_login_fail'], $text,false);
|
||||
}
|
||||
if ($iv == "yes") {
|
||||
check_code ($_POST['imagehash'], $_POST['imagestring'],'login.php',true);
|
||||
check_code($_POST['imagehash'] ?? null, $_POST['imagestring'] ?? null, 'login.php', true);
|
||||
}
|
||||
//同时支持新旧两种登录方式
|
||||
$useChallengeResponse = \App\Models\Setting::getIsUseChallengeResponseAuthentication();
|
||||
|
||||
@@ -21,13 +21,13 @@ if ($type == 'invite'){
|
||||
registration_check();
|
||||
failedloginscheck ("Invite Signup");
|
||||
if ($iv == "yes")
|
||||
check_code ($_POST['imagehash'], $_POST['imagestring'],'signup.php?type=invite&invitenumber='.htmlspecialchars($_POST['hash']));
|
||||
check_code ($_POST['imagehash'] ?? null, $_POST['imagestring'] ?? null,'signup.php?type=invite&invitenumber='.htmlspecialchars($_POST['hash']));
|
||||
}
|
||||
else{
|
||||
registration_check("normal");
|
||||
failedloginscheck ("Signup");
|
||||
if ($iv == "yes")
|
||||
check_code ($_POST['imagehash'], $_POST['imagestring']);
|
||||
check_code ($_POST['imagehash'] ?? null, $_POST['imagestring'] ?? null);
|
||||
}
|
||||
function isportopen($port)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user