#!/bin/bash
#
# /usr/bin/cryptfs: mount/umount encrypted partitions
#

set -e

if [ $# -eq 1 ] && [ "x$1" = 'x-u' ]; then
  unmount=true
elif [ $# -eq 0 ]; then
  unmount=false
else
  >&2 printf 'Usage: %s [-u]\n' "$0"
  exit 1
fi

extract_ssh_host() {
  {
    printf '%s\n' "$1"
    if [ -f ~/.ssh/config ]; then
      sed '
        /^Host\s\+'"$1"'$/,/^Host\s/ {
          s/^\s*Hostname\s\+//
          t
        }
        d
      ' ~/.ssh/config
    fi
  } \
    | tail -n1
}

give_all_parent_dirs() {
  local odir
  local dir
  odir="$1"
  odir="/${odir#/}"
  dir="${odir%/*}"
  while [ "${dir}" != '' ]; do
    printf '%s %s\n' "${odir}" "${dir}"
    dir="${dir%/*}"
  done
}

inner_fs_to_mountorder() {
  {
    printf '%s\n' "${fstab}" | \
      grep "^$1 " | \
      awk '{print "x " $3}'
    printf '%s\n' "${fstab}" | \
      sed -n '
        s|^\(\S\+\) \S\+ \(\S\+\)\( .*\)\?$|\1 \2|
        T
        p
      ' | \
      while read -r order dir; do
        give_all_parent_dirs "${dir}" | \
          awk '{print "'"${order}"' " $1 " " $2}'
      done | \
        sort -k3,3 -k2r,2 | \
        uniq -f2 | \
        cut -d' ' -f1,3
  } | \
    sort -k2r,2 -k1,1 | \
    uniq -f1 -d | \
    cut -d' ' -f1
}

mountpoint_to_file() {
  {
    printf '%s\n' "${fstab}" | \
      awk '{print $1 " " $3}'
    give_all_parent_dirs "$1" | \
      awk '{print "x " $2}'
  } | \
    sort -k2r,2 -k1,1 | \
    uniq -f1 -d | \
    cut -d' ' -f1 | \
    head -n1
}

mountorder_to_device() {
  {
    printf 'x %s\n' "$1"
    printf '%s\n' "${fstab}" | \
      awk '{print $1 " " $2}' | \
      sort -k2,2 -u
  } | \
    sort -k2,2 -k1,1 | \
    uniq -f1 -d | \
    cut -d' ' -f1
}

raid_device_to_disk() {
  printf '%s\n' "${mdadm_conf}" | \
    grep -F " $1 " | \
    cut -d' ' -f1
}

crypttab=$(
  sed '
    s|^\s*# cryptfs\s*||
    /^\s*#/d
    /^\s*$/d
    s/\s\+/ /g
    s|UUID=|/dev/disk/by-uuid/|g
    s/^ //
  ' '/etc/crypttab' | \
    sort -u
)
fstab=$(
  sed '
    s|^\s*# cryptfs\s*||
    /^\s*#/d
    /^\s*\S\+\s\+[^\/[:space:]]/d
    /^\s*$/d
    s/\s\+/ /g
  ' '/etc/fstab' | \
    sort -u | \
    cat -n | \
    sed '
      s|UUID=|/dev/disk/by-uuid/|
      s/\s\+/ /g
      s/^ //
      s/ $//
    '
)
mdadm_conf=$(
  sed '
    s|^\s*# cryptfs\s*||
    /^\s*#/d
  ' /etc/mdadm.conf 2>/dev/null | \
    sed -n '
      /^ARRAY / {
        N
        s,^ARRAY \(\S\+\) [^\n]\+\n\s*devices=\(\S\+\)$,\1 \2 ,
        y/,/ /
        s|UUID=|/dev/disk/by-uuid/|
        p
      }
    ' | \
    sort -u
)

mount_details() {
  printf '%s\n' "${fstab}" | \
    grep "^$2 " | \
    while read -r _ source dest fs opts _; do
      case "$1" in
        'mp')
          printf '%s\n' "${dest}"
        ;;
        'dev')
          printf '%s\n' "${source}"
        ;;
        'fs')
          printf '%s\n' "${fs}"
        ;;
        'all')
          printf -- '-t %s ' "${fs}"
          if [ -n "${opts}" ] && \
            [ "${opts}" != 'defaults' ]; then
            printf -- '-o %s ' "${opts}"
          fi
          printf '%s %s\n' "${source}" "${dest}"
        ;;
        *)
          >&2 printf 'unknown mount_details "%s"\n' "$1"
          exit 42
        ;;
      esac
    done
}

is_mounted() {
  mountpoint -q "$(mount_details mp "$1")"
}

is_unlocked() {
  cryptsetup status "$1" >/dev/null 2>&1
}

do_mount() {
  local inner_fs
  local max_tries
  local mount_paramters
  if is_mounted "$1"; then
    return
  fi
  inner_fs=$(inner_fs_to_mountorder "$1")

  mount_dev=$(mount_details dev "$1")

  if md_dev=$(readlink -e "${mount_dev}" \
  | grep -x '/dev/md[0-9]\+'); then
    if ! grep -qxF 'clean' '/sys/block/'"${md_dev##*/}"'/md/array_state'; then
      >&2 printf '%s is not clean - will not mount\n' "${md_dev}"
      exit 1
    fi
  fi

  case "$(mount_details fs "$1")" in
    'ext'*)
      if cryptsetup status "${mount_dev}" \
      | grep -q '^\s*mode:\s\+readonly$'; then
        e2fsck_option='-n'
      else
        e2fsck_option='-p'
      fi
      e2fsck ${e2fsck_option} "${mount_dev}"
    ;;
    'fuse.sshfs'|'fuse.ftps')
      local max_wait
      local host
      max_wait=$(($(date +%s)+120))
      host=$(
        echo "${mount_dev}" | \
          sed 's>^\([^:@]*@\)\?\([^:@]\+\):\S*$>\2>'
      )
      host=$(extract_ssh_host "${host}")
      while [ $(date +%s) -lt ${max_wait} ] && \
        ! timeout 1 ping -6 -c1 "${host}" >/dev/null 2>&1 && \
        ! timeout 1 ping -4 -c1 "${host}" >/dev/null 2>&1; do
        >&2 printf ','
        sleep 1
      done
    ;;
  esac

  if [ -n "${inner_fs}" ]; then
    do_unmount -l "${inner_fs}"
  fi
  max_tries=10
  mount_parameters=$(mount_details all "$1")
  mount_command="mount ${mount_parameters}"
  mount_command="${mount_command#mount -t fuse.}"
  while ! ${mount_command}; do
    >&2 printf '"%s" failed.\n' \
      "${mount_command}"
    max_tries=$((max_tries-1))
    if [ ${max_tries} -le 0 ]; then
      exit 1
    fi
    sleep 1
  done
  if [ -n "${inner_fs}" ]; then
    do_mount "${inner_fs}"
  fi
}

do_unmount() {
  local inner_fs
  local lazy
  local mount_point
  local timeout
  if [ "x$1" = 'x-l' ]; then
    lazy='-l'
    shift
  else
    lazy=''
  fi
  if ! is_mounted "$1"; then
    return
  fi
  inner_fs=$(inner_fs_to_mountorder "$1")
  if [ -n "${inner_fs}" ]; then
    do_unmount -l "${inner_fs}"
  fi
  mount_point=$(mount_details mp "$1")
  timeout=120
  while [ ${timeout} -ge 0 ] && ! umount ${lazy} "${mount_point}"; do
    >&2 printf '"umount %s" failed.\n' \
      "${lazy} ${mount_point}"
    fuser -k -M -m "${mount_point}"
    sleep 1
    timeout=$((timeout-1))
  done
  if [ ${timeout} -lt 0 ]; then
    return 1
  fi
  if [ -n "${inner_fs}" ]; then
    do_mount "${inner_fs}"
  fi
}

do_crypt_close() {
  if is_unlocked "$1"; then
    while ! cryptsetup luksClose "$1"; do
      >&2 printf '"cryptsetup luksClose %s" failed.\n' \
        "$1"
      sleep 1
    done
  fi
}

do_crypt_open() {
  local key
  local max_tries
  local name
  local raw
  name="$1"
  raw="$2"
  key="$3"
  shift 3
  if ! is_unlocked "${name}"; then
    max_tries=10
    while ! cryptsetup luksOpen "${raw}" "${name}" --key-file="${key}" "$@"; do
      >&2 printf '"cryptsetup luksOpen %s %s --key-file=%s %s" failed.\n' \
        "${raw}" "${name}" "${key}" "$*"
      sleep 1
      max_tries=$((max_tries-1))
      if [ ${max_tries} -le 0 ]; then
        exit 1
      fi
    done
  fi
}

do_integrity_close() {
  if is_unlocked "$1"; then
    while ! integritysetup close "$1" && ! dmsetup remove "$1"; do
      >&2 printf '"integritysetup close %s || dmsetup remove %s" failed.\n' \
        "$1" "$1"
      sleep 1
    done
  fi
}

do_integrity_open() {
  local name
  local raw
  name="$1"
  raw="$2"
  if ! is_unlocked "${name}"; then
    max_tries=10
    while ! integritysetup open "${raw}" "${name}"; do
      >&2 printf '"integritysetup open %s %s" failed.\n' \
        "${raw}" "${name}"
      sleep 1
      max_tries=$((max_tries-1))
      if [ ${max_tries} -le 0 ]; then
        exit 1
      fi
    done
  fi
}

do_mdadm_stop() {
  device="$1"
  if mdadm --detail "${device}" | \
    grep -q '^\s*State : \(active\|clean\)\s*$'; then
    mdadm --stop "${device}"
  fi
}

printf '%s\n' "${crypttab}" | \
  grep -vxF '' | \
  while read -r name raw key options; do
    if [ "x${options}" = 'xro' ]; then
      options='--readonly'
    else
      unset options
    fi
    if [ "${key#\[*]}" != "${key}" ]; then
      key_slot_option="${key%%]*}"
      key_slot_option='--key-slot='"${key_slot_option#\[}"''
      key="${key#\[*]}"
    else
      unset key_slot_option
    fi
    key_mount=$(mountpoint_to_file "${key}")
    raid_device=$(raid_device_to_disk "/dev/mapper/${name}")
    if [ -n "${raid_device}" ]; then
      mount_order=$(mountorder_to_device "${raid_device}")
    else
      mount_order=$(mountorder_to_device "/dev/mapper/${name}")
    fi
    if ${unmount}; then
      if [ -n "${mount_order}" ]; then
        printf '0 do_unmount %s\n' "${mount_order}"
      fi
      if [ -n "${raid_device}" ]; then
        printf '1 do_mdadm_stop %s\n' "${raid_device}"
      fi
      if [ -n "${key}" ]; then
        printf '3 do_crypt_close %s\n' "${name}"
      else
        printf '2 do_integrity_close %s\n' "${name}"
      fi
    else
      if [ -n "${key}" ]; then
        printf '0 do_mount %s\n' "${key_mount}"
        printf '1 do_crypt_open %s\n' "${name} ${raw} ${key} ${key_slot_option} ${options}"
        printf '2 do_unmount -l %s\n' "${key_mount}"
      else
        printf '3 do_integrity_open %s\n' "${name} ${raw}"
      fi
      if [ -n "${mount_order}" ]; then
        printf '4 do_mount %s\n' "${mount_order}"
      fi
    fi
  done | \
  sort -u | \
  sort -k1n,1 | \
  while read -r _ aktion params; do
    ${aktion} ${params}
  done

# End of file
