|
| 1 | +#!/usr/bin/env bash |
| 2 | +set -euo pipefail |
| 3 | + |
| 4 | +caddyfile="${REND_CADDYFILE:-/etc/caddy/Caddyfile}" |
| 5 | +dry_run=false |
| 6 | +skip_reload=false |
| 7 | + |
| 8 | +usage() { |
| 9 | + cat <<'EOF' |
| 10 | +Usage: scripts/sync-edge-caddy-playback-routes.sh [options] |
| 11 | +
|
| 12 | +Patch the edge Caddy public playback matcher so HLS ladder playlists and |
| 13 | +segments reach rend-edge. The script edits only the signed playback regexp, |
| 14 | +backs up the Caddyfile, validates the result, and reloads Caddy. Hosts that |
| 15 | +do not keep a Caddy playback allowlist are left unchanged. |
| 16 | +
|
| 17 | +Options: |
| 18 | + --caddyfile FILE Caddyfile path. Default: /etc/caddy/Caddyfile. |
| 19 | + --dry-run Print whether the file would change without writing. |
| 20 | + --skip-reload Skip caddy fmt/validate and systemctl reload. |
| 21 | + -h, --help Show this help. |
| 22 | +EOF |
| 23 | +} |
| 24 | + |
| 25 | +die() { |
| 26 | + echo "error: $*" >&2 |
| 27 | + exit 1 |
| 28 | +} |
| 29 | + |
| 30 | +require_command() { |
| 31 | + command -v "$1" >/dev/null 2>&1 || die "$1 is required" |
| 32 | +} |
| 33 | + |
| 34 | +while [[ $# -gt 0 ]]; do |
| 35 | + case "$1" in |
| 36 | + --caddyfile) |
| 37 | + caddyfile="${2:?missing value for $1}" |
| 38 | + shift 2 |
| 39 | + ;; |
| 40 | + --dry-run) |
| 41 | + dry_run=true |
| 42 | + shift |
| 43 | + ;; |
| 44 | + --skip-reload) |
| 45 | + skip_reload=true |
| 46 | + shift |
| 47 | + ;; |
| 48 | + -h | --help) |
| 49 | + usage |
| 50 | + exit 0 |
| 51 | + ;; |
| 52 | + *) |
| 53 | + die "unknown argument: $1" |
| 54 | + ;; |
| 55 | + esac |
| 56 | +done |
| 57 | + |
| 58 | +[[ -f "$caddyfile" ]] || die "missing Caddyfile: $caddyfile" |
| 59 | +require_command python3 |
| 60 | + |
| 61 | +tmp="$(mktemp "${TMPDIR:-/tmp}/rend-edge-caddy.XXXXXX")" |
| 62 | +cleanup() { |
| 63 | + rm -f "$tmp" |
| 64 | +} |
| 65 | +trap cleanup EXIT |
| 66 | + |
| 67 | +patch_status="$( |
| 68 | + python3 - "$caddyfile" "$tmp" <<'PY' |
| 69 | +import sys |
| 70 | +from pathlib import Path |
| 71 | +
|
| 72 | +source = Path(sys.argv[1]) |
| 73 | +target = Path(sys.argv[2]) |
| 74 | +ladder = r"hls/(720p|1080p|2k|4k)/(index\.m3u8|segment_[0-9]+\.ts)" |
| 75 | +strict_pattern = ( |
| 76 | + r"^/v/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-" |
| 77 | + r"[0-9a-f]{4}-[0-9a-f]{12}/" |
| 78 | + r"(opener\.mp4|hls/master\.m3u8|hls/segment_[0-9]+\.ts|" |
| 79 | + + ladder |
| 80 | + + r")$" |
| 81 | +) |
| 82 | +lines = source.read_text(encoding="utf-8").splitlines(keepends=True) |
| 83 | +found = False |
| 84 | +changed = False |
| 85 | +output = [] |
| 86 | +
|
| 87 | +for line in lines: |
| 88 | + stripped = line.rstrip("\n") |
| 89 | + newline = "\n" if line.endswith("\n") else "" |
| 90 | + leading = stripped[: len(stripped) - len(stripped.lstrip())] |
| 91 | + tokens = stripped.strip().split() |
| 92 | + if ( |
| 93 | + "path_regexp" in line |
| 94 | + and "^/v/" in line |
| 95 | + and "hls/master\\.m3u8" in line |
| 96 | + and "hls/segment_[0-9]+\\.ts" in line |
| 97 | + ): |
| 98 | + found = True |
| 99 | + if ladder in stripped: |
| 100 | + output.append(line) |
| 101 | + continue |
| 102 | + if not stripped.endswith(")$"): |
| 103 | + raise SystemExit("signed playback regexp has an unsupported shape") |
| 104 | + output.append(f"{stripped[:-2]}|{ladder})${newline}") |
| 105 | + changed = True |
| 106 | + elif ( |
| 107 | + len(tokens) >= 2 |
| 108 | + and "/v/" in line |
| 109 | + and "hls/master.m3u8" in line |
| 110 | + and "hls/segment_" in line |
| 111 | + and ("path" in {tokens[0], tokens[1]}) |
| 112 | + ): |
| 113 | + found = True |
| 114 | + if tokens[0].startswith("@") and tokens[1] == "path": |
| 115 | + output.append(f"{leading}{tokens[0]} path_regexp canonical_playback {strict_pattern}{newline}") |
| 116 | + elif tokens[0] == "path": |
| 117 | + output.append(f"{leading}path_regexp canonical_playback {strict_pattern}{newline}") |
| 118 | + else: |
| 119 | + raise SystemExit("signed playback path matcher has an unsupported shape") |
| 120 | + changed = True |
| 121 | + else: |
| 122 | + output.append(line) |
| 123 | +
|
| 124 | +if not found: |
| 125 | + target.write_text("".join(output), encoding="utf-8") |
| 126 | + print("not_found") |
| 127 | + raise SystemExit(0) |
| 128 | +
|
| 129 | +target.write_text("".join(output), encoding="utf-8") |
| 130 | +print("changed" if changed else "unchanged") |
| 131 | +PY |
| 132 | +)" |
| 133 | + |
| 134 | +case "$patch_status" in |
| 135 | + changed | unchanged | not_found) ;; |
| 136 | + *) die "unexpected patch status: $patch_status" ;; |
| 137 | +esac |
| 138 | + |
| 139 | +if [[ "$patch_status" == "not_found" ]]; then |
| 140 | + echo "Edge Caddy playback routes do not use a deploy-managed allowlist; leaving $caddyfile unchanged" |
| 141 | + exit 0 |
| 142 | +fi |
| 143 | + |
| 144 | +if [[ "$patch_status" == "unchanged" ]]; then |
| 145 | + echo "Edge Caddy playback routes already include HLS ladder paths" |
| 146 | + exit 0 |
| 147 | +fi |
| 148 | + |
| 149 | +if [[ "$dry_run" == "true" ]]; then |
| 150 | + echo "Edge Caddy playback routes would be updated in $caddyfile" |
| 151 | + exit 0 |
| 152 | +fi |
| 153 | + |
| 154 | +stamp="$(date -u +%Y%m%dT%H%M%SZ)" |
| 155 | +cp -p "$caddyfile" "${caddyfile}.bak.${stamp}" |
| 156 | +cat "$tmp" > "$caddyfile" |
| 157 | +echo "Updated edge Caddy playback routes in $caddyfile" |
| 158 | + |
| 159 | +if [[ "$skip_reload" == "true" ]]; then |
| 160 | + echo "Skipped Caddy validate/reload" |
| 161 | + exit 0 |
| 162 | +fi |
| 163 | + |
| 164 | +require_command caddy |
| 165 | +require_command systemctl |
| 166 | + |
| 167 | +caddy fmt --overwrite "$caddyfile" |
| 168 | +caddy validate --config "$caddyfile" |
| 169 | +systemctl reload caddy |
| 170 | +echo "Caddy reload completed" |
0 commit comments