7 languages, 3 pages (What/How/Start), 80-line router, 98KB total.
Features:
- No database, no Composer, no build step — just files on disk
- nginx PHP isolation: only index.php executes
- Bot detection with browser heuristic bypass
- Rate limiting, structured weekly logs, geo-location, Telegram alerts
- Multi-language: Accept-Language auto-detect, nav dropdown
- Article sub-page routing via /start/{slug}
- Apache + Caddy config alternatives documented in Start page
- Sample config — no real keys or secrets
332 lines
11 KiB
PHP
332 lines
11 KiB
PHP
<?php
|
|
//====================================================
|
|
// LOGGING FUNCTIONS
|
|
//====================================================
|
|
|
|
function logging_errors($error_code, $data) {
|
|
switch ($error_code) {
|
|
case '400': $redirect = 'error/400'; break;
|
|
case '401': $redirect = 'error/401'; break;
|
|
case '403': $redirect = 'error/403'; break;
|
|
case '404': $redirect = 'error/404'; break;
|
|
case '405': $redirect = 'error/405'; break;
|
|
case '408': $redirect = 'error/408'; break;
|
|
case '409': $redirect = 'error/409'; break;
|
|
case '410': $redirect = 'error/410'; break;
|
|
case '415': $redirect = 'error/415'; break;
|
|
case '429': $redirect = 'error/429'; break;
|
|
case '500': $redirect = 'error/500'; break;
|
|
case '501': $redirect = 'error/501'; break;
|
|
case '502': $redirect = 'error/502'; break;
|
|
case '503': $redirect = 'error/503'; break;
|
|
case '504': $redirect = 'error/504'; break;
|
|
default: $redirect = 'error/default'; break;
|
|
}
|
|
|
|
$visitor = !empty($_SESSION['VISITOR']) && is_array($_SESSION['VISITOR']) ? $_SESSION['VISITOR'] : [];
|
|
|
|
$log_data = [
|
|
'correlation_id' => $_SESSION['ID'] ?? 'unknown',
|
|
'timestamp' => gmdate(DATE_ATOM),
|
|
'error_code' => $error_code,
|
|
'log_data' => $data,
|
|
'ip' => $visitor['ip'] ?? 'unknown',
|
|
'state' => $visitor['state'] ?? 'unknown',
|
|
'country' => $visitor['country'] ?? 'unknown'
|
|
];
|
|
|
|
$log_entry = json_encode($log_data, JSON_UNESCAPED_SLASHES);
|
|
$week = gmdate('o-\WW');
|
|
file_put_contents(__ROOT__ . '/logs/errors_' . $week . '.log', $log_entry . PHP_EOL, FILE_APPEND | LOCK_EX);
|
|
return $redirect;
|
|
}
|
|
|
|
function logging_requests() {
|
|
$requests_per_second = calculateRequestVelocity($_SESSION['ID']);
|
|
$visitor = !empty($_SESSION['VISITOR']) && is_array($_SESSION['VISITOR']) ? $_SESSION['VISITOR'] : [];
|
|
|
|
if($requests_per_second < BLOCK_THRESHOLD) {
|
|
$log_data = [
|
|
'correlation_id' => $_SESSION['ID'],
|
|
'timestamp' => gmdate(DATE_ATOM),
|
|
];
|
|
|
|
if(isRealVisitor() == true) {
|
|
$log_data['type'] = 'visitor';
|
|
sendVisitorNotification();
|
|
} else {
|
|
$log_data['type'] = 'bot';
|
|
}
|
|
|
|
$log_data = array_merge($log_data, [
|
|
'method' => $_SERVER['REQUEST_METHOD'] ?? 'UNKNOWN',
|
|
'uri' => $_SERVER['REQUEST_URI'] ?? 'UNKNOWN',
|
|
'ip' => $visitor['ip'] ?? 'unknown',
|
|
'state' => $visitor['state'] ?? 'unknown',
|
|
'country' => $visitor['country'] ?? 'unknown',
|
|
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'UNKNOWN',
|
|
'requests_per_second' => $requests_per_second
|
|
]);
|
|
|
|
$log_entry = json_encode($log_data, JSON_UNESCAPED_SLASHES);
|
|
$week = gmdate('o-\WW');
|
|
file_put_contents(__ROOT__ . '/logs/requests_' . $week . '.log', $log_entry . PHP_EOL, FILE_APPEND | LOCK_EX);
|
|
} else {
|
|
$redirect = logging_errors("429", $_SERVER['REQUEST_URI']);
|
|
header('Location: ' . __URL__ . $redirect);
|
|
exit;
|
|
}
|
|
}
|
|
|
|
//====================================================
|
|
// SECURITY FUNCTIONS
|
|
//====================================================
|
|
|
|
function calculateRequestVelocity($session_id) {
|
|
$velocity_file = __ROOT__ . '/logs/velocity.csv';
|
|
$now = time();
|
|
$csv_line = $session_id . ',' . $now . PHP_EOL;
|
|
file_put_contents($velocity_file, $csv_line, FILE_APPEND | LOCK_EX);
|
|
|
|
$time_window = 1;
|
|
$requests_per_second = calculateRecentRequests($session_id, $time_window);
|
|
return $requests_per_second;
|
|
}
|
|
|
|
function calculateRecentRequests($session_id, $time_window = 1) {
|
|
$velocity_file = __ROOT__ . '/logs/velocity.csv';
|
|
$now = time();
|
|
$count = 0;
|
|
|
|
if (!file_exists($velocity_file)) {
|
|
return 0;
|
|
}
|
|
|
|
$file = fopen($velocity_file, 'r');
|
|
while (($line = fgetcsv($file)) !== FALSE) {
|
|
if (count($line) >= 2) {
|
|
$stored_session = $line[0];
|
|
$timestamp = (int)$line[1];
|
|
if ($stored_session === $session_id && ($now - $timestamp) <= $time_window) {
|
|
$count++;
|
|
}
|
|
}
|
|
}
|
|
fclose($file);
|
|
return $count;
|
|
}
|
|
|
|
function calculateTotalSessionRequests($session_id) {
|
|
$velocity_file = __ROOT__ . '/logs/velocity.csv';
|
|
$count = 0;
|
|
|
|
if (!file_exists($velocity_file)) {
|
|
return 0;
|
|
}
|
|
|
|
$file = fopen($velocity_file, 'r');
|
|
while (($line = fgetcsv($file)) !== FALSE) {
|
|
if (count($line) >= 2 && $line[0] === $session_id) {
|
|
$count++;
|
|
}
|
|
}
|
|
fclose($file);
|
|
return $count;
|
|
}
|
|
|
|
function sendVisitorNotification() {
|
|
if (!defined('TELEGRAM_BOT_TOKEN') || empty(TELEGRAM_BOT_TOKEN)) return;
|
|
if (isset($_SESSION['visitor_notification_sent'])) return;
|
|
|
|
$total_requests = calculateTotalSessionRequests($_SESSION['ID']);
|
|
$visitor = !empty($_SESSION['VISITOR']) && is_array($_SESSION['VISITOR']) ? $_SESSION['VISITOR'] : [];
|
|
|
|
if ($total_requests >= 2) {
|
|
$_SESSION['visitor_notification_sent'] = true;
|
|
|
|
$ua = $_SERVER['HTTP_USER_AGENT'] ?? 'UNKNOWN';
|
|
$ua_short = strlen($ua) > 80 ? substr($ua, 0, 77) . '...' : $ua;
|
|
$referrer = $_SERVER['HTTP_REFERER'] ?? 'direct';
|
|
$timestamp = gmdate('Y-m-d H:i:s') . ' UTC';
|
|
|
|
$message = "👤 *Visitor — " . ($visitor['country'] ?? 'Unknown') . "*\n"
|
|
. "• *Time:* $timestamp\n"
|
|
. "• *Page:* " . ($_SERVER['REQUEST_URI'] ?? '/') . "\n"
|
|
. "• *Referrer:* $referrer\n"
|
|
. "• *IP:* " . ($visitor['ip'] ?? 'unknown') . "\n"
|
|
. "• *Location:* " . ($visitor['city'] ?? '') . ", " . ($visitor['country'] ?? '') . "\n"
|
|
. "• *Pages:* " . $total_requests . "\n"
|
|
. "• *Browser:* $ua_short\n"
|
|
. "• *Session:* `" . $_SESSION['ID'] . "`";
|
|
|
|
send2telegram($message);
|
|
}
|
|
}
|
|
|
|
function isRealVisitor() {
|
|
$user_agent = $_SERVER['HTTP_USER_AGENT'] ?? '';
|
|
$uri = $_SERVER['REQUEST_URI'] ?? '';
|
|
$method = $_SERVER['REQUEST_METHOD'] ?? '';
|
|
$accept = $_SERVER['HTTP_ACCEPT'] ?? '';
|
|
$accept_lang = $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? '';
|
|
|
|
$browser_patterns = [
|
|
'/Mozilla\/5\.0.*(Chrome|Firefox|Safari|Edg)\//i',
|
|
'/Mozilla\/5\.0.*Mobile.*Safari/i',
|
|
'/Mozilla\/5\.0.*(iPhone|Android).*(Chrome|Safari)/i',
|
|
];
|
|
foreach ($browser_patterns as $pattern) {
|
|
if (preg_match($pattern, $user_agent)) return true;
|
|
}
|
|
|
|
foreach (BOT_USER_AGENT_PATTERNS as $pattern) {
|
|
if (preg_match($pattern, $user_agent)) return false;
|
|
}
|
|
|
|
if (empty($accept) || empty($accept_lang)) return false;
|
|
if (empty($user_agent) || $user_agent === 'UNKNOWN' || strlen($user_agent) < 20) return false;
|
|
|
|
foreach (BLOCKED_PATHS as $path) {
|
|
if (strpos($uri, $path) !== false) return false;
|
|
}
|
|
|
|
$allowed_methods = ['GET', 'POST', 'HEAD'];
|
|
if (!in_array($method, $allowed_methods)) return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
//====================================================
|
|
// UTILITY FUNCTIONS
|
|
//====================================================
|
|
|
|
function getVisitor() {
|
|
if (!defined('GEO_KEY') || empty(GEO_KEY)) return false;
|
|
|
|
if (!empty($_SERVER['HTTP_X_REAL_IP'])) {
|
|
$ip = $_SERVER['HTTP_X_REAL_IP'];
|
|
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
|
|
$ipList = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
|
|
$ip = trim($ipList[0]);
|
|
} elseif (!empty($_SERVER['HTTP_CLIENT_IP'])) {
|
|
$ip = $_SERVER['HTTP_CLIENT_IP'];
|
|
} else {
|
|
$ip = $_SERVER['REMOTE_ADDR'];
|
|
}
|
|
|
|
$curl = curl_init();
|
|
curl_setopt_array($curl, [
|
|
CURLOPT_URL => 'https://api.ipgeolocation.io/v2/ipgeo?apiKey='.GEO_KEY.'&ip='.$ip,
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_TIMEOUT => 5,
|
|
CURLOPT_CONNECTTIMEOUT => 3,
|
|
CURLOPT_FOLLOWLOCATION => true,
|
|
CURLOPT_MAXREDIRS => 2,
|
|
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
|
|
]);
|
|
|
|
$response = curl_exec($curl);
|
|
$httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
|
|
$curlError = curl_error($curl);
|
|
curl_close($curl);
|
|
|
|
if ($response === false || $httpCode !== 200) {
|
|
$err = $curlError ?: "HTTP $httpCode";
|
|
error_log("Geo API failed ($err) for IP $ip");
|
|
return false;
|
|
}
|
|
|
|
$data = json_decode($response, true);
|
|
if (isset($data)) {
|
|
return [
|
|
'ip' => $data['ip'],
|
|
'city' => $data['location']['city'],
|
|
'state' => $data['location']['state_prov'],
|
|
'country' => $data['location']['country_name']
|
|
];
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function loadLanguage() {
|
|
$language = !empty($_SESSION['SITE']['language']) ? $_SESSION['SITE']['language'] : 'en';
|
|
$languageFile = __ROOT__ . "/lng/{$language}/menu.php";
|
|
|
|
if (file_exists($languageFile)) {
|
|
require_once($languageFile);
|
|
} else {
|
|
require_once(__ROOT__ . "/lng/en/menu.php");
|
|
}
|
|
}
|
|
|
|
function generateCorrelationId(): string {
|
|
$data = random_bytes(16);
|
|
$data[6] = chr(ord($data[6]) & 0x0f | 0x40);
|
|
$data[8] = chr(ord($data[8]) & 0x3f | 0x80);
|
|
return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
|
|
}
|
|
|
|
function cleanupVelocityLog() {
|
|
$velocity_file = __ROOT__ . '/logs/velocity.csv';
|
|
$temp_file = __ROOT__ . '/logs/velocity_temp.csv';
|
|
$now = time();
|
|
$max_age = 86400;
|
|
|
|
if (!file_exists($velocity_file)) return;
|
|
|
|
$input = fopen($velocity_file, 'r');
|
|
$output = fopen($temp_file, 'w');
|
|
|
|
while (($line = fgetcsv($input)) !== FALSE) {
|
|
if (count($line) >= 2) {
|
|
$timestamp = (int)$line[1];
|
|
if (($now - $timestamp) <= $max_age) {
|
|
fputcsv($output, $line);
|
|
}
|
|
}
|
|
}
|
|
|
|
fclose($input);
|
|
fclose($output);
|
|
rename($temp_file, $velocity_file);
|
|
}
|
|
|
|
//====================================================
|
|
// TELEGRAM FUNCTIONS — optional, requires TELEGRAM_BOT_TOKEN + TELEGRAM_CHAT_ID
|
|
//====================================================
|
|
|
|
function send2telegram($message) {
|
|
if (!defined('TELEGRAM_BOT_TOKEN') || !defined('TELEGRAM_CHAT_ID') ||
|
|
empty(TELEGRAM_BOT_TOKEN) || empty(TELEGRAM_CHAT_ID)) {
|
|
return false;
|
|
}
|
|
|
|
$url = "https://api.telegram.org/bot" . TELEGRAM_BOT_TOKEN . "/sendMessage";
|
|
$data = [
|
|
'chat_id' => TELEGRAM_CHAT_ID,
|
|
'text' => $message,
|
|
'parse_mode' => 'Markdown'
|
|
];
|
|
|
|
$ch = curl_init();
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_URL => $url,
|
|
CURLOPT_POST => true,
|
|
CURLOPT_POSTFIELDS => $data,
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_TIMEOUT => 3,
|
|
CURLOPT_CONNECTTIMEOUT => 2
|
|
]);
|
|
|
|
$result = curl_exec($ch);
|
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
$error = curl_error($ch);
|
|
curl_close($ch);
|
|
|
|
if ($error || $httpCode !== 200) {
|
|
error_log("Telegram API failed: " . ($error ?: "HTTP $httpCode"));
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|