# -*- 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_re = re.compile ('([^;?#]*)(;[^?#]*)?(\?[^#]*)?(#.*)?') # HTTP/ 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 ( ['', 'Error response', '', '', '

Error response

', '

Error code %(code)d.', '

Message: %(message)s.', '

Reason: %(reason)s.', '', '' ], '\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)