#!/usr/bin/python

# tz_http_server.py
#       --copyright--                   Copyright 2007 (C) Tranzoa, Co. All rights reserved.    Warranty: You're free and on your own here. This code is not necessarily up-to-date or of public quality.
#       --url--                         http://www.tranzoa.net/tzpython/
#       --email--                       pycode is the name to send to. tranzoa.com is the place to send to.
#       --bodstamps--
#       December 13, 2007       bar
#       December 14, 2007       bar     filter out errors 'cause the client disconnected or whatever
#       December 15, 2007       bar     yet another exception grab
#                                       test at high speed
#       December 16, 2007       bar     showing exceptions nicer and optional
#       December 20, 2007       bar     allow single threading
#       December 23, 2007       bar     fix an exception exception
#       March 16, 2008          bar     allow ambiguous host name, ""
#       March 18, 2008          bar     exception class and throwing it at __init__ time
#       May 17, 2008            bar     email adr
#       August 29, 2008         bar     try to find out how to stop aggressive Firefox hits to us left as a legacy at close time
#       September 4, 2008       bar     name our thread
#       November 29, 2009       bar     helper http response routines
#       December 1, 2009        bar     response number option for send_404()
#                                       cache_control option to re-named send_http_headers()
#       December 7, 2009        bar     cache_control option for send_404()
#       January 31, 2010        bar     print that an exception that's handled has been handled
#       February 18, 2010       bar     get_local_ip_address()
#       March 31, 2010          bar     catch exception when we don't know our own host name
#       March 25, 2011          bar     notes about javascript produced query string values with %uXXXX in them instead of, say, utf8'ing them and then using %XX or whatever
#       August 3, 2011          bar     safe_path()
#       September 29, 2011      bar     OPTIONS handled by do_OPTIONS() routine
#                                       in safe_path() get rid of ?args and #frags
#       October 10, 2011        bar     get_remote_address_name()
#       October 30, 2011        bar     handler.parse_args, handler.qs handler.args and decode the query string as utf8, first choice
#       October 31, 2011        bar     simpler raise
#       November 21, 2011       bar     add IOError and OSError to try to find an IOError in a multi-threaded program
#       November 29, 2011       bar     pyflake cleanup
#       --eodstamps--
##      \file
#
#
#
#       Here's an idea:
#
#           Let's create code that's already been done, better, any number of places.
#
#
#       But what the heck.
#
#       This code provides a framework to call a routine (do_http_args()) in the owner's code whenever an HTTP HEAD, GET, POST, or OPTIONS comes in.
#
#       The server is multi-threaded.
#
#       Parsing of the FORM data is done for the callback routine, do_http_args(), which can be overridden by a sub-class or be stand-alone.
#
#

import  BaseHTTPServer                  # web server logic
import  SocketServer                    # generic TCP/UDP socket server stuff
import  cgi                             # handy, dandy "cgi" (parsing and such) routines
import  re                              # regular expression routines
import  socket                          # fundamental socket stuff
import  struct                          # low-level data manipulation logic
import  sys                             # handy, dandy system routines
import  threading                       # multi-threading stuff
import  time                            # handy, dandy time/date routine
import  urllib                          # routines to do things with URLs - we decode escaped characters to get 'em back in to straight, proper text
import  urlparse                        # routines to parse URLs - we split up the URL to get the form input data, for instance

try :
    import  fcntl
except ImportError :
    fcntl   = None

import  tzlib



def get_local_ip_address() :
    try :
        ipadr           = socket.gethostbyname(socket.gethostname())
    except socket.gaierror :
        ipadr           = "127.0.0.1"
    if  ipadr.startswith('127.') :
        if  fcntl   :
            s   = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
            for ethi in range(10) :
                try :
                    ifn = "eth%u" % ethi
                    adr = socket.inet_ntoa(fcntl.ioctl(s.fileno(), 0x8915, struct.pack('256s', ifn[:15]))[20:24])
                    if  not adr.startswith('127.') :
                        ipadr   = adr
                        break
                    pass
                except IOError :
                    pass
                pass
            s.close()
            del(s)
        else :
            try :
                for res in socket.getaddrinfo('', 41419, socket.AF_UNSPEC, socket.SOCK_STREAM) :
                    ( af, socktype, proto, canonname, sa ) = res
                    if  sa[0] and not sa[0].startswith("127.") :
                        ipadr       = sa[0]
                        break
                    pass
                pass
            except  socket.gaierror :
                pass
            pass
        pass

    return(ipadr)




class   an_ip_adr(object) :
    def __init__(me, ip_adr, name) :
        me.ip_adr   = ip_adr    or ""
        me.name     = name      or ""
    #   an_ip_adr


def get_remote_address_name(ip_adr) :
    """ Given an IP address, try to find its name. Return an array of an_ip_adr's, the first one of which is best. """

    try :
        ( hn, aliases, ipadrs ) = socket.gethostbyaddr(ip_adr)
    except ( IOError, OSError, socket.error, socket.herror, socket.gaierror, socket.timeout ) :
        hn      = None
        aliases = []
        ipadrs  = []

    if  hn      :
        aliases = [ hn,     ]
        ipadrs  = [ ip_adr, ]
    a           = [ an_ip_adr(ipadrs[i], aliases[i]) for i in xrange(min(len(ipadrs), len(aliases))) ]

    return(a)



#
#               This is the machine you're running on
#               It's used by the browser and server for obscure reasons (e.g. virtual hosting).
#               Specifically, localhost allows only the PC we're running on to access us.  "" is how to be ambiguous.
#
HOST_NAME   = 'localhost'

#
#               This is the normal, default web/http port, 80.
#               If you change it,
#                 the urls must change to something like,
#                 say, "http://localhost:12345/blah",
#                 where 12345 is the port number.
#
PORT_NUMBER = 80




SHOW_EXECEPTIONS    = True



def show_file_and_line() :
    import  linecache

    etype, value, tb = sys.exc_info()

    while tb is not None :
        f = tb.tb_frame
        lineno = tb.tb_lineno
        co = f.f_code
        filename = co.co_filename
        name = co.co_name
        pln = '  File "%s", line %d, in %s' % (filename,lineno,name)
        linecache.checkcache(filename)
        line = linecache.getline(filename, lineno)
        if line: pln += "\n" + '    ' + line.strip()
        tb = tb.tb_next
        print pln

    print pln



def safe_path(pth) :
    """
        Convert the given path to a safe-to-use-on-local-drive path.
        The path ends up being restricted to sub-directories and with on A-Z a-z 0-9 _ . - characters leading with only a-z A-Z.
    """

    pth = re.sub(r"\?.*",                   "",     pth)            # get rid of ?args...
    pth = re.sub(r"\#.*",                   "",     pth)            # get rid of frag
    pth = re.sub(r"\\",                     "\/",   pth)            # convert to unix slashes
    pth = re.sub(r"[^/]+/+\.\.",            "",     pth)            # get rid of blah/..
    pth = re.sub(r"^/+?\.+/+",              "",     pth)            # get rid of leading /./ and ./ (should do multiple ones !!!! )
    pth = re.sub(r"^(/+?\.+/+)+",           "",     pth)            #                                ok
    pth = re.sub(r"^.*:",                   "",     pth)            # get rid of WinDos disk drives (should get rid of all colons !!!! )
    pth = re.sub(r".*:",                    "",     pth)            #                                ok
    pth = re.sub(r"/+",                     "\/",   pth)            # make multiple slash singletons
    pth = re.sub(r"[^A-Za-z0-9_\./\-]+",    "",     pth)            # whack everything but a-z 0-9 _ . -
    pth = re.sub(r"^[^A-Za-z]+",            "",     pth)            # whack any leading non a-z

    return(pth)



def best_decode(s) :
    if  not isinstance(s, unicode) :
        try :
            s       = s.decode('utf8')
        except UnicodeDecodeError :
            try :
                s   = s.decode('latin1')
            except UnicodeDecodeError :
                pass
            pass
        pass

    return(s)




class a_handler(BaseHTTPServer.BaseHTTPRequestHandler):

    def send_http_headers(me, is_head = False, content_type = None, cache_control = None, response = 200, hdrs = {}) :
        if  (content_type  == None) or (content_type != "") :
            content_type    = content_type  or "text/html"

        if  (cache_control == None) or (cache_control != "") :
            cache_control   = cache_control or "no-store"

        response            = response      or 200

        try :
            me.send_response(response)
            if  cache_control :
                me.send_header("Cache-control", cache_control)
            if  content_type :
                me.send_header("Content-type",  content_type)
            for hdr in hdrs.keys() :
                me.send_header(hdr, hdrs[hdr])
            me.end_headers()
        except ( IOError, OSError, socket.error, socket.herror, socket.gaierror, socket.timeout ) :         # can happen if he hits the refresh button
            tzlib.print_exception()
            print "Exception has been handled"
            is_head = True

        return(not is_head)


    def write_data(me, htm) :
        try :
            me.wfile.write(htm)
        except ( IOError, OSError, socket.error, socket.herror, socket.gaierror, socket.timeout ) :
            pass
        pass


    def send_404(me, is_head = False, htm = "", cache_control = None, response = 404) :
        response    = response or 404
        htm         = htm      or "I'm sorry, Dave. ... I'm afraid I can't do that."
        if  me.send_http_headers(is_head, cache_control = cache_control, response = response) :
            me.write_data("""<html><body>%s</body></html>""" % htm )
        # print "Response: 404 [%s] [%s]" % ( me.path, htm )
        pass


    def do_http_args(me, server_httpd, me_handler, args, is_head = False) :
        """
            This routine is to be overloaded by the owner of a_threaded_http_server object.
        """

        if  False :
            print "a_handler.do_http_args"

            print "server", server_httpd

            if  me != me_handler :
                print "me!=me_handler"

            print "me", me
            print "me_handler", me_handler

            print args

        if  me.send_http_headers(is_head) :
            me.wfile.write("<HTML><BODY>tz_http_server.a_handler - " + tzlib.safe_html(me.path) + "<BR>\n" + tzlib.safe_html(str(args)) + "</BODY></HTML")

        return(True)




    def log_request(me, response_code, size = 0) :       # we override the default log_request() routine so that the default routine doesn't get a chance to print a log entry to the console.
        #       response_code   is, for instance, 200
        #       size            is either defaulted or '-' in the python library code I see
        pass


    def parse_args(me)      :
        me.qs               = ""
        qspos               = me.path.find('?')
        if  qspos          >= 0 :
            me.qs           = me.path[qspos + 1 : ]

        ctype, pdict        = cgi.parse_header(me.headers.getheader('content-type', ''))
        if  ctype          == 'multipart/form-data' :
            me.qs           = best_decode(me.qs)
            args            = cgi.parse_qs(me.qs, keep_blank_values = 1)
            fargs           = cgi.parse_multipart(me.rfile, pdict)
            for k in tzlib.make_dictionary(args.keys() + fargs.keys()).keys() :
                args[k]     = args.get(k, []) + fargs.get(k, [])
            pass
        else                :
            if  ctype      == 'application/x-www-form-urlencoded':
                length      = int(me.headers.getheader('content-length', -1))
                if  length >= 0 :
                    qs      = me.rfile.read(length)
                else        :
                    qs      = me.rfile.read()
                if  me.qs and qs and (qs[0] != '&') :
                    me.qs  += '&'
                me.qs      += qs
            me.qs           = best_decode(me.qs)
            args            = cgi.parse_qs(me.qs, keep_blank_values = 1)

        me.args             = args

        return(args)



    def do_HEAD(me):
        me.do_it(me.parse_args(), True)


    def do_OPTIONS(me):
        me.do_it(me.parse_args())


    def do_POST(me) :                                # some logic comes from http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/440665
        me.do_it(me.parse_args())


    def do_GET(me) :
        if  False :
            scheme, host, path, parameters, query, fragment = urlparse.urlparse(me.path)

            args                = {}
            for pair in query.split("&") :
                if  pair.find("=") >= 0 :
                    key, value  = pair.split("=", 1)
                    value       = urllib.unquote_plus(re.sub(r"%u([0-9a-fA-F]{4})", lambda g : unichr(int(g.group(1), 16)), value))         # can return a unicode string. Javascript creates these %uXXXX characters and cgi.parse_qs (aka urlparse.parse_qs aka urlparse.unquote) doesn't handle them
                    args.setdefault(key, []).append(value)
                else :
                    args[pair]  = []
                pass
            pass

        me.do_it(me.parse_args())


    def do_it(me, args, is_head = False) :

        # print me.headers['host']
        # print dir(me)
        # print me.headers

        # print "cmd", me.command            # me.command is "GET", in practice - or "POST" if a normal form were used.
        # print "path", me.path              # me.path is the whole URL after the "http://localhost"

        try :
            if  hasattr(me.server, 'do_http_args') :
                if not me.server.do_http_args(me.server, me, args, is_head) :
                    me.do_http_args(          me.server, me, args, is_head)
                pass
            else :
                    me.do_http_args(          me.server, me, args, is_head)
            pass
        except socket.error :
            me.server.error_count  += 1
            if  SHOW_EXECEPTIONS and me.server._show_exceptions :
                e   = sys.exc_info()
                print "tz_http_server.do_it: Socket Error %s %s" % ( str(e[0]), str(e[1]) )
                show_file_and_line()
            pass
        except :                                                                    # until i can figure out how to trap such errors as "error: (10054, 'Connection reset by peer')" !!!!
            me.server.error_count  += 1
            if  SHOW_EXECEPTIONS and me.server._show_exceptions :
                e   = sys.exc_info()
                print "tz_http_server.do_it: Error %s %s" % ( str(e[0]), str(e[1]) )
                show_file_and_line()
            pass
        pass

        if  True :
            try :
                me.connection.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack('ii', 0, 0))
                me.connection.shutdown(socket.SHUT_RDWR)
                me.connection.close()
            except socket.error :
                pass                                                                # at quit-program time?
            except :
                e   = sys.exc_info()
                print "tz_http_server.do_it: Error %s %s" % ( str(e[0]), str(e[1]) )
                show_file_and_line()
            pass
        pass


    pass        # a_handler




#
#
#       This is the same as a BaseHTTPServer.HTTPServer except that we use ThreadingTCPServer.   (note that FireFox doesn't connect to the same URL twice, from two tabs or two windows)
#
#
class an_http_server(SocketServer.ThreadingTCPServer):

    #
    #       We'll override the routines that need changing.
    #

    allow_reuse_address = 1                     # sometimes, if this is 0, then as long as FF is still running, the server socket stays listening after program shutdown and we can't restart the program and listen on the port

    def server_bind(me):
        # print "\n".join(dir(socket))
        SocketServer.ThreadingTCPServer.server_bind(me)
        # me.socket.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack('ii', 0, 0))            does not help that it does not shut down in linux
        host, port      = me.socket.getsockname()[:2]
        me.server_name  = socket.getfqdn(host)
        me.server_port  = port
        me.error_count  = 0

    def handle_error(me, request, client_address) :
        """ This routine is called by the Python library code when any exception happens. It normally prints a traceback stack dump. """
        me.error_count += 1
        if  SHOW_EXECEPTIONS and me._show_exceptions :
            print client_address
            e   = sys.exc_info()
            print "tz_http_server.handle_error: Error %s %s" % ( str(e[0]), str(e[1]) )
            show_file_and_line()
        pass

    pass    # an_http_server




class a_single_threaded_http_server(an_http_server):

    #
    #       We'll override the routines that need changing.
    #

    def server_bind(me):
        SocketServer.TCPServer.server_bind(me)
        host, port      = me.socket.getsockname()[:2]
        me.server_name  = socket.getfqdn(host)
        me.server_port  = port
        me.error_count  = 0

    pass    # a_single_threaded_http_server



class   a_threaded_http_server(threading.Thread) :

    class   exception(Exception) :
        """ Named exception """
        pass



    def do_http_args(me, me_httpd, handler, args, is_head = False) :
        """
            This is here so that this routine can be overloaded by some sub-class.
        """

        if  False :
            print "a_threaded_http_server.do_http_args"

            print "me_httpd", me_httpd

            if  me.me_httpd != me_httpd :
                print "me!=me_httpd"

            print "me", me
            print "handler", handler

            print args


        if  handler.send_http_headers(is_head) :
            handler.wfile.write("<HTML><BODY>tz_http_server.a_threaded_http_server - " + tzlib.safe_html(handler.path) + "<BR>\n" + tzlib.safe_html(str(args)) + "</BODY></HTML")

        return(True)




    def set_do_http_args(me, do_http_args = None) :
        ov                          = None

        httpd                       = me.httpd
        if  httpd :
            ov                      = me.do_http_args
            if  do_http_args        :
                httpd.do_http_args  = do_http_args
            else :
                httpd.do_http_args  = a_threaded_http_server.do_http_args
            pass

        return(ov)



    def __init__(me, host = None, port = None, do_http_args_rtn = None, threaded = True, show_exceptions = True, *args, **kwargs) :

        kwargs['name']  = kwargs.get('name', "tz_http_server.a_threaded_http_server")
        super(a_threaded_http_server, me).__init__(args = args, kwargs = kwargs)

        me.closed               = False

        if  host == None :
            host                = HOST_NAME
        # host                    = host or HOST_NAME       # "" is an ambiguous server and that's what we want if we want to be exposed to everyone. Otherwise, 'localhost' restricts inbound to us (might be ZoneAlarm is involved)
        port                    = port or PORT_NUMBER

        me._show_exceptions     = show_exceptions

        me.setDaemon(True)

        try :
            if  threaded :
                me.httpd                = an_http_server((host, port), a_handler)
                me.httpd.daemon_threads = True
            else :
                me.httpd                = a_single_threaded_http_server((host, port), a_handler)
            pass
        except socket.error :
            raise

        me.set_do_http_args(do_http_args_rtn or me.do_http_args)




    def close(me) :
        me.closed       = True
        if  hasattr(me, 'httpd') :
            httpd       = me.httpd
            me.httpd    = None
            if  httpd :
                if  True :
                    try :
                        httpd.socket.shutdown(socket.SHUT_RDWR)                                 # under Linux this let's us restart the program even though external FF is still running
                    except socket.error :
                        pass                                                                    # under Windows this triggers the exception
                    # httpd.socket.close()
                httpd.server_close()
            pass
        pass


    def run(me) :
        cnt = 0
        while True :
            if  me.closed :
                pass # break
            httpd   = me.httpd
            if  not httpd :
                break


            httpd._show_exceptions  = me._show_exceptions
            try :
                try :
                    httpd.handle_request()
                    cnt    += 1
                    # print "http", cnt
                except socket.error :
                    httpd.error_count  += 1
                    if  SHOW_EXECEPTIONS and httpd._show_exceptions :
                        e   = sys.exc_info()
                        print "tz_http_server.run: Socket Error %s %s" % ( str(e[0]), str(e[1]) )
                        show_file_and_line()
                    pass
                except :                                                                        # until i can figure out how to trap such errors as "error: (10054, 'Connection reset by peer')" !!!!
                    httpd.error_count  += 1
                    if  SHOW_EXECEPTIONS and httpd._show_exceptions :
                        e   = sys.exc_info()
                        print "tz_http_server.run: Error %s %s" % ( str(e[0]), str(e[1]) )
                        show_file_and_line()
                    pass
                pass
            except :
                httpd.error_count  += 1
                if  SHOW_EXECEPTIONS and httpd._show_exceptions :
                    e   = sys.exc_info()
                    print "tz_http_server.run: Meta-error %s %s" % ( str(e[0]), str(e[1]) )     # when shutting down, socket may have gone away and socket.error will cause an exception
                    show_file_and_line()
                pass
            pass

        me.close()


    def stop(me) :
        me.close()



    def __del__(me) :                               # this whole dance is here 'cause some time while developing the code I ate an XP winsock server socket and could never be a server on that socket's port again. Maybe this will help that.
        me.close()
        if  hasattr(threading.Thread, '__del__') :
            threading.Thread.__del__(me)
        pass



    def show_exceptions(me, show_exceptions = None) :
        ov  = me._show_exceptions
        if  show_exceptions != None :
            me.show_exceptions  = show_exceptions

        return(ov)



    pass            # a_threaded_http_server




#
#
#
#
#
if __name__ == '__main__':


    import  url_getter


    timeout = 10.0

    def do_http_args(httpd, handler, args, is_head = False) :
        """
            A stand-alone routine like this one could be used, too.
        """

        if  True :
            print "stand alone do_http_args"
            print "server", httpd
            print "handler", handler, handler.path

            print args

        t   = tzlib.elapsed_time()
        while (tzlib.elapsed_time() - t < timeout) :
            time.sleep(1)

        if  handler.send_http_headers(is_head) :
            handler.wfile.write("<HTML><BODY>tz_http_server stand-alone - " + tzlib.safe_html(handler.path) + "<BR>\n" + tzlib.safe_html(str(args)) + "</BODY></HTML")

        return(True)




    host            = HOST_NAME
    port            = PORT_NUMBER
    if  len(sys.argv) > 1 :
        port        = int(sys.argv[1])                      # first command line parameter, if there, is the port number

    try :
        me              = a_threaded_http_server(host, port, do_http_args_rtn = do_http_args)
    except socket.error :
        print
        print "Perhaps we can't serve this port?"
        print

        raise

        e   = sys.exc_info()
        print "Error %s %s" % ( str(e[0]), str(e[1]) )

        sys.exit(101)


    me.start()

    # timeout = 0.0
    i       = 1
    while True :
        print "again"
        r   = url_getter.url_open_read_with_timeout("http://" + host + ":" + str(port) + "/%u" % i)
        print r
        time.sleep(timeout / 10)
        i  += 1

    me.close()


#
#
#
# eof

