# -*- Mode: Python -*-

# Copyright 1999 by eGroups, Inc.
# 
#                         All Rights Reserved
# 
# Permission to use, copy, modify, and distribute this software and
# its documentation for any purpose and without fee is hereby
# granted, provided that the above copyright notice appear in all
# copies and that both that copyright notice and this permission
# notice appear in supporting documentation, and that the name of
# eGroups not be used in advertising or publicity pertaining to
# distribution of the software without specific, written prior
# permission.
# 
# EGROUPS DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
# INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN
# NO EVENT SHALL EGROUPS BE LIABLE FOR ANY SPECIAL, INDIRECT OR
# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
# OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

VERSION_STRING = '$Id: //depot/main/findmail/src/coroutine/coro_ehttpd.py#13 $'

#
# coro_ehttpd
#   This is an infrastructure for having a http server using coroutines.
#   There are three major classes defined here:
#   http_client
#     This is a descendent of coro.Thread.  It handles the connection
#     to the client, spawned by http_server.  Its run method goes through 
#     the stages of reading the request, filling out a http_request and
#     finding the right handler, etc.
#   http_request
#     This object collects all of the data for a request.  It is initialized
#     from the http_client thread with the http request data, and is then
#     passed to the handler to receive data.  It attempts to enforce a valid
#     http protocol on the response
#   http_server
#     This is a thread which just sits accepting on a socket, and spawning
#     http_clients to handle incoming requests
#
#   Additionally, the server expects http handler classes which respond
#   to match and handle_request.  There is an example class, 
#   http_file_handler, which is a basic handler to respond to GET requests
#   to a document root.  It'll return any file which exists.
#
#   To use, implement your own handler class which responds to match and
#   handle_request.  Then, create a server, add handlers to the server, 
#   and start it.  You then need to call the event_loop yourself.  
#   Something like:
#
#     server = http_server(args = (('0.0.0.0', 7001),))
#     file_handler = http_file_handler ('/home/htdocs/')
#     server.push_handler (file_handler)
#     server.start()
#     coro.event_loop(30.0)
#

import os
import coro
import socket
import string
import sys
import time
import re

class http_client (coro.Thread):
  def __init__ (self, group=None, target=None, name=None, args=(), kwargs={}):
    self.lines = []
    self._bytes = 0
    coro.Thread.__init__ (self, group, target, name, args, kwargs)

  def run (self, conn, handlers):
    self.conn = conn
    request_line = self.read_line()
    headers = self.read_header()
    request = http_request (self, request_line, headers)
    if request._error:
      # Bad Request
      request.error (400)
      request.done ()
    else:
      try:
        handler = self.pick_handler (handlers, request)

        if handler:
          #print 'coro_ehttpd: calling handler'
          handler.handle_request (request)
        else:
          self.not_found (request)
      except:
        #print 'coro_ehttpd: no handler!'
        tb = coro.compact_traceback()
        self.log(coro.LOG_ERROR, tb)
        request.error (500, tb)
        tb = None

      if not request._done:
        request.done()

    sys.stderr.write ("%s" % request.log(self._bytes))
    return None

  def not_found (self, request):
    request.error (404)
    request.done ()
      
  def pick_handler (self, handlers, request):
    for handler in handlers:
      if handler.match (request):
        return handler

    #print 'coro_ehttpd: handler not found'
    return None

  # This is for handlers that process PUT/POST themselves.
  # This whole thing needs to be redone with a file-like
  # interface to 'stdin' for requests, and we need to think
  # about HTTP/1.1 and pipelining, etc...

  def read (self, size):
    prepend = ''
    if self.lines:
      prepend = string.join (self.lines, '\r\n')
      self.lines = []
    if self.buffer:
      prepend = prepend + self.buffer
      self.buffer = ''
      
    if len(prepend) >= size:
      result = prepend[:size]
      self.buffer = prepend[size:]
      return result
    else:
      buffer = self.conn.recv (size)
      result = prepend + buffer
      return result

  def read_line (self):
    if self.lines:
      l = self.lines[0]
      self.lines = self.lines[1:]
      return l
    else:
      while not self.lines:
        buffer = self.conn.recv (8192)
        lines = string.split (buffer, '\r\n')
        for l in lines[:-1]:
          self.lines.append (l)
        self.buffer = lines[-1]
      return self.read_line()

  def read_header (self):
    header = []
    while 1:
      l = self.read_line()
      if not l:
        break
      else:
        header.append (l)
    return header

  def send (self, data):
    olb = lb = len(data)
    while lb:
      ns = self.conn.send (data[olb-lb:])
      lb = lb - ns
    self._bytes = self._bytes + olb
    return olb

  def close (self):
    self.conn.close()

class http_request:
  request_count = 0
  # <path>;<params>?<query>#<fragment>
  path_re = re.compile ('([^;?#]*)(;[^?#]*)?(\?[^#]*)?(#.*)?')
  # <method> <uri> HTTP/<version>
  request_re = re.compile ('([^ ]+) ([^ ]+) *(HTTP/([0-9.]+))?')

  def __init__ (self, client, request, headers):
    self._reply_headers = {}
    self._reply_code = 200
    http_request.request_count = http_request.request_count + 1
    self._request_number = http_request.request_count
    self._request = request
    self._request_headers = headers
    self._client = client
    self._sent_headers = 0
    self._done = 0
    self._error = 0
    self._tstart = time.time()
    m = http_request.request_re.match (request)
    if m:
      (self._method, self._uri, ver, self._version) = m.groups()
      self._method = string.lower (self._method)
      if not self._version:
        self._version = "0.9"
      m = http_request.path_re.match (self._uri)
      if m:
        (self._path, self._params, self._query, self._frag) = m.groups()
        if self._query and self._query[0] == '?':
          self._query = self._query[1:]
      else:
        self._error = 1
    else:
      self._error = 1

  # --------------------------------------------------
  # reply header management
  # --------------------------------------------------
  def __setitem__ (self, key, value):
    self._reply_headers[key] = value

  def __getitem__ (self, key):
    return self._reply_headers[key]

  def has_key (self, key):
    return self._reply_headers.has_key (key)

  def push (self, s):
    if not self._sent_headers:
      self.send_headers()
    return self._client.send (s)

  def done (self):
    if not self._sent_headers:
      self.error (500)
      #print 'coro_ehttpd: header not sent!'
    self._client.close ()
    self._done = 1

  def send_headers (self):
    self._sent_headers = 1

    headers = []
    headers.append (self.response(self._reply_code))
    for (key, value) in self._reply_headers.items():
      headers.append ('%s: %s' % (key, value))
    headers.append ('\r\n')
    
    self.push (string.join (headers, '\r\n'))

  def response (self, code=200):
    message = self.responses[code]
    self._reply_code = code
    return 'HTTP/%s %d %s' % (self._version, code, message)

  def error (self, code, reason=None):
    self._reply_code = code
    message = self.responses[code]
    s = self.DEFAULT_ERROR_MESSAGE % {
      'code': code, 'message': message, 'reason': reason
    }
    self['Content-Length'] = len(s)
    self['Content-Type'] = 'text/html'
    self.push (s)
    self.done()

  def log_date_string (self, when):
    return time.strftime (
            '%d/%b/%Y:%H:%M:%S ',
            time.gmtime(when)
            ) + tz_for_log

  def log (self, bytes):
    tend = time.time()
    return '%d - - [%s] "%s" %d %d %0.2f\n' % (
      0,
      self.log_date_string (tend),
      self._request,
      self._reply_code,
      bytes, tend - self._tstart)

  responses = {
          100: "Continue",
          101: "Switching Protocols",
          200: "OK",
          201: "Created",
          202: "Accepted",
          203: "Non-Authoritative Information",
          204: "No Content",
          205: "Reset Content",
          206: "Partial Content",
          300: "Multiple Choices",
          301: "Moved Permanently",
          302: "Moved Temporarily",
          303: "See Other",
          304: "Not Modified",
          305: "Use Proxy",
          400: "Bad Request",
          401: "Unauthorized",
          402: "Payment Required",
          403: "Forbidden",
          404: "Not Found",
          405: "Method Not Allowed",
          406: "Not Acceptable",
          407: "Proxy Authentication Required",
          408: "Request Time-out",
          409: "Conflict",
          410: "Gone",
          411: "Length Required",
          412: "Precondition Failed",
          413: "Request Entity Too Large",
          414: "Request-URI Too Large",
          415: "Unsupported Media Type",
          500: "Internal Server Error",
          501: "Not Implemented",
          502: "Bad Gateway",
          503: "Service Unavailable",
          504: "Gateway Time-out",
          505: "HTTP Version not supported"
          }

  # Default error message
  DEFAULT_ERROR_MESSAGE = string.join (
          ['<head>',
           '<title>Error response</title>',
           '</head>',
           '<body>',
           '<h1>Error response</h1>',
           '<p>Error code %(code)d.',
           '<p>Message: %(message)s.',
           '<p>Reason: %(reason)s.',
           '</body>',
           ''
           ],
          '\r\n'
          )

class http_file_handler:
  def __init__ (self, doc_root):
    self.doc_root = doc_root

  def match (self, request):
    path = request._path
    filename = os.path.join (self.doc_root, path[1:])
    if os.path.exists (filename):
      return 1
    return 0

  def handle_request (self, request):
    path = request._path
    filename = os.path.join (self.doc_root, path[1:])
    
    if os.path.isdir (filename):
      filename = os.path.join (filename, 'index.html')

    if not os.path.isfile (filename):
      request.error (404)
    else:

      f = open (filename, 'rb')
      request['Content-Type'] = 'text/html'
      request['Connection'] = 'close'
      bc = 0

      block = f.read(8192)
      if not block:
        request.error (204) # no content
      else:
        while 1:
          bc = bc + request.push (block)
          block = f.read (8192)
          if not block:
            break
    
class http_server (coro.Thread):
  def __init__ (self, group=None, target=None, name=None, args=(), kwargs={}):
    self._handlers = []
    coro.Thread.__init__ (self, group, target, name, args, kwargs)

  def push_handler (self, handler):
    self._handlers.append (handler)

  def run (self, addr):
    server_s = coro._socket (socket.AF_INET, socket.SOCK_STREAM)
    server_s.set_reuse_addr ()
    server_s.bind (addr)
    server_s.listen (1024)
    self.log (1, "http_server: listening on %s:%d" % addr)
    while 1:
      try:  
        conn, addr = server_s.accept ()
      except:
        self.log (LOG_ERROR, 'accept_thread: accept error, exiting')
        break

      coro.insert_thread(http_client(args = (conn, self._handlers,)))

    server_s.close()
    return None

# Copied from medusa/http_server.py
def compute_timezone_for_log ():
	if time.daylight:
		tz = time.altzone
	else:
		tz = time.timezone
	if tz > 0:
		neg = 1
	else:
		neg = 0
		tz = -tz
	h, rem = divmod (tz, 3600)
	m, rem = divmod (rem, 60)
	if neg:
		return '-%02d%02d' % (h, m)
	else:
		return '+%02d%02d' % (h, m)

# if you run this program over a TZ change boundary, this will be invalid.
tz_for_log = compute_timezone_for_log()


if __name__ == '__main__':
  import backdoor
  import sys
  if len (sys.argv) > 1:
    doc_root = sys.argv[1]
  else:
    doc_root = '/usr/local/httpd/htdocs'

  server = http_server(args = (('0.0.0.0', 7001),))
  file_handler = http_file_handler (doc_root)
  server.push_handler (file_handler)
  server.start()
  coro.spawn (backdoor.serve)
  coro.event_loop (30.0)
