blob: 3e112190959a5f8dc610735ec049eacaae21bd2c [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
Thomas Wouters0e3f5912006-08-11 14:57:12 +000023class BaseHandler:
24 """Manage the invocation of a WSGI application"""
25
26 # Configuration parameters; can override per-subclass or per-instance
27 wsgi_version = (1,0)
28 wsgi_multithread = True
29 wsgi_multiprocess = True
30 wsgi_run_once = False
31
32 origin_server = True # We are transmitting direct to client
33 http_version = "1.0" # Version that should be used for response
34 server_software = None # String name of server software, if any
35
36 # os_environ is used to supply configuration from the OS environment:
37 # by default it's a copy of 'os.environ' as of import time, but you can
38 # override this in e.g. your __init__ method.
39 os_environ = dict(os.environ.items())
40
41 # Collaborator classes
42 wsgi_file_wrapper = FileWrapper # set to None to disable
43 headers_class = Headers # must be a Headers-like class
44
45 # Error handling (also per-subclass or per-instance)
46 traceback_limit = None # Print entire traceback to self.get_stderr()
Ezio Melottia3211ee2010-02-16 23:59:54 +000047 error_status = "500 Internal Server Error"
Thomas Wouters0e3f5912006-08-11 14:57:12 +000048 error_headers = [('Content-Type','text/plain')]
Phillip J. Ebye1594222010-11-02 22:28:59 +000049 error_body = b"A server error occurred. Please contact the administrator."
Thomas Wouters0e3f5912006-08-11 14:57:12 +000050
51 # State variables (don't mess with these)
52 status = result = None
53 headers_sent = False
54 headers = None
55 bytes_sent = 0
56
Thomas Wouters0e3f5912006-08-11 14:57:12 +000057 def run(self, application):
58 """Invoke the application"""
59 # Note to self: don't move the close()! Asynchronous servers shouldn't
60 # call close() from finish_response(), so if you close() anywhere but
61 # the double-error branch here, you'll break asynchronous servers by
62 # prematurely closing. Async servers must return from 'run()' without
63 # closing if there might still be output to iterate over.
64 try:
65 self.setup_environ()
66 self.result = application(self.environ, self.start_response)
67 self.finish_response()
68 except:
69 try:
70 self.handle_error()
71 except:
72 # If we get an error handling an error, just give up already!
73 self.close()
74 raise # ...and let the actual server figure it out.
75
76
77 def setup_environ(self):
78 """Set up the environment for one request"""
79
80 env = self.environ = self.os_environ.copy()
81 self.add_cgi_vars()
82
83 env['wsgi.input'] = self.get_stdin()
84 env['wsgi.errors'] = self.get_stderr()
85 env['wsgi.version'] = self.wsgi_version
86 env['wsgi.run_once'] = self.wsgi_run_once
87 env['wsgi.url_scheme'] = self.get_scheme()
88 env['wsgi.multithread'] = self.wsgi_multithread
89 env['wsgi.multiprocess'] = self.wsgi_multiprocess
90
91 if self.wsgi_file_wrapper is not None:
92 env['wsgi.file_wrapper'] = self.wsgi_file_wrapper
93
94 if self.origin_server and self.server_software:
95 env.setdefault('SERVER_SOFTWARE',self.server_software)
96
97
98 def finish_response(self):
99 """Send any iterable data, then close self and the iterable
100
101 Subclasses intended for use in asynchronous servers will
102 want to redefine this method, such that it sets up callbacks
103 in the event loop to iterate over the data, and to call
104 'self.close()' once the response is finished.
105 """
106 if not self.result_is_file() or not self.sendfile():
107 for data in self.result:
108 self.write(data)
109 self.finish_content()
110 self.close()
111
112
113 def get_scheme(self):
114 """Return the URL scheme being used"""
115 return guess_scheme(self.environ)
116
117
118 def set_content_length(self):
119 """Compute Content-Length or switch to chunked encoding if possible"""
120 try:
121 blocks = len(self.result)
122 except (TypeError,AttributeError,NotImplementedError):
123 pass
124 else:
125 if blocks==1:
126 self.headers['Content-Length'] = str(self.bytes_sent)
127 return
128 # XXX Try for chunked encoding if origin server and client is 1.1
129
130
131 def cleanup_headers(self):
132 """Make any necessary header changes or defaults
133
134 Subclasses can extend this to add other defaults.
135 """
Guido van Rossume2b70bc2006-08-18 22:13:04 +0000136 if 'Content-Length' not in self.headers:
Thomas Wouters0e3f5912006-08-11 14:57:12 +0000137 self.set_content_length()
138
139 def start_response(self, status, headers,exc_info=None):
Phillip J. Ebye1594222010-11-02 22:28:59 +0000140 """'start_response()' callable as specified by PEP 3333"""
Thomas Wouters0e3f5912006-08-11 14:57:12 +0000141
142 if exc_info:
143 try:
144 if self.headers_sent:
145 # Re-raise original exception if headers sent
Collin Winter828f04a2007-08-31 00:04:24 +0000146 raise exc_info[0](exc_info[1]).with_traceback(exc_info[2])
Thomas Wouters0e3f5912006-08-11 14:57:12 +0000147 finally:
148 exc_info = None # avoid dangling circular ref
149 elif self.headers is not None:
150 raise AssertionError("Headers already set!")
151
Phillip J. Ebye1594222010-11-02 22:28:59 +0000152 self.status = status
153 self.headers = self.headers_class(headers)
Antoine Pitrou38a66ad2009-01-03 18:41:49 +0000154 status = self._convert_string_type(status, "Status")
Thomas Wouters0e3f5912006-08-11 14:57:12 +0000155 assert len(status)>=4,"Status must be at least 4 characters"
156 assert int(status[:3]),"Status message must begin w/3-digit code"
157 assert status[3]==" ", "Status message must have a space after code"
Antoine Pitrou38a66ad2009-01-03 18:41:49 +0000158
Phillip J. Ebye1594222010-11-02 22:28:59 +0000159 if __debug__:
160 for name, val in headers:
161 name = self._convert_string_type(name, "Header name")
162 val = self._convert_string_type(val, "Header value")
163 assert not is_hop_by_hop(name),"Hop-by-hop headers not allowed"
Antoine Pitrou38a66ad2009-01-03 18:41:49 +0000164
Thomas Wouters0e3f5912006-08-11 14:57:12 +0000165 return self.write
166
Antoine Pitrou38a66ad2009-01-03 18:41:49 +0000167 def _convert_string_type(self, value, title):
168 """Convert/check value type."""
Phillip J. Ebye1594222010-11-02 22:28:59 +0000169 if type(value) is str:
Antoine Pitrou38a66ad2009-01-03 18:41:49 +0000170 return value
Phillip J. Ebye1594222010-11-02 22:28:59 +0000171 raise AssertionError(
172 "{0} must be of type str (got {1})".format(title, repr(value))
173 )
Thomas Wouters0e3f5912006-08-11 14:57:12 +0000174
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():
Phillip J. Ebye1594222010-11-02 22:28:59 +0000179 self._write(('HTTP/%s %s\r\n' % (self.http_version,self.status)).encode('iso-8859-1'))
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(
Phillip J. Ebye1594222010-11-02 22:28:59 +0000182 ('Date: %s\r\n' % format_date_time(time.time())).encode('iso-8859-1')
Thomas Wouters0e3f5912006-08-11 14:57:12 +0000183 )
Guido van Rossume2b70bc2006-08-18 22:13:04 +0000184 if self.server_software and 'Server' not in self.headers:
Phillip J. Ebye1594222010-11-02 22:28:59 +0000185 self._write(('Server: %s\r\n' % self.server_software).encode('iso-8859-1'))
Thomas Wouters0e3f5912006-08-11 14:57:12 +0000186 else:
Phillip J. Ebye1594222010-11-02 22:28:59 +0000187 self._write(('Status: %s\r\n' % self.status).encode('iso-8859-1'))
Thomas Wouters0e3f5912006-08-11 14:57:12 +0000188
189 def write(self, data):
Phillip J. Ebye1594222010-11-02 22:28:59 +0000190 """'write()' callable as specified by PEP 3333"""
Thomas Wouters0e3f5912006-08-11 14:57:12 +0000191
Phillip J. Ebye1594222010-11-02 22:28:59 +0000192 assert type(data) is bytes, \
193 "write() argument must be a bytes instance"
Thomas Wouters0e3f5912006-08-11 14:57:12 +0000194
195 if not self.status:
196 raise AssertionError("write() before start_response()")
197
198 elif not self.headers_sent:
199 # Before the first output, send the stored headers
200 self.bytes_sent = len(data) # make sure we know content-length
201 self.send_headers()
202 else:
203 self.bytes_sent += len(data)
204
205 # XXX check Content-Length and truncate if too many bytes written?
206 self._write(data)
207 self._flush()
208
209
210 def sendfile(self):
211 """Platform-specific file transmission
212
213 Override this method in subclasses to support platform-specific
214 file transmission. It is only called if the application's
215 return iterable ('self.result') is an instance of
216 'self.wsgi_file_wrapper'.
217
218 This method should return a true value if it was able to actually
219 transmit the wrapped file-like object using a platform-specific
220 approach. It should return a false value if normal iteration
221 should be used instead. An exception can be raised to indicate
222 that transmission was attempted, but failed.
223
224 NOTE: this method should call 'self.send_headers()' if
225 'self.headers_sent' is false and it is going to attempt direct
226 transmission of the file.
227 """
228 return False # No platform-specific transmission by default
229
230
231 def finish_content(self):
232 """Ensure headers and content have both been sent"""
233 if not self.headers_sent:
234 self.headers['Content-Length'] = "0"
235 self.send_headers()
236 else:
237 pass # XXX check if content-length was too short?
238
239 def close(self):
240 """Close the iterable (if needed) and reset all instance vars
241
242 Subclasses may want to also drop the client connection.
243 """
244 try:
245 if hasattr(self.result,'close'):
246 self.result.close()
247 finally:
248 self.result = self.headers = self.status = self.environ = None
249 self.bytes_sent = 0; self.headers_sent = False
250
251
252 def send_headers(self):
253 """Transmit headers to the client, via self._write()"""
254 self.cleanup_headers()
255 self.headers_sent = True
256 if not self.origin_server or self.client_is_modern():
257 self.send_preamble()
Phillip J. Ebye1594222010-11-02 22:28:59 +0000258 self._write(bytes(self.headers))
Thomas Wouters0e3f5912006-08-11 14:57:12 +0000259
260
261 def result_is_file(self):
262 """True if 'self.result' is an instance of 'self.wsgi_file_wrapper'"""
263 wrapper = self.wsgi_file_wrapper
264 return wrapper is not None and isinstance(self.result,wrapper)
265
266
267 def client_is_modern(self):
268 """True if client can accept status and headers"""
269 return self.environ['SERVER_PROTOCOL'].upper() != 'HTTP/0.9'
270
271
272 def log_exception(self,exc_info):
273 """Log the 'exc_info' tuple in the server log
274
275 Subclasses may override to retarget the output or change its format.
276 """
277 try:
278 from traceback import print_exception
279 stderr = self.get_stderr()
280 print_exception(
281 exc_info[0], exc_info[1], exc_info[2],
282 self.traceback_limit, stderr
283 )
284 stderr.flush()
285 finally:
286 exc_info = None
287
288 def handle_error(self):
289 """Log current error, and send error output to client if possible"""
290 self.log_exception(sys.exc_info())
291 if not self.headers_sent:
292 self.result = self.error_output(self.environ, self.start_response)
293 self.finish_response()
294 # XXX else: attempt advanced recovery techniques for HTML or text?
295
296 def error_output(self, environ, start_response):
297 """WSGI mini-app to create error output
298
299 By default, this just uses the 'error_status', 'error_headers',
300 and 'error_body' attributes to generate an output page. It can
301 be overridden in a subclass to dynamically generate diagnostics,
302 choose an appropriate message for the user's preferred language, etc.
303
304 Note, however, that it's not recommended from a security perspective to
305 spit out diagnostics to any old user; ideally, you should have to do
306 something special to enable diagnostic output, which is why we don't
307 include any here!
308 """
309 start_response(self.error_status,self.error_headers[:],sys.exc_info())
310 return [self.error_body]
311
312
313 # Pure abstract methods; *must* be overridden in subclasses
314
315 def _write(self,data):
316 """Override in subclass to buffer data for send to client
317
318 It's okay if this method actually transmits the data; BaseHandler
319 just separates write and flush operations for greater efficiency
320 when the underlying system actually has such a distinction.
321 """
322 raise NotImplementedError
323
324 def _flush(self):
325 """Override in subclass to force sending of recent '_write()' calls
326
327 It's okay if this method is a no-op (i.e., if '_write()' actually
328 sends the data.
329 """
330 raise NotImplementedError
331
332 def get_stdin(self):
333 """Override in subclass to return suitable 'wsgi.input'"""
334 raise NotImplementedError
335
336 def get_stderr(self):
337 """Override in subclass to return suitable 'wsgi.errors'"""
338 raise NotImplementedError
339
340 def add_cgi_vars(self):
341 """Override in subclass to insert CGI variables in 'self.environ'"""
342 raise NotImplementedError
343
344
Thomas Wouters0e3f5912006-08-11 14:57:12 +0000345class SimpleHandler(BaseHandler):
346 """Handler that's just initialized with streams, environment, etc.
347
348 This handler subclass is intended for synchronous HTTP/1.0 origin servers,
349 and handles sending the entire response output, given the correct inputs.
350
351 Usage::
352
353 handler = SimpleHandler(
354 inp,out,err,env, multithread=False, multiprocess=True
355 )
356 handler.run(app)"""
357
358 def __init__(self,stdin,stdout,stderr,environ,
359 multithread=True, multiprocess=False
360 ):
361 self.stdin = stdin
362 self.stdout = stdout
363 self.stderr = stderr
364 self.base_env = environ
365 self.wsgi_multithread = multithread
366 self.wsgi_multiprocess = multiprocess
367
368 def get_stdin(self):
369 return self.stdin
370
371 def get_stderr(self):
372 return self.stderr
373
374 def add_cgi_vars(self):
375 self.environ.update(self.base_env)
376
377 def _write(self,data):
378 self.stdout.write(data)
Thomas Wouters0e3f5912006-08-11 14:57:12 +0000379
380 def _flush(self):
381 self.stdout.flush()
382 self._flush = self.stdout.flush
383
384
385class BaseCGIHandler(SimpleHandler):
386
387 """CGI-like systems using input/output/error streams and environ mapping
388
389 Usage::
390
391 handler = BaseCGIHandler(inp,out,err,env)
392 handler.run(app)
393
394 This handler class is useful for gateway protocols like ReadyExec and
395 FastCGI, that have usable input/output/error streams and an environment
396 mapping. It's also the base class for CGIHandler, which just uses
397 sys.stdin, os.environ, and so on.
398
399 The constructor also takes keyword arguments 'multithread' and
400 'multiprocess' (defaulting to 'True' and 'False' respectively) to control
401 the configuration sent to the application. It sets 'origin_server' to
402 False (to enable CGI-like output), and assumes that 'wsgi.run_once' is
403 False.
404 """
405
406 origin_server = False
407
408
Thomas Wouters0e3f5912006-08-11 14:57:12 +0000409class CGIHandler(BaseCGIHandler):
410
411 """CGI-based invocation via sys.stdin/stdout/stderr and os.environ
412
413 Usage::
414
415 CGIHandler().run(app)
416
417 The difference between this class and BaseCGIHandler is that it always
418 uses 'wsgi.run_once' of 'True', 'wsgi.multithread' of 'False', and
419 'wsgi.multiprocess' of 'True'. It does not take any initialization
420 parameters, but always uses 'sys.stdin', 'os.environ', and friends.
421
422 If you need to override any of these parameters, use BaseCGIHandler
423 instead.
424 """
425
426 wsgi_run_once = True
Barry Warsawb1938262010-03-01 21:53:00 +0000427 # Do not allow os.environ to leak between requests in Google App Engine
428 # and other multi-run CGI use cases. This is not easily testable.
429 # See http://bugs.python.org/issue7250
430 os_environ = {}
Thomas Wouters0e3f5912006-08-11 14:57:12 +0000431
432 def __init__(self):
433 BaseCGIHandler.__init__(
434 self, sys.stdin, sys.stdout, sys.stderr, dict(os.environ.items()),
435 multithread=False, multiprocess=True
436 )