web/lib/django/core/servers/basehttp.py
changeset 0 0d40e90630ef
child 29 cc9b7e14412b
equal deleted inserted replaced
-1:000000000000 0:0d40e90630ef
       
     1 """
       
     2 BaseHTTPServer that implements the Python WSGI protocol (PEP 333, rev 1.21).
       
     3 
       
     4 Adapted from wsgiref.simple_server: http://svn.eby-sarna.com/wsgiref/
       
     5 
       
     6 This is a simple server for use in testing or debugging Django apps. It hasn't
       
     7 been reviewed for security issues. Don't use it for production use.
       
     8 """
       
     9 
       
    10 from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
       
    11 import mimetypes
       
    12 import os
       
    13 import re
       
    14 import stat
       
    15 import sys
       
    16 import urllib
       
    17 
       
    18 from django.utils.http import http_date
       
    19 from django.utils._os import safe_join
       
    20 
       
    21 __version__ = "0.1"
       
    22 __all__ = ['WSGIServer','WSGIRequestHandler']
       
    23 
       
    24 server_version = "WSGIServer/" + __version__
       
    25 sys_version = "Python/" + sys.version.split()[0]
       
    26 software_version = server_version + ' ' + sys_version
       
    27 
       
    28 class WSGIServerException(Exception):
       
    29     pass
       
    30 
       
    31 class FileWrapper(object):
       
    32     """Wrapper to convert file-like objects to iterables"""
       
    33 
       
    34     def __init__(self, filelike, blksize=8192):
       
    35         self.filelike = filelike
       
    36         self.blksize = blksize
       
    37         if hasattr(filelike,'close'):
       
    38             self.close = filelike.close
       
    39 
       
    40     def __getitem__(self,key):
       
    41         data = self.filelike.read(self.blksize)
       
    42         if data:
       
    43             return data
       
    44         raise IndexError
       
    45 
       
    46     def __iter__(self):
       
    47         return self
       
    48 
       
    49     def next(self):
       
    50         data = self.filelike.read(self.blksize)
       
    51         if data:
       
    52             return data
       
    53         raise StopIteration
       
    54 
       
    55 # Regular expression that matches `special' characters in parameters, the
       
    56 # existence of which force quoting of the parameter value.
       
    57 tspecials = re.compile(r'[ \(\)<>@,;:\\"/\[\]\?=]')
       
    58 
       
    59 def _formatparam(param, value=None, quote=1):
       
    60     """Convenience function to format and return a key=value pair.
       
    61 
       
    62     This will quote the value if needed or if quote is true.
       
    63     """
       
    64     if value is not None and len(value) > 0:
       
    65         if quote or tspecials.search(value):
       
    66             value = value.replace('\\', '\\\\').replace('"', r'\"')
       
    67             return '%s="%s"' % (param, value)
       
    68         else:
       
    69             return '%s=%s' % (param, value)
       
    70     else:
       
    71         return param
       
    72 
       
    73 class Headers(object):
       
    74     """Manage a collection of HTTP response headers"""
       
    75     def __init__(self,headers):
       
    76         if not isinstance(headers, list):
       
    77             raise TypeError("Headers must be a list of name/value tuples")
       
    78         self._headers = headers
       
    79 
       
    80     def __len__(self):
       
    81         """Return the total number of headers, including duplicates."""
       
    82         return len(self._headers)
       
    83 
       
    84     def __setitem__(self, name, val):
       
    85         """Set the value of a header."""
       
    86         del self[name]
       
    87         self._headers.append((name, val))
       
    88 
       
    89     def __delitem__(self,name):
       
    90         """Delete all occurrences of a header, if present.
       
    91 
       
    92         Does *not* raise an exception if the header is missing.
       
    93         """
       
    94         name = name.lower()
       
    95         self._headers[:] = [kv for kv in self._headers if kv[0].lower()<>name]
       
    96 
       
    97     def __getitem__(self,name):
       
    98         """Get the first header value for 'name'
       
    99 
       
   100         Return None if the header is missing instead of raising an exception.
       
   101 
       
   102         Note that if the header appeared multiple times, the first exactly which
       
   103         occurrance gets returned is undefined.  Use getall() to get all
       
   104         the values matching a header field name.
       
   105         """
       
   106         return self.get(name)
       
   107 
       
   108     def has_key(self, name):
       
   109         """Return true if the message contains the header."""
       
   110         return self.get(name) is not None
       
   111 
       
   112     __contains__ = has_key
       
   113 
       
   114     def get_all(self, name):
       
   115         """Return a list of all the values for the named field.
       
   116 
       
   117         These will be sorted in the order they appeared in the original header
       
   118         list or were added to this instance, and may contain duplicates.  Any
       
   119         fields deleted and re-inserted are always appended to the header list.
       
   120         If no fields exist with the given name, returns an empty list.
       
   121         """
       
   122         name = name.lower()
       
   123         return [kv[1] for kv in self._headers if kv[0].lower()==name]
       
   124 
       
   125 
       
   126     def get(self,name,default=None):
       
   127         """Get the first header value for 'name', or return 'default'"""
       
   128         name = name.lower()
       
   129         for k,v in self._headers:
       
   130             if k.lower()==name:
       
   131                 return v
       
   132         return default
       
   133 
       
   134     def keys(self):
       
   135         """Return a list of all the header field names.
       
   136 
       
   137         These will be sorted in the order they appeared in the original header
       
   138         list, or were added to this instance, and may contain duplicates.
       
   139         Any fields deleted and re-inserted are always appended to the header
       
   140         list.
       
   141         """
       
   142         return [k for k, v in self._headers]
       
   143 
       
   144     def values(self):
       
   145         """Return a list of all header values.
       
   146 
       
   147         These will be sorted in the order they appeared in the original header
       
   148         list, or were added to this instance, and may contain duplicates.
       
   149         Any fields deleted and re-inserted are always appended to the header
       
   150         list.
       
   151         """
       
   152         return [v for k, v in self._headers]
       
   153 
       
   154     def items(self):
       
   155         """Get all the header fields and values.
       
   156 
       
   157         These will be sorted in the order they were in the original header
       
   158         list, or were added to this instance, and may contain duplicates.
       
   159         Any fields deleted and re-inserted are always appended to the header
       
   160         list.
       
   161         """
       
   162         return self._headers[:]
       
   163 
       
   164     def __repr__(self):
       
   165         return "Headers(%s)" % `self._headers`
       
   166 
       
   167     def __str__(self):
       
   168         """str() returns the formatted headers, complete with end line,
       
   169         suitable for direct HTTP transmission."""
       
   170         return '\r\n'.join(["%s: %s" % kv for kv in self._headers]+['',''])
       
   171 
       
   172     def setdefault(self,name,value):
       
   173         """Return first matching header value for 'name', or 'value'
       
   174 
       
   175         If there is no header named 'name', add a new header with name 'name'
       
   176         and value 'value'."""
       
   177         result = self.get(name)
       
   178         if result is None:
       
   179             self._headers.append((name,value))
       
   180             return value
       
   181         else:
       
   182             return result
       
   183 
       
   184     def add_header(self, _name, _value, **_params):
       
   185         """Extended header setting.
       
   186 
       
   187         _name is the header field to add.  keyword arguments can be used to set
       
   188         additional parameters for the header field, with underscores converted
       
   189         to dashes.  Normally the parameter will be added as key="value" unless
       
   190         value is None, in which case only the key will be added.
       
   191 
       
   192         Example:
       
   193 
       
   194         h.add_header('content-disposition', 'attachment', filename='bud.gif')
       
   195 
       
   196         Note that unlike the corresponding 'email.Message' method, this does
       
   197         *not* handle '(charset, language, value)' tuples: all values must be
       
   198         strings or None.
       
   199         """
       
   200         parts = []
       
   201         if _value is not None:
       
   202             parts.append(_value)
       
   203         for k, v in _params.items():
       
   204             if v is None:
       
   205                 parts.append(k.replace('_', '-'))
       
   206             else:
       
   207                 parts.append(_formatparam(k.replace('_', '-'), v))
       
   208         self._headers.append((_name, "; ".join(parts)))
       
   209 
       
   210 def guess_scheme(environ):
       
   211     """Return a guess for whether 'wsgi.url_scheme' should be 'http' or 'https'
       
   212     """
       
   213     if environ.get("HTTPS") in ('yes','on','1'):
       
   214         return 'https'
       
   215     else:
       
   216         return 'http'
       
   217 
       
   218 _hop_headers = {
       
   219     'connection':1, 'keep-alive':1, 'proxy-authenticate':1,
       
   220     'proxy-authorization':1, 'te':1, 'trailers':1, 'transfer-encoding':1,
       
   221     'upgrade':1
       
   222 }
       
   223 
       
   224 def is_hop_by_hop(header_name):
       
   225     """Return true if 'header_name' is an HTTP/1.1 "Hop-by-Hop" header"""
       
   226     return header_name.lower() in _hop_headers
       
   227 
       
   228 class ServerHandler(object):
       
   229     """Manage the invocation of a WSGI application"""
       
   230 
       
   231     # Configuration parameters; can override per-subclass or per-instance
       
   232     wsgi_version = (1,0)
       
   233     wsgi_multithread = True
       
   234     wsgi_multiprocess = True
       
   235     wsgi_run_once = False
       
   236 
       
   237     origin_server = True    # We are transmitting direct to client
       
   238     http_version  = "1.0"   # Version that should be used for response
       
   239     server_software = software_version
       
   240 
       
   241     # os_environ is used to supply configuration from the OS environment:
       
   242     # by default it's a copy of 'os.environ' as of import time, but you can
       
   243     # override this in e.g. your __init__ method.
       
   244     os_environ = dict(os.environ.items())
       
   245 
       
   246     # Collaborator classes
       
   247     wsgi_file_wrapper = FileWrapper     # set to None to disable
       
   248     headers_class = Headers             # must be a Headers-like class
       
   249 
       
   250     # Error handling (also per-subclass or per-instance)
       
   251     traceback_limit = None  # Print entire traceback to self.get_stderr()
       
   252     error_status = "500 INTERNAL SERVER ERROR"
       
   253     error_headers = [('Content-Type','text/plain')]
       
   254 
       
   255     # State variables (don't mess with these)
       
   256     status = result = None
       
   257     headers_sent = False
       
   258     headers = None
       
   259     bytes_sent = 0
       
   260 
       
   261     def __init__(self, stdin, stdout, stderr, environ, multithread=True,
       
   262         multiprocess=False):
       
   263         self.stdin = stdin
       
   264         self.stdout = stdout
       
   265         self.stderr = stderr
       
   266         self.base_env = environ
       
   267         self.wsgi_multithread = multithread
       
   268         self.wsgi_multiprocess = multiprocess
       
   269 
       
   270     def run(self, application):
       
   271         """Invoke the application"""
       
   272         # Note to self: don't move the close()!  Asynchronous servers shouldn't
       
   273         # call close() from finish_response(), so if you close() anywhere but
       
   274         # the double-error branch here, you'll break asynchronous servers by
       
   275         # prematurely closing.  Async servers must return from 'run()' without
       
   276         # closing if there might still be output to iterate over.
       
   277         try:
       
   278             self.setup_environ()
       
   279             self.result = application(self.environ, self.start_response)
       
   280             self.finish_response()
       
   281         except:
       
   282             try:
       
   283                 self.handle_error()
       
   284             except:
       
   285                 # If we get an error handling an error, just give up already!
       
   286                 self.close()
       
   287                 raise   # ...and let the actual server figure it out.
       
   288 
       
   289     def setup_environ(self):
       
   290         """Set up the environment for one request"""
       
   291 
       
   292         env = self.environ = self.os_environ.copy()
       
   293         self.add_cgi_vars()
       
   294 
       
   295         env['wsgi.input']        = self.get_stdin()
       
   296         env['wsgi.errors']       = self.get_stderr()
       
   297         env['wsgi.version']      = self.wsgi_version
       
   298         env['wsgi.run_once']     = self.wsgi_run_once
       
   299         env['wsgi.url_scheme']   = self.get_scheme()
       
   300         env['wsgi.multithread']  = self.wsgi_multithread
       
   301         env['wsgi.multiprocess'] = self.wsgi_multiprocess
       
   302 
       
   303         if self.wsgi_file_wrapper is not None:
       
   304             env['wsgi.file_wrapper'] = self.wsgi_file_wrapper
       
   305 
       
   306         if self.origin_server and self.server_software:
       
   307             env.setdefault('SERVER_SOFTWARE',self.server_software)
       
   308 
       
   309     def finish_response(self):
       
   310         """
       
   311         Send any iterable data, then close self and the iterable
       
   312 
       
   313         Subclasses intended for use in asynchronous servers will want to
       
   314         redefine this method, such that it sets up callbacks in the event loop
       
   315         to iterate over the data, and to call 'self.close()' once the response
       
   316         is finished.
       
   317         """
       
   318         if not self.result_is_file() or not self.sendfile():
       
   319             for data in self.result:
       
   320                 self.write(data)
       
   321             self.finish_content()
       
   322         self.close()
       
   323 
       
   324     def get_scheme(self):
       
   325         """Return the URL scheme being used"""
       
   326         return guess_scheme(self.environ)
       
   327 
       
   328     def set_content_length(self):
       
   329         """Compute Content-Length or switch to chunked encoding if possible"""
       
   330         try:
       
   331             blocks = len(self.result)
       
   332         except (TypeError, AttributeError, NotImplementedError):
       
   333             pass
       
   334         else:
       
   335             if blocks==1:
       
   336                 self.headers['Content-Length'] = str(self.bytes_sent)
       
   337                 return
       
   338         # XXX Try for chunked encoding if origin server and client is 1.1
       
   339 
       
   340     def cleanup_headers(self):
       
   341         """Make any necessary header changes or defaults
       
   342 
       
   343         Subclasses can extend this to add other defaults.
       
   344         """
       
   345         if 'Content-Length' not in self.headers:
       
   346             self.set_content_length()
       
   347 
       
   348     def start_response(self, status, headers,exc_info=None):
       
   349         """'start_response()' callable as specified by PEP 333"""
       
   350 
       
   351         if exc_info:
       
   352             try:
       
   353                 if self.headers_sent:
       
   354                     # Re-raise original exception if headers sent
       
   355                     raise exc_info[0], exc_info[1], exc_info[2]
       
   356             finally:
       
   357                 exc_info = None        # avoid dangling circular ref
       
   358         elif self.headers is not None:
       
   359             raise AssertionError("Headers already set!")
       
   360 
       
   361         assert isinstance(status, str),"Status must be a string"
       
   362         assert len(status)>=4,"Status must be at least 4 characters"
       
   363         assert int(status[:3]),"Status message must begin w/3-digit code"
       
   364         assert status[3]==" ", "Status message must have a space after code"
       
   365         if __debug__:
       
   366             for name,val in headers:
       
   367                 assert isinstance(name, str),"Header names must be strings"
       
   368                 assert isinstance(val, str),"Header values must be strings"
       
   369                 assert not is_hop_by_hop(name),"Hop-by-hop headers not allowed"
       
   370         self.status = status
       
   371         self.headers = self.headers_class(headers)
       
   372         return self.write
       
   373 
       
   374     def send_preamble(self):
       
   375         """Transmit version/status/date/server, via self._write()"""
       
   376         if self.origin_server:
       
   377             if self.client_is_modern():
       
   378                 self._write('HTTP/%s %s\r\n' % (self.http_version,self.status))
       
   379                 if 'Date' not in self.headers:
       
   380                     self._write(
       
   381                         'Date: %s\r\n' % http_date()
       
   382                     )
       
   383                 if self.server_software and 'Server' not in self.headers:
       
   384                     self._write('Server: %s\r\n' % self.server_software)
       
   385         else:
       
   386             self._write('Status: %s\r\n' % self.status)
       
   387 
       
   388     def write(self, data):
       
   389         """'write()' callable as specified by PEP 333"""
       
   390 
       
   391         assert isinstance(data, str), "write() argument must be string"
       
   392 
       
   393         if not self.status:
       
   394             raise AssertionError("write() before start_response()")
       
   395 
       
   396         elif not self.headers_sent:
       
   397             # Before the first output, send the stored headers
       
   398             self.bytes_sent = len(data)    # make sure we know content-length
       
   399             self.send_headers()
       
   400         else:
       
   401             self.bytes_sent += len(data)
       
   402 
       
   403         # XXX check Content-Length and truncate if too many bytes written?
       
   404 
       
   405         # If data is too large, socket will choke, so write chunks no larger
       
   406         # than 32MB at a time.
       
   407         length = len(data)
       
   408         if length > 33554432:
       
   409             offset = 0
       
   410             while offset < length:
       
   411                 chunk_size = min(33554432, length)
       
   412                 self._write(data[offset:offset+chunk_size])
       
   413                 self._flush()
       
   414                 offset += chunk_size
       
   415         else:
       
   416             self._write(data)
       
   417             self._flush()
       
   418 
       
   419     def sendfile(self):
       
   420         """Platform-specific file transmission
       
   421 
       
   422         Override this method in subclasses to support platform-specific
       
   423         file transmission.  It is only called if the application's
       
   424         return iterable ('self.result') is an instance of
       
   425         'self.wsgi_file_wrapper'.
       
   426 
       
   427         This method should return a true value if it was able to actually
       
   428         transmit the wrapped file-like object using a platform-specific
       
   429         approach.  It should return a false value if normal iteration
       
   430         should be used instead.  An exception can be raised to indicate
       
   431         that transmission was attempted, but failed.
       
   432 
       
   433         NOTE: this method should call 'self.send_headers()' if
       
   434         'self.headers_sent' is false and it is going to attempt direct
       
   435         transmission of the file1.
       
   436         """
       
   437         return False   # No platform-specific transmission by default
       
   438 
       
   439     def finish_content(self):
       
   440         """Ensure headers and content have both been sent"""
       
   441         if not self.headers_sent:
       
   442             self.headers['Content-Length'] = "0"
       
   443             self.send_headers()
       
   444         else:
       
   445             pass # XXX check if content-length was too short?
       
   446 
       
   447     def close(self):
       
   448         try:
       
   449             self.request_handler.log_request(self.status.split(' ',1)[0], self.bytes_sent)
       
   450         finally:
       
   451             try:
       
   452                 if hasattr(self.result,'close'):
       
   453                     self.result.close()
       
   454             finally:
       
   455                 self.result = self.headers = self.status = self.environ = None
       
   456                 self.bytes_sent = 0; self.headers_sent = False
       
   457 
       
   458     def send_headers(self):
       
   459         """Transmit headers to the client, via self._write()"""
       
   460         self.cleanup_headers()
       
   461         self.headers_sent = True
       
   462         if not self.origin_server or self.client_is_modern():
       
   463             self.send_preamble()
       
   464             self._write(str(self.headers))
       
   465 
       
   466     def result_is_file(self):
       
   467         """True if 'self.result' is an instance of 'self.wsgi_file_wrapper'"""
       
   468         wrapper = self.wsgi_file_wrapper
       
   469         return wrapper is not None and isinstance(self.result,wrapper)
       
   470 
       
   471     def client_is_modern(self):
       
   472         """True if client can accept status and headers"""
       
   473         return self.environ['SERVER_PROTOCOL'].upper() != 'HTTP/0.9'
       
   474 
       
   475     def log_exception(self,exc_info):
       
   476         """Log the 'exc_info' tuple in the server log
       
   477 
       
   478         Subclasses may override to retarget the output or change its format.
       
   479         """
       
   480         try:
       
   481             from traceback import print_exception
       
   482             stderr = self.get_stderr()
       
   483             print_exception(
       
   484                 exc_info[0], exc_info[1], exc_info[2],
       
   485                 self.traceback_limit, stderr
       
   486             )
       
   487             stderr.flush()
       
   488         finally:
       
   489             exc_info = None
       
   490 
       
   491     def handle_error(self):
       
   492         """Log current error, and send error output to client if possible"""
       
   493         self.log_exception(sys.exc_info())
       
   494         if not self.headers_sent:
       
   495             self.result = self.error_output(self.environ, self.start_response)
       
   496             self.finish_response()
       
   497         # XXX else: attempt advanced recovery techniques for HTML or text?
       
   498 
       
   499     def error_output(self, environ, start_response):
       
   500         import traceback
       
   501         start_response(self.error_status, self.error_headers[:], sys.exc_info())
       
   502         return ['\n'.join(traceback.format_exception(*sys.exc_info()))]
       
   503 
       
   504     # Pure abstract methods; *must* be overridden in subclasses
       
   505 
       
   506     def _write(self,data):
       
   507         self.stdout.write(data)
       
   508         self._write = self.stdout.write
       
   509 
       
   510     def _flush(self):
       
   511         self.stdout.flush()
       
   512         self._flush = self.stdout.flush
       
   513 
       
   514     def get_stdin(self):
       
   515         return self.stdin
       
   516 
       
   517     def get_stderr(self):
       
   518         return self.stderr
       
   519 
       
   520     def add_cgi_vars(self):
       
   521         self.environ.update(self.base_env)
       
   522 
       
   523 class WSGIServer(HTTPServer):
       
   524     """BaseHTTPServer that implements the Python WSGI protocol"""
       
   525     application = None
       
   526 
       
   527     def server_bind(self):
       
   528         """Override server_bind to store the server name."""
       
   529         try:
       
   530             HTTPServer.server_bind(self)
       
   531         except Exception, e:
       
   532             raise WSGIServerException, e
       
   533         self.setup_environ()
       
   534 
       
   535     def setup_environ(self):
       
   536         # Set up base environment
       
   537         env = self.base_environ = {}
       
   538         env['SERVER_NAME'] = self.server_name
       
   539         env['GATEWAY_INTERFACE'] = 'CGI/1.1'
       
   540         env['SERVER_PORT'] = str(self.server_port)
       
   541         env['REMOTE_HOST']=''
       
   542         env['CONTENT_LENGTH']=''
       
   543         env['SCRIPT_NAME'] = ''
       
   544 
       
   545     def get_app(self):
       
   546         return self.application
       
   547 
       
   548     def set_app(self,application):
       
   549         self.application = application
       
   550 
       
   551 class WSGIRequestHandler(BaseHTTPRequestHandler):
       
   552     server_version = "WSGIServer/" + __version__
       
   553 
       
   554     def __init__(self, *args, **kwargs):
       
   555         from django.conf import settings
       
   556         self.admin_media_prefix = settings.ADMIN_MEDIA_PREFIX
       
   557         # We set self.path to avoid crashes in log_message() on unsupported
       
   558         # requests (like "OPTIONS").
       
   559         self.path = ''
       
   560         BaseHTTPRequestHandler.__init__(self, *args, **kwargs)
       
   561 
       
   562     def get_environ(self):
       
   563         env = self.server.base_environ.copy()
       
   564         env['SERVER_PROTOCOL'] = self.request_version
       
   565         env['REQUEST_METHOD'] = self.command
       
   566         if '?' in self.path:
       
   567             path,query = self.path.split('?',1)
       
   568         else:
       
   569             path,query = self.path,''
       
   570 
       
   571         env['PATH_INFO'] = urllib.unquote(path)
       
   572         env['QUERY_STRING'] = query
       
   573         env['REMOTE_ADDR'] = self.client_address[0]
       
   574 
       
   575         if self.headers.typeheader is None:
       
   576             env['CONTENT_TYPE'] = self.headers.type
       
   577         else:
       
   578             env['CONTENT_TYPE'] = self.headers.typeheader
       
   579 
       
   580         length = self.headers.getheader('content-length')
       
   581         if length:
       
   582             env['CONTENT_LENGTH'] = length
       
   583 
       
   584         for h in self.headers.headers:
       
   585             k,v = h.split(':',1)
       
   586             k=k.replace('-','_').upper(); v=v.strip()
       
   587             if k in env:
       
   588                 continue                    # skip content length, type,etc.
       
   589             if 'HTTP_'+k in env:
       
   590                 env['HTTP_'+k] += ','+v     # comma-separate multiple headers
       
   591             else:
       
   592                 env['HTTP_'+k] = v
       
   593         return env
       
   594 
       
   595     def get_stderr(self):
       
   596         return sys.stderr
       
   597 
       
   598     def handle(self):
       
   599         """Handle a single HTTP request"""
       
   600         self.raw_requestline = self.rfile.readline()
       
   601         if not self.parse_request(): # An error code has been sent, just exit
       
   602             return
       
   603         handler = ServerHandler(self.rfile, self.wfile, self.get_stderr(), self.get_environ())
       
   604         handler.request_handler = self      # backpointer for logging
       
   605         handler.run(self.server.get_app())
       
   606 
       
   607     def log_message(self, format, *args):
       
   608         # Don't bother logging requests for admin images or the favicon.
       
   609         if self.path.startswith(self.admin_media_prefix) or self.path == '/favicon.ico':
       
   610             return
       
   611         sys.stderr.write("[%s] %s\n" % (self.log_date_time_string(), format % args))
       
   612 
       
   613 class AdminMediaHandler(object):
       
   614     """
       
   615     WSGI middleware that intercepts calls to the admin media directory, as
       
   616     defined by the ADMIN_MEDIA_PREFIX setting, and serves those images.
       
   617     Use this ONLY LOCALLY, for development! This hasn't been tested for
       
   618     security and is not super efficient.
       
   619     """
       
   620     def __init__(self, application, media_dir=None):
       
   621         from django.conf import settings
       
   622         self.application = application
       
   623         if not media_dir:
       
   624             import django
       
   625             self.media_dir = \
       
   626                 os.path.join(django.__path__[0], 'contrib', 'admin', 'media')
       
   627         else:
       
   628             self.media_dir = media_dir
       
   629         self.media_url = settings.ADMIN_MEDIA_PREFIX
       
   630 
       
   631     def file_path(self, url):
       
   632         """
       
   633         Returns the path to the media file on disk for the given URL.
       
   634 
       
   635         The passed URL is assumed to begin with ADMIN_MEDIA_PREFIX.  If the
       
   636         resultant file path is outside the media directory, then a ValueError
       
   637         is raised.
       
   638         """
       
   639         # Remove ADMIN_MEDIA_PREFIX.
       
   640         relative_url = url[len(self.media_url):]
       
   641         relative_path = urllib.url2pathname(relative_url)
       
   642         return safe_join(self.media_dir, relative_path)
       
   643 
       
   644     def __call__(self, environ, start_response):
       
   645         import os.path
       
   646 
       
   647         # Ignore requests that aren't under ADMIN_MEDIA_PREFIX. Also ignore
       
   648         # all requests if ADMIN_MEDIA_PREFIX isn't a relative URL.
       
   649         if self.media_url.startswith('http://') or self.media_url.startswith('https://') \
       
   650             or not environ['PATH_INFO'].startswith(self.media_url):
       
   651             return self.application(environ, start_response)
       
   652 
       
   653         # Find the admin file and serve it up, if it exists and is readable.
       
   654         try:
       
   655             file_path = self.file_path(environ['PATH_INFO'])
       
   656         except ValueError: # Resulting file path was not valid.
       
   657             status = '404 NOT FOUND'
       
   658             headers = {'Content-type': 'text/plain'}
       
   659             output = ['Page not found: %s' % environ['PATH_INFO']]
       
   660             start_response(status, headers.items())
       
   661             return output
       
   662         if not os.path.exists(file_path):
       
   663             status = '404 NOT FOUND'
       
   664             headers = {'Content-type': 'text/plain'}
       
   665             output = ['Page not found: %s' % environ['PATH_INFO']]
       
   666         else:
       
   667             try:
       
   668                 fp = open(file_path, 'rb')
       
   669             except IOError:
       
   670                 status = '401 UNAUTHORIZED'
       
   671                 headers = {'Content-type': 'text/plain'}
       
   672                 output = ['Permission denied: %s' % environ['PATH_INFO']]
       
   673             else:
       
   674                 # This is a very simple implementation of conditional GET with
       
   675                 # the Last-Modified header. It makes media files a bit speedier
       
   676                 # because the files are only read off disk for the first
       
   677                 # request (assuming the browser/client supports conditional
       
   678                 # GET).
       
   679                 mtime = http_date(os.stat(file_path)[stat.ST_MTIME])
       
   680                 headers = {'Last-Modified': mtime}
       
   681                 if environ.get('HTTP_IF_MODIFIED_SINCE', None) == mtime:
       
   682                     status = '304 NOT MODIFIED'
       
   683                     output = []
       
   684                 else:
       
   685                     status = '200 OK'
       
   686                     mime_type = mimetypes.guess_type(file_path)[0]
       
   687                     if mime_type:
       
   688                         headers['Content-Type'] = mime_type
       
   689                     output = [fp.read()]
       
   690                     fp.close()
       
   691         start_response(status, headers.items())
       
   692         return output
       
   693 
       
   694 def run(addr, port, wsgi_handler):
       
   695     server_address = (addr, port)
       
   696     httpd = WSGIServer(server_address, WSGIRequestHandler)
       
   697     httpd.set_app(wsgi_handler)
       
   698     httpd.serve_forever()