|
0
|
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 |
|