Added CaptchaManager

This commit is contained in:
Zi Xing 2022-02-05 15:45:46 -05:00
parent 8ed19d25e3
commit 2c85589be3
13 changed files with 550 additions and 0 deletions

25
database/captcha.sql Normal file
View File

@ -0,0 +1,25 @@
create table if not exists captcha
(
id varchar(126) not null comment 'The unique public ID for the captcha instance'
primary key,
captcha_instance_id varchar(32) not null comment 'The captcha instance that generates and owns this captcha',
captcha_type varchar(64) null comment 'The type of captcha that is shown to the user',
value varchar(256) null comment 'The value(s) used to generate the captcha to the user',
answer varchar(256) null comment 'The expected answer from the user or software',
host varchar(128) null comment 'The host that initialized the captcha instance',
fail_reason varchar(64) null comment 'The current fail reason of the captcha',
created_timestamp int null comment 'The Unix Timestamp for when this captcha was first created',
expiration_timestamp int null comment 'The Unix Timestamp for when this captcha will be expired',
constraint captcha_id_uindex
unique (id),
constraint captcha_instances_id_fk
foreign key (captcha_instance_id) references instances (id)
)
comment 'Table for housing generated captcha instances';
create index captcha_fail_reason_index
on captcha (fail_reason);
create index captcha_host_index
on captcha (host);

25
database/instances.sql Normal file
View File

@ -0,0 +1,25 @@
create table if not exists instances
(
id varchar(32) not null comment 'The Unique ID of the captcha instance'
primary key,
name varchar(256) null comment 'The name of the captcha instance (URL Encoded)',
captcha_type varchar(64) not null comment 'The type of captcha that this instance uses',
owner_id varchar(64) not null comment 'The ID of the owner that manages this captcha instance',
secret_key varchar(48) not null comment 'The secret key for the captcha instance (API Key)',
enabled tinyint(1) default 1 not null comment 'Indicates if the instance is enabled or not',
firewall_options blob not null comment 'ZiProto encoded object of the firewall options that this captcha has enabled',
created_timestamp int default 0 not null comment 'The Unix Timestamp for when this instance was created',
last_updated_timestamp int default 0 null comment 'The Unix Timestamp for when this instance was last updated',
constraint instances_id_owner_id_uindex
unique (id, owner_id),
constraint instances_id_uindex
unique (id)
)
comment 'Table for housing captcha instances';
create index instances_enabled_index
on instances (enabled);
create index instances_owner_id_index
on instances (owner_id);

View File

@ -0,0 +1,21 @@
<?php
namespace vCaptcha\Abstracts;
abstract class CaptchaStatus
{
/**
* The captcha is awaiting the verification system to pass
*/
const AwaitingVerification = 'AWAITING_VERIFICATION';
/**
* The captcha verification passed successfully
*/
const VerificationPassed = 'VERIFICATION_PASSED';
/**
* The captcha verification failed, see the fail reason for more details
*/
const VerificationFailed = 'VERIFICATION_FAILED';
}

View File

@ -4,5 +4,7 @@
abstract class CaptchaType
{
const None = 'NONE';
const ImageTextScramble = 'IMAGE_TEXT_SCRAMBLE';
}

View File

@ -0,0 +1,47 @@
<?php
namespace vCaptcha\Abstracts;
abstract class FailReason
{
/**
* No fail reason has been set, the captcha state is awaiting an answer or the verification was a success
*/
const None = 'NONE';
/**
* The user provided an incorrect answer, a new captcha must be generated.
*/
const IncorrectAnswer = 'INCORRECT_ANSWER';
/**
* The firewall blocked the user due to it being identified as tor traffic
*/
const TorBlocked = 'TOR_BLOCKED';
/**
* The firewall blocked the user due to reports of malicious activities coming from the host
*/
const MaliciousTrafficBlocked = 'MALICIOUS_TRAFFIC_BLOCKED';
/**
* The captcha was already used, a new captcha must be generated
*/
const AlreadyUsed = 'ALREADY_USED';
/**
* The host has changed since the time the captcha was created and the captcha was validated, this could mean
* the captcha was resolved by a third-party source. A new captcha must be generated
*/
const HostMismatch = 'HOST_MISMATCH';
/**
* The captcha has expired, a new captcha must be generated
*/
const Expired = 'EXPIRED';
/**
* There was an unexpected issue on the server-side
*/
const UnexpectedError = 'UNEXPECTED_ERROR';
}

View File

@ -38,4 +38,25 @@
{
return hash('crc32', $id) . hash('crc32', $owner_id) . hash('haval128,3', self::pepper($id . $owner_id . time()));
}
/**
* Generates a random string
*
* @param int $min_length
* @param int $max_length
* @param $characters
* @return string
*/
public static function generateRandomString(int $min_length=3, int $max_length=5, $characters='ABCDEFGHIJKLMNOPQRSTUVWXYZ'): string
{
$charactersLength = strlen($characters);
$randomString = '';
for ($i = 0; $i < rand($min_length, $max_length); $i++)
{
$randomString .= $characters[rand(0, $charactersLength - 1)];
}
return $randomString;
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace vCaptcha\Exceptions;
use JetBrains\PhpStorm\Pure;
use Throwable;
class CaptchaNotFoundException extends \Exception
{
/**
* @param string $message
* @param int $code
* @param Throwable|null $previous
*/
#[Pure] public function __construct(string $message = "", int $code = 0, ?Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
$this->message = $message;
$this->code = $code;
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace vCaptcha\Exceptions;
use Exception;
use JetBrains\PhpStorm\Pure;
use Throwable;
class HostRequiredException extends Exception
{
/**
* @param string $message
* @param int $code
* @param Throwable|null $previous
*/
#[Pure] public function __construct(string $message = "", int $code = 0, Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
}
}

View File

@ -0,0 +1,155 @@
<?php
/** @noinspection PhpMissingFieldTypeInspection */
namespace vCaptcha\Managers;
use msqg\QueryBuilder;
use Symfony\Component\Uid\Uuid;
use vCaptcha\Abstracts\CaptchaStatus;
use vCaptcha\Abstracts\CaptchaType;
use vCaptcha\Abstracts\FailReason;
use vCaptcha\Classes\Security;
use vCaptcha\Exceptions\CaptchaNotFoundException;
use vCaptcha\Exceptions\DatabaseException;
use vCaptcha\Exceptions\HostRequiredException;
use vCaptcha\Objects\Captcha;
use vCaptcha\Objects\CaptchaInstance;
use vCaptcha\vCaptcha;
class CaptchaManager
{
private $vcaptcha;
/**
* @param vCaptcha $vcaptcha
*/
public function __construct(vCaptcha $vcaptcha)
{
$this->vcaptcha = $vcaptcha;
}
/**
* Creates a new Captcha instance
*
* @param CaptchaInstance $captchaInstance
* @param string|null $host
* @return Captcha
* @throws DatabaseException
* @throws HostRequiredException
* @noinspection PhpCastIsUnnecessaryInspection
*/
public function createCaptcha(CaptchaInstance $captchaInstance, ?string $host=null): Captcha
{
$CaptchaObject = new Captcha();
$CaptchaObject->ID = Uuid::v1()->toRfc4122();
$CaptchaObject->CaptchaInstanceID = $captchaInstance->ID;
$CaptchaObject->CaptchaType = $captchaInstance->CaptchaType;
$CaptchaObject->Host = $host;
$CaptchaObject->FailReason = FailReason::None;
$CaptchaObject->CaptchaStatus = CaptchaStatus::AwaitingVerification;
$CaptchaObject->CreatedTimestamp = time();
$CaptchaObject->ExpirationTimestamp = time() + 130;
switch($captchaInstance->CaptchaType)
{
case CaptchaType::ImageTextScramble:
$CaptchaObject->Value = Security::generateRandomString();
$CaptchaObject->Answer = $CaptchaObject->Value;
break;
case CaptchaType::None:
default:
$CaptchaObject->Value = null;
$CaptchaObject->Answer = null;
break;
}
if($CaptchaObject->CaptchaType == CaptchaType::None && $host == null)
throw new HostRequiredException('The host must be set if the captcha type is None');
if($captchaInstance->FirewallOptions->HostMismatchProtection = true)
throw new HostRequiredException('The host must be set if Host Mismatch Protection is enabled');
if($captchaInstance->FirewallOptions->DisallowAbusiveHosts)
throw new HostRequiredException('The host must be set if Abusive Hosts Protection is enabled');
if($captchaInstance->FirewallOptions->DisallowTor)
throw new HostRequiredException('The host must be set if Tor traffic filter is enabled');
$Query = QueryBuilder::insert_into('captcha', [
'id' => $this->vcaptcha->getDatabase()->real_escape_string($CaptchaObject->ID),
'captcha_instance_id' => $this->vcaptcha->getDatabase()->real_escape_string($CaptchaObject->CaptchaInstanceID),
'captcha_type' => $this->vcaptcha->getDatabase()->real_escape_string($CaptchaObject->CaptchaType),
'value' => ($CaptchaObject->Value == null ? null : $this->vcaptcha->getDatabase()->real_escape_string($CaptchaObject->Value)),
'answer' => ($CaptchaObject->Answer == null ? null : $this->vcaptcha->getDatabase()->real_escape_string($CaptchaObject->Answer)),
'host' => ($CaptchaObject->Host == null ? null : $this->vcaptcha->getDatabase()->real_escape_string($CaptchaObject->Host)),
'fail_reason' => $this->vcaptcha->getDatabase()->real_escape_string($CaptchaObject->FailReason),
'created_timestamp' => (int)$CaptchaObject->CreatedTimestamp,
'expiration_timestamp' => (int)$CaptchaObject->ExpirationTimestamp
]);
$QueryResults = $this->vcaptcha->getDatabase()->query($Query);
if($QueryResults == false)
throw new DatabaseException($this->vcaptcha->getDatabase()->error, $Query, $this->vcaptcha->getDatabase()->errno);
return $CaptchaObject;
}
/**
* Returns an existing captcha instance from the database
*
* @param string $captcha_id
* @return Captcha
* @throws CaptchaNotFoundException
* @throws DatabaseException
*/
public function getCaptcha(string $captcha_id): Captcha
{
$Query = QueryBuilder::select('captcha', [
'id',
'captcha_instance_id',
'captcha_type',
'value',
'answer',
'host',
'fail_reason',
'created_timestamp',
'expiration_timestamp'
], 'id', $this->vcaptcha->getDatabase()->real_escape_string($captcha_id));
$QueryResults = $this->vcaptcha->getDatabase()->query($Query);
if($QueryResults == false)
throw new DatabaseException($this->vcaptcha->getDatabase()->error, $Query, $this->vcaptcha->getDatabase()->errno);
if($QueryResults->num_rows == 0)
throw new CaptchaNotFoundException();
return Captcha::fromArray($QueryResults->fetch_array(MYSQLI_ASSOC));
}
/**
* Updates an existing captcha in the database
*
* @param Captcha $captcha
* @return Captcha
* @throws DatabaseException
*/
public function updateCaptcha(Captcha $captcha): Captcha
{
$captcha->sync();
$Query = QueryBuilder::update('captcha', [
'fail_reason' => $this->vcaptcha->getDatabase()->real_escape_string($captcha->FailReason)
], 'id', $this->vcaptcha->getDatabase()->real_escape_string($captcha->ID));
$QueryResults = $this->vcaptcha->getDatabase()->query($Query);
if($QueryResults == false)
throw new DatabaseException($this->vcaptcha->getDatabase()->error, $Query, $this->vcaptcha->getDatabase()->errno);
return $captcha;
}
}

View File

@ -0,0 +1,161 @@
<?php
/** @noinspection PhpMissingFieldTypeInspection */
namespace vCaptcha\Objects;
use vCaptcha\Abstracts\CaptchaStatus;
use vCaptcha\Abstracts\CaptchaType;
use vCaptcha\Abstracts\FailReason;
class Captcha
{
/**
* The ID of the captcha
*
* @var string
*/
public $ID;
/**
* The Captcha Instance ID that owns this captcha
*
* @var string
*/
public $CaptchaInstanceID;
/**
* The captcha type that was created
*
* @var string|CaptchaType
*/
public $CaptchaType;
/**
* The value to display to the user
*
* @var string|null
*/
public $Value;
/**
* The captcha answer that user must provide to be correct
*
* @var string|null
*/
public $Answer;
/**
* The IPV4/IPV6 Host that initialized the captcha verification
*
* @var string
*/
public $Host;
/**
* The current status of the captcha
*
* @var string|CaptchaStatus
*/
public $CaptchaStatus;
/**
* The reason the captcha verification failed if the captcha status is set to VERIFICATION_FAILED
*
* @var string|FailReason
*/
public $FailReason;
/**
* The Unix Timestamp for when this captcha was created
*
* @var int
*/
public $CreatedTimestamp;
/**
* The Unix Timestamp for when this captcha expires
*
* @var int
*/
public $ExpirationTimestamp;
/**
* Syncs the captcha state
*
* @return void
*/
public function sync()
{
if(time() >= $this->ExpirationTimestamp)
{
$this->CaptchaStatus = CaptchaStatus::VerificationFailed;
$this->FailReason = FailReason::Expired;
}
}
/**
* Returns an array representation of the object
*
* @return array
*/
public function toArray(): array
{
return [
'id' => $this->ID,
'captcha_instance_id' => $this->CaptchaInstanceID,
'captcha_type' => $this->CaptchaType,
'value' => $this->Value,
'answer' => $this->Answer,
'host' => $this->Host,
'captcha_status' => $this->CaptchaStatus,
'fail_reason' => $this->FailReason,
'created_timestamp' => $this->CreatedTimestamp,
'expiration_timestamp' => $this->ExpirationTimestamp
];
}
/**
* Constructs object from an array representation
*
* @param array $data
* @return Captcha
*/
public static function fromArray(array $data): Captcha
{
$CaptchaObject = new Captcha();
if(isset($data['id']))
$CaptchaObject->ID = $data['id'];
if(isset($data['captcha_instance_id']))
$CaptchaObject->CaptchaInstanceID = $data['captcha_instance_id'];
if(isset($data['captcha_type']))
$CaptchaObject->CaptchaType = $data['captcha_type'];
if(isset($data['value']))
$CaptchaObject->Value = $data['value'];
if(isset($data['answer']))
$CaptchaObject->Answer = $data['answer'];
if(isset($data['host']))
$CaptchaObject->Host = $data['host'];
if(isset($data['captcha_status']))
$CaptchaObject->CaptchaStatus = $data['captcha_status'];
if(isset($data['fail_reason']))
$CaptchaObject->FailReason = $data['fail_reason'];
if(isset($data['created_timestamp']))
$CaptchaObject->CreatedTimestamp = (int)$data['created_timestamp'];
if(isset($data['expiration_timestamp']))
$CaptchaObject->ExpirationTimestamp = (int)$data['expiration_timestamp'];
$CaptchaObject->sync();
return $CaptchaObject;
}
}

View File

@ -35,10 +35,18 @@
*/
public $CountryFilterMode;
/**
* Indicates if the server should block requests from host mismatches
*
* @var bool
*/
public $HostMismatchProtection;
public function __construct()
{
$this->DisallowTor = false;
$this->DisallowAbusiveHosts = true;
$this->HostMismatchProtection = true;
$this->CountryFilterList = [];
$this->CountryFilterMode = CountryFilterMode::Disabled;
}
@ -51,6 +59,7 @@
return [
'disallow_tor' => $this->DisallowTor,
'disallow_abusive_hosts' => $this->DisallowAbusiveHosts,
'host_mismatch_protection' => $this->HostMismatchProtection,
'country_filter_list' => $this->CountryFilterList,
'country_filter_mode' => $this->CountryFilterMode
];
@ -72,6 +81,9 @@
if(isset($data['disallow_abusive_hosts']))
$FirewallOptionsObject->DisallowAbusiveHosts = (bool)$data['disallow_abusive_hosts'];
if(isset($data['host_mismatch_protection']))
$FirewallOptionsObject->HostMismatchProtection = (bool)$data['host_mismatch_protection'];
if(isset($data['country_filter_list']))
$FirewallOptionsObject->CountryFilterList = $data['country_filter_list'];

View File

@ -45,6 +45,14 @@
"required": true,
"file": "Abstracts/CountryFilterMode.php"
},
{
"required": true,
"file": "Abstracts/CaptchaStatus.php"
},
{
"required": true,
"file": "Abstracts/FailReason.php"
},
{
"required": true,
"file": "Abstracts/CaptchaType.php"
@ -61,10 +69,18 @@
"required": true,
"file": "vCaptcha.php"
},
{
"required": true,
"file": "Exceptions/HostRequiredException.php"
},
{
"required": true,
"file": "Exceptions/DatabaseException.php"
},
{
"required": true,
"file": "Exceptions/CaptchaNotFoundException.php"
},
{
"required": true,
"file": "Exceptions/CaptchaInstanceNotFoundException.php"
@ -73,6 +89,10 @@
"required": true,
"file": "Exceptions/InvalidCaptchaNameException.php"
},
{
"required": true,
"file": "Objects/Captcha.php"
},
{
"required": true,
"file": "Objects/SampleCaptchaInstance.php"
@ -85,6 +105,10 @@
"required": true,
"file": "Objects/CaptchaInstance/FirewallOptions.php"
},
{
"required": true,
"file": "Managers/CaptchaManager.php"
},
{
"required": true,
"file": "Managers/CaptchaInstanceManager.php"

View File

@ -9,6 +9,7 @@
use acm2\Objects\Schema;
use mysqli;
use vCaptcha\Managers\CaptchaInstanceManager;
use vCaptcha\Managers\CaptchaManager;
class vCaptcha
{
@ -32,6 +33,11 @@
*/
private $CaptchaInstanceManager;
/**
* @var CaptchaManager
*/
private $CaptchaManager;
/**
* @throws ConfigurationNotDefinedException
*/
@ -52,6 +58,7 @@
$this->DatabaseConfiguration = $this->acm->getConfiguration('Database');
$this->CaptchaInstanceManager = new CaptchaInstanceManager($this);
$this->CaptchaManager = new CaptchaManager($this);
}
/**
@ -102,4 +109,12 @@
{
return $this->CaptchaInstanceManager;
}
/**
* @return CaptchaManager
*/
public function getCaptchaManager(): CaptchaManager
{
return $this->CaptchaManager;
}
}