# -*- coding: iso-8859-1 -*-
"""IP functions and classes

Copyright © 2005-2012, Anders Andersen, University of Tromsų, Norway.
See http://www.cs.uit.no/~aa/dist/tools/noop/COPYING (../COPYING) for
details.


Provides some constants, functions, and the `IPaddr` class.

The `IPaddr` class is the high-level API of this library, and for most
users this is the only API used from this library.  The class provides
a constructor and three generators.  The class represents an IP
address, and the three generators genrates arguments to the socket
constructor, the socket function, and the socket bind function.  Each
call to the generators generate a unique key or id (a string), and one
set of argument (an instance of the `IPaddr` class can represent a
list of addresses where family, type, protocol and so on differs).

A small example follows.  The example performs almost no error
checking and should not be used as it is in a real application. The
server side of an TCP conection:

>>> # Create TCP/IP address (replace <host> with actual address/name)
>>> addr = IPaddr(<host>, 5678, stype=SOCK_STREAM)

>>> # Create sockets (both IPv4 and IPv6, if available)
>>> sock = {}
>>> for id, args in addr.socket_args():
>>>     sock[id] = socket(*args)

>>> # Bind and listen to each address
>>> for id, args in addr.bind_args():
>>>     sock[id].bind(*args)
>>>     sock[id].listen(1)

>>> # Any request?
>>> (isockets, o, e) = select(sock.values(), [], [])
>>> rsock, addr = isockets[0].accept()

>>> # Read message
>>> msg = rsock.recv(BUFSIZ)

The client side of this example:

>>> # Create TCP/IP address (replace <host> with remote address/name)
>>> addr = IPaddr(<host>, 5678, stype=SOCK_STREAM)

>>> # Create sockets (both IPv4 and IPv6 if available)
>>> sock = {}
>>> for id, args in addr.socket_args():
>>>     sock[id] = socket(*args)

>>> # Connect to remote socket
>>> connected = 0
>>> for id, args in addr.connect_args():
>>>     try:
>>>         sock[id].connect(*args)
>>>     except OSError:
>>>         continue
>>>     else:
>>>         connected = id
>>>         break

>>> # Send message
>>> if connected:
>>>     sock[connected].send("Hello")

"""


# CVS/RCS information

__file__    = r"$RCSfile: ip.py,v $"
__version__ = r"$Revision: 1.32 $"
__date__    = r"$Date: 2012-11-20 15:32:38 $"
__state__   = r"$State: Exp $"
__author__  = r"$Author: aa $"

__log__     = r"""

$Log: ip.py,v $
Revision 1.32  2012-11-20 15:32:38  aa
Removed debug messages.

Revision 1.31  2012-11-20 15:11:23  aa
Changed exception error to OSError (Python 3.3)

Revision 1.30  2012-10-09 10:56:28  aa
Removed a wasted `debugmsg()` statement.

Revision 1.29  2012-10-08 20:09:52  aa
Minor typo corrected.

Revision 1.28  2012-10-08 20:07:35  aa
Added copyright notice.

Revision 1.27  2011-10-25 15:19:47  aa
Added signatures to functions and methods.

Revision 1.26  2011-10-21 12:38:34  aa
Added a usage section and better comments in the code.

Revision 1.25  2011-10-18 20:39:12  aa
Corrected debug messages ib `fetchaddr`.

Revision 1.24  2011-10-18 20:30:23  aa
Updated for Python 3.

Revision 1.23  2005-03-22 17:18:31  aa
Added copyright notice.

Revision 1.22  2005/03/21 14:11:52  aa
Removed prefix argument from debugmsg (see debug module).

Revision 1.21  2005/03/21 12:38:23  aa
Added documentation of the arguments and return values.

Revision 1.20  2005/03/20 16:31:24  aa
Updated the code to match the style guide for python code [2].

Revision 1.19  2005/03/14 12:45:50  aa
Minor constant to string mapping update.

Revision 1.18  2005/03/14 12:39:05  aa
New approach used to create constant to string mappings.

Revision 1.17  2005/03/14 10:22:46  aa
Corrected usage of debugmsg.

Revision 1.16  2005/03/14 10:14:09  aa
Rewrote it to use new version of the debug module.

Revision 1.15  2005/03/12 16:26:32  aa
Upgraded debug functions (fdebug and mdebug, see module debug).

Revision 1.14  2005/03/10 19:52:02  aa
Added mapping from constants to names (stripcons + some functions).

Revision 1.13  2005/03/09 13:39:32  aa
Added debug information printing.

Revision 1.12  2005/03/09 01:34:06  aa
Added comments (argument description) to `fetchaddr`.

Revision 1.11  2005/03/07 01:12:08  aa
Added proto and flags argument to IPaddr and fetchaddr.

Revision 1.10  2005/03/06 09:49:37  aa
Added 'import sys' (see sys.exc_info).

Revision 1.9  2005/03/06 09:46:44  aa
Catch gaierror in fetchaddr.

Revision 1.8  2005/03/06 09:30:29  aa
Fixed problem with getaddrinfo returning single address on Windows.

Revision 1.7  2005/03/04 10:34:27  aa
Fixed tcpFlush so that the client closes the sockets before the server.

Revision 1.6  2005/03/03 20:42:56  aa
Do not enforce a given address family.

Revision 1.5  2005/03/03 14:22:00  aa
Removed timeout (use non-blocking in this rare case).

Revision 1.4  2005/03/03 13:27:43  aa
Fixed two way communication.

Revision 1.3  2005/03/03 13:13:39  aa
Improved how reuse of sockets are done.

Revision 1.2  2005/03/03 12:27:47  aa
Added comments and rcs information.

Revision 1.1  2005/03/03 12:27:38  aa
First ip implementation with examples.

"""

# [1] http://hea-www.harvard.edu/~fine/Tech/addrinuse.html
# [2] http://www.python.org/peps/pep-0008.html


# Standard Python libraries
from socket import *
from types import FunctionType, MethodType
import sys


# Load noop libraries (if available)
try:
    from noop.misc.debug import debugit, debugmsg
    from noop.core.signature import signature, one, opt
except ImportError:
    def signature(f): return f
    class one:
        def __init__(*args): pass
    opt = one
    try:
        from debug import *
    except ImportError:
        def debugit(obj): pass
        def debugmsg(msg, obj=None, prefix=True): pass


# Useful constants
UNUSED = 0
CONNECTED = 1
LOCAL = "localhost"
FAM = 0
TYPE = 1
PROTO = 2
CANON = 3
SOCKADDR = 4
CLOSETIMEOUT = 2.0
MYPORT = 3456
BUFSIZ = 1024


# Generates a dictionary of available constant-to-string mappings from
# address family, address type, and protocol type (use `dir()` since
# the socket library has been imported with `from socket import *`):
# >>> stripcons[FAM][AF_INET] == 'AF_INET'
stripcons = {
    FAM: dict(map(lambda y: (eval(y), y),
                  filter(lambda x: "AF_" in x, dir()))),
    TYPE: dict(map(lambda y: (eval(y), y),
                   filter(lambda x: "SOCK_" in x, dir()))),
    PROTO: dict(map(lambda y: (eval(y), y),
                    filter(lambda x: "IPPROTO_" in x, dir())))
}


@signature
def strgaiargs(args: (str, one(int, str), opt(int), opt(int), opt(int), opt(int))) -> str:
    """Convert `getaddrinfo` arguments to a string

    Convert `getaddrinfo` arguments to a string replacing constants with
    their names.

    args -- the argument tuple to `getaddrinfo`
    returns -- a string representation of the `getaddrinfo` arguments

    """

    # Number of arguments
    lnargs = len(args)

    # Node and port number
    strargs = "(" + repr(args[0]) + ", " + str(args[1]) + ", "

    # Remove trailing None and 0s (a hack?)
    for i in range(lnargs - 2):
        if (lnargs - i) == 6:
            if args[lnargs-(i+1)] == 0:
                args = args[:-1]
            else:
                break
        elif args[lnargs-(i+1)] == None:
            args = args[:-1]
        else:
            break

    # Do the mapping for the rest of the arguments (fam, stype, proto, flags)
    for i in range(len(args) - 2):
        if i < 3:
            try:
                strargs += (stripcons[i][args[i+2]] + ", ")
            except KeyError:
                strargs += (str(args[i+2]) + ", ")
        else:
            strargs += (str(args[i+2]) + ", ")

    # Return a string representation of the arguments
    return strargs[:-2] + ")"


@signature
def strgaival(val: (int, int, int, str, tuple)) -> str:
    """Convert a `getaddrinfo` value to a string

    Convert each value of `getaddrinfo` result (list) to a string
    replacing constants with their names.

    val -- an element in the list returned by `getaddrinfo`
    returns -- a string representation of the value

    """

    # The first 3 values are fam, stype and proto
    strres = "("
    for i in range(3):
        try:
            strres += (stripcons[i][val[i]] + ", ")
        except KeyError:
            strres += (str(val[i]) + ", ")

    # The last 2 are canon and a tuple with address and port
    return strres + repr(val[3]) + ", " + str(val[4]) + ")"


@signature
def strgaires(res: [(int, int, int, str, tuple)]) -> str:
    """Convert `getaddrinfo` result to a string

    Convert `getaddrinfo` result (list) to a string replacing
    constants with their names.

    res -- the list returned by `getaddrinfo`
    returns -- a string representation of the returned list
    
    """

    # `getaddrinfo` always returns a list
    strres = "["

    # Each address in the list
    for v in res:
        strres += strgaival(v) + ", "

    # Return a string representation of the list
    return strres[:-2] + "]"
       

@signature
def fetchaddr(node: opt(str) = LOCAL,
              port: opt(one(str, int)) = None,
              fam: opt(int) = 0,
              stype: opt(int) = 0,
              proto: opt(int) = 0,
              flags: opt(int) = 0) -> [(int, int, int, str, tuple)]:
    """Fetch address information

    Returns a list of addresses matching the request.  The function is
    doing several `getaddrinfo` calls since I've experienced that the
    Windows implementation of `getaddrinfo` only returns a single
    address (when family and socket type are not specified).  `node`
    is an IP address or a host name.  `port` is a valid port number,
    `fam` is the protocol family (AF_INET, AF_INET6, AF_UNIX, or
    AF_UNSPEC).  `stype` is the socket type (SOCK_STREAM, SOCK_DGRAM,
    SOCK_RAW, or 0, where 0 means all matching socket types).  `proto`
    is the protocol selected (IPPROTO_TCP, IPPROTO_UDP, IPPROTO_RAW,
    or 0, where 0 means the default protocol for the given
    family/type).  `flags` might depend on your local `getaddrinfo`
    implementation (AI_PASSIVE and AI_CANONNAME are examples).

    node -- the address or host name (default 'localhost')
    port -- the ip port number (default None)
    fam -- the address family, see above (default 0)
    stype -- the socket type, see above (default 0)
    proto -- the protocol, see above (default 0)
    flags -- the `getaddrinfo` flags (default 0)
    returns -- a list of addresses matching the given arguments
    
    """

    # Select address family
    if fam == None:
        if has_ipv6:
            famlist = [AF_INET6, AF_INET]
        else:
            famlist = [AF_INET]
    else:
        famlist = [fam]

    # Select socket type
    if stype == None:
        typelist = [SOCK_DGRAM, SOCK_STREAM]
    else:
        typelist = [stype]

    # Create proto/flags arguments
    if proto != None:
        pfargs = (proto,)
        if flags:
            pfargs += (flags,)
    elif flags:
        pfargs = (0, flags)
    else:
        pfargs = ()

    # Fetch a list of addresses
    addrlist = []

    # In given families (default AF_INET6 and AF_INET)
    for f in famlist:

        # Of given types (default SOCK_DGRAM and SOCK_STREAM)
        for t in typelist:

            # Try to get an address
            args = (node, port, f, t) + pfargs
            try:
                #debugmsg("\tgetaddrinfo%s" % (strgaiargs(args),), fetchaddr)
                addrlist += getaddrinfo(*args)
            except gaierror:
                etype, value = sys.exc_info()[:2]
                continue

    # Any addresses?
    if len(addrlist) == 0:
        raise etype("fetchaddr: unable to get addresses: %s" % (value,))

    # Fetch the address information
    #debugmsg("\t-> %s" % (strgaires(addrlist),), fetchaddr)
    return addrlist


class IPaddr:
    """IP address

    Holds IP address.  This is a list of addresses because a single
    host can have several addresses, each supporting different
    protocol families and protocol types.  An IP address object also
    provides methods that creates the arguments to the different
    socket operations.  These methods are generators (since we have a
    list of addresses).

    """

    @signature
    def __init__(self,
                 node: opt(str) = LOCAL,
                 port: opt(one(str, int)) = MYPORT,
                 fam: opt(int) = 0,
                 stype: opt(int) = 0,
                 proto: opt(int) = 0,
                 flags: opt(int) = 0):
        """Create an address

        Create an list of IP addresses matching the provided address
        information.

        node --  the address or host name (default 'localhost')
        port -- the ip port number (default MYPORT)
        fam -- the address family, see `fetchaddr` above (default 0)
        stype -- the socket type, see `fetchaddr` above (default 0)
        proto -- the protocol, see `fetchaddr` above (default 0)
        flags -- the `getaddrinfo` flags (default 0)

        """
        self.addrinfo = fetchaddr(node, port, fam, stype, proto, flags)
        self.str = strgaiargs((node, port, fam, stype, proto, flags))
        #debugmsg(self.str, self.__init__)

    @signature
    def socket_args(self) -> (str, (int, int, int)):
        """A generator for socket arguments

        This is a generator for socket arguments providing the
        arguments to the Python `socket` constructor for all listed
        addresses.

        """
        for info in self.addrinfo:
            yield strgaival(info), (info[FAM], info[TYPE], info[PROTO])

    @signature
    def connect_args(self) -> (str, (str, one(str, int))):
        """A generator for connect arguments

        This is a generator for connect arguments providing the
        arguments to the socket `connect` method for all listed
        addresses.

        """
        for info in self.addrinfo:
            yield strgaival(info), (info[SOCKADDR][:2],)

    @signature
    def bind_args(self) -> (str, (str, one(str, int))):
        """A generator for bind arguments

        This is a generator for bind arguments providing the
        arguments to the socket `bind` method for all listed
        addresses.

        """
        for info in self.addrinfo:
            yield strgaival(info), (info[SOCKADDR][:2],)        

    @signature
    def __str__(self) -> str:
        """String representation

        The string representation of the object.

        returns -- a string representation of the address (see `strgaiargs`)

        """
        return self.str
