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