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