| 1 | #!/bin/bash |
|---|
| 2 | # --------------------------------------------------------------------------- |
|---|
| 3 | # nginx-header-diff.sh |
|---|
| 4 | # |
|---|
| 5 | # Vergleicht die add_header-Direktiven einer NGINX-.conf-Datei mit dem |
|---|
| 6 | # effektiven Header-Satz einer Include-Datei (Includes werden rekursiv |
|---|
| 7 | # aufgeloest) und meldet je Block, was doppelt, was abweichend und was nur |
|---|
| 8 | # in der .conf steht. |
|---|
| 9 | # |
|---|
| 10 | # Aufruf: |
|---|
| 11 | # nginx-header-diff.sh [-p PREFIX] [-d DIREKTIVE] <datei.conf> <datei.inc> |
|---|
| 12 | # |
|---|
| 13 | # -p PREFIX Basisverzeichnis fuer relative include-Pfade. |
|---|
| 14 | # Vorgabe: /etc/nginx |
|---|
| 15 | # -d DIREKTIVE Zusaetzlich zu beruecksichtigende Direktive, mehrfach |
|---|
| 16 | # angebbar. Vorgabe: nur add_header. |
|---|
| 17 | # Sinnvoll z.B. -d proxy_hide_header. |
|---|
| 18 | # |
|---|
| 19 | # Ausgabeformat der Fundstellen entspricht grep -n: datei:zeile: text |
|---|
| 20 | # |
|---|
| 21 | # Markierungen: |
|---|
| 22 | # [DUPLIKAT] Name und Wert identisch mit dem Include -> Zeile loeschen |
|---|
| 23 | # [KONFLIKT] Name gleich, Wert abweichend -> entscheiden |
|---|
| 24 | # [NUR-CONF] im Include nicht enthalten -> Zeile behalten |
|---|
| 25 | # |
|---|
| 26 | # Grenzen: |
|---|
| 27 | # - Vollzeilen-Kommentare werden ignoriert, ebenso Kommentare am |
|---|
| 28 | # Zeilenende ausserhalb von Anfuehrungszeichen. |
|---|
| 29 | # - Direktiven ueber mehrere Zeilen werden zusammengefasst, solange die |
|---|
| 30 | # Fortsetzungszeilen kein ; { } enthalten. |
|---|
| 31 | # - Geschweifte Klammern innerhalb von Direktiv-Werten (z.B. in |
|---|
| 32 | # log_format) koennen die Blockerkennung stoeren. |
|---|
| 33 | # - include-Pfade mit Platzhaltern (conf.d/*.inc) werden nicht aufgeloest. |
|---|
| 34 | # --------------------------------------------------------------------------- |
|---|
| 35 | |
|---|
| 36 | set -u |
|---|
| 37 | |
|---|
| 38 | prefix=/etc/nginx |
|---|
| 39 | directives=(add_header) |
|---|
| 40 | |
|---|
| 41 | usage() { |
|---|
| 42 | sed -n '3,30p' "$0" | sed 's/^# \{0,1\}//' |
|---|
| 43 | exit "${1:-1}" |
|---|
| 44 | } |
|---|
| 45 | |
|---|
| 46 | while getopts ':p:d:h' opt; do |
|---|
| 47 | case "$opt" in |
|---|
| 48 | p) prefix=$OPTARG ;; |
|---|
| 49 | d) directives+=("$OPTARG") ;; |
|---|
| 50 | h) usage 0 ;; |
|---|
| 51 | :) echo "Fehlendes Argument zu -$OPTARG" >&2; exit 2 ;; |
|---|
| 52 | \?) echo "Unbekannte Option -$OPTARG" >&2; exit 2 ;; |
|---|
| 53 | esac |
|---|
| 54 | done |
|---|
| 55 | shift $((OPTIND - 1)) |
|---|
| 56 | |
|---|
| 57 | [ $# -eq 2 ] || usage 2 |
|---|
| 58 | |
|---|
| 59 | conf=$1 |
|---|
| 60 | inc=$2 |
|---|
| 61 | |
|---|
| 62 | for f in "$conf" "$inc"; do |
|---|
| 63 | [ -r "$f" ] || { echo "Nicht lesbar: $f" >&2; exit 2; } |
|---|
| 64 | done |
|---|
| 65 | |
|---|
| 66 | dirlist=$(IFS='|'; echo "${directives[*]}") |
|---|
| 67 | |
|---|
| 68 | # --------------------------------------------------------------------------- |
|---|
| 69 | # Gemeinsamer Scanner. Gibt je logischer Direktive eine Zeile aus: |
|---|
| 70 | # D <tab> startzeile <tab> direktive <tab> rest |
|---|
| 71 | # O <tab> startzeile <tab> blocktext (Blockanfang) |
|---|
| 72 | # C <tab> startzeile (Blockende) |
|---|
| 73 | # --------------------------------------------------------------------------- |
|---|
| 74 | scan_awk=' |
|---|
| 75 | function trim(s) { gsub(/^[ \t\r]+|[ \t\r]+$/, "", s); gsub(/[ \t]+/, " ", s); return s } |
|---|
| 76 | |
|---|
| 77 | function emit(stmt, d, rest) { |
|---|
| 78 | stmt = trim(stmt) |
|---|
| 79 | if (stmt == "") return |
|---|
| 80 | d = stmt |
|---|
| 81 | sub(/[ \t].*$/, "", d) |
|---|
| 82 | rest = stmt |
|---|
| 83 | sub(/^[^ \t]+[ \t]*/, "", rest) |
|---|
| 84 | printf "D\t%d\t%s\t%s\n", startLine, d, trim(rest) |
|---|
| 85 | } |
|---|
| 86 | |
|---|
| 87 | # Zeichenweise, damit ; { } innerhalb von Anfuehrungszeichen die Direktive |
|---|
| 88 | # nicht zerreissen (z.B. "max-age=63072000; includeSubDomains"). |
|---|
| 89 | { |
|---|
| 90 | len = length($0) |
|---|
| 91 | for (i = 1; i <= len; i++) { |
|---|
| 92 | c = substr($0, i, 1) |
|---|
| 93 | |
|---|
| 94 | if (q != "") { # innerhalb eines Strings |
|---|
| 95 | buf = buf c |
|---|
| 96 | if (c == q) q = "" |
|---|
| 97 | continue |
|---|
| 98 | } |
|---|
| 99 | |
|---|
| 100 | if (c == "#") break # Rest der Zeile ist Kommentar |
|---|
| 101 | |
|---|
| 102 | if (c == "\"" || c == "\x27") { |
|---|
| 103 | if (trim(buf) == "") startLine = NR |
|---|
| 104 | buf = buf c; q = c |
|---|
| 105 | continue |
|---|
| 106 | } |
|---|
| 107 | |
|---|
| 108 | if (c == ";") { emit(buf); buf = ""; continue } |
|---|
| 109 | |
|---|
| 110 | if (c == "{") { |
|---|
| 111 | printf "O\t%d\t%s\n", (trim(buf) == "" ? NR : startLine), trim(buf) |
|---|
| 112 | buf = "" |
|---|
| 113 | continue |
|---|
| 114 | } |
|---|
| 115 | |
|---|
| 116 | if (c == "}") { |
|---|
| 117 | if (trim(buf) != "") emit(buf) |
|---|
| 118 | printf "C\t%d\n", NR |
|---|
| 119 | buf = "" |
|---|
| 120 | continue |
|---|
| 121 | } |
|---|
| 122 | |
|---|
| 123 | if (trim(buf) == "") { |
|---|
| 124 | if (c == " " || c == "\t" || c == "\r") continue |
|---|
| 125 | startLine = NR |
|---|
| 126 | } |
|---|
| 127 | buf = buf c |
|---|
| 128 | } |
|---|
| 129 | buf = buf " " # Zeilenumbruch als Worttrenner |
|---|
| 130 | } |
|---|
| 131 | END { emit(buf) } |
|---|
| 132 | ' |
|---|
| 133 | |
|---|
| 134 | # --------------------------------------------------------------------------- |
|---|
| 135 | # Header-Wert normalisieren: Anfuehrungszeichen entfernen, Whitespace |
|---|
| 136 | # zusammenfassen, "always" als eigenes Merkmal fuehren. |
|---|
| 137 | # --------------------------------------------------------------------------- |
|---|
| 138 | norm_awk=' |
|---|
| 139 | function normValue(v, n, i, tok, out, always, arr) { |
|---|
| 140 | always = 0 |
|---|
| 141 | if (v ~ /[ \t]always$/ || v == "always") { always = 1; sub(/[ \t]*always$/, "", v) } |
|---|
| 142 | gsub(/^[ \t]+|[ \t]+$/, "", v) |
|---|
| 143 | n = split(v, arr, /[ \t]+/) |
|---|
| 144 | out = "" |
|---|
| 145 | for (i = 1; i <= n; i++) { |
|---|
| 146 | tok = arr[i] |
|---|
| 147 | gsub(/^["\x27]|["\x27]$/, "", tok) |
|---|
| 148 | out = out (out == "" ? "" : " ") tok |
|---|
| 149 | } |
|---|
| 150 | gsub(/[ \t]*;[ \t]*/, "; ", out) |
|---|
| 151 | gsub(/[ \t]+/, " ", out) |
|---|
| 152 | return out (always ? " +always" : "") |
|---|
| 153 | } |
|---|
| 154 | ' |
|---|
| 155 | |
|---|
| 156 | # --------------------------------------------------------------------------- |
|---|
| 157 | # Includes rekursiv aufloesen und die effektiven Header einsammeln. |
|---|
| 158 | # --------------------------------------------------------------------------- |
|---|
| 159 | tmp=$(mktemp -d) || exit 1 |
|---|
| 160 | trap 'rm -rf "$tmp"' EXIT |
|---|
| 161 | |
|---|
| 162 | seen_file=$tmp/seen |
|---|
| 163 | : >"$seen_file" |
|---|
| 164 | : >"$tmp/inc-headers" |
|---|
| 165 | |
|---|
| 166 | resolve() { |
|---|
| 167 | local file=$1 depth=$2 |
|---|
| 168 | local real |
|---|
| 169 | real=$(readlink -f -- "$file" 2>/dev/null || echo "$file") |
|---|
| 170 | |
|---|
| 171 | if grep -qxF -- "$real" "$seen_file"; then |
|---|
| 172 | echo "Warnung: Include-Zyklus bei $file uebersprungen" >&2 |
|---|
| 173 | return |
|---|
| 174 | fi |
|---|
| 175 | echo "$real" >>"$seen_file" |
|---|
| 176 | |
|---|
| 177 | if [ ! -r "$file" ]; then |
|---|
| 178 | echo "Warnung: Include nicht lesbar: $file" >&2 |
|---|
| 179 | return |
|---|
| 180 | fi |
|---|
| 181 | if [ "$depth" -gt 16 ]; then |
|---|
| 182 | echo "Warnung: Include-Tiefe > 16 bei $file" >&2 |
|---|
| 183 | return |
|---|
| 184 | fi |
|---|
| 185 | |
|---|
| 186 | local type line directive rest target |
|---|
| 187 | while IFS=$'\t' read -r type line directive rest; do |
|---|
| 188 | [ "$type" = "D" ] || continue |
|---|
| 189 | case "$directive" in |
|---|
| 190 | include) |
|---|
| 191 | target=${rest%;} |
|---|
| 192 | target=$(printf '%s' "$target" | tr -d "\"'" ) |
|---|
| 193 | target=$(printf '%s' "$target" | sed 's/[[:space:]]*$//') |
|---|
| 194 | case "$target" in |
|---|
| 195 | /*) : ;; |
|---|
| 196 | *) target=$prefix/$target ;; |
|---|
| 197 | esac |
|---|
| 198 | resolve "$target" $((depth + 1)) |
|---|
| 199 | ;; |
|---|
| 200 | *) |
|---|
| 201 | printf '%s\t%s\t%s\t%s\n' "$directive" "$rest" "$file" "$line" \ |
|---|
| 202 | >>"$tmp/inc-headers" |
|---|
| 203 | ;; |
|---|
| 204 | esac |
|---|
| 205 | done < <(awk "$scan_awk" "$file") |
|---|
| 206 | } |
|---|
| 207 | |
|---|
| 208 | resolve "$inc" 0 |
|---|
| 209 | |
|---|
| 210 | # Nur die interessierenden Direktiven behalten und normalisieren. |
|---|
| 211 | awk -F'\t' -v dirs="$dirlist" "$norm_awk"' |
|---|
| 212 | BEGIN { n = split(dirs, d, "|"); for (i = 1; i <= n; i++) want[d[i]] = 1 } |
|---|
| 213 | { |
|---|
| 214 | if (!($1 in want)) next |
|---|
| 215 | rest = $2 |
|---|
| 216 | name = rest |
|---|
| 217 | sub(/[ \t].*$/, "", name) |
|---|
| 218 | value = rest |
|---|
| 219 | sub(/^[^ \t]+[ \t]*/, "", value) |
|---|
| 220 | # Feld 6: Originalschreibweise wie in der Datei, nur Whitespace normiert |
|---|
| 221 | printf "%s\t%s\t%s\t%s\t%s\t%s\n", $1, tolower(name), normValue(value), $3, $4, rest |
|---|
| 222 | }' "$tmp/inc-headers" >"$tmp/inc-norm" |
|---|
| 223 | |
|---|
| 224 | # --------------------------------------------------------------------------- |
|---|
| 225 | # Bericht |
|---|
| 226 | # --------------------------------------------------------------------------- |
|---|
| 227 | echo "=== Effektiver Header-Satz aus $inc ===" |
|---|
| 228 | if [ ! -s "$tmp/inc-norm" ]; then |
|---|
| 229 | echo " (keine passenden Direktiven gefunden)" |
|---|
| 230 | else |
|---|
| 231 | awk -F'\t' '{ printf " %-66s [%s:%s]\n", $1 " " $6 ";", $4, $5 }' \ |
|---|
| 232 | "$tmp/inc-norm" |
|---|
| 233 | fi |
|---|
| 234 | echo |
|---|
| 235 | |
|---|
| 236 | awk "$scan_awk" "$conf" \ |
|---|
| 237 | | awk -F'\t' -v conf="$conf" -v inc="$inc" -v dirs="$dirlist" \ |
|---|
| 238 | -v incfile="$tmp/inc-norm" "$norm_awk"' |
|---|
| 239 | BEGIN { |
|---|
| 240 | n = split(dirs, d, "|"); for (i = 1; i <= n; i++) want[d[i]] = 1 |
|---|
| 241 | |
|---|
| 242 | while ((getline l < incfile) > 0) { |
|---|
| 243 | split(l, f, "\t") |
|---|
| 244 | key = f[1] "\t" f[2] |
|---|
| 245 | incName[key] = 1 |
|---|
| 246 | incPair[key "\t" f[3]] = 1 |
|---|
| 247 | # Anzeige in der Originalschreibweise der Include-Datei |
|---|
| 248 | incVals[key] = incVals[key] (incVals[key] == "" ? "" : " | ") f[1] " " f[6] ";" |
|---|
| 249 | } |
|---|
| 250 | close(incfile) |
|---|
| 251 | |
|---|
| 252 | # Basisname der Include-Datei fuer den Abgleich der include-Zeilen |
|---|
| 253 | incBase = inc |
|---|
| 254 | sub(/^.*\//, "", incBase) |
|---|
| 255 | |
|---|
| 256 | depth = 0 |
|---|
| 257 | blockCount = 0 |
|---|
| 258 | } |
|---|
| 259 | |
|---|
| 260 | function blockLabel(i) { return blockText[i] } |
|---|
| 261 | |
|---|
| 262 | $1 == "O" { |
|---|
| 263 | depth++ |
|---|
| 264 | blockCount++ |
|---|
| 265 | stack[depth] = blockCount |
|---|
| 266 | blockText[blockCount] = ($3 == "" ? "(anonym)" : $3) |
|---|
| 267 | blockLine[blockCount] = $2 |
|---|
| 268 | blockDepth[blockCount] = depth |
|---|
| 269 | next |
|---|
| 270 | } |
|---|
| 271 | |
|---|
| 272 | $1 == "C" { |
|---|
| 273 | if (depth > 0) depth-- |
|---|
| 274 | next |
|---|
| 275 | } |
|---|
| 276 | |
|---|
| 277 | $1 == "D" { |
|---|
| 278 | cur = (depth > 0 ? stack[depth] : 0) |
|---|
| 279 | if (cur == 0 && blockText[0] == "") { |
|---|
| 280 | blockText[0] = "(Dateiebene)" |
|---|
| 281 | blockLine[0] = 1 |
|---|
| 282 | blockDepth[0] = 0 |
|---|
| 283 | } |
|---|
| 284 | |
|---|
| 285 | if ($3 == "include") { |
|---|
| 286 | t = $4 |
|---|
| 287 | sub(/[ \t]*;?[ \t]*$/, "", t) |
|---|
| 288 | gsub(/["\x27]/, "", t) |
|---|
| 289 | b = t |
|---|
| 290 | sub(/^.*\//, "", b) |
|---|
| 291 | if (b == incBase) hasInc[cur] = 1 |
|---|
| 292 | else otherInc[cur] = otherInc[cur] (otherInc[cur] == "" ? "" : ", ") b |
|---|
| 293 | next |
|---|
| 294 | } |
|---|
| 295 | |
|---|
| 296 | if (!($3 in want)) next |
|---|
| 297 | |
|---|
| 298 | rest = $4 |
|---|
| 299 | sub(/[ \t]*;[ \t]*$/, "", rest) |
|---|
| 300 | name = rest |
|---|
| 301 | sub(/[ \t].*$/, "", name) |
|---|
| 302 | value = rest |
|---|
| 303 | sub(/^[^ \t]+[ \t]*/, "", value) |
|---|
| 304 | |
|---|
| 305 | lname = tolower(name) |
|---|
| 306 | key = $3 "\t" lname |
|---|
| 307 | nv = normValue(value) |
|---|
| 308 | |
|---|
| 309 | if ((key "\t" nv) in incPair) mark = "[DUPLIKAT]" |
|---|
| 310 | else if (key in incName) mark = "[KONFLIKT]" |
|---|
| 311 | else mark = "[NUR-CONF]" |
|---|
| 312 | |
|---|
| 313 | hdrCount[cur]++ |
|---|
| 314 | i = hdrCount[cur] |
|---|
| 315 | hLine[cur, i] = $2 |
|---|
| 316 | hText[cur, i] = $3 " " rest ";" |
|---|
| 317 | hMark[cur, i] = mark |
|---|
| 318 | hAlt [cur, i] = (mark == "[KONFLIKT]" ? incVals[key] : "") |
|---|
| 319 | |
|---|
| 320 | if (mark == "[DUPLIKAT]") nDup++ |
|---|
| 321 | else if (mark == "[KONFLIKT]") nCon++ |
|---|
| 322 | else nOnly++ |
|---|
| 323 | next |
|---|
| 324 | } |
|---|
| 325 | |
|---|
| 326 | END { |
|---|
| 327 | print "=== " conf " ===" |
|---|
| 328 | print "" |
|---|
| 329 | |
|---|
| 330 | any = 0 |
|---|
| 331 | for (b = 0; b <= blockCount; b++) { |
|---|
| 332 | if (hdrCount[b] == 0) continue |
|---|
| 333 | any = 1 |
|---|
| 334 | |
|---|
| 335 | indent = "" |
|---|
| 336 | for (k = 1; k < blockDepth[b]; k++) indent = indent " " |
|---|
| 337 | |
|---|
| 338 | printf "%s%s (Zeile %s)\n", indent, blockLabel(b), blockLine[b] |
|---|
| 339 | if (hasInc[b]) |
|---|
| 340 | printf "%s include %s: vorhanden\n", indent, incBase |
|---|
| 341 | else |
|---|
| 342 | printf "%s include %s: FEHLT -> einfuegen\n", indent, incBase |
|---|
| 343 | if (otherInc[b] != "") |
|---|
| 344 | printf "%s weitere includes: %s\n", indent, otherInc[b] |
|---|
| 345 | |
|---|
| 346 | for (i = 1; i <= hdrCount[b]; i++) { |
|---|
| 347 | printf "%s %s:%s: %s %s\n", indent, conf, hLine[b, i], hText[b, i], hMark[b, i] |
|---|
| 348 | if (hAlt[b, i] != "") |
|---|
| 349 | printf "%s Include hat: %s\n", indent, hAlt[b, i] |
|---|
| 350 | } |
|---|
| 351 | print "" |
|---|
| 352 | } |
|---|
| 353 | |
|---|
| 354 | if (!any) print "(keine passenden Direktiven gefunden)" |
|---|
| 355 | |
|---|
| 356 | print "---" |
|---|
| 357 | printf "DUPLIKAT %d KONFLIKT %d NUR-CONF %d\n", nDup+0, nCon+0, nOnly+0 |
|---|
| 358 | if (nCon > 0) |
|---|
| 359 | print "Hinweis: KONFLIKT-Zeilen von Hand entscheiden, bevor etwas geloescht wird." |
|---|
| 360 | } |
|---|
| 361 | ' |
|---|