diff --git a/.gitignore b/.gitignore index 8dec80f..c67bf5b 100644 --- a/.gitignore +++ b/.gitignore @@ -68,3 +68,4 @@ config.production.lua config/furt.conf +scripts/production_test_sequence.sh diff --git a/.version_history b/.version_history index 900c60b..26b078b 100644 --- a/.version_history +++ b/.version_history @@ -20,4 +20,14 @@ 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 -57ce9c01,8fecb01,feature/structured-logging-health-monitoring,2025-09-05T18:31:36Z,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 diff --git a/README.md b/README.md index d27042b..a18d120 100644 --- a/README.md +++ b/README.md @@ -1,160 +1,83 @@ # Furt API Gateway -**HTTP-Server in Lua für Service-Integration** +**Pure Lua HTTP-Server für digitale Souveränität** ## Überblick -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. +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. ## Features - HTTP-Server mit JSON-APIs -- Mail-Versendung über SMTP -- Request-Routing und Authentication +- Multi-Tenant Mail-Routing über SMTP +- API-Key-basierte Authentifizierung - Health-Check-Endpoints -- Konfigurierbare Rate-Limiting -- Hugo/Website-Integration +- Rate-Limiting pro API-Key +- CORS-Support für Frontend-Integration -## Dependencies +## Quick Start -**Erforderlich:** -- `lua` 5.4+ -- `lua-socket` (HTTP-Server) -- `lua-cjson` (JSON-Verarbeitung) +**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 +``` **Installation:** ```bash -# Arch Linux -pacman -S lua lua-socket lua-cjson - -# Ubuntu/Debian -apt install lua5.4 lua-socket lua-cjson -``` - -## Installation - -```bash -# Repository klonen -git clone +git clone https://smida.dragons-at-work.de/DAW/furt.git cd furt - -# Scripts ausführbar machen -chmod +x scripts/*.sh - -# Server starten -./scripts/start.sh +sudo ./install.sh ``` -**Server läuft auf:** http://127.0.0.1:8080 +**Server läuft auf:** http://127.0.0.1:7811 ## API-Endpoints -### Health Check +**Health Check:** ```bash -GET /health -→ {"status":"healthy","service":"furt","version":"1.0.0"} +curl http://127.0.0.1:7811/health ``` -### Mail senden +**Mail senden:** ```bash -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 \ +curl -X POST http://127.0.0.1:7811/v1/mail/send \ + -H "X-API-Key: your-api-key" \ -H "Content-Type: application/json" \ - -d '{"name":"Test","email":"test@example.com","message":"Test"}' + -d '{"name":"Test","email":"test@example.com","subject":"Test","message":"Test-Nachricht"}' ``` -## Deployment +## Dokumentation -**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 -``` +**Installation & Konfiguration:** [Furt Wiki](https://smida.dragons-at-work.de/DAW/furt/wiki) ## Projektstruktur ``` furt/ ├── src/ # Lua-Source-Code -│ ├── main.lua # HTTP-Server -│ ├── routes/ # API-Endpoints -│ └── smtp.lua # Mail-Integration -├── config/ # Konfiguration -├── scripts/ # Start/Test-Scripts -├── tests/ # Test-Suite -└── deployment/ # System-Integration +├── config/ # Konfiguration +├── scripts/ # Installation & Management +└── deployment/ # System-Integration ``` -## Hugo-Integration +## Integration -**Shortcode-Beispiel:** -```html -
- - - - -
-``` +**merkwerk:** Versionierte Furt-Deployment über [merkwerk](https://smida.dragons-at-work.de/DAW/merkwerk) -## Development +## License -**Code-Struktur:** -- Module unter 200 Zeilen -- Funktionen unter 50 Zeilen -- Klare Fehlerbehandlung -- Testbare Komponenten +ISC - Siehe [LICENSE](LICENSE) für Details. -**Dependencies minimal halten:** -- Nur lua-socket und lua-cjson -- Keine externen HTTP-Libraries -- Standard-Lua-Funktionen bevorzugen +## 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) diff --git a/VERSION b/VERSION index 6da28dd..845639e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.1.1 \ No newline at end of file +0.1.4 diff --git a/deployment/linux/furt.service b/deployment/linux/furt.service index f09104b..5dd1150 100644 --- a/deployment/linux/furt.service +++ b/deployment/linux/furt.service @@ -1,18 +1,33 @@ [Unit] -Description=furt Multi-Tenant API Gateway +Description=furt Multi-Tenant API Gateway (Security-Hardened) After=network.target [Service] Type=forking User=furt Group=furt -ExecStart=/usr/local/share/furt/scripts/start.sh start +ExecStart=/usr/local/share/furt/scripts/start.sh +PIDFile=/var/run/furt/furt.pid 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 465af19..bcdb4b9 100644 --- a/deployment/openbsd/rc.d-furt +++ b/deployment/openbsd/rc.d-furt @@ -3,11 +3,52 @@ 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 -pexp="lua.*src/main.lua" +# 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 +} rc_cmd $1 diff --git a/docs/setup-guide.md b/docs/setup-guide.md deleted file mode 100644 index 2dc790e..0000000 --- a/docs/setup-guide.md +++ /dev/null @@ -1,176 +0,0 @@ -# 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 9de2751..4711c2b 100755 --- a/install.sh +++ b/install.sh @@ -102,7 +102,7 @@ else echo "" echo "Next steps:" echo "1. Edit configuration file:" - if [ "$(uname)" = "OpenBSD" ] || [ "$(uname)" = "FreeBSD" ]; then + if [ "$(uname)" = "OpenBSD" ]; 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 100644 new mode 100755 diff --git a/scripts/create-service.sh b/scripts/create-service.sh index 9732479..eed3ebe 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 100644 new mode 100755 index 3f8002f..6a1497c --- 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:8080/v1/mail/send \ +curl -X POST http://127.0.0.1:7811/v1/mail/send \ -H "Content-Type: application/json" \ -d '{ "name": "Furt Test User", - "email": "michael@dragons-at-work.de", + "email": "admin@example.com", "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 deleted file mode 100644 index 36a6455..0000000 --- a/scripts/production_test_sequence.sh +++ /dev/null @@ -1,80 +0,0 @@ -#!/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 2fdbad6..63881c3 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" ] || [ "$(uname)" = "FreeBSD" ]; then +if [ "$(uname)" = "OpenBSD" ]; then CONFIG_DIR="/usr/local/etc/furt" USER="_furt" GROUP="_furt" @@ -18,12 +18,15 @@ 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 29cdb61..9188626 100755 --- a/scripts/setup-user.sh +++ b/scripts/setup-user.sh @@ -4,7 +4,7 @@ set -e # Detect operating system -if [ "$(uname)" = "OpenBSD" ] || [ "$(uname)" = "FreeBSD" ]; then +if [ "$(uname)" = "OpenBSD" ]; 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 deleted file mode 100755 index 858436e..0000000 --- a/scripts/setup_env.sh +++ /dev/null @@ -1,101 +0,0 @@ -#!/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 4ad5591..1aadf21 100755 --- a/scripts/start.sh +++ b/scripts/start.sh @@ -17,10 +17,12 @@ echo -e "${GREEN}=== Furt Lua HTTP-Server Startup ===${NC}" LUA_COMMAND="" # Config check first -if [ "$(uname)" = "OpenBSD" ] || [ "$(uname)" = "FreeBSD" ]; then +if [ "$(uname)" = "OpenBSD" ]; 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 @@ -87,12 +89,38 @@ 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 + # Service mode - Background + PID-File + echo -e "${GREEN}Service mode: Background + PID-File${NC}" + + # Start process in 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 + # Interactive mode - Foreground (no PID-File) + echo -e "${GREEN}Interactive mode: Foreground${NC}" exec "$LUA_COMMAND" src/main.lua fi diff --git a/scripts/stress_test.sh b/scripts/stress_test.sh index 56be1bb..05c47ff 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="hugo-dev-key-change-in-production" +API_KEY="YOUR_API_KEY_HERE" 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 b495a78..34d3957 100755 --- a/scripts/sync-files.sh +++ b/scripts/sync-files.sh @@ -24,8 +24,13 @@ cp -r integrations/ "$TARGET/" [ -f "VERSION" ] && cp VERSION "$TARGET/" [ -f ".version_history" ] && cp .version_history "$TARGET/" -# Set proper permissions -chown -R root:wheel "$TARGET" 2>/dev/null || chown -R root:root "$TARGET" +# Set proper permissions based on operating system +if [ "$(uname)" = "OpenBSD" ]; then + chown -R root:wheel "$TARGET" +else + chown -R root:root "$TARGET" +fi + 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 fb892a1..007179c 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="hugo-dev-key-change-in-production" -ADMIN_API_KEY="admin-dev-key-change-in-production" +HUGO_API_KEY="YOUR_API_KEY_HERE" +ADMIN_API_KEY="YOUR_ADMIN_KEY_HERE" 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 100644 new mode 100755 index 398aef6..85149fe --- 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="hugo-dev-key-change-in-production" +HUGO_API_KEY="YOUR_API_KEY_HERE" echo "🧩 Testing Modular Furt Architecture" echo "====================================" diff --git a/scripts/test_smtp.sh b/scripts/test_smtp.sh old mode 100644 new mode 100755 index c014a52..205c0f1 --- 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 michael@dragons-at-work.de" +echo "WARNING: This will send a real email to admin@example.com" 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@dragons-at-work.de", + "email": "test@example.com", "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 michael@dragons-at-work.de inbox" - + echo "Check admin@example.com 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 7b59dc7..220cf69 100755 --- a/scripts/validate-config.sh +++ b/scripts/validate-config.sh @@ -4,7 +4,7 @@ set -e # Detect config file location -if [ "$(uname)" = "OpenBSD" ] || [ "$(uname)" = "FreeBSD" ]; then +if [ "$(uname)" = "OpenBSD" ]; then CONFIG_FILE="/usr/local/etc/furt/furt.conf" else CONFIG_FILE="/etc/furt/furt.conf" @@ -24,12 +24,13 @@ if ! grep -q '^\[server\]' "$CONFIG_FILE"; then exit 1 fi -if ! grep -q '^port\s*=' "$CONFIG_FILE"; then +# Fix: Use POSIX-compatible regex patterns +if ! grep -q '^[ \t]*port[ \t]*=' "$CONFIG_FILE"; then echo "Error: server port not configured" exit 1 fi -if ! grep -q '^host\s*=' "$CONFIG_FILE"; then +if ! grep -q '^[ \t]*host[ \t]*=' "$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 6fa36d5..8760014 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/FreeBSD + "/usr/local/etc/furt/furt.conf", -- OpenBSD "/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 72db9b5..c9b85b2 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 with Structured Logging +-- HTTP Server Core for Furt API-Gateway -- Dragons@Work Digital Sovereignty Project local socket = require("socket") @@ -10,7 +10,6 @@ end local config = require("config.server") local Auth = require("src.auth") -local Logger = require("src.logger") -- HTTP-Server Module local FurtServer = {} @@ -33,22 +32,11 @@ 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 @@ -61,7 +49,6 @@ 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 @@ -104,9 +91,7 @@ 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", - "https://dragons-at-work.de", - "https://www.dragons-at-work.de" + "http://127.0.0.1:1313" } -- Check if request has Origin header @@ -187,62 +172,22 @@ 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 - -- 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 + print(string.format("[%s] %s %s", os.date("%Y-%m-%d %H:%M:%S"), + request.method, request.path)) -- 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 @@ -252,78 +197,49 @@ 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 - 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) + print("Handler error: " .. tostring(result)) + local error_response = self:create_response(500, {error = "Internal server error"}, nil, nil, request) client:send(error_response) end else - 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) + 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) 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() - -- Structured startup logging - Logger.log_startup(self.host, self.port, 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") - -- Log configuration details + -- Show actual configured rate limits local rate_limits = config.security and config.security.rate_limits - 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() - }) + 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.info("Furt server ready - Press Ctrl+C to stop") + print("CORS enabled for " .. (#config.cors.allowed_origins) .. " configured origins") + print("Press Ctrl+C to stop") while true do local client = self.server:accept() diff --git a/src/logger.lua b/src/logger.lua deleted file mode 100644 index 4e36fa7..0000000 --- a/src/logger.lua +++ /dev/null @@ -1,200 +0,0 @@ --- 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 614aac5..0949efe 100644 --- a/src/main.lua +++ b/src/main.lua @@ -1,137 +1,34 @@ -- src/main.lua --- Furt API-Gateway - Application Entry Point with Structured Logging +-- Furt API-Gateway - Application Entry Point -- Dragons@Work Digital Sovereignty Project --- Load Logger first for startup logging -local Logger = require("src.logger") +-- Load HTTP Server Core +local FurtServer = require("src.http_server") -Logger.info("Furt API-Gateway starting up", { - startup_phase = "module_loading" -}) +-- Load Route Modules +local MailRoute = require("src.routes.mail") +local AuthRoute = require("src.routes.auth") +local HealthRoute = require("src.routes.health") --- 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" -}) +-- Load configuration +local config = require("config.server") -- 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) - routes_registered = routes_registered + 1 - - Logger.warn("Development test endpoint enabled", { - endpoint = "POST /test", - security_note = "Should be disabled in production" - }) + print("[WARN] Test endpoint enabled via configuration") 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 -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 -}) +-- Start server +server:start() diff --git a/src/smtp.lua b/src/smtp.lua index b419a75..a3340a8 100644 --- a/src/smtp.lua +++ b/src/smtp.lua @@ -1,6 +1,6 @@ --- furt-lua/src/smtp.lua +-- src/smtp.lua -- Universal SMTP implementation with SSL compatibility --- Supports both luaossl (Arch/karl) and luasec (OpenBSD/walter) +-- Supports both luaossl (Arch) and luasec (OpenBSD) -- 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 or "mail.dragons-at-work.de", - port = config.smtp_port or 465, + server = config.smtp_server, + port = config.smtp_port, username = config.username, password = config.password, - from_address = config.from_address or "noreply@dragons-at-work.de", + from_address = config.from_address, 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,64 +219,96 @@ 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( @@ -284,7 +316,10 @@ 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" .. ".", @@ -293,19 +328,20 @@ 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