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:
14 return exc_info[0](exc_info[1]).with_traceback(exc_info[2])
15 else:
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")
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")
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."""
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")
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
209 if environ["REQUEST_METHOD"] != "GET":
210 return False
211 return True
212
214 if environ.get("HTTP_CACHE_CONTROL", "") == "max-age=0":
215 return False
216 return True
217
218 __all__.append("CachingMiddleware")
220 """Caches reponses to requests based on C{SCRIPT_NAME}, C{PATH_INFO} and
221 C{QUERY_STRING}."""
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
239 if now is None:
240 now = time.time()
241 self.cache[key] = obj
242 self.lastcached.append((key, now))
243
245 if now is None:
246 now = time.time()
247 old = now - self.maxage
248 while self.lastcached and maxclean > 0:
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")
315 """Verifies usernames and passwords by looking them up in a dict."""
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")
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
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:
370 result = self.check_function(username, password)
371 if result:
372 return dict(user=username)
373 raise AuthenticationRequired("credentials not valid")
374
376 return ("WWW-Authenticate", 'Basic realm="%s"' % self.realm)
377
383
384 __all__.append("TracebackMiddleware")
386 """In case the application throws an exception this middleware will show an
387 html-formatted traceback using C{cgitb}."""
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
403
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