Files
argus-wp-defence/includes/class-argus-challenge.php
T
root df0f2fccb8 ARGUS WordPress Defence 1.0.0 — first production release
Automatic WordPress security: local firewall, malware and file-integrity
scanning, vulnerability protection, quarantine, scheduled backups, an
optional page cache, and automatic global threat intelligence.

See README.md for installation, update, and uninstall instructions.
2026-08-09 13:40:16 +00:00

147 lines
4.5 KiB
PHP

<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class Argus_Challenge {
const TRANSIENT_PREFIX = 'argus_wpd_pow_';
const COOKIE_NAME = 'argus_wpd_ct';
const DEFAULT_DIFFICULTY = 4;
const TTL_SECONDS = 120;
const PASS_TTL_SECONDS = 30 * MINUTE_IN_SECONDS;
public static function issue( $ip ) {
$challenge_id = wp_generate_password( 20, false );
$prefix = wp_generate_password( 16, false );
$difficulty = (int) apply_filters( 'argus_wpd_pow_difficulty', self::DEFAULT_DIFFICULTY );
set_transient(
self::TRANSIENT_PREFIX . $challenge_id,
array(
'ip' => $ip,
'prefix' => $prefix,
'difficulty' => $difficulty,
'issued_at' => time(),
),
self::TTL_SECONDS
);
return array(
'challenge_id' => $challenge_id,
'prefix' => $prefix,
'difficulty' => $difficulty,
);
}
public static function verify( $challenge_id, $counter, $ip ) {
$data = get_transient( self::TRANSIENT_PREFIX . $challenge_id );
if ( ! $data || $data['ip'] !== $ip ) {
return false;
}
$hash = hash( 'sha256', $data['prefix'] . $counter );
$ok = 0 === strncmp( $hash, str_repeat( '0', $data['difficulty'] ), $data['difficulty'] );
if ( $ok ) {
delete_transient( self::TRANSIENT_PREFIX . $challenge_id );
self::issue_pass_cookie( $ip );
}
return $ok;
}
protected static function issue_pass_cookie( $ip ) {
$token = wp_generate_password( 32, false );
set_transient( 'argus_wpd_pass_' . $token, $ip, self::PASS_TTL_SECONDS );
setcookie( self::COOKIE_NAME, $token, time() + self::PASS_TTL_SECONDS, COOKIEPATH ?: '/', COOKIE_DOMAIN, is_ssl(), true );
}
public static function has_valid_pass( $ip ) {
if ( empty( $_COOKIE[ self::COOKIE_NAME ] ) ) { // phpcs:ignore
return false;
}
$token = sanitize_text_field( wp_unslash( $_COOKIE[ self::COOKIE_NAME ] ) ); // phpcs:ignore
$stored_ip = get_transient( 'argus_wpd_pass_' . $token );
return $stored_ip && hash_equals( (string) $stored_ip, $ip );
}
public static function render_and_exit( $ip ) {
$challenge = self::issue( $ip );
nocache_headers();
status_header( 429 );
header( 'Content-Type: text/html; charset=utf-8' );
?>
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Verifying your browser&hellip;</title>
<meta name="robots" content="noindex">
<style>
body{font-family:system-ui,sans-serif;background:#0b0f19;color:#e2e8f0;display:flex;align-items:center;justify-content:center;height:100vh;margin:0}
.box{text-align:center;max-width:420px;padding:32px}
.spinner{width:32px;height:32px;border:3px solid rgba(148,163,184,.25);border-top-color:#00d4ff;border-radius:50%;margin:0 auto 20px;animation:spin 1s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
p{color:#94a3b8;font-size:13px}
</style>
</head>
<body>
<div class="box">
<div class="spinner"></div>
<h3>Verifying your browser</h3>
<p>This site is protected by ARGUS WordPress Defence. This should only take a moment.</p>
</div>
<script>
(async function () {
const prefix = <?php echo wp_json_encode( $challenge['prefix'] ); ?>;
const difficulty = <?php echo (int) $challenge['difficulty']; ?>;
const challengeId = <?php echo wp_json_encode( $challenge['challenge_id'] ); ?>;
const target = '0'.repeat(difficulty);
async function sha256Hex(input) {
const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(input));
return Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join('');
}
let counter = 0;
while (true) {
const hash = await sha256Hex(prefix + counter);
if (hash.startsWith(target)) break;
counter++;
}
const form = document.createElement('form');
form.method = 'POST';
form.action = window.location.href;
for (const [name, value] of Object.entries({ argus_wpd_challenge_id: challengeId, argus_wpd_counter: counter })) {
const input = document.createElement('input');
input.type = 'hidden';
input.name = name;
input.value = value;
form.appendChild(input);
}
document.body.appendChild(form);
form.submit();
})();
</script>
</body>
</html>
<?php
exit;
}
public static function maybe_handle_submission( $ip ) {
if ( empty( $_POST['argus_wpd_challenge_id'] ) || ! isset( $_POST['argus_wpd_counter'] ) ) { // phpcs:ignore
return false;
}
$challenge_id = sanitize_text_field( wp_unslash( $_POST['argus_wpd_challenge_id'] ) ); // phpcs:ignore
$counter = sanitize_text_field( wp_unslash( $_POST['argus_wpd_counter'] ) ); // phpcs:ignore
return self::verify( $challenge_id, $counter, $ip );
}
}