Package wsgitools :: Module middlewares
[hide private]
[frames] | no frames]

Source Code for Module wsgitools.middlewares

  1  __all__ = [] 
  2   
  3  import time 
  4  import sys 
  5  import cgitb 
  6  import binascii 
  7  from wsgitools.filters import CloseableList, CloseableIterator 
  8  try: 
  9      import io 
 10  except ImportError: 
 11      try: 
 12          import cStringIO as io 
 13      except ImportError: 
 14          import StringIO as io 
 15  try: 
 16      next 
 17  except NameError: 
18 - def next(it):
19 return it.next()
20 21 __all__.append("SubdirMiddleware")
22 -class SubdirMiddleware:
23 """Middleware choosing wsgi applications based on a dict."""
24 - def __init__(self, default, mapping={}):
25 """ 26 @type default: wsgi app 27 @type mapping: {str: wsgi app} 28 """ 29 self.default = default 30 self.mapping = mapping
31 - def __call__(self, environ, start_response):
32 """wsgi interface 33 @type environ: {str: str} 34 @rtype: gen([str]) 35 """ 36 assert isinstance(environ, dict) 37 app = None 38 script = environ["PATH_INFO"] 39 path_info = "" 40 while '/' in script: 41 if script in self.mapping: 42 app = self.mapping[script] 43 break 44 script, tail = script.rsplit('/', 1) 45 path_info = "/%s%s" % (tail, path_info) 46 if app is None: 47 app = self.mapping.get(script, None) 48 if app is None: 49 app = self.default 50 environ["SCRIPT_NAME"] += script 51 environ["PATH_INFO"] = path_info 52 return app(environ, start_response)
53 54 __all__.append("NoWriteCallableMiddleware")
55 -class NoWriteCallableMiddleware:
56 """This middleware wraps a wsgi application that needs the return value of 57 C{start_response} function to a wsgi application that doesn't need one by 58 writing the data to a C{StringIO} and then making it be the first result 59 element."""
60 - def __init__(self, app):
61 """Wraps wsgi application app.""" 62 self.app = app
63 - def __call__(self, environ, start_response):
64 """wsgi interface 65 @type environ: {str, str} 66 @rtype: gen([str]) 67 """ 68 assert isinstance(environ, dict) 69 todo = [] 70 def modified_start_response(status, headers, exc_info=None): 71 assert isinstance(status, str) 72 assert isinstance(headers, list) 73 if exc_info is not None: 74 todo.append(None) 75 return start_response(status, headers, exc_info) 76 else: 77 sio = io.StringIO() 78 todo.append((status, headers, sio)) 79 return sio.write
80 81 ret = self.app(environ, modified_start_response) 82 assert hasattr(ret, "__iter__") 83 assert len(todo) == 1 84 85 if todo[0] is None: 86 return ret 87 88 status, headers, data = todo[0] 89 data = data.getvalue() 90 91 if not data: 92 start_response(status, headers) 93 return ret 94 95 if isinstance(ret, list): 96 ret.insert(0, data) 97 start_response(status, headers) 98 return ret 99 100 ret = iter(ret) 101 stopped = False 102 try: 103 first = next(ret) 104 except StopIteration: 105 stopped = True 106 107 start_response(status, headers) 108 109 if stopped: 110 return CloseableList(getattr(ret, "close", None), (data,)) 111 112 return CloseableIterator(getattr(ret, "close", None), 113 (data, first), ret)
114 115 __all__.append("ContentLengthMiddleware")
116 -class ContentLengthMiddleware:
117 """Guesses the content length header if possible. 118 @note: The application used must not use the C{write} callable returned by 119 C{start_response}."""
120 - def __init__(self, app, maxstore=0):
121 """Wraps wsgi application app. It can also store the first result bytes 122 to possibly return a list of strings which will make guessing the size 123 of iterators possible. At most maxstore bytes will be accumulated. 124 Please note that a value larger than 0 will violate the wsgi standard. 125 The magical value C{()} will make it always gather all data. 126 @type maxstore: int or () 127 """ 128 self.app = app 129 self.maxstore = maxstore
130 - def __call__(self, environ, start_response):
131 """wsgi interface""" 132 assert isinstance(environ, dict) 133 todo = [] 134 def modified_start_response(status, headers, exc_info=None): 135 assert isinstance(status, str) 136 assert isinstance(headers, list) 137 if (exc_info is not None or 138 [v for h, v in headers if h.lower() == "content-length"]): 139 todo[:] = (None,) 140 return start_response(status, headers, exc_info) 141 else: 142 todo[:] = ((status, headers),) 143 def raise_not_imp(*args): 144 raise NotImplementedError
145 return raise_not_imp
146 147 ret = self.app(environ, modified_start_response) 148 assert hasattr(ret, "__iter__") 149 150 if todo and todo[0] is None: # nothing to do 151 #print "content-length: nothing" 152 return ret 153 154 if isinstance(ret, list): 155 #print "content-length: simple" 156 status, headers = todo[0] 157 length = sum(map(len, ret)) 158 headers.append(("Content-length", str(length))) 159 start_response(status, headers) 160 return ret 161 162 ret = iter(ret) 163 stopped = False 164 data = CloseableList(getattr(ret, "close", None)) 165 length = 0 166 try: 167 data.append(next(ret)) # fills todo 168 length += len(data[-1]) 169 except StopIteration: 170 stopped = True 171 172 status, headers = todo[0] 173 174 while (not stopped) and length < self.maxstore: 175 try: 176 data.append(next(ret)) 177 length += len(data[-1]) 178 except StopIteration: 179 stopped = True 180 181 if stopped: 182 #print "content-length: gathered" 183 headers.append(("Content-length", str(length))) 184 start_response(status, headers) 185 return data 186 187 #print "content-length: passthrough" 188 start_response(status, headers) 189 190 return CloseableIterator(getattr(ret, "close", None), data, ret) 191
192 -def storable(environ):
193 if environ["REQUEST_METHOD"] != "GET": 194 return False 195 return True
196
197 -def cacheable(environ):
198 if environ.get("HTTP_CACHE_CONTROL", "") == "max-age=0": 199 return False 200 return True
201 202 __all__.append("CachingMiddleware")
203 -class CachingMiddleware:
204 """Caches reponses to requests based on C{SCRIPT_NAME}, C{PATH_INFO} and 205 C{QUERY_STRING}."""
206 - def __init__(self, app, maxage=60, storable=storable, cacheable=cacheable):
207 """ 208 @param app: is a wsgi application to be cached. 209 @type maxage: int 210 @param maxage: is the number of seconds a reponse may be cached. 211 @param storable: is a predicate that determines whether the response 212 may be cached at all based on the C{environ} dict. 213 @param cacheable: is a predicate that determines whether this request 214 invalidates the cache.""" 215 self.app = app 216 self.maxage = maxage 217 self.storable = storable 218 self.cacheable = cacheable 219 self.cache = {}
220 - def __call__(self, environ, start_response):
221 """wsgi interface 222 @type environ: {str: str} 223 """ 224 assert isinstance(environ, dict) 225 if not self.storable(environ): 226 return self.app(environ, start_response) 227 path = environ.get("SCRIPT_NAME", "/") 228 path += environ.get("PATH_INFO", '') 229 path += "?" + environ.get("QUERY_STRING", "") 230 if self.cacheable(environ) and path in self.cache: 231 if self.cache[path][0] + self.maxage >= time.time(): 232 start_response(self.cache[path][1], self.cache[path][2]) 233 return self.cache[path][3] 234 else: 235 del self.cache[path] 236 cache_object = [time.time(), "", [], []] 237 def modified_start_respesponse(status, headers, exc_info=None): 238 assert isinstance(status, str) 239 assert isinstance(headers, list) 240 if exc_info is not None: 241 return self.app(status, headers, exc_info) 242 cache_object[1] = status 243 cache_object[2] = headers 244 write = start_response(status, headers) 245 def modified_write(data): 246 cache_object[3].append(data) 247 write(data)
248 return modified_write
249 250 ret = self.app(environ, modified_start_respesponse) 251 assert hasattr(ret, "__iter__") 252 253 if isinstance(ret, list): 254 cache_object[3].extend(ret) 255 self.cache[path] = cache_object 256 return ret 257 def pass_through(): 258 for data in ret: 259 cache_object[3].append(data) 260 yield data 261 self.cache[path] = cache_object 262 return CloseableIterator(getattr(ret, "close", None), pass_through()) 263 264 __all__.append("DictAuthChecker")
265 -class DictAuthChecker:
266 """Verifies usernames and passwords by looking them up in a dict."""
267 - def __init__(self, users):
268 """ 269 @type users: {str: str} 270 @param users: is a dict mapping usernames to password.""" 271 self.users = users
272 - def __call__(self, username, password):
273 """check_function interface taking username and password and resulting 274 in a bool. 275 @type username: str 276 @type password: str 277 @rtype: bool 278 """ 279 return username in self.users and self.users[username] == password
280 281 __all__.append("BasicAuthMiddleware")
282 -class BasicAuthMiddleware:
283 """Middleware implementing HTTP Basic Auth."""
284 - def __init__(self, app, check_function, realm='www', app401=None):
285 """ 286 @param app: is a WSGI application. 287 @param check_function: is a function taking three arguments username, 288 password and environment returning a bool indicating whether the 289 request may is allowed. The older interface of taking only the 290 first two arguments is still supported via catching a 291 C{TypeError}. 292 @type realm: str 293 @param app401: is an optional WSGI application to be used for error 294 messages 295 """ 296 self.app = app 297 self.check_function = check_function 298 self.realm = realm 299 self.app401 = app401
300
301 - def __call__(self, environ, start_response):
302 """wsgi interface 303 @type environ: {str: str} 304 """ 305 assert isinstance(environ, dict) 306 auth = environ.get("HTTP_AUTHORIZATION") 307 if not auth or ' ' not in auth: 308 return self.authorization_required(environ, start_response) 309 auth_type, enc_auth_info = auth.split(None, 1) 310 try: 311 auth_info = enc_auth_info.decode("base64") 312 except binascii.Error: 313 return self.authorization_required(environ, start_response) 314 if auth_type.lower() != "basic" or ':' not in auth_info: 315 return self.authorization_required(environ, start_response) 316 username, password = auth_info.split(':', 1) 317 try: 318 result = self.check_function(username, password, environ) 319 except TypeError: # catch old interface 320 result = self.check_function(username, password) 321 if result: 322 environ["REMOTE_USER"] = username 323 return self.app(environ, start_response) 324 return self.authorization_required(environ, start_response)
325
326 - def authorization_required(self, environ, start_response):
327 """wsgi application for indicating authorization is required. 328 @type environ: {str: str} 329 """ 330 if self.app401 is None: 331 status = "401 Authorization required" 332 html = "<html><head><title>Authorization required</title></head>" \ 333 "<body><h1>Authorization required</h1></body></html>\n" 334 headers = [('Content-type', 'text/html'), 335 ('WWW-Authenticate', 'Basic realm="%s"' % self.realm), 336 ("Content-length", str(len(html)))] 337 start_response(status, headers) 338 if environ["REQUEST_METHOD"].upper() == "HEAD": 339 return [] 340 return [html] 341 return self.app401(environ, start_response)
342 343 __all__.append("TracebackMiddleware")
344 -class TracebackMiddleware:
345 """In case the application throws an exception this middleware will show an 346 html-formatted traceback using C{cgitb}."""
347 - def __init__(self, app):
348 """app is the wsgi application to proxy.""" 349 self.app = app
350 - def __call__(self, environ, start_response):
351 """wsgi interface 352 @type environ: {str: str} 353 """ 354 try: 355 assert isinstance(environ, dict) 356 ret = self.app(environ, start_response) 357 assert hasattr(ret, "__iter__") 358 359 if isinstance(ret, list): 360 return ret 361 # Take the first element of the iterator and possibly catch an 362 # exception there. 363 ret = iter(ret) 364 try: 365 first = next(ret) 366 except StopIteration: 367 return CloseableList(getattr(ret, "close", None), []) 368 return CloseableIterator(getattr(ret, "close", None), [first], ret) 369 except: 370 exc_info = sys.exc_info() 371 data = cgitb.html(exc_info) 372 start_response("200 OK", [("Content-type", "text/html"), 373 ("Content-length", str(len(data)))]) 374 if environ["REQUEST_METHOD"].upper() == "HEAD": 375 return [] 376 return [data]
377