#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
XMPP output plugin
"""

from __future__ import absolute_import

from sys import version_info
from threading import Event, Thread
from time import gmtime, strftime

from core import output
from core.config import CONFIG

import xmpp.dispatcher
import xmpp.simplexml
import xmpp.transports
from xmpp import Client
from xmpp.protocol import JID, Message

from twisted.python.log import msg


# ---------------------------------------------------------------------------
# Python 2.7 compatibility patches for xmpppy.
#
# xmpppy 0.7.3 has two functions with Python 3-only syntax that crash
# on Python 2.7:
#
# 1. simplexml.ustr(): calls str(r, ENCODING) which is Python 3 bytes-to-str
#    syntax. On Python 2.7 the equivalent is unicode(r).
#
# 2. dispatcher.Dispatcher.Process(): calls .with_traceback() unconditionally
#    on exception objects, but that method only exists on Python 3.
#
# Both are patched below before any xmpppy code runs.
# ---------------------------------------------------------------------------
if version_info[0] < 3:
    # Patch 1: simplexml.ustr
    def _ustr(what):
        if isinstance(what, str):
            return what
        try:
            r = what.__str__()
        except AttributeError:
            r = str(what)
        if not isinstance(r, str):
            return r
        return r

    xmpp.simplexml.ustr = _ustr
    xmpp.transports.ustr = _ustr
    # xmpp.protocol also does 'from .simplexml import ustr' — patch it too
    import xmpp.protocol as _xmpp_protocol
    if hasattr(_xmpp_protocol, 'ustr'):
        _xmpp_protocol.ustr = _ustr

    # Patch 2: dispatcher.Dispatcher - must be named 'Dispatcher' so that
    # PlugIn() registers it under the right key in owner.__dict__, and must
    # define plugin/plugout explicitly so PlugIn()'s __class__.__dict__ check
    # finds them (inherited methods are not in __class__.__dict__ on Py2
    # old-style classes).
    _OriginalDispatcher = xmpp.dispatcher.Dispatcher

    class Dispatcher(_OriginalDispatcher):
        def plugin(self, owner):
            _OriginalDispatcher.plugin(self, owner)

        def plugout(self):
            _OriginalDispatcher.plugout(self)

        def Process(self, timeout=0):
            for handler in self._cycleHandlers:
                handler(self)
            if len(self._pendingExceptions) > 0:
                _pendingException = self._pendingExceptions.pop()
                ex = _pendingException[0](_pendingException[1])
                if hasattr(ex, 'with_traceback'):
                    ex = ex.with_traceback(_pendingException[2])
                raise ex
            if self._owner.Connection.pending_data(timeout):
                try:
                    data = self._owner.Connection.receive()
                except IOError:
                    return
                self.Stream.Parse(data)
                if len(self._pendingExceptions) > 0:
                    _pendingException = self._pendingExceptions.pop()
                    ex = _pendingException[0](_pendingException[1])
                    if hasattr(ex, 'with_traceback'):
                        ex = ex.with_traceback(_pendingException[2])
                    raise ex
                if data:
                    return len(data)
            return '0'

    xmpp.dispatcher.Dispatcher = Dispatcher


class Output(output.Output):
    """
    XMPP output plugin.
    Sends honeypot events as plain-text messages to a configured JID.

    xmpppy's Process(1) blocks for up to one second per call, so it runs
    in a daemon thread rather than in Twisted's reactor thread.  write()
    calls send() directly from the reactor thread; send() only writes to
    the socket while Process() only reads, so there is no write-write race.
    """

    def start(self):
        jabberid = CONFIG.get('output_xmpp', 'user')
        password = CONFIG.get('output_xmpp', 'password', raw=True)
        self.receiver = CONFIG.get('output_xmpp', 'muc')
        self._stop_event = Event()
        self._thread = None

        jid = JID(jabberid)
        self.connection = Client(server=jid.getDomain(), debug=[])

        result = self.connection.connect()
        if not result:
            msg('output_xmpp: unable to connect to {}'.format(jid.getDomain()))
            self.connection = None
            return
        msg('output_xmpp: connected ({})'.format(result))

        if not self.connection.auth(user=jid.getNode(), password=password,
                                    resource=jid.getResource()):
            msg('output_xmpp: authentication failed for {}'.format(jabberid))
            self.connection.disconnect()
            self.connection = None
            return
        msg('output_xmpp: authenticated as {}'.format(jabberid))

        self._thread = Thread(target=self._process_loop)
        self._thread.daemon = True
        self._thread.start()

    def _process_loop(self):
        while not self._stop_event.is_set():
            try:
                conn = self.connection
                if conn is None:
                    break
                if not conn.Process(1):
                    msg('output_xmpp: connection lost')
                    self.connection = None
                    break
            except Exception as e:
                msg('output_xmpp: error in Process loop: {}'.format(e))
                self.connection = None
                break

    def stop(self):
        self._stop_event.set()
        if self._thread is not None:
            self._thread.join(timeout=2)
        if self.connection is not None:
            self.connection.disconnect()
            self.connection = None

    def write(self, event):
        if self.connection is None:
            return

        message = '[{} UTC] [SIPHoney on {}]: {} from {}:{}.'.format(
            strftime('%Y-%m-%d %H:%M:%S', gmtime(event['unixtime'])),
            event['sensor'], event['message'], event['src_ip'], event['dst_port']
        )

        try:
            self.connection.send(Message(to=self.receiver, body=message))
        except Exception as e:
            msg('output_xmpp: send failed: {}'.format(e))
            self.connection = None
