Skip to content

Commit 2ba26d1

Browse files
authored
Merge pull request #790 from akx/alpn-2
Add ALPN support
2 parents e9f3815 + 704621c commit 2ba26d1

10 files changed

Lines changed: 100 additions & 33 deletions

src/paho/mqtt/client.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -866,6 +866,7 @@ def tls_set(
866866
tls_version: int | None = None,
867867
ciphers: str | None = None,
868868
keyfile_password: str | None = None,
869+
alpn_protocols: list[str] | None = None,
869870
) -> None:
870871
"""Configure network encryption and authentication options. Enables SSL/TLS support.
871872
@@ -945,6 +946,11 @@ def tls_set(
945946
else:
946947
context.load_default_certs()
947948

949+
if alpn_protocols is not None:
950+
if not getattr(ssl, "HAS_ALPN", None):
951+
raise ValueError("SSL library has no support for ALPN")
952+
context.set_alpn_protocols(alpn_protocols)
953+
948954
self.tls_set_context(context)
949955

950956
if cert_reqs != ssl.CERT_NONE:
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import os
2+
3+
import paho.mqtt.client as mqtt
4+
5+
from tests.paho_test import get_test_server_port, loop_until_keyboard_interrupt
6+
7+
8+
def on_connect(mqttc, obj, flags, rc):
9+
assert rc == 0, f"Connect failed ({rc})"
10+
mqttc.disconnect()
11+
12+
13+
mqttc = mqtt.Client("08-ssl-connect-alpn", clean_session=True)
14+
mqttc.tls_set(
15+
os.path.join(os.environ["PAHO_SSL_PATH"], "all-ca.crt"),
16+
os.path.join(os.environ["PAHO_SSL_PATH"], "client.crt"),
17+
os.path.join(os.environ["PAHO_SSL_PATH"], "client.key"),
18+
alpn_protocols=["paho-test-protocol"],
19+
)
20+
mqttc.on_connect = on_connect
21+
22+
mqttc.connect("localhost", get_test_server_port())
23+
loop_until_keyboard_interrupt(mqttc)

tests/lib/conftest.py

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,29 +6,39 @@
66
import pytest
77

88
from tests.consts import ssl_path, tests_path
9-
from tests.paho_test import create_server_socket, create_server_socket_ssl
9+
from tests.paho_test import create_server_socket, create_server_socket_ssl, ssl
1010

1111
clients_path = tests_path / "lib" / "clients"
1212

1313

14-
@pytest.fixture()
15-
def server_socket(monkeypatch):
16-
sock, port = create_server_socket()
14+
def _yield_server(monkeypatch, sockport):
15+
sock, port = sockport
1716
monkeypatch.setenv("PAHO_SERVER_PORT", str(port))
1817
try:
1918
yield sock
2019
finally:
2120
sock.close()
2221

2322

23+
@pytest.fixture()
24+
def server_socket(monkeypatch):
25+
yield from _yield_server(monkeypatch, create_server_socket())
26+
27+
2428
@pytest.fixture()
2529
def ssl_server_socket(monkeypatch):
26-
sock, port = create_server_socket_ssl()
27-
monkeypatch.setenv("PAHO_SERVER_PORT", str(port))
28-
try:
29-
yield sock
30-
finally:
31-
sock.close()
30+
if ssl is None:
31+
pytest.skip("no ssl module")
32+
yield from _yield_server(monkeypatch, create_server_socket_ssl())
33+
34+
35+
@pytest.fixture()
36+
def alpn_ssl_server_socket(monkeypatch):
37+
if ssl is None:
38+
pytest.skip("no ssl module")
39+
if not getattr(ssl, "HAS_ALPN", False):
40+
pytest.skip("ALPN not supported in this version of Python")
41+
yield from _yield_server(monkeypatch, create_server_socket_ssl(alpn_protocols=["paho-test-protocol"]))
3242

3343

3444
def stop_process(proc: subprocess.Popen) -> None:

tests/lib/test_08_ssl_bad_cacert.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
import paho.mqtt.client as mqtt
22
import pytest
33

4-
from tests.paho_test import ssl
54

6-
7-
@pytest.mark.skipif(ssl is None, reason="no ssl module")
85
def test_08_ssl_bad_cacert():
96
with pytest.raises(IOError):
107
mqttc = mqtt.Client("08-ssl-bad-cacert")
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Test whether a client produces a correct connect and subsequent disconnect when using SSL.
2+
# Client must provide a certificate.
3+
#
4+
# The client should connect with keepalive=60, clean session set,
5+
# and client id 08-ssl-connect-alpn
6+
# It should use the CA certificate ssl/all-ca.crt for verifying the server.
7+
# The test will send a CONNACK message to the client with rc=0. Upon receiving
8+
# the CONNACK and verifying that rc=0, the client should send a DISCONNECT
9+
# message. If rc!=0, the client should exit with an error.
10+
#
11+
# Additionally, the secure socket must have been negotiated with the "paho-test-protocol"
12+
13+
14+
from tests import paho_test
15+
from tests.paho_test import ssl
16+
17+
18+
def test_08_ssl_connect_alpn(alpn_ssl_server_socket, start_client):
19+
connect_packet = paho_test.gen_connect("08-ssl-connect-alpn", keepalive=60)
20+
connack_packet = paho_test.gen_connack(rc=0)
21+
disconnect_packet = paho_test.gen_disconnect()
22+
23+
start_client("08-ssl-connect-alpn.py")
24+
25+
(conn, address) = alpn_ssl_server_socket.accept()
26+
conn.settimeout(10)
27+
28+
paho_test.expect_packet(conn, "connect", connect_packet)
29+
conn.send(connack_packet)
30+
31+
paho_test.expect_packet(conn, "disconnect", disconnect_packet)
32+
33+
if ssl.HAS_ALPN:
34+
negotiated_protocol = conn.selected_alpn_protocol()
35+
if negotiated_protocol != "paho-test-protocol":
36+
raise Exception(f"Unexpected protocol '{negotiated_protocol}'")
37+
38+
conn.close()

tests/lib/test_08_ssl_connect_cert_auth.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,20 @@
11
# Test whether a client produces a correct connect and subsequent disconnect when using SSL.
22
# Client must provide a certificate.
3-
import pytest
4-
5-
import tests.paho_test as paho_test
6-
from tests.paho_test import ssl
7-
3+
#
84
# The client should connect with keepalive=60, clean session set,
95
# and client id 08-ssl-connect-crt-auth
106
# It should use the CA certificate ssl/all-ca.crt for verifying the server.
117
# The test will send a CONNACK message to the client with rc=0. Upon receiving
128
# the CONNACK and verifying that rc=0, the client should send a DISCONNECT
139
# message. If rc!=0, the client should exit with an error.
1410

11+
import tests.paho_test as paho_test
12+
1513
connect_packet = paho_test.gen_connect("08-ssl-connect-crt-auth", keepalive=60)
1614
connack_packet = paho_test.gen_connack(rc=0)
1715
disconnect_packet = paho_test.gen_disconnect()
1816

1917

20-
@pytest.mark.skipif(ssl is None, reason="no ssl module")
2118
def test_08_ssl_connect_crt_auth(ssl_server_socket, start_client):
2219
start_client("08-ssl-connect-cert-auth.py")
2320

tests/lib/test_08_ssl_connect_cert_auth_pw.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,20 @@
11
# Test whether a client produces a correct connect and subsequent disconnect when using SSL.
22
# Client must provide a certificate - the private key is encrypted with a password.
3-
import pytest
4-
5-
import tests.paho_test as paho_test
6-
from tests.paho_test import ssl
7-
3+
#
84
# The client should connect with keepalive=60, clean session set,
95
# and client id 08-ssl-connect-crt-auth
106
# It should use the CA certificate ssl/all-ca.crt for verifying the server.
117
# The test will send a CONNACK message to the client with rc=0. Upon receiving
128
# the CONNACK and verifying that rc=0, the client should send a DISCONNECT
139
# message. If rc!=0, the client should exit with an error.
1410

11+
import tests.paho_test as paho_test
12+
1513
connect_packet = paho_test.gen_connect("08-ssl-connect-crt-auth-pw", keepalive=60)
1614
connack_packet = paho_test.gen_connack(rc=0)
1715
disconnect_packet = paho_test.gen_disconnect()
1816

1917

20-
@pytest.mark.skipif(ssl is None, reason="no ssl module")
2118
def test_08_ssl_connect_crt_auth_pw(ssl_server_socket, start_client):
2219
start_client("08-ssl-connect-cert-auth-pw.py")
2320

tests/lib/test_08_ssl_connect_no_auth.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,13 @@
55
# The test will send a CONNACK message to the client with rc=0. Upon receiving
66
# the CONNACK and verifying that rc=0, the client should send a DISCONNECT
77
# message. If rc!=0, the client should exit with an error.
8-
import pytest
9-
108
import tests.paho_test as paho_test
11-
from tests.paho_test import ssl
129

1310
connect_packet = paho_test.gen_connect("08-ssl-connect-no-auth", keepalive=60)
1411
connack_packet = paho_test.gen_connack(rc=0)
1512
disconnect_packet = paho_test.gen_disconnect()
1613

1714

18-
@pytest.mark.skipif(ssl is None, reason="no ssl module")
1915
def test_08_ssl_connect_no_auth(ssl_server_socket, start_client):
2016
start_client("08-ssl-connect-no-auth.py")
2117

tests/lib/test_08_ssl_fake_cacert.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
from tests.paho_test import ssl
44

55

6-
@pytest.mark.skipif(ssl is None, reason="no ssl module")
76
def test_08_ssl_fake_cacert(ssl_server_socket, start_client):
87
start_client("08-ssl-fake-cacert.py")
98
with pytest.raises(ssl.SSLError):

tests/paho_test.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def create_server_socket():
3232
return (sock, port)
3333

3434

35-
def create_server_socket_ssl(cert_reqs=None):
35+
def create_server_socket_ssl(*, verify_mode=None, alpn_protocols=None):
3636
assert ssl, "SSL not available"
3737

3838
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
@@ -43,8 +43,12 @@ def create_server_socket_ssl(cert_reqs=None):
4343
str(ssl_path / "server.crt"),
4444
str(ssl_path / "server.key"),
4545
)
46-
if cert_reqs:
47-
context.verify_mode = cert_reqs
46+
if verify_mode:
47+
context.verify_mode = verify_mode
48+
49+
if alpn_protocols is not None:
50+
context.set_alpn_protocols(alpn_protocols)
51+
4852
ssock = context.wrap_socket(sock, server_side=True)
4953
ssock.settimeout(10)
5054
port = bind_to_any_free_port(ssock)

0 commit comments

Comments
 (0)