# RPC sockets and all that
# (c) Wijnand 'tehmaze' Modderman - http://tehmaze.com
# BSD License
# THIS MODULE STILL NEEDS A LOT OF WORK, USE AT YOUR OWN RISK!!!

import Queue
import os
import socket
import time
from gozerbot.commands import cmnds
from gozerbot.config import config
from gozerbot.datadir import datadir
from gozerbot.fleet import fleet
from gozerbot.generic import handle_exception, rlog, toascii, waitforqueue
from gozerbot.ircevent import Ircevent
from gozerbot.persistconfig import PersistConfig
from gozerbot.pdod import Pdod
from gozerbot.plugins import plugins
from gozerbot.users import users
import gozerbot.thr as thr

cfg = PersistConfig()
cfg.define('enabled', False)
cfg.define('ssl-bufsize', 1024)
cfg.define('ssl-bundle', '')
cfg.define('ssl-ca', '')
cfg.define('ssl-host', '127.0.0.1')
cfg.define('ssl-port', '10000')
cfg.define('unix-bufsize', 4096)
cfg.define('unix-path', 'gozerrpc')
cfg.define('unix-timeout', 2.0)
cfg.define('umask', '022')

from errno import EWOULDBLOCK

try:
    from OpenSSL import SSL
except:
    SSL = None

class GozerRpcException(Exception):
    pass

class GozerRpcProtocol:
    """ I'm the basic RPC protocol. """

    NL   = '\n'
    type = 'undefined'
    name = 'undefined'
    userhost = 'rpc@rpc'

    def __init__(self, name):
        self.name = name
        if not users.getname(self.userhost):
            users.add('rpc', [self.userhost, ], ['WEB', ])

    def dispatch(self, data):
        bot = fleet.byname(self.name)
        ievent = Ircevent()
        ievent.txt = data
        ievent.nick = 'rpc'
        ievent.userhost = self.userhost
        ievent.channel = 'rpc'
        q = Queue.Queue()
        ievent.queues.append(q)
        ievent.speed = 3
        ievent.bot = bot
        result = []
        if plugins.woulddispatch(bot, ievent):
            thr.start_new_thread(plugins.trydispatch, (bot, ievent))
        else:
            return ["can't dispatch %s" % data, ]
        result = waitforqueue(q, 60)
        if not result:
            return ["can't dispatch %s" % data, ]
        return result

    def handle(self, data, sock=None):
        rlog(0, 'rpc.%s.%s' % (self.type, self.name), '<<< %s' % data)
        reply = self.dispatch(data)
        for line in reply:
            self.sendline(line, sock)

    def sendline(self, line, sock=None):
        if not sock and hasattr(self, 'sock'):
            sock = self.sock
        if sock:
            sock.send('%s%s\n' % (line.rstrip(), self.NL))

    def start(self):
        rlog(5, 'rpc', 'starting RPC %s type %s' % (self.name, self.type))
        self.run = True

    def stop(self):
        self.run = False

if SSL:
    class GozerRpcSsl(GozerRpcProtocol):
        """ I'm a TCP/SSL socket RPC """

        type = 'ssl'

        def __init__(self, name):
            if not cfg.get('ssl-bundle'):
                raise GozerRpcException('no ssl-bundle file is configured')
            if not os.path.isfile(cfg.get('ssl-bundle')):
                raise GozerRpcException('could not read ssl-bundle file')
            if not cfg.get('ssl-ca'):
                raise GozerRpcException('no ssl-ca file is configured')
            if not os.path.isfile(cfg.get('ssl-ca')):
                raise GozerRpcException('could not read ssl-ca file')
            self.ctx = SSL.Context(SSL.SSLv23_METHOD)
            #self.ctx.set_options(SSL.OP_NO_SSLv2)
            #self.ctx.set_verify(SSL.VERIFY_PEER|SSL.VERIFY_FAIL_IF_NO_PEER_CERT, self.verify_cb) # Demand a certificate
            self.ctx.use_privatekey_file(cfg.get('ssl-bundle'))
            self.ctx.use_certificate_file(cfg.get('ssl-bundle'))
            self.ctx.load_verify_locations(cfg.get('ssl-ca'))
            GozerRpcProtocol.__init__(self, name)
            self.run = False
            self.addrs = {}

        def start(self):
            self.addr = ((cfg.get('ssl-host'), int(cfg.get('ssl-port'))))
            rlog(5, 'rpc', 'starting RPC %s type %s on %s:%d' % (self.name, self.type, self.addr[0], self.addr[1]))
            self.run = True
            self.plainsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            self.sock = SSL.Connection(self.ctx, self.plainsock)
            self.sock.bind(self.addr)
            thr.start_new_thread(self.readloop, ())

        def stop(self):
            self.run = False
            try:
                self.sock.shutdown()
                self.sock.close()
            except:
                pass

        def readloop(self):
            self.sock.listen(0)
            self.sock.setblocking(1)
            while self.run:
                try:
                    remote, addr = self.sock.accept()
                    self.addrs[remote] = tuple(addr)
                    rlog(5, 'rpc.ssl.%s' % self.name, 'connection from %s:%d' % tuple(addr))
                    thr.start_new_thread(self.readloop_client, (remote, ))
                except socket.timeout, e:
                    pass

        def readloop_client(self, remote):
            runclient = True
            bot = fleet.byname(self.name)
            self.sendline('%s (%s) ready' % (config['version'], bot.nick), remote)
            while self.run and runclient:
                try:
                    time.sleep(0.001)
                    res = remote.recv(cfg.get('ssl-bufsize'))
                    res = toascii(res.rstrip())
                    rlog(2, 'rpc.%s.%s' % (self.type, self.name), res)
                    self.handle(res, remote)
                except (SSL.WantReadError, SSL.WantWriteError, SSL.WantX509LookupError):
                    pass
                except SSL.ZeroReturnError:
                    self.drop(remote)
                except SSL.Error, e:
                    self.drop(remote, e)
                    handle_exception()
                    runclient = False 
                except socket.timeout, e:
                    if e.args[0] == EWOULDBLOCK:
                        pass
                    else:
                        handle_exception()
                        runclient = False 
                except Exception, e:
                    handle_exception()
                    runclient = False

        def sendline(self, line, sock=None):
            if not sock and hasattr(self, 'sock'):
                sock = self.sock
            if sock:
                try:
                    sock.send('%s%s\n' % (line.rstrip(), self.NL))
                except (SSL.WantReadError, SSL.WantWriteError, SSL.WantX509LookupError):
                    pass
                except SSL.ZeroReturnError:
                    self.drop(sock)
        
        def drop(self, remote, errors=None):
            try:
                if errors:
                    rlog(2, 'rpc.ssl.%s' % self.name, 'client %s:%d disconnected unexpectedly' % self.addrs[remote])
                else:
                    rlog(2, 'rpc.ssl.%s' % self.name, 'client %s:%d disconnected' % self.addrs[remote])
                del self.addrs[remote]
            except KeyError:
                pass
            remote.shutdown()
            remote.close()
    
        def verify_cb(self, conn, cert, errnum, depth, ok):
            # This obviously has to be updated
            rlog(2, 'rpc.ssl.%s' % self.name, 'got certificate: %s' % cert.get_subject())
            return ok

else:
    class GozerRpcSsl(GozerRpcProtocol):
        def __init__(self, name):
            raise GozerRpcException('this system has no PyOpenSSL installed, get it from http://pyopenssl.sourceforge.net/')

class GozerRpcUnix(GozerRpcProtocol):
    """ I'm a UNIX domains socket RPC """

    type = 'unix'
    userhost = 'rpc@127.0.0.1'

    def __init__(self, name):
        if not hasattr(socket, 'AF_UNIX'):
            raise GozerRpcException('AF_UNIX not supported by operating system')
        GozerRpcProtocol.__init__(self, name)
        self.run = False
        if not os.path.isdir(cfg.get('unix-path')):
            rlog(10, 'rpc', 'creating directory %s' % cfg.get('unix-path'))
            os.mkdir(cfg.get('unix-path'))

    def start(self):
        self.sockname = cfg.get('unix-path') + os.sep + self.name + '.sock'
        GozerRpcProtocol.start(self)
        if os.path.exists(self.sockname):
            rlog(2, 'rpc.%s.%s' % (self.type, self.name), 'cleaning up %s' % self.sockname)
            os.remove(self.sockname)
        rlog(2, 'rpc.%s.%s' % (self.type, self.name), 'binding socket to %s' % self.sockname)
        self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
        self.sock.bind(self.sockname)
        self.sock.settimeout(cfg.get('unix-timeout'))
        thr.start_new_thread(self.readloop, ())

    def readloop(self):
        self.sock.listen(0)
        while self.run:
            try:
                remote = self.sock.accept()[0] # discard unix domain socket null address
                thr.start_new_thread(self.readloop_client, (remote, ))
            except socket.timeout, e:
                pass
        self.sock.close()

    def readloop_client(self, remote):
        runclient = True
        bot = fleet.byname(self.name)
        self.sendline('%s (%s) ready' % (config['version'], bot.nick), remote)
        while self.run and runclient:
            try:
                time.sleep(0.001)
                res = remote.recv(cfg.get('unix-bufsize'))
                res = toascii(res.rstrip())
                rlog(2, 'rpc.%s.%s' % (self.type, self.name), res)
                self.handle(res, remote)
            except socket.timeout, e:
                if e.args[0] == EWOULDBLOCK:
                    pass
                else:
                    handle_exception()
                    runclient = False 
            except Exception, e:
                handle_exception()
                runclient = False
        try:
            remote.send('\ntimeout, bye\n')
            remote.close()
        except:
            pass

class GozerRpc:

    types = {
        'ssl':  GozerRpcSsl,
        'unix': GozerRpcUnix
        }

    def __init__(self):
        self.cfg = Pdod(datadir + os.sep + 'rpc')
        print 'RPCCFG', self.cfg.data
        self.run = False
        self.rpcs = {}
        for type in self.types.keys():
            self.rpcs[type] = {}

    def add(self, type, name, new = False):
        if not self.types.has_key(type):
            raise GozerRpcException('no such type "%s"' % type)
        else:
            self.rpcs[type][name] = self.types[type](name)
            if new:
                self.cfg.set(type, name, {})
                self.cfg.save()
            self.rpcs[type][name].start()

    def start(self):
        for type in self.cfg.data.keys():
            for name in self.cfg.data[type].keys():
                rlog(2, 'rpc', 'starting %s.%s' % (type, name))
                self.add(type, name, False)

    def shutdown(self):
        for type in self.rpcs.keys():
            for name in self.rpcs[type].keys():
                self.rpcs[type][name].stop()
        self.cfg.save()

gozerrpc = GozerRpc()

def init():
    if cfg.get('enabled'):
        gozerrpc.start()
    return 1

def shutdown():
    try:
        gozerrpc.shutdown()
        del gozerrpc
    except:
        pass
    return 1

def handle_rpc_enable(bot, ievent):
    gozerrpc.start()
    cfg.set('enabled', True)
    ievent.reply('ok')

cmnds.add('rpc-enable', handle_rpc_enable, ['OPER'])

def handle_rpc_disable(bot, ievent):
    gozerrpc.shutdown()
    cfg.set('enabled', False)
    ievent.reply('ok')

cmnds.add('rpc-disable', handle_rpc_disable, ['OPER'])

def handle_rpc_add(bot, ievent):
    if len(ievent.args) < 2:
        return ievent.missing('<type> <bot name>')
    type = ievent.args[0]
    name = ievent.args[1]
    if not fleet.byname(name):
        return ievent.reply('no such bot name')
    if gozerrpc.types.has_key(type):
        gozerrpc.add(type, name, True)
        ievent.reply('ok')
    else:
        return ievent.reply('no such type')

cmnds.add('rpc-add', handle_rpc_add, ['OPER'])
 
