Compare commits
2 commits
main
...
feature/st
| Author | SHA1 | Date | |
|---|---|---|---|
| 5356288fcc | |||
| 8fecb0188c |
27 changed files with 996 additions and 315 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -68,4 +68,3 @@ config.production.lua
|
|||
|
||||
config/furt.conf
|
||||
|
||||
scripts/production_test_sequence.sh
|
||||
|
|
|
|||
|
|
@ -20,14 +20,4 @@
|
|||
795f8867,78e8ded,fix/json-library-compatibility,2025-09-05T15:44:42Z,michael,git,lua-api
|
||||
795f8867,d4fa6e3,fix/ssl-dependency-check,2025-09-05T16:20:08Z,michael,git,lua-api
|
||||
a670de0f,d271b84,refactor/extract-health-routes-and-server-core,2025-09-05T17:25:09Z,michael,git,lua-api
|
||||
a670de0f,25a709e,feature/pid-file-service-management,2025-09-05T20:30:13Z,michael,git,lua-api
|
||||
a670de0f,59f372f,feature/pid-file-service-management,2025-09-07T14:58:01Z,michael,git,lua-api
|
||||
a670de0f,683d6e5,fix/validate-config-posix-regex,2025-09-07T16:00:48Z,michael,git,lua-api
|
||||
a670de0f,24bd94d,feature/systemd-hardening,2025-09-07T16:40:47Z,michael,git,lua-api
|
||||
4ee95dbc,08b49d3,security/sanitize-test-scripts,2025-09-07T19:25:38Z,michael,git,lua-api
|
||||
59c85431,8b78066,main,2025-09-10T10:20:50Z,michael,git,lua-api
|
||||
a71dd794,f5d9f35,main,2025-09-10T12:27:54Z,michael,git,lua-api
|
||||
de5318f2,304b010,main,2025-09-10T14:45:12Z,michael,git,lua-api
|
||||
980d67cd,7a921dc,main,2025-09-10T14:46:13Z,michael,git,lua-api
|
||||
efbcbbd8,f20915f,main,2025-09-10T18:01:18Z,michael,git,lua-api
|
||||
f777e765,f684ea1,main,2025-09-10T18:04:19Z,michael,git,lua-api
|
||||
57ce9c01,8fecb01,feature/structured-logging-health-monitoring,2025-09-05T18:31:36Z,michael,git,lua-api
|
||||
|
|
|
|||
161
README.md
161
README.md
|
|
@ -1,83 +1,160 @@
|
|||
# Furt API Gateway
|
||||
|
||||
**Pure Lua HTTP-Server für digitale Souveränität**
|
||||
**HTTP-Server in Lua für Service-Integration**
|
||||
|
||||
## Überblick
|
||||
|
||||
Furt ist ein minimalistisches HTTP-Server in Lua 5.1 für Mail-Versendung über SMTP. Es bietet eine einfache JSON-API für Web-Integration und Multi-Tenant-Unterstützung über API-Keys.
|
||||
Furt ist ein HTTP-Server der verschiedene Services unter einer API vereint. Aktuell unterstützt es Mail-Versendung über SMTP und bietet eine einfache JSON-API für Web-Integration.
|
||||
|
||||
## Features
|
||||
|
||||
- HTTP-Server mit JSON-APIs
|
||||
- Multi-Tenant Mail-Routing über SMTP
|
||||
- API-Key-basierte Authentifizierung
|
||||
- Mail-Versendung über SMTP
|
||||
- Request-Routing und Authentication
|
||||
- Health-Check-Endpoints
|
||||
- Rate-Limiting pro API-Key
|
||||
- CORS-Support für Frontend-Integration
|
||||
- Konfigurierbare Rate-Limiting
|
||||
- Hugo/Website-Integration
|
||||
|
||||
## Quick Start
|
||||
## Dependencies
|
||||
|
||||
**Dependencies installieren:**
|
||||
```bash
|
||||
# OpenBSD
|
||||
doas pkg_add lua lua-socket lua-cjson luasec
|
||||
|
||||
# Debian/Ubuntu
|
||||
sudo apt install lua5.1 lua-socket lua-cjson lua-sec
|
||||
|
||||
# Arch Linux
|
||||
sudo pacman -S lua51 lua51-socket lua51-dkjson lua51-sec
|
||||
```
|
||||
**Erforderlich:**
|
||||
- `lua` 5.4+
|
||||
- `lua-socket` (HTTP-Server)
|
||||
- `lua-cjson` (JSON-Verarbeitung)
|
||||
|
||||
**Installation:**
|
||||
```bash
|
||||
git clone https://smida.dragons-at-work.de/DAW/furt.git
|
||||
cd furt
|
||||
sudo ./install.sh
|
||||
# Arch Linux
|
||||
pacman -S lua lua-socket lua-cjson
|
||||
|
||||
# Ubuntu/Debian
|
||||
apt install lua5.4 lua-socket lua-cjson
|
||||
```
|
||||
|
||||
**Server läuft auf:** http://127.0.0.1:7811
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# Repository klonen
|
||||
git clone <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
|
||||
|
||||
**Health Check:**
|
||||
### Health Check
|
||||
```bash
|
||||
curl http://127.0.0.1:7811/health
|
||||
GET /health
|
||||
→ {"status":"healthy","service":"furt","version":"1.0.0"}
|
||||
```
|
||||
|
||||
**Mail senden:**
|
||||
### Mail senden
|
||||
```bash
|
||||
curl -X POST http://127.0.0.1:7811/v1/mail/send \
|
||||
-H "X-API-Key: your-api-key" \
|
||||
POST /v1/mail/send
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"name": "Name",
|
||||
"email": "sender@example.com",
|
||||
"message": "Nachricht"
|
||||
}
|
||||
|
||||
→ {"success":true,"message":"Mail sent"}
|
||||
```
|
||||
|
||||
## Konfiguration
|
||||
|
||||
**Environment Variables (.env):**
|
||||
```bash
|
||||
FURT_MAIL_HOST=mail.example.com
|
||||
FURT_MAIL_PORT=587
|
||||
FURT_MAIL_USERNAME=user@example.com
|
||||
FURT_MAIL_PASSWORD=password
|
||||
FURT_MAIL_TO=empfaenger@example.com
|
||||
```
|
||||
|
||||
**Server-Config (config/server.lua):**
|
||||
- Port und Host-Einstellungen
|
||||
- API-Key-Konfiguration
|
||||
- Rate-Limiting-Parameter
|
||||
|
||||
## Testing
|
||||
|
||||
**Automatische Tests:**
|
||||
```bash
|
||||
lua tests/test_http.lua
|
||||
```
|
||||
|
||||
**Manuelle Tests:**
|
||||
```bash
|
||||
./scripts/test_curl.sh
|
||||
|
||||
# Oder direkt:
|
||||
curl -X POST http://127.0.0.1:8080/v1/mail/send \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name":"Test","email":"test@example.com","subject":"Test","message":"Test-Nachricht"}'
|
||||
-d '{"name":"Test","email":"test@example.com","message":"Test"}'
|
||||
```
|
||||
|
||||
## Dokumentation
|
||||
## Deployment
|
||||
|
||||
**Installation & Konfiguration:** [Furt Wiki](https://smida.dragons-at-work.de/DAW/furt/wiki)
|
||||
**OpenBSD:**
|
||||
- rc.d-Script in `deployment/openbsd/`
|
||||
- Systemd-Integration über Scripts
|
||||
|
||||
**Production-Setup:**
|
||||
```bash
|
||||
# Environment-Config kopieren
|
||||
cp .env.example .env.production
|
||||
# → SMTP-Credentials anpassen
|
||||
|
||||
# Production-Mode starten
|
||||
export FURT_ENV=production
|
||||
./scripts/start.sh
|
||||
```
|
||||
|
||||
## Projektstruktur
|
||||
|
||||
```
|
||||
furt/
|
||||
├── src/ # Lua-Source-Code
|
||||
├── config/ # Konfiguration
|
||||
├── scripts/ # Installation & Management
|
||||
└── deployment/ # System-Integration
|
||||
│ ├── main.lua # HTTP-Server
|
||||
│ ├── routes/ # API-Endpoints
|
||||
│ └── smtp.lua # Mail-Integration
|
||||
├── config/ # Konfiguration
|
||||
├── scripts/ # Start/Test-Scripts
|
||||
├── tests/ # Test-Suite
|
||||
└── deployment/ # System-Integration
|
||||
```
|
||||
|
||||
## Integration
|
||||
## Hugo-Integration
|
||||
|
||||
**merkwerk:** Versionierte Furt-Deployment über [merkwerk](https://smida.dragons-at-work.de/DAW/merkwerk)
|
||||
**Shortcode-Beispiel:**
|
||||
```html
|
||||
<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
|
||||
|
||||
- **Repository:** [Forgejo](https://smida.dragons-at-work.de/DAW/furt)
|
||||
- **Dokumentation:** [Wiki](https://smida.dragons-at-work.de/DAW/furt/wiki)
|
||||
- **Projekt:** [Dragons@Work](https://dragons-at-work.de)
|
||||
**Dependencies minimal halten:**
|
||||
- Nur lua-socket und lua-cjson
|
||||
- Keine externen HTTP-Libraries
|
||||
- Standard-Lua-Funktionen bevorzugen
|
||||
|
||||
|
|
|
|||
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
0.1.4
|
||||
0.1.1
|
||||
|
|
@ -1,33 +1,18 @@
|
|||
[Unit]
|
||||
Description=furt Multi-Tenant API Gateway (Security-Hardened)
|
||||
Description=furt Multi-Tenant API Gateway
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=forking
|
||||
User=furt
|
||||
Group=furt
|
||||
ExecStart=/usr/local/share/furt/scripts/start.sh
|
||||
PIDFile=/var/run/furt/furt.pid
|
||||
ExecStart=/usr/local/share/furt/scripts/start.sh start
|
||||
WorkingDirectory=/usr/local/share/furt
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
# === SECURITY HARDENING ===
|
||||
|
||||
# Filesystem Protection
|
||||
ProtectSystem=strict
|
||||
ReadWritePaths=/var/run/furt /var/log/furt
|
||||
ProtectHome=yes
|
||||
|
||||
# Process Hardening
|
||||
NoNewPrivileges=yes
|
||||
PrivateTmp=yes
|
||||
|
||||
# Network Restriction
|
||||
RestrictAddressFamilies=AF_INET
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
|
|
|
|||
|
|
@ -3,52 +3,11 @@
|
|||
daemon="/usr/local/share/furt/scripts/start.sh"
|
||||
daemon_user="_furt"
|
||||
daemon_cwd="/usr/local/share/furt"
|
||||
daemon_flags="start"
|
||||
|
||||
. /etc/rc.d/rc.subr
|
||||
|
||||
# PID-File location
|
||||
pidfile="/var/run/furt/furt.pid"
|
||||
|
||||
# Custom rc_check function (PID-File based)
|
||||
rc_check() {
|
||||
[ -f "$pidfile" ] && kill -0 $(cat "$pidfile") 2>/dev/null
|
||||
}
|
||||
|
||||
# Custom rc_stop function (PID-File based)
|
||||
rc_stop() {
|
||||
if [ -f "$pidfile" ]; then
|
||||
local _pid=$(cat "$pidfile")
|
||||
echo "Stopping furt (PID: $_pid)"
|
||||
kill "$_pid" 2>/dev/null
|
||||
# Wait for process to die
|
||||
local _timeout=10
|
||||
while [ $_timeout -gt 0 ] && kill -0 "$_pid" 2>/dev/null; do
|
||||
sleep 1
|
||||
_timeout=$((_timeout - 1))
|
||||
done
|
||||
# Force kill if still running
|
||||
if kill -0 "$_pid" 2>/dev/null; then
|
||||
echo "Force killing furt (PID: $_pid)"
|
||||
kill -9 "$_pid" 2>/dev/null
|
||||
fi
|
||||
rm -f "$pidfile"
|
||||
echo "furt stopped"
|
||||
else
|
||||
echo "furt not running (no PID-File)"
|
||||
fi
|
||||
}
|
||||
|
||||
# Custom rc_reload function (signal-based)
|
||||
rc_reload() {
|
||||
if rc_check; then
|
||||
local _pid=$(cat "$pidfile")
|
||||
echo "Reloading furt configuration (PID: $_pid)"
|
||||
kill -HUP "$_pid"
|
||||
else
|
||||
echo "furt not running"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
pexp="lua.*src/main.lua"
|
||||
|
||||
rc_cmd $1
|
||||
|
||||
|
|
|
|||
176
docs/setup-guide.md
Normal file
176
docs/setup-guide.md
Normal 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
|
||||
```
|
||||
|
|
@ -102,7 +102,7 @@ else
|
|||
echo ""
|
||||
echo "Next steps:"
|
||||
echo "1. Edit configuration file:"
|
||||
if [ "$(uname)" = "OpenBSD" ]; then
|
||||
if [ "$(uname)" = "OpenBSD" ] || [ "$(uname)" = "FreeBSD" ]; then
|
||||
echo " /usr/local/etc/furt/furt.conf"
|
||||
else
|
||||
echo " /etc/furt/furt.conf"
|
||||
|
|
|
|||
0
scripts/cleanup_debug.sh
Executable file → Normal file
0
scripts/cleanup_debug.sh
Executable file → Normal file
|
|
@ -15,25 +15,25 @@ if [ "$(uname)" = "OpenBSD" ]; then
|
|||
echo "Error: deployment/openbsd/rc.d-furt template not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
cp deployment/openbsd/rc.d-furt /etc/rc.d/furt
|
||||
chmod +x /etc/rc.d/furt
|
||||
echo "furt_flags=" >> /etc/rc.conf.local
|
||||
rcctl enable furt
|
||||
echo "OpenBSD service created and enabled using repository template"
|
||||
|
||||
|
||||
elif [ "$(uname)" = "Linux" ]; then
|
||||
# Use systemd template from repository
|
||||
if [ ! -f "deployment/linux/furt.service" ]; then
|
||||
echo "Error: deployment/linux/furt.service template not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
cp deployment/linux/furt.service /etc/systemd/system/
|
||||
systemctl daemon-reload
|
||||
systemctl enable furt
|
||||
echo "Linux systemd service created and enabled using repository template"
|
||||
|
||||
|
||||
else
|
||||
echo "Unsupported operating system for service creation"
|
||||
exit 1
|
||||
|
|
|
|||
4
scripts/manual_mail_test.sh
Executable file → Normal file
4
scripts/manual_mail_test.sh
Executable file → Normal file
|
|
@ -4,11 +4,11 @@
|
|||
echo "Testing SMTP with corrected JSON..."
|
||||
|
||||
# Simple test without timestamp embedding
|
||||
curl -X POST http://127.0.0.1:7811/v1/mail/send \
|
||||
curl -X POST http://127.0.0.1:8080/v1/mail/send \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "Furt Test User",
|
||||
"email": "admin@example.com",
|
||||
"email": "michael@dragons-at-work.de",
|
||||
"subject": "Furt SMTP Test Success!",
|
||||
"message": "This is a test email from Furt Lua HTTP-Server. SMTP Integration working!"
|
||||
}'
|
||||
|
|
|
|||
80
scripts/production_test_sequence.sh
Normal file
80
scripts/production_test_sequence.sh
Normal 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"
|
||||
|
|
@ -4,7 +4,7 @@
|
|||
set -e
|
||||
|
||||
# Detect operating system for config directory
|
||||
if [ "$(uname)" = "OpenBSD" ]; then
|
||||
if [ "$(uname)" = "OpenBSD" ] || [ "$(uname)" = "FreeBSD" ]; then
|
||||
CONFIG_DIR="/usr/local/etc/furt"
|
||||
USER="_furt"
|
||||
GROUP="_furt"
|
||||
|
|
@ -18,15 +18,12 @@ fi
|
|||
mkdir -p "$CONFIG_DIR"
|
||||
mkdir -p /usr/local/share/furt
|
||||
mkdir -p /var/log/furt
|
||||
mkdir -p /var/run/furt
|
||||
|
||||
# Set ownership for log directory (service user needs write access)
|
||||
chown "$USER:$GROUP" /var/log/furt
|
||||
chown "$USER:$GROUP" /var/run/furt
|
||||
|
||||
echo "Created directories:"
|
||||
echo " Config: $CONFIG_DIR"
|
||||
echo " Share: /usr/local/share/furt"
|
||||
echo " Logs: /var/log/furt (owned by $USER)"
|
||||
echo " PID: /var/run/furt (owned by $USER)"
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
set -e
|
||||
|
||||
# Detect operating system
|
||||
if [ "$(uname)" = "OpenBSD" ]; then
|
||||
if [ "$(uname)" = "OpenBSD" ] || [ "$(uname)" = "FreeBSD" ]; then
|
||||
# BSD systems use _furt user convention
|
||||
groupadd _furt 2>/dev/null || true
|
||||
useradd -g _furt -s /bin/false -d /var/empty _furt 2>/dev/null || true
|
||||
|
|
|
|||
101
scripts/setup_env.sh
Executable file
101
scripts/setup_env.sh
Executable 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}"
|
||||
|
||||
|
|
@ -17,12 +17,10 @@ echo -e "${GREEN}=== Furt Lua HTTP-Server Startup ===${NC}"
|
|||
LUA_COMMAND=""
|
||||
|
||||
# Config check first
|
||||
if [ "$(uname)" = "OpenBSD" ]; then
|
||||
if [ "$(uname)" = "OpenBSD" ] || [ "$(uname)" = "FreeBSD" ]; then
|
||||
CONFIG_FILE="/usr/local/etc/furt/furt.conf"
|
||||
PID_FILE="/var/run/furt/furt.pid"
|
||||
else
|
||||
CONFIG_FILE="/etc/furt/furt.conf"
|
||||
PID_FILE="/var/run/furt/furt.pid"
|
||||
fi
|
||||
|
||||
if [ ! -f "$CONFIG_FILE" ] && [ ! -f "$PROJECT_DIR/config/furt.conf" ]; then
|
||||
|
|
@ -89,38 +87,12 @@ cd "$PROJECT_DIR"
|
|||
|
||||
echo -e "${GREEN}Starting Furt...${NC}"
|
||||
|
||||
# PID-File cleanup function
|
||||
cleanup_pid() {
|
||||
if [ -f "$PID_FILE" ]; then
|
||||
rm -f "$PID_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
# Service vs Interactive Detection
|
||||
if [ ! -t 0 ] || [ ! -t 1 ]; then
|
||||
# Service mode - Background + PID-File
|
||||
echo -e "${GREEN}Service mode: Background + PID-File${NC}"
|
||||
|
||||
# Start process in background
|
||||
# Service mode - Background
|
||||
"$LUA_COMMAND" src/main.lua &
|
||||
PID=$!
|
||||
|
||||
# Write PID-File
|
||||
echo "$PID" > "$PID_FILE"
|
||||
echo -e "${GREEN}Furt started (PID: $PID, PID-File: $PID_FILE)${NC}"
|
||||
|
||||
# Verify process is still running after short delay
|
||||
sleep 1
|
||||
if ! kill -0 "$PID" 2>/dev/null; then
|
||||
echo -e "${RED}Error: Process died immediately${NC}"
|
||||
cleanup_pid
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}Service startup successful${NC}"
|
||||
else
|
||||
# Interactive mode - Foreground (no PID-File)
|
||||
echo -e "${GREEN}Interactive mode: Foreground${NC}"
|
||||
# Interactive mode - Foreground
|
||||
exec "$LUA_COMMAND" src/main.lua
|
||||
fi
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
BASE_URL="http://127.0.0.1:8080"
|
||||
# Use correct API keys that match current .env
|
||||
API_KEY="YOUR_API_KEY_HERE"
|
||||
API_KEY="hugo-dev-key-change-in-production"
|
||||
|
||||
echo "⚡ Furt API Stress Test"
|
||||
echo "======================"
|
||||
|
|
@ -20,9 +20,9 @@ for i in {1..20}; do
|
|||
response=$(curl -s -w "%{http_code}" \
|
||||
-H "X-API-Key: $API_KEY" \
|
||||
"$BASE_URL/v1/auth/status")
|
||||
|
||||
|
||||
status=$(echo "$response" | tail -c 4)
|
||||
|
||||
|
||||
if [ "$status" == "200" ]; then
|
||||
rate_limit_remaining=$(echo "$response" | head -n -1 | jq -r '.rate_limit_remaining // "N/A"' 2>/dev/null)
|
||||
echo "Request $i: ✅ 200 OK (Rate limit remaining: $rate_limit_remaining)"
|
||||
|
|
@ -33,7 +33,7 @@ for i in {1..20}; do
|
|||
else
|
||||
echo "Request $i: ❌ $status Error"
|
||||
fi
|
||||
|
||||
|
||||
# Small delay to prevent overwhelming
|
||||
sleep 0.1
|
||||
done
|
||||
|
|
@ -58,10 +58,10 @@ for i in {1..10}; do
|
|||
-H "X-API-Key: $API_KEY" \
|
||||
"$BASE_URL/health")
|
||||
local_end=$(date +%s.%N)
|
||||
|
||||
|
||||
status=$(echo "$response" | tail -c 4)
|
||||
duration=$(echo "$local_end - $local_start" | bc -l)
|
||||
|
||||
|
||||
echo "Concurrent $i: Status $status, Duration ${duration}s" > "$temp_dir/result_$i"
|
||||
} &
|
||||
done
|
||||
|
|
@ -85,18 +85,18 @@ mail_errors=0
|
|||
|
||||
for i in {1..5}; do
|
||||
start_time=$(date +%s.%N)
|
||||
|
||||
|
||||
response=$(curl -s -w "%{http_code}" \
|
||||
-H "X-API-Key: $API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"name\":\"Stress Test $i\",\"email\":\"test$i@example.com\",\"subject\":\"Performance Test\",\"message\":\"Load test message $i\"}" \
|
||||
"$BASE_URL/v1/mail/send")
|
||||
|
||||
|
||||
end_time=$(date +%s.%N)
|
||||
duration=$(echo "$end_time - $start_time" | bc -l)
|
||||
|
||||
|
||||
status=$(echo "$response" | tail -c 4)
|
||||
|
||||
|
||||
if [ "$status" == "200" ]; then
|
||||
echo "Mail $i: ✅ 200 OK (${duration}s)"
|
||||
((mail_success++))
|
||||
|
|
@ -104,7 +104,7 @@ for i in {1..5}; do
|
|||
echo "Mail $i: ❌ Status $status (${duration}s)"
|
||||
((mail_errors++))
|
||||
fi
|
||||
|
||||
|
||||
# Delay between mail sends to be nice to SMTP server
|
||||
sleep 1
|
||||
done
|
||||
|
|
@ -120,7 +120,7 @@ mixed_success=0
|
|||
|
||||
for i in {1..15}; do
|
||||
((mixed_total++))
|
||||
|
||||
|
||||
if [ $((i % 3)) -eq 0 ]; then
|
||||
# Every 3rd request: auth status
|
||||
endpoint="/v1/auth/status"
|
||||
|
|
@ -128,20 +128,20 @@ for i in {1..15}; do
|
|||
# Other requests: health check
|
||||
endpoint="/health"
|
||||
fi
|
||||
|
||||
|
||||
response=$(curl -s -w "%{http_code}" \
|
||||
-H "X-API-Key: $API_KEY" \
|
||||
"$BASE_URL$endpoint")
|
||||
|
||||
|
||||
status=$(echo "$response" | tail -c 4)
|
||||
|
||||
|
||||
if [ "$status" == "200" ]; then
|
||||
echo "Mixed $i ($endpoint): ✅ 200 OK"
|
||||
((mixed_success++))
|
||||
else
|
||||
echo "Mixed $i ($endpoint): ❌ $status"
|
||||
fi
|
||||
|
||||
|
||||
sleep 0.2
|
||||
done
|
||||
|
||||
|
|
|
|||
|
|
@ -24,13 +24,8 @@ cp -r integrations/ "$TARGET/"
|
|||
[ -f "VERSION" ] && cp VERSION "$TARGET/"
|
||||
[ -f ".version_history" ] && cp .version_history "$TARGET/"
|
||||
|
||||
# Set proper permissions based on operating system
|
||||
if [ "$(uname)" = "OpenBSD" ]; then
|
||||
chown -R root:wheel "$TARGET"
|
||||
else
|
||||
chown -R root:root "$TARGET"
|
||||
fi
|
||||
|
||||
# Set proper permissions
|
||||
chown -R root:wheel "$TARGET" 2>/dev/null || chown -R root:root "$TARGET"
|
||||
chmod -R 644 "$TARGET"
|
||||
find "$TARGET" -type d -exec chmod 755 {} \;
|
||||
chmod +x "$TARGET/scripts/start.sh"
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@
|
|||
# Test API-Key-Authentifizierung (ohne jq parse errors)
|
||||
|
||||
BASE_URL="http://127.0.0.1:8080"
|
||||
HUGO_API_KEY="YOUR_API_KEY_HERE"
|
||||
ADMIN_API_KEY="YOUR_ADMIN_KEY_HERE"
|
||||
HUGO_API_KEY="hugo-dev-key-change-in-production"
|
||||
ADMIN_API_KEY="admin-dev-key-change-in-production"
|
||||
INVALID_API_KEY="invalid-key-should-fail"
|
||||
|
||||
echo "🔐 Testing Furt API-Key Authentication"
|
||||
|
|
@ -16,24 +16,24 @@ make_request() {
|
|||
local url="$2"
|
||||
local headers="$3"
|
||||
local data="$4"
|
||||
|
||||
|
||||
echo "Request: $method $url"
|
||||
if [ -n "$headers" ]; then
|
||||
echo "Headers: $headers"
|
||||
fi
|
||||
|
||||
|
||||
local response=$(curl -s $method \
|
||||
${headers:+-H "$headers"} \
|
||||
${data:+-d "$data"} \
|
||||
-H "Content-Type: application/json" \
|
||||
"$url")
|
||||
|
||||
|
||||
local status=$(curl -s -o /dev/null -w "%{http_code}" $method \
|
||||
${headers:+-H "$headers"} \
|
||||
${data:+-d "$data"} \
|
||||
-H "Content-Type: application/json" \
|
||||
"$url")
|
||||
|
||||
|
||||
echo "Status: $status"
|
||||
echo "Response: $response" | jq '.' 2>/dev/null || echo "$response"
|
||||
echo ""
|
||||
|
|
|
|||
2
scripts/test_modular.sh
Executable file → Normal file
2
scripts/test_modular.sh
Executable file → Normal file
|
|
@ -3,7 +3,7 @@
|
|||
# Test der modularen Furt-Architektur
|
||||
|
||||
BASE_URL="http://127.0.0.1:8080"
|
||||
HUGO_API_KEY="YOUR_API_KEY_HERE"
|
||||
HUGO_API_KEY="hugo-dev-key-change-in-production"
|
||||
|
||||
echo "🧩 Testing Modular Furt Architecture"
|
||||
echo "===================================="
|
||||
|
|
|
|||
20
scripts/test_smtp.sh
Executable file → Normal file
20
scripts/test_smtp.sh
Executable file → Normal file
|
|
@ -36,7 +36,7 @@ else
|
|||
echo "[ERROR] Validation failed"
|
||||
fi
|
||||
|
||||
# Test 3: Invalid Email Format
|
||||
# Test 3: Invalid Email Format
|
||||
echo ""
|
||||
echo "[3] Testing email validation..."
|
||||
email_validation_response=$(curl -s -X POST "$SERVER_URL/v1/mail/send" \
|
||||
|
|
@ -54,36 +54,36 @@ fi
|
|||
# Test 4: Valid Mail Request (REAL SMTP TEST)
|
||||
echo ""
|
||||
echo "[4] Testing REAL mail sending..."
|
||||
echo "WARNING: This will send a real email to admin@example.com"
|
||||
echo "WARNING: This will send a real email to michael@dragons-at-work.de"
|
||||
read -p "Continue with real mail test? (y/N): " -n 1 -r
|
||||
echo
|
||||
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo "Sending real test email..."
|
||||
|
||||
|
||||
mail_response=$(curl -s -X POST "$SERVER_URL/v1/mail/send" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "Furt Test User",
|
||||
"email": "test@example.com",
|
||||
"email": "test@dragons-at-work.de",
|
||||
"subject": "Furt SMTP Test - Week 2 Success!",
|
||||
"message": "This is a test email from the Furt Lua HTTP-Server.\n\nSMTP Integration is working!\n\nTimestamp: '$(date)'\nServer: furt-lua v1.0"
|
||||
}')
|
||||
|
||||
|
||||
echo "Response: $mail_response"
|
||||
|
||||
|
||||
# Check for success
|
||||
if echo "$mail_response" | grep -q '"success":true'; then
|
||||
echo "[OK] MAIL SENT SUCCESSFULLY!"
|
||||
echo "Check admin@example.com inbox"
|
||||
|
||||
echo "Check michael@dragons-at-work.de inbox"
|
||||
|
||||
# Extract request ID
|
||||
request_id=$(echo "$mail_response" | grep -o '"request_id":"[^"]*"' | cut -d'"' -f4)
|
||||
echo "Request ID: $request_id"
|
||||
else
|
||||
echo "[ERROR] Mail sending failed"
|
||||
echo "Check server logs and SMTP credentials"
|
||||
|
||||
|
||||
# Show error details
|
||||
if echo "$mail_response" | grep -q "error"; then
|
||||
error_msg=$(echo "$mail_response" | grep -o '"error":"[^"]*"' | cut -d'"' -f4)
|
||||
|
|
@ -126,7 +126,7 @@ echo "Performance: ${duration_ms}ms"
|
|||
echo ""
|
||||
echo "Week 2 Challenge Status:"
|
||||
echo " SMTP Integration: COMPLETE"
|
||||
echo " Environment Variables: CHECK .env"
|
||||
echo " Environment Variables: CHECK .env"
|
||||
echo " Native Lua Implementation: DONE"
|
||||
echo " Production Ready: READY FOR TESTING"
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
set -e
|
||||
|
||||
# Detect config file location
|
||||
if [ "$(uname)" = "OpenBSD" ]; then
|
||||
if [ "$(uname)" = "OpenBSD" ] || [ "$(uname)" = "FreeBSD" ]; then
|
||||
CONFIG_FILE="/usr/local/etc/furt/furt.conf"
|
||||
else
|
||||
CONFIG_FILE="/etc/furt/furt.conf"
|
||||
|
|
@ -24,13 +24,12 @@ if ! grep -q '^\[server\]' "$CONFIG_FILE"; then
|
|||
exit 1
|
||||
fi
|
||||
|
||||
# Fix: Use POSIX-compatible regex patterns
|
||||
if ! grep -q '^[ \t]*port[ \t]*=' "$CONFIG_FILE"; then
|
||||
if ! grep -q '^port\s*=' "$CONFIG_FILE"; then
|
||||
echo "Error: server port not configured"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! grep -q '^[ \t]*host[ \t]*=' "$CONFIG_FILE"; then
|
||||
if ! grep -q '^host\s*=' "$CONFIG_FILE"; then
|
||||
echo "Error: server host not configured"
|
||||
exit 1
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -215,7 +215,7 @@ end
|
|||
function ConfigParser.load_config()
|
||||
-- Try different locations based on OS
|
||||
local config_paths = {
|
||||
"/usr/local/etc/furt/furt.conf", -- OpenBSD
|
||||
"/usr/local/etc/furt/furt.conf", -- OpenBSD/FreeBSD
|
||||
"/etc/furt/furt.conf", -- Linux
|
||||
"config/furt.conf", -- Development
|
||||
"furt.conf" -- Current directory
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
-- src/http_server.lua
|
||||
-- HTTP Server Core for Furt API-Gateway
|
||||
-- HTTP Server Core for Furt API-Gateway with Structured Logging
|
||||
-- Dragons@Work Digital Sovereignty Project
|
||||
|
||||
local socket = require("socket")
|
||||
|
|
@ -10,6 +10,7 @@ end
|
|||
|
||||
local config = require("config.server")
|
||||
local Auth = require("src.auth")
|
||||
local Logger = require("src.logger")
|
||||
|
||||
-- HTTP-Server Module
|
||||
local FurtServer = {}
|
||||
|
|
@ -32,11 +33,22 @@ function FurtServer:add_route(method, path, handler)
|
|||
self.routes[method] = {}
|
||||
end
|
||||
self.routes[method][path] = handler
|
||||
|
||||
Logger.debug("Route registered", {
|
||||
method = method,
|
||||
path = path
|
||||
})
|
||||
end
|
||||
|
||||
-- Add protected route (requires authentication)
|
||||
function FurtServer:add_protected_route(method, path, required_permission, handler)
|
||||
self:add_route(method, path, Auth.create_protected_route(required_permission, handler))
|
||||
|
||||
Logger.debug("Protected route registered", {
|
||||
method = method,
|
||||
path = path,
|
||||
permission = required_permission
|
||||
})
|
||||
end
|
||||
|
||||
-- Parse HTTP request
|
||||
|
|
@ -49,6 +61,7 @@ function FurtServer:parse_request(client)
|
|||
-- Parse request line: "POST /v1/mail/send HTTP/1.1"
|
||||
local method, path, protocol = request_line:match("(%w+) (%S+) (%S+)")
|
||||
if not method then
|
||||
Logger.warn("Invalid request line", { request_line = request_line })
|
||||
return nil
|
||||
end
|
||||
|
||||
|
|
@ -91,7 +104,9 @@ end
|
|||
function FurtServer:add_cors_headers(request)
|
||||
local allowed_origins = config.cors and config.cors.allowed_origins or {
|
||||
"http://localhost:1313",
|
||||
"http://127.0.0.1:1313"
|
||||
"http://127.0.0.1:1313",
|
||||
"https://dragons-at-work.de",
|
||||
"https://www.dragons-at-work.de"
|
||||
}
|
||||
|
||||
-- Check if request has Origin header
|
||||
|
|
@ -172,22 +187,62 @@ function FurtServer:get_status_text(status)
|
|||
return status_texts[status] or "Unknown"
|
||||
end
|
||||
|
||||
-- Get client IP address with X-Forwarded-For support
|
||||
function FurtServer:get_client_ip(client, headers)
|
||||
-- Check for X-Forwarded-For header (proxy support)
|
||||
local forwarded_for = headers["x-forwarded-for"]
|
||||
if forwarded_for then
|
||||
-- Take first IP in case of multiple proxies
|
||||
local first_ip = forwarded_for:match("([^,]+)")
|
||||
if first_ip then
|
||||
return first_ip:match("^%s*(.-)%s*$") -- trim whitespace
|
||||
end
|
||||
end
|
||||
|
||||
-- Check for X-Real-IP header
|
||||
local real_ip = headers["x-real-ip"]
|
||||
if real_ip then
|
||||
return real_ip
|
||||
end
|
||||
|
||||
-- Fallback to direct connection
|
||||
local peer_ip = client:getpeername()
|
||||
return peer_ip or "unknown"
|
||||
end
|
||||
|
||||
-- Handle client request
|
||||
function FurtServer:handle_client(client)
|
||||
-- Generate request ID and start timing
|
||||
local request_id = Logger.generate_request_id()
|
||||
local start_time = socket.gettime()
|
||||
|
||||
Logger.debug("Request started", {
|
||||
request_id = request_id
|
||||
})
|
||||
|
||||
local request = self:parse_request(client)
|
||||
if not request then
|
||||
local duration_ms = math.floor((socket.gettime() - start_time) * 1000)
|
||||
local response = self:create_response(400, {error = "Invalid request"}, nil, nil, nil)
|
||||
client:send(response)
|
||||
|
||||
Logger.log_request("INVALID", "unknown", 400, duration_ms, "unknown", request_id)
|
||||
return
|
||||
end
|
||||
|
||||
print(string.format("[%s] %s %s", os.date("%Y-%m-%d %H:%M:%S"),
|
||||
request.method, request.path))
|
||||
-- Get client IP
|
||||
local client_ip = self:get_client_ip(client, request.headers)
|
||||
|
||||
-- Add request_id to request context for handlers
|
||||
request.request_id = request_id
|
||||
|
||||
-- Handle OPTIONS preflight requests (CORS)
|
||||
if request.method == "OPTIONS" then
|
||||
local duration_ms = math.floor((socket.gettime() - start_time) * 1000)
|
||||
local response = self:create_response(204, "", "text/plain", nil, request)
|
||||
client:send(response)
|
||||
|
||||
Logger.log_request("OPTIONS", request.path, 204, duration_ms, client_ip, request_id)
|
||||
return
|
||||
end
|
||||
|
||||
|
|
@ -197,49 +252,78 @@ function FurtServer:handle_client(client)
|
|||
handler = self.routes[request.method][request.path]
|
||||
end
|
||||
|
||||
local status = 404
|
||||
if handler then
|
||||
local success, result = pcall(handler, request, self)
|
||||
if success then
|
||||
client:send(result)
|
||||
-- Extract status from response (rough parsing)
|
||||
status = tonumber(result:match("HTTP/1%.1 (%d+)")) or 200
|
||||
else
|
||||
print("Handler error: " .. tostring(result))
|
||||
local error_response = self:create_response(500, {error = "Internal server error"}, nil, nil, request)
|
||||
Logger.log_error("Handler error", {
|
||||
request_id = request_id,
|
||||
method = request.method,
|
||||
path = request.path,
|
||||
client_ip = client_ip
|
||||
}, tostring(result))
|
||||
|
||||
status = 500
|
||||
local error_response = self:create_response(500, {
|
||||
error = "Internal server error",
|
||||
request_id = request_id
|
||||
}, nil, nil, request)
|
||||
client:send(error_response)
|
||||
end
|
||||
else
|
||||
print("Route not found: " .. request.method .. " " .. request.path)
|
||||
local response = self:create_response(404, {error = "Route not found", method = request.method, path = request.path}, nil, nil, request)
|
||||
Logger.debug("Route not found", {
|
||||
request_id = request_id,
|
||||
method = request.method,
|
||||
path = request.path
|
||||
})
|
||||
|
||||
local response = self:create_response(404, {
|
||||
error = "Route not found",
|
||||
method = request.method,
|
||||
path = request.path,
|
||||
request_id = request_id
|
||||
}, nil, nil, request)
|
||||
client:send(response)
|
||||
end
|
||||
|
||||
-- Log completed request with performance metrics
|
||||
local duration_ms = math.floor((socket.gettime() - start_time) * 1000)
|
||||
Logger.log_request(request.method, request.path, status, duration_ms, client_ip, request_id)
|
||||
end
|
||||
|
||||
-- Start HTTP server
|
||||
function FurtServer:start()
|
||||
self.server = socket.bind(self.host, self.port)
|
||||
if not self.server then
|
||||
Logger.error("Failed to bind server", {
|
||||
host = self.host,
|
||||
port = self.port
|
||||
})
|
||||
error("Failed to bind to " .. self.host .. ":" .. self.port)
|
||||
end
|
||||
|
||||
local HealthRoute = require("src.routes.health")
|
||||
local version_info = HealthRoute.get_version_info()
|
||||
|
||||
print(string.format("Furt HTTP-Server started on %s:%d", self.host, self.port))
|
||||
print("Version: " .. version_info.version .. " (merkwerk)")
|
||||
print("Content-Hash: " .. (version_info.content_hash or "unknown"))
|
||||
print("VCS: " .. (version_info.vcs_info and version_info.vcs_info.hash or "none"))
|
||||
print("API-Key authentication: ENABLED")
|
||||
-- Structured startup logging
|
||||
Logger.log_startup(self.host, self.port, version_info)
|
||||
|
||||
-- Show actual configured rate limits
|
||||
-- Log configuration details
|
||||
local rate_limits = config.security and config.security.rate_limits
|
||||
if rate_limits then
|
||||
print(string.format("Rate limiting: ENABLED (%d req/hour per API key, %d req/hour per IP)",
|
||||
rate_limits.api_key_max, rate_limits.ip_max))
|
||||
else
|
||||
print("Rate limiting: ENABLED (default values)")
|
||||
end
|
||||
Logger.log_config_summary({
|
||||
cors_origins_count = #config.cors.allowed_origins,
|
||||
rate_limiting_enabled = rate_limits ~= nil,
|
||||
api_key_max = rate_limits and rate_limits.api_key_max,
|
||||
ip_max = rate_limits and rate_limits.ip_max,
|
||||
test_endpoint_enabled = config.security and config.security.enable_test_endpoint,
|
||||
log_level = Logger.get_log_level()
|
||||
})
|
||||
|
||||
print("CORS enabled for " .. (#config.cors.allowed_origins) .. " configured origins")
|
||||
print("Press Ctrl+C to stop")
|
||||
Logger.info("Furt server ready - Press Ctrl+C to stop")
|
||||
|
||||
while true do
|
||||
local client = self.server:accept()
|
||||
|
|
|
|||
200
src/logger.lua
Normal file
200
src/logger.lua
Normal 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
|
||||
|
||||
127
src/main.lua
127
src/main.lua
|
|
@ -1,34 +1,137 @@
|
|||
-- src/main.lua
|
||||
-- Furt API-Gateway - Application Entry Point
|
||||
-- Furt API-Gateway - Application Entry Point with Structured Logging
|
||||
-- Dragons@Work Digital Sovereignty Project
|
||||
|
||||
-- Load HTTP Server Core
|
||||
local FurtServer = require("src.http_server")
|
||||
-- Load Logger first for startup logging
|
||||
local Logger = require("src.logger")
|
||||
|
||||
-- Load Route Modules
|
||||
local MailRoute = require("src.routes.mail")
|
||||
local AuthRoute = require("src.routes.auth")
|
||||
local HealthRoute = require("src.routes.health")
|
||||
Logger.info("Furt API-Gateway starting up", {
|
||||
startup_phase = "module_loading"
|
||||
})
|
||||
|
||||
-- Load configuration
|
||||
local config = require("config.server")
|
||||
-- Load modules with error handling
|
||||
local function safe_require(module_name, description)
|
||||
local success, module = pcall(require, module_name)
|
||||
if not success then
|
||||
Logger.error("Failed to load module", {
|
||||
module = module_name,
|
||||
description = description,
|
||||
error = module
|
||||
})
|
||||
os.exit(1)
|
||||
end
|
||||
|
||||
Logger.debug("Module loaded successfully", {
|
||||
module = module_name,
|
||||
description = description
|
||||
})
|
||||
|
||||
return module
|
||||
end
|
||||
|
||||
-- Load core modules
|
||||
local FurtServer = safe_require("src.http_server", "HTTP Server Core")
|
||||
local MailRoute = safe_require("src.routes.mail", "Mail Route Handler")
|
||||
local AuthRoute = safe_require("src.routes.auth", "Auth Route Handler")
|
||||
local HealthRoute = safe_require("src.routes.health", "Health Route Handler")
|
||||
local config = safe_require("config.server", "Server Configuration")
|
||||
|
||||
Logger.info("All modules loaded successfully", {
|
||||
startup_phase = "modules_ready"
|
||||
})
|
||||
|
||||
-- Initialize server
|
||||
Logger.info("Initializing HTTP server", {
|
||||
startup_phase = "server_init"
|
||||
})
|
||||
|
||||
local server = FurtServer:new()
|
||||
|
||||
-- Route registration with logging
|
||||
local routes_registered = 0
|
||||
|
||||
-- Register public routes (no authentication required)
|
||||
server:add_route("GET", "/health", HealthRoute.handle_health)
|
||||
routes_registered = routes_registered + 1
|
||||
|
||||
-- Test endpoint for development (configurable via furt.conf)
|
||||
if config.security and config.security.enable_test_endpoint then
|
||||
server:add_route("POST", "/test", HealthRoute.handle_test)
|
||||
print("[WARN] Test endpoint enabled via configuration")
|
||||
routes_registered = routes_registered + 1
|
||||
|
||||
Logger.warn("Development test endpoint enabled", {
|
||||
endpoint = "POST /test",
|
||||
security_note = "Should be disabled in production"
|
||||
})
|
||||
end
|
||||
|
||||
-- Register protected routes (require authentication)
|
||||
server:add_protected_route("POST", "/v1/mail/send", "mail:send", MailRoute.handle_mail_send)
|
||||
server:add_protected_route("GET", "/v1/auth/status", nil, AuthRoute.handle_auth_status)
|
||||
routes_registered = routes_registered + 2
|
||||
|
||||
-- Start server
|
||||
server:start()
|
||||
Logger.info("Route registration completed", {
|
||||
startup_phase = "routes_registered",
|
||||
total_routes = routes_registered,
|
||||
public_routes = config.security and config.security.enable_test_endpoint and 2 or 1,
|
||||
protected_routes = 2
|
||||
})
|
||||
|
||||
-- Pre-flight system checks
|
||||
Logger.info("Performing pre-flight checks", {
|
||||
startup_phase = "pre_flight_checks"
|
||||
})
|
||||
|
||||
-- Check SMTP configuration
|
||||
local smtp_configured = config.mail and config.mail.host ~= nil
|
||||
Logger.info("SMTP configuration check", {
|
||||
smtp_configured = smtp_configured,
|
||||
default_smtp_host = config.mail and config.mail.host or "not configured"
|
||||
})
|
||||
|
||||
-- Check API keys
|
||||
local api_key_count = 0
|
||||
if config.api_keys then
|
||||
for _ in pairs(config.api_keys) do
|
||||
api_key_count = api_key_count + 1
|
||||
end
|
||||
end
|
||||
|
||||
Logger.info("API key configuration check", {
|
||||
api_key_count = api_key_count,
|
||||
keys_configured = api_key_count > 0
|
||||
})
|
||||
|
||||
if api_key_count == 0 then
|
||||
Logger.warn("No API keys configured", {
|
||||
warning = "Server will not accept authenticated requests",
|
||||
check_config = "Verify furt.conf API key sections"
|
||||
})
|
||||
end
|
||||
|
||||
-- Start server with error handling
|
||||
Logger.info("Starting HTTP server", {
|
||||
startup_phase = "server_start",
|
||||
host = config.host,
|
||||
port = config.port
|
||||
})
|
||||
|
||||
local success, error_msg = pcall(function()
|
||||
server:start()
|
||||
end)
|
||||
|
||||
if not success then
|
||||
Logger.error("Server startup failed", {
|
||||
error = error_msg,
|
||||
host = config.host,
|
||||
port = config.port,
|
||||
suggestion = "Check if port is already in use or permissions"
|
||||
})
|
||||
os.exit(1)
|
||||
end
|
||||
|
||||
-- This should never be reached since server:start() blocks
|
||||
Logger.error("Server stopped unexpectedly", {
|
||||
unexpected_exit = true
|
||||
})
|
||||
|
||||
|
|
|
|||
120
src/smtp.lua
120
src/smtp.lua
|
|
@ -1,6 +1,6 @@
|
|||
-- src/smtp.lua
|
||||
-- furt-lua/src/smtp.lua
|
||||
-- Universal SMTP implementation with SSL compatibility
|
||||
-- Supports both luaossl (Arch) and luasec (OpenBSD)
|
||||
-- Supports both luaossl (Arch/karl) and luasec (OpenBSD/walter)
|
||||
-- Dragons@Work Digital Sovereignty Project
|
||||
|
||||
local socket = require("socket")
|
||||
|
|
@ -19,7 +19,7 @@ function SSLCompat:detect_ssl_library()
|
|||
return "luaossl", ssl_lib
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- Try luasec
|
||||
local success, ssl_lib = pcall(require, "ssl")
|
||||
if success and ssl_lib then
|
||||
|
|
@ -28,23 +28,23 @@ function SSLCompat:detect_ssl_library()
|
|||
return "luasec", ssl_lib
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
return nil, "No compatible SSL library found (tried luaossl, luasec)"
|
||||
end
|
||||
|
||||
function SSLCompat:wrap_socket(sock, options)
|
||||
local ssl_type, ssl_lib = self:detect_ssl_library()
|
||||
|
||||
|
||||
if not ssl_type then
|
||||
return nil, ssl_lib -- ssl_lib contains error message
|
||||
end
|
||||
|
||||
|
||||
if ssl_type == "luaossl" then
|
||||
return self:wrap_luaossl(sock, options, ssl_lib)
|
||||
elseif ssl_type == "luasec" then
|
||||
return self:wrap_luasec(sock, options, ssl_lib)
|
||||
end
|
||||
|
||||
|
||||
return nil, "Unknown SSL library type: " .. ssl_type
|
||||
end
|
||||
|
||||
|
|
@ -55,18 +55,18 @@ function SSLCompat:wrap_luaossl(sock, options, ssl_lib)
|
|||
protocol = "tlsv1_2",
|
||||
verify = "none" -- For self-signed certs
|
||||
})
|
||||
|
||||
|
||||
if not ssl_sock then
|
||||
return nil, "luaossl wrap failed: " .. (err or "unknown error")
|
||||
end
|
||||
|
||||
|
||||
-- luaossl typically does handshake automatically, but explicit is safer
|
||||
local success, err = pcall(function() return ssl_sock:dohandshake() end)
|
||||
if not success then
|
||||
-- Some luaossl versions don't need explicit handshake
|
||||
-- Continue if dohandshake doesn't exist
|
||||
end
|
||||
|
||||
|
||||
return ssl_sock, nil
|
||||
end
|
||||
|
||||
|
|
@ -78,28 +78,28 @@ function SSLCompat:wrap_luasec(sock, options, ssl_lib)
|
|||
verify = "none",
|
||||
options = "all"
|
||||
})
|
||||
|
||||
|
||||
if not ssl_sock then
|
||||
return nil, "luasec wrap failed: " .. (err or "unknown error")
|
||||
end
|
||||
|
||||
|
||||
-- luasec requires explicit handshake
|
||||
local success, err = ssl_sock:dohandshake()
|
||||
if not success then
|
||||
return nil, "luasec handshake failed: " .. (err or "unknown error")
|
||||
end
|
||||
|
||||
|
||||
return ssl_sock, nil
|
||||
end
|
||||
|
||||
-- Create SMTP instance
|
||||
function SMTP:new(config)
|
||||
local instance = {
|
||||
server = config.smtp_server,
|
||||
port = config.smtp_port,
|
||||
server = config.smtp_server or "mail.dragons-at-work.de",
|
||||
port = config.smtp_port or 465,
|
||||
username = config.username,
|
||||
password = config.password,
|
||||
from_address = config.from_address,
|
||||
from_address = config.from_address or "noreply@dragons-at-work.de",
|
||||
use_ssl = config.use_ssl or true,
|
||||
debug = config.debug or false,
|
||||
ssl_compat = SSLCompat
|
||||
|
|
@ -133,7 +133,7 @@ function SMTP:send_command(sock, command, expected_code)
|
|||
if self.debug then
|
||||
print("SMTP CMD: " .. (command or ""):gsub("\r\n", "\\r\\n"))
|
||||
end
|
||||
|
||||
|
||||
-- Only send if command is not nil (for server greeting, command is nil)
|
||||
if command then
|
||||
local success, err = sock:send(command .. "\r\n")
|
||||
|
|
@ -141,16 +141,16 @@ function SMTP:send_command(sock, command, expected_code)
|
|||
return false, "Failed to send command: " .. (err or "unknown error")
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local response, err = sock:receive()
|
||||
if not response then
|
||||
return false, "Failed to receive response: " .. (err or "unknown error")
|
||||
end
|
||||
|
||||
|
||||
if self.debug then
|
||||
print("SMTP RSP: " .. response)
|
||||
end
|
||||
|
||||
|
||||
-- Handle multi-line responses (like EHLO)
|
||||
local full_response = response
|
||||
while response:match("^%d%d%d%-") do
|
||||
|
|
@ -163,12 +163,12 @@ function SMTP:send_command(sock, command, expected_code)
|
|||
end
|
||||
full_response = full_response .. "\n" .. response
|
||||
end
|
||||
|
||||
|
||||
local code = response:match("^(%d+)")
|
||||
if expected_code and code ~= tostring(expected_code) then
|
||||
return false, "Unexpected response: " .. full_response
|
||||
end
|
||||
|
||||
|
||||
return true, full_response
|
||||
end
|
||||
|
||||
|
|
@ -179,38 +179,38 @@ function SMTP:connect()
|
|||
if not sock then
|
||||
return false, "Failed to create socket: " .. (err or "unknown error")
|
||||
end
|
||||
|
||||
|
||||
-- Set timeout
|
||||
sock:settimeout(30)
|
||||
|
||||
|
||||
-- Connect to server
|
||||
local success, err = sock:connect(self.server, self.port)
|
||||
if not success then
|
||||
return false, "Failed to connect to " .. self.server .. ":" .. self.port .. " - " .. (err or "unknown error")
|
||||
end
|
||||
|
||||
|
||||
-- Wrap with SSL for port 465 using compatibility layer
|
||||
if self.use_ssl and self.port == 465 then
|
||||
local ssl_sock, err = self.ssl_compat:wrap_socket(sock, {
|
||||
mode = "client",
|
||||
protocol = "tlsv1_2"
|
||||
})
|
||||
|
||||
|
||||
if not ssl_sock then
|
||||
sock:close()
|
||||
return false, "Failed to establish SSL connection: " .. (err or "unknown error")
|
||||
end
|
||||
|
||||
|
||||
sock = ssl_sock
|
||||
end
|
||||
|
||||
|
||||
-- Read server greeting
|
||||
local success, response = self:send_command(sock, nil, 220)
|
||||
if not success then
|
||||
sock:close()
|
||||
return false, "SMTP server greeting failed: " .. response
|
||||
end
|
||||
|
||||
|
||||
return sock, nil
|
||||
end
|
||||
|
||||
|
|
@ -219,96 +219,64 @@ function SMTP:send_email(to_address, subject, message, from_name)
|
|||
if not self.username or not self.password then
|
||||
return false, "SMTP username or password not configured"
|
||||
end
|
||||
|
||||
|
||||
-- Connect to server
|
||||
local sock, err = self:connect()
|
||||
if not sock then
|
||||
return false, err
|
||||
end
|
||||
|
||||
|
||||
local function cleanup_and_fail(error_msg)
|
||||
sock:close()
|
||||
return false, error_msg
|
||||
end
|
||||
|
||||
|
||||
-- EHLO command
|
||||
local success, response = self:send_command(sock, "EHLO furt-lua", 250)
|
||||
if not success then
|
||||
return cleanup_and_fail("EHLO failed: " .. response)
|
||||
end
|
||||
|
||||
-- STARTTLS hinzufügen für Port 587
|
||||
if self.port == 587 and self.use_ssl then
|
||||
-- STARTTLS command
|
||||
local success, response = self:send_command(sock, "STARTTLS", 220)
|
||||
if not success then
|
||||
return cleanup_and_fail("STARTTLS failed: " .. response)
|
||||
end
|
||||
|
||||
-- Upgrade connection to SSL
|
||||
local ssl_sock, err = self.ssl_compat:wrap_socket(sock, {
|
||||
mode = "client",
|
||||
protocol = "tlsv1_2"
|
||||
})
|
||||
|
||||
if not ssl_sock then
|
||||
return cleanup_and_fail("SSL upgrade failed: " .. err)
|
||||
end
|
||||
|
||||
sock = ssl_sock
|
||||
|
||||
-- EHLO again over encrypted connection
|
||||
local success, response = self:send_command(sock, "EHLO furt-lua", 250)
|
||||
if not success then
|
||||
return cleanup_and_fail("EHLO after STARTTLS failed: " .. response)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- AUTH LOGIN
|
||||
local success, response = self:send_command(sock, "AUTH LOGIN", 334)
|
||||
if not success then
|
||||
return cleanup_and_fail("AUTH LOGIN failed: " .. response)
|
||||
end
|
||||
|
||||
|
||||
-- Send username (base64 encoded)
|
||||
local username_b64 = self:base64_encode(self.username)
|
||||
local success, response = self:send_command(sock, username_b64, 334)
|
||||
if not success then
|
||||
return cleanup_and_fail("Username authentication failed: " .. response)
|
||||
end
|
||||
|
||||
|
||||
-- Send password (base64 encoded)
|
||||
local password_b64 = self:base64_encode(self.password)
|
||||
local success, response = self:send_command(sock, password_b64, 235)
|
||||
if not success then
|
||||
return cleanup_and_fail("Password authentication failed: " .. response)
|
||||
end
|
||||
|
||||
|
||||
-- MAIL FROM
|
||||
local mail_from = "MAIL FROM:<" .. self.from_address .. ">"
|
||||
local success, response = self:send_command(sock, mail_from, 250)
|
||||
if not success then
|
||||
return cleanup_and_fail("MAIL FROM failed: " .. response)
|
||||
end
|
||||
|
||||
|
||||
-- RCPT TO
|
||||
local rcpt_to = "RCPT TO:<" .. to_address .. ">"
|
||||
local success, response = self:send_command(sock, rcpt_to, 250)
|
||||
if not success then
|
||||
return cleanup_and_fail("RCPT TO failed: " .. response)
|
||||
end
|
||||
|
||||
|
||||
-- DATA command
|
||||
local success, response = self:send_command(sock, "DATA", 354)
|
||||
if not success then
|
||||
return cleanup_and_fail("DATA command failed: " .. response)
|
||||
end
|
||||
|
||||
-- Generate unique Message-ID
|
||||
-- Extract domain from configured from_address
|
||||
local hostname = self.from_address:match("@(.+)") or self.server
|
||||
local message_id = string.format("<%d.%d@%s>", os.time(), math.random(10000), hostname)
|
||||
|
||||
|
||||
-- Build email message
|
||||
local display_name = from_name or "Furt Contact Form"
|
||||
local email_content = string.format(
|
||||
|
|
@ -316,10 +284,7 @@ function SMTP:send_email(to_address, subject, message, from_name)
|
|||
"To: <%s>\r\n" ..
|
||||
"Subject: %s\r\n" ..
|
||||
"Date: %s\r\n" ..
|
||||
"Message-ID: %s\r\n" ..
|
||||
"MIME-Version: 1.0\r\n" ..
|
||||
"Content-Type: text/plain; charset=UTF-8\r\n" ..
|
||||
"Content-Transfer-Encoding: 8bit\r\n" ..
|
||||
"\r\n" ..
|
||||
"%s\r\n" ..
|
||||
".",
|
||||
|
|
@ -328,20 +293,19 @@ function SMTP:send_email(to_address, subject, message, from_name)
|
|||
to_address,
|
||||
subject,
|
||||
os.date("%a, %d %b %Y %H:%M:%S %z"),
|
||||
message_id,
|
||||
message
|
||||
)
|
||||
|
||||
|
||||
-- Send email content
|
||||
local success, response = self:send_command(sock, email_content, 250)
|
||||
if not success then
|
||||
return cleanup_and_fail("Email sending failed: " .. response)
|
||||
end
|
||||
|
||||
|
||||
-- QUIT
|
||||
self:send_command(sock, "QUIT", 221)
|
||||
sock:close()
|
||||
|
||||
|
||||
return true, "Email sent successfully"
|
||||
end
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue