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/scgi/asynchronous.py | 2 +- wsgitools/scgi/forkpool.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'wsgitools/scgi') 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 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(-) (limited to 'wsgitools/scgi') 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 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/scgi') 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(-) (limited to 'wsgitools/scgi') 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 (limited to 'wsgitools/scgi') 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(-) (limited to 'wsgitools/scgi') 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 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(-) (limited to 'wsgitools/scgi') 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