true, 'cookie_samesite' => 'Strict', 'use_strict_mode' => true, ]); /** يسجّل حدثاً. القيم لا تُسجَّل أبداً — أسماء المفاتيح فقط. */ function auditLog(string $file, string $event, string $detail = ''): void { $line = sprintf("[%s] %s | %s | %s\n", date('Y-m-d H:i:s'), $_SERVER['REMOTE_ADDR'] ?? '?', $event, $detail); @file_put_contents($file, $line, FILE_APPEND); @chmod($file, 0600); } // ═══════════════════════════════════════════════════════════════ // القيد ٢ — عبارة المرور // ═══════════════════════════════════════════════════════════════ $env = envRead($envFile); $hash = $env['ENV_PANEL_PASSPHRASE'] ?? ''; if ($hash === '' || !str_starts_with($hash, '$2y$')) { http_response_code(503); echo '
';
echo "اللوحة معطّلة: لم تُضبط ENV_PANEL_PASSPHRASE.\n\n";
echo "ولّد تجزئة وضعها في docker/.env:\n\n";
echo " php -r 'echo password_hash(\"عبارتك\", PASSWORD_BCRYPT), PHP_EOL;'\n\n";
echo "ثم: ENV_PANEL_PASSPHRASE='\$2y\$...'\n\n";
echo "اجعلها عبارة لا تشبه أي كلمة مرور أخرى عندك.\n";
echo '';
exit;
}
$error = '';
if (($_POST['action'] ?? '') === 'login') {
// تأخير ثابت: يجعل تخمين العبارة بطيئاً بما يكفي، وbcrypt نفسه
// يتكفّل بالباقي.
usleep(400_000);
if (password_verify((string) ($_POST['passphrase'] ?? ''), $hash)) {
session_regenerate_id(true);
$_SESSION['ok'] = true;
$_SESSION['csrf'] = bin2hex(random_bytes(16));
auditLog($logFile, 'LOGIN_OK');
} else {
$error = 'عبارة غير صحيحة.';
auditLog($logFile, 'LOGIN_FAIL');
}
}
if (($_GET['logout'] ?? '') === '1') {
session_destroy();
header('Location: ?');
exit;
}
$authed = !empty($_SESSION['ok']);
// ═══════════════════════════════════════════════════════════════
// العمليات
// ═══════════════════════════════════════════════════════════════
$notice = '';
if ($authed && ($_POST['action'] ?? '') !== '' && ($_POST['action'] ?? '') !== 'login') {
if (!hash_equals((string) ($_SESSION['csrf'] ?? ''), (string) ($_POST['csrf'] ?? ''))) {
http_response_code(400);
exit('CSRF mismatch');
}
$key = (string) ($_POST['key'] ?? '');
$action = (string) $_POST['action'];
// ── الحارس الجامع ──
// قبل أي فرع: المجمّدة ممنوعة. الفحص هنا لا في كل فرع، فلا يمكن
// لفرع جديد يُضاف لاحقاً أن ينساه.
if (in_array($key, ENV_FROZEN, true)) {
http_response_code(403);
auditLog($logFile, 'BLOCKED_FROZEN', $key);
exit('هذا المفتاح مجمّد ولا يمكن تعديله من اللوحة.');
}
$rotatable = ENV_GENERATED + ENV_SESSION_SCOPED;
if ($action === 'rotate' && isset($rotatable[$key])) {
$backup = envWrite($envFile, [$key => envGenerate($rotatable[$key])]);
if ($backup) {
auditLog($logFile, 'ROTATE', $key);
$notice = "دُوِّر $key. نسخة احتياطية: " . basename($backup)
. ' — أعد تشغيل حاويات php.';
} else {
$error = 'تعذّرت الكتابة.';
}
} elseif ($action === 'set' && in_array($key, ENV_EXTERNAL, true)) {
$value = trim((string) ($_POST['value'] ?? ''));
if ($value === '') {
$error = 'القيمة فارغة.';
} else {
$backup = envWrite($envFile, [$key => $value]);
if ($backup) {
auditLog($logFile, 'SET_EXTERNAL', $key);
$notice = "حُدِّث $key.";
} else {
$error = 'تعذّرت الكتابة.';
}
}
} elseif ($action === 'rotate_db' && isset(ENV_DB_CREDENTIALS[$key])) {
[$notice, $error] = rotateDbPassword($envFile, $logFile, $env, $key);
} else {
$error = 'عملية غير معروفة أو مفتاح غير قابل للتعديل.';
}
$env = envRead($envFile); // إعادة القراءة بعد أي تغيير
}
/**
* يدوّر كلمة مرور مستخدم قاعدة بيانات بالترتيب الصحيح.
*
* الترتيب هو كل شيء: ALTER USER أولاً، ثم **تحقّق باتصال فعلي**، ثم
* كتابة .env. لو كتبنا .env أولاً وفشل ALTER، لصار التطبيق يحمل كلمة
* لا يقبلها الخادم — انقطاع كامل. ولو نجح ALTER وفشلت الكتابة، نرجع
* كلمة المرور القديمة فوراً بدل ترك الطرفين متنافرين.
*
* ولا تُلمس البيانات في أي خطوة: هذا تغيير في mysql.user لا غير.
*/
function rotateDbPassword(string $envFile, string $logFile, array $env, string $key): array
{
$map = ENV_DB_CREDENTIALS[$key];
$user = $env[$map['user']] ?? '';
$dbName = $env[$map['db']] ?? '';
$rootPass = $env['MYSQL_ROOT_PASSWORD'] ?? '';
$oldPass = $env[$key] ?? '';
if ($user === '' || $dbName === '' || $rootPass === '') {
return ['', "ينقص {$map['user']} أو {$map['db']} أو MYSQL_ROOT_PASSWORD."];
}
// كلمة مرور بلا رموز تحتاج هروباً في الشل — hex آمنة في كل سياق.
$newPass = envGenerate(20);
$alter = sprintf(
"ALTER USER %s@'%%' IDENTIFIED BY %s; FLUSH PRIVILEGES;",
quoteIdent($user), quoteLiteral($newPass)
);
[$code] = runMysql($rootPass, $alter);
if ($code !== 0) {
auditLog($logFile, 'DB_ROTATE_FAIL_ALTER', $key);
return ['', 'فشل ALTER USER — لم يتغيّر شيء.'];
}
// تحقّق: هل تعمل الكلمة الجديدة فعلاً؟ الثقة بأن ALTER نجح لا تكفي
// (قد يكون المستخدم معرَّفاً بمضيف آخر مثلاً).
if (!verifyDbLogin($user, $newPass, $dbName)) {
runMysql($rootPass, sprintf(
"ALTER USER %s@'%%' IDENTIFIED BY %s; FLUSH PRIVILEGES;",
quoteIdent($user), quoteLiteral($oldPass)
));
auditLog($logFile, 'DB_ROTATE_ROLLBACK', $key);
return ['', 'الكلمة الجديدة لم تعمل — أُعيدت القديمة.'];
}
$backup = envWrite($envFile, [$key => $newPass]);
if (!$backup) {
runMysql($rootPass, sprintf(
"ALTER USER %s@'%%' IDENTIFIED BY %s; FLUSH PRIVILEGES;",
quoteIdent($user), quoteLiteral($oldPass)
));
auditLog($logFile, 'DB_ROTATE_ROLLBACK_WRITE', $key);
return ['', 'تعذّرت كتابة .env — أُعيدت كلمة المرور القديمة.'];
}
auditLog($logFile, 'DB_ROTATE', "$key ($user@$dbName)");
return ["دُوِّرت كلمة مرور $user. أعد تشغيل حاويات php الآن.", ''];
}
/** هروب معرّف MySQL بالعلامة الخلفية. */
function quoteIdent(string $s): string
{
return '`' . str_replace('`', '``', $s) . '`';
}
/** هروب سلسلة نصية لـ MySQL. */
function quoteLiteral(string $s): string
{
return "'" . addcslashes($s, "\\'\"\0\n\r\032") . "'";
}
/**
* ينفّذ SQL في حاوية mysql.
*
* كلمة المرور تمرّ عبر متغيّر بيئة لا وسيطاً في سطر الأوامر: الوسيط
* يظهر في `ps` لأي مستخدم على الجهاز.
*/
function runMysql(string $rootPass, string $sql): array
{
$cmd = 'docker compose -f ' . escapeshellarg(dirname(__DIR__, 3) . '/docker/docker-compose.yml')
. ' exec -T -e MYSQL_PWD mysql mysql -uroot -e ' . escapeshellarg($sql) . ' 2>&1';
$descriptors = [1 => ['pipe', 'w'], 2 => ['pipe', 'w']];
$proc = proc_open($cmd, $descriptors, $pipes, null, ['MYSQL_PWD' => $rootPass]);
if (!is_resource($proc)) return [1, 'proc_open failed'];
$out = stream_get_contents($pipes[1]) . stream_get_contents($pipes[2]);
fclose($pipes[1]);
fclose($pipes[2]);
return [proc_close($proc), $out];
}
/** يتأكد أن الاعتماد الجديد يسجّل دخولاً فعلياً. */
function verifyDbLogin(string $user, string $pass, string $db): bool
{
$cmd = 'docker compose -f ' . escapeshellarg(dirname(__DIR__, 3) . '/docker/docker-compose.yml')
. ' exec -T -e MYSQL_PWD mysql mysql -u' . escapeshellarg($user)
. ' ' . escapeshellarg($db) . ' -e "SELECT 1" 2>&1';
$descriptors = [1 => ['pipe', 'w'], 2 => ['pipe', 'w']];
$proc = proc_open($cmd, $descriptors, $pipes, null, ['MYSQL_PWD' => $pass]);
if (!is_resource($proc)) return false;
stream_get_contents($pipes[1]);
stream_get_contents($pipes[2]);
fclose($pipes[1]);
fclose($pipes[2]);
return proc_close($proc) === 0;
}
/** حالة المفتاح بلا كشف قيمته. */
function keyState(array $env, string $name): string
{
if (!array_key_exists($name, $env)) return 'مفقود';
return envIsPlaceholder($env[$name]) ? 'نائب' : 'مضبوط';
}
/** بصمة قصيرة تكفي للتمييز ولا تكفي للاستعمال. */
function keyFingerprint(array $env, string $name): string
{
$v = $env[$name] ?? '';
return $v === '' ? '—' : substr(hash('sha256', $v), 0, 8);
}
$csrf = $_SESSION['csrf'] ?? '';
?>
وصول محلي فقط عبر نفق SSH.
= htmlspecialchars($envFile) ?> ·
خروج
طرفاها كلاهما لنا. التدوير آمن — يكفي إعادة تشغيل ما يحملها.
| المفتاح | الحالة | بصمة | |
|---|---|---|---|
= htmlspecialchars($name) ?> |
= $s ?> | = keyFingerprint($env, $name) ?> |
تدويرها يُخرج كل السائقين والركّاب من حساباتهم ويطالبهم بتسجيل دخول جديد. ليس عطلاً، لكنه ليس شيئاً يُفعل وقت الذروة.
| المفتاح | الحالة | بصمة | |
|---|---|---|---|
= htmlspecialchars($name) ?> |
= $s ?> | = keyFingerprint($env, $name) ?> |
التدوير يجري بالترتيب الصحيح: ALTER USER ثم تحقّق باتصال
فعلي ثم كتابة .env — وإن فشلت أي خطوة تُستعاد الكلمة
القديمة فوراً. لا مساس بالبيانات ولا إعادة بناء؛ هذا
تغيير في mysql.user لا غير.
| المفتاح | المستخدم | القاعدة | الحالة | |
|---|---|---|---|---|
= htmlspecialchars($name) ?> |
= htmlspecialchars($env[$map['user']] ?? '—') ?> |
= htmlspecialchars($env[$map['db']] ?? '—') ?> |
= $s ?> | — |
لا تُولَّد — تُنسخ من لوحة المزوّد وتُلصق هنا.
| المفتاح | الحالة | قيمة جديدة | |
|---|---|---|---|
بيانات مخزَّنة في القاعدة مشتقّة من هذه المفاتيح: أسماء الركّاب
وهواتفهم ولوحات السيارات مشفّرة بـ ENC_KEY، وأعمدة
*_bidx محسوبة بـ BLIND_INDEX_PEPPER.
تغيير أيٍّ منها لا يعيد تشفير شيء — يجعل كل صف قديم
غير قابل للقراءة إلى الأبد، ولا نسخة احتياطية تنفع لأنها هي نفسها
مشفّرة بالمفتاح القديم. تدويرها ترحيلُ بيانات لا تغييرُ إعداد،
ولذلك لا يوجد لها زر هنا ولا مسار يكتبها في هذا الملف.
| المفتاح | الحالة | بصمة |
|---|---|---|
= htmlspecialchars($name) ?> |
= $s ?> | = keyFingerprint($env, $name) ?> |
سجلّ التغييرات: backend/tools/panel/panel_audit.log