blob: e082823d35d8f788e35309fd18e71cb2a73eeab0 [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:
243 self.headers['Content-Length'] = "0"
244 self.send_headers()
245 else:
246 pass # XXX check if content-length was too short?
247
248 def close(self):
249 """Close the iterable (if needed) and reset all instance vars
250
251 Subclasses may want to also drop the client connection.
252 """
253 try:
254 if hasattr(self.result,'close'):
255 self.result.close()
256 finally:
257 self.result = self.headers = self.status = self.environ = None
258 self.bytes_sent = 0; self.headers_sent = False
259
260
261 def send_headers(self):
262 """Transmit headers to the client, via self._write()"""
263 self.cleanup_headers()
264 self.headers_sent = True
265 if not self.origin_server or self.client_is_modern():
266 self.send_preamble()
267 self._write(str(self.headers))
268
269
270 def result_is_file(self):
271 """True if 'self.result' is an instance of 'self.wsgi_file_wrapper'"""
272 wrapper = self.wsgi_file_wrapper
273 return wrapper is not None and isinstance(self.result,wrapper)
274
275
276 def client_is_modern(self):
277 """True if client can accept status and headers"""
278 return self.environ['SERVER_PROTOCOL'].upper() != 'HTTP/0.9'
279
280
281 def log_exception(self,exc_info):
282 """Log the 'exc_info' tuple in the server log
283
284 Subclasses may override to retarget the output or change its format.
285 """
286 try:
287 from traceback import print_exception
288 stderr = self.get_stderr()
289 print_exception(
290 exc_info[0], exc_info[1], exc_info[2],
291 self.traceback_limit, stderr
292 )
293 stderr.flush()
294 finally:
295 exc_info = None
296
297 def handle_error(self):
298 """Log current error, and send error output to client if possible"""
299 self.log_exception(sys.exc_info())
300 if not self.headers_sent:
301 self.result = self.error_output(self.environ, self.start_response)
302 self.finish_response()
303 # XXX else: attempt advanced recovery techniques for HTML or text?
304
305 def error_output(self, environ, start_response):
306 """WSGI mini-app to create error output
307
308 By default, this just uses the 'error_status', 'error_headers',
309 and 'error_body' attributes to generate an output page. It can
310 be overridden in a subclass to dynamically generate diagnostics,
311 choose an appropriate message for the user's preferred language, etc.
312
313 Note, however, that it's not recommended from a security perspective to
314 spit out diagnostics to any old user; ideally, you should have to do
315 something special to enable diagnostic output, which is why we don't
316 include any here!
317 """
318 start_response(self.error_status,self.error_headers[:],sys.exc_info())
319 return [self.error_body]
320
321
322 # Pure abstract methods; *must* be overridden in subclasses
323
324 def _write(self,data):
325 """Override in subclass to buffer data for send to client
326
327 It's okay if this method actually transmits the data; BaseHandler
328 just separates write and flush operations for greater efficiency
329 when the underlying system actually has such a distinction.
330 """
331 raise NotImplementedError
332
333 def _flush(self):
334 """Override in subclass to force sending of recent '_write()' calls
335
336 It's okay if this method is a no-op (i.e., if '_write()' actually
337 sends the data.
338 """
339 raise NotImplementedError
340
341 def get_stdin(self):
342 """Override in subclass to return suitable 'wsgi.input'"""
343 raise NotImplementedError
344
345 def get_stderr(self):
346 """Override in subclass to return suitable 'wsgi.errors'"""
347 raise NotImplementedError
348
349 def add_cgi_vars(self):
350 """Override in subclass to insert CGI variables in 'self.environ'"""
351 raise NotImplementedError
352
353
354
355
356
357
358
359
360
361
362
363class SimpleHandler(BaseHandler):
364 """Handler that's just initialized with streams, environment, etc.
365
366 This handler subclass is intended for synchronous HTTP/1.0 origin servers,
367 and handles sending the entire response output, given the correct inputs.
368
369 Usage::
370
371 handler = SimpleHandler(
372 inp,out,err,env, multithread=False, multiprocess=True
373 )
374 handler.run(app)"""
375
376 def __init__(self,stdin,stdout,stderr,environ,
377 multithread=True, multiprocess=False
378 ):
379 self.stdin = stdin
380 self.stdout = stdout
381 self.stderr = stderr
382 self.base_env = environ
383 self.wsgi_multithread = multithread
384 self.wsgi_multiprocess = multiprocess
385
386 def get_stdin(self):
387 return self.stdin
388
389 def get_stderr(self):
390 return self.stderr
391
392 def add_cgi_vars(self):
393 self.environ.update(self.base_env)
394
395 def _write(self,data):
Antoine Pitrou38a66ad2009-01-03 18:41:49 +0000396 if isinstance(data, str):
397 try:
398 data = data.encode("iso-8859-1")
399 except UnicodeEncodeError:
400 raise ValueError("Unicode data must contain only code points"
401 " representable in ISO-8859-1 encoding")
Thomas Wouters0e3f5912006-08-11 14:57:12 +0000402 self.stdout.write(data)
Thomas Wouters0e3f5912006-08-11 14:57:12 +0000403
404 def _flush(self):
405 self.stdout.flush()
406 self._flush = self.stdout.flush
407
408
409class BaseCGIHandler(SimpleHandler):
410
411 """CGI-like systems using input/output/error streams and environ mapping
412
413 Usage::
414
415 handler = BaseCGIHandler(inp,out,err,env)
416 handler.run(app)
417
418 This handler class is useful for gateway protocols like ReadyExec and
419 FastCGI, that have usable input/output/error streams and an environment
420 mapping. It's also the base class for CGIHandler, which just uses
421 sys.stdin, os.environ, and so on.
422
423 The constructor also takes keyword arguments 'multithread' and
424 'multiprocess' (defaulting to 'True' and 'False' respectively) to control
425 the configuration sent to the application. It sets 'origin_server' to
426 False (to enable CGI-like output), and assumes that 'wsgi.run_once' is
427 False.
428 """
429
430 origin_server = False
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450class CGIHandler(BaseCGIHandler):
451
452 """CGI-based invocation via sys.stdin/stdout/stderr and os.environ
453
454 Usage::
455
456 CGIHandler().run(app)
457
458 The difference between this class and BaseCGIHandler is that it always
459 uses 'wsgi.run_once' of 'True', 'wsgi.multithread' of 'False', and
460 'wsgi.multiprocess' of 'True'. It does not take any initialization
461 parameters, but always uses 'sys.stdin', 'os.environ', and friends.
462
463 If you need to override any of these parameters, use BaseCGIHandler
464 instead.
465 """
466
467 wsgi_run_once = True
468
469 def __init__(self):
470 BaseCGIHandler.__init__(
471 self, sys.stdin, sys.stdout, sys.stderr, dict(os.environ.items()),
472 multithread=False, multiprocess=True
473 )
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490#