#!/usr/bin/env python
#
# Copyright (c) 2009 Evgeni Golov <evgeni.golov@uni-duesseldorf.de>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
# 3. Neither the name of the University nor the names of its contributors
#    may be used to endorse or promote products derived from this software
#    without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.

import os
import sys
import datetime

from optparse import OptionParser
from ConfigParser import SafeConfigParser

# Find the best reactor
reactorchoices = ["epollreactor", "pollreactor", "selectreactor", "posixbase"]
for choice in reactorchoices:
    try:
        exec("from twisted.internet import %s as bestreactor" % choice)
        break
    except:
        pass
try:
    bestreactor.install()
except:
    print "Unable to find a reactor. Exiting..."
    sys.exit(1)

from twisted.internet import reactor

try:
    from twisted.scripts import _twistd_unix as twistd
except:
    from twisted.scripts import twistd

from bley import BleyPolicyFactory

__CREATE_DB_QUERY = '''
  CREATE TABLE IF NOT EXISTS bley_status
  (
    ip VARCHAR(39) NOT NULL,
    status SMALLINT NOT NULL DEFAULT 1,
    last_action TIMESTAMP NOT NULL,
    sender VARCHAR(254),
    recipient VARCHAR(254),
    fail_count INT DEFAULT 0
  );
'''
__CREATE_DB_QUERY_PG = '''
  CREATE TABLE bley_status
  (
    ip VARCHAR(39) NOT NULL,
    status SMALLINT NOT NULL DEFAULT 1,
    last_action TIMESTAMP NOT NULL,
    sender VARCHAR(254),
    recipient VARCHAR(254),
    fail_count INT DEFAULT 0
  );
'''
__CHECK_DB_QUERY_PG = '''
  SELECT tablename FROM pg_catalog.pg_tables WHERE tablename = 'bley_status'
'''

__CLEAN_DB_QUERY = '''
   DELETE FROM bley_status WHERE last_action<%(old)s OR (last_action<%(old_bad)s AND status>=2)
'''

def bley_start():

    parser = OptionParser(version='0.1.4')
    parser.add_option("-p", "--pidfile", dest="pid_file",
                      help="use PID_FILE for storing the PID")
    parser.add_option("-c", "--config", dest="conffile",
                      help="load configuration from CONFFILE")
    parser.add_option("-v", "--verbose",
                      action="store_true", dest="verbose",
                      help="use verbose output")
    parser.add_option("-d", "--debug",
                      action="store_true", dest="debug",
                      help="don't daemonize the process and log to stdout")
    (settings, args) = parser.parse_args()

    if not settings.conffile:
        settings.conffile = '/etc/bley/bley.conf'

    defaults = {
        'listen_addr': '127.0.0.1',
        'log_file': None,
        'pid_file': None,
    }
    config = SafeConfigParser(defaults)
    config.read(settings.conffile)

    settings.listen_addr = config.get('bley', 'listen_addr')
    settings.listen_port = config.getint('bley', 'listen_port')
    settings.pid_file    = settings.pid_file or config.get('bley', 'pid_file')
    settings.log_file    = config.get('bley', 'log_file')
    settings.dbtype      = config.get('bley', 'dbtype')
    if settings.dbtype == 'pgsql':
        database = 'psycopg2'
        settings.dbsettings = {'host': config.get('bley', 'dbhost'), 'database': config.get('bley', 'dbname'),
                               'user': config.get('bley', 'dbuser'), 'password': config.get('bley', 'dbpass')}
    elif settings.dbtype == 'mysql':
        database = 'MySQLdb'
        settings.dbsettings = {'host': config.get('bley', 'dbhost'), 'db': config.get('bley', 'dbname'),
                               'user': config.get('bley', 'dbuser'), 'passwd': config.get('bley', 'dbpass')}
    else:
        print "No supported database configured."
        sys.exit(1)

    exec("import %s as database" % database)
    settings.database = database
    settings.reject_msg  = config.get('bley', 'reject_msg')

    settings.dnswls      = [d.strip() for d in config.get('bley', 'dnswls').split(',')]
    settings.dnsbls      = [d.strip() for d in config.get('bley', 'dnsbls').split(',')]

    settings.dnswl_threshold  = config.getint('bley', 'dnswl_threshold')
    settings.dnsbl_threshold  = config.getint('bley', 'dnsbl_threshold')
    settings.rfc_threshold    = config.getint('bley', 'rfc_threshold')
    settings.greylist_period  = datetime.timedelta(0, config.getint('bley', 'greylist_period')*60, 0)
    settings.greylist_max     = datetime.timedelta(0, config.getint('bley', 'greylist_max')*60, 0)
    settings.greylist_penalty = datetime.timedelta(0, config.getint('bley', 'greylist_penalty')*60, 0)
    settings.purge_days       = config.getint('bley', 'purge_days')
    settings.purge_bad_days   = config.getint('bley', 'purge_bad_days')

    if settings.debug:
        settings.log_file = None

    if settings.log_file == 'syslog':
        import syslog
        syslog.openlog('bley', syslog.LOG_PID, syslog.LOG_MAIL)
        settings.logger = syslog.syslog
    elif settings.log_file in ['-', '', None]:
        settings.logger = sys.stdout.write
    else:
        settings.logger = open(settings.log_file, 'a').write

    db = settings.database.connect(**settings.dbsettings)
    dbc = db.cursor()
    if settings.dbtype == 'pgsql':
        dbc.execute(__CHECK_DB_QUERY_PG)
        if not dbc.fetchall():
            dbc.execute(__CREATE_DB_QUERY_PG)
    else:
        dbc.execute(__CREATE_DB_QUERY)
    db.commit()
    dbc.close()
    db.close()

    settings.db = settings.database.connect(**settings.dbsettings)

    reactor.listenTCP(settings.listen_port, BleyPolicyFactory(settings), interface=settings.listen_addr)
    reactor.addSystemEventTrigger('before', 'shutdown', bley_stop, settings)
    reactor.callWhenRunning(clean_db, settings)
    if not settings.debug:
        twistd.checkPID(settings.pid_file)
        twistd.daemonize()
        if settings.pid_file:
            writePID(settings.pid_file)
    reactor.run()

def bley_stop(settings):
    if settings.pid_file and not settings.debug:
        delPID(settings.pid_file)
    if settings.log_file == 'syslog':
        syslog.closelog()

def writePID(pidfile):
    # Create a PID file
    pid = str(os.getpid())
    pf = open(pidfile, "w")
    pf.write("%s\n" % pid)
    pf.close()

def delPID(pidfile):
    if os.path.exists(pidfile):
        os.remove(pidfile)

def clean_db(settings):
    if settings.verbose:
        settings.logger("cleaning database")
    db = settings.database.connect(**settings.dbsettings)
    dbc = db.cursor()
    now = datetime.datetime.now()
    old = now - datetime.timedelta(settings.purge_days, 0, 0)
    old_bad = now - datetime.timedelta(settings.purge_bad_days, 0, 0)
    p = {'old': str(old), 'old_bad': str(old_bad)}
    dbc.execute(__CLEAN_DB_QUERY, p)
    db.commit()
    dbc.close()
    db.close()
    reactor.callLater(30*60, clean_db, settings)

bley_start()
