|
1 from threading import Lock |
|
2 from pprint import pformat |
|
3 try: |
|
4 from cStringIO import StringIO |
|
5 except ImportError: |
|
6 from StringIO import StringIO |
|
7 |
|
8 from django import http |
|
9 from django.core import signals |
|
10 from django.core.handlers import base |
|
11 from django.core.urlresolvers import set_script_prefix |
|
12 from django.utils import datastructures |
|
13 from django.utils.encoding import force_unicode, iri_to_uri |
|
14 |
|
15 # See http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html |
|
16 STATUS_CODE_TEXT = { |
|
17 100: 'CONTINUE', |
|
18 101: 'SWITCHING PROTOCOLS', |
|
19 200: 'OK', |
|
20 201: 'CREATED', |
|
21 202: 'ACCEPTED', |
|
22 203: 'NON-AUTHORITATIVE INFORMATION', |
|
23 204: 'NO CONTENT', |
|
24 205: 'RESET CONTENT', |
|
25 206: 'PARTIAL CONTENT', |
|
26 300: 'MULTIPLE CHOICES', |
|
27 301: 'MOVED PERMANENTLY', |
|
28 302: 'FOUND', |
|
29 303: 'SEE OTHER', |
|
30 304: 'NOT MODIFIED', |
|
31 305: 'USE PROXY', |
|
32 306: 'RESERVED', |
|
33 307: 'TEMPORARY REDIRECT', |
|
34 400: 'BAD REQUEST', |
|
35 401: 'UNAUTHORIZED', |
|
36 402: 'PAYMENT REQUIRED', |
|
37 403: 'FORBIDDEN', |
|
38 404: 'NOT FOUND', |
|
39 405: 'METHOD NOT ALLOWED', |
|
40 406: 'NOT ACCEPTABLE', |
|
41 407: 'PROXY AUTHENTICATION REQUIRED', |
|
42 408: 'REQUEST TIMEOUT', |
|
43 409: 'CONFLICT', |
|
44 410: 'GONE', |
|
45 411: 'LENGTH REQUIRED', |
|
46 412: 'PRECONDITION FAILED', |
|
47 413: 'REQUEST ENTITY TOO LARGE', |
|
48 414: 'REQUEST-URI TOO LONG', |
|
49 415: 'UNSUPPORTED MEDIA TYPE', |
|
50 416: 'REQUESTED RANGE NOT SATISFIABLE', |
|
51 417: 'EXPECTATION FAILED', |
|
52 500: 'INTERNAL SERVER ERROR', |
|
53 501: 'NOT IMPLEMENTED', |
|
54 502: 'BAD GATEWAY', |
|
55 503: 'SERVICE UNAVAILABLE', |
|
56 504: 'GATEWAY TIMEOUT', |
|
57 505: 'HTTP VERSION NOT SUPPORTED', |
|
58 } |
|
59 |
|
60 def safe_copyfileobj(fsrc, fdst, length=16*1024, size=0): |
|
61 """ |
|
62 A version of shutil.copyfileobj that will not read more than 'size' bytes. |
|
63 This makes it safe from clients sending more than CONTENT_LENGTH bytes of |
|
64 data in the body. |
|
65 """ |
|
66 if not size: |
|
67 return |
|
68 while size > 0: |
|
69 buf = fsrc.read(min(length, size)) |
|
70 if not buf: |
|
71 break |
|
72 fdst.write(buf) |
|
73 size -= len(buf) |
|
74 |
|
75 class WSGIRequest(http.HttpRequest): |
|
76 def __init__(self, environ): |
|
77 script_name = base.get_script_name(environ) |
|
78 path_info = force_unicode(environ.get('PATH_INFO', u'/')) |
|
79 if not path_info or path_info == script_name: |
|
80 # Sometimes PATH_INFO exists, but is empty (e.g. accessing |
|
81 # the SCRIPT_NAME URL without a trailing slash). We really need to |
|
82 # operate as if they'd requested '/'. Not amazingly nice to force |
|
83 # the path like this, but should be harmless. |
|
84 # |
|
85 # (The comparison of path_info to script_name is to work around an |
|
86 # apparent bug in flup 1.0.1. Se Django ticket #8490). |
|
87 path_info = u'/' |
|
88 self.environ = environ |
|
89 self.path_info = path_info |
|
90 self.path = '%s%s' % (script_name, path_info) |
|
91 self.META = environ |
|
92 self.META['PATH_INFO'] = path_info |
|
93 self.META['SCRIPT_NAME'] = script_name |
|
94 self.method = environ['REQUEST_METHOD'].upper() |
|
95 self._post_parse_error = False |
|
96 |
|
97 def __repr__(self): |
|
98 # Since this is called as part of error handling, we need to be very |
|
99 # robust against potentially malformed input. |
|
100 try: |
|
101 get = pformat(self.GET) |
|
102 except: |
|
103 get = '<could not parse>' |
|
104 if self._post_parse_error: |
|
105 post = '<could not parse>' |
|
106 else: |
|
107 try: |
|
108 post = pformat(self.POST) |
|
109 except: |
|
110 post = '<could not parse>' |
|
111 try: |
|
112 cookies = pformat(self.COOKIES) |
|
113 except: |
|
114 cookies = '<could not parse>' |
|
115 try: |
|
116 meta = pformat(self.META) |
|
117 except: |
|
118 meta = '<could not parse>' |
|
119 return '<WSGIRequest\nGET:%s,\nPOST:%s,\nCOOKIES:%s,\nMETA:%s>' % \ |
|
120 (get, post, cookies, meta) |
|
121 |
|
122 def get_full_path(self): |
|
123 # RFC 3986 requires query string arguments to be in the ASCII range. |
|
124 # Rather than crash if this doesn't happen, we encode defensively. |
|
125 return '%s%s' % (self.path, self.environ.get('QUERY_STRING', '') and ('?' + iri_to_uri(self.environ.get('QUERY_STRING', ''))) or '') |
|
126 |
|
127 def is_secure(self): |
|
128 return 'wsgi.url_scheme' in self.environ \ |
|
129 and self.environ['wsgi.url_scheme'] == 'https' |
|
130 |
|
131 def _load_post_and_files(self): |
|
132 # Populates self._post and self._files |
|
133 if self.method == 'POST': |
|
134 if self.environ.get('CONTENT_TYPE', '').startswith('multipart'): |
|
135 self._raw_post_data = '' |
|
136 try: |
|
137 self._post, self._files = self.parse_file_upload(self.META, self.environ['wsgi.input']) |
|
138 except: |
|
139 # An error occured while parsing POST data. Since when |
|
140 # formatting the error the request handler might access |
|
141 # self.POST, set self._post and self._file to prevent |
|
142 # attempts to parse POST data again. |
|
143 self._post = http.QueryDict('') |
|
144 self._files = datastructures.MultiValueDict() |
|
145 # Mark that an error occured. This allows self.__repr__ to |
|
146 # be explicit about it instead of simply representing an |
|
147 # empty POST |
|
148 self._post_parse_error = True |
|
149 raise |
|
150 else: |
|
151 self._post, self._files = http.QueryDict(self.raw_post_data, encoding=self._encoding), datastructures.MultiValueDict() |
|
152 else: |
|
153 self._post, self._files = http.QueryDict('', encoding=self._encoding), datastructures.MultiValueDict() |
|
154 |
|
155 def _get_request(self): |
|
156 if not hasattr(self, '_request'): |
|
157 self._request = datastructures.MergeDict(self.POST, self.GET) |
|
158 return self._request |
|
159 |
|
160 def _get_get(self): |
|
161 if not hasattr(self, '_get'): |
|
162 # The WSGI spec says 'QUERY_STRING' may be absent. |
|
163 self._get = http.QueryDict(self.environ.get('QUERY_STRING', ''), encoding=self._encoding) |
|
164 return self._get |
|
165 |
|
166 def _set_get(self, get): |
|
167 self._get = get |
|
168 |
|
169 def _get_post(self): |
|
170 if not hasattr(self, '_post'): |
|
171 self._load_post_and_files() |
|
172 return self._post |
|
173 |
|
174 def _set_post(self, post): |
|
175 self._post = post |
|
176 |
|
177 def _get_cookies(self): |
|
178 if not hasattr(self, '_cookies'): |
|
179 self._cookies = http.parse_cookie(self.environ.get('HTTP_COOKIE', '')) |
|
180 return self._cookies |
|
181 |
|
182 def _set_cookies(self, cookies): |
|
183 self._cookies = cookies |
|
184 |
|
185 def _get_files(self): |
|
186 if not hasattr(self, '_files'): |
|
187 self._load_post_and_files() |
|
188 return self._files |
|
189 |
|
190 def _get_raw_post_data(self): |
|
191 try: |
|
192 return self._raw_post_data |
|
193 except AttributeError: |
|
194 buf = StringIO() |
|
195 try: |
|
196 # CONTENT_LENGTH might be absent if POST doesn't have content at all (lighttpd) |
|
197 content_length = int(self.environ.get('CONTENT_LENGTH', 0)) |
|
198 except (ValueError, TypeError): |
|
199 # If CONTENT_LENGTH was empty string or not an integer, don't |
|
200 # error out. We've also seen None passed in here (against all |
|
201 # specs, but see ticket #8259), so we handle TypeError as well. |
|
202 content_length = 0 |
|
203 if content_length > 0: |
|
204 safe_copyfileobj(self.environ['wsgi.input'], buf, |
|
205 size=content_length) |
|
206 self._raw_post_data = buf.getvalue() |
|
207 buf.close() |
|
208 return self._raw_post_data |
|
209 |
|
210 GET = property(_get_get, _set_get) |
|
211 POST = property(_get_post, _set_post) |
|
212 COOKIES = property(_get_cookies, _set_cookies) |
|
213 FILES = property(_get_files) |
|
214 REQUEST = property(_get_request) |
|
215 raw_post_data = property(_get_raw_post_data) |
|
216 |
|
217 class WSGIHandler(base.BaseHandler): |
|
218 initLock = Lock() |
|
219 request_class = WSGIRequest |
|
220 |
|
221 def __call__(self, environ, start_response): |
|
222 from django.conf import settings |
|
223 |
|
224 # Set up middleware if needed. We couldn't do this earlier, because |
|
225 # settings weren't available. |
|
226 if self._request_middleware is None: |
|
227 self.initLock.acquire() |
|
228 # Check that middleware is still uninitialised. |
|
229 if self._request_middleware is None: |
|
230 self.load_middleware() |
|
231 self.initLock.release() |
|
232 |
|
233 set_script_prefix(base.get_script_name(environ)) |
|
234 signals.request_started.send(sender=self.__class__) |
|
235 try: |
|
236 try: |
|
237 request = self.request_class(environ) |
|
238 except UnicodeDecodeError: |
|
239 response = http.HttpResponseBadRequest() |
|
240 else: |
|
241 response = self.get_response(request) |
|
242 |
|
243 # Apply response middleware |
|
244 for middleware_method in self._response_middleware: |
|
245 response = middleware_method(request, response) |
|
246 response = self.apply_response_fixes(request, response) |
|
247 finally: |
|
248 signals.request_finished.send(sender=self.__class__) |
|
249 |
|
250 try: |
|
251 status_text = STATUS_CODE_TEXT[response.status_code] |
|
252 except KeyError: |
|
253 status_text = 'UNKNOWN STATUS CODE' |
|
254 status = '%s %s' % (response.status_code, status_text) |
|
255 response_headers = [(str(k), str(v)) for k, v in response.items()] |
|
256 for c in response.cookies.values(): |
|
257 response_headers.append(('Set-Cookie', str(c.output(header='')))) |
|
258 start_response(status, response_headers) |
|
259 return response |
|
260 |