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