# The contents of this file are subject to the BitTorrent Open Source License
# Version 1.1 (the License).  You may not copy or use this file, in either
# source code or executable form, except in compliance with the License.  You
# may obtain a copy of the License at http://www.bittorrent.com/license/.
#
# Software distributed under the License is distributed on an AS IS basis,
# WITHOUT WARRANTY OF ANY KIND, either express or implied.  See the License
# for the specific language governing rights and limitations under the
# License.

# Written by Greg Hazel

import os
import sys
import socket
import signal
import struct
import thread
from cStringIO import StringIO
from traceback import print_exc, print_stack

from BitTorrent import BTFailure, WARNING, CRITICAL, FAQ_URL
    
noSignals = True

if os.name == 'nt':
    try:
        from twisted.internet import iocpreactor
        iocpreactor.proactor.install()
        noSignals = False
    except:
        # just as limited (if not more) as select, and also (supposedly) buggy
        #try:
        #    from twisted.internet import win32eventreactor
        #    win32eventreactor.install()
        #except:
        #    pass
        pass
else:
    try:
        from twisted.internet import kqreactor
        kqreactor.install()
    except:
        try:
            from twisted.internet import pollreactor
            pollreactor.install()
        except:
            pass

#the default reactor is select-based, and will be install()ed if another has not    
from twisted.internet import reactor, task, error

import twisted.copyright
if int(twisted.copyright.version.split('.')[0]) < 2:
    raise ImportError("RawServer_twisted requires twisted 2.0.0 or greater")

from twisted.internet.protocol import DatagramProtocol, Protocol, Factory, ClientFactory
from twisted.protocols.policies import TimeoutMixin

NOLINGER = struct.pack('ii', 1, 0)

class Handler(object):

    # there is only a semantic difference between "made" and "started".
    # I prefer "started"
    def connection_started(self, s):
        self.connection_made(s)
    def connection_made(self, s):
        pass

    def connection_lost(self, s):
        pass

    # Maybe connection_lost should just have a default 'None' exception parameter
    def connection_failed(self, addr, exception):
        pass
    
    def connection_flushed(self, s):
        pass
    def data_came_in(self, addr, datagram):
        pass

class ConnectionWrapper(object):
    def __init__(self, rawserver, handler, context, tos=0):
        self.dying = 0
        self.ip = None
        self.port = None
        self.transport = None
        self.reset_timeout = None

        self.post_init(rawserver, handler, context)

        self.tos = tos

        self.buffer = OutputBuffer(self)        

    def post_init(self, rawserver, handler, context):
        self.rawserver = rawserver
        self.handler = handler
        self.context = context
        if self.rawserver:
            self.rawserver.single_sockets[self] = self

    def get_socket(self):
        s = None
        try:
            s = self.transport.getHandle()
        except:
            try:
                # iocpreactor doesn't implement ISystemHandle like it should
                s = self.transport.socket
            except:
                pass
        return s            

    def attach_transport(self, transport, reset_timeout):
        self.transport = transport
        self.reset_timeout = reset_timeout

        try:        
            address = self.transport.getPeer()
        except:
            try:
                # udp, for example
                address = self.transport.getHost()
            except:
                if not self.transport.__dict__.has_key("state"):
                    self.transport.state = "NO STATE!"
                sys.stderr.write("UNKNOWN HOST/PEER: " + str(self.transport) + ":" + str(self.transport.state)+ ":" + str(self.handler) + "\n")
                print_stack()
                # fallback incase the unknown happens,
                # there's no use raising an exception
                address = ("unknown", -1)
                pass

        try:            
            self.ip = address.host
            self.port = address.port
        except:
            #unix sockets, for example
            pass

        if self.tos != 0:
            s = self.get_socket()
            
            try:
                s.setsockopt(socket.IPPROTO_IP, socket.IP_TOS, self.tos)
            except:
                pass

    def sendto(self, packet, flags, addr):
        # all this can go away once we pin down the bug
        if not hasattr(self.transport, "listening"):
            self.rawserver.errorfunc(WARNING, "UDP port never setup properly when asked to write")
        elif not self.transport.listening:
            self.rawserver.errorfunc(WARNING, "UDP port cleaned up already when asked to write")

        ret = None
        try:
            ret = self.transport.write(packet, addr)
        except Exception, e:
            self.rawserver.errorfunc(WARNING, "UDP sendto failed: %s" % str(e))
        
        return ret

    def write(self, b):
        self.buffer.add(b)

    def _flushed(self):
        s = self        
        #why do you tease me so?
        if s.handler is not None:
            #calling flushed from the write is bad form
            self.rawserver.add_task(s.handler.connection_flushed, 0, (s,))

    def is_flushed(self):
        return self.buffer.is_flushed()

    def shutdown(self, how):
        if how == socket.SHUT_WR:
            self.transport.loseWriteConnection()
            self.buffer.stopWriting()
        elif how == socket.SHUT_RD:
            self.transport.stopListening()
        else:
            self.close()
            
    def close(self):
        self.buffer.stopWriting()
        
        # opt for no "connection_lost" callback since we know that
        self.dying = 1

        if self.rawserver.config['close_with_rst']:
            try:
                s = self.get_socket()
                s.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, NOLINGER)
            except:
                pass

        if self.rawserver.udp_sockets.has_key(self):
            # udp connections should only call stopListening
            self.transport.stopListening()
        else:
            self.transport.loseConnection()


class OutputBuffer(object):

    def __init__(self, connection):
        self.connection = connection
        self.consumer = None
        self.buffer = StringIO()

    def is_flushed(self):
        return (self.buffer.tell() == 0)

    def add(self, b):
        # sometimes we get strings, sometimes we get buffers. ugg.
        if (isinstance(b, buffer)):
            b = str(b)
        self.buffer.write(b)

        if self.consumer is None:
            self.beginWriting()
        
    def beginWriting(self):
        self.stopWriting()
        self.consumer = self.connection.transport
        self.consumer.registerProducer(self, False)

    def stopWriting(self):
        if self.consumer is not None:
            self.consumer.unregisterProducer()
        self.consumer = None

    def resumeProducing(self):
        if self.consumer is not None:
            if self.buffer.tell() > 0:
                self.consumer.write(self.buffer.getvalue())
                self.buffer.seek(0)
                self.buffer.truncate(0)
                self.connection._flushed()
            else:
                self.stopWriting()


    def pauseProducing(self):
        pass

    def stopProducing(self):
        pass

class CallbackConnection(object):

    def attachTransport(self, transport, connection, *args):
        s = connection
        if s is None:
            s = ConnectionWrapper(*args)

        s.attach_transport(transport, self.optionalResetTimeout)
        self.connection = s

    def connectionMade(self):
        s = self.connection
        s.handler.connection_started(s)
        self.optionalResetTimeout()

    def connectionLost(self, reason):
        reactor.callLater(0, self.post_connectionLost, reason)
        
    #twisted api inconsistancy workaround
    #sometimes connectionLost is called (not fired) from inside write()
    def post_connectionLost(self, reason):
        #hack to try and dig up the connection if one was ever made
        if not self.__dict__.has_key("connection"):
            self.connection = self.factory.connection
        if self.connection is not None:
            self.factory.rawserver._remove_socket(self.connection)

    def dataReceived(self, data):
        self.optionalResetTimeout()

        s = self.connection
        s.rawserver._make_wrapped_call(s.handler.data_came_in,
                                       (s, data), s)

    def datagramReceived(self, data, (host, port)):
        s = self.connection
        s.rawserver._make_wrapped_call(s.handler.data_came_in,
                                       ((host, port), data), s)
        
    def connectionRefused(self):
        s = self.connection
        dns = (s.ip, s.port)
        reason = "connection refused"
        
        if not s.dying:
            # this might not work - reason is not an exception
            s.handler.connection_failed(dns, reason)

            #so we don't get failed then closed
            s.dying = 1
        
        s.rawserver._remove_socket(s)

    def optionalResetTimeout(self):
        if self.can_timeout:
            self.resetTimeout()

class CallbackProtocol(CallbackConnection, TimeoutMixin, Protocol):

    def makeConnection(self, transport):
        self.can_timeout = 1
        self.setTimeout(self.factory.rawserver.config['socket_timeout'])
        self.attachTransport(transport, self.factory.connection, *self.factory.connection_args)
        Protocol.makeConnection(self, transport)

class CallbackDatagramProtocol(CallbackConnection, DatagramProtocol):

    def startProtocol(self):
        self.can_timeout = 0
        self.attachTransport(self.transport, self.connection, ())
        DatagramProtocol.startProtocol(self)

    def connectionRefused(self):
        # we don't use these at all for udp, so skip the CallbackConnection
        DatagramProtocol.connectionRefused(self)
        
class OutgoingConnectionFactory(ClientFactory):
        
    def clientConnectionFailed(self, connector, reason):
        peer = connector.getDestination()
        dns = (peer.host, peer.port)
        # opt-out        
        if not self.connection.dying:
            # this might not work - reason is not an exception
            self.connection.handler.connection_failed(dns, reason)

            #so we don't get failed then closed
            self.connection.dying = 1
            
        self.rawserver._remove_socket(self.connection)

def UnimplementedWarning(msg):
    #ok, I think people get the message
    #print "UnimplementedWarning: " + str(msg)
    pass

#Encoder calls stop_listening(socket) then socket.close()
#to us they mean the same thing, so swallow the second call
class CloseSwallower:
    def close(self):
        pass

#storage for socket creation requestions, and proxy once the connection is made
class SocketProxy(object):
    def __init__(self, port, bind, reuse, tos, protocol):
        self.port = port
        self.bind = bind
        self.reuse = reuse
        self.tos = tos
        self.protocol = protocol
        self.connection = None

    def __getattr__(self, name):
        try:
            return getattr(self.connection, name)
        except:
            raise AttributeError, name

def default_error_handler(level, message):
    print message

class RawServerMixin(object):

    def __init__(self, doneflag, config, noisy=True,
                 errorfunc=default_error_handler, tos=0):
        self.doneflag = doneflag
        self.noisy = noisy
        self.errorfunc = errorfunc
        self.config = config
        self.tos = tos
        self.ident = thread.get_ident()

    def _make_wrapped_call(self, function, args, wrapper=None, context=None):
        try:
            function(*args)
        except KeyboardInterrupt:
            raise
        except Exception, e:         # hopefully nothing raises strings
            # Incoming sockets can be assigned to a particular torrent during
            # a data_came_in call, and it's possible (though not likely) that
            # there could be a torrent-specific exception during the same call.
            # Therefore read the context after the call.
            if wrapper is not None:
                context = wrapper.context
            if self.noisy and context is None:
                data = StringIO()
                print_exc(file=data)
                data.seek(-1)
                self.errorfunc(CRITICAL, data.read())
            if context is not None:
                context.got_exception(e)

    # must be called from the main thread
    def install_sigint_handler(self):
        signal.signal(signal.SIGINT, self._handler)

    def _handler(self, signum, frame):
        self.external_add_task(self.doneflag.set, 0)
        # Allow pressing ctrl-c multiple times to raise KeyboardInterrupt,
        # in case the program is in an infinite loop
        signal.signal(signal.SIGINT, signal.default_int_handler)

class RawServer(RawServerMixin):

    def __init__(self, doneflag, config, noisy=True,
                 errorfunc=default_error_handler, tos=0):
        RawServerMixin.__init__(self, doneflag, config, noisy, errorfunc, tos)

        self.listening_handlers = {}
        self.single_sockets = {}
        self.udp_sockets = {}
        self.live_contexts = {None : 1}
        self.listened = 0
        
    def add_context(self, context):
        self.live_contexts[context] = 1

    def remove_context(self, context):
        del self.live_contexts[context]

    def autoprune(self, f, *a, **kw):
        if self.live_contexts.has_key(kw['context']):
            f(*a, **kw)

    def add_task(self, func, delay, args=(), context=None):
        assert thread.get_ident() == self.ident
        assert type(args) == list or type(args) == tuple

        #we're going to check again later anyway
        #if self.live_contexts.has_key(context):
        reactor.callLater(delay, self.autoprune, self._make_wrapped_call,
                          func, args, context=context)
        
    def external_add_task(self, func, delay, args=(), context=None):
        assert type(args) == list or type(args) == tuple
        reactor.callFromThread(self.add_task, func, delay, args, context)

    def create_unixserversocket(filename):
        s = SocketProxy(0, filename, True, 0, 'tcp')
        s.factory = Factory()
        
        if s.reuse == False:
            UnimplementedWarning("You asked for reuse to be off when binding. Sorry, I can't do that.")

        listening_port = reactor.listenUNIX(s.bind, s.factory)
        listening_port.listening = 1
        s.listening_port = listening_port
        
        return s
    create_unixserversocket = staticmethod(create_unixserversocket)

    def create_serversocket(port, bind='', reuse=False, tos=0):
        s = SocketProxy(port, bind, reuse, tos, 'tcp')
        s.factory = Factory()
        
        if s.reuse == False:
            UnimplementedWarning("You asked for reuse to be off when binding. Sorry, I can't do that.")

        try:        
            listening_port = reactor.listenTCP(s.port, s.factory, interface=s.bind)
        except error.CannotListenError, e:
            if e[0] != 0:
                raise e.socketError
            else:
                raise
        listening_port.listening = 1
        s.listening_port = listening_port
        
        return s
    create_serversocket = staticmethod(create_serversocket)

    def create_udpsocket(port, bind='', reuse=False, tos=0):
        s = SocketProxy(port, bind, reuse, tos, 'udp')
        s.protocol = CallbackDatagramProtocol()
        c = ConnectionWrapper(None, None, None, tos)
        s.connection = c
        s.protocol.connection = c

        if s.reuse == False:
            UnimplementedWarning("You asked for reuse to be off when binding. Sorry, I can't do that.")
                         
        try:        
            listening_port = reactor.listenUDP(s.port, s.protocol, interface=s.bind)
        except error.CannotListenError, e:
            raise e.socketError       
        listening_port.listening = 1
        s.listening_port = listening_port
        
        return s
    create_udpsocket = staticmethod(create_udpsocket)

    def start_listening(self, serversocket, handler, context=None):
        s = serversocket
        s.factory.rawserver = self
        s.factory.protocol = CallbackProtocol
        s.factory.connection = None
        s.factory.connection_args = (self, handler, context, serversocket.tos)

        if not s.listening_port.listening:
            s.listening_port.startListening()
            s.listening_port.listening = 1

        self.listening_handlers[s] = s.listening_port

        #provides a harmless close() method
        s.connection = CloseSwallower()        

    def start_listening_udp(self, serversocket, handler, context=None):
        s = serversocket
        
        c = s.connection
        c.post_init(self, handler, context)

        if not s.listening_port.listening:
            s.listening_port.startListening()
            s.listening_port.listening = 1

        self.listening_handlers[serversocket] = s.listening_port
            
        self.udp_sockets[c] = c

    def stop_listening(self, serversocket):
        listening_port = self.listening_handlers[serversocket]
        try:
            listening_port.stopListening()
            listening_port.listening = 0
        except error.NotListeningError:
            pass
        del self.listening_handlers[serversocket]

    def stop_listening_udp(self, serversocket):
        listening_port = self.listening_handlers[serversocket]
        listening_port.stopListening()
        del self.listening_handlers[serversocket]
        del self.udp_sockets[serversocket.connection]
        del self.single_sockets[serversocket.connection]
        
    def start_connection(self, dns, handler, context=None, do_bind=True):
        addr = dns[0]
        port = int(dns[1])

        bindaddr = None
        if do_bind:
            bindaddr = self.config['bind']
            if bindaddr and len(bindaddr) >= 0:
                bindaddr = (bindaddr, 0)
            else:
                bindaddr = None

        factory = OutgoingConnectionFactory()
        factory.protocol = CallbackProtocol
        factory.rawserver = self

        c = ConnectionWrapper(self, handler, context, self.tos)
        
        factory.connection = c
        factory.connection_args = ()

        connector = reactor.connectTCP(addr, port, factory, bindAddress=bindaddr)

        self.single_sockets[c] = c
        return c

    def async_start_connection(self, dns, handler, context=None, do_bind=True):
        self.start_connection(dns, handler, context, do_bind)

    def wrap_socket(self, sock, handler, context=None, ip=None):
        raise Unimplemented("wrap_socket")

    def listen_forever(self):
        self.ident = thread.get_ident()
        if self.listened:
            UnimplementedWarning("listen_forever() should only be called once per reactor.")
        self.listened = 1
        
        l = task.LoopingCall(self.stop)
        l.start(1, now = False)
        
        if noSignals:
            reactor.run(installSignalHandlers=False)
        else:
            reactor.run()
            
    def listen_once(self, period=1e9):
        UnimplementedWarning("listen_once() Might not return until there is activity, and might not process the event you want. Use listen_forever().")
        reactor.iterate(period)
    
    def stop(self):
        if (self.doneflag.isSet()):

            for connection in self.single_sockets.values():
                try:
                    #I think this still sends all the data
                    connection.close()
                except:
                    pass
                
            reactor.suggestThreadPoolSize(0)
            reactor.stop()

    def _remove_socket(self, s):
        # opt-out        
        if not s.dying:
            self._make_wrapped_call(s.handler.connection_lost, (s,), s)
            s.handler = None

        del self.single_sockets[s]
        
