Django

Code

root/django/trunk/django/core/servers/basehttp.py

Revision 9055, 24.3 kB (checked in by adrian, 2 months ago)

Fixed #8409 -- The runserver now uses conditional GET for admin media files, instead of reloading the files off disk for every request. Thanks for reporting, andylowry

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