#!/usr/bin/env bash
set -eo pipefail
[[ $SSHCOMMAND_TRACE ]] && set -x
shopt -s nocasematch # For case insensitive string matching, for the first parameter

if [[ -f /etc/defaults/sshcommand ]]; then
  # shellcheck disable=SC1091
  source /etc/defaults/sshcommand
fi

declare SSHCOMMAND_VERSION=""
declare SSHCOMMAND_CHECK_DUPLICATE_FINGERPRINT=${SSHCOMMAND_CHECK_DUPLICATE_FINGERPRINT:="true"}
declare SSHCOMMAND_CHECK_DUPLICATE_NAME=${SSHCOMMAND_CHECK_DUPLICATE_NAME:="true"}

cmd-help() {
  declare desc="Shows help information for a command"
  declare args="$*"
  if [[ "$args" ]]; then
    for cmd; do true; done # last arg
    local fn="sshcommand-$cmd"
    fn-info "$fn" 1
  fi
}

fn-args() {
  declare desc="Inspect a function's arguments"
  local argline
  argline=$(type "$1" | grep declare | grep -v "declare desc" | head -1)
  echo -e "${argline// /"\\n"}" | awk -F= '/=/{print "<"$1">"}' | tr "\\n" " "
}

fn-desc() {
  declare desc="Inspect a function's description"
  desc=""
  eval "$(type "$1" | grep desc | head -1)"
  echo "$desc"
}

fn-info() {
  declare desc="Inspects a function"
  declare fn="$1" showsource="$2"
  local fn_name="${1//sshcommand-/}"
  echo "$fn_name $(fn-args "$fn")"
  echo "  $(fn-desc "$fn")"
  echo
  if [[ "$showsource" ]]; then
    type "$fn" | tail -n +2
    echo
  fi
}

fn-print-os-id() {
  declare desc="Returns the release id of the operating system"
  local OSRELEASE="${SSHCOMMAND_OSRELEASE:="/etc/os-release"}"
  if [[ -f $OSRELEASE ]]; then
    sed -n 's#^ID=\(.*\)#\1#p' "$OSRELEASE" | tr -d '"'
  else
    echo unknown
  fi
  return 0
}

fn-adduser() {
  declare desc="Add a user to the system"
  local l_user l_platform

  l_user=$1
  l_platform="$(fn-print-os-id)"
  case $l_platform in
    alpine)
      adduser -D -g "" -s /bin/bash "$l_user"
      passwd -u "$l_user"
      ;;
    debian* | ubuntu | raspbian*)
      adduser --disabled-password --gecos "" "$l_user"
      ;;
    arch | amzn)
      useradd -m -s /bin/bash "$l_user"
      usermod -L -aG "$l_user" "$l_user"
      ;;
    *)
      useradd -m -s /bin/bash "$l_user"
      groupadd "$l_user"
      usermod -L -aG "$l_user" "$l_user"
      ;;
  esac
}

fn-verify-file() {
  declare desc="Test that public key is valid"
  declare file="$1"
  local has_errors=false

  local key line=0
  local TMP_KEY_FILE
  TMP_KEY_FILE=$(mktemp "/tmp/dokku-${DOKKU_PID}-${FUNCNAME[0]}.XXXXXX")
  # shellcheck disable=SC2064
  trap "rm -rf '$TMP_KEY_FILE' >/dev/null" RETURN INT TERM EXIT

  SSHCOMMAND_IGNORE_LIST_WARNINGS="${SSHCOMMAND_IGNORE_LIST_WARNINGS:-false}"
  while read -r key; do
    line=$((line + 1))
    [[ -z "$key" ]] && continue
    [[ "$key" =~ ^#.*$ ]] && continue

    echo "$key" >"$TMP_KEY_FILE"
    if ! ssh-keygen -lf "$TMP_KEY_FILE" &>/dev/null; then
      has_errors=true
      if [[ "$SSHCOMMAND_IGNORE_LIST_WARNINGS" == "false" ]]; then
        log-warn "${file} line $line failed ssh-keygen check."
      else
        log-warn "${file} line $line failed ssh-keygen check, ignoring."
      fi
    fi
  done <"${file}"

  if [[ "$has_errors" == "true" ]]; then
    return 1
  fi
}

log-fail() {
  declare desc="Log fail formatter"
  echo "$@" 1>&2
  exit 1
}

log-warn() {
  declare desc="Log warn formatter"
  echo "$@" 1>&2
}

log-verbose() {
  declare desc="Log verbose formatter"
  if [[ -n "$SSHCOMMAND_VERBOSE_OUTPUT" ]]; then
    echo "$@"
  fi
}

sshcommand-create() {
  declare desc="Creates a local system user and installs sshcommand skeleton"
  declare USER="$1" COMMAND="$2"
  local USERHOME

  if [[ -z "$USER" ]] || [[ -z "$COMMAND" ]]; then
    log-fail "Usage: sshcommand create" "$(fn-args "sshcommand-create")"
  fi

  if id -u "$USER" >/dev/null 2>&1; then
    log-verbose "User '$USER' already exists"
  else
    fn-adduser "$USER"
  fi

  USERHOME=$(sh -c "echo ~$USER")
  mkdir -p "$USERHOME/.ssh"
  touch "$USERHOME/.ssh/authorized_keys"
  chmod 0700 "$USERHOME/.ssh/authorized_keys"
  echo "$COMMAND" >"$USERHOME/.sshcommand"
  chown -R "$USER" "$USERHOME"
}

sshcommand-acl-add() {
  declare desc="Adds named SSH key to user from STDIN or argument"
  declare USER="$1" NAME="$2" KEY_FILE="$3"
  local ALLOWED_KEYS FINGERPRINT KEY KEY_FILE KEY_PREFIX NEW_KEY USERHOME

  if [[ -z "$USER" ]] || [[ -z "$NAME" ]]; then
    log-fail "Usage: sshcommand acl-add" "$(fn-args "sshcommand-acl-add")"
  fi

  getent passwd "$USER" >/dev/null || false
  USERHOME=$(sh -c "echo ~$USER")

  NEW_KEY=$(grep "NAME=\\\\\"$NAME"\\\\\" "$USERHOME/.ssh/authorized_keys" || true)
  if [[ "$SSHCOMMAND_CHECK_DUPLICATE_NAME" == "true" ]] && [[ -n "$NEW_KEY" ]]; then
    log-fail "Duplicate ssh key name"
  fi

  if [[ -z "$KEY_FILE" ]]; then
    KEY=$(cat)
  else
    KEY=$(cat "$KEY_FILE")
  fi

  local count
  count="$(wc -l <<<"$KEY")"
  [[ "$count" -eq 1 ]] || log-fail "Too many keys provided, set one per invocation of sshcommand acl-add <USER> <NAME>"

  FINGERPRINT=$(ssh-keygen -lf <(echo "command=\"dummy to fail when options already exist\" $KEY") | awk '{print $2}')

  if [[ ! "$FINGERPRINT" =~ :.* ]]; then
    log-fail "Invalid ssh public key"
  fi

  if [[ "$SSHCOMMAND_CHECK_DUPLICATE_FINGERPRINT" == "true" ]] && grep -qF "$FINGERPRINT" "$USERHOME/.ssh/authorized_keys"; then
    log-fail "Duplicate ssh public key specified"
  fi

  ALLOWED_KEYS="${SSHCOMMAND_ALLOWED_KEYS:="no-agent-forwarding,no-user-rc,no-X11-forwarding,no-port-forwarding"}"
  KEY_PREFIX="command=\"FINGERPRINT=$FINGERPRINT NAME=\\\"$NAME\\\" \`cat $USERHOME/.sshcommand\` \$SSH_ORIGINAL_COMMAND\",$ALLOWED_KEYS"
  echo "$KEY_PREFIX $KEY" >>"$USERHOME/.ssh/authorized_keys"
  chmod 0700 "$USERHOME/.ssh/authorized_keys"
  echo "$FINGERPRINT"
}

sshcommand-acl-remove() {
  declare desc="Removes SSH key by name"
  declare USER="$1" NAME="$2"
  local USERHOME

  if [[ -z "$USER" ]] || [[ -z "$NAME" ]]; then
    log-fail "Usage: sshcommand acl-remove" "$(fn-args "sshcommand-acl-remove")"
  fi

  getent passwd "$USER" >/dev/null || false
  USERHOME=$(sh -c "echo ~$USER")

  sed --in-place "/ NAME=\\\\\"$NAME\\\\\" /d" "$USERHOME/.ssh/authorized_keys"
  chmod 0700 "$USERHOME/.ssh/authorized_keys"
}

sshcommand-acl-remove-by-fingerprint() {
  declare desc="Removes SSH key by fingerprint"
  declare USER="$1" FINGERPRINT="$2"
  local USERHOME

  if [[ -z "$USER" ]] || [[ -z "$FINGERPRINT" ]]; then
    log-fail "Usage: sshcommand acl-remove-by-fingerprint" "$(fn-args "sshcommand-acl-remove-by-fingerprint")"
  fi

  getent passwd "$USER" >/dev/null || false
  USERHOME=$(sh -c "echo ~$USER")

  # shellcheck disable=SC1117
  sed --in-place "\#\"FINGERPRINT=$FINGERPRINT #d" "$USERHOME/.ssh/authorized_keys"
  chmod 0700 "$USERHOME/.ssh/authorized_keys"
}

sshcommand-list() {
  declare desc="Lists SSH keys by user, an optional name and a optional output format (JSON)"
  declare userhome USER="$1" NAME="$2" OUTPUT_TYPE="${3:-$2}"
  [[ -z "$USER" ]] && log-fail "Usage: sshcommand list" "$(fn-args "sshcommand-list")"
  SSHCOMMAND_IGNORE_LIST_WARNINGS="${SSHCOMMAND_IGNORE_LIST_WARNINGS:-false}"

  getent passwd "$USER" >/dev/null || log-fail "\"$USER\" is not a user on this system"
  userhome=$(sh -c "echo ~$USER")
  if [[ ! -e "$userhome/.ssh/authorized_keys" ]]; then
    log-warn "authorized_keys not found for $USER"
    if [[ "$SSHCOMMAND_IGNORE_LIST_WARNINGS" == "false" ]]; then
      return 1
    fi
    return
  fi

  if [[ ! -s "$userhome/.ssh/authorized_keys" ]]; then
    log-warn "authorized_keys is empty for $USER"
    if [[ "$SSHCOMMAND_IGNORE_LIST_WARNINGS" == "false" ]]; then
      return 1
    fi

    if [[ "$OUTPUT_TYPE" == "json" ]]; then
      echo "[]"
    fi
    return
  fi

  if ! fn-verify-file "$userhome/.ssh/authorized_keys" && [[ "$SSHCOMMAND_IGNORE_LIST_WARNINGS" == "false" ]]; then
    return 1
  fi

  if [[ -n "$OUTPUT_TYPE" ]] && [[ "$OUTPUT_TYPE" == "json" ]]; then
    data=$(sed --silent --regexp-extended \
      's/^command="FINGERPRINT=(\S+) NAME=(\\"|)(.*)\2 `.*",(\S+) (.*)/{ "fingerprint": "\1", "name": "\3", "SSHCOMMAND_ALLOWED_KEYS": "\4", "public-key": "\5" }/p' \
      "$userhome/.ssh/authorized_keys" | tr '\n' ',' | sed '$s/,$/\n/')

    if [[ -n "$NAME" ]]; then
      echo "[${data}]" | jq -cM --arg NAME "$NAME" 'map( select (.name == $NAME) )'
    else
      echo "[${data}]"
    fi
  else
    OUTPUT="$(sed --silent --regexp-extended \
      's/^command="FINGERPRINT=(\S+) NAME=(\\"|)(.*)\2 `.*",(\S+).*/\1 NAME="\3" SSHCOMMAND_ALLOWED_KEYS="\4"/p' \
      "$userhome/.ssh/authorized_keys")"
    if [[ -n "$NAME" ]]; then
      echo "$OUTPUT" | grep "NAME=\"$NAME\""
    else
      echo "$OUTPUT"
    fi
  fi
}

sshcommand-help() {
  declare desc="Shows help information"
  declare COMMAND="$1"

  if [[ -n "$COMMAND" ]]; then
    cmd-help "$COMMAND"
    return 0
  fi

  echo "sshcommand ${SSHCOMMAND_VERSION}"
  echo ""
  printf "  %-25s %-30s %s\\n" "create" "$(fn-args "sshcommand-create")" "$(fn-desc "sshcommand-create")"
  printf "  %-25s %-30s %s\\n" "acl-add" "$(fn-args "sshcommand-acl-add")" "$(fn-desc "sshcommand-acl-add")"
  printf "  %-25s %-30s %s\\n" "acl-remove" "$(fn-args "sshcommand-acl-remove")" "$(fn-desc "sshcommand-acl-remove")"
  printf "  %-25s %-30s %s\\n" "acl-remove-by-fingerprint" "$(fn-args "sshcommand-acl-remove-by-fingerprint")" "$(fn-desc "sshcommand-acl-remove-by-fingerprint")"
  printf "  %-25s %-30s %s\\n" "list" "$(fn-args "sshcommand-list")" "$(fn-desc "sshcommand-list")"
  printf "  %-25s %-30s %s\\n" "help" "$(fn-args "sshcommand-help")" "$(fn-desc "sshcommand-help")"
  printf "  %-25s %-30s %s\\n" "version" "$(fn-args "sshcommand-version")" "$(fn-desc "sshcommand-version")"
}

sshcommand-version() {
  declare desc="Shows version"
  echo "sshcommand ${SSHCOMMAND_VERSION}"
}

main() {
  declare COMMAND_SUFFIX="$1"
  if [[ -z "$COMMAND_SUFFIX" ]]; then
    sshcommand-help "$@"
    exit 1
  fi

  if [[ "$COMMAND_SUFFIX" == "-h" ]] || [[ "$COMMAND_SUFFIX" == "--help" ]]; then
    COMMAND_SUFFIX="help"
  fi

  if [[ "$COMMAND_SUFFIX" == "-v" ]] || [[ "$COMMAND_SUFFIX" == "--version" ]]; then
    COMMAND_SUFFIX="version"
  fi

  local cmd="sshcommand-$COMMAND_SUFFIX"
  shift 1

  if declare -f "$cmd" >/dev/null; then
    $cmd "$@"
  else
    log-fail "Invalid command"
  fi
}

# shellcheck disable=SC2128
if [[ "$0" == "$BASH_SOURCE" ]]; then
  main "$@"
fi
