diff --git a/Cargo.lock b/Cargo.lock
index 4dee713..2b70e77 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -348,6 +348,19 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+[[package]]
+name = "bcrypt"
+version = "0.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e65938ed058ef47d92cf8b346cc76ef48984572ade631927e9937b5ffc7662c7"
+dependencies = [
+ "base64 0.22.1",
+ "blowfish",
+ "getrandom 0.2.16",
+ "subtle",
+ "zeroize",
+]
+
[[package]]
name = "bindgen"
version = "0.69.5"
@@ -405,6 +418,16 @@ dependencies = [
"piper",
]
+[[package]]
+name = "blowfish"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7"
+dependencies = [
+ "byteorder",
+ "cipher",
+]
+
[[package]]
name = "bumpalo"
version = "3.19.0"
@@ -482,6 +505,16 @@ dependencies = [
"windows-link",
]
+[[package]]
+name = "cipher"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
+dependencies = [
+ "crypto-common",
+ "inout",
+]
+
[[package]]
name = "clang-sys"
version = "1.8.1"
@@ -1411,6 +1444,15 @@ dependencies = [
"libc",
]
+[[package]]
+name = "inout"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
+dependencies = [
+ "generic-array",
+]
+
[[package]]
name = "io-uring"
version = "0.7.8"
@@ -2027,6 +2069,8 @@ dependencies = [
"anyhow",
"async-stream",
"async-trait",
+ "base64 0.22.1",
+ "bcrypt",
"bytes",
"chrono",
"clap",
@@ -2039,6 +2083,7 @@ dependencies = [
"http-body-util",
"hyper 1.6.0",
"hyper-util",
+ "ipnet",
"metrics",
"metrics-exporter-prometheus",
"notify",
@@ -2052,6 +2097,7 @@ dependencies = [
"rustls-pki-types",
"serde",
"serde_json",
+ "tempfile",
"thiserror 1.0.69",
"tokio",
"tokio-rustls",
diff --git a/Cargo.toml b/Cargo.toml
index 6675d5e..80c6e8a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -45,6 +45,10 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
toml = "0.8"
+# Authentication
+bcrypt = "0.15"
+base64 = "0.22"
+
# Logging and tracing
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
@@ -74,6 +78,9 @@ rand = "0.8"
# Regular expressions
regex = "1.0"
+# IP network parsing
+ipnet = "2.9"
+
# Async traits
async-trait = "0.1"
@@ -82,6 +89,9 @@ file-sync = { path = "file-sync" }
futures-util = "0.3.31"
async-stream = "0.3.6"
+[dev-dependencies]
+tempfile = "3.0"
+
[lib]
name = "quantum"
path = "src/lib.rs"
@@ -97,3 +107,15 @@ path = "src/bin/sync-client.rs"
[[bin]]
name = "realtime-sync-client"
path = "src/bin/realtime-sync-client.rs"
+
+[[bin]]
+name = "caddy-import"
+path = "src/bin/caddy-import.rs"
+
+[[bin]]
+name = "test-server"
+path = "src/bin/test-server.rs"
+
+[[bin]]
+name = "minimal-test"
+path = "src/bin/minimal-test.rs"
diff --git a/caddy-to-quantum.json b/caddy-to-quantum.json
new file mode 100644
index 0000000..daba958
--- /dev/null
+++ b/caddy-to-quantum.json
@@ -0,0 +1,495 @@
+{
+ "admin": {"listen": ":2019"},
+ "apps": {
+ "http": {
+ "servers": {
+ "api_server": {
+ "listen": [":443"],
+ "routes": [
+ {
+ "match": [{"hosts": ["api.rockvilletollandsda.church", "api.adventisthymnarium.app"]}],
+ "handle": [
+ {
+ "handler": "subroute",
+ "routes": [
+ {
+ "match": [{"path": ["/uploads/rtsda_android/*"]}],
+ "handle": [
+ {
+ "handler": "headers",
+ "response": {
+ "set": {
+ "Content-Type": ["application/vnd.android.package-archive"],
+ "Content-Disposition": ["attachment; filename=rtsda.apk"]
+ }
+ }
+ },
+ {
+ "handler": "file_server",
+ "root": "/opt/rtsda/church-api"
+ }
+ ]
+ },
+ {
+ "match": [{"path": ["/uploads/*"]}],
+ "handle": [
+ {
+ "handler": "file_server",
+ "root": "/opt/rtsda/church-api"
+ }
+ ]
+ },
+ {
+ "match": [{"path": ["/*"]}],
+ "handle": [
+ {
+ "handler": "reverse_proxy",
+ "upstreams": [{"dial": "localhost:3002"}]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "stream_server": {
+ "listen": [":443"],
+ "routes": [
+ {
+ "match": [{"hosts": ["stream.rockvilletollandsda.church"]}],
+ "handle": [
+ {
+ "handler": "subroute",
+ "routes": [
+ {
+ "match": [
+ {
+ "not": [
+ {"path": ["/admin*"]},
+ {"path": ["/_next/*"]},
+ {"path": ["/styles/*"]},
+ {"path": ["/api/*"]},
+ {"path": ["/hls/*"]}
+ ]
+ }
+ ],
+ "handle": [
+ {
+ "handler": "static_response",
+ "status_code": 301,
+ "headers": {
+ "Location": ["https://rockvilletollandsda.church/live"]
+ }
+ }
+ ]
+ },
+ {
+ "match": [{"path": ["/admin*", "/_next/*", "/styles/*", "/api/*", "/hls/*"]}],
+ "handle": [
+ {
+ "handler": "reverse_proxy",
+ "upstreams": [{"dial": "localhost:8080"}]
+ }
+ ]
+ },
+ {
+ "handle": [
+ {
+ "handler": "file_server"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "contact_server": {
+ "listen": [":443"],
+ "routes": [
+ {
+ "match": [{"hosts": ["contact.rockvilletollandsda.church"]}],
+ "handle": [
+ {
+ "handler": "reverse_proxy",
+ "upstreams": [{"dial": "localhost:3002"}]
+ }
+ ]
+ }
+ ]
+ },
+ "jellyfin_server": {
+ "listen": [":443"],
+ "routes": [
+ {
+ "match": [{"hosts": ["jellyfin.rockvilletollandsda.church"]}],
+ "handle": [
+ {
+ "handler": "reverse_proxy",
+ "upstreams": [{"dial": "localhost:8096"}]
+ }
+ ]
+ }
+ ]
+ },
+ "pocketbase_server": {
+ "listen": [":443"],
+ "routes": [
+ {
+ "match": [{"hosts": ["pocketbase.rockvilletollandsda.church"]}],
+ "handle": [
+ {
+ "handler": "subroute",
+ "routes": [
+ {
+ "match": [{"path": ["/api/files/rtsda_android_collection/legacy_update_bridge/current"]}],
+ "handle": [
+ {
+ "handler": "static_response",
+ "status_code": 302,
+ "headers": {
+ "Location": ["https://api.rockvilletollandsda.church/uploads/rtsda_android/rtsda-1.0.apk"]
+ }
+ }
+ ]
+ },
+ {
+ "match": [{"path": ["/api/collections/rtsda_android/records"]}],
+ "handle": [
+ {
+ "handler": "static_response",
+ "status_code": 301,
+ "headers": {
+ "Location": ["https://api.rockvilletollandsda.church/api/collections/rtsda_android/records"]
+ }
+ }
+ ]
+ },
+ {
+ "handle": [
+ {
+ "handler": "static_response",
+ "status_code": 410,
+ "body": "PocketBase has been migrated. Please update your app."
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "static_servers": {
+ "listen": [":443"],
+ "routes": [
+ {
+ "match": [{"hosts": ["adventisthymnarium.rockvilletollandsda.church"]}],
+ "handle": [
+ {
+ "handler": "file_server",
+ "root": "/media/archive/AdventistHymnarium-Assets"
+ }
+ ]
+ },
+ {
+ "match": [{"hosts": ["privacy-policy.adventisthymnarium.app"]}],
+ "handle": [
+ {
+ "handler": "file_server",
+ "root": "/media/archive/AdventistHymnarium-Assets",
+ "try_files": ["{http.request.uri.path}", "/privacy-policy.html"]
+ }
+ ]
+ },
+ {
+ "match": [{"hosts": ["privacy-policy.rockvilletollandsda.church"]}],
+ "handle": [
+ {
+ "handler": "file_server",
+ "root": "/media/archive/AdventistHymnarium-Assets",
+ "try_files": ["{http.request.uri.path}", "/privacy-policy-rtsda.html"]
+ }
+ ]
+ },
+ {
+ "match": [{"hosts": ["bible.rockvilletollandsda.church"]}],
+ "handle": [
+ {
+ "handler": "file_server",
+ "root": "/media/archive/bibles"
+ }
+ ]
+ },
+ {
+ "match": [{"hosts": ["quarterlies.rockvilletollandsda.church"]}],
+ "handle": [
+ {
+ "handler": "headers",
+ "response": {
+ "set": {
+ "X-Content-Type-Options": ["nosniff"],
+ "X-Frame-Options": ["DENY"],
+ "X-XSS-Protection": ["1; mode=block"],
+ "Referrer-Policy": ["strict-origin-when-cross-origin"],
+ "Cache-Control": ["public, max-age=3600"]
+ }
+ }
+ },
+ {
+ "handler": "file_server",
+ "root": "/var/www/quarterlies"
+ }
+ ]
+ },
+ {
+ "match": [{"hosts": ["schedule.rockvilletollandsda.church"]}],
+ "handle": [
+ {
+ "handler": "headers",
+ "response": {
+ "set": {
+ "X-Content-Type-Options": ["nosniff"],
+ "X-Frame-Options": ["DENY"],
+ "X-XSS-Protection": ["1; mode=block"]
+ }
+ }
+ },
+ {
+ "handler": "file_server",
+ "root": "/var/www/schedule"
+ }
+ ]
+ },
+ {
+ "match": [{"hosts": ["admin.rockvilletollandsda.church"]}],
+ "handle": [
+ {
+ "handler": "headers",
+ "response": {
+ "set": {
+ "X-Content-Type-Options": ["nosniff"],
+ "X-Frame-Options": ["DENY"],
+ "X-XSS-Protection": ["1; mode=block"],
+ "Referrer-Policy": ["strict-origin-when-cross-origin"],
+ "Permissions-Policy": ["camera=(), microphone=(), geolocation=()"],
+ "Cache-Control": ["public, max-age=31536000, immutable"]
+ }
+ }
+ },
+ {
+ "handler": "file_server",
+ "root": "/var/www/admin",
+ "try_files": ["{http.request.uri.path}", "/index.html"]
+ }
+ ]
+ }
+ ]
+ },
+ "proxy_servers": {
+ "listen": [":443"],
+ "routes": [
+ {
+ "match": [{"hosts": ["openlp.rockvilletollandsda.church"]}],
+ "handle": [
+ {
+ "handler": "reverse_proxy",
+ "upstreams": [{"dial": "localhost:4316"}]
+ }
+ ]
+ },
+ {
+ "match": [{"hosts": ["obs.rockvilletollandsda.church"]}],
+ "handle": [
+ {
+ "handler": "reverse_proxy",
+ "upstreams": [{"dial": "localhost:4455"}],
+ "headers": {
+ "request": {
+ "set": {
+ "X-Real-IP": ["{http.request.remote_host}"],
+ "X-Forwarded-For": ["{http.request.remote_host}"],
+ "Host": ["{http.request.host}"]
+ }
+ }
+ }
+ }
+ ]
+ },
+ {
+ "match": [{"hosts": ["remote.rockvilletollandsda.church"]}],
+ "handle": [
+ {
+ "handler": "reverse_proxy",
+ "upstreams": [{"dial": "localhost:8443"}]
+ }
+ ]
+ },
+ {
+ "match": [{"hosts": ["syncthing.rockvilletollandsda.church"]}],
+ "handle": [
+ {
+ "handler": "reverse_proxy",
+ "upstreams": [{"dial": "localhost:22000"}]
+ }
+ ]
+ },
+ {
+ "match": [{"hosts": ["rockvilletollandsda.church", "rockvilletollandsda.org"]}],
+ "handle": [
+ {
+ "handler": "reverse_proxy",
+ "upstreams": [{"dial": "localhost:4321"}]
+ }
+ ]
+ },
+ {
+ "match": [{"hosts": ["webrtc.rockvilletollandsda.church"]}],
+ "handle": [
+ {
+ "handler": "reverse_proxy",
+ "upstreams": [{"dial": "localhost:8081"}]
+ }
+ ]
+ },
+ {
+ "match": [{"hosts": ["git.rockvilletollandsda.church"]}],
+ "handle": [
+ {
+ "handler": "reverse_proxy",
+ "upstreams": [{"dial": "localhost:3000"}]
+ }
+ ]
+ }
+ ]
+ },
+ "special_servers": {
+ "listen": [":443", ":4317"],
+ "routes": [
+ {
+ "match": [{"hosts": ["openlp.rockvilletollandsda.church"]}, {"port": "4317"}],
+ "handle": [
+ {
+ "handler": "subroute",
+ "routes": [
+ {
+ "match": [{"path": ["/poll*", "/messages*"]}],
+ "handle": [
+ {
+ "handler": "reverse_proxy",
+ "upstreams": [{"dial": "localhost:4318"}]
+ }
+ ]
+ },
+ {
+ "match": [{"path": ["/"]}],
+ "handle": [
+ {
+ "handler": "reverse_proxy",
+ "upstreams": [{"dial": "localhost:4318"}]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "match": [{"hosts": ["events.rockvilletollandsda.church"]}],
+ "handle": [
+ {
+ "handler": "static_response",
+ "status_code": 301,
+ "headers": {
+ "Location": ["https://rockvilletollandsda.church/events/submit"]
+ }
+ }
+ ]
+ },
+ {
+ "match": [{"hosts": ["nominating.rockvilletollandsda.church"]}],
+ "handle": [
+ {
+ "handler": "subroute",
+ "routes": [
+ {
+ "match": [{"path": ["/admin*"]}],
+ "handle": [
+ {
+ "handler": "authentication",
+ "providers": {
+ "http_basic": {
+ "accounts": [
+ {
+ "username": "admin",
+ "password": "$2a$14$p4vzY.AzQynxA6BIRBBtF.pdiIMv7F9ooOtzznCZbm7HaHNX/vBJi"
+ }
+ ]
+ }
+ }
+ },
+ {
+ "handler": "file_server",
+ "root": "/var/www/nmc"
+ }
+ ]
+ },
+ {
+ "match": [{"path": ["/"]}],
+ "handle": [
+ {
+ "handler": "file_server",
+ "root": "/var/www/nmc",
+ "try_files": ["{http.request.uri.path}", "/index.html"]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ }
+ },
+ "tls": {
+ "automation": {
+ "policies": [
+ {
+ "subjects": [
+ "api.rockvilletollandsda.church",
+ "api.adventisthymnarium.app",
+ "stream.rockvilletollandsda.church",
+ "contact.rockvilletollandsda.church",
+ "jellyfin.rockvilletollandsda.church",
+ "pocketbase.rockvilletollandsda.church",
+ "adventisthymnarium.rockvilletollandsda.church",
+ "privacy-policy.adventisthymnarium.app",
+ "privacy-policy.rockvilletollandsda.church",
+ "bible.rockvilletollandsda.church",
+ "openlp.rockvilletollandsda.church",
+ "obs.rockvilletollandsda.church",
+ "remote.rockvilletollandsda.church",
+ "syncthing.rockvilletollandsda.church",
+ "quarterlies.rockvilletollandsda.church",
+ "schedule.rockvilletollandsda.church",
+ "events.rockvilletollandsda.church",
+ "admin.rockvilletollandsda.church",
+ "rockvilletollandsda.church",
+ "rockvilletollandsda.org",
+ "webrtc.rockvilletollandsda.church",
+ "nominating.rockvilletollandsda.church",
+ "git.rockvilletollandsda.church"
+ ],
+ "issuer": {
+ "module": "acme"
+ }
+ }
+ ]
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/examples/full-caddy-replacement.json b/examples/full-caddy-replacement.json
new file mode 100644
index 0000000..2c09415
--- /dev/null
+++ b/examples/full-caddy-replacement.json
@@ -0,0 +1,544 @@
+{
+ "admin": {
+ "listen": "localhost:2019"
+ },
+ "apps": {
+ "http": {
+ "servers": {
+ "main": {
+ "listen": [":80", ":443"],
+ "routes": [
+ {
+ "match": [
+ {
+ "host": ["rockvilletollandsda.church", "www.rockvilletollandsda.church"]
+ }
+ ],
+ "handle": [
+ {
+ "handler": "subroute",
+ "routes": [
+ {
+ "match": [
+ {
+ "path": ["/admin*"]
+ }
+ ],
+ "handle": [
+ {
+ "handler": "http_basic_auth",
+ "accounts": [
+ {
+ "username": "admin",
+ "password": "$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LeH6Gn0.oJD0V8XZe"
+ }
+ ],
+ "realm": "Admin Area"
+ },
+ {
+ "handler": "headers",
+ "response": {
+ "set": {
+ "Cache-Control": ["no-store, no-cache, must-revalidate"]
+ }
+ }
+ },
+ {
+ "handler": "reverse_proxy",
+ "upstreams": [
+ {
+ "dial": "localhost:4321"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "match": [
+ {
+ "path": ["/api/*"]
+ }
+ ],
+ "handle": [
+ {
+ "handler": "headers",
+ "response": {
+ "set": {
+ "Access-Control-Allow-Origin": ["*"],
+ "Access-Control-Allow-Methods": ["GET, POST, PUT, DELETE, OPTIONS"],
+ "Access-Control-Allow-Headers": ["Content-Type, Authorization"]
+ }
+ }
+ },
+ {
+ "handler": "reverse_proxy",
+ "upstreams": [
+ {
+ "dial": "localhost:3002"
+ }
+ ],
+ "load_balancing": {
+ "selection_policy": "round_robin"
+ },
+ "health_checks": {
+ "active": {
+ "uri": "/health",
+ "interval": "30s",
+ "timeout": "5s",
+ "expect_status": 200
+ }
+ }
+ }
+ ]
+ },
+ {
+ "match": [
+ {
+ "path": ["/uploads/*"]
+ }
+ ],
+ "handle": [
+ {
+ "handler": "file_server",
+ "root": "/opt/rtsda/church-api",
+ "browse": {
+ "template": "browse.html"
+ }
+ }
+ ]
+ },
+ {
+ "match": [
+ {
+ "path": ["/_next/*", "/styles/*", "/images/*", "/fonts/*"]
+ }
+ ],
+ "handle": [
+ {
+ "handler": "headers",
+ "response": {
+ "set": {
+ "Cache-Control": ["public, max-age=31536000, immutable"]
+ }
+ }
+ },
+ {
+ "handler": "file_server",
+ "root": "/var/www/nextjs",
+ "precompressed": {
+ "encodings": ["br", "gzip"]
+ }
+ }
+ ]
+ },
+ {
+ "handle": [
+ {
+ "handler": "file_server",
+ "root": "/var/www/rockville",
+ "index_names": ["index.html", "index.htm"],
+ "canonical_uris": true
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "match": [
+ {
+ "host": ["api.rockvilletollandsda.church"]
+ }
+ ],
+ "handle": [
+ {
+ "handler": "headers",
+ "response": {
+ "set": {
+ "Strict-Transport-Security": ["max-age=31536000; includeSubDomains"],
+ "X-Content-Type-Options": ["nosniff"],
+ "X-Frame-Options": ["DENY"],
+ "X-XSS-Protection": ["1; mode=block"],
+ "Referrer-Policy": ["strict-origin-when-cross-origin"]
+ }
+ }
+ },
+ {
+ "handler": "reverse_proxy",
+ "upstreams": [
+ {
+ "dial": "localhost:3002"
+ },
+ {
+ "dial": "localhost:3003"
+ }
+ ],
+ "load_balancing": {
+ "selection_policy": "least_conn",
+ "try_duration": "30s",
+ "try_interval": "1s"
+ },
+ "health_checks": {
+ "active": {
+ "uri": "/health",
+ "port": 3002,
+ "headers": {
+ "User-Agent": ["Quantum-HealthCheck/1.0"]
+ },
+ "interval": "10s",
+ "timeout": "3s",
+ "expect_status": 200
+ },
+ "passive": {
+ "unhealthy_status": [500, 502, 503, 504],
+ "unhealthy_latency": "10s",
+ "unhealthy_request_count": 3,
+ "healthy_count": 2
+ }
+ },
+ "circuit_breaker": {
+ "trip_duration": "30s",
+ "recovery_duration": "10s",
+ "failure_threshold": 0.5,
+ "success_threshold": 0.8
+ }
+ }
+ ]
+ },
+ {
+ "match": [
+ {
+ "host": ["jellyfin.rockvilletollandsda.church"]
+ }
+ ],
+ "handle": [
+ {
+ "handler": "headers",
+ "request": {
+ "set": {
+ "X-Forwarded-For": ["{remote_ip}"],
+ "X-Real-IP": ["{remote_ip}"]
+ }
+ }
+ },
+ {
+ "handler": "reverse_proxy",
+ "upstreams": [
+ {
+ "dial": "localhost:8096"
+ }
+ ],
+ "transport": {
+ "keep_alive": {
+ "enabled": true,
+ "probe_interval": "30s",
+ "max_idle_conns": 100,
+ "idle_conn_timeout": "90s"
+ },
+ "dial_timeout": "5s",
+ "response_header_timeout": "10s"
+ }
+ }
+ ]
+ },
+ {
+ "match": [
+ {
+ "host": ["webrtc.rockvilletollandsda.church"]
+ }
+ ],
+ "handle": [
+ {
+ "handler": "reverse_proxy",
+ "upstreams": [
+ {
+ "dial": "localhost:8081"
+ }
+ ],
+ "headers": {
+ "request": {
+ "set": {
+ "Upgrade": ["websocket"],
+ "Connection": ["upgrade"]
+ }
+ }
+ }
+ }
+ ]
+ },
+ {
+ "match": [
+ {
+ "host": ["bible.rockvilletollandsda.church"]
+ }
+ ],
+ "handle": [
+ {
+ "handler": "rate_limit",
+ "key": "{remote_ip}",
+ "rate": "100r/m",
+ "burst": 20,
+ "window": "1m"
+ },
+ {
+ "handler": "file_server",
+ "root": "/media/archive/bibles",
+ "browse": {
+ "template": "bible-browse.html"
+ }
+ }
+ ]
+ },
+ {
+ "match": [
+ {
+ "host": ["adventisthymnarium.rockvilletollandsda.church", "adventisthymnarium.app"]
+ }
+ ],
+ "handle": [
+ {
+ "handler": "subroute",
+ "routes": [
+ {
+ "match": [
+ {
+ "path": ["/api/*"]
+ }
+ ],
+ "handle": [
+ {
+ "handler": "reverse_proxy",
+ "upstreams": [
+ {
+ "dial": "localhost:3004"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "handle": [
+ {
+ "handler": "file_server",
+ "root": "/media/archive/AdventistHymnarium-Assets",
+ "canonical_uris": true,
+ "index_names": ["index.html"]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "match": [
+ {
+ "host": ["privacy-policy.adventisthymnarium.app", "privacy-policy.rockvilletollandsda.church"]
+ }
+ ],
+ "handle": [
+ {
+ "handler": "file_server",
+ "root": "/media/archive/AdventistHymnarium-Assets/privacy",
+ "canonical_uris": true
+ }
+ ]
+ },
+ {
+ "match": [
+ {
+ "host": ["schedule.rockvilletollandsda.church"]
+ }
+ ],
+ "handle": [
+ {
+ "handler": "authentication",
+ "providers": {
+ "local": {
+ "method": "basic"
+ }
+ }
+ },
+ {
+ "handler": "file_server",
+ "root": "/var/www/schedule",
+ "index_names": ["index.html", "schedule.html"]
+ }
+ ]
+ },
+ {
+ "match": [
+ {
+ "path": ["/.well-known/acme-challenge/*"]
+ }
+ ],
+ "handle": [
+ {
+ "handler": "file_server",
+ "root": "/var/lib/acme/.well-known/acme-challenge",
+ "pass_thru": true
+ }
+ ]
+ },
+ {
+ "match": [
+ {
+ "path": ["/health", "/status"]
+ }
+ ],
+ "handle": [
+ {
+ "handler": "health"
+ }
+ ]
+ },
+ {
+ "match": [
+ {
+ "path": ["/metrics"]
+ }
+ ],
+ "handle": [
+ {
+ "handler": "ip_whitelist",
+ "source": "remote_ip",
+ "rules": [
+ {
+ "action": "allow",
+ "rule": "127.0.0.1"
+ },
+ {
+ "action": "allow",
+ "rule": "10.0.0.0/8"
+ }
+ ]
+ },
+ {
+ "handler": "metrics",
+ "path": "/metrics"
+ }
+ ]
+ },
+ {
+ "match": [
+ {
+ "path": ["/redirect-test"]
+ }
+ ],
+ "handle": [
+ {
+ "handler": "redirect",
+ "to": "https://rockvilletollandsda.church/",
+ "status_code": 301
+ }
+ ]
+ },
+ {
+ "handle": [
+ {
+ "handler": "static_response",
+ "status_code": 404,
+ "headers": {
+ "Content-Type": ["text/html; charset=utf-8"]
+ },
+ "body": "
404 Not FoundPage Not Found
The requested resource could not be found.
"
+ }
+ ]
+ }
+ ],
+ "automatic_https": {
+ "disable": false,
+ "disable_redirects": false
+ },
+ "tls_connection_policies": [
+ {
+ "match": {
+ "sni": ["*.rockvilletollandsda.church", "*.adventisthymnarium.app"]
+ },
+ "cipher_suites": [
+ "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
+ "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
+ "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305",
+ "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305",
+ "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
+ "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"
+ ],
+ "protocols": {
+ "min": "tls1.2",
+ "max": "tls1.3"
+ },
+ "alpn": ["h2", "http/1.1"]
+ }
+ ],
+ "protocols": ["h1", "h2", "h3"],
+ "experimental_http3": true,
+ "request_timeout": "30s",
+ "read_timeout": "30s",
+ "write_timeout": "30s",
+ "idle_timeout": "2m",
+ "max_header_bytes": 1048576
+ }
+ }
+ },
+ "tls": {
+ "automation": {
+ "policies": [
+ {
+ "subjects": [
+ "rockvilletollandsda.church",
+ "*.rockvilletollandsda.church",
+ "adventisthymnarium.app",
+ "*.adventisthymnarium.app"
+ ],
+ "issuer": {
+ "module": "acme",
+ "ca": "https://acme-v02.api.letsencrypt.org/directory",
+ "email": "admin@rockvilletollandsda.church",
+ "challenges": {
+ "http": {
+ "disabled": false
+ },
+ "dns": {
+ "provider": "cloudflare",
+ "disabled": false,
+ "propagation_delay": "2m",
+ "propagation_timeout": "10m"
+ }
+ },
+ "preferred_chains": {
+ "smallest": true
+ }
+ },
+ "key_type": "ec256",
+ "must_staple": true
+ }
+ ],
+ "on_demand": {
+ "rate_limit": {
+ "interval": "1h",
+ "burst": 10
+ },
+ "ask": "https://example.com/check-cert"
+ },
+ "ocsp_interval": "1h",
+ "renew_ahead": "30d"
+ },
+ "session_tickets": {
+ "rotation_interval": "1h",
+ "max_keys": 4,
+ "disabled": false
+ }
+ },
+ "pki": {
+ "certificate_authorities": {
+ "internal": {
+ "name": "Quantum Internal CA",
+ "root_common_name": "Quantum Root CA",
+ "intermediate_common_name": "Quantum Intermediate CA",
+ "intermediate_lifetime": "365d"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/public/index.html b/public/index.html
index 5d039b9..25bd8d7 100644
--- a/public/index.html
+++ b/public/index.html
@@ -1 +1 @@
-File Server Test
This is served from the file system.
+Static Test Page
diff --git a/simple-church-config.json b/simple-church-config.json
new file mode 100644
index 0000000..062446f
--- /dev/null
+++ b/simple-church-config.json
@@ -0,0 +1,30 @@
+{
+ "proxy": {
+ "localhost:3002": "api.rockvilletollandsda.church:443",
+ "localhost:3002": "api.adventisthymnarium.app:443",
+ "localhost:3002": "contact.rockvilletollandsda.church:443",
+ "localhost:8080": "stream.rockvilletollandsda.church:443",
+ "localhost:8096": "jellyfin.rockvilletollandsda.church:443",
+ "localhost:4316": "openlp.rockvilletollandsda.church:443",
+ "localhost:4318": "openlp.rockvilletollandsda.church:4317",
+ "localhost:4455": "obs.rockvilletollandsda.church:443",
+ "localhost:8443": "remote.rockvilletollandsda.church:443",
+ "localhost:22000": "syncthing.rockvilletollandsda.church:443",
+ "localhost:4321": "rockvilletollandsda.church:443",
+ "localhost:4321": "rockvilletollandsda.org:443",
+ "localhost:8081": "webrtc.rockvilletollandsda.church:443",
+ "localhost:3000": "git.rockvilletollandsda.church:443"
+ },
+ "static_files": {
+ "/opt/rtsda/church-api": "api.rockvilletollandsda.church:443/uploads/*",
+ "/media/archive/AdventistHymnarium-Assets": "adventisthymnarium.rockvilletollandsda.church:443",
+ "/media/archive/AdventistHymnarium-Assets": "privacy-policy.adventisthymnarium.app:443",
+ "/media/archive/AdventistHymnarium-Assets": "privacy-policy.rockvilletollandsda.church:443",
+ "/media/archive/bibles": "bible.rockvilletollandsda.church:443",
+ "/var/www/quarterlies": "quarterlies.rockvilletollandsda.church:443",
+ "/var/www/schedule": "schedule.rockvilletollandsda.church:443",
+ "/var/www/admin": "admin.rockvilletollandsda.church:443",
+ "/var/www/nmc": "nominating.rockvilletollandsda.church:443"
+ },
+ "tls": "auto"
+}
\ No newline at end of file
diff --git a/src/bin/caddy-import.rs b/src/bin/caddy-import.rs
new file mode 100644
index 0000000..30369df
--- /dev/null
+++ b/src/bin/caddy-import.rs
@@ -0,0 +1,190 @@
+use anyhow::Result;
+use clap::{Arg, Command};
+use quantum::caddy::CaddyConverter;
+use std::path::PathBuf;
+use tracing::{info, error};
+
+#[tokio::main]
+async fn main() -> Result<()> {
+ // Initialize logging
+ tracing_subscriber::fmt()
+ .with_env_filter("info")
+ .init();
+
+ let matches = Command::new("caddy-import")
+ .version("0.2.0")
+ .author("Quantum Contributors")
+ .about("Import and convert Caddy configurations to Quantum")
+ .arg(
+ Arg::new("input")
+ .short('i')
+ .long("input")
+ .value_name("FILE")
+ .help("Input Caddy configuration file (JSON or Caddyfile)")
+ .required(true)
+ )
+ .arg(
+ Arg::new("output")
+ .short('o')
+ .long("output")
+ .value_name("FILE")
+ .help("Output Quantum configuration file")
+ .default_value("quantum-config.json")
+ )
+ .arg(
+ Arg::new("format")
+ .short('f')
+ .long("format")
+ .value_name("FORMAT")
+ .help("Output format (json, toml)")
+ .default_value("json")
+ )
+ .arg(
+ Arg::new("validate")
+ .short('v')
+ .long("validate")
+ .help("Validate the configuration after conversion")
+ .action(clap::ArgAction::SetTrue)
+ )
+ .arg(
+ Arg::new("dry-run")
+ .short('d')
+ .long("dry-run")
+ .help("Show conversion result without writing to file")
+ .action(clap::ArgAction::SetTrue)
+ )
+ .get_matches();
+
+ let input_path = PathBuf::from(matches.get_one::("input").unwrap());
+ let output_path = PathBuf::from(matches.get_one::("output").unwrap());
+ let format = matches.get_one::("format").unwrap();
+ let validate = matches.get_flag("validate");
+ let dry_run = matches.get_flag("dry-run");
+
+ info!("Converting Caddy configuration from {:?}", input_path);
+
+ // Load and convert the configuration
+ let quantum_config = match CaddyConverter::load_and_convert(&input_path) {
+ Ok(config) => config,
+ Err(e) => {
+ error!("Failed to convert configuration: {}", e);
+ std::process::exit(1);
+ }
+ };
+
+ info!("Configuration converted successfully");
+
+ // Validate if requested
+ if validate {
+ info!("Validating converted configuration...");
+ if let Err(e) = validate_config(&quantum_config) {
+ error!("Configuration validation failed: {}", e);
+ std::process::exit(1);
+ }
+ info!("Configuration is valid");
+ }
+
+ // Serialize the configuration
+ let output_content = match format.as_str() {
+ "json" => {
+ serde_json::to_string_pretty(&quantum_config)?
+ }
+ "toml" => {
+ toml::to_string_pretty(&quantum_config)?
+ }
+ _ => {
+ error!("Unsupported format: {}", format);
+ std::process::exit(1);
+ }
+ };
+
+ if dry_run {
+ println!("Converted configuration:");
+ println!("{}", output_content);
+ } else {
+ // Write to output file
+ std::fs::write(&output_path, output_content)?;
+ info!("Configuration written to {:?}", output_path);
+ }
+
+ // Print migration summary
+ print_migration_summary(&quantum_config);
+
+ Ok(())
+}
+
+fn validate_config(config: &quantum::config::Config) -> Result<()> {
+ // Basic validation checks
+ if config.apps.http.servers.is_empty() {
+ return Err(anyhow::anyhow!("No HTTP servers configured"));
+ }
+
+ for (server_name, server) in &config.apps.http.servers {
+ if server.listen.is_empty() {
+ return Err(anyhow::anyhow!("Server '{}' has no listen addresses", server_name));
+ }
+
+ if server.routes.is_empty() {
+ return Err(anyhow::anyhow!("Server '{}' has no routes", server_name));
+ }
+
+ // Validate each route
+ for (i, route) in server.routes.iter().enumerate() {
+ if route.handle.is_empty() {
+ return Err(anyhow::anyhow!(
+ "Route {} in server '{}' has no handlers",
+ i,
+ server_name
+ ));
+ }
+ }
+ }
+
+ Ok(())
+}
+
+fn print_migration_summary(config: &quantum::config::Config) {
+ println!("\n=== Migration Summary ===");
+ println!("Servers: {}", config.apps.http.servers.len());
+
+ let mut total_routes = 0;
+ let mut handler_counts = std::collections::HashMap::new();
+
+ for (server_name, server) in &config.apps.http.servers {
+ println!(" Server '{}': {} listen addresses, {} routes",
+ server_name,
+ server.listen.len(),
+ server.routes.len());
+
+ total_routes += server.routes.len();
+
+ for route in &server.routes {
+ for handler in &route.handle {
+ let handler_type = match handler {
+ quantum::config::Handler::BasicAuth { .. } => "BasicAuth",
+ quantum::config::Handler::FileServer { .. } => "FileServer",
+ quantum::config::Handler::ReverseProxy { .. } => "ReverseProxy",
+ quantum::config::Handler::StaticResponse { .. } => "StaticResponse",
+ quantum::config::Handler::Redirect { .. } => "Redirect",
+ quantum::config::Handler::Rewrite { .. } => "Rewrite",
+ quantum::config::Handler::Headers { .. } => "Headers",
+ quantum::config::Handler::Error { .. } => "Error",
+ quantum::config::Handler::FileSync { .. } => "FileSync",
+ };
+ *handler_counts.entry(handler_type).or_insert(0) += 1;
+ }
+ }
+ }
+
+ println!("Total routes: {}", total_routes);
+ println!("Handler distribution:");
+ for (handler_type, count) in handler_counts {
+ println!(" {}: {}", handler_type, count);
+ }
+
+ println!("\n=== Next Steps ===");
+ println!("1. Review the converted configuration");
+ println!("2. Test with: quantum --config quantum-config.json");
+ println!("3. Monitor performance and adjust as needed");
+ println!("4. Migrate traffic gradually from Caddy to Quantum");
+}
\ No newline at end of file
diff --git a/src/bin/minimal-test.rs b/src/bin/minimal-test.rs
new file mode 100644
index 0000000..acfb133
--- /dev/null
+++ b/src/bin/minimal-test.rs
@@ -0,0 +1,62 @@
+use anyhow::Result;
+use hyper::body::Bytes;
+use hyper::server::conn::http1;
+use hyper::service::service_fn;
+use hyper::{Request, Response};
+use hyper_util::rt::TokioIo;
+use http_body_util::Full;
+use std::net::SocketAddr;
+use tokio::net::TcpListener;
+use tracing::{info, error};
+
+#[tokio::main]
+async fn main() -> Result<()> {
+ // Initialize logging
+ tracing_subscriber::fmt()
+ .with_env_filter("info")
+ .init();
+
+ info!("Starting minimal Quantum test server on :8080");
+
+ let addr = SocketAddr::from(([0, 0, 0, 0], 8080));
+ let listener = TcpListener::bind(addr).await?;
+
+ info!("Server listening on http://localhost:8080");
+ info!("Test with: curl http://localhost:8080");
+
+ loop {
+ let (stream, remote_addr) = listener.accept().await?;
+ let io = TokioIo::new(stream);
+
+ tokio::task::spawn(async move {
+ if let Err(err) = http1::Builder::new()
+ .serve_connection(io, service_fn(handle_request))
+ .await
+ {
+ error!("Error serving connection from {}: {:?}", remote_addr, err);
+ }
+ });
+ }
+}
+
+async fn handle_request(
+ req: Request,
+) -> Result>, hyper::Error> {
+ info!("Received {} {}", req.method(), req.uri().path());
+
+ let response_body = format!(
+ "Hello from Quantum!\n\nMethod: {}\nPath: {}\nHeaders: {:#?}\n",
+ req.method(),
+ req.uri(),
+ req.headers()
+ );
+
+ let response = Response::builder()
+ .status(200)
+ .header("content-type", "text/plain")
+ .header("server", "Quantum/0.2.0")
+ .body(Full::new(Bytes::from(response_body)))
+ .unwrap();
+
+ Ok(response)
+}
\ No newline at end of file
diff --git a/src/bin/test-server.rs b/src/bin/test-server.rs
new file mode 100644
index 0000000..11c391e
--- /dev/null
+++ b/src/bin/test-server.rs
@@ -0,0 +1,74 @@
+use anyhow::Result;
+use quantum::{Config, ServiceRegistry, Server};
+use tracing::{info, error};
+
+#[tokio::main]
+async fn main() -> Result<()> {
+ // Initialize logging
+ tracing_subscriber::fmt()
+ .with_env_filter("info")
+ .init();
+
+ info!("Starting Quantum test server");
+
+ // Create a simple config programmatically
+ let config = create_test_config();
+
+ info!("Created test configuration");
+
+ // Initialize services
+ let services = match ServiceRegistry::new(&config).await {
+ Ok(s) => s,
+ Err(e) => {
+ error!("Failed to initialize services: {}", e);
+ return Err(e);
+ }
+ };
+
+ info!("Services initialized");
+
+ // Create and start server
+ let server = match Server::new(config, services).await {
+ Ok(s) => s,
+ Err(e) => {
+ error!("Failed to create server: {}", e);
+ return Err(e);
+ }
+ };
+
+ info!("Server created, starting on port 8080");
+ info!("Test with: curl http://localhost:8080");
+
+ server.run().await
+}
+
+fn create_test_config() -> Config {
+ use std::collections::HashMap;
+ use quantum::config::*;
+
+ let mut servers = HashMap::new();
+ servers.insert("test".to_string(), Server {
+ listen: vec![":8080".to_string()],
+ routes: vec![Route {
+ handle: vec![Handler::StaticResponse {
+ status_code: Some(200),
+ headers: None,
+ body: Some("Hello from Quantum! The server is working.".to_string()),
+ }],
+ match_rules: None,
+ }],
+ automatic_https: AutomaticHttps::default(),
+ tls: None,
+ });
+
+ Config {
+ admin: AdminConfig {
+ listen: Some("localhost:2019".to_string()),
+ },
+ apps: Apps {
+ http: HttpApp {
+ servers,
+ },
+ },
+ }
+}
\ No newline at end of file
diff --git a/src/caddy/mod.rs b/src/caddy/mod.rs
new file mode 100644
index 0000000..bbd51fd
--- /dev/null
+++ b/src/caddy/mod.rs
@@ -0,0 +1,1044 @@
+use anyhow::Result;
+use serde::{Deserialize, Serialize};
+use std::collections::HashMap;
+use std::path::PathBuf;
+
+/// Complete Caddy configuration structure for 100% compatibility
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct CaddyConfig {
+ /// Global admin settings
+ pub admin: Option,
+ /// App configurations
+ pub apps: Apps,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct AdminConfig {
+ /// Admin endpoint listen address
+ pub listen: Option,
+ /// API origins
+ pub origins: Option>,
+ /// Remote admin endpoint
+ pub remote: Option,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct RemoteAdmin {
+ pub endpoint: String,
+ pub access_id: String,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct Apps {
+ /// HTTP app configuration
+ pub http: HttpApp,
+ /// TLS app configuration
+ pub tls: Option,
+ /// PKI app configuration
+ pub pki: Option,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct HttpApp {
+ /// HTTP servers configuration
+ pub servers: HashMap,
+ /// Grace period for graceful shutdown
+ pub grace_period: Option,
+ /// Shutdown delay
+ pub shutdown_delay: Option,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct HttpServer {
+ /// Listen addresses
+ pub listen: Vec,
+ /// Routes configuration
+ pub routes: Vec,
+ /// Error handling
+ pub errors: Option,
+ /// TLS connection policies
+ pub tls_connection_policies: Option>,
+ /// Automatic HTTPS
+ pub automatic_https: Option,
+ /// Protocol configuration
+ pub protocols: Option>,
+ /// Strict SNI host matching
+ pub strict_sni_host: Option,
+ /// Request timeout
+ pub request_timeout: Option,
+ /// Read timeout
+ pub read_timeout: Option,
+ /// Read header timeout
+ pub read_header_timeout: Option,
+ /// Write timeout
+ pub write_timeout: Option,
+ /// Idle timeout
+ pub idle_timeout: Option,
+ /// Max header bytes
+ pub max_header_bytes: Option,
+ /// Enable H2C
+ pub allow_h2c: Option,
+ /// Experimental HTTP/3
+ pub experimental_http3: Option,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct Route {
+ /// Route matchers
+ #[serde(rename = "match")]
+ pub match_rules: Option>,
+ /// Handler chain
+ pub handle: Vec,
+ /// Terminal route (default: true)
+ pub terminal: Option,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(tag = "handler")]
+pub enum Handler {
+ /// Authentication handler
+ #[serde(rename = "authentication")]
+ Authentication {
+ providers: HashMap,
+ },
+ /// Basic auth handler
+ #[serde(rename = "http_basic_auth")]
+ BasicAuth {
+ accounts: Vec,
+ realm: Option,
+ hash: Option,
+ },
+ /// Static file server
+ #[serde(rename = "file_server")]
+ FileServer {
+ root: Option,
+ hide: Option>,
+ index_names: Option>,
+ browse: Option,
+ precompressed: Option,
+ status_code: Option,
+ canonical_uris: Option,
+ pass_thru: Option,
+ },
+ /// Reverse proxy
+ #[serde(rename = "reverse_proxy")]
+ ReverseProxy {
+ upstreams: Vec,
+ load_balancing: Option,
+ health_checks: Option,
+ circuit_breaker: Option,
+ headers: Option,
+ transport: Option,
+ handle_response: Option>,
+ trusted_proxies: Option>,
+ replace_status: Option>,
+ buffer_requests: Option,
+ buffer_responses: Option,
+ max_buffer_size: Option,
+ stream_timeout: Option,
+ stream_close_delay: Option,
+ flush_interval: Option,
+ },
+ /// Static response
+ #[serde(rename = "static_response")]
+ StaticResponse {
+ status_code: Option,
+ headers: Option>>,
+ body: Option,
+ close: Option,
+ },
+ /// Redirect handler
+ #[serde(rename = "redirect")]
+ Redirect {
+ to: Option,
+ status_code: Option,
+ },
+ /// Rewrite handler
+ #[serde(rename = "rewrite")]
+ Rewrite {
+ uri: Option,
+ strip_path_prefix: Option,
+ strip_path_suffix: Option,
+ uri_substring: Option>,
+ method: Option,
+ },
+ /// Headers handler
+ #[serde(rename = "headers")]
+ Headers {
+ request: Option,
+ response: Option,
+ },
+ /// Copy response headers handler
+ #[serde(rename = "copy_response_headers")]
+ CopyResponseHeaders {
+ include: Option>,
+ exclude: Option>,
+ },
+ /// Request body handler
+ #[serde(rename = "request_body")]
+ RequestBody {
+ max_size: Option,
+ },
+ /// Response compression
+ #[serde(rename = "encode")]
+ Encode {
+ encodings: Option>,
+ prefer: Option>,
+ minimum_length: Option,
+ },
+ /// Template handler
+ #[serde(rename = "templates")]
+ Templates {
+ file_root: Option,
+ mime_types: Option>,
+ delimiters: Option>,
+ },
+ /// Subroute handler
+ #[serde(rename = "subroute")]
+ Subroute {
+ routes: Vec,
+ errors: Option,
+ },
+ /// Error handler
+ #[serde(rename = "error")]
+ Error {
+ error: Option,
+ status_code: Option,
+ },
+ /// Map handler
+ #[serde(rename = "map")]
+ Map {
+ source: String,
+ destinations: HashMap,
+ default: Option,
+ },
+ /// Rate limit handler
+ #[serde(rename = "rate_limit")]
+ RateLimit {
+ key: Option,
+ rate: Option,
+ burst: Option,
+ window: Option,
+ },
+ /// IP whitelist handler
+ #[serde(rename = "ip_whitelist")]
+ IpWhitelist {
+ source: Option,
+ rules: Vec,
+ },
+ /// Request ID handler
+ #[serde(rename = "request_id")]
+ RequestId {
+ header_name: Option,
+ size: Option,
+ },
+ /// Metrics handler
+ #[serde(rename = "metrics")]
+ Metrics {
+ path: Option,
+ },
+ /// Health check handler
+ #[serde(rename = "health")]
+ Health {
+ path: Option,
+ },
+ /// Vars handler
+ #[serde(rename = "vars")]
+ Vars {
+ #[serde(flatten)]
+ variables: HashMap,
+ },
+ /// Custom handler
+ #[serde(rename = "custom")]
+ Custom {
+ module: String,
+ #[serde(flatten)]
+ config: HashMap,
+ },
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(untagged)]
+pub enum Matcher {
+ /// Host matcher
+ Host(Vec),
+ /// Path matcher
+ Path(Vec),
+ /// Path regexp matcher
+ PathRegexp(Vec),
+ /// Method matcher
+ Method(Vec),
+ /// Query matcher
+ Query(HashMap>),
+ /// Header matcher
+ Header(HashMap>),
+ /// Header regexp matcher
+ HeaderRegexp(HashMap>),
+ /// Remote IP matcher
+ RemoteIp {
+ ranges: Vec,
+ forwarded: Option,
+ },
+ /// Protocol matcher
+ Protocol(String),
+ /// File matcher
+ File {
+ root: Option,
+ files: Vec,
+ try_files: Option>,
+ try_policy: Option,
+ split_path: Option>,
+ },
+ /// Expression matcher
+ Expression {
+ expr: String,
+ },
+ /// Vars matcher
+ Vars(HashMap),
+ /// Not matcher
+ Not {
+ #[serde(rename = "match")]
+ matcher: Box,
+ },
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct AuthProvider {
+ #[serde(flatten)]
+ pub config: HashMap,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct BasicAuthAccount {
+ pub username: String,
+ pub password: String,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct BasicAuthHash {
+ pub algorithm: Option,
+ pub cost: Option,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct BrowseConfig {
+ pub template: Option,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct PrecompressedConfig {
+ pub encodings: Option>,
+ pub min_length: Option,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct Upstream {
+ pub dial: String,
+ pub max_requests: Option,
+ pub max_requests_per_host: Option,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct LoadBalancing {
+ pub selection_policy: Option,
+ pub try_duration: Option,
+ pub try_interval: Option,
+ pub unhealthy_request_count: Option,
+ pub unhealthy_status: Option>,
+ pub unhealthy_latency: Option,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case")]
+pub enum SelectionPolicy {
+ RoundRobin,
+ LeastConn,
+ Random,
+ First,
+ IpHash,
+ UriHash,
+ Header,
+ Cookie,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct HealthChecks {
+ pub active: Option,
+ pub passive: Option,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct ActiveHealthCheck {
+ pub uri: String,
+ pub port: Option,
+ pub headers: Option>>,
+ pub interval: Option,
+ pub timeout: Option,
+ pub max_size: Option,
+ pub expect_status: Option,
+ pub expect_body: Option,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct PassiveHealthCheck {
+ pub unhealthy_status: Option>,
+ pub unhealthy_latency: Option,
+ pub unhealthy_request_count: Option,
+ pub healthy_count: Option,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct CircuitBreaker {
+ pub trip_duration: Option,
+ pub recovery_duration: Option,
+ pub failure_threshold: Option,
+ pub success_threshold: Option,
+ pub latency_threshold: Option,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct HeaderOperations {
+ pub add: Option>>,
+ pub set: Option>>,
+ pub delete: Option>,
+ pub replace: Option>>,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct HeaderReplacement {
+ pub search: String,
+ pub replace: String,
+ pub search_regexp: Option,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct Transport {
+ pub protocol: Option,
+ pub tls: Option,
+ pub keep_alive: Option,
+ pub compression: Option,
+ pub max_conns_per_host: Option,
+ pub dial_timeout: Option,
+ pub dial_fallback_delay: Option,
+ pub response_header_timeout: Option,
+ pub expect_continue_timeout: Option,
+ pub max_response_header_size: Option,
+ pub write_buffer_size: Option,
+ pub read_buffer_size: Option,
+ pub versions: Option>,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct TransportTls {
+ pub client_certificate_file: Option,
+ pub client_certificate_key_file: Option,
+ pub client_certificate_automate: Option,
+ pub root_ca_pool: Option>,
+ pub root_ca_pem_files: Option>,
+ pub server_name: Option,
+ pub insecure_skip_verify: Option,
+ pub handshake_timeout: Option,
+ pub versions: Option>,
+ pub cipher_suites: Option>,
+ pub curves: Option>,
+ pub alpn: Option>,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct KeepAlive {
+ pub enabled: Option,
+ pub probe_interval: Option,
+ pub max_idle_conns: Option,
+ pub max_idle_conns_per_host: Option,
+ pub idle_conn_timeout: Option,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct ResponseHandler {
+ #[serde(rename = "match")]
+ pub match_rules: Option,
+ pub routes: Vec,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct ResponseMatcher {
+ pub status_code: Option>,
+ pub headers: Option>>,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct StatusReplacement {
+ pub status_code: i32,
+ pub with: i32,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct UriSubstring {
+ pub find: String,
+ pub replace: String,
+ pub limit: Option,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct EncodingConfig {
+ #[serde(flatten)]
+ pub config: HashMap,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct IpRule {
+ pub action: String,
+ pub rule: String,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct ErrorHandling {
+ pub routes: Vec,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct TlsConnectionPolicy {
+ #[serde(rename = "match")]
+ pub match_rules: Option,
+ pub certificate_selection: Option,
+ pub cipher_suites: Option>,
+ pub curves: Option>,
+ pub alpn: Option>,
+ pub protocols: Option,
+ pub client_authentication: Option,
+ pub insecure_secrets_log: Option,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct TlsConnectionMatcher {
+ pub sni: Option>,
+ pub remote_ip: Option,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct RemoteIpMatcher {
+ pub ranges: Vec,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct CertificateSelection {
+ pub any_tag: Option>,
+ pub all_tags: Option>,
+ pub public_key_algorithm: Option,
+ pub serial_number: Option,
+ pub subject_organization: Option,
+ pub subject: Option>,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct ProtocolRange {
+ pub min: Option,
+ pub max: Option,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct ClientAuthentication {
+ pub mode: Option,
+ pub trusted_ca_certs: Option>,
+ pub trusted_ca_certs_pem_files: Option>,
+ pub trusted_leaf_certs: Option>,
+ pub trusted_leaf_certs_pem_files: Option>,
+ pub verify_client_certificate: Option,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct AutomaticHttpsConfig {
+ pub disable: Option,
+ pub disable_redirects: Option,
+ pub disable_certs: Option,
+ pub ignore_loaded_certs: Option,
+ pub skip: Option>,
+ pub skip_certificates: Option>,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct TlsApp {
+ pub automation: Option,
+ pub session_tickets: Option,
+ pub certificates: Option,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct AutomationConfig {
+ pub policies: Vec,
+ pub on_demand: Option,
+ pub ocsp_interval: Option,
+ pub renew_ahead: Option,
+ pub storage: Option,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct AutomationPolicy {
+ pub subjects: Option>,
+ pub issuer: Option,
+ pub must_staple: Option,
+ pub key_type: Option,
+ pub storage: Option,
+ pub on_demand: Option,
+ pub disable: Option,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(tag = "module")]
+pub enum IssuerConfig {
+ /// ACME issuer
+ #[serde(rename = "acme")]
+ Acme {
+ ca: Option,
+ test_ca: Option,
+ email: Option,
+ account_key_pem: Option,
+ external_account: Option,
+ challenges: Option,
+ preferred_chains: Option,
+ must_staple: Option,
+ trusted_roots_pem_files: Option>,
+ },
+ /// Internal issuer
+ #[serde(rename = "internal")]
+ Internal {
+ ca: Option,
+ lifetime: Option,
+ sign_with_root: Option,
+ },
+ /// External issuer
+ #[serde(rename = "external")]
+ External {
+ command: Vec,
+ timeout: Option,
+ env: Option>,
+ },
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct ExternalAccount {
+ pub key_id: String,
+ pub mac_key: String,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct ChallengeConfig {
+ pub http: Option,
+ pub dns: Option,
+ pub tls_alpn: Option,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct HttpChallengeConfig {
+ pub disabled: Option