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