Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/paho/mqtt/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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:
Expand Down
23 changes: 23 additions & 0 deletions tests/lib/clients/08-ssl-connect-alpn.py
Original file line number Diff line number Diff line change
@@ -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)
30 changes: 20 additions & 10 deletions tests/lib/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,29 +6,39 @@
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
finally:
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:
Expand Down
3 changes: 0 additions & 3 deletions tests/lib/test_08_ssl_bad_cacert.py
Original file line number Diff line number Diff line change
@@ -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")
Expand Down
38 changes: 38 additions & 0 deletions tests/lib/test_08_ssl_connect_alpn.py
Original file line number Diff line number Diff line change
@@ -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()
9 changes: 3 additions & 6 deletions tests/lib/test_08_ssl_connect_cert_auth.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,20 @@
# 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.
# 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 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")

Expand Down
9 changes: 3 additions & 6 deletions tests/lib/test_08_ssl_connect_cert_auth_pw.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,20 @@
# 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.
# 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 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")

Expand Down
4 changes: 0 additions & 4 deletions tests/lib/test_08_ssl_connect_no_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down
1 change: 0 additions & 1 deletion tests/lib/test_08_ssl_fake_cacert.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
10 changes: 7 additions & 3 deletions tests/paho_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down