Sanitize codebase, reorganize docs, and add missing deploy files

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.
This commit is contained in:
2026-09-11 12:18:34 +03:00
parent 1c5186463a
commit 46602f1933
226 changed files with 3999 additions and 15737 deletions
+93 -170
View File
@@ -37,43 +37,110 @@ class CaddyConfigGenerator:
"""Generate Caddyfile configuration based on HTTPSConfig."""
@staticmethod
def generate_caddyfile(config: Optional['HTTPSConfig'] = None) -> str:
def generate_caddyfile(config: Optional['HTTPSConfig'] = None,
http_fallback: bool = True,
http_port: int = 80,
https_port: int = 443) -> str:
"""Generate a complete Caddyfile.
Behaviour:
- HTTPS disabled / no domain → HTTP-only on port 80 (initial deploy mode).
- HTTPS enabled + real domain → Caddy auto-provisions a Let's Encrypt cert
for that domain; HTTP redirects to HTTPS automatically.
- HTTPS enabled + IP only (no domain) → TLS with Caddy's internal CA
(self-signed, trusted within the Docker network).
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 = config.https_enabled if config else False
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 ""
global_block = f"""{{\n admin 0.0.0.0:2019\n email {email}\n}}\n\n"""
# 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)
if https_enabled and domain:
# Caddy handles Let's Encrypt + HTTP→HTTPS redirect automatically
# when a plain hostname (no scheme) is used.
caddyfile = global_block
caddyfile += f"{domain} {{\n{_PROXY_SNIPPET}}}\n"
# Also accept requests on the raw IP (HTTP only, no cert needed)
if ip_address:
caddyfile += f"\nhttp://{ip_address} {{\n{_PROXY_SNIPPET}}}\n"
elif https_enabled and ip_address:
# No public domain — use Caddy's internal CA (self-signed)
caddyfile = global_block
caddyfile += f"https://{ip_address} {{\n tls internal\n{_PROXY_SNIPPET}}}\n"
caddyfile += f"\nhttp://{ip_address} {{\n redir https://{ip_address}{{uri}} 301\n}}\n"
else:
# HTTP-only fallback (first deploy, before HTTPS is configured)
caddyfile = "{\n admin 0.0.0.0:2019\n}\n\n"
caddyfile += f":80 {{\n{_PROXY_SNIPPET}}}\n"
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
@@ -121,148 +188,4 @@ class CaddyConfigGenerator:
return response.status == 200
except Exception as e:
print(f"Caddy reload error: {str(e)}")
return False
"""Generate complete Caddyfile content.
Args:
config: HTTPSConfig instance or None
Returns:
Complete Caddyfile content as string
"""
# Get config from database if not provided
if config is None:
config = HTTPSConfig.get_config()
# Base configuration
email = "admin@localhost"
if config and config.email:
email = config.email
base_config = f"""{{
# Global options
email {email}
# Admin API for configuration management (listen on all interfaces)
admin 0.0.0.0:2019
# Uncomment for testing to avoid rate limits
# acme_ca https://acme-staging-v02.api.letsencrypt.org/directory
}}
# Shared reverse proxy configuration
(reverse_proxy_config) {{
reverse_proxy digiserver-app:5000 {{
header_up Host {{host}}
header_up X-Real-IP {{remote_host}}
header_up X-Forwarded-Proto {{scheme}}
# Timeouts for large uploads
transport http {{
read_timeout 300s
write_timeout 300s
}}
}}
# File upload size limit (2GB)
request_body {{
max_size 2GB
}}
# Security headers
header {{
X-Frame-Options "SAMEORIGIN"
X-Content-Type-Options "nosniff"
X-XSS-Protection "1; mode=block"
}}
# Logging
log {{
output file /var/log/caddy/access.log
}}
}}
# Localhost (development/local access)
http://localhost {{
import reverse_proxy_config
}}
"""
# Add main domain/IP configuration if HTTPS is enabled
if config and config.https_enabled and config.domain and config.ip_address:
# Internal domain configuration
domain_config = f"""
# Internal domain (HTTP only - internal use)
http://{config.domain} {{
import reverse_proxy_config
}}
# Handle IP address access
http://{config.ip_address} {{
import reverse_proxy_config
}}
"""
base_config += domain_config
else:
# Default fallback configuration
base_config += """
# Internal domain (HTTP only - internal use)
http://digiserver.sibiusb.harting.intra {
import reverse_proxy_config
}
# Handle IP address access
http://10.76.152.164 {
import reverse_proxy_config
}
"""
# Add catch-all for any other HTTP requests
base_config += """
# Catch-all for any other HTTP requests
http://* {
import reverse_proxy_config
}
"""
return base_config
@staticmethod
def write_caddyfile(caddyfile_content: str, path: str = '/app/Caddyfile') -> bool:
"""Write Caddyfile to disk.
Args:
caddyfile_content: Content to write
path: Path to Caddyfile
Returns:
True if successful, False otherwise
"""
try:
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:
"""Reload Caddy configuration without restart.
Note: Caddy monitoring is handled via file watching. After writing the Caddyfile,
Caddy should automatically reload. If it doesn't, you may need to restart the
Caddy container manually.
Returns:
True if configuration was written successfully (Caddy will auto-reload)
"""
try:
# Just verify that Caddy is reachable
import urllib.request
response = urllib.request.urlopen('http://caddy:2019/config/', timeout=2)
return response.status == 200
except Exception as e:
# Caddy might not be reachable, but Caddyfile was already written
# Caddy should reload automatically when it detects file changes
print(f"Note: Caddy reload check returned: {str(e)}")
return True # Return True anyway since Caddyfile was written