summaryrefslogtreecommitdiffstats
path: root/static/api/contact.php
diff options
context:
space:
mode:
Diffstat (limited to 'static/api/contact.php')
-rw-r--r--static/api/contact.php180
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;
+}