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

Source Code for Module wsgitools.middlewares

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