0, 'path' => '/', 'domain' => '', 'secure' => true, 'httponly' => true, 'samesite' => 'Lax', ]); session_start(); } function json_response(array $data, int $status_code = 200): void { http_response_code($status_code); header('Content-Type: application/json'); echo json_encode($data); exit; } function require_same_origin_request(): void { if (!in_array($_SERVER['REQUEST_METHOD'] ?? 'GET', ['POST', 'PUT', 'PATCH', 'DELETE'], true)) { return; } $host = $_SERVER['HTTP_HOST'] ?? ''; $source = $_SERVER['HTTP_ORIGIN'] ?? $_SERVER['HTTP_REFERER'] ?? ''; if ($source === '') { return; } $source_host = parse_url($source, PHP_URL_HOST); $source_port = parse_url($source, PHP_URL_PORT); if ($source_host && $source_port) { $source_host .= ':' . $source_port; } if (!$source_host || !hash_equals(strtolower($host), strtolower($source_host))) { json_response(['success' => false, 'message' => 'Invalid request origin.'], 403); } } function csrf_token(): string { if (empty($_SESSION['csrf_token']) || !is_string($_SESSION['csrf_token'])) { $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); } return $_SESSION['csrf_token']; } function print_csrf_script(): void { echo ''; } function require_csrf_token(): void { if (!in_array($_SERVER['REQUEST_METHOD'] ?? 'GET', ['POST', 'PUT', 'PATCH', 'DELETE'], true)) { return; } $token = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? $_POST['csrf_token'] ?? ''; if (empty($_SESSION['csrf_token']) || !is_string($token) || !hash_equals($_SESSION['csrf_token'], $token)) { json_response(['success' => false, 'message' => 'Invalid CSRF token.'], 403); } } function require_logged_in(): void { if (!isset($_SESSION['logged_in']) || $_SESSION['logged_in'] !== true || empty($_SESSION['id'])) { json_response(['success' => false, 'message' => 'Not logged in'], 401); } } function is_admin_session(): bool { return !empty($_SESSION['permissions']) && is_string($_SESSION['permissions']) && isset($_SESSION['permissions'][0]) && $_SESSION['permissions'][0] === '1'; } 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 { setcookie($name, $value, [ 'expires' => $expires, 'path' => '/', 'secure' => true, 'httponly' => true, 'samesite' => 'Lax', ]); } function delete_cookie(string $name): void { set_secure_cookie($name, '', time() - 3600); } function normalize_redirect_target(?string $target): string { $target = trim((string) $target); if ($target === '') { return '/account/'; } if (preg_match('/[\r\n]/', $target)) { return '/account/'; } if (str_starts_with($target, '/') && !str_starts_with($target, '//')) { return $target; } $parts = parse_url($target); if (!$parts || empty($parts['scheme']) || empty($parts['host'])) { return '/account/'; } if (!in_array(strtolower($parts['scheme']), ['http', 'https'], true)) { return '/account/'; } return $target; } function append_auth_token_to_redirect(string $redirect, string $auth_token): string { $separator = str_contains($redirect, '?') ? '&' : '?'; return $redirect . $separator . 'auth=' . rawurlencode($auth_token); } function log_activity(mysqli $conn, int $user_id, string $action, string $details = ''): void { $forwarded_for = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'] ?? ''; $ip = trim(explode(',', $forwarded_for)[0]); $user_agent = $_SERVER['HTTP_USER_AGENT'] ?? ''; $sql = "INSERT INTO activity_log (user_id, action, ip, user_agent, details) VALUES (?, ?, ?, ?, ?)"; $stmt = mysqli_prepare($conn, $sql); mysqli_stmt_bind_param($stmt, 'issss', $user_id, $action, $ip, $user_agent, $details); mysqli_stmt_execute($stmt); mysqli_stmt_close($stmt); } function is_external_domain(string $url): ?string { if (!str_starts_with($url, 'http://') && !str_starts_with($url, 'https://')) { return null; } $host = parse_url($url, PHP_URL_HOST); if ($host === null || $host === '') { return null; } $host = strtolower($host); if ($host === 'auth.jakach.ch' || str_ends_with($host, '.jakach.ch')) { return null; } return $host; } ?>