Compare commits

..

2 commits

Author SHA1 Message Date
5356288fcc chore: merkwerk auto-update 2025-09-05 20:31:36 +02:00
8fecb0188c stop(logging): implement structured JSON logging then halt due to YAGNI
Add comprehensive structured logging system, then stop development:

- Create Logger module with JSON output and configurable log levels
- Implement hash-based request ID generation for request tracing
- Add performance timing and client IP detection to HTTP server
- Enhance startup logging with module loading and configuration checks

STOPPED: Feature violates Low-Tech principles
- 200+ lines logging vs 100 lines business logic (code bloat)
- JSON serialization overhead reduces performance
- No current production need for structured monitoring
- Simple print() statements sufficient for current scale

Branch parked for future consideration when monitoring requirements
actually emerge. Issue #54 deferred to v0.2.x milestone.
2025-09-05 20:31:27 +02:00
27 changed files with 996 additions and 315 deletions

1
.gitignore vendored
View file

@ -68,4 +68,3 @@ config.production.lua
config/furt.conf config/furt.conf
scripts/production_test_sequence.sh

View file

@ -20,14 +20,4 @@
795f8867,78e8ded,fix/json-library-compatibility,2025-09-05T15:44:42Z,michael,git,lua-api 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 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,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 57ce9c01,8fecb01,feature/structured-logging-health-monitoring,2025-09-05T18:31:36Z,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

161
README.md
View file

@ -1,83 +1,160 @@
# Furt API Gateway # Furt API Gateway
**Pure Lua HTTP-Server für digitale Souveränität** **HTTP-Server in Lua für Service-Integration**
## Überblick ## Überblick
Furt ist ein minimalistisches HTTP-Server in Lua 5.1 für Mail-Versendung über SMTP. Es bietet eine einfache JSON-API für Web-Integration und Multi-Tenant-Unterstützung über API-Keys. Furt ist ein HTTP-Server der verschiedene Services unter einer API vereint. Aktuell unterstützt es Mail-Versendung über SMTP und bietet eine einfache JSON-API für Web-Integration.
## Features ## Features
- HTTP-Server mit JSON-APIs - HTTP-Server mit JSON-APIs
- Multi-Tenant Mail-Routing über SMTP - Mail-Versendung über SMTP
- API-Key-basierte Authentifizierung - Request-Routing und Authentication
- Health-Check-Endpoints - Health-Check-Endpoints
- Rate-Limiting pro API-Key - Konfigurierbare Rate-Limiting
- CORS-Support für Frontend-Integration - Hugo/Website-Integration
## Quick Start ## Dependencies
**Dependencies installieren:** **Erforderlich:**
```bash - `lua` 5.4+
# OpenBSD - `lua-socket` (HTTP-Server)
doas pkg_add lua lua-socket lua-cjson luasec - `lua-cjson` (JSON-Verarbeitung)
# 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:** **Installation:**
```bash ```bash
git clone https://smida.dragons-at-work.de/DAW/furt.git # Arch Linux
cd furt pacman -S lua lua-socket lua-cjson
sudo ./install.sh
# Ubuntu/Debian
apt install lua5.4 lua-socket lua-cjson
``` ```
**Server läuft auf:** http://127.0.0.1:7811 ## Installation
```bash
# Repository klonen
git clone <repository-url>
cd furt
# Scripts ausführbar machen
chmod +x scripts/*.sh
# Server starten
./scripts/start.sh
```
**Server läuft auf:** http://127.0.0.1:8080
## API-Endpoints ## API-Endpoints
**Health Check:** ### Health Check
```bash ```bash
curl http://127.0.0.1:7811/health GET /health
→ {"status":"healthy","service":"furt","version":"1.0.0"}
``` ```
**Mail senden:** ### Mail senden
```bash ```bash
curl -X POST http://127.0.0.1:7811/v1/mail/send \ POST /v1/mail/send
-H "X-API-Key: your-api-key" \ Content-Type: application/json
{
"name": "Name",
"email": "sender@example.com",
"message": "Nachricht"
}
→ {"success":true,"message":"Mail sent"}
```
## Konfiguration
**Environment Variables (.env):**
```bash
FURT_MAIL_HOST=mail.example.com
FURT_MAIL_PORT=587
FURT_MAIL_USERNAME=user@example.com
FURT_MAIL_PASSWORD=password
FURT_MAIL_TO=empfaenger@example.com
```
**Server-Config (config/server.lua):**
- Port und Host-Einstellungen
- API-Key-Konfiguration
- Rate-Limiting-Parameter
## Testing
**Automatische Tests:**
```bash
lua tests/test_http.lua
```
**Manuelle Tests:**
```bash
./scripts/test_curl.sh
# Oder direkt:
curl -X POST http://127.0.0.1:8080/v1/mail/send \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{"name":"Test","email":"test@example.com","subject":"Test","message":"Test-Nachricht"}' -d '{"name":"Test","email":"test@example.com","message":"Test"}'
``` ```
## Dokumentation ## Deployment
**Installation & Konfiguration:** [Furt Wiki](https://smida.dragons-at-work.de/DAW/furt/wiki) **OpenBSD:**
- rc.d-Script in `deployment/openbsd/`
- Systemd-Integration über Scripts
**Production-Setup:**
```bash
# Environment-Config kopieren
cp .env.example .env.production
# → SMTP-Credentials anpassen
# Production-Mode starten
export FURT_ENV=production
./scripts/start.sh
```
## Projektstruktur ## Projektstruktur
``` ```
furt/ furt/
├── src/ # Lua-Source-Code ├── src/ # Lua-Source-Code
├── config/ # Konfiguration │ ├── main.lua # HTTP-Server
├── scripts/ # Installation & Management │ ├── routes/ # API-Endpoints
└── deployment/ # System-Integration │ └── smtp.lua # Mail-Integration
├── config/ # Konfiguration
├── scripts/ # Start/Test-Scripts
├── tests/ # Test-Suite
└── deployment/ # System-Integration
``` ```
## Integration ## Hugo-Integration
**merkwerk:** Versionierte Furt-Deployment über [merkwerk](https://smida.dragons-at-work.de/DAW/merkwerk) **Shortcode-Beispiel:**
```html
<form action="http://your-server:8080/v1/mail/send" method="POST">
<input name="name" type="text" required>
<input name="email" type="email" required>
<textarea name="message" required></textarea>
<button type="submit">Senden</button>
</form>
```
## License ## Development
ISC - Siehe [LICENSE](LICENSE) für Details. **Code-Struktur:**
- Module unter 200 Zeilen
- Funktionen unter 50 Zeilen
- Klare Fehlerbehandlung
- Testbare Komponenten
## Links **Dependencies minimal halten:**
- Nur lua-socket und lua-cjson
- **Repository:** [Forgejo](https://smida.dragons-at-work.de/DAW/furt) - Keine externen HTTP-Libraries
- **Dokumentation:** [Wiki](https://smida.dragons-at-work.de/DAW/furt/wiki) - Standard-Lua-Funktionen bevorzugen
- **Projekt:** [Dragons@Work](https://dragons-at-work.de)

View file

@ -1 +1 @@
0.1.4 0.1.1

View file

@ -1,33 +1,18 @@
[Unit] [Unit]
Description=furt Multi-Tenant API Gateway (Security-Hardened) Description=furt Multi-Tenant API Gateway
After=network.target After=network.target
[Service] [Service]
Type=forking Type=forking
User=furt User=furt
Group=furt Group=furt
ExecStart=/usr/local/share/furt/scripts/start.sh ExecStart=/usr/local/share/furt/scripts/start.sh start
PIDFile=/var/run/furt/furt.pid
WorkingDirectory=/usr/local/share/furt WorkingDirectory=/usr/local/share/furt
Restart=always Restart=always
RestartSec=5 RestartSec=5
StandardOutput=journal StandardOutput=journal
StandardError=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] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target

View file

@ -3,52 +3,11 @@
daemon="/usr/local/share/furt/scripts/start.sh" daemon="/usr/local/share/furt/scripts/start.sh"
daemon_user="_furt" daemon_user="_furt"
daemon_cwd="/usr/local/share/furt" daemon_cwd="/usr/local/share/furt"
daemon_flags="start"
. /etc/rc.d/rc.subr . /etc/rc.d/rc.subr
# PID-File location pexp="lua.*src/main.lua"
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 rc_cmd $1

176
docs/setup-guide.md Normal file
View file

@ -0,0 +1,176 @@
# Multi-Tenant furt Setup-Anleitung
## Installation
### 1. Dateien platzieren
```bash
# OpenBSD/FreeBSD
mkdir -p /usr/local/etc/furt
mkdir -p /usr/local/share/furt
# Oder Linux
mkdir -p /etc/furt
mkdir -p /usr/local/share/furt
# Source code
cp -r src/ /usr/local/share/furt/
cp -r config/ /usr/local/share/furt/
```
### 2. Konfiguration erstellen
```bash
# Beispiel-Config kopieren und anpassen
# OpenBSD/FreeBSD:
cp furt.conf.example /usr/local/etc/furt/furt.conf
# Linux:
cp furt.conf.example /etc/furt/furt.conf
# Config editieren
vi /usr/local/etc/furt/furt.conf # oder /etc/furt/furt.conf
```
### 3. Start-Script
```bash
#!/bin/sh
# /usr/local/bin/furt
cd /usr/local/share/furt
lua src/main.lua
```
## Multi-Tenant Konfiguration
### Beispiel für 3 Websites
```ini
[server]
host = 127.0.0.1
port = 8080
[smtp_default]
host = mail.dragons-at-work.de
port = 465
user = noreply@dragons-at-work.de
password = your-smtp-password
# Website 1: Dragons@Work
[api_key "daw-key-abc123"]
name = "Dragons@Work Website"
permissions = mail:send
allowed_ips = 1.2.3.4/32, 10.0.0.0/8
mail_to = admin@dragons-at-work.de
mail_from = noreply@dragons-at-work.de
mail_subject_prefix = "[DAW] "
# Website 2: Biocodie (gleiche SMTP, andere Empfänger)
[api_key "bio-key-def456"]
name = "Biocodie Website"
permissions = mail:send
allowed_ips = 5.6.7.8/32
mail_to = contact@biocodie.de
mail_from = noreply@biocodie.de
mail_subject_prefix = "[Biocodie] "
# Website 3: Kunde mit eigenem SMTP
[api_key "kunde-key-ghi789"]
name = "Kunde X Website"
permissions = mail:send
allowed_ips = 9.10.11.12/32
mail_to = info@kunde-x.de
mail_from = noreply@kunde-x.de
mail_smtp_host = mail.kunde-x.de
mail_smtp_user = noreply@kunde-x.de
mail_smtp_pass = kunde-smtp-password
```
## Admin-Workflow
### Neue Website hinzufügen
1. **Config editieren:**
```bash
vi /usr/local/etc/furt/furt.conf
```
2. **Neuen API-Key-Block hinzufügen:**
```ini
[api_key "neue-website-key"]
name = "Neue Website"
permissions = mail:send
allowed_ips = 12.34.56.78/32
mail_to = contact@neue-website.de
mail_from = noreply@neue-website.de
```
3. **furt neu starten:**
```bash
systemctl restart furt
# oder
pkill -f "lua.*main.lua" && /usr/local/bin/furt &
```
### Website testen
```bash
# Test mit curl
curl -X POST http://localhost:8080/v1/mail/send \
-H "X-API-Key: neue-website-key" \
-H "Content-Type: application/json" \
-d '{
"name": "Test User",
"email": "test@example.com",
"subject": "Test Message",
"message": "This is a test message"
}'
```
## Vorteile des Multi-Tenant-Systems
### ✅ Ein Server, viele Websites
- Alle Websites nutzen eine furt-Instanz
- Jede Website hat eigenen API-Key
- Verschiedene Empfänger-Adressen
- Verschiedene SMTP-Server möglich
### ✅ Admin-freundlich
- Nginx-style Config-Format
- Einfach neue Websites hinzufügen
- Klare Struktur pro Website
- Kommentare möglich
### ✅ Sicher
- IP-Restrictions pro Website
- Permissions pro API-Key
- Separate SMTP-Credentials möglich
- Rate-Limiting bleibt erhalten
### ✅ Flexibel
- Default SMTP + website-spezifische SMTP
- Subject-Prefix pro Website
- Verschiedene Mail-Adressen
- Beliebig viele Websites
## Backward Compatibility
Das neue System ist **vollständig kompatibel** mit der alten config/server.lua API. Bestehende Module (auth.lua, main.lua, etc.) funktionieren ohne Änderungen.
## Troubleshooting
### Config-Parsing-Fehler
```bash
# Config-Syntax prüfen
lua -e "require('src.config_parser').parse_file('/usr/local/etc/furt/furt.conf')"
```
### Mail-Routing testen
```bash
# Logs anschauen
tail -f /var/log/furt.log
# Debug-Mode
FURT_DEBUG=true lua src/main.lua
```

View file

@ -102,7 +102,7 @@ else
echo "" echo ""
echo "Next steps:" echo "Next steps:"
echo "1. Edit configuration file:" echo "1. Edit configuration file:"
if [ "$(uname)" = "OpenBSD" ]; then if [ "$(uname)" = "OpenBSD" ] || [ "$(uname)" = "FreeBSD" ]; then
echo " /usr/local/etc/furt/furt.conf" echo " /usr/local/etc/furt/furt.conf"
else else
echo " /etc/furt/furt.conf" echo " /etc/furt/furt.conf"

0
scripts/cleanup_debug.sh Executable file → Normal file
View file

4
scripts/manual_mail_test.sh Executable file → Normal file
View file

@ -4,11 +4,11 @@
echo "Testing SMTP with corrected JSON..." echo "Testing SMTP with corrected JSON..."
# Simple test without timestamp embedding # Simple test without timestamp embedding
curl -X POST http://127.0.0.1:7811/v1/mail/send \ curl -X POST http://127.0.0.1:8080/v1/mail/send \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{ -d '{
"name": "Furt Test User", "name": "Furt Test User",
"email": "admin@example.com", "email": "michael@dragons-at-work.de",
"subject": "Furt SMTP Test Success!", "subject": "Furt SMTP Test Success!",
"message": "This is a test email from Furt Lua HTTP-Server. SMTP Integration working!" "message": "This is a test email from Furt Lua HTTP-Server. SMTP Integration working!"
}' }'

View file

@ -0,0 +1,80 @@
#!/bin/bash
# Production Test für api.dragons-at-work.de
echo "Testing Production API via Apache Proxy"
echo "======================================="
# Test 1: HTTPS Health Check
echo ""
echo "[1] Testing HTTPS Health Check..."
https_health=$(curl -s https://api.dragons-at-work.de/health)
echo "HTTPS Response: $https_health"
if echo "$https_health" | grep -q "healthy"; then
echo "[OK] HTTPS Proxy working"
else
echo "[ERROR] HTTPS Proxy failed"
exit 1
fi
# Test 2: SMTP Status via HTTPS
echo ""
echo "[2] Testing SMTP Configuration via HTTPS..."
if echo "$https_health" | grep -q '"smtp_configured":true'; then
echo "[OK] SMTP configured and accessible via HTTPS"
else
echo "[ERROR] SMTP not configured or not accessible"
fi
# Test 3: CORS Headers
echo ""
echo "[3] Testing CORS Headers..."
cors_test=$(curl -s -I https://api.dragons-at-work.de/health | grep -i "access-control")
if [ -n "$cors_test" ]; then
echo "[OK] CORS headers present: $cors_test"
else
echo "[WARN] CORS headers missing - add to Apache config"
fi
# Test 4: Production Mail Test
echo ""
echo "[4] Testing Production Mail via HTTPS..."
echo "WARNING: This sends real email via production API"
read -p "Continue with production mail test? (y/N): " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
echo "Sending production test email..."
prod_mail_response=$(curl -s -X POST https://api.dragons-at-work.de/v1/mail/send \
-H "Content-Type: application/json" \
-d '{
"name": "Production Test",
"email": "test@dragons-at-work.de",
"subject": "Production API Test - Apache Proxy Success!",
"message": "This email was sent via the production API at api.dragons-at-work.de through Apache proxy to Furt-Lua backend. HTTPS integration working!"
}')
echo "Production Response: $prod_mail_response"
if echo "$prod_mail_response" | grep -q '"success":true'; then
echo "[OK] PRODUCTION MAIL SENT VIA HTTPS!"
echo "Check admin@dragons-at-work.de for delivery confirmation"
else
echo "[ERROR] Production mail failed"
fi
else
echo "Skipping production mail test"
fi
# Test 5: Security Headers
echo ""
echo "[5] Testing Security Headers..."
security_headers=$(curl -s -I https://api.dragons-at-work.de/health)
echo "Security Headers:"
echo "$security_headers" | grep -i "x-content-type-options\|x-frame-options\|strict-transport"
echo ""
echo "Production Test Complete!"
echo "========================"
echo "Next: Hugo integration with https://api.dragons-at-work.de/v1/mail/send"

View file

@ -4,7 +4,7 @@
set -e set -e
# Detect operating system for config directory # Detect operating system for config directory
if [ "$(uname)" = "OpenBSD" ]; then if [ "$(uname)" = "OpenBSD" ] || [ "$(uname)" = "FreeBSD" ]; then
CONFIG_DIR="/usr/local/etc/furt" CONFIG_DIR="/usr/local/etc/furt"
USER="_furt" USER="_furt"
GROUP="_furt" GROUP="_furt"
@ -18,15 +18,12 @@ fi
mkdir -p "$CONFIG_DIR" mkdir -p "$CONFIG_DIR"
mkdir -p /usr/local/share/furt mkdir -p /usr/local/share/furt
mkdir -p /var/log/furt mkdir -p /var/log/furt
mkdir -p /var/run/furt
# Set ownership for log directory (service user needs write access) # Set ownership for log directory (service user needs write access)
chown "$USER:$GROUP" /var/log/furt chown "$USER:$GROUP" /var/log/furt
chown "$USER:$GROUP" /var/run/furt
echo "Created directories:" echo "Created directories:"
echo " Config: $CONFIG_DIR" echo " Config: $CONFIG_DIR"
echo " Share: /usr/local/share/furt" echo " Share: /usr/local/share/furt"
echo " Logs: /var/log/furt (owned by $USER)" echo " Logs: /var/log/furt (owned by $USER)"
echo " PID: /var/run/furt (owned by $USER)"

View file

@ -4,7 +4,7 @@
set -e set -e
# Detect operating system # Detect operating system
if [ "$(uname)" = "OpenBSD" ]; then if [ "$(uname)" = "OpenBSD" ] || [ "$(uname)" = "FreeBSD" ]; then
# BSD systems use _furt user convention # BSD systems use _furt user convention
groupadd _furt 2>/dev/null || true groupadd _furt 2>/dev/null || true
useradd -g _furt -s /bin/false -d /var/empty _furt 2>/dev/null || true useradd -g _furt -s /bin/false -d /var/empty _furt 2>/dev/null || true

101
scripts/setup_env.sh Executable file
View file

@ -0,0 +1,101 @@
#!/bin/bash
# furt-lua/scripts/setup_env.sh
# Add SMTP environment variables to existing .env (non-destructive)
set -e
# Colors for output
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m' # No Color
echo -e "${GREEN}=== Furt SMTP Environment Setup ===${NC}"
# Navigate to furt project root
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(dirname "$(dirname "$SCRIPT_DIR")")"
ENV_FILE="$PROJECT_ROOT/.env"
echo -e "${YELLOW}Project root:${NC} $PROJECT_ROOT"
echo -e "${YELLOW}Environment file:${NC} $ENV_FILE"
# Check if .env exists
if [ ! -f "$ENV_FILE" ]; then
echo -e "${YELLOW}Creating new .env file...${NC}"
cat > "$ENV_FILE" << 'EOF'
# Dragons@Work Project Environment Variables
# Furt SMTP Configuration for mail.dragons-at-work.de
SMTP_HOST="mail.dragons-at-work.de"
SMTP_PORT="465"
SMTP_USERNAME="your_email@dragons-at-work.de"
SMTP_PASSWORD="your_smtp_password"
SMTP_FROM="noreply@dragons-at-work.de"
SMTP_TO="michael@dragons-at-work.de"
EOF
echo -e "${GREEN}[OK] Created new .env file${NC}"
echo -e "${YELLOW}[EDIT] Please edit:${NC} nano $ENV_FILE"
exit 0
fi
echo -e "${GREEN}[OK] Found existing .env file${NC}"
# Check if SMTP variables already exist
smtp_username_exists=$(grep -c "^SMTP_USERNAME=" "$ENV_FILE" 2>/dev/null || echo "0")
smtp_password_exists=$(grep -c "^SMTP_PASSWORD=" "$ENV_FILE" 2>/dev/null || echo "0")
if [ "$smtp_username_exists" -gt 0 ] && [ "$smtp_password_exists" -gt 0 ]; then
echo -e "${GREEN}[OK] SMTP variables already configured${NC}"
# Load and show current values
source "$ENV_FILE"
echo -e "${YELLOW}Current SMTP User:${NC} ${SMTP_USERNAME:-NOT_SET}"
echo -e "${YELLOW}Current SMTP Password:${NC} ${SMTP_PASSWORD:+[CONFIGURED]}${SMTP_PASSWORD:-NOT_SET}"
echo ""
echo -e "${YELLOW}To update SMTP settings:${NC} nano $ENV_FILE"
exit 0
fi
# Add missing SMTP variables
echo -e "${YELLOW}Adding SMTP configuration to existing .env...${NC}"
# Add section header if not present
if ! grep -q "SMTP_" "$ENV_FILE" 2>/dev/null; then
echo "" >> "$ENV_FILE"
echo "# Furt SMTP Configuration for mail.dragons-at-work.de" >> "$ENV_FILE"
fi
# Add username if missing
if [ "$smtp_username_exists" -eq 0 ]; then
echo "SMTP_HOST=\"mail.dragons-at-work.de\"" >> "$ENV_FILE"
echo "SMTP_PORT=\"465\"" >> "$ENV_FILE"
echo "SMTP_USERNAME=\"your_email@dragons-at-work.de\"" >> "$ENV_FILE"
echo -e "${GREEN}[OK] Added SMTP_HOST, SMTP_PORT, SMTP_USERNAME${NC}"
fi
# Add password if missing
if [ "$smtp_password_exists" -eq 0 ]; then
echo "SMTP_PASSWORD=\"your_smtp_password\"" >> "$ENV_FILE"
echo "SMTP_FROM=\"noreply@dragons-at-work.de\"" >> "$ENV_FILE"
echo "SMTP_TO=\"michael@dragons-at-work.de\"" >> "$ENV_FILE"
echo -e "${GREEN}[OK] Added SMTP_PASSWORD, SMTP_FROM, SMTP_TO${NC}"
fi
echo -e "${GREEN}[OK] SMTP configuration added to .env${NC}"
echo ""
echo -e "${YELLOW}Next steps:${NC}"
echo "1. Edit SMTP credentials: nano $ENV_FILE"
echo "2. Set your actual email@dragons-at-work.de in SMTP_USERNAME"
echo "3. Set your actual SMTP password in SMTP_PASSWORD"
echo "4. Test with: ./scripts/start.sh"
echo ""
echo -e "${YELLOW}Current .env content:${NC}"
echo "==================="
cat "$ENV_FILE"
echo "==================="
echo ""
echo -e "${GREEN}Ready for SMTP testing!${NC}"

View file

@ -17,12 +17,10 @@ echo -e "${GREEN}=== Furt Lua HTTP-Server Startup ===${NC}"
LUA_COMMAND="" LUA_COMMAND=""
# Config check first # Config check first
if [ "$(uname)" = "OpenBSD" ]; then if [ "$(uname)" = "OpenBSD" ] || [ "$(uname)" = "FreeBSD" ]; then
CONFIG_FILE="/usr/local/etc/furt/furt.conf" CONFIG_FILE="/usr/local/etc/furt/furt.conf"
PID_FILE="/var/run/furt/furt.pid"
else else
CONFIG_FILE="/etc/furt/furt.conf" CONFIG_FILE="/etc/furt/furt.conf"
PID_FILE="/var/run/furt/furt.pid"
fi fi
if [ ! -f "$CONFIG_FILE" ] && [ ! -f "$PROJECT_DIR/config/furt.conf" ]; then if [ ! -f "$CONFIG_FILE" ] && [ ! -f "$PROJECT_DIR/config/furt.conf" ]; then
@ -89,38 +87,12 @@ cd "$PROJECT_DIR"
echo -e "${GREEN}Starting Furt...${NC}" 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 # Service vs Interactive Detection
if [ ! -t 0 ] || [ ! -t 1 ]; then if [ ! -t 0 ] || [ ! -t 1 ]; then
# Service mode - Background + PID-File # Service mode - Background
echo -e "${GREEN}Service mode: Background + PID-File${NC}"
# Start process in background
"$LUA_COMMAND" src/main.lua & "$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 else
# Interactive mode - Foreground (no PID-File) # Interactive mode - Foreground
echo -e "${GREEN}Interactive mode: Foreground${NC}"
exec "$LUA_COMMAND" src/main.lua exec "$LUA_COMMAND" src/main.lua
fi fi

View file

@ -4,7 +4,7 @@
BASE_URL="http://127.0.0.1:8080" BASE_URL="http://127.0.0.1:8080"
# Use correct API keys that match current .env # Use correct API keys that match current .env
API_KEY="YOUR_API_KEY_HERE" API_KEY="hugo-dev-key-change-in-production"
echo "⚡ Furt API Stress Test" echo "⚡ Furt API Stress Test"
echo "======================" echo "======================"

View file

@ -24,13 +24,8 @@ cp -r integrations/ "$TARGET/"
[ -f "VERSION" ] && cp VERSION "$TARGET/" [ -f "VERSION" ] && cp VERSION "$TARGET/"
[ -f ".version_history" ] && cp .version_history "$TARGET/" [ -f ".version_history" ] && cp .version_history "$TARGET/"
# Set proper permissions based on operating system # Set proper permissions
if [ "$(uname)" = "OpenBSD" ]; then chown -R root:wheel "$TARGET" 2>/dev/null || chown -R root:root "$TARGET"
chown -R root:wheel "$TARGET"
else
chown -R root:root "$TARGET"
fi
chmod -R 644 "$TARGET" chmod -R 644 "$TARGET"
find "$TARGET" -type d -exec chmod 755 {} \; find "$TARGET" -type d -exec chmod 755 {} \;
chmod +x "$TARGET/scripts/start.sh" chmod +x "$TARGET/scripts/start.sh"

View file

@ -3,8 +3,8 @@
# Test API-Key-Authentifizierung (ohne jq parse errors) # Test API-Key-Authentifizierung (ohne jq parse errors)
BASE_URL="http://127.0.0.1:8080" BASE_URL="http://127.0.0.1:8080"
HUGO_API_KEY="YOUR_API_KEY_HERE" HUGO_API_KEY="hugo-dev-key-change-in-production"
ADMIN_API_KEY="YOUR_ADMIN_KEY_HERE" ADMIN_API_KEY="admin-dev-key-change-in-production"
INVALID_API_KEY="invalid-key-should-fail" INVALID_API_KEY="invalid-key-should-fail"
echo "🔐 Testing Furt API-Key Authentication" echo "🔐 Testing Furt API-Key Authentication"

2
scripts/test_modular.sh Executable file → Normal file
View file

@ -3,7 +3,7 @@
# Test der modularen Furt-Architektur # Test der modularen Furt-Architektur
BASE_URL="http://127.0.0.1:8080" BASE_URL="http://127.0.0.1:8080"
HUGO_API_KEY="YOUR_API_KEY_HERE" HUGO_API_KEY="hugo-dev-key-change-in-production"
echo "🧩 Testing Modular Furt Architecture" echo "🧩 Testing Modular Furt Architecture"
echo "====================================" echo "===================================="

6
scripts/test_smtp.sh Executable file → Normal file
View file

@ -54,7 +54,7 @@ fi
# Test 4: Valid Mail Request (REAL SMTP TEST) # Test 4: Valid Mail Request (REAL SMTP TEST)
echo "" echo ""
echo "[4] Testing REAL mail sending..." echo "[4] Testing REAL mail sending..."
echo "WARNING: This will send a real email to admin@example.com" echo "WARNING: This will send a real email to michael@dragons-at-work.de"
read -p "Continue with real mail test? (y/N): " -n 1 -r read -p "Continue with real mail test? (y/N): " -n 1 -r
echo echo
@ -65,7 +65,7 @@ if [[ $REPLY =~ ^[Yy]$ ]]; then
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{ -d '{
"name": "Furt Test User", "name": "Furt Test User",
"email": "test@example.com", "email": "test@dragons-at-work.de",
"subject": "Furt SMTP Test - Week 2 Success!", "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" "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"
}') }')
@ -75,7 +75,7 @@ if [[ $REPLY =~ ^[Yy]$ ]]; then
# Check for success # Check for success
if echo "$mail_response" | grep -q '"success":true'; then if echo "$mail_response" | grep -q '"success":true'; then
echo "[OK] MAIL SENT SUCCESSFULLY!" echo "[OK] MAIL SENT SUCCESSFULLY!"
echo "Check admin@example.com inbox" echo "Check michael@dragons-at-work.de inbox"
# Extract request ID # Extract request ID
request_id=$(echo "$mail_response" | grep -o '"request_id":"[^"]*"' | cut -d'"' -f4) request_id=$(echo "$mail_response" | grep -o '"request_id":"[^"]*"' | cut -d'"' -f4)

View file

@ -4,7 +4,7 @@
set -e set -e
# Detect config file location # Detect config file location
if [ "$(uname)" = "OpenBSD" ]; then if [ "$(uname)" = "OpenBSD" ] || [ "$(uname)" = "FreeBSD" ]; then
CONFIG_FILE="/usr/local/etc/furt/furt.conf" CONFIG_FILE="/usr/local/etc/furt/furt.conf"
else else
CONFIG_FILE="/etc/furt/furt.conf" CONFIG_FILE="/etc/furt/furt.conf"
@ -24,13 +24,12 @@ if ! grep -q '^\[server\]' "$CONFIG_FILE"; then
exit 1 exit 1
fi fi
# Fix: Use POSIX-compatible regex patterns if ! grep -q '^port\s*=' "$CONFIG_FILE"; then
if ! grep -q '^[ \t]*port[ \t]*=' "$CONFIG_FILE"; then
echo "Error: server port not configured" echo "Error: server port not configured"
exit 1 exit 1
fi fi
if ! grep -q '^[ \t]*host[ \t]*=' "$CONFIG_FILE"; then if ! grep -q '^host\s*=' "$CONFIG_FILE"; then
echo "Error: server host not configured" echo "Error: server host not configured"
exit 1 exit 1
fi fi

View file

@ -215,7 +215,7 @@ end
function ConfigParser.load_config() function ConfigParser.load_config()
-- Try different locations based on OS -- Try different locations based on OS
local config_paths = { local config_paths = {
"/usr/local/etc/furt/furt.conf", -- OpenBSD "/usr/local/etc/furt/furt.conf", -- OpenBSD/FreeBSD
"/etc/furt/furt.conf", -- Linux "/etc/furt/furt.conf", -- Linux
"config/furt.conf", -- Development "config/furt.conf", -- Development
"furt.conf" -- Current directory "furt.conf" -- Current directory

View file

@ -1,5 +1,5 @@
-- src/http_server.lua -- src/http_server.lua
-- HTTP Server Core for Furt API-Gateway -- HTTP Server Core for Furt API-Gateway with Structured Logging
-- Dragons@Work Digital Sovereignty Project -- Dragons@Work Digital Sovereignty Project
local socket = require("socket") local socket = require("socket")
@ -10,6 +10,7 @@ end
local config = require("config.server") local config = require("config.server")
local Auth = require("src.auth") local Auth = require("src.auth")
local Logger = require("src.logger")
-- HTTP-Server Module -- HTTP-Server Module
local FurtServer = {} local FurtServer = {}
@ -32,11 +33,22 @@ function FurtServer:add_route(method, path, handler)
self.routes[method] = {} self.routes[method] = {}
end end
self.routes[method][path] = handler self.routes[method][path] = handler
Logger.debug("Route registered", {
method = method,
path = path
})
end end
-- Add protected route (requires authentication) -- Add protected route (requires authentication)
function FurtServer:add_protected_route(method, path, required_permission, handler) function FurtServer:add_protected_route(method, path, required_permission, handler)
self:add_route(method, path, Auth.create_protected_route(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 end
-- Parse HTTP request -- Parse HTTP request
@ -49,6 +61,7 @@ function FurtServer:parse_request(client)
-- Parse request line: "POST /v1/mail/send HTTP/1.1" -- Parse request line: "POST /v1/mail/send HTTP/1.1"
local method, path, protocol = request_line:match("(%w+) (%S+) (%S+)") local method, path, protocol = request_line:match("(%w+) (%S+) (%S+)")
if not method then if not method then
Logger.warn("Invalid request line", { request_line = request_line })
return nil return nil
end end
@ -91,7 +104,9 @@ end
function FurtServer:add_cors_headers(request) function FurtServer:add_cors_headers(request)
local allowed_origins = config.cors and config.cors.allowed_origins or { local allowed_origins = config.cors and config.cors.allowed_origins or {
"http://localhost:1313", "http://localhost:1313",
"http://127.0.0.1:1313" "http://127.0.0.1:1313",
"https://dragons-at-work.de",
"https://www.dragons-at-work.de"
} }
-- Check if request has Origin header -- Check if request has Origin header
@ -172,22 +187,62 @@ function FurtServer:get_status_text(status)
return status_texts[status] or "Unknown" return status_texts[status] or "Unknown"
end 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 -- Handle client request
function FurtServer:handle_client(client) 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) local request = self:parse_request(client)
if not request then 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) local response = self:create_response(400, {error = "Invalid request"}, nil, nil, nil)
client:send(response) client:send(response)
Logger.log_request("INVALID", "unknown", 400, duration_ms, "unknown", request_id)
return return
end end
print(string.format("[%s] %s %s", os.date("%Y-%m-%d %H:%M:%S"), -- Get client IP
request.method, request.path)) local client_ip = self:get_client_ip(client, request.headers)
-- Add request_id to request context for handlers
request.request_id = request_id
-- Handle OPTIONS preflight requests (CORS) -- Handle OPTIONS preflight requests (CORS)
if request.method == "OPTIONS" then 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) local response = self:create_response(204, "", "text/plain", nil, request)
client:send(response) client:send(response)
Logger.log_request("OPTIONS", request.path, 204, duration_ms, client_ip, request_id)
return return
end end
@ -197,49 +252,78 @@ function FurtServer:handle_client(client)
handler = self.routes[request.method][request.path] handler = self.routes[request.method][request.path]
end end
local status = 404
if handler then if handler then
local success, result = pcall(handler, request, self) local success, result = pcall(handler, request, self)
if success then if success then
client:send(result) client:send(result)
-- Extract status from response (rough parsing)
status = tonumber(result:match("HTTP/1%.1 (%d+)")) or 200
else else
print("Handler error: " .. tostring(result)) Logger.log_error("Handler error", {
local error_response = self:create_response(500, {error = "Internal server error"}, nil, nil, request) request_id = request_id,
method = request.method,
path = request.path,
client_ip = client_ip
}, tostring(result))
status = 500
local error_response = self:create_response(500, {
error = "Internal server error",
request_id = request_id
}, nil, nil, request)
client:send(error_response) client:send(error_response)
end end
else else
print("Route not found: " .. request.method .. " " .. request.path) Logger.debug("Route not found", {
local response = self:create_response(404, {error = "Route not found", method = request.method, path = request.path}, nil, nil, request) request_id = request_id,
method = request.method,
path = request.path
})
local response = self:create_response(404, {
error = "Route not found",
method = request.method,
path = request.path,
request_id = request_id
}, nil, nil, request)
client:send(response) client:send(response)
end 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 end
-- Start HTTP server -- Start HTTP server
function FurtServer:start() function FurtServer:start()
self.server = socket.bind(self.host, self.port) self.server = socket.bind(self.host, self.port)
if not self.server then 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) error("Failed to bind to " .. self.host .. ":" .. self.port)
end end
local HealthRoute = require("src.routes.health") local HealthRoute = require("src.routes.health")
local version_info = HealthRoute.get_version_info() local version_info = HealthRoute.get_version_info()
print(string.format("Furt HTTP-Server started on %s:%d", self.host, self.port)) -- Structured startup logging
print("Version: " .. version_info.version .. " (merkwerk)") Logger.log_startup(self.host, self.port, version_info)
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")
-- Show actual configured rate limits -- Log configuration details
local rate_limits = config.security and config.security.rate_limits local rate_limits = config.security and config.security.rate_limits
if rate_limits then Logger.log_config_summary({
print(string.format("Rate limiting: ENABLED (%d req/hour per API key, %d req/hour per IP)", cors_origins_count = #config.cors.allowed_origins,
rate_limits.api_key_max, rate_limits.ip_max)) rate_limiting_enabled = rate_limits ~= nil,
else api_key_max = rate_limits and rate_limits.api_key_max,
print("Rate limiting: ENABLED (default values)") ip_max = rate_limits and rate_limits.ip_max,
end test_endpoint_enabled = config.security and config.security.enable_test_endpoint,
log_level = Logger.get_log_level()
})
print("CORS enabled for " .. (#config.cors.allowed_origins) .. " configured origins") Logger.info("Furt server ready - Press Ctrl+C to stop")
print("Press Ctrl+C to stop")
while true do while true do
local client = self.server:accept() local client = self.server:accept()

200
src/logger.lua Normal file
View file

@ -0,0 +1,200 @@
-- src/logger.lua
-- Structured JSON Logger for Furt API-Gateway
-- Dragons@Work Digital Sovereignty Project
local found_cjson, cjson = pcall(require, 'cjson')
if not found_cjson then
cjson = require('dkjson')
end
local config = require("config.server")
-- Hash-based request ID generator for collision resistance
local function generate_request_id()
local data = string.format("%d-%d-%d-%d",
os.time(),
math.random(1000000, 9999999),
math.random(1000000, 9999999),
os.clock() * 1000000)
-- Simple hash function (Lua-native, no dependencies)
local hash = 0
for i = 1, #data do
hash = (hash * 31 + string.byte(data, i)) % 2147483647
end
return string.format("req-%x", hash)
end
-- Export request ID generator
Logger = {}
Logger.generate_request_id = generate_request_id
local Logger = {}
-- Log levels with numeric values for filtering
local LOG_LEVELS = {
debug = 1,
info = 2,
warn = 3,
error = 4
}
-- Current log level from config
local current_log_level = LOG_LEVELS[config.log_level] or LOG_LEVELS.info
-- Service identification
local SERVICE_NAME = "furt-lua"
-- Generate timestamp in ISO format
local function get_timestamp()
return os.date("!%Y-%m-%dT%H:%M:%SZ")
end
-- Core logging function
local function log_structured(level, message, context)
-- Skip if log level is below threshold
if LOG_LEVELS[level] < current_log_level then
return
end
-- Build log entry
local log_entry = {
timestamp = get_timestamp(),
level = level,
service = SERVICE_NAME,
message = message
}
-- Add context data if provided
if context then
for key, value in pairs(context) do
log_entry[key] = value
end
end
-- Output as JSON
local json_output = cjson.encode(log_entry)
print(json_output)
end
-- Public logging functions
function Logger.debug(message, context)
log_structured("debug", message, context)
end
function Logger.info(message, context)
log_structured("info", message, context)
end
function Logger.warn(message, context)
log_structured("warn", message, context)
end
function Logger.error(message, context)
log_structured("error", message, context)
end
-- Request logging with performance metrics
function Logger.log_request(method, path, status, duration_ms, client_ip, request_id)
if not config.log_requests then
return
end
local context = {
method = method,
path = path,
status = status,
duration_ms = duration_ms,
client_ip = client_ip
}
-- Add request_id if provided
if request_id then
context.request_id = request_id
end
log_structured("info", "HTTP request", context)
end
-- Service startup logging
function Logger.log_startup(host, port, version_info)
Logger.info("Furt HTTP-Server starting", {
host = host,
port = port,
version = version_info.version,
content_hash = version_info.content_hash,
vcs_hash = version_info.vcs_info and version_info.vcs_info.hash
})
end
-- Service health logging
function Logger.log_health_check(status, details)
local level = status == "healthy" and "info" or "warn"
log_structured(level, "Health check", {
health_status = status,
details = details
})
end
-- Error logging with stack trace support
function Logger.log_error(error_message, context, stack_trace)
local error_context = context or {}
if stack_trace then
error_context.stack_trace = stack_trace
end
log_structured("error", error_message, error_context)
end
-- Configuration logging
function Logger.log_config_summary(summary)
Logger.info("Configuration loaded", summary)
end
-- Rate limiting events
function Logger.log_rate_limit(api_key, client_ip, limit_type)
Logger.warn("Rate limit exceeded", {
api_key = api_key and "***masked***" or nil,
client_ip = client_ip,
limit_type = limit_type
})
end
-- SMTP/Mail logging
function Logger.log_mail_event(event_type, recipient, success, error_message)
local level = success and "info" or "error"
log_structured(level, "Mail event", {
event_type = event_type,
recipient = recipient,
success = success,
error = error_message
})
end
-- Set log level dynamically (useful for debugging)
function Logger.set_log_level(level)
if LOG_LEVELS[level] then
current_log_level = LOG_LEVELS[level]
Logger.info("Log level changed", { new_level = level })
else
Logger.error("Invalid log level", { attempted_level = level })
end
end
-- Get current log level
function Logger.get_log_level()
for level, value in pairs(LOG_LEVELS) do
if value == current_log_level then
return level
end
end
return "unknown"
end
-- Check if a log level would be output
function Logger.would_log(level)
return LOG_LEVELS[level] and LOG_LEVELS[level] >= current_log_level
end
return Logger

View file

@ -1,34 +1,137 @@
-- src/main.lua -- src/main.lua
-- Furt API-Gateway - Application Entry Point -- Furt API-Gateway - Application Entry Point with Structured Logging
-- Dragons@Work Digital Sovereignty Project -- Dragons@Work Digital Sovereignty Project
-- Load HTTP Server Core -- Load Logger first for startup logging
local FurtServer = require("src.http_server") local Logger = require("src.logger")
-- Load Route Modules Logger.info("Furt API-Gateway starting up", {
local MailRoute = require("src.routes.mail") startup_phase = "module_loading"
local AuthRoute = require("src.routes.auth") })
local HealthRoute = require("src.routes.health")
-- Load configuration -- Load modules with error handling
local config = require("config.server") local function safe_require(module_name, description)
local success, module = pcall(require, module_name)
if not success then
Logger.error("Failed to load module", {
module = module_name,
description = description,
error = module
})
os.exit(1)
end
Logger.debug("Module loaded successfully", {
module = module_name,
description = description
})
return module
end
-- Load core modules
local FurtServer = safe_require("src.http_server", "HTTP Server Core")
local MailRoute = safe_require("src.routes.mail", "Mail Route Handler")
local AuthRoute = safe_require("src.routes.auth", "Auth Route Handler")
local HealthRoute = safe_require("src.routes.health", "Health Route Handler")
local config = safe_require("config.server", "Server Configuration")
Logger.info("All modules loaded successfully", {
startup_phase = "modules_ready"
})
-- Initialize server -- Initialize server
Logger.info("Initializing HTTP server", {
startup_phase = "server_init"
})
local server = FurtServer:new() local server = FurtServer:new()
-- Route registration with logging
local routes_registered = 0
-- Register public routes (no authentication required) -- Register public routes (no authentication required)
server:add_route("GET", "/health", HealthRoute.handle_health) server:add_route("GET", "/health", HealthRoute.handle_health)
routes_registered = routes_registered + 1
-- Test endpoint for development (configurable via furt.conf) -- Test endpoint for development (configurable via furt.conf)
if config.security and config.security.enable_test_endpoint then if config.security and config.security.enable_test_endpoint then
server:add_route("POST", "/test", HealthRoute.handle_test) server:add_route("POST", "/test", HealthRoute.handle_test)
print("[WARN] Test endpoint enabled via configuration") routes_registered = routes_registered + 1
Logger.warn("Development test endpoint enabled", {
endpoint = "POST /test",
security_note = "Should be disabled in production"
})
end end
-- Register protected routes (require authentication) -- Register protected routes (require authentication)
server:add_protected_route("POST", "/v1/mail/send", "mail:send", MailRoute.handle_mail_send) 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) server:add_protected_route("GET", "/v1/auth/status", nil, AuthRoute.handle_auth_status)
routes_registered = routes_registered + 2
-- Start server Logger.info("Route registration completed", {
server:start() 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
})

View file

@ -1,6 +1,6 @@
-- src/smtp.lua -- furt-lua/src/smtp.lua
-- Universal SMTP implementation with SSL compatibility -- Universal SMTP implementation with SSL compatibility
-- Supports both luaossl (Arch) and luasec (OpenBSD) -- Supports both luaossl (Arch/karl) and luasec (OpenBSD/walter)
-- Dragons@Work Digital Sovereignty Project -- Dragons@Work Digital Sovereignty Project
local socket = require("socket") local socket = require("socket")
@ -95,11 +95,11 @@ end
-- Create SMTP instance -- Create SMTP instance
function SMTP:new(config) function SMTP:new(config)
local instance = { local instance = {
server = config.smtp_server, server = config.smtp_server or "mail.dragons-at-work.de",
port = config.smtp_port, port = config.smtp_port or 465,
username = config.username, username = config.username,
password = config.password, password = config.password,
from_address = config.from_address, from_address = config.from_address or "noreply@dragons-at-work.de",
use_ssl = config.use_ssl or true, use_ssl = config.use_ssl or true,
debug = config.debug or false, debug = config.debug or false,
ssl_compat = SSLCompat ssl_compat = SSLCompat
@ -237,33 +237,6 @@ function SMTP:send_email(to_address, subject, message, from_name)
return cleanup_and_fail("EHLO failed: " .. response) return cleanup_and_fail("EHLO failed: " .. response)
end 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 -- AUTH LOGIN
local success, response = self:send_command(sock, "AUTH LOGIN", 334) local success, response = self:send_command(sock, "AUTH LOGIN", 334)
if not success then if not success then
@ -304,11 +277,6 @@ function SMTP:send_email(to_address, subject, message, from_name)
return cleanup_and_fail("DATA command failed: " .. response) return cleanup_and_fail("DATA command failed: " .. response)
end 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 -- Build email message
local display_name = from_name or "Furt Contact Form" local display_name = from_name or "Furt Contact Form"
local email_content = string.format( local email_content = string.format(
@ -316,10 +284,7 @@ function SMTP:send_email(to_address, subject, message, from_name)
"To: <%s>\r\n" .. "To: <%s>\r\n" ..
"Subject: %s\r\n" .. "Subject: %s\r\n" ..
"Date: %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-Type: text/plain; charset=UTF-8\r\n" ..
"Content-Transfer-Encoding: 8bit\r\n" ..
"\r\n" .. "\r\n" ..
"%s\r\n" .. "%s\r\n" ..
".", ".",
@ -328,7 +293,6 @@ function SMTP:send_email(to_address, subject, message, from_name)
to_address, to_address,
subject, subject,
os.date("%a, %d %b %Y %H:%M:%S %z"), os.date("%a, %d %b %Y %H:%M:%S %z"),
message_id,
message message
) )