Skip to content

Commit a58ff32

Browse files
authored
Merge branch 'master' into fix-pool-repr
2 parents 5c4177f + 36619a5 commit a58ff32

12 files changed

+179
-28
lines changed

CHANGES

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
* Close Unix sockets if the connection attempt fails. This prevents `ResourceWarning`s. (#3314)
7272
* Close SSL sockets if the connection attempt fails, or if validations fail. (#3317)
7373
* Eliminate mutable default arguments in the `redis.commands.core.Script` class. (#3332)
74+
* Fix SSL verification with `ssl_cert_reqs="none"` and `ssl_check_hostname=True` by automatically setting `check_hostname=False` when `verify_mode=ssl.CERT_NONE` (#3635)
7475
* Allow newer versions of PyJWT as dependency. (#3630)
7576

7677
* 4.1.3 (Feb 8, 2022)

README.md

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,17 @@ The Python interface to the Redis key-value store.
3131

3232
## Installation
3333

34-
Start a redis via docker:
34+
Start a redis via docker (for Redis versions >= 8.0):
3535

3636
``` bash
37-
docker run -p 6379:6379 -it redis/redis-stack:latest
37+
docker run -p 6379:6379 -it redis:latest
3838
```
3939

40+
Start a redis via docker (for Redis versions < 8.0):
41+
42+
``` bash
43+
docker run -p 6379:6379 -it redis/redis-stack:latest
44+
4045
To install redis-py, simply:
4146

4247
``` bash
@@ -54,15 +59,16 @@ Looking for a high-level library to handle object mapping? See [redis-om-python]
5459

5560
## Supported Redis Versions
5661

57-
The most recent version of this library supports redis version [5.0](https://github.com/redis/redis/blob/5.0/00-RELEASENOTES), [6.0](https://github.com/redis/redis/blob/6.0/00-RELEASENOTES), [6.2](https://github.com/redis/redis/blob/6.2/00-RELEASENOTES), [7.0](https://github.com/redis/redis/blob/7.0/00-RELEASENOTES), [7.2](https://github.com/redis/redis/blob/7.2/00-RELEASENOTES) and [7.4](https://github.com/redis/redis/blob/7.4/00-RELEASENOTES).
62+
The most recent version of this library supports Redis version [7.2](https://github.com/redis/redis/blob/7.2/00-RELEASENOTES), [7.4](https://github.com/redis/redis/blob/7.4/00-RELEASENOTES) and [8.0](https://github.com/redis/redis/blob/8.0/00-RELEASENOTES).
5863

5964
The table below highlights version compatibility of the most-recent library versions and redis versions.
6065

6166
| Library version | Supported redis versions |
6267
|-----------------|-------------------|
6368
| 3.5.3 | <= 6.2 Family of releases |
6469
| >= 4.5.0 | Version 5.0 to 7.0 |
65-
| >= 5.0.0 | Version 5.0 to current |
70+
| >= 5.0.0 | Version 5.0 to 7.4 |
71+
| >= 6.0.0 | Version 7.2 to current |
6672

6773

6874
## Usage
@@ -152,8 +158,42 @@ The following example shows how to utilize [Redis Pub/Sub](https://redis.io/docs
152158
{'pattern': None, 'type': 'subscribe', 'channel': b'my-second-channel', 'data': 1}
153159
```
154160

161+
### Redis’ search and query capabilities default dialect
162+
163+
Release 6.0.0 introduces a client-side default dialect for Redis’ search and query capabilities.
164+
By default, the client now overrides the server-side dialect with version 2, automatically appending *DIALECT 2* to commands like *FT.AGGREGATE* and *FT.SEARCH*.
155165

156-
--------------------------
166+
**Important**: Be aware that the query dialect may impact the results returned. If needed, you can revert to a different dialect version by configuring the client accordingly.
167+
168+
``` python
169+
>>> from redis.commands.search.field import TextField
170+
>>> from redis.commands.search.query import Query
171+
>>> from redis.commands.search.index_definition import IndexDefinition
172+
>>> import redis
173+
174+
>>> r = redis.Redis(host='localhost', port=6379, db=0)
175+
>>> r.ft().create_index(
176+
>>> (TextField("name"), TextField("lastname")),
177+
>>> definition=IndexDefinition(prefix=["test:"]),
178+
>>> )
179+
180+
>>> r.hset("test:1", "name", "James")
181+
>>> r.hset("test:1", "lastname", "Brown")
182+
183+
>>> # Query with default DIALECT 2
184+
>>> query = "@name: James Brown"
185+
>>> q = Query(query)
186+
>>> res = r.ft().search(q)
187+
188+
>>> # Query with explicit DIALECT 1
189+
>>> query = "@name: James Brown"
190+
>>> q = Query(query).dialect(1)
191+
>>> res = r.ft().search(q)
192+
```
193+
194+
You can find further details in the [query dialect documentation](https://redis.io/docs/latest/develop/interact/search-and-query/advanced-concepts/dialects/).
195+
196+
---------------------------------------------
157197

158198
### Author
159199

@@ -169,4 +209,4 @@ Special thanks to:
169209
system.
170210
- Paul Hubbard for initial packaging support.
171211

172-
[![Redis](./docs/_static/logo-redis.svg)](https://redis.io)
212+
[![Redis](./docs/_static/logo-redis.svg)](https://redis.io)

docker-compose.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
---
2+
# image tag 8.0-RC2-pre is the one matching the 8.0 GA release
23
x-client-libs-stack-image: &client-libs-stack-image
3-
image: "redislabs/client-libs-test:${CLIENT_LIBS_TEST_STACK_IMAGE_TAG:-rs-7.4.0-v2}"
4+
image: "redislabs/client-libs-test:${CLIENT_LIBS_TEST_STACK_IMAGE_TAG:-8.0-RC2-pre}"
45

56
x-client-libs-image: &client-libs-image
6-
image: "redislabs/client-libs-test:${CLIENT_LIBS_TEST_IMAGE_TAG:-7.4.2}"
7+
image: "redislabs/client-libs-test:${CLIENT_LIBS_TEST_IMAGE_TAG:-8.0-RC2-pre}"
78

89
services:
910

docs/examples/ssl_connection_examples.ipynb

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@
3737
" host='localhost',\n",
3838
" port=6666,\n",
3939
" ssl=True,\n",
40-
" ssl_check_hostname=False,\n",
4140
" ssl_cert_reqs=\"none\",\n",
4241
")\n",
4342
"r.ping()"
@@ -69,7 +68,7 @@
6968
"source": [
7069
"import redis\n",
7170
"\n",
72-
"r = redis.from_url(\"rediss://localhost:6666?ssl_cert_reqs=none&ssl_check_hostname=False&decode_responses=True&health_check_interval=2\")\n",
71+
"r = redis.from_url(\"rediss://localhost:6666?ssl_cert_reqs=none&decode_responses=True&health_check_interval=2\")\n",
7372
"r.ping()"
7473
]
7574
},
@@ -103,7 +102,6 @@
103102
" host=\"localhost\",\n",
104103
" port=6666,\n",
105104
" connection_class=redis.SSLConnection,\n",
106-
" ssl_check_hostname=False,\n",
107105
" ssl_cert_reqs=\"none\",\n",
108106
")\n",
109107
"\n",
@@ -143,7 +141,6 @@
143141
" port=6666,\n",
144142
" ssl=True,\n",
145143
" ssl_min_version=ssl.TLSVersion.TLSv1_3,\n",
146-
" ssl_check_hostname=False,\n",
147144
" ssl_cert_reqs=\"none\",\n",
148145
")\n",
149146
"r.ping()"

redis/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ def int_or_str(value):
4545
return value
4646

4747

48-
__version__ = "5.2.1"
48+
__version__ = "6.1.0"
4949
VERSION = tuple(map(int_or_str, __version__.split(".")))
5050

5151

redis/asyncio/cluster.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,14 @@ class RedisCluster(AbstractRedis, AbstractRedisCluster, AsyncRedisClusterCommand
134134
| Enable read from replicas in READONLY mode and defines the load balancing
135135
strategy that will be used for cluster node selection.
136136
The data read from replicas is eventually consistent with the data in primary nodes.
137+
:param dynamic_startup_nodes:
138+
| Set the RedisCluster's startup nodes to all the discovered nodes.
139+
If true (default value), the cluster's discovered nodes will be used to
140+
determine the cluster nodes-slots mapping in the next topology refresh.
141+
It will remove the initial passed startup nodes if their endpoints aren't
142+
listed in the CLUSTER SLOTS output.
143+
If you use dynamic DNS endpoints for startup nodes but CLUSTER SLOTS lists
144+
specific IP addresses, it is best to set it to false.
137145
:param reinitialize_steps:
138146
| Specifies the number of MOVED errors that need to occur before reinitializing
139147
the whole cluster topology. If a MOVED error occurs and the cluster does not
@@ -250,6 +258,7 @@ def __init__(
250258
require_full_coverage: bool = True,
251259
read_from_replicas: bool = False,
252260
load_balancing_strategy: Optional[LoadBalancingStrategy] = None,
261+
dynamic_startup_nodes: bool = True,
253262
reinitialize_steps: int = 5,
254263
cluster_error_retry_attempts: int = 3,
255264
max_connections: int = 2**31,
@@ -388,6 +397,7 @@ def __init__(
388397
startup_nodes,
389398
require_full_coverage,
390399
kwargs,
400+
dynamic_startup_nodes=dynamic_startup_nodes,
391401
address_remap=address_remap,
392402
event_dispatcher=self._event_dispatcher,
393403
)
@@ -1162,6 +1172,7 @@ async def _mock(self, error: RedisError):
11621172

11631173
class NodesManager:
11641174
__slots__ = (
1175+
"_dynamic_startup_nodes",
11651176
"_moved_exception",
11661177
"_event_dispatcher",
11671178
"connection_kwargs",
@@ -1179,6 +1190,7 @@ def __init__(
11791190
startup_nodes: List["ClusterNode"],
11801191
require_full_coverage: bool,
11811192
connection_kwargs: Dict[str, Any],
1193+
dynamic_startup_nodes: bool = True,
11821194
address_remap: Optional[Callable[[Tuple[str, int]], Tuple[str, int]]] = None,
11831195
event_dispatcher: Optional[EventDispatcher] = None,
11841196
) -> None:
@@ -1191,6 +1203,8 @@ def __init__(
11911203
self.nodes_cache: Dict[str, "ClusterNode"] = {}
11921204
self.slots_cache: Dict[int, List["ClusterNode"]] = {}
11931205
self.read_load_balancer = LoadBalancer()
1206+
1207+
self._dynamic_startup_nodes: bool = dynamic_startup_nodes
11941208
self._moved_exception: MovedError = None
11951209
if event_dispatcher is None:
11961210
self._event_dispatcher = EventDispatcher()
@@ -1433,8 +1447,10 @@ async def initialize(self) -> None:
14331447
# Set the tmp variables to the real variables
14341448
self.slots_cache = tmp_slots
14351449
self.set_nodes(self.nodes_cache, tmp_nodes_cache, remove_old=True)
1436-
# Populate the startup nodes with all discovered nodes
1437-
self.set_nodes(self.startup_nodes, self.nodes_cache, remove_old=True)
1450+
1451+
if self._dynamic_startup_nodes:
1452+
# Populate the startup nodes with all discovered nodes
1453+
self.set_nodes(self.startup_nodes, self.nodes_cache, remove_old=True)
14381454

14391455
# Set the default node
14401456
self.default_node = self.get_nodes_by_server_type(PRIMARY)[0]

redis/asyncio/connection.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -868,7 +868,7 @@ def __init__(
868868
cert_reqs: Optional[Union[str, ssl.VerifyMode]] = None,
869869
ca_certs: Optional[str] = None,
870870
ca_data: Optional[str] = None,
871-
check_hostname: bool = False,
871+
check_hostname: bool = True,
872872
min_version: Optional[TLSVersion] = None,
873873
ciphers: Optional[str] = None,
874874
):
@@ -893,7 +893,9 @@ def __init__(
893893
self.cert_reqs = cert_reqs
894894
self.ca_certs = ca_certs
895895
self.ca_data = ca_data
896-
self.check_hostname = check_hostname
896+
self.check_hostname = (
897+
check_hostname if self.cert_reqs != ssl.CERT_NONE else False
898+
)
897899
self.min_version = min_version
898900
self.ciphers = ciphers
899901
self.context: Optional[SSLContext] = None

redis/cluster.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ def parse_cluster_myshardid(resp, **options):
185185
"ssl_cert_reqs",
186186
"ssl_keyfile",
187187
"ssl_password",
188+
"ssl_check_hostname",
188189
"unix_socket_path",
189190
"username",
190191
"cache",

redis/connection.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1083,7 +1083,9 @@ def __init__(
10831083
self.ca_certs = ssl_ca_certs
10841084
self.ca_data = ssl_ca_data
10851085
self.ca_path = ssl_ca_path
1086-
self.check_hostname = ssl_check_hostname
1086+
self.check_hostname = (
1087+
ssl_check_hostname if self.cert_reqs != ssl.CERT_NONE else False
1088+
)
10871089
self.certificate_password = ssl_password
10881090
self.ssl_validate_ocsp = ssl_validate_ocsp
10891091
self.ssl_validate_ocsp_stapled = ssl_validate_ocsp_stapled

tests/test_asyncio/test_cluster.py

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2723,6 +2723,27 @@ def cmd_init_mock(self, r: ClusterNode) -> None:
27232723
assert rc.get_node(host=default_host, port=7001) is not None
27242724
assert rc.get_node(host=default_host, port=7002) is not None
27252725

2726+
@pytest.mark.parametrize("dynamic_startup_nodes", [True, False])
2727+
async def test_init_slots_dynamic_startup_nodes(self, dynamic_startup_nodes):
2728+
rc = await get_mocked_redis_client(
2729+
host="my@DNS.com",
2730+
port=7000,
2731+
cluster_slots=default_cluster_slots,
2732+
dynamic_startup_nodes=dynamic_startup_nodes,
2733+
)
2734+
# Nodes are taken from default_cluster_slots
2735+
discovered_nodes = [
2736+
"127.0.0.1:7000",
2737+
"127.0.0.1:7001",
2738+
"127.0.0.1:7002",
2739+
"127.0.0.1:7003",
2740+
]
2741+
startup_nodes = list(rc.nodes_manager.startup_nodes.keys())
2742+
if dynamic_startup_nodes is True:
2743+
assert sorted(startup_nodes) == sorted(discovered_nodes)
2744+
else:
2745+
assert startup_nodes == ["my@DNS.com:7000"]
2746+
27262747

27272748
class TestClusterPipeline:
27282749
"""Tests for the ClusterPipeline class."""
@@ -3118,9 +3139,7 @@ async def test_ssl_with_invalid_cert(
31183139
async def test_ssl_connection(
31193140
self, create_client: Callable[..., Awaitable[RedisCluster]]
31203141
) -> None:
3121-
async with await create_client(
3122-
ssl=True, ssl_check_hostname=False, ssl_cert_reqs="none"
3123-
) as rc:
3142+
async with await create_client(ssl=True, ssl_cert_reqs="none") as rc:
31243143
assert await rc.ping()
31253144

31263145
@pytest.mark.parametrize(
@@ -3136,7 +3155,6 @@ async def test_ssl_connection_tls12_custom_ciphers(
31363155
) -> None:
31373156
async with await create_client(
31383157
ssl=True,
3139-
ssl_check_hostname=False,
31403158
ssl_cert_reqs="none",
31413159
ssl_min_version=ssl.TLSVersion.TLSv1_2,
31423160
ssl_ciphers=ssl_ciphers,
@@ -3148,7 +3166,6 @@ async def test_ssl_connection_tls12_custom_ciphers_invalid(
31483166
) -> None:
31493167
async with await create_client(
31503168
ssl=True,
3151-
ssl_check_hostname=False,
31523169
ssl_cert_reqs="none",
31533170
ssl_min_version=ssl.TLSVersion.TLSv1_2,
31543171
ssl_ciphers="foo:bar",
@@ -3170,7 +3187,6 @@ async def test_ssl_connection_tls13_custom_ciphers(
31703187
# TLSv1.3 does not support changing the ciphers
31713188
async with await create_client(
31723189
ssl=True,
3173-
ssl_check_hostname=False,
31743190
ssl_cert_reqs="none",
31753191
ssl_min_version=ssl.TLSVersion.TLSv1_2,
31763192
ssl_ciphers=ssl_ciphers,

tests/test_asyncio/test_ssl.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
from urllib.parse import urlparse
2+
import pytest
3+
import pytest_asyncio
4+
import redis.asyncio as redis
5+
6+
# Skip test or not based on cryptography installation
7+
try:
8+
import cryptography # noqa
9+
10+
skip_if_cryptography = pytest.mark.skipif(False, reason="")
11+
skip_if_nocryptography = pytest.mark.skipif(False, reason="")
12+
except ImportError:
13+
skip_if_cryptography = pytest.mark.skipif(True, reason="cryptography not installed")
14+
skip_if_nocryptography = pytest.mark.skipif(
15+
True, reason="cryptography not installed"
16+
)
17+
18+
19+
@pytest.mark.ssl
20+
class TestSSL:
21+
"""Tests for SSL connections in asyncio."""
22+
23+
@pytest_asyncio.fixture()
24+
async def _get_client(self, request):
25+
ssl_url = request.config.option.redis_ssl_url
26+
p = urlparse(ssl_url)[1].split(":")
27+
client = redis.Redis(host=p[0], port=p[1], ssl=True)
28+
yield client
29+
await client.aclose()
30+
31+
async def test_ssl_with_invalid_cert(self, _get_client):
32+
"""Test SSL connection with invalid certificate."""
33+
pass
34+
35+
async def test_cert_reqs_none_with_check_hostname(self, request):
36+
"""Test that when ssl_cert_reqs=none is used with ssl_check_hostname=True,
37+
the connection is created successfully with check_hostname internally set to False"""
38+
ssl_url = request.config.option.redis_ssl_url
39+
parsed_url = urlparse(ssl_url)
40+
r = redis.Redis(
41+
host=parsed_url.hostname,
42+
port=parsed_url.port,
43+
ssl=True,
44+
ssl_cert_reqs="none",
45+
# Check that ssl_check_hostname is ignored, when ssl_cert_reqs=none
46+
ssl_check_hostname=True,
47+
)
48+
try:
49+
# Connection should be successful
50+
assert await r.ping()
51+
# check_hostname should have been automatically set to False
52+
assert r.connection_pool.connection_class == redis.SSLConnection
53+
conn = r.connection_pool.make_connection()
54+
assert conn.check_hostname is False
55+
finally:
56+
await r.aclose()

0 commit comments

Comments
 (0)