#!/bin/bash

# This is a helper script which generates issue file displayed at the console
# before logging in. Partly inspired by the issue-generator package
# (https://github.com/thkukuk/issue-generator).
#
# Generates several issue files:
#  - Welcome message with Agama version number (--welcome option)
#  - Agama SSL certificate fingerprints (--ssl option)
#  - SSH host key fingerprints (--ssh option)
#  - Agama access URL for all network devices (--watch-network option)
#    NOTE: in this case the script does not finish, it watches the changes in
#          the NetworkManager configuration and updates the URL if needed
#  - Agama access URL using the mDNS (Avahi) URL (--watch-avahi option),
#    NOTE: in this case the script does not finish, it watches the changes in
#          the Avahi service and updates the URL if needed
#

# issue location for the Agama SSL certificate fingerprints, it is generated as
# the last one and is used as a trigger to allow refreshing all other generated
# issues
CERT_ISSUE=/run/issue.d/50-agama-ssl-certificate.issue

# a helper function which generates the Agama welcome message displayed at the
# console
generate_welcome() {
  # get the latest version of any Agama package (except the integration tests, it lives in a
  # separate git repository and has different number of commits than the rest)
  AGAMA_VERSION=$(rpm -qa | grep agama | grep -v agama-integration-tests | xargs rpm -q --queryformat \
    "%{VERSION}\n" | sed -e "s/\\.devel/+/" -e 's/+0$//' | sort -V | tail -n 1)

  ISSUE=/run/issue.d/10-agama-welcome.issue
  printf "Welcome to \\\\e{lightgreen}Agama\\\\e{reset} installer version %s! (\\\\l)\n\n" "$AGAMA_VERSION" > "$ISSUE"
}

# a helper function which displays the SSH host key fingerprints at the console
generate_ssh_fingerprints() {
  FINGERPRINTS=$(find /etc/ssh -type f -name "ssh_host_*_key" -exec ssh-keygen -l -f \{\} \; | cut -d ' ' -f 2,4 | sed -e "s/^/    /")
  ISSUE=/run/issue.d/30-live-ssh-fingerprints.issue

  printf "SSH host key fingerprints:\n%s\n\n" "$FINGERPRINTS" > "$ISSUE"
}

# a helper function which generates the Agama SSL certificate fingerprints
# displayed at the console
generate_certificate_fingerprints() {
  AGAMA_CERT=/etc/agama.d/ssl/cert.pem

  # delete the previous file if it is there
  rm -f "$CERT_ISSUE"

  if [ -e "$AGAMA_CERT" ]; then
    SHA256=$(openssl x509 -fingerprint -sha256 -noout -in "$AGAMA_CERT" | sed -e "s/^sha256 Fingerprint=//")
    SHA1=$(openssl x509 -fingerprint -sha1 -noout -in "$AGAMA_CERT" | sed -e "s/^sha1 Fingerprint=//")

    if [ -n "$SHA256" ] && [ -n "$SHA1" ]; then
      printf "Agama installer SSL certificate fingerprints:\n    SHA256: %s\n    SHA1: %s\n\n" "$SHA256" "$SHA1" \
        > "$CERT_ISSUE"
    fi
  fi

  # tell agetty to use the issues from /run
  touch /run/issue

  # reload the issues to activate the changes
  touch /run/agetty.reload
}

# message file for the Agama mDNS URL
AVAHI_MESSAGE=/run/issue.d/70-agama-connect-avahi.message
# symlink for the issue file, created only when network is available
AVAHI_ISSUE=/run/issue.d/70-agama-connect-avahi.issue
# issue file with Agama URLs
URL_ISSUES="/run/issue.d/70-agama-connect-urls.issue"
# issue displayed when there is no network connection
DISCONNECTED_ISSUE="/run/issue.d/70-agama-disconnected.issue"

# helper function, build the Agama URL messages or display a warning when
# network is not available
write_url_headers() {
  # generate a header and footer around the Agama URL issues
  ISSUE_HEADER=/run/issue.d/69-agama-connect.issue
  ISSUE_FOOTER=/run/issue.d/71-agama-connect.issue

  if [ -e "$URL_ISSUES" ]; then
    # if Avahi URL is set then display it as well
    if [ -e "$AVAHI_MESSAGE" ]; then
      ln -sf "$AVAHI_MESSAGE" "$AVAHI_ISSUE"
    fi

    rm -f "$DISCONNECTED_ISSUE"

    # at least one address present, display the header and footer
    echo "Connect to the Agama installer using these URLs:" > "$ISSUE_HEADER"
    echo > "$ISSUE_FOOTER"
  else
    # no network, delete the header, footer and the Avahi issue symlink
    rm -f "$ISSUE_HEADER" "$ISSUE_FOOTER" "$AVAHI_ISSUE"

    # display a warning message
    printf "\\\\e{brown}Network is not available, the Agama installer cannot \
be used remotely.\\\\e{reset}\n\n" > "$DISCONNECTED_ISSUE"
  fi
}

# a helper function which generates the mDNS URL for accessing the Agama server
# displayed at the console
generate_avahi_url() {
  # track the name, update the issue file only if the name is changed
  OLDNAME=""

  # watch for a systemd signal describing the status message change from the Avahi daemon
  dbus-monitor --system "sender='org.freedesktop.systemd1',\
  interface='org.freedesktop.DBus.Properties',\
  path='/org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice',type=signal" \
  2> /dev/null | while read -r line;
    do
      AVAHINAME=$(echo "$line" | grep "Server startup complete. Host name is" \
      | sed -e "s/^.*Server startup complete. Host name is \(.*\)\. Local.*$/\\1/")

      # mDNS host name found and it is different than the previous one (or the initial value)
      if [ -n "$AVAHINAME" ] && [ "$AVAHINAME" != "$OLDNAME" ]; then
        OLDNAME="$AVAHINAME"
        echo "    https://$AVAHINAME" > "$AVAHI_MESSAGE"
        write_url_headers

        # reload if not in the initial state
        if [ -e "$CERT_ISSUE" ]; then
          touch /run/agetty.reload
        fi
      fi

      # daemon stopped, remove the message file
      if echo "$line" | grep -q "avahi-daemon .* exiting"; then
        OLDNAME=""
        rm -f "$AVAHI_MESSAGE"
        write_url_headers
        touch /run/agetty.reload
      fi
    done
}

# function for centering text
# $1 - the text
# $2 - requested width
function center_text() {
  LEN=${#1}
  PADDING_LEN=$(($2 - LEN))
  PADDING_LEN=$((PADDING_LEN / 2))
  PADDING="$(printf '%*s' $PADDING_LEN)"
  echo "$PADDING$1$PADDING"
}

# generate QR codes for the access URLs
function create_qr_codes() {
    ADDRESSES=("$@")
    # width of the generated QR code
    QR_WIDTH=30

    # check if serial console is used, get the terminal size (width)
    TERM_WIDTH=$(stty -F /dev/ttyS0 size 2> /dev/null | cut -d " " -f 2)

    # otherwise check the first console size
    if [ -z "$TERM_WIDTH" ]; then
      TERM_WIDTH=$(stty -F /dev/tty1 size 2> /dev/null | cut -d " " -f 2)
      echo "Linux console width $TERM_WIDTH"
    else
      echo "Serial console width $TERM_WIDTH"
    fi

    # display QR codes only if the terminal is bigger than the 80x24(25) default
    if [ -n "$TERM_WIDTH" ] && [ "$TERM_WIDTH" -gt 80 ]; then
      # compute how much QR codes can fit on the screen side-by-side
      QR_NUM=$(( TERM_WIDTH / QR_WIDTH ))
      # split the list into the IPv4 and IPv6 addresses, display QR codes only for the IPv4 addresses
      IP4ADDR=()
      IP6ADDR=()
      for ADDR in "${ADDRESSES[@]}"; do [[ "$ADDR" =~ : ]] && IP6ADDR+=("$ADDR") || IP4ADDR+=("$ADDR"); done
      # split the IPv4 list into 2 parts, for the first part display the QR codes,
      QR_ADDRESSES=("${IP4ADDR[@]:0:$QR_NUM}")
      SZ="${#IP4ADDR[@]}"
      # for the rest display just the text URL as the list is too long
      REST_ADDRESSES=("${IP4ADDR[@]:$QR_NUM:$SZ}")
    else
      QR_ADDRESSES=()
      REST_ADDRESSES=("${ADDRESSES[@]}")
    fi

    if [ -n "${REST_ADDRESSES[*]}" ]; then
      printf "    https://%s\n" "${REST_ADDRESSES[@]}" >> "$URL_ISSUES"
    fi
    if [ -n "${IP6ADDR[*]}" ]; then
      printf "    https://[%s]\n" "${IP6ADDR[@]}" >> "$URL_ISSUES"
    fi

    if [ -z "${QR_ADDRESSES[*]}" ]; then
      return 0
    fi

    # temporary file for generated QR code
    QR_TEMP=$(mktemp)
    # temporary file for merged QR codes (displayed side-by-side)
    QR_RESULT=$(mktemp)
    # copy of the merged QR codes (the merged file cannot be used as input and
    # output at the same time)
    QR_RESULT_COPY=$(mktemp)

    # label with URLs displayed below the QR codes
    LABEL=""

    # generate the QR codes and merge them side-by-side
    for ADDR in "${QR_ADDRESSES[@]}"; do
      cp "$QR_RESULT" "$QR_RESULT_COPY"
      URL="https://$ADDR"
      echo "Rendering QR code for $URL"
      # force (the -v option) using at least symbol version 2 (QR size 25x25),
      # for short addresses (like https://1.1.1.1) it would be enough using
      # version 1 (QR size 21x21), but longer addresses need version 2 and
      # putting different sizes side-by-side breaks formatting, see `man
      # qrencode` and https://www.qrcode.com/en/about/version.html
      qrencode -t ANSIUTF8 -m 2 -v 2 -o "$QR_TEMP" "$URL"
      # put the QR codes side-by-side
      paste -d " " "$QR_RESULT_COPY" "$QR_TEMP" > "$QR_RESULT"
      PADDED_URL=$(center_text "$URL" "$QR_WIDTH")
      LABEL="$LABEL$PADDED_URL "
    done

    cat "$QR_RESULT" >> "$URL_ISSUES"
    echo "$LABEL" >> "$URL_ISSUES"

    # delete the temporary files
    rm -f "$QR_TEMP" "$QR_RESULT_COPY" "$QR_RESULT"
}

# helper function, write the issue with the currently available URLs for
# accessing Agama from outside
build_addresses() {
  ADDRESSES=()

  readarray -t CONNECTIONS < <(busctl -j get-property org.freedesktop.NetworkManager /org/freedesktop/NetworkManager org.freedesktop.NetworkManager ActiveConnections | jq --raw-output ".data[]")
  for CONNECTION in "${CONNECTIONS[@]}"; do
    TYPE=$(busctl -j get-property org.freedesktop.NetworkManager "$CONNECTION" org.freedesktop.NetworkManager.Connection.Active Type 2> /dev/null | jq --raw-output ".data")

    # ignore loopbacks, we need external addresses
    if [ "$TYPE" != "loopback" ]; then
      IP4CONFIG=$(busctl -j get-property org.freedesktop.NetworkManager "$CONNECTION" org.freedesktop.NetworkManager.Connection.Active Ip4Config 2> /dev/null | jq --raw-output ".data")
      mapfile -t IP4ADDRESSES < <(busctl -j get-property org.freedesktop.NetworkManager "$IP4CONFIG" org.freedesktop.NetworkManager.IP4Config AddressData 2> /dev/null | jq --raw-output ".data[].address.data")
      ADDRESSES+=("${IP4ADDRESSES[@]}")

      IP6CONFIG=$(busctl -j get-property org.freedesktop.NetworkManager "$CONNECTION" org.freedesktop.NetworkManager.Connection.Active Ip6Config 2> /dev/null | jq --raw-output ".data")
      # ignore IPv6 link local addresses starting with "fe80:", they are not supported by browsers
      mapfile -t IP6ADDRESSES < <(busctl -j get-property org.freedesktop.NetworkManager "$IP6CONFIG" org.freedesktop.NetworkManager.IP6Config AddressData 2> /dev/null | jq --raw-output ".data[].address.data" | grep -i -v ^fe80:)
      ADDRESSES+=("${IP6ADDRESSES[@]}")
    fi
  done

  # remove duplicates
  readarray -t ADDRESSES < <(printf "%s\n" "${ADDRESSES[@]}" | sort -u)
  echo "Found external addresses: ${ADDRESSES[*]}"

  # delete the old file
  rm -f "$URL_ISSUES"

  if [ -n "${ADDRESSES[*]}" ]; then
    create_qr_codes "${ADDRESSES[@]}"
  fi

  write_url_headers

  # reload if not in the initial state
  if [ -e "$CERT_ISSUE" ]; then
    touch /run/agetty.reload
  fi
}

# a helper function which generates the URLs for accessing the Agama server
# displayed at the console
generate_network_url() {
  # build a message with the current URLs
  build_addresses

  # watch for IP address changes in the NetworkManager service
  dbus-monitor --system "sender='org.freedesktop.NetworkManager',\
  interface='org.freedesktop.DBus.Properties',member='PropertiesChanged',\
  type=signal" 2> /dev/null | while read -r line; do
    # some IP4 configuration has been changed, rebuild the URLs
    if echo "$line" | grep -q -E 'string "org.freedesktop.NetworkManager.IP(4|6)Config"'; then
      echo "Network configuration changed"
      build_addresses
    fi
  done
}

# wait until the SSL fingreprint issue is created with a time limit passed as
# the second argument (in seconds)
wait_for_ssl_issue() {
  for _ in $(seq 1 "$1"); do
    [ -f "$CERT_ISSUE" ] && exit 0
    sleep 1
  done
}

# make sure the parent directory for the issues exists
mkdir -p /run/issue.d

# create the issue file for specified item
if [ "$1" = "--welcome" ]; then
  generate_welcome
elif [ "$1" = "--ssh" ]; then
  generate_ssh_fingerprints
elif [ "$1" = "--ssl" ]; then
  generate_certificate_fingerprints
elif [ "$1" = "--wait-for-ssl" ]; then
  wait_for_ssl_issue "$2"
elif [ "$1" = "--watch-network" ]; then
  generate_network_url "$2" "$3"
elif [ "$1" = "--watch-avahi" ]; then
  generate_avahi_url
else
  echo "Missing or incorrect argument"
  exit 1
fi
