| """ |
| BaseHTTPServer that implements the Python WSGI protocol (PEP 333, rev 1.21). |
| |
| Adapted from wsgiref.simple_server: http://svn.eby-sarna.com/wsgiref/ |
| |
| This is a simple server for use in testing or debugging Django apps. It hasn't |
| been reviewed for security issues. Don't use it for production use. |
| """ |
| |
| from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer |
| import mimetypes |
| import os |
| import re |
| import sys |
| import urllib |
| |
| from django.utils.http import http_date |
| |
| __version__ = "0.1" |
| __all__ = ['WSGIServer','WSGIRequestHandler'] |
| |
| server_version = "WSGIServer/" + __version__ |
| sys_version = "Python/" + sys.version.split()[0] |
| software_version = server_version + ' ' + sys_version |
| |
| class WSGIServerException(Exception): |
| pass |
| |
| class FileWrapper(object): |
| """Wrapper to convert file-like objects to iterables""" |
| |
| def __init__(self, filelike, blksize=8192): |
| self.filelike = filelike |
| self.blksize = blksize |
| if hasattr(filelike,'close'): |
| self.close = filelike.close |
| |
| def __getitem__(self,key): |
| data = self.filelike.read(self.blksize) |
| if data: |
| return data |
| raise IndexError |
| |
| def __iter__(self): |
| return self |
| |
| def next(self): |
| data = self.filelike.read(self.blksize) |
| if data: |
| return data |
| raise StopIteration |
| |
| # Regular expression that matches `special' characters in parameters, the |
| # existence of which force quoting of the parameter value. |
| tspecials = re.compile(r'[ \(\)<>@,;:\\"/\[\]\?=]') |
| |
| def _formatparam(param, value=None, quote=1): |
| """Convenience function to format and return a key=value pair. |
| |
| This will quote the value if needed or if quote is true. |
| """ |
| if value is not None and len(value) > 0: |
| if quote or tspecials.search(value): |
| value = value.replace('\\', '\\\\').replace('"', r'\"') |
| return '%s="%s"' % (param, value) |
| else: |
| return '%s=%s' % (param, value) |
| else: |
| return param |
| |
| class Headers(object): |
| """Manage a collection of HTTP response headers""" |
| def __init__(self,headers): |
| if not isinstance(headers, list): |
| raise TypeError("Headers must be a list of name/value tuples") |
| self._headers = headers |
| |
| def __len__(self): |
| """Return the total number of headers, including duplicates.""" |
| return len(self._headers) |
| |
| def __setitem__(self, name, val): |
| """Set the value of a header.""" |
| del self[name] |
| self._headers.append((name, val)) |
| |
| def __delitem__(self,name): |
| """Delete all occurrences of a header, if present. |
| |
| Does *not* raise an exception if the header is missing. |
| """ |
| name = name.lower() |
| self._headers[:] = [kv for kv in self._headers if kv[0].lower()<>name] |
| |
| def __getitem__(self,name): |
| """Get the first header value for 'name' |
| |
| Return None if the header is missing instead of raising an exception. |
| |
| Note that if the header appeared multiple times, the first exactly which |
| occurrance gets returned is undefined. Use getall() to get all |
| the values matching a header field name. |
| """ |
| return self.get(name) |
| |
| def has_key(self, name): |
| """Return true if the message contains the header.""" |
| return self.get(name) is not None |
| |
| __contains__ = has_key |
| |
| def get_all(self, name): |
| """Return a list of all the values for the named field. |
| |
| These will be sorted in the order they appeared in the original header |
| list or were added to this instance, and may contain duplicates. Any |
| fields deleted and re-inserted are always appended to the header list. |
| If no fields exist with the given name, returns an empty list. |
| """ |
| name = name.lower() |
| return [kv[1] for kv in self._headers if kv[0].lower()==name] |
| |
| |
| def get(self,name,default=None): |
| """Get the first header value for 'name', or return 'default'""" |
| name = name.lower() |
| for k,v in self._headers: |
| if k.lower()==name: |
| return v |
| return default |
| |
| def keys(self): |
| """Return a list of all the header field names. |
| |
| These will be sorted in the order they appeared in the original header |
| list, or were added to this instance, and may contain duplicates. |
| Any fields deleted and re-inserted are always appended to the header |
| list. |
| """ |
| return [k for k, v in self._headers] |
| |
| def values(self): |
| """Return a list of all header values. |
| |
| These will be sorted in the order they appeared in the original header |
| list, or were added to this instance, and may contain duplicates. |
| Any fields deleted and re-inserted are always appended to the header |
| list. |
| """ |
| return [v for k, v in self._headers] |
| |
| def items(self): |
| """Get all the header fields and values. |
| |
| These will be sorted in the order they were in the original header |
| list, or were added to this instance, and may contain duplicates. |
| Any fields deleted and re-inserted are always appended to the header |
| list. |
| """ |
| return self._headers[:] |
| |
| def __repr__(self): |
| return "Headers(%s)" % `self._headers` |
| |
| def __str__(self): |
| """str() returns the formatted headers, complete with end line, |
| suitable for direct HTTP transmission.""" |
| return '\r\n'.join(["%s: %s" % kv for kv in self._headers]+['','']) |
| |
| def setdefault(self,name,value): |
| """Return first matching header value for 'name', or 'value' |
| |
| If there is no header named 'name', add a new header with name 'name' |
| and value 'value'.""" |
| result = self.get(name) |
| if result is None: |
| self._headers.append((name,value)) |
| return value |
| else: |
| return result |
| |
| def add_header(self, _name, _value, **_params): |
| """Extended header setting. |
| |
| _name is the header field to add. keyword arguments can be used to set |
| additional parameters for the header field, with underscores converted |
| to dashes. Normally the parameter will be added as key="value" unless |
| value is None, in which case only the key will be added. |
| |
| Example: |
| |
| h.add_header('content-disposition', 'attachment', filename='bud.gif') |
| |
| Note that unlike the corresponding 'email.Message' method, this does |
| *not* handle '(charset, language, value)' tuples: all values must be |
| strings or None. |
| """ |
| parts = [] |
| if _value is not None: |
| parts.append(_value) |
| for k, v in _params.items(): |
| if v is None: |
| parts.append(k.replace('_', '-')) |
| else: |
| parts.append(_formatparam(k.replace('_', '-'), v)) |
| self._headers.append((_name, "; ".join(parts))) |
| |
| def guess_scheme(environ): |
| """Return a guess for whether 'wsgi.url_scheme' should be 'http' or 'https' |
| """ |
| if environ.get("HTTPS") in ('yes','on','1'): |
| return 'https' |
| else: |
| return 'http' |
| |
| _hop_headers = { |
| 'connection':1, 'keep-alive':1, 'proxy-authenticate':1, |
| 'proxy-authorization':1, 'te':1, 'trailers':1, 'transfer-encoding':1, |
| 'upgrade':1 |
| } |
| |
| def is_hop_by_hop(header_name): |
| """Return true if 'header_name' is an HTTP/1.1 "Hop-by-Hop" header""" |
| return header_name.lower() in _hop_headers |
| |
| class ServerHandler(object): |
| """Manage the invocation of a WSGI application""" |
| |
| # Configuration parameters; can override per-subclass or per-instance |
| wsgi_version = (1,0) |
| wsgi_multithread = True |
| wsgi_multiprocess = True |
| wsgi_run_once = False |
| |
| origin_server = True # We are transmitting direct to client |
| http_version = "1.0" # Version that should be used for response |
| server_software = software_version |
| |
| # os_environ is used to supply configuration from the OS environment: |
| # by default it's a copy of 'os.environ' as of import time, but you can |
| # override this in e.g. your __init__ method. |
| os_environ = dict(os.environ.items()) |
| |
| # Collaborator classes |
| wsgi_file_wrapper = FileWrapper # set to None to disable |
| headers_class = Headers # must be a Headers-like class |
| |
| # Error handling (also per-subclass or per-instance) |
| traceback_limit = None # Print entire traceback to self.get_stderr() |
| error_status = "500 INTERNAL SERVER ERROR" |
| error_headers = [('Content-Type','text/plain')] |
| |
| # State variables (don't mess with these) |
| status = result = None |
| headers_sent = False |
| headers = None |
| bytes_sent = 0 |
| |
| def __init__(self, stdin, stdout, stderr, environ, multithread=True, |
| multiprocess=False): |
| self.stdin = stdin |
| self.stdout = stdout |
| self.stderr = stderr |
| self.base_env = environ |
| self.wsgi_multithread = multithread |
| self.wsgi_multiprocess = multiprocess |
| |
| def run(self, application): |
| """Invoke the application""" |
| # Note to self: don't move the close()! Asynchronous servers shouldn't |
| # call close() from finish_response(), so if you close() anywhere but |
| # the double-error branch here, you'll break asynchronous servers by |
| # prematurely closing. Async servers must return from 'run()' without |
| # closing if there might still be output to iterate over. |
| try: |
| self.setup_environ() |
| self.result = application(self.environ, self.start_response) |
| self.finish_response() |
| except: |
| try: |
| self.handle_error() |
| except: |
| # If we get an error handling an error, just give up already! |
| self.close() |
| raise # ...and let the actual server figure it out. |
| |
| def setup_environ(self): |
| """Set up the environment for one request""" |
| |
| env = self.environ = self.os_environ.copy() |
| self.add_cgi_vars() |
| |
| env['wsgi.input'] = self.get_stdin() |
| env['wsgi.errors'] = self.get_stderr() |
| env['wsgi.version'] = self.wsgi_version |
| env['wsgi.run_once'] = self.wsgi_run_once |
| env['wsgi.url_scheme'] = self.get_scheme() |
| env['wsgi.multithread'] = self.wsgi_multithread |
| env['wsgi.multiprocess'] = self.wsgi_multiprocess |
| |
| if self.wsgi_file_wrapper is not None: |
| env['wsgi.file_wrapper'] = self.wsgi_file_wrapper |
| |
| if self.origin_server and self.server_software: |
| env.setdefault('SERVER_SOFTWARE',self.server_software) |
| |
| def finish_response(self): |
| """Send any iterable data, then close self and the iterable |
| |
| Subclasses intended for use in asynchronous servers will |
| want to redefine this method, such that it sets up callbacks |
| in the event loop to iterate over the data, and to call |
| 'self.close()' once the response is finished. |
| """ |
| if not self.result_is_file() and not self.sendfile(): |
| for data in self.result: |
| self.write(data) |
| self.finish_content() |
| self.close() |
| |
| def get_scheme(self): |
| """Return the URL scheme being used""" |
| return guess_scheme(self.environ) |
| |
| def set_content_length(self): |
| """Compute Content-Length or switch to chunked encoding if possible""" |
| try: |
| blocks = len(self.result) |
| except (TypeError, AttributeError, NotImplementedError): |
| pass |
| else: |
| if blocks==1: |
| self.headers['Content-Length'] = str(self.bytes_sent) |
| return |
| # XXX Try for chunked encoding if origin server and client is 1.1 |
| |
| def cleanup_headers(self): |
| """Make any necessary header changes or defaults |
| |
| Subclasses can extend this to add other defaults. |
| """ |
| if 'Content-Length' not in self.headers: |
| self.set_content_length() |
| |
| def start_response(self, status, headers,exc_info=None): |
| """'start_response()' callable as specified by PEP 333""" |
| |
| if exc_info: |
| try: |
| if self.headers_sent: |
| # Re-raise original exception if headers sent |
| raise exc_info[0], exc_info[1], exc_info[2] |
| finally: |
| exc_info = None # avoid dangling circular ref |
| elif self.headers is not None: |
| raise AssertionError("Headers already set!") |
| |
| assert isinstance(status, str),"Status must be a string" |
| assert len(status)>=4,"Status must be at least 4 characters" |
| assert int(status[:3]),"Status message must begin w/3-digit code" |
| assert status[3]==" ", "Status message must have a space after code" |
| if __debug__: |
| for name,val in headers: |
| assert isinstance(name, str),"Header names must be strings" |
| assert isinstance(val, str),"Header values must be strings" |
| assert not is_hop_by_hop(name),"Hop-by-hop headers not allowed" |
| self.status = status |
| self.headers = self.headers_class(headers) |
| return self.write |
| |
| def send_preamble(self): |
| """Transmit version/status/date/server, via self._write()""" |
| if self.origin_server: |
| if self.client_is_modern(): |
| self._write('HTTP/%s %s\r\n' % (self.http_version,self.status)) |
| if 'Date' not in self.headers: |
| self._write( |
| 'Date: %s\r\n' % http_date() |
| ) |
| if self.server_software and 'Server' not in self.headers: |
| self._write('Server: %s\r\n' % self.server_software) |
| else: |
| self._write('Status: %s\r\n' % self.status) |
| |
| def write(self, data): |
| """'write()' callable as specified by PEP 333""" |
| |
| assert isinstance(data, str), "write() argument must be string" |
| |
| if not self.status: |
| raise AssertionError("write() before start_response()") |
| |
| elif not self.headers_sent: |
| # Before the first output, send the stored headers |
| self.bytes_sent = len(data) # make sure we know content-length |
| self.send_headers() |
| else: |
| self.bytes_sent += len(data) |
| |
| # XXX check Content-Length and truncate if too many bytes written? |
| |
| # If data is too large, socket will choke, so write chunks no larger |
| # than 32MB at a time. |
| length = len(data) |
| if length > 33554432: |
| offset = 0 |
| while offset < length: |
| chunk_size = min(33554432, length) |
| self._write(data[offset:offset+chunk_size]) |
| self._flush() |
| offset += chunk_size |
| else: |
| self._write(data) |
| self._flush() |
| |
| def sendfile(self): |
| """Platform-specific file transmission |
| |
| Override this method in subclasses to support platform-specific |
| file transmission. It is only called if the application's |
| return iterable ('self.result') is an instance of |
| 'self.wsgi_file_wrapper'. |
| |
| This method should return a true value if it was able to actually |
| transmit the wrapped file-like object using a platform-specific |
| approach. It should return a false value if normal iteration |
| should be used instead. An exception can be raised to indicate |
| that transmission was attempted, but failed. |
| |
| NOTE: this method should call 'self.send_headers()' if |
| 'self.headers_sent' is false and it is going to attempt direct |
| transmission of the file1. |
| """ |
| return False # No platform-specific transmission by default |
| |
| def finish_content(self): |
| """Ensure headers and content have both been sent""" |
| if not self.headers_sent: |
| self.headers['Content-Length'] = "0" |
| self.send_headers() |
| else: |
| pass # XXX check if content-length was too short? |
| |
| def close(self): |
| try: |
| self.request_handler.log_request(self.status.split(' ',1)[0], self.bytes_sent) |
| finally: |
| try: |
| if hasattr(self.result,'close'): |
| self.result.close() |
| finally: |
| self.result = self.headers = self.status = self.environ = None |
| self.bytes_sent = 0; self.headers_sent = False |
| |
| def send_headers(self): |
| """Transmit headers to the client, via self._write()""" |
| self.cleanup_headers() |
| self.headers_sent = True |
| if not self.origin_server or self.client_is_modern(): |
| self.send_preamble() |
| self._write(str(self.headers)) |
| |
| def result_is_file(self): |
| """True if 'self.result' is an instance of 'self.wsgi_file_wrapper'""" |
| wrapper = self.wsgi_file_wrapper |
| return wrapper is not None and isinstance(self.result,wrapper) |
| |
| def client_is_modern(self): |
| """True if client can accept status and headers""" |
| return self.environ['SERVER_PROTOCOL'].upper() != 'HTTP/0.9' |
| |
| def log_exception(self,exc_info): |
| """Log the 'exc_info' tuple in the server log |
| |
| Subclasses may override to retarget the output or change its format. |
| """ |
| try: |
| from traceback import print_exception |
| stderr = self.get_stderr() |
| print_exception( |
| exc_info[0], exc_info[1], exc_info[2], |
| self.traceback_limit, stderr |
| ) |
| stderr.flush() |
| finally: |
| exc_info = None |
| |
| def handle_error(self): |
| """Log current error, and send error output to client if possible""" |
| self.log_exception(sys.exc_info()) |
| if not self.headers_sent: |
| self.result = self.error_output(self.environ, self.start_response) |
| self.finish_response() |
| # XXX else: attempt advanced recovery techniques for HTML or text? |
| |
| def error_output(self, environ, start_response): |
| import traceback |
| start_response(self.error_status, self.error_headers[:], sys.exc_info()) |
| return ['\n'.join(traceback.format_exception(*sys.exc_info()))] |
| |
| # Pure abstract methods; *must* be overridden in subclasses |
| |
| def _write(self,data): |
| self.stdout.write(data) |
| self._write = self.stdout.write |
| |
| def _flush(self): |
| self.stdout.flush() |
| self._flush = self.stdout.flush |
| |
| def get_stdin(self): |
| return self.stdin |
| |
| def get_stderr(self): |
| return self.stderr |
| |
| def add_cgi_vars(self): |
| self.environ.update(self.base_env) |
| |
| class WSGIServer(HTTPServer): |
| """BaseHTTPServer that implements the Python WSGI protocol""" |
| application = None |
| |
| def server_bind(self): |
| """Override server_bind to store the server name.""" |
| try: |
| HTTPServer.server_bind(self) |
| except Exception, e: |
| raise WSGIServerException, e |
| self.setup_environ() |
| |
| def setup_environ(self): |
| # Set up base environment |
| env = self.base_environ = {} |
| env['SERVER_NAME'] = self.server_name |
| env['GATEWAY_INTERFACE'] = 'CGI/1.1' |
| env['SERVER_PORT'] = str(self.server_port) |
| env['REMOTE_HOST']='' |
| env['CONTENT_LENGTH']='' |
| env['SCRIPT_NAME'] = '' |
| |
| def get_app(self): |
| return self.application |
| |
| def set_app(self,application): |
| self.application = application |
| |
| class WSGIRequestHandler(BaseHTTPRequestHandler): |
| server_version = "WSGIServer/" + __version__ |
| |
| def __init__(self, *args, **kwargs): |
| from django.conf import settings |
| self.admin_media_prefix = settings.ADMIN_MEDIA_PREFIX |
| # We set self.path to avoid crashes in log_message() on unsupported |
| # requests (like "OPTIONS"). |
| self.path = '' |
| BaseHTTPRequestHandler.__init__(self, *args, **kwargs) |
| |
| def get_environ(self): |
| env = self.server.base_environ.copy() |
| env['SERVER_PROTOCOL'] = self.request_version |
| env['REQUEST_METHOD'] = self.command |
| if '?' in self.path: |
| path,query = self.path.split('?',1) |
| else: |
| path,query = self.path,'' |
| |
| env['PATH_INFO'] = urllib.unquote(path) |
| env['QUERY_STRING'] = query |
| env['REMOTE_ADDR'] = self.client_address[0] |
| |
| if self.headers.typeheader is None: |
| env['CONTENT_TYPE'] = self.headers.type |
| else: |
| env['CONTENT_TYPE'] = self.headers.typeheader |
| |
| length = self.headers.getheader('content-length') |
| if length: |
| env['CONTENT_LENGTH'] = length |
| |
| for h in self.headers.headers: |
| k,v = h.split(':',1) |
| k=k.replace('-','_').upper(); v=v.strip() |
| if k in env: |
| continue # skip content length, type,etc. |
| if 'HTTP_'+k in env: |
| env['HTTP_'+k] += ','+v # comma-separate multiple headers |
| else: |
| env['HTTP_'+k] = v |
| return env |
| |
| def get_stderr(self): |
| return sys.stderr |
| |
| def handle(self): |
| """Handle a single HTTP request""" |
| self.raw_requestline = self.rfile.readline() |
| if not self.parse_request(): # An error code has been sent, just exit |
| return |
| handler = ServerHandler(self.rfile, self.wfile, self.get_stderr(), self.get_environ()) |
| handler.request_handler = self # backpointer for logging |
| handler.run(self.server.get_app()) |
| |
| def log_message(self, format, *args): |
| # Don't bother logging requests for admin images or the favicon. |
| if self.path.startswith(self.admin_media_prefix) or self.path == '/favicon.ico': |
| return |
| sys.stderr.write("[%s] %s\n" % (self.log_date_time_string(), format % args)) |
| |
| class AdminMediaHandler(object): |
| """ |
| WSGI middleware that intercepts calls to the admin media directory, as |
| defined by the ADMIN_MEDIA_PREFIX setting, and serves those images. |
| Use this ONLY LOCALLY, for development! This hasn't been tested for |
| security and is not super efficient. |
| """ |
| def __init__(self, application, media_dir=None): |
| from django.conf import settings |
| self.application = application |
| if not media_dir: |
| import django |
| self.media_dir = django.__path__[0] + '/contrib/admin/media' |
| else: |
| self.media_dir = media_dir |
| self.media_url = settings.ADMIN_MEDIA_PREFIX |
| |
| def __call__(self, environ, start_response): |
| import os.path |
| |
| # Ignore requests that aren't under ADMIN_MEDIA_PREFIX. Also ignore |
| # all requests if ADMIN_MEDIA_PREFIX isn't a relative URL. |
| if self.media_url.startswith('http://') or self.media_url.startswith('https://') \ |
| or not environ['PATH_INFO'].startswith(self.media_url): |
| return self.application(environ, start_response) |
| |
| # Find the admin file and serve it up, if it exists and is readable. |
| relative_url = environ['PATH_INFO'][len(self.media_url):] |
| file_path = os.path.join(self.media_dir, relative_url) |
| if not os.path.exists(file_path): |
| status = '404 NOT FOUND' |
| headers = {'Content-type': 'text/plain'} |
| output = ['Page not found: %s' % file_path] |
| else: |
| try: |
| fp = open(file_path, 'rb') |
| except IOError: |
| status = '401 UNAUTHORIZED' |
| headers = {'Content-type': 'text/plain'} |
| output = ['Permission denied: %s' % file_path] |
| else: |
| status = '200 OK' |
| headers = {} |
| mime_type = mimetypes.guess_type(file_path)[0] |
| if mime_type: |
| headers['Content-Type'] = mime_type |
| output = [fp.read()] |
| fp.close() |
| start_response(status, headers.items()) |
| return output |
| |
| def run(addr, port, wsgi_handler): |
| server_address = (addr, port) |
| httpd = WSGIServer(server_address, WSGIRequestHandler) |
| httpd.set_app(wsgi_handler) |
| httpd.serve_forever() |