#!/bin/sh

# Client script for LXC container images.
#
# Copyright @ Daniel Lezcano <daniel.lezcano@free.fr>
# Copyright © 2018 Christian Brauner <christian.brauner@ubuntu.com>
#
#  This library is free software; you can redistribute it and/or
#  modify it under the terms of the GNU Lesser General Public
#  License as published by the Free Software Foundation; either
#  version 2.1 of the License, or (at your option) any later version.

#  This library is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
#  Lesser General Public License for more details.

#  You should have received a copy of the GNU Lesser General Public
#  License along with this library; if not, write to the Free Software
#  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301
#  USA

LXC_MAPPED_UID=
LXC_MAPPED_GID=

BUSYBOX_EXE=`which busybox`

# Make sure the usual locations are in PATH
export PATH=$PATH:/usr/sbin:/usr/bin:/sbin:/bin

in_userns() {
  [ -e /proc/self/uid_map ] || { echo no; return; }
  while read -r line; do
    fields="$(echo "$line" | awk '{ print $1 " " $2 " " $3 }')"
    if [ "${fields}" = "0 0 4294967295" ]; then
      echo no;
      return;
    fi
    if echo "${fields}" | grep -q " 0 1$"; then
      echo userns-root;
      return;
    fi
  done < /proc/self/uid_map

  if [ -e /proc/1/uid_map ]; then
    if [ "$(cat /proc/self/uid_map)" = "$(cat /proc/1/uid_map)" ]; then
      echo userns-root
      return
    fi
  fi
  echo yes
}

USERNS="$(in_userns)"

install_busybox()
{
  rootfs="${1}"
  name="${2}"
  res=0
  fstree="\
${rootfs}/selinux \
${rootfs}/dev \
${rootfs}/home \
${rootfs}/root \
${rootfs}/etc \
${rootfs}/etc/init.d \
${rootfs}/bin \
${rootfs}/usr/bin \
${rootfs}/sbin \
${rootfs}/usr/sbin \
${rootfs}/proc \
${rootfs}/sys \
${rootfs}/mnt \
${rootfs}/tmp \
${rootfs}/var/log \
${rootfs}/var/run \
${rootfs}/dev/pts \
${rootfs}/lib \
${rootfs}/usr/lib \
${rootfs}/lib64 \
${rootfs}/usr/lib64"

  # shellcheck disable=SC2086
  mkdir -p ${fstree} || return 1
  # shellcheck disable=SC2086
  chmod 755 ${fstree} || return 1

  # minimal devices needed for busybox
  if [ "${USERNS}" = "yes" ]; then
    for dev in tty console tty0 tty1 ram0 null urandom; do
      echo "lxc.mount.entry = /dev/${dev} dev/${dev} none bind,optional,create=file 0 0" >> "${path}/config"
    done
  else
    mknod -m 666 "${rootfs}/dev/tty" c 5 0       || res=1
    mknod -m 666 "${rootfs}/dev/console" c 5 1   || res=1
    mknod -m 666 "${rootfs}/dev/tty0" c 4 0      || res=1
    mknod -m 666 "${rootfs}/dev/tty1" c 4 0      || res=1
    mknod -m 666 "${rootfs}/dev/tty5" c 4 0      || res=1
    mknod -m 600 "${rootfs}/dev/ram0" b 1 0      || res=1
    mknod -m 666 "${rootfs}/dev/null" c 1 3      || res=1
    mknod -m 666 "${rootfs}/dev/zero" c 1 5      || res=1
    mknod -m 666 "${rootfs}/dev/urandom" c 1 9   || res=1
  fi
  
  # make /tmp accessible to any user (with sticky bit)
  chmod 1777 "${rootfs}/tmp" || return 1

  # root user defined
  cat <<EOF >> "${rootfs}/etc/passwd"
root:x:0:0:root:/root:/bin/sh
EOF

  cat <<EOF >> "${rootfs}/etc/group"
root:x:0:root
EOF

    # mount everything
  cat <<EOF >> "${rootfs}/etc/init.d/rcS"
#!/bin/sh
/bin/syslogd
/bin/mount -a
/bin/udhcpc
EOF

  # executable
  chmod 744 "${rootfs}/etc/init.d/rcS" || return 1

  # launch rcS first then make a console available
  # and propose a shell on the tty, the last one is
  # not needed
  cat <<EOF >> "${rootfs}/etc/inittab"
::sysinit:/etc/init.d/rcS
tty1::respawn:/bin/getty -L tty1 115200 vt100
console::askfirst:/bin/sh
EOF
  # writable and readable for other
  chmod 644 "${rootfs}/etc/inittab" || return 1

  # Look for the pathname of "default.script" from the help of udhcpc
  DEF_SCRIPT=`${BUSYBOX_EXE} udhcpc --help 2>&1 | egrep -- '-s.*Run PROG' | cut -d'/' -f2- | cut -d')' -f1`
  DEF_SCRIPT_DIR=`dirname /${DEF_SCRIPT}`
  mkdir -p ${rootfs}/${DEF_SCRIPT_DIR}
  chmod 644 ${rootfs}/${DEF_SCRIPT_DIR} || return 1

  cat <<EOF >> ${rootfs}/${DEF_SCRIPT}
#!/bin/sh
case "\$1" in
  deconfig)
    ip addr flush dev \$interface
    ;;

  renew|bound)
    # flush all the routes
    if [ -n "\$router" ]; then
      ip route del default 2> /dev/null
    fi

    # check broadcast
    if [ -n "\$broadcast" ]; then
      broadcast="broadcast \$broadcast"
    fi

    # add a new ip address
    ip addr add \$ip/\$mask \$broadcast dev \$interface

    if [ -n "\$router" ]; then
      ip route add default via \$router dev \$interface
    fi

    [ -n "\$domain" ] && echo search \$domain > /etc/resolv.conf
    for i in \$dns ; do
      grep "nameserver \$i" /etc/resolv.conf > /dev/null 2>&1
      if [ \$? -ne 0 ]; then
        echo nameserver \$i >> /etc/resolv.conf
      fi
    done
    ;;
esac
exit 0
EOF

 chmod 744 ${rootfs}/${DEF_SCRIPT}

 return "${res}"
}

configure_busybox()
{
  rootfs="${1}"

  # copy busybox in the rootfs
  if ! cp "${BUSYBOX_EXE}" "${rootfs}/bin"; then
    echo "ERROR: Failed to copy busybox binary" 1>&2
    return 1
  fi

  # symlink busybox for the commands it supports
  # it would be nice to just use "chroot $rootfs busybox --install -s /bin"
  # but that only works right in a chroot with busybox >= 1.19.0
  (
    cd "${rootfs}/bin" || return 1
    ./busybox --list | grep -v busybox | xargs -n1 ln -s busybox
  )

  # relink /sbin/init
  ln "${rootfs}/bin/busybox" "${rootfs}/sbin/init"

  # /etc/fstab must exist for "mount -a"
  touch "${rootfs}/etc/fstab"

  # passwd exec must be setuid
  chmod +s "${rootfs}/bin/passwd"
  touch "${rootfs}/etc/shadow"

  return 0
}

copy_configuration()
{
  path="${1}"
  rootfs="${2}"
  name="${3}"

grep -q "^lxc.rootfs.path" "${path}/config" 2>/dev/null || echo "lxc.rootfs.path = ${rootfs}" >> "${path}/config"
cat <<EOF >> "${path}/config"
lxc.signal.halt = SIGUSR1
lxc.signal.reboot = SIGTERM
lxc.uts.name = "${name}"
lxc.tty.max = 1
lxc.pty.max = 1
lxc.cap.drop = sys_module mac_admin mac_override sys_time

# When using LXC with apparmor, uncomment the next line to run unconfined:
#lxc.apparmor.profile = unconfined

lxc.mount.auto = cgroup:mixed proc:mixed sys:mixed
lxc.mount.entry = shm dev/shm tmpfs defaults,create=dir 0 0
lxc.mount.entry = mqueue dev/mqueue mqueue defaults,optional,create=dir 0 0
EOF

  libdirs="\
    lib \
    usr/lib \
    lib64 \
    usr/lib64"

  for dir in ${libdirs}; do
    if [ -d "/${dir}" ] && [ -d "${rootfs}/${dir}" ]; then
      echo "lxc.mount.entry = /${dir} ${dir} none ro,bind 0 0" >> "${path}/config"
    fi
  done
  echo "lxc.mount.entry = /sys/kernel/security sys/kernel/security none ro,bind,optional 0 0" >> "${path}/config"
}

remap_userns()
{
 path="${1}"

 if [ -n "$LXC_MAPPED_UID" ] && [ "$LXC_MAPPED_UID" != "-1" ]; then
   chown "${LXC_MAPPED_UID}" "${path}/config" > /dev/null 2>&1
   chown -R root "${path}/rootfs" > /dev/null 2>&1
 fi

 if [ -n "$LXC_MAPPED_GID" ] && [ "$LXC_MAPPED_GID" != "-1" ]; then
   chgrp "${LXC_MAPPED_GID}" "${path}/config" > /dev/null 2>&1
   chgrp -R root "${path}/rootfs" > /dev/null 2>&1
 fi
}

usage() {
  cat <<EOF
LXC busybox image builder

Special arguments:

  [ -h | --help ]: Print this help message and exit.

LXC internal arguments:

  [ --name <name> ]: The container name
  [ --path <path> ]: The path to the container
  [ --rootfs <rootfs> ]: The path to the container's rootfs (default: config or <path>/rootfs)
  [ --mapped-uid <map> ]: A uid map (user namespaces)
  [ --mapped-gid <map> ]: A gid map (user namespaces)

BUSYBOX template specific arguments:

  [ --busybox-path <path> ]: busybox pathname (default: ${BUSYBOX_EXE})

EOF
  return 0
}

if ! options=$(getopt -o hp:n: -l help,rootfs:,path:,name:,mapped-uid:,mapped-gid:,busybox-path: -- "$@"); then
  usage
  exit 1
fi
eval set -- "$options"

while true
do
  case "$1" in
    -h|--help)    usage && exit 0;;
    -n|--name)    name=$2; shift 2;;
    -p|--path)    path=$2; shift 2;;
    --rootfs)     rootfs=$2; shift 2;;
    --mapped-uid) LXC_MAPPED_UID=$2; shift 2;;
    --mapped-gid) LXC_MAPPED_GID=$2; shift 2;;
    --busybox-path) BUSYBOX_EXE=$2; shift 2;;
    --)           shift 1; break ;;
    *)            break ;;
  esac
done

# Check that we have all variables we need
if [ -z "${name}" ] || [ -z "${path}" ]; then
    echo "ERROR: Please pass the name and path for the container" 1>&2
    exit 1
fi

# Make sure busybox is present
if [ -z "${BUSYBOX_EXE}" ]; then
    echo "ERROR: Please pass a pathname for busybox binary" 1>&2
    exit 1
fi
if [ ! -x "${BUSYBOX_EXE}" ]; then
    echo "ERROR: Failed to find busybox binary (${BUSYBOX_EXE})" 1>&2
    exit 1
fi

# detect rootfs
config="$path/config"
if [ -z "$rootfs" ]; then
  if grep -q '^lxc.rootfs.path' "${config}" 2> /dev/null ; then
    rootfs=$(awk -F= '/^lxc.rootfs.path =/{ print $2 }' "${config}")
  else
    rootfs="${path}/rootfs"
  fi
fi

if ! install_busybox "${rootfs}" "${name}"; then
  echo "ERROR: Failed to install rootfs" 1>&2
  exit 1
fi

if ! configure_busybox "${rootfs}"; then
  echo "ERROR: Failed to configure busybox" 1>&2
  exit 1
fi

if ! copy_configuration "${path}" "${rootfs}" "${name}"; then
  echo "ERROR: Failed to write config file" 1>&2
  exit 1
fi

if ! remap_userns "${path}"; then
  echo "ERROR: Failed to change idmappings" 1>&2
  exit 1
fi
