Added IP registration blacklisting.
This commit is contained in:
parent
a88464d67f
commit
abf67dd366
8 changed files with 223 additions and 70 deletions
|
@ -1,9 +1,12 @@
|
|||
.profile__warning {
|
||||
margin: 2px;
|
||||
padding: 1px;
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--accent-colour);
|
||||
|
||||
&__container {
|
||||
margin: 2px 0;
|
||||
}
|
||||
|
||||
&--warning {
|
||||
--accent-colour: #666;
|
||||
}
|
||||
|
@ -16,31 +19,58 @@
|
|||
--accent-colour: #c33;
|
||||
}
|
||||
|
||||
&__public {
|
||||
&__background {
|
||||
background-color: var(--accent-colour);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&__content {
|
||||
background-color: var(--background-colour-translucent);
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: 1px;
|
||||
}
|
||||
|
||||
&__type,
|
||||
&__created,
|
||||
&__duration {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&__type {
|
||||
min-width: 60px;
|
||||
text-align: center;
|
||||
min-width: 80px;
|
||||
background-color: var(--accent-colour);
|
||||
border-radius: 1px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
&__datetime {
|
||||
&__created,
|
||||
&__duration {
|
||||
min-width: 100px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
&__note {
|
||||
padding: 1px 4px;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
&__private {
|
||||
border-top: 1px solid var(--accent-colour);
|
||||
margin-top: 1px;
|
||||
padding: 1px 4px;
|
||||
width: 100%;
|
||||
opacity: .5;
|
||||
transition: opacity .2s;
|
||||
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,4 +13,15 @@
|
|||
background-color: rgba(17, 17, 17, .9);
|
||||
padding: 2px 5px;
|
||||
}
|
||||
|
||||
&__link {
|
||||
color: inherit;
|
||||
text-decoration: underline dotted;
|
||||
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,13 +13,14 @@ function migrate_up(PDO $conn): void
|
|||
`issuer_id` INT(10) UNSIGNED NULL DEFAULT NULL,
|
||||
`issuer_ip` VARBINARY(16) NOT NULL,
|
||||
`warning_created` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`warning_duration` TIMESTAMP NULL DEFAULT NULL,
|
||||
`warning_type` TINYINT(3) UNSIGNED NOT NULL,
|
||||
`warning_note` VARCHAR(255) NOT NULL,
|
||||
`warning_note_private` TEXT NOT NULL,
|
||||
`warning_note_private` TEXT NULL DEFAULT NULL,
|
||||
PRIMARY KEY (`warning_id`),
|
||||
INDEX `user_warnings_user_foreign` (`user_id`),
|
||||
INDEX `user_warnings_issuer_foreign` (`issuer_id`),
|
||||
INDEX `user_warnings_indices` (`warning_created`, `warning_type`),
|
||||
INDEX `user_warnings_indices` (`warning_created`, `warning_type`, `warning_duration`),
|
||||
CONSTRAINT `user_warnings_issuer_foreign`
|
||||
FOREIGN KEY (`issuer_id`)
|
||||
REFERENCES `msz_users` (`user_id`)
|
||||
|
@ -32,6 +33,14 @@ function migrate_up(PDO $conn): void
|
|||
ON DELETE CASCADE
|
||||
)
|
||||
");
|
||||
|
||||
$conn->exec("
|
||||
CREATE TABLE `msz_ip_blacklist` (
|
||||
`ip_subnet` VARBINARY(16) NOT NULL,
|
||||
`ip_mask` TINYINT(3) UNSIGNED NOT NULL,
|
||||
UNIQUE INDEX `ip_blacklist_unique` (`ip_subnet`, `ip_mask`)
|
||||
)
|
||||
");
|
||||
}
|
||||
|
||||
function migrate_down(PDO $conn): void
|
||||
|
|
|
@ -23,6 +23,7 @@ $authEmail = $isSubmission ? ($_POST['auth']['email'] ?? '') : ($_GET['email'] ?
|
|||
$authPassword = $_POST['auth']['password'] ?? '';
|
||||
$authVerification = $_POST['auth']['verification'] ?? '';
|
||||
$authRedirect = $_POST['auth']['redirect'] ?? $_GET['redirect'] ?? $_SERVER['HTTP_REFERER'] ?? '/';
|
||||
$authRestricted = ip_blacklist_check(ip_remote_address());
|
||||
|
||||
tpl_vars([
|
||||
'can_create_account' => $canCreateAccount,
|
||||
|
@ -31,6 +32,7 @@ tpl_vars([
|
|||
'auth_username' => $authUsername,
|
||||
'auth_email' => $authEmail,
|
||||
'auth_redirect' => $authRedirect,
|
||||
'auth_restricted' => $authRestricted,
|
||||
]);
|
||||
|
||||
switch ($authMode) {
|
||||
|
@ -300,7 +302,7 @@ MSG;
|
|||
$authRegistrationError = '';
|
||||
|
||||
while ($isSubmission) {
|
||||
if (!$canCreateAccount) {
|
||||
if (!$canCreateAccount || $authRestricted) {
|
||||
$authRegistrationError = 'You may not create an account right now.';
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -58,7 +58,7 @@ function ip_get_raw_width(int $version): int
|
|||
return MSZ_IP_SIZES[$version] ?? 0;
|
||||
}
|
||||
|
||||
function ip_match_cidr_raw(string $address, string $subnet, int $mask = 0): bool
|
||||
function ip_match_cidr_raw(string $address, string $subnet, ?int $mask = null): bool
|
||||
{
|
||||
$version = ip_get_raw_version($subnet);
|
||||
$bits = ip_get_raw_width($version) * 8;
|
||||
|
@ -72,7 +72,7 @@ function ip_match_cidr_raw(string $address, string $subnet, int $mask = 0): bool
|
|||
}
|
||||
|
||||
for ($i = 0; $i < ceil($mask / 8); $i++) {
|
||||
$byteMask = (0xFF00 >> min(8, $mask - ($i * 8))) & 0xFF;
|
||||
$byteMask = (0xFF00 >> max(0, min(8, $mask - ($i * 8)))) & 0xFF;
|
||||
$addressByte = ord($address[$i]) & $byteMask;
|
||||
$subnetByte = ord($subnet[$i]) & $byteMask;
|
||||
|
||||
|
@ -85,6 +85,15 @@ function ip_match_cidr_raw(string $address, string $subnet, int $mask = 0): bool
|
|||
}
|
||||
|
||||
function ip_match_cidr(string $address, string $cidr): bool
|
||||
{
|
||||
$address = inet_pton($address);
|
||||
[$subnet, $mask] = ['', 0];
|
||||
extract(ip_cidr_to_raw($cidr));
|
||||
|
||||
return ip_match_cidr_raw($address, $subnet, $mask);
|
||||
}
|
||||
|
||||
function ip_cidr_to_raw(string $cidr): array
|
||||
{
|
||||
if (strpos($cidr, '/') !== false) {
|
||||
[$subnet, $mask] = explode('/', $cidr, 2);
|
||||
|
@ -92,8 +101,56 @@ function ip_match_cidr(string $address, string $cidr): bool
|
|||
$subnet = $cidr;
|
||||
}
|
||||
|
||||
$address = inet_pton($address);
|
||||
$subnet = inet_pton($subnet);
|
||||
$mask = empty($mask) ? null : $mask;
|
||||
|
||||
return ip_match_cidr_raw($address, $subnet, $mask ?? 0);
|
||||
return compact('subnet', 'mask');
|
||||
}
|
||||
|
||||
function ip_blacklist_check(string $address): bool
|
||||
{
|
||||
$checkBlacklist = db_prepare("
|
||||
SELECT COUNT(*) > 0
|
||||
FROM `msz_ip_blacklist`
|
||||
WHERE LENGTH(`ip_subnet`) = LENGTH(INET6_ATON(:ip1))
|
||||
AND `ip_subnet` & LPAD('', LENGTH(`ip_subnet`), X'FF') << LENGTH(`ip_subnet`) * 8 - `ip_mask`
|
||||
= INET6_ATON(:ip2) & LPAD('', LENGTH(`ip_subnet`), X'FF') << LENGTH(`ip_subnet`) * 8 - `ip_mask`
|
||||
");
|
||||
$checkBlacklist->bindValue('ip1', $address);
|
||||
$checkBlacklist->bindValue('ip2', $address);
|
||||
return (bool)($checkBlacklist->execute() ? $checkBlacklist->fetchColumn() : false);
|
||||
}
|
||||
|
||||
function ip_blacklist_add_raw(string $subnet, ?int $mask = null): bool
|
||||
{
|
||||
$version = ip_get_raw_version($subnet);
|
||||
|
||||
if ($version === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$bits = ip_get_raw_width($version) * 8;
|
||||
|
||||
if (empty($mask)) {
|
||||
$mask = $bits;
|
||||
} elseif ($mask < 1 || $mask > $bits) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$addBlacklist = db_prepare('
|
||||
INSERT INTO `msz_ip_blacklist`
|
||||
(`ip_subnet`, `ip_mask`)
|
||||
VALUES
|
||||
(:subnet, :mask)
|
||||
');
|
||||
$addBlacklist->bindValue('subnet', $subnet);
|
||||
$addBlacklist->bindValue('mask', $mask);
|
||||
return $addBlacklist->execute();
|
||||
}
|
||||
|
||||
function ip_blacklist_add(string $cidr): bool
|
||||
{
|
||||
[$subnet, $mask] = ['', 0];
|
||||
extract(ip_cidr_to_raw($cidr));
|
||||
return ip_blacklist_add_raw($subnet, $mask);
|
||||
}
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
<?php
|
||||
define('MSZ_WARN_NOTE', 0);
|
||||
define('MSZ_WARN_SILENCE', 1);
|
||||
define('MSZ_WARN_BAN', 2);
|
||||
define('MSZ_WARN_WARNING', 1);
|
||||
define('MSZ_WARN_SILENCE', 2);
|
||||
define('MSZ_WARN_BAN', 3);
|
||||
|
||||
define('MSZ_WARN_TYPES', [
|
||||
MSZ_WARN_NOTE, MSZ_WARN_SILENCE, MSZ_WARN_BAN,
|
||||
MSZ_WARN_NOTE, MSZ_WARN_WARNING, MSZ_WARN_SILENCE, MSZ_WARN_BAN,
|
||||
]);
|
||||
|
||||
function user_warning_add(
|
||||
|
@ -78,7 +79,8 @@ function user_warning_fetch(
|
|||
'
|
||||
SELECT
|
||||
uw.`warning_id`, uw.`warning_created`, uw.`warning_type`, uw.`warning_note`,
|
||||
uw.`warning_note_private`, uw.`user_id`, uw.`issuer_id`,
|
||||
uw.`warning_note_private`, uw.`user_id`, uw.`issuer_id`, uw.`warning_duration`,
|
||||
TIMESTAMPDIFF(SECOND, uw.`warning_created`, uw.`warning_duration`) AS `warning_duration_secs`,
|
||||
INET6_NTOA(uw.`user_ip`) AS `user_ip`, INET6_NTOA(uw.`issuer_ip`) AS `issuer_ip`,
|
||||
iu.`username` AS `issuer_username`
|
||||
FROM `msz_user_warnings` AS uw
|
||||
|
|
|
@ -19,22 +19,30 @@
|
|||
|
||||
{{ container_title('Register') }}
|
||||
|
||||
{% if auth_register_error is defined %}
|
||||
{% if auth_restricted %}
|
||||
<div class="warning auth__warning">
|
||||
<div class="warning__content">
|
||||
{{ auth_register_error }}
|
||||
The IP address from which you are visiting the website appears on our blacklist, you are not allowed to register from this address but if you already have an account you can log in just fine using the form above. If you think this blacklisting is a mistake, please <a href="/info.php/contact" class="warning__link">contact us</a>!
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
{% if auth_register_error is defined %}
|
||||
<div class="warning auth__warning">
|
||||
<div class="warning__content">
|
||||
{{ auth_register_error }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="auth__form">
|
||||
{{ input_text('auth[username]', 'auth__input', auth_username|default(''), 'text', 'Username', true, null, 0, auth_mode == 'register') }}
|
||||
{{ input_text('auth[password]', 'auth__input', '', 'password', 'Password', true) }}
|
||||
{{ input_text('auth[email]', 'auth__input', auth_email|default(''), 'text', 'E-mail', true) }}
|
||||
{{ input_text('auth[meow]', 'auth__input', '', 'text', 'What is the outcome of nine plus ten?', true) }}
|
||||
|
||||
<button class="input__button">Sign up</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="auth__form">
|
||||
{{ input_text('auth[username]', 'auth__input', auth_username|default(''), 'text', 'Username', true, null, 0, auth_mode == 'register') }}
|
||||
{{ input_text('auth[password]', 'auth__input', '', 'password', 'Password', true) }}
|
||||
{{ input_text('auth[email]', 'auth__input', auth_email|default(''), 'text', 'E-mail', true) }}
|
||||
{{ input_text('auth[meow]', 'auth__input', '', 'text', 'What is the outcome of nine plus ten?', true) }}
|
||||
|
||||
<button class="input__button">Sign up</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
|
|
|
@ -156,47 +156,6 @@
|
|||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if warnings|length > 0 %}
|
||||
<div class="container">
|
||||
{{ container_title('Account Standing') }}
|
||||
|
||||
{% for warning in warnings %}
|
||||
{% if warning.warning_type == constant('MSZ_WARN_SILENCE') %}
|
||||
{% set warning_text = 'Silence' %}
|
||||
{% set warning_class = 'silence' %}
|
||||
{% elseif warning.warning_type == constant('MSZ_WARN_BAN') %}
|
||||
{% set warning_text = 'Ban' %}
|
||||
{% set warning_class = 'ban' %}
|
||||
{% else %}
|
||||
{% set warning_text = 'Warning' %}
|
||||
{% set warning_class = 'warning' %}
|
||||
{% endif %}
|
||||
|
||||
<div class="profile__warning profile__warning--{{ warning_class }}" id="warning-{{ warning.warning_id }}">
|
||||
<div class="profile__warning__public">
|
||||
<div class="profile__warning__type">
|
||||
{{ warning_text }}
|
||||
</div>
|
||||
|
||||
<time datetime="{{ warning.warning_created|date('c') }}" title="{{ warning.warning_created|date('r') }}" class="profile__warning__datetime">
|
||||
{{ warning.warning_created|time_diff }}
|
||||
</time>
|
||||
|
||||
<div class="profile__warning__note">
|
||||
{{ warning.warning_note }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if can_view_private_note and warning.warning_note_private|length > 0 %}
|
||||
<div class="profile__warning__private">
|
||||
{{ warning.warning_note_private|nl2br }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
@ -216,6 +175,81 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if warnings|length > 0 %}
|
||||
<div class="container profile__warning__container">
|
||||
{{ container_title('Account Standing') }}
|
||||
|
||||
<div class="profile__warning">
|
||||
<div class="profile__warning__background"></div>
|
||||
|
||||
<div class="profile__warning__content">
|
||||
<div class="profile__warning__type">
|
||||
Type
|
||||
</div>
|
||||
|
||||
<div class="profile__warning__created">
|
||||
Created
|
||||
</div>
|
||||
|
||||
<div class="profile__warning__duration">
|
||||
Expires
|
||||
</div>
|
||||
|
||||
<div class="profile__warning__note">
|
||||
Note
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% for warning in warnings %}
|
||||
{% if warning.warning_type == constant('MSZ_WARN_SILENCE') %}
|
||||
{% set warning_text = 'Silence' %}
|
||||
{% set warning_class = 'silence' %}
|
||||
{% elseif warning.warning_type == constant('MSZ_WARN_BAN') %}
|
||||
{% set warning_text = 'Ban' %}
|
||||
{% set warning_class = 'ban' %}
|
||||
{% elseif warning.warning_type == constant('MSZ_WARN_WARNING') %}
|
||||
{% set warning_text = 'Warning' %}
|
||||
{% set warning_class = 'warning' %}
|
||||
{% else %}
|
||||
{% set warning_text = 'Note' %}
|
||||
{% set warning_class = 'note' %}
|
||||
{% endif %}
|
||||
|
||||
<div class="profile__warning profile__warning--{{ warning_class }}">
|
||||
<div class="profile__warning__background"></div>
|
||||
|
||||
<div class="profile__warning__content">
|
||||
<div class="profile__warning__type">
|
||||
{{ warning_text }}
|
||||
</div>
|
||||
|
||||
<time datetime="{{ warning.warning_created|date('c') }}" title="{{ warning.warning_created|date('r') }}" class="profile__warning__created">
|
||||
{{ warning.warning_created|time_diff }}
|
||||
</time>
|
||||
|
||||
{% if warning.warning_duration_secs > 0 %}
|
||||
<time datetime="{{ warning.warning_duration|date('c') }}" title="{{ warning.warning_duration|date('r') }}" class="profile__warning__duration">
|
||||
{{ warning.warning_created|time_diff }}
|
||||
</time>
|
||||
{% else %}
|
||||
<div class="profile__warning__duration"></div>
|
||||
{% endif %}
|
||||
|
||||
<div class="profile__warning__note">
|
||||
{{ warning.warning_note }}
|
||||
|
||||
{% if can_view_private_note and warning.warning_note_private|length > 0 %}
|
||||
<div class="profile__warning__private">
|
||||
{{ warning.warning_note_private|nl2br }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if is_editing %}
|
||||
|
|
Loading…
Reference in a new issue