"""
Handles P2P connections.
All networking functions are ultimately done through
this class.
"""

import socket
import random
import select
import netifaces
import copy
from threading import Thread
import hashlib
import re
import netifaces
import signal
import os


from .upnp import *
from .nat_pmp import NatPMP
from .lib import *
from .sock import *
from .rendezvous_client import *
from .hybrid_reply import *
from .unl import UNL
from .dht_msg import DHT

# How many times a single message can be retransmitted.
max_retransmissions = 100

# Minimum time that must pass between retransmissions.
min_retransmit_interval = 5

# A theoretical time for a message to propagate across the network.
propagation_delay = 5

# A table of message hashes for received messages.
seen_messages = {}

# How often to get new DHT messages.
dht_msg_interval = 5

# How often to re-bootstrap.
rendezvous_interval = 30 * 60

# How often to re-advertise node.
# Update bootstrapping server every 10 minutes.
advertise_interval = 10 * 60

# Time that must elapse between accepting simultaneous opens.
sim_open_interval = 2

# Bootstrapping + TCP hole punching server.
rendezvous_servers = [
    {
        "addr": "158.69.201.105",
        "port": 8000
    },
    {
        "addr": "185.86.149.128",
        "port": 8000
    }
]

# Web server running script to test port forwarding.
forwarding_servers = [
    {
        "addr": "158.69.201.105",
        "port": 80,
        "url": "/pyp2p/net.php"
    }
] 

def is_msg_old(msg, record_seen=0):
    if type(msg) == str:
        msg = msg.encode("ascii")
    response_hash = hashlib.sha256(msg).hexdigest()
    if response_hash in seen_messages:
        seen = seen_messages[response_hash]
        elapsed = int(time.time()) - seen["last"]
        if elapsed < min_retransmit_interval:
            return 1

        if seen["times"] >= max_retransmissions:
            return 1

    if record_seen:
        record_msg_hash(msg)

    return 0

def record_msg_hash(msg):
    if type(msg) == str:
        msg = msg.encode("ascii")
    response_hash = hashlib.sha256(msg).hexdigest()

    if not is_msg_old(msg):
        timestamp = int(time.time())
        if response_hash in seen_messages:
            seen = seen_messages[response_hash]
            seen["times"] += 1
            seen["last"] = timestamp
        else:
            seen_messages[response_hash] = {
                "times": 1,
                "last": timestamp
            }

        return 1
    else:
        return 0

def clear_seen_messages():
    global seen_messages
    seen_messages = {}

class Net():
    def __init__(self, net_type="p2p", nat_type="unknown", node_type="unknown", max_outbound=10, max_inbound=10, passive_bind="0.0.0.0", passive_port=50500, interface="default", dht_node=None, error_log_path="error.log", debug=0):
        # List of outbound connections (from us, to another node.)
        self.outbound = []

        # List of inbound connections (to us, from another node.)
        self.inbound = []

        # Socket to receive inbound connections on.
        self.passive = None

        # Type of node: simultaneous, active, passive.
        self.node_type = node_type

        # NAT type: preserving, delta, reuse, random.
        self.nat_type = nat_type

        # Address to listen() on for inbound cons.
        self.passive_bind = passive_bind

        # Port to listen() on for inbound cons.
        self.passive_port = int(passive_port)

        # How many connections can we accept from other nodes?
        self.max_outbound = int(max_outbound)

        # How many connections can we make to other nodes?
        self.max_inbound = int(max_inbound)

        # List of servers to do port forwarding checks.
        self.forwarding_servers = forwarding_servers

        # Unix timestamp of last bootstrap.
        self.last_bootstrap = None

        # Unix timestamp of last DHT direct message.
        self.last_dht_msg = None

        # Unix timestamp of last advertise.
        self.last_advertise = None

        # What interface to make outbound connections from?
        self.interface = interface

        # Skip advertise if we have at least this many inbound connections.
        self.min_connected = 3

        # Unix timestamp of last simultaneous open challenge.
        self.last_passive_sim_open = 0

        # Does this Net instance need to bootstrap?
        self.enable_bootstrap = 1

        # Does this Net instance need to advertise?
        self.enable_advertise = 1

        # Should we try open ports?
        self.enable_forwarding = 1

        # Is simultaneous open enabled?
        self.enable_simultaneous = 1

        # Does this Net instance reject duplicate messages
        # (same hash as previous messages)?
        self.enable_duplicates = 1

        # Where should I store errors?
        self.error_log_path = error_log_path

        # Indicates port forwarding state.
        self.forwarding_type = "manual"

        # Debug mode shows debug messages.
        self.debug = debug

        # Network: p2p or direct.
        self.net_type = net_type

        # Rendezvous / boostrapping client.
        self.rendezvous = RendezvousClient(self.nat_type, rendezvous_servers=rendezvous_servers, interface=self.interface)

        # DHT node for receiving direct messages from other nodes.
        self.dht_node = dht_node

        # Node type details only known after network is start()ed.
        self.unl = None

        # List of connections that still need to respond to
        # our reverse query.
        self.pending_reverse_queries = []

        # Time frame for connection to respond to reverse query.
        self.reverse_query_expiry = 5

        # Enable more than one connection to the same IP.
        self.enable_duplicate_ip_cons = 0

        # Net instances hide their con details to prioritise direct cons.
        if self.net_type == "direct":
            self.disable_bootstrap()    
            self.disable_advertise()
            self.enable_duplicate_ip_cons = 1

        # Set to 1 when self.start() has been called.
        self.is_net_started = 0

    # Outputs debug statements if debug mode is enabled.
    def debug_print(self, msg):
        if self.debug:
            print("> " + str(msg))

    def disable_duplicates(self):
        self.enable_duplicates = 0

    def disable_bootstrap(self):
        self.enable_bootstrap = 0

    def disable_advertise(self):
        self.enable_advertise = 0
    
    def disable_simultaneous(self):
        self.enable_simultaneous = 0

    def disable_forwarding(self):
        self.enable_forwarding = 0

    def get_connection_no(self):
        return (len(self.outbound) + len(self.inbound))

    # Used to reject duplicate connections.
    def validate_node(self, node_ip, node_port=None, same_nodes=1):
        self.debug_print("Validating: " + node_ip)

        # Is this a valid IP?
        if not is_ip_valid(node_ip) or node_ip == "0.0.0.0":
            self.debug_print("Invalid node ip in validate node")
            return 0

        # Is this a valid port?
        if node_port != 0 and node_port != None:
            if not is_valid_port(node_port):
                self.debug_print("Invalid node port in validate port")
                return 0

        """
        Don't accept connections from self to passive server
        or connections to already connected nodes.
        """
        if not self.enable_duplicate_ip_cons:
            # Don't connect to ourself.
            if node_ip == "127.0.0.1" or node_ip == get_lan_ip(self.interface) or node_ip == get_wan_ip():
                self.debug_print("Cannot connect to ourself.")
                return 0

            # No, really: don't connect to ourself.
            if node_ip == self.passive_bind and node_port == self.passive_port:
                self.debug_print("Error connecting to same listen server.")
                return 0

            # Don't connect to same nodes.
            if same_nodes:
                for node in self.outbound + self.inbound:
                    try:
                        addr, port = node["con"].s.getpeername()
                        if node_ip == addr:
                            self.debug_print("Already connected to this node.")
                            return 0
                    except Exception as e:
                        print(e)
                        return 0
        
        return 1

    # Make an outbound con to a passive or simultaneous node.
    def add_node(self, node_ip, node_port, node_type, timeout=5):
        # Correct type for port.
        node_port = int(node_port)

        # Debug info.
        msg = "Attempting to connect to %s:%s:%s" % (node_ip, str(node_port), node_type)
        self.debug_print(msg)

        # Already connected to them.
        if not self.enable_duplicate_ip_cons:
            for node in self.outbound + self.inbound:
                if node_ip == node["ip"]:
                    self.debug_print("Already connected.")
                    return node["con"]

        # Avoid connecting to ourself.
        if not self.validate_node(node_ip, node_port):
            self.debug_print("Validate node failed.")
            return None
        
        # Make a simultaneous open connection.
        con = None
        if node_type == "simultaneous" and self.enable_simultaneous:
            # Check they've started net first
            # If they haven't we won't know the NAT details / node type.
            if not self.is_net_started:
                raise Exception("Make sure to start net before you add node.")

            if self.nat_type in self.rendezvous.predictable_nats:
                # Attempt to make active simultaneous connection.
                old_timeout = self.rendezvous.timeout
                try:
                    self.rendezvous.timeout = timeout
                    con = self.rendezvous.simultaneous_challenge(node_ip, node_port, "TCP")
                except Exception as e:
                    error = parse_exception(e)
                    log_exception(self.error_log_path, error)
                    return None
                self.rendezvous.timeout = old_timeout

                # Record node details and return con.
                self.rendezvous.simultaneous_cons = []
                if con != None:
                    node = {
                        "con": con,
                        "type": "simultaneous",
                        "ip": node_ip,
                        "port": 0
                    }
                    self.outbound.append(node)
                    self.debug_print("SUCCESS")
                else:
                    self.debug_print("FAILURE")

        # Passive outbound -- easiest to connect to.
        if node_type == "passive":
            try:
                # Try connect to passive server.
                con = Sock(node_ip, node_port, blocking=0, timeout=timeout, interface=self.interface)
                node = {
                    "con": con,
                    "type": "passive",
                    "ip": node_ip,
                    "port": node_port
                }
                self.outbound.append(node)
                self.debug_print("SUCCESS")
            except Exception as e:
                self.debug_print("FAILURE")
                error = parse_exception(e)
                self.debug_print(error)
                log_exception(self.error_log_path, error)
                return None

        # Filter(): Remove undesirable messages from replies.
        # Save message: 0 = no, 1 = yes.
        def filter_msg_check_builder():
            def filter_msg_check(msg):
                # Allow duplicate replies?
                record_seen = not self.enable_duplicates

                # Is this a reply to a reverse query?
                for reverse_query in self.pending_reverse_queries:
                    pattern  = "^REVERSE_ORIGIN:" + reverse_query["unl"]
                    pattern += "$"
                    if re.match(pattern, msg) != None:
                        self.pending_reverse_queries.remove(reverse_query)
                        return 0

                # Check if message is old.
                return not is_msg_old(msg, record_seen)

            return filter_msg_check

        # Patch sock object to reject duplicate replies.
        # (if enabled)
        con.reply_filter = filter_msg_check_builder()

        # Return new connection.
        return con

    def bootstrap(self):
        """
        When the software is first started, it needs to retrieve
        a list of nodes to connect to the network to. This function
        asks the server for N nodes which consists of at least N
        passive nodes and N simultaneous nodes. The simultaneous
        nodes are prioritized if the node_type for the machine
        running this software is simultaneous, with passive nodes
        being used as a fallback. Otherwise, the node exclusively
        uses passive nodes to bootstrap.

        This algorithm is designed to preserve passive node's
        inbound connection slots.
        """
        # Disable bootstrap.
        if not self.enable_bootstrap:
            return

        # Avoid raping the rendezvous server.
        t = time.time()
        if self.last_bootstrap != None:
            if t - self.last_bootstrap <= rendezvous_interval:
                return
        self.last_bootstrap = t
        self.debug_print("Searching for nodes to connect to.")

        try:
            connection_slots = self.max_outbound - (len(self.outbound))
            passive_outbound = []
            simultaneous_outbound = []
            if connection_slots > 0:
                # Connect to rendezvous server.
                rendezvous_con = Sock(self.rendezvous.rendezvous_servers[0]["addr"], self.rendezvous.rendezvous_servers[0]["port"], blocking=1, interface=self.interface, timeout=3)

                # Retrieve random nodes to bootstrap with.
                rendezvous_con.send_line("BOOTSTRAP " + str(self.max_outbound * 2))
                choices = rendezvous_con.recv_line(timeout=2)
                if choices == "NODES EMPTY":
                    rendezvous_con.close()
                    self.debug_print("Node list is empty.")
                    return
                else:
                    self.debug_print("Found node list.")

                # Parse node list.
                choices = re.findall("(?:(p|s)[:]([0-9]+[.][0-9]+[.][0-9]+[.][0-9]+)[:]([0-9]+))+\s?", choices)
                rendezvous_con.s.close()

                # Attempt to make active simultaneous connections.
                passive_nodes = []
                for node in choices:
                    # Out of connection slots.
                    if not connection_slots:
                        break

                    """
                    Note: This code has been disabled because bootstrapping with simultaneous nodes was never a good idea (plus this code is blocking.) Instead, TCP hole punching is reserved as a last resort.

                    # Attempt TCP hole punch (blocking.)
                    node_type, node_ip, node_port = node
                    if node_type == "s":
                        con = self.add_node(node_ip, node_port, "simultaneous")
                        if con != None:
                            connection_slots -= 1
                            self.debug_print("Con successful.")
                        else:
                            self.debug_print("Con failed.")
                    """

                    # Add to list of passive nodes.
                    if node_type == "p":
                        passive_nodes.append(node)

                # Use passive to make up the remaining cons.
                i = 0
                while i < len(passive_nodes) and connection_slots > 0:
                    node_type, node_ip, node_port = passive_nodes[i]
                    con = self.add_node(node_ip, node_port, "passive")
                    if con != None:
                        connection_slots -= 1
                        self.debug_print("Con successful.")
                    else:
                        self.debug_print("Con failed.")

                    i += 1

        except Exception as e:
            error = parse_exception(e)
            log_exception(self.error_log_path, error)

    def advertise(self):
        """
        This function tells the rendezvous server that our node is ready to accept connections from other nodes on the P2P network that run the bootstrap function. It's only used when net_type == p2p
        """

        # Advertise is disabled.
        if not self.enable_advertise:
            return

        # Direct net is reserved for direct connections only.
        if self.net_type == "direct":
            return

        # Net isn't started!.
        if not self.is_net_started:
            raise Exception("Please call start() before you call advertise()")

        # Avoid raping the rendezvous server with excessive requests.
        t = time.time()
        if self.last_advertise != None:
            if t - self.last_advertise <= advertise_interval:
                return

            if len(self.inbound) >= self.min_connected:
                return

        self.last_advertise = t

        # Tell rendezvous server to list us.
        try:
            # We're a passive node.
            if self.node_type == "passive" and self.passive_port != None and self.enable_advertise:
                self.rendezvous.passive_listen(self.passive_port, self.max_inbound)

            """
            Simultaneous open is only used as a fail-safe for connections to nodes on the direct_net and only direct_net can list itself as simultaneous so its safe to leave this enabled.
            """
            if self.node_type == "simultaneous":
                self.rendezvous.simultaneous_listen()
        except Exception as e:
            error = parse_exception(e)
            log_exception(self.error_log_path, error)

    def determine_node(self):
        """
        Determines the type of node based on a combination of forwarding reachability and NAT type.
        """

        # Manually set node_type as simultaneous.
        if self.node_type == "simultaneous":
            if self.nat_type != "unknown":
                return "simultaneous"

        # Get IP of binding interface.
        unspecific_bind = ["0.0.0.0", "127.0.0.1", "localhost"]
        if self.passive_bind in unspecific_bind:
            lan_ip = get_lan_ip(self.interface)
        else:
            lan_ip = self.passive_bind

        # Passive node checks.
        if lan_ip != None and self.passive_port != None and self.enable_forwarding:
            self.debug_print("Checking if port is forwarded.")

            # Check port isn't already forwarded.
            if is_port_forwarded(lan_ip, self.passive_port, "TCP", self.forwarding_servers):
                self.debug_print("Port already forwarded. Skipping NAT traversal.")

                self.forwarding_type = "forwarded"
                return "passive"
            else:
                self.debug_print("Port is not already forwarded.")

            # Most routers.
            try:
                self.debug_print("Trying UPnP")

                UPnP(self.interface).forward_port("TCP", self.passive_port, lan_ip)

                if is_port_forwarded(lan_ip, self.passive_port, "TCP", self.forwarding_servers):
                    self.forwarding_type = "UPnP"
                    self.debug_print("Forwarded port with UPnP.")
                else:
                    self.debug_print("UPnP failed to forward port.")

            except Exception as e:
                # Log exception.
                error = parse_exception(e)
                log_exception(self.error_log_path, error)
                self.debug_print("UPnP failed to forward port.")

                # Apple devices.
                try:
                    self.debug_print("Trying NATPMP.")
                    NatPMP(self.interface).forward_port("TCP", self.passive_port, lan_ip)
                    if is_port_forwarded(lan_ip, self.passive_port, "TCP", self.forwarding_servers):
                        self.forwarding_type = "NATPMP"
                        self.debug_print("Port forwarded with NATPMP.")
                    else:
                        self.debug_print("Failed to forward port with NATPMP.")
                        self.debug_print("Falling back on TCP hole punching or proxying.")
                except Exception as e:
                    # Log exception
                    error = parse_exception(e)
                    log_exception(self.error_log_path, error)
                    self.debug_print("Failed to forward port with NATPMP.")

            # Check it worked.
            if self.forwarding_type != "manual":
                return "passive"

        # Fail-safe node types.
        if self.nat_type != "unknown":
            return "simultaneous"
        else:
            return "active"

    # Receive inbound connections.
    def start_passive_server(self):
        self.passive = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.passive.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self.passive.bind((self.passive_bind, self.passive_port))
        self.passive.listen(self.max_inbound)

    def start(self):
        """
        This function determines node and NAT type, saves connectivity details, and starts any needed servers to be a part of the network. This is usually the first function called after initialising the Net class.
        """

        self.debug_print("Starting networking.")
        self.debug_print("Make sure to iterate over replies if you need connection alive management!")

        # Register a cnt + c handler
        signal.signal(signal.SIGINT, self.stop)

        # Check this netifaces version supports gateway function.
        # Older versions don't.
        try:
            netifaces.gateways()
        except AttributeError:
            raise Exception("You must be using at least version 0.10.0 of netifaces.")

        # Save WAN IP.
        self.debug_print("WAN IP = " + str(get_wan_ip()))

        # Check rendezvous server is up.
        try:
            rendezvous_con = Sock(self.rendezvous.rendezvous_servers[0]["addr"], self.rendezvous.rendezvous_servers[0]["port"], blocking=1, interface=self.interface, timeout=3)
            rendezvous_con.close()
        except:
            raise Exception("Unable to connect to rendezvous server.")

        # Determine NAT type.
        if self.nat_type == "unknown":
            self.debug_print("Determining NAT type.")
            nat_type = self.rendezvous.determine_nat()
            if nat_type != None and nat_type != "unknown":
                self.nat_type = nat_type
                self.rendezvous.nat_type = nat_type
                self.debug_print("NAT type = " + nat_type)
            else:
                self.debug_print("Unable to determine NAT type.")

        # Check NAT type if node is simultaneous
        # is manually specified.
        if self.node_type == "simultaneous":
            if self.nat_type not in self.rendezvous.predictable_nats:
                self.debug_print("Manual setting of simultanous specified but ignored since NAT does not support it.")
                self.node_type = "unknown"
        else:
            # Determine node type.
            self.debug_print("Determining node type.")
            self.start_passive_server()

            # No checks for manually specifying passive
            # (there probably should be.)
            if (self.node_type != "passive" and self.debug != 1) or self.node_type == "unknown":
                self.node_type = self.determine_node()
                if self.net_type == "p2p":
                    """
                    TCP hole punching is reserved specifically for direct networks (a net object reserved for receiving direct connections -- p2p is for connecting to the main network. The reason for this is you can't do multiple TCP hole punches at the same time so its reserved for direct network where it's most needed.
                    """
                    if self.node_type == "simultaneous":
                        self.node_type = "active"
                        self.disable_simultaneous()

        self.debug_print("Node type = " + self.node_type)

        # Close stray cons from determine_node() tests.
        self.close_cons()

        # Set net started status.
        self.is_net_started = 1

        # Initialise our UNL details.
        self.unl = UNL(self, self.dht_node)

    def stop(self, signum=None, frame=None):
        self.debug_print("Stopping networking.")

        if self.passive != None:
            self.passive.shutdown(1)
            self.passive.close()
            self.passive = None

        if self.last_advertise != None:
            self.rendezvous.leave_fight()

        for con in self:
            con.close()

        if signum != None:
            raise Exception("Process was interrupted.")

    # Return a connection by its IP.
    def con_by_ip(self, ip):
        for node in self.outbound + self.inbound:
            if node["ip"] == ip:
                return node["con"]

        return None
    
    # Send a message to all currently established connections.
    def broadcast(self, msg, source_con=None):
        for node in self.outbound + self.inbound:
            if node["con"] != source_con:
                node["con"].send_line(msg)

    def close_cons(self):
        # Close all connections.
        for node in self.inbound + self.outbound:
            node["con"].close()

        # Flush client queue for passive server.
        if self.node_type == "passive" and self.passive != None:
            self.passive.close()
            self.start_passive_server()

        # Start from scratch.
        self.inbound = []
        self.outbound = []

    def synchronize(self):
        # Clean up dead connections.
        for node_list_name in ["self.inbound", "self.outbound"]:
            node_list = eval(node_list_name)[:]
            for node in node_list:
                if not node["con"].connected:
                    self.debug_print("Removing disconnected: " + str(node))
                    eval(node_list_name).remove(node)

        # Timeout connections that haven't responded to reverse query.
        old_reverse_queries = []
        for reverse_query in self.pending_reverse_queries:
            duration = time.time() - reverse_query["timestamp"]
            if duration >= self.reverse_query_expiry:
                reverse_query["con"].close()
                old_reverse_queries.append(reverse_query)

        # Remove old reverse queries.
        for reverse_query in old_reverse_queries:
            self.pending_reverse_queries.remove(reverse_query)

        # Check for reverse connect requests.
        if self.dht_node != None and self.net_type == "direct":
            # Don't do this every synch cycle.
            t = time.time()
            skip_dht_check = 0
            if self.last_dht_msg != None:
                if t - self.last_dht_msg > dht_msg_interval:
                    skip_dht_check = 1

            if not skip_dht_check and self.dht_node.has_messages():
                dht_messages = []
                for msg in self.dht_node.get_messages():
                    # Found reverse connect request.
                    if re.match("^REVERSE_CONNECT:[a-zA-Z0-9+/-=_\s]+$", msg) != None:
                        call, their_unl = msg.split(":")

                        # Ask if the source sent it.
                        def success_builder():
                            def success(con):
                                # Did you send this?
                                con.send_line("REVERSE_QUERY:" + self.unl.value)

                                # Record pending query state.
                                query = {
                                    "unl": their_unl,
                                    "con": con,
                                    "timestamp": time.time()
                                }
                                self.pending_reverse_queries.append(query)

                            return success

                        self.unl.connect(their_unl, {"success": success_builder()}, reverse_connect=1)
                    else:
                        dht_messages.append(msg)

                # Put messages back.
                for msg in dht_messages:
                    self.debug_print(msg)
                    self.dht_node.messages_received.put_nowait(msg)

            self.last_dht_msg = t

        # Accept inbound connections.
        if len(self.inbound) < self.max_inbound:
            # Accept new passive inbound connections.
            if self.passive != None:
                r, w, e = select.select([self.passive], [], [], 0)
                for s in r:
                    if s == self.passive:
                        # Accept a new con from the listen queue.
                        client, address = self.passive.accept()
                        con = Sock()
                        con.set_sock(client)
                        node_ip, node_port = con.s.getpeername()

                        # Reject duplicate connections.
                        if self.validate_node(node_ip, node_port):
                            node = {
                                "type": "accept",
                                "con": con,
                                "ip": con.s.getpeername()[0],
                                "port": con.s.getpeername()[1],
                            }
                            self.inbound.append(node)
                            self.debug_print("Accepted new passive connection: " + str(node))
                        else:
                            con.close()

            # Accept new passive simultaneous connections.
            if self.node_type == "simultaneous":
                """
                This is basically the code that passive simultaneous
                nodes periodically call to parse any responses from the
                Rendezvous Server which should hopefully be new
                requests to initiate hole punching from active
                simultaneous nodes.

                If a challenge comes in, the passive simultaneous
                node accepts the challenge by giving details to the
                server for the challenging node (active simultaneous)
                to complete the simultaneous open.
                """

                # try:
                t = time.time()
                if self.rendezvous.server_con != None:
                    for reply in self.rendezvous.server_con:
                        # Reconnect.
                        if re.match("^RECONNECT$", reply) != None:
                            if self.enable_advertise:
                                self.rendezvous.simultaneous_listen()
                            continue

                        # Find any challenges.
                        # CHALLENGE 192.168.0.1 50184 50185 50186 50187 TCP
                        parts = re.findall("^CHALLENGE ([0-9]+[.][0-9]+[.][0-9]+[.][0-9]+) ((?:[0-9]+\s?)+) (TCP|UDP)$", reply)
                        if not len(parts):
                            continue
                        candidate_ip, candidate_predictions, candidate_proto = parts[0]

                        # Already connected.
                        if not self.validate_node(candidate_ip):
                            continue

                        # Last meeting was too recent.
                        if t - self.last_passive_sim_open < sim_open_interval:
                            continue

                        # Accept challenge.
                        our_ntp = get_ntp()
                        if our_ntp == None:
                            continue
                        msg = "ACCEPT %s %s TCP %s" % (candidate_ip, self.rendezvous.predictions, str(our_ntp))
                        ret = self.rendezvous.server_con.send_line(msg)
                        if not ret:
                            continue

                        """
                        Adding threading here doesn't work because Python's
                        fake threads and the act of starting a thread ruins
                        the timing between code synchronisation - especially
                        code running on the same host or in a LAN. Will
                        compensate by reducing the NTP delay to have the
                        meetings occur faster and setting a limit for meetings
                        to occur within the same period. 
                        """
                        # Walk to fight and return holes made.
                        self.last_passive_sim_open = t
                        con = self.rendezvous.attend_fight(self.rendezvous.mappings, candidate_ip, candidate_predictions, our_ntp)
                        if con != None:
                            node = {
                                "type": "simultaneous",
                                "con": con,
                                "ip": con.s.getpeername()[0],
                                "port": con.s.getpeername()[1],
                            }
                            self.inbound.append(node)

                        # Create new predictions ready to accept next client.
                        self.rendezvous.simultaneous_cons = []
                        if self.enable_advertise:
                            self.rendezvous.simultaneous_listen()

        # QUIT - remove us from bootstrapping server.
        if len(self.inbound) == self.max_inbound:
            try:
                # Remove advertise.
                self.rendezvous.leave_fight()
            except:
                pass

        # Bootstrap again if needed.
        self.bootstrap()

        # Relist node again if noded.
        self.advertise()
                    
    """
    These functions here make the class behave like a list. The
    list is a collection of connections (inbound) + (outbound.)
    Every iteration also has the bonus of reaping dead connections,
    making new ones (if needed), and accepting connections
    """
    def __len__(self):
        self.synchronize()
        return len(self.inbound) + len(self.outbound)

    def __iter__(self):
        # Process connections.
        self.synchronize()

        # Copy all connections to single buffer.
        cons = []
        for node in self.inbound:
            cons.append(node["con"])
        for node in self.outbound:
            cons.append(node["con"])

        # Return all cons.
        return iter(cons)

if __name__ == "__main__":

    """
    net = Net(debug=1)
    net.disable_bootstrap()
    net.disable_advertise()
    net.disable_forwarding()
    net.start()
    print(net.unl.value)
    print(net.unl.deconstruct(net.unl.value))

    while 1:
        time.sleep(0.5)
    """

    # Test simultaneous open.
    p2p_net = Net(debug=1, nat_type="preserving", node_type="simultaneous")
    p2p_net.start()
    p2p_net.disable_advertise()
    p2p_net.disable_bootstrap()
    # p2p_net.add_node("192.187.97.131", 0, "simultaneous") # Behind NAT
    # print(p2p_net.add_node("162.218.239.6", 0, "simultaneous")) # Not behind NAT

    def success_notify(con):
        print("SUCCESS THREADING.")


    #Test UNL
    events = {
        "success": success_notify
    }
    #print(p2p_net.unl.deconstruct("AQAAAAAAAAAAAAAAAAAAAAAAAAAAc2dtRMUG79qiBu/aos6tMVYAAAAAWMYQkz0OjrI="))
    #exit()


    #p2p_net.unl.connect("AQAAAAAAAAAAAAAAAAAAAAAAAAAAc2dtRMUG79qiBu/aos6tMVYAAAAAWMYQkz0OjrI=", events)

    while 1:
        time.sleep(0.5)

    exit()



    #P2P network example.
    p2p_net = Net(debug=1)
    p2p_net.start()
    p2p_net.bootstrap()
    p2p_net.advertise()

    #Event loop.
    while 1:
        for con in p2p_net:
            for reply in con:
                print(reply)

            """
            Excluses con from broadcast since we got this message from them
            """
            p2p_net.broadcast("Something.", con)

        time.sleep(0.5)

    #Direct network example.
    dht_node = DHT()
    direct_net = Net(dht_node=dht_node, debug=1)
    direct_net.start()

    #Connect to some UNL.
    def success(con):
        con.send_line("Thanks.")

    #Note: this isn't a valid UNL.
    #To get your UNL do: direct_net.unl.value.
    direct_net.unl.connect("Some guys UNL...", {"success": success})

