#!/usr/bin/env python3
"""Python netcat implementation."""

# Behaviour Server
# 1. (tty and non-tty mode) TCP Server should always quit if client disconnects
# 2. (tty and non-tty mode) TCP Server should always quit if connection is gone/broken
# 3. (tty and non-tty mode) UDP Will always stay open (can't determine if connection is gone)

from __future__ import print_function
from abc import abstractmethod
from abc import ABCMeta

from subprocess import PIPE
from subprocess import Popen
from subprocess import STDOUT

import argparse
import atexit
import logging
import os
import re
import socket
import sys
import threading
import time

# Abstract class with Python 2 + Python 3 support: https://stackoverflow.com/questions/35673474
ABC = ABCMeta("ABC", (object,), {"__slots__": ()})

# Only used with mypy for static source code analysis
if os.environ.get("MYPY_CHECK", False):
    from typing import Optional, Iterator, List, Dict, Any, Callable

# TODO: Find windows import
if os.name != "nt":
    import select

# -------------------------------------------------------------------------------------------------
# GLOBALS
# -------------------------------------------------------------------------------------------------

APPNAME = "pwncat"
APPREPO = "https://github.com/cytopia/pwncat"
VERSION = "0.0.10-alpha"

# Default timeout for timeout-based sys.stdin and socket.recv
TIMEOUT_READ_STDIN = 0.1
TIMEOUT_RECV_SOCKET = 0.1
TIMEOUT_RECV_SOCKET_RETRY = 2


# #################################################################################################
# #################################################################################################
# ###
# ###   1 / 3   L I B R A R Y   C L A S S E S
# ###
# #################################################################################################
# #################################################################################################

# -------------------------------------------------------------------------------------------------
# CLASS: TraceLogger
# -------------------------------------------------------------------------------------------------
class TraceLogger(logging.getLoggerClass()):  # type: ignore
    """Logger class with additional TRACE level logging."""

    LEVEL_NUM = 9
    LEVEL_NAME = "TRACE"

    def __init__(self, name, level=logging.NOTSET):
        # type: (str, int) -> None
        """Create a custom Logger instance with TRACE level logging."""
        super(TraceLogger, self).__init__(name, level)
        logging.addLevelName(self.LEVEL_NUM, self.LEVEL_NAME)

    def trace(self, msg, *args, **kwargs):
        # type: (str, Any, Any) -> None
        """Set custom log level for TRACE."""
        if self.isEnabledFor(self.LEVEL_NUM):
            # Yes, logger takes its '*args' as 'args'.
            self._log(self.LEVEL_NUM, msg, args, **kwargs)


# -------------------------------------------------------------------------------------------------
# CLASS: ColoredLogFormatter
# -------------------------------------------------------------------------------------------------
class ColoredLogFormatter(logging.Formatter):
    """Logging Formatter to add colors and count warning / errors."""

    COLORS = {
        logging.CRITICAL: "\x1b[31;1m",  # bold red
        logging.ERROR: "\x1b[31;21m",  # red
        logging.WARNING: "\x1b[33;21m",  # yellow
        logging.INFO: "\x1b[32;21m",  # green
        logging.DEBUG: "\x1b[30;21m",  # gray
    }
    COLOR_DEF = COLORS[logging.DEBUG]
    COLOR_RST = "\x1b[0m"

    def __init__(self, color, loglevel):
        # type: (str, int) -> None
        """Construct a colored log formatter.

        Args:
            color (str):  Either be "alway", "never" or "auto"
            loglevel (int):  Current desired log level
        """
        super(ColoredLogFormatter, self).__init__()
        self.color = color
        self.loglevel = loglevel
        self.tty = sys.stderr.isatty()

    def __get_format(self):
        # type: () -> str
        """Return format string based on currently applied log level."""
        # In debug logging we add slightly more info to all formats
        if self.loglevel == logging.DEBUG:
            return "%(levelname)s [%(threadName)s]: %(message)s"
        # In lower than debug logging we will add even more info to all log formats
        if self.loglevel < logging.DEBUG:
            return "%(levelname)s [%(threadName)s] %(lineno)d:%(funcName)s(): %(message)s"
        # By default, we will only add basic info
        return "%(levelname)s: %(message)s"

    def __colorize(self, level, fmt):
        # type: (int, str) -> str
        """Colorize a log message based on its level."""
        if self.color == "never":
            return fmt

        # If stderr is redirected to a file or we're running on windows, do not do colorize
        if self.color == "auto" and (not self.tty or os.name == "nt"):
            return fmt

        return self.COLORS.get(level, self.COLOR_DEF) + fmt + self.COLOR_RST

    def format(self, record):
        # type: (logging.LogRecord) -> str
        """Apply custom formatting to log message."""
        log_fmt = self.__get_format()
        log_fmt = self.__colorize(record.levelno, log_fmt)
        formatter = logging.Formatter(log_fmt)
        return formatter.format(record)


# -------------------------------------------------------------------------------------------------
# CLASS: LibSocket
# -------------------------------------------------------------------------------------------------
class LibSocket(object):
    """Provides TCP, UDP and IPv4, IPv6 Socket funcionality."""

    # Store the address of the remote end.
    # If we are in server role and running in UDP mode,
    # it must wait for the client to connect first in order
    # to retrieve its addr and port in order to be able
    # to send data back to it.
    remote_addr = None  # type: str
    remote_port = None  # type: int

    # Available optional settings with sane defaults
    __options = {
        "bufsize": 1024,  # Receive buffer size
        "backlog": 0,  # Listen backlog
        "recv_timeout": None,  # seconds for socket timeout or None if blocking
        "nodns": False,  # Do not resolve hostname
        "udp": False,  # Is TCP or UDP?
    }  # type: Dict[str, Any]

    # ------------------------------------------------------------------------------
    # Constructor / Destructor
    # ------------------------------------------------------------------------------
    def __init__(self, encoder, options):
        # type: (StringEncoder, Dict[str, Any]) -> None
        """Construct a socket.

        Args:
            encoder (StringEncoder): Instance of StringEncoder (Python2/3 str/byte compat)
            options (dict):          Dict of required options (see __options)

        Attributes:
            remote_addr (str):    Remote hostname
            remote_port (int):    Remote port
        """
        assert type(self) is not LibSocket, "LibSocket cannot be instantiated directly."

        # Ensure to provide all items (be explicit - currently helps to find bugs)
        for index in self.__options:
            assert index in options, "Provided options are missing key: {}".format(index)
        # Ensure to only provide option itenms that are defined/exist (helps to find bugs)
        for index in options:
            assert index in self.__options, "Provided an invalid option item: {}".format(index)

        # Assign options
        self.log = logging.getLogger(__name__)
        self.enc = encoder
        self.__options = options

    # ------------------------------------------------------------------------------
    # Public Functions
    # ------------------------------------------------------------------------------
    def gethostbyname(self, host, port):
        # type: (str, int) -> str
        """Translate hostname into IP address.

        Args:
            host (str): The hostname to resolve
            port (int): The port of the hostname to resolve
        Returns:
            str: Numeric IP address or False on error

        Raises:
            socket.gaierror: If hostname cannot be resolved.
        """
        socktype = socket.SOCK_DGRAM if self.__options["udp"] else socket.SOCK_STREAM
        flags = 0
        if self.__options["nodns"]:
            flags = socket.AI_NUMERICHOST
        try:
            self.log.debug("Resolving hostname: %s", host)
            info = socket.getaddrinfo(host, port, socket.AF_INET, socktype, flags)[0]
            sockaddr = info[4]
            # (family, socktype, proto, canonname, sockaddr) = socket.getaddrinfo(
            #     host, port, socket.AF_INET, socktype, flags
            # )[0]
        except socket.gaierror as error:
            self.log.error("Resolve Error: %s", error)
            raise socket.gaierror(error)  # type: ignore
        self.log.debug("Resolved hostname: %s", sockaddr[0])
        return str(sockaddr[0])

    def create_socket(self):
        # type: () -> Optional[socket.socket]
        """Create TCP or UDP socket.

        Returns
            socket.socket: TCP or UDP socket or False on error
        """
        try:
            if self.__options["udp"]:
                self.log.debug("Creating UDP socket")
                sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
            else:
                self.log.debug("Creating TCP socket")
                sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        except socket.error as error:
            self.log.error("Failed to create the socket: %s", error)
            return None
        # Get around the "[Errno 98] Address already in use" error, if the socket is still in wait
        # we instruct it to reuse the address anyway.
        sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        return sock

    def close_socket(self, sock):
        # type: (socket.socket) -> None
        """Shuts down and closes a socket.

        Args:
            sock (socket.socket): The socket to shutdown and close
        """
        try:
            # (SHUT_RD)   0 = Done receiving (disallows receiving)
            # (SHUT_WR)   1 = Done sending (disallows sending)
            # (SHUT_RDWR) 2 = Both
            self.log.trace("Shutting down socket")  # type: ignore
            sock.shutdown(socket.SHUT_RDWR)
        except (AttributeError, OSError, socket.error):
            self.log.trace("Could not shutdown socket")  # type: ignore
        try:
            self.log.trace("Closing socket")  # type: ignore
            sock.close()
        except (AttributeError, OSError, socket.error):
            self.log.trace("Could not close socket")  # type: ignore

    def bind(self, sock, addr, port):
        # type: (socket.socket, str, int) -> bool
        """Bind the socket to an address.

        Args:
            sock (socket.socket): The socket to bind
            addr (str):           The address to bind to
            port (int):           The port to bind to
        Returns:
            bool: True on success and False on Failure
        """
        try:
            self.log.debug("Binding socket to %s:%d", addr, port)
            sock.bind((addr, port))
            return True
        except (OverflowError, OSError, socket.error) as error:
            self.log.error("Binding socket to %s:%d failed: %s", addr, port, error)
            return False

    def listen(self, sock):
        # type: (socket.socket) -> bool
        """Listen for connections made to the socket.

        Args:
            sock (socket.socket): The socket to listen on
        Returns:
            bool: True on success and False on Failure
        """
        try:
            self.log.debug("Listening with backlog=%d", self.__options["backlog"])
            sock.listen(self.__options["backlog"])
            return True
        except socket.error as error:
            self.log.error("Listening failed: %s", error)
            return False

    def accept(self, sock):
        # type: (socket.socket) -> Optional[socket.socket]
        """Accept a connection. The socket must be bound to an addr and listening for connections.

        Args:
            sock (socket.socket): The socket to accept on
        Returns:
            socket.socket: The connection socket or None on error
        """
        try:
            self.log.debug("Waiting for TCP client")
            conn, client = sock.accept()
            addr, port = client
            self.remote_addr = addr
            self.remote_port = port
            self.log.info("Client connected from %s:%d", addr, port)
            return conn
        except (socket.gaierror, socket.error) as error:
            self.log.error("Accept failed: %s", error)
            return None

    def connect(self, sock):
        # type: (socket.socket) -> bool
        """Connect to a remote socket at given address and port (TCP-only).

        Args:
            sock (socket.socket): The socket to use for connecting
        Returns:
            bool: True on success and False on Failure
        """
        try:
            self.log.debug("Connecting to %s:%d", self.remote_addr, self.remote_port)
            sock.connect((self.remote_addr, self.remote_port))
            return True
        except socket.error as error:
            self.log.error(
                "Connecting to %s:%d failed: %s", self.remote_addr, self.remote_port, error
            )
            return False

    def settimeout(self, sock):
        # type: (socket.socket) -> None
        """Set the receive timeout on a socket.

        Args:
            sock (socket.socket): The socket to set the receive timeout for.
        """
        self.log.debug("Setting sock recv timeout to %f sec", self.__options["recv_timeout"])
        sock.settimeout(self.__options["recv_timeout"])

    # ------------------------------------------------------------------------------
    # Protected Send / Receive Functions
    # ------------------------------------------------------------------------------
    def _send(self, sock, data):
        # type: (socket.socket, str) -> int
        """Send data to a socker.

        Args:
            sock (socket.socket): The socket to send data through.
            data (str):           The data to send
        Returns:
            int: Returns total bytes sent or False on error
        """
        # In case of sending data back to an udp client we need to wait
        # until the client has first connected and told us its addr/port
        if self.__options["udp"] and self.remote_addr is None and self.remote_port is None:
            self.log.warning("UDP client has not yet connected. Queueing message")
            while self.remote_addr is None and self.remote_port is None:
                time.sleep(0.1)  # Less wastefull than using 'pass'

        curr = 0  # bytes send during one loop iteration
        send = 0  # total bytes send
        size = len(data)  # bytes of data that needs to be send
        byte = self.enc.encode(data)
        assert size == len(byte), "Encoding messed up string length, might need to do len() after."

        # Loop until all bytes have been send
        while send < size:
            try:
                self.log.debug(
                    "Trying to send %d bytes to %s:%d",
                    size - send,
                    self.remote_addr,
                    self.remote_port,
                )
                self.log.trace("Trying to send: %s", repr(byte))  # type: ignore
                if self.__options["udp"]:
                    curr = sock.sendto(byte, (self.remote_addr, self.remote_port))
                    send += curr
                else:
                    curr = sock.send(byte)
                    send += curr
                if curr == 0:
                    self.log.error("No bytes send during loop round.")
                    return 0
                # Remove 'curr' many bytes from byte for the next round
                byte = byte[curr:]
                self.log.debug(
                    "Sent %d bytes to %s:%d (%d bytes remaining)",
                    curr,
                    self.remote_addr,
                    self.remote_port,
                    size - send,
                )
            except (OSError, socket.error) as error:
                self.log.error("Socket OS Error: %s", error)
                return False
        return send

    def _receive(self, sock):
        # type: (socket.socket) -> str
        """Return data from the given socket.

        Args:
            sock (socket.socket): The socket to receive data on.

        Returns:
            str: data received
        Raises:
            socket.timeout: Except here to do an action when the socket is not busy.
            socket.error:   Except here when unconnected or connection was forcibly closed.
            EOFError:       Except here when upstream has closed the connection.
        """
        try:
            # https://manpages.debian.org/buster/manpages-dev/recv.2.en.html
            (byte, addr) = sock.recvfrom(self.__options["bufsize"])

        # [1/5] Non-blocking socket if socket.settimeout() is set
        # NOTE: This is the place where we can do any checks in between reads as the
        # socket has been changed from blocking to time-out based.
        # NOTE: This is also the place, where we quit in case we want to.
        except socket.timeout as error:
            raise socket.timeout(error)  # type: ignore

        # [2/5] Connection was forcibly closed
        # [Errno 107] Transport endpoint is not connected
        # [Errno 10054] An existing connection was forcibly closed by the remote host
        # [WinError 10054] An existing connection was forcibly closed by the remote host
        except (OSError, socket.error) as error:
            self.log.warning("Connection error: %s", error)
            raise socket.error(error)

        # [3/5] When closing itself (e.g.: via Ctrl+c and the socket_close() funcs are called)
        except AttributeError:
            msg = "Connection was closed by self."
            self.log.warning(msg)
            raise socket.error(msg)

        # If we're receiving data from a UDP client
        # we can firstly/finally set its addr/port in order
        # to send data back to it (see send() function)
        if self.__options["udp"]:
            self.remote_addr, self.remote_port = addr
            self.log.debug("Client connected: %s:%d", self.remote_addr, self.remote_port)

        # [4/5] Upstream (server or client) is gone.
        # In TCP, there is no such thing as an empty message, so zero means a peer disconnect.
        # In UDP, there is no such thing as a peer disconnect, so zero means an empty datagram.
        if not byte:
            msg = "Upstream has closed the connection."
            self.log.info(msg)
            raise EOFError(msg)

        # [5/5] We have data to process
        data = self.enc.decode(byte)
        self.log.debug(
            "Received %d bytes from %s:%d", len(data), self.remote_addr, self.remote_port
        )
        self.log.trace("Received: %s", repr(data))  # type: ignore
        return data


# -------------------------------------------------------------------------------------------------
# CLASS: StringEncoder
# -------------------------------------------------------------------------------------------------
class StringEncoder(object):
    """Takes care about Python 2/3 string encoding/decoding.

    This allows to parse all string/byte values internally between all
    classes or functions as strings to keep full Python 2/3 compat.
    """

    # https://stackoverflow.com/questions/606191/27527728#27527728
    codec = "cp437"

    def __init__(self):
        # type: () -> None
        """Create a StringEncoder instance which converts str/bytes according to Python version."""
        self.py3 = sys.version_info >= (3, 0)  # type: bool

    def encode(self, data):
        # type: (str) -> bytes
        """Convert string into a byte type for Python3."""
        if self.py3:
            return data.encode(self.codec)
        return data  # type: ignore

    def decode(self, data):
        # type: (bytes) -> str
        """Convert bytes into a string type for Python3."""
        if self.py3:
            return data.decode(self.codec)
        return data  # type: ignore


# -------------------------------------------------------------------------------------------------
# CLASS: Pwncat
# -------------------------------------------------------------------------------------------------
class Pwncat(LibSocket):
    """Pwncat implementation based on custom Socket library."""

    sock = None  # type: socket.socket
    conn = None  # type: socket.socket
    ssig = None  # type: StopSignal

    # ------------------------------------------------------------------------------
    # Constructor / Destructor
    # ------------------------------------------------------------------------------
    def __init__(
        self,
        encoder,  # type: StringEncoder
        host,  # type: str
        port,  # type: int
        role,  # type: str
        srv_opts,  # type: Dict[str, Any]
        cli_opts,  # type: Dict[str, Any]
        sock_opts,  # type: Dict[str, Any]
    ):
        # type: (...) -> None
        """Create a Pwncat instance of either a server or a client.

        Args:
            encoder (StringEncoder): Instance of StringEncoder (Python2/3 str/byte compat)
            host (str):              The hostname to resolve
            port (int):              The port of the hostname to resolve
            role (str):              Either "server" or "client"
            srv_opts (dict):         Options for the server
            cli_opts (dict):         Options for the client
            sock_opts (dict):        Options to parse back to LibSocket

        Attributes:
            sock (socket.socket): Server listen socket
            conn (socket.socket): Server/client send/receive socket
            role (str):           Determines role: either "server" or "client"
            ssig (StopSignal):    Instance of StopSignal
        """
        # Define destructor
        atexit.register(self.__destruct__)

        assert role in ["server", "client"], "The role must be 'server' or 'client'."
        self.role = role

        super(Pwncat, self).__init__(
            encoder,
            {
                "bufsize": sock_opts["bufsize"],
                "backlog": sock_opts["backlog"],
                "recv_timeout": sock_opts["recv_timeout"],
                "nodns": sock_opts["nodns"],
                "udp": sock_opts["udp"],
            },
        )
        self.__sock_opts = {
            "udp": sock_opts["udp"],
            "recv_retry": sock_opts["recv_retry"],
        }
        if role == "server":
            self.__srv_opts = {
                "keep_open": srv_opts["keep_open"],
                "rebind": srv_opts["rebind"],
                "rebind_wait": srv_opts["rebind_wait"],
                "rebind_robin": srv_opts["rebind_robin"],
            }
        if role == "client":
            self.__cli_opts = {
                "reconn": cli_opts["reconn"],
                "reconn_wait": cli_opts["reconn_wait"],
                "reconn_robin": cli_opts["reconn_robin"],
            }

        # Resolve hostname
        try:
            addr = self.gethostbyname(host, port)
        except socket.gaierror:
            sys.exit(1)
        # Create server or client
        if role == "server":
            if not self.__server_create(addr, port):
                sys.exit(1)
        if role == "client":
            if not self.__client_create(addr, port):
                sys.exit(1)

    def __destruct__(self):
        # type: () -> None
        """Destructor."""
        if hasattr(self, "conn"):
            self.close_socket(self.conn)
        if hasattr(self, "sock"):
            self.close_socket(self.sock)

    # ------------------------------------------------------------------------------
    # Public Functions
    # ------------------------------------------------------------------------------
    def stop(self):
        # type: () -> None
        """Ensure everything in this instance will stop."""
        self.__destruct__()

    def receive(self, ssig):
        # type: (StopSignal) -> Iterator[str]
        """Network receive generator which hooks into the receive function and adds features.

        Args:
            ssig (StopSignal): A StopSignal instance to raise or request program stop.
        """
        # Counter for receive retries once this side of the program
        # shuts down (e.g.: Ctrl+c) as there could be data left on the wire.
        curr_recv_timeout_retry = 0

        # NOTE: This is a work-around to store this internally.
        # __client_reconnect_to_server() is run called at the bottom of this function
        # and runs recursively (blocking receive()) and if --reconn 0 is specified
        # it runs as long as it successfully connects back to the server - so it blocks
        # receive(), which is then not able to check the ssig.has_stop() and the chec
        # must be done in __client_reconnect_to_server() itself.
        self.ssig = ssig

        # Loop endlessly and yield data back to the caller
        while True:
            # [1/3] Generate data
            try:
                yield super(Pwncat, self)._receive(self.conn)
            # [2/3] Non-blocking socket is finished receiving data and allows us to do some action
            except socket.timeout:
                # Let's ask the interrupter() function if we should terminate?
                if not ssig.has_stop():
                    continue
                # Stop signal is raied when my own side of the network was closed.
                # Happened most likely that the user pressed Ctrl+c
                # Before quitting, we will check x many times, if there is still
                # data left to receive, before shutting down.
                if curr_recv_timeout_retry < self.__sock_opts["recv_retry"]:
                    self.log.trace(  # type: ignore
                        "Final socket read: %d/%d before quitting.",
                        curr_recv_timeout_retry + 1,
                        self.__sock_opts["recv_retry"],
                    )
                    curr_recv_timeout_retry += 1
                    continue
                # We ware all done reading, shut down
                ssig.raise_stop()
                return
            # [3/3] Upstream is gone
            except (EOFError, socket.error):
                # Do we have a stop signal?
                if ssig.has_stop():
                    return
                # Do we re-accept new clients?
                if self.role == "server" and self.__server_reaccept_from_client():
                    continue
                if self.role == "client" and self.__client_reconnect_to_server():
                    continue
                return

    def send(self, data):
        # type: (str) -> None
        """Send data to a socket."""
        super(Pwncat, self)._send(self.conn, data)

    # ------------------------------------------------------------------------------
    # Private Functions
    # ------------------------------------------------------------------------------
    def __client_reconnect_to_server(self):
        # type: () -> bool
        """Ensure the client re-connects to the remote server, if the remote server hang up.

        Args:
            ssig (StopSignal): Instance of StopSignal if set or None

        Returns:
            bool: True on success and False on failure
        """
        assert not self.__sock_opts["udp"], "This should have been caught during arg check."
        assert self.role == "client", "This should have been caught during arg check."

        while self.__cli_opts["reconn"]:

            # NOTE: This is a recursive function, so we need to check on the stop signal
            # in here, as it is blocking receive() until it finishes.
            if self.ssig is not None:
                if self.ssig.has_stop():
                    return False

            if type(self.__cli_opts["reconn"]) is int:
                self.log.info(
                    "Reconnecting in %f sec (%d more times left)",
                    self.__cli_opts["reconn_wait"],
                    self.__cli_opts["reconn"],
                )
            else:
                self.log.info(
                    "Reconnecting in %f sec (indefinitely)", self.__cli_opts["reconn_wait"]
                )
            # [1/5] Decrease reconnect counter
            if (type(self.__cli_opts["reconn"])) is int:
                self.__cli_opts["reconn"] -= 1

            # [2/5] Wait
            time.sleep(self.__cli_opts["reconn_wait"])

            # [3/5] Close current socket
            self.close_socket(self.conn)

            # [4/5] Recurse until True or reconnect count is used up
            if self.__client_create(self.remote_addr, self.remote_port):
                return True

        # [5/5] Signal failure
        self.log.info("Reconnect count is used up. Shutting down.")
        return False

    def __server_reaccept_from_client(self):
        # type: () -> bool
        """Ensure the server is able to keep connection open by re-accepting new clients.

        Returns:
            bool: True on success and False on failure
        """
        # Do not re-accept for UDP
        assert not self.__sock_opts["udp"], "This should have been caught during arg check."
        assert self.role == "server", "This should have been caught during arg check."

        # [NO] Do not re-accept
        if not self.__srv_opts["keep_open"]:
            self.log.info("No automatic re-accept specified. Shutting down.")
            return False
        # [YES] Re-accept indefinitely
        self.log.info("Re-accepting new clients")
        self.close_socket(self.conn)
        conn = self.accept(self.sock)
        if conn:
            self.conn = conn
        # NOTE: we're returning True here (even if accept failed and returned False
        # in order to re-accept indefinitely
        return True

    def __client_create(self, addr, port):
        # type: (str, int) -> bool
        """Create a network client.

        Args:
            addr (str): IP address to listen on
            port (int): Port to listen on

        Returns:
            bool: True on success and False on failure
        """
        # [1/4] Create socket
        conn = self.create_socket()
        if not conn:
            return False

        # [2/5] Store socket in instance
        self.conn = conn

        # [3/4] Set current receive timeout for socket to make it non-blocking
        self.settimeout(self.conn)

        # [3/4] Set remote addr/port
        self.remote_addr = addr
        self.remote_port = port

        # [UDP 4/4] done
        if self.__sock_opts["udp"]:
            return True

        # [TCP 4/4] connect
        if self.connect(self.conn):
            return True
        if self.__client_reconnect_to_server():
            return True
        self.close_socket(self.conn)
        return False

    def __server_create(self, addr, port):
        # type: (str, int) -> bool
        # TODO: Integrate: --rebind(-wait|-robin)
        """Create a listening server. TCP or UDP.

        Args:
            addr (str): IP address to listen on
            port (int): Port to listen on

        Returns:
            bool: True on success and False on failure
        """
        # [1/4] Create socket
        sock = self.create_socket()
        if not sock:
            return False

        # [2/5] Bind socket
        if not self.bind(sock, addr, port):
            self.close_socket(sock)
            return False

        # [UDP 3/4] There is no listen or accept for UDP
        if self.__sock_opts["udp"]:
            self.conn = sock
            self.log.info("Listening on %s (family %d/UDP, port %d)", addr, socket.AF_INET, port)
        # [TCP 3/4] Requires listen and accept
        else:
            # Listen
            if not self.listen(sock):
                self.close_socket(sock)
                return False
            self.log.info("Listening on %s (family %d/TCP, port %d)", addr, socket.AF_INET, port)
            # Accept
            conn = self.accept(sock)
            if not conn:
                self.close_socket(sock)
                return False
            self.sock = sock
            self.conn = conn

        # [4/4] Set current receive timeout for socket to make it non-blocking
        self.settimeout(self.conn)
        return True


# #################################################################################################
# #################################################################################################
# ###
# ###   2 / 3   N E T C A T   P L U G I N S
# ###
# #################################################################################################
# #################################################################################################

# -------------------------------------------------------------------------------------------------
# ABSTRACT CLASS: AbstractNetcatPlugin
# -------------------------------------------------------------------------------------------------
class AbstractNetcatPlugin(ABC):  # type: ignore
    """Abstract class to for netcat plugins.

    This is a skeleton that defines how the plugins for Netcat should look like.

    The "producer" should constantly yield data received from some sort of input
    which could be user input, output from a shell command data from a socket.

    The "callback" will apply some sort of action on the data received from a producer
    which could be output to stdout, send it to the shell or to a socket.

    "The "interrupts" are a list of funtions that trigger the producer to stop
    and return to its parent thread/function. The producer must also be implemented
    in a way that it is able to act on the event which the "interrupt" func emitted.
    """

    def __init__(self, options):  # pylint: disable=unused-argument
        # type: (Dict[str, Any]) -> None
        """Set specific options for this plugin.

        Args:
            options (dict):    Custom options for a plugin
        """
        super(AbstractNetcatPlugin, self).__init__()

    @abstractmethod
    def producer(self, ssig):
        # type: (StopSignal) -> Iterator[str]
        """Implement a generator function which constantly yields data.

        The data could be from various sources such as: received from a socket,
        received from user input, received from shell command output or anything else.

        Args:
            ssig (StopSignal): A StopSignal instance providing has_stop() and raise_stop() functions

        Yields:
            str: Data generated/produced by this function
        """
        raise NotImplementedError("Should have implemented this")

    @abstractmethod
    def consumer(self, data):
        # type: (str) -> None
        """Define a consumer callback which will apply an action on the producer output.

        Args:
            data (str): Data retrieved from the producer to work on
        """
        raise NotImplementedError("Should have implemented this")

    @abstractmethod
    def interrupt(self):
        # type: () -> None
        """Define an interrupt function which will stop the producer.

        Various producer might call blocking functions and they won't be able to stop themself
        as they hang on that blocking function. This method is triggered from outside and is
        supposed to stop/shutdown the producer.
        If no such interrupt is required, imeplemt it empty.
        """
        raise NotImplementedError("Should have implemented this")


# -------------------------------------------------------------------------------------------------
# CLASS: NetcatPluginOutput (Module for: user-input -> send -> receive -> output)
# -------------------------------------------------------------------------------------------------
class NetcatPluginOutput(AbstractNetcatPlugin):
    """Implement basic input/output plugin.

    This plugin provides a generator which continuously reads from stdin
    (non-blocking on POSIX and blocking on windows) as well as a
    callback that writes to stdout.
    """

    # Replace '\n' linefeeds (if they exist) with CRLF ('\r\n')?
    crlf = False

    # Non-blocking read from stdin achieved via timeout.
    # Specify timeout in seconds.
    input_timeout = None

    # ------------------------------------------------------------------------------
    # Constructor / Destructor
    # ------------------------------------------------------------------------------
    def __init__(self, options):
        # type: (Dict[str, Any]) -> None
        """Set specific options for this plugin."""
        super(NetcatPluginOutput, self).__init__(options)
        assert "encoder" in options
        assert "input_timeout" in options
        assert "crlf" in options

        self.log = logging.getLogger(__name__)
        self.enc = options["encoder"]
        if "input_timeout" in options:
            self.input_timeout = options["input_timeout"]
        if "crlf" in options:
            self.crlf = options["crlf"]

    # ------------------------------------------------------------------------------
    # Private Functions
    # ------------------------------------------------------------------------------
    def __use_linefeeds(self, data):
        # type: (str) -> str
        """Ensure the user input has the desired linefeeds --crlf or not."""
        # No replacement requested or already CRLF
        if not self.crlf or data.endswith("\r\n"):
            return data
        # Replace current newline character with CRLF
        if data.endswith("\n"):
            self.log.debug("Replacing LF with CRLF")
            return data[:-1] + "\r\n"
        # Otherwise just return as it is
        return data

    def __set_input_timeout(self):
        # type: () -> None
        """Throws a catchable TimeoutError exception for sys.stdin after timeout (Linux only)."""
        if not select.select([sys.stdin], [], [], self.input_timeout)[0]:
            raise TimeoutError("timed out")

    # ------------------------------------------------------------------------------
    # Public Functions
    # ------------------------------------------------------------------------------

    def producer(self, ssig):
        # type: (StopSignal) -> Iterator[str]
        """Constantly ask for user input.

        Args:
            ssig (StopSignal): A StopSignal instance providing has_stop() and raise_stop() functions

        Yields:
            str: Data read from stdin
        """
        # https://stackoverflow.com/questions/1450393/#38670261
        # while True: line = sys.stdin.readline() <- reads a whole line (faster)
        # for line in sys.stdin.readlin():        <- reads one byte at a time
        while True:
            if ssig.has_stop():
                self.log.trace("Stop signal acknowledged for reading STDIN-1")  # type: ignore
                return
            try:
                # TODO: select() does not work for windows on stdin/stdout
                if os.name != "nt":
                    self.__set_input_timeout()
                line = sys.stdin.readline()
            except TimeoutError:
                # When using select() with timeout, we don't have any input
                # at this point and simply continue the loop or quit if
                # a terminate request has been made by other threads.
                if ssig.has_stop():
                    self.log.trace("Stop signal acknowledged for reading STDIN-2")  # type: ignore
                    return
                continue
            if line:
                self.log.debug("Received %d bytes from STDIN", len(line))
                self.log.trace("Received: %s", repr(line))  # type: ignore
                yield self.__use_linefeeds(line)
            # EOF or <Ctrl>+<d>
            else:
                # DO NOT RETURN HERE BLINDLY, THE UPSTREAM CONNECTION MUST GO FIRST!
                if ssig.has_stop():
                    self.log.trace("Stop signal acknowledged for reading STDIN-3")  # type: ignore
                    return

    def consumer(self, data):
        # type: (str) -> None
        """Print received data to stdout."""
        print(data, end="")
        sys.stdout.flush()

    def interrupt(self):
        # type: () -> None
        """Empty interrupt."""


# -------------------------------------------------------------------------------------------------
# CLASS: NetcatPluginCommand (Module for user-input -> send -> execute -> send-back -> output)
# -------------------------------------------------------------------------------------------------
class NetcatPluginCommand(AbstractNetcatPlugin):
    """Implement command execution functionality."""

    # ------------------------------------------------------------------------------
    # Constructor / Destructor
    # ------------------------------------------------------------------------------
    def __init__(self, options):
        # type: (Dict[str, Any]) -> None
        """Set specific options for this plugin.

        Args:
            options (dict): Custom module options

        Attributes:
            encoder (StringEncoder):   Instance of StringEncoder (Python2/3 str/byte compat)
            executable (str):          Executable to run
            proc (subprocess.Popen):   subprocess.Popen instance
        """
        # Define destructor
        atexit.register(self.__destruct__)
        super(NetcatPluginCommand, self).__init__(options)

        assert "encoder" in options
        assert "executable" in options

        self.log = logging.getLogger(__name__)
        self.enc = options["encoder"]  # type: StringEncoder
        self.executable = options["executable"]
        self.log.debug("Setting '%s' as executable", self.executable)

        # Open executable to wait for commands
        env = os.environ.copy()
        try:
            self.proc = Popen(
                # TODO: should I also use 'bufsize=1'?
                self.executable,
                stdin=PIPE,
                stdout=PIPE,
                stderr=STDOUT,
                shell=False,
                env=env,
            )
        except FileNotFoundError:
            self.log.error("Specified executable '%s' not found", self.executable)
            sys.exit(1)
        # Python-2 compat (doesn't have FileNotFoundError)
        except OSError:
            self.log.error("Specified executable '%s' not found", self.executable)
            sys.exit(1)

    def __destruct__(self):
        # type: () -> None
        """Destructor."""
        self.log.trace(  # type: ignore
            "Killing executable: %s with pid %d", self.executable, self.proc.pid
        )
        self.proc.kill()

    def __set_input_timeout(self, timeout=0.1):
        # type: (float) -> None
        """Throw a TimeOutError Exception for sys.stdin (Linux only)."""
        # select((rlist, wlist, xlist, timeout)
        # rlist: wait until ready for reading
        # wlist: wait until ready for writing
        # xlist: wait for an "exceptional condition"
        if not select.select([self.proc.stdout], [], [], timeout)[0]:
            raise TimeoutError("timed out")

    # ------------------------------------------------------------------------------
    # Public Functions
    # ------------------------------------------------------------------------------
    def interrupt(self):
        # type: () -> None
        """Stop function that can be called externally to close this instance."""
        self.log.trace(  # type: ignore
            "[NetcatPluginCommand] subprocess.kill() was raised by input_unterrupter()"
        )
        self.proc.kill()

    def producer(self, ssig):
        # type: (StopSignal) -> Iterator[str]
        """Constantly ask for input.

        Args:
            ssig (StopSignal): A StopSignal instance providing has_stop() and raise_stop() functions

        Yields:
            str: Data received from command output
        """
        assert self.proc.stdout is not None
        while True:
            if ssig.has_stop():
                self.log.trace("Stop signal acknowledged in Command")  # type: ignore
                return
            self.log.trace("Reading command output")  # type: ignore
            # TODO: non-blocking read does not seem to work or?
            # try:
            # self.__set_input_timeout(timeout=1.5)
            data = self.proc.stdout.readline()  # Much better performance than self.proc.read(1)
            # except TimeoutError:
            #    if THREAD_TERMINATE:
            #        return
            #    # No input, just check again
            #    #self.proc.stdout.flush()
            #    continue
            self.log.trace("Command output: %s", repr(data))  # type: ignore
            if not data:
                self.log.trace("Command output was empty. Exiting loop.")  # type: ignore
                break
            yield self.enc.decode(data)

    def consumer(self, data):
        # type: (str) -> None
        """Send data received to stdin (command input).

        Args:
            data (str): Command to execute.
        """
        assert self.proc.stdin is not None
        byte = self.enc.encode(data)
        self.log.trace("Appending to stdin: %s", repr(byte))  # type: ignore
        self.proc.stdin.write(byte)
        self.proc.stdin.flush()


# #################################################################################################
# #################################################################################################
# ###
# ###   3 / 3   N E T C A T   P L U G I N   R U N N E R
# ###
# #################################################################################################
# #################################################################################################

# -------------------------------------------------------------------------------------------------
# CLASS: Runner
# -------------------------------------------------------------------------------------------------
class StopSignal(object):
    """Provide a simple boolean switch."""

    def __init__(self):
        # type: () -> None
        """Create a StopSignal instance."""
        self.__stop = False

    def has_stop(self):
        # type: () -> bool
        """Check if a stop signal has been raised."""
        return self.__stop

    def raise_stop(self):
        # type: () -> None
        """Raise a stop signal."""
        self.__stop = True


class Runner(object):
    """Runner class that takes care about putting everything into threads."""

    # Dict of producer/consumer action to run in a thread.
    # Each list item will be run in its own thread
    # {
    #   "name": {
    #     {
    #       "producer": "function",  # A func which yields data
    #       "consumer": "function",  # A callback func to process the data
    #       "signal": "StopSignal",  # An instance providing has_stop() and raise_stop()
    #       "interrupts": ["func"],  # List of funcs to be called outside the thread to stop it
    #   }
    # }
    __actions = {}  # type: Dict[str, Dict[str, Any]]

    # Dict of timed function definition to run in a thread.
    # Each list item will be run in its own thread.
    # {
    #   "name": {
    #     {
    #       "action": "function",   # A func which does any action
    #       "intvl": "function",    # The action func will be called at this intervall
    #       "args": "function",     # args for the action func
    #       "kwargs": "function",   # kwargs for the action func
    #       "signal": "StopSignal", # An instance providing has_stop() and raise_stop()
    #   }
    # }
    __timers = {}  # type: Dict[str, Dict[str, Any]]

    # A dict which holds the threads created from actions.
    # The name is based on the __actions name
    # {"name": "<thread>"}
    __threads = {}  # type: Dict[str, threading.Thread]

    # ------------------------------------------------------------------------------
    # Constructor / Destructor
    # ------------------------------------------------------------------------------
    def __init__(self):
        # type: () -> None
        """Create a new Runner object."""
        self.log = logging.getLogger(__name__)

    # ------------------------------------------------------------------------------
    # Public Functions
    # ------------------------------------------------------------------------------
    def add_action(self, action):
        # type: (Dict[str, Any]) -> None
        """Add a function to the producer/consumer thread pool runner.

        Args:
            action (dict): A dictionary of producer, consumer and interrupt funcs
        """
        assert "name" in action
        assert "producer" in action
        assert "consumer" in action
        assert "signal" in action
        assert "interrupts" in action
        self.__actions[action["name"]] = {
            "name": action["name"],
            "producer": action["producer"],
            "consumer": action["consumer"],
            "signal": action["signal"],
            "interrupts": action["interrupts"],
        }

    def add_timer(self, timer):
        # type: (Dict[str, Any]) -> None
        """Add a function to the timer thread pool runner.

        Args:
            timer (dict): A dictionary timer functions.
        """
        self.__timers[timer["name"]] = {
            "action": timer["action"],
            "intvl": timer["intvl"],
            "args": timer["args"] if "args" in timer else None,
            "kwargs": timer["kwargs"] if "kwargs" in timer else {},
            "signal": timer["signal"],
        }

    def run(self):
        # type: () -> None
        """Run threaded NetCat."""

        def run_action(name, producer, consumer, ssig):
            # type: (str, Callable[[StopSignal], str], Callable[[str], None],StopSignal) -> None
            """Producer/consumer run function to be thrown into a thread.

            Args:
                name (str):          Name for logging output
                producer (function): A generator function which yields data
                consumer (function): A callback which consumes data from the generator
                ssig (StopSignal):   Providing has_stop() and raise_stop()
            """
            self.log.trace("[%s] Producer Start", name)  # type: ignore
            for data in producer(ssig):
                self.log.trace("[%s] Producer received: %s", name, repr(data))  # type: ignore
                consumer(data)
            self.log.trace("[%s] Producer Stop", name)  # type: ignore

        def run_timer(name, action, intvl, ssig, *args, **kwargs):
            # type: (str, Callable[..., None], int, StopSignal, Any, Any) -> None
            """Timer run function to be thrown into a thread (Execs periodic tasks).

            Args:
                name (str):        Name for logging output
                action (function): Function to be called in a given intervall
                intvl (float):     Intervall at which the action function will be called
                ssig (StopSignal): Providing has_stop() and raise_stop()
                args (*args):      *args for action func
                kwargs (**kwargs): **kwargs for action func
            """
            self.log.trace("[%s] Timer Start (exec every %f sec)", name, intvl)  # type: ignore
            time_last = int(time.time())
            while True:
                if ssig.has_stop():
                    self.log.trace("Stop signal acknowledged for timer %s", name)  # type: ignore
                    return
                time_now = int(time.time())
                if time_now > time_last + intvl:
                    self.log.debug("[%s] Executing timed function", time_now)
                    # TODO: The following if/else could probably be simplified somehow
                    if args is not None:
                        if kwargs:
                            action(*args, **kwargs)
                        else:
                            action(*args)
                    else:
                        if kwargs:
                            action(**kwargs)
                        else:
                            action()
                    time_last = time_now  # Reset previous time
                time.sleep(0.1)

        # Start available action in a thread
        for key in self.__actions:
            # Create Thread object
            thread = threading.Thread(
                target=run_action,
                name=key,
                args=(
                    key,
                    self.__actions[key]["producer"],
                    self.__actions[key]["consumer"],
                    self.__actions[key]["signal"],
                ),
            )
            thread.daemon = False
            thread.start()
            self.__threads[key] = thread
        # Start available timers in a thread
        for key in self.__timers:
            # Create Thread object
            thread = threading.Thread(
                target=run_timer,
                name=key,
                args=(
                    key,
                    self.__timers[key]["action"],
                    self.__timers[key]["intvl"],
                    self.__timers[key]["signal"],
                    self.__timers[key]["args"],
                ),
                kwargs=self.__timers[key]["kwargs"],
            )
            thread.daemon = False
            thread.start()

        def check_stop(force):
            # type: (int) -> bool
            """Stop threads."""
            for key in self.__threads:
                if not self.__threads[key].is_alive() or force:
                    # [1/3] Inform all threads (inside) about a stop signal.
                    # All threads with non-blocking funcs will be able to stop themselves
                    self.log.trace(  # type: ignore
                        "Raise stop signal: %s.%s() for %s",
                        self.__actions[key]["signal"].raise_stop.__self__.__class__.__name__,
                        self.__actions[key]["signal"].raise_stop.__name__,
                        self.__threads[key].getName(),
                    )
                    self.__actions[key]["signal"].raise_stop()
                    # [2/3] Call external interrupters
                    # These will shutdown all blocking functions inside a thread,
                    # so that they are actually able to join
                    for interrupt in self.__actions[key]["interrupts"]:
                        self.log.trace(  # type: ignore
                            "Call INTERRUPT: %s.%s() for %s",
                            interrupt.__self__.__class__.__name__,
                            interrupt.__name__,
                            self.__threads[key].getName(),
                        )
                        interrupt()
                    # [3/3] All blocking events inside the threads are gone, now join them
                    self.log.trace("Joining %s", self.__threads[key].getName())  # type: ignore
                    self.__threads[key].join(timeout=0.1)
            # If all threads have died or force is requested, then exit
            if not all([self.__threads[key].is_alive() for key in self.__threads]) or force:
                return True
            return False

        try:
            while True:
                if check_stop(False):
                    sys.exit(0)
                # Need a timeout to not skyrocket the CPU
                time.sleep(0.1)
        except KeyboardInterrupt:
            print()
            check_stop(True)
            sys.exit(1)


# -------------------------------------------------------------------------------------------------
# COMMAND LINE ARGUMENTS
# -------------------------------------------------------------------------------------------------
def get_version():
    # type: () -> str
    """Return version information."""
    return """%(prog)s: Version %(version)s (%(url)s) by %(author)s""" % (
        {"prog": APPNAME, "version": VERSION, "url": APPREPO, "author": "cytopia"}
    )


def _args_check_color(value):
    # type: (str) -> str
    if value not in ["auto", "always", "never"]:
        raise argparse.ArgumentTypeError("%s is an invalid color definition" % value)
    return value


def _args_check_port(value):
    # type: (str) -> int
    """Check arguments for invalid port number."""
    min_port = 1
    max_port = 65535
    intvalue = int(value)

    if intvalue < min_port or intvalue > max_port:
        raise argparse.ArgumentTypeError("%s is an invalid port number" % value)
    return intvalue


def _args_check_forwards(value):
    # type: (str) -> str
    """Check forward argument (-L/-R) for correct pattern."""
    match = re.search(r"(.+):(.+)", value)
    if match is None or len(match.groups()) != 2:
        raise argparse.ArgumentTypeError("%s is not a valid 'addr:port' format." % value)
    _args_check_port(match.group(2))
    return value


def _args_check_rebind(value):
    # type: (str) -> int
    """Check rebind argument for correct value."""
    intvalue = int(value)
    if intvalue < 0:
        raise argparse.ArgumentTypeError("must be equal or greater than 0. Got: %s" % value)
    return intvalue


def _args_check_reconn(value):
    # type: (str) -> int
    """Check reconn argument for correct value."""
    intvalue = int(value)
    if intvalue < 0:
        raise argparse.ArgumentTypeError("must be equal or greater than 0. Got: %s" % value)
    return intvalue


def _args_check_robin_ports(value):
    # type: (str) -> List[int]
    """Check round-robin argument for comma separated string or range."""
    mcomma = re.search(r"^[0-9]+(,([0-9]+))*$", value)
    mrange = re.search(r"^[0-9]+\-[0-9]+$", value)

    if mcomma is None:
        if mrange is None:
            raise argparse.ArgumentTypeError("%s is not a valid port specifier" % value)

    if mcomma:
        ports = [int(port) for port in mcomma.group(0).split(",")]
        for port in ports:
            _args_check_port(str(port))
        return ports

    if mrange:
        ranges = [int(r) for r in mrange.group(0).split("-")]
        if ranges[0] >= (ranges[1] + 1):
            raise argparse.ArgumentTypeError(
                "Left side of range must be smaller or equal than right side."
            )
        ports = []
        for port in range(ranges[0], ranges[1] + 1):
            print(type(port))
            _args_check_port(str(port))
            ports.append(port)
        return ports

    return []


def _args_check_mutually_exclusive(parser, args):
    # type: (argparse.ArgumentParser, argparse.Namespace) -> None
    """Check mutually exclusive arguments."""
    # This is connect mode
    connect_mode = not args.listen and not args.zero and not args.local and not args.remote

    # [MODE] --listen
    if args.listen and (args.zero or args.local or args.remote):
        parser.print_usage()
        print(
            "%s: error: -l/--listen mutually excl. with -z/-zero, -L or -R" % (APPNAME),
            file=sys.stderr,
        )
        sys.exit(1)

    # [MODE] --zero
    if args.zero and (args.listen or args.local or args.remote):
        parser.print_usage()
        print(
            "%s: error: -z/--zero mutually excl. with -l/--listen, -L or -R" % (APPNAME),
            file=sys.stderr,
        )
        sys.exit(1)

    # [MODE --local
    if args.local and (args.listen or args.zero or args.remote):
        parser.print_usage()
        print(
            "%s: error: -L/--local mutually excl. with -l/--listen, -z/--zero or -R" % (APPNAME),
            file=sys.stderr,
        )
        sys.exit(1)

    # [MODE --remote
    if args.remote and (args.listen or args.zero or args.local):
        parser.print_usage()
        print(
            "%s: error: -R/--remote mutually excl. with -l/--listen, -z/--zero or -L" % (APPNAME),
            file=sys.stderr,
        )
        sys.exit(1)

    # [MODULE] --exec
    if args.cmd and (args.local or args.remote or args.zero):
        parser.print_usage()
        print(
            "%s: error: -e/--exec mutually excl. with -L, -R or -z/--zero" % (APPNAME),
            file=sys.stderr,
        )
        sys.exit(1)

    # [OPTIONS] --udp
    if args.udp and args.zero:
        parser.print_usage()
        print(
            "%s: error: -u/--udp mutually excl. with -z/--zero" % (APPNAME), file=sys.stderr,
        )
        sys.exit(1)

    # [ADVANCED] --http
    if args.http and (args.https or args.udp or args.zero):
        parser.print_usage()
        print(
            "%s: error: --http mutually excl. with --https, -u/--udp or -z/--zero" % (APPNAME),
            file=sys.stderr,
        )
        sys.exit(1)

    # [ADVANCED] --https
    if args.https and (args.http or args.udp or args.zero):
        parser.print_usage()
        print(
            "%s: error: --https mutually excl. with --http, -z/--udp or -z/--zero" % (APPNAME),
            file=sys.stderr,
        )
        sys.exit(1)

    # [ADVANCED] --keep-open
    if args.keep_open and (args.udp):
        parser.print_usage()
        print(
            "%s: error: --keep-open mutually excl. with -u/--udp" % (APPNAME), file=sys.stderr,
        )
        sys.exit(1)
    if args.keep_open and not args.listen:
        parser.print_usage()
        print(
            "%s: error: --keep-open only works with -l/--listen" % (APPNAME), file=sys.stderr,
        )
        sys.exit(1)

    # [ADVANCED] --rebind
    if args.rebind is not False and not args.listen:
        parser.print_usage()
        print(
            "%s: error: --rebind only works with -l/--listen" % (APPNAME), file=sys.stderr,
        )
        sys.exit(1)

    # [ADVANCED] --reconn
    if args.reconn is not False and (args.udp):
        parser.print_usage()
        print(
            "%s: error: --reconn mutually excl. with -u/--udp" % (APPNAME), file=sys.stderr,
        )
        sys.exit(1)
    if args.reconn is not False and not (connect_mode or args.zero):
        parser.print_usage()
        print(
            "%s: error: --reconn only works with connect mode or -z/--zero" % (APPNAME),
            file=sys.stderr,
        )
        sys.exit(1)

    # [ADVANCED] --wait
    if args.wait is not False and args.udp:
        parser.print_usage()
        print(
            "%s: error: -w/--wait mutually excl. with -u/--udp" % (APPNAME), file=sys.stderr,
        )
        sys.exit(1)
    if args.wait is not False and not connect_mode:
        parser.print_usage()
        print(
            "%s: error: -w/--wait only works with connect mode" % (APPNAME), file=sys.stderr,
        )
        sys.exit(1)

    # [ADVANCED] --udp-ping-init
    if args.udp_ping_init is not False and (args.listen or args.local):
        parser.print_usage()
        print(
            "%s: error: --udp-ping-init mutually excl. with -l/--listen or -L/--local" % (APPNAME),
            file=sys.stderr,
        )
        sys.exit(1)
    if args.udp_ping_init is not False and not args.udp:
        parser.print_usage()
        print(
            "%s: error: --udp-ping-init only works with -u/--udp" % (APPNAME), file=sys.stderr,
        )
        sys.exit(1)


def get_args():
    # type: () -> argparse.Namespace
    """Retrieve command line arguments."""
    parser = argparse.ArgumentParser(
        formatter_class=argparse.RawTextHelpFormatter,
        add_help=False,
        usage="""%(prog)s [-Cnuv] [-e cmd] hostname port
       %(prog)s [-Cnuv] [-e cmd] -l [hostname] port
       %(prog)s [-Cnuv] -z hostname port
       %(prog)s [-Cnuv] -L addr:port hostname port
       %(prog)s [-Cnuv] -R addr:port hostname port
       %(prog)s -V, --version
       %(prog)s -h, --help
       """
        % ({"prog": APPNAME}),
        description="""
Enhanced and comptaible Netcat implementation written in Python (2 and 3) with
connect, zero-i/o, listen and forward modes and techniques to detect and evade
firewalls and intrusion detection/prevention systems.

If no mode arguments are specified, pwncat will run in connect mode and act as
a client to connect to a remote endpoint. If the connection to the remote
endoint is lost, pwncat will quit. See advanced options for how to automatically
reconnect.""",
    )

    positional = parser.add_argument_group("positional arguments")
    mode = parser.add_argument_group("mode arguments")
    optional = parser.add_argument_group("optional arguments")
    advanced = parser.add_argument_group("advanced arguments")
    misc = parser.add_argument_group("misc arguments")

    positional.add_argument(
        "hostname", nargs="?", type=str, help="Address to listen, forward or connect to"
    )
    positional.add_argument(
        "port", type=_args_check_port, help="Port to listen, forward or connect to"
    )

    mode.add_argument(
        "-l",
        "--listen",
        action="store_true",
        default=False,
        help="""[Listen mode]:
Start a server and listen for incoming connections.
If using TCP and a connected client disconnects or the
connection is interrupted otherwise, the server will
quit. See -k/--keep-open to change this behaviour.

""",
    )
    mode.add_argument(
        "-z",
        "--zero",
        action="store_true",
        default=False,
        help="""[Zero-I/0 mode]:
Connect to a remote endpoint and report status only.
Used for port scanning.

""",
    )
    mode.add_argument(
        "-L",
        "--local",
        metavar="addr:port",
        default=False,
        type=_args_check_forwards,
        help="""[Local forward mode]:
This mode will start a server and a client internally.
The internal server will listen locally on specified
hostname/port (positional arguments). Same as with -l.
The server will then forward traffic to the internal
client which connects to another server specified by
address given via -L/--local addr:port.
(I.e.: proxies a remote service to a local address)

""",
    )
    mode.add_argument(
        "-R",
        "--remote",
        metavar="addr:port",
        default=False,
        type=_args_check_forwards,
        help="""[Remote forward mode]:
This mode will start two clients internally. One is
connecting to the target and one is connecting to
another pwncat/netcat server you have started some-
where. Once connected, it will then proxy traffic
between you and the target.
This mode should be applied on machines that block
incoming traffic and only allow outbound.
The connection to your listening server is given by
-R/--remote addr:port and the connection to the
target machine via the positional arguments.
""",
    )
    optional.add_argument(
        "-e",
        "--exec",
        metavar="cmd",
        dest="cmd",
        default=False,
        type=str,
        help="Execute shell command. Only for connect or listen mode.",
    )
    optional.add_argument(
        "-C",
        "--crlf",
        action="store_true",
        default=False,
        help="Replace LF with CRLF from stdin (default: don't)",
    )
    optional.add_argument(
        "-n", "--nodns", action="store_true", default=False, help="Do not resolve DNS.",
    )
    optional.add_argument(
        "-u",
        "--udp",
        action="store_true",
        default=False,
        help="Use UDP for the connection instead of TCP.",
    )
    optional.add_argument(
        "-v",
        "--verbose",
        action="count",
        default=0,
        help="""Be verbose and print info to stderr. Use -v, -vv, -vvv
or -vvvv for more verbosity. The server performance will
decrease drastically if you use more than three times.""",
    )
    optional.add_argument(
        "-c",
        "--color",
        metavar="str",
        default="auto",
        type=_args_check_color,
        help="""Colored log output. Specify 'always', 'never' or 'auto'.
In 'auto' mode, color is displayed as long as the output
goes to a terminal. If it is piped into a file, color
will automatically be disabled. This mode also disables
color on Windows by default. (default: auto)
""",
    )

    advanced.add_argument(
        "--http",
        action="store_true",
        default=False,
        help="""Connect / Listen / Local forward mode (TCP only):
Hide traffic in http packets to fool Firewalls/IDS/IPS.

""",
    )
    advanced.add_argument(
        "--https",
        action="store_true",
        default=False,
        help="""Connect / Listen / Local forward mode (TCP only):
Hide traffic in https packets to fool Firewalls/IDS/IPS.

""",
    )
    advanced.add_argument(
        "-k",
        "--keep-open",
        action="store_true",
        default=False,
        help="""Listen mode (TCP only):
Re-accept new clients in listen mode after a client has
disconnected or the connection is unterrupted otherwise.
(default: server will quit after connection is gone)

""",
    )
    advanced.add_argument(
        "--rebind",
        metavar="x",
        default=False,
        type=_args_check_rebind,
        help="""Listen mode (TCP and UDP):
If the server is unable to bind, it will re-initialize
itself x many times before giving up. Use 0 to re-init
endlessly. (default: fail after first unsuccessful try).

""",
    )
    advanced.add_argument(
        "--rebind-wait",
        metavar="s",
        default=1,
        type=int,
        help="""Listen mode (TCP and UDP):
Wait x seconds between re-initialization. (default: 1)

""",
    )
    advanced.add_argument(
        "--rebind-robin",
        metavar="port",
        default=[],
        type=_args_check_robin_ports,
        help="""Listen mode (TCP and UDP):
If the server is unable to initialize (e.g: cannot bind
and --rebind is specified, it it will shuffle ports in
round-robin mode to bind to. Use comma separated string
such as '80,81,82' or a range of ports '80-100'.
Set --rebind to at least the number of ports to probe +1
This option requires --rebind to be specified.

""",
    )
    advanced.add_argument(
        "--reconn",
        metavar="x",
        default=False,
        type=_args_check_reconn,
        help="""Connect mode / Zero-I/O mode (TCP only):
If the remote server is not reachable or the connection
is interrupted, the client will connect again x many
times before giving up. Use 0 to retry endlessly.
(default: quit if the remote is not available or the
connection was interrupted)
This might be handy for stable TCP reverse shells ;-)

""",
    )
    advanced.add_argument(
        "--reconn-wait",
        metavar="s",
        default=1,
        type=int,
        help="""Connect mode / Zero-I/O mode (TCP only):
Wait x seconds between re-connects. (default: 1)

""",
    )
    advanced.add_argument(
        "--reconn-robin",
        metavar="port",
        default=[],
        type=_args_check_robin_ports,
        help="""Connect mode / Zero-I/O mode (TCP only):
If the remote server is not reachable or the connection
is interrupted and --reconn is specified, the client
will shuffle ports in round-robin mode to connect to.
Use comma separated string such as '80,81,82' or a range
of ports '80-100'.
Set --reconn to at least the number of ports to probe +1
This helps reverse shell to evade intrusiona prevention
systems that will cut your connection and block the
outbound port.
This is also useful in Connect or Zero-I/O mode to
figure out what outbound ports are allowed.

""",
    )
    advanced.add_argument(
        "-w",
        "--wait",
        metavar="s",
        default=False,
        type=int,
        help="""Connect mode (TCP only):
If a connection and stdin are idle for more than s sec,
then the connection is silently closed and the client
will exit. (default: wait forever).
Note: if --reconn is specified, the connection will be
re-opened.

""",
    )
    advanced.add_argument(
        "--udp-ping-init",
        action="store_true",
        default=False,
        help="""Connect mode / Zero-I/O mode (UDP only):
UDP is a stateless protocol unlike TCP, so no hand-
shake communication takes place and the client just
sends data to a server without being "accepted" by
the server first.
This means a server waiting for an UDP client to
connect to, is unable to send any data to the client,
before the client hasn't send data first. The server
simply doesn't know the IP address before an initial
connect.
The --udp-ping-init option instructs the client to send
one single initial ping packet to the server, so that it
is able to talk to the client.
This is the only way to make a UDP reverse shell work.
See --udp-ping-word for what char/string to send as
initial ping packet (default: '\\0')

""",
    )
    advanced.add_argument(
        "--udp-ping-intvl",
        metavar="s",
        default=False,
        type=int,
        help="""Connect mode / Zero-I/O mode (UDP only):
Instruct the UDP client to send ping intervalls every
s seconds. This allows you to restart your UDP server
and just wait for the client to report back in.
This might be handy for stable UDP reverse shells ;-)
See --udp-ping-word for what char/string to send as
initial ping packet (default: '\\0')

""",
    )
    advanced.add_argument(
        "--udp-ping-word",
        metavar="str",
        default="\0",
        type=str,
        help="""Connect mode / Zero-I/O mode (UDP only):
Change the default character '\\0' to use for upd ping.
Single character or strings are supported.

""",
    )
    advanced.add_argument(
        "--udp-ping-robin",
        metavar="port",
        default=[],
        type=_args_check_robin_ports,
        help="""Zero-I/O mode (UDP only):
Instruct the UDP client to shuffle the specified ports
in round-robin mode for a remote server to ping.
This might be handy to scan outbound allowed ports.
Use --udp-ping-intvl 0 to be faster.

""",
    )
    advanced.add_argument(
        "--safe-word",
        metavar="str",
        default=False,
        type=str,
        help="""All modes:
If %(prog)s is started with this argument, it will shut
down as soon as it receives the specified string. The
--keep-open (server) or --reconn (client) options will
be ignored and it won't listen again or reconnect to you.
Use a very unique string to not have it shut down
accidentally by other input.
"""
        % ({"prog": APPNAME}),
    )
    misc.add_argument("-h", "--help", action="help", help="Show this help message and exit")
    misc.add_argument(
        "-V",
        "--version",
        action="version",
        version=get_version(),
        help="Show version information and exit",
    )

    # Retrieve arguments
    args = parser.parse_args()

    # Check mutually exclive arguments
    _args_check_mutually_exclusive(parser, args)

    # Connect mode and Zero-I/O mode require hostname and port to be set
    connect_mode = not (args.listen or args.zero or args.local or args.remote)
    if (connect_mode or args.zero) and not args.hostname:
        parser.print_usage()
        print(
            "%s: error: the following arguments are required: hostname" % (APPNAME),
            file=sys.stderr,
        )
        sys.exit(1)

    # Deny unimplemented modes
    if args.http or args.https or args.rebind or args.wait or args.udp_ping_init or args.safe_word:
        print("Unimplemented options", file=sys.stderr)
        sys.exit(1)

    return args


# -------------------------------------------------------------------------------------------------
# MAIN ENTRYPOINT
# -------------------------------------------------------------------------------------------------
def main():
    # type: () -> None
    """Run the program."""
    args = get_args()
    host = args.hostname
    port = args.port

    # Set pwncat options
    sock_opts = {
        "bufsize": 8192,
        "backlog": 0,
        "recv_timeout": TIMEOUT_RECV_SOCKET,
        "recv_retry": TIMEOUT_RECV_SOCKET_RETRY,
        "nodns": args.nodns,
        "udp": args.udp,
    }
    srv_opts = {
        "keep_open": args.keep_open,
        "rebind": True if (type(args.rebind) is int and args.rebind == 0) else args.rebind,
        "rebind_wait": args.rebind_wait,
        "rebind_robin": args.rebind_robin,
    }
    cli_opts = {
        "reconn": True if (type(args.reconn) is int and args.reconn == 0) else args.reconn,
        "reconn_wait": args.reconn_wait,
        "reconn_robin": args.reconn_wait,
    }
    # TODO:
    # "wait": args.wait,
    # "udp_ping_init": args.udp_ping_init,
    # "udp_ping_intvl": args.udp_ping_intvl,
    # "udp_ping_word": args.udp_ping_word,
    # "udp_ping_robing": args.udp_ping_robin,
    # "safe_word": args.safe_word,

    # Map pwncat verbosity to Python's Logger loglevel
    logmap = {
        0: logging.ERROR,
        1: logging.WARNING,
        2: logging.INFO,
        3: logging.DEBUG,
    }
    loglevel = logmap.get(args.verbose, TraceLogger.LEVEL_NUM)

    # Use a colored log formatter
    formatter = ColoredLogFormatter(args.color, loglevel)
    handler = logging.StreamHandler()
    handler.setLevel(loglevel)
    handler.setFormatter(formatter)

    # Use a custom logger with TRACE level
    logging.setLoggerClass(TraceLogger)
    logger = logging.getLogger(__name__)
    logger.setLevel(loglevel)
    logger.addHandler(handler)

    # Initialize encoder
    encoder = StringEncoder()

    # Use command modulde
    if args.cmd:
        module_opts = {
            "encoder": encoder,
            "executable": args.cmd,
        }
        mod = NetcatPluginCommand(module_opts)
    # Use output module
    else:
        module_opts = {
            "encoder": encoder,
            "crlf": args.crlf,
            "input_timeout": TIMEOUT_READ_STDIN,
        }
        mod = NetcatPluginOutput(module_opts)

    # Run local port-forward
    # -> listen locally and forward traffic to remote (connect)
    if args.local:
        ssig = StopSignal()
        # TODO: Make the listen address optional!
        srv_opts["keep_open"] = True
        cli_opts["reconn"] = True
        cli_opts["reconn_wait"] = 0
        lhost = args.local.split(":")[0]
        lport = int(args.local.split(":")[1])
        # Create listen and client instances
        net_srv = Pwncat(encoder, lhost, lport, "server", srv_opts, cli_opts, sock_opts)
        net_cli = Pwncat(encoder, host, port, "client", srv_opts, cli_opts, sock_opts)
        # Create Runner
        run = Runner()
        run.add_action(
            {
                "name": "TRANSMIT",
                "producer": net_srv.receive,  # USER sends data to PC-SERVER
                "consumer": net_cli.send,  # Data parsed on to PC-CLIENT to send to TARGET
                "signal": ssig,
                "interrupts": [net_cli.stop, net_srv.stop],
            }
        )
        run.add_action(
            {
                "name": "RECEIVE",
                "producer": net_cli.receive,  # Data comes back from TARGET to PC-CLIENT
                "consumer": net_srv.send,  # Data parsed on to PC-SERVER to back send to USER
                "signal": ssig,
                "interrupts": [net_cli.stop, net_srv.stop],
            }
        )
        run.run()
    # Run remote port-forward
    # -> connect to client, connect to target and proxy traffic in between.
    if args.remote:
        ssig = StopSignal()
        # TODO: Make the listen address optional!
        cli_opts["reconn"] = True
        cli_opts["reconn_wait"] = 0.2
        lhost = args.remote.split(":")[0]
        lport = int(args.remote.split(":")[1])
        # Create local and remote client
        net_cli_l = Pwncat(encoder, lhost, lport, "client", srv_opts, cli_opts, sock_opts)
        net_cli_r = Pwncat(encoder, host, port, "client", srv_opts, cli_opts, sock_opts)
        # Create Runner
        run = Runner()
        run.add_action(
            {
                "name": "TRANSMIT",
                "producer": net_cli_l.receive,  # USER sends data to PC-SERVER
                "consumer": net_cli_r.send,  # Data parsed on to PC-CLIENT to send to TARGET
                "signal": ssig,
                "interrupts": [],
            }
        )
        run.add_action(
            {
                "name": "RECEIVE",
                "producer": net_cli_r.receive,  # Data comes back from TARGET to PC-CLIENT
                "consumer": net_cli_l.send,  # Data parsed on to PC-SERVER to back send to USER
                "signal": ssig,
                "interrupts": [],
            }
        )
        run.run()
    # Run server
    if args.listen:
        ssig = StopSignal()
        net = Pwncat(encoder, host, port, "server", srv_opts, cli_opts, sock_opts)
        run = Runner()
        run.add_action(
            {
                "name": "RECV",
                "producer": net.receive,
                "consumer": mod.consumer,
                "signal": ssig,
                "interrupts": [net.stop, mod.interrupt],  # Also force the prod. to stop on net err
            }
        )
        run.add_action(
            {
                "name": "STDIN",
                "producer": mod.producer,
                "consumer": net.send,
                "signal": ssig,
                "interrupts": [mod.interrupt],  # Externally stop the produer itself
            }
        )
        run.run()

    # Run client
    else:
        ssig = StopSignal()
        net = Pwncat(encoder, host, port, "client", srv_opts, cli_opts, sock_opts)
        run = Runner()
        run.add_action(
            {
                "name": "RECV",
                "producer": net.receive,
                "consumer": mod.consumer,
                "signal": ssig,
                "interrupts": [net.stop, mod.interrupt],  # Also force the prod. to stop on net err
            }
        )
        run.add_action(
            {
                "name": "STDIN",
                "producer": mod.producer,
                "consumer": net.send,
                "signal": ssig,
                "interrupts": [net.stop, mod.interrupt],  # Externally stop the produer itself
            }
        )
        if type(args.udp_ping_intvl) is int and args.udp_ping_intvl > 0:
            run.add_timer(
                {
                    "name": "PING",
                    "action": net.send,
                    "intvl": args.udp_ping_intvl,
                    "args": ("\x00"),
                    "signal": ssig,
                }
            )
        run.run()


if __name__ == "__main__":
    # Catch Ctrl+c and exit without error message
    try:
        main()
    except KeyboardInterrupt:
        print()
        sys.exit(1)
