Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-kafka-python for 
openSUSE:Factory checked in at 2026-07-15 16:43:45
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-kafka-python (Old)
 and      /work/SRC/openSUSE:Factory/.python-kafka-python.new.1991 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-kafka-python"

Wed Jul 15 16:43:45 2026 rev:16 rq:1365772 version:3.0.8

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-kafka-python/python-kafka-python.changes  
2026-07-07 21:05:51.103758828 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-kafka-python.new.1991/python-kafka-python.changes
        2026-07-15 17:01:39.815234631 +0200
@@ -1,0 +2,6 @@
+Tue Jul 14 20:16:24 UTC 2026 - Dirk Müller <[email protected]>
+
+- update to 3.0.8:
+  * transport: Fix TLS SNI when `ssl_check_hostname=False`
+
+-------------------------------------------------------------------

Old:
----
  python-kafka-python-3.0.7.tar.gz

New:
----
  python-kafka-python-3.0.8.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-kafka-python.spec ++++++
--- /var/tmp/diff_new_pack.yryZnL/_old  2026-07-15 17:01:41.575294408 +0200
+++ /var/tmp/diff_new_pack.yryZnL/_new  2026-07-15 17:01:41.579294544 +0200
@@ -23,7 +23,7 @@
 %endif
 
 Name:           python-kafka-python
-Version:        3.0.7
+Version:        3.0.8
 Release:        0
 Summary:        Pure Python client for Apache Kafka
 License:        Apache-2.0

++++++ python-kafka-python-3.0.7.tar.gz -> python-kafka-python-3.0.8.tar.gz 
++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/kafka-python-3.0.7/CHANGES.md 
new/kafka-python-3.0.8/CHANGES.md
--- old/kafka-python-3.0.7/CHANGES.md   2026-06-29 00:11:16.000000000 +0200
+++ new/kafka-python-3.0.8/CHANGES.md   2026-07-09 20:43:02.000000000 +0200
@@ -1,3 +1,8 @@
+# 3.0.8 (Jul 9, 2026)
+
+### Fixes
+* transport: Fix TLS SNI when `ssl_check_hostname=False` (#3114)
+
 # 3.0.7 (Jun 28, 2026)
 
 ### Fixes
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/kafka-python-3.0.7/docs/changelog.rst 
new/kafka-python-3.0.8/docs/changelog.rst
--- old/kafka-python-3.0.7/docs/changelog.rst   2026-06-29 00:11:16.000000000 
+0200
+++ new/kafka-python-3.0.8/docs/changelog.rst   2026-07-09 20:43:02.000000000 
+0200
@@ -1,6 +1,14 @@
 Changelog
 =========
 
+3.0.8 (Jul 9, 2026)
+###################
+
+Fixes
+-----
+* transport: Fix TLS SNI when `ssl_check_hostname=False` (#3114)
+
+
 3.0.7 (Jun 28, 2026)
 ####################
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/kafka-python-3.0.7/kafka/net/manager.py 
new/kafka-python-3.0.8/kafka/net/manager.py
--- old/kafka-python-3.0.7/kafka/net/manager.py 2026-06-29 00:11:16.000000000 
+0200
+++ new/kafka-python-3.0.8/kafka/net/manager.py 2026-07-09 20:43:02.000000000 
+0200
@@ -3,7 +3,6 @@
 import inspect
 import random
 import socket
-import ssl
 import time
 
 from .inet import create_connection
@@ -205,35 +204,16 @@
     def ssl_enabled(self):
         return self.config['security_protocol'] in ('SSL', 'SASL_SSL')
 
-    def _build_ssl_context(self):
-        if self.config['ssl_context'] is not None:
-            return self.config['ssl_context']
-        ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
-        ctx.minimum_version = ssl.TLSVersion.TLSv1_2
-        ctx.check_hostname = self.config['ssl_check_hostname']
-        if self.config['ssl_cafile']:
-            ctx.load_verify_locations(self.config['ssl_cafile'])
-        else:
-            ctx.load_default_certs()
-        if self.config['ssl_certfile']:
-            ctx.load_cert_chain(
-                certfile=self.config['ssl_certfile'],
-                keyfile=self.config['ssl_keyfile'],
-                password=self.config['ssl_password'],
-            )
-        if self.config['ssl_crlfile']:
-            ctx.load_verify_locations(crl=self.config['ssl_crlfile'])
-            ctx.verify_flags |= ssl.VERIFY_CRL_CHECK_LEAF
-        return ctx
-
     async def _build_transport(self, node, timeout_at=None):
         sock = await create_connection(self._net, node.host, node.port,
                                        self.config['socket_options'],
                                        proxy_url=self.config['proxy_url'],
                                        timeout_at=timeout_at)
         if self.ssl_enabled:
-            transport = KafkaSSLTransport(self._net, sock, 
self._build_ssl_context(),
-                                          host=node.host, 
ssl_check_hostname=self.config['ssl_check_hostname'])
+            ssl_configs = {key: value
+                           for key, value in self.config.items()
+                           if key.startswith('ssl_')}
+            transport = KafkaSSLTransport(self._net, sock, host=node.host, 
**ssl_configs)
         else:
             transport = KafkaTCPTransport(self._net, sock, host=node.host)
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/kafka-python-3.0.7/kafka/net/transport.py 
new/kafka-python-3.0.8/kafka/net/transport.py
--- old/kafka-python-3.0.7/kafka/net/transport.py       2026-06-29 
00:11:16.000000000 +0200
+++ new/kafka-python-3.0.8/kafka/net/transport.py       2026-07-09 
20:43:02.000000000 +0200
@@ -1,4 +1,5 @@
 from collections import deque
+import copy
 import logging
 import selectors
 import socket
@@ -371,13 +372,49 @@
 
 
 class KafkaSSLTransport(KafkaTCPTransport):
-    def __init__(self, net, sock, ssl_context, host=None, 
ssl_check_hostname=False):
-        self._ssl_context = ssl_context
-        server_hostname = host if ssl_check_hostname else None
-        sock = ssl_context.wrap_socket(
-            sock, server_hostname=server_hostname, 
do_handshake_on_connect=False)
+    DEFAULT_CONFIG = {
+        'ssl_context': None,
+        'ssl_check_hostname': True,
+        'ssl_cafile': None,
+        'ssl_certfile': None,
+        'ssl_keyfile': None,
+        'ssl_password': None,
+        'ssl_crlfile': None,
+    }
+    def __init__(self, net, sock, host=None, **configs):
+        self.ssl_config = copy.copy(self.DEFAULT_CONFIG)
+        for key in self.ssl_config:
+            if key in configs:
+                self.ssl_config[key] = configs[key]
+        self._ssl_context = self._build_ssl_context(self.ssl_config)
+        server_hostname = host.rstrip('.') if host is not None else None
+        sock = self._ssl_context.wrap_socket(
+            sock, server_hostname=server_hostname,
+            do_handshake_on_connect=False)
         super().__init__(net, sock, host=host)
 
+    @staticmethod
+    def _build_ssl_context(config):
+        if config['ssl_context'] is not None:
+            return config['ssl_context']
+        ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
+        ctx.minimum_version = ssl.TLSVersion.TLSv1_2
+        ctx.check_hostname = config['ssl_check_hostname']
+        if config['ssl_cafile']:
+            ctx.load_verify_locations(config['ssl_cafile'])
+        else:
+            ctx.load_default_certs()
+        if config['ssl_certfile']:
+            ctx.load_cert_chain(
+                certfile=config['ssl_certfile'],
+                keyfile=config['ssl_keyfile'],
+                password=config['ssl_password'],
+            )
+        if config['ssl_crlfile']:
+            ctx.load_verify_locations(crl=config['ssl_crlfile'])
+            ctx.verify_flags |= ssl.VERIFY_CRL_CHECK_LEAF
+        return ctx
+
     async def handshake(self):
         while True:
             try:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/kafka-python-3.0.7/kafka/version.py 
new/kafka-python-3.0.8/kafka/version.py
--- old/kafka-python-3.0.7/kafka/version.py     2026-06-29 00:11:16.000000000 
+0200
+++ new/kafka-python-3.0.8/kafka/version.py     2026-07-09 20:43:02.000000000 
+0200
@@ -1 +1 @@
-__version__ = '3.0.7'
+__version__ = '3.0.8'
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/kafka-python-3.0.7/test/net/test_transport.py 
new/kafka-python-3.0.8/test/net/test_transport.py
--- old/kafka-python-3.0.7/test/net/test_transport.py   2026-06-29 
00:11:16.000000000 +0200
+++ new/kafka-python-3.0.8/test/net/test_transport.py   2026-07-09 
20:43:02.000000000 +0200
@@ -7,7 +7,7 @@
 import kafka.errors as Errors
 from kafka.future import Future
 from kafka.net.selector import NetworkSelector, TaskState
-from kafka.net.transport import KafkaTCPTransport
+from kafka.net.transport import KafkaSSLTransport, KafkaTCPTransport
 
 
 @pytest.fixture
@@ -367,6 +367,93 @@
         assert 'closed' in s
 
 
+class TestKafkaSSLTransport:
+    """Regression tests for https://github.com/dpkp/kafka-python/issues/3113:
+    TLS SNI (server_hostname) must be sent regardless of ssl_check_hostname so
+    that SNI-routed clusters (nginx/Istio/Strimzi ingress) remain reachable 
when
+    hostname verification is disabled.
+    """
+
+    def _make_ssl_sock(self):
+        # wrap_socket returns a wrapped socket; give it the peer/name accessors
+        # that KafkaTCPTransport.__init__ pokes at via str()/repr helpers.
+        wrapped = _make_mock_sock()
+        sock = _make_mock_sock()
+        ctx = MagicMock()
+        ctx.wrap_socket.return_value = wrapped
+        return sock, ctx, wrapped
+
+    def test_sni_sent_when_check_hostname_true(self, net):
+        sock, ctx, _ = self._make_ssl_sock()
+        KafkaSSLTransport(net, sock, host='broker.example.com',
+                          ssl_context=ctx, ssl_check_hostname=True)
+        _, kwargs = ctx.wrap_socket.call_args
+        assert kwargs['server_hostname'] == 'broker.example.com'
+
+    def test_sni_sent_when_check_hostname_false(self, net):
+        # The bug: SNI used to be suppressed when verification was disabled.
+        sock, ctx, _ = self._make_ssl_sock()
+        KafkaSSLTransport(net, sock, host='broker.example.com',
+                          ssl_context=ctx, ssl_check_hostname=False)
+        _, kwargs = ctx.wrap_socket.call_args
+        assert kwargs['server_hostname'] == 'broker.example.com'
+
+    def test_sni_strips_trailing_dot(self, net):
+        # A trailing dot is a valid FQDN but illegal in the SNI extension.
+        sock, ctx, _ = self._make_ssl_sock()
+        KafkaSSLTransport(net, sock, host='broker.example.com.',
+                          ssl_context=ctx, ssl_check_hostname=False)
+        _, kwargs = ctx.wrap_socket.call_args
+        assert kwargs['server_hostname'] == 'broker.example.com'
+
+    def test_sni_none_when_host_missing(self, net):
+        sock, ctx, _ = self._make_ssl_sock()
+        KafkaSSLTransport(net, sock, host=None, ssl_context=ctx)
+        _, kwargs = ctx.wrap_socket.call_args
+        assert kwargs['server_hostname'] is None
+
+    def test_handshake_not_done_on_connect(self, net):
+        sock, ctx, _ = self._make_ssl_sock()
+        KafkaSSLTransport(net, sock, host='broker.example.com', 
ssl_context=ctx)
+        _, kwargs = ctx.wrap_socket.call_args
+        assert kwargs['do_handshake_on_connect'] is False
+
+    def test_provided_ssl_context_is_used(self, net):
+        sock, ctx, wrapped = self._make_ssl_sock()
+        t = KafkaSSLTransport(net, sock, host='broker.example.com',
+                              ssl_context=ctx)
+        assert t._ssl_context is ctx
+        assert t._sock is wrapped
+
+    def test_config_defaults_and_overrides(self, net):
+        sock, ctx, _ = self._make_ssl_sock()
+        t = KafkaSSLTransport(net, sock, host='h', ssl_context=ctx,
+                              ssl_check_hostname=False)
+        # Explicitly-passed ssl_* keys land in ssl_config...
+        assert t.ssl_config['ssl_check_hostname'] is False
+        assert t.ssl_config['ssl_context'] is ctx
+        # ...unspecified keys keep their defaults.
+        assert t.ssl_config['ssl_cafile'] is None
+        assert t.ssl_config['ssl_check_hostname'] is not None
+
+
+class TestBuildSSLContext:
+    def test_returns_provided_context(self):
+        ctx = MagicMock()
+        config = dict(KafkaSSLTransport.DEFAULT_CONFIG, ssl_context=ctx)
+        assert KafkaSSLTransport._build_ssl_context(config) is ctx
+
+    def test_check_hostname_propagates_to_context(self):
+        config = dict(KafkaSSLTransport.DEFAULT_CONFIG, 
ssl_check_hostname=False)
+        ctx = KafkaSSLTransport._build_ssl_context(config)
+        assert ctx.check_hostname is False
+
+    def test_check_hostname_true_requires_verification(self):
+        config = dict(KafkaSSLTransport.DEFAULT_CONFIG, 
ssl_check_hostname=True)
+        ctx = KafkaSSLTransport._build_ssl_context(config)
+        assert ctx.check_hostname is True
+
+
 class TestTransportWaiterCleanup:
     """Regression: a locally-initiated close()/abort() must reclaim the socket
     read/write coroutine tasks parked in the event loop.

Reply via email to