#!/bin/bash # Per-app routine WebUI-refresh hook (appWebuiRefresh_): webui_updater calls # it on every WebUI update while gluetun is installed; the installer (gluetun.sh) # and the 'gluetun_refresh_providers' tool call it directly too. # # Fetches gluetun's upstream servers.json, slims it down to a # { providers: { : { vpnTypes, countries } } } shape, and writes it # to the WebUI data dir so the per-app config dropdowns stay honest as # gluetun adds/removes providers and protocols. Falls back silently to the # previous snapshot (or the bundled default) on network failure. appWebuiRefresh_gluetun() { local output_file="${containers_dir}libreportal/frontend/data/apps/generated/gluetun-providers.json" local upstream="https://raw.githubusercontent.com/qdm12/gluetun/master/internal/storage/servers.json" local tmp="$(mktemp)" local raw="${output_file}.raw.$$" runFileOp mkdir -p "$(dirname "$output_file")" if ! command -v jq >/dev/null 2>&1; then isNotice "jq not installed; skipping gluetun provider refresh." return 0 fi # GitHub raw only sends ETag (no Last-Modified), so use If-None-Match # via a sidecar to skip the 7MB body when nothing has changed upstream. local etag_file="${output_file}.etag" local etag="" [[ -s "$etag_file" ]] && etag=$(<"$etag_file") local headers="${output_file}.hdr.$$" local http_code http_code=$(curl -sSL \ ${etag:+-H "If-None-Match: $etag"} \ --speed-limit 1000 --speed-time 30 \ --retry 2 --retry-delay 2 --retry-all-errors \ -D "$headers" -o "$raw" \ -w '%{http_code}' "$upstream") || http_code="" # $raw and $headers live next to $output_file (under containers_dir/ # libreportal/frontend/data/, dockerinstall-owned in rootless). The # manager can't `rm` them directly without a Permission denied — same # class of bug as the updateConfigOption sed-i issue. runFileOp routes # the rm through the right user. $tmp is from mktemp (/tmp), so # `rm -f $tmp` stays unwrapped. if [[ "$http_code" == "304" ]]; then runFileOp rm -f "$raw" "$headers" return 0 fi if [[ "$http_code" != "200" ]]; then isNotice "Upstream fetch failed (${http_code:-no response}); keeping existing snapshot." runFileOp rm -f "$raw" "$headers" return 0 fi local new_etag new_etag=$(awk 'tolower($1)=="etag:"{print $2}' "$headers" | tr -d '\r') runFileOp rm -f "$headers" # servers.json is a top-level object keyed by provider; each provider # entry has a `servers` array whose items have `vpn` (wireguard|openvpn), # `country`, `city`, etc. We collapse that into per-provider unique # vpn-type and country lists. Drop the `version` key (it's not a provider). if ! jq ' [ to_entries[] | select(.key != "version") | { key: .key, value: { vpnTypes: ((.value.servers // []) | map(.vpn) | unique | map(select(. != null and . != ""))), countries: ((.value.servers // []) | map(.country) | unique | map(select(. != null and . != ""))) } } ] | from_entries | { providers: . } ' "$raw" > "$tmp" 2>/dev/null; then isNotice "Failed to parse gluetun servers.json; keeping existing provider snapshot." runFileOp rm -f "$raw" rm -f "$tmp" return 0 fi runFileOp rm -f "$raw" if [ -s "$tmp" ]; then runFileWrite "$output_file" < "$tmp"; rm -f "$tmp" [[ -n "$new_etag" ]] && echo "$new_etag" | runFileWrite "$etag_file" isSuccessful "Refreshed gluetun provider snapshot ($(jq '.providers | length' "$output_file") providers)." else rm -f "$tmp" isNotice "Empty gluetun snapshot generated; ignoring." fi }