Skip to content

Commit 97bd8a1

Browse files
authored
Merge pull request #1748 from ClickHouse/feat_proxy_support
[client-v2] Proxy support
2 parents 4845b68 + bcc0f63 commit 97bd8a1

File tree

5 files changed

+275
-45
lines changed

5 files changed

+275
-45
lines changed

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

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,12 @@ public Builder addProxy(ProxyType type, String host, int port) {
424424
return this;
425425
}
426426

427+
public Builder setProxyCredentials(String user, String pass) {
428+
this.configuration.put("proxy_user", user);
429+
this.configuration.put("proxy_password", pass);
430+
return this;
431+
}
432+
427433
/**
428434
* Sets the maximum time for operation to complete. By default, it is set to 3 hours.
429435
* @param timeout
@@ -444,6 +450,12 @@ public Builder useNewImplementation(boolean useNewImplementation) {
444450
return this;
445451
}
446452

453+
public Builder setHttpCookiesEnabled(boolean enabled) {
454+
//TODO: extract to settings string constants
455+
this.configuration.put("client.http.cookies_enabled", String.valueOf(enabled));
456+
return this;
457+
}
458+
447459
public Client build() {
448460
// check if endpoint are empty. so can not initiate client
449461
if (this.endpoints.isEmpty()) {
@@ -963,6 +975,8 @@ public CompletableFuture<QueryResponse> query(String sqlQuery, Map<String, Objec
963975
metrics.operationComplete();
964976

965977
return new QueryResponse(httpResponse, finalSettings, metrics);
978+
} catch (ClientException e) {
979+
throw e;
966980
} catch (Exception e) {
967981
throw new ClientException("Failed to execute query", e);
968982
}
@@ -1145,7 +1159,7 @@ public CompletableFuture<CommandResponse> execute(String sql) {
11451159
} catch (Exception e) {
11461160
throw new ClientException("Failed to get command response", e);
11471161
}
1148-
});
1162+
}, sharedOperationExecutor);
11491163
}
11501164

11511165
private String startOperation() {
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.clickhouse.client.api;
2+
3+
/**
4+
* Represents errors caused by a client misconfiguration.
5+
*/
6+
public class ClientMisconfigurationException extends ClientException {
7+
public ClientMisconfigurationException(String message) {
8+
super(message);
9+
}
10+
11+
public ClientMisconfigurationException(String message, Throwable cause) {
12+
super(message, cause);
13+
}
14+
}

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

Lines changed: 71 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,27 @@
33
import com.clickhouse.client.ClickHouseNode;
44
import com.clickhouse.client.api.Client;
55
import com.clickhouse.client.api.ClientException;
6+
import com.clickhouse.client.api.ClientMisconfigurationException;
67
import com.clickhouse.client.api.ServerException;
8+
import com.clickhouse.client.api.enums.ProxyType;
79
import com.clickhouse.client.config.ClickHouseClientOption;
810
import com.clickhouse.client.http.ClickHouseHttpProto;
911
import com.clickhouse.client.http.config.ClickHouseHttpOption;
10-
import org.apache.hc.client5.http.SchemePortResolver;
1112
import org.apache.hc.client5.http.classic.methods.HttpPost;
1213
import org.apache.hc.client5.http.config.RequestConfig;
14+
import org.apache.hc.client5.http.impl.auth.CredentialsProviderBuilder;
1315
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
1416
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
15-
import org.apache.hc.client5.http.impl.routing.DefaultRoutePlanner;
17+
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
1618
import org.apache.hc.client5.http.protocol.HttpClientContext;
1719
import org.apache.hc.core5.http.ClassicHttpResponse;
1820
import org.apache.hc.core5.http.ContentType;
1921
import org.apache.hc.core5.http.Header;
2022
import org.apache.hc.core5.http.HttpHeaders;
2123
import org.apache.hc.core5.http.HttpHost;
24+
import org.apache.hc.core5.http.HttpStatus;
2225
import org.apache.hc.core5.http.NoHttpResponseException;
26+
import org.apache.hc.core5.http.io.SocketConfig;
2327
import org.apache.hc.core5.http.io.entity.EntityTemplate;
2428
import org.apache.hc.core5.io.IOCallback;
2529
import org.apache.hc.core5.net.URIBuilder;
@@ -29,12 +33,14 @@
2933
import java.io.ByteArrayOutputStream;
3034
import java.io.IOException;
3135
import java.io.OutputStream;
32-
import java.io.Serializable;
3336
import java.net.ConnectException;
37+
import java.net.InetSocketAddress;
3438
import java.net.NoRouteToHostException;
3539
import java.net.URI;
3640
import java.net.URISyntaxException;
3741
import java.net.UnknownHostException;
42+
import java.util.Base64;
43+
import java.util.EnumSet;
3844
import java.util.Map;
3945
import java.util.concurrent.TimeUnit;
4046
import java.util.function.Function;
@@ -50,25 +56,61 @@ public class HttpAPIClientHelper {
5056

5157
private RequestConfig baseRequestConfig;
5258

59+
private String proxyAuthHeaderValue;
60+
5361
public HttpAPIClientHelper(Map<String, String> configuration) {
5462
this.chConfiguration = configuration;
55-
this.httpClient = createHttpClient(configuration, null);
56-
this.baseRequestConfig = RequestConfig.custom()
57-
.setConnectionRequestTimeout(1000, TimeUnit.MILLISECONDS)
58-
.build();
59-
}
63+
this.httpClient = createHttpClient();
64+
65+
RequestConfig.Builder reqConfBuilder = RequestConfig.custom();
66+
MapUtils.applyLong(chConfiguration, ClickHouseClientOption.CONNECTION_TIMEOUT.getKey(),
67+
(t) -> reqConfBuilder.setConnectionRequestTimeout(t, TimeUnit.MILLISECONDS));
6068

61-
public CloseableHttpClient createHttpClient(Map<String, String> chConfig, Map<String, Serializable> requestConfig) {
62-
HttpClientBuilder httpclient = HttpClientBuilder.create();
69+
this.baseRequestConfig = reqConfBuilder.build();
70+
}
6371

64-
String proxyHost = chConfig.get(ClickHouseClientOption.PROXY_HOST.getKey());
65-
String proxyPort = chConfig.get(ClickHouseClientOption.PROXY_PORT.getKey());
72+
public CloseableHttpClient createHttpClient() {
73+
HttpClientBuilder clientBuilder = HttpClientBuilder.create();
74+
CredentialsProviderBuilder credProviderBuilder = CredentialsProviderBuilder.create();
75+
SocketConfig.Builder soCfgBuilder = SocketConfig.custom();
76+
PoolingHttpClientConnectionManagerBuilder connMgrBuilder = PoolingHttpClientConnectionManagerBuilder.create();
77+
78+
MapUtils.applyInt(chConfiguration, ClickHouseClientOption.SOCKET_TIMEOUT.getKey(),
79+
(t) -> soCfgBuilder.setSoTimeout(t, TimeUnit.MILLISECONDS));
80+
MapUtils.applyInt(chConfiguration, ClickHouseClientOption.SOCKET_RCVBUF.getKey(),
81+
soCfgBuilder::setRcvBufSize);
82+
MapUtils.applyInt(chConfiguration, ClickHouseClientOption.SOCKET_SNDBUF.getKey(),
83+
soCfgBuilder::setSndBufSize);
84+
85+
String proxyHost = chConfiguration.get(ClickHouseClientOption.PROXY_HOST.getKey());
86+
String proxyPort = chConfiguration.get(ClickHouseClientOption.PROXY_PORT.getKey());
87+
HttpHost proxy = null;
6688
if (proxyHost != null && proxyPort != null) {
67-
HttpHost proxy = new HttpHost(proxyHost, Integer.parseInt(proxyPort));
68-
httpclient.setProxy(proxy);
89+
proxy = new HttpHost(proxyHost, Integer.parseInt(proxyPort));
6990
}
7091

71-
return httpclient.build();
92+
93+
String proxyTypeVal = chConfiguration.get(ClickHouseClientOption.PROXY_TYPE.getKey());
94+
ProxyType proxyType = proxyTypeVal == null ? null : ProxyType.valueOf(proxyTypeVal);
95+
if (proxyType == ProxyType.HTTP) {
96+
clientBuilder.setProxy(proxy);
97+
if (chConfiguration.containsKey("proxy_password") && chConfiguration.containsKey("proxy_user")) {
98+
proxyAuthHeaderValue = "Basic " + Base64.getEncoder().encodeToString(
99+
(chConfiguration.get("proxy_user") + ":" + chConfiguration.get("proxy_password")).getBytes());
100+
}
101+
} else if (proxyType == ProxyType.SOCKS) {
102+
soCfgBuilder.setSocksProxyAddress(new InetSocketAddress(proxyHost, Integer.parseInt(proxyPort)));
103+
}
104+
105+
if (chConfiguration.getOrDefault("client.http.cookies_enabled", "true")
106+
.equalsIgnoreCase("false")) {
107+
clientBuilder.disableCookieManagement();
108+
}
109+
clientBuilder.setDefaultCredentialsProvider(credProviderBuilder.build());
110+
111+
connMgrBuilder.setDefaultSocketConfig(soCfgBuilder.build());
112+
clientBuilder.setConnectionManager(connMgrBuilder.build());
113+
return clientBuilder.build();
72114
}
73115

74116
/**
@@ -113,13 +155,19 @@ public ClassicHttpResponse executeRequest(ClickHouseNode server, Map<String, Obj
113155

114156
try {
115157
ClassicHttpResponse httpResponse = httpClient.executeOpen(target, req, context);
116-
if (httpResponse.getCode() >= 400 && httpResponse.getCode() < 500) {
158+
if (httpResponse.getCode() == HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED) {
159+
throw new ClientMisconfigurationException("Proxy authentication required. Please check your proxy settings.");
160+
} else if (httpResponse.getCode() >= HttpStatus.SC_BAD_REQUEST &&
161+
httpResponse.getCode() < HttpStatus.SC_SERVER_ERROR) {
117162
try {
118163
throw readError(httpResponse);
119164
} finally {
120165
httpResponse.close();
121166
}
122-
} else if (httpResponse.getCode() >= 500) {
167+
} else if (httpResponse.getCode() == HttpStatus.SC_BAD_GATEWAY) {
168+
httpResponse.close();
169+
throw new ClientException("Server returned '502 Bad gateway'. Check network and proxy settings.");
170+
} else if (httpResponse.getCode() >= HttpStatus.SC_INTERNAL_SERVER_ERROR) {
123171
httpResponse.close();
124172
return httpResponse;
125173
}
@@ -133,6 +181,8 @@ public ClassicHttpResponse executeRequest(ClickHouseNode server, Map<String, Obj
133181
throw e;
134182
} catch (NoHttpResponseException e) {
135183
throw e;
184+
} catch (ClientException e) {
185+
throw e;
136186
} catch (Exception e) {
137187
throw new ClientException("Failed to execute request", e);
138188
}
@@ -150,6 +200,10 @@ private void addHeaders(HttpPost req, Map<String, String> chConfig, Map<String,
150200
}
151201
}
152202
req.addHeader(ClickHouseHttpProto.HEADER_DATABASE, chConfig.get(ClickHouseClientOption.DATABASE.getKey()));
203+
204+
if (proxyAuthHeaderValue != null) {
205+
req.addHeader(HttpHeaders.PROXY_AUTHORIZATION, proxyAuthHeaderValue);
206+
}
153207
}
154208
private void addQueryParams(URIBuilder req, Map<String, String> chConfig, Map<String, Object> requestConfig) {
155209
if (requestConfig != null) {
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package com.clickhouse.client.api.internal;
2+
3+
4+
import java.util.Map;
5+
import java.util.function.Consumer;
6+
7+
/**
8+
* Collection of utility methods for working with maps.
9+
*/
10+
public class MapUtils {
11+
12+
public static void applyLong(Map<String, String> map, String key, Consumer<Long> consumer) {
13+
String val = map.get(key);
14+
if (val != null) {
15+
try {
16+
consumer.accept(Long.valueOf(val));
17+
} catch (NumberFormatException e) {
18+
throw new RuntimeException("Invalid value for key " + key + ": " + val, e);
19+
}
20+
}
21+
}
22+
public static void applyInt(Map<String, String> map, String key, Consumer<Integer> consumer) {
23+
String val = map.get(key);
24+
if (val != null) {
25+
try {
26+
consumer.accept(Integer.valueOf(val));
27+
} catch (NumberFormatException e) {
28+
throw new RuntimeException("Invalid value for key " + key + ": " + val, e);
29+
}
30+
}
31+
}
32+
}

0 commit comments

Comments
 (0)