git: d5d30594edb0 - main - www/bunkerweb: Fix Let's Encrypt, multisite and the profile page

From: Jochen Neumeister <joneum_at_FreeBSD.org>
Date: Mon, 12 Oct 2026 14:36:07 UTC
The branch main has been updated by joneum:

URL: https://cgit.FreeBSD.org/ports/commit/?id=d5d30594edb00cdeb63008a853eff747ddc601b6

commit d5d30594edb00cdeb63008a853eff747ddc601b6
Author:     Jochen Neumeister <joneum@FreeBSD.org>
AuthorDate: 2026-10-12 14:35:22 +0000
Commit:     Jochen Neumeister <joneum@FreeBSD.org>
CommitDate: 2026-10-12 14:35:22 +0000

    www/bunkerweb: Fix Let's Encrypt, multisite and the profile page
    
    - Let's Encrypt: BunkerWeb runs certbot from its own Linux layout. Add
      security/py-certbot to RUN_DEPENDS, point CERTBOT_BIN, DEPS_PATH and
      the plugin path to the installed locations, and install the certbot
      hook scripts executable since certbot runs them directly.
    - Multisite: the Templator patch forced NGINX_PREFIX to the global
      directory for every server. With MULTISITE=yes each server then
      included /usr/local/etc/nginx/set-lua.conf, which only exists without
      multisite, and nginx -t failed. Only set the prefix when the server
      configuration does not provide one.
    - Profile page: devel/py-user_agents is still at 1.0.1 and lacks
      get_browser(), get_os() and get_device(), so /profile failed with an
      AttributeError and two-factor setup was unreachable. Build the same
      strings from the attributes 1.0.1 provides.
    
    Sponsored by:   Netzkommune GmbH
---
 www/bunkerweb/Makefile                             | 12 ++++++-
 .../files/patch-src_common_gen_Templator.py        |  5 +--
 .../files/patch-src_ui_app_routes_profile.py       | 37 ++++++++++++++++++++++
 3 files changed, 51 insertions(+), 3 deletions(-)

diff --git a/www/bunkerweb/Makefile b/www/bunkerweb/Makefile
index 9f42aa167a8c..0102d275a9b9 100644
--- a/www/bunkerweb/Makefile
+++ b/www/bunkerweb/Makefile
@@ -1,6 +1,6 @@
 PORTNAME=	bunkerweb
 DISTVERSION=	1.6.15
-PORTREVISION=	2
+PORTREVISION=	3
 CATEGORIES=	www security
 
 MAINTAINER=	joneum@FreeBSD.org
@@ -28,6 +28,7 @@ RUN_DEPENDS=	openresty:www/openresty \
 		${PYTHON_PKGNAMEPREFIX}pymysql>0:databases/py-pymysql@${PY_FLAVOR} \
 		${LUA_MODLIBDIR}/cjson.so:devel/lua-cjson@${LUA_FLAVOR} \
 		${PYTHON_PKGNAMEPREFIX}bcrypt>0:security/py-bcrypt@${PY_FLAVOR} \
+		${PYTHON_PKGNAMEPREFIX}certbot>0:security/py-certbot@${PY_FLAVOR} \
 		${PYTHON_PKGNAMEPREFIX}biscuit-python>0:security/py-biscuit-python@${PY_FLAVOR} \
 		${PYTHON_PKGNAMEPREFIX}passlib>0:security/py-passlib@${PY_FLAVOR} \
 		${PYTHON_PKGNAMEPREFIX}docker>0:sysutils/py-docker@${PY_FLAVOR} \
@@ -70,6 +71,12 @@ post-patch:
 	${FIND} ${WRKSRC} -name "*.orig" -delete
 	${REINPLACE_CMD} -e 's|python3 ${DATADIR}/common/utils/|${PYTHON_CMD} ${DATADIR}/common/utils/|' \
 		${WRKSRC}/src/bw/lua/bunkerweb/api.lua
+	${REINPLACE_CMD} \
+		-e 's|join(sep, "usr", "share", "bunkerweb", "deps", "python", "bin", "certbot")|"${LOCALBASE}/bin/certbot"|' \
+		-e 's|join(sep, "usr", "share", "bunkerweb", "deps", "python")|"${DATADIR}/deps/python"|' \
+		-e 's|Path(sep, "usr", "share", "bunkerweb", "core", "letsencrypt")|Path("${DATADIR}/common/core/letsencrypt")|' \
+		${WRKSRC}/src/common/core/letsencrypt/jobs/letsencrypt_utils.py \
+		${WRKSRC}/src/common/core/letsencrypt/ui/blueprints/letsencrypt.py
 
 do-install:
 	${MKDIR} ${STAGEDIR}${DATADIR}
@@ -88,6 +95,9 @@ post-install:
 	${FIND} ${STAGEDIR}${DATADIR} -type f -name '*.sh' -exec ${CHMOD} 0755 {} \;
 	${CHMOD} 0755 ${STAGEDIR}${DATADIR}/common/gen/main.py
 	${CHMOD} 0755 ${STAGEDIR}${DATADIR}/common/gen/save_config.py
+	${CHMOD} 0755 ${STAGEDIR}${DATADIR}/common/core/letsencrypt/jobs/certbot-auth.py \
+		${STAGEDIR}${DATADIR}/common/core/letsencrypt/jobs/certbot-cleanup.py \
+		${STAGEDIR}${DATADIR}/common/core/letsencrypt/jobs/certbot-deploy.py
 
 	${INSTALL_SCRIPT} ${FILESDIR}/freebsd-migrate.py \
 		${STAGEDIR}${DATADIR}/common/db/
diff --git a/www/bunkerweb/files/patch-src_common_gen_Templator.py b/www/bunkerweb/files/patch-src_common_gen_Templator.py
index 86484eb742eb..abbf0823ef75 100644
--- a/www/bunkerweb/files/patch-src_common_gen_Templator.py
+++ b/www/bunkerweb/files/patch-src_common_gen_Templator.py
@@ -17,11 +17,12 @@
          }
  
          self._server_env_cache: Dict[str, Environment] = {}
-@@ -721,6 +722,7 @@
+@@ -721,6 +722,8 @@
          template_vars = self._base_template_vars.copy()
          template_vars["all"] = full_config
          template_vars.update(config)
-+        template_vars["NGINX_PREFIX"] = f"{self._target}/"
++        if "NGINX_PREFIX" not in config:
++            template_vars["NGINX_PREFIX"] = f"{self._target}/"
  
          for template in templates:
              name = basename(template) if any(template.endswith(root_conf) for root_conf in self._global_templates) else None
diff --git a/www/bunkerweb/files/patch-src_ui_app_routes_profile.py b/www/bunkerweb/files/patch-src_ui_app_routes_profile.py
new file mode 100644
index 000000000000..d33ab13d8c23
--- /dev/null
+++ b/www/bunkerweb/files/patch-src_ui_app_routes_profile.py
@@ -0,0 +1,37 @@
+--- src/ui/app/routes/profile.py.orig	2026-09-21 08:05:39 UTC
++++ src/ui/app/routes/profile.py
+@@ -5,6 +5,21 @@
+ from flask_login import current_user, login_required
+ from user_agents import parse
+ 
++
++# devel/py-user_agents is still at 1.0.1, which lacks the get_browser(),
++# get_os() and get_device() helpers added in 2.1.  Build the same strings
++# from the attributes 1.0.1 already provides.
++def _ua_browser(ua):
++    return ("%s %s" % (ua.browser.family, ua.browser.version_string)).strip()
++
++
++def _ua_os(ua):
++    return ("%s %s" % (ua.os.family, ua.os.version_string)).strip()
++
++
++def _ua_device(ua):
++    return "PC" if ua.is_pc else ua.device.family
++
+ from app.models.totp import totp as TOTP
+ 
+ from app.dependencies import DB
+@@ -51,9 +66,9 @@
+                 "current": (
+                     db_session.get("id") == session.get("session_id") if isinstance(db_session, dict) and "session_id" in session else "id" not in db_session
+                 ),
+-                "browser": ua_data.get_browser(),
+-                "os": ua_data.get_os(),
+-                "device": ua_data.get_device(),
++                "browser": _ua_browser(ua_data),
++                "os": _ua_os(ua_data),
++                "device": _ua_device(ua_data),
+                 "ip": (db_session.get("ip") if isinstance(db_session, dict) else session.get("ip", "-")) or "-",
+                 "creation_date": _fmt_dt(creation_dt),
+                 "last_activity": (