Update scripts/generate-caddyfile.sh
This commit is contained in:
@@ -13,6 +13,9 @@ fi
|
||||
APPFACTORY_DIR="${APPFACTORY_DIR:-/opt/appfactory}"
|
||||
CATALOG_FILE="$APPFACTORY_DIR/apps/catalog.yml"
|
||||
CADDY_FILE="$APPFACTORY_DIR/gateway/Caddyfile"
|
||||
DB_FILE="$APPFACTORY_DIR/data/appfactory/appfactory.db"
|
||||
STATIC_DIR="$APPFACTORY_DIR/gateway/static"
|
||||
APPS_JSON_FILE="$STATIC_DIR/apps.json"
|
||||
|
||||
APPFACTORY_ENABLE_HTTPS="${APPFACTORY_ENABLE_HTTPS:-false}"
|
||||
APPFACTORY_PORTAL_DOMAIN="${APPFACTORY_PORTAL_DOMAIN:-}"
|
||||
@@ -20,7 +23,7 @@ APPFACTORY_GITEA_DOMAIN="${APPFACTORY_GITEA_DOMAIN:-}"
|
||||
APPFACTORY_REGISTRY_DOMAIN="${APPFACTORY_REGISTRY_DOMAIN:-}"
|
||||
|
||||
mkdir -p "$(dirname "$CADDY_FILE")"
|
||||
mkdir -p "$APPFACTORY_DIR/gateway/static"
|
||||
mkdir -p "$STATIC_DIR"
|
||||
|
||||
write_site_header() {
|
||||
local site="$1"
|
||||
@@ -36,18 +39,22 @@ EOF_CADDY
|
||||
fi
|
||||
}
|
||||
|
||||
write_app_routes() {
|
||||
local target_file="$1"
|
||||
|
||||
write_apps_json() {
|
||||
if [ ! -f "$CATALOG_FILE" ]; then
|
||||
cat > "$APPS_JSON_FILE" <<'EOF_JSON'
|
||||
{
|
||||
"apps": []
|
||||
}
|
||||
EOF_JSON
|
||||
return 0
|
||||
fi
|
||||
|
||||
python3 - "$CATALOG_FILE" "$target_file" <<'PY'
|
||||
python3 - "$CATALOG_FILE" "$APPS_JSON_FILE" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
|
||||
catalog_file = sys.argv[1]
|
||||
caddy_file = sys.argv[2]
|
||||
apps_json_file = sys.argv[2]
|
||||
|
||||
apps = []
|
||||
current = None
|
||||
@@ -67,6 +74,147 @@ with open(catalog_file, "r", encoding="utf-8") as f:
|
||||
if current:
|
||||
apps.append(current)
|
||||
|
||||
result = {"apps": []}
|
||||
|
||||
for app in apps:
|
||||
app_id = app.get("id")
|
||||
if not app_id:
|
||||
continue
|
||||
|
||||
name = app.get("name") or app_id
|
||||
base_path = app.get("base_path") or f"/apps/{app_id}"
|
||||
health = app.get("health") or f"{base_path}/health"
|
||||
docs = app.get("docs") or f"{base_path}/docs"
|
||||
|
||||
result["apps"].append({
|
||||
"id": app_id,
|
||||
"name": name,
|
||||
"base_url": base_path,
|
||||
"health_url": health,
|
||||
"docs_url": docs,
|
||||
})
|
||||
|
||||
with open(apps_json_file, "w", encoding="utf-8") as f:
|
||||
json.dump(result, f, ensure_ascii=False, indent=2)
|
||||
f.write("\n")
|
||||
PY
|
||||
}
|
||||
|
||||
write_app_routes() {
|
||||
local target_file="$1"
|
||||
|
||||
if [ ! -f "$CATALOG_FILE" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
python3 - "$CATALOG_FILE" "$target_file" "$DB_FILE" <<'PY'
|
||||
import re
|
||||
import sqlite3
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
|
||||
catalog_file = sys.argv[1]
|
||||
caddy_file = sys.argv[2]
|
||||
db_file = sys.argv[3]
|
||||
|
||||
READ_METHODS = ["GET"]
|
||||
WRITE_METHODS = ["POST", "PUT", "PATCH", "DELETE"]
|
||||
ALL_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"]
|
||||
|
||||
OPEN_PATH_SUFFIXES = [
|
||||
"/health",
|
||||
"/docs",
|
||||
"/docs/*",
|
||||
"/openapi.json",
|
||||
"/swagger.json",
|
||||
"/swagger/*",
|
||||
]
|
||||
|
||||
apps = []
|
||||
current = None
|
||||
|
||||
with open(catalog_file, "r", encoding="utf-8") as f:
|
||||
for raw in f:
|
||||
stripped = raw.strip()
|
||||
|
||||
if stripped.startswith("- id:"):
|
||||
if current:
|
||||
apps.append(current)
|
||||
current = {"id": stripped.split(":", 1)[1].strip().strip('"')}
|
||||
elif current and ":" in stripped:
|
||||
key, value = stripped.split(":", 1)
|
||||
current[key.strip()] = value.strip().strip('"')
|
||||
|
||||
if current:
|
||||
apps.append(current)
|
||||
|
||||
|
||||
def safe_matcher_name(value: str) -> str:
|
||||
value = re.sub(r"[^a-zA-Z0-9_]+", "_", value)
|
||||
value = value.strip("_")
|
||||
if not value:
|
||||
value = "app"
|
||||
if value[0].isdigit():
|
||||
value = "app_" + value
|
||||
return value
|
||||
|
||||
|
||||
def parse_methods(value: str) -> set[str]:
|
||||
value = (value or "").strip().upper()
|
||||
result: set[str] = set()
|
||||
|
||||
if not value:
|
||||
return result
|
||||
|
||||
for part in value.split(","):
|
||||
part = part.strip().upper()
|
||||
if not part:
|
||||
continue
|
||||
if part == "READ":
|
||||
result.update(READ_METHODS)
|
||||
elif part == "WRITE":
|
||||
result.update(WRITE_METHODS)
|
||||
elif part == "ALL":
|
||||
result.update(ALL_METHODS)
|
||||
else:
|
||||
result.add(part)
|
||||
|
||||
return {m for m in result if m in ALL_METHODS}
|
||||
|
||||
|
||||
rules_by_app: dict[str, list[dict]] = defaultdict(list)
|
||||
|
||||
try:
|
||||
con = sqlite3.connect(db_file)
|
||||
con.row_factory = sqlite3.Row
|
||||
|
||||
rows = con.execute(
|
||||
"""
|
||||
SELECT app_id, ip_cidr, methods
|
||||
FROM app_ip_access_rules
|
||||
WHERE is_enabled = 1
|
||||
ORDER BY app_id, id
|
||||
"""
|
||||
).fetchall()
|
||||
|
||||
for row in rows:
|
||||
app_id = (row["app_id"] or "").strip()
|
||||
ip_cidr = (row["ip_cidr"] or "").strip()
|
||||
methods = parse_methods(row["methods"] or "")
|
||||
|
||||
if not app_id or not ip_cidr or not methods:
|
||||
continue
|
||||
|
||||
rules_by_app[app_id].append({
|
||||
"ip_cidr": ip_cidr,
|
||||
"methods": methods,
|
||||
})
|
||||
|
||||
con.close()
|
||||
except Exception as exc:
|
||||
print(f"WARN: Unable to read app_ip_access_rules from {db_file}: {exc}", file=sys.stderr)
|
||||
|
||||
|
||||
with open(caddy_file, "a", encoding="utf-8") as f:
|
||||
for app in apps:
|
||||
app_id = app.get("id")
|
||||
@@ -75,6 +223,41 @@ with open(caddy_file, "a", encoding="utf-8") as f:
|
||||
if not app_id:
|
||||
continue
|
||||
|
||||
rules = rules_by_app.get(app_id, [])
|
||||
safe_name = safe_matcher_name(app_id)
|
||||
open_paths = [f"/apps/{app_id}{suffix}" for suffix in OPEN_PATH_SUFFIXES]
|
||||
|
||||
if rules:
|
||||
allowed_by_method = {method: [] for method in ALL_METHODS}
|
||||
|
||||
for rule in rules:
|
||||
for method in rule["methods"]:
|
||||
allowed_by_method.setdefault(method, []).append(rule["ip_cidr"])
|
||||
|
||||
for method in WRITE_METHODS:
|
||||
matcher = f"blocked_{safe_name}_{method.lower()}"
|
||||
allowed_ips = sorted(set(allowed_by_method.get(method, [])))
|
||||
|
||||
f.write(f" @{matcher} {{\n")
|
||||
f.write(f" path /apps/{app_id}/*\n")
|
||||
f.write(f" method {method}\n")
|
||||
f.write(" not path " + " ".join(open_paths) + "\n")
|
||||
if allowed_ips:
|
||||
f.write(" not remote_ip " + " ".join(allowed_ips) + "\n")
|
||||
f.write(" }\n")
|
||||
f.write(f" respond @{matcher} \"Forbidden\" 403\n\n")
|
||||
|
||||
get_allowed_ips = sorted(set(allowed_by_method.get("GET", [])))
|
||||
if get_allowed_ips:
|
||||
matcher = f"blocked_{safe_name}_get"
|
||||
f.write(f" @{matcher} {{\n")
|
||||
f.write(f" path /apps/{app_id}/*\n")
|
||||
f.write(" method GET\n")
|
||||
f.write(" not path " + " ".join(open_paths) + "\n")
|
||||
f.write(" not remote_ip " + " ".join(get_allowed_ips) + "\n")
|
||||
f.write(" }\n")
|
||||
f.write(f" respond @{matcher} \"Forbidden\" 403\n\n")
|
||||
|
||||
f.write(f" handle_path /apps/{app_id}/* {{\n")
|
||||
f.write(f" reverse_proxy {app_id}:{port}\n")
|
||||
f.write(" }\n\n")
|
||||
@@ -98,7 +281,15 @@ write_portal_routes() {
|
||||
|
||||
handle /apps {
|
||||
root * /srv/static
|
||||
rewrite * /apps.html
|
||||
rewrite * /apps.json
|
||||
header Content-Type application/json
|
||||
file_server
|
||||
}
|
||||
|
||||
handle /apps/ {
|
||||
root * /srv/static
|
||||
rewrite * /apps.json
|
||||
header Content-Type application/json
|
||||
file_server
|
||||
}
|
||||
|
||||
@@ -199,12 +390,15 @@ EOF_CADDY
|
||||
fi
|
||||
}
|
||||
|
||||
write_apps_json
|
||||
|
||||
if [ -n "$APPFACTORY_PORTAL_DOMAIN" ] || [ -n "$APPFACTORY_GITEA_DOMAIN" ] || [ -n "$APPFACTORY_REGISTRY_DOMAIN" ]; then
|
||||
write_domain_caddyfile
|
||||
else
|
||||
write_http_only_caddyfile
|
||||
fi
|
||||
|
||||
echo "Generated: $APPS_JSON_FILE"
|
||||
echo "Generated: $CADDY_FILE"
|
||||
|
||||
if docker inspect appfactory-caddy >/dev/null 2>&1; then
|
||||
@@ -212,4 +406,4 @@ if docker inspect appfactory-caddy >/dev/null 2>&1; then
|
||||
docker exec appfactory-caddy caddy reload --config /etc/caddy/Caddyfile
|
||||
else
|
||||
echo "WARN: appfactory-caddy container not found, skipping validate/reload"
|
||||
fi
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user