adding ratelimiting with reddis db
Deploy / deploy (push) Failing after 3s

This commit is contained in:
2026-05-06 09:27:02 +02:00
parent d82a08f77b
commit 5deb0e1056
16 changed files with 312 additions and 37 deletions
+40 -2
View File
@@ -192,7 +192,11 @@ if (!isset($_SESSION["logged_in"]) || $_SESSION["logged_in"] !== true) {
<!-- Success message will go here -->
</div>
<div id="qrcode"></div>
<div class="p-3">
<input type="text" id="twofa-confirm-pin" class="form-control" placeholder="Current 2FA code">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" onclick="confirm2FaEnrollment()">Confirm 2FA</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
@@ -362,8 +366,12 @@ if (!isset($_SESSION["logged_in"]) || $_SESSION["logged_in"] !== true) {
// Handle success
if(isEnabled==false){
showSuccessModal(result.message || (isEnabled ? '2FA enabled successfully.' : '2FA disabled successfully.'));
}else{
show2FaModal(result.message, result.token);
}else{
if (result.pending) {
show2FaModal(result.message, result.token);
} else {
showSuccessModal(result.message || '2FA enabled successfully.');
}
}
} else {
// Handle error
@@ -427,11 +435,41 @@ if (!isset($_SESSION["logged_in"]) || $_SESSION["logged_in"] !== true) {
}
function show2FaModal(message,secret) {
document.getElementById('twofaModalMessage').textContent = message;
document.getElementById('qrcode').innerHTML = '';
document.getElementById('twofa-confirm-pin').value = '';
const errorModal = new bootstrap.Modal(document.getElementById('twofaModal'));
generate2FAQRCode("Jakach Login",'<?php echo($_SESSION["username"]) ?>',secret);
errorModal.show();
}
async function confirm2FaEnrollment() {
const pin = document.getElementById('twofa-confirm-pin').value.trim();
if (!pin) {
showErrorModal('Enter the current 2FA code.');
return;
}
const response = await fetch('/api/account/update_2fa.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': window.csrfToken,
},
body: JSON.stringify({
enable_2fa: true,
twofa_pin: pin
}),
});
const result = await response.json();
if (response.ok && result.success) {
bootstrap.Modal.getInstance(document.getElementById('twofaModal')).hide();
document.getElementById('2fa-switch').checked = true;
showSuccessModal(result.message || '2FA enabled successfully.');
} else {
showErrorModal(result.message || 'Invalid 2FA code.');
}
}
function generate2FAQRCode(issuer, accountName, secret) {
// Create the OTP URI
const uri = `otpauth://totp/${encodeURIComponent(issuer)}:${encodeURIComponent(accountName)}?secret=${secret}&issuer=${encodeURIComponent(issuer)}`;
+20 -5
View File
@@ -39,13 +39,28 @@ if(!isset($data->enable_2fa) || !is_bool($data->enable_2fa)){
exit();
}
if($data->enable_2fa==true){
//create 2fa secret key
$twofa_secret=generateBase32Secret();
$twofa_pin = trim((string)($data->twofa_pin ?? ""));
if ($twofa_pin === "") {
$twofa_secret=generateBase32Secret();
$_SESSION["pending_2fa_secret"]=$twofa_secret;
echo json_encode(['success' => true, 'pending' => true, 'message' => 'Scan this QR code, then enter the current 2FA code to confirm enrollment.', 'token' => $twofa_secret]);
exit();
}
check_rate_limit($conn, 'setup_2fa', 5, 10 * 60, (string)$id);
$twofa_secret = $_SESSION["pending_2fa_secret"] ?? "";
if ($twofa_secret === "" || !hash_equals(generateTOTP($twofa_secret), $twofa_pin)) {
echo json_encode(['success' => false, 'message' => 'Invalid 2FA code.']);
exit();
}
$sql="UPDATE users SET 2fa = ?, auth_method_enabled_2fa = 1, auth_method_required_2fa = 1 WHERE id = ?";
if ($update_stmt = $conn->prepare($sql)) {
$update_stmt->bind_param("si", $twofa_secret, $id);
if ($update_stmt->execute()) {
echo json_encode(['success' => true, 'message' => '2FA enabled. Your 2fa secret is: '.$twofa_secret.'', 'token' => $twofa_secret]);
unset($_SESSION["pending_2fa_secret"]);
clear_rate_limit($conn, 'setup_2fa', (string)$id);
echo json_encode(['success' => true, 'message' => '2FA enabled.']);
} else {
echo json_encode(['success' => false, 'message' => 'Failed to enable 2fa.']);
}
@@ -56,8 +71,8 @@ if($data->enable_2fa==true){
}
if($data->enable_2fa==false){
//create 2fa secret key
$sql="UPDATE users SET auth_method_enabled_2fa = 0, auth_method_required_2fa = 0 WHERE id = ?";
unset($_SESSION["pending_2fa_secret"]);
$sql="UPDATE users SET 2fa = '', auth_method_enabled_2fa = 0, auth_method_required_2fa = 0 WHERE id = ?";
if ($update_stmt = $conn->prepare($sql)) {
$update_stmt->bind_param("i",$id);
if ($update_stmt->execute()) {
+30 -8
View File
@@ -30,15 +30,19 @@ try {
// read get argument and post body
$fn = filter_input(INPUT_GET, 'fn');
if (!in_array($fn, ['getCreateArgs', 'processCreate'], true)) {
throw new Exception('Invalid passkey operation.');
}
$requireResidentKey = !!filter_input(INPUT_GET, 'requireResidentKey');
$userVerification = filter_input(INPUT_GET, 'userVerification', FILTER_SANITIZE_SPECIAL_CHARS);
$userId = filter_input(INPUT_GET, 'userId', FILTER_SANITIZE_SPECIAL_CHARS);
$userName = filter_input(INPUT_GET, 'userName', FILTER_SANITIZE_SPECIAL_CHARS);
$userDisplayName = filter_input(INPUT_GET, 'userDisplayName', FILTER_SANITIZE_SPECIAL_CHARS);
$userVerification = 'preferred';
$userName = preg_replace('/[^0-9a-z_]/i', '', $_SESSION["username"] ?? "");
if ($userName === "") {
throw new Exception('Missing account session.');
}
$userId = bin2hex($userName);
$userDisplayName = $userName;
$userId = preg_replace('/[^0-9a-f]/i', '', $userId);
$userName = preg_replace('/[^0-9a-z]/i', '', $_SESSION["username"]);
$userDisplayName = preg_replace('/[^0-9a-z öüäéèàÖÜÄÉÈÀÂÊÎÔÛâêîôû]/i', '', $userDisplayName);
$post = trim(file_get_contents('php://input'));
@@ -96,6 +100,7 @@ try {
// Handle different functions
if ($fn === 'getCreateArgs') {
check_rate_limit($conn, 'passkey_register_args', 10, 60 * 60, $userName);
$createArgs = $WebAuthn->getCreateArgs(\hex2bin($userId), $userName, $userDisplayName, 60*4, $requireResidentKey, $userVerification, $crossPlatformAttachment);
header('Content-Type: application/json');
@@ -103,6 +108,7 @@ try {
// save challange to session. you have to deliver it to processGet later.
$_SESSION['challenge'] = $WebAuthn->getChallenge();
$_SESSION['passkey_register_username'] = $userName;
} else if ($fn === 'getGetArgs') {
$ids = [];
@@ -137,6 +143,15 @@ try {
// save challange to session. you have to deliver it to processGet later.
$_SESSION['challenge'] = $WebAuthn->getChallenge();
} else if ($fn === 'processCreate') {
check_rate_limit($conn, 'passkey_register_process', 5, 60 * 60, $userName);
if (empty($_SESSION['challenge']) || ($_SESSION['passkey_register_username'] ?? '') !== $userName) {
throw new Exception('Invalid passkey session.');
}
foreach (['clientDataJSON', 'attestationObject'] as $requiredField) {
if (!is_object($post) || empty($post->{$requiredField})) {
throw new Exception('Invalid passkey response.');
}
}
// Process create
$challenge = $_SESSION['challenge'];
$clientDataJSON = base64_decode($post->clientDataJSON);
@@ -151,8 +166,15 @@ try {
$data->userDisplayName = $userDisplayName;
// Store registration data in the database
$credentialId = $data->credentialId;
$credentialPublicKey = $data->credentialPublicKey;
$signatureCounter = (int)$data->signatureCounter;
$stmt = $conn->prepare("UPDATE users set credential_id = ?, public_key = ?, counter = ?, auth_method_enabled_passkey = 1, auth_method_required_passkey = 1 WHERE username = ?");
$stmt->execute([ $data->credentialId, $data->credentialPublicKey, $data->signatureCounter,$userName]);
$stmt->bind_param("ssis", $credentialId, $credentialPublicKey, $signatureCounter, $userName);
$stmt->execute();
$stmt->close();
unset($_SESSION['challenge'], $_SESSION['passkey_register_username']);
clear_rate_limit($conn, 'passkey_register_process', $userName);
$msg = 'registration success.';
$return = new stdClass();
@@ -165,7 +187,7 @@ try {
} catch (Throwable $ex) {
$return = new stdClass();
$return->success = false;
$return->msg = $ex->getMessage();
$return->msg = 'Passkey registration failed.';
header('Content-Type: application/json');
print(json_encode($return));
+5 -2
View File
@@ -1,7 +1,9 @@
<?php
header('Content-Type: application/json');
include "../utils/security.php";
include "../../config/config.php";
$conn = new mysqli($DB_SERVERNAME, $DB_USERNAME, $DB_PASSWORD, $DB_DATABASE);
check_rate_limit($conn, 'check_auth_key', 120, 60);
$now=time();
$sql="DELETE FROM auth_tokens WHERE valid_until < ?;";
$stmt = mysqli_prepare($conn, $sql);
@@ -10,9 +12,10 @@ mysqli_stmt_execute($stmt);
mysqli_stmt_close($stmt);
$auth_key=$_GET["auth_token"] ?? "";
$auth_key_hash=auth_token_hash($auth_key);
$sql="SELECT user_id FROM auth_tokens WHERE auth_token = ? AND valid_until > ?;";
$stmt = mysqli_prepare($conn, $sql);
mysqli_stmt_bind_param($stmt, 'si', $auth_key,$now);
mysqli_stmt_bind_param($stmt, 'si', $auth_key_hash,$now);
mysqli_stmt_execute($stmt);
mysqli_stmt_store_result($stmt);
//if auth key is valid
@@ -48,7 +51,7 @@ if(mysqli_stmt_num_rows($stmt) == 1){
//remove auth key
$sql="DELETE FROM auth_tokens WHERE auth_token = ?;";
$stmt = mysqli_prepare($conn, $sql);
mysqli_stmt_bind_param($stmt, 's', $auth_key);
mysqli_stmt_bind_param($stmt, 's', $auth_key_hash);
mysqli_stmt_execute($stmt);
echo(json_encode($data));
}else{
+2
View File
@@ -14,6 +14,7 @@ include "../utils/generate_pin.php";
$conn = new mysqli($DB_SERVERNAME, $DB_USERNAME, $DB_PASSWORD, $DB_DATABASE);
$username=$_SESSION["username"];
check_rate_limit($conn, 'login_mfa', 5, 10 * 60, $username);
$sql="SELECT 2fa FROM users WHERE username = ?";
$stmt = mysqli_prepare($conn, $sql);
mysqli_stmt_bind_param($stmt, 's', $username);
@@ -28,6 +29,7 @@ $twofa_pin=$_POST["twofa_pin"] ?? "";
if($twofa_secret !== "" && hash_equals(generateTOTP($twofa_secret), $twofa_pin)){
$_SESSION["mfa_authenticated"]=1;
session_regenerate_id(true);
clear_rate_limit($conn, 'login_mfa', $username);
$data = [
'status' => 'success'
];
+39 -12
View File
@@ -15,15 +15,19 @@ if ($conn->connect_error) {
try {
// read get argument and post body
$fn = filter_input(INPUT_GET, 'fn');
if (!in_array($fn, ['getGetArgs', 'processGet'], true)) {
throw new Exception('Invalid passkey operation.');
}
$requireResidentKey = !!filter_input(INPUT_GET, 'requireResidentKey');
$userVerification = filter_input(INPUT_GET, 'userVerification', FILTER_SANITIZE_SPECIAL_CHARS);
$userId = filter_input(INPUT_GET, 'userId', FILTER_SANITIZE_SPECIAL_CHARS);
$userName = filter_input(INPUT_GET, 'userName', FILTER_SANITIZE_SPECIAL_CHARS);
$userDisplayName = filter_input(INPUT_GET, 'userDisplayName', FILTER_SANITIZE_SPECIAL_CHARS);
$userVerification = 'preferred';
$userName = preg_replace('/[^0-9a-z_]/i', '', $_SESSION["username"] ?? "");
if ($userName === "") {
throw new Exception('Missing login session.');
}
$userId = bin2hex($userName);
$userDisplayName = $userName;
$userId = preg_replace('/[^0-9a-f]/i', '', $userId);
$userName = preg_replace('/[^0-9a-z]/i', '', $userName);
$userDisplayName = preg_replace('/[^0-9a-z öüäéèàÖÜÄÉÈÀÂÊÎÔÛâêîôû]/i', '', $userDisplayName);
$post = trim(file_get_contents('php://input'));
@@ -88,6 +92,7 @@ try {
// Save challenge to session or somewhere else if needed
} else if ($fn === 'getGetArgs') {
check_rate_limit($conn, 'passkey_get_args', 10, 5 * 60, $userName);
$ids = [];
//get registrations form user table
@@ -100,11 +105,12 @@ try {
$row = $registration->fetch_assoc();
if ($registration->num_rows <= 0) {
if ($registration->num_rows <= 0 || empty($row["credential_id"])) {
throw new Exception('User does not exist');
}
$_SESSION["registrations"]["credentialId"]=$row["credential_id"];
$_SESSION["registrations"]["username"]=$userName;
$ids[]=$row["credential_id"];
$_SESSION["registrations"]["userId"]=$userId;
@@ -118,16 +124,28 @@ try {
$_SESSION['challenge'] = $WebAuthn->getChallenge();
}else if ($fn === 'processGet') {
check_rate_limit($conn, 'passkey_process', 5, 10 * 60, $userName);
if (empty($_SESSION["challenge"]) || empty($_SESSION["registrations"]["credentialId"]) || ($_SESSION["registrations"]["username"] ?? "") !== $userName) {
throw new Exception('Invalid passkey session.');
}
foreach (['id', 'clientDataJSON', 'authenticatorData', 'signature'] as $requiredField) {
if (!is_object($post) || empty($post->{$requiredField})) {
throw new Exception('Invalid passkey response.');
}
}
// Process get
// Retrieve registration data from the database based on credential ID
$id = base64_decode($post->id);
$stmt = $conn->prepare("SELECT * FROM users WHERE credential_id = ?");
$stmt->bind_param("s", $_SESSION["registrations"]["credentialId"]);
if (!hash_equals($_SESSION["registrations"]["credentialId"], $id)) {
throw new Exception('Invalid credential.');
}
$stmt = $conn->prepare("SELECT public_key, counter FROM users WHERE credential_id = ? AND username = ?");
$stmt->bind_param("ss", $_SESSION["registrations"]["credentialId"], $userName);
$stmt->execute();
$registration = $stmt->get_result();
$row = $registration->fetch_assoc();
if (!$registration) {
if (!$row) {
throw new Exception('Public Key for credential ID not found!');
}
@@ -139,7 +157,14 @@ try {
$credentialPublicKey = $row['public_key'];
// Process the get request
$WebAuthn->processGet($clientDataJSON, $authenticatorData, $signature, $credentialPublicKey, $challenge, null, $userVerification === 'required');
$WebAuthn->processGet($clientDataJSON, $authenticatorData, $signature, $credentialPublicKey, $challenge, (int)$row['counter'], $userVerification === 'required');
$newCounter = $WebAuthn->getSignatureCounter();
if ($newCounter !== null) {
$stmt = $conn->prepare("UPDATE users SET counter = ? WHERE username = ?");
$stmt->bind_param("is", $newCounter, $userName);
$stmt->execute();
$stmt->close();
}
// Authentication success
//set sessionso user is authenticated
@@ -147,6 +172,8 @@ try {
$_SESSION["pw_authenticated"]=1;
$_SESSION["passkey_authenticated"]=1;
session_regenerate_id(true);
unset($_SESSION["challenge"], $_SESSION["registrations"]);
clear_rate_limit($conn, 'passkey_process', $userName);
$return = new stdClass();
$return->success = true;
@@ -158,7 +185,7 @@ try {
} catch (Throwable $ex) {
$return = new stdClass();
$return->success = false;
$return->msg = $ex->getMessage();
$return->msg = 'Passkey authentication failed.';
header('Content-Type: application/json');
print(json_encode($return));
+2
View File
@@ -13,6 +13,7 @@ include "../../config/config.php";
$conn = new mysqli($DB_SERVERNAME, $DB_USERNAME, $DB_PASSWORD, $DB_DATABASE);
$username=$_SESSION["username"];
check_rate_limit($conn, 'login_pw', 5, 15 * 60, $username);
$sql="SELECT password,pepper FROM users WHERE username = ?";
$stmt = mysqli_prepare($conn, $sql);
mysqli_stmt_bind_param($stmt, 's', $username);
@@ -28,6 +29,7 @@ $password=$_POST["password"] ?? "";
if($pw !== "" && password_verify($password.$pepper,$pw)){
$_SESSION["pw_authenticated"]=1;
session_regenerate_id(true);
clear_rate_limit($conn, 'login_pw', $username);
$data = [
'status' => 'success'
];
+2 -1
View File
@@ -49,9 +49,10 @@ else if ($_SESSION["needs_auth"]===false && $_SESSION["mfa_authenticated"]==1 &&
$user_id=$_SESSION["id"];
$valid_until=time()+(15*60);
$auth_token=bin2hex(random_bytes(128));
$auth_token_hash=auth_token_hash($auth_token);
$sql="INSERT INTO auth_tokens (auth_token,user_id, valid_until) VALUES(?,?,?);";
$stmt = mysqli_prepare($conn, $sql);
mysqli_stmt_bind_param($stmt, 'sii', $auth_token,$user_id,$valid_until);
mysqli_stmt_bind_param($stmt, 'sii', $auth_token_hash,$user_id,$valid_until);
mysqli_stmt_execute($stmt);
mysqli_stmt_close($stmt);
if(!empty($send_to)){
+5 -2
View File
@@ -15,13 +15,15 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$conn = new mysqli($DB_SERVERNAME, $DB_USERNAME, $DB_PASSWORD, $DB_DATABASE);
$token = $_POST['token'];
$token_hash = auth_token_hash($token);
$user_id="";
$valid_until=0;
$password = $_POST['password'];
$confirmPassword = $_POST['confirm_password'];
$sql="SELECT user_id, valid_until FROM reset_tokens WHERE auth_token=?;";
check_rate_limit($conn, 'reset_pw', 5, 60 * 60, $token_hash);
$stmt = mysqli_prepare($conn, $sql);
mysqli_stmt_bind_param($stmt, 's', $token);
mysqli_stmt_bind_param($stmt, 's', $token_hash);
mysqli_stmt_execute($stmt);
mysqli_stmt_store_result($stmt);
mysqli_stmt_bind_result($stmt, $user_id,$valid_until);
@@ -47,6 +49,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if ($update_stmt = $conn->prepare($update_sql)) {
$update_stmt->bind_param("ssi", $hashed_password, $new_pepper, $user_id);
if ($update_stmt->execute()) {
clear_rate_limit($conn, 'reset_pw', $token_hash);
echo json_encode(['status' => 'success','success' => true, 'message' => 'Password updated successfully.']);
} else {
echo json_encode(['success' => false, 'message' => 'Failed to update password.']);
@@ -62,7 +65,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
//remove token
$sql="DELETE FROM reset_tokens WHERE auth_token = ?;";
$stmt = mysqli_prepare($conn, $sql);
mysqli_stmt_bind_param($stmt, 's', $token);
mysqli_stmt_bind_param($stmt, 's', $token_hash);
mysqli_stmt_execute($stmt);
mysqli_stmt_close($stmt);
+4 -2
View File
@@ -11,12 +11,13 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
include "../../config/config.php";
include "../utils/get_location.php";
$username=$_SESSION["username"] ?? "";
$conn = new mysqli($DB_SERVERNAME, $DB_USERNAME, $DB_PASSWORD, $DB_DATABASE);
check_rate_limit($conn, 'send_reset_link', 3, 60 * 60, $username);
if ($username === "") {
echo json_encode(['success' => false, 'message' => 'Missing username.']);
exit;
}
$sql="SELECT id, email, telegram_id FROM users WHERE username = ?;";
$conn = new mysqli($DB_SERVERNAME, $DB_USERNAME, $DB_PASSWORD, $DB_DATABASE);
$mail="";
$id="";
$telegram_id="";
@@ -40,6 +41,7 @@ $ip=trim(explode(",",$forwarded_for)[0]);
$location=get_location_from_ip($ip);
$date=date('Y-m-d H:i:s');
$token=bin2hex(random_bytes(128));
$token_hash=auth_token_hash($token);
$link="https://auth.jakach.ch/login/reset_pw.php?token=$token";
$message = "*Password reset token*\n\n"
@@ -648,7 +650,7 @@ if(!empty($mail)){
$valid_until=time()+(12 * 60 * 60);
$sql="INSERT INTO reset_tokens (auth_token, user_id,valid_until) VALUES (?,?,?);";
$stmt = mysqli_prepare($conn, $sql);
mysqli_stmt_bind_param($stmt, 'sii', $token,$id,$valid_until);
mysqli_stmt_bind_param($stmt, 'sii', $token_hash,$id,$valid_until);
mysqli_stmt_execute($stmt);
mysqli_stmt_close($stmt);
+3
View File
@@ -6,6 +6,9 @@ require_csrf_token();
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
json_response(['success' => false, 'message' => 'Invalid request method.'], 405);
}
include "../../config/config.php";
$conn = new mysqli($DB_SERVERNAME, $DB_USERNAME, $DB_PASSWORD, $DB_DATABASE);
check_rate_limit($conn, 'set_username', 30, 60);
$_SESSION["needs_auth"]=true;
$_SESSION["logged_in"]=false;
$username = strtolower((string) ($_POST["username"] ?? ""));
+1
View File
@@ -9,6 +9,7 @@ header('Content-Type: application/json');
include "../../config/config.php";
// Connect to the database
$conn = new mysqli($DB_SERVERNAME, $DB_USERNAME, $DB_PASSWORD, $DB_DATABASE);
check_rate_limit($conn, 'register', 5, 60 * 60);
// Check the connection
if ($conn === false) {
+144
View File
@@ -99,6 +99,150 @@ function remember_token_hash(string $token): string
return hash('sha256', $token);
}
function auth_token_hash(string $token): string
{
return hash('sha256', $token);
}
function client_ip(): string
{
return $_SERVER['REMOTE_ADDR'] ?? 'unknown';
}
function ensure_rate_limit_table(mysqli $conn): void
{
static $created = false;
if ($created) {
return;
}
$conn->query("CREATE TABLE IF NOT EXISTS rate_limits (
id VARCHAR(64) PRIMARY KEY,
attempts INT NOT NULL,
reset_at INT NOT NULL
)");
$created = true;
}
function redis_rate_limit_connection()
{
static $redis = null;
static $checked = false;
if ($checked) {
return $redis;
}
$checked = true;
if (!class_exists('Redis')) {
return null;
}
$host = $GLOBALS['REDIS_HOST'] ?? getenv('REDIS_HOST') ?: null;
if (!$host) {
$host = 'jakach-login-redis';
}
$port = $GLOBALS['REDIS_PORT'] ?? getenv('REDIS_PORT') ?: null;
$port = $port ? (int)$port : 6379;
try {
$candidate = new Redis();
if (!$candidate->connect($host, $port, 0.2)) {
return null;
}
$redis = $candidate;
} catch (Throwable $ex) {
$redis = null;
}
return $redis;
}
function rate_limit_key(string $bucket, string $identifier = ''): string
{
return hash('sha256', $bucket . '|' . client_ip() . '|' . strtolower($identifier));
}
function check_rate_limit(mysqli $conn, string $bucket, int $max_attempts, int $window_seconds, string $identifier = ''): void
{
$redis = redis_rate_limit_connection();
if ($redis !== null) {
$key = 'rl:' . rate_limit_key($bucket, $identifier);
$attempts = (int) $redis->incr($key);
if ($attempts === 1) {
$redis->expire($key, $window_seconds);
}
if ($attempts > $max_attempts) {
json_response([
'success' => false,
'status' => 'failure',
'message' => 'Too many attempts. Please try again later.'
], 429);
}
return;
}
ensure_rate_limit_table($conn);
$now = time();
$key = rate_limit_key($bucket, $identifier);
$attempts = 0;
$reset_at = 0;
$sql = "SELECT attempts, reset_at FROM rate_limits WHERE id = ?";
$stmt = mysqli_prepare($conn, $sql);
mysqli_stmt_bind_param($stmt, 's', $key);
mysqli_stmt_execute($stmt);
mysqli_stmt_store_result($stmt);
mysqli_stmt_bind_result($stmt, $attempts, $reset_at);
$found = mysqli_stmt_fetch($stmt);
mysqli_stmt_close($stmt);
if (!$found || $reset_at <= $now) {
$attempts = 1;
$reset_at = $now + $window_seconds;
$sql = "REPLACE INTO rate_limits (id, attempts, reset_at) VALUES (?, ?, ?)";
$stmt = mysqli_prepare($conn, $sql);
mysqli_stmt_bind_param($stmt, 'sii', $key, $attempts, $reset_at);
mysqli_stmt_execute($stmt);
mysqli_stmt_close($stmt);
return;
}
if ($attempts >= $max_attempts) {
json_response([
'success' => false,
'status' => 'failure',
'message' => 'Too many attempts. Please try again later.'
], 429);
}
$attempts++;
$sql = "UPDATE rate_limits SET attempts = ? WHERE id = ?";
$stmt = mysqli_prepare($conn, $sql);
mysqli_stmt_bind_param($stmt, 'is', $attempts, $key);
mysqli_stmt_execute($stmt);
mysqli_stmt_close($stmt);
}
function clear_rate_limit(mysqli $conn, string $bucket, string $identifier = ''): void
{
$redis = redis_rate_limit_connection();
if ($redis !== null) {
$redis->del('rl:' . rate_limit_key($bucket, $identifier));
return;
}
ensure_rate_limit_table($conn);
$key = rate_limit_key($bucket, $identifier);
$sql = "DELETE FROM rate_limits WHERE id = ?";
$stmt = mysqli_prepare($conn, $sql);
mysqli_stmt_bind_param($stmt, 's', $key);
mysqli_stmt_execute($stmt);
mysqli_stmt_close($stmt);
}
function set_secure_cookie(string $name, string $value, int $expires): void
{
$is_https = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
+2
View File
@@ -6,4 +6,6 @@
$TELEGRAM_BOT_API="";
$SENDGRID_KEY="";
$SENDGRID_MAIL="";
$REDIS_HOST="jakach-login-redis";
$REDIS_PORT=6379;
?>
+10 -2
View File
@@ -1,5 +1,3 @@
version: '3.3'
services:
jakach-login-db:
image: yobasystems/alpine-mariadb:latest
@@ -13,6 +11,15 @@ services:
volumes:
- jakach-login-db-storage:/var/lib/mysql
jakach-login-redis:
image: redis:7-alpine
container_name: jakach-login-redis
restart: unless-stopped
command: ["redis-server", "--save", "", "--appendonly", "no"]
networks:
jakach-login-network:
ipv4_address: 192.168.5.4
jakach-login-srv:
build:
context: .
@@ -28,6 +35,7 @@ services:
- "447:80"
depends_on:
- jakach-login-db
- jakach-login-redis
volumes:
- ./app-code:/var/www/html
- ./apache-conf:/etc/apache2/sites-available
+3 -1
View File
@@ -3,7 +3,9 @@ FROM php:apache
# Install necessary PHP extensions and tools
RUN apt-get update && \
apt-get install -y libzip-dev zip zlib1g-dev git unzip && \
docker-php-ext-install mysqli zip
docker-php-ext-install mysqli zip && \
pecl install redis && \
docker-php-ext-enable redis
# Enable SSL module for Apache
RUN a2enmod ssl