From 6f6107d923c06b4cda7a0542857e23e7b19dd5f5 Mon Sep 17 00:00:00 2001 From: "Mads R. Havmand" Date: Fri, 5 Apr 2024 10:21:24 +0200 Subject: [PATCH 1/3] tidied up #1815 --- tests/server.crt | 33 ++++++++++++++++++++++++++++ tests/server.key | 52 ++++++++++++++++++++++++++++++++++++++++++++ tests/test_config.py | 30 +++++++++++++++++++++++++ uvicorn/config.py | 38 ++++++++++++++++++++++++++++++-- uvicorn/main.py | 2 ++ 5 files changed, 153 insertions(+), 2 deletions(-) create mode 100644 tests/server.crt create mode 100644 tests/server.key diff --git a/tests/server.crt b/tests/server.crt new file mode 100644 index 000000000..6fa0990aa --- /dev/null +++ b/tests/server.crt @@ -0,0 +1,33 @@ +-----BEGIN CERTIFICATE----- +MIIFozCCA4ugAwIBAgIUYFGwS/vDAQNfUry6qiNhr7c6xvYwDQYJKoZIhvcNAQEL +BQAwYTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEaMBgGA1UEAwwRdXZpY29ybi5sb2Nh +bGhvc3QwHhcNMjQwNDA1MDc1MjU0WhcNMzQwNDAzMDc1MjU0WjBhMQswCQYDVQQG +EwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lk +Z2l0cyBQdHkgTHRkMRowGAYDVQQDDBF1dmljb3JuLmxvY2FsaG9zdDCCAiIwDQYJ +KoZIhvcNAQEBBQADggIPADCCAgoCggIBAMxgwPo6zf6NeB3UFieLSTsZ0h2HB8Nx +YlV4Mh4fhlk6YXr5xNuFCgV5Q4ZVGA5tjlphaTxSfQLpH+Wo6LX1Xj6xoPGrGtCp +rfW6VaL/AByVSXKFPsyW32BjsXgLmIg63eh9lLBjKpH/M8/YfR4xqFDnassYyRxw +OJ+OFLmKkvG8QNN4Z14sqYL5gg7bnAeCEj+kZowuaW5yCCbArwii+eGTS0P5Mufh +xD/TlclWnNA1EO1NVSbimj3FkZ3vqqRK6bUu0HEPoaoLOkdsY4JFbFMGl3x+dc9u +LJuBNFnnGHQgHwxrughAf2+tvSnOdofWUFI/1LaNdUo+qN6rATdbbyAYLfn7aR/f +PwrTmNBG8GYd3FVQEw6hfRNGr4pHo6awC7izb7DAssxDkwetxuhXXO2fB5N04UYF +ZlQAxrDCORlo9atUexZMTOvQIUHaHFeIj+HasTBs+D0gEQRgvQbA0+orpyF1yczV +7laMvY/41BOvLoPaY8GQ33WrDFxuO2tRV7RF4+5rozC951nP78dU0cG5VXS2UqIj +hz2A3iYEjSc52uKMdEdlfdPkDYI8v4qivnv7J893i3rcAWeRqzkXnHRwPfobCtG/ +Nnbgo9fwuqbIVLBNAw02JCgSYbLLRxgcMwV9t8Jj5Xd06v35lL1gunsKg6aPRH7g +5hYecI271akrAgMBAAGjUzBRMB0GA1UdDgQWBBQzMQrX2PX397oBZwfl98mw1fmd +MTAfBgNVHSMEGDAWgBQzMQrX2PX397oBZwfl98mw1fmdMTAPBgNVHRMBAf8EBTAD +AQH/MA0GCSqGSIb3DQEBCwUAA4ICAQAf36TLtdQOChLeQok5Vr9x2woyxYROr3XA +V0GO/ZLxTJsRGXwb5GP/3vr1g0gmsEFWi4lYhlxmOPY12zQShf772IncgS8o2TYg +NMoEqBRlTlWVXbyHZGXcKEdQ6Vlt/yvscJ8c30AYPIJRDzsI8W8BehKMxevnSSKh ++tdViEVU1T0bck1pgLyc2gkV64ZlnjPFFT5s65TGTp+DGzsDkExMPwQ3GE0mzWEU +dKLjAHK0VHqmanXmrhWIG2/oKVQybHqP7Df+8HFz54G4ubxdixR8LU6dV630xoAt +/s6+BvdpplQoWS+WZhgFoOcpr36DoUNboZn0c0QGGKP/aqf6RwQFCyDurmkasXRi +6JGMcMt0NFDMykTkuY6UkDcOIINhUqfrNkF7qwuOwfgWB7xB2d4i/9YLofxZo3wK +Hc317ATQRgKzI/1igPaW7bk6pJxhpYCsvgWyi0MSFctvN+R3J+AKJiQAg9RmvFRJ +Xet4mnj+aEDDp7k8KemHxHN9bCIkeHU2ZvvVPS5PzSkPbK/JyfcMpraXfOAHdKVv +kRLll+7G4itsu45MzQnh6qq5bR7b18Qgee3o8iKfZKCzct7HEUU91H89jfCCl62L +V6L2pS4YXe8N3Bdjc5aSy3lAw6RiDzR3eoQaLzqLd3RyAEp7/UmptwWhaHOUNma+ +IeY7UK1ylg== +-----END CERTIFICATE----- diff --git a/tests/server.key b/tests/server.key new file mode 100644 index 000000000..ecccb22e4 --- /dev/null +++ b/tests/server.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +n+nYHLy1vGIssY5ODapJLixk/nt/Jg== +-----END PRIVATE KEY----- diff --git a/tests/test_config.py b/tests/test_config.py index ca305f6c2..bb5db201c 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -4,6 +4,7 @@ import logging import os import socket +import ssl import sys import typing from pathlib import Path @@ -279,6 +280,35 @@ def test_ssl_config( assert config.is_ssl is True +def ssl_context(): + context = ssl.SSLContext(int(ssl.PROTOCOL_TLS_SERVER)) + allowed_ciphers = "DEFAULT:!aNULL:!eNULL:!MD5:!3DES:!DES:!RC4:!IDEA:!SEED:!aDSS:!SRP:!PSK" + context.set_ciphers(allowed_ciphers) + list_options = [ssl.OP_NO_RENEGOTIATION] + for each_option in list_options: + context.options |= each_option + return context + + +def test_ssl_context() -> None: + config = Config(app=asgi_app, ssl_context=ssl_context) + config.load() + if config.ssl_context is not None: + assert ssl.PROTOCOL_TLS_SERVER is config.ssl_version + assert "TLSv1" in config.ssl_ciphers + if config.ssl is not None: + assert ssl.OP_NO_RENEGOTIATION in config.ssl.options + + +def test_ssl_context_load_cert() -> None: + config = Config( + app=asgi_app, ssl_context=ssl_context, ssl_certfile="tests/server.crt", ssl_keyfile="tests/server.key" + ) + config.load() + assert config.ssl_context is not None + ctx = ssl_context() + + def test_ssl_config_combined(tls_certificate_key_and_chain_path: str) -> None: config = Config( app=asgi_app, diff --git a/uvicorn/config.py b/uvicorn/config.py index 3cad1d90f..976a801f2 100644 --- a/uvicorn/config.py +++ b/uvicorn/config.py @@ -98,6 +98,27 @@ logger = logging.getLogger("uvicorn.error") +def update_ssl_context( + ctx: ssl.SSLContext, + certfile: str | os.PathLike[str] | None, + keyfile: str | os.PathLike[str] | None, + password: str | None, + cert_reqs: int, + ca_certs: str | os.PathLike[str] | None, + ciphers: str | None, +) -> ssl.SSLContext: + get_password = (lambda: password) if password else None + if certfile and keyfile: + ctx.load_cert_chain(certfile, keyfile, get_password) + if cert_reqs: + ctx.verify_mode = ssl.VerifyMode(cert_reqs) + if ca_certs: + ctx.load_verify_locations(ca_certs) + if ciphers: + ctx.set_ciphers(ciphers) + return ctx + + def create_ssl_context( certfile: str | os.PathLike[str], keyfile: str | os.PathLike[str] | None, @@ -219,6 +240,7 @@ def __init__( ssl_cert_reqs: int = ssl.CERT_NONE, ssl_ca_certs: str | None = None, ssl_ciphers: str = "TLSv1", + ssl_context: Callable[[], Any] | None = None, headers: list[tuple[str, str]] | None = None, factory: bool = False, h11_max_incomplete_event_size: int | None = None, @@ -263,6 +285,7 @@ def __init__( self.ssl_cert_reqs = ssl_cert_reqs self.ssl_ca_certs = ssl_ca_certs self.ssl_ciphers = ssl_ciphers + self.ssl_context = ssl_context self.headers: list[tuple[str, str]] = headers or [] self.encoded_headers: list[tuple[bytes, bytes]] = [] self.factory = factory @@ -394,9 +417,19 @@ def configure_logging(self) -> None: def load(self) -> None: assert not self.loaded - if self.is_ssl: + if self.ssl_context: + self.ssl: ssl.SSLContext | None = update_ssl_context( + self.ssl_context(), + keyfile=self.ssl_keyfile, + certfile=self.ssl_certfile, + password=self.ssl_keyfile_password, + cert_reqs=self.ssl_cert_reqs, + ca_certs=self.ssl_ca_certs, + ciphers=self.ssl_ciphers, + ) + elif self.is_ssl and not self.ssl_context: assert self.ssl_certfile - self.ssl: ssl.SSLContext | None = create_ssl_context( + self.ssl = create_ssl_context( keyfile=self.ssl_keyfile, certfile=self.ssl_certfile, password=self.ssl_keyfile_password, @@ -405,6 +438,7 @@ def load(self) -> None: ca_certs=self.ssl_ca_certs, ciphers=self.ssl_ciphers, ) + else: self.ssl = None diff --git a/uvicorn/main.py b/uvicorn/main.py index ace2b70d7..2e7a918df 100644 --- a/uvicorn/main.py +++ b/uvicorn/main.py @@ -501,6 +501,7 @@ def run( ssl_cert_reqs: int = ssl.CERT_NONE, ssl_ca_certs: str | None = None, ssl_ciphers: str = "TLSv1", + ssl_context: Callable[[], Any] | None = None, headers: list[tuple[str, str]] | None = None, use_colors: bool | None = None, app_dir: str | None = None, @@ -553,6 +554,7 @@ def run( ssl_cert_reqs=ssl_cert_reqs, ssl_ca_certs=ssl_ca_certs, ssl_ciphers=ssl_ciphers, + ssl_context=ssl_context, headers=headers, use_colors=use_colors, factory=factory, From fd67ddd3b5a8382e39d4d3f71b8c8b53be2dceb0 Mon Sep 17 00:00:00 2001 From: "Mads R. Havmand" Date: Fri, 5 Apr 2024 10:46:23 +0200 Subject: [PATCH 2/3] minor lint fix --- tests/test_config.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_config.py b/tests/test_config.py index bb5db201c..090a3284a 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -306,7 +306,6 @@ def test_ssl_context_load_cert() -> None: ) config.load() assert config.ssl_context is not None - ctx = ssl_context() def test_ssl_config_combined(tls_certificate_key_and_chain_path: str) -> None: From 80b8336d0b54e10aad34f16552cf1423a5d51e02 Mon Sep 17 00:00:00 2001 From: "Mads R. Havmand" Date: Mon, 15 Apr 2024 23:10:29 +0200 Subject: [PATCH 3/3] added test for OSError in is_dir function --- tests/test_config.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/tests/test_config.py b/tests/test_config.py index 44e6d76c5..463a0026a 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -26,7 +26,7 @@ Scope, StartResponse, ) -from uvicorn.config import Config +from uvicorn.config import Config, is_dir from uvicorn.middleware.proxy_headers import ProxyHeadersMiddleware from uvicorn.middleware.wsgi import WSGIMiddleware from uvicorn.protocols.http.h11_impl import H11Protocol @@ -268,6 +268,19 @@ def test_socket_bind() -> None: sock.close() +def test_is_dir() -> None: + class P: + @staticmethod + def is_absolute(): + return True + + @staticmethod + def is_dir(): + raise OSError + + assert not is_dir(path=P) # type: ignore + + def test_ssl_config( tls_ca_certificate_pem_path: str, tls_ca_certificate_private_key_path: str,