diff --git a/README.rst b/README.rst index 061c9b7..d5a3f3b 100644 --- a/README.rst +++ b/README.rst @@ -106,6 +106,10 @@ Pyramid (wsgiref.simple_server) By ``wsgiref.simple_server`` you must wrap the **WSGI application** in ``liveandletdie.WsgirefSimpleServer.wrap(app)``. +If you set the ``ssl`` keyword argument to ``True``, the app will be run with +a self-signed certificate, and the schema of the ``self.check_url`` +will be ``"https"``. + .. code-block:: python # pyramid/app/main.py @@ -213,4 +217,4 @@ Or bootstrap and run tests in one step: $ sh bootstrap-and-test.sh -Enjoy! \ No newline at end of file +Enjoy! diff --git a/liveandletdie/__init__.py b/liveandletdie/__init__.py index 94d0a8d..84ad2fa 100644 --- a/liveandletdie/__init__.py +++ b/liveandletdie/__init__.py @@ -4,9 +4,12 @@ import os import re import signal +import ssl import subprocess import sys +import tempfile import time +from werkzeug.serving import make_ssl_devcert # pylint: disable=wrong-import-order try: from urllib.parse import urlsplit, splitport @@ -419,11 +422,10 @@ def wrap(cls, app): """ host, port = cls.parse_args() - ssl = cls._argument_parser.parse_args().ssl ssl_context = None if host: - if ssl: + if cls._argument_parser.parse_args().ssl: try: import OpenSSL # pylint: disable=unused-variable except ImportError: @@ -483,6 +485,41 @@ def create_command(self): class WsgirefSimpleServer(WrapperBase): + def __init__(self, *args, **kwargs): + """ + :param bool ssl: + If true, the app will be run with ssl enabled and the + scheme of the ``self.check_url`` will be ``"https"``. + """ + self.ssl = kwargs.pop('ssl', None) + super(WsgirefSimpleServer, self).__init__(*args, **kwargs) + if self.ssl: + self.scheme = 'https' + + def create_command(self): + command = super(WsgirefSimpleServer, self).create_command() + if self.ssl is True: + command += ['--ssl=1'] + return command + + def check(self, check_url=None): + url = self.check_url if check_url is None else \ + self._normalize_check_url(check_url) + + if self.ssl: + url = url.replace('http://', 'https://') + + super(WsgirefSimpleServer, self).check(url) + + @classmethod + def _add_args(cls): + super(WsgirefSimpleServer, cls)._add_args() + cls._argument_parser.add_argument('--ssl', + help='Run with ssl enabled.', + type=bool, + nargs='?', + default=False) + @classmethod def wrap(cls, app): host, port = cls.parse_args() @@ -490,6 +527,24 @@ def wrap(cls, app): from wsgiref.simple_server import make_server server = make_server(host, port, app) + if cls._argument_parser.parse_args().ssl: + # Set HTTPS='1' makes wsgiref set wsgi.url_scheme='https' + # This in turn makes pyramid set request.scheme='https' + server.base_environ['HTTPS'] = '1' + + with tempfile.TemporaryDirectory() as td: + # Generate temporary self-signed cert/key pair + # using the library used by Flask for 'adhoc' ssl_context + certpath = '{}/liveandletdie'.format(td) + make_ssl_devcert(certpath) + + server.socket = ssl.wrap_socket( + server.socket, + server_side=True, + certfile='{}.crt'.format(certpath), + keyfile='{}.key'.format(certpath), + ) + server.serve_forever() server.server_close() sys.exit() diff --git a/sample_apps/pyramid/main.py b/sample_apps/pyramid/main.py index 0b3fe22..a878cad 100644 --- a/sample_apps/pyramid/main.py +++ b/sample_apps/pyramid/main.py @@ -5,7 +5,11 @@ def home(request): - return Response('Home Pyramid') + content = 'Home Pyramid' + if request.scheme == 'https': + content += ' SSL' + + return Response(content) if __name__ == '__main__': @@ -22,4 +26,4 @@ def home(request): server = make_server('127.0.0.1', 8080, app) - server.serve_forever() \ No newline at end of file + server.serve_forever() diff --git a/test_examples/pytest_example/tests.py b/test_examples/pytest_example/tests.py index c1170a4..f9f9762 100644 --- a/test_examples/pytest_example/tests.py +++ b/test_examples/pytest_example/tests.py @@ -21,6 +21,11 @@ def abspath(pth): abspath('sample_apps/pyramid/main.py'), port=PORT ), + 'Pyramid SSL': liveandletdie.WsgirefSimpleServer( + abspath('sample_apps/pyramid/main.py'), + port=PORT, + ssl=True + ), 'Flask': liveandletdie.Flask( abspath('sample_apps/flask/main.py'), port=PORT diff --git a/test_examples/unittest_example/tests.py b/test_examples/unittest_example/tests.py index e0f501a..87dca41 100644 --- a/test_examples/unittest_example/tests.py +++ b/test_examples/unittest_example/tests.py @@ -62,6 +62,13 @@ class TestPyramid(unittest.TestCase): app = liveandletdie.WsgirefSimpleServer(abspath('sample_apps/pyramid/main.py'), port=PORT) +@test_decorator +class TestPyramidSSL(unittest.TestCase): + EXPECTED_TEXT = 'Home Pyramid SSL' + app = liveandletdie.WsgirefSimpleServer(abspath('sample_apps/pyramid/main.py'), port=PORT, + ssl=True) + + @test_decorator class TestDjango(unittest.TestCase): EXPECTED_TEXT = 'Home Django' diff --git a/tests/test_all.py b/tests/test_all.py index 6276bf5..925eb31 100644 --- a/tests/test_all.py +++ b/tests/test_all.py @@ -86,3 +86,7 @@ def test__normalize_check_url(self): class TestFlask(SSLBase): app_path = os.path.join(SAMPLE_APPS_DIR, 'flask', 'main.py') class_ = liveandletdie.Flask + +class TestPyramid(SSLBase): + app_path = os.path.join(SAMPLE_APPS_DIR, 'pyramid', 'main.py') + class_ = liveandletdie.Pyramid diff --git a/tox.ini b/tox.ini index e498ff5..7765544 100644 --- a/tox.ini +++ b/tox.ini @@ -10,6 +10,7 @@ deps= pytest pyopenssl requests + werkzeug setenv = PYTHONPATH={toxinidir} PYTHONWARNINGS=ignore