blob: d9347e55a6e7f5d7fe3a287c7c7fc4f856ca931e [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
154 raise exc_info[0], exc_info[1], exc_info[2]
155 finally:
156 exc_info = None # avoid dangling circular ref
157 elif self.headers is not None:
158 raise AssertionError("Headers already set!")
159
Collin Winterec8e7162007-08-08 03:59:26 +0000160 assert type(status) is str,"Status must be a string"
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"
164 if __debug__:
165 for name,val in headers:
Collin Winterec8e7162007-08-08 03:59:26 +0000166 assert type(name) is str,"Header names must be strings"
167 assert type(val) is str,"Header values must be strings"
Thomas Wouters0e3f5912006-08-11 14:57:12 +0000168 assert not is_hop_by_hop(name),"Hop-by-hop headers not allowed"
169 self.status = status
170 self.headers = self.headers_class(headers)
171 return self.write
172
173
174 def send_preamble(self):
175 """Transmit version/status/date/server, via self._write()"""
176 if self.origin_server:
177 if self.client_is_modern():
178 self._write('HTTP/%s %s\r\n' % (self.http_version,self.status))
Guido van Rossume2b70bc2006-08-18 22:13:04 +0000179 if 'Date' not in self.headers:
Thomas Wouters0e3f5912006-08-11 14:57:12 +0000180 self._write(
181 'Date: %s\r\n' % format_date_time(time.time())
182 )
Guido van Rossume2b70bc2006-08-18 22:13:04 +0000183 if self.server_software and 'Server' not in self.headers:
Thomas Wouters0e3f5912006-08-11 14:57:12 +0000184 self._write('Server: %s\r\n' % self.server_software)
185 else:
186 self._write('Status: %s\r\n' % self.status)
187
188 def write(self, data):
189 """'write()' callable as specified by PEP 333"""
190
Collin Winterec8e7162007-08-08 03:59:26 +0000191 assert type(data) is str,"write() argument must be string"
Thomas Wouters0e3f5912006-08-11 14:57:12 +0000192
193 if not self.status:
194 raise AssertionError("write() before start_response()")
195
196 elif not self.headers_sent:
197 # Before the first output, send the stored headers
198 self.bytes_sent = len(data) # make sure we know content-length
199 self.send_headers()
200 else:
201 self.bytes_sent += len(data)
202
203 # XXX check Content-Length and truncate if too many bytes written?
204 self._write(data)
205 self._flush()
206
207
208 def sendfile(self):
209 """Platform-specific file transmission
210
211 Override this method in subclasses to support platform-specific
212 file transmission. It is only called if the application's
213 return iterable ('self.result') is an instance of
214 'self.wsgi_file_wrapper'.
215
216 This method should return a true value if it was able to actually
217 transmit the wrapped file-like object using a platform-specific
218 approach. It should return a false value if normal iteration
219 should be used instead. An exception can be raised to indicate
220 that transmission was attempted, but failed.
221
222 NOTE: this method should call 'self.send_headers()' if
223 'self.headers_sent' is false and it is going to attempt direct
224 transmission of the file.
225 """
226 return False # No platform-specific transmission by default
227
228
229 def finish_content(self):
230 """Ensure headers and content have both been sent"""
231 if not self.headers_sent:
232 self.headers['Content-Length'] = "0"
233 self.send_headers()
234 else:
235 pass # XXX check if content-length was too short?
236
237 def close(self):
238 """Close the iterable (if needed) and reset all instance vars
239
240 Subclasses may want to also drop the client connection.
241 """
242 try:
243 if hasattr(self.result,'close'):
244 self.result.close()
245 finally:
246 self.result = self.headers = self.status = self.environ = None
247 self.bytes_sent = 0; self.headers_sent = False
248
249
250 def send_headers(self):
251 """Transmit headers to the client, via self._write()"""
252 self.cleanup_headers()
253 self.headers_sent = True
254 if not self.origin_server or self.client_is_modern():
255 self.send_preamble()
256 self._write(str(self.headers))
257
258
259 def result_is_file(self):
260 """True if 'self.result' is an instance of 'self.wsgi_file_wrapper'"""
261 wrapper = self.wsgi_file_wrapper
262 return wrapper is not None and isinstance(self.result,wrapper)
263
264
265 def client_is_modern(self):
266 """True if client can accept status and headers"""
267 return self.environ['SERVER_PROTOCOL'].upper() != 'HTTP/0.9'
268
269
270 def log_exception(self,exc_info):
271 """Log the 'exc_info' tuple in the server log
272
273 Subclasses may override to retarget the output or change its format.
274 """
275 try:
276 from traceback import print_exception
277 stderr = self.get_stderr()
278 print_exception(
279 exc_info[0], exc_info[1], exc_info[2],
280 self.traceback_limit, stderr
281 )
282 stderr.flush()
283 finally:
284 exc_info = None
285
286 def handle_error(self):
287 """Log current error, and send error output to client if possible"""
288 self.log_exception(sys.exc_info())
289 if not self.headers_sent:
290 self.result = self.error_output(self.environ, self.start_response)
291 self.finish_response()
292 # XXX else: attempt advanced recovery techniques for HTML or text?
293
294 def error_output(self, environ, start_response):
295 """WSGI mini-app to create error output
296
297 By default, this just uses the 'error_status', 'error_headers',
298 and 'error_body' attributes to generate an output page. It can
299 be overridden in a subclass to dynamically generate diagnostics,
300 choose an appropriate message for the user's preferred language, etc.
301
302 Note, however, that it's not recommended from a security perspective to
303 spit out diagnostics to any old user; ideally, you should have to do
304 something special to enable diagnostic output, which is why we don't
305 include any here!
306 """
307 start_response(self.error_status,self.error_headers[:],sys.exc_info())
308 return [self.error_body]
309
310
311 # Pure abstract methods; *must* be overridden in subclasses
312
313 def _write(self,data):
314 """Override in subclass to buffer data for send to client
315
316 It's okay if this method actually transmits the data; BaseHandler
317 just separates write and flush operations for greater efficiency
318 when the underlying system actually has such a distinction.
319 """
320 raise NotImplementedError
321
322 def _flush(self):
323 """Override in subclass to force sending of recent '_write()' calls
324
325 It's okay if this method is a no-op (i.e., if '_write()' actually
326 sends the data.
327 """
328 raise NotImplementedError
329
330 def get_stdin(self):
331 """Override in subclass to return suitable 'wsgi.input'"""
332 raise NotImplementedError
333
334 def get_stderr(self):
335 """Override in subclass to return suitable 'wsgi.errors'"""
336 raise NotImplementedError
337
338 def add_cgi_vars(self):
339 """Override in subclass to insert CGI variables in 'self.environ'"""
340 raise NotImplementedError
341
342
343
344
345
346
347
348
349
350
351
352class SimpleHandler(BaseHandler):
353 """Handler that's just initialized with streams, environment, etc.
354
355 This handler subclass is intended for synchronous HTTP/1.0 origin servers,
356 and handles sending the entire response output, given the correct inputs.
357
358 Usage::
359
360 handler = SimpleHandler(
361 inp,out,err,env, multithread=False, multiprocess=True
362 )
363 handler.run(app)"""
364
365 def __init__(self,stdin,stdout,stderr,environ,
366 multithread=True, multiprocess=False
367 ):
368 self.stdin = stdin
369 self.stdout = stdout
370 self.stderr = stderr
371 self.base_env = environ
372 self.wsgi_multithread = multithread
373 self.wsgi_multiprocess = multiprocess
374
375 def get_stdin(self):
376 return self.stdin
377
378 def get_stderr(self):
379 return self.stderr
380
381 def add_cgi_vars(self):
382 self.environ.update(self.base_env)
383
384 def _write(self,data):
385 self.stdout.write(data)
386 self._write = self.stdout.write
387
388 def _flush(self):
389 self.stdout.flush()
390 self._flush = self.stdout.flush
391
392
393class BaseCGIHandler(SimpleHandler):
394
395 """CGI-like systems using input/output/error streams and environ mapping
396
397 Usage::
398
399 handler = BaseCGIHandler(inp,out,err,env)
400 handler.run(app)
401
402 This handler class is useful for gateway protocols like ReadyExec and
403 FastCGI, that have usable input/output/error streams and an environment
404 mapping. It's also the base class for CGIHandler, which just uses
405 sys.stdin, os.environ, and so on.
406
407 The constructor also takes keyword arguments 'multithread' and
408 'multiprocess' (defaulting to 'True' and 'False' respectively) to control
409 the configuration sent to the application. It sets 'origin_server' to
410 False (to enable CGI-like output), and assumes that 'wsgi.run_once' is
411 False.
412 """
413
414 origin_server = False
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434class CGIHandler(BaseCGIHandler):
435
436 """CGI-based invocation via sys.stdin/stdout/stderr and os.environ
437
438 Usage::
439
440 CGIHandler().run(app)
441
442 The difference between this class and BaseCGIHandler is that it always
443 uses 'wsgi.run_once' of 'True', 'wsgi.multithread' of 'False', and
444 'wsgi.multiprocess' of 'True'. It does not take any initialization
445 parameters, but always uses 'sys.stdin', 'os.environ', and friends.
446
447 If you need to override any of these parameters, use BaseCGIHandler
448 instead.
449 """
450
451 wsgi_run_once = True
452
453 def __init__(self):
454 BaseCGIHandler.__init__(
455 self, sys.stdin, sys.stdout, sys.stderr, dict(os.environ.items()),
456 multithread=False, multiprocess=True
457 )
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474#