Skip to content

Commit b94fdf2

Browse files
authored
Server version check for CSC activation (#3954)
* checking server version for CSC * fix format change * fix noauth hello exception in integration tests * fix version check * remove redundant check * remove unused imports * 'toString' for Version * rename to RedisVersion * moving RedisVersion package
1 parent d369cf6 commit b94fdf2

File tree

6 files changed

+134
-36
lines changed

6 files changed

+134
-36
lines changed

Diff for: src/main/java/redis/clients/jedis/Connection.java

+45-36
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import java.util.ArrayList;
1313
import java.util.Arrays;
1414
import java.util.List;
15+
import java.util.Map;
1516
import java.util.function.Supplier;
1617

1718
import redis.clients.jedis.Protocol.Command;
@@ -41,6 +42,8 @@ public class Connection implements Closeable {
4142
private boolean broken = false;
4243
private boolean strValActive;
4344
private String strVal;
45+
protected String server;
46+
protected String version;
4447

4548
public Connection() {
4649
this(Protocol.DEFAULT_HOST, Protocol.DEFAULT_PORT);
@@ -453,12 +456,12 @@ protected void initializeFromClientConfig(final JedisClientConfig config) {
453456
final RedisCredentialsProvider redisCredentialsProvider = (RedisCredentialsProvider) credentialsProvider;
454457
try {
455458
redisCredentialsProvider.prepare();
456-
helloOrAuth(protocol, redisCredentialsProvider.get());
459+
helloAndAuth(protocol, redisCredentialsProvider.get());
457460
} finally {
458461
redisCredentialsProvider.cleanUp();
459462
}
460463
} else {
461-
helloOrAuth(protocol, credentialsProvider != null ? credentialsProvider.get()
464+
helloAndAuth(protocol, credentialsProvider != null ? credentialsProvider.get()
462465
: new DefaultRedisCredentials(config.getUser(), config.getPassword()));
463466
}
464467

@@ -517,50 +520,56 @@ protected void initializeFromClientConfig(final JedisClientConfig config) {
517520
}
518521
}
519522

520-
private void helloOrAuth(final RedisProtocol protocol, final RedisCredentials credentials) {
521-
522-
if (credentials == null || credentials.getPassword() == null) {
523-
if (protocol != null) {
524-
sendCommand(Command.HELLO, encode(protocol.version()));
525-
getOne();
523+
private void helloAndAuth(final RedisProtocol protocol, final RedisCredentials credentials) {
524+
Map<String, Object> helloResult = null;
525+
if (protocol != null && credentials != null && credentials.getUser() != null) {
526+
byte[] rawPass = encodeToBytes(credentials.getPassword());
527+
try {
528+
helloResult = hello(encode(protocol.version()), Keyword.AUTH.getRaw(), encode(credentials.getUser()), rawPass);
529+
} finally {
530+
Arrays.fill(rawPass, (byte) 0); // clear sensitive data
526531
}
527-
return;
532+
} else {
533+
auth(credentials);
534+
helloResult = protocol == null ? null : hello(encode(protocol.version()));
535+
}
536+
if (helloResult != null) {
537+
server = (String) helloResult.get("server");
538+
version = (String) helloResult.get("version");
528539
}
529540

530-
// Source: https://stackoverflow.com/a/9670279/4021802
531-
ByteBuffer passBuf = Protocol.CHARSET.encode(CharBuffer.wrap(credentials.getPassword()));
532-
byte[] rawPass = Arrays.copyOfRange(passBuf.array(), passBuf.position(), passBuf.limit());
533-
Arrays.fill(passBuf.array(), (byte) 0); // clear sensitive data
541+
// clearing 'char[] credentials.getPassword()' should be
542+
// handled in RedisCredentialsProvider.cleanUp()
543+
}
534544

545+
private void auth(RedisCredentials credentials) {
546+
if (credentials == null || credentials.getPassword() == null) {
547+
return;
548+
}
549+
byte[] rawPass = encodeToBytes(credentials.getPassword());
535550
try {
536-
/// actual HELLO or AUTH -->
537-
if (protocol != null) {
538-
if (credentials.getUser() != null) {
539-
sendCommand(Command.HELLO, encode(protocol.version()),
540-
Keyword.AUTH.getRaw(), encode(credentials.getUser()), rawPass);
541-
getOne(); // Map
542-
} else {
543-
sendCommand(Command.AUTH, rawPass);
544-
getStatusCodeReply(); // OK
545-
sendCommand(Command.HELLO, encode(protocol.version()));
546-
getOne(); // Map
547-
}
548-
} else { // protocol == null
549-
if (credentials.getUser() != null) {
550-
sendCommand(Command.AUTH, encode(credentials.getUser()), rawPass);
551-
} else {
552-
sendCommand(Command.AUTH, rawPass);
553-
}
554-
getStatusCodeReply(); // OK
551+
if (credentials.getUser() == null) {
552+
sendCommand(Command.AUTH, rawPass);
553+
} else {
554+
sendCommand(Command.AUTH, encode(credentials.getUser()), rawPass);
555555
}
556-
/// <-- actual HELLO or AUTH
557556
} finally {
558-
559557
Arrays.fill(rawPass, (byte) 0); // clear sensitive data
560558
}
559+
getStatusCodeReply();
560+
}
561561

562-
// clearing 'char[] credentials.getPassword()' should be
563-
// handled in RedisCredentialsProvider.cleanUp()
562+
protected Map<String, Object> hello(byte[]... args) {
563+
sendCommand(Command.HELLO, args);
564+
return BuilderFactory.ENCODED_OBJECT_MAP.build(getOne());
565+
}
566+
567+
protected byte[] encodeToBytes(char[] chars) {
568+
// Source: https://stackoverflow.com/a/9670279/4021802
569+
ByteBuffer passBuf = Protocol.CHARSET.encode(CharBuffer.wrap(chars));
570+
byte[] rawPass = Arrays.copyOfRange(passBuf.array(), passBuf.position(), passBuf.limit());
571+
Arrays.fill(passBuf.array(), (byte) 0); // clear sensitive data
572+
return rawPass;
564573
}
565574

566575
public String select(final int index) {

Diff for: src/main/java/redis/clients/jedis/csc/AbstractCache.java

+4
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,10 @@ public CacheStats getAndResetStats() {
193193
return result;
194194
}
195195

196+
@Override
197+
public boolean compatibilityMode() {
198+
return false;
199+
}
196200
// End of Cache interface methods
197201

198202
// abstract methods to be implemented by the concrete classes

Diff for: src/main/java/redis/clients/jedis/csc/Cache.java

+5
Original file line numberDiff line numberDiff line change
@@ -105,4 +105,9 @@ public interface Cache {
105105
* @return The statistics of the cache
106106
*/
107107
CacheStats getAndResetStats();
108+
109+
/**
110+
* @return The compatibility of cache against different Redis versions
111+
*/
112+
boolean compatibilityMode();
108113
}

Diff for: src/main/java/redis/clients/jedis/csc/CacheConnection.java

+9
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,22 @@ public class CacheConnection extends Connection {
1616

1717
private final Cache cache;
1818
private ReentrantLock lock;
19+
private static final String REDIS = "redis";
20+
private static final String MIN_REDIS_VERSION = "7.4";
1921

2022
public CacheConnection(final JedisSocketFactory socketFactory, JedisClientConfig clientConfig, Cache cache) {
2123
super(socketFactory, clientConfig);
2224

2325
if (protocol != RedisProtocol.RESP3) {
2426
throw new JedisException("Client side caching is only supported with RESP3.");
2527
}
28+
if (!cache.compatibilityMode()) {
29+
RedisVersion current = new RedisVersion(version);
30+
RedisVersion required = new RedisVersion(MIN_REDIS_VERSION);
31+
if (!REDIS.equals(server) || current.compareTo(required) < 0) {
32+
throw new JedisException(String.format("Client side caching is only supported with 'Redis %s' or later.", MIN_REDIS_VERSION));
33+
}
34+
}
2635
this.cache = Objects.requireNonNull(cache);
2736
initializeClientSideCache();
2837
}
+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package redis.clients.jedis.csc;
2+
3+
import java.util.Arrays;
4+
5+
class RedisVersion implements Comparable<RedisVersion> {
6+
7+
private String version;
8+
private Integer[] numbers;
9+
10+
public RedisVersion(String version) {
11+
if (version == null) throw new IllegalArgumentException("Version can not be null");
12+
this.version = version;
13+
this.numbers = Arrays.stream(version.split("\\.")).map(n -> Integer.parseInt(n)).toArray(Integer[]::new);
14+
}
15+
16+
@Override
17+
public int compareTo(RedisVersion other) {
18+
int max = Math.max(this.numbers.length, other.numbers.length);
19+
for (int i = 0; i < max; i++) {
20+
int thisNumber = this.numbers.length > i ? this.numbers[i]:0;
21+
int otherNumber = other.numbers.length > i ? other.numbers[i]:0;
22+
if (thisNumber < otherNumber) return -1;
23+
if (thisNumber > otherNumber) return 1;
24+
}
25+
return 0;
26+
}
27+
28+
@Override
29+
public String toString() {
30+
return this.version;
31+
}
32+
33+
@Override
34+
public boolean equals(Object that) {
35+
if (this == that) return true;
36+
if (that == null) return false;
37+
if (this.getClass() != that.getClass()) return false;
38+
return this.compareTo((RedisVersion) that) == 0;
39+
}
40+
41+
}
+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package redis.clients.jedis.csc;
2+
3+
import static org.junit.Assert.assertEquals;
4+
5+
import org.junit.Test;
6+
7+
public class VersionTest {
8+
9+
@Test
10+
public void compareSameVersions() {
11+
RedisVersion a = new RedisVersion("5.2.4");
12+
RedisVersion b = new RedisVersion("5.2.4");
13+
assertEquals(a, b);
14+
15+
RedisVersion c = new RedisVersion("5.2.0.0");
16+
RedisVersion d = new RedisVersion("5.2");
17+
assertEquals(a, b);
18+
}
19+
20+
@Test
21+
public void compareDifferentVersions() {
22+
RedisVersion a = new RedisVersion("5.2.4");
23+
RedisVersion b = new RedisVersion("5.1.4");
24+
assertEquals(1, a.compareTo(b));
25+
26+
RedisVersion c = new RedisVersion("5.2.4");
27+
RedisVersion d = new RedisVersion("5.2.5");
28+
assertEquals(-1, c.compareTo(d));
29+
}
30+
}

0 commit comments

Comments
 (0)