#!/bin/bash
# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
source "$(dirname "$0")/lib/ulib/ulib" || exit 1
urequire DEFAULTS

function display_help() {
    uecho "$scriptname: se connecter par ssh à un ou plusieurs hôtes

USAGE
    $scriptname [options] hosts
    $scriptname [options] @hostsfile
    $scriptname -r hosts
    $scriptname --parse hosts

OPTIONS
    hosts
    @hostsfile
        Spécifier un ou plusieurs hôtes distants sur lequels faire la connexion.
        Pour spécifier plusieurs hôtes, il faut les séparer par un espace ou le
        caractère ':', e.g. 'host1 host2' ou 'host1:host2'. Si la spécification
        contient les caractères { et }, l'expansion est effectuée, e.g
            'root@{host1,host2}.univ.run'
        La forme @hostsfile permet de lire la liste des hôtes depuis le fichier
        hostsfile, à raison d'un hôte par ligne.

Toutes les options de ssh sont reconnues. Les options longues suivantes sont
reconnues comme alias de certaines options courtes de ssh:
    --quiet
        alias de -q, activer le mode non verbeux
    --tty
        alias de -t, forcer l'allocation d'un TTY
    --login USER
        alias de -l, spécifier le user avec lequel se connecter
    --port PORT
        alias de -p, spécifier le port sur lequel se connecter

Les options suivantes sont exclusives à ce script:
    -d, --domain DOMAIN
        Spécifier un domaine par défaut pour les hôtes qui sont spécifiés sans
        domaine.
    -z, --ssh SSH
        Spécifier l'exécutable à utiliser pour lancer ssh.
    -r, --remove
        Lancer 'ssh-keygen -R' pour chacun des hôtes spécifiés avant de s'y
        connecter. Par exemple:
            $scriptname -r host.tld
        est équivalent à:
            ssh-keygen -R host.tld
            ssh-keygen -R host
            ssh-keygen -R 10.10.1.5
            ssh host.tld
        si l'adresse ip de host.tld est 10.10.1.5
        Quand cette option est spécifié, l'option -j est reconnue et permet de
        NE PAS se reconnecter à l'hôte juste après avoir nettoyé les clés. Avec
        l'option -j, TOUS les arguments sont des noms d'hôte puisqu'aucune
        connexion n'est effectuée.
    --exec
    --no-exec
        Avec --exec, si un seul hôte est spécifié, lancer le processus ssh avec
        exec, pour éviter d'encombrer la mémoire. C'est l'option par défaut.
        Avec --no-exec, ne jamais utiliser exec pour lancer ssh.
    --parse
        Afficher la définition des variables ssh, options, hosts et args qui
        permettent d'effectuer la connexion à partir d'un autre script. Exemple:
            "'eval "$(ussh --parse args...)"
            for host in "${hosts[@]}"; do
                ${exec:+exec} "$ssh" "${options[@]}" "$host" "${args[@]}"
            done'"
    --cc
        Assumer que nutools est installé sur l'hôte distant, et y lancer uwatch
        avec l'option --cc, pour permettre de garder la connexion active dans le
        cadre d'une redirection de port.

Si la variable UTOOLS_USSH_RSYNC_SUPPORT contient une valeur non vide, l'analyse
des arguments s'arrête à la première valeur qui n'est pas une option, afin de
permettre l'utilisation de ce script avec l'option -e de rsync."
}

__PARSED_HOSTS=()
__PARSED_FILES=()
function parse_hostsfile() {
    # Lire chacun des fichiers $* et initialiser __PARSED_HOSTS avec la liste
    # des hôtes mentionnés dans les fichiers.
    local inputfile basedir inputs input
    for inputfile in "$@"; do
        inputfile="$(abspath "$inputfile")"
        array_contains __PARSED_FILES "$inputfile" && {
            ewarn "$(ppath "$inputfile"): inclusion récursive"
            continue
        }
        array_add __PARSED_FILES "$inputfile"
        basedir="$(dirname "$inputfile")"

        array_from_lines inputs "$(<"$inputfile" filter_conf)" || {
            ewarn "$inputfile: fichier ingnoré"
            continue
        }
        for input in "${inputs[@]}"; do
            if [ "${input#@}" != "$input" ]; then
                # fichier inclus
                parse_hostsfile "$(abspath "${input#@}" "$basedir")"
            else
                array_addu __PARSED_HOSTS "$input"
            fi
        done
    done
}
function __expand_braces() {
    if [[ "$1" == *{* ]] && [[ "$1" == *}* ]]; then
        eval "echo $1"
    else
        echo "$1"
    fi
}
function __dot_is_localhost() {
    [ "$1" == "." ] && echo "localhost" || echo "$1"
}
function __fix_domain() {
    local user host
    splituserhost "$1" user host
    if [ "$host" == localhost ]; then
        : # ne pas corriger localhost
    elif [ "${host%.}" != "$host" ]; then
        # si le nom se termine par ., le prendre tel quel
        host="${host%.}"
    elif ! [[ "$host" == *.* ]]; then
        # sinon rajouter le domaine par défaut le cas échéant
        host="$host${DOMAIN:+.$DOMAIN}"
    fi
    echo "${user:+$user@}$host"
}
function fix_hosts() {
    # Si hosts contient des éléments multiple, comme a:b, séparer ces
    # éléments. i.e (a b:c "d e") --> (a b c d e)
    # Supporter la syntaxe @hostsfile qui permet de charger la liste des hôtes
    # depuis un fichier.
    # Remplacer aussi les '.' par 'localhost'
    array_map hosts __expand_braces
    array_fix_paths hosts ":"
    array_fix_paths hosts " "

    local -a _hosts _tmphosts host
    for host in "${hosts[@]}"; do
        host="${host%/}"
        if [ "${host#@}" != "$host" ]; then
            __PARSED_HOSTS=()
            parse_hostsfile "${host#@}"
            array_fix_paths __PARSED_HOSTS
            array_extendu _hosts __PARSED_HOSTS
        else
            array_addu _hosts "$host"
        fi
    done
    array_map _hosts __dot_is_localhost
    array_map _hosts __fix_domain
    array_copy hosts _hosts
}

function remove_key() {
    estep "$1"
    ssh-keygen -R "$1" >&/dev/null
}
function remove_keys() {
    urequire ipcalc

    local -a __hosts; array_copy __hosts hosts
    local -a allhosts hosts ips; local host hostname user ip
    array_copy allhosts __hosts

    etitle "Suppression des entrées dans ~/.ssh/known_hosts"
    for host in "${allhosts[@]}"; do
        splituserhost "$host" user host
        if ipcalc_checkip "$host" >/dev/null; then
            ip="$host"
            remove_key "$ip"

            resolv_hosts hosts "$ip"
            for host in "${hosts[@]}"; do
                remove_key "$host"
                hostname="${host%%.*}"
                [ "$hostname" != "$host" ] && remove_key "$hostname"
            done
        else
            remove_key "$host"
            hostname="${host%%.*}"
            [ "$hostname" != "$host" ] && remove_key "$hostname"

            resolv_ips ips "$host"
            for ip in "${ips[@]}"; do
                remove_key "$ip"
            done
        fi
    done
    eend
}

function __parse_ssh_config_user() {
    local config="$1"
    if [ -f "$config" ]; then
        awkrun host="$host" '
BEGIN {
  in_host = 0
  found_user = 0
}
!found_user && tolower($1) == "host" {
  in_host = 0
  for (i = 2; i <= NF; i++) {
    if ($i == host) {
      in_host = 1
      break
    }
  }
}
in_host && tolower($1) == "user" {
  print "SSH_USER=\"" $2 "\""
  found_user = 1
}
END {
  if (found_user) print "true"
  else print "false"
}
' -- "$config"
    else
        echo "false"
    fi
}
function __update_sshopts_user() {
    # Déterminer le compte utilisateur de connexion et le placer dans la
    # variables SSH_USER. Analyser pour cela les arguments qui sont fournis dans
    # les paramètres 2..@. Ajouter l'option -l $DEFAULT_USER au tableau sshopts
    # si aucune définition pour l'hôte $1 n'existe dans l'un des fichiers de
    # configuration de ssh et si l'utilisateur n'a pas déjà spécifié un compte
    # utilisateur.
    SSH_USER=
    # L'utilisateur est peut-être fourni avec l'hôte
    if [ -n "$user" ]; then
        SSH_USER="$user"
        return
    fi
    # Analyser les configuration de ssh
    eval "$(__parse_ssh_config_user ~/.ssh/config)" && return
    eval "$(__parse_ssh_config_user /etc/ssh/ssh_config)" && return
    # Analyser les arguments
    local i=1
    while [ $i -le $# ]; do
        if [ "${!i}" == -l ]; then
            i=$(($i + 1))
            SSH_USER="${!i}"
        fi
        i=$(($i + 1))
    done
    [ -n "$SSH_USER" ] && return
    # Rajouter l'option -l au début. De cette façon, on laisse la possibilité à
    # l'utilisateur de surcharger cette option, dans le cas où nous n'avons pas
    # pu analyser les arguments correctement
    SSH_USER="$DEFAULT_USER"
    sshopts=(${DEFAULT_USER:+-l "$DEFAULT_USER"} "${sshopts[@]}")
}
function __parse_ssh_config_port() {
    local config="$1"
    if [ -f "$config" ]; then
        awkrun host="$host" '
BEGIN {
  in_host = 0
  found_port = 0
}
!found_port && tolower($1) == "host" {
  in_host = 0
  for (i = 2; i <= NF; i++) {
    if ($i == host) {
      in_host = 1
      break
    }
  }
}
in_host && tolower($1) == "port" {
  print "SSH_PORT=\"" $2 "\""
  found_port = 1
}
END {
  if (found_port) print "true"
  else print "false"
}
' -- "$config"
    else
        echo "false"
    fi
}
function __update_sshopts_port() {
    # Déterminer le port de connexion et le placer dans la variable SSH_PORT.
    # Analyser pour cela les arguments qui sont fournis dans les paramètres 2..@
    SSH_PORT=22
    # Analyser les configuration de ssh
    eval "$(__parse_ssh_config_port ~/.ssh/config)" && return
    eval "$(__parse_ssh_config_port /etc/ssh/ssh_config)" && return
    # Analyser les arguments
    local i=1
    while [ $i -le $# ]; do
        if [ "${!i}" == -p ]; then
            i=$(($i + 1))
            SSH_PORT="${!i}"
        fi
        i=$(($i + 1))
    done
}
function __update_sshopts_cm() {
    # Rajouter le cas échéant les options pour le multiplexage des connexions au
    # tableau sshopts, pour l'hôte $1
    # SSH_USER doit être défini
    local found cmhost cmkey tmphost shared control persist
    local -a hosts
    for cmhost in "${CMHOSTS[@]}"; do
        [ -n "$cmhost" ] || continue
        cmkey="$cmhost"; cmkey="${cmkey//./_}"; cmkey="${cmkey//-/_}"
        array_copy hosts "${cmkey}_HOSTS"
        if [ -z "${hosts[0]}" ]; then
            if [[ "$cmhost" == *.* ]]; then
                hosts=("$cmhost" "${cmhost%%.*}")
            else
                hosts=("$cmhost${DEFAULT_DOMAIN:+.$DEFAULT_DOMAIN}" "$cmhost")
            fi
        fi
        if array_contains hosts "$host"; then
            found=1
            break
        fi
    done
    [ -n "$found" ] || return
    shared="${cmkey}_SHARED"; shared="${!shared}"
    [ -n "$shared" ] || shared=1 # connexion multiplexée par défaut
    is_yes "$shared" || return
    control="${cmkey}_CONTROL"; control="${!control}"
    [ -n "$control" ] || control="$HOME/.ssh/@user@@host:@port"
    control="${control//@user/$SSH_USER}"
    control="${control//@port/$SSH_PORT}"
    control="${control//@host/${hosts[0]}}"
    persist="${cmkey}_PERSIST"; persist="${!persist}"
    [ -n "$persist" ] || persist=auto

    sshopts=("${sshopts[@]}" -o ControlPath="$control" -o ControlMaster=auto)
    if [ "$persist" == auto ]; then
        local version major minor
        version="$(ssh -V 2>&1)"
        version="${version#OpenSSH_}"; major="${version%%.*}"
        minor="${version#$major.}"; minor="${minor:0:1}"
        if [ -z "$major" ]; then
            # ne devrait pas se produire... mais au cas où
            persist=
        elif [ "$major" -ge 6 ]; then
            persist="$DEFAULT_PERSIST"
        elif [ "$major" -eq 5 ]; then
            if [ "$minor" -ge 6 ]; then
                persist="$DEFAULT_PERSIST"
            else
                persist=
            fi
        elif [ "$major" -le 4 ]; then
            persist=
        fi
    fi
    [ -n "$persist" ] && sshopts=("${sshopts[@]}" -o ControlPersist="$persist")
}
function __update_sshopts() {
    local user host
    splituserhost "$1" user host; shift
    __update_sshopts_user "$@"
    __update_sshopts_port "$@"
    __update_sshopts_cm
}

function show_vars() {
    local -a sshopts
    [ "${#hosts[*]}" -gt 1 ] && exec=
    set_array_cmd hosts
    echo_setv ssh "$SSH"
    echo_setv exec "$exec"
    set_array_cmd args @ "$@"
    for host in "${hosts[@]}"; do
        array_copy sshopts SSHOPTS
        __update_sshopts "$host" "$@"
        echo_setv host "$host"
        set_array_cmd options sshopts
    done
}

function do_ssh() {
    local -a sshopts
    local onehost r
    if [ ${#hosts[*]} -eq 0 -a $# -eq 0 ]; then
        ${exec:+exec} "$SSH" "${SSHOPTS[@]}"
        return
    elif [ "${#hosts[*]}" -gt 1 ]; then
        onehost=
        exec=
    else
        onehost=1
    fi
    for host in "${hosts[@]}"; do
        [ -z "$onehost" ] && etitle "$host"

        array_copy sshopts SSHOPTS
        __update_sshopts "$host" "$@"
        ${exec:+exec} "$SSH" "${sshopts[@]}" "$host" "$@" || r=$?

        [ -z "$onehost" ] && eend
    done
    return "${r:-0}"
}

function __update_SSHOPTS() { SSHOPTS=("${SSHOPTS[@]}" "$@"); }

# charger la configuration
set_defaults nutools
set_defaults ussh
[ -n "$DEFAULT_USER" ] || DEFAULT_USER="$USSH_USER"
[ -n "$DEFAULT_DOMAIN" ] || DEFAULT_DOMAIN="$USSH_DOMAIN"
[ -n "$DEFAULT_PERSIST" ] || DEFAULT_PERSIST=5m

DOMAIN="$DEFAULT_DOMAIN"
SSH=
remove=
remove_only=
exec=1
parse=
uwatch=
parse_opts ${UTOOLS_USSH_RSYNC_SUPPORT:++} "${PRETTYOPTS[@]}" \
    --help '$exit_with display_help' \
    -1,-2,-4,-6,-A,-a,-C,-f,-g,-K,-k,-M,-N,-n,-q,-s,-T,-t,-V,-v,-X,-x,-Y,-y '$__update_SSHOPTS "$option_"' \
    -b:,-c:,-D:,-e:,-F:,-I:,-i:,-L:,-l:,-m:,-O:,-o:,-p:,-R:,-S:,-W:,-w: '$__update_SSHOPTS "$option_" "$value_"' \
    --quiet '$__update_SSHOPTS -q' \
    --tty '$__update_SSHOPTS -t' \
    --login: '$__update_SSHOPTS -l "$value_"' \
    --port: '$__update_SSHOPTS -p "$value_"' \
    -d:,--domain: DOMAIN= \
    -z:,--ssh: SSH= \
    -r,--remove '$remove=1; parse=' \
    -j remove_only=1 \
    --exec exec=1 \
    --no-exec exec= \
    --parse '$parse=1; remove=' \
    --cc uwatch=1 \
    @ args -- "$@" && set -- "${args[@]}" || die "$args"

if [ -n "$remove" ]; then
    [ -n "$*" ] || die "Vous devez spécifier les hôtes à supprimer de ~/.ssh/known_hosts"
    if [ -n "$remove_only" ]; then
        hosts=("$@")
        fix_hosts
        remove_keys
        exit 0
    fi

    hosts=("$1"); shift
    fix_hosts
    remove_keys
elif [ -n "$1" ]; then
    hosts=("$1"); shift
    fix_hosts
fi

[ -n "$SSH" ] || SSH=ssh
if [ -n "$parse" ]; then
    show_vars "$@"
elif [ -n "$uwatch" ]; then
    __update_SSHOPTS -t
    do_ssh "/usr/local/nutools/uwatch --cc"
else
    do_ssh "$@"
fi