Skip to content

Commit f2f9acb

Browse files
refactor(client-v2): parse ssl_cipher_suites in the config layer
Move the ssl_cipher_suites string->list sanitization (drop blank tokens from a leading/trailing/doubled comma, trim each surviving name) out of HttpAPIClientHelper transport code and into a ClientConfigProperties SSL_CIPHER_SUITES parseValue override, mirroring CLIENT_RETRY_ON_FAILURE. The transport layer now consumes an already-sanitized list (an unset or empty list means "no restriction"), keeping transport code free of config parsing per the repo architecture convention. Relocate the sanitization coverage to ClientConfigPropertiesTest, and replace the CustomSSLConnectionFactory super(...) block comment with an inline null /* supportedProtocols */. Addresses @chernser review feedback on #2919.
1 parent 07a9e67 commit f2f9acb

4 files changed

Lines changed: 69 additions & 55 deletions

File tree

client-v2/src/main/java/com/clickhouse/client/api/ClientConfigProperties.java

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,10 +211,28 @@ public Object parseValue(String value) {
211211
* support); when unset, the transport defaults are used (Apache HttpClient enables the JVM's default
212212
* suites minus those it considers weak).
213213
* <p>
214+
* The value is parsed into a sanitized list here, in the config-parsing layer (not in transport code):
215+
* blank tokens produced by a leading, trailing or doubled comma are dropped and each surviving name is
216+
* trimmed, so consumers receive a ready-to-use list. A null, empty or whitespace-only entry would
217+
* otherwise be rejected by the SSL socket and break the handshake even alongside valid suites.
218+
* <p>
214219
* Appended at the end of the enum on purpose: adding a constant in the middle would shift the ordinal
215220
* of every following constant (see {@code docs/changes_checklist.md}).
216221
*/
217-
SSL_CIPHER_SUITES("ssl_cipher_suites", List.class),
222+
SSL_CIPHER_SUITES("ssl_cipher_suites", List.class) {
223+
@Override
224+
@SuppressWarnings("unchecked")
225+
public Object parseValue(String value) {
226+
List<String> suites = (List<String>) super.parseValue(value);
227+
if (suites == null) {
228+
return null;
229+
}
230+
return suites.stream()
231+
.filter(s -> s != null && !s.trim().isEmpty())
232+
.map(String::trim)
233+
.collect(Collectors.toList());
234+
}
235+
},
218236
;
219237

220238
private static final Logger LOG = LoggerFactory.getLogger(ClientConfigProperties.class);

client-v2/src/main/java/com/clickhouse/client/api/internal/HttpAPIClientHelper.java

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -289,18 +289,11 @@ public CloseableHttpClient createHttpClient(boolean initSslContext, Map<String,
289289
// set because the connection hostname will not match the certificate.
290290
boolean trustAllHostnames = sslMode == SSLMode.TRUST || sslMode == SSLMode.VERIFY_CA;
291291
boolean hasSNI = socketSNI != null && !socketSNI.trim().isEmpty();
292+
// ssl_cipher_suites is parsed and sanitized in ClientConfigProperties (blank tokens dropped,
293+
// names trimmed); an unset or empty list means "no restriction" so the transport defaults apply.
292294
List<String> cipherSuites = ClientConfigProperties.SSL_CIPHER_SUITES.getOrDefault(configuration);
293-
// Drop blank tokens (e.g. from a leading, trailing or doubled comma in ssl_cipher_suites) and trim
294-
// each name: a null, empty or whitespace-only entry is rejected by the SSL socket and breaks the
295-
// handshake even when valid suites are also present.
296-
String[] enabledCipherSuites = cipherSuites == null ? null
297-
: cipherSuites.stream()
298-
.filter(s -> s != null && !s.trim().isEmpty())
299-
.map(String::trim)
300-
.toArray(String[]::new);
301-
if (enabledCipherSuites != null && enabledCipherSuites.length == 0) {
302-
enabledCipherSuites = null;
303-
}
295+
String[] enabledCipherSuites = cipherSuites == null || cipherSuites.isEmpty() ? null
296+
: cipherSuites.toArray(new String[0]);
304297
if (hasSNI || trustAllHostnames || enabledCipherSuites != null) {
305298
// Skip hostname verification only for trust-all modes or when a custom SNI is used (the
306299
// connection hostname would not match the certificate); otherwise a null verifier makes the
@@ -1097,10 +1090,7 @@ public CustomSSLConnectionFactory(String defaultSNI, SSLContext sslContext, Host
10971090

10981091
public CustomSSLConnectionFactory(String defaultSNI, SSLContext sslContext, HostnameVerifier hostnameVerifier,
10991092
String[] supportedCipherSuites) {
1100-
// supportedProtocols is left as null (transport defaults apply); supportedCipherSuites, when
1101-
// provided, restricts the cipher suites the base factory enables on each socket. A null
1102-
// hostnameVerifier makes the base factory fall back to its default verifier.
1103-
super(sslContext, null, supportedCipherSuites, hostnameVerifier);
1093+
super(sslContext, null /* supportedProtocols */, supportedCipherSuites, hostnameVerifier);
11041094
this.defaultSNI = defaultSNI == null || defaultSNI.trim().isEmpty() ? null : new SNIHostName(defaultSNI);
11051095
}
11061096

client-v2/src/test/java/com/clickhouse/client/api/ClientConfigPropertiesTest.java

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,13 @@
22

33

44
import org.testng.Assert;
5+
import org.testng.annotations.DataProvider;
56
import org.testng.annotations.Test;
67

8+
import java.util.Arrays;
9+
import java.util.Collections;
10+
import java.util.HashMap;
11+
import java.util.List;
712
import java.util.Map;
813

914

@@ -31,4 +36,44 @@ public void testToKeyValuePairs() {
3136
// Assert.assertEquals(map.get("key1"), "value1");
3237
// Assert.assertEquals(map.get("key2"), "value2");
3338
}
39+
40+
@DataProvider(name = "sslCipherSuites")
41+
public static Object[][] sslCipherSuites() {
42+
return new Object[][]{
43+
// raw ssl_cipher_suites value -> expected parsed list
44+
{"TLS_AES_256_GCM_SHA384,TLS_AES_128_GCM_SHA256",
45+
Arrays.asList("TLS_AES_256_GCM_SHA384", "TLS_AES_128_GCM_SHA256")},
46+
{"TLS_AES_128_GCM_SHA256", Collections.singletonList("TLS_AES_128_GCM_SHA256")},
47+
// each surviving name is trimmed
48+
{" TLS_AES_256_GCM_SHA384 , TLS_AES_128_GCM_SHA256 ",
49+
Arrays.asList("TLS_AES_256_GCM_SHA384", "TLS_AES_128_GCM_SHA256")},
50+
// blank tokens from a leading, doubled or trailing comma (and whitespace-only) are dropped
51+
{",TLS_AES_256_GCM_SHA384,, TLS_AES_128_GCM_SHA256 ,",
52+
Arrays.asList("TLS_AES_256_GCM_SHA384", "TLS_AES_128_GCM_SHA256")},
53+
// no usable suite -> empty list (treated downstream as "no restriction")
54+
{"", Collections.emptyList()},
55+
{" ", Collections.emptyList()},
56+
{",, ,", Collections.emptyList()},
57+
};
58+
}
59+
60+
@Test(groups = {"unit"}, dataProvider = "sslCipherSuites")
61+
public void testSslCipherSuitesParsedAndSanitized(String raw, List<String> expected) {
62+
Assert.assertEquals(ClientConfigProperties.SSL_CIPHER_SUITES.parseValue(raw), expected);
63+
}
64+
65+
@Test(groups = {"unit"})
66+
public void testSslCipherSuitesUnsetParsesToNull() {
67+
Assert.assertNull(ClientConfigProperties.SSL_CIPHER_SUITES.parseValue(null));
68+
}
69+
70+
@Test(groups = {"unit"})
71+
public void testParseConfigMapSanitizesSslCipherSuites() {
72+
Map<String, String> raw = new HashMap<>();
73+
raw.put(ClientConfigProperties.SSL_CIPHER_SUITES.getKey(),
74+
",TLS_AES_256_GCM_SHA384,, TLS_AES_128_GCM_SHA256 ,");
75+
Map<String, Object> parsed = ClientConfigProperties.parseConfigMap(raw);
76+
Assert.assertEquals(parsed.get(ClientConfigProperties.SSL_CIPHER_SUITES.getKey()),
77+
Arrays.asList("TLS_AES_256_GCM_SHA384", "TLS_AES_128_GCM_SHA256"));
78+
}
3479
}

client-v2/src/test/java/com/clickhouse/client/api/internal/HttpAPIClientHelperTest.java

Lines changed: 0 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -228,45 +228,6 @@ public void testCreateHttpClientEmptyCipherSuitesTreatedAsNoRestriction() {
228228
+ "(transport defaults), so the plain SSLConnectionSocketFactory is used");
229229
}
230230

231-
/**
232-
* Malformed cipher-suite input - blank tokens from a leading, trailing or doubled comma, and
233-
* whitespace-padded names - must be sanitized before reaching the SSL socket: a null, empty or
234-
* whitespace-only entry is rejected by {@code SSLSocket#setEnabledCipherSuites} ("invalid null or empty
235-
* string elements") and breaks the handshake even when valid suites are also present. The blanks must be
236-
* dropped and the surviving names trimmed, preserving order.
237-
*/
238-
@Test
239-
public void testCreateHttpClientDropsBlankAndWhitespaceCipherTokens() {
240-
Map<String, Object> config = new HashMap<>();
241-
config.put(ClientConfigProperties.SSL_CIPHER_SUITES.getKey(),
242-
Arrays.asList("", "TLS_AES_256_GCM_SHA384", "", " ", " TLS_AES_128_GCM_SHA256 "));
243-
244-
List<List<?>> calls = captureCustomFactoryConstruction(config);
245-
246-
assertEquals(calls.size(), 1, "configured (non-blank) cipher suites must still build the custom factory");
247-
List<?> args = calls.get(0);
248-
assertEquals((String[]) args.get(3),
249-
new String[]{"TLS_AES_256_GCM_SHA384", "TLS_AES_128_GCM_SHA256"},
250-
"blank tokens (leading/doubled comma, whitespace-only) must be dropped and surviving names "
251-
+ "trimmed before being forwarded to the SSL socket factory");
252-
}
253-
254-
/**
255-
* Boundary case: a cipher-suite list that contains only blank tokens (e.g. from a property that is just
256-
* commas/whitespace) has no usable suite, so - like an empty or unset list - it must be treated as "no
257-
* restriction" (transport defaults) rather than forwarding an all-blank array that would fail every handshake.
258-
*/
259-
@Test
260-
public void testCreateHttpClientBlankOnlyCipherSuitesTreatedAsNoRestriction() {
261-
Map<String, Object> config = new HashMap<>();
262-
config.put(ClientConfigProperties.SSL_CIPHER_SUITES.getKey(), Arrays.asList("", " "));
263-
264-
List<List<?>> calls = captureCustomFactoryConstruction(config);
265-
266-
assertEquals(calls.size(), 0, "a cipher-suite list of only blank tokens must be treated as no "
267-
+ "restriction (transport defaults), so the plain SSLConnectionSocketFactory is used");
268-
}
269-
270231
@Test
271232
public void testExecuteRequestThrowsConnectExceptionOn502() throws Exception {
272233
Map<String, Object> configuration = new HashMap<>();

0 commit comments

Comments
 (0)