servnest/fn/common.php

159 lines
4.9 KiB
PHP

<?php declare(strict_types=1);
function output(int $code, string $msg = '', array $logs = [''], array $data = []): never {
http_response_code($code);
$shortCode = intval($code / 100);
if ($shortCode === 5)
error_log('Internal error: ' . strip_tags($msg) . implode(LF, $logs));
$final_message = match ($shortCode) {
2 => ($msg === '') ? '' : '<p><output>' . _('<strong>Success</strong>: ') . '<em>' . $msg . '</em></output></p>' . LF,
4 => '<p><output>' . _('<strong>User error</strong>: ') . '<em>' . $msg . '</em></output></p>' . LF,
5 => '<p><output>' . _('<strong>Server error</strong>: ') . '<em>' . $msg . '</em></output></p>' . LF,
};
if (is_callable('displayPage'))
displayPage(array_merge(['final_message' => $final_message], $data));
echo $final_message;
exit();
}
function exescape(array $args, array &$output = NULL, int &$result_code = NULL): int {
exec('2>&1 ' . implode(' ', array_map('escapeshellarg', $args)), $output, $result_code);
return $result_code;
}
function kdig(string $name, string $type, string $server = NULL): array {
exescape([
CONF['dns']['kdig_path'],
'+json',
'+timeout=5',
'+retry=0',
'+noidn',
'-q',
$name,
'-t',
$type,
...(isset($server) ? ['@' . $server] : []),
], $output, $code);
if ($code !== 0)
throw new KdigException($name . ' ' . $type . ' resolution failed.');
return json_decode(implode(LF, $output), true, flags: JSON_THROW_ON_ERROR);
}
function insert(string $table, array $values): void {
$query = 'INSERT INTO "' . $table . '"(';
foreach ($values as $key => $val) {
if ($key === array_key_last($values))
$query .= "$key";
else
$query .= "$key, ";
}
$query .= ') VALUES(';
foreach ($values as $key => $val) {
if ($key === array_key_last($values))
$query .= ":$key";
else
$query .= ":$key, ";
}
$query .= ')';
DB->prepare($query)
->execute($values);
}
function query(string $action, string $table, array $conditions = [], array $columns = NULL): array {
$query = match ($action) {
'select' => 'SELECT ' . implode(',', $columns ?? ['*']),
'delete' => 'DELETE',
};
$query .= ' FROM "' . $table . '"';
foreach ($conditions as $key => $val) {
if ($key === array_key_first($conditions))
$query .= " WHERE $key = :$key";
else
$query .= " AND $key = :$key";
}
$stmt = DB->prepare($query);
$stmt->execute($conditions);
if (count($columns ?? []) === 1)
return array_column($stmt->fetchAll(PDO::FETCH_ASSOC), $columns[0]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
function displayIndex(): void { ?>
<nav>
<dl>
<?php foreach (PAGES[SERVICE] as $pageId => $page) {
if ($pageId === 'index') continue;
?>
<dt><a href="<?= $pageId ?>"><?= $page['title'] ?></a></dt>
<dd>
<?= $page['description'] ?>
</dd>
<?php } ?>
</dl>
</nav>
<?php
}
function redirUrl(string $pageId): string {
return CONF['common']['prefix'] . '/' . $pageId . '?redir=' . PAGE_URL;
}
function redir(string $redir_to = NULL): never {
$redir_to ??= $_GET['redir'] ?? NULL;
if ($redir_to === NULL) {
header('Location: ' . CONF['common']['prefix'] . '/');
exit();
}
if (preg_match('/^[0-9a-z\/-]{0,128}$/D', $redir_to) !== 1)
output(403, 'Wrong character in <code>redir</code>.');
header('Location: ' . CONF['common']['prefix'] . '/' . $redir_to);
exit();
}
// PHP rmdir() only works on empty directories
function removeDirectory(string $dir): void {
$dirObj = new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS);
$files = new RecursiveIteratorIterator($dirObj, RecursiveIteratorIterator::CHILD_FIRST);
foreach ($files as $file)
$file->isDir() && !$file->isLink() ? rmdir($file->getPathname()) : unlink($file->getPathname());
if (rmdir($dir) !== true)
output(500, 'Unable to remove directory.');
}
function equalArrays(array $a, array $b): bool {
return array_diff($a, $b) === [] AND array_diff($b, $a) === [];
}
/*
This token authenticates the user to the server through a public communication (the DNS).
It is therefore also designed to keep private:
- the user's id
- that a same user used a token multiple times (by using a unique salt for each token)
*/
if (time() - query('select', 'params', ['name' => 'secret_key_last_change'], ['value'])[0] >= 86400 * 20) {
DB->prepare("UPDATE params SET value = :secret_key WHERE name = 'secret_key';")
->execute([':secret_key' => bin2hex(random_bytes(32))]);
DB->prepare("UPDATE params SET value = :last_change WHERE name = 'secret_key_last_change';")
->execute([':last_change' => time()]);
}
define('SECRET_KEY', hex2bin(query('select', 'params', ['name' => 'secret_key'], ['value'])[0]));
function getAuthToken(): string {
$salt = bin2hex(random_bytes(4));
$hash = hash_hmac('sha256', $salt . ($_SESSION['id'] ?? ''), SECRET_KEY);
return $salt . '-' . substr($hash, 0, 32);
}
function checkAuthToken(string $salt, string $hash): void {
$correctProof = substr(hash_hmac('sha256', $salt . $_SESSION['id'], SECRET_KEY), 0, 32);
if (hash_equals($correctProof, $hash) !== true)
output(403, _('Wrong proof.'));
}