Skip to content

Commit 32ddad3

Browse files
committed
Disabled handling redirects by default. Implemented configuration for redirect
1 parent 8a3e33a commit 32ddad3

7 files changed

Lines changed: 420 additions & 21 deletions

File tree

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

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import com.clickhouse.client.api.data_formats.internal.ProcessParser;
1313
import com.clickhouse.client.api.enums.Protocol;
1414
import com.clickhouse.client.api.enums.ProxyType;
15+
import com.clickhouse.client.api.http.HttpRedirectPolicy;
1516
import com.clickhouse.client.api.http.ClickHouseHttpProto;
1617
import com.clickhouse.client.api.insert.InsertResponse;
1718
import com.clickhouse.client.api.insert.InsertSettings;
@@ -144,7 +145,8 @@ public class Client implements AutoCloseable {
144145

145146
private Client(Collection<Endpoint> endpoints, Map<String,String> configuration,
146147
ExecutorService sharedOperationExecutor, ColumnToMethodMatchingStrategy columnToMethodMatchingStrategy,
147-
Object metricsRegistry, Supplier<String> queryIdGenerator) {
148+
Object metricsRegistry, Supplier<String> queryIdGenerator,
149+
HttpRedirectPolicy httpRedirectPolicy) {
148150
this.configuration = ClientConfigProperties.parseConfigMap(configuration);
149151
this.readOnlyConfig = Collections.unmodifiableMap(configuration);
150152
this.metricsRegistry = metricsRegistry;
@@ -191,7 +193,7 @@ private Client(Collection<Endpoint> endpoints, Map<String,String> configuration,
191193
this.lz4Factory = LZ4Factory.fastestJavaInstance();
192194
}
193195

194-
this.httpClientHelper = new HttpAPIClientHelper(this.configuration, metricsRegistry, initSslContext, lz4Factory);
196+
this.httpClientHelper = new HttpAPIClientHelper(this.configuration, metricsRegistry, initSslContext, lz4Factory, httpRedirectPolicy);
195197
this.serverVersion = configuration.getOrDefault(ClientConfigProperties.SERVER_VERSION.getKey(), "unknown");
196198
this.dbUser = configuration.getOrDefault(ClientConfigProperties.USER.getKey(), ClientConfigProperties.USER.getDefObjVal());
197199
this.typeHintMapping = (Map<ClickHouseDataType, Class<?>>) this.configuration.get(ClientConfigProperties.TYPE_HINT_MAPPING.getKey());
@@ -264,6 +266,7 @@ public static class Builder {
264266
private ColumnToMethodMatchingStrategy columnToMethodMatchingStrategy;
265267
private Object metricRegistry = null;
266268
private Supplier<String> queryIdGenerator;
269+
private HttpRedirectPolicy httpRedirectPolicy;
267270

268271
public Builder() {
269272
this.endpoints = new HashSet<>();
@@ -704,6 +707,45 @@ public Builder setHttpCookiesEnabled(boolean enabled) {
704707
return this;
705708
}
706709

710+
/**
711+
* Sets which redirect status codes are allowed for redirect handling.
712+
* Redirect handling is enabled only when at least one status code is configured.
713+
* Supported values are: 301, 302, 303, 307, 308.
714+
* <p>
715+
* Security note: following redirects may send credentials and request payload
716+
* to another endpoint on cross-host redirects.
717+
*
718+
* @param statusCodes list of allowed redirect status codes
719+
* @return this builder instance
720+
*/
721+
public Builder setHttpAllowedRedirectCodes(int... statusCodes) {
722+
if (statusCodes == null || statusCodes.length == 0) {
723+
throw new IllegalArgumentException("At least one HTTP redirect status code is required");
724+
}
725+
StringJoiner joiner = new StringJoiner(VALUES_LIST_DELIMITER);
726+
for (int statusCode : statusCodes) {
727+
if (!ClientConfigProperties.isSupportedHttpRedirectStatus(statusCode)) {
728+
throw new IllegalArgumentException("Unsupported HTTP redirect status code: " + statusCode
729+
+ ". Supported values are: " + ClientConfigProperties.SUPPORTED_HTTP_REDIRECT_STATUS_CODES);
730+
}
731+
joiner.add(String.valueOf(statusCode));
732+
}
733+
this.configuration.put(ClientConfigProperties.HTTP_ALLOWED_REDIRECT_CODES.getKey(), joiner.toString());
734+
return this;
735+
}
736+
737+
/**
738+
* Sets HTTP redirect policy used by redirect strategy.
739+
* By default cross-origin redirects are not allowed.
740+
*
741+
* @param strategy redirect policy
742+
* @return this builder instance
743+
*/
744+
public Builder setHttpRedirectStrategy(HttpRedirectPolicy strategy) {
745+
this.httpRedirectPolicy = strategy;
746+
return this;
747+
}
748+
707749
/**
708750
* Defines path to the trust store file. It cannot be combined with
709751
* certificates. Either trust store or certificates should be used.
@@ -1152,7 +1194,7 @@ public Client build() {
11521194
}
11531195

11541196
return new Client(this.endpoints, this.configuration, this.sharedOperationExecutor,
1155-
this.columnToMethodMatchingStrategy, this.metricRegistry, this.queryIdGenerator);
1197+
this.columnToMethodMatchingStrategy, this.metricRegistry, this.queryIdGenerator, this.httpRedirectPolicy);
11561198
}
11571199
}
11581200

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

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
import org.slf4j.Logger;
88
import org.slf4j.LoggerFactory;
99

10-
import java.util.ArrayList;
1110
import java.util.Arrays;
1211
import java.util.Collection;
1312
import java.util.Collections;
@@ -17,6 +16,7 @@
1716
import java.util.List;
1817
import java.util.Locale;
1918
import java.util.Map;
19+
import java.util.Set;
2020
import java.util.TimeZone;
2121
import java.util.function.Consumer;
2222
import java.util.function.Function;
@@ -138,14 +138,7 @@ public enum ClientConfigProperties {
138138
ClientFaultCause.ConnectionRequestTimeout.name(), ClientFaultCause.ServerRetryable.name())) {
139139
@Override
140140
public Object parseValue(String value) {
141-
List<String> strValues = (List<String>) super.parseValue(value);
142-
List<ClientFaultCause> failures = new ArrayList<ClientFaultCause>();
143-
if (strValues != null) {
144-
for (String strValue : strValues) {
145-
failures.add(ClientFaultCause.valueOf(strValue));
146-
}
147-
}
148-
return failures;
141+
return parseStringAsList(value, ClientFaultCause::valueOf);
149142
}
150143
},
151144

@@ -170,6 +163,20 @@ public Object parseValue(String value) {
170163

171164
HTTP_SAVE_COOKIES("client.http.cookies_enabled", Boolean.class, "false"),
172165

166+
/**
167+
* Allowed HTTP redirect response codes for Apache HTTP Client redirect strategy.
168+
* Empty by default (redirects disabled).
169+
* <p>
170+
* Security note: following redirects may send credentials and request payload to another endpoint
171+
* if server/proxy returns a cross-host redirect.
172+
*/
173+
HTTP_ALLOWED_REDIRECT_CODES("client.http.allowed_redirect_codes", List.class, "") {
174+
@Override
175+
public Object parseValue(String value) {
176+
return parseStringAsList(value, ClientConfigProperties::parseAndValidateRedirectStatusCode);
177+
}
178+
},
179+
173180
BINARY_READER_USE_PREALLOCATED_BUFFERS("client_allow_binary_reader_to_reuse_buffers", Boolean.class, "false"),
174181

175182
/**
@@ -236,6 +243,9 @@ public <T> T getDefObjVal() {
236243

237244
public static final String SERVER_SETTING_PREFIX = "clickhouse_setting_";
238245

246+
public static final Set<Integer> SUPPORTED_HTTP_REDIRECT_STATUS_CODES = Collections.unmodifiableSet(
247+
new HashSet<Integer>(Arrays.asList(301, 302, 303, 307, 308)));
248+
239249
// Key used to identify default value in configuration map
240250
public static final String DEFAULT_KEY = "_default_";
241251

@@ -249,6 +259,10 @@ public static String httpHeader(String key) {
249259
return HTTP_HEADER_PREFIX + key.toUpperCase(Locale.US);
250260
}
251261

262+
public static boolean isSupportedHttpRedirectStatus(int statusCode) {
263+
return SUPPORTED_HTTP_REDIRECT_STATUS_CODES.contains(statusCode);
264+
}
265+
252266
public static String commaSeparated(Collection<?> values) {
253267
StringBuilder sb = new StringBuilder();
254268
for (Object value : values) {
@@ -262,11 +276,16 @@ public static String commaSeparated(Collection<?> values) {
262276
}
263277

264278
public static List<String> valuesFromCommaSeparated(String value) {
279+
return parseStringAsList(value, Function.identity());
280+
}
281+
282+
public static <T> List<T> parseStringAsList(String value, Function<String, T> transformation) {
265283
if (value == null || value.isEmpty()) {
266284
return Collections.emptyList();
267285
}
268-
269-
return Arrays.stream(value.split("(?<!\\\\),")).map(s -> s.replaceAll("\\\\,", ","))
286+
return Arrays.stream(value.split("(?<!\\\\),"))
287+
.map(s -> s.replaceAll("\\\\,", ","))
288+
.map(transformation)
270289
.collect(Collectors.toList());
271290
}
272291

@@ -294,7 +313,7 @@ public Object parseValue(String value) {
294313
}
295314

296315
if (valueType.equals(List.class)) {
297-
return valuesFromCommaSeparated(value);
316+
return parseStringAsList(value, Function.identity());
298317
}
299318

300319
if (valueType.isEnum()) {
@@ -461,4 +480,18 @@ public static Map<ClickHouseDataType, Class<?>> translateTypeHintMapping(String
461480
}
462481
return hintMapping;
463482
}
483+
484+
private static Integer parseAndValidateRedirectStatusCode(String strValue) {
485+
int statusCode;
486+
try {
487+
statusCode = Integer.parseInt(strValue);
488+
} catch (NumberFormatException e) {
489+
throw new IllegalArgumentException("HTTP redirect status code must be integer, but was: " + strValue, e);
490+
}
491+
if (!isSupportedHttpRedirectStatus(statusCode)) {
492+
throw new IllegalArgumentException("Unsupported HTTP redirect status code: " + statusCode
493+
+ ". Supported values are: " + SUPPORTED_HTTP_REDIRECT_STATUS_CODES);
494+
}
495+
return statusCode;
496+
}
464497
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package com.clickhouse.client.api.http;
2+
3+
import org.apache.hc.client5.http.impl.LaxRedirectStrategy;
4+
import org.apache.hc.client5.http.protocol.RedirectStrategy;
5+
import org.apache.hc.core5.http.HttpException;
6+
import org.apache.hc.core5.http.HttpRequest;
7+
import org.apache.hc.core5.http.HttpResponse;
8+
import org.apache.hc.core5.http.ProtocolException;
9+
import org.apache.hc.core5.http.protocol.HttpContext;
10+
11+
import java.net.URI;
12+
import java.net.URISyntaxException;
13+
import java.util.Collection;
14+
import java.util.Collections;
15+
import java.util.HashSet;
16+
import java.util.Set;
17+
18+
/**
19+
* Redirect strategy with support for:
20+
* <ul>
21+
* <li>restricting redirect status codes</li>
22+
* <li>optionally allowing cross-origin redirects</li>
23+
* <li>always blocking HTTP -&gt; HTTPS redirects</li>
24+
* </ul>
25+
*/
26+
public class CrossOriginAwareRedirectStrategy implements HttpRedirectPolicy, RedirectStrategy {
27+
private final RedirectStrategy delegate = LaxRedirectStrategy.INSTANCE;
28+
private final Set<Integer> allowedRedirectStatusCodes;
29+
private final boolean allowCrossOriginRedirects;
30+
31+
public CrossOriginAwareRedirectStrategy(boolean allowCrossOriginRedirects) {
32+
this(Collections.<Integer>emptyList(), allowCrossOriginRedirects);
33+
}
34+
35+
public CrossOriginAwareRedirectStrategy(Collection<Integer> allowedRedirectStatusCodes, boolean allowCrossOriginRedirects) {
36+
this.allowedRedirectStatusCodes = Collections.unmodifiableSet(new HashSet<Integer>(allowedRedirectStatusCodes));
37+
this.allowCrossOriginRedirects = allowCrossOriginRedirects;
38+
}
39+
40+
public CrossOriginAwareRedirectStrategy withAllowedRedirectStatusCodes(Collection<Integer> allowedRedirectStatusCodes) {
41+
return new CrossOriginAwareRedirectStrategy(allowedRedirectStatusCodes, allowCrossOriginRedirects);
42+
}
43+
44+
@Override
45+
public boolean isCrossOriginRedirectAllowed() {
46+
return allowCrossOriginRedirects;
47+
}
48+
49+
@Override
50+
public boolean isRedirected(HttpRequest request, HttpResponse response, HttpContext context) throws HttpException {
51+
if (!allowedRedirectStatusCodes.contains(response.getCode()) || !delegate.isRedirected(request, response, context)) {
52+
return false;
53+
}
54+
55+
URI requestUri = getRequestUri(request);
56+
URI redirectUri = delegate.getLocationURI(request, response, context);
57+
if (isHttpToHttpsRedirect(requestUri, redirectUri)) {
58+
return false;
59+
}
60+
if (!allowCrossOriginRedirects && !isSameOrigin(requestUri, redirectUri)) {
61+
return false;
62+
}
63+
return true;
64+
}
65+
66+
@Override
67+
public URI getLocationURI(HttpRequest request, HttpResponse response, HttpContext context) throws HttpException {
68+
return delegate.getLocationURI(request, response, context);
69+
}
70+
71+
private static URI getRequestUri(HttpRequest request) throws HttpException {
72+
try {
73+
return request.getUri();
74+
} catch (URISyntaxException e) {
75+
throw new ProtocolException("Failed to read request URI", e);
76+
}
77+
}
78+
79+
private static boolean isHttpToHttpsRedirect(URI source, URI target) {
80+
return "http".equalsIgnoreCase(source.getScheme()) && "https".equalsIgnoreCase(target.getScheme());
81+
}
82+
83+
private static boolean isSameOrigin(URI source, URI target) {
84+
if (source.getScheme() == null || source.getHost() == null
85+
|| target.getScheme() == null || target.getHost() == null) {
86+
return false;
87+
}
88+
return source.getScheme().equalsIgnoreCase(target.getScheme())
89+
&& source.getHost().equalsIgnoreCase(target.getHost());
90+
}
91+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.clickhouse.client.api.http;
2+
3+
/**
4+
* Controls high-level redirect policy for HTTP requests.
5+
*/
6+
public interface HttpRedirectPolicy {
7+
/**
8+
* @return true when cross-origin redirects are allowed
9+
*/
10+
boolean isCrossOriginRedirectAllowed();
11+
}

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

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
import com.clickhouse.client.api.DataTransferException;
1313
import com.clickhouse.client.api.ServerException;
1414
import com.clickhouse.client.api.enums.ProxyType;
15+
import com.clickhouse.client.api.http.CrossOriginAwareRedirectStrategy;
16+
import com.clickhouse.client.api.http.HttpRedirectPolicy;
1517
import com.clickhouse.client.api.http.ClickHouseHttpProto;
1618
import com.clickhouse.client.api.transport.Endpoint;
1719
import com.clickhouse.data.ClickHouseFormat;
@@ -131,9 +133,10 @@ public class HttpAPIClientHelper {
131133

132134
LZ4Factory lz4Factory;
133135

134-
public HttpAPIClientHelper(Map<String, Object> configuration, Object metricsRegistry, boolean initSslContext, LZ4Factory lz4Factory) {
136+
public HttpAPIClientHelper(Map<String, Object> configuration, Object metricsRegistry, boolean initSslContext,
137+
LZ4Factory lz4Factory, HttpRedirectPolicy httpRedirectPolicy) {
135138
this.metricsRegistry = metricsRegistry;
136-
this.httpClient = createHttpClient(initSslContext, configuration);
139+
this.httpClient = createHttpClient(initSslContext, configuration, httpRedirectPolicy);
137140
this.lz4Factory = lz4Factory;
138141
assert this.lz4Factory != null;
139142

@@ -266,7 +269,8 @@ private HttpClientConnectionManager poolConnectionManager(LayeredConnectionSocke
266269
return phccm;
267270
}
268271

269-
public CloseableHttpClient createHttpClient(boolean initSslContext, Map<String, Object> configuration) {
272+
public CloseableHttpClient createHttpClient(boolean initSslContext, Map<String, Object> configuration,
273+
HttpRedirectPolicy httpRedirectPolicy) {
270274
// Top Level builders
271275
HttpClientBuilder clientBuilder = HttpClientBuilder.create();
272276
SSLContext sslContext = initSslContext ? createSSLContext(configuration) : null;
@@ -338,6 +342,15 @@ public CloseableHttpClient createHttpClient(boolean initSslContext, Map<String,
338342

339343
clientBuilder.disableContentCompression(); // will handle ourselves
340344

345+
List<Integer> allowedRedirectCodes = ClientConfigProperties.HTTP_ALLOWED_REDIRECT_CODES.getOrDefault(configuration);
346+
if (allowedRedirectCodes.isEmpty()) {
347+
clientBuilder.disableRedirectHandling();
348+
} else {
349+
CrossOriginAwareRedirectStrategy strategy = new CrossOriginAwareRedirectStrategy(
350+
allowedRedirectCodes, httpRedirectPolicy != null && httpRedirectPolicy.isCrossOriginRedirectAllowed());
351+
clientBuilder.setRedirectStrategy(strategy);
352+
}
353+
341354
return clientBuilder.build();
342355
}
343356

@@ -1016,4 +1029,5 @@ protected void prepareSocket(SSLSocket socket, HttpContext context) throws IOExc
10161029
}
10171030
}
10181031
}
1032+
10191033
}

0 commit comments

Comments
 (0)