'''
webq.py - An service for sagator's quarantine over HTTP.

(c) 2005-2007 Jan ONDREJ (SAL) <ondrejj(at)salstar.sk>

 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.

'''

import re, db, gettext, avlib
from aglib import *
from av.basic import const
from avlib import trans_class, _

from BaseHTTPServer import HTTPServer
from SimpleHTTPServer import SimpleHTTPRequestHandler
from cgi import parse_qs
from crypt import crypt
import base64, binascii

__all__=['webq']

class condition:
  def __init__(self, WA):
      self.WA=WA
  def like(self, key, value):
      if not value:
        return None
      if value[:1]=='!':
        self.WA.append(value[1:])
        return key+" NOT LIKE %s"
      else:
        self.WA.append(value)
        return key+" LIKE %s"
  def date(self, key, value, dir):
      if not value:
        return None
      self.WA.append(value)
      return "%s %s %%s" % (key, dir)
  def eq(self, key, user):
      if not user:
        return None
      if user[:1]=='!':
        oper="!="
        user=user[1:]
      else:
        oper="="
      self.WA.append(user)
      return key+oper+'%s'

class webq(ServiceTCPServer,HTTPServer):
  '''
  Web service for sagator's quaratine access.
  
  This service can be used to access email collected by sagator via
  web interface.
  
  Requirements: python-kid-0.9.5 or higher

  Usage: webq(host='0.0.0.0', port=8008, db, log='/var/log/sagator/webq.log',
              scanner, userconv)

  Where: host is an string, which defines IP address to bind,
           default: 0.0.0.0
         port is an integer, which defines tcp port to listen, default: 8008
         db is a database connection. For description see Databases.txt.
         log is defining a log file name, by default /var/log/sagator/webq.log
         scanner is a scanner to use for checking (only one scanner
           can be used here and it must be a buffer scanner!)
         userconv is an array, which defines regular expression
           and substitution strings. Usernames from login prompt
           are marched against this regular expression and substitued
           by substitution string.
  
  It is recommended to use apache mod_proxy module to redirect standard
  web traffic from port 80 to webq()'s 8008. For example:
      ProxyPass /webq http://localhost:8008
      ProxyPassReverse /webq http://localhost:8008

  Example: See default config file for example.

  New in version 0.9.0.
  '''
  name='webq()'
  def __init__(self, host='0.0.0.0', port=8008, db=None,
                     log='/var/log/sagator/webq.log',
                     scanner=const(0),
                     userconv=['^(.*)$','\\1']):
      self.BINDTO=(host, port)
      self.LOGFILE=log
      self.SCANNERS=[scanner]
      self.WORK_DIR=os.path.dirname(os.path.abspath(avlib.__file__))
      self.WEB_ROOT=os.path.join(self.WORK_DIR, 'srv/web')
      self.REQUEST_HANDLER=webq_request_handler
      self.REQUEST_HANDLER.ENV={
        'gettext': _,
        'DB': db,
        'SCANNER': scanner,
        'REQUESTS': {},
        'REQ': {},
        'LANGS': ['en_US', 'sk_SK'],
        'condition': condition,
        'check_passwd': self.check_passwd,
        'change_passwd': self.change_passwd
      }
      import kid
      # email parser is required to run in chroot
      import email
      email.message_from_string('') # ignore output, just load module
      # enable KID import
      kid.enable_import()
      self.REQUEST_HANDLER.kid = kid
      self.REQUEST_HANDLER.USERCONV_REG = re.compile(userconv[0])
      self.REQUEST_HANDLER.USERCONV_REPL = userconv[1]
      try:
        import cracklib
      except ImportError, err:
        try:
          # debian hack to load renamed cracklib
          import crack as cracklib
        except ImportError:
          cracklib = None
          debug.echo(1, "webq(): Cracklib import failed (%s), "
                        "disabling cracklib functionality." % err)
      self.cracklib = cracklib
      if cracklib:
        self.cracklib_trans = trans_class('cracklib')
  def test_scanners(self, scanner):
      ServiceTCPServer.test_scanners(self, self.SCANNERS)
      # copy all templates
      import shutil
      try:
        import srv.web
      except ImportError:
        debug.echo(1, 'webq(): ERROR: webq not installed, '
                      'please install sagator-webq package')
        raise
      webqsource=os.path.abspath(os.path.dirname(srv.web.__file__))
      webqtarget=safe.fn(self.WEB_ROOT)
      if webqsource!=webqtarget:
        try:
          if not os.path.isdir(webqtarget):
            debug.echo(9, "Making webq chroot directory: ", webqtarget)
            os.makedirs(webqtarget)
          for f in os.listdir(webqsource):
            if os.path.isfile(os.path.join(webqsource, f)):
              debug.echo(9, "Copying webq file: ", f)
              shutil.copy(os.path.join(webqsource, f),
                          os.path.join(webqtarget, f))
        except OSError, e:
          debug.echo(9, "OSError: %s" % e)
  def serve_forever(self):
      if safe.fn('/usr')=='/usr':
        os.chdir(self.WORK_DIR)
      sys.path.insert(0, 'srv/web')
      import srv.web # reimport for chroot
      self.sighup(0,0) # redirect logs
      HTTPServer.serve_forever(self)
  def sighup(self,sn,stack):
      if debug.logfile=="-":
        return
      # reopen logs
      os.close(1)
      os.open(self.LOGFILE, os.O_CREAT|os.O_WRONLY|os.O_APPEND, 0640)
      os.dup2(1,2) # copy to stderr fd
  def check_passwd(self, DB, login, old):
      passwd, perms, lang, showrows = DB.query(
        "SELECT pass,perms,lang,showrows "
        "FROM webaccess WHERE email=%s",
        [login]
      )[0]
      if passwd!=crypt(old, passwd):
        return {}
      return {
        'REMOTE_LOGIN': login,
        'PERMS': perms,
        'LANG': lang or "en_US",
        'SHOW_ROWS': showrows or 50
      }
  def change_passwd(self, DB, login, old, new, retyped, lang='C'):
      if not new:
        return ""
      if not self.check_passwd(DB, login, old):
        return "Wrong password!"
      if self.cracklib:
        e = self.cracklib.FascistCheck(new)
        if e:
          self.cracklib_trans.set_lang(lang)
          return _('Password %s.') % self.cracklib_trans.gettext(e)
      if new != retyped:
        return "New and retyped passwords does not match!"
      DB.execute("UPDATE webaccess SET pass=%s WHERE email=%s",
                 [crypt(new, '$1$'+randomchars(8)), login])
      return "Password changed successfully."

class webq_request_handler(SimpleHTTPRequestHandler):
  def do_POST(self):
      return self.send_head()
  def send_head(self):
      path = self.path
      i = path.rfind('?')
      if i>=0:
        path, query = path[:i], path[i+1:]
      else:
        query = ''
      if not path.strip('/'):
          path = '/index.kid'
      scriptfile = self.translate_path("srv/web/"+path)
      if not os.path.exists(scriptfile):
          self.send_error(404, "No such Kid template (%r)" % scriptfile)
          return
      if not os.path.isfile(scriptfile):
          self.send_error(403, "Kid template is not a plain file (%r)" % path)
          return
      ns=self.ENV.copy()
      if query:
        ns['QUERY_STRING'] = query
        ns['REQUESTS'].update(parse_qs(query, True))
      length = int(self.headers.getheader('content-length') or '0')
      if length > 0:
        data=self.rfile.read(length)
        ns['REQUESTS'].update(parse_qs(data, True))
      for key,value in ns['REQUESTS'].items():
        ns['REQ'][key]=value[0].decode('UTF-8')
      ns['CONTENT_LENGTH'] = length
      ns['REMOTE_ADDR'] = self.client_address[0]
      ns['REMOTE_USER'] = ''
      ns['REMOTE_LOGIN'] = ''
      ns['PERMS'] = ''
      ns['LANG'] = 'C'
      ns.update(self.check_auth())
      ns['gettext'].set_lang(ns['LANG'])
      if not ns.get('REMOTE_USER'):
        self.send_response(401, "Authorization Required")
        self.send_header("WWW-Authenticate", 'Basic realm="Restricted access"')
        self.end_headers()
        self.wfile.write("Authorization Required")
        return
      try:
        template_module = self.kid.load_template(scriptfile, cache=False)
        template = template_module.Template(**ns)
        s=template.serialize(
            output=self.kid.XHTMLSerializer(
                     decl=False,
                     doctype='xhtml-strict',
                     entity_map=True
                   )
          )
        self.send_response(200, "Script output follows")
        self.send_header("Content-type", "text/html; charset=UTF-8")
        self.send_header("Content-Length", str(len(s)))
        self.end_headers()
        self.wfile.write(s)
      except Exception, e:
        import traceback
        s=traceback.format_exc()
        self.send_response(500, "Internal server error!")
        self.send_header("Content-type", "text/plain; charset=UTF-8")
        self.send_header("Content-Length", str(len(s)))
        self.end_headers()
        self.wfile.write(s)
  def check_auth(self):
      '''
      Check authorization request.
      Return value is an empty dictionary, if authorization fails or an
      namespace update on success.
      '''
      env={}
      authorization = self.headers.getheader("authorization")
      if authorization:
          authorization = authorization.split()
          if len(authorization) == 2:
              env['AUTH_TYPE'] = authorization[0]
              if authorization[0].lower() == "basic":
                  try:
                      authorization = base64.decodestring(authorization[1])
                  except binascii.Error:
                      pass
                  else:
                      authorization = authorization.split(':')
                      if len(authorization) == 2:
                        env.update(self.check_pass(authorization))
      return env
  def check_pass(self, auth):
      '''
      Check user password. Parameter auth is an array of:
         ['login','plain text password']
      Return value is an empty dictionary, if authorization fails,
      or an namespace update on success.
      '''
      try:
        passwd,perms,lang,showrows = self.ENV['DB'].query(
          "SELECT pass,perms,lang,showrows FROM webaccess WHERE email=%s",
          auth[:1]
        )[0]
      except IndexError:
        return {} # no record found for this login
      cryptpass = crypt(auth[1], passwd)
      if passwd!=cryptpass:
        return {}
      return {
        'REMOTE_USER': self.USERCONV_REG.sub(self.USERCONV_REPL, auth[0]),
        'REMOTE_LOGIN': auth[0],
        'PERMS': perms,
        'LANG': lang or "en_US",
        'SHOW_ROWS': showrows or 50
      }
