From fd38036b9f1693f8f368851d40928bc5922ce606 Mon Sep 17 00:00:00 2001 From: Helmut Grohne Date: Thu, 28 Jun 2012 16:38:03 +0200 Subject: remove workarounds for missing next() and hashlib --- wsgitools/middlewares.py | 5 ----- 1 file changed, 5 deletions(-) (limited to 'wsgitools/middlewares.py') diff --git a/wsgitools/middlewares.py b/wsgitools/middlewares.py index 804d474..dbf2020 100644 --- a/wsgitools/middlewares.py +++ b/wsgitools/middlewares.py @@ -11,11 +11,6 @@ try: import cStringIO as io except ImportError: import StringIO as io -try: - next -except NameError: - def next(iterator): - return iterator.next() if sys.version_info[0] >= 3: def exc_info_for_raise(exc_info): -- cgit v1.2.3 From 7e2e9173b2afcc2a8dca9e6047d0b82ad70c9dff Mon Sep 17 00:00:00 2001 From: Helmut Grohne Date: Thu, 28 Jun 2012 22:38:28 +0200 Subject: first part of bytes conversion Convert the request body data from str to bytes. This replaces all StringIOs with BytesIOs (removing backwards one more backwards compatibility). Also all character sequences involved in request bodies get a b"" prefix. The StaticContent application takes bytes instead of str (no difference for py2x). The GzipWSGIFilter needs a fixed as a truncate of a BytesIO does not rewind the stream position. --- test.py | 43 +++++++++++++++++++----------------------- wsgitools/applications.py | 6 +++--- wsgitools/authentication.py | 4 ++-- wsgitools/filters.py | 21 ++++++++------------- wsgitools/middlewares.py | 11 +++-------- wsgitools/scgi/asynchronous.py | 9 ++------- 6 files changed, 37 insertions(+), 57 deletions(-) (limited to 'wsgitools/middlewares.py') diff --git a/test.py b/test.py index f46a512..f2e8910 100755 --- a/test.py +++ b/test.py @@ -3,12 +3,7 @@ import unittest import doctest import wsgiref.validate -# Cannot use io module as it is broken in 2.6. -# Writing a str to a io.StringIO results in an exception. -try: - import cStringIO as io -except ImportError: - import StringIO as io +import io from hashlib import md5 import sys @@ -27,7 +22,7 @@ class Request: QUERY_STRING="") self.environ.update({ "wsgi.version": (1, 0), - "wsgi.input": io.StringIO(), + "wsgi.input": io.BytesIO(), "wsgi.errors": sys.stderr, "wsgi.url_scheme": "http", "wsgi.multithread": False, @@ -122,14 +117,14 @@ class Result: self.testcase.fail("header %s not found" % name) def get_data(self): - return "".join(self.writtendata) + "".join(self.returneddata) + return b"".join(self.writtendata) + b"".join(self.returneddata) from wsgitools import applications class StaticContentTest(unittest.TestCase): def setUp(self): self.app = applications.StaticContent( - "200 Found", [("Content-Type", "text/plain")], "nothing") + "200 Found", [("Content-Type", "text/plain")], b"nothing") self.req = Request(self) def testGet(self): @@ -146,7 +141,7 @@ class StaticContentTest(unittest.TestCase): class StaticFileTest(unittest.TestCase): def setUp(self): - self.app = applications.StaticFile(io.StringIO("success"), "200 Found", + self.app = applications.StaticFile(io.BytesIO(b"success"), "200 Found", [("Content-Type", "text/plain")]) self.req = Request(self) @@ -167,7 +162,7 @@ from wsgitools import digest class AuthDigestMiddlewareTest(unittest.TestCase): def setUp(self): self.staticapp = applications.StaticContent( - "200 Found", [("Content-Type", "text/plain")], "success") + "200 Found", [("Content-Type", "text/plain")], b"success") token_gen = digest.AuthTokenGenerator("foo", lambda _: "baz") self.app = digest.AuthDigestMiddleware( wsgiref.validate.validator(self.staticapp), token_gen) @@ -232,28 +227,28 @@ from wsgitools import middlewares def writing_application(environ, start_response): write = start_response("404 Not found", [("Content-Type", "text/plain")]) write = start_response("200 Ok", [("Content-Type", "text/plain")]) - write("first") - yield "" - yield "second" + write(b"first") + yield b"" + yield b"second" def write_only_application(environ, start_response): write = start_response("200 Ok", [("Content-Type", "text/plain")]) - write("first") - write("second") - yield "" + write(b"first") + write(b"second") + yield b"" class NoWriteCallableMiddlewareTest(unittest.TestCase): def testWrite(self): app = middlewares.NoWriteCallableMiddleware(writing_application) res = Request(self)(app) self.assertEqual(res.writtendata, []) - self.assertEqual("".join(res.returneddata), "firstsecond") + self.assertEqual(b"".join(res.returneddata), b"firstsecond") def testWriteOnly(self): app = middlewares.NoWriteCallableMiddleware(write_only_application) res = Request(self)(app) self.assertEqual(res.writtendata, []) - self.assertEqual("".join(res.returneddata), "firstsecond") + self.assertEqual(b"".join(res.returneddata), b"firstsecond") class StupidIO: """file-like without tell method, so StaticFile is not able to @@ -273,7 +268,7 @@ class StupidIO: class ContentLengthMiddlewareTest(unittest.TestCase): def setUp(self): - self.staticapp = applications.StaticFile(StupidIO("success"), + self.staticapp = applications.StaticFile(StupidIO(b"success"), "200 Found", [("Content-Type", "text/plain")]) self.app = middlewares.ContentLengthMiddleware(self.staticapp, maxstore=10) @@ -296,7 +291,7 @@ class ContentLengthMiddlewareTest(unittest.TestCase): class BasicAuthMiddlewareTest(unittest.TestCase): def setUp(self): self.staticapp = applications.StaticContent( - "200 Found", [("Content-Type", "text/plain")], "success") + "200 Found", [("Content-Type", "text/plain")], b"success") checkpw = middlewares.DictAuthChecker({"bar": "baz"}) self.app = middlewares.BasicAuthMiddleware( wsgiref.validate.validator(self.staticapp), checkpw) @@ -340,13 +335,13 @@ import gzip class GzipWSGIFilterTest(unittest.TestCase): def testSimple(self): app = applications.StaticContent("200 Found", - [("Content-Type", "text/plain")], "nothing") + [("Content-Type", "text/plain")], b"nothing") app = filters.WSGIFilterMiddleware(app, filters.GzipWSGIFilter) req = Request(self) req.environ["HTTP_ACCEPT_ENCODING"] = "gzip" res = req(app) - data = gzip.GzipFile(fileobj=io.StringIO(res.get_data())).read() - self.assertEqual(data, "nothing") + data = gzip.GzipFile(fileobj=io.BytesIO(res.get_data())).read() + self.assertEqual(data, b"nothing") def alltests(case): return unittest.TestLoader().loadTestsFromTestCase(case) diff --git a/wsgitools/applications.py b/wsgitools/applications.py index 8a02fe8..cdaf0ae 100644 --- a/wsgitools/applications.py +++ b/wsgitools/applications.py @@ -21,7 +21,7 @@ class StaticContent: @type headers: list @param headers: is a list of C{(header, value)} pairs being delivered as HTTP headers - @type content: basestring + @type content: bytes @param content: contains the data to be delivered to the client. It is either a string or some kind of iterable yielding strings. @type anymethod: boolean @@ -30,12 +30,12 @@ class StaticContent: """ assert isinstance(status, str) assert isinstance(headers, list) - assert isinstance(content, basestring) or hasattr(content, "__iter__") + assert isinstance(content, bytes) or hasattr(content, "__iter__") self.status = status self.headers = headers self.anymethod = anymethod length = -1 - if isinstance(content, basestring): + if isinstance(content, bytes): self.content = [content] length = len(content) else: diff --git a/wsgitools/authentication.py b/wsgitools/authentication.py index 963dc00..c076d7f 100644 --- a/wsgitools/authentication.py +++ b/wsgitools/authentication.py @@ -97,8 +97,8 @@ class AuthenticationMiddleware: @param exception: reason for the authentication failure """ status = "401 Authorization required" - html = "401 Authorization required" \ - "

401 Authorization required

" + html = b"401 Authorization required" \ + b"

401 Authorization required

" headers = [("Content-Type", "text/html"), self.www_authenticate(exception), ("Content-Length", str(len(html)))] diff --git a/wsgitools/filters.py b/wsgitools/filters.py index 4305c9d..6f90903 100644 --- a/wsgitools/filters.py +++ b/wsgitools/filters.py @@ -10,13 +10,7 @@ __all__ = [] import sys import time import gzip -# Cannot use io module as it is broken in 2.6. -# Writing a str to a io.StringIO results in an exception. -try: - import cStringIO as io -except ImportError: - import StringIO as io - +import io __all__.append("CloseableIterator") class CloseableIterator: @@ -397,7 +391,7 @@ class GzipWSGIFilter(BaseWSGIFilter): acceptenc = map(str.strip, acceptenc) if "gzip" in acceptenc: self.compress = True - self.sio = io.StringIO() + self.sio = io.BytesIO() self.gzip = gzip.GzipFile(fileobj=self.sio, mode="w") return environ def filter_header(self, headername, headervalue): @@ -431,6 +425,7 @@ class GzipWSGIFilter(BaseWSGIFilter): self.gzip.flush() data = self.sio.getvalue() self.sio.truncate(0) + self.sio.seek(0) return data def append_data(self): """BaseWSGIFilter interface @@ -446,7 +441,7 @@ class ReusableWSGIInputFilter(BaseWSGIFilter): """Make C{environ["wsgi.input"]} readable multiple times. Although this is not required by the standard it is sometimes desirable to read C{wsgi.input} multiple times. This filter will therefore replace that variable with a - C{StringIO} instance which provides a C{seek} method. + C{BytesIO} instance which provides a C{seek} method. """ @classmethod def creator(cls, maxrequestsize): @@ -457,14 +452,14 @@ class ReusableWSGIInputFilter(BaseWSGIFilter): adapter to eat this data.) @type maxrequestsize: int @param maxrequestsize: is the maximum number of bytes to store in the - C{StringIO} + C{BytesIO} """ return lambda:cls(maxrequestsize) def __init__(self, maxrequestsize=65536): """ReusableWSGIInputFilters constructor. @type maxrequestsize: int @param maxrequestsize: is the maximum number of bytes to store in the - C{StringIO}, see L{creator} + C{BytesIO}, see L{creator} """ BaseWSGIFilter.__init__(self) self.maxrequestsize = maxrequestsize @@ -474,12 +469,12 @@ class ReusableWSGIInputFilter(BaseWSGIFilter): @type environ: {str: str} """ - if isinstance(environ["wsgi.input"], io.StringIO): + if isinstance(environ["wsgi.input"], io.BytesIO): return environ # nothing to be done # XXX: is this really a good idea? use with care environ["wsgitools.oldinput"] = environ["wsgi.input"] - data = io.StringIO(environ["wsgi.input"].read(self.maxrequestsize)) + data = io.BytesIO(environ["wsgi.input"].read(self.maxrequestsize)) environ["wsgi.input"] = data return environ diff --git a/wsgitools/middlewares.py b/wsgitools/middlewares.py index dbf2020..e6ede9d 100644 --- a/wsgitools/middlewares.py +++ b/wsgitools/middlewares.py @@ -5,12 +5,7 @@ import sys import cgitb import binascii import collections -# Cannot use io module as it is broken in 2.6. -# Writing a str to a io.StringIO results in an exception. -try: - import cStringIO as io -except ImportError: - import StringIO as io +import io if sys.version_info[0] >= 3: def exc_info_for_raise(exc_info): @@ -60,7 +55,7 @@ __all__.append("NoWriteCallableMiddleware") class NoWriteCallableMiddleware: """This middleware wraps a wsgi application that needs the return value of C{start_response} function to a wsgi application that doesn't need one by - writing the data to a C{StringIO} and then making it be the first result + writing the data to a C{BytesIO} and then making it be the first result element.""" def __init__(self, app): """Wraps wsgi application app.""" @@ -72,7 +67,7 @@ class NoWriteCallableMiddleware: """ assert isinstance(environ, dict) todo = [None] - sio = io.StringIO() + sio = io.BytesIO() gotiterdata = False def write_calleable(data): assert not gotiterdata diff --git a/wsgitools/scgi/asynchronous.py b/wsgitools/scgi/asynchronous.py index 1dee283..7eb1a30 100644 --- a/wsgitools/scgi/asynchronous.py +++ b/wsgitools/scgi/asynchronous.py @@ -1,14 +1,9 @@ __all__ = [] import asyncore +import io import socket import sys -# Cannot use io module as it is broken in 2.6. -# Writing a str to a io.StringIO results in an exception. -try: - import cStringIO as io -except ImportError: - import StringIO as io import errno from wsgitools.scgi import _convert_environ, FileWrapper @@ -48,7 +43,7 @@ class SCGIConnection(asyncore.dispatcher): self.wsgiiterator = None # wsgi application iterator self.outheaders = () # headers to be sent # () -> unset, (..,..) -> set, True -> sent - self.body = io.StringIO() # request body + self.body = io.BytesIO() # request body def _try_send_headers(self): if self.outheaders != True: -- cgit v1.2.3 From 472144ac68188056eb41c9cb198df04b454a1da2 Mon Sep 17 00:00:00 2001 From: Helmut Grohne Date: Fri, 29 Jun 2012 08:47:51 +0200 Subject: fix hashlib, base64 and other bytes issues * hashlib.md5 wants bytes now. * string.decode("base64") is now base64.b64decode and works on bytes * binascii.unhexlify is now base64.b16decode and also works on bytes * str.isalnum accepts umlauts, use bytes.isalnum instead --- test.py | 19 ++++++++++------ wsgitools/digest.py | 59 +++++++++++++++++++++++++++++------------------- wsgitools/middlewares.py | 14 +++++++----- 3 files changed, 56 insertions(+), 36 deletions(-) (limited to 'wsgitools/middlewares.py') diff --git a/test.py b/test.py index f2e8910..1183d63 100755 --- a/test.py +++ b/test.py @@ -1,5 +1,6 @@ #!/usr/bin/env python +import base64 import unittest import doctest import wsgiref.validate @@ -7,6 +8,8 @@ import io from hashlib import md5 import sys +from wsgitools.internal import bytes2str, str2bytes + class Request: def __init__(self, case): """ @@ -193,9 +196,9 @@ class AuthDigestMiddlewareTest(unittest.TestCase): res.getheader("WWW-Authenticate").split()))) nonce = nonce.split('"')[1] req = self.req.copy() - token = md5("bar:foo:%s" % password).hexdigest() - other = md5("GET:").hexdigest() - resp = md5("%s:%s:%s" % (token, nonce, other)).hexdigest() + token = md5(str2bytes("bar:foo:%s" % password)).hexdigest() + other = md5(str2bytes("GET:")).hexdigest() + resp = md5(str2bytes("%s:%s:%s" % (token, nonce, other))).hexdigest() req.setheader('http-authorization', 'Digest algorithm=md5,nonce="%s",' \ 'uri=,username=bar,response="%s"' % (nonce, resp)) res = req(self.app) @@ -213,9 +216,10 @@ class AuthDigestMiddlewareTest(unittest.TestCase): res.getheader("WWW-Authenticate").split()))) nonce = nonce.split('"')[1] req = self.req.copy() - token = md5("bar:foo:baz").hexdigest() - other = md5("GET:").hexdigest() - resp = md5("%s:%s:1:qux:auth:%s" % (token, nonce, other)).hexdigest() + token = md5(str2bytes("bar:foo:baz")).hexdigest() + other = md5(str2bytes("GET:")).hexdigest() + resp = "%s:%s:1:qux:auth:%s" % (token, nonce, other) + resp = md5(str2bytes(resp)).hexdigest() req.setheader('http-authorization', 'Digest algorithm=md5,nonce="%s",' \ 'uri=,username=bar,response="%s",qop=auth,nc=1,' \ 'cnonce=qux' % (nonce, resp)) @@ -318,7 +322,8 @@ class BasicAuthMiddlewareTest(unittest.TestCase): def doauth(self, password="baz", status=200): req = self.req.copy() - token = ("bar:%s" % password).encode("base64").strip() + token = "bar:%s" % password + token = bytes2str(base64.b64encode(str2bytes(token))) req.setheader('http-authorization', 'Basic %s' % token) res = req(self.app) res.status(status) diff --git a/wsgitools/digest.py b/wsgitools/digest.py index 4b5f8fb..532b371 100644 --- a/wsgitools/digest.py +++ b/wsgitools/digest.py @@ -14,32 +14,39 @@ database using C{DBAPI2NonceStore}. __all__ = [] import random -try: - from hashlib import md5 -except ImportError: - from md5 import md5 -import binascii import base64 +import hashlib import time import os +from wsgitools.internal import bytes2str, str2bytes from wsgitools.authentication import AuthenticationRequired, \ ProtocolViolation, AuthenticationMiddleware sysrand = random.SystemRandom() -def gen_rand_str(bytes=33): +def md5hex(data): + """ + @type data: str + @rtype: str + """ + return hashlib.md5(str2bytes(data)).hexdigest() + +def gen_rand_str(bytesentropy=33): """ Generates a string of random base64 characters. - @param bytes: is the number of random 8bit values to be used + @param bytesentropy: is the number of random 8bit values to be used + @rtype: str >>> gen_rand_str() != gen_rand_str() True """ - randnum = sysrand.getrandbits(bytes*8) - randstr = ("%%0%dX" % (2*bytes)) % randnum - randstr = binascii.unhexlify(randstr) - randstr = base64.encodestring(randstr).strip() + randnum = sysrand.getrandbits(bytesentropy*8) + randstr = ("%%0%dX" % (2*bytesentropy)) % randnum + randbytes = str2bytes(randstr) + randbytes = base64.b16decode(randbytes) + randbytes = base64.b64encode(randbytes) + randstr = bytes2str(randbytes) return randstr def parse_digest_response(data): @@ -120,6 +127,8 @@ def format_digest(mapping): assert isinstance(mapping, dict) result = [] for key, (value, needsquoting) in mapping.items(): + assert isinstance(key, str) + assert isinstance(value, str) if needsquoting: value = '"%s"' % value.replace('\\', '\\\\').replace('"', '\\"') else: @@ -172,8 +181,8 @@ class AbstractTokenGenerator: """ assert isinstance(username, str) assert isinstance(password, str) - token = md5("%s:%s:%s" % (username, self.realm, password)).hexdigest() - return token == self(username) + token = "%s:%s:%s" % (username, self.realm, password) + return md5hex(token) == self(username) __all__.append("AuthTokenGenerator") class AuthTokenGenerator(AbstractTokenGenerator): @@ -199,7 +208,7 @@ class AuthTokenGenerator(AbstractTokenGenerator): if password is None: return None a1 = "%s:%s:%s" % (username, self.realm, password) - return md5(a1).hexdigest() + return md5hex(a1) __all__.append("HtdigestTokenGenerator") class HtdigestTokenGenerator(AbstractTokenGenerator): @@ -367,7 +376,7 @@ class StatelessNonceStore(NonceStoreBase): token = "%s:%s:%s" % (nonce_time, nonce_value, self.server_secret) if ident is not None: token = "%s:%s" % (token, ident) - token = md5(token).hexdigest() + token = md5hex(token) return "%s:%s:%s" % (nonce_time, nonce_value, token) def checknonce(self, nonce, count=1, ident=None): @@ -387,7 +396,7 @@ class StatelessNonceStore(NonceStoreBase): token = "%s:%s:%s" % (nonce_time, nonce_value, self.server_secret) if ident is not None: token = "%s:%s" % (token, ident) - token = md5(token).hexdigest() + token = md5hex(token) if token != nonce_hash: return False @@ -449,7 +458,7 @@ class MemoryNonceStore(NonceStoreBase): token = "%s:%s:%s" % (nonce_time, nonce_value, self.server_secret) if ident is not None: token = "%s:%s" % (token, ident) - token = md5(token).hexdigest() + token = md5hex(token) return "%s:%s:%s" % (nonce_time, nonce_value, token) def checknonce(self, nonce, count=1, ident=None): @@ -468,7 +477,7 @@ class MemoryNonceStore(NonceStoreBase): token = "%s:%s:%s" % (nonce_time, nonce_value, self.server_secret) if ident is not None: token = "%s:%s" % (token, ident) - token = md5(token).hexdigest() + token = md5hex(token) if token != nonce_hash: return False @@ -595,7 +604,7 @@ class DBAPI2NonceStore(NonceStoreBase): token = "%s:%s" % (dbkey, self.server_secret) if ident is not None: token = "%s:%s" % (token, ident) - token = md5(token).hexdigest() + token = md5hex(token) return "%s:%s:%s" % (nonce_time, nonce_value, token) def checknonce(self, nonce, count=1, ident=None): @@ -604,19 +613,22 @@ class DBAPI2NonceStore(NonceStoreBase): count on returning True. @type nonce: str @type count: int + @type ident: str or None @rtype: bool """ try: nonce_time, nonce_value, nonce_hash = nonce.split(':') except ValueError: return False - if not nonce_time.isalnum() or not nonce_value.replace("+", ""). \ - replace("/", "").replace("=", "").isalnum(): + # use bytes.isalnum to avoid locale specific interpretation + if not str2bytes(nonce_time).isalnum() or \ + not str2bytes(nonce_value.replace("+", "").replace("/", "") \ + .replace("=", "")).isalnum(): return False token = "%s:%s:%s" % (nonce_time, nonce_value, self.server_secret) if ident is not None: token = "%s:%s" % (token, ident) - token = md5(token).hexdigest() + token = md5hex(token) if token != nonce_hash: return False @@ -681,7 +693,7 @@ class AuthDigestMiddleware(AuthenticationMiddleware): by a REMOTE_USER key before being passed to the wrapped application.""" authorization_method = "digest" - algorithms = {"md5": lambda data: md5(data).hexdigest()} + algorithms = {"md5": md5hex} def __init__(self, app, gentoken, maxage=300, maxuses=5, store=None): """ @param app: is the wsgi application to be served with authentication. @@ -708,6 +720,7 @@ class AuthDigestMiddleware(AuthenticationMiddleware): self.noncestore = store def authenticate(self, auth, environ): + assert isinstance(auth, str) try: credentials = parse_digest_response(auth) except ValueError: diff --git a/wsgitools/middlewares.py b/wsgitools/middlewares.py index e6ede9d..725deb1 100644 --- a/wsgitools/middlewares.py +++ b/wsgitools/middlewares.py @@ -1,12 +1,14 @@ __all__ = [] +import base64 import time import sys import cgitb -import binascii import collections import io +from wsgitools.internal import bytes2str, str2bytes + if sys.version_info[0] >= 3: def exc_info_for_raise(exc_info): return exc_info[0](exc_info[1]).with_traceback(exc_info[2]) @@ -347,14 +349,14 @@ class BasicAuthMiddleware(AuthenticationMiddleware): self.app401 = app401 def authenticate(self, auth, environ): - """ - @type environ: {str: object} - """ + assert isinstance(auth, str) assert isinstance(environ, dict) + auth = str2bytes(auth) try: - auth_info = auth.decode("base64") - except binascii.Error: + auth_info = base64.b64decode(auth) + except TypeError: raise ProtocolViolation("failed to base64 decode auth_info") + auth_info = bytes2str(auth_info) try: username, password = auth_info.split(':', 1) except ValueError: -- cgit v1.2.3 From 3f2f7f72a73caf087066c75d2e2b6e5ed908d34d Mon Sep 17 00:00:00 2001 From: Helmut Grohne Date: Fri, 29 Jun 2012 09:26:09 +0200 Subject: fix more bytes related issues not covered by test.py * applications returned errors as str instead of bytes * filters documentation updated with bytes * various filters expecting str where bytes are passed * escape_string also needs to use bytes.isalnum instead of str.isalnum * middlewares injecting str where bytes are expected --- wsgitools/applications.py | 6 +++--- wsgitools/filters.py | 40 ++++++++++++++++------------------------ wsgitools/middlewares.py | 8 ++++---- 3 files changed, 23 insertions(+), 31 deletions(-) (limited to 'wsgitools/middlewares.py') diff --git a/wsgitools/applications.py b/wsgitools/applications.py index cdaf0ae..dfd8a2f 100644 --- a/wsgitools/applications.py +++ b/wsgitools/applications.py @@ -50,7 +50,7 @@ class StaticContent: assert isinstance(environ, dict) if environ["REQUEST_METHOD"].upper() not in ["GET", "HEAD"] and \ not self.anymethod: - resp = "Request method not implemented" + resp = b"Request method not implemented" start_response("501 Not Implemented", [("Content-length", str(len(resp)))]) return [resp] @@ -102,7 +102,7 @@ class StaticFile: assert isinstance(environ, dict) if environ["REQUEST_METHOD"].upper() not in ["GET", "HEAD"]: - resp = "Request method not implemented" + resp = b"Request method not implemented" start_response("501 Not Implemented", [("Content-length", str(len(resp)))]) return [resp] @@ -121,7 +121,7 @@ class StaticFile: size = stream.tell() stream.seek(0) except IOError: - resp = "File not found" + resp = b"File not found" start_response("404 File not found", [("Content-length", str(len(resp)))]) return [resp] diff --git a/wsgitools/filters.py b/wsgitools/filters.py index 6f90903..882a0bf 100644 --- a/wsgitools/filters.py +++ b/wsgitools/filters.py @@ -12,6 +12,8 @@ import time import gzip import io +from wsgitools.internal import str2bytes + __all__.append("CloseableIterator") class CloseableIterator: """Concatenating iterator with close attribute.""" @@ -122,15 +124,15 @@ class BaseWSGIFilter: """For each string that is either written by the C{write} callable or returned from the wrapped wsgi application this method is invoked. It must return a string. - @type data: str - @rtype: str + @type data: bytes + @rtype: bytes """ return data def append_data(self): """This function can be used to append data to the response. A list of strings or some kind of iterable yielding strings has to be returned. The default is to return an empty list. - @rtype: gen([str]) + @rtype: gen([bytes]) """ return [] def handle_close(self): @@ -152,7 +154,7 @@ class WSGIFilterMiddleware: def __call__(self, environ, start_response): """wsgi interface @type environ: {str, str} - @rtype: gen([str]) + @rtype: gen([bytes]) """ assert isinstance(environ, dict) reqfilter = self.filterclass() @@ -196,7 +198,7 @@ class WSGIFilterMiddleware: # default arguments. Also note that neither ' nor " are considered printable. # For escape_string to be reversible \ is also not considered printable. def escape_string(string, replacer=list(map( - lambda i: chr(i) if chr(i).isalnum() or + lambda i: chr(i) if str2bytes(chr(i)).isalnum() or chr(i) in '!#$%&()*+,-./:;<=>?@[]^_`{|}~ ' else r"\x%2.2x" % i, range(256)))): @@ -267,11 +269,7 @@ class RequestLogWSGIFilter(BaseWSGIFilter): self.status = status.split()[0] return status def filter_data(self, data): - """BaseWSGIFilter interface - @type data: str - @rtype: str - """ - assert isinstance(data, str) + assert isinstance(data, bytes) self.length += len(data) return data def handle_close(self): @@ -306,30 +304,31 @@ class TimerWSGIFilter(BaseWSGIFilter): def creator(cls, pattern): """Returns a function creating L{TimerWSGIFilter}s with a given pattern beeing a string of exactly eight bytes. - @type pattern: str + @type pattern: bytes """ return lambda:cls(pattern) - def __init__(self, pattern="?GenTime"): + def __init__(self, pattern=b"?GenTime"): """ @type pattern: str """ BaseWSGIFilter.__init__(self) + assert isinstance(pattern, bytes) self.pattern = pattern self.start = time.time() def filter_data(self, data): """BaseWSGIFilter interface - @type data: str - @rtype: str + @type data: bytes + @rtype: bytes """ if data == self.pattern: - return "%8.3g" % (time.time() - self.start) + return str2bytes("%8.3g" % (time.time() - self.start)) return data __all__.append("EncodeWSGIFilter") class EncodeWSGIFilter(BaseWSGIFilter): """Encodes all body data (no headers) with given charset. @note: This violates the wsgi standard as it requires unicode objects - whereas wsgi mandates the use of str. + whereas wsgi mandates the use of bytes. """ @classmethod def creator(cls, charset): @@ -347,7 +346,7 @@ class EncodeWSGIFilter(BaseWSGIFilter): def filter_data(self, data): """BaseWSGIFilter interface @type data: str - @rtype: str + @rtype: bytes """ return data.encode(self.charset) def filter_header(self, header, value): @@ -414,10 +413,6 @@ class GzipWSGIFilter(BaseWSGIFilter): headers.append(("Content-encoding", "gzip")) return headers def filter_data(self, data): - """BaseWSGIFilter interface - @type data: str - @rtype: str - """ if not self.compress: return data self.gzip.write(data) @@ -428,9 +423,6 @@ class GzipWSGIFilter(BaseWSGIFilter): self.sio.seek(0) return data def append_data(self): - """BaseWSGIFilter interface - @rtype: [str] - """ if not self.compress: return [] self.gzip.close() diff --git a/wsgitools/middlewares.py b/wsgitools/middlewares.py index 725deb1..4061d3b 100644 --- a/wsgitools/middlewares.py +++ b/wsgitools/middlewares.py @@ -33,7 +33,7 @@ class SubdirMiddleware: def __call__(self, environ, start_response): """wsgi interface @type environ: {str: str} - @rtype: gen([str]) + @rtype: gen([bytes]) """ assert isinstance(environ, dict) app = None @@ -65,7 +65,7 @@ class NoWriteCallableMiddleware: def __call__(self, environ, start_response): """wsgi interface @type environ: {str, str} - @rtype: gen([str]) + @rtype: gen([bytes]) """ assert isinstance(environ, dict) todo = [None] @@ -89,7 +89,7 @@ class NoWriteCallableMiddleware: ret = self.app(environ, modified_start_response) assert hasattr(ret, "__iter__") - first = "" + first = b"" if not isinstance(ret, list): ret = iter(ret) stopped = False @@ -171,7 +171,7 @@ class ContentLengthMiddleware: return ret ret = iter(ret) - first = "" + first = b"" stopped = False while not (first or stopped): try: -- cgit v1.2.3