diff --git a/README.md b/README.md
index 2449d77..c68747e 100644
--- a/README.md
+++ b/README.md
@@ -172,6 +172,27 @@ HexaHost-Frontend/
### Produktion
Für den Produktivbetrieb `public/` als Webroot konfigurieren.
+### Production-Build & Veröffentlichung
+
+Der Quellcode bleibt auf `dev`, der veröffentlichte Stand liegt auf `main` (ohne Kommentare, obfuskiertes JS).
+
+**Voraussetzungen:** Node.js 18+ (inkl. npm), PHP 8+ CLI, Git
+
+```powershell
+# Windows
+.\scripts\run-build.ps1
+.\scripts\publish-to-main.ps1 -Push
+```
+
+```bash
+# Linux / macOS
+chmod +x scripts/*.sh
+./scripts/run-build.sh
+./scripts/publish-to-main.sh --push
+```
+
+Details: `scripts/build/README.md`
+
## 🔗 Backend-Integration
Das Backend-Repository enthält folgende wiederverwendbare Komponenten:
diff --git a/backend/.htaccess b/backend/.htaccess
new file mode 100644
index 0000000..4e55896
--- /dev/null
+++ b/backend/.htaccess
@@ -0,0 +1,8 @@
+# Direkten Zugriff auf Backend-Dateien verhindern (Document Root = public/)
+
+ Require all denied
+
+
+ Order deny,allow
+ Deny from all
+
diff --git a/backend/api/dns-lookup.php b/backend/api/dns-lookup.php
index ad7cf59..9b9bd5f 100644
--- a/backend/api/dns-lookup.php
+++ b/backend/api/dns-lookup.php
@@ -1,12 +1,14 @@
'Nur GET-Anfragen erlaubt']);
exit;
}
-// Domain-Parameter prüfen
-$domain = isset($_GET['domain']) ? trim($_GET['domain']) : '';
-
-if (empty($domain)) {
- http_response_code(400);
- echo json_encode(['error' => 'Domain-Parameter fehlt']);
- exit;
+if (!checkApiRateLimit('dns-lookup')) {
+ rejectApiRateLimit();
}
-// Domain validieren (einfache Prüfung)
-if (!preg_match('/^[a-zA-Z0-9][a-zA-Z0-9\-\.]*\.[a-zA-Z]{2,}$/', $domain)) {
+$domain = getValidatedDomainParam();
+
+if ($domain === null) {
http_response_code(400);
- echo json_encode(['error' => 'Ungültiges Domain-Format']);
+ echo json_encode(['error' => empty($_GET['domain']) ? 'Domain-Parameter fehlt' : 'Ungültiges Domain-Format']);
exit;
}
diff --git a/backend/api/dns-propagation.php b/backend/api/dns-propagation.php
index 62f2949..15b8f1e 100644
--- a/backend/api/dns-propagation.php
+++ b/backend/api/dns-propagation.php
@@ -1,12 +1,14 @@
'Google', 'ip' => '8.8.8.8', 'location' => 'Global'],
@@ -29,18 +35,12 @@ $dnsServers = [
['name' => 'Level3', 'ip' => '4.2.2.1', 'location' => 'USA'],
];
-$domain = isset($_GET['domain']) ? trim($_GET['domain']) : '';
+$domain = getValidatedDomainParam();
$type = isset($_GET['type']) ? strtoupper(trim($_GET['type'])) : 'A';
-if (empty($domain)) {
+if ($domain === null) {
http_response_code(400);
- echo json_encode(['error' => 'Domain-Parameter fehlt']);
- exit;
-}
-
-if (!preg_match('/^[a-zA-Z0-9][a-zA-Z0-9\-\.]*\.[a-zA-Z]{2,}$/', $domain)) {
- http_response_code(400);
- echo json_encode(['error' => 'Ungültiges Domain-Format']);
+ echo json_encode(['error' => empty($_GET['domain']) ? 'Domain-Parameter fehlt' : 'Ungültiges Domain-Format']);
exit;
}
@@ -100,7 +100,12 @@ function queryDnsServer(string $domain, string $type, string $server): array {
$records = [];
// Versuche zuerst dig zu verwenden (genauer)
- $digResult = @shell_exec("dig @{$server} {$domain} {$type} +short +time=2 +tries=1 2>/dev/null");
+ $digResult = @shell_exec(
+ 'dig @' . escapeshellarg($server) . ' '
+ . escapeshellarg($domain) . ' '
+ . escapeshellarg($type)
+ . ' +short +time=2 +tries=1 2>/dev/null'
+ );
if ($digResult !== null && trim($digResult) !== '') {
$lines = array_filter(explode("\n", trim($digResult)));
diff --git a/backend/api/ping-check.php b/backend/api/ping-check.php
index 0ee176a..1fa326b 100644
--- a/backend/api/ping-check.php
+++ b/backend/api/ping-check.php
@@ -7,6 +7,8 @@
* Verwendung: GET /api/ping-check.php?domain=example.com
*/
+require_once __DIR__ . '/../includes/api-helpers.php';
+
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, OPTIONS');
@@ -17,21 +19,15 @@ if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
exit;
}
-$domain = isset($_GET['domain']) ? trim($_GET['domain']) : '';
-
-if (empty($domain)) {
- http_response_code(400);
- echo json_encode(['error' => 'Domain-Parameter fehlt']);
- exit;
+if (!checkApiRateLimit('ping-check')) {
+ rejectApiRateLimit();
}
-// Protokoll und Pfad entfernen
-$domain = preg_replace('/^(https?:\/\/)?/', '', $domain);
-$domain = explode('/', $domain)[0];
+$domain = getValidatedDomainParam();
-if (!preg_match('/^[a-zA-Z0-9][a-zA-Z0-9\-\.]*\.[a-zA-Z]{2,}$/', $domain)) {
+if ($domain === null) {
http_response_code(400);
- echo json_encode(['error' => 'Ungültiges Domain-Format']);
+ echo json_encode(['error' => empty($_GET['domain']) ? 'Domain-Parameter fehlt' : 'Ungültiges Domain-Format']);
exit;
}
@@ -99,7 +95,8 @@ function checkIcmpPing(string $domain): array {
];
// Versuche ping-Kommando
- $pingResult = @shell_exec("ping -c 3 -W 2 {$domain} 2>/dev/null");
+ $safeDomain = escapeshellarg($domain);
+ $pingResult = @shell_exec("ping -c 3 -W 2 {$safeDomain} 2>/dev/null");
if ($pingResult) {
// Prüfe auf erfolgreiche Antworten
diff --git a/backend/api/reverse-dns.php b/backend/api/reverse-dns.php
index 4aa832d..661429a 100644
--- a/backend/api/reverse-dns.php
+++ b/backend/api/reverse-dns.php
@@ -1,12 +1,14 @@
'Domain-Parameter fehlt']);
- exit;
+if (!checkApiRateLimit('ssl-check')) {
+ rejectApiRateLimit();
}
-// Protokoll und Pfad entfernen
-$domain = preg_replace('/^(https?:\/\/)?/', '', $domain);
-$domain = explode('/', $domain)[0];
-$domain = explode(':', $domain)[0]; // Port entfernen
+$domain = getValidatedDomainParam();
-if (!preg_match('/^[a-zA-Z0-9][a-zA-Z0-9\-\.]*\.[a-zA-Z]{2,}$/', $domain)) {
+if ($domain === null) {
http_response_code(400);
- echo json_encode(['error' => 'Ungültiges Domain-Format']);
+ echo json_encode(['error' => empty($_GET['domain']) ? 'Domain-Parameter fehlt' : 'Ungültiges Domain-Format']);
exit;
}
diff --git a/backend/api/whois-lookup.php b/backend/api/whois-lookup.php
index 5dc9ed6..c0a76f1 100644
--- a/backend/api/whois-lookup.php
+++ b/backend/api/whois-lookup.php
@@ -1,12 +1,14 @@
Betreff-Schlüssel => Anzeigename
+ */
+function getContactSubjectMap(): array {
+ return [
+ 'allgemeine-anfrage' => 'Allgemeine Anfrage',
+ 'vpc-anfrage' => 'Virtual Private Container Anfrage',
+ 'vps-anfrage' => 'Virtual Private Server Anfrage',
+ 'mail-gateway-anfrage' => 'Mail Gateway Anfrage',
+ 'webhosting-anfrage' => 'Webhosting Anfrage',
+ 'it-beratung' => 'IT-Beratung',
+ 'it-support' => 'IT-Support & Fehlerbehebung',
+ 'netzwerk-wlan' => 'Netzwerk & WLAN-Einrichtung',
+ 'it-sicherheit-backup' => 'IT-Sicherheit & Backup',
+ 'webseiten-hosting-service' => 'Webseiten- & Hosting-Service',
+ 'wartung-betreuung' => 'Wartung & Betreuung',
+ 'support' => 'Technischer Support',
+ 'beratung' => 'Persönliche Beratung',
+ 'migration' => 'Migration/Umzug',
+ 'sonstiges' => 'Sonstige Anfrage',
+ ];
+}
+
+/**
+ * @param string $subjectKey
+ */
+function isAllowedContactSubject(string $subjectKey): bool {
+ return array_key_exists($subjectKey, getContactSubjectMap());
+}
+
+/**
+ * Betreff aus ?product= oder ?package= für die Kontaktseite ableiten
+ */
+function getPreselectedContactSubject(): string {
+ $productMap = [
+ 'vpc' => 'vpc-anfrage',
+ 'vps' => 'vps-anfrage',
+ 'mail-gateway' => 'mail-gateway-anfrage',
+ 'webhosting' => 'webhosting-anfrage',
+ ];
+
+ if (!empty($_GET['product'])) {
+ $product = strtolower(preg_replace('/[^a-z0-9-]/', '', (string) $_GET['product']));
+ if (isset($productMap[$product])) {
+ return $productMap[$product];
+ }
+ }
+
+ if (!empty($_GET['package'])) {
+ $package = strtolower(preg_replace('/[^a-z0-9-]/', '', (string) $_GET['package']));
+ foreach ($productMap as $productId => $subjectKey) {
+ if (str_starts_with($package, $productId . '-')) {
+ return $subjectKey;
+ }
+ }
+ }
+
+ return '';
+}
diff --git a/backend/config/mail-config.php b/backend/config/mail-config.php
index 46f0164..55850c0 100644
--- a/backend/config/mail-config.php
+++ b/backend/config/mail-config.php
@@ -140,24 +140,6 @@ function isValidEmail($email) {
return true;
}
-// CSRF Token generieren (wird in functions.php verwendet)
-// Hinweis: Diese Funktion existiert auch in functions.php - hier nur als Fallback
-if (!function_exists('generateCSRFToken')) {
- function generateCSRFToken() {
- if (!isset($_SESSION['csrf_token'])) {
- $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
- }
- return $_SESSION['csrf_token'];
- }
-}
-
-// CSRF Token validieren
-if (!function_exists('validateCSRFToken')) {
- function validateCSRFToken($token) {
- return isset($_SESSION['csrf_token']) && hash_equals($_SESSION['csrf_token'], $token);
- }
-}
-
/**
* Hilfsfunktion zum Abrufen der Konfiguration als Array
* Kompatibilität mit contact-handler.php
@@ -183,6 +165,9 @@ function getHexaHostConfig($key = null) {
// Sicherheit
'max_requests_per_hour' => MAX_REQUESTS_PER_HOUR,
'honeypot_field' => 'website',
+ 'enable_csrf' => ENABLE_CSRF_PROTECTION,
+ 'min_message_length' => MIN_MESSAGE_LENGTH,
+ 'max_message_length' => MAX_MESSAGE_LENGTH,
// Debug
'debug_mode' => DEBUG_MODE,
diff --git a/backend/includes/api-helpers.php b/backend/includes/api-helpers.php
new file mode 100644
index 0000000..9a98864
--- /dev/null
+++ b/backend/includes/api-helpers.php
@@ -0,0 +1,112 @@
+ []];
+
+ $handle = @fopen($cacheFile, 'c+');
+ if ($handle === false) {
+ return true;
+ }
+
+ try {
+ if (!flock($handle, LOCK_EX)) {
+ return true;
+ }
+
+ $contents = stream_get_contents($handle);
+ if ($contents !== false && $contents !== '') {
+ $decoded = json_decode($contents, true);
+ if (is_array($decoded) && isset($decoded['requests'])) {
+ $data = $decoded;
+ }
+ }
+
+ $data['requests'] = array_values(array_filter(
+ $data['requests'],
+ static fn($timestamp) => ($currentTime - (int) $timestamp) < 3600
+ ));
+
+ if (count($data['requests']) >= $maxPerHour) {
+ return false;
+ }
+
+ $data['requests'][] = $currentTime;
+ ftruncate($handle, 0);
+ rewind($handle);
+ fwrite($handle, json_encode($data));
+ } finally {
+ flock($handle, LOCK_UN);
+ fclose($handle);
+ }
+
+ return true;
+}
+
+/**
+ * Domain aus GET-Parameter normalisieren und validieren
+ */
+function getValidatedDomainParam(string $param = 'domain'): ?string {
+ if (empty($_GET[$param])) {
+ return null;
+ }
+
+ $domain = trim((string) $_GET[$param]);
+ $domain = preg_replace('/^(https?:\/\/)?/', '', $domain);
+ $domain = explode('/', $domain)[0];
+ $domain = explode(':', $domain)[0];
+
+ if (!preg_match('/^[a-zA-Z0-9][a-zA-Z0-9\-\.]*\.[a-zA-Z]{2,}$/', $domain)) {
+ return null;
+ }
+
+ return $domain;
+}
+
+/**
+ * Rate-Limit-JSON-Antwort senden und beenden
+ */
+function rejectApiRateLimit(): void {
+ http_response_code(429);
+ echo json_encode(['error' => 'Zu viele Anfragen. Bitte versuchen Sie es später erneut.']);
+ exit;
+}
diff --git a/backend/includes/footer.php b/backend/includes/footer.php
index b00634e..03118f4 100644
--- a/backend/includes/footer.php
+++ b/backend/includes/footer.php
@@ -160,7 +160,7 @@
-
+