Files

227 lines
8.6 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
function load_pnpaas_env(string $path): array
{
if (!is_readable($path)) {
throw new RuntimeException('PnPaaS-Konfiguration ist nicht lesbar.');
}
$values = [];
foreach (file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
$line = trim($line);
if ($line === '' || str_starts_with($line, '#') || !str_contains($line, '=')) {
continue;
}
[$key, $value] = explode('=', $line, 2);
$key = trim($key);
$value = trim($value);
if ($key !== '') {
$values[$key] = trim($value, " \t\r\n\"");
}
}
return $values;
}
function pnpaas_config(): array
{
static $config;
if ($config === null) {
$runtimeConfigPath = __DIR__ . '/runtime-config.php';
if (is_file($runtimeConfigPath)) {
$runtimeConfig = require $runtimeConfigPath;
$config = is_array($runtimeConfig) ? $runtimeConfig : [];
} else {
$config = load_pnpaas_env(dirname(__DIR__) . '/.env');
}
}
return $config;
}
function pnpaas_db(): PDO
{
static $pdo;
if (!$pdo instanceof PDO) {
$config = pnpaas_config();
foreach (['PNPAAS_DB_DSN', 'PNPAAS_DB_USER', 'PNPAAS_DB_PASSWORD'] as $key) {
if (!array_key_exists($key, $config)) {
throw new RuntimeException('Unvollständige PnPaaS-Datenbankkonfiguration.');
}
}
$pdo = new PDO($config['PNPAAS_DB_DSN'], $config['PNPAAS_DB_USER'], $config['PNPAAS_DB_PASSWORD'], [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]);
}
return $pdo;
}
function pnpaas_security_headers(): void
{
if (headers_sent()) return;
header('X-Content-Type-Options: nosniff');
header('X-Frame-Options: DENY');
header('Referrer-Policy: no-referrer');
header('Permissions-Policy: camera=(), microphone=(), geolocation=()');
header('Cross-Origin-Opener-Policy: same-origin');
header("Content-Security-Policy: default-src 'self'; style-src 'self' https://fonts.googleapis.com 'unsafe-inline'; font-src 'self' https://fonts.gstatic.com; script-src 'none'; img-src 'self' data:; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'; connect-src 'self'");
if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') {
header('Strict-Transport-Security: max-age=31536000; includeSubDomains');
}
}
function pnpaas_session_start(): void
{
pnpaas_security_headers();
if (session_status() === PHP_SESSION_ACTIVE) {
return;
}
session_start([
'use_strict_mode' => true,
'use_only_cookies' => true,
'cookie_httponly' => true,
'cookie_samesite' => 'Lax',
'cookie_secure' => !empty(pnpaas_config()['PNPAAS_FORCE_SECURE_COOKIES']) || (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'),
]);
}
function pnpaas_csrf_token(): string
{
pnpaas_session_start();
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return (string)$_SESSION['csrf_token'];
}
function pnpaas_valid_csrf(string $token): bool
{
pnpaas_session_start();
return $token !== '' && hash_equals((string)($_SESSION['csrf_token'] ?? ''), $token);
}
function pnpaas_registration_documents(): array
{
$config = pnpaas_config();
return [
'terms' => ['title' => 'AGB', 'url' => trim((string)($config['PNPAAS_TERMS_URL'] ?? '')), 'version' => trim((string)($config['PNPAAS_TERMS_VERSION'] ?? ''))],
'privacy' => ['title' => 'Datenschutzerklärung', 'url' => trim((string)($config['PNPAAS_PRIVACY_URL'] ?? '')), 'version' => trim((string)($config['PNPAAS_PRIVACY_VERSION'] ?? ''))],
'withdrawal' => ['title' => 'Widerrufsbelehrung', 'url' => trim((string)($config['PNPAAS_WITHDRAWAL_URL'] ?? '')), 'version' => trim((string)($config['PNPAAS_WITHDRAWAL_VERSION'] ?? ''))],
];
}
function pnpaas_app_url(): string
{
$config = pnpaas_config();
if (!empty($config['PNPAAS_APP_URL'])) {
return rtrim($config['PNPAAS_APP_URL'], '/');
}
$scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
$host = $_SERVER['HTTP_HOST'] ?? 'localhost';
return $scheme . '://' . $host . '/pnpaas';
}
function pnpaas_send_account_activation(string $recipient, string $token): bool
{
$config = pnpaas_config();
$from = $config['PNPAAS_MAIL_FROM'] ?? ('no-reply@' . ($_SERVER['HTTP_HOST'] ?? 'localhost'));
$link = pnpaas_app_url() . '/activate-account.php?token=' . rawurlencode($token);
$subject = 'PnPaaS Konto aktivieren';
$body = "Hallo,\n\n" .
"vielen Dank für Ihre Registrierung bei PnPaaS.\n\n" .
"Aktivieren Sie Ihr Konto innerhalb der nächsten 24 Stunden über diesen Link:\n" . $link . "\n\n" .
"Wenn Sie diese Registrierung nicht angefordert haben, können Sie diese E-Mail ignorieren.\n\n" .
"Viele Grüße\nPnPaaS";
$headers = [
'From: ' . $from,
'Content-Type: text/plain; charset=UTF-8',
'X-Mailer: PnPaaS',
];
return mail($recipient, $subject, $body, implode("\r\n", $headers));
}
function pnpaas_send_password_reset(string $recipient, string $token): bool
{
$config = pnpaas_config();
$from = $config['PNPAAS_MAIL_FROM'] ?? ('no-reply@' . ($_SERVER['HTTP_HOST'] ?? 'localhost'));
$link = pnpaas_app_url() . '/reset-password.php?token=' . rawurlencode($token);
$subject = 'PnPaaS Passwort zurücksetzen';
$body = "Hallo,\n\n" .
"für Ihr PnPaaS-Konto wurde eine Passwort-Zurücksetzung angefordert.\n\n" .
"Öffnen Sie innerhalb der nächsten Stunde diesen Link:\n" . $link . "\n\n" .
"Wenn Sie die Anfrage nicht gestellt haben, können Sie diese E-Mail ignorieren.\n\n" .
"Viele Grüße\nPnPaaS";
$headers = [
'From: ' . $from,
'Content-Type: text/plain; charset=UTF-8',
'X-Mailer: PnPaaS',
];
return mail($recipient, $subject, $body, implode("\r\n", $headers));
}
function pnpaas_portainer_request(string $path, string $method = 'GET', ?array $payload = null): array
{
$config = pnpaas_config();
foreach (['PORTAINER_URL', 'PORTAINER_API_KEY'] as $key) {
if (empty($config[$key])) {
throw new RuntimeException('Unvollständige Portainer-Konfiguration.');
}
}
$url = rtrim($config['PORTAINER_URL'], '/') . '/' . ltrim($path, '/');
$handle = curl_init($url);
if ($handle === false) {
throw new RuntimeException('Portainer-Anfrage konnte nicht initialisiert werden.');
}
curl_setopt_array($handle, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_TIMEOUT => 15,
CURLOPT_HTTPHEADER => [
'Accept: application/json',
'Content-Type: application/json',
'X-API-Key: ' . $config['PORTAINER_API_KEY'],
],
CURLOPT_POSTFIELDS => $payload === null ? null : json_encode($payload, JSON_THROW_ON_ERROR),
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
]);
$body = curl_exec($handle);
$error = curl_error($handle);
$status = (int)curl_getinfo($handle, CURLINFO_HTTP_CODE);
curl_close($handle);
if ($body === false || $error !== '') {
$detail = trim($error);
$detail = preg_replace('/(?:X-API-Key|Authorization|Bearer)\s*:\s*[^\s,;]+/i', '[credential redacted]', $detail) ?? $detail;
throw new RuntimeException('Portainer ist nicht erreichbar' . ($detail !== '' ? ': ' . substr($detail, 0, 400) : '.'));
}
if ($status < 200 || ($status >= 300 && $status !== 304)) {
$detail = trim(preg_replace('/\s+/', ' ', (string)$body) ?? '');
$detail = preg_replace('/("?(?:token|password|api[_-]?key|secret)"?\s*:\s*)"[^"]*"/i', '$1"[redacted]"', $detail) ?? $detail;
$detail = substr($detail, 0, 600);
throw new RuntimeException('Portainer HTTP ' . $status . ($detail !== '' ? ': ' . $detail : '.'));
}
if ($body === '' || $body === null) {
return [];
}
$data = json_decode($body, true);
if (!is_array($data)) {
throw new RuntimeException('Portainer hat eine ungültige Antwort geliefert.');
}
return $data;
}
function pnpaas_portainer_endpoint_id(): int
{
$config = pnpaas_config();
$id = (int)($config['PNPAAS_PORTAINER_ENDPOINT_ID'] ?? 3);
if ($id < 1) {
throw new RuntimeException('Ungültige Portainer-Umgebungs-ID.');
}
return $id;
}