Update scripts/generate-caddyfile.sh
This commit is contained in:
@@ -13,6 +13,9 @@ fi
|
|||||||
APPFACTORY_DIR="${APPFACTORY_DIR:-/opt/appfactory}"
|
APPFACTORY_DIR="${APPFACTORY_DIR:-/opt/appfactory}"
|
||||||
CATALOG_FILE="$APPFACTORY_DIR/apps/catalog.yml"
|
CATALOG_FILE="$APPFACTORY_DIR/apps/catalog.yml"
|
||||||
CADDY_FILE="$APPFACTORY_DIR/gateway/Caddyfile"
|
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_ENABLE_HTTPS="${APPFACTORY_ENABLE_HTTPS:-false}"
|
||||||
APPFACTORY_PORTAL_DOMAIN="${APPFACTORY_PORTAL_DOMAIN:-}"
|
APPFACTORY_PORTAL_DOMAIN="${APPFACTORY_PORTAL_DOMAIN:-}"
|
||||||
@@ -20,7 +23,7 @@ APPFACTORY_GITEA_DOMAIN="${APPFACTORY_GITEA_DOMAIN:-}"
|
|||||||
APPFACTORY_REGISTRY_DOMAIN="${APPFACTORY_REGISTRY_DOMAIN:-}"
|
APPFACTORY_REGISTRY_DOMAIN="${APPFACTORY_REGISTRY_DOMAIN:-}"
|
||||||
|
|
||||||
mkdir -p "$(dirname "$CADDY_FILE")"
|
mkdir -p "$(dirname "$CADDY_FILE")"
|
||||||
mkdir -p "$APPFACTORY_DIR/gateway/static"
|
mkdir -p "$STATIC_DIR"
|
||||||
|
|
||||||
write_site_header() {
|
write_site_header() {
|
||||||
local site="$1"
|
local site="$1"
|
||||||
@@ -36,18 +39,22 @@ EOF_CADDY
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
write_app_routes() {
|
write_apps_json() {
|
||||||
local target_file="$1"
|
|
||||||
|
|
||||||
if [ ! -f "$CATALOG_FILE" ]; then
|
if [ ! -f "$CATALOG_FILE" ]; then
|
||||||
|
cat > "$APPS_JSON_FILE" <<'EOF_JSON'
|
||||||
|
{
|
||||||
|
"apps": []
|
||||||
|
}
|
||||||
|
EOF_JSON
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
python3 - "$CATALOG_FILE" "$target_file" <<'PY'
|
python3 - "$CATALOG_FILE" "$APPS_JSON_FILE" <<'PY'
|
||||||
|
import json
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
catalog_file = sys.argv[1]
|
catalog_file = sys.argv[1]
|
||||||
caddy_file = sys.argv[2]
|
apps_json_file = sys.argv[2]
|
||||||
|
|
||||||
apps = []
|
apps = []
|
||||||
current = None
|
current = None
|
||||||
@@ -67,6 +74,147 @@ with open(catalog_file, "r", encoding="utf-8") as f:
|
|||||||
if current:
|
if current:
|
||||||
apps.append(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:
|
with open(caddy_file, "a", encoding="utf-8") as f:
|
||||||
for app in apps:
|
for app in apps:
|
||||||
app_id = app.get("id")
|
app_id = app.get("id")
|
||||||
@@ -75,6 +223,41 @@ with open(caddy_file, "a", encoding="utf-8") as f:
|
|||||||
if not app_id:
|
if not app_id:
|
||||||
continue
|
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" handle_path /apps/{app_id}/* {{\n")
|
||||||
f.write(f" reverse_proxy {app_id}:{port}\n")
|
f.write(f" reverse_proxy {app_id}:{port}\n")
|
||||||
f.write(" }\n\n")
|
f.write(" }\n\n")
|
||||||
@@ -98,7 +281,15 @@ write_portal_routes() {
|
|||||||
|
|
||||||
handle /apps {
|
handle /apps {
|
||||||
root * /srv/static
|
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
|
file_server
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,12 +390,15 @@ EOF_CADDY
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
write_apps_json
|
||||||
|
|
||||||
if [ -n "$APPFACTORY_PORTAL_DOMAIN" ] || [ -n "$APPFACTORY_GITEA_DOMAIN" ] || [ -n "$APPFACTORY_REGISTRY_DOMAIN" ]; then
|
if [ -n "$APPFACTORY_PORTAL_DOMAIN" ] || [ -n "$APPFACTORY_GITEA_DOMAIN" ] || [ -n "$APPFACTORY_REGISTRY_DOMAIN" ]; then
|
||||||
write_domain_caddyfile
|
write_domain_caddyfile
|
||||||
else
|
else
|
||||||
write_http_only_caddyfile
|
write_http_only_caddyfile
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
echo "Generated: $APPS_JSON_FILE"
|
||||||
echo "Generated: $CADDY_FILE"
|
echo "Generated: $CADDY_FILE"
|
||||||
|
|
||||||
if docker inspect appfactory-caddy >/dev/null 2>&1; then
|
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
|
docker exec appfactory-caddy caddy reload --config /etc/caddy/Caddyfile
|
||||||
else
|
else
|
||||||
echo "WARN: appfactory-caddy container not found, skipping validate/reload"
|
echo "WARN: appfactory-caddy container not found, skipping validate/reload"
|
||||||
fi
|
fi
|
||||||
|
|||||||
Reference in New Issue
Block a user