improved debugging + health endpoint

This commit is contained in:
tom.hempel
2026-06-01 19:09:27 +02:00
parent a5e58e88c1
commit 6895aa5674
4 changed files with 117 additions and 38 deletions

View File

@ -50,6 +50,7 @@ $routes = [
'backup' => __DIR__ . '/../handlers/backup.php', 'backup' => __DIR__ . '/../handlers/backup.php',
'dev' => __DIR__ . '/../handlers/dev.php', 'dev' => __DIR__ . '/../handlers/dev.php',
'dev/import' => __DIR__ . '/../handlers/dev.php', 'dev/import' => __DIR__ . '/../handlers/dev.php',
'health' => __DIR__ . '/../handlers/health.php',
]; ];
try { try {

View File

@ -6,17 +6,67 @@ require_once __DIR__ . '/lib/text_sanitize.php';
/** /**
* Values loaded from the project .env file (see qdb_load_dotenv). * Values loaded from the project .env file (see qdb_load_dotenv).
* Checked before getenv so a stale/wrong FPM pool variable cannot override .env.
* *
* @var array<string, string> * @var array<string, string>
*/ */
$GLOBALS['qdb_dotenv'] = []; $GLOBALS['qdb_dotenv'] = [];
function qdb_env_file_path(): string {
return __DIR__ . '/.env';
}
/**
* Parse a dotenv file (cached per path). Does not use getenv/putenv.
*
* @return array<string, string>
*/
function qdb_parse_env_file(string $path): array {
static $cache = [];
if (isset($cache[$path])) {
return $cache[$path];
}
$out = [];
if (!is_readable($path)) {
return $cache[$path] = $out;
}
$lines = file($path, FILE_IGNORE_NEW_LINES);
if ($lines === false) {
return $cache[$path] = $out;
}
foreach ($lines as $line) {
$line = trim($line);
if ($line === '' || $line[0] === '#') {
continue;
}
$eq = strpos($line, '=');
if ($eq === false) {
continue;
}
$name = trim(substr($line, 0, $eq));
$value = trim(substr($line, $eq + 1));
if ($name === '') {
continue;
}
$len = strlen($value);
if ($len >= 2) {
$q = $value[0];
if (($q === '"' && $value[$len - 1] === '"') || ($q === "'" && $value[$len - 1] === "'")) {
$value = substr($value, 1, -1);
}
}
$out[$name] = $value;
}
return $cache[$path] = $out;
}
/** /**
* Read an environment variable (.env file first, then getenv / $_ENV / $_SERVER). * Read an environment variable (.env file first, then getenv / $_ENV / $_SERVER).
* PHP-FPM often disables putenv(); .env is applied via qdb_dotenv + $_ENV.
*/ */
function qdb_env_get(string $name): ?string { function qdb_env_get(string $name): ?string {
$fileVars = qdb_parse_env_file(qdb_env_file_path());
if (isset($fileVars[$name]) && $fileVars[$name] !== '') {
return $fileVars[$name];
}
if (isset($GLOBALS['qdb_dotenv'][$name]) && $GLOBALS['qdb_dotenv'][$name] !== '') { if (isset($GLOBALS['qdb_dotenv'][$name]) && $GLOBALS['qdb_dotenv'][$name] !== '') {
return $GLOBALS['qdb_dotenv'][$name]; return $GLOBALS['qdb_dotenv'][$name];
} }
@ -42,52 +92,24 @@ function qdb_env_set(string $name, string $value): void {
} }
} }
/** /** Load KEY=value pairs from .env into $GLOBALS['qdb_dotenv'] and $_ENV. */
* Load KEY=value pairs from .env in the project root.
* Values from the file take precedence over PHP-FPM / getenv for the same keys.
*/
function qdb_load_dotenv(string $path): void { function qdb_load_dotenv(string $path): void {
if (!is_readable($path)) { foreach (qdb_parse_env_file($path) as $name => $value) {
return;
}
$lines = file($path, FILE_IGNORE_NEW_LINES);
if ($lines === false) {
return;
}
foreach ($lines as $line) {
$line = trim($line);
if ($line === '' || $line[0] === '#') {
continue;
}
$eq = strpos($line, '=');
if ($eq === false) {
continue;
}
$name = trim(substr($line, 0, $eq));
$value = trim(substr($line, $eq + 1));
if ($name === '') {
continue;
}
$len = strlen($value);
if ($len >= 2) {
$q = $value[0];
if (($q === '"' && $value[$len - 1] === '"') || ($q === "'" && $value[$len - 1] === "'")) {
$value = substr($value, 1, -1);
}
}
$GLOBALS['qdb_dotenv'][$name] = $value; $GLOBALS['qdb_dotenv'][$name] = $value;
qdb_env_set($name, $value); qdb_env_set($name, $value);
} }
} }
qdb_load_dotenv(__DIR__ . '/.env'); qdb_load_dotenv(qdb_env_file_path());
// --- MASTER-KEY (Server-seitig zum Speichern der DB) --- // --- MASTER-KEY (Server-seitig zum Speichern der DB) ---
// Per ENV 'QDB_MASTER_KEY' (Base64- oder ASCII-String) oder Fallback auf bisherigen Wert. // Per ENV 'QDB_MASTER_KEY' (Base64- oder ASCII-String).
function get_master_key_bytes(): string { function get_master_key_bytes(): string {
$env = qdb_env_get('QDB_MASTER_KEY'); $env = qdb_env_get('QDB_MASTER_KEY');
if ($env === null) { if ($env === null) {
throw new RuntimeException('QDB_MASTER_KEY environment variable is not set. Cannot proceed without a master key.'); $hint = is_readable(qdb_env_file_path()) ? 'missing QDB_MASTER_KEY in .env'
: '.env not readable by PHP (' . qdb_env_file_path() . ')';
throw new RuntimeException('QDB_MASTER_KEY is not set. ' . $hint);
} }
$b = base64_decode($env, true); $b = base64_decode($env, true);
if ($b !== false && strlen($b) > 0) { if ($b !== false && strlen($b) > 0) {

View File

@ -194,7 +194,21 @@ function qdb_open(bool $writable = false): array {
if ($lockFp) { flock($lockFp, LOCK_UN); fclose($lockFp); } if ($lockFp) { flock($lockFp, LOCK_UN); fclose($lockFp); }
throw new Exception("Could not read stored DB"); throw new Exception("Could not read stored DB");
} }
$decrypted = aes256_cbc_decrypt_bytes($storedEnc, $masterKey); try {
$decrypted = aes256_cbc_decrypt_bytes($storedEnc, $masterKey);
} catch (Throwable $e) {
error_log(sprintf(
'qdb_open decrypt failed: path=%s realpath=%s enc_bytes=%d key_sha=%s sapi=%s env_readable=%s common=%s',
$dbPath,
realpath($dbPath) ?: 'none',
strlen($storedEnc),
hash('sha256', $masterKey),
PHP_SAPI,
is_readable(__DIR__ . '/.env') ? 'yes' : 'no',
realpath(__DIR__ . '/common.php') ?: __DIR__ . '/common.php'
));
throw $e;
}
if (file_put_contents($tmpDb, $decrypted) === false) { if (file_put_contents($tmpDb, $decrypted) === false) {
if ($lockFp) { flock($lockFp, LOCK_UN); fclose($lockFp); } if ($lockFp) { flock($lockFp, LOCK_UN); fclose($lockFp); }
throw new Exception("Could not write temp DB"); throw new Exception("Could not write temp DB");

42
handlers/health.php Normal file
View File

@ -0,0 +1,42 @@
<?php
/**
* GET /api/health — verify FPM sees the same project root, key, and DB as CLI.
*/
if ($method !== 'GET') {
json_error('METHOD_NOT_ALLOWED', 'Method not allowed', 405);
}
$dbOpenOk = false;
$dbError = null;
try {
[$pdo, $tmpDb, $lockFp] = qdb_open(false);
qdb_discard($tmpDb, $lockFp);
$dbOpenOk = true;
} catch (Throwable $e) {
$dbError = $e->getMessage();
}
$keySha = null;
$keyError = null;
try {
$keySha = hash('sha256', get_master_key_bytes());
} catch (Throwable $e) {
$keyError = $e->getMessage();
}
json_success([
'sapi' => PHP_SAPI,
'projectRoot' => realpath(__DIR__ . '/..') ?: (__DIR__ . '/..'),
'commonFile' => realpath(__DIR__ . '/../common.php') ?: (__DIR__ . '/../common.php'),
'commonMtime' => @filemtime(__DIR__ . '/../common.php') ?: 0,
'envFile' => qdb_env_file_path(),
'envReadable' => is_readable(qdb_env_file_path()),
'keySha256' => $keySha,
'keyError' => $keyError,
'dbPath' => QDB_PATH,
'dbRealpath' => realpath(QDB_PATH) ?: null,
'dbBytes' => is_file(QDB_PATH) ? filesize(QDB_PATH) : 0,
'dbOpenOk' => $dbOpenOk,
'dbError' => $dbError,
'expectedKeySha'=> 'e584e16eb81fbb4f1e3aac965b5225c5fefabc2a67ea20e26e55671c7d9f1ae7',
]);