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 1551cfc4..30ab6fdb 100644 --- a/tests/lib/conftest.py +++ b/tests/lib/conftest.py @@ -6,14 +6,13 @@ import pytest from tests.consts import ssl_path, tests_path -from tests.paho_test import create_server_socket, create_server_socket_ssl +from tests.paho_test import create_server_socket, create_server_socket_ssl, ssl clients_path = tests_path / "lib" / "clients" -@pytest.fixture() -def server_socket(monkeypatch): - sock, port = create_server_socket() +def _yield_server(monkeypatch, sockport): + sock, port = sockport monkeypatch.setenv("PAHO_SERVER_PORT", str(port)) try: yield sock @@ -21,14 +20,25 @@ def server_socket(monkeypatch): sock.close() +@pytest.fixture() +def server_socket(monkeypatch): + yield from _yield_server(monkeypatch, create_server_socket()) + + @pytest.fixture() def ssl_server_socket(monkeypatch): - sock, port = create_server_socket_ssl() - monkeypatch.setenv("PAHO_SERVER_PORT", str(port)) - try: - yield sock - finally: - sock.close() + if ssl is None: + pytest.skip("no ssl module") + 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: diff --git a/tests/lib/test_08_ssl_bad_cacert.py b/tests/lib/test_08_ssl_bad_cacert.py index 14d48cb5..1cb5ec8b 100644 --- a/tests/lib/test_08_ssl_bad_cacert.py +++ b/tests/lib/test_08_ssl_bad_cacert.py @@ -1,10 +1,7 @@ import paho.mqtt.client as mqtt import pytest -from tests.paho_test import ssl - -@pytest.mark.skipif(ssl is None, reason="no ssl module") def test_08_ssl_bad_cacert(): with pytest.raises(IOError): mqttc = mqtt.Client("08-ssl-bad-cacert") 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/lib/test_08_ssl_connect_cert_auth.py b/tests/lib/test_08_ssl_connect_cert_auth.py index 24e00d99..630773a0 100644 --- a/tests/lib/test_08_ssl_connect_cert_auth.py +++ b/tests/lib/test_08_ssl_connect_cert_auth.py @@ -1,10 +1,6 @@ # Test whether a client produces a correct connect and subsequent disconnect when using SSL. # Client must provide a certificate. -import pytest - -import tests.paho_test as paho_test -from tests.paho_test import ssl - +# # The client should connect with keepalive=60, clean session set, # and client id 08-ssl-connect-crt-auth # It should use the CA certificate ssl/all-ca.crt for verifying the server. @@ -12,12 +8,13 @@ # the CONNACK and verifying that rc=0, the client should send a DISCONNECT # message. If rc!=0, the client should exit with an error. +import tests.paho_test as paho_test + connect_packet = paho_test.gen_connect("08-ssl-connect-crt-auth", keepalive=60) connack_packet = paho_test.gen_connack(rc=0) disconnect_packet = paho_test.gen_disconnect() -@pytest.mark.skipif(ssl is None, reason="no ssl module") def test_08_ssl_connect_crt_auth(ssl_server_socket, start_client): start_client("08-ssl-connect-cert-auth.py") diff --git a/tests/lib/test_08_ssl_connect_cert_auth_pw.py b/tests/lib/test_08_ssl_connect_cert_auth_pw.py index 435bd6a7..333d6f75 100644 --- a/tests/lib/test_08_ssl_connect_cert_auth_pw.py +++ b/tests/lib/test_08_ssl_connect_cert_auth_pw.py @@ -1,10 +1,6 @@ # Test whether a client produces a correct connect and subsequent disconnect when using SSL. # Client must provide a certificate - the private key is encrypted with a password. -import pytest - -import tests.paho_test as paho_test -from tests.paho_test import ssl - +# # The client should connect with keepalive=60, clean session set, # and client id 08-ssl-connect-crt-auth # It should use the CA certificate ssl/all-ca.crt for verifying the server. @@ -12,12 +8,13 @@ # the CONNACK and verifying that rc=0, the client should send a DISCONNECT # message. If rc!=0, the client should exit with an error. +import tests.paho_test as paho_test + connect_packet = paho_test.gen_connect("08-ssl-connect-crt-auth-pw", keepalive=60) connack_packet = paho_test.gen_connack(rc=0) disconnect_packet = paho_test.gen_disconnect() -@pytest.mark.skipif(ssl is None, reason="no ssl module") def test_08_ssl_connect_crt_auth_pw(ssl_server_socket, start_client): start_client("08-ssl-connect-cert-auth-pw.py") diff --git a/tests/lib/test_08_ssl_connect_no_auth.py b/tests/lib/test_08_ssl_connect_no_auth.py index efe64e10..d284658e 100644 --- a/tests/lib/test_08_ssl_connect_no_auth.py +++ b/tests/lib/test_08_ssl_connect_no_auth.py @@ -5,17 +5,13 @@ # 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. -import pytest - import tests.paho_test as paho_test -from tests.paho_test import ssl connect_packet = paho_test.gen_connect("08-ssl-connect-no-auth", keepalive=60) connack_packet = paho_test.gen_connack(rc=0) disconnect_packet = paho_test.gen_disconnect() -@pytest.mark.skipif(ssl is None, reason="no ssl module") def test_08_ssl_connect_no_auth(ssl_server_socket, start_client): start_client("08-ssl-connect-no-auth.py") diff --git a/tests/lib/test_08_ssl_fake_cacert.py b/tests/lib/test_08_ssl_fake_cacert.py index 4cb42b08..09b0e3c2 100644 --- a/tests/lib/test_08_ssl_fake_cacert.py +++ b/tests/lib/test_08_ssl_fake_cacert.py @@ -3,7 +3,6 @@ from tests.paho_test import ssl -@pytest.mark.skipif(ssl is None, reason="no ssl module") def test_08_ssl_fake_cacert(ssl_server_socket, start_client): start_client("08-ssl-fake-cacert.py") with pytest.raises(ssl.SSLError): diff --git a/tests/paho_test.py b/tests/paho_test.py index 4c77fb30..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(cert_reqs=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) @@ -43,8 +43,12 @@ def create_server_socket_ssl(cert_reqs=None): str(ssl_path / "server.crt"), str(ssl_path / "server.key"), ) - if cert_reqs: - context.verify_mode = cert_reqs + 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)