From 7e640f804cacc2189e6f539b7b8861a2ef1a2461 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Ba=C5=A1ti?= <mbasti@redhat.com> Date: Thu, 11 Jul 2019 19:01:43 +0200 Subject: [PATCH 01/14] Logging: Handle auth type case insensitively MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit According RFC-7617 (inherited from RFC-2978) schema and parameter names are handled case insensitively: ``` Note that both scheme and parameter names are matched case- insensitively. ``` Signed-off-by: Martin Bašti <mbasti@redhat.com> --- gunicorn/glogging.py | 2 +- tests/test_logger.py | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/gunicorn/glogging.py b/gunicorn/glogging.py index 3f7b4ac79..a096f9679 100644 --- a/gunicorn/glogging.py +++ b/gunicorn/glogging.py @@ -445,7 +445,7 @@ def _set_syslog_handler(self, log, cfg, fmt, name): def _get_user(self, environ): user = None http_auth = environ.get("HTTP_AUTHORIZATION") - if http_auth and http_auth.startswith('Basic'): + if http_auth and http_auth.lower().startswith('basic'): auth = http_auth.split(" ", 1) if len(auth) == 2: try: diff --git a/tests/test_logger.py b/tests/test_logger.py index 5b8c0d42f..54801266c 100644 --- a/tests/test_logger.py +++ b/tests/test_logger.py @@ -1,6 +1,8 @@ import datetime from types import SimpleNamespace +import pytest + from gunicorn.config import Config from gunicorn.glogging import Logger @@ -47,7 +49,13 @@ def test_atoms_zero_bytes(): assert atoms['B'] == 0 -def test_get_username_from_basic_auth_header(): +@pytest.mark.parametrize('auth', [ + # auth type is case in-sensitive + 'Basic YnJrMHY6', + 'basic YnJrMHY6', + 'BASIC YnJrMHY6', +]) +def test_get_username_from_basic_auth_header(auth): request = SimpleNamespace(headers=()) response = SimpleNamespace( status='200', response_length=1024, sent=1024, @@ -57,7 +65,7 @@ def test_get_username_from_basic_auth_header(): 'REQUEST_METHOD': 'GET', 'RAW_URI': '/my/path?foo=bar', 'PATH_INFO': '/my/path', 'QUERY_STRING': 'foo=bar', 'SERVER_PROTOCOL': 'HTTP/1.1', - 'HTTP_AUTHORIZATION': 'Basic YnJrMHY6', + 'HTTP_AUTHORIZATION': auth, } logger = Logger(Config()) atoms = logger.atoms(response, request, environ, datetime.timedelta(seconds=1)) From f38f717539b1b7296720805b8ae3969c3509b9c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Ba=C5=A1ti?= <mbasti@redhat.com> Date: Thu, 11 Jul 2019 19:12:16 +0200 Subject: [PATCH 02/14] Fix pytest 5.0.0 compatibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pytest.raises() returns exception info not the exception itself. They changed implementation of exception info, so now .value property must be used to get the exception instance and have proper output from str() method. https://github.com/pytest-dev/pytest/issues/5412 Signed-off-by: Martin Bašti <mbasti@redhat.com> --- tests/test_util.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/test_util.py b/tests/test_util.py index 3b8688a21..2494d2c54 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -29,15 +29,15 @@ def test_parse_address(test_input, expected): def test_parse_address_invalid(): - with pytest.raises(RuntimeError) as err: + with pytest.raises(RuntimeError) as exc_info: util.parse_address('127.0.0.1:test') - assert "'test' is not a valid port number." in str(err) + assert "'test' is not a valid port number." in str(exc_info.value) def test_parse_fd_invalid(): - with pytest.raises(RuntimeError) as err: + with pytest.raises(RuntimeError) as exc_info: util.parse_address('fd://asd') - assert "'asd' is not a valid file descriptor." in str(err) + assert "'asd' is not a valid file descriptor." in str(exc_info.value) def test_http_date(): @@ -63,24 +63,24 @@ def test_warn(capsys): def test_import_app(): assert util.import_app('support:app') - with pytest.raises(ImportError) as err: + with pytest.raises(ImportError) as exc_info: util.import_app('a:app') - assert 'No module' in str(err) + assert 'No module' in str(exc_info.value) - with pytest.raises(AppImportError) as err: + with pytest.raises(AppImportError) as exc_info: util.import_app('support:wrong_app') msg = "Failed to find application object 'wrong_app' in 'support'" - assert msg in str(err) + assert msg in str(exc_info.value) def test_to_bytestring(): assert util.to_bytestring('test_str', 'ascii') == b'test_str' assert util.to_bytestring('test_str®') == b'test_str\xc2\xae' assert util.to_bytestring(b'byte_test_str') == b'byte_test_str' - with pytest.raises(TypeError) as err: + with pytest.raises(TypeError) as exc_info: util.to_bytestring(100) msg = '100 is not a string' - assert msg in str(err) + assert msg in str(exc_info.value) @pytest.mark.parametrize('test_input, expected', [ From 40802904ebba5a8b1ab6bdec927bacd09fd1b099 Mon Sep 17 00:00:00 2001 From: Randall Leeds <randall@bleeds.info> Date: Sun, 16 Jun 2019 23:50:45 -0400 Subject: [PATCH 03/14] Avoid unnecessary chown of temporary files When Gunicorn is configured to change the effective user or group of the worker processes, it changes the owner and group fo the the temporary files used for interprocess communication. With this change, Gunicorn does not change the owner or group of the files if the worker processes will run as the current effective user and gorup. This change avoids calling chown when it is not necessary, which may allow Gunicorn to be used in environments that restrict use of the chown syscall. Relates to #2059. --- gunicorn/workers/workertmp.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/gunicorn/workers/workertmp.py b/gunicorn/workers/workertmp.py index 22aaef34c..a37ed1558 100644 --- a/gunicorn/workers/workertmp.py +++ b/gunicorn/workers/workertmp.py @@ -21,11 +21,13 @@ def __init__(self, cfg): if fdir and not os.path.isdir(fdir): raise RuntimeError("%s doesn't exist. Can't create workertmp." % fdir) fd, name = tempfile.mkstemp(prefix="wgunicorn-", dir=fdir) - - # allows the process to write to the file - util.chown(name, cfg.uid, cfg.gid) os.umask(old_umask) + # change the owner and group of the file if the worker will run as + # a different user or group, so that the worker can modify the file + if cfg.uid != os.geteuid() or cfg.gid != os.getegid(): + util.chown(name, cfg.uid, cfg.gid) + # unlink the file so we don't leak tempory files try: if not IS_CYGWIN: From 40d22ae38d577a6d364730ae8fba669693e87706 Mon Sep 17 00:00:00 2001 From: John Whitlock <jwhitlock@mozilla.com> Date: Mon, 19 Aug 2019 19:27:59 -0500 Subject: [PATCH 04/14] Add setproctitle to extras_require (#2094) This allows you to specify that you want setproctitle installed so that gunicorn can set meaningful process names at install time or in a requirements file. --- docs/source/custom.rst | 3 ++- docs/source/install.rst | 22 ++++++++++++++++++++++ setup.py | 1 + 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/docs/source/custom.rst b/docs/source/custom.rst index 0b8c366c0..0fb392501 100644 --- a/docs/source/custom.rst +++ b/docs/source/custom.rst @@ -13,7 +13,8 @@ Here is a small example where we create a very small WSGI app and load it with a custom Application: .. literalinclude:: ../../examples/standalone_app.py - :lines: 11-60 + :start-after: # See the NOTICE for more information + :lines: 2- Direct Usage of Existing WSGI Apps ---------------------------------- diff --git a/docs/source/install.rst b/docs/source/install.rst index 3002a6110..d6d146d22 100644 --- a/docs/source/install.rst +++ b/docs/source/install.rst @@ -52,6 +52,28 @@ want to consider one of the alternate worker types. installed, this is the most likely reason. +Extra Packages +============== +Some Gunicorn options require additional packages. You can use the ``[extra]`` +syntax to install these at the same time as Gunicorn. + +Most extra packages are needed for alternate worker types. See the +`design docs`_ for more information on when you'll want to consider an +alternate worker type. + +* ``gunicorn[eventlet]`` - Eventlet-based greenlets workers +* ``gunicorn[gevent]`` - Gevent-based greenlets workers +* ``gunicorn[gthread]`` - Threaded workers +* ``gunicorn[tornado]`` - Tornado-based workers, not recommended + +If you are running more than one instance of Gunicorn, the :ref:`proc-name` +setting will help distinguish between them in tools like ``ps`` and ``top``. + +* ``gunicorn[setproctitle]`` - Enables setting the process name + +Multiple extras can be combined, like +``pip install gunicorn[gevent,setproctitle]``. + Debian GNU/Linux ================ diff --git a/setup.py b/setup.py index ee898d884..31d173f62 100644 --- a/setup.py +++ b/setup.py @@ -78,6 +78,7 @@ def run_tests(self): 'eventlet': ['eventlet>=0.9.7'], 'tornado': ['tornado>=0.2'], 'gthread': [], + 'setproctitle': ['setproctitle'], } setup( From d765f0d123fff5da0f36da8f087a8dd0da778411 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=A5=EC=A4=80=EC=98=81?= <wnsdud3256@gmail.com> Date: Tue, 20 Aug 2019 09:34:18 +0900 Subject: [PATCH 05/14] Group exceptions with same body together in Arbiter.run() (#2081) --- gunicorn/arbiter.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/gunicorn/arbiter.py b/gunicorn/arbiter.py index 7eaa2c177..bca671d17 100644 --- a/gunicorn/arbiter.py +++ b/gunicorn/arbiter.py @@ -223,9 +223,7 @@ def run(self): self.log.info("Handling signal: %s", signame) handler() self.wakeup() - except StopIteration: - self.halt() - except KeyboardInterrupt: + except (StopIteration, KeyboardInterrupt): self.halt() except HaltServer as inst: self.halt(reason=inst.reason, exit_status=inst.exit_status) From 799df751c71c3c4024bfe5d4cb884ca159370a04 Mon Sep 17 00:00:00 2001 From: Leonardo Furtado <srleonardofurtado@gmail.com> Date: Mon, 19 Aug 2019 21:46:22 -0300 Subject: [PATCH 06/14] Add link to CONTRIBUTING.md from README.rst (#2069) --- README.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.rst b/README.rst index 6b9bcaf19..c9e3ebdff 100644 --- a/README.rst +++ b/README.rst @@ -52,6 +52,12 @@ Example with test app:: $ gunicorn --workers=2 test:app +Contributing +------------ + +See `our complete contributor's guide <CONTRIBUTING.md>`_ for more details. + + License ------- From f35ae584b41b4808a23a689d3168e87328d5ebb1 Mon Sep 17 00:00:00 2001 From: johnthagen <johnthagen@users.noreply.github.com> Date: Sat, 7 Sep 2019 21:55:26 -0400 Subject: [PATCH 07/14] Add pypy3 to list of tested environments (#2105) --- .travis.yml | 3 +++ setup.py | 2 ++ tox.ini | 2 +- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 97c578aaf..1d569a76c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,6 +16,9 @@ matrix: env: TOXENV=py37 dist: xenial sudo: true + - python: pypy3 + env: TOXENV=pypy3 + dist: xenial - python: 3.8-dev env: TOXENV=py38-dev dist: xenial diff --git a/setup.py b/setup.py index 31d173f62..8d79fb7a5 100644 --- a/setup.py +++ b/setup.py @@ -26,6 +26,8 @@ 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3 :: Only', + 'Programming Language :: Python :: Implementation :: CPython', + 'Programming Language :: Python :: Implementation :: PyPy', 'Topic :: Internet', 'Topic :: Utilities', 'Topic :: Software Development :: Libraries :: Python Modules', diff --git a/tox.ini b/tox.ini index 47249d6e6..96388fa74 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py34, py35, py36, py37, py38-dev, pypy, lint +envlist = py34, py35, py36, py37, py38-dev, pypy3, lint skipsdist = True [testenv] From 49341f1fb35fec7f0606faa1e3129174e12dd28a Mon Sep 17 00:00:00 2001 From: Tyler Lubeck <tyler@tylerlubeck.com> Date: Thu, 26 Sep 2019 14:54:54 -0700 Subject: [PATCH 08/14] Terminology changes --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1a6880bb4..ac93950dc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -141,7 +141,7 @@ The relevant maintainer for a pull request is assigned in 3 steps: * Step 2: Find the MAINTAINERS file which affects this directory. If the directory itself does not have a MAINTAINERS file, work your way up the the repo hierarchy until you find one. -* Step 3: The first maintainer listed is the primary maintainer. The pull request is assigned to him. He may assign it to other listed maintainers, at his discretion. +* Step 3: The first maintainer listed is the primary maintainer. The pull request is assigned to them. They may assign it to other listed maintainers, at their discretion. ### I'm a maintainer, should I make pull requests too? From ce03c192f4ca312dac93f52e61ccdf50e01b09d8 Mon Sep 17 00:00:00 2001 From: Benoit Chesneau <bchesneau@gmail.com> Date: Fri, 27 Sep 2019 01:45:03 +0200 Subject: [PATCH 09/14] fix formatting --- gunicorn/http/body.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gunicorn/http/body.py b/gunicorn/http/body.py index e75d72de2..afde36854 100644 --- a/gunicorn/http/body.py +++ b/gunicorn/http/body.py @@ -7,7 +7,7 @@ import sys from gunicorn.http.errors import (NoMoreData, ChunkMissingTerminator, - InvalidChunkSize) + InvalidChunkSize) class ChunkedReader(object): @@ -187,6 +187,7 @@ def __next__(self): if not ret: raise StopIteration() return ret + next = __next__ def getsize(self, size): From e6a88dbfcd78052a2f03e741d9e732ecb6c17e22 Mon Sep 17 00:00:00 2001 From: Benoit Chesneau <bchesneau@gmail.com> Date: Fri, 27 Sep 2019 01:47:03 +0200 Subject: [PATCH 10/14] bump to 20.0.0 --- gunicorn/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gunicorn/__init__.py b/gunicorn/__init__.py index 78204797b..7b38ab044 100644 --- a/gunicorn/__init__.py +++ b/gunicorn/__init__.py @@ -3,6 +3,6 @@ # This file is part of gunicorn released under the MIT license. # See the NOTICE for more information. -version_info = (19, 9, 0) +version_info = (20, 0, 0) __version__ = ".".join([str(v) for v in version_info]) SERVER_SOFTWARE = "gunicorn/%s" % __version__ From c6bb90ca827e1566158a00ab26e6cfd86fdedb00 Mon Sep 17 00:00:00 2001 From: Tyler Lubeck <tyler@tylerlubeck.com> Date: Fri, 27 Sep 2019 11:09:45 -0700 Subject: [PATCH 11/14] Update CONTRIBUTING.md --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ac93950dc..7bd82abda 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -141,7 +141,7 @@ The relevant maintainer for a pull request is assigned in 3 steps: * Step 2: Find the MAINTAINERS file which affects this directory. If the directory itself does not have a MAINTAINERS file, work your way up the the repo hierarchy until you find one. -* Step 3: The first maintainer listed is the primary maintainer. The pull request is assigned to them. They may assign it to other listed maintainers, at their discretion. +* Step 3: The first maintainer listed is the primary maintainer who is assigned the Pull Request. The primary maintainer can reassign a Pull Request to other listed maintainers. ### I'm a maintainer, should I make pull requests too? From e147feaf8b12267ff9bb3c06ad45a2738a4027df Mon Sep 17 00:00:00 2001 From: Benoit Chesneau <bchesneau@gmail.com> Date: Fri, 27 Sep 2019 23:15:59 +0200 Subject: [PATCH 12/14] fix echo example on python 3.7 --- examples/echo.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/examples/echo.py b/examples/echo.py index 06f616020..e10332d87 100644 --- a/examples/echo.py +++ b/examples/echo.py @@ -5,12 +5,9 @@ # # Example code from Eventlet sources -from wsgiref.validate import validator - from gunicorn import __version__ -@validator def app(environ, start_response): """Simplest possible application object""" From 54c820feb3f8a7c75d35769504de19a3fdcf04cc Mon Sep 17 00:00:00 2001 From: Jeff Brooks <jeph.brooks@gmail.com> Date: Thu, 10 Oct 2019 10:41:22 -0500 Subject: [PATCH 13/14] Ensure header value is string before conducting regex search on it. --- gunicorn/http/wsgi.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/gunicorn/http/wsgi.py b/gunicorn/http/wsgi.py index 32e7a2acc..b786bc095 100644 --- a/gunicorn/http/wsgi.py +++ b/gunicorn/http/wsgi.py @@ -253,10 +253,12 @@ def process_headers(self, headers): if HEADER_RE.search(name): raise InvalidHeaderName('%r' % name) + value = str(value) + if HEADER_VALUE_RE.search(value): raise InvalidHeader('%r' % value) - value = str(value).strip() + value = value.strip() lname = name.lower().strip() if lname == "content-length": self.response_length = int(value) From ad6ed3f4c835eb6a86ba61dadfd3896ddcbb48e3 Mon Sep 17 00:00:00 2001 From: Jeff Brooks <jeph.brooks@gmail.com> Date: Tue, 15 Oct 2019 09:03:44 -0500 Subject: [PATCH 14/14] Implement check and exception for str type on value in Response process_headers method. --- gunicorn/http/wsgi.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gunicorn/http/wsgi.py b/gunicorn/http/wsgi.py index b786bc095..3524471fc 100644 --- a/gunicorn/http/wsgi.py +++ b/gunicorn/http/wsgi.py @@ -253,7 +253,8 @@ def process_headers(self, headers): if HEADER_RE.search(name): raise InvalidHeaderName('%r' % name) - value = str(value) + if not isinstance(value, str): + raise TypeError('%r is not a string' % value) if HEADER_VALUE_RE.search(value): raise InvalidHeader('%r' % value)