Compare commits

...
Sign in to create a new pull request.

26 commits

Author SHA1 Message Date
83e267a608 chore: merkwerk auto-update 2025-09-10 20:04:19 +02:00
f684ea1b4c bump version to 0.1.4 2025-09-10 20:04:19 +02:00
caeb5662d4 chore: merkwerk auto-update 2025-09-10 20:01:19 +02:00
f20915ff33 fix(smtp): add missing headers to prevent spam classification
Add required SMTP headers to fix spam classification issues:
- Message-ID: generated from timestamp and from_address domain
- MIME-Version: 1.0 header for proper email formatting
- Content-Transfer-Encoding: 8bit for UTF-8 content

Fixes rspamd spam score from 10.42/10.00 (reject) to 4.80/10.00 (clean)
by resolving MISSING_MID (-2.50), MISSING_MIME_VERSION (-2.00),
and R_BAD_CTE_7BIT (-1.05) penalties.

Tested with mail-tester.com (10/10 score) and production deployment
on tiamat shows successful delivery to inbox instead of spam folder.

Related DAW/infrastruktur#35
2025-09-10 20:00:34 +02:00
4af068e15c chore: merkwerk auto-update 2025-09-10 16:46:13 +02:00
7a921dc791 Release v0.1.3: Add STARTTLS support for port 587 2025-09-10 16:46:12 +02:00
ec7086259e chore: merkwerk auto-update 2025-09-10 16:45:13 +02:00
304b010a56 fix(smtp): add STARTTLS support for port 587
- Add STARTTLS handshake after EHLO for port 587
- Upgrade socket to SSL after STARTTLS command
- Perform second EHLO over encrypted connection
- Resolves authentication issues with Hetzner and other SMTP providers
- Fixes 'Must issue a STARTTLS command first' error

Closes #113
2025-09-10 16:45:12 +02:00
9cd8f4bce0 chore: merkwerk auto-update 2025-09-10 14:27:55 +02:00
f5d9f359de Release v0.1.2: Complete API Gateway with Multi-Tenant Mail 2025-09-10 14:27:54 +02:00
166325b133 chore: merkwerk auto-update 2025-09-10 12:20:50 +02:00
8b7806670c docs: simplify README and remove FreeBSD support
- Strip README to essentials with wiki references only
- Remove non-existent API docs and troubleshooting links
- Focus on quick start and actual integrations (merkwerk)
- Remove FreeBSD support from all installation scripts
- Clean up platform detection logic in scripts
- Maintain OpenBSD and Linux support only

Reduces maintenance burden and aligns with actual project scope.
2025-09-10 12:20:41 +02:00
6c60d88f62 Merge branch 'security/sanitize-test-scripts' 2025-09-07 21:26:52 +02:00
54c594e656 chore: merkwerk auto-update 2025-09-07 21:25:38 +02:00
08b49d3d75 security: sanitize internal infrastructure details from open source package
- Remove production_test_sequence.sh (DAW-specific production tests)
- Remove setup_env.sh (obsolete .env setup, replaced by furt.conf)
- Sanitize test scripts: replace dragons-at-work.de with example.com
- Sanitize API keys: replace dev keys with placeholder values
- Remove hardcoded DAW fallbacks from http_server.lua and smtp.lua
- Update .gitignore to exclude production-specific test files

Tests remain functional for developers with example domains.
All internal DAW infrastructure details removed from package.

Closes #101
2025-09-07 21:25:25 +02:00
baa2490bbe feat(security): systemd service hardening implementation
Merge feature/systemd-hardening

This merge introduces production-ready security hardening for the
systemd service with 6 pragmatic security options:

- ProtectSystem=strict for read-only filesystem
- ReadWritePaths for required directories only
- ProtectHome=yes to block home directory access
- NoNewPrivileges=yes to prevent privilege escalation
- PrivateTmp=yes for isolated temporary space
- RestrictAddressFamilies=AF_INET for IPv4-only networking

Testing completed successfully on:
- Debian 12 (systemd 247)
- Arch Linux (systemd 256)

No performance impact observed (812K RAM usage).

Closes DAW/furt#110
2025-09-07 19:11:37 +02:00
32c51e326e chore: merkwerk auto-update 2025-09-07 18:40:47 +02:00
24bd94dec4 feat(deployment): add systemd security hardening
- Add ProtectSystem=strict for read-only filesystem
- Add ReadWritePaths for required directories
- Add ProtectHome=yes to block home access
- Add NoNewPrivileges=yes to prevent escalation
- Add PrivateTmp=yes for isolated temp space
- Add RestrictAddressFamilies=AF_INET for IPv4-only

Related DAW/furt#110
2025-09-07 18:40:32 +02:00
77b9685231 Merge branch 'fix/validate-config-posix-regex' 2025-09-07 18:05:30 +02:00
b4bc104750 chore: merkwerk auto-update 2025-09-07 18:00:48 +02:00
683d6e5e5d fix(scripts): resolve POSIX regex compatibility in validate-config.sh
- Replace \s* with [ \t]* for POSIX-compatible whitespace matching
- Addresses false positive 'server port not configured' error
- Ensures validation works correctly across all POSIX-compliant systems

Related to DAW/furt#111
2025-09-07 18:00:41 +02:00
df1edf3dc5 feat(service): merge PID-file based service management (#100)
This merge introduces reliable cross-platform service detection using
PID-files instead of fragile pexp patterns, resolving rcctl check
issues on OpenBSD.

Key improvements:
- PID-file creation in /var/run/furt/ with proper permissions
- Updated start.sh for service vs interactive mode detection
- Fixed OpenBSD rc.d script with PID-file based rc_check()
- Corrected systemd service PIDFile parameter
- Enhanced setup-directories.sh for PID directory creation

Tested successfully on werner (OpenBSD):
- rcctl check furt now shows (ok) instead of (failed)
- Service start/stop/restart works reliably
- PID-file management handles permissions correctly

Closes #100
2025-09-07 17:41:33 +02:00
bbbbeef072 chore: merkwerk auto-update 2025-09-07 16:58:01 +02:00
59f372f2b0 feat(service): implement PID-file based service management
- Add PID directory creation in setup-directories.sh
- Update start.sh to use /var/run/furt/furt.pid for both platforms
- Fix OpenBSD rc.d script pidfile variable path
- Correct systemd service PIDFile parameter path
- Resolve rcctl check detection issues on OpenBSD

Fixes service detection problems where rcctl check would show (failed)
even when service was running. PID-file approach provides reliable
cross-platform service status detection instead of fragile pexp patterns.
Related DAW/furt#100
2025-09-07 16:57:35 +02:00
7ee990b052 chore: merkwerk auto-update 2025-09-05 22:30:13 +02:00
25a709ebbe feat(service): implement PID-file based service management (DAW/furt#100)
- Replace unreliable pexp patterns with PID-file approach
- Add graceful shutdown with timeout handling in rc.d script
- Implement process validation after startup
- Add SIGHUP config reload support for Unix services
- Ensure PID-file cleanup on service exit
- Update systemd service to use PIDFile parameter

Platform improvements:
- OpenBSD: rc_check/rc_stop functions now PID-file based
- Linux: systemd Type=forking with proper PIDFile support
- Cross-platform: /var/run/furt.pid standard location

Resolves service status detection issues where rcctl check showed
(failed) despite running service due to process name variations
across platforms.
2025-09-05 22:30:07 +02:00
25 changed files with 281 additions and 576 deletions

1
.gitignore vendored
View file

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

View file

@ -20,3 +20,14 @@
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
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

159
README.md
View file

@ -1,160 +1,83 @@
# Furt API Gateway # Furt API Gateway
**HTTP-Server in Lua für Service-Integration** **Pure Lua HTTP-Server für digitale Souveränität**
## Überblick ## Überblick
Furt ist ein HTTP-Server der verschiedene Services unter einer API vereint. Aktuell unterstützt es Mail-Versendung über SMTP und bietet eine einfache JSON-API für Web-Integration. Furt ist ein minimalistisches HTTP-Server in Lua 5.1 für Mail-Versendung über SMTP. Es bietet eine einfache JSON-API für Web-Integration und Multi-Tenant-Unterstützung über API-Keys.
## Features ## Features
- HTTP-Server mit JSON-APIs - HTTP-Server mit JSON-APIs
- Mail-Versendung über SMTP - Multi-Tenant Mail-Routing über SMTP
- Request-Routing und Authentication - API-Key-basierte Authentifizierung
- Health-Check-Endpoints - Health-Check-Endpoints
- Konfigurierbare Rate-Limiting - Rate-Limiting pro API-Key
- Hugo/Website-Integration - CORS-Support für Frontend-Integration
## Dependencies ## Quick Start
**Erforderlich:** **Dependencies installieren:**
- `lua` 5.4+ ```bash
- `lua-socket` (HTTP-Server) # OpenBSD
- `lua-cjson` (JSON-Verarbeitung) doas pkg_add lua lua-socket lua-cjson luasec
# Debian/Ubuntu
sudo apt install lua5.1 lua-socket lua-cjson lua-sec
# Arch Linux
sudo pacman -S lua51 lua51-socket lua51-dkjson lua51-sec
```
**Installation:** **Installation:**
```bash ```bash
# Arch Linux git clone https://smida.dragons-at-work.de/DAW/furt.git
pacman -S lua lua-socket lua-cjson
# Ubuntu/Debian
apt install lua5.4 lua-socket lua-cjson
```
## Installation
```bash
# Repository klonen
git clone <repository-url>
cd furt cd furt
sudo ./install.sh
# Scripts ausführbar machen
chmod +x scripts/*.sh
# Server starten
./scripts/start.sh
``` ```
**Server läuft auf:** http://127.0.0.1:8080 **Server läuft auf:** http://127.0.0.1:7811
## API-Endpoints ## API-Endpoints
### Health Check **Health Check:**
```bash ```bash
GET /health curl http://127.0.0.1:7811/health
→ {"status":"healthy","service":"furt","version":"1.0.0"}
``` ```
### Mail senden **Mail senden:**
```bash ```bash
POST /v1/mail/send curl -X POST http://127.0.0.1:7811/v1/mail/send \
Content-Type: application/json -H "X-API-Key: your-api-key" \
{
"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","message":"Test"}' -d '{"name":"Test","email":"test@example.com","subject":"Test","message":"Test-Nachricht"}'
``` ```
## Deployment ## Dokumentation
**OpenBSD:** **Installation & Konfiguration:** [Furt Wiki](https://smida.dragons-at-work.de/DAW/furt/wiki)
- 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
│ ├── main.lua # HTTP-Server ├── config/ # Konfiguration
│ ├── routes/ # API-Endpoints ├── scripts/ # Installation & Management
│ └── smtp.lua # Mail-Integration └── deployment/ # System-Integration
├── config/ # Konfiguration
├── scripts/ # Start/Test-Scripts
├── tests/ # Test-Suite
└── deployment/ # System-Integration
``` ```
## Hugo-Integration ## Integration
**Shortcode-Beispiel:** **merkwerk:** Versionierte Furt-Deployment über [merkwerk](https://smida.dragons-at-work.de/DAW/merkwerk)
```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>
```
## Development ## License
**Code-Struktur:** ISC - Siehe [LICENSE](LICENSE) für Details.
- Module unter 200 Zeilen
- Funktionen unter 50 Zeilen
- Klare Fehlerbehandlung
- Testbare Komponenten
**Dependencies minimal halten:** ## Links
- Nur lua-socket und lua-cjson
- Keine externen HTTP-Libraries - **Repository:** [Forgejo](https://smida.dragons-at-work.de/DAW/furt)
- Standard-Lua-Funktionen bevorzugen - **Dokumentation:** [Wiki](https://smida.dragons-at-work.de/DAW/furt/wiki)
- **Projekt:** [Dragons@Work](https://dragons-at-work.de)

View file

@ -1 +1 @@
0.1.1 0.1.4

View file

@ -1,18 +1,33 @@
[Unit] [Unit]
Description=furt Multi-Tenant API Gateway Description=furt Multi-Tenant API Gateway (Security-Hardened)
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 start ExecStart=/usr/local/share/furt/scripts/start.sh
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,11 +3,52 @@
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
pexp="lua.*src/main.lua" # PID-File location
pidfile="/var/run/furt/furt.pid"
# Custom rc_check function (PID-File based)
rc_check() {
[ -f "$pidfile" ] && kill -0 $(cat "$pidfile") 2>/dev/null
}
# Custom rc_stop function (PID-File based)
rc_stop() {
if [ -f "$pidfile" ]; then
local _pid=$(cat "$pidfile")
echo "Stopping furt (PID: $_pid)"
kill "$_pid" 2>/dev/null
# Wait for process to die
local _timeout=10
while [ $_timeout -gt 0 ] && kill -0 "$_pid" 2>/dev/null; do
sleep 1
_timeout=$((_timeout - 1))
done
# Force kill if still running
if kill -0 "$_pid" 2>/dev/null; then
echo "Force killing furt (PID: $_pid)"
kill -9 "$_pid" 2>/dev/null
fi
rm -f "$pidfile"
echo "furt stopped"
else
echo "furt not running (no PID-File)"
fi
}
# Custom rc_reload function (signal-based)
rc_reload() {
if rc_check; then
local _pid=$(cat "$pidfile")
echo "Reloading furt configuration (PID: $_pid)"
kill -HUP "$_pid"
else
echo "furt not running"
return 1
fi
}
rc_cmd $1 rc_cmd $1

View file

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

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" ] || [ "$(uname)" = "FreeBSD" ]; then if [ "$(uname)" = "OpenBSD" ]; 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 Normal file → Executable 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 Normal file → Executable 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:8080/v1/mail/send \ curl -X POST http://127.0.0.1:7811/v1/mail/send \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{ -d '{
"name": "Furt Test User", "name": "Furt Test User",
"email": "michael@dragons-at-work.de", "email": "admin@example.com",
"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

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

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" ] || [ "$(uname)" = "FreeBSD" ]; then if [ "$(uname)" = "OpenBSD" ]; then
CONFIG_DIR="/usr/local/etc/furt" CONFIG_DIR="/usr/local/etc/furt"
USER="_furt" USER="_furt"
GROUP="_furt" GROUP="_furt"
@ -18,12 +18,15 @@ 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" ] || [ "$(uname)" = "FreeBSD" ]; then if [ "$(uname)" = "OpenBSD" ]; 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

View file

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

View file

@ -17,10 +17,12 @@ echo -e "${GREEN}=== Furt Lua HTTP-Server Startup ===${NC}"
LUA_COMMAND="" LUA_COMMAND=""
# Config check first # Config check first
if [ "$(uname)" = "OpenBSD" ] || [ "$(uname)" = "FreeBSD" ]; then if [ "$(uname)" = "OpenBSD" ]; 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
@ -87,12 +89,38 @@ 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 # Service mode - Background + PID-File
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 # Interactive mode - Foreground (no PID-File)
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="hugo-dev-key-change-in-production" API_KEY="YOUR_API_KEY_HERE"
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,8 +24,13 @@ 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 # Set proper permissions based on operating system
chown -R root:wheel "$TARGET" 2>/dev/null || chown -R root:root "$TARGET" if [ "$(uname)" = "OpenBSD" ]; then
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="hugo-dev-key-change-in-production" HUGO_API_KEY="YOUR_API_KEY_HERE"
ADMIN_API_KEY="admin-dev-key-change-in-production" ADMIN_API_KEY="YOUR_ADMIN_KEY_HERE"
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 Normal file → Executable 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="hugo-dev-key-change-in-production" HUGO_API_KEY="YOUR_API_KEY_HERE"
echo "🧩 Testing Modular Furt Architecture" echo "🧩 Testing Modular Furt Architecture"
echo "====================================" echo "===================================="

20
scripts/test_smtp.sh Normal file → Executable 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 michael@dragons-at-work.de" echo "WARNING: This will send a real email to admin@example.com"
read -p "Continue with real mail test? (y/N): " -n 1 -r 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@dragons-at-work.de", "email": "test@example.com",
"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 michael@dragons-at-work.de inbox" echo "Check admin@example.com 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" ] || [ "$(uname)" = "FreeBSD" ]; then if [ "$(uname)" = "OpenBSD" ]; 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,12 +24,13 @@ if ! grep -q '^\[server\]' "$CONFIG_FILE"; then
exit 1 exit 1
fi fi
if ! grep -q '^port\s*=' "$CONFIG_FILE"; then # Fix: Use POSIX-compatible regex patterns
if ! grep -q '^[ \t]*port[ \t]*=' "$CONFIG_FILE"; then
echo "Error: server port not configured" echo "Error: server port not configured"
exit 1 exit 1
fi fi
if ! grep -q '^host\s*=' "$CONFIG_FILE"; then if ! grep -q '^[ \t]*host[ \t]*=' "$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/FreeBSD "/usr/local/etc/furt/furt.conf", -- OpenBSD
"/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

@ -91,9 +91,7 @@ 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

View file

@ -1,6 +1,6 @@
-- furt-lua/src/smtp.lua -- src/smtp.lua
-- Universal SMTP implementation with SSL compatibility -- Universal SMTP implementation with SSL compatibility
-- Supports both luaossl (Arch/karl) and luasec (OpenBSD/walter) -- Supports both luaossl (Arch) and luasec (OpenBSD)
-- Dragons@Work Digital Sovereignty Project -- 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 or "mail.dragons-at-work.de", server = config.smtp_server,
port = config.smtp_port or 465, port = config.smtp_port,
username = config.username, username = config.username,
password = config.password, password = config.password,
from_address = config.from_address or "noreply@dragons-at-work.de", from_address = config.from_address,
use_ssl = config.use_ssl or true, 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,64 +219,96 @@ 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(
@ -284,7 +316,10 @@ 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" ..
".", ".",
@ -293,19 +328,20 @@ 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