From ee5abb0e0b24b4e1ac31412a279a40e166482fce Mon Sep 17 00:00:00 2001 From: Helmut Grohne Date: Thu, 28 Jun 2012 16:12:52 +0200 Subject: drop support for python2.5, use except ... as ... --- wsgitools/authentication.py | 2 +- wsgitools/digest.py | 13 +++++++------ wsgitools/scgi/asynchronous.py | 2 +- wsgitools/scgi/forkpool.py | 4 ++-- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/wsgitools/authentication.py b/wsgitools/authentication.py index 6f5d07b..963dc00 100644 --- a/wsgitools/authentication.py +++ b/wsgitools/authentication.py @@ -64,7 +64,7 @@ class AuthenticationMiddleware: raise AuthenticationRequired( "authorization method not implemented: %r" % method) result = self.authenticate(rest, environ) - except AuthenticationRequired, exc: + except AuthenticationRequired as exc: return self.authorization_required(environ, start_response, exc) assert isinstance(result, dict) assert "user" in result diff --git a/wsgitools/digest.py b/wsgitools/digest.py index 085047f..4b5f8fb 100644 --- a/wsgitools/digest.py +++ b/wsgitools/digest.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python2.5 """ This module contains an C{AuthDigestMiddleware} for authenticating HTTP requests using the method described in RFC2617. The credentials are to be @@ -259,7 +258,7 @@ class UpdatingHtdigestTokenGenerator(HtdigestTokenGenerator): # modifications. try: self.statcache = os.stat(htdigestfile) - except OSError, err: + except OSError as err: raise IOError(str(err)) HtdigestTokenGenerator.__init__(self, realm, htdigestfile, ignoreparseerrors) @@ -276,7 +275,9 @@ class UpdatingHtdigestTokenGenerator(HtdigestTokenGenerator): if self.statcache != statcache: try: self.readhtdigest(self.htdigestfile, self.ignoreparseerrors) - except (IOError, ValueError): + except IOError: + return None + except ValueError: return None return HtdigestTokenGenerator.__call__(self, user, algo) @@ -724,7 +725,7 @@ class AuthDigestMiddleware(AuthenticationMiddleware): try: nonce = credentials["nonce"] credresponse = credentials["response"] - except KeyError, err: + except KeyError as err: raise ProtocolViolation("%s missing in credentials" % err.args[0]) noncecount = 1 @@ -765,7 +766,7 @@ class AuthDigestMiddleware(AuthenticationMiddleware): username = credentials["username"] algo = credentials["algorithm"] uri = credentials["uri"] - except KeyError, err: + except KeyError as err: raise ProtocolViolation("%s missing in credentials" % err.args[0]) try: dig = [credentials["nonce"]] @@ -778,7 +779,7 @@ class AuthDigestMiddleware(AuthenticationMiddleware): try: dig.append(credentials["nc"]) dig.append(credentials["cnonce"]) - except KeyError, err: + except KeyError as err: raise ProtocolViolation( "missing %s in credentials with qop=auth" % err.args[0]) dig.append(qop) diff --git a/wsgitools/scgi/asynchronous.py b/wsgitools/scgi/asynchronous.py index 386e1d0..1dee283 100644 --- a/wsgitools/scgi/asynchronous.py +++ b/wsgitools/scgi/asynchronous.py @@ -262,7 +262,7 @@ class SCGIServer(asyncore.dispatcher): """asyncore interface""" try: ret = self.accept() - except socket.error, err: + except socket.error as err: # See http://bugs.python.org/issue6706 if err.args[0] not in (errno.ECONNABORTED, errno.EAGAIN): raise diff --git a/wsgitools/scgi/forkpool.py b/wsgitools/scgi/forkpool.py index cdd50f0..1bf0c6f 100644 --- a/wsgitools/scgi/forkpool.py +++ b/wsgitools/scgi/forkpool.py @@ -41,7 +41,7 @@ class SocketFileWrapper: return "" try: data = self.sock.recv(toread) - except socket.error, why: + except socket.error as why: if why[0] in (errno.ECONNRESET, errno.ENOTCONN, errno.ESHUTDOWN): data = "" else: @@ -250,7 +250,7 @@ class SCGIServer: self.spawnworker() try: rs, _, _ = select.select(self.workers.keys(), [], []) - except select.error, e: + except select.error as e: if e[0] != errno.EINTR: raise rs = [] -- cgit v1.2.3 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 --- test.py | 11 +---------- wsgitools/adapters.py | 6 ------ wsgitools/filters.py | 5 ----- wsgitools/middlewares.py | 5 ----- 4 files changed, 1 insertion(+), 26 deletions(-) diff --git a/test.py b/test.py index 1acf5aa..f46a512 100755 --- a/test.py +++ b/test.py @@ -9,18 +9,9 @@ try: import cStringIO as io except ImportError: import StringIO as io -try: - from hashlib import md5 -except ImportError: - from md5 import md5 +from hashlib import md5 import sys -try: - next -except NameError: - def next(iterator): - return iterator.next() - class Request: def __init__(self, case): """ diff --git a/wsgitools/adapters.py b/wsgitools/adapters.py index 6c6bbca..4c82200 100644 --- a/wsgitools/adapters.py +++ b/wsgitools/adapters.py @@ -9,12 +9,6 @@ __all__ = [] from wsgitools.filters import CloseableIterator, CloseableList -try: - next -except NameError: - def next(it): - return it.next() - __all__.append("WSGI2to1Adapter") class WSGI2to1Adapter: """Adapts an application with an interface that might somewhen be known as diff --git a/wsgitools/filters.py b/wsgitools/filters.py index 7ae1b69..4c7ff20 100644 --- a/wsgitools/filters.py +++ b/wsgitools/filters.py @@ -17,11 +17,6 @@ try: except ImportError: import StringIO as io -try: - next -except NameError: - def next(it): - return it.next() __all__.append("CloseableIterator") class CloseableIterator: 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 53c7d892904c9f133d55f052a27dafd3911d0c5c Mon Sep 17 00:00:00 2001 From: Helmut Grohne Date: Thu, 28 Jun 2012 17:23:06 +0200 Subject: provide py3 style __next__ methods --- wsgitools/filters.py | 4 +++- wsgitools/scgi/__init__.py | 4 +++- wsgitools/scgi/forkpool.py | 4 +++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/wsgitools/filters.py b/wsgitools/filters.py index 4c7ff20..4305c9d 100644 --- a/wsgitools/filters.py +++ b/wsgitools/filters.py @@ -35,7 +35,7 @@ class CloseableIterator: @rtype: gen() """ return self - def next(self): + def __next__(self): """iterator interface""" if not self.iterators: raise StopIteration @@ -44,6 +44,8 @@ class CloseableIterator: except StopIteration: self.iterators.pop(0) return next(self) + def next(self): + return self.__next__() __all__.append("CloseableList") class CloseableList(list): diff --git a/wsgitools/scgi/__init__.py b/wsgitools/scgi/__init__.py index cbe7a80..4e60b74 100644 --- a/wsgitools/scgi/__init__.py +++ b/wsgitools/scgi/__init__.py @@ -45,13 +45,15 @@ class FileWrapper: def __iter__(self): return self - def next(self): + def __next__(self): assert self.offset <= 0 self.offset = -1 data = self.filelike.read(self.blksize) if data: return data raise StopIteration + def next(self): + return self.__next__() def _convert_environ(environ, multithread=False, multiprocess=False, run_once=False): diff --git a/wsgitools/scgi/forkpool.py b/wsgitools/scgi/forkpool.py index 1bf0c6f..7cc6d18 100644 --- a/wsgitools/scgi/forkpool.py +++ b/wsgitools/scgi/forkpool.py @@ -135,7 +135,7 @@ class SocketFileWrapper: def __iter__(self): """see pep333""" return self - def next(self): + def __next__(self): """ see pep333 @raise socket.error: @@ -144,6 +144,8 @@ class SocketFileWrapper: if not data: raise StopIteration return data + def next(self): + return self.__next__() def flush(self): """see pep333""" pass -- cgit v1.2.3 From b83f5682c9d81cd53b8b45a6baedc844a68b85d2 Mon Sep 17 00:00:00 2001 From: Helmut Grohne Date: Thu, 28 Jun 2012 17:55:49 +0200 Subject: update python versions in README --- README | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/README b/README index fef9f25..eb9ce21 100644 --- a/README +++ b/README @@ -2,13 +2,11 @@ The software should be usable by reading the docstrings. If you think that certain features are missing or you found a bug, don't hesitate to ask me via mail! -Supported Python versions currently are 2.5 and 2.6. It might still support -2.4, but I did not test that. 3.x is currently not supported, because the -syntax is incompatible. However the fixes needed to make the code usable -with 3.x should be small. Eventually there will be a 3.x branch. +Supported Python versions currently are 2.6 and 2.7. Work is in progress to +support 3.x. Installation should be easy using setup.py. I recommend running the test suite by invoking "python test.py" from the source tree to spot problems early. This -is especially true if you try Python versions other than 2.5 and 2.6. +is especially true if you try Python versions other than 2.6 and 2.7. Helmut Grohne -- 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(-) 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 dd015aa622b25f8638ab0a13b5d75004d16004c8 Mon Sep 17 00:00:00 2001 From: Helmut Grohne Date: Thu, 28 Jun 2012 23:40:17 +0200 Subject: make scgi.asynchronous work with py3 --- wsgitools/scgi/asynchronous.py | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/wsgitools/scgi/asynchronous.py b/wsgitools/scgi/asynchronous.py index 7eb1a30..2c97f42 100644 --- a/wsgitools/scgi/asynchronous.py +++ b/wsgitools/scgi/asynchronous.py @@ -37,8 +37,8 @@ class SCGIConnection(asyncore.dispatcher): self.state = SCGIConnection.NEW # internal state self.environ = config.copy() # environment passed to wsgi app self.reqlen = -1 # request length used in two different meanings - self.inbuff = "" # input buffer - self.outbuff = "" # output buffer + self.inbuff = b"" # input buffer + self.outbuff = b"" # output buffer self.wsgihandler = None # wsgi application self.wsgiiterator = None # wsgi application iterator self.outheaders = () # headers to be sent @@ -50,7 +50,10 @@ class SCGIConnection(asyncore.dispatcher): assert not self.outbuff status, headers = self.outheaders headdata = "".join(map("%s: %s\r\n".__mod__, headers)) - self.outbuff = "Status: %s\r\n%s\r\n" % (status, headdata) + headdata = "Status: %s\r\n%s\r\n" % (status, headdata) + if not isinstance(headdata, bytes): + headdata = headdata.encode("iso-8859-1") + self.outbuff = headdata self.outheaders = True def _wsgi_write(self, data): @@ -74,8 +77,8 @@ class SCGIConnection(asyncore.dispatcher): data = self.recv(self.blocksize) self.inbuff += data if self.state == SCGIConnection.NEW: - if ':' in self.inbuff: - reqlen, self.inbuff = self.inbuff.split(':', 1) + if b':' in self.inbuff: + reqlen, self.inbuff = self.inbuff.split(b':', 1) if not reqlen.isdigit(): self.close() return # invalid request format @@ -93,15 +96,18 @@ class SCGIConnection(asyncore.dispatcher): buff = self.inbuff[:self.reqlen] remainder = self.inbuff[self.reqlen:] - while buff.count('\0') >= 2: - key, value, buff = buff.split('\0', 2) + while buff.count(b'\0') >= 2: + key, value, buff = buff.split(b'\0', 2) + if not isinstance(key, str): + key = key.decode("iso-8859-1") + value = value.decode("iso-8859-1") self.environ[key] = value self.reqlen -= len(key) + len(value) + 2 self.inbuff = buff + remainder if self.reqlen == 0: - if self.inbuff.startswith(','): + if self.inbuff.startswith(b','): self.inbuff = self.inbuff[1:] if not self.environ.get("CONTENT_LENGTH", "bad").isdigit(): self.close() @@ -119,7 +125,7 @@ class SCGIConnection(asyncore.dispatcher): if len(self.inbuff) >= self.reqlen: self.body.write(self.inbuff[:self.reqlen]) self.body.seek(0) - self.inbuff = "" + self.inbuff = b"" self.reqlen = 0 _convert_environ(self.environ) self.environ["wsgi.input"] = self.body @@ -136,7 +142,7 @@ class SCGIConnection(asyncore.dispatcher): else: self.body.write(self.inbuff) self.reqlen -= len(self.inbuff) - self.inbuff = "" + self.inbuff = b"" def start_response(self, status, headers, exc_info=None): assert isinstance(status, str) @@ -165,7 +171,7 @@ class SCGIConnection(asyncore.dispatcher): if len(self.outbuff) < self.blocksize: self._try_send_headers() for data in self.wsgiiterator: - assert isinstance(data, str) + assert isinstance(data, bytes) if data: self.outbuff += data break -- cgit v1.2.3 From d43d3d8947964392644b84ec1bce76c6f4193bea Mon Sep 17 00:00:00 2001 From: Helmut Grohne Date: Fri, 29 Jun 2012 07:38:57 +0200 Subject: scgi.asynchronous: move {en,de}coding to internal module --- wsgitools/internal.py | 14 ++++++++++++++ wsgitools/scgi/asynchronous.py | 11 +++-------- 2 files changed, 17 insertions(+), 8 deletions(-) create mode 100644 wsgitools/internal.py diff --git a/wsgitools/internal.py b/wsgitools/internal.py new file mode 100644 index 0000000..c392b6a --- /dev/null +++ b/wsgitools/internal.py @@ -0,0 +1,14 @@ +if bytes is str: + def bytes2str(bstr): + assert isinstance(bstr, bytes) + return bstr + def str2bytes(sstr): + assert isinstance(sstr, str) + return sstr +else: + def bytes2str(bstr): + assert isinstance(bstr, bytes) + return bstr.decode("iso-8859-1") # always successful + def str2bytes(sstr): + assert isinstance(sstr, str) + return sstr.encode("iso-8859-1") # might fail, but spec says it doesn't diff --git a/wsgitools/scgi/asynchronous.py b/wsgitools/scgi/asynchronous.py index 2c97f42..3009593 100644 --- a/wsgitools/scgi/asynchronous.py +++ b/wsgitools/scgi/asynchronous.py @@ -6,6 +6,7 @@ import socket import sys import errno +from wsgitools.internal import bytes2str, str2bytes from wsgitools.scgi import _convert_environ, FileWrapper if sys.version_info[0] >= 3: @@ -51,9 +52,7 @@ class SCGIConnection(asyncore.dispatcher): status, headers = self.outheaders headdata = "".join(map("%s: %s\r\n".__mod__, headers)) headdata = "Status: %s\r\n%s\r\n" % (status, headdata) - if not isinstance(headdata, bytes): - headdata = headdata.encode("iso-8859-1") - self.outbuff = headdata + self.outbuff = str2bytes(headdata) self.outheaders = True def _wsgi_write(self, data): @@ -98,10 +97,7 @@ class SCGIConnection(asyncore.dispatcher): while buff.count(b'\0') >= 2: key, value, buff = buff.split(b'\0', 2) - if not isinstance(key, str): - key = key.decode("iso-8859-1") - value = value.decode("iso-8859-1") - self.environ[key] = value + self.environ[bytes2str(key)] = bytes2str(value) self.reqlen -= len(key) + len(value) + 2 self.inbuff = buff + remainder @@ -276,4 +272,3 @@ class SCGIServer(asyncore.dispatcher): """Runs the server. It will not return and you can invoke C{asyncore.loop()} instead achieving the same effect.""" asyncore.loop() - -- cgit v1.2.3 From eba0855c881bea9f533a8d4b359f8711125e5037 Mon Sep 17 00:00:00 2001 From: Helmut Grohne Date: Fri, 29 Jun 2012 07:41:56 +0200 Subject: make scgi.forkpool work with py3k Note that the construction of the header moved from our internal sendheaders function to the start_response function. This way users supplying unicode characters no representable in iso-8859-1 will get a UnicodeEncodeError back from start_response, which is more useful than failing later while yielding bytes. --- wsgitools/scgi/forkpool.py | 71 +++++++++++++++++++++++----------------------- 1 file changed, 35 insertions(+), 36 deletions(-) diff --git a/wsgitools/scgi/forkpool.py b/wsgitools/scgi/forkpool.py index 7cc6d18..88f64be 100644 --- a/wsgitools/scgi/forkpool.py +++ b/wsgitools/scgi/forkpool.py @@ -12,6 +12,7 @@ import sys import errno import signal +from wsgitools.internal import bytes2str, str2bytes from wsgitools.scgi import _convert_environ, FileWrapper if sys.version_info[0] >= 3: @@ -28,22 +29,22 @@ class SocketFileWrapper: def __init__(self, sock, toread): """@param sock: is a C{socket.socket()}""" self.sock = sock - self.buff = "" + self.buff = b"" self.toread = toread def _recv(self, size=4096): """ internal method for receiving and counting incoming data - @raise socket.error: + @raises socket.error: """ toread = min(size, self.toread) if not toread: - return "" + return b"" try: data = self.sock.recv(toread) except socket.error as why: if why[0] in (errno.ECONNRESET, errno.ENOTCONN, errno.ESHUTDOWN): - data = "" + data = b"" else: raise self.toread -= len(data) @@ -63,12 +64,12 @@ class SocketFileWrapper: def read(self, size=None): """ see pep333 - @raise socket.error: + @raises socket.error: """ if size is None: retl = [] data = self.buff - self.buff = "" + self.buff = b"" while True: retl.append(data) try: @@ -77,7 +78,7 @@ class SocketFileWrapper: break if not data: break - return "".join(retl) + return b"".join(retl) datalist = [self.buff] datalen = len(self.buff) while datalen < size: @@ -89,22 +90,22 @@ class SocketFileWrapper: break datalist.append(data) datalen += len(data) - self.buff = "".join(datalist) + self.buff = b"".join(datalist) if size <= len(self.buff): ret, self.buff = self.buff[:size], self.buff[size:] return ret - ret, self.buff = self.buff, "" + ret, self.buff = self.buff, b"" return ret def readline(self, size=None): """ see pep333 - @raise socket.error: + @raises socket.error: """ while True: try: - split = self.buff.index('\n') + 1 + split = self.buff.index(b'\n') + 1 if size is not None and split > size: split = size ret, self.buff = self.buff[:split], self.buff[split:] @@ -119,14 +120,14 @@ class SocketFileWrapper: else: data = self._recv(4096) if not data: - ret, self.buff = self.buff, "" + ret, self.buff = self.buff, b"" return ret self.buff += data def readlines(self): """ see pep333 - @raise socket.error: + @raises socket.error: """ data = self.readline() while data: @@ -138,7 +139,7 @@ class SocketFileWrapper: def __next__(self): """ see pep333 - @raise socket.error: + @raises socket.error: """ data = self.read(4096) if not data: @@ -151,7 +152,7 @@ class SocketFileWrapper: pass def write(self, data): """see pep333""" - assert isinstance(data, str) + assert isinstance(data, bytes) try: self.sock.sendall(data) except socket.error: @@ -261,11 +262,11 @@ class SCGIServer: data = self.workers[s].sock.recv(1) except socket.error: # we cannot handle errors here, so drop the connection. - data = '' - if data == '': + data = b'' + if data == b'': self.workers[s].sock.close() del self.workers[s] - elif data in ('0', '1'): + elif data in (b'0', b'1'): self.workers[s].state = int(data) else: raise RuntimeError("unexpected data from worker") @@ -338,14 +339,14 @@ class SCGIServer: def work(self, worksock): """ internal! serves maxrequests times - @raise socket.error: + @raises socket.error: """ for _ in range(self.maxrequests): (con, addr) = self.server.accept() # we cannot handle socket.errors here. - worksock.sendall('1') # tell server we're working + worksock.sendall(b'1') # tell server we're working self.process(con) - worksock.sendall('0') # tell server we've finished + worksock.sendall(b'0') # tell server we've finished def process(self, con): """ @@ -362,10 +363,10 @@ class SCGIServer: except socket.error: con.close() return - if not ':' in data: + if not b':' in data: con.close() return - length, data = data.split(':', 1) + length, data = data.split(b':', 1) if not length.isdigit(): # clear protocol violation con.close() return @@ -383,35 +384,32 @@ class SCGIServer: data += t # netstrings! - data = data.split('\0') + data = data.split(b'\0') # the byte beyond has to be a ','. # and the number of netstrings excluding the final ',' has to be even - if data.pop() != ',' or len(data) % 2 != 0: + if data.pop() != b',' or len(data) % 2 != 0: con.close() return environ = self.config.copy() while data: - key = data.pop(0) - value = data.pop(0) + key = bytes2str(data.pop(0)) + value = bytes2str(data.pop(0)) environ[key] = value # elements: # 0 -> None: no headers set # 0 -> False: set but unsent # 0 -> True: sent - # 1 -> status string - # 2 -> header list - response_head = [None, None, None] + # 1 -> bytes of the complete header + response_head = [None, None] def sendheaders(): assert response_head[0] is not None # headers set if response_head[0] != True: response_head[0] = True try: - con.sendall('Status: %s\r\n%s\r\n\r\n' % (response_head[1], - '\r\n'.join(map("%s: %s".__mod__, - response_head[2])))) + con.sendall(response_head[1]) except socket.error: pass @@ -429,9 +427,10 @@ class SCGIServer: finally: exc_info = None assert not response_head[0] # unset or not sent + headers = "".join(map("%s: %s\r\n".__mod__, headers)) + full_header = "Status: %s\r\n%s\r\n" % (status, headers) + response_head[1] = str2bytes(full_header) response_head[0] = False # set but nothing sent - response_head[1] = status - response_head[2] = headers return dumbsend if not environ.get("CONTENT_LENGTH", "bad").isdigit(): @@ -455,7 +454,7 @@ class SCGIServer: assert response_head[0] is not None result_iter = iter(result) for data in result_iter: - assert isinstance(data, str) + assert isinstance(data, bytes) dumbsend(data) if response_head[0] != True: sendheaders() -- 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(-) 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(-) 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 From 85a4d0c404c767460887eafe5e7aa2511f70bad6 Mon Sep 17 00:00:00 2001 From: Helmut Grohne Date: Sun, 1 Jul 2012 11:30:22 +0200 Subject: make StaticFile work with py3k There is no file builtin, and binary mode gives bytes instead of str. --- wsgitools/applications.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wsgitools/applications.py b/wsgitools/applications.py index dfd8a2f..9c6dad8 100644 --- a/wsgitools/applications.py +++ b/wsgitools/applications.py @@ -112,7 +112,7 @@ class StaticFile: try: if isinstance(self.filelike, basestring): # raises IOError - stream = file(self.filelike) + stream = open(self.filelike, "rb") size = os.path.getsize(self.filelike) else: stream = self.filelike -- cgit v1.2.3 From b0938bb51c915ea5d888e2e88bbb62f4d1da199c Mon Sep 17 00:00:00 2001 From: Helmut Grohne Date: Sun, 1 Jul 2012 11:38:29 +0200 Subject: make HtdigestTokenGenerator work with py3k Define a textopen function that returns "native strings" (in the sense of pep3333). Therefore textopen needs to decode using iso-8859-1 iff running on py3k. Additionally use a with construct to close the file being read in all circumstances. --- wsgitools/digest.py | 25 +++++++++++++------------ wsgitools/internal.py | 5 +++++ 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/wsgitools/digest.py b/wsgitools/digest.py index 532b371..6395d02 100644 --- a/wsgitools/digest.py +++ b/wsgitools/digest.py @@ -19,7 +19,7 @@ import hashlib import time import os -from wsgitools.internal import bytes2str, str2bytes +from wsgitools.internal import bytes2str, str2bytes, textopen from wsgitools.authentication import AuthenticationRequired, \ ProtocolViolation, AuthenticationMiddleware @@ -239,18 +239,19 @@ class HtdigestTokenGenerator(AbstractTokenGenerator): """ assert isinstance(htdigestfile, str) self.users = {} - for line in file(htdigestfile): - parts = line.rstrip("\n").split(":") - if len(parts) != 3: - if ignoreparseerrors: + with textopen(htdigestfile, "r") as htdigest: + for line in htdigest: + parts = line.rstrip("\n").split(":") + if len(parts) != 3: + if ignoreparseerrors: + continue + raise ValueError("invalid number of colons in htdigest file") + user, realm, token = parts + if realm != self.realm: continue - raise ValueError("invalid number of colons in htdigest file") - user, realm, token = parts - if realm != self.realm: - continue - if user in self.users and not ignoreparseerrors: - raise ValueError("duplicate user in htdigest file") - self.users[user] = token + if user in self.users and not ignoreparseerrors: + raise ValueError("duplicate user in htdigest file") + self.users[user] = token def __call__(self, user, algo="md5"): assert algo.lower() in ["md5", "md5-sess"] diff --git a/wsgitools/internal.py b/wsgitools/internal.py index c392b6a..c4f1da1 100644 --- a/wsgitools/internal.py +++ b/wsgitools/internal.py @@ -5,6 +5,8 @@ if bytes is str: def str2bytes(sstr): assert isinstance(sstr, str) return sstr + def textopen(filename, mode): + return open(filename, mode) else: def bytes2str(bstr): assert isinstance(bstr, bytes) @@ -12,3 +14,6 @@ else: def str2bytes(sstr): assert isinstance(sstr, str) return sstr.encode("iso-8859-1") # might fail, but spec says it doesn't + def textopen(filename, mode): + # We use the same encoding as for all wsgi strings here. + return open(filename, mode, encoding="iso-8859-1") -- cgit v1.2.3 From ae660ee64afb068efd61274853d9d3e05300446e Mon Sep 17 00:00:00 2001 From: Helmut Grohne Date: Thu, 6 Jun 2013 10:36:49 +0200 Subject: port CachingMiddlewareTest to py3k --- test.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test.py b/test.py index 8e628cd..946ee29 100755 --- a/test.py +++ b/test.py @@ -309,24 +309,24 @@ class CachingMiddlewareTest(unittest.TestCase): if "maxage0" in environ["SCRIPT_NAME"]: headers.append(("Cache-Control", "max-age=0")) start_response("200 Found", headers) - return ["%d" % count] + return [str2bytes("%d" % count)] def testCache(self): res = Request(self)(self.cached) res.status(200) - self.assertEqual(res.get_data(), "1") + self.assertEqual(res.get_data(), b"1") res = Request(self)(self.cached) res.status(200) - self.assertEqual(res.get_data(), "1") + self.assertEqual(res.get_data(), b"1") def testNoCache(self): res = Request(self)(self.cached) res.status(200) - self.assertEqual(res.get_data(), "1") + self.assertEqual(res.get_data(), b"1") res = Request(self).setheader( "Cache-Control", "max-age=0")(self.cached) res.status(200) - self.assertEqual(res.get_data(), "2") + self.assertEqual(res.get_data(), b"2") class BasicAuthMiddlewareTest(unittest.TestCase): def setUp(self): -- cgit v1.2.3 From a5634dea0d4f24d6f27ba5d7e50c773979eec93c Mon Sep 17 00:00:00 2001 From: Helmut Grohne Date: Thu, 6 Jun 2013 11:02:33 +0200 Subject: fix the py3k part of RequestLogWSGIFilterTest --- test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test.py b/test.py index 946ee29..cf40b92 100755 --- a/test.py +++ b/test.py @@ -376,7 +376,7 @@ import gzip class RequestLogWSGIFilterTest(unittest.TestCase): def testSimple(self): app = applications.StaticContent("200 Found", - [("Content-Type", "text/plain")], "nothing") + [("Content-Type", "text/plain")], b"nothing") log = io.StringIO() logfilter = filters.RequestLogWSGIFilter.creator(log) app = filters.WSGIFilterMiddleware(app, logfilter) -- cgit v1.2.3 From 27ed9839582c4fce9a0fff82281fb2e302be808e Mon Sep 17 00:00:00 2001 From: Helmut Grohne Date: Thu, 6 Jun 2013 11:06:26 +0200 Subject: fix RequestLogWSGIFilterTest Clarify the type of the log file-like passed to RequestLogWSGIFilter. --- test.py | 5 ++++- wsgitools/filters.py | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/test.py b/test.py index cf40b92..2d1d29d 100755 --- a/test.py +++ b/test.py @@ -377,7 +377,10 @@ class RequestLogWSGIFilterTest(unittest.TestCase): def testSimple(self): app = applications.StaticContent("200 Found", [("Content-Type", "text/plain")], b"nothing") - log = io.StringIO() + if isinstance("x", bytes): + log = io.BytesIO() + else: + log = io.StringIO() logfilter = filters.RequestLogWSGIFilter.creator(log) app = filters.WSGIFilterMiddleware(app, logfilter) req = Request(self) diff --git a/wsgitools/filters.py b/wsgitools/filters.py index 882a0bf..471c949 100644 --- a/wsgitools/filters.py +++ b/wsgitools/filters.py @@ -217,6 +217,9 @@ class RequestLogWSGIFilter(BaseWSGIFilter): """Returns a function creating L{RequestLogWSGIFilter}s on given log file. log has to be a file-like object. @type log: file-like + @param log: elements of type str are written to the log. That means in + Py3.X the contents are decoded and in Py2.X the log is assumed + to be encoded in latin1. This follows the spirit of WSGI. @type flush: bool @param flush: if True, invoke the flush method on log after each write invocation -- cgit v1.2.3 From c1ba0c783fc59dc8d00b9b8aed7250569bcc14d4 Mon Sep 17 00:00:00 2001 From: Helmut Grohne Date: Mon, 9 Dec 2013 07:38:18 +0100 Subject: fix possible uncaught ValueError from scgi servers With unicode strings it no longer holds that if s.isdigit() then you can safely int(s), because there are more digits (such as ^3 \xb3) accepted by isdigit. This can cause an uncaught ValueError in certain places if the remote scgi server presents bogus data. Thanks to Klaus Aehlig for pointing out what isdigit accepts. --- wsgitools/scgi/asynchronous.py | 12 +++++++----- wsgitools/scgi/forkpool.py | 11 +++++++---- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/wsgitools/scgi/asynchronous.py b/wsgitools/scgi/asynchronous.py index 3009593..51c1d55 100644 --- a/wsgitools/scgi/asynchronous.py +++ b/wsgitools/scgi/asynchronous.py @@ -78,10 +78,11 @@ class SCGIConnection(asyncore.dispatcher): if self.state == SCGIConnection.NEW: if b':' in self.inbuff: reqlen, self.inbuff = self.inbuff.split(b':', 1) - if not reqlen.isdigit(): + try: + reqlen = int(reqlen) + except ValueError: # invalid request format self.close() - return # invalid request format - reqlen = int(reqlen) + return if reqlen > self.maxrequestsize: self.close() return # request too long @@ -105,10 +106,11 @@ class SCGIConnection(asyncore.dispatcher): if self.reqlen == 0: if self.inbuff.startswith(b','): self.inbuff = self.inbuff[1:] - if not self.environ.get("CONTENT_LENGTH", "bad").isdigit(): + try: + self.reqlen = int(self.environ["CONTENT_LENGTH"]) + except ValueError: self.close() return - self.reqlen = int(self.environ["CONTENT_LENGTH"]) if self.reqlen > self.maxpostsize: self.close() return diff --git a/wsgitools/scgi/forkpool.py b/wsgitools/scgi/forkpool.py index 150ed44..1f4cdee 100644 --- a/wsgitools/scgi/forkpool.py +++ b/wsgitools/scgi/forkpool.py @@ -405,10 +405,11 @@ class SCGIServer: con.close() return length, data = data.split(b':', 1) - if not length.isdigit(): # clear protocol violation + try: + length = int(length) + except ValueError: # clear protocol violation con.close() return - length = int(length) while len(data) != length + 1: # read one byte beyond try: @@ -471,12 +472,14 @@ class SCGIServer: response_head[0] = False # set but nothing sent return dumbsend - if not environ.get("CONTENT_LENGTH", "bad").isdigit(): + try: + content_length = int(environ["CONTENT_LENGTH"]) + except ValueError: con.close() return _convert_environ(environ, multiprocess=True) - sfw = SocketFileWrapper(con, int(environ["CONTENT_LENGTH"])) + sfw = SocketFileWrapper(con, content_length) environ["wsgi.input"] = sfw result = self.wsgiapp(environ, start_response) -- cgit v1.2.3