diff --git a/haproxy_manager.py b/haproxy_manager.py index 52d3743..659a4a0 100644 --- a/haproxy_manager.py +++ b/haproxy_manager.py @@ -58,6 +58,14 @@ def blocked_ip_page(): return render_template('blocked_ip_page.html'), 403 +@default_app.route('/suspended', methods=_ANY_METHOD) +def suspended_page(): + """Serve the suspended-site page (HTTP 503) for hosts listed in + /etc/haproxy/suspended_domains.list. Routed here via the frontend + path-rewrite ACL when HAPROXY_SUSPENSION_ENABLED=true.""" + return render_template('suspended_page.html'), 503 + + # Configuration DB_FILE = '/etc/haproxy/haproxy_config.db' TEMPLATE_DIR = Path('templates') @@ -1718,14 +1726,18 @@ def generate_config(): # image) -> the generated haproxy.cfg is byte-identical to today's. coraza_spoe_backend = os.environ.get('HAPROXY_CORAZA_SPOE_BACKEND') - # Optional site-suspension routing. When HAPROXY_SUSPENSION_BACKEND is - # set (e.g. "whp-suspended:80"), we render bk_suspended + a frontend - # ACL that routes hosts in /etc/haproxy/suspended_domains.list to it. + # Optional site-suspension routing. When HAPROXY_SUSPENSION_ENABLED is + # set (any truthy value), the frontend gets an ACL that rewrites the + # path to /suspended and routes through default-backend for any host + # listed in /etc/haproxy/suspended_domains.list. The /suspended Flask + # route in this same process returns HTTP 503 + a static page — no + # separate container needed (mirrors the existing /blocked-ip pattern). # Same opt-in shape as Coraza: unset -> config byte-identical to today. - # The list file is maintained by external tooling; we just ensure it - # exists (haproxy refuses to start with -f pointing at a missing file). - suspension_backend_target = os.environ.get('HAPROXY_SUSPENSION_BACKEND') - if suspension_backend_target: + # We just ensure the list file exists (haproxy refuses to start with + # `-f` pointing at a missing file). + suspension_raw = os.environ.get('HAPROXY_SUSPENSION_ENABLED', '').strip().lower() + suspension_enabled = suspension_raw in ('1', 'true', 'yes', 'on') + if suspension_enabled: suspended_list_path = '/etc/haproxy/suspended_domains.list' if not os.path.exists(suspended_list_path): try: @@ -1747,7 +1759,7 @@ def generate_config(): listener_block = template_env.get_template('hap_listener.tpl').render( crt_path = SSL_CERTS_DIR, coraza_spoe_backend = coraza_spoe_backend, - suspension_enabled = bool(suspension_backend_target), + suspension_enabled = suspension_enabled, ) config_parts.append(listener_block) @@ -1868,14 +1880,6 @@ backend default-backend # Add Backends config_parts.append('\n' .join(config_backends) + '\n') - # Suspended-site backend (only when env var set). Inserted before the - # Coraza backend so config_parts ordering remains deterministic. - if suspension_backend_target: - suspended_backend_block = template_env.get_template( - 'hap_suspended_backend.tpl' - ).render(target=suspension_backend_target) - config_parts.append(suspended_backend_block + '\n') - # Coraza WAF backend + SPOE engine config file (only when env var set). # Writing /etc/haproxy/coraza-spoe.cfg here keeps it in sync with the # filter line that hap_listener.tpl just rendered into the frontend. diff --git a/templates/hap_listener.tpl b/templates/hap_listener.tpl index e45abe7..3a714cd 100644 --- a/templates/hap_listener.tpl +++ b/templates/hap_listener.tpl @@ -56,15 +56,17 @@ frontend web {%- if suspension_enabled %} # Site suspension routing. Any Host header listed in - # /etc/haproxy/suspended_domains.list is routed to bk_suspended (a - # backend serving a static 503 "site unavailable" page). External - # tooling (e.g. WHP's site_disable.php) maintains the list file via - # `docker cp`. An empty list is safe — the ACL simply doesn't match. - # Sits after IP-blocking (so 429/403 still trigger first) and before - # any per-domain use_backend rules, so suspension takes precedence - # over normal site routing. + # /etc/haproxy/suspended_domains.list is rewritten to /suspended and + # routed through default-backend, which is the same Flask app that + # serves the default page and blocked-ip page (port 8080 inside this + # container). The `/suspended` route returns HTTP 503 with a static + # suspension page. External tooling (e.g. WHP's site_disable.php) + # maintains the list file via `docker cp`. An empty list is safe — + # the ACL simply doesn't match. Sits after IP-blocking so 429/403 + # still trigger first. acl is_suspended_domain hdr(host),lower -f /etc/haproxy/suspended_domains.list - use_backend bk_suspended if is_suspended_domain + http-request set-path /suspended if is_suspended_domain + use_backend default-backend if is_suspended_domain {%- endif %} {%- if coraza_spoe_backend %} diff --git a/templates/hap_suspended_backend.tpl b/templates/hap_suspended_backend.tpl deleted file mode 100644 index 6fb76ff..0000000 --- a/templates/hap_suspended_backend.tpl +++ /dev/null @@ -1,18 +0,0 @@ -# Suspended-site backend. Used when external tooling adds a host to -# /etc/haproxy/suspended_domains.list (read by an ACL in the frontend). -# The backend points at a single upstream that serves a static 503 -# "site temporarily unavailable" page. Only rendered when the -# HAPROXY_SUSPENSION_BACKEND env var is set on the haproxy-manager -# container; non-WHP deployments (home networks, standalone use) see -# no change to haproxy.cfg. -backend bk_suspended - mode http - option http-server-close - http-request set-header X-Forwarded-Proto https if { ssl_fc } - http-request set-header X-Forwarded-For %[src] - # init-addr last,none: tolerate startup-time DNS resolution failure - # (the upstream container may not be up yet when haproxy-manager starts). - # resolvers docker_dns: re-resolve via Docker's embedded DNS at 127.0.0.11 - # so the server picks up the real IP once the upstream becomes available - # (the docker_dns block is defined in hap_header.tpl). - server suspended {{ target }} check inter 30s init-addr last,none resolvers docker_dns diff --git a/templates/suspended_page.html b/templates/suspended_page.html new file mode 100644 index 0000000..799998b --- /dev/null +++ b/templates/suspended_page.html @@ -0,0 +1,58 @@ + + +
+ + + +The site you are trying to reach is currently offline.
+Site owners: please contact support to restore service.
+