blob: 4a771374519528d79126a7cdbccabb3bd00b5ab4 [file] [log] [blame]
Thomas Wouters0e3f5912006-08-11 14:57:12 +00001"""Base classes for server/gateway implementations"""
2
Guido van Rossum06a2dc72006-08-17 08:56:08 +00003from .util import FileWrapper, guess_scheme, is_hop_by_hop
4from .headers import Headers
Thomas Wouters0e3f5912006-08-11 14:57:12 +00005
6import sys, os, time
7
8__all__ = ['BaseHandler', 'SimpleHandler', 'BaseCGIHandler', 'CGIHandler']
9
Thomas Wouters0e3f5912006-08-11 14:57:12 +000010# Weekday and month names for HTTP date/time formatting; always English!
11_weekdayname = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
12_monthname = [None, # Dummy so we can use 1-based month numbers
13 "Jan", "Feb", "Mar", "Apr", "May", "Jun",
14 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
15
16def format_date_time(timestamp):
17 year, month, day, hh, mm, ss, wd, y, z = time.gmtime(timestamp)
18 return "%s, %02d %3s %4d %02d:%02d:%02d GMT" % (
19 _weekdayname[wd], day, _monthname[month], year, hh, mm, ss
20 )
21
22
23
24class BaseHandler:
25 """Manage the invocation of a WSGI application"""
26
27 # Configuration parameters; can override per-subclass or per-instance
28 wsgi_version = (1,0)
29 wsgi_multithread = True
30 wsgi_multiprocess = True
31 wsgi_run_once = False
32
33 origin_server = True # We are transmitting direct to client
34 http_version = "1.0" # Version that should be used for response
35 server_software = None # String name of server software, if any
36
37 # os_environ is used to supply configuration from the OS environment:
38 # by default it's a copy of 'os.environ' as of import time, but you can
39 # override this in e.g. your __init__ method.
40 os_environ = dict(os.environ.items())
41
42 # Collaborator classes
43 wsgi_file_wrapper = FileWrapper # set to None to disable
44 headers_class = Headers # must be a Headers-like class
45
46 # Error handling (also per-subclass or per-instance)
47 traceback_limit = None # Print entire traceback to self.get_stderr()
48 error_status = "500 Dude, this is whack!"
49 error_headers = [('Content-Type','text/plain')]
50 error_body = "A server error occurred. Please contact the administrator."
51
52 # State variables (don't mess with these)
53 status = result = None
54 headers_sent = False
55 headers = None
56 bytes_sent = 0
57
58
59
60
61
62
63
64
65 def run(self, application):
66 """Invoke the application"""
67 # Note to self: don't move the close()! Asynchronous servers shouldn't
68 # call close() from finish_response(), so if you close() anywhere but
69 # the double-error branch here, you'll break asynchronous servers by
70 # prematurely closing. Async servers must return from 'run()' without
71 # closing if there might still be output to iterate over.
72 try:
73 self.setup_environ()
74 self.result = application(self.environ, self.start_response)
75 self.finish_response()
76 except:
77 try:
78 self.handle_error()
79 except:
80 # If we get an error handling an error, just give up already!
81 self.close()
82 raise # ...and let the actual server figure it out.
83
84
85 def setup_environ(self):
86 """Set up the environment for one request"""
87
88 env = self.environ = self.os_environ.copy()
89 self.add_cgi_vars()
90
91 env['wsgi.input'] = self.get_stdin()
92 env['wsgi.errors'] = self.get_stderr()
93 env['wsgi.version'] = self.wsgi_version
94 env['wsgi.run_once'] = self.wsgi_run_once
95 env['wsgi.url_scheme'] = self.get_scheme()
96 env['wsgi.multithread'] = self.wsgi_multithread
97 env['wsgi.multiprocess'] = self.wsgi_multiprocess
98
99 if self.wsgi_file_wrapper is not None:
100 env['wsgi.file_wrapper'] = self.wsgi_file_wrapper
101
102 if self.origin_server and self.server_software:
103 env.setdefault('SERVER_SOFTWARE',self.server_software)
104
105
106 def finish_response(self):
107 """Send any iterable data, then close self and the iterable
108
109 Subclasses intended for use in asynchronous servers will
110 want to redefine this method, such that it sets up callbacks
111 in the event loop to iterate over the data, and to call
112 'self.close()' once the response is finished.
113 """
114 if not self.result_is_file() or not self.sendfile():
115 for data in self.result:
116 self.write(data)
117 self.finish_content()
118 self.close()
119
120
121 def get_scheme(self):
122 """Return the URL scheme being used"""
123 return guess_scheme(self.environ)
124
125
126 def set_content_length(self):
127 """Compute Content-Length or switch to chunked encoding if possible"""
128 try:
129 blocks = len(self.result)
130 except (TypeError,AttributeError,NotImplementedError):
131 pass
132 else:
133 if blocks==1:
134 self.headers['Content-Length'] = str(self.bytes_sent)
135 return
136 # XXX Try for chunked encoding if origin server and client is 1.1
137
138
139 def cleanup_headers(self):
140 """Make any necessary header changes or defaults
141
142 Subclasses can extend this to add other defaults.
143 """
Guido van Rossume2b70bc2006-08-18 22:13:04 +0000144 if 'Content-Length' not in self.headers:
Thomas Wouters0e3f5912006-08-11 14:57:12 +0000145 self.set_content_length()
146
147 def start_response(self, status, headers,exc_info=None):
148 """'start_response()' callable as specified by PEP 333"""
149
150 if exc_info:
151 try:
152 if self.headers_sent:
153 # Re-raise original exception if headers sent
Collin Winter828f04a2007-08-31 00:04:24 +0000154 raise exc_info[0](exc_info[1]).with_traceback(exc_info[2])
Thomas Wouters0e3f5912006-08-11 14:57:12 +0000155 finally:
156 exc_info = None # avoid dangling circular ref
157 elif self.headers is not None:
158 raise AssertionError("Headers already set!")
159
Antoine Pitrou38a66ad2009-01-03 18:41:49 +0000160 status = self._convert_string_type(status, "Status")
Thomas Wouters0e3f5912006-08-11 14:57:12 +0000161 assert len(status)>=4,"Status must be at least 4 characters"
162 assert int(status[:3]),"Status message must begin w/3-digit code"
163 assert status[3]==" ", "Status message must have a space after code"
Antoine Pitrou38a66ad2009-01-03 18:41:49 +0000164
165 str_headers = []
166 for name,val in headers:
167 name = self._convert_string_type(name, "Header name")
168 val = self._convert_string_type(val, "Header value")
169 str_headers.append((name, val))
170 assert not is_hop_by_hop(name),"Hop-by-hop headers not allowed"
171
Thomas Wouters0e3f5912006-08-11 14:57:12 +0000172 self.status = status
Antoine Pitrou38a66ad2009-01-03 18:41:49 +0000173 self.headers = self.headers_class(str_headers)
Thomas Wouters0e3f5912006-08-11 14:57:12 +0000174 return self.write
175
Antoine Pitrou38a66ad2009-01-03 18:41:49 +0000176 def _convert_string_type(self, value, title):
177 """Convert/check value type."""
178 if isinstance(value, str):
179 return value
180 assert isinstance(value, bytes), \
181 "{0} must be a string or bytes object (not {1})".format(title, value)
182 return str(value, "iso-8859-1")
Thomas Wouters0e3f5912006-08-11 14:57:12 +0000183
184 def send_preamble(self):
185 """Transmit version/status/date/server, via self._write()"""
186 if self.origin_server:
187 if self.client_is_modern():
188 self._write('HTTP/%s %s\r\n' % (self.http_version,self.status))
Guido van Rossume2b70bc2006-08-18 22:13:04 +0000189 if 'Date' not in self.headers:
Thomas Wouters0e3f5912006-08-11 14:57:12 +0000190 self._write(
191 'Date: %s\r\n' % format_date_time(time.time())
192 )
Guido van Rossume2b70bc2006-08-18 22:13:04 +0000193 if self.server_software and 'Server' not in self.headers:
Thomas Wouters0e3f5912006-08-11 14:57:12 +0000194 self._write('Server: %s\r\n' % self.server_software)
195 else:
196 self._write('Status: %s\r\n' % self.status)
197
198 def write(self, data):
199 """'write()' callable as specified by PEP 333"""
200
Antoine Pitrou38a66ad2009-01-03 18:41:49 +0000201 assert isinstance(data, (str, bytes)), \
202 "write() argument must be a string or bytes"
Thomas Wouters0e3f5912006-08-11 14:57:12 +0000203
204 if not self.status:
205 raise AssertionError("write() before start_response()")
206
207 elif not self.headers_sent:
208 # Before the first output, send the stored headers
209 self.bytes_sent = len(data) # make sure we know content-length
210 self.send_headers()
211 else:
212 self.bytes_sent += len(data)
213
214 # XXX check Content-Length and truncate if too many bytes written?
215 self._write(data)
216 self._flush()
217
218
219 def sendfile(self):
220 """Platform-specific file transmission
221
222 Override this method in subclasses to support platform-specific
223 file transmission. It is only called if the application's
224 return iterable ('self.result') is an instance of
225 'self.wsgi_file_wrapper'.
226
227 This method should return a true value if it was able to actually
228 transmit the wrapped file-like object using a platform-specific
229 approach. It should return a false value if normal iteration
230 should be used instead. An exception can be raised to indicate
231 that transmission was attempted, but failed.
232
233 NOTE: this method should call 'self.send_headers()' if
234 'self.headers_sent' is false and it is going to attempt direct
235 transmission of the file.
236 """
237 return False # No platform-specific transmission by default
238
239
240 def finish_content(self):
241 """Ensure headers and content have both been sent"""
242 if not self.headers_sent:
Antoine Pitrou59bf7382011-01-06 17:18:32 +0000243 # Only zero Content-Length if not set by the application (so
244 # that HEAD requests can be satisfied properly, see #3839)
245 self.headers.setdefault('Content-Length', "0")
Thomas Wouters0e3f5912006-08-11 14:57:12 +0000246 self.send_headers()
247 else:
248 pass # XXX check if content-length was too short?
249
250 def close(self):
251 """Close the iterable (if needed) and reset all instance vars
252
253 Subclasses may want to also drop the client connection.
254 """
255 try:
256 if hasattr(self.result,'close'):
257 self.result.close()
258 finally:
259 self.result = self.headers = self.status = self.environ = None
260 self.bytes_sent = 0; self.headers_sent = False
261
262
263 def send_headers(self):
264 """Transmit headers to the client, via self._write()"""
265 self.cleanup_headers()
266 self.headers_sent = True
267 if not self.origin_server or self.client_is_modern():
268 self.send_preamble()
269 self._write(str(self.headers))
270
271
272 def result_is_file(self):
273 """True if 'self.result' is an instance of 'self.wsgi_file_wrapper'"""
274 wrapper = self.wsgi_file_wrapper
275 return wrapper is not None and isinstance(self.result,wrapper)
276
277
278 def client_is_modern(self):
279 """True if client can accept status and headers"""
280 return self.environ['SERVER_PROTOCOL'].upper() != 'HTTP/0.9'
281
282
283 def log_exception(self,exc_info):
284 """Log the 'exc_info' tuple in the server log
285
286 Subclasses may override to retarget the output or change its format.
287 """
288 try:
289 from traceback import print_exception
290 stderr = self.get_stderr()
291 print_exception(
292 exc_info[0], exc_info[1], exc_info[2],
293 self.traceback_limit, stderr
294 )
295 stderr.flush()
296 finally:
297 exc_info = None
298
299 def handle_error(self):
300 """Log current error, and send error output to client if possible"""
301 self.log_exception(sys.exc_info())
302 if not self.headers_sent:
303 self.result = self.error_output(self.environ, self.start_response)
304 self.finish_response()
305 # XXX else: attempt advanced recovery techniques for HTML or text?
306
307 def error_output(self, environ, start_response):
308 """WSGI mini-app to create error output
309
310 By default, this just uses the 'error_status', 'error_headers',
311 and 'error_body' attributes to generate an output page. It can
312 be overridden in a subclass to dynamically generate diagnostics,
313 choose an appropriate message for the user's preferred language, etc.
314
315 Note, however, that it's not recommended from a security perspective to
316 spit out diagnostics to any old user; ideally, you should have to do
317 something special to enable diagnostic output, which is why we don't
318 include any here!
319 """
320 start_response(self.error_status,self.error_headers[:],sys.exc_info())
321 return [self.error_body]
322
323
324 # Pure abstract methods; *must* be overridden in subclasses
325
326 def _write(self,data):
327 """Override in subclass to buffer data for send to client
328
329 It's okay if this method actually transmits the data; BaseHandler
330 just separates write and flush operations for greater efficiency
331 when the underlying system actually has such a distinction.
332 """
333 raise NotImplementedError
334
335 def _flush(self):
336 """Override in subclass to force sending of recent '_write()' calls
337
338 It's okay if this method is a no-op (i.e., if '_write()' actually
339 sends the data.
340 """
341 raise NotImplementedError
342
343 def get_stdin(self):
344 """Override in subclass to return suitable 'wsgi.input'"""
345 raise NotImplementedError
346
347 def get_stderr(self):
348 """Override in subclass to return suitable 'wsgi.errors'"""
349 raise NotImplementedError
350
351 def add_cgi_vars(self):
352 """Override in subclass to insert CGI variables in 'self.environ'"""
353 raise NotImplementedError
354
355
356
357
358
359
360
361
362
363
364
365class SimpleHandler(BaseHandler):
366 """Handler that's just initialized with streams, environment, etc.
367
368 This handler subclass is intended for synchronous HTTP/1.0 origin servers,
369 and handles sending the entire response output, given the correct inputs.
370
371 Usage::
372
373 handler = SimpleHandler(
374 inp,out,err,env, multithread=False, multiprocess=True
375 )
376 handler.run(app)"""
377
378 def __init__(self,stdin,stdout,stderr,environ,
379 multithread=True, multiprocess=False
380 ):
381 self.stdin = stdin
382 self.stdout = stdout
383 self.stderr = stderr
384 self.base_env = environ
385 self.wsgi_multithread = multithread
386 self.wsgi_multiprocess = multiprocess
387
388 def get_stdin(self):
389 return self.stdin
390
391 def get_stderr(self):
392 return self.stderr
393
394 def add_cgi_vars(self):
395 self.environ.update(self.base_env)
396
397 def _write(self,data):
Antoine Pitrou38a66ad2009-01-03 18:41:49 +0000398 if isinstance(data, str):
399 try:
400 data = data.encode("iso-8859-1")
401 except UnicodeEncodeError:
402 raise ValueError("Unicode data must contain only code points"
403 " representable in ISO-8859-1 encoding")
Thomas Wouters0e3f5912006-08-11 14:57:12 +0000404 self.stdout.write(data)
Thomas Wouters0e3f5912006-08-11 14:57:12 +0000405
406 def _flush(self):
407 self.stdout.flush()
408 self._flush = self.stdout.flush
409
410
411class BaseCGIHandler(SimpleHandler):
412
413 """CGI-like systems using input/output/error streams and environ mapping
414
415 Usage::
416
417 handler = BaseCGIHandler(inp,out,err,env)
418 handler.run(app)
419
420 This handler class is useful for gateway protocols like ReadyExec and
421 FastCGI, that have usable input/output/error streams and an environment
422 mapping. It's also the base class for CGIHandler, which just uses
423 sys.stdin, os.environ, and so on.
424
425 The constructor also takes keyword arguments 'multithread' and
426 'multiprocess' (defaulting to 'True' and 'False' respectively) to control
427 the configuration sent to the application. It sets 'origin_server' to
428 False (to enable CGI-like output), and assumes that 'wsgi.run_once' is
429 False.
430 """
431
432 origin_server = False
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452class CGIHandler(BaseCGIHandler):
453
454 """CGI-based invocation via sys.stdin/stdout/stderr and os.environ
455
456 Usage::
457
458 CGIHandler().run(app)
459
460 The difference between this class and BaseCGIHandler is that it always
461 uses 'wsgi.run_once' of 'True', 'wsgi.multithread' of 'False', and
462 'wsgi.multiprocess' of 'True'. It does not take any initialization
463 parameters, but always uses 'sys.stdin', 'os.environ', and friends.
464
465 If you need to override any of these parameters, use BaseCGIHandler
466 instead.
467 """
468
469 wsgi_run_once = True
Barry Warsaw07129212010-03-01 21:56:18 +0000470 # Do not allow os.environ to leak between requests in Google App Engine
471 # and other multi-run CGI use cases. This is not easily testable.
472 # See http://bugs.python.org/issue7250
473 os_environ = {}
Thomas Wouters0e3f5912006-08-11 14:57:12 +0000474
475 def __init__(self):
476 BaseCGIHandler.__init__(
477 self, sys.stdin, sys.stdout, sys.stderr, dict(os.environ.items()),
478 multithread=False, multiprocess=True
479 )
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496#