"""\ @file httpd.py @author Donovan Preston Copyright (c) 2005-2006, Donovan Preston Copyright (c) 2007, Linden Research, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ import cgi import errno import socket import sys import time import urllib import socket import traceback import BaseHTTPServer try: from cStringIO import StringIO except ImportError: from StringIO import StringIO from eventlet import api from eventlet import coros DEFAULT_MAX_HTTP_VERSION = 'HTTP/1.1' USE_ACCESS_LOG = True CONNECTION_CLOSED = (errno.EPIPE, errno.ECONNRESET) class ErrorResponse(Exception): _responses = BaseHTTPServer.BaseHTTPRequestHandler.responses def __init__(self, code, reason_phrase=None, headers=None, body=None): Exception.__init__(self, reason_phrase) self.code = code if reason_phrase is None: self.reason = self._responses[code][0] else: self.reason = reason_phrase self.headers = headers if body is None: self.body = self._responses[code][1] else: self.body = body class Request(object): _method = None _path = None _responsecode = 200 _reason_phrase = None _request_started = False _chunked = False _producer_adapters = {} depth = 0 def __init__(self, protocol, method, path, headers): self.context = {} self.request_start_time = time.time() self.site = protocol.server.site self.protocol = protocol self._method = method if '?' in path: self._path, self._query = path.split('?', 1) self._query = self._query.replace('&', '&') else: self._path, self._query = path, None self._incoming_headers = headers self._outgoing_headers = dict() def response(self, code, reason_phrase=None, headers=None, body=None): """Change the response code. This will not be sent until some data is written; last call to this method wins. Default is 200 if this is not called. """ self._responsecode = code self._reason_phrase = reason_phrase self.protocol.set_response_code(self, code, reason_phrase) if headers is not None: try: headers = headers.iteritems() except AttributeError: pass for key, value in headers: self.set_header(key, value) if body is not None: self.write(body) def is_okay(self): return 200 <= self._responsecode <= 299 def full_url(self): path = self.path() query = self.query() if query: path = path + '?' + query via = self.get_header('via', '') if via.strip(): next_part = iter(via.split()).next received_protocol = next_part() received_by = next_part() if received_by.endswith(','): received_by = received_by[:-1] else: comment = '' while not comment.endswith(','): try: comment += next_part() except StopIteration: comment += ',' break comment = comment[:-1] else: received_by = self.get_header('host') return '%s://%s%s' % (self.request_protocol(), received_by, path) def begin_response(self, length="-"): """Begin the response, and return the initial response text """ self._request_started = True request_time = time.time() - self.request_start_time code = self._responsecode proto = self.protocol if USE_ACCESS_LOG: proto.server.write_access_log_line( proto.client_address[0], time.strftime("%d/%b/%Y %H:%M:%S"), proto.requestline, code, length, request_time) if self._reason_phrase is not None: message = self._reason_phrase.split("\n")[0] elif code in proto.responses: message = proto.responses[code][0] else: message = '' if proto.request_version == 'HTTP/0.9': return [] response_lines = proto.generate_status_line() if not self._outgoing_headers.has_key('connection'): con = self.get_header('connection') if con is None and proto.request_version == 'HTTP/1.0': con = 'close' if con is not None: self.set_header('connection', con) for key, value in self._outgoing_headers.items(): key = '-'.join([x.capitalize() for x in key.split('-')]) response_lines.append("%s: %s" % (key, value)) response_lines.append("") return response_lines def write(self, obj): """Writes an arbitrary object to the response, using the sitemap's adapt method to convert it to bytes. """ if isinstance(obj, str): self._write_bytes(obj) elif isinstance(obj, unicode): # use utf8 encoding for now, *TODO support charset negotiation # Content-Type: text/html; charset=utf-8 ctype = self._outgoing_headers.get('content-type', 'text/html') ctype = ctype + '; charset=utf-8' self._outgoing_headers['content-type'] = ctype self._write_bytes(obj.encode('utf8')) else: self.site.adapt(obj, self) def _write_bytes(self, data): """Write all the data of the response. Can be called just once. """ if self._request_started: print "Request has already written a response:" traceback.print_stack() return self._outgoing_headers['content-length'] = len(data) response_lines = self.begin_response(len(data)) response_lines.append(data) self.protocol.wfile.write("\r\n".join(response_lines)) if hasattr(self.protocol.wfile, 'flush'): self.protocol.wfile.flush() def method(self): return self._method def path(self): return self._path def path_segments(self): return [urllib.unquote_plus(x) for x in self._path.split('/')[1:]] def query(self): return self._query def uri(self): if self._query: return '%s?%s' % ( self._path, self._query) return self._path def get_headers(self): return self._incoming_headers def get_header(self, header_name, default=None): return self.get_headers().get(header_name.lower(), default) def get_query_pairs(self): if not hasattr(self, '_split_query'): if self._query is None: self._split_query = () else: spl = self._query.split('&') spl = [x.split('=', 1) for x in spl if x] self._split_query = [] for query in spl: if len(query) == 1: key = query[0] value = '' else: key, value = query self._split_query.append((urllib.unquote_plus(key), urllib.unquote_plus(value))) return self._split_query def get_queries_generator(self, name): """Generate all query parameters matching the given name. """ for key, value in self.get_query_pairs(): if key == name or not name: yield value get_queries = lambda self, name: list(self.get_queries_generator) def get_query(self, name, default=None): try: return self.get_queries_generator(name).next() except StopIteration: return default def get_arg_list(self, name): return self.get_field_storage().getlist(name) def get_arg(self, name, default=None): return self.get_field_storage().getfirst(name, default) def get_field_storage(self): if not hasattr(self, '_field_storage'): if self.method() == 'GET': data = '' if self._query: data = self._query else: data = self.read_body() fl = StringIO(data) ## Allow our resource to provide the FieldStorage instance for ## customization purposes. headers = self.get_headers() environ = dict( REQUEST_METHOD='POST', QUERY_STRING=self._query or '') self._field_storage = cgi.FieldStorage(fl, headers, environ=environ) return self._field_storage def set_header(self, key, value): if key.lower() == 'connection' and value.lower() == 'close': self.protocol.close_connection = 1 self._outgoing_headers[key.lower()] = value __setitem__ = set_header def get_outgoing_header(self, key): return self._outgoing_headers[key.lower()] def has_outgoing_header(self, key): return self._outgoing_headers.has_key(key.lower()) def socket(self): return self.protocol.socket def error(self, response=None, body=None, log_traceback=True): if log_traceback: traceback.print_exc(file=self.log) if response is None: response = 500 if body is None: typ, val, tb = sys.exc_info() body = dict(type=str(typ), error=True, reason=str(val)) self.response(response) if(type(body) is str and not self.response_written()): self.write(body) return try: produce(body, self) except Exception, e: traceback.print_exc(file=self.log) if not self.response_written(): self.write('Internal Server Error') def not_found(self): self.error(404, 'Not Found\n', log_traceback=False) def raw_body(self): if not hasattr(self, '_cached_body'): self.read_body() return self._cached_body def read_body(self): """ Returns the string body that was read off the request, or the empty string if there was no request body. Requires a content-length header. Caches the body so multiple calls to read_body() are free. """ if not hasattr(self, '_cached_body'): length = self.get_header('content-length') if length: length = int(length) if length: self._cached_body = self.protocol.rfile.read(length) else: self._cached_body = '' return self._cached_body def parsed_body(self): """ Returns the parsed version of the body, using the content-type header to select from the parsers on the site object. If no parser is found, returns the string body from read_body(). Caches the parsed body so multiple calls to parsed_body() are free. """ if not hasattr(self, '_cached_parsed_body'): body = self.read_body() if hasattr(self.site, 'parsers'): ct = self.get_header('content-type') parser = self.site.parsers.get(ct) if parser is not None: body = parser(body) else: ex = ValueError("Could not find parser for content-type: %s" % ct) ex.body = body raise ex self._cached_parsed_body = body return self._cached_parsed_body def override_body(self, body): if not hasattr(self, '_cached_parsed_body'): self.read_body() ## Read and discard body self._cached_parsed_body = body def response_written(self): ## TODO change badly named variable return self._request_started def request_version(self): return self.protocol.request_version def request_protocol(self): if self.protocol.is_secure: return "https" return "http" def server_address(self): return self.protocol.server.address def __repr__(self): return "" % ( getattr(self, '_method'), getattr(self, '_path')) DEFAULT_TIMEOUT = 300 # This value was chosen because apache 2 has a default limit of 8190. # I believe that slightly smaller number is because apache does not # count the \r\n. MAX_REQUEST_LINE = 8192 class Timeout(RuntimeError): pass class HttpProtocol(BaseHTTPServer.BaseHTTPRequestHandler): def __init__(self, request, client_address, server): self.rfile = self.wfile = request.makefile() self.is_secure = request.is_secure request.close() # close this now so that when rfile and wfile are closed, the socket gets closed self.client_address = client_address self.server = server self.set_response_code(None, 200, None) self.protocol_version = server.max_http_version def close(self): self.rfile.close() self.wfile.close() def set_response_code(self, request, code, message): self._code = code if message is not None: self._message = message.split("\n")[0] elif code in self.responses: self._message = self.responses[code][0] else: self._message = '' def generate_status_line(self): return [ "%s %d %s" % ( self.protocol_version, self._code, self._message)] def write_bad_request(self, status, reason): self.set_response_code(self, status, reason) self.wfile.write(''.join(self.generate_status_line())) self.wfile.write('\r\nServer: %s\r\n' % self.version_string()) self.wfile.write('Date: %s\r\n' % self.date_time_string()) self.wfile.write('Content-Length: 0\r\n\r\n') def handle(self): self.close_connection = 0 timeout = DEFAULT_TIMEOUT while not self.close_connection: if timeout == 0: break cancel = api.exc_after(timeout, Timeout) try: self.raw_requestline = self.rfile.readline(MAX_REQUEST_LINE) if self.raw_requestline is not None: if len(self.raw_requestline) == MAX_REQUEST_LINE: # Someone sent a request line which is too # large. Be helpful and tell them. self.write_bad_request(414, 'Request-URI Too Long') self.close_connection = True continue except socket.error, e: if e[0] in CONNECTION_CLOSED: self.close_connection = True cancel.cancel() continue except Timeout: self.close_connection = True continue except Exception, e: try: if e[0][0][0].startswith('SSL'): print "SSL Error:", e[0][0] self.close_connection = True cancel.cancel() continue except Exception, f: print "Exception in ssl test:",f pass raise e cancel.cancel() if not self.raw_requestline or not self.parse_request(): self.close_connection = True continue self.set_response_code(None, 200, None) request = Request(self, self.command, self.path, self.headers) request.set_header('Server', self.version_string()) request.set_header('Date', self.date_time_string()) try: timeout = int(request.get_header('keep-alive', timeout)) except TypeError, ValueError: pass try: try: try: self.server.site.handle_request(request) except ErrorResponse, err: request.response(code=err.code, reason_phrase=err.reason, headers=err.headers, body=err.body) finally: # clean up any timers that might have been left around by the handling code api.get_hub().cancel_timers(api.getcurrent()) # throw an exception if it failed to write a body if not request.response_written(): raise NotImplementedError("Handler failed to write response to request: %s" % request) if not hasattr(self, '_cached_body'): try: request.read_body() ## read & discard body except: pass except socket.error, e: # Broken pipe, connection reset by peer if e[0] in CONNECTION_CLOSED: #print "Remote host closed connection before response could be sent" pass else: raise except Exception, e: self.server.log_message("Exception caught in HttpRequest.handle():\n") self.server.log_exception(*sys.exc_info()) if not request.response_written(): request.response(500) request.write('Internal Server Error') self.close() raise e # can't do a plain raise since exc_info might have been cleared self.close() class Server(BaseHTTPServer.HTTPServer): def __init__(self, socket, address, site, log, max_http_version=DEFAULT_MAX_HTTP_VERSION): self.socket = socket self.address = address self.site = site self.max_http_version = max_http_version if log: self.log = log if hasattr(log, 'info'): log.write = log.info else: self.log = self def write(self, something): sys.stdout.write('%s' % (something, )); sys.stdout.flush() def log_message(self, message): self.log.write(message) def log_exception(self, type, value, tb): self.log.write(''.join(traceback.format_exception(type, value, tb))) def write_access_log_line(self, *args): """Write a line to the access.log. Arguments: client_address, date_time, requestline, code, size, request_time """ self.log.write( '%s - - [%s] "%s" %s %s %.6f\n' % args) def server(sock, site, log=None, max_size=512, serv=None, max_http_version=DEFAULT_MAX_HTTP_VERSION): pool = coros.CoroutinePool(max_size=max_size) if serv is None: serv = Server(sock, sock.getsockname(), site, log, max_http_version=max_http_version) try: serv.log.write("httpd starting up on %s\n" % (sock.getsockname(), )) while True: try: new_sock, address = sock.accept() proto = HttpProtocol(new_sock, address, serv) pool.execute_async(proto.handle) api.sleep(0) # sleep to allow other coros to run except KeyboardInterrupt: api.get_hub().remove_descriptor(sock.fileno()) serv.log.write("httpd exiting\n") break finally: try: sock.close() except socket.error: pass if __name__ == '__main__': class TestSite(object): def handle_request(self, req): req.write('hello') server( api.tcp_listener(('127.0.0.1', 8080)), TestSite())