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

View file

@ -15,25 +15,25 @@ if [ "$(uname)" = "OpenBSD" ]; then
echo "Error: deployment/openbsd/rc.d-furt template not found" echo "Error: deployment/openbsd/rc.d-furt template not found"
exit 1 exit 1
fi fi
cp deployment/openbsd/rc.d-furt /etc/rc.d/furt cp deployment/openbsd/rc.d-furt /etc/rc.d/furt
chmod +x /etc/rc.d/furt chmod +x /etc/rc.d/furt
echo "furt_flags=" >> /etc/rc.conf.local echo "furt_flags=" >> /etc/rc.conf.local
rcctl enable furt rcctl enable furt
echo "OpenBSD service created and enabled using repository template" echo "OpenBSD service created and enabled using repository template"
elif [ "$(uname)" = "Linux" ]; then elif [ "$(uname)" = "Linux" ]; then
# Use systemd template from repository # Use systemd template from repository
if [ ! -f "deployment/linux/furt.service" ]; then if [ ! -f "deployment/linux/furt.service" ]; then
echo "Error: deployment/linux/furt.service template not found" echo "Error: deployment/linux/furt.service template not found"
exit 1 exit 1
fi fi
cp deployment/linux/furt.service /etc/systemd/system/ cp deployment/linux/furt.service /etc/systemd/system/
systemctl daemon-reload systemctl daemon-reload
systemctl enable furt systemctl enable furt
echo "Linux systemd service created and enabled using repository template" echo "Linux systemd service created and enabled using repository template"
else else
echo "Unsupported operating system for service creation" echo "Unsupported operating system for service creation"
exit 1 exit 1

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 "======================"
@ -20,9 +20,9 @@ for i in {1..20}; do
response=$(curl -s -w "%{http_code}" \ response=$(curl -s -w "%{http_code}" \
-H "X-API-Key: $API_KEY" \ -H "X-API-Key: $API_KEY" \
"$BASE_URL/v1/auth/status") "$BASE_URL/v1/auth/status")
status=$(echo "$response" | tail -c 4) status=$(echo "$response" | tail -c 4)
if [ "$status" == "200" ]; then if [ "$status" == "200" ]; then
rate_limit_remaining=$(echo "$response" | head -n -1 | jq -r '.rate_limit_remaining // "N/A"' 2>/dev/null) 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)" echo "Request $i: ✅ 200 OK (Rate limit remaining: $rate_limit_remaining)"
@ -33,7 +33,7 @@ for i in {1..20}; do
else else
echo "Request $i: ❌ $status Error" echo "Request $i: ❌ $status Error"
fi fi
# Small delay to prevent overwhelming # Small delay to prevent overwhelming
sleep 0.1 sleep 0.1
done done
@ -58,10 +58,10 @@ for i in {1..10}; do
-H "X-API-Key: $API_KEY" \ -H "X-API-Key: $API_KEY" \
"$BASE_URL/health") "$BASE_URL/health")
local_end=$(date +%s.%N) local_end=$(date +%s.%N)
status=$(echo "$response" | tail -c 4) status=$(echo "$response" | tail -c 4)
duration=$(echo "$local_end - $local_start" | bc -l) duration=$(echo "$local_end - $local_start" | bc -l)
echo "Concurrent $i: Status $status, Duration ${duration}s" > "$temp_dir/result_$i" echo "Concurrent $i: Status $status, Duration ${duration}s" > "$temp_dir/result_$i"
} & } &
done done
@ -85,18 +85,18 @@ mail_errors=0
for i in {1..5}; do for i in {1..5}; do
start_time=$(date +%s.%N) start_time=$(date +%s.%N)
response=$(curl -s -w "%{http_code}" \ response=$(curl -s -w "%{http_code}" \
-H "X-API-Key: $API_KEY" \ -H "X-API-Key: $API_KEY" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d "{\"name\":\"Stress Test $i\",\"email\":\"test$i@example.com\",\"subject\":\"Performance Test\",\"message\":\"Load test message $i\"}" \ -d "{\"name\":\"Stress Test $i\",\"email\":\"test$i@example.com\",\"subject\":\"Performance Test\",\"message\":\"Load test message $i\"}" \
"$BASE_URL/v1/mail/send") "$BASE_URL/v1/mail/send")
end_time=$(date +%s.%N) end_time=$(date +%s.%N)
duration=$(echo "$end_time - $start_time" | bc -l) duration=$(echo "$end_time - $start_time" | bc -l)
status=$(echo "$response" | tail -c 4) status=$(echo "$response" | tail -c 4)
if [ "$status" == "200" ]; then if [ "$status" == "200" ]; then
echo "Mail $i: ✅ 200 OK (${duration}s)" echo "Mail $i: ✅ 200 OK (${duration}s)"
((mail_success++)) ((mail_success++))
@ -104,7 +104,7 @@ for i in {1..5}; do
echo "Mail $i: ❌ Status $status (${duration}s)" echo "Mail $i: ❌ Status $status (${duration}s)"
((mail_errors++)) ((mail_errors++))
fi fi
# Delay between mail sends to be nice to SMTP server # Delay between mail sends to be nice to SMTP server
sleep 1 sleep 1
done done
@ -120,7 +120,7 @@ mixed_success=0
for i in {1..15}; do for i in {1..15}; do
((mixed_total++)) ((mixed_total++))
if [ $((i % 3)) -eq 0 ]; then if [ $((i % 3)) -eq 0 ]; then
# Every 3rd request: auth status # Every 3rd request: auth status
endpoint="/v1/auth/status" endpoint="/v1/auth/status"
@ -128,20 +128,20 @@ for i in {1..15}; do
# Other requests: health check # Other requests: health check
endpoint="/health" endpoint="/health"
fi fi
response=$(curl -s -w "%{http_code}" \ response=$(curl -s -w "%{http_code}" \
-H "X-API-Key: $API_KEY" \ -H "X-API-Key: $API_KEY" \
"$BASE_URL$endpoint") "$BASE_URL$endpoint")
status=$(echo "$response" | tail -c 4) status=$(echo "$response" | tail -c 4)
if [ "$status" == "200" ]; then if [ "$status" == "200" ]; then
echo "Mixed $i ($endpoint): ✅ 200 OK" echo "Mixed $i ($endpoint): ✅ 200 OK"
((mixed_success++)) ((mixed_success++))
else else
echo "Mixed $i ($endpoint): ❌ $status" echo "Mixed $i ($endpoint): ❌ $status"
fi fi
sleep 0.2 sleep 0.2
done done

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"
@ -16,24 +16,24 @@ make_request() {
local url="$2" local url="$2"
local headers="$3" local headers="$3"
local data="$4" local data="$4"
echo "Request: $method $url" echo "Request: $method $url"
if [ -n "$headers" ]; then if [ -n "$headers" ]; then
echo "Headers: $headers" echo "Headers: $headers"
fi fi
local response=$(curl -s $method \ local response=$(curl -s $method \
${headers:+-H "$headers"} \ ${headers:+-H "$headers"} \
${data:+-d "$data"} \ ${data:+-d "$data"} \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
"$url") "$url")
local status=$(curl -s -o /dev/null -w "%{http_code}" $method \ local status=$(curl -s -o /dev/null -w "%{http_code}" $method \
${headers:+-H "$headers"} \ ${headers:+-H "$headers"} \
${data:+-d "$data"} \ ${data:+-d "$data"} \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
"$url") "$url")
echo "Status: $status" echo "Status: $status"
echo "Response: $response" | jq '.' 2>/dev/null || echo "$response" echo "Response: $response" | jq '.' 2>/dev/null || echo "$response"
echo "" echo ""

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 "===================================="

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

@ -36,7 +36,7 @@ else
echo "[ERROR] Validation failed" echo "[ERROR] Validation failed"
fi fi
# Test 3: Invalid Email Format # Test 3: Invalid Email Format
echo "" echo ""
echo "[3] Testing email validation..." echo "[3] Testing email validation..."
email_validation_response=$(curl -s -X POST "$SERVER_URL/v1/mail/send" \ 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) # 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
if [[ $REPLY =~ ^[Yy]$ ]]; then if [[ $REPLY =~ ^[Yy]$ ]]; then
echo "Sending real test email..." echo "Sending real test email..."
mail_response=$(curl -s -X POST "$SERVER_URL/v1/mail/send" \ mail_response=$(curl -s -X POST "$SERVER_URL/v1/mail/send" \
-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"
}') }')
echo "Response: $mail_response" echo "Response: $mail_response"
# 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)
echo "Request ID: $request_id" echo "Request ID: $request_id"
else else
echo "[ERROR] Mail sending failed" echo "[ERROR] Mail sending failed"
echo "Check server logs and SMTP credentials" echo "Check server logs and SMTP credentials"
# Show error details # Show error details
if echo "$mail_response" | grep -q "error"; then if echo "$mail_response" | grep -q "error"; then
error_msg=$(echo "$mail_response" | grep -o '"error":"[^"]*"' | cut -d'"' -f4) error_msg=$(echo "$mail_response" | grep -o '"error":"[^"]*"' | cut -d'"' -f4)
@ -126,7 +126,7 @@ echo "Performance: ${duration_ms}ms"
echo "" echo ""
echo "Week 2 Challenge Status:" echo "Week 2 Challenge Status:"
echo " SMTP Integration: COMPLETE" echo " SMTP Integration: COMPLETE"
echo " Environment Variables: CHECK .env" echo " Environment Variables: CHECK .env"
echo " Native Lua Implementation: DONE" echo " Native Lua Implementation: DONE"
echo " Production Ready: READY FOR TESTING" echo " Production Ready: READY FOR TESTING"

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")
@ -19,7 +19,7 @@ function SSLCompat:detect_ssl_library()
return "luaossl", ssl_lib return "luaossl", ssl_lib
end end
end end
-- Try luasec -- Try luasec
local success, ssl_lib = pcall(require, "ssl") local success, ssl_lib = pcall(require, "ssl")
if success and ssl_lib then if success and ssl_lib then
@ -28,23 +28,23 @@ function SSLCompat:detect_ssl_library()
return "luasec", ssl_lib return "luasec", ssl_lib
end end
end end
return nil, "No compatible SSL library found (tried luaossl, luasec)" return nil, "No compatible SSL library found (tried luaossl, luasec)"
end end
function SSLCompat:wrap_socket(sock, options) function SSLCompat:wrap_socket(sock, options)
local ssl_type, ssl_lib = self:detect_ssl_library() local ssl_type, ssl_lib = self:detect_ssl_library()
if not ssl_type then if not ssl_type then
return nil, ssl_lib -- ssl_lib contains error message return nil, ssl_lib -- ssl_lib contains error message
end end
if ssl_type == "luaossl" then if ssl_type == "luaossl" then
return self:wrap_luaossl(sock, options, ssl_lib) return self:wrap_luaossl(sock, options, ssl_lib)
elseif ssl_type == "luasec" then elseif ssl_type == "luasec" then
return self:wrap_luasec(sock, options, ssl_lib) return self:wrap_luasec(sock, options, ssl_lib)
end end
return nil, "Unknown SSL library type: " .. ssl_type return nil, "Unknown SSL library type: " .. ssl_type
end end
@ -55,18 +55,18 @@ function SSLCompat:wrap_luaossl(sock, options, ssl_lib)
protocol = "tlsv1_2", protocol = "tlsv1_2",
verify = "none" -- For self-signed certs verify = "none" -- For self-signed certs
}) })
if not ssl_sock then if not ssl_sock then
return nil, "luaossl wrap failed: " .. (err or "unknown error") return nil, "luaossl wrap failed: " .. (err or "unknown error")
end end
-- luaossl typically does handshake automatically, but explicit is safer -- luaossl typically does handshake automatically, but explicit is safer
local success, err = pcall(function() return ssl_sock:dohandshake() end) local success, err = pcall(function() return ssl_sock:dohandshake() end)
if not success then if not success then
-- Some luaossl versions don't need explicit handshake -- Some luaossl versions don't need explicit handshake
-- Continue if dohandshake doesn't exist -- Continue if dohandshake doesn't exist
end end
return ssl_sock, nil return ssl_sock, nil
end end
@ -78,28 +78,28 @@ function SSLCompat:wrap_luasec(sock, options, ssl_lib)
verify = "none", verify = "none",
options = "all" options = "all"
}) })
if not ssl_sock then if not ssl_sock then
return nil, "luasec wrap failed: " .. (err or "unknown error") return nil, "luasec wrap failed: " .. (err or "unknown error")
end end
-- luasec requires explicit handshake -- luasec requires explicit handshake
local success, err = ssl_sock:dohandshake() local success, err = ssl_sock:dohandshake()
if not success then if not success then
return nil, "luasec handshake failed: " .. (err or "unknown error") return nil, "luasec handshake failed: " .. (err or "unknown error")
end end
return ssl_sock, nil return ssl_sock, nil
end 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
@ -133,7 +133,7 @@ function SMTP:send_command(sock, command, expected_code)
if self.debug then if self.debug then
print("SMTP CMD: " .. (command or ""):gsub("\r\n", "\\r\\n")) print("SMTP CMD: " .. (command or ""):gsub("\r\n", "\\r\\n"))
end end
-- Only send if command is not nil (for server greeting, command is nil) -- Only send if command is not nil (for server greeting, command is nil)
if command then if command then
local success, err = sock:send(command .. "\r\n") 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") return false, "Failed to send command: " .. (err or "unknown error")
end end
end end
local response, err = sock:receive() local response, err = sock:receive()
if not response then if not response then
return false, "Failed to receive response: " .. (err or "unknown error") return false, "Failed to receive response: " .. (err or "unknown error")
end end
if self.debug then if self.debug then
print("SMTP RSP: " .. response) print("SMTP RSP: " .. response)
end end
-- Handle multi-line responses (like EHLO) -- Handle multi-line responses (like EHLO)
local full_response = response local full_response = response
while response:match("^%d%d%d%-") do while response:match("^%d%d%d%-") do
@ -163,12 +163,12 @@ function SMTP:send_command(sock, command, expected_code)
end end
full_response = full_response .. "\n" .. response full_response = full_response .. "\n" .. response
end end
local code = response:match("^(%d+)") local code = response:match("^(%d+)")
if expected_code and code ~= tostring(expected_code) then if expected_code and code ~= tostring(expected_code) then
return false, "Unexpected response: " .. full_response return false, "Unexpected response: " .. full_response
end end
return true, full_response return true, full_response
end end
@ -179,38 +179,38 @@ function SMTP:connect()
if not sock then if not sock then
return false, "Failed to create socket: " .. (err or "unknown error") return false, "Failed to create socket: " .. (err or "unknown error")
end end
-- Set timeout -- Set timeout
sock:settimeout(30) sock:settimeout(30)
-- Connect to server -- Connect to server
local success, err = sock:connect(self.server, self.port) local success, err = sock:connect(self.server, self.port)
if not success then if not success then
return false, "Failed to connect to " .. self.server .. ":" .. self.port .. " - " .. (err or "unknown error") return false, "Failed to connect to " .. self.server .. ":" .. self.port .. " - " .. (err or "unknown error")
end end
-- Wrap with SSL for port 465 using compatibility layer -- Wrap with SSL for port 465 using compatibility layer
if self.use_ssl and self.port == 465 then if self.use_ssl and self.port == 465 then
local ssl_sock, err = self.ssl_compat:wrap_socket(sock, { local ssl_sock, err = self.ssl_compat:wrap_socket(sock, {
mode = "client", mode = "client",
protocol = "tlsv1_2" protocol = "tlsv1_2"
}) })
if not ssl_sock then if not ssl_sock then
sock:close() sock:close()
return false, "Failed to establish SSL connection: " .. (err or "unknown error") return false, "Failed to establish SSL connection: " .. (err or "unknown error")
end end
sock = ssl_sock sock = ssl_sock
end end
-- Read server greeting -- Read server greeting
local success, response = self:send_command(sock, nil, 220) local success, response = self:send_command(sock, nil, 220)
if not success then if not success then
sock:close() sock:close()
return false, "SMTP server greeting failed: " .. response return false, "SMTP server greeting failed: " .. response
end end
return sock, nil return sock, nil
end end
@ -219,96 +219,64 @@ function SMTP:send_email(to_address, subject, message, from_name)
if not self.username or not self.password then if not self.username or not self.password then
return false, "SMTP username or password not configured" return false, "SMTP username or password not configured"
end end
-- Connect to server -- Connect to server
local sock, err = self:connect() local sock, err = self:connect()
if not sock then if not sock then
return false, err return false, err
end end
local function cleanup_and_fail(error_msg) local function cleanup_and_fail(error_msg)
sock:close() sock:close()
return false, error_msg return false, error_msg
end end
-- EHLO command -- EHLO command
local success, response = self:send_command(sock, "EHLO furt-lua", 250) local success, response = self:send_command(sock, "EHLO furt-lua", 250)
if not success then if not success then
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
return cleanup_and_fail("AUTH LOGIN failed: " .. response) return cleanup_and_fail("AUTH LOGIN failed: " .. response)
end end
-- Send username (base64 encoded) -- Send username (base64 encoded)
local username_b64 = self:base64_encode(self.username) local username_b64 = self:base64_encode(self.username)
local success, response = self:send_command(sock, username_b64, 334) local success, response = self:send_command(sock, username_b64, 334)
if not success then if not success then
return cleanup_and_fail("Username authentication failed: " .. response) return cleanup_and_fail("Username authentication failed: " .. response)
end end
-- Send password (base64 encoded) -- Send password (base64 encoded)
local password_b64 = self:base64_encode(self.password) local password_b64 = self:base64_encode(self.password)
local success, response = self:send_command(sock, password_b64, 235) local success, response = self:send_command(sock, password_b64, 235)
if not success then if not success then
return cleanup_and_fail("Password authentication failed: " .. response) return cleanup_and_fail("Password authentication failed: " .. response)
end end
-- MAIL FROM -- MAIL FROM
local mail_from = "MAIL FROM:<" .. self.from_address .. ">" local mail_from = "MAIL FROM:<" .. self.from_address .. ">"
local success, response = self:send_command(sock, mail_from, 250) local success, response = self:send_command(sock, mail_from, 250)
if not success then if not success then
return cleanup_and_fail("MAIL FROM failed: " .. response) return cleanup_and_fail("MAIL FROM failed: " .. response)
end end
-- RCPT TO -- RCPT TO
local rcpt_to = "RCPT TO:<" .. to_address .. ">" local rcpt_to = "RCPT TO:<" .. to_address .. ">"
local success, response = self:send_command(sock, rcpt_to, 250) local success, response = self:send_command(sock, rcpt_to, 250)
if not success then if not success then
return cleanup_and_fail("RCPT TO failed: " .. response) return cleanup_and_fail("RCPT TO failed: " .. response)
end end
-- DATA command -- DATA command
local success, response = self:send_command(sock, "DATA", 354) local success, response = self:send_command(sock, "DATA", 354)
if not success then if not success then
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,20 +293,19 @@ 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
) )
-- Send email content -- Send email content
local success, response = self:send_command(sock, email_content, 250) local success, response = self:send_command(sock, email_content, 250)
if not success then if not success then
return cleanup_and_fail("Email sending failed: " .. response) return cleanup_and_fail("Email sending failed: " .. response)
end end
-- QUIT -- QUIT
self:send_command(sock, "QUIT", 221) self:send_command(sock, "QUIT", 221)
sock:close() sock:close()
return true, "Email sent successfully" return true, "Email sent successfully"
end end