• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1"""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
20# Uncomment for 2.2 compatibility.
21#try:
22#    True
23#    False
24#except NameError:
25#    True = not None
26#    False = not True
27
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
42class 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()
66    error_status = "500 Internal Server Error"
67    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
76    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        """
155        if 'Content-Length' not in self.headers:
156            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))
190                if 'Date' not in self.headers:
191                    self._write(
192                        'Date: %s\r\n' % format_date_time(time.time())
193                    )
194                if self.server_software and 'Server' not in self.headers:
195                    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:
205            raise AssertionError("write() before start_response()")
206
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            # Only zero Content-Length if not set by the application (so
244            # that HEAD requests can be satisfied properly, see #3839)
245            self.headers.setdefault('Content-Length', "0")
246            self.send_headers()
247        else:
248            pass # XXX check if content-length was too short?
249
250    def close(self):
251        """Close the iterable (if needed) and reset all instance vars
252
253        Subclasses may want to also drop the client connection.
254        """
255        try:
256            if hasattr(self.result,'close'):
257                self.result.close()
258        finally:
259            self.result = self.headers = self.status = self.environ = None
260            self.bytes_sent = 0; self.headers_sent = False
261
262
263    def send_headers(self):
264        """Transmit headers to the client, via self._write()"""
265        self.cleanup_headers()
266        self.headers_sent = True
267        if not self.origin_server or self.client_is_modern():
268            self.send_preamble()
269            self._write(str(self.headers))
270
271
272    def result_is_file(self):
273        """True if 'self.result' is an instance of 'self.wsgi_file_wrapper'"""
274        wrapper = self.wsgi_file_wrapper
275        return wrapper is not None and isinstance(self.result,wrapper)
276
277
278    def client_is_modern(self):
279        """True if client can accept status and headers"""
280        return self.environ['SERVER_PROTOCOL'].upper() != 'HTTP/0.9'
281
282
283    def log_exception(self,exc_info):
284        """Log the 'exc_info' tuple in the server log
285
286        Subclasses may override to retarget the output or change its format.
287        """
288        try:
289            from traceback import print_exception
290            stderr = self.get_stderr()
291            print_exception(
292                exc_info[0], exc_info[1], exc_info[2],
293                self.traceback_limit, stderr
294            )
295            stderr.flush()
296        finally:
297            exc_info = None
298
299    def handle_error(self):
300        """Log current error, and send error output to client if possible"""
301        self.log_exception(sys.exc_info())
302        if not self.headers_sent:
303            self.result = self.error_output(self.environ, self.start_response)
304            self.finish_response()
305        # XXX else: attempt advanced recovery techniques for HTML or text?
306
307    def error_output(self, environ, start_response):
308        """WSGI mini-app to create error output
309
310        By default, this just uses the 'error_status', 'error_headers',
311        and 'error_body' attributes to generate an output page.  It can
312        be overridden in a subclass to dynamically generate diagnostics,
313        choose an appropriate message for the user's preferred language, etc.
314
315        Note, however, that it's not recommended from a security perspective to
316        spit out diagnostics to any old user; ideally, you should have to do
317        something special to enable diagnostic output, which is why we don't
318        include any here!
319        """
320        start_response(self.error_status,self.error_headers[:],sys.exc_info())
321        return [self.error_body]
322
323
324    # Pure abstract methods; *must* be overridden in subclasses
325
326    def _write(self,data):
327        """Override in subclass to buffer data for send to client
328
329        It's okay if this method actually transmits the data; BaseHandler
330        just separates write and flush operations for greater efficiency
331        when the underlying system actually has such a distinction.
332        """
333        raise NotImplementedError
334
335    def _flush(self):
336        """Override in subclass to force sending of recent '_write()' calls
337
338        It's okay if this method is a no-op (i.e., if '_write()' actually
339        sends the data.
340        """
341        raise NotImplementedError
342
343    def get_stdin(self):
344        """Override in subclass to return suitable 'wsgi.input'"""
345        raise NotImplementedError
346
347    def get_stderr(self):
348        """Override in subclass to return suitable 'wsgi.errors'"""
349        raise NotImplementedError
350
351    def add_cgi_vars(self):
352        """Override in subclass to insert CGI variables in 'self.environ'"""
353        raise NotImplementedError
354
355
356class SimpleHandler(BaseHandler):
357    """Handler that's just initialized with streams, environment, etc.
358
359    This handler subclass is intended for synchronous HTTP/1.0 origin servers,
360    and handles sending the entire response output, given the correct inputs.
361
362    Usage::
363
364        handler = SimpleHandler(
365            inp,out,err,env, multithread=False, multiprocess=True
366        )
367        handler.run(app)"""
368
369    def __init__(self,stdin,stdout,stderr,environ,
370        multithread=True, multiprocess=False
371    ):
372        self.stdin = stdin
373        self.stdout = stdout
374        self.stderr = stderr
375        self.base_env = environ
376        self.wsgi_multithread = multithread
377        self.wsgi_multiprocess = multiprocess
378
379    def get_stdin(self):
380        return self.stdin
381
382    def get_stderr(self):
383        return self.stderr
384
385    def add_cgi_vars(self):
386        self.environ.update(self.base_env)
387
388    def _write(self,data):
389        self.stdout.write(data)
390        self._write = self.stdout.write
391
392    def _flush(self):
393        self.stdout.flush()
394        self._flush = self.stdout.flush
395
396
397class BaseCGIHandler(SimpleHandler):
398
399    """CGI-like systems using input/output/error streams and environ mapping
400
401    Usage::
402
403        handler = BaseCGIHandler(inp,out,err,env)
404        handler.run(app)
405
406    This handler class is useful for gateway protocols like ReadyExec and
407    FastCGI, that have usable input/output/error streams and an environment
408    mapping.  It's also the base class for CGIHandler, which just uses
409    sys.stdin, os.environ, and so on.
410
411    The constructor also takes keyword arguments 'multithread' and
412    'multiprocess' (defaulting to 'True' and 'False' respectively) to control
413    the configuration sent to the application.  It sets 'origin_server' to
414    False (to enable CGI-like output), and assumes that 'wsgi.run_once' is
415    False.
416    """
417
418    origin_server = False
419
420
421class CGIHandler(BaseCGIHandler):
422
423    """CGI-based invocation via sys.stdin/stdout/stderr and os.environ
424
425    Usage::
426
427        CGIHandler().run(app)
428
429    The difference between this class and BaseCGIHandler is that it always
430    uses 'wsgi.run_once' of 'True', 'wsgi.multithread' of 'False', and
431    'wsgi.multiprocess' of 'True'.  It does not take any initialization
432    parameters, but always uses 'sys.stdin', 'os.environ', and friends.
433
434    If you need to override any of these parameters, use BaseCGIHandler
435    instead.
436    """
437
438    wsgi_run_once = True
439    # Do not allow os.environ to leak between requests in Google App Engine
440    # and other multi-run CGI use cases.  This is not easily testable.
441    # See http://bugs.python.org/issue7250
442    os_environ = {}
443
444    def __init__(self):
445        BaseCGIHandler.__init__(
446            self, sys.stdin, sys.stdout, sys.stderr, dict(os.environ.items()),
447            multithread=False, multiprocess=True
448        )
449