Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 45 additions & 3 deletions client-v2/src/main/java/com/clickhouse/client/api/Client.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import com.clickhouse.client.api.data_formats.internal.ProcessParser;
import com.clickhouse.client.api.enums.Protocol;
import com.clickhouse.client.api.enums.ProxyType;
import com.clickhouse.client.api.http.HttpRedirectPolicy;
import com.clickhouse.client.api.http.ClickHouseHttpProto;
import com.clickhouse.client.api.insert.InsertResponse;
import com.clickhouse.client.api.insert.InsertSettings;
Expand Down Expand Up @@ -144,7 +145,8 @@ public class Client implements AutoCloseable {

private Client(Collection<Endpoint> endpoints, Map<String,String> configuration,
ExecutorService sharedOperationExecutor, ColumnToMethodMatchingStrategy columnToMethodMatchingStrategy,
Object metricsRegistry, Supplier<String> queryIdGenerator) {
Object metricsRegistry, Supplier<String> queryIdGenerator,
HttpRedirectPolicy httpRedirectPolicy) {
this.configuration = ClientConfigProperties.parseConfigMap(configuration);
this.readOnlyConfig = Collections.unmodifiableMap(configuration);
this.metricsRegistry = metricsRegistry;
Expand Down Expand Up @@ -191,7 +193,7 @@ private Client(Collection<Endpoint> endpoints, Map<String,String> configuration,
this.lz4Factory = LZ4Factory.fastestJavaInstance();
}

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

public Builder() {
this.endpoints = new HashSet<>();
Expand Down Expand Up @@ -704,6 +707,45 @@ public Builder setHttpCookiesEnabled(boolean enabled) {
return this;
}

/**
* Sets which redirect status codes are allowed for redirect handling.
* Redirect handling is enabled only when at least one status code is configured.
* Supported values are: 301, 302, 303, 307, 308.
* <p>
* Security note: following redirects may send credentials and request payload
* to another endpoint on cross-host redirects.
*
* @param statusCodes list of allowed redirect status codes
* @return this builder instance
*/
public Builder setHttpAllowedRedirectCodes(int... statusCodes) {
if (statusCodes == null || statusCodes.length == 0) {
throw new IllegalArgumentException("At least one HTTP redirect status code is required");
}
StringJoiner joiner = new StringJoiner(VALUES_LIST_DELIMITER);
for (int statusCode : statusCodes) {
if (!ClientConfigProperties.isSupportedHttpRedirectStatus(statusCode)) {
throw new IllegalArgumentException("Unsupported HTTP redirect status code: " + statusCode
+ ". Supported values are: " + ClientConfigProperties.SUPPORTED_HTTP_REDIRECT_STATUS_CODES);
}
joiner.add(String.valueOf(statusCode));
}
this.configuration.put(ClientConfigProperties.HTTP_ALLOWED_REDIRECT_CODES.getKey(), joiner.toString());
return this;
}

/**
* Sets HTTP redirect policy used by redirect strategy.
* By default cross-origin redirects are not allowed.
*
* @param strategy redirect policy
* @return this builder instance
*/
public Builder setHttpRedirectStrategy(HttpRedirectPolicy strategy) {
this.httpRedirectPolicy = strategy;
return this;
}

/**
* Defines path to the trust store file. It cannot be combined with
* certificates. Either trust store or certificates should be used.
Expand Down Expand Up @@ -1152,7 +1194,7 @@ public Client build() {
}

return new Client(this.endpoints, this.configuration, this.sharedOperationExecutor,
this.columnToMethodMatchingStrategy, this.metricRegistry, this.queryIdGenerator);
this.columnToMethodMatchingStrategy, this.metricRegistry, this.queryIdGenerator, this.httpRedirectPolicy);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
Expand All @@ -17,6 +16,7 @@
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;
import java.util.function.Consumer;
import java.util.function.Function;
Expand Down Expand Up @@ -138,14 +138,7 @@
ClientFaultCause.ConnectionRequestTimeout.name(), ClientFaultCause.ServerRetryable.name())) {
@Override
public Object parseValue(String value) {
List<String> strValues = (List<String>) super.parseValue(value);
List<ClientFaultCause> failures = new ArrayList<ClientFaultCause>();
if (strValues != null) {
for (String strValue : strValues) {
failures.add(ClientFaultCause.valueOf(strValue));
}
}
return failures;
return parseStringAsList(value, ClientFaultCause::valueOf);
}
},

Expand All @@ -170,6 +163,20 @@

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

/**
* Allowed HTTP redirect response codes for Apache HTTP Client redirect strategy.
* Empty by default (redirects disabled).
* <p>
* Security note: following redirects may send credentials and request payload to another endpoint
* if server/proxy returns a cross-host redirect.
*/
HTTP_ALLOWED_REDIRECT_CODES("client.http.allowed_redirect_codes", List.class, "") {
@Override
public Object parseValue(String value) {
return parseStringAsList(value, ClientConfigProperties::parseAndValidateRedirectStatusCode);
}
},

BINARY_READER_USE_PREALLOCATED_BUFFERS("client_allow_binary_reader_to_reuse_buffers", Boolean.class, "false"),

/**
Expand Down Expand Up @@ -236,6 +243,9 @@

public static final String SERVER_SETTING_PREFIX = "clickhouse_setting_";

public static final Set<Integer> SUPPORTED_HTTP_REDIRECT_STATUS_CODES = Collections.unmodifiableSet(
new HashSet<Integer>(Arrays.asList(301, 302, 303, 307, 308)));

// Key used to identify default value in configuration map
public static final String DEFAULT_KEY = "_default_";

Expand All @@ -249,6 +259,10 @@
return HTTP_HEADER_PREFIX + key.toUpperCase(Locale.US);
}

public static boolean isSupportedHttpRedirectStatus(int statusCode) {
return SUPPORTED_HTTP_REDIRECT_STATUS_CODES.contains(statusCode);
}

public static String commaSeparated(Collection<?> values) {
StringBuilder sb = new StringBuilder();
for (Object value : values) {
Expand All @@ -262,11 +276,16 @@
}

public static List<String> valuesFromCommaSeparated(String value) {
return parseStringAsList(value, Function.identity());
}

public static <T> List<T> parseStringAsList(String value, Function<String, T> transformation) {
if (value == null || value.isEmpty()) {
return Collections.emptyList();
}

return Arrays.stream(value.split("(?<!\\\\),")).map(s -> s.replaceAll("\\\\,", ","))
return Arrays.stream(value.split("(?<!\\\\),"))
.map(s -> s.replaceAll("\\\\,", ","))

Check failure on line 287 in client-v2/src/main/java/com/clickhouse/client/api/ClientConfigProperties.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace this call to "replaceAll()" by a call to the "replace()" method.

See more on https://sonarcloud.io/project/issues?id=ClickHouse_clickhouse-java&issues=AZ0m_R8v7Lq6732fNgXx&open=AZ0m_R8v7Lq6732fNgXx&pullRequest=2806
.map(transformation)
.collect(Collectors.toList());
}

Expand Down Expand Up @@ -294,7 +313,7 @@
}

if (valueType.equals(List.class)) {
return valuesFromCommaSeparated(value);
return parseStringAsList(value, Function.identity());
}

if (valueType.isEnum()) {
Expand Down Expand Up @@ -461,4 +480,18 @@
}
return hintMapping;
}

private static Integer parseAndValidateRedirectStatusCode(String strValue) {
int statusCode;
try {
statusCode = Integer.parseInt(strValue);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("HTTP redirect status code must be integer, but was: " + strValue, e);
}
if (!isSupportedHttpRedirectStatus(statusCode)) {
throw new IllegalArgumentException("Unsupported HTTP redirect status code: " + statusCode
+ ". Supported values are: " + SUPPORTED_HTTP_REDIRECT_STATUS_CODES);
}
return statusCode;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package com.clickhouse.client.api.http;

import org.apache.hc.client5.http.impl.LaxRedirectStrategy;
import org.apache.hc.client5.http.protocol.RedirectStrategy;
import org.apache.hc.core5.http.HttpException;
import org.apache.hc.core5.http.HttpRequest;
import org.apache.hc.core5.http.HttpResponse;
import org.apache.hc.core5.http.ProtocolException;
import org.apache.hc.core5.http.protocol.HttpContext;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

/**
* Redirect strategy with support for:
* <ul>
* <li>restricting redirect status codes</li>
* <li>optionally allowing cross-origin redirects</li>
* <li>always blocking HTTP -&gt; HTTPS redirects</li>
* </ul>
*/
public class CrossOriginAwareRedirectStrategy implements HttpRedirectPolicy, RedirectStrategy {
private final RedirectStrategy delegate = LaxRedirectStrategy.INSTANCE;

Check warning on line 27 in client-v2/src/main/java/com/clickhouse/client/api/http/CrossOriginAwareRedirectStrategy.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Make this final field static too.

See more on https://sonarcloud.io/project/issues?id=ClickHouse_clickhouse-java&issues=AZ0m_R8b7Lq6732fNgXv&open=AZ0m_R8b7Lq6732fNgXv&pullRequest=2806
private final Set<Integer> allowedRedirectStatusCodes;
private final boolean allowCrossOriginRedirects;

public CrossOriginAwareRedirectStrategy(boolean allowCrossOriginRedirects) {
this(Collections.<Integer>emptyList(), allowCrossOriginRedirects);
}

public CrossOriginAwareRedirectStrategy(Collection<Integer> allowedRedirectStatusCodes, boolean allowCrossOriginRedirects) {
this.allowedRedirectStatusCodes = Collections.unmodifiableSet(new HashSet<Integer>(allowedRedirectStatusCodes));
this.allowCrossOriginRedirects = allowCrossOriginRedirects;
}

public CrossOriginAwareRedirectStrategy withAllowedRedirectStatusCodes(Collection<Integer> allowedRedirectStatusCodes) {
return new CrossOriginAwareRedirectStrategy(allowedRedirectStatusCodes, allowCrossOriginRedirects);
}

@Override
public boolean isCrossOriginRedirectAllowed() {
return allowCrossOriginRedirects;
}

@Override
public boolean isRedirected(HttpRequest request, HttpResponse response, HttpContext context) throws HttpException {
if (!allowedRedirectStatusCodes.contains(response.getCode()) || !delegate.isRedirected(request, response, context)) {
return false;
}

URI requestUri = getRequestUri(request);
URI redirectUri = delegate.getLocationURI(request, response, context);
if (isHttpToHttpsRedirect(requestUri, redirectUri)) {
return false;
}
if (!allowCrossOriginRedirects && !isSameOrigin(requestUri, redirectUri)) {

Check warning on line 60 in client-v2/src/main/java/com/clickhouse/client/api/http/CrossOriginAwareRedirectStrategy.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace this if-then-else statement by a single return statement.

See more on https://sonarcloud.io/project/issues?id=ClickHouse_clickhouse-java&issues=AZ0m_R8b7Lq6732fNgXw&open=AZ0m_R8b7Lq6732fNgXw&pullRequest=2806
return false;
}
return true;
}

@Override
public URI getLocationURI(HttpRequest request, HttpResponse response, HttpContext context) throws HttpException {
return delegate.getLocationURI(request, response, context);
}

private static URI getRequestUri(HttpRequest request) throws HttpException {
try {
return request.getUri();
} catch (URISyntaxException e) {
throw new ProtocolException("Failed to read request URI", e);
}
}

private static boolean isHttpToHttpsRedirect(URI source, URI target) {
return "http".equalsIgnoreCase(source.getScheme()) && "https".equalsIgnoreCase(target.getScheme());
}

private static boolean isSameOrigin(URI source, URI target) {
if (source.getScheme() == null || source.getHost() == null
|| target.getScheme() == null || target.getHost() == null) {
return false;
}
return source.getScheme().equalsIgnoreCase(target.getScheme())
&& source.getHost().equalsIgnoreCase(target.getHost());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.clickhouse.client.api.http;

/**
* Controls high-level redirect policy for HTTP requests.
*/
public interface HttpRedirectPolicy {
/**
* @return true when cross-origin redirects are allowed
*/
boolean isCrossOriginRedirectAllowed();
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
import com.clickhouse.client.api.DataTransferException;
import com.clickhouse.client.api.ServerException;
import com.clickhouse.client.api.enums.ProxyType;
import com.clickhouse.client.api.http.CrossOriginAwareRedirectStrategy;
import com.clickhouse.client.api.http.HttpRedirectPolicy;
import com.clickhouse.client.api.http.ClickHouseHttpProto;
import com.clickhouse.client.api.transport.Endpoint;
import com.clickhouse.data.ClickHouseFormat;
Expand Down Expand Up @@ -131,9 +133,10 @@

LZ4Factory lz4Factory;

public HttpAPIClientHelper(Map<String, Object> configuration, Object metricsRegistry, boolean initSslContext, LZ4Factory lz4Factory) {
public HttpAPIClientHelper(Map<String, Object> configuration, Object metricsRegistry, boolean initSslContext,
LZ4Factory lz4Factory, HttpRedirectPolicy httpRedirectPolicy) {
this.metricsRegistry = metricsRegistry;
this.httpClient = createHttpClient(initSslContext, configuration);
this.httpClient = createHttpClient(initSslContext, configuration, httpRedirectPolicy);
this.lz4Factory = lz4Factory;
assert this.lz4Factory != null;

Expand Down Expand Up @@ -266,7 +269,8 @@
return phccm;
}

public CloseableHttpClient createHttpClient(boolean initSslContext, Map<String, Object> configuration) {
public CloseableHttpClient createHttpClient(boolean initSslContext, Map<String, Object> configuration,

Check failure on line 272 in client-v2/src/main/java/com/clickhouse/client/api/internal/HttpAPIClientHelper.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 23 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=ClickHouse_clickhouse-java&issues=AZ0m_R707Lq6732fNgXu&open=AZ0m_R707Lq6732fNgXu&pullRequest=2806
HttpRedirectPolicy httpRedirectPolicy) {
// Top Level builders
HttpClientBuilder clientBuilder = HttpClientBuilder.create();
SSLContext sslContext = initSslContext ? createSSLContext(configuration) : null;
Expand Down Expand Up @@ -338,6 +342,15 @@

clientBuilder.disableContentCompression(); // will handle ourselves

List<Integer> allowedRedirectCodes = ClientConfigProperties.HTTP_ALLOWED_REDIRECT_CODES.getOrDefault(configuration);
if (allowedRedirectCodes.isEmpty()) {
clientBuilder.disableRedirectHandling();
} else {
CrossOriginAwareRedirectStrategy strategy = new CrossOriginAwareRedirectStrategy(
allowedRedirectCodes, httpRedirectPolicy != null && httpRedirectPolicy.isCrossOriginRedirectAllowed());
clientBuilder.setRedirectStrategy(strategy);
}

return clientBuilder.build();
}

Expand Down Expand Up @@ -1016,4 +1029,5 @@
}
}
}

}
Loading
Loading