#!/bin/bash

#
# Generate OpenVPN configs
#

# Convert 1.2.3.4/24 -> 255.255.255.0
cidr2mask()
{
    local i
    local subnetmask=""
    local cidr=${1#*/}
    local full_octets=$(($cidr/8))
    local partial_octet=$(($cidr%8))

    for ((i=0;i<4;i+=1)); do
        if [ $i -lt $full_octets ]; then
            subnetmask+=255
        elif [ $i -eq $full_octets ]; then
            subnetmask+=$((256 - 2**(8-$partial_octet)))
        else
            subnetmask+=0
        fi
        [ $i -lt 3 ] && subnetmask+=.
    done
    echo $subnetmask
}

# Used often enough to justify a function
getroute() {
    echo ${1%/*} $(cidr2mask $1)
}

usage() {
    echo "usage: $0 [-d]"
    echo "                  -u SERVER_PUBLIC_URL"
    echo "                 [-s SERVER_SUBNET]"
    echo "                 [-r ROUTE ...]"
    echo "                 [-p PUSH ...]"
    echo "                 [-n DNS_SERVER ...]"
    echo
    echo "optional arguments:"
    echo " -d    Disable NAT routing and default route"
    echo " -c    Enable client-to-client option"
    echo " -D    Do not push dns servers"
    echo " -N    Configure NAT to access external server network"
    echo " -m    Set client MTU"
    echo " -t    Use TAP device (instead of TUN device)"
    echo " -T    Encrypt packets with the given cipher algorithm instead of the default one (tls-cipher)."
    echo " -C    A list of allowable TLS ciphers delimited by a colon (cipher)."
    echo " -a    Authenticate  packets with HMAC using the given message digest algorithm (auth)."
    echo " -z    Enable comp-lzo compression."
    echo " -2    Enable two factor authentication using Google Authenticator."
    echo " -f    Set the fragment directive."
    echo " -e    Add extra server config"
}

if [ "$DEBUG" == "1" ]; then
  set -x
fi

set -e

OVPN_ENV=$OPENVPN/ovpn_env.sh
OVPN_SERVER=192.168.255.0/24
OVPN_DEFROUTE=1
OVPN_NAT=0
OVPN_DNS=1
OVPN_DEVICE="tun"
OVPN_DEVICEN=0
OVPN_ROUTES=()
TMP_ROUTES=()
OVPN_PUSH=()
TMP_PUSH=()
OVPN_DNS_SERVERS=("8.8.8.8" "8.8.4.4")
TMP_DNS_SERVERS=()
OVPN_TLS_CIPHER=''
OVPN_CIPHER=''
OVPN_AUTH=''
OVPN_EXTRA_CONFIG=''

# Import defaults if present
[ -r "$OVPN_ENV" ] && source "$OVPN_ENV"

# Parse arguments
while getopts ":a:e:C:T:r:s:du:cp:n:DNmf:tz2" opt; do
    case $opt in
        a)
            OVPN_AUTH="$OPTARG"
            ;;
        e)
            OVPN_EXTRA_CONFIG="$OPTARG"
            ;;
        C)
            OVPN_CIPHER="$OPTARG"
            ;;
        T)
            OVPN_TLS_CIPHER="$OPTARG"
            ;;
        r)
            TMP_ROUTES+=("$OPTARG")
            ;;
        s)
            OVPN_SERVER=$OPTARG
            ;;
        d)
            OVPN_DEFROUTE=0
            ;;
        u)
            OVPN_SERVER_URL=$OPTARG
            ;;
        c)
            OVPN_CLIENT_TO_CLIENT=1
            ;;
        p)
            TMP_PUSH+=("$OPTARG")
            ;;
        n)
            TMP_DNS_SERVERS+=("$OPTARG")
            ;;
        D)
            OVPN_DNS=0
            ;;
        N)
            OVPN_NAT=1
            ;;
        m)
            OVPN_MTU=$OPTARG
            ;;
        t)
            OVPN_DEVICE="tap"
            ;;
        z)
            OVPN_COMP_LZO=1
            ;;
        2)
            OVPN_OTP_AUTH=1
            ;;
        f)
            OVPN_FRAGMENT=$OPTARG
            ;;
        \?)
            set +x
            echo "Invalid option: -$OPTARG" >&2
            usage
            exit 1
            ;;
        :)
            set +x
            echo "Option -$OPTARG requires an argument." >&2
            usage
            exit 1
            ;;
    esac
done

# Create ccd directory for static routes
[ ! -d "$OPENVPN/ccd" ] && mkdir -p $OPENVPN/ccd

# if new routes were not defined with -r, use default
[ ${#TMP_ROUTES[@]} -gt 0 ] && OVPN_ROUTES=("${TMP_ROUTES[@]}")

# if new push directives were not defined with -p, use default
[ ${#TMP_PUSH[@]} -gt 0 ] && OVPN_PUSH=("${TMP_PUSH[@]}")

# if dns servers were not defined with -n, use google nameservers
[ ${#TMP_DNS_SERVERS[@]} -gt 0 ] && OVPN_DNS_SERVERS=("${TMP_DNS_SERVERS[@]}")

# Server name is in the form "udp://vpn.example.com:1194"
if [[ "$OVPN_SERVER_URL" =~ ^((udp|tcp)://)?([0-9a-zA-Z\.\-]+)(:([0-9]+))?$ ]]; then
    OVPN_PROTO=${BASH_REMATCH[2]};
    OVPN_CN=${BASH_REMATCH[3]};
    OVPN_PORT=${BASH_REMATCH[5]};
else
    set +x
    echo "Common name not specified, see '-u'"
    usage
    exit 1
fi

# Apply defaults
[ -z "$OVPN_PROTO" ] && OVPN_PROTO=udp
[ -z "$OVPN_PORT" ] && OVPN_PORT=1194
[ ${#OVPN_ROUTES[@]} -eq 0 ] && OVPN_ROUTES=("192.168.254.0/24")

export OVPN_SERVER OVPN_ROUTES OVPN_DEFROUTE
export OVPN_SERVER_URL OVPN_ENV OVPN_PROTO OVPN_CN OVPN_PORT
export OVPN_CLIENT_TO_CLIENT OVPN_PUSH OVPN_NAT OVPN_DNS OVPN_MTU OVPN_DEVICE
export OVPN_TLS_CIPHER OVPN_CIPHER OVPN_AUTH
export OVPN_COMP_LZO
export OVPN_OTP_AUTH
export OVPN_FRAGMENT

# Preserve config
if [ -f "$OVPN_ENV" ]; then
    bak_env=$OVPN_ENV.$(date +%s).bak
    echo "Backing up $OVPN_ENV -> $bak_env"
    mv "$OVPN_ENV" "$bak_env"
fi
export | grep OVPN_ > "$OVPN_ENV"

conf=$OPENVPN/openvpn.conf
if [ -f "$conf" ]; then
    bak=$conf.$(date +%s).bak
    echo "Backing up $conf -> $bak"
    mv "$conf" "$bak"
fi

cat > "$conf" <<EOF
server $(getroute $OVPN_SERVER)
verb 3
key $EASYRSA_PKI/private/${OVPN_CN}.key
ca $EASYRSA_PKI/ca.crt
cert $EASYRSA_PKI/issued/${OVPN_CN}.crt
dh $EASYRSA_PKI/dh.pem
tls-auth $EASYRSA_PKI/ta.key
key-direction 0
keepalive 10 60
persist-key
persist-tun

proto $OVPN_PROTO
# Rely on Docker to do port mapping, internally always 1194
port 1194
dev $OVPN_DEVICE$OVPN_DEVICEN
status /tmp/openvpn-status.log

user nobody
group nogroup
EOF

[ -n "$OVPN_TLS_CIPHER" ] && echo "tls-cipher $OVPN_TLS_CIPHER" >> "$conf"
[ -n "$OVPN_CIPHER" ] && echo "cipher $OVPN_CIPHER" >> "$conf"
[ -n "$OVPN_AUTH" ] && echo "auth $OVPN_AUTH" >> "$conf"

[ -n "$OVPN_CLIENT_TO_CLIENT" ] && echo "client-to-client" >> "$conf"
[ -n "$OVPN_COMP_LZO" ] && echo "comp-lzo" >> "$conf"

[ -n "$OVPN_FRAGMENT" ] && echo "fragment $OVPN_FRAGMENT" >> "$conf"

[ -n "$OVPN_EXTRA_CONFIG" ] && echo "$OVPN_EXTRA_CONFIG" >> "$conf"

[ "$OVPN_DNS" == "1" ] && for i in "${OVPN_DNS_SERVERS[@]}"; do
  echo "push dhcp-option DNS $i" >> "$conf"
done
# Append Routes
for i in "${OVPN_ROUTES[@]}"; do
    # If user passed "0" skip this, assume no extra routes
    [ "$i" = "0" ] && break;
    echo route $(getroute "$i") >> "$conf"
done

# Append push commands
for i in "${OVPN_PUSH[@]}"; do
    echo push \"$i\" >> "$conf"
done

# Optional OTP authentication support
if [ -n "$OVPN_OTP_AUTH" ]; then
    echo -e "\n\n# Enable OTP+PAM for user authentication" >> "$conf"
    echo "plugin /usr/lib/openvpn/plugins/openvpn-plugin-auth-pam.so openvpn" >> "$conf"
fi

set +e

# Clean-up duplicate configs
if diff -q "$bak_env" "$OVPN_ENV" 2>/dev/null; then
    echo "Removing duplicate back-up: $bak_env"
    rm -fv "$bak_env"
fi
if diff -q "$bak" "$conf" 2>/dev/null; then
    echo "Removing duplicate back-up: $bak"
    rm -fv "$bak"
fi

echo "Successfully generated config"