From 704621caf8765d10935f58df6596e409a8a0641f Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Wed, 3 Jan 2024 10:26:50 +0200 Subject: [PATCH] Add ALPN support Co-authored-by: Michael Boulton Signed-off-by: Aarni Koskela --- src/paho/mqtt/client.py | 6 ++++ tests/lib/clients/08-ssl-connect-alpn.py | 23 ++++++++++++++ tests/lib/conftest.py | 9 ++++++ tests/lib/test_08_ssl_connect_alpn.py | 38 ++++++++++++++++++++++++ tests/paho_test.py | 5 +++- 5 files changed, 80 insertions(+), 1 deletion(-) create mode 100755 tests/lib/clients/08-ssl-connect-alpn.py create mode 100755 tests/lib/test_08_ssl_connect_alpn.py diff --git a/src/paho/mqtt/client.py b/src/paho/mqtt/client.py index 1e2f13cc..a0732aa1 100644 --- a/src/paho/mqtt/client.py +++ b/src/paho/mqtt/client.py @@ -866,6 +866,7 @@ def tls_set( tls_version: int | None = None, ciphers: str | None = None, keyfile_password: str | None = None, + alpn_protocols: list[str] | None = None, ) -> None: """Configure network encryption and authentication options. Enables SSL/TLS support. @@ -945,6 +946,11 @@ def tls_set( else: context.load_default_certs() + if alpn_protocols is not None: + if not getattr(ssl, "HAS_ALPN", None): + raise ValueError("SSL library has no support for ALPN") + context.set_alpn_protocols(alpn_protocols) + self.tls_set_context(context) if cert_reqs != ssl.CERT_NONE: diff --git a/tests/lib/clients/08-ssl-connect-alpn.py b/tests/lib/clients/08-ssl-connect-alpn.py new file mode 100755 index 00000000..513f2b4c --- /dev/null +++ b/tests/lib/clients/08-ssl-connect-alpn.py @@ -0,0 +1,23 @@ +import os + +import paho.mqtt.client as mqtt + +from tests.paho_test import get_test_server_port, loop_until_keyboard_interrupt + + +def on_connect(mqttc, obj, flags, rc): + assert rc == 0, f"Connect failed ({rc})" + mqttc.disconnect() + + +mqttc = mqtt.Client("08-ssl-connect-alpn", clean_session=True) +mqttc.tls_set( + os.path.join(os.environ["PAHO_SSL_PATH"], "all-ca.crt"), + os.path.join(os.environ["PAHO_SSL_PATH"], "client.crt"), + os.path.join(os.environ["PAHO_SSL_PATH"], "client.key"), + alpn_protocols=["paho-test-protocol"], +) +mqttc.on_connect = on_connect + +mqttc.connect("localhost", get_test_server_port()) +loop_until_keyboard_interrupt(mqttc) diff --git a/tests/lib/conftest.py b/tests/lib/conftest.py index 975bf3ef..30ab6fdb 100644 --- a/tests/lib/conftest.py +++ b/tests/lib/conftest.py @@ -32,6 +32,15 @@ def ssl_server_socket(monkeypatch): yield from _yield_server(monkeypatch, create_server_socket_ssl()) +@pytest.fixture() +def alpn_ssl_server_socket(monkeypatch): + if ssl is None: + pytest.skip("no ssl module") + if not getattr(ssl, "HAS_ALPN", False): + pytest.skip("ALPN not supported in this version of Python") + yield from _yield_server(monkeypatch, create_server_socket_ssl(alpn_protocols=["paho-test-protocol"])) + + def stop_process(proc: subprocess.Popen) -> None: if sys.platform == "win32": proc.send_signal(signal.CTRL_C_EVENT) diff --git a/tests/lib/test_08_ssl_connect_alpn.py b/tests/lib/test_08_ssl_connect_alpn.py new file mode 100755 index 00000000..af1ecc4f --- /dev/null +++ b/tests/lib/test_08_ssl_connect_alpn.py @@ -0,0 +1,38 @@ +# Test whether a client produces a correct connect and subsequent disconnect when using SSL. +# Client must provide a certificate. +# +# The client should connect with keepalive=60, clean session set, +# and client id 08-ssl-connect-alpn +# It should use the CA certificate ssl/all-ca.crt for verifying the server. +# The test will send a CONNACK message to the client with rc=0. Upon receiving +# the CONNACK and verifying that rc=0, the client should send a DISCONNECT +# message. If rc!=0, the client should exit with an error. +# +# Additionally, the secure socket must have been negotiated with the "paho-test-protocol" + + +from tests import paho_test +from tests.paho_test import ssl + + +def test_08_ssl_connect_alpn(alpn_ssl_server_socket, start_client): + connect_packet = paho_test.gen_connect("08-ssl-connect-alpn", keepalive=60) + connack_packet = paho_test.gen_connack(rc=0) + disconnect_packet = paho_test.gen_disconnect() + + start_client("08-ssl-connect-alpn.py") + + (conn, address) = alpn_ssl_server_socket.accept() + conn.settimeout(10) + + paho_test.expect_packet(conn, "connect", connect_packet) + conn.send(connack_packet) + + paho_test.expect_packet(conn, "disconnect", disconnect_packet) + + if ssl.HAS_ALPN: + negotiated_protocol = conn.selected_alpn_protocol() + if negotiated_protocol != "paho-test-protocol": + raise Exception(f"Unexpected protocol '{negotiated_protocol}'") + + conn.close() diff --git a/tests/paho_test.py b/tests/paho_test.py index f0282c32..9274fed6 100644 --- a/tests/paho_test.py +++ b/tests/paho_test.py @@ -32,7 +32,7 @@ def create_server_socket(): return (sock, port) -def create_server_socket_ssl(*, verify_mode=None): +def create_server_socket_ssl(*, verify_mode=None, alpn_protocols=None): assert ssl, "SSL not available" sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) @@ -46,6 +46,9 @@ def create_server_socket_ssl(*, verify_mode=None): if verify_mode: context.verify_mode = verify_mode + if alpn_protocols is not None: + context.set_alpn_protocols(alpn_protocols) + ssock = context.wrap_socket(sock, server_side=True) ssock.settimeout(10) port = bind_to_any_free_port(ssock)