Skip to content

Commit d351710

Browse files
authored
Merge pull request #2808 from ClickHouse/03/25/26/resolve_underscore_hostname
[client-v2] Implemented proper handling of hostnames with `_` Added DNS resolver
2 parents 0a3fdec + 84f84ab commit d351710

6 files changed

Lines changed: 181 additions & 57 deletions

File tree

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

Lines changed: 9 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,6 @@
5353
import java.io.InputStream;
5454
import java.io.OutputStream;
5555
import java.lang.reflect.InvocationTargetException;
56-
import java.net.MalformedURLException;
57-
import java.net.URL;
5856
import java.time.Duration;
5957
import java.time.ZoneId;
6058
import java.time.temporal.ChronoUnit;
@@ -293,32 +291,10 @@ public Builder() {
293291
*/
294292
public Builder addEndpoint(String endpoint) {
295293
try {
296-
URL endpointURL = new URL(endpoint);
297-
298-
String protocolStr = endpointURL.getProtocol();
299-
if (!protocolStr.equalsIgnoreCase("https") &&
300-
!protocolStr.equalsIgnoreCase("http")) {
301-
throw new IllegalArgumentException("Only HTTP and HTTPS protocols are supported");
302-
}
303-
304-
boolean secure = protocolStr.equalsIgnoreCase("https");
305-
String host = endpointURL.getHost();
306-
if (host == null || host.isEmpty()) {
307-
throw new IllegalArgumentException("Host cannot be empty in endpoint: " + endpoint);
308-
}
309-
310-
int port = endpointURL.getPort();
311-
if (port <= 0) {
312-
throw new ValidationUtils.SettingsValidationException("port", "Valid port must be specified");
313-
}
314-
315-
String path = endpointURL.getPath();
316-
if (path == null || path.isEmpty()) {
317-
path = "/";
318-
}
319-
320-
return addEndpoint(Protocol.HTTP, host, port, secure, path);
321-
} catch (MalformedURLException e) {
294+
return addEndpoint(new HttpEndpoint(endpoint));
295+
} catch (ValidationUtils.SettingsValidationException e) {
296+
throw e;
297+
} catch (IllegalArgumentException e) {
322298
throw new IllegalArgumentException("Endpoint should be a valid URL string, but was " + endpoint, e);
323299
}
324300
}
@@ -336,19 +312,19 @@ public Builder addEndpoint(Protocol protocol, String host, int port, boolean sec
336312
}
337313

338314
public Builder addEndpoint(Protocol protocol, String host, int port, boolean secure, String basePath) {
339-
ValidationUtils.checkNonBlank(host, "host");
340315
ValidationUtils.checkNotNull(protocol, "protocol");
341-
ValidationUtils.checkRange(port, 1, ValidationUtils.TCP_PORT_NUMBER_MAX, "port");
342316
ValidationUtils.checkNotNull(basePath, "basePath");
343317

344318
if (protocol == Protocol.HTTP) {
345-
HttpEndpoint httpEndpoint = new HttpEndpoint(host, port, secure, basePath);
346-
this.endpoints.add(httpEndpoint);
319+
return addEndpoint(new HttpEndpoint(host, port, secure, basePath));
347320
} else {
348321
throw new IllegalArgumentException("Unsupported protocol: " + protocol);
349322
}
350-
return this;
323+
}
351324

325+
private Builder addEndpoint(Endpoint endpoint) {
326+
this.endpoints.add(endpoint);
327+
return this;
352328
}
353329

354330

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

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,9 @@
3131
import org.apache.hc.client5.http.io.HttpClientConnectionManager;
3232
import org.apache.hc.client5.http.io.ManagedHttpClientConnection;
3333
import org.apache.hc.client5.http.protocol.HttpClientContext;
34-
import org.apache.hc.client5.http.socket.ConnectionSocketFactory;
3534
import org.apache.hc.client5.http.socket.LayeredConnectionSocketFactory;
36-
import org.apache.hc.client5.http.socket.PlainConnectionSocketFactory;
3735
import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
36+
import org.apache.hc.client5.http.ssl.TlsSocketStrategy;
3837
import org.apache.hc.core5.http.ClassicHttpResponse;
3938
import org.apache.hc.core5.http.ConnectionRequestTimeoutException;
4039
import org.apache.hc.core5.http.ContentType;
@@ -45,8 +44,10 @@
4544
import org.apache.hc.core5.http.HttpRequest;
4645
import org.apache.hc.core5.http.HttpStatus;
4746
import org.apache.hc.core5.http.NoHttpResponseException;
47+
import org.apache.hc.core5.http.URIScheme;
4848
import org.apache.hc.core5.http.config.CharCodingConfig;
4949
import org.apache.hc.core5.http.config.Http1Config;
50+
import org.apache.hc.core5.http.config.Lookup;
5051
import org.apache.hc.core5.http.config.RegistryBuilder;
5152
import org.apache.hc.core5.http.impl.io.DefaultHttpResponseParserFactory;
5253
import org.apache.hc.core5.http.io.SocketConfig;
@@ -204,11 +205,13 @@ private ConnectionConfig createConnectionConfig(Map<String, Object> configuratio
204205
}
205206

206207
private HttpClientConnectionManager basicConnectionManager(LayeredConnectionSocketFactory sslConnectionSocketFactory, SocketConfig socketConfig, Map<String, Object> configuration) {
207-
RegistryBuilder<ConnectionSocketFactory> registryBuilder = RegistryBuilder.create();
208-
registryBuilder.register("http", PlainConnectionSocketFactory.getSocketFactory());
209-
registryBuilder.register("https", sslConnectionSocketFactory);
208+
Lookup<TlsSocketStrategy> tlsSocketStrategyLookup = RegistryBuilder.<TlsSocketStrategy>create()
209+
.register(URIScheme.HTTPS.id, (socket, target, port, attachment, context) ->
210+
(SSLSocket) sslConnectionSocketFactory.createLayeredSocket(socket, target, port, context))
211+
.build();
210212

211-
BasicHttpClientConnectionManager connManager = new BasicHttpClientConnectionManager(registryBuilder.build());
213+
BasicHttpClientConnectionManager connManager = BasicHttpClientConnectionManager.create(
214+
null, null, tlsSocketStrategyLookup, null);
212215
connManager.setConnectionConfig(createConnectionConfig(configuration));
213216
connManager.setSocketConfig(socketConfig);
214217

client-v2/src/main/java/com/clickhouse/client/api/transport/HttpEndpoint.java

Lines changed: 91 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package com.clickhouse.client.api.transport;
22

33
import com.clickhouse.client.api.ClientMisconfigurationException;
4+
import com.clickhouse.client.api.internal.ValidationUtils;
45

56
import java.net.URI;
7+
import java.net.MalformedURLException;
68
import java.net.URL;
79

810
public class HttpEndpoint implements Endpoint {
@@ -19,24 +21,28 @@ public class HttpEndpoint implements Endpoint {
1921

2022
private final String path;
2123

22-
public HttpEndpoint(String host, int port, boolean secure, String path){
23-
this.host = host;
24-
this.port = port;
25-
this.secure = secure;
26-
if (path != null && !path.isEmpty()) {
27-
// Ensure basePath starts with /
28-
this.path = path.startsWith("/") ? path : "/" + path;
29-
} else {
30-
this.path = "/";
31-
}
32-
33-
// Use URI constructor to properly handle encoding of path segments
34-
// Encode path segments separately to preserve slashes
35-
try {
36-
this.uri = new URI(secure ? "https" : "http", null, host, port, this.path, null, null);
37-
} catch (Exception e) {
38-
throw new ClientMisconfigurationException("Failed to create endpoint URL", e);
39-
}
24+
public HttpEndpoint(String endpoint) {
25+
this(parseEndpointUrl(endpoint));
26+
}
27+
28+
public HttpEndpoint(String host, int port, boolean secure, String path) {
29+
this(new EndpointDetails(validateHost(host), validatePort(port), secure, normalizePath(path)));
30+
}
31+
32+
private HttpEndpoint(URL endpointUrl) {
33+
this(new EndpointDetails(
34+
validateHost(endpointUrl.getHost()),
35+
validatePort(endpointUrl.getPort()),
36+
isSecure(endpointUrl.getProtocol()),
37+
decodePath(endpointUrl.getPath())));
38+
}
39+
40+
private HttpEndpoint(EndpointDetails endpointDetails) {
41+
this.host = endpointDetails.host;
42+
this.port = endpointDetails.port;
43+
this.secure = endpointDetails.secure;
44+
this.path = endpointDetails.path;
45+
this.uri = createUri(endpointDetails.host, endpointDetails.port, endpointDetails.secure, endpointDetails.path);
4046
this.info = uri.toString();
4147
}
4248

@@ -77,4 +83,71 @@ public boolean equals(Object obj) {
7783
public int hashCode() {
7884
return uri.hashCode();
7985
}
86+
87+
private static URL parseEndpointUrl(String endpoint) {
88+
try {
89+
return new URL(endpoint);
90+
} catch (MalformedURLException e) {
91+
throw new IllegalArgumentException("Failed to parse endpoint URL", e);
92+
}
93+
}
94+
95+
private static String validateHost(String host) {
96+
ValidationUtils.checkNonBlank(host, "host");
97+
return host;
98+
}
99+
100+
private static int validatePort(int port) {
101+
if (port <= 0) {
102+
throw new ValidationUtils.SettingsValidationException("port", "Valid port must be specified");
103+
}
104+
ValidationUtils.checkRange(port, 1, ValidationUtils.TCP_PORT_NUMBER_MAX, "port");
105+
return port;
106+
}
107+
108+
private static boolean isSecure(String protocol) {
109+
if ("https".equalsIgnoreCase(protocol)) {
110+
return true;
111+
}
112+
if ("http".equalsIgnoreCase(protocol)) {
113+
return false;
114+
}
115+
throw new IllegalArgumentException("Only HTTP and HTTPS protocols are supported");
116+
}
117+
118+
private static String normalizePath(String path) {
119+
if (path != null && !path.isEmpty()) {
120+
return path.startsWith("/") ? path : "/" + path;
121+
}
122+
return "/";
123+
}
124+
125+
private static String decodePath(String path) {
126+
String normalizedPath = normalizePath(path);
127+
return URI.create(normalizedPath.replace(" ", "%20")).getPath();
128+
}
129+
130+
private static URI createUri(String host, int port, boolean secure, String path) {
131+
try {
132+
String scheme = secure ? "https" : "http";
133+
String authority = host + ":" + port;
134+
return new URI(scheme, authority, path, null, null);
135+
} catch (Exception e) {
136+
throw new ClientMisconfigurationException("Failed to create endpoint URL", e);
137+
}
138+
}
139+
140+
private static final class EndpointDetails {
141+
private final String host;
142+
private final int port;
143+
private final boolean secure;
144+
private final String path;
145+
146+
private EndpointDetails(String host, int port, boolean secure, String path) {
147+
this.host = host;
148+
this.port = port;
149+
this.secure = secure;
150+
this.path = path;
151+
}
152+
}
80153
}

client-v2/src/test/java/com/clickhouse/client/ClientTests.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import com.clickhouse.client.api.ClientException;
66
import com.clickhouse.client.api.ClientFaultCause;
77
import com.clickhouse.client.api.ClientMisconfigurationException;
8+
import com.clickhouse.client.api.ConnectionInitiationException;
89
import com.clickhouse.client.api.ConnectionReuseStrategy;
910
import com.clickhouse.client.api.ServerException;
1011
import com.clickhouse.client.api.enums.Protocol;
@@ -30,6 +31,7 @@
3031

3132
import java.io.ByteArrayInputStream;
3233
import java.net.ConnectException;
34+
import java.net.UnknownHostException;
3335
import java.util.ArrayList;
3436
import java.util.Arrays;
3537
import java.util.Collections;
@@ -569,6 +571,21 @@ public void testQueryIdGenerator() throws Exception {
569571
Assert.assertEquals(actualIds, new ArrayList<>(queryIds));
570572
}
571573

574+
@Test(groups = {"integration"})
575+
public void testHostnameWithUnderscore() throws Exception {
576+
577+
try (Client client = new Client.Builder().addEndpoint("http://localhost_db:8123")
578+
.setUsername("default")
579+
.build()) {
580+
client.queryAll("SELECT 1");
581+
fail("Exception expected");
582+
} catch (ClientException e) {
583+
Assert.assertTrue(e.getCause() instanceof ConnectionInitiationException);
584+
ConnectionInitiationException ce = (ConnectionInitiationException) e.getCause();
585+
Assert.assertTrue(ce.getCause() instanceof UnknownHostException);
586+
}
587+
}
588+
572589
public boolean isVersionMatch(String versionExpression, Client client) {
573590
List<GenericRecord> serverVersion = client.queryAll("SELECT version()");
574591
return ClickHouseVersion.of(serverVersion.get(0).getString(1)).check(versionExpression);
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package com.clickhouse.client.api;
2+
3+
import org.testng.Assert;
4+
import org.testng.annotations.Test;
5+
6+
import java.lang.reflect.Field;
7+
import java.util.List;
8+
9+
public class ClientBuilderTest {
10+
11+
@Test
12+
public void testAddEndpointToleratesUnderscoreHostname() throws Exception {
13+
try (Client client = new Client.Builder()
14+
.addEndpoint("http://host_with_underscore:8123")
15+
.setUsername("default")
16+
.setPassword("")
17+
.build()) {
18+
19+
String firstEndpoint = extractFirstEndpointUri(client);
20+
Assert.assertEquals(firstEndpoint, "http://host_with_underscore:8123/",
21+
"Endpoint URI should preserve original hostname");
22+
}
23+
}
24+
25+
private static String extractFirstEndpointUri(Client client) throws Exception {
26+
Field endpointsField = Client.class.getDeclaredField("endpoints");
27+
endpointsField.setAccessible(true);
28+
29+
@SuppressWarnings("unchecked")
30+
List<com.clickhouse.client.api.transport.Endpoint> endpoints =
31+
(List<com.clickhouse.client.api.transport.Endpoint>) endpointsField.get(client);
32+
return endpoints.get(0).getURI().toString();
33+
}
34+
}

client-v2/src/test/java/com/clickhouse/client/api/transport/HttpEndpointTest.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,4 +160,25 @@ public void testUtf8CharactersInPath() {
160160
Assert.assertTrue(cyrillicEndpoint.getURI().toASCIIString().contains("%"),
161161
"Cyrillic path should be percent-encoded in ASCII representation");
162162
}
163+
164+
@Test
165+
public void testUnderscoreHostIsAcceptedInUri() {
166+
HttpEndpoint endpoint = new HttpEndpoint("host_with_underscore", 8123, false, "/");
167+
Assert.assertEquals(endpoint.getHost(), "host_with_underscore", "Original host should be preserved");
168+
Assert.assertEquals(endpoint.getURI().toString(), "http://host_with_underscore:8123/");
169+
}
170+
171+
@Test
172+
public void testUrlEndpointPreservesUnderscoreHost() {
173+
HttpEndpoint endpoint = new HttpEndpoint("http://host_with_underscore:8123/");
174+
Assert.assertEquals(endpoint.getHost(), "host_with_underscore", "Original host should be preserved");
175+
Assert.assertEquals(endpoint.getURI().toString(), "http://host_with_underscore:8123/");
176+
}
177+
178+
@Test
179+
public void testUrlEndpointIgnoresQueryAndFragment() {
180+
HttpEndpoint endpoint = new HttpEndpoint("http://localhost:8123/sales%20db?ignored=value#fragment");
181+
Assert.assertEquals(endpoint.getPath(), "/sales db", "Path should be decoded before URI creation");
182+
Assert.assertEquals(endpoint.getURI().toString(), "http://localhost:8123/sales%20db");
183+
}
163184
}

0 commit comments

Comments
 (0)