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 777e6ad..26b078b 100644 --- a/.version_history +++ b/.version_history @@ -20,3 +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 +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 08fbfd5..c9b85b2 100644 --- a/src/http_server.lua +++ b/src/http_server.lua @@ -91,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 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