diff --git a/packages/ns-api/Makefile b/packages/ns-api/Makefile index 40ec89cc7..0202ff17f 100644 --- a/packages/ns-api/Makefile +++ b/packages/ns-api/Makefile @@ -6,7 +6,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=ns-api -PKG_VERSION:=3.7.1 +PKG_VERSION:=3.8.0 PKG_RELEASE:=1 PKG_BUILD_DIR:=$(BUILD_DIR)/ns-api-$(PKG_VERSION) diff --git a/packages/ns-api/files/ns.controller b/packages/ns-api/files/ns.controller index 8e7a7d3a2..d9156fd7a 100755 --- a/packages/ns-api/files/ns.controller +++ b/packages/ns-api/files/ns.controller @@ -50,6 +50,14 @@ def get_version(): return version.replace("-", " ", 1) return version +def get_package_version(package): + # installed version of an apk package, or an empty string if it is not installed + try: + output = subprocess.run(["apk", "list", "-I", package], capture_output=True, text=True).stdout.strip() + return output.split()[0].removeprefix(f"{package}-") if output else "" + except (subprocess.SubprocessError, IndexError): + return "" + def parse_key(line): # parse key: tmp = line.strip().split(" ") @@ -123,7 +131,11 @@ def info(): ret["fqdn"] = get_hostname() ret["system_id"] = u.get("ns-plug", "config", "system_id", default="") ret["subscription_type"] = u.get("ns-plug", "config", "type", default="") - ret["api_version"] = subprocess.run(["apk", "list", "-I", "ns-api"], capture_output=True, text=True).stdout.strip().split()[0].removeprefix("ns-api-") + ret["api_version"] = get_package_version("ns-api") + # reported separately from api_version: the controller uses it to decide whether this unit can + # serve its own UI under the controller's per-unit path prefix, and the two packages are + # versioned independently + ret["ui_version"] = get_package_version("ns-ui") return ret def add_ssh_key(ssh_key): diff --git a/packages/ns-api/files/ns.update b/packages/ns-api/files/ns.update index 0732af949..4a4206694 100755 --- a/packages/ns-api/files/ns.update +++ b/packages/ns-api/files/ns.update @@ -73,6 +73,24 @@ def check_package_updates(): ret.sort(key=lambda package: package["package"]) return {"updates": ret} +def list_installed_packages(): + try: + p = subprocess.run(["/usr/bin/apk", "query", "--installed", "--format", "json", + "--fields", "name,version,description", "*"], + check=True, capture_output=True, text=True) + except Exception as e: + print(e, file=sys.stderr) + return utils.generic_error("apk_query_failed") + ret = [] + for package in json.loads(p.stdout): + ret.append({ + "name": package.get("name", ""), + "version": package.get("version", ""), + "description": package.get("description", "") + }) + ret.sort(key=lambda package: package["name"]) + return {"packages": ret} + def get_package_updates_lat_check(): return {"lastCheck": last_package_check()} @@ -184,6 +202,7 @@ if cmd == "list": json.dumps( { "check-package-updates": {}, + "list-installed-packages": {}, "get-package-updates-last-check": {}, "get-automatic-updates-status": {}, "install-package-updates": {}, @@ -201,6 +220,8 @@ elif cmd == "call": action = sys.argv[2] if action == "check-package-updates": ret = check_package_updates() + elif action == "list-installed-packages": + ret = list_installed_packages() elif action == "get-package-updates-last-check": ret = get_package_updates_lat_check() elif action == "get-automatic-updates-status": diff --git a/packages/ns-ui/Makefile b/packages/ns-ui/Makefile index 93e71da91..eb420a64f 100644 --- a/packages/ns-ui/Makefile +++ b/packages/ns-ui/Makefile @@ -7,12 +7,14 @@ include $(TOPDIR)/rules.mk PKG_NAME:=ns-ui # renovate: datasource=github-releases depName=NethServer/nethsecurity-ui -PKG_VERSION:=2.23.3 +PKG_VERSION:=2.24.0 PKG_RELEASE:=1 PKG_SOURCE_PROTO:=git PKG_SOURCE_URL:=https://github.com/NethServer/nethsecurity-ui.git -PKG_SOURCE_VERSION:=$(PKG_VERSION) +# PKG_VERSION is what the controller gates on (ns-api reports it as info.ui_version), but renovate +# maintains it independently of this SHA. Once the UI release is tagged, use $(PKG_VERSION) here. +PKG_SOURCE_VERSION:=82b3c57b7e4d3d2c0fe79ffece3811b0d078148e PKG_SOURCE_SUBDIR:=nethsecurity-ui-$(PKG_SOURCE_VERSION) PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_SOURCE_SUBDIR) PKG_MIRROR_HASH:=skip diff --git a/packages/ns-ui/files/ns-ui b/packages/ns-ui/files/ns-ui index 2643b5c29..e301afa90 100755 --- a/packages/ns-ui/files/ns-ui +++ b/packages/ns-ui/files/ns-ui @@ -10,6 +10,7 @@ set -e NSUI_FILE=/etc/nginx/conf.d/00ns.locations NSUI_EXTRA_FILE=/etc/nginx/conf.d/ns-ui.conf NSUI_API_LOCATIONS_FILE=/etc/nginx/conf.d/ns-ui-api.inc +NSUI_STATIC_LOCATIONS_FILE=/etc/nginx/conf.d/ns-ui-static.inc LUCI_FILE=/etc/nginx/conf.d/luci.locations # Shared API locations, included by both the default and extra ns-ui instances @@ -17,35 +18,79 @@ LUCI_FILE=/etc/nginx/conf.d/luci.locations # https://github.com/NethServer/nethsecurity/issues/1834 cat <<'EOF' > "$NSUI_API_LOCATIONS_FILE" # body size limits on unauthenticated routes to prevent memory exhaustion +# X-Forwarded-For is appended, not overwritten: when a controller proxies this unit, $remote_addr +# is the controller's VPN address, so overwriting would collapse every remote client into one +# address and make rate limiting and audit logs useless. location = /api/login { client_max_body_size 32k; proxy_set_header Host $host; - proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_pass http://127.0.0.1:8090; } location = /api/logout { client_max_body_size 1k; proxy_set_header Host $host; - proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_pass http://127.0.0.1:8090; } location = /api/2fa/otp-verify { client_max_body_size 32k; proxy_set_header Host $host; - proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_pass http://127.0.0.1:8090; } location /api/ { proxy_set_header Host $host; - proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_pass http://127.0.0.1:8090/api/; proxy_read_timeout 180s; } EOF +# Shared static locations, included by both the default and extra ns-ui instances. +# +# Cache headers matter more than they look: index.html references content-hashed chunks, and +# without them browsers apply heuristic freshness and can serve a stale index.html for days. After +# a package upgrade the chunks it names are gone, try_files returns 404 and the UI is a white +# screen until a hard reload. +cat <<'EOF' > "$NSUI_STATIC_LOCATIONS_FILE" +location / { + gzip on; + gzip_types text/css application/javascript image/svg+xml; + root /www-ns; + try_files $uri $uri/ =404; +} + +# hashed filenames, so these are safe to keep forever +location /assets/ { + gzip on; + gzip_types text/css application/javascript image/svg+xml; + root /www-ns; + try_files $uri =404; + expires 1y; + add_header Cache-Control "public, immutable"; +} + +# unhashed, and the entry point to everything else: must always be revalidated +location = /index.html { + root /www-ns; + add_header Cache-Control "no-cache"; +} + +location = /branding.js { + root /www-ns; + add_header Cache-Control "no-cache"; +} + +location = /favicon.ico { + root /www-ns; + add_header Cache-Control "no-cache"; +} +EOF + # Manage default ns-ui instance on port 443 nsui_enable=$(uci -q get ns-ui.config.nsui_enable) rm -f "$NSUI_FILE.disabled" @@ -53,11 +98,7 @@ if [ "$nsui_enable" == "1" ]; then cat < "$NSUI_FILE" # NethSecurity UI and API -location / { - root /www-ns; - try_files \$uri \$uri/ /index.html; -} - +include $NSUI_STATIC_LOCATIONS_FILE; include $NSUI_API_LOCATIONS_FILE; EOF else @@ -93,13 +134,7 @@ server { server_tokens $server_tokens; # enable NS UI - location / { - gzip on; - gzip_types text/css application/javascript image/svg+xml; - root /www-ns; - try_files \$uri \$uri/ /index.html; - } - + include $NSUI_STATIC_LOCATIONS_FILE; include $NSUI_API_LOCATIONS_FILE; } EOF