#!/bin/sh
# httpd-iworx-h2-check (CPS-60): turn HTTP/2 on the InterWorx panel off
# while its TLS 1.2 cipher order would make every HTTP/2 client fail.
#
# HTTP/2 forbids a set of TLS 1.2 ciphers (RFC 7540 appendix A). With
# SSLHonorCipherOrder on, the panel's own cipher order picks the cipher, so
# if the first TLS 1.2 suite that fits its certificate is on that list (for
# example a CBC-first list set in NodeWorx), no TLS 1.2 client can use
# HTTP/2. On EL8/EL9 that is harmless: our mod_ssl patch reports the
# negotiated cipher at ALPN time, mod_http2 declines h2 for that connection
# and the client falls back to http/1.1. EL7's OpenSSL 1.0.2 can't report it
# in time, so h2 would be accepted and the session then torn down. This
# script is therefore installed on EL7 only, run by the systemd drop-in at
# every start and every reload of httpd-iworx.service (a NodeWorx cipher
# change ends in one of those), before httpd reads its config.
#
# It writes "Define IWORX_HTTP2_OFF" to httpd-http2.conf (read by
# httpd-24.conf) when h2 has to be off, and removes that file again once the
# cipher order no longer rules h2 out. It only ever touches a file it wrote
# itself (first line below), leaves everything alone when any part of the
# check can't be evaluated, and always exits 0 so it can never keep the
# panel from starting.
#
# IWORX_HTTPD_CONF and IWORX_HTTP2_CONF override the paths, for testing.

CONF=${IWORX_HTTPD_CONF:-/home/interworx/etc/httpd/httpd.conf}
H2CONF=${IWORX_HTTP2_CONF:-/home/interworx/etc/httpd/httpd-http2.conf}
MARKER='# Written by the httpd-iworx package'

# Run by systemd with --systemd (JOURNAL_STREAM doesn't exist before systemd
# 231, so EL7 can't tell): stderr already lands in the journal, and logging it
# again would show every message twice. Elsewhere (the package's %post) do both.
[ "$1" = --systemd ] && SYSTEMD=1
warn() {
    echo "httpd-iworx: $1" >&2
    [ -n "$SYSTEMD" ] || logger -t httpd-iworx "$1" 2>/dev/null || :
}

# The effective config as logical lines, in order, with Include and
# IncludeOptional followed (up to a depth of 5) and "\" continuations joined.
emit() {
    [ -r "$1" ] && [ "$2" -lt 6 ] || return 0
    awk '{ sub(/\r$/, "")
           if (sub(/\\$/, "")) { acc = acc $0; next }
           print acc $0; acc = "" }
         END { if (acc != "") print acc }' "$1" | while IFS= read -r LINE; do
        INC=$(printf '%s\n' "$LINE" | awk '/^[ \t]*#/ { next }
            tolower($1) == "include" || tolower($1) == "includeoptional" { p = $2; gsub(/"/, "", p); print p }')
        if [ -n "$INC" ]; then
            case $INC in
                /*) ;;
                *) INC=$SERVERROOT/$INC ;;
            esac
            for F in $INC; do
                if [ -d "$F" ]; then
                    for G in "$F"/*; do [ -f "$G" ] && emit "$G" $(($2 + 1)); done
                elif [ -f "$F" ]; then
                    emit "$F" $(($2 + 1))
                fi
            done
        else
            printf '%s\n' "$LINE"
        fi
    done
}

# True if the file is one this script wrote.
ours() { [ -f "$H2CONF" ] && head -n 1 "$H2CONF" 2>/dev/null | grep -q "^$MARKER"; }

[ -r "$CONF" ] && command -v openssl >/dev/null 2>&1 || exit 0

SERVERROOT=/home/interworx/html
CFG=$(emit "$CONF" 0)
# Last setting of each directive wins. SSLCipherSuite also has a two-argument
# form: "SSL <list>" is the TLS 1.2 list, "TLSv1.3 <list>" is not.
SR=$(printf '%s\n' "$CFG" | awk '/^[ \t]*#/ { next } tolower($1) == "serverroot" { r = $2 } END { gsub(/"/, "", r); print r }')
[ -n "$SR" ] && SERVERROOT=$SR
CERT=$(printf '%s\n' "$CFG" | awk '/^[ \t]*#/ { next } tolower($1) == "sslcertificatefile" { c = $2 } END { gsub(/"/, "", c); print c }')
HONOR=$(printf '%s\n' "$CFG" | awk '/^[ \t]*#/ { next } tolower($1) == "sslhonorcipherorder" { h = tolower($2) } END { print h }')
CIPHERS=$(printf '%s\n' "$CFG" | awk '/^[ \t]*#/ { next }
    tolower($1) == "sslciphersuite" {
        if ($2 == "TLSv1.3") next
        c = ($2 == "SSL") ? $3 : $2
    }
    END { gsub(/"/, "", c); print c }')

# Verdict: off (h2 has to be off), ok, or unknown (changes nothing).
VERDICT=unknown
if [ "$HONOR" != on ]; then
    # The client's own order decides; nothing for us to judge.
    VERDICT=ok
elif [ -n "$CIPHERS" ] && [ -n "$CERT" ]; then
    case $CERT in
        /*) ;;
        *) CERT=$SERVERROOT/$CERT ;;
    esac
    case $(openssl x509 -in "$CERT" -noout -text 2>/dev/null | grep -m 1 'Public Key Algorithm') in
        *rsaEncryption*) AU=RSA ;;
        *id-ecPublicKey*) AU=ECDSA ;;
        *) AU= ;;
    esac
    if [ -n "$AU" ]; then
        FIRST=$(openssl ciphers -v "$CIPHERS" 2>/dev/null | awk -v au="Au=$AU" '$2 != "TLSv1.3" && $4 == au { print; exit }')
        if [ -n "$FIRST" ]; then
            if echo "$FIRST" | awk '$3 ~ /^Kx=(ECDH|DH)$/ && $5 ~ /GCM|CHACHA20|CCM/ { ok = 1 } END { exit !ok }'; then
                VERDICT=ok
            else
                VERDICT=off
            fi
        fi
    fi
fi

case $VERDICT in
    off)
        NAME=$(echo "$FIRST" | awk '{ print $1 }')
        BODY=$(printf '%s\n# The panel SSL cipher order prefers %s, which HTTP/2 does not allow, so\n# HTTP/2 is turned off. Put an ECDHE/DHE GCM or CHACHA20 cipher first in\n# NodeWorx SSL settings; this file is removed at the next start or reload.\nDefine IWORX_HTTP2_OFF' "$MARKER (CPS-60)" "$NAME")
        # An admin's own file (no marker) is left as it is.
        if [ ! -e "$H2CONF" ] || { ours && [ "$(cat "$H2CONF")" != "$BODY" ]; }; then
            TMP=$H2CONF.tmp.$$
            printf '%s\n' "$BODY" > "$TMP" &&
                chown --reference="$(dirname "$H2CONF")" "$TMP" 2>/dev/null
            chmod 0600 "$TMP" 2>/dev/null
            if mv -f "$TMP" "$H2CONF"; then
                warn "WARNING: HTTP/2 turned off for the InterWorx panel: its SSL cipher order prefers $NAME, which HTTP/2 does not allow. Put an ECDHE/DHE GCM or CHACHA20 cipher first in NodeWorx SSL settings; HTTP/2 comes back at the next start or reload."
            else
                rm -f "$TMP"
            fi
        fi
        ;;
    ok)
        if ours && rm -f "$H2CONF"; then
            warn "HTTP/2 turned back on for the InterWorx panel: its SSL cipher order no longer rules it out."
        fi
        ;;
esac
exit 0
