46602f1933
Remove dead code identified in docs/SANITIZATION-REVIEW.md:
- app/blueprints/content_old.py and app/blueprints/playlist.py
- app/models/group.py, app/utils/nginx_config_reader.py
- orphaned templates (content_list, edit_content, upload_content,
player_page) and the related group/Template references
Result: 6 blueprints, 82 routes, no dead modules or orphan templates.
Add files that deploy.sh and docker-entrypoint.sh already require but
which were never tracked:
- https_manager.py (referenced by deploy.sh, migrate_network.sh,
docker-entrypoint.sh)
- Caddyfile.example (seeded by deploy.sh; its absence aborts deploy)
Relocate generated Graphify artifacts from graphify-out/ to
docs/graphify-out/ (110 files, no content change) and archive the
superseded docs under docs/.
Ignore hygiene:
- ignore ad-hoc .env backups (.env.bak*) — they contain live secrets
- keep the pre-sanitization snapshots (docs/legacy code/,
docs/old_code_documentation/) on disk but out of the repo
Fix .env.example: drop a duplicated config block, genericize the
hardcoded host IP, and document HOSTNAME_INTERNAL.
192 lines
7.9 KiB
Python
192 lines
7.9 KiB
Python
"""Caddy configuration generator and manager."""
|
|
import os
|
|
from typing import Optional
|
|
from app.models.https_config import HTTPSConfig
|
|
|
|
# Shared reverse-proxy snippet used in every Caddy site block
|
|
_PROXY_SNIPPET = """\
|
|
reverse_proxy digiserver-app:5000 {
|
|
header_up Host {host}
|
|
header_up X-Real-IP {remote_host}
|
|
header_up X-Forwarded-Proto {scheme}
|
|
transport http {
|
|
read_timeout 300s
|
|
write_timeout 300s
|
|
}
|
|
}
|
|
|
|
request_body {
|
|
max_size 2GB
|
|
}
|
|
|
|
encode gzip
|
|
|
|
header {
|
|
X-Frame-Options "SAMEORIGIN"
|
|
X-Content-Type-Options "nosniff"
|
|
X-XSS-Protection "1; mode=block"
|
|
}
|
|
|
|
log {
|
|
output file /var/log/caddy/access.log
|
|
}
|
|
"""
|
|
|
|
|
|
class CaddyConfigGenerator:
|
|
"""Generate Caddyfile configuration based on HTTPSConfig."""
|
|
|
|
@staticmethod
|
|
def generate_caddyfile(config: Optional['HTTPSConfig'] = None,
|
|
http_fallback: bool = True,
|
|
http_port: int = 80,
|
|
https_port: int = 443) -> str:
|
|
"""Generate a complete Caddyfile.
|
|
|
|
Design goals
|
|
------------
|
|
* **One HTTP endpoint** (port 80) that always answers, whatever the Host
|
|
header is — so ``http://<ip>`` and ``http://<hostname>`` both work.
|
|
* **HTTPS on port 443** for the same names when it is enabled.
|
|
* If HTTPS is disabled or never configured, port 80 simply serves the
|
|
app — there is no separate "HTTP mode" to configure.
|
|
|
|
Behaviour by configuration
|
|
--------------------------
|
|
* HTTPS off, or no address configured → plain HTTP on ``:http_port``.
|
|
* HTTPS on → the app is served on port 80 for every configured name and
|
|
on port 443 over TLS. Whether port 80 *serves* or *redirects* to
|
|
HTTPS is controlled by ``http_fallback``.
|
|
|
|
Which certificate each name gets
|
|
--------------------------------
|
|
* ``domain`` (when set) → Caddy obtains a certificate automatically
|
|
(Let's Encrypt/ACME). Only valid for a **publicly resolvable** name.
|
|
* ``ip_address`` / ``hostname`` → ``tls internal`` (Caddy's local CA).
|
|
This needs no public DNS and no ACME, which is the right choice for an
|
|
intranet name such as ``digiserver.sibiusb.harting.intra``.
|
|
|
|
Args:
|
|
config: HTTPSConfig instance, or None to load from the database.
|
|
http_fallback: When True, port 80 keeps *serving* the app alongside
|
|
HTTPS. This is the resilient default: clients that cannot trust
|
|
the internal CA (e.g. a Kivy player with ``verify_ssl: true``)
|
|
are still able to connect. When False, port 80 issues a 301
|
|
redirect to HTTPS instead.
|
|
http_port: Port Caddy listens on for plain HTTP (default 80).
|
|
https_port: Port used to build redirect targets when
|
|
``http_fallback`` is False (default 443).
|
|
|
|
Returns:
|
|
The complete Caddyfile as a string.
|
|
"""
|
|
if config is None:
|
|
config = HTTPSConfig.get_config()
|
|
|
|
email = (config.email or "admin@localhost") if config else "admin@localhost"
|
|
https_enabled = bool(config.https_enabled) if config else False
|
|
domain = (config.domain or "").strip() if config else ""
|
|
ip_address = (config.ip_address or "").strip() if config else ""
|
|
hostname = (config.hostname or "").strip() if config else ""
|
|
|
|
# Every name the server should answer to, in priority order, without
|
|
# duplicates. The IP comes first because it always resolves.
|
|
names: list[str] = []
|
|
for candidate in (ip_address, hostname, domain):
|
|
if candidate and candidate not in names:
|
|
names.append(candidate)
|
|
|
|
global_block = f"{{\n admin 0.0.0.0:2019\n email {email}\n"
|
|
|
|
# ── TLS with no SNI ────────────────────────────────────────────────
|
|
# Browsers do NOT send SNI when the URL is an IP address (an IP is not
|
|
# a valid SNI hostname). Without a fallback Caddy would identify such a
|
|
# connection by the container's own internal IP, match no certificate
|
|
# and abort the handshake with:
|
|
# "no certificate available for '<container-ip>'"
|
|
# `default_sni` makes a SNI-less ClientHello resolve to a name we do
|
|
# serve, so https://<ip> works in the browser.
|
|
if https_enabled and not domain and ip_address:
|
|
global_block += f" default_sni {ip_address}\n"
|
|
|
|
global_block += "}\n\n"
|
|
|
|
# ── Plain HTTP only: HTTPS disabled, or no address to certify ───────
|
|
if not (https_enabled and names):
|
|
return global_block + f":{http_port} {{\n{_PROXY_SNIPPET}}}\n"
|
|
|
|
caddyfile = global_block
|
|
|
|
# ── Port 80: catch-all so ANY Host header is answered ──────────────
|
|
# Without this, a request for an unexpected name (e.g. a bare IP when
|
|
# only a hostname is configured) would hit no site block and fail.
|
|
caddyfile += f":{http_port} {{\n{_PROXY_SNIPPET}}}\n\n"
|
|
|
|
# ── Port 80: explicit per-name blocks ──────────────────────────────
|
|
for name in names:
|
|
if http_fallback:
|
|
caddyfile += f"http://{name} {{\n{_PROXY_SNIPPET}}}\n\n"
|
|
else:
|
|
# Redirect to the port the host actually publishes.
|
|
https_url = (f"https://{name}" if https_port == 443
|
|
else f"https://{name}:{https_port}")
|
|
caddyfile += f"http://{name} {{\n redir {https_url}{{uri}} 301\n}}\n\n"
|
|
|
|
# ── Port 443: TLS listeners ────────────────────────────────────────
|
|
for name in names:
|
|
if domain and name == domain:
|
|
# Public name → let Caddy obtain a real certificate.
|
|
caddyfile += f"https://{name} {{\n{_PROXY_SNIPPET}}}\n\n"
|
|
else:
|
|
# IP or intranet name → Caddy's internal CA.
|
|
caddyfile += (f"https://{name} {{\n tls internal\n"
|
|
f"{_PROXY_SNIPPET}}}\n\n")
|
|
|
|
return caddyfile
|
|
|
|
@staticmethod
|
|
def write_caddyfile(caddyfile_content: str,
|
|
path: str = '/etc/caddy/Caddyfile') -> bool:
|
|
"""Write Caddyfile to disk.
|
|
|
|
The default path is /etc/caddy/Caddyfile — the standard location inside
|
|
the caddy:2-alpine container when a volume is mounted there.
|
|
"""
|
|
try:
|
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
with open(path, 'w') as f:
|
|
f.write(caddyfile_content)
|
|
return True
|
|
except Exception as e:
|
|
print(f"Error writing Caddyfile: {str(e)}")
|
|
return False
|
|
|
|
@staticmethod
|
|
def reload_caddy() -> bool:
|
|
"""Push the current Caddyfile to Caddy via its admin API (/load).
|
|
|
|
Caddy applies the new config live without dropping connections.
|
|
"""
|
|
try:
|
|
import urllib.request
|
|
|
|
caddyfile_path = '/etc/caddy/Caddyfile'
|
|
if not os.path.exists(caddyfile_path):
|
|
print(f"Caddyfile not found at {caddyfile_path}")
|
|
return False
|
|
|
|
with open(caddyfile_path, 'rb') as f:
|
|
caddyfile_bytes = f.read()
|
|
|
|
req = urllib.request.Request(
|
|
'http://caddy:2019/load',
|
|
data=caddyfile_bytes,
|
|
headers={'Content-Type': 'text/caddyfile'},
|
|
method='POST',
|
|
)
|
|
response = urllib.request.urlopen(req, timeout=10)
|
|
return response.status == 200
|
|
except Exception as e:
|
|
print(f"Caddy reload error: {str(e)}")
|
|
|