blob: a5f7109c266e5008376560544780532334c4cd57 [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
Phillip J. Eby5cf565d2006-06-09 16:40:18 +000042class 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()
Ezio Melotti6f903032010-02-16 23:58:49 +000066 error_status = "500 Internal Server Error"
Phillip J. Eby5cf565d2006-06-09 16:40:18 +000067 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
Phillip J. Eby5cf565d2006-06-09 16:40:18 +000076 def run(self, application):
77 """Invoke the application"""
78 # Note to self: don't move the close()! Asynchronous servers shouldn't
79 # call close() from finish_response(), so if you close() anywhere but
80 # the double-error branch here, you'll break asynchronous servers by
81 # prematurely closing. Async servers must return from 'run()' without
82 # closing if there might still be output to iterate over.
83 try:
84 self.setup_environ()
85 self.result = application(self.environ, self.start_response)
86 self.finish_response()
87 except:
88 try:
89 self.handle_error()
90 except:
91 # If we get an error handling an error, just give up already!
92 self.close()
93 raise # ...and let the actual server figure it out.
94
95
96 def setup_environ(self):
97 """Set up the environment for one request"""
98
99 env = self.environ = self.os_environ.copy()
100 self.add_cgi_vars()
101
102 env['wsgi.input'] = self.get_stdin()
103 env['wsgi.errors'] = self.get_stderr()
104 env['wsgi.version'] = self.wsgi_version
105 env['wsgi.run_once'] = self.wsgi_run_once
106 env['wsgi.url_scheme'] = self.get_scheme()
107 env['wsgi.multithread'] = self.wsgi_multithread
108 env['wsgi.multiprocess'] = self.wsgi_multiprocess
109
110 if self.wsgi_file_wrapper is not None:
111 env['wsgi.file_wrapper'] = self.wsgi_file_wrapper
112
113 if self.origin_server and self.server_software:
114 env.setdefault('SERVER_SOFTWARE',self.server_software)
115
116
117 def finish_response(self):
118 """Send any iterable data, then close self and the iterable
119
120 Subclasses intended for use in asynchronous servers will
121 want to redefine this method, such that it sets up callbacks
122 in the event loop to iterate over the data, and to call
123 'self.close()' once the response is finished.
124 """
125 if not self.result_is_file() or not self.sendfile():
126 for data in self.result:
127 self.write(data)
128 self.finish_content()
129 self.close()
130
131
132 def get_scheme(self):
133 """Return the URL scheme being used"""
134 return guess_scheme(self.environ)
135
136
137 def set_content_length(self):
138 """Compute Content-Length or switch to chunked encoding if possible"""
139 try:
140 blocks = len(self.result)
141 except (TypeError,AttributeError,NotImplementedError):
142 pass
143 else:
144 if blocks==1:
145 self.headers['Content-Length'] = str(self.bytes_sent)
146 return
147 # XXX Try for chunked encoding if origin server and client is 1.1
148
149
150 def cleanup_headers(self):
151 """Make any necessary header changes or defaults
152
153 Subclasses can extend this to add other defaults.
154 """
Benjamin Peterson6e3dbbd2009-10-09 22:15:50 +0000155 if 'Content-Length' not in self.headers:
Phillip J. Eby5cf565d2006-06-09 16:40:18 +0000156 self.set_content_length()
157
158 def start_response(self, status, headers,exc_info=None):
159 """'start_response()' callable as specified by PEP 333"""
160
161 if exc_info:
162 try:
163 if self.headers_sent:
164 # Re-raise original exception if headers sent
165 raise exc_info[0], exc_info[1], exc_info[2]
166 finally:
167 exc_info = None # avoid dangling circular ref
168 elif self.headers is not None:
169 raise AssertionError("Headers already set!")
170
171 assert type(status) is StringType,"Status must be a string"
172 assert len(status)>=4,"Status must be at least 4 characters"
173 assert int(status[:3]),"Status message must begin w/3-digit code"
174 assert status[3]==" ", "Status message must have a space after code"
175 if __debug__:
176 for name,val in headers:
177 assert type(name) is StringType,"Header names must be strings"
178 assert type(val) is StringType,"Header values must be strings"
179 assert not is_hop_by_hop(name),"Hop-by-hop headers not allowed"
180 self.status = status
181 self.headers = self.headers_class(headers)
182 return self.write
183
184
185 def send_preamble(self):
186 """Transmit version/status/date/server, via self._write()"""
187 if self.origin_server:
188 if self.client_is_modern():
189 self._write('HTTP/%s %s\r\n' % (self.http_version,self.status))
Benjamin Peterson6e3dbbd2009-10-09 22:15:50 +0000190 if 'Date' not in self.headers:
Phillip J. Eby5cf565d2006-06-09 16:40:18 +0000191 self._write(
192 'Date: %s\r\n' % format_date_time(time.time())
193 )
Benjamin Peterson6e3dbbd2009-10-09 22:15:50 +0000194 if self.server_software and 'Server' not in self.headers:
Phillip J. Eby5cf565d2006-06-09 16:40:18 +0000195 self._write('Server: %s\r\n' % self.server_software)
196 else:
197 self._write('Status: %s\r\n' % self.status)
198
199 def write(self, data):
200 """'write()' callable as specified by PEP 333"""
201
202 assert type(data) is StringType,"write() argument must be string"
203
204 if not self.status:
Tim Peters982c30b2006-06-09 17:47:00 +0000205 raise AssertionError("write() before start_response()")
Phillip J. Eby5cf565d2006-06-09 16:40:18 +0000206
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
Phillip J. Eby5cf565d2006-06-09 16:40:18 +0000354class SimpleHandler(BaseHandler):
355 """Handler that's just initialized with streams, environment, etc.
356
357 This handler subclass is intended for synchronous HTTP/1.0 origin servers,
358 and handles sending the entire response output, given the correct inputs.
359
360 Usage::
361
362 handler = SimpleHandler(
363 inp,out,err,env, multithread=False, multiprocess=True
364 )
365 handler.run(app)"""
366
367 def __init__(self,stdin,stdout,stderr,environ,
368 multithread=True, multiprocess=False
369 ):
370 self.stdin = stdin
371 self.stdout = stdout
372 self.stderr = stderr
373 self.base_env = environ
374 self.wsgi_multithread = multithread
375 self.wsgi_multiprocess = multiprocess
376
377 def get_stdin(self):
378 return self.stdin
379
380 def get_stderr(self):
381 return self.stderr
382
383 def add_cgi_vars(self):
384 self.environ.update(self.base_env)
385
386 def _write(self,data):
387 self.stdout.write(data)
388 self._write = self.stdout.write
389
390 def _flush(self):
391 self.stdout.flush()
392 self._flush = self.stdout.flush
393
394
395class BaseCGIHandler(SimpleHandler):
396
397 """CGI-like systems using input/output/error streams and environ mapping
398
399 Usage::
400
401 handler = BaseCGIHandler(inp,out,err,env)
402 handler.run(app)
403
404 This handler class is useful for gateway protocols like ReadyExec and
405 FastCGI, that have usable input/output/error streams and an environment
406 mapping. It's also the base class for CGIHandler, which just uses
407 sys.stdin, os.environ, and so on.
408
409 The constructor also takes keyword arguments 'multithread' and
410 'multiprocess' (defaulting to 'True' and 'False' respectively) to control
411 the configuration sent to the application. It sets 'origin_server' to
412 False (to enable CGI-like output), and assumes that 'wsgi.run_once' is
413 False.
414 """
415
416 origin_server = False
417
418
Phillip J. Eby5cf565d2006-06-09 16:40:18 +0000419class CGIHandler(BaseCGIHandler):
420
421 """CGI-based invocation via sys.stdin/stdout/stderr and os.environ
422
423 Usage::
424
425 CGIHandler().run(app)
426
427 The difference between this class and BaseCGIHandler is that it always
428 uses 'wsgi.run_once' of 'True', 'wsgi.multithread' of 'False', and
429 'wsgi.multiprocess' of 'True'. It does not take any initialization
430 parameters, but always uses 'sys.stdin', 'os.environ', and friends.
431
432 If you need to override any of these parameters, use BaseCGIHandler
433 instead.
434 """
435
436 wsgi_run_once = True
Barry Warsaw08a8fb32010-03-01 21:46:51 +0000437 # Do not allow os.environ to leak between requests in Google App Engine
438 # and other multi-run CGI use cases. This is not easily testable.
439 # See http://bugs.python.org/issue7250
440 os_environ = {}
Phillip J. Eby5cf565d2006-06-09 16:40:18 +0000441
442 def __init__(self):
443 BaseCGIHandler.__init__(
444 self, sys.stdin, sys.stdout, sys.stderr, dict(os.environ.items()),
445 multithread=False, multiprocess=True
446 )