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
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
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
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
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))