diff --git a/.gitignore b/.gitignore index c67bf5b..8dec80f 100644 --- a/.gitignore +++ b/.gitignore @@ -68,4 +68,3 @@ config.production.lua config/furt.conf -scripts/production_test_sequence.sh diff --git a/.version_history b/.version_history index 26b078b..900c60b 100644 --- a/.version_history +++ b/.version_history @@ -20,14 +20,4 @@ 795f8867,78e8ded,fix/json-library-compatibility,2025-09-05T15:44:42Z,michael,git,lua-api 795f8867,d4fa6e3,fix/ssl-dependency-check,2025-09-05T16:20:08Z,michael,git,lua-api a670de0f,d271b84,refactor/extract-health-routes-and-server-core,2025-09-05T17:25:09Z,michael,git,lua-api -a670de0f,25a709e,feature/pid-file-service-management,2025-09-05T20:30:13Z,michael,git,lua-api -a670de0f,59f372f,feature/pid-file-service-management,2025-09-07T14:58:01Z,michael,git,lua-api -a670de0f,683d6e5,fix/validate-config-posix-regex,2025-09-07T16:00:48Z,michael,git,lua-api -a670de0f,24bd94d,feature/systemd-hardening,2025-09-07T16:40:47Z,michael,git,lua-api -4ee95dbc,08b49d3,security/sanitize-test-scripts,2025-09-07T19:25:38Z,michael,git,lua-api -59c85431,8b78066,main,2025-09-10T10:20:50Z,michael,git,lua-api -a71dd794,f5d9f35,main,2025-09-10T12:27:54Z,michael,git,lua-api -de5318f2,304b010,main,2025-09-10T14:45:12Z,michael,git,lua-api -980d67cd,7a921dc,main,2025-09-10T14:46:13Z,michael,git,lua-api -efbcbbd8,f20915f,main,2025-09-10T18:01:18Z,michael,git,lua-api -f777e765,f684ea1,main,2025-09-10T18:04:19Z,michael,git,lua-api +57ce9c01,8fecb01,feature/structured-logging-health-monitoring,2025-09-05T18:31:36Z,michael,git,lua-api diff --git a/README.md b/README.md index a18d120..d27042b 100644 --- a/README.md +++ b/README.md @@ -1,83 +1,160 @@ # Furt API Gateway -**Pure Lua HTTP-Server für digitale Souveränität** +**HTTP-Server in Lua für Service-Integration** ## Überblick -Furt ist ein minimalistisches HTTP-Server in Lua 5.1 für Mail-Versendung über SMTP. Es bietet eine einfache JSON-API für Web-Integration und Multi-Tenant-Unterstützung über API-Keys. +Furt ist ein HTTP-Server der verschiedene Services unter einer API vereint. Aktuell unterstützt es Mail-Versendung über SMTP und bietet eine einfache JSON-API für Web-Integration. ## Features - HTTP-Server mit JSON-APIs -- Multi-Tenant Mail-Routing über SMTP -- API-Key-basierte Authentifizierung +- Mail-Versendung über SMTP +- Request-Routing und Authentication - Health-Check-Endpoints -- Rate-Limiting pro API-Key -- CORS-Support für Frontend-Integration +- Konfigurierbare Rate-Limiting +- Hugo/Website-Integration -## Quick Start +## Dependencies -**Dependencies installieren:** -```bash -# OpenBSD -doas pkg_add lua lua-socket lua-cjson luasec - -# Debian/Ubuntu -sudo apt install lua5.1 lua-socket lua-cjson lua-sec - -# Arch Linux -sudo pacman -S lua51 lua51-socket lua51-dkjson lua51-sec -``` +**Erforderlich:** +- `lua` 5.4+ +- `lua-socket` (HTTP-Server) +- `lua-cjson` (JSON-Verarbeitung) **Installation:** ```bash -git clone https://smida.dragons-at-work.de/DAW/furt.git -cd furt -sudo ./install.sh +# Arch Linux +pacman -S lua lua-socket lua-cjson + +# Ubuntu/Debian +apt install lua5.4 lua-socket lua-cjson ``` -**Server läuft auf:** http://127.0.0.1:7811 +## Installation + +```bash +# Repository klonen +git clone +cd furt + +# Scripts ausführbar machen +chmod +x scripts/*.sh + +# Server starten +./scripts/start.sh +``` + +**Server läuft auf:** http://127.0.0.1:8080 ## API-Endpoints -**Health Check:** +### Health Check ```bash -curl http://127.0.0.1:7811/health +GET /health +→ {"status":"healthy","service":"furt","version":"1.0.0"} ``` -**Mail senden:** +### Mail senden ```bash -curl -X POST http://127.0.0.1:7811/v1/mail/send \ - -H "X-API-Key: your-api-key" \ +POST /v1/mail/send +Content-Type: application/json + +{ + "name": "Name", + "email": "sender@example.com", + "message": "Nachricht" +} + +→ {"success":true,"message":"Mail sent"} +``` + +## Konfiguration + +**Environment Variables (.env):** +```bash +FURT_MAIL_HOST=mail.example.com +FURT_MAIL_PORT=587 +FURT_MAIL_USERNAME=user@example.com +FURT_MAIL_PASSWORD=password +FURT_MAIL_TO=empfaenger@example.com +``` + +**Server-Config (config/server.lua):** +- Port und Host-Einstellungen +- API-Key-Konfiguration +- Rate-Limiting-Parameter + +## Testing + +**Automatische Tests:** +```bash +lua tests/test_http.lua +``` + +**Manuelle Tests:** +```bash +./scripts/test_curl.sh + +# Oder direkt: +curl -X POST http://127.0.0.1:8080/v1/mail/send \ -H "Content-Type: application/json" \ - -d '{"name":"Test","email":"test@example.com","subject":"Test","message":"Test-Nachricht"}' + -d '{"name":"Test","email":"test@example.com","message":"Test"}' ``` -## Dokumentation +## Deployment -**Installation & Konfiguration:** [Furt Wiki](https://smida.dragons-at-work.de/DAW/furt/wiki) +**OpenBSD:** +- rc.d-Script in `deployment/openbsd/` +- Systemd-Integration über Scripts + +**Production-Setup:** +```bash +# Environment-Config kopieren +cp .env.example .env.production +# → SMTP-Credentials anpassen + +# Production-Mode starten +export FURT_ENV=production +./scripts/start.sh +``` ## Projektstruktur ``` furt/ ├── src/ # Lua-Source-Code -├── config/ # Konfiguration -├── scripts/ # Installation & Management -└── deployment/ # System-Integration +│ ├── main.lua # HTTP-Server +│ ├── routes/ # API-Endpoints +│ └── smtp.lua # Mail-Integration +├── config/ # Konfiguration +├── scripts/ # Start/Test-Scripts +├── tests/ # Test-Suite +└── deployment/ # System-Integration ``` -## Integration +## Hugo-Integration -**merkwerk:** Versionierte Furt-Deployment über [merkwerk](https://smida.dragons-at-work.de/DAW/merkwerk) +**Shortcode-Beispiel:** +```html +
+ + + + +
+``` -## License +## Development -ISC - Siehe [LICENSE](LICENSE) für Details. +**Code-Struktur:** +- Module unter 200 Zeilen +- Funktionen unter 50 Zeilen +- Klare Fehlerbehandlung +- Testbare Komponenten -## Links - -- **Repository:** [Forgejo](https://smida.dragons-at-work.de/DAW/furt) -- **Dokumentation:** [Wiki](https://smida.dragons-at-work.de/DAW/furt/wiki) -- **Projekt:** [Dragons@Work](https://dragons-at-work.de) +**Dependencies minimal halten:** +- Nur lua-socket und lua-cjson +- Keine externen HTTP-Libraries +- Standard-Lua-Funktionen bevorzugen diff --git a/VERSION b/VERSION index 845639e..6da28dd 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.1.4 +0.1.1 \ No newline at end of file diff --git a/deployment/linux/furt.service b/deployment/linux/furt.service index 5dd1150..f09104b 100644 --- a/deployment/linux/furt.service +++ b/deployment/linux/furt.service @@ -1,33 +1,18 @@ [Unit] -Description=furt Multi-Tenant API Gateway (Security-Hardened) +Description=furt Multi-Tenant API Gateway After=network.target [Service] Type=forking User=furt Group=furt -ExecStart=/usr/local/share/furt/scripts/start.sh -PIDFile=/var/run/furt/furt.pid +ExecStart=/usr/local/share/furt/scripts/start.sh start WorkingDirectory=/usr/local/share/furt Restart=always RestartSec=5 StandardOutput=journal StandardError=journal -# === SECURITY HARDENING === - -# Filesystem Protection -ProtectSystem=strict -ReadWritePaths=/var/run/furt /var/log/furt -ProtectHome=yes - -# Process Hardening -NoNewPrivileges=yes -PrivateTmp=yes - -# Network Restriction -RestrictAddressFamilies=AF_INET - [Install] WantedBy=multi-user.target diff --git a/deployment/openbsd/rc.d-furt b/deployment/openbsd/rc.d-furt index bcdb4b9..465af19 100644 --- a/deployment/openbsd/rc.d-furt +++ b/deployment/openbsd/rc.d-furt @@ -3,52 +3,11 @@ daemon="/usr/local/share/furt/scripts/start.sh" daemon_user="_furt" daemon_cwd="/usr/local/share/furt" +daemon_flags="start" . /etc/rc.d/rc.subr -# PID-File location -pidfile="/var/run/furt/furt.pid" - -# Custom rc_check function (PID-File based) -rc_check() { - [ -f "$pidfile" ] && kill -0 $(cat "$pidfile") 2>/dev/null -} - -# Custom rc_stop function (PID-File based) -rc_stop() { - if [ -f "$pidfile" ]; then - local _pid=$(cat "$pidfile") - echo "Stopping furt (PID: $_pid)" - kill "$_pid" 2>/dev/null - # Wait for process to die - local _timeout=10 - while [ $_timeout -gt 0 ] && kill -0 "$_pid" 2>/dev/null; do - sleep 1 - _timeout=$((_timeout - 1)) - done - # Force kill if still running - if kill -0 "$_pid" 2>/dev/null; then - echo "Force killing furt (PID: $_pid)" - kill -9 "$_pid" 2>/dev/null - fi - rm -f "$pidfile" - echo "furt stopped" - else - echo "furt not running (no PID-File)" - fi -} - -# Custom rc_reload function (signal-based) -rc_reload() { - if rc_check; then - local _pid=$(cat "$pidfile") - echo "Reloading furt configuration (PID: $_pid)" - kill -HUP "$_pid" - else - echo "furt not running" - return 1 - fi -} +pexp="lua.*src/main.lua" rc_cmd $1 diff --git a/docs/setup-guide.md b/docs/setup-guide.md new file mode 100644 index 0000000..2dc790e --- /dev/null +++ b/docs/setup-guide.md @@ -0,0 +1,176 @@ +# Multi-Tenant furt Setup-Anleitung + +## Installation + +### 1. Dateien platzieren + +```bash +# OpenBSD/FreeBSD +mkdir -p /usr/local/etc/furt +mkdir -p /usr/local/share/furt + +# Oder Linux +mkdir -p /etc/furt +mkdir -p /usr/local/share/furt + +# Source code +cp -r src/ /usr/local/share/furt/ +cp -r config/ /usr/local/share/furt/ +``` + +### 2. Konfiguration erstellen + +```bash +# Beispiel-Config kopieren und anpassen +# OpenBSD/FreeBSD: +cp furt.conf.example /usr/local/etc/furt/furt.conf + +# Linux: +cp furt.conf.example /etc/furt/furt.conf + +# Config editieren +vi /usr/local/etc/furt/furt.conf # oder /etc/furt/furt.conf +``` + +### 3. Start-Script + +```bash +#!/bin/sh +# /usr/local/bin/furt + +cd /usr/local/share/furt +lua src/main.lua +``` + +## Multi-Tenant Konfiguration + +### Beispiel für 3 Websites + +```ini +[server] +host = 127.0.0.1 +port = 8080 + +[smtp_default] +host = mail.dragons-at-work.de +port = 465 +user = noreply@dragons-at-work.de +password = your-smtp-password + +# Website 1: Dragons@Work +[api_key "daw-key-abc123"] +name = "Dragons@Work Website" +permissions = mail:send +allowed_ips = 1.2.3.4/32, 10.0.0.0/8 +mail_to = admin@dragons-at-work.de +mail_from = noreply@dragons-at-work.de +mail_subject_prefix = "[DAW] " + +# Website 2: Biocodie (gleiche SMTP, andere Empfänger) +[api_key "bio-key-def456"] +name = "Biocodie Website" +permissions = mail:send +allowed_ips = 5.6.7.8/32 +mail_to = contact@biocodie.de +mail_from = noreply@biocodie.de +mail_subject_prefix = "[Biocodie] " + +# Website 3: Kunde mit eigenem SMTP +[api_key "kunde-key-ghi789"] +name = "Kunde X Website" +permissions = mail:send +allowed_ips = 9.10.11.12/32 +mail_to = info@kunde-x.de +mail_from = noreply@kunde-x.de +mail_smtp_host = mail.kunde-x.de +mail_smtp_user = noreply@kunde-x.de +mail_smtp_pass = kunde-smtp-password +``` + +## Admin-Workflow + +### Neue Website hinzufügen + +1. **Config editieren:** +```bash +vi /usr/local/etc/furt/furt.conf +``` + +2. **Neuen API-Key-Block hinzufügen:** +```ini +[api_key "neue-website-key"] +name = "Neue Website" +permissions = mail:send +allowed_ips = 12.34.56.78/32 +mail_to = contact@neue-website.de +mail_from = noreply@neue-website.de +``` + +3. **furt neu starten:** +```bash +systemctl restart furt +# oder +pkill -f "lua.*main.lua" && /usr/local/bin/furt & +``` + +### Website testen + +```bash +# Test mit curl +curl -X POST http://localhost:8080/v1/mail/send \ + -H "X-API-Key: neue-website-key" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "Test User", + "email": "test@example.com", + "subject": "Test Message", + "message": "This is a test message" + }' +``` + +## Vorteile des Multi-Tenant-Systems + +### ✅ Ein Server, viele Websites +- Alle Websites nutzen eine furt-Instanz +- Jede Website hat eigenen API-Key +- Verschiedene Empfänger-Adressen +- Verschiedene SMTP-Server möglich + +### ✅ Admin-freundlich +- Nginx-style Config-Format +- Einfach neue Websites hinzufügen +- Klare Struktur pro Website +- Kommentare möglich + +### ✅ Sicher +- IP-Restrictions pro Website +- Permissions pro API-Key +- Separate SMTP-Credentials möglich +- Rate-Limiting bleibt erhalten + +### ✅ Flexibel +- Default SMTP + website-spezifische SMTP +- Subject-Prefix pro Website +- Verschiedene Mail-Adressen +- Beliebig viele Websites + +## Backward Compatibility + +Das neue System ist **vollständig kompatibel** mit der alten config/server.lua API. Bestehende Module (auth.lua, main.lua, etc.) funktionieren ohne Änderungen. + +## Troubleshooting + +### Config-Parsing-Fehler +```bash +# Config-Syntax prüfen +lua -e "require('src.config_parser').parse_file('/usr/local/etc/furt/furt.conf')" +``` + +### Mail-Routing testen +```bash +# Logs anschauen +tail -f /var/log/furt.log + +# Debug-Mode +FURT_DEBUG=true lua src/main.lua +``` \ No newline at end of file diff --git a/install.sh b/install.sh index 4711c2b..9de2751 100755 --- a/install.sh +++ b/install.sh @@ -102,7 +102,7 @@ else echo "" echo "Next steps:" echo "1. Edit configuration file:" - if [ "$(uname)" = "OpenBSD" ]; then + if [ "$(uname)" = "OpenBSD" ] || [ "$(uname)" = "FreeBSD" ]; then echo " /usr/local/etc/furt/furt.conf" else echo " /etc/furt/furt.conf" diff --git a/scripts/cleanup_debug.sh b/scripts/cleanup_debug.sh old mode 100755 new mode 100644 diff --git a/scripts/create-service.sh b/scripts/create-service.sh index eed3ebe..9732479 100755 --- a/scripts/create-service.sh +++ b/scripts/create-service.sh @@ -15,25 +15,25 @@ if [ "$(uname)" = "OpenBSD" ]; then echo "Error: deployment/openbsd/rc.d-furt template not found" exit 1 fi - + cp deployment/openbsd/rc.d-furt /etc/rc.d/furt chmod +x /etc/rc.d/furt echo "furt_flags=" >> /etc/rc.conf.local rcctl enable furt echo "OpenBSD service created and enabled using repository template" - + elif [ "$(uname)" = "Linux" ]; then # Use systemd template from repository if [ ! -f "deployment/linux/furt.service" ]; then echo "Error: deployment/linux/furt.service template not found" exit 1 fi - + cp deployment/linux/furt.service /etc/systemd/system/ systemctl daemon-reload systemctl enable furt echo "Linux systemd service created and enabled using repository template" - + else echo "Unsupported operating system for service creation" exit 1 diff --git a/scripts/manual_mail_test.sh b/scripts/manual_mail_test.sh old mode 100755 new mode 100644 index 6a1497c..3f8002f --- a/scripts/manual_mail_test.sh +++ b/scripts/manual_mail_test.sh @@ -4,11 +4,11 @@ echo "Testing SMTP with corrected JSON..." # Simple test without timestamp embedding -curl -X POST http://127.0.0.1:7811/v1/mail/send \ +curl -X POST http://127.0.0.1:8080/v1/mail/send \ -H "Content-Type: application/json" \ -d '{ "name": "Furt Test User", - "email": "admin@example.com", + "email": "michael@dragons-at-work.de", "subject": "Furt SMTP Test Success!", "message": "This is a test email from Furt Lua HTTP-Server. SMTP Integration working!" }' diff --git a/scripts/production_test_sequence.sh b/scripts/production_test_sequence.sh new file mode 100644 index 0000000..36a6455 --- /dev/null +++ b/scripts/production_test_sequence.sh @@ -0,0 +1,80 @@ +#!/bin/bash +# Production Test für api.dragons-at-work.de + +echo "Testing Production API via Apache Proxy" +echo "=======================================" + +# Test 1: HTTPS Health Check +echo "" +echo "[1] Testing HTTPS Health Check..." +https_health=$(curl -s https://api.dragons-at-work.de/health) +echo "HTTPS Response: $https_health" + +if echo "$https_health" | grep -q "healthy"; then + echo "[OK] HTTPS Proxy working" +else + echo "[ERROR] HTTPS Proxy failed" + exit 1 +fi + +# Test 2: SMTP Status via HTTPS +echo "" +echo "[2] Testing SMTP Configuration via HTTPS..." +if echo "$https_health" | grep -q '"smtp_configured":true'; then + echo "[OK] SMTP configured and accessible via HTTPS" +else + echo "[ERROR] SMTP not configured or not accessible" +fi + +# Test 3: CORS Headers +echo "" +echo "[3] Testing CORS Headers..." +cors_test=$(curl -s -I https://api.dragons-at-work.de/health | grep -i "access-control") +if [ -n "$cors_test" ]; then + echo "[OK] CORS headers present: $cors_test" +else + echo "[WARN] CORS headers missing - add to Apache config" +fi + +# Test 4: Production Mail Test +echo "" +echo "[4] Testing Production Mail via HTTPS..." +echo "WARNING: This sends real email via production API" +read -p "Continue with production mail test? (y/N): " -n 1 -r +echo + +if [[ $REPLY =~ ^[Yy]$ ]]; then + echo "Sending production test email..." + + prod_mail_response=$(curl -s -X POST https://api.dragons-at-work.de/v1/mail/send \ + -H "Content-Type: application/json" \ + -d '{ + "name": "Production Test", + "email": "test@dragons-at-work.de", + "subject": "Production API Test - Apache Proxy Success!", + "message": "This email was sent via the production API at api.dragons-at-work.de through Apache proxy to Furt-Lua backend. HTTPS integration working!" + }') + + echo "Production Response: $prod_mail_response" + + if echo "$prod_mail_response" | grep -q '"success":true'; then + echo "[OK] PRODUCTION MAIL SENT VIA HTTPS!" + echo "Check admin@dragons-at-work.de for delivery confirmation" + else + echo "[ERROR] Production mail failed" + fi +else + echo "Skipping production mail test" +fi + +# Test 5: Security Headers +echo "" +echo "[5] Testing Security Headers..." +security_headers=$(curl -s -I https://api.dragons-at-work.de/health) +echo "Security Headers:" +echo "$security_headers" | grep -i "x-content-type-options\|x-frame-options\|strict-transport" + +echo "" +echo "Production Test Complete!" +echo "========================" +echo "Next: Hugo integration with https://api.dragons-at-work.de/v1/mail/send" \ No newline at end of file diff --git a/scripts/setup-directories.sh b/scripts/setup-directories.sh index 63881c3..2fdbad6 100755 --- a/scripts/setup-directories.sh +++ b/scripts/setup-directories.sh @@ -4,7 +4,7 @@ set -e # Detect operating system for config directory -if [ "$(uname)" = "OpenBSD" ]; then +if [ "$(uname)" = "OpenBSD" ] || [ "$(uname)" = "FreeBSD" ]; then CONFIG_DIR="/usr/local/etc/furt" USER="_furt" GROUP="_furt" @@ -18,15 +18,12 @@ fi mkdir -p "$CONFIG_DIR" mkdir -p /usr/local/share/furt mkdir -p /var/log/furt -mkdir -p /var/run/furt # Set ownership for log directory (service user needs write access) chown "$USER:$GROUP" /var/log/furt -chown "$USER:$GROUP" /var/run/furt echo "Created directories:" echo " Config: $CONFIG_DIR" echo " Share: /usr/local/share/furt" echo " Logs: /var/log/furt (owned by $USER)" -echo " PID: /var/run/furt (owned by $USER)" diff --git a/scripts/setup-user.sh b/scripts/setup-user.sh index 9188626..29cdb61 100755 --- a/scripts/setup-user.sh +++ b/scripts/setup-user.sh @@ -4,7 +4,7 @@ set -e # Detect operating system -if [ "$(uname)" = "OpenBSD" ]; then +if [ "$(uname)" = "OpenBSD" ] || [ "$(uname)" = "FreeBSD" ]; then # BSD systems use _furt user convention groupadd _furt 2>/dev/null || true useradd -g _furt -s /bin/false -d /var/empty _furt 2>/dev/null || true diff --git a/scripts/setup_env.sh b/scripts/setup_env.sh new file mode 100755 index 0000000..858436e --- /dev/null +++ b/scripts/setup_env.sh @@ -0,0 +1,101 @@ +#!/bin/bash +# furt-lua/scripts/setup_env.sh +# Add SMTP environment variables to existing .env (non-destructive) + +set -e + +# Colors for output +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' # No Color + +echo -e "${GREEN}=== Furt SMTP Environment Setup ===${NC}" + +# Navigate to furt project root +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(dirname "$(dirname "$SCRIPT_DIR")")" +ENV_FILE="$PROJECT_ROOT/.env" + +echo -e "${YELLOW}Project root:${NC} $PROJECT_ROOT" +echo -e "${YELLOW}Environment file:${NC} $ENV_FILE" + +# Check if .env exists +if [ ! -f "$ENV_FILE" ]; then + echo -e "${YELLOW}Creating new .env file...${NC}" + cat > "$ENV_FILE" << 'EOF' +# Dragons@Work Project Environment Variables + +# Furt SMTP Configuration for mail.dragons-at-work.de +SMTP_HOST="mail.dragons-at-work.de" +SMTP_PORT="465" +SMTP_USERNAME="your_email@dragons-at-work.de" +SMTP_PASSWORD="your_smtp_password" +SMTP_FROM="noreply@dragons-at-work.de" +SMTP_TO="michael@dragons-at-work.de" +EOF + echo -e "${GREEN}[OK] Created new .env file${NC}" + echo -e "${YELLOW}[EDIT] Please edit:${NC} nano $ENV_FILE" + exit 0 +fi + +echo -e "${GREEN}[OK] Found existing .env file${NC}" + +# Check if SMTP variables already exist +smtp_username_exists=$(grep -c "^SMTP_USERNAME=" "$ENV_FILE" 2>/dev/null || echo "0") +smtp_password_exists=$(grep -c "^SMTP_PASSWORD=" "$ENV_FILE" 2>/dev/null || echo "0") + +if [ "$smtp_username_exists" -gt 0 ] && [ "$smtp_password_exists" -gt 0 ]; then + echo -e "${GREEN}[OK] SMTP variables already configured${NC}" + + # Load and show current values + source "$ENV_FILE" + echo -e "${YELLOW}Current SMTP User:${NC} ${SMTP_USERNAME:-NOT_SET}" + echo -e "${YELLOW}Current SMTP Password:${NC} ${SMTP_PASSWORD:+[CONFIGURED]}${SMTP_PASSWORD:-NOT_SET}" + + echo "" + echo -e "${YELLOW}To update SMTP settings:${NC} nano $ENV_FILE" + exit 0 +fi + +# Add missing SMTP variables +echo -e "${YELLOW}Adding SMTP configuration to existing .env...${NC}" + +# Add section header if not present +if ! grep -q "SMTP_" "$ENV_FILE" 2>/dev/null; then + echo "" >> "$ENV_FILE" + echo "# Furt SMTP Configuration for mail.dragons-at-work.de" >> "$ENV_FILE" +fi + +# Add username if missing +if [ "$smtp_username_exists" -eq 0 ]; then + echo "SMTP_HOST=\"mail.dragons-at-work.de\"" >> "$ENV_FILE" + echo "SMTP_PORT=\"465\"" >> "$ENV_FILE" + echo "SMTP_USERNAME=\"your_email@dragons-at-work.de\"" >> "$ENV_FILE" + echo -e "${GREEN}[OK] Added SMTP_HOST, SMTP_PORT, SMTP_USERNAME${NC}" +fi + +# Add password if missing +if [ "$smtp_password_exists" -eq 0 ]; then + echo "SMTP_PASSWORD=\"your_smtp_password\"" >> "$ENV_FILE" + echo "SMTP_FROM=\"noreply@dragons-at-work.de\"" >> "$ENV_FILE" + echo "SMTP_TO=\"michael@dragons-at-work.de\"" >> "$ENV_FILE" + echo -e "${GREEN}[OK] Added SMTP_PASSWORD, SMTP_FROM, SMTP_TO${NC}" +fi + +echo -e "${GREEN}[OK] SMTP configuration added to .env${NC}" +echo "" +echo -e "${YELLOW}Next steps:${NC}" +echo "1. Edit SMTP credentials: nano $ENV_FILE" +echo "2. Set your actual email@dragons-at-work.de in SMTP_USERNAME" +echo "3. Set your actual SMTP password in SMTP_PASSWORD" +echo "4. Test with: ./scripts/start.sh" + +echo "" +echo -e "${YELLOW}Current .env content:${NC}" +echo "===================" +cat "$ENV_FILE" +echo "===================" +echo "" +echo -e "${GREEN}Ready for SMTP testing!${NC}" + diff --git a/scripts/start.sh b/scripts/start.sh index 1aadf21..4ad5591 100755 --- a/scripts/start.sh +++ b/scripts/start.sh @@ -17,12 +17,10 @@ echo -e "${GREEN}=== Furt Lua HTTP-Server Startup ===${NC}" LUA_COMMAND="" # Config check first -if [ "$(uname)" = "OpenBSD" ]; then +if [ "$(uname)" = "OpenBSD" ] || [ "$(uname)" = "FreeBSD" ]; then CONFIG_FILE="/usr/local/etc/furt/furt.conf" - PID_FILE="/var/run/furt/furt.pid" else CONFIG_FILE="/etc/furt/furt.conf" - PID_FILE="/var/run/furt/furt.pid" fi if [ ! -f "$CONFIG_FILE" ] && [ ! -f "$PROJECT_DIR/config/furt.conf" ]; then @@ -89,38 +87,12 @@ cd "$PROJECT_DIR" echo -e "${GREEN}Starting Furt...${NC}" -# PID-File cleanup function -cleanup_pid() { - if [ -f "$PID_FILE" ]; then - rm -f "$PID_FILE" - fi -} - # Service vs Interactive Detection if [ ! -t 0 ] || [ ! -t 1 ]; then - # Service mode - Background + PID-File - echo -e "${GREEN}Service mode: Background + PID-File${NC}" - - # Start process in background + # Service mode - Background "$LUA_COMMAND" src/main.lua & - PID=$! - - # Write PID-File - echo "$PID" > "$PID_FILE" - echo -e "${GREEN}Furt started (PID: $PID, PID-File: $PID_FILE)${NC}" - - # Verify process is still running after short delay - sleep 1 - if ! kill -0 "$PID" 2>/dev/null; then - echo -e "${RED}Error: Process died immediately${NC}" - cleanup_pid - exit 1 - fi - - echo -e "${GREEN}Service startup successful${NC}" else - # Interactive mode - Foreground (no PID-File) - echo -e "${GREEN}Interactive mode: Foreground${NC}" + # Interactive mode - Foreground exec "$LUA_COMMAND" src/main.lua fi diff --git a/scripts/stress_test.sh b/scripts/stress_test.sh index 05c47ff..56be1bb 100755 --- a/scripts/stress_test.sh +++ b/scripts/stress_test.sh @@ -4,7 +4,7 @@ BASE_URL="http://127.0.0.1:8080" # Use correct API keys that match current .env -API_KEY="YOUR_API_KEY_HERE" +API_KEY="hugo-dev-key-change-in-production" echo "⚡ Furt API Stress Test" echo "======================" @@ -20,9 +20,9 @@ for i in {1..20}; do response=$(curl -s -w "%{http_code}" \ -H "X-API-Key: $API_KEY" \ "$BASE_URL/v1/auth/status") - + status=$(echo "$response" | tail -c 4) - + if [ "$status" == "200" ]; then rate_limit_remaining=$(echo "$response" | head -n -1 | jq -r '.rate_limit_remaining // "N/A"' 2>/dev/null) echo "Request $i: ✅ 200 OK (Rate limit remaining: $rate_limit_remaining)" @@ -33,7 +33,7 @@ for i in {1..20}; do else echo "Request $i: ❌ $status Error" fi - + # Small delay to prevent overwhelming sleep 0.1 done @@ -58,10 +58,10 @@ for i in {1..10}; do -H "X-API-Key: $API_KEY" \ "$BASE_URL/health") local_end=$(date +%s.%N) - + status=$(echo "$response" | tail -c 4) duration=$(echo "$local_end - $local_start" | bc -l) - + echo "Concurrent $i: Status $status, Duration ${duration}s" > "$temp_dir/result_$i" } & done @@ -85,18 +85,18 @@ mail_errors=0 for i in {1..5}; do start_time=$(date +%s.%N) - + response=$(curl -s -w "%{http_code}" \ -H "X-API-Key: $API_KEY" \ -H "Content-Type: application/json" \ -d "{\"name\":\"Stress Test $i\",\"email\":\"test$i@example.com\",\"subject\":\"Performance Test\",\"message\":\"Load test message $i\"}" \ "$BASE_URL/v1/mail/send") - + end_time=$(date +%s.%N) duration=$(echo "$end_time - $start_time" | bc -l) - + status=$(echo "$response" | tail -c 4) - + if [ "$status" == "200" ]; then echo "Mail $i: ✅ 200 OK (${duration}s)" ((mail_success++)) @@ -104,7 +104,7 @@ for i in {1..5}; do echo "Mail $i: ❌ Status $status (${duration}s)" ((mail_errors++)) fi - + # Delay between mail sends to be nice to SMTP server sleep 1 done @@ -120,7 +120,7 @@ mixed_success=0 for i in {1..15}; do ((mixed_total++)) - + if [ $((i % 3)) -eq 0 ]; then # Every 3rd request: auth status endpoint="/v1/auth/status" @@ -128,20 +128,20 @@ for i in {1..15}; do # Other requests: health check endpoint="/health" fi - + response=$(curl -s -w "%{http_code}" \ -H "X-API-Key: $API_KEY" \ "$BASE_URL$endpoint") - + status=$(echo "$response" | tail -c 4) - + if [ "$status" == "200" ]; then echo "Mixed $i ($endpoint): ✅ 200 OK" ((mixed_success++)) else echo "Mixed $i ($endpoint): ❌ $status" fi - + sleep 0.2 done diff --git a/scripts/sync-files.sh b/scripts/sync-files.sh index 34d3957..b495a78 100755 --- a/scripts/sync-files.sh +++ b/scripts/sync-files.sh @@ -24,13 +24,8 @@ cp -r integrations/ "$TARGET/" [ -f "VERSION" ] && cp VERSION "$TARGET/" [ -f ".version_history" ] && cp .version_history "$TARGET/" -# Set proper permissions based on operating system -if [ "$(uname)" = "OpenBSD" ]; then - chown -R root:wheel "$TARGET" -else - chown -R root:root "$TARGET" -fi - +# Set proper permissions +chown -R root:wheel "$TARGET" 2>/dev/null || chown -R root:root "$TARGET" chmod -R 644 "$TARGET" find "$TARGET" -type d -exec chmod 755 {} \; chmod +x "$TARGET/scripts/start.sh" diff --git a/scripts/test_auth.sh b/scripts/test_auth.sh index 007179c..fb892a1 100755 --- a/scripts/test_auth.sh +++ b/scripts/test_auth.sh @@ -3,8 +3,8 @@ # Test API-Key-Authentifizierung (ohne jq parse errors) BASE_URL="http://127.0.0.1:8080" -HUGO_API_KEY="YOUR_API_KEY_HERE" -ADMIN_API_KEY="YOUR_ADMIN_KEY_HERE" +HUGO_API_KEY="hugo-dev-key-change-in-production" +ADMIN_API_KEY="admin-dev-key-change-in-production" INVALID_API_KEY="invalid-key-should-fail" echo "🔐 Testing Furt API-Key Authentication" @@ -16,24 +16,24 @@ make_request() { local url="$2" local headers="$3" local data="$4" - + echo "Request: $method $url" if [ -n "$headers" ]; then echo "Headers: $headers" fi - + local response=$(curl -s $method \ ${headers:+-H "$headers"} \ ${data:+-d "$data"} \ -H "Content-Type: application/json" \ "$url") - + local status=$(curl -s -o /dev/null -w "%{http_code}" $method \ ${headers:+-H "$headers"} \ ${data:+-d "$data"} \ -H "Content-Type: application/json" \ "$url") - + echo "Status: $status" echo "Response: $response" | jq '.' 2>/dev/null || echo "$response" echo "" diff --git a/scripts/test_modular.sh b/scripts/test_modular.sh old mode 100755 new mode 100644 index 85149fe..398aef6 --- a/scripts/test_modular.sh +++ b/scripts/test_modular.sh @@ -3,7 +3,7 @@ # Test der modularen Furt-Architektur BASE_URL="http://127.0.0.1:8080" -HUGO_API_KEY="YOUR_API_KEY_HERE" +HUGO_API_KEY="hugo-dev-key-change-in-production" echo "🧩 Testing Modular Furt Architecture" echo "====================================" diff --git a/scripts/test_smtp.sh b/scripts/test_smtp.sh old mode 100755 new mode 100644 index 205c0f1..c014a52 --- a/scripts/test_smtp.sh +++ b/scripts/test_smtp.sh @@ -36,7 +36,7 @@ else echo "[ERROR] Validation failed" fi -# Test 3: Invalid Email Format +# Test 3: Invalid Email Format echo "" echo "[3] Testing email validation..." email_validation_response=$(curl -s -X POST "$SERVER_URL/v1/mail/send" \ @@ -54,36 +54,36 @@ fi # Test 4: Valid Mail Request (REAL SMTP TEST) echo "" echo "[4] Testing REAL mail sending..." -echo "WARNING: This will send a real email to admin@example.com" +echo "WARNING: This will send a real email to michael@dragons-at-work.de" read -p "Continue with real mail test? (y/N): " -n 1 -r echo if [[ $REPLY =~ ^[Yy]$ ]]; then echo "Sending real test email..." - + mail_response=$(curl -s -X POST "$SERVER_URL/v1/mail/send" \ -H "Content-Type: application/json" \ -d '{ "name": "Furt Test User", - "email": "test@example.com", + "email": "test@dragons-at-work.de", "subject": "Furt SMTP Test - Week 2 Success!", "message": "This is a test email from the Furt Lua HTTP-Server.\n\nSMTP Integration is working!\n\nTimestamp: '$(date)'\nServer: furt-lua v1.0" }') - + echo "Response: $mail_response" - + # Check for success if echo "$mail_response" | grep -q '"success":true'; then echo "[OK] MAIL SENT SUCCESSFULLY!" - echo "Check admin@example.com inbox" - + echo "Check michael@dragons-at-work.de inbox" + # Extract request ID request_id=$(echo "$mail_response" | grep -o '"request_id":"[^"]*"' | cut -d'"' -f4) echo "Request ID: $request_id" else echo "[ERROR] Mail sending failed" echo "Check server logs and SMTP credentials" - + # Show error details if echo "$mail_response" | grep -q "error"; then error_msg=$(echo "$mail_response" | grep -o '"error":"[^"]*"' | cut -d'"' -f4) @@ -126,7 +126,7 @@ echo "Performance: ${duration_ms}ms" echo "" echo "Week 2 Challenge Status:" echo " SMTP Integration: COMPLETE" -echo " Environment Variables: CHECK .env" +echo " Environment Variables: CHECK .env" echo " Native Lua Implementation: DONE" echo " Production Ready: READY FOR TESTING" diff --git a/scripts/validate-config.sh b/scripts/validate-config.sh index 220cf69..7b59dc7 100755 --- a/scripts/validate-config.sh +++ b/scripts/validate-config.sh @@ -4,7 +4,7 @@ set -e # Detect config file location -if [ "$(uname)" = "OpenBSD" ]; then +if [ "$(uname)" = "OpenBSD" ] || [ "$(uname)" = "FreeBSD" ]; then CONFIG_FILE="/usr/local/etc/furt/furt.conf" else CONFIG_FILE="/etc/furt/furt.conf" @@ -24,13 +24,12 @@ if ! grep -q '^\[server\]' "$CONFIG_FILE"; then exit 1 fi -# Fix: Use POSIX-compatible regex patterns -if ! grep -q '^[ \t]*port[ \t]*=' "$CONFIG_FILE"; then +if ! grep -q '^port\s*=' "$CONFIG_FILE"; then echo "Error: server port not configured" exit 1 fi -if ! grep -q '^[ \t]*host[ \t]*=' "$CONFIG_FILE"; then +if ! grep -q '^host\s*=' "$CONFIG_FILE"; then echo "Error: server host not configured" exit 1 fi diff --git a/src/config_parser.lua b/src/config_parser.lua index 8760014..6fa36d5 100644 --- a/src/config_parser.lua +++ b/src/config_parser.lua @@ -215,7 +215,7 @@ end function ConfigParser.load_config() -- Try different locations based on OS local config_paths = { - "/usr/local/etc/furt/furt.conf", -- OpenBSD + "/usr/local/etc/furt/furt.conf", -- OpenBSD/FreeBSD "/etc/furt/furt.conf", -- Linux "config/furt.conf", -- Development "furt.conf" -- Current directory diff --git a/src/http_server.lua b/src/http_server.lua index c9b85b2..72db9b5 100644 --- a/src/http_server.lua +++ b/src/http_server.lua @@ -1,5 +1,5 @@ -- src/http_server.lua --- HTTP Server Core for Furt API-Gateway +-- HTTP Server Core for Furt API-Gateway with Structured Logging -- Dragons@Work Digital Sovereignty Project local socket = require("socket") @@ -10,6 +10,7 @@ end local config = require("config.server") local Auth = require("src.auth") +local Logger = require("src.logger") -- HTTP-Server Module local FurtServer = {} @@ -32,11 +33,22 @@ function FurtServer:add_route(method, path, handler) self.routes[method] = {} end self.routes[method][path] = handler + + Logger.debug("Route registered", { + method = method, + path = path + }) end -- Add protected route (requires authentication) function FurtServer:add_protected_route(method, path, required_permission, handler) self:add_route(method, path, Auth.create_protected_route(required_permission, handler)) + + Logger.debug("Protected route registered", { + method = method, + path = path, + permission = required_permission + }) end -- Parse HTTP request @@ -49,6 +61,7 @@ function FurtServer:parse_request(client) -- Parse request line: "POST /v1/mail/send HTTP/1.1" local method, path, protocol = request_line:match("(%w+) (%S+) (%S+)") if not method then + Logger.warn("Invalid request line", { request_line = request_line }) return nil end @@ -91,7 +104,9 @@ end function FurtServer:add_cors_headers(request) local allowed_origins = config.cors and config.cors.allowed_origins or { "http://localhost:1313", - "http://127.0.0.1:1313" + "http://127.0.0.1:1313", + "https://dragons-at-work.de", + "https://www.dragons-at-work.de" } -- Check if request has Origin header @@ -172,22 +187,62 @@ function FurtServer:get_status_text(status) return status_texts[status] or "Unknown" end +-- Get client IP address with X-Forwarded-For support +function FurtServer:get_client_ip(client, headers) + -- Check for X-Forwarded-For header (proxy support) + local forwarded_for = headers["x-forwarded-for"] + if forwarded_for then + -- Take first IP in case of multiple proxies + local first_ip = forwarded_for:match("([^,]+)") + if first_ip then + return first_ip:match("^%s*(.-)%s*$") -- trim whitespace + end + end + + -- Check for X-Real-IP header + local real_ip = headers["x-real-ip"] + if real_ip then + return real_ip + end + + -- Fallback to direct connection + local peer_ip = client:getpeername() + return peer_ip or "unknown" +end + -- Handle client request function FurtServer:handle_client(client) + -- Generate request ID and start timing + local request_id = Logger.generate_request_id() + local start_time = socket.gettime() + + Logger.debug("Request started", { + request_id = request_id + }) + local request = self:parse_request(client) if not request then + local duration_ms = math.floor((socket.gettime() - start_time) * 1000) local response = self:create_response(400, {error = "Invalid request"}, nil, nil, nil) client:send(response) + + Logger.log_request("INVALID", "unknown", 400, duration_ms, "unknown", request_id) return end - print(string.format("[%s] %s %s", os.date("%Y-%m-%d %H:%M:%S"), - request.method, request.path)) + -- Get client IP + local client_ip = self:get_client_ip(client, request.headers) + + -- Add request_id to request context for handlers + request.request_id = request_id -- Handle OPTIONS preflight requests (CORS) if request.method == "OPTIONS" then + local duration_ms = math.floor((socket.gettime() - start_time) * 1000) local response = self:create_response(204, "", "text/plain", nil, request) client:send(response) + + Logger.log_request("OPTIONS", request.path, 204, duration_ms, client_ip, request_id) return end @@ -197,49 +252,78 @@ function FurtServer:handle_client(client) handler = self.routes[request.method][request.path] end + local status = 404 if handler then local success, result = pcall(handler, request, self) if success then client:send(result) + -- Extract status from response (rough parsing) + status = tonumber(result:match("HTTP/1%.1 (%d+)")) or 200 else - print("Handler error: " .. tostring(result)) - local error_response = self:create_response(500, {error = "Internal server error"}, nil, nil, request) + Logger.log_error("Handler error", { + request_id = request_id, + method = request.method, + path = request.path, + client_ip = client_ip + }, tostring(result)) + + status = 500 + local error_response = self:create_response(500, { + error = "Internal server error", + request_id = request_id + }, nil, nil, request) client:send(error_response) end else - print("Route not found: " .. request.method .. " " .. request.path) - local response = self:create_response(404, {error = "Route not found", method = request.method, path = request.path}, nil, nil, request) + Logger.debug("Route not found", { + request_id = request_id, + method = request.method, + path = request.path + }) + + local response = self:create_response(404, { + error = "Route not found", + method = request.method, + path = request.path, + request_id = request_id + }, nil, nil, request) client:send(response) end + + -- Log completed request with performance metrics + local duration_ms = math.floor((socket.gettime() - start_time) * 1000) + Logger.log_request(request.method, request.path, status, duration_ms, client_ip, request_id) end -- Start HTTP server function FurtServer:start() self.server = socket.bind(self.host, self.port) if not self.server then + Logger.error("Failed to bind server", { + host = self.host, + port = self.port + }) error("Failed to bind to " .. self.host .. ":" .. self.port) end local HealthRoute = require("src.routes.health") local version_info = HealthRoute.get_version_info() - print(string.format("Furt HTTP-Server started on %s:%d", self.host, self.port)) - print("Version: " .. version_info.version .. " (merkwerk)") - print("Content-Hash: " .. (version_info.content_hash or "unknown")) - print("VCS: " .. (version_info.vcs_info and version_info.vcs_info.hash or "none")) - print("API-Key authentication: ENABLED") + -- Structured startup logging + Logger.log_startup(self.host, self.port, version_info) - -- Show actual configured rate limits + -- Log configuration details local rate_limits = config.security and config.security.rate_limits - if rate_limits then - print(string.format("Rate limiting: ENABLED (%d req/hour per API key, %d req/hour per IP)", - rate_limits.api_key_max, rate_limits.ip_max)) - else - print("Rate limiting: ENABLED (default values)") - end + Logger.log_config_summary({ + cors_origins_count = #config.cors.allowed_origins, + rate_limiting_enabled = rate_limits ~= nil, + api_key_max = rate_limits and rate_limits.api_key_max, + ip_max = rate_limits and rate_limits.ip_max, + test_endpoint_enabled = config.security and config.security.enable_test_endpoint, + log_level = Logger.get_log_level() + }) - print("CORS enabled for " .. (#config.cors.allowed_origins) .. " configured origins") - print("Press Ctrl+C to stop") + Logger.info("Furt server ready - Press Ctrl+C to stop") while true do local client = self.server:accept() diff --git a/src/logger.lua b/src/logger.lua new file mode 100644 index 0000000..4e36fa7 --- /dev/null +++ b/src/logger.lua @@ -0,0 +1,200 @@ +-- src/logger.lua +-- Structured JSON Logger for Furt API-Gateway +-- Dragons@Work Digital Sovereignty Project + +local found_cjson, cjson = pcall(require, 'cjson') +if not found_cjson then + cjson = require('dkjson') +end + +local config = require("config.server") + +-- Hash-based request ID generator for collision resistance +local function generate_request_id() + local data = string.format("%d-%d-%d-%d", + os.time(), + math.random(1000000, 9999999), + math.random(1000000, 9999999), + os.clock() * 1000000) + + -- Simple hash function (Lua-native, no dependencies) + local hash = 0 + for i = 1, #data do + hash = (hash * 31 + string.byte(data, i)) % 2147483647 + end + + return string.format("req-%x", hash) +end + +-- Export request ID generator +Logger = {} +Logger.generate_request_id = generate_request_id + +local Logger = {} + +-- Log levels with numeric values for filtering +local LOG_LEVELS = { + debug = 1, + info = 2, + warn = 3, + error = 4 +} + +-- Current log level from config +local current_log_level = LOG_LEVELS[config.log_level] or LOG_LEVELS.info + +-- Service identification +local SERVICE_NAME = "furt-lua" + +-- Generate timestamp in ISO format +local function get_timestamp() + return os.date("!%Y-%m-%dT%H:%M:%SZ") +end + +-- Core logging function +local function log_structured(level, message, context) + -- Skip if log level is below threshold + if LOG_LEVELS[level] < current_log_level then + return + end + + -- Build log entry + local log_entry = { + timestamp = get_timestamp(), + level = level, + service = SERVICE_NAME, + message = message + } + + -- Add context data if provided + if context then + for key, value in pairs(context) do + log_entry[key] = value + end + end + + -- Output as JSON + local json_output = cjson.encode(log_entry) + print(json_output) +end + +-- Public logging functions +function Logger.debug(message, context) + log_structured("debug", message, context) +end + +function Logger.info(message, context) + log_structured("info", message, context) +end + +function Logger.warn(message, context) + log_structured("warn", message, context) +end + +function Logger.error(message, context) + log_structured("error", message, context) +end + +-- Request logging with performance metrics +function Logger.log_request(method, path, status, duration_ms, client_ip, request_id) + if not config.log_requests then + return + end + + local context = { + method = method, + path = path, + status = status, + duration_ms = duration_ms, + client_ip = client_ip + } + + -- Add request_id if provided + if request_id then + context.request_id = request_id + end + + log_structured("info", "HTTP request", context) +end + +-- Service startup logging +function Logger.log_startup(host, port, version_info) + Logger.info("Furt HTTP-Server starting", { + host = host, + port = port, + version = version_info.version, + content_hash = version_info.content_hash, + vcs_hash = version_info.vcs_info and version_info.vcs_info.hash + }) +end + +-- Service health logging +function Logger.log_health_check(status, details) + local level = status == "healthy" and "info" or "warn" + log_structured(level, "Health check", { + health_status = status, + details = details + }) +end + +-- Error logging with stack trace support +function Logger.log_error(error_message, context, stack_trace) + local error_context = context or {} + if stack_trace then + error_context.stack_trace = stack_trace + end + log_structured("error", error_message, error_context) +end + +-- Configuration logging +function Logger.log_config_summary(summary) + Logger.info("Configuration loaded", summary) +end + +-- Rate limiting events +function Logger.log_rate_limit(api_key, client_ip, limit_type) + Logger.warn("Rate limit exceeded", { + api_key = api_key and "***masked***" or nil, + client_ip = client_ip, + limit_type = limit_type + }) +end + +-- SMTP/Mail logging +function Logger.log_mail_event(event_type, recipient, success, error_message) + local level = success and "info" or "error" + log_structured(level, "Mail event", { + event_type = event_type, + recipient = recipient, + success = success, + error = error_message + }) +end + +-- Set log level dynamically (useful for debugging) +function Logger.set_log_level(level) + if LOG_LEVELS[level] then + current_log_level = LOG_LEVELS[level] + Logger.info("Log level changed", { new_level = level }) + else + Logger.error("Invalid log level", { attempted_level = level }) + end +end + +-- Get current log level +function Logger.get_log_level() + for level, value in pairs(LOG_LEVELS) do + if value == current_log_level then + return level + end + end + return "unknown" +end + +-- Check if a log level would be output +function Logger.would_log(level) + return LOG_LEVELS[level] and LOG_LEVELS[level] >= current_log_level +end + +return Logger + diff --git a/src/main.lua b/src/main.lua index 0949efe..614aac5 100644 --- a/src/main.lua +++ b/src/main.lua @@ -1,34 +1,137 @@ -- src/main.lua --- Furt API-Gateway - Application Entry Point +-- Furt API-Gateway - Application Entry Point with Structured Logging -- Dragons@Work Digital Sovereignty Project --- Load HTTP Server Core -local FurtServer = require("src.http_server") +-- Load Logger first for startup logging +local Logger = require("src.logger") --- Load Route Modules -local MailRoute = require("src.routes.mail") -local AuthRoute = require("src.routes.auth") -local HealthRoute = require("src.routes.health") +Logger.info("Furt API-Gateway starting up", { + startup_phase = "module_loading" +}) --- Load configuration -local config = require("config.server") +-- Load modules with error handling +local function safe_require(module_name, description) + local success, module = pcall(require, module_name) + if not success then + Logger.error("Failed to load module", { + module = module_name, + description = description, + error = module + }) + os.exit(1) + end + + Logger.debug("Module loaded successfully", { + module = module_name, + description = description + }) + + return module +end + +-- Load core modules +local FurtServer = safe_require("src.http_server", "HTTP Server Core") +local MailRoute = safe_require("src.routes.mail", "Mail Route Handler") +local AuthRoute = safe_require("src.routes.auth", "Auth Route Handler") +local HealthRoute = safe_require("src.routes.health", "Health Route Handler") +local config = safe_require("config.server", "Server Configuration") + +Logger.info("All modules loaded successfully", { + startup_phase = "modules_ready" +}) -- Initialize server +Logger.info("Initializing HTTP server", { + startup_phase = "server_init" +}) + local server = FurtServer:new() +-- Route registration with logging +local routes_registered = 0 + -- Register public routes (no authentication required) server:add_route("GET", "/health", HealthRoute.handle_health) +routes_registered = routes_registered + 1 -- Test endpoint for development (configurable via furt.conf) if config.security and config.security.enable_test_endpoint then server:add_route("POST", "/test", HealthRoute.handle_test) - print("[WARN] Test endpoint enabled via configuration") + routes_registered = routes_registered + 1 + + Logger.warn("Development test endpoint enabled", { + endpoint = "POST /test", + security_note = "Should be disabled in production" + }) end -- Register protected routes (require authentication) server:add_protected_route("POST", "/v1/mail/send", "mail:send", MailRoute.handle_mail_send) server:add_protected_route("GET", "/v1/auth/status", nil, AuthRoute.handle_auth_status) +routes_registered = routes_registered + 2 --- Start server -server:start() +Logger.info("Route registration completed", { + startup_phase = "routes_registered", + total_routes = routes_registered, + public_routes = config.security and config.security.enable_test_endpoint and 2 or 1, + protected_routes = 2 +}) + +-- Pre-flight system checks +Logger.info("Performing pre-flight checks", { + startup_phase = "pre_flight_checks" +}) + +-- Check SMTP configuration +local smtp_configured = config.mail and config.mail.host ~= nil +Logger.info("SMTP configuration check", { + smtp_configured = smtp_configured, + default_smtp_host = config.mail and config.mail.host or "not configured" +}) + +-- Check API keys +local api_key_count = 0 +if config.api_keys then + for _ in pairs(config.api_keys) do + api_key_count = api_key_count + 1 + end +end + +Logger.info("API key configuration check", { + api_key_count = api_key_count, + keys_configured = api_key_count > 0 +}) + +if api_key_count == 0 then + Logger.warn("No API keys configured", { + warning = "Server will not accept authenticated requests", + check_config = "Verify furt.conf API key sections" + }) +end + +-- Start server with error handling +Logger.info("Starting HTTP server", { + startup_phase = "server_start", + host = config.host, + port = config.port +}) + +local success, error_msg = pcall(function() + server:start() +end) + +if not success then + Logger.error("Server startup failed", { + error = error_msg, + host = config.host, + port = config.port, + suggestion = "Check if port is already in use or permissions" + }) + os.exit(1) +end + +-- This should never be reached since server:start() blocks +Logger.error("Server stopped unexpectedly", { + unexpected_exit = true +}) diff --git a/src/smtp.lua b/src/smtp.lua index a3340a8..b419a75 100644 --- a/src/smtp.lua +++ b/src/smtp.lua @@ -1,6 +1,6 @@ --- src/smtp.lua +-- furt-lua/src/smtp.lua -- Universal SMTP implementation with SSL compatibility --- Supports both luaossl (Arch) and luasec (OpenBSD) +-- Supports both luaossl (Arch/karl) and luasec (OpenBSD/walter) -- Dragons@Work Digital Sovereignty Project local socket = require("socket") @@ -19,7 +19,7 @@ function SSLCompat:detect_ssl_library() return "luaossl", ssl_lib end end - + -- Try luasec local success, ssl_lib = pcall(require, "ssl") if success and ssl_lib then @@ -28,23 +28,23 @@ function SSLCompat:detect_ssl_library() return "luasec", ssl_lib end end - + return nil, "No compatible SSL library found (tried luaossl, luasec)" end function SSLCompat:wrap_socket(sock, options) local ssl_type, ssl_lib = self:detect_ssl_library() - + if not ssl_type then return nil, ssl_lib -- ssl_lib contains error message end - + if ssl_type == "luaossl" then return self:wrap_luaossl(sock, options, ssl_lib) elseif ssl_type == "luasec" then return self:wrap_luasec(sock, options, ssl_lib) end - + return nil, "Unknown SSL library type: " .. ssl_type end @@ -55,18 +55,18 @@ function SSLCompat:wrap_luaossl(sock, options, ssl_lib) protocol = "tlsv1_2", verify = "none" -- For self-signed certs }) - + if not ssl_sock then return nil, "luaossl wrap failed: " .. (err or "unknown error") end - + -- luaossl typically does handshake automatically, but explicit is safer local success, err = pcall(function() return ssl_sock:dohandshake() end) if not success then -- Some luaossl versions don't need explicit handshake -- Continue if dohandshake doesn't exist end - + return ssl_sock, nil end @@ -78,28 +78,28 @@ function SSLCompat:wrap_luasec(sock, options, ssl_lib) verify = "none", options = "all" }) - + if not ssl_sock then return nil, "luasec wrap failed: " .. (err or "unknown error") end - + -- luasec requires explicit handshake local success, err = ssl_sock:dohandshake() if not success then return nil, "luasec handshake failed: " .. (err or "unknown error") end - + return ssl_sock, nil end -- Create SMTP instance function SMTP:new(config) local instance = { - server = config.smtp_server, - port = config.smtp_port, + server = config.smtp_server or "mail.dragons-at-work.de", + port = config.smtp_port or 465, username = config.username, password = config.password, - from_address = config.from_address, + from_address = config.from_address or "noreply@dragons-at-work.de", use_ssl = config.use_ssl or true, debug = config.debug or false, ssl_compat = SSLCompat @@ -133,7 +133,7 @@ function SMTP:send_command(sock, command, expected_code) if self.debug then print("SMTP CMD: " .. (command or ""):gsub("\r\n", "\\r\\n")) end - + -- Only send if command is not nil (for server greeting, command is nil) if command then local success, err = sock:send(command .. "\r\n") @@ -141,16 +141,16 @@ function SMTP:send_command(sock, command, expected_code) return false, "Failed to send command: " .. (err or "unknown error") end end - + local response, err = sock:receive() if not response then return false, "Failed to receive response: " .. (err or "unknown error") end - + if self.debug then print("SMTP RSP: " .. response) end - + -- Handle multi-line responses (like EHLO) local full_response = response while response:match("^%d%d%d%-") do @@ -163,12 +163,12 @@ function SMTP:send_command(sock, command, expected_code) end full_response = full_response .. "\n" .. response end - + local code = response:match("^(%d+)") if expected_code and code ~= tostring(expected_code) then return false, "Unexpected response: " .. full_response end - + return true, full_response end @@ -179,38 +179,38 @@ function SMTP:connect() if not sock then return false, "Failed to create socket: " .. (err or "unknown error") end - + -- Set timeout sock:settimeout(30) - + -- Connect to server local success, err = sock:connect(self.server, self.port) if not success then return false, "Failed to connect to " .. self.server .. ":" .. self.port .. " - " .. (err or "unknown error") end - + -- Wrap with SSL for port 465 using compatibility layer if self.use_ssl and self.port == 465 then local ssl_sock, err = self.ssl_compat:wrap_socket(sock, { mode = "client", protocol = "tlsv1_2" }) - + if not ssl_sock then sock:close() return false, "Failed to establish SSL connection: " .. (err or "unknown error") end - + sock = ssl_sock end - + -- Read server greeting local success, response = self:send_command(sock, nil, 220) if not success then sock:close() return false, "SMTP server greeting failed: " .. response end - + return sock, nil end @@ -219,96 +219,64 @@ function SMTP:send_email(to_address, subject, message, from_name) if not self.username or not self.password then return false, "SMTP username or password not configured" end - + -- Connect to server local sock, err = self:connect() if not sock then return false, err end - + local function cleanup_and_fail(error_msg) sock:close() return false, error_msg end - + -- EHLO command local success, response = self:send_command(sock, "EHLO furt-lua", 250) if not success then return cleanup_and_fail("EHLO failed: " .. response) end - - -- STARTTLS hinzufügen für Port 587 - if self.port == 587 and self.use_ssl then - -- STARTTLS command - local success, response = self:send_command(sock, "STARTTLS", 220) - if not success then - return cleanup_and_fail("STARTTLS failed: " .. response) - end - - -- Upgrade connection to SSL - local ssl_sock, err = self.ssl_compat:wrap_socket(sock, { - mode = "client", - protocol = "tlsv1_2" - }) - - if not ssl_sock then - return cleanup_and_fail("SSL upgrade failed: " .. err) - end - - sock = ssl_sock - - -- EHLO again over encrypted connection - local success, response = self:send_command(sock, "EHLO furt-lua", 250) - if not success then - return cleanup_and_fail("EHLO after STARTTLS failed: " .. response) - end - end - + -- AUTH LOGIN local success, response = self:send_command(sock, "AUTH LOGIN", 334) if not success then return cleanup_and_fail("AUTH LOGIN failed: " .. response) end - + -- Send username (base64 encoded) local username_b64 = self:base64_encode(self.username) local success, response = self:send_command(sock, username_b64, 334) if not success then return cleanup_and_fail("Username authentication failed: " .. response) end - + -- Send password (base64 encoded) local password_b64 = self:base64_encode(self.password) local success, response = self:send_command(sock, password_b64, 235) if not success then return cleanup_and_fail("Password authentication failed: " .. response) end - + -- MAIL FROM local mail_from = "MAIL FROM:<" .. self.from_address .. ">" local success, response = self:send_command(sock, mail_from, 250) if not success then return cleanup_and_fail("MAIL FROM failed: " .. response) end - + -- RCPT TO local rcpt_to = "RCPT TO:<" .. to_address .. ">" local success, response = self:send_command(sock, rcpt_to, 250) if not success then return cleanup_and_fail("RCPT TO failed: " .. response) end - + -- DATA command local success, response = self:send_command(sock, "DATA", 354) if not success then return cleanup_and_fail("DATA command failed: " .. response) end - - -- Generate unique Message-ID - -- Extract domain from configured from_address - local hostname = self.from_address:match("@(.+)") or self.server - local message_id = string.format("<%d.%d@%s>", os.time(), math.random(10000), hostname) - + -- Build email message local display_name = from_name or "Furt Contact Form" local email_content = string.format( @@ -316,10 +284,7 @@ function SMTP:send_email(to_address, subject, message, from_name) "To: <%s>\r\n" .. "Subject: %s\r\n" .. "Date: %s\r\n" .. - "Message-ID: %s\r\n" .. - "MIME-Version: 1.0\r\n" .. "Content-Type: text/plain; charset=UTF-8\r\n" .. - "Content-Transfer-Encoding: 8bit\r\n" .. "\r\n" .. "%s\r\n" .. ".", @@ -328,20 +293,19 @@ function SMTP:send_email(to_address, subject, message, from_name) to_address, subject, os.date("%a, %d %b %Y %H:%M:%S %z"), - message_id, message ) - + -- Send email content local success, response = self:send_command(sock, email_content, 250) if not success then return cleanup_and_fail("Email sending failed: " .. response) end - + -- QUIT self:send_command(sock, "QUIT", 221) sock:close() - + return true, "Email sent successfully" end