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.