# start/shutdown server
start_server() {
    STRACE_LINGER="y" STDOUT="/dev/null" netcat_listen ${NETCAT_DGRAM:+-u} "$@"
}
shutdown_server() {
    if [ -z "$NETCAT_DGRAM" ]; then
        wait $PID
    else
        kill_and_wait $PID
    fi
    exec {STRACE_FD}<&-
}

# connect the client
connect() {
    local pid fd
    if [ -z "$NETCAT_DGRAM" ]; then
        # we use `-N` on the client to shutdown immediately, and since the
        # server terminates after the first connection we don't need to kill $PID
        $NC -N "$@" <<<"foo"
    else
        mkfifo -- "$TEMPDIR/connect.fifo"
        strace -Dqo "$TEMPDIR/connect.fifo" -I1 -ze trace="%net" -- \
            $NC -Nu "$@" <<<"foo" & pid=$!
        { grep -m1 -Eq '^shutdown\('; kill_and_wait $pid; } <"$TEMPDIR/connect.fifo"
        kill_and_wait $pid
        rm -f -- "$TEMPDIR/connect.fifo"
    fi
}

# check peer (client) address
check_peer_addr_fam() {
    local fam="$1" syscall regex strace_accept peer_fam
    local lineno=${BASH_LINENO[${2-0}]}

    if [ -z "$NETCAT_DGRAM" ]; then
        syscall="accept4?"
        regex='^[A-Za-z0-9_]+\([0-9]+,[[:blank:]]*(\{sa_family=([^{},[:blank:]]+)(,[^{}]+)?\}),'
    else
        syscall="recvfrom"
        regex='^[A-Za-z0-9_]+\([0-9]+,[[:blank:]]*"[^"]*",[[:blank:]]*[0-9]+,[[:blank:]]*[^,[:blank:]]+,[[:blank:]]*(\{sa_family=([^{},[:blank:]]+)(,[^{}]+)?\}),'
    fi
    strace_accept="$(grep -m1 -E "^$syscall\\($BIND_FD," <&$STRACE_FD)"

    if [[ "$strace_accept" =~ $regex ]]; then
        PEER="${BASH_REMATCH[1]}"
        peer_fam="${BASH_REMATCH[2]}"
        PEER_REST="${BASH_REMATCH[3]}"
    else
        printf "ERROR at line %d: Couldn't extract accept address from \"%s\"\\n" $lineno "$strace_accept" >&2
        exit 1
    fi

    if [ "$peer_fam" != "$fam" ]; then
        printf "ERROR at line %d: Peer \"%s\" isn't of address family %s\\n" $lineno "$PEER" "$fam" >&2
        exit 1
    fi
}
check_peer_addr() {
    local fam="$1" PEER_REST in p
    check_peer_addr_fam "$fam" 1

    if [ "$fam" = "AF_INET" ] && [[ "$PEER_REST" =~ ^,[[:blank:]]*sin_port=htons\(([0-9]+)\),[[:blank:]]*sin_addr=inet_addr\(\"([^\"]+)\"\)$ ]]; then
        in="${BASH_REMATCH[2]}"
        p="${BASH_REMATCH[1]}"
    elif [ "$fam" = "AF_INET6" ] && [[ "$PEER_REST" =~ ^,[[:blank:]]*sin6_port=htons\(([0-9]+)\),[[:blank:]]*sin6_flowinfo=htonl\([0-9]+\),[[:blank:]]*inet_pton\($fam,[[:blank:]]*\"([^\"]+)\",[[:blank:]]*\&sin6_addr\),[[:blank:]]*sin6_scope_id=[0-9]+$ ]]; then
        in="${BASH_REMATCH[2]}"
        p="${BASH_REMATCH[1]}"
    elif [ "$fam" = "AF_UNIX" ] && [[ "$PEER_REST" =~ ^(,[[:blank:]]*sun_path=\"([^\"]+)\")?$ ]]; then
        in="${BASH_REMATCH[2]-}"
        p=""
    else
        printf "ERROR at line %d: Unable to parse \"%s\"\\n" ${BASH_LINENO[0]} "$PEER" >&2
        exit 1
    fi

    if [ -n "${2+x}" ] && [ "$in" != "$2" ]; then
        printf "ERROR at line %d: Expected peer address \"%s\", got \"%s\"\\n" ${BASH_LINENO[0]} "$2" "$PEER" >&2
        exit 1
    elif [ -n "${3+x}" ] && [ "$p" != "$3" ]; then
        printf "ERROR at line %d: Expected peer port \"%s\", got \"%s\"\\n" ${BASH_LINENO[0]} "$3" "$PEER" >&2
        exit 1
    fi
}

# IPv4
DPORT=$(random_port)
start_server "0.0.0.0" $DPORT
connect "127.0.0.1" $DPORT
check_peer_addr AF_INET "127.0.0.1"
shutdown_server

# IPv4 with source address
DPORT=$(random_port)
start_server "0.0.0.0" $DPORT
connect -s "127.0.1.1" "127.0.0.1" $DPORT
check_peer_addr AF_INET "127.0.1.1"
shutdown_server

# IPv4 with source port
DPORT=$(random_port)
start_server "0.0.0.0" $DPORT
SPORT=$(random_port)
connect -p $SPORT "127.0.0.1" $DPORT
check_peer_addr AF_INET "127.0.0.1" $SPORT
shutdown_server

# IPv4 with both source address and port
DPORT=$(random_port)
start_server "0.0.0.0" $DPORT
SPORT=$(random_port)
connect -s "127.0.1.1" -p $SPORT "127.0.0.1" $DPORT
check_peer_addr AF_INET "127.0.1.1" "$SPORT"
shutdown_server

# IPv6
DPORT=$(random_port)
start_server "::" $DPORT
connect "::1" $DPORT
check_peer_addr AF_INET6 "::1"
shutdown_server

# IPv6 with source port
DPORT=$(random_port)
start_server "::" $DPORT
SPORT=$(random_port)
connect -p $SPORT "::1" $DPORT
check_peer_addr AF_INET6 "::1" "$SPORT"
shutdown_server

if [ $Bindv6Only -eq 0 ]; then
    # IPv4-mapped addresses, cf. #921446
    DPORT=$(random_port)
    start_server "::" $DPORT
    connect "127.0.0.1" $DPORT
    check_peer_addr AF_INET6 "::ffff:127.0.0.1"
    shutdown_server
fi


#######################################################################
# UNIX-domain sockets

check_unix_domain_socket() {
    local sun_path="$1" sun_path_tmpl="/tmp/nc-[0-9A-Za-z]{6}/recv\.sock" PEER
    start_server -U -- "$sun_path"
    connect ${2+-s "$2"} -U -- "$sun_path"
    check_peer_addr AF_UNIX ${2+"$2"}
    shutdown_server
    # check default temporary socket name for datagram sockets
    if [ -n "$NETCAT_DGRAM" ] && [ -z "${2+x}" ] && ! [[ "$PEER" =~ ^\{sa_family=AF_UNIX,[[:blank:]]*sun_path=\"$sun_path_tmpl\"\}$ ]]; then
        printf "ERROR at line %d: Expected sun_path =~ /^%s\$/, got \"%s\"\\n" ${BASH_LINENO[0]} "$sun_path_tmpl" "$PEER" >&2
        exit 1
    fi
}

DPORT=$(random_port)
check_unix_domain_socket "$TEMPDIR/$DPORT.sock"

if [ -n "$NETCAT_DGRAM" ]; then
    # the local temporary socket file only makes sense for UNIX-domain datagram sockets
    DPORT=$(random_port)
    SPORT=$(random_port)
    install -m0600 /dev/null -- "$TEMPDIR/.$SPORT.sock" # nc should delete existing files before bind()'ing
    check_unix_domain_socket "$TEMPDIR/$DPORT.sock" "$TEMPDIR/.$SPORT.sock"
    test \! -e "$TEMPDIR/.$SPORT.sock" -o -S "$TEMPDIR/.$SPORT.sock"
fi

# check size limit
UNIX_PATH_MAX="$( $(dirname -- "$0")/sun_path-size )"
REAL_NC="$(realpath -e -- "$NC")"
sun_path="$(head -c $UNIX_PATH_MAX </dev/urandom | base64 -w0 | tr '+/' '-_' | head -c $((UNIX_PATH_MAX - 1)))"
( cd "$TEMPDIR" && NC="$REAL_NC" check_unix_domain_socket "$sun_path" )
( cd "$TEMPDIR" && ! "$REAL_NC" -U -- "-$sun_path" 2>"$TEMPDIR/nc.err" )
grep -Fxq "nc: -$sun_path: File name too long" <"$TEMPDIR/nc.err"


#######################################################################
# d/p/abstract-unix-domain-socket.patch

check_abstract_unix_domain_datagram_socket() { # abstract source socket
    local sun_path_dst="$1" sun_path_src="$2" PEER PEER_REST sockaddr
    start_server -U "$sun_path_dst"
    connect -s @"$sun_path_src" -U "$sun_path_dst"
    check_peer_addr_fam "AF_UNIX" 1
    shutdown_server

    if ! [[ "$PEER_REST" =~ ^,[[:blank:]]*sun_path=(.*) ]]; then
        printf "ERROR at line %d: Unable to parse \"%s\"\\n" ${BASH_LINENO[0]} "$PEER" >&2
        exit 1
    fi

    sockaddr="{sa_family=AF_UNIX, sun_path=@\"$sun_path_src\"}"
    if [ "@\"$sun_path_src\"" != "${BASH_REMATCH[1]}" ]; then
        printf "ERROR at line %d: Expected peer address %s, got %s\\n" ${BASH_LINENO[0]} "$sockaddr" "$PEER" >&2
        exit 1
    fi
}

uname_s="$(uname -s)"
if [ "${uname_s^[A-Z]}" = "Linux" ]; then
    check_unix_domain_socket @"/nonexistent/$0"            # non-existing path
    check_unix_domain_socket @"$0"                         # existing path
    check_unix_domain_socket @"@netcat-openbsd/$TEST_NAME" # multiple leading @

    if [ -n "$NETCAT_DGRAM" ]; then
        check_abstract_unix_domain_datagram_socket @"netcat-openbsd/$TEST_NAME.dst" "netcat-openbsd/$TEST_NAME.src"
        DPORT=$(random_port)
        check_abstract_unix_domain_datagram_socket "$TEMPDIR/$DPORT.sock" "netcat-openbsd/$TEST_NAME.src"
        SPORT=$(random_port)
        check_unix_domain_socket @"netcat-openbsd/$TEST_NAME.dst" "$TEMPDIR/$SPORT.sock"
    fi

    # check size limit
    sun_path="$(head -c $UNIX_PATH_MAX </dev/urandom | base64 -w0 | head -c $((UNIX_PATH_MAX - 1)))"
    check_unix_domain_socket @"$sun_path"
    ! "$NC" -U @"#$sun_path" 2>"$TEMPDIR/nc.err"
    grep -Fxq "nc: @#$sun_path: File name too long" <"$TEMPDIR/nc.err"
fi

if [ -n "$NETCAT_DGRAM" ]; then
    DPORT=$(random_port)
    sun_path="netcat-openbsd.$TEST_NAME"
    install -m0600 /dev/null -- "$TEMPDIR/@$sun_path"
    st1="$(stat -c"%i %z" "$TEMPDIR/@$sun_path")"
    (
        cd "$TEMPDIR" && \
        NC="$REAL_NC" start_server -U "./$DPORT.sock" && \
        NC="$REAL_NC" connect -s "@$sun_path" -U "./$DPORT.sock" && \
        shutdown_server
    )

    if [ "${uname_s^[A-Z]}" = "Linux" ]; then
        # "$TEMPDIR/@$sun_path" should be preserved since we bound to an abstract socket
        test -f "$TEMPDIR/@$sun_path"
        st2="$(stat -c"%i %z" "$TEMPDIR/@$sun_path")"
        test "$st1" = "$st2"
    else
        test \! -e "$TEMPDIR/@$sun_path" -o -S "$TEMPDIR/@$sun_path"
    fi
fi

# vim: set filetype=bash :
