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