0001import simplejson
0002import cPickle as pickle
0003import urllib
0004from wsgiproxy import protocol_version
0005from wsgiproxy.secretloader import get_secret
0006from paste import httpexceptions
0007
0008class WSGIProxyMiddleware(object):
0009
0010    """
0011    Fixes up the environment given special headers set by WSGIProxy,
0012    or by configuration.
0013
0014    Accepts the configuration:
0015
0016    ``secret_file``:
0017    
0018        A location where a secret is kept, used to sign the request
0019        when its coming from ``wsgiproxy.app``
0020
0021    ``trust_ips``:
0022
0023        Instead of ``secret_file`` you can give a list of IPs that are
0024        trusted.  Trusted hosts can send pickle headers.
0025
0026    ``prefix``:
0027
0028        This is used for explicitly configuring the value of
0029        SCRIPT_NAME.  A request to ``/foo`` with ``prefix='/bar'``
0030        will result in a SCRIPT_NAME of ``'/bar'`` and a PATH_INFO of
0031        ``'/foo'``.
0032
0033    ``pop_prefix``:
0034
0035        This is a prefix that is popped off the actual request path,
0036        and put into SCRIPT_NAME.  A request to ``/bar/foo`` where
0037        ``pop_prefix='/bar'`` will result in a SCRIPT_NAME of
0038        ``'/bar'`` and a PATH_INFO of ``'/foo'``.
0039
0040    ``scheme``:
0041
0042        Force the scheme; e.g., to ``'https'``
0043
0044    ``host``:
0045
0046        Force the host (including port!).  You must give something
0047        like ``foo.com:80``.  This will replace the ``HTTP_HOST``
0048        value, as well as ``SERVER_NAME`` and ``SERVER_PORT``.
0049
0050    ``domain``:
0051
0052        Force the domain (not including port).  If you give
0053        ``foo.com`` it will rewrite ``HTTP_HOST`` to be
0054        ``foo.com:{port}``, with whatever port was used for the actual
0055        request.  Usually ``host`` will be more useful.  Also
0056        ``SERVER_NAME`` will be set.
0057
0058    ``port``:
0059
0060        Force the port (not including domain).  If you give ``80`` it
0061        will set ``SERVER_PORT`` and the port portion of
0062        ``HTTP_HOST``.
0063    """
0064
0065    def __init__(self, application,
0066                 secret_file=None,
0067                 trust_ips=None,
0068                 prefix=None,
0069                 pop_prefix=None,
0070                 scheme=None,
0071                 host=None,
0072                 domain=None,
0073                 port=None):
0074        self.application = application
0075        if trust_ips is not None:
0076            if isinstance(trust_ips, basestring):
0077                trust_ips = [trust_ips]
0078            self.trust_ips = trust_ips
0079        else:
0080            self.trust_ips = None
0081        if prefix is not None:
0082            self.prefix = prefix.rstrip('/')
0083        else:
0084            self.prefix = None
0085        if self.pop_prefix is not None:
0086            assert self.prefix is None, (
0087                "You cannot give both prefix and pop_prefix values")
0088            self.pop_prefix = pop_prefix.rstrip('/')
0089        else:
0090            self.pop_prefix = None
0091        if self.scheme is not None:
0092            self.scheme = scheme.lower()
0093        else:
0094            self.scheme = None
0095        self.host = host
0096        if self.host is not None:
0097            assert ':' in self.host, (
0098                "The host argument must contain a port (use domain otherwise)")
0099            assert port is None, (
0100                "You cannot give both a port and host argument")
0101            assert domain is None, (
0102                "You cannot give both a domain and host argument")
0103        self.domain = domain
0104        if self.port is not None:
0105            self.port = str(port)
0106        else:
0107            self.port = None
0108
0109    def __call__(self, environ, start_response):
0110        self._fixup_environ(environ)
0111        try:
0112            self._fixup_configured(environ)
0113        except httpexceptions.HTTPException, exc:
0114            return exc(environ, start_response)
0115        return self.application(environ, start_response)
0116
0117    def _fixup_environ(self, environ):
0118        # @@: Obviously better errors here:
0119        if 'HTTP_X_WSGIPROXY_VERSION' in environ:
0120            version = environ.pop('HTTP_X_WSGIPROXY_VERSION')
0121            assert version == protocol_version
0122        secure = False
0123        if self.secret_file is not None:
0124            secret = get_secret(self.secret_file)
0125            # @@: Should catch error:
0126            check_request(environ, secret)
0127            secure = True
0128        if self.trust_ips:
0129            ip = environ.get('REMOTE_ADDR')
0130            if ip in trust_ips:
0131                # @@: Should allow ranges and whatnot:
0132                secure = True
0133        if 'HTTP_X_FORWARDED_SERVER' in environ:
0134            environ['HTTP_HOST'] = environ.pop('HTTP_X_FORWARDED_SERVER')
0135        if 'HTTP_X_FORWARDED_SCHEME' in environ:
0136            environ['wsgi.url_scheme'] = environ.pop('HTTP_X_FORWARDED_SCHEME')
0137        if 'HTTP_X_FORWARDED_FOR' in environ:
0138            environ['REMOTE_ADDR'] = environ.pop('HTTP_X_FORWARDED_FOR')
0139        script_name = environ.get('SCRIPT_NAME', '')
0140        path_info = environ.get('PATH_INFO', '')
0141        if 'HTTP_X_TRAVERSAL_PATH' in environ:
0142            traversal_path = environ['HTTP_X_TRAVERSAL_PATH'].rstrip('/')
0143            if traversal_path == path_info:
0144                path_info = ''
0145            elif not path_info.startswith(traversal_path+'/'):
0146                exc = httpexceptions.HTTPBadRequest(
0147                    "The header X-Traversal-Path gives the value %r but "
0148                    "the path is %r (it should start with "
0149                    "X-Traversal-Path)" % (traversal_path, path_info))
0150                return exc(environ, start_response)
0151            else:
0152                path_info = path_info[len(traversal_path):]
0153        if 'HTTP_X_SCRIPT_NAME' in environ:
0154            add_script_name = environ.pop('HTTP_X_SCRIPT_NAME').rstrip('/')
0155            if not add_script_name.startswith('/'):
0156                exc = httpexceptions.HTTPBadRequest(
0157                    "The header X-Script-Name gives %r which does not "
0158                    "start with /" % add_script_name)
0159                return exc(environ, start_response)
0160            script_name = add_script_name + script_name
0161        environ['SCRIPT_NAME'] = script_name
0162        environ['PATH_INFO'] = path_info
0163        for header, key in [
0164            ('HTTP_HOST', 'HTTP_HOST'),
0165            ('SCRIPT_NAME', 'SCRIPT_NAME'),
0166            ('PATH_INFO', 'PATH_INFO'),
0167            ('QUERY_STRING', 'QUERY_STRING'),
0168            ('WSGI_URL_SCHEME', 'wsgi.url_scheme')]:
0169            header = 'HTTP_X_WSGIPROXY_%s' % header
0170            if header in environ:
0171                environ[key] = environ.pop(header)
0172        for prefix, decoder, is_secure in [
0173            ('STR', self.str_decode, True),
0174            ('UNICODE', self.unicode_decode, True),
0175            ('JSON', self.json_decode, True),
0176            ('PICKLE', self.pickle_decode, False)]:
0177            expect = 'HTTP_X_WSGIPROXY_%s' % prefix
0178            for key in environ:
0179                if key.startswith(expect):
0180                    if not is_secure and not secure:
0181                        # Better error again!
0182                        assert 0
0183                    key_name, value = environ[key].split(None, 1)
0184                    key_name = urllib.unquote(key_name)
0185                    value = decoder(value)
0186                    environ[key_name] = value
0187
0188    def _fixup_configured(self, environ):
0189        path_info = environ['PATH_INFO']
0190        script_name = environ['SCRIPT_NAME']
0191        if self.prefix is not None:
0192            environ['SCRIPT_NAME'] = self.prefix
0193        elif self.pop_prefix is not None:
0194            if self.pop_prefix == path_info:
0195                path_info = ''
0196                script_name = script_name + self.pop_prefix
0197            elif path_info.startswith(self.pop_prefix + '/'):
0198                path_info = path_info[len(self.pop_prefix):]
0199                script_name = script_name + self.pop_prefix
0200            else:
0201                exc = httpexception.HTTPBadRequest(
0202                    "It was expected that all requests would start with "
0203                    "the path %r, but I got a request with %r"
0204                    % (self.pop_prefix, path_info))
0205                raise exc
0206        if self.scheme is not None:
0207            environ['wsgi.url_scheme'] = self.scheme
0208        if self.host is not None:
0209            domain, port = self.host.split(':', 1)
0210            environ['HTTP_HOST'] = self.host
0211            environ['SERVER_NAME'] = domain
0212            environ['SERVER_PORT'] = port
0213        if self.port is not None:
0214            environ['SERVER_PORT'] = self.port
0215            if self.domain is None:
0216                host = environ['HTTP_HOST'].split(':', 1) + ':' + self.port
0217                environ['HTTP_HOST'] = host
0218        if self.domain is not None:
0219            host = self.domain + ':' + environ['SERVER_PORT']
0220            environ['HTTP_HOST'] = host
0221            environ['SERVER_NAME'] = self.domain
0222
0223    def str_decode(self, value):
0224        if value.startswith('b64'):
0225            return value[3:].decode('base64')
0226        else:
0227            return value
0228
0229    def unicode_decode(self, value):
0230        return self.str_decode(value).decode('utf8')
0231
0232    def json_decode(self, value):
0233        return simplejson.loads(self.str_decode(value))
0234
0235    def pickle_decode(self, value):
0236        return pickle.loads(self.str_decode(value))