Skip to content

Commit

Permalink
Merge pull request #790 from akx/alpn-2
Browse files Browse the repository at this point in the history
Add ALPN support
  • Loading branch information
PierreF authored Jan 13, 2024
2 parents e9f3815 + 704621c commit 2ba26d1
Show file tree
Hide file tree
Showing 10 changed files with 100 additions and 33 deletions.
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

0 comments on commit 2ba26d1

Please sign in to comment.