#!/bin/bash

#
# lxc: linux Container library

# Authors:
# Daniel Lezcano <daniel.lezcano@free.fr>

# 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=
SSH=

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

am_in_userns() {
    [ -e /proc/self/uid_map ] || { echo no; return; }
    [ "$(wc -l /proc/self/uid_map | awk '{ print $1 }')" -eq 1 ] || { echo yes; return; }
    line=$(awk '{ print $1 " " $2 " " $3 }' /proc/self/uid_map)
    [ "$line" = "0 0 4294967295" ] && { echo no; return; }
    echo yes
}

in_userns=0
[ $(am_in_userns) = "yes" ] && in_userns=1

copy_binary()
{
    binary_path=`which $1`
    if [ $? -ne 0 ]; then
        echo "Unable to find $1 binary on the system"
        return 1
    fi

    dir_path="${binary_path%/*}"
    echo /{,usr/}{,s}bin | grep $dir_path >/dev/null 2>&1
    if [ $? -ne 0 ]; then
        echo "Binary $1 is located at $binary_path and will not be copied"
        echo "($dir_path not supported)"
        return 1
    fi

    cp $binary_path $rootfs/$binary_path
    if [ $? -ne 0 ]; then
        echo "Failed to copy $binary_path to rootfs"
        return 1
    fi

    return 0
}

install_busybox()
{
    rootfs=$1
    name=$2
    res=0
    tree="\
$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/usr/share/udhcpc \
$rootfs/dev/pts \
$rootfs/dev/shm \
$rootfs/lib \
$rootfs/usr/lib \
$rootfs/lib64 \
$rootfs/usr/lib64"

    mkdir -p $tree || return 1
    chmod 755 $tree || return 1

    pushd $rootfs/dev > /dev/null || return 1

    # minimal devices needed for busybox
    if [ $in_userns -eq 1 ]; 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 tty c 5 0       || res=1
        mknod -m 666 console c 5 1   || res=1
        mknod -m 666 tty0 c 4 0      || res=1
        mknod -m 666 tty1 c 4 0      || res=1
        mknod -m 666 tty5 c 4 0      || res=1
        mknod -m 600 ram0 b 1 0      || res=1
        mknod -m 666 null c 1 3      || res=1
        mknod -m 666 zero c 1 5      || res=1
        mknod -m 666 urandom c 1 9   || res=1
    fi

    popd > /dev/null

    # 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

    cat <<EOF >> $rootfs/usr/share/udhcpc/default.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
            echo nameserver \$i >> /etc/resolv.conf
        done
        ;;
esac
exit 0
EOF

    chmod 744 $rootfs/usr/share/udhcpc/default.script

    return $res
}

install_dropbear()
{
    # copy dropbear binary
    copy_binary dropbear || return 1

    # make symlinks to various ssh utilities
    utils="\
        $rootfs/usr/bin/dbclient \
        $rootfs/usr/bin/scp \
        $rootfs/usr/bin/ssh \
        $rootfs/usr/sbin/dropbearkey \
        $rootfs/usr/sbin/dropbearconvert \
    "
    echo $utils | xargs -n1 ln -s /usr/sbin/dropbear

    # add necessary config files
    mkdir $rootfs/etc/dropbear
    dropbearkey -t rsa -f $rootfs/etc/dropbear/dropbear_rsa_host_key > /dev/null 2>&1
    dropbearkey -t dss -f $rootfs/etc/dropbear/dropbear_dss_host_key > /dev/null 2>&1

    echo "'dropbear' ssh utility installed"

    return 0
}

install_openssh()
{
    # tools to be installed
    server_utils="sshd"
    client_utils="\
        ssh \
        scp \
        "
    client_optional_utils="\
        sftp \
        ssh-add \
        ssh-agent \
        ssh-keygen \
        ssh-keyscan \
        ssh-argv0 \
        ssh-copy-id \
        "

    # new folders used by ssh
    ssh_tree="\
$rootfs/etc/ssh \
$rootfs/var/empty/sshd \
$rootfs/var/lib/empty/sshd \
$rootfs/var/run/sshd \
"

    # create folder structure
    mkdir -p $ssh_tree
    if [ $? -ne 0 ]; then
        return 1
    fi

    # copy binaries
    for bin in $server_utils $client_utils; do
        copy_binary $bin || return 1
    done

    for bin in $client_optional_utils; do
        tool_path=`which $bin` && copy_binary $bin
    done

    # add user and group
    cat <<EOF >> $rootfs/etc/passwd
sshd:x:74:74:Privilege-separated SSH:/var/empty/sshd:/sbin/nologin
EOF

    cat <<EOF >> $rootfs/etc/group
sshd:x:74:
EOF

    # generate container keys
    ssh-keygen -t rsa -N "" -f $rootfs/etc/ssh/ssh_host_rsa_key >/dev/null 2>&1
    ssh-keygen -t dsa -N "" -f $rootfs/etc/ssh/ssh_host_dsa_key >/dev/null 2>&1

    # by default setup root password with no password
    cat <<EOF > $rootfs/etc/ssh/sshd_config
Port 22
Protocol 2
HostKey /etc/ssh/ssh_host_rsa_key
HostKey /etc/ssh/ssh_host_dsa_key
UsePrivilegeSeparation yes
KeyRegenerationInterval 3600
ServerKeyBits 768
SyslogFacility AUTH
LogLevel INFO
LoginGraceTime 120
PermitRootLogin yes
StrictModes yes
RSAAuthentication yes
PubkeyAuthentication yes
IgnoreRhosts yes
RhostsRSAAuthentication no
HostbasedAuthentication no
PermitEmptyPasswords yes
ChallengeResponseAuthentication no
EOF

    echo "'OpenSSH' utility installed"

    return 0
}

configure_busybox()
{
    rootfs=$1

    which busybox >/dev/null 2>&1

    if [ $? -ne 0 ]; then
        echo "busybox executable is not accessible"
        return 1
    fi

    # copy busybox in the rootfs
    cp $(which busybox) $rootfs/bin
    if [ $? -ne 0 ]; then
        echo "failed to copy busybox in the rootfs"
        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
    pushd $rootfs/bin > /dev/null || return 1
    ./busybox --help | grep 'Currently defined functions:' -A300 | \
      grep -v 'Currently defined functions:' | tr , '\n' | \
      xargs -n1 ln -s busybox
    popd > /dev/null

    # 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

    # setting passwd for root
    CHPASSWD_FILE=$rootfs/root/chpasswd.sh

    cat <<EOF >$CHPASSWD_FILE
echo "setting root password to \"root\""

mount -n --bind /lib $rootfs/lib
if [ \$? -ne 0 ]; then
    echo "Failed bind-mounting /lib at $rootfs/lib"
    exit 1
fi

chroot $rootfs chpasswd <<EOFF 2>/dev/null
root:root
EOFF


if [ \$? -ne 0 ]; then
    echo "Failed to change root password"
    exit 1
fi

umount $rootfs/lib

EOF

    lxc-unshare -s MOUNT -- /bin/sh < $CHPASSWD_FILE
    rm $CHPASSWD_FILE

    return 0
}

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

grep -q "^lxc.rootfs" $path/config 2>/dev/null || echo "lxc.rootfs = $rootfs" >> $path/config
cat <<EOF >> $path/config
lxc.haltsignal = SIGUSR1
lxc.rebootsignal = SIGTERM
lxc.utsname = $name
lxc.tty = 1
lxc.pts = 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.aa_profile = unconfined

lxc.mount.auto = cgroup:mixed proc:mixed sys:mixed
lxc.mount.entry = shm /dev/shm tmpfs defaults 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
$1 -h|--help -p|--path=<path> -s|--ssh={dropbear,openssh}
EOF
    return 0
}

options=$(getopt -o hp:n:s: -l help,rootfs:,path:,name:,mapped-uid:,mapped-gid:,ssh: -- "$@")
if [ $? -ne 0 ]; then
    usage $(basename $0)
    exit 1
fi
eval set -- "$options"

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

if [ "$(id -u)" != "0" ]; then
    echo "This script should be run as 'root'"
    exit 1
fi

if [ -z "$path" ]; then
    echo "'path' parameter is required"
    exit 1
fi

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

install_busybox $rootfs $name
if [ $? -ne 0 ]; then
    echo "failed to install busybox's rootfs"
    exit 1
fi

configure_busybox $rootfs
if [ $? -ne 0 ]; then
    echo "failed to configure busybox template"
    exit 1
fi

copy_configuration $path $rootfs $name
if [ $? -ne 0 ]; then
    echo "failed to write configuration file"
    exit 1
fi

remap_userns $path
if [ $? -ne 0 ]; then
    echo "failed to remap files to user"
    exit 1
fi

if [ -n "$SSH" ]; then
    case "$SSH" in
        "dropbear")
            install_dropbear
            if [ $? -ne 0 ]; then
                echo "Unable to install 'dropbear' ssh utility"
                exit 1
            fi ;;
        "openssh")
            install_openssh
            if [ $? -ne 0 ]; then
                echo "Unable to install 'OpenSSH' utility"
                exit 1
            fi ;;
        *)
            echo "$SSH: unrecognized ssh utility"
            exit 1
    esac
else
    which dropbear >/dev/null 2>&1
    if [ $? -eq 0 ]; then
        install_dropbear
    fi
fi
