#!/usr/bin/env python2
# Copyright (C) 2013 Gunnar Beutner
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

TRUST_HOST = '127.0.0.1'
TRUST_PORT = 5776
TRUST_NAME = 'irc.midiandmore.net'
TRUST_PASS = 'password'
BIND_HOST  = '0.0.0.0'

# qwebirc patch from Andreas Pschorn
# snircd needs for qwebirc the webirc.patch file,
# please download it from https://github.com/quakenet/snircd-patchqueue/blob/master/webirc.patch 
# Please remove the "diff -r e1de3d14c9df doc/readme.iauth" section in this file,
# for correctly parsing this patch!
# Go to the snircd sources directory and then enter "git apply webirc.patch",
# After that then enter "./configure", "make" and "make install",
# and the ircd will now working with the Patch.
# Further configuration in the "ircd.conf" is needed or modified to:
#
# IAuth {
#  program = "/usr/bin/env" "python2" "<path to quakenet-iauthd-file>";
# };
#
# And set in the features section:
#
# "HIS_STATS_IAUTH" = "TRUE";
#
# Have fun for the using of this script :)

WEBIRC_HOST = '127.0.0.1'
WEBIRC_PASS = 'password'
WEBIRC_USERNAME = 'qwebirc'


# Do not modify beyond this point.

VERSION = 4

import os
import sys
import hmac
import fcntl
import asyncore
import socket
import base64
import resource
from time import time

               
class LineDispatcher(asyncore.dispatcher):
    def __init__(self, fd=None, map=None):
        asyncore.dispatcher.__init__(self, None, map)

        self.in_buffer = ''
        self.out_buffer = ''

        self.send('VERSION %s\n' % (VERSION))

        if fd == None:
            return

        self.connected = True
        try:
            fd = fd.fileno()
        except AttributeError:
            pass
        self.set_file(fd)
        # set it to non-blocking mode
        flags = fcntl.fcntl(fd, fcntl.F_GETFL, 0)
        flags = flags | os.O_NONBLOCK
        fcntl.fcntl(fd, fcntl.F_SETFL, flags)

    def set_file(self, fd):
        self.socket = asyncore.file_wrapper(fd)
        self._fileno = self.socket.fileno()
        self.add_channel()

    def handle_read(self):
        data = self.recv(8192)

        if not data:
            return

        self.in_buffer += data

        while True:
            pos = self.in_buffer.find('\n')

            if pos == -1:
                break

            line = self.in_buffer[0:pos]
            self.in_buffer = self.in_buffer[pos + 1:]

            # remove new-line characters
            line = line.rstrip('\r\n')
 
            self.handle_line(line)

    def writable(self):
        return self.out_buffer != ''

    def send(self, data):
        self.out_buffer += data

    def handle_write(self):
        sent = 0
        sent = asyncore.dispatcher.send(self, self.out_buffer)
        self.out_buffer = self.out_buffer[sent:]

    def log_info(self, message, type='info'):
        if __debug__ or type != 'info':
            sys.stderr.write('%s: %s\n' % (type, message))

class IAuthHandler(LineDispatcher):
    def __init__(self, fin):
        LineDispatcher.__init__(self, fin)

        self.clients = {}
        self.next_unique_id = 0

        self.send("V :quakenet-iauthd\n")
        self.send("O AU\n")

    def handle_line(self, line):
        global trust_handler

        # tokenize the line according to RFC 2812
        if line.find(' :') != -1:
            line, trailing = line.split(' :', 1)
            tokens = line.split()
            tokens.append(trailing)
        else:
            tokens = line.split()

        if len(tokens) < 3: # too few tokens
            return

        id = tokens[0]
        command = tokens[1]
        params = tokens[2:]

        if id != '-1' and not id in self.clients:
            self.clients[id] = {}

        # qwebirc patch from Andreas Pschorn 
        if command == 'W': # configure webirc
            if len(params) < 4: # too few parameters
                return

            self.clients[id]['password'] = params[0]
            self.clients[id]['username'] = params[1]
            self.clients[id]['hostname'] = params[2]
            self.clients[id]['ip'] = params[3]
            if self.clients[id]['password'] == WEBIRC_PASS and self.clients[id]['username'] == WEBIRC_USERNAME:
                self.send('N %s %s %s %s\n' % (id, self.clients[id]['remoteip'], self.clients[id]['remoteport'], self.clients[id]['hostname']))
        elif command == 'C': # new client
            if len(params) < 2: # too few parameters
                return

            self.clients[id]['remoteip'] = params[0]
            self.clients[id]['remoteport'] = params[1]

            self.clients[id]['unique_id'] = str(self.next_unique_id)
            self.next_unique_id += 1
        elif command == 'H': # hurry state (ircd has finished DNS/ident check)
            if trust_handler and trust_handler.connected and trust_handler.authed:
                trust_handler.send('CHECK %s-%s %s %s\n' %
                    (id, self.clients[id]['unique_id'], self.clients[id]['username'], self.clients[id]['remoteip']))
            else:
                self.send_verdict('%s-%s' % (id, self.clients[id]['unique_id']), 'PASS')
        elif command == 'u' or command == 'U': # trusted/untrusted username
            username = params[0]

            # untrusted username (i.e. non-working identd)
            if command == 'U':
                username = '~' + username

            if not 'username' in self.clients[id] or username[0] != '~':
                self.clients[id]['username'] = username
        elif command == 'D': # client disconnected
            if id in self.clients:
                del self.clients[id]

    def handle_close(self):
        sys.exit(0)

    def send_verdict(self, combined_id, verdict, message=None):
        global stats_passed, stats_killed, stats_unthrottled

        tokens = combined_id.split('-', 1)

        if len(tokens) < 2:
            return

        id, unique_id = tokens

        if not id in self.clients or self.clients[id]['unique_id'] != unique_id:
            return

        if verdict == 'PASS':
            # Every 10000th accepted connection gets a free cow.
            if stats_passed % 10000 == 0:
                cow = [ '(__)', ' oo\\\\\\~', '  !!!!' ]

                for line in cow:
                    self.send('C %s %s %s :%s\n' %
                        (id, self.clients[id]['remoteip'], self.clients[id]['remoteport'], line))

            if message:
                self.send('C %s %s %s :%s\n' %
                    (id, self.clients[id]['remoteip'], self.clients[id]['remoteport'], message))

            self.send('D %s %s %s\n' %
                (id, self.clients[id]['remoteip'], self.clients[id]['remoteport']))

            stats_passed += 1
        elif verdict == 'UNTHROTTLE':
            self.send('T %s %s %s\n' % 
                (id, self.clients[id]['remoteip'], self.clients[id]['remoteport']))
            stats_unthrottled += 1
            return # not a final verdict, just an addition
        else:
            if not message:
                message = 'Connections from your host cannot be accepted at this time.'

            self.send('k %s %s %s :%s\n' %
                (id, self.clients[id]['remoteip'], self.clients[id]['remoteport'], message))

            stats_killed += 1

        del self.clients[id]

    def send_snotice(self, message):
        self.send('> :iauthd: %s\n' % (message))

    def clear_stats(self):
        self.send('s\n');

    def add_stats(self, message):
        self.send('S quakenet-iauthd :%s\n' % message)

class TrustHandler(LineDispatcher):
    def __init__(self, host, port):
        LineDispatcher.__init__(self)

        self.authed = False
        self.connected_since = False

        self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
        if BIND_HOST != '0.0.0.0': # maybe unnecessary?
            self.bind( (BIND_HOST, 0) )
        self.connect( (TRUST_HOST, TRUST_PORT) )

    def handle_connect_event(self):
        self.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
        self.setsockopt(socket.SOL_TCP, socket.TCP_KEEPIDLE, 10)
        self.setsockopt(socket.SOL_TCP, socket.TCP_KEEPCNT, 3)
        self.setsockopt(socket.SOL_TCP, socket.TCP_KEEPINTVL, 10)

        try:
            LineDispatcher.handle_connect_event(self)
        except:
            iauth_handler.send_snotice('Could not connect to trusts backend: ' + str(sys.exc_info()[1]))

    def handle_line(self, line):
        global iauth_handler

        tokens = line.split(' ', 1)

        if tokens[0] == 'KILL' or tokens[0] == 'PASS' or tokens[0] == 'UNTHROTTLE':
            if len(tokens) < 2:
                return

            arguments = tokens[1].split(' ', 1)

            verdict = tokens[0]
            id = arguments[0]
            message = None

            if len(arguments) > 1:
                message = arguments[1]

            iauth_handler.send_verdict(id, verdict, message)
        elif tokens[0] == 'AUTH':
            if len(tokens) < 2:
                return

            iauth_handler.send_snotice('Received authentication request from trusts backend.')

            nonce = tokens[1]

            self.send('AUTH %s %s\n' %
                (TRUST_NAME, hmac.HMAC(TRUST_PASS, nonce).hexdigest()))
        elif tokens[0] == 'AUTHOK':
            iauth_handler.send_snotice('Successfully authenticated with trusts backend.')
            self.authed =  True
            self.connected_since = time()
        elif tokens[0] == 'QUIT':
            if len(tokens) < 2:
                message = 'No reason specified.'
            else:
                message = tokens[1]

            iauth_handler.send_snotice('Trusts backend closed connection: ' + message)
            self.close()

    def handle_connect(self):
        global iauth_handler
        iauth_handler.send_snotice('Reconnected to trusts backend.')

    def handle_close(self):
        global iauth_handler
        iauth_handler.send_snotice('Connection to trusts backend failed.')

        self.close()

iauth_handler = IAuthHandler(sys.stdin)
trust_handler = None

last_restart = time()
last_reconnect = 0
last_stats = 0
stats_passed = 0
stats_killed = 0
stats_unthrottled = 0

while True:
    # Try to (re-)connect to trusts backend if necessary
    if (not trust_handler or not trust_handler.connected) and last_reconnect + 10 < time():
        if trust_handler:
            trust_handler.close()

        iauth_handler.send_snotice('Attempting to reconnect to trusts backend.')
        trust_handler = TrustHandler(TRUST_HOST, TRUST_PORT)
        last_reconnect = time()

    if last_stats + 15 < time():
        # Update statistics
        iauth_handler.clear_stats()

        iauth_handler.add_stats('Version: %s' % (VERSION))
        iauth_handler.add_stats('Started: %s seconds ago' % (int(time() - last_restart)))

        if trust_handler and trust_handler.connected and trust_handler.authed:
            iauth_handler.add_stats('Connected to trusts backend for %s seconds.' % (int(time() - trust_handler.connected_since)))
        else:
            iauth_handler.add_stats('Not connected to trusts backend.')

        iauth_handler.add_stats('Accepted connections: %d' % (stats_passed))
        iauth_handler.add_stats('Rejected connections: %d' % (stats_killed))
        iauth_handler.add_stats('Unthrottled connections: %d' % (stats_unthrottled))
        iauth_handler.add_stats('Pending connections: %d' % (len(iauth_handler.clients)))

        ru = resource.getrusage(resource.RUSAGE_SELF)

        iauth_handler.add_stats('--')
        iauth_handler.add_stats('Memory usage: %s kB' % (ru[2] / 1024))
        iauth_handler.add_stats('CPU usage: %.2f%%' % ((ru[0] + ru[1]) / (time() - last_restart)))

        last_stats = time()

    asyncore.loop(timeout=10, count=1)
