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