#! /usr/local/bin/python

# Copyright (C) 2003 by the Free Software Foundation, Inc.
#
# 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.

"""Provide an authentication wrapper around htdig's htsearch when
it is running on a different machine to Mailman.

This relies on htsearch not normally being able to access per list htdig 
conf files for list archives without the intervention of this script,
which inserts a 'CONFIG_DIR' environment variable to enable that acccess.

The security available with this script is limited as it cannot
consult the user authentication information held by Mailman. Instead
we rely on the mmsearch CGI script on the Mailman server doing that and 
then passing the request to this script using an HTTP request. 

The only protection against malicious request to this script is to restrict
this script to responding only when an HTTP request originates from a machine
with a particular IP number: e.g. we only allow requests from our 'trusted'
MM server.
"""

# Edit the following configuration variables to suit your installation
#
# For example:
#
#MAILTO = 'mailman@mailman.yourdomain.com'
#VALID_IP_LIST = ['192.168.1.111']
#HTDIG_CONF_LINK_DIR = '/mailman/run/archives/htdig'
#HTDIG_HTSEARCH_PATH = '/opt/www/htdig/bin/htsearch'

MAILTO = ''
VALID_IP_LIST = []
HTDIG_CONF_LINK_DIR = ''
HTDIG_HTSEARCH_PATH = ''

# End of things for you to edit

import sys
import os
import cgi
import re
import popen2
import exceptions
import types
import httplib
import urllib
import urlparse

errors = {'cgi': 'CGI problem.',
          'info': 'Path info.',
          'list': 'The requested list cannot be accessed.',
          'htsearch': 'htearch failed',
          'auth': 'Authentication failure.',
         }

class _search_exception(exceptions.Exception):
    
    def __init__(self, listname, reason, detail):
        self.listname = listname
        self.reason = reason 
        self.detail = detail

    def __str__(self):
        return 'listname: %s, reason: %s' % (self.listname, self.reason)
    	

def true_path(path):
    "Ensure that the path is safe by removing .."
    path = path.replace("../", "")
    path = path.replace("./", "")
    return path[1:]

def GetPathPieces(envar='PATH_INFO'):
    path = os.environ.get(envar)
    if path:
        return [p for p in path.split('/') if p]
    return None

def make_inserts(listname):
    return {
            'mailto': MAILTO,
            'listname': listname,
            'referer': os.environ.get('HTTP_REFERER', 'Referer not known'),
            'uri': os.environ.get('REQUEST_URI', 'URI not known'),
           }

def error_quit(why):
    d = make_inserts(why.listname)
    d['error'] = errors[why.reason] + ' ' + why.detail
    print """\
Content-type: text/html

<HTML>
<HEAD>
    <TITLE>htdig Archives Access Failure</TITLE> 
</HEAD>
<BODY BGCOLOR="#ffffff">
<H1>htdig Archives Access Failure</H1> 
%(error)s
<P>
    Searching the archives of list %(listname)s failed.
</P>
<P>
    If this problem persists then please e-mail the following information to the 
<A HREF="mailto:%(mailto)s">%(mailto)s</A>:
</P>
<PRE>
    %(referer)s
    %(uri)s
</PRE>
<HR>
</BODY>
</HTML>
""" % d
    sys.exit(0)

_required_fields = ('method',
                    'format',
                    'sort',
                    'config',
                   )

_allowed_fields = {'method': 0,
                   'format': 0,
                   'sort': 0,
                   'config': 1,
                   'words': 0,
                   'submit': 0,
                   'restrict': 0,
                   'exclude': 0,
                   'page': 0,
                  }

def check_params(listname, fs, detail):
    detail = detail + ' fields: ' + ','.join(fs.keys())
    for fieldname in _required_fields:
        if not fs.has_key(fieldname):
            raise _search_exception(listname, 'cgi',  '-5-' + detail)
    fieldhash = {}
    for fieldname in fs.keys():
        if not _allowed_fields.has_key(fieldname):
            raise _search_exception(listname, 'cgi', '-6- ' + detail)
        if type(fs[fieldname]) is types.ListType:
            raise _search_exception(listname, 'cgi', '-8- ' + detail)        
        fieldhash[fieldname] = fs[fieldname].value
    return urllib.urlencode(fieldhash)

def main():
    try:
        try:
            request_method = os.environ['REQUEST_METHOD']
            form = cgi.FieldStorage()
        except:
            raise _search_exception('', 'cgi', 'No list -1-')
        list_info = GetPathPieces()
        if not list_info or len(list_info) != 1:
            raise _search_exception('', 'info', 'No list -2-')
        path_listname = list_info[0].lower()
        # Check right number of expected fields and get them
        # url encoded
        params = check_params(path_listname, form, 'Field count -4-')
        # Extract the listname from the form
        listname = form['config'].value
        if listname.endswith('.htsearch'):
            listname = listname[:-len('.htsearch')]
        # Access the list OK?
        if path_listname != listname:
            raise _search_exception(listname, 'list', '%s:%s' % \
                                    (path_listname, listname))
        # We check to origin IP of the request if restrictions are specified
        if len(VALID_IP_LIST):
            try:
                request_bits = os.environ['REMOTE_ADDR'].split('.')
                for ip in VALID_IP_LIST:
                    valid_bits = ip.split('.')
                    for i in range(4):
                    	if request_bits[i] != valid_bits[i]: 
                            break
                    else:
                    	break
                else:
                    raise _search_exception(listname, 'auth', ' -10- %s' % \
                                            os.environ['REMOTE_ADDR'])
            except _search_exception:
                raise
            except:
                raise _search_exception(listname, 'auth', ' -12-')
        # We are going to call execute htsearch and return its response.
        # We need to let htsearch get at the list specific htdig
        # conf file
        if request_method == 'POST':
            os.environ['CONTENT_LENGTH'] = str(len(params))
        else:
            raise _search_exception(listname, 'auth', ' -11- method')
        os.environ['CONFIG_DIR'] = HTDIG_CONF_LINK_DIR
        cmd = HTDIG_HTSEARCH_PATH
        child = popen2.Popen3(cmd)
        child.tochild.write(params)
        child.tochild.close()
        response = ''
        while (1):
            data = child.fromchild.read()
            if data == "": 
                break
            response += data
        exitstatus = child.wait()
        exitstatus = (exitstatus >> 8) & 0xff
        if exitstatus:
            raise _search_exception(listname, 'htsearch', ' -12-  exit: %d' \
                                    % existatus, )
        if not response:
            raise _search_exception(listname, 'htsearch', ' -13-') 
        print response
    except _search_exception, e:
        error_quit(e)
    sys.exit(0)

if __name__ == '__main__' and \
   MAILTO and \
   os.path.isdir(HTDIG_CONF_LINK_DIR) and \
   os.access(HTDIG_HTSEARCH_PATH, os.X_OK):
    main()
else:
    error_quit(_search_exception('', 'cgi', '-14- misconfigured'))
