diff --git a/plugins/lookup/cdn.py b/plugins/lookup/cdn.py index a66950a683..08c22ba8f8 100644 --- a/plugins/lookup/cdn.py +++ b/plugins/lookup/cdn.py @@ -10,6 +10,7 @@ # root, shared.{root,css,js,img,fonts}, vendor, # role.{id,root,version,release.{root,css,js,img,fonts}}, # urls: +# relative_urls: # } # # Examples: @@ -30,6 +31,8 @@ from ansible.plugins.lookup import LookupBase from utils.tls_common import as_str +SAME_ORIGIN_PATH = "/_infinito/cdn" + def _cdn_paths(cdn_root: str, application_id: str, version: str) -> dict: cdn_root = str(Path(cdn_root).resolve()) @@ -109,5 +112,6 @@ class LookupModule(LookupBase): paths = _cdn_paths(cdn_root, application_id, "latest") result = dict(paths) result["urls"] = _to_url_tree(paths, paths["root"], cdn_base_url) + result["relative_urls"] = _to_url_tree(paths, paths["root"], SAME_ORIGIN_PATH) return [result] diff --git a/plugins/lookup/design.py b/plugins/lookup/design.py index 16c8f8bf6d..de5d85b799 100644 --- a/plugins/lookup/design.py +++ b/plugins/lookup/design.py @@ -25,6 +25,7 @@ class LookupModule(LookupBase): domain canonical domain of the role dest CDN directory the generated assets are written to urls {"favicon_ico": url, "": {"png": url, "svg": url}} + relative_urls the same tree as host-relative URLs every vhost serves """ def _sub(self, name: str, terms: list, variables: dict[str, Any]) -> Any: @@ -52,6 +53,9 @@ class LookupModule(LookupBase): cdn = self._sub("cdn", [application_id], variables) base_url = f"{cdn['urls']['role']['release']['img'].rstrip('/')}/{ASSET_DIR}" + relative_base = ( + f"{cdn['relative_urls']['role']['release']['img'].rstrip('/')}/{ASSET_DIR}" + ) return [ { @@ -59,5 +63,6 @@ class LookupModule(LookupBase): "domain": self._sub("domain", [application_id], variables), "dest": str(Path(cdn["role"]["release"]["img"]) / ASSET_DIR), "urls": asset_urls(base_url, branding["slots"]), + "relative_urls": asset_urls(relative_base, branding["slots"]), } ] diff --git a/roles/sys-svc-proxy/templates/cdn.conf.j2 b/roles/sys-svc-proxy/templates/cdn.conf.j2 new file mode 100644 index 0000000000..7e71be19b3 --- /dev/null +++ b/roles/sys-svc-proxy/templates/cdn.conf.j2 @@ -0,0 +1,5 @@ +location ^~ {{ lookup('cdn', 'web-svc-cdn').relative_urls.root }} { + alias {{ lookup('nginx', 'directories.data.cdn') | regex_replace('/+$', '') }}/; + add_header X-Content-Type-Options "nosniff" always; + add_header Content-Security-Policy "sandbox; default-src 'none'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; font-src 'self'" always; +} diff --git a/roles/sys-svc-proxy/templates/vhost/basic.conf.j2 b/roles/sys-svc-proxy/templates/vhost/basic.conf.j2 index 1dce59e6e4..709975e8e0 100644 --- a/roles/sys-svc-proxy/templates/vhost/basic.conf.j2 +++ b/roles/sys-svc-proxy/templates/vhost/basic.conf.j2 @@ -3,6 +3,7 @@ server { server_name {{ domain }}; + {% include 'roles/sys-svc-proxy/templates/cdn.conf.j2' %} {% include 'roles/sys-svc-proxy/templates/headers/buffers.conf.j2' %} {% if lookup('sso', application_id, 'is_proxy_gated') %} diff --git a/roles/sys-svc-proxy/templates/vhost/ws_generic.conf.j2 b/roles/sys-svc-proxy/templates/vhost/ws_generic.conf.j2 index 8578b13c0c..06ef63bf21 100644 --- a/roles/sys-svc-proxy/templates/vhost/ws_generic.conf.j2 +++ b/roles/sys-svc-proxy/templates/vhost/ws_generic.conf.j2 @@ -1,5 +1,6 @@ server { server_name {{ domain }}; + {% include 'roles/sys-svc-proxy/templates/cdn.conf.j2' %} {% include 'roles/sys-front-tls/templates/nginx_ssl_header.j2' %} diff --git a/roles/web-app-baserow/templates/proxy.conf.j2 b/roles/web-app-baserow/templates/proxy.conf.j2 index f5231ade80..43bfa11f08 100644 --- a/roles/web-app-baserow/templates/proxy.conf.j2 +++ b/roles/web-app-baserow/templates/proxy.conf.j2 @@ -3,6 +3,7 @@ server { server_name {{ domain }}; + {% include 'roles/sys-svc-proxy/templates/cdn.conf.j2' %} {% include 'roles/sys-svc-proxy/templates/headers/buffers.conf.j2' %} {% if lookup('sso', application_id, 'is_proxy_gated') %} diff --git a/roles/web-app-checkmk/templates/proxy.conf.j2 b/roles/web-app-checkmk/templates/proxy.conf.j2 index 1bdcbe6524..92e9ac9241 100644 --- a/roles/web-app-checkmk/templates/proxy.conf.j2 +++ b/roles/web-app-checkmk/templates/proxy.conf.j2 @@ -3,6 +3,7 @@ server { server_name {{ domain }}; + {% include 'roles/sys-svc-proxy/templates/cdn.conf.j2' %} {% include 'roles/sys-svc-proxy/templates/headers/buffers.conf.j2' %} {% if lookup('sso', application_id, 'is_proxy_gated') %} diff --git a/roles/web-app-hermes/templates/proxy.conf.j2 b/roles/web-app-hermes/templates/proxy.conf.j2 index c08909056a..779adaf51e 100644 --- a/roles/web-app-hermes/templates/proxy.conf.j2 +++ b/roles/web-app-hermes/templates/proxy.conf.j2 @@ -6,6 +6,7 @@ server { server_name {{ domain }}; + {% include 'roles/sys-svc-proxy/templates/cdn.conf.j2' %} {% include 'roles/sys-svc-proxy/templates/headers/buffers.conf.j2' %} {% if lookup('sso', application_id, 'is_proxy_gated') %} diff --git a/roles/web-app-matrix/templates/synapse.conf.j2 b/roles/web-app-matrix/templates/synapse.conf.j2 index ea3b6845ef..39b0d4ef18 100644 --- a/roles/web-app-matrix/templates/synapse.conf.j2 +++ b/roles/web-app-matrix/templates/synapse.conf.j2 @@ -1,5 +1,6 @@ server { server_name {{ MATRIX_SYNAPSE_DOMAIN }}; + {% include 'roles/sys-svc-proxy/templates/cdn.conf.j2' %} {% include 'roles/sys-front-tls/templates/nginx_ssl_header.j2' %} listen {{ MATRIX_SYNAPSE_LISTEN_FLAGS }}; diff --git a/roles/web-app-n8n/templates/proxy.conf.j2 b/roles/web-app-n8n/templates/proxy.conf.j2 index da9ff760ba..c50ac70d75 100644 --- a/roles/web-app-n8n/templates/proxy.conf.j2 +++ b/roles/web-app-n8n/templates/proxy.conf.j2 @@ -3,6 +3,7 @@ server { server_name {{ domain }}; + {% include 'roles/sys-svc-proxy/templates/cdn.conf.j2' %} {% include 'roles/sys-svc-proxy/templates/headers/buffers.conf.j2' %} {% if lookup('sso', application_id, 'is_proxy_gated') %} diff --git a/roles/web-app-nextcloud/templates/proxy.conf.j2 b/roles/web-app-nextcloud/templates/proxy.conf.j2 index e8a4e16324..ead885d3a5 100644 --- a/roles/web-app-nextcloud/templates/proxy.conf.j2 +++ b/roles/web-app-nextcloud/templates/proxy.conf.j2 @@ -1,6 +1,7 @@ server { server_name {{ domain }}; + {% include 'roles/sys-svc-proxy/templates/cdn.conf.j2' %} {% include 'roles/sys-front-tls/templates/nginx_ssl_header.j2' %} diff --git a/roles/web-app-openbao/templates/proxy.conf.j2 b/roles/web-app-openbao/templates/proxy.conf.j2 index 97d5a2c23c..c8c5188288 100644 --- a/roles/web-app-openbao/templates/proxy.conf.j2 +++ b/roles/web-app-openbao/templates/proxy.conf.j2 @@ -3,6 +3,7 @@ server { server_name {{ domain }}; + {% include 'roles/sys-svc-proxy/templates/cdn.conf.j2' %} {% include 'roles/sys-svc-proxy/templates/headers/buffers.conf.j2' %} {% if lookup('sso', application_id, 'is_proxy_gated') %} diff --git a/roles/web-app-openproject/templates/proxy.conf.j2 b/roles/web-app-openproject/templates/proxy.conf.j2 index 37b96c18d3..7b9b4c16f9 100644 --- a/roles/web-app-openproject/templates/proxy.conf.j2 +++ b/roles/web-app-openproject/templates/proxy.conf.j2 @@ -3,6 +3,7 @@ server { server_name {{ domain }}; + {% include 'roles/sys-svc-proxy/templates/cdn.conf.j2' %} {% include 'roles/sys-svc-proxy/templates/headers/buffers.conf.j2' %} {% if lookup('sso', application_id, 'is_proxy_gated') %} diff --git a/roles/web-app-peertube/templates/proxy.conf.j2 b/roles/web-app-peertube/templates/proxy.conf.j2 index b561078b5b..8a69291de6 100644 --- a/roles/web-app-peertube/templates/proxy.conf.j2 +++ b/roles/web-app-peertube/templates/proxy.conf.j2 @@ -1,5 +1,6 @@ server { server_name {{ domain }}; + {% include 'roles/sys-svc-proxy/templates/cdn.conf.j2' %} {% include 'roles/sys-front-tls/templates/nginx_ssl_header.j2' %} diff --git a/roles/web-app-postmarks/templates/proxy.conf.j2 b/roles/web-app-postmarks/templates/proxy.conf.j2 index fd8c545c4c..93e8aba158 100644 --- a/roles/web-app-postmarks/templates/proxy.conf.j2 +++ b/roles/web-app-postmarks/templates/proxy.conf.j2 @@ -3,6 +3,7 @@ server { server_name {{ domain }}; + {% include 'roles/sys-svc-proxy/templates/cdn.conf.j2' %} {% include 'roles/sys-svc-proxy/templates/headers/buffers.conf.j2' %} {% if lookup('sso', application_id, 'is_proxy_gated') %} diff --git a/roles/web-app-seaweedfs/templates/proxy.conf.j2 b/roles/web-app-seaweedfs/templates/proxy.conf.j2 index 94b4e003b4..637d82bf29 100644 --- a/roles/web-app-seaweedfs/templates/proxy.conf.j2 +++ b/roles/web-app-seaweedfs/templates/proxy.conf.j2 @@ -3,6 +3,7 @@ server { server_name {{ domain }}; + {% include 'roles/sys-svc-proxy/templates/cdn.conf.j2' %} {% include 'roles/sys-svc-proxy/templates/headers/buffers.conf.j2' %} {% if lookup('sso', application_id, 'is_proxy_gated') %} diff --git a/roles/web-svc-cdn/README.md b/roles/web-svc-cdn/README.md index ed1c9f8c92..e53278ac81 100644 --- a/roles/web-svc-cdn/README.md +++ b/roles/web-svc-cdn/README.md @@ -15,6 +15,7 @@ It also publishes the `css` and `javascript` aliases as canonical references bac - **Canonical CDN service:** Provides the primary `cdn` service entry consumed via `services.cdn` across the stack. - **Aliased asset channels:** Exposes `css` and `javascript` as canonical aliases of `cdn` for clearer per-asset wiring. - **TLS-aware delivery:** Runs behind the project's reverse proxy and inherits its certificate management. +- **Same-origin path:** Every front-proxy vhost also serves the CDN tree under `/_infinito/cdn/`; `lookup('cdn', ...)` and `lookup('design', ...)` return those host-relative URLs as `relative_urls`, so a page served on any network loads its assets from its own host. - **Container-managed:** Deploys via Docker Compose with project-standard healthchecks, restart policy, and resource limits. - **Matomo and Prometheus aware:** Toggles tracker and metrics integration based on the presence of the corresponding application roles. diff --git a/roles/web-svc-cdn/templates/proxy.conf.j2 b/roles/web-svc-cdn/templates/proxy.conf.j2 index 8fa0ce8da6..1c9baf3de6 100644 --- a/roles/web-svc-cdn/templates/proxy.conf.j2 +++ b/roles/web-svc-cdn/templates/proxy.conf.j2 @@ -1,6 +1,7 @@ server { server_name {{ domain }}; + {% include 'roles/sys-svc-proxy/templates/cdn.conf.j2' %} {% include 'roles/sys-front-tls/templates/nginx_ssl_header.j2' %} diff --git a/roles/web-svc-collabora/templates/proxy.conf.j2 b/roles/web-svc-collabora/templates/proxy.conf.j2 index b1647ad9c2..7480741e1d 100644 --- a/roles/web-svc-collabora/templates/proxy.conf.j2 +++ b/roles/web-svc-collabora/templates/proxy.conf.j2 @@ -1,5 +1,6 @@ server { server_name {{ domain }}; + {% include 'roles/sys-svc-proxy/templates/cdn.conf.j2' %} {% include 'roles/sys-front-tls/templates/nginx_ssl_header.j2' %} diff --git a/roles/web-svc-file/templates/proxy.conf.j2 b/roles/web-svc-file/templates/proxy.conf.j2 index cc649f6a4f..b1594049cb 100644 --- a/roles/web-svc-file/templates/proxy.conf.j2 +++ b/roles/web-svc-file/templates/proxy.conf.j2 @@ -3,6 +3,7 @@ server { server_name {{ domain }}; + {% include 'roles/sys-svc-proxy/templates/cdn.conf.j2' %} {% include 'roles/sys-front-tls/templates/nginx_ssl_header.j2' %} diff --git a/roles/web-svc-html/templates/proxy.conf.j2 b/roles/web-svc-html/templates/proxy.conf.j2 index 0905e9e8ef..667a6cb92c 100644 --- a/roles/web-svc-html/templates/proxy.conf.j2 +++ b/roles/web-svc-html/templates/proxy.conf.j2 @@ -3,6 +3,7 @@ server { server_name {{ domain }}; + {% include 'roles/sys-svc-proxy/templates/cdn.conf.j2' %} {% include 'roles/sys-front-tls/templates/nginx_ssl_header.j2' %} diff --git a/roles/web-svc-mirror/templates/proxy.conf.j2 b/roles/web-svc-mirror/templates/proxy.conf.j2 index f5f87fb785..3cb028c227 100644 --- a/roles/web-svc-mirror/templates/proxy.conf.j2 +++ b/roles/web-svc-mirror/templates/proxy.conf.j2 @@ -2,6 +2,7 @@ server { server_name {{ domain }}; + {% include 'roles/sys-svc-proxy/templates/cdn.conf.j2' %} {% include 'roles/sys-front-tls/templates/nginx_ssl_header.j2' %} diff --git a/roles/web-svc-seaweedfs/templates/proxy.conf.j2 b/roles/web-svc-seaweedfs/templates/proxy.conf.j2 index 2da47f9496..cc2449e69b 100644 --- a/roles/web-svc-seaweedfs/templates/proxy.conf.j2 +++ b/roles/web-svc-seaweedfs/templates/proxy.conf.j2 @@ -5,6 +5,7 @@ server { server_name {{ domain }}; + {% include 'roles/sys-svc-proxy/templates/cdn.conf.j2' %} {% include 'roles/sys-svc-proxy/templates/headers/buffers.conf.j2' %} {% include 'roles/sys-front-tls/templates/nginx_ssl_header.j2' %} diff --git a/tests/unit/python/plugins/lookup/test_cdn.py b/tests/unit/python/plugins/lookup/test_cdn.py index 7313c0c315..11d3de9909 100644 --- a/tests/unit/python/plugins/lookup/test_cdn.py +++ b/tests/unit/python/plugins/lookup/test_cdn.py @@ -2,7 +2,7 @@ import tempfile import unittest from pathlib import Path -from plugins.lookup.cdn import _cdn_paths, _to_url_tree +from plugins.lookup.cdn import SAME_ORIGIN_PATH, _cdn_paths, _to_url_tree class TestCdnPaths(unittest.TestCase): @@ -78,6 +78,14 @@ class TestCdnToUrlTree(unittest.TestCase): urls_without = _to_url_tree(self.tree, self.root, self.base_url) self.assertEqual(urls_with["shared"]["css"], urls_without["shared"]["css"]) + def test_same_origin_tree_is_host_relative(self): + urls = _to_url_tree(self.tree, self.root, SAME_ORIGIN_PATH) + self.assertEqual(urls["root"], "/_infinito/cdn/") + self.assertEqual( + urls["role"]["release"]["img"], + f"/_infinito/cdn/roles/{self.app}/latest/img", + ) + def test_different_apps_produce_different_role_urls(self): tree_other = _cdn_paths(self.root, "web-app-nextcloud", "latest") urls_a = _to_url_tree(self.tree, self.root, self.base_url)