diff --git a/gunicorn/http/__init__.py b/gunicorn/http/__init__.py new file mode 100644 index 00000000..0781c783 --- /dev/null +++ b/gunicorn/http/__init__.py @@ -0,0 +1,3 @@ + +from request import HTTPRequest +from response import HTTPResponse \ No newline at end of file diff --git a/gunicorn/http/request.py b/gunicorn/http/request.py new file mode 100644 index 00000000..5352e94b --- /dev/null +++ b/gunicorn/http/request.py @@ -0,0 +1,288 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2008,2009 Benoit Chesneau +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at# +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import re +import StringIO +import sys +from urllib import unquote + +from gunicorn import __version__ + +NORMALIZE_SPACE = re.compile(r'(?:\r\n)?[ \t]+') + +def _normalize_name(name): + return ["-".join([w.capitalize() for w in name.split("-")])] + +class RequestError(Exception): + + def __init__(self, status_code, reason): + self.status_code = status_code + self.reason = reason + Exception.__init__(self, (status_code, reason)) + +class HTTPRequest(object): + + SERVER_VERSION = "gunicorn/%s" % __version__ + CHUNK_SIZE = 4096 + + def __init__(self, socket, client_address, server_address): + self.socket = socket + self.client_address = client_address + self.server_address = server_address + self.version = None + self.method = None + self.path = None + self.headers = {} + self.response_status = None + self.response_headers = {} + self._version = 11 + self.fp = socket.makefile("rw", self.CHUNK_SIZE) + + + def read(self): + # get status line + self.first_line(self.fp.readline()) + + # read headers + self.read_headers() + + if "?" in self.path: + path_info, query = self.path.split('?', 1) + else: + path_info = self.path + query = "" + + length = self.body_length() + if not length: + wsgi_input = StringIO.StringIO() + elif length == "chunked": + length, wsgi_input = self.decode_chunked() + else: + wsgi_input = FileInput(self) + + + environ = { + "wsgi.url_scheme": 'http', + "wsgi.input": wsgi_input, + "wsgi.errors": sys.stderr, + "wsgi.version": (1, 0), + "wsgi.multithread": False, + "wsgi.multiprocess": True, + "wsgi.run_once": False, + "SCRIPT_NAME": "", + "SERVER_SOFTWARE": self.SERVER_VERSION, + "REQUEST_METHOD": self.method, + "PATH_INFO": unquote(path_info), + "QUERY_STRING": query, + "RAW_URI": self.path, + "CONTENT_TYPE": self.headers.get('content-type', ''), + "CONTENT_LENGTH": length, + "REMOTE_ADDR": self.client_address[0], + "REMOTE_PORT": self.client_address[1], + "SERVER_NAME": self.server_address[0], + "SERVER_PORT": self.server_address[1], + "SERVER_PROTOCOL": self.version + } + + for key, value in self.headers.items(): + key = 'HTTP_' + key.replace('-', '_') + if key not in ('HTTP_CONTENT_TYPE', 'HTTP_CONTENT_LENGTH'): + environ[key] = value + return environ + + def read_headers(self): + hname = "" + while True: + line = self.fp.readline() + + if line == "\r\n": + # end of headers + break + + if line == "\t": + # It's a continuation line. + self.headers[hname] += line.strip() + else: + try: + hname =self.parse_header(line) + except ValueError: + # bad headers + pass + + + def body_length(self): + transfert_encoding = self.headers.get('TRANSFERT-ENCODING') + content_length = self.headers.get('CONTENT-LENGTH') + if transfert_encoding is None: + if content_length is None: + return None + return content_length + elif transfert_encoding == "chunked": + return "chunked" + else: + return None + + def should_close(self): + if self.headers.get("CONNECTION") == "close": + return True + if self.headers.get("CONNECTION") == "Keep-Alive": + return False + if self.version < "HTTP/1.1": + return True + + def decode_chunked(self): + """Decode the 'chunked' transfer coding.""" + length = 0 + data = StringIO.StringIO() + while True: + line = self.fp.readline().strip().split(";", 1) + chunk_size = int(line.pop(0), 16) + if chunk_size <= 0: + break + length += chunk_size + data.write(self.fp.read(chunk_size)) + crlf = self.fp.read(2) + if crlf != "\r\n": + raise RequestError((400, "Bad chunked transfer coding " + "(expected '\\r\\n', got %r)" % crlf)) + return + + # Grab any trailer headers + self.read_headers() + + data.seek(0) + return data, str(length) or "" + + def start_response(self, status, response_headers): + resp_head = [] + self.response_status = status + self.response_headers = {} + resp_head.append("%s %s" % (self.version, status)) + for name, value in response_headers: + resp_head.append("%s: %s" % (name, value)) + self.response_headers[name.lower()] = value + self.fp.write("%s\r\n\r\n" % "\r\n".join(resp_head)) + + def write(self, data): + self.fp.write(data) + + def close(self): + self.fp.close() + if self.should_close(): + self.socket.close() + + def first_line(self, line): + method, path, version = line.split(" ") + self.version = version.strip() + self.method = method.upper() + self.path = path + + def parse_header(self, line): + name, value = line.split(": ", 1) + name = name.strip().upper() + self.headers[name] = value.strip() + return name + + + +class FileInput(object): + + def __init__(self, req): + self.length = req.body_length() + self.fp = req.fp + self.eof = False + + def close(self): + self.eof = False + + def read(self, amt=None): + if self.fp is None or self.eof: + return '' + + if amt is None: + # unbounded read + s = self._safe_read(self.length) + self.close() # we read everything + return s + + if amt > self.length: + amt = self.length + + s = self.fp.read(amt) + self.length -= len(s) + if not self.length: + self.close() + return s + + def readline(self, size=None): + if self.fp is None or self.eof: + return '' + + if size is not None: + data = self.fp.readline(size) + else: + # User didn't specify a size ... + # We read the line in chunks to make sure it's not a 100MB line ! + # cherrypy trick + res = [] + while True: + data = self.fp.readline(256) + res.append(data) + if len(data) < 256 or data[-1:] == "\n": + data = ''.join(res) + break + self.length -= len(data) + if not self.length: + self.close() + return data + + def readlines(self, sizehint=0): + # Shamelessly stolen from StringIO + total = 0 + lines = [] + line = self.readline() + while line: + lines.append(line) + total += len(line) + if 0 < sizehint <= total: + break + line = self.readline() + return lines + + + def _safe_read(self, amt): + """Read the number of bytes requested, compensating for partial reads. + """ + s = [] + while amt > 0: + chunk = self.fp.read(amt) + if not chunk: + raise RequestError(500, "Incomplete read %s" % s) + s.append(chunk) + amt -= len(chunk) + return ''.join(s) + + + def __iter__(self): + return self + + def next(self): + if self.eof: + raise StopIteration() + return self.readline() + + + \ No newline at end of file diff --git a/gunicorn/http/response.py b/gunicorn/http/response.py new file mode 100644 index 00000000..fb1211c5 --- /dev/null +++ b/gunicorn/http/response.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2008,2009 Benoit Chesneau +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at# +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +class HTTPResponse(object): + + def __init__(self, req, data): + self.req = req + self.data = data + self.headers = self.req.response_headers or {} + self.fp = req.fp + + def write(self, data): + self.fp.write(data) + + def send(self): + if not self.data: return + for chunk in self.data: + self.write(chunk) + self.fp.flush() \ No newline at end of file diff --git a/gunicorn/util.py b/gunicorn/util.py index 6e53fb8f..704f29fb 100644 --- a/gunicorn/util.py +++ b/gunicorn/util.py @@ -1,5 +1,5 @@ -def import_mod(module): +def import_app(module): parts = module.rsplit(":", 1) if len(parts) == 1: module, obj = module, "application" @@ -11,5 +11,10 @@ def import_mod(module): mod = getattr(mod, p, None) if mod is None: raise ImportError("Failed to import: %s" % module) - return mod + app = getattr(mod, obj, None) + if app is None: + raise ImportError("Failed to find application object: %r" % obj) + if not callable(app): + raise TypeError("Application object must be callable.") + return app \ No newline at end of file diff --git a/gunicorn/worker.py b/gunicorn/worker.py index 63185173..47e2a87e 100644 --- a/gunicorn/worker.py +++ b/gunicorn/worker.py @@ -5,29 +5,11 @@ import os import select import signal +import http import util log = logging.getLogger(__name__) -class Handler(BaseHTTPServer.BaseHTTPRequestHandler): - - protocol = 'HTTP/1.1' - worker = None - - def do_GET(self): - self.respond("Hello, World!\n") - - def respond(self, body): - log.info("Responding") - self.send_response(200, 'OK') - self.send_header("Content-Type", "text/plain") - self.send_header("Content-Length", len(body)) - self.end_headers() - log.info("Sending body.") - self.wfile.write(body) - self.wfile.flush() - log.info("Done.") - class Worker(object): SIGNALS = map( @@ -38,8 +20,9 @@ class Worker(object): def __init__(self, workerid, socket, module): self.id = workerid self.socket = socket + self.address = socket.getsockname() self.tmp = os.tmpfile() - self.module = util.import_mod(module) + self.app = util.import_app(module) def init_signals(self): map(lambda s: signal.signal(s, signal.SIG_DFL), self.SIGNALS) @@ -58,17 +41,17 @@ class Worker(object): conn.setblocking(1) try: self.handle(conn, addr) + except: + log.exception("Error processing request.") finally: - log.info("Client disconnected.") conn.close() def handle(self, conn, client): while True: - req = Handler(conn, client, self.socket) - req.setup() - req.worker = self - log.info("Handling request.") - req.handle() - log.info("Done.") - if req.close_connection: + req = http.HTTPRequest(conn, client, self.address) + result = self.app(req.read(), req.start_response) + response = http.HTTPResponse(req, result) + response.send() + if req.should_close(): + conn.close() return diff --git a/test.py b/test.py index 41e3d8b3..a0d25959 100644 --- a/test.py +++ b/test.py @@ -4,12 +4,16 @@ from gunicorn.httpserver import WSGIServer -def simple_app(environ, start_response): +def app(environ, start_response): """Simplest possible application object""" + data = 'Hello, World!\n' status = '200 OK' - response_headers = [('Content-type','text/plain')] + response_headers = [ + ('Content-type','text/plain'), + ('Content-Length', len(data)) + ] start_response(status, response_headers) - return ['Hello world!\n'] + return [data] if __name__ == '__main__': server = WSGIServer(("127.0.0.1", 8000), 1, simple_app)