#!/bin/bash

net_conf="$ROOT"/etc/network/interfaces
static_resolv_conf="$ROOT"/etc/network/resolv.conf.static

start_tag="# START-spxsysconfig"
start_comm="DO NOT REMOVE THE START AND END TAGS"
end_tag="# END-spxsysconfig"

# conditionally use fsync if present, but only on native system
[ -z "$ROOT" ] && \
    type fsync 1> /dev/null 2> /dev/null && fsync=fsync || fsync=:

# Creates a temporary file in the same directory as the source
# file (arg 1) and copies the contents. The name of the temp
# file is output to stdout if successful
function cptemp()
{
    local tmp="$(mktemp "$1".XXXXXX)" || return 1
    # preserve mode, ownership and any security contexts
    if cp -p -- "$1" "$tmp"; then
        echo "$tmp"
        return 0
    else
        rm -f -- "$tmp"
        return 1
    fi
}

# Copies a file (arg1) atomically using a temporary file in the same
# directory as the destination file (arg2)
# NOTE: on filesystems such as ext4 the directory entry change may hit
# storage before the file contents, so use fsync to order things
function cpatomic()
{
    local tmp="$(mktemp "$2".XXXXXX)" || return 1
    # preserve mode, ownership and any security contexts
    if cp -p -- "$1" "$tmp" && "$fsync" "$tmp"; then
        mv -f -- "$tmp" "$2"
        return
    else
        rm -f -- "$tmp"
        return 1
    fi
}

# The file to be edited inplace must be third argument !
# File replacement is atomic
# NOTE: on filesystems such as ext4 the directory entry change may hit
# storage before the file contents, so use fsync to order things
function sed_inplace()
{
    local tmp="$(cptemp "$3")"
    if [ $? -ne 0 ]; then
        echo "ERROR: failed editing file in spxsysconf (no temp)" >&2
        return 1
    fi

    sed "$@" > "$tmp" && "$fsync" "$tmp"
    if [ $? -ne 0 ]; then
        echo "ERROR: failed editing file in spxsysconf" >&2
    else
        mv -f -- "$tmp" "$3" && return 0
        echo "ERROR: failed editing file in spxsysconf (cannot replace)" >&2
    fi

    # failed

    rm -f -- "$tmp"
    return 1
}

function config_clean()
{
    sed_inplace -e "/^$start_tag/,/^$end_tag/d" "$1"
}

function check_config()
{
    grep -q "^$start_tag" "$1"
}

# tests if an IPv6 address is a link-local address (i.e. in the fe80::/10 block)
# arg1: the IPv6 address
function is_ipv6_link_local()
{
    [[ "$1" =~ ^[fF][eE][8-9a-bA-B][0-9a-fA-F]: ]]
}

# outputs the resolver nameserver option for a given DNS server
# arg1: the DNS server IP address (IPv4 or IPv6)
net_resolver_nameserver()
{
    local dns="$1"
    local iface="$2"

    if [[ "$dns" =~ ^[a-fA-F0-9:]+(%[-:._@+a-z0-9]+|)$ ]]; then
        # IPv6: need special processing with zone identifier
        dns="${dns%%%*}" # remove zone identifier, if any
        if is_ipv6_link_local "$dns"; then
            dns="${dns}%${iface}"
        fi
    fi
    echo nameserver "$dns"
}

# arg1: configuration string
# arg2: local interface name (for local-link addresses)
# arg3: static resolv.conf file path (optional)
function net_config_static_dns()
{
    local ret dns1 dns2 dns3 domain junk
    local saved_ifs
    local params="$1"
    local iface="$2"
    local dnsfile="$3"

    [ -n "$dnsfile" ] || dnsfile="$static_resolv_conf"

    saved_ifs="$IFS"
    IFS=";"
    read -r dns1 dns2 dns3 domain junk < <( echo "$params" )
    ret=$?
    IFS="$saved_ifs"

    [ $ret -eq 0 ] || return 1

    [ -z "$dns1" -a -z "$dns2" -a -z "$dns3" ] && return

    : > "$dnsfile"
    [ -n "$dns1" ] && { net_resolver_nameserver "$dns1" "$iface" >> "$dnsfile" || return; }
    [ -n "$dns2" ] && { net_resolver_nameserver "$dns2" "$iface" >> "$dnsfile" || return; }
    [ -n "$dns3" ] && { net_resolver_nameserver "$dns3" "$iface" >> "$dnsfile" || return; }
    if [ -n "$domain" ]; then
        echo "domain $domain" >> "$dnsfile" || return
    fi

    return 0
}

# tests if an interface is already declared auto in the interfaces file
# arg1: the interface name
# arg2: the interfaces configuration file
function net_config_is_iface_auto()
{
    local iface="$1"
    local netfile="$2"
    grep -q "^[[:space:]]*auto\([[:space:]]\+.*\|\)[[:space:]]\+${iface}\([[:space:]]\|\$\)" "$netfile"
}

# Sets up network configuration for DHCP on an interface
# arg1: network interface to configure
# arg2: network configuration file path (optional)
function net_config_iface_dhcp()
{
    local iface="$1"
    local file="$2"
    local auto

    [ -n "$file" ] || file="$net_conf"

    echo "$start_tag $start_comm" >> "$file" || return 1

    net_config_is_iface_auto "$iface" "$file" || auto="auto $iface"

    cat >> "$file" <<EOF

# Configure $iface for DHCP
$auto
iface $iface inet dhcp

EOF
    [ $? -eq 0 ] || return 1

    echo "$end_tag" >> "$file" || return 1

    return 0
}

# This function leaves the config in the NETCONFIG variable
# arg1: network interface to configure
# arg2: configuration string
# arg3: network configuration file path (optional)
# arg4: static resolv.conf file path (optional)
function net_config_iface_static()
{
    local ret ipaddr netmask brd gw dns1 dns2 dns3 domain junk
    local saved_ifs ipvals auto
    local iface="$1"
    local params="$2"
    local netfile="$3"
    local dnsfile="$4"

    [ -n "$netfile" ] || netfile="$net_conf"
    [ -n "$dnsfile" ] || dnsfile="$static_resolv_conf"

    saved_ifs="$IFS"
    IFS=";"
    read -r ipaddr netmask brd gw dns1 dns2 dns3 domain junk < <( echo "$params" )
    ret=$?
    IFS="$saved_ifs"

    [ $ret -eq 0 ] || return 1

    if [ -z "$ipaddr" -o -z "$netmask" ]; then
        echo 'ERROR: static network config missing parameters' >&2
        return 1
    fi

    ipvals="$(ipcalc -b -m -s "$ipaddr" "$netmask")"
    if [ $? -ne 0 ]; then
        echo 'ERROR: invalid parameters in static network config' >&2
        return 1
    fi
    eval "$ipvals" || return 1

    # NETMASK and BROADCAST come from eval above
    NETCONFIG="static=${ipaddr};${NETMASK};${BROADCAST}"

    echo "$start_tag $start_comm" >> "$netfile" || return 1

    net_config_is_iface_auto "$iface" "$netfile" || auto="auto $iface"

    cat >> "$netfile" <<EOF

# Configured static IP address
$auto
iface $iface inet static
EOF
    [ $? -eq 0 ] || return 1

    echo -e "    address ${ipaddr}\n    netmask ${NETMASK}\n    broadcast ${BROADCAST}" >> "$netfile" || return 1

    if [ -n "$gw" ]; then
        NETCONFIG="${NETCONFIG};${gw}"
        echo -e "    gateway ${gw}\n" >> "$netfile" || return 1
    else
        echo "" >> "$netfile" || return 1
    fi

    echo "$end_tag" >> "$netfile" || return 1

    if [ -n "$dns1" ] || [ -n "$dns2" ] || [ -n "$dns3" ]; then
        [ -z "$gw" ] && NETCONFIG="${NETCONFIG};"
        NETCONFIG="${NETCONFIG};${dns1};${dns2};${dns3}"
        [ -n "$domain" ] && NETCONFIG="${NETCONFIG};${domain}"
        net_config_static_dns "${dns1};${dns2};${dns3};${domain}" "$iface" "$dnsfile" || return 1
    fi

    return 0
}

# arg1: interface to configure
# arg2: configuration string
# arg3: network configuration file path (optional)
function net_config_iface_static6()
{
    local ret ipaddr netmask brd gw dns1 dns2 dns3 domain junk
    local saved_ifs auto
    local iface="$1"
    local params="$2"
    local netfile="$3"

    [ -n "$netfile" ] || netfile="$net_conf"

    saved_ifs="$IFS"
    IFS=";"
    read -r ipaddr plen gw junk < <( echo "$params" )
    ret=$?
    IFS="$saved_ifs"

    [ $ret -eq 0 ] || return 1

    if [ -z "$ipaddr" -o -z "$plen" ]; then
        echo 'ERROR: static network6 config missing parameters' >&2
        return 1
    fi

    if ! [[ "$ipaddr" =~ ^[a-fA-F0-9:]+$ ]]; then
        echo 'ERROR: illegal characters in IPv6 address' >&2
        return 1
    fi
    if ! [[ "$plen" =~ ^[0-9]+$ ]]; then
        echo 'ERROR: illegal characters in IPv6 prefix length' >&2
        return 1
    fi
    if ! [ "$plen" -ge 0 -a "$plen" -le 128 ]; then
        echo 'ERROR: invalid IPv6 prefix length' >&2
        return 1
    fi
    if [ -n "$gw" ] && ! [[ "$gw" =~ ^[a-fA-F0-9:]+$ ]]; then
        echo 'ERROR: illegal characters in IPv6 gateway address' >&2
        return 1
    fi

    echo "$start_tag $start_comm" >> "$netfile" || return 1

    net_config_is_iface_auto "$iface" "$netfile" || auto="auto $iface"

    cat >> "$netfile" <<EOF

# Configured static IPv6 address
$auto
iface $iface inet6 static
EOF
    [ $? -eq 0 ] || return 1

    echo -e "    address ${ipaddr}\n    netmask ${plen}\n" >> "$netfile" || return 1

    # The routes added by the kernel via IPv6 RAs get metric 1024, so use a
    # lower metric for the static default route to win and have no conflict
    # when adding it (RTNETLINK answers: File exists).
    # Routes automatically generated by the kernel get metric 256.
    if [ -n "$gw" ]; then
        echo -e "    gateway ${gw}\n    metric 768\n" >> "$netfile" || return 1
    else
        echo "" >> "$netfile" || return 1
    fi

    echo "$end_tag" >> "$netfile" || return 1

    return 0
}
