diff options
Diffstat (limited to 'static/api/contact.php')
| -rw-r--r-- | static/api/contact.php | 180 |
1 files changed, 180 insertions, 0 deletions
diff --git a/static/api/contact.php b/static/api/contact.php new file mode 100644 index 0000000..4689b99 --- /dev/null +++ b/static/api/contact.php @@ -0,0 +1,180 @@ +<?php +/** + * Contact form handler for danix.me + * + * Spam protection: + * 1. Honeypot field — bots fill it, humans don't see it + * 2. Timing check — reject submissions faster than MIN_ELAPSED seconds + * 3. IP rate limit — one submission per IP per RATE_WINDOW seconds + */ + +ob_start(); +header('Content-Type: application/json; charset=utf-8'); +header('X-Content-Type-Options: nosniff'); +header('X-Frame-Options: DENY'); +header('Referrer-Policy: strict-origin-when-cross-origin'); +header('Cache-Control: no-store'); + +// ── Configuration ───────────────────────────────────────────────────────────── +// Credentials are read from server environment variables — never hardcoded here. +// Set them in your nginx fastcgi_param, Apache SetEnv, or hosting panel. +const SENDER_DOMAIN = 'danix.xyz'; // used in From name and subject prefix +const ALLOWED_ORIGIN = 'https://danix.me'; // CSRF: only accept from this origin +const MIN_ELAPSED = 4; // seconds before submission is accepted +const RATE_WINDOW = 300; // seconds between allowed submissions per IP +const RATE_DIR = '/var/lib/danixme/cf_rate'; // owned by www-data:www-data, mode 0700 + +// $cfg = [ +// 'recipient' => getenv('MAIL_RECIPIENT') ?: '', +// 'smtp_host' => getenv('MAIL_SMTP_HOST') ?: '', +// 'smtp_port' => (int)(getenv('MAIL_SMTP_PORT') ?: 587), +// 'smtp_secure' => getenv('MAIL_SMTP_SECURE') ?: 'tls', +// 'smtp_user' => getenv('MAIL_SMTP_USER') ?: '', +// 'smtp_pass' => getenv('MAIL_SMTP_PASS') ?: '', +// ]; + +$cfg = require dirname(__DIR__, 2) . '/mail-config.php'; + +if ($cfg['recipient'] === '' || $cfg['smtp_host'] === '' || $cfg['smtp_user'] === '') { + out(500, 'Mail not configured'); +} +// ────────────────────────────────────────────────────────────────────────────── + +// Only accept POST +if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + out(405, 'Method not allowed'); +} + +// ── CSRF: verify request originates from our own domain ─────────────────────── +$origin = $_SERVER['HTTP_ORIGIN'] ?? ''; +$referer = $_SERVER['HTTP_REFERER'] ?? ''; + +if ($origin !== '') { + if ($origin !== ALLOWED_ORIGIN) { + out(403, 'Forbidden'); + } +} elseif (strpos($referer, ALLOWED_ORIGIN . '/') !== 0) { + out(403, 'Forbidden'); +} +// ───────────────────────────────────────────────────────────────────────────── + +// ── Honeypot ────────────────────────────────────────────────────────────────── +// If the hidden "website" field is filled, it's a bot — silently pretend success. +if (!empty($_POST['website'])) { + out(200, null, true); +} + +// ── Timing check ────────────────────────────────────────────────────────────── +$loadedAt = (int) filter_input(INPUT_POST, '_t', FILTER_SANITIZE_NUMBER_INT); +$elapsed = time() - $loadedAt; + +if ($loadedAt === 0 || $elapsed < MIN_ELAPSED || $elapsed > 7200) { + out(400, 'Invalid submission'); +} + +// ── Rate limiting ───────────────────────────────────────────────────────────── +$ip = preg_replace('/[^a-f0-9:.]/', '', $_SERVER['REMOTE_ADDR'] ?? ''); +$lockDir = rtrim(RATE_DIR, '/'); + +if (!is_dir($lockDir)) { + @mkdir($lockDir, 0700, true); +} + +$lockFile = $lockDir . '/' . md5($ip) . '.lock'; + +// Atomic check+write using exclusive lock — prevents TOCTOU race condition +$fp = fopen($lockFile, 'c+'); +if ($fp === false || !flock($fp, LOCK_EX)) { + out(500, 'Server error'); +} +$lastTime = (int) fread($fp, 20); +if ($lastTime > 0 && (time() - $lastTime) < RATE_WINDOW) { + flock($fp, LOCK_UN); + fclose($fp); + out(429, 'Too many requests'); +} + +// ── Sanitize & validate ─────────────────────────────────────────────────────── +$name = clean(INPUT_POST, 'name', 100); +$email = trim(filter_input(INPUT_POST, 'email', FILTER_SANITIZE_EMAIL) ?? ''); +$subject = clean(INPUT_POST, 'subject', 200); +$message = clean(INPUT_POST, 'message', 5000); + +if ($name === '' || $email === '' || $message === '') { + flock($fp, LOCK_UN); fclose($fp); + out(400, 'Missing required fields'); +} + +if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { + flock($fp, LOCK_UN); fclose($fp); + out(400, 'Invalid email address'); +} + +// ── Build and send the email via PHPMailer ──────────────────────────────────── +require __DIR__ . '/vendor/autoload.php'; + +use PHPMailer\PHPMailer\PHPMailer; +use PHPMailer\PHPMailer\SMTP; +use PHPMailer\PHPMailer\Exception as MailerException; + +$subjectLine = '[contact from danix.me] - ' . ($subject !== '' ? $subject : 'New message from ' . $name); + +$body = implode("\n", [ + 'Name: ' . $name, + 'Email: ' . $email, + 'Subject: ' . ($subject ?: '—'), + '', + str_repeat('─', 48), + '', + $message, +]); + +$mail = new PHPMailer(true); + +try { + $mail->isSMTP(); + $mail->Host = $cfg['smtp_host']; + $mail->SMTPAuth = true; + $mail->Username = $cfg['smtp_user']; + $mail->Password = $cfg['smtp_pass']; + $mail->SMTPSecure = $cfg['smtp_secure'] === 'ssl' ? PHPMailer::ENCRYPTION_SMTPS : PHPMailer::ENCRYPTION_STARTTLS; + $mail->Port = $cfg['smtp_port']; + $mail->CharSet = 'UTF-8'; + + $mail->setFrom('danix@' . SENDER_DOMAIN, SENDER_DOMAIN); + $mail->addReplyTo($email, $name); + $mail->addAddress($cfg['recipient']); + + $mail->Subject = $subjectLine; + $mail->Body = $body; + + $mail->send(); + + rewind($fp); + ftruncate($fp, 0); + fwrite($fp, (string) time()); + flock($fp, LOCK_UN); + fclose($fp); + out(200, null, true); +} catch (MailerException $e) { + flock($fp, LOCK_UN); + fclose($fp); + out(500, 'Failed to send email'); +} + +// ── Helpers ─────────────────────────────────────────────────────────────────── + +function clean(int $type, string $key, int $maxLen): string +{ + $val = filter_input($type, $key, FILTER_UNSAFE_RAW) ?? ''; + $val = strip_tags(trim((string) $val)); + return mb_substr($val, 0, $maxLen); +} + +function out(int $code, ?string $error, bool $success = false): void +{ + ob_end_clean(); + http_response_code($code); + echo json_encode(['success' => $success, 'error' => $error]); + exit; +} |
