From 32ddad37098ad0d203a13fa5ceff77296d89f271 Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Mon, 23 Mar 2026 21:57:14 -0700 Subject: [PATCH] Disabled handling redirects by default. Implemented configuration for redirect --- .../com/clickhouse/client/api/Client.java | 48 ++++- .../client/api/ClientConfigProperties.java | 57 ++++-- .../CrossOriginAwareRedirectStrategy.java | 91 +++++++++ .../client/api/http/HttpRedirectPolicy.java | 11 ++ .../api/internal/HttpAPIClientHelper.java | 20 +- .../com/clickhouse/client/ClientTests.java | 35 +++- .../clickhouse/client/HttpTransportTests.java | 179 ++++++++++++++++++ 7 files changed, 420 insertions(+), 21 deletions(-) create mode 100644 client-v2/src/main/java/com/clickhouse/client/api/http/CrossOriginAwareRedirectStrategy.java create mode 100644 client-v2/src/main/java/com/clickhouse/client/api/http/HttpRedirectPolicy.java diff --git a/client-v2/src/main/java/com/clickhouse/client/api/Client.java b/client-v2/src/main/java/com/clickhouse/client/api/Client.java index 73ec21155..36a0c6752 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/Client.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/Client.java @@ -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; @@ -144,7 +145,8 @@ public class Client implements AutoCloseable { private Client(Collection endpoints, Map configuration, ExecutorService sharedOperationExecutor, ColumnToMethodMatchingStrategy columnToMethodMatchingStrategy, - Object metricsRegistry, Supplier queryIdGenerator) { + Object metricsRegistry, Supplier queryIdGenerator, + HttpRedirectPolicy httpRedirectPolicy) { this.configuration = ClientConfigProperties.parseConfigMap(configuration); this.readOnlyConfig = Collections.unmodifiableMap(configuration); this.metricsRegistry = metricsRegistry; @@ -191,7 +193,7 @@ private Client(Collection endpoints, Map 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>) this.configuration.get(ClientConfigProperties.TYPE_HINT_MAPPING.getKey()); @@ -264,6 +266,7 @@ public static class Builder { private ColumnToMethodMatchingStrategy columnToMethodMatchingStrategy; private Object metricRegistry = null; private Supplier queryIdGenerator; + private HttpRedirectPolicy httpRedirectPolicy; public Builder() { this.endpoints = new HashSet<>(); @@ -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. + *

+ * 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. @@ -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); } } diff --git a/client-v2/src/main/java/com/clickhouse/client/api/ClientConfigProperties.java b/client-v2/src/main/java/com/clickhouse/client/api/ClientConfigProperties.java index e548a90f9..802450723 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/ClientConfigProperties.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/ClientConfigProperties.java @@ -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; @@ -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; @@ -138,14 +138,7 @@ public enum ClientConfigProperties { ClientFaultCause.ConnectionRequestTimeout.name(), ClientFaultCause.ServerRetryable.name())) { @Override public Object parseValue(String value) { - List strValues = (List) super.parseValue(value); - List failures = new ArrayList(); - if (strValues != null) { - for (String strValue : strValues) { - failures.add(ClientFaultCause.valueOf(strValue)); - } - } - return failures; + return parseStringAsList(value, ClientFaultCause::valueOf); } }, @@ -170,6 +163,20 @@ public Object parseValue(String value) { 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). + *

+ * 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"), /** @@ -236,6 +243,9 @@ public T getDefObjVal() { public static final String SERVER_SETTING_PREFIX = "clickhouse_setting_"; + public static final Set SUPPORTED_HTTP_REDIRECT_STATUS_CODES = Collections.unmodifiableSet( + new HashSet(Arrays.asList(301, 302, 303, 307, 308))); + // Key used to identify default value in configuration map public static final String DEFAULT_KEY = "_default_"; @@ -249,6 +259,10 @@ public static String httpHeader(String key) { 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) { @@ -262,11 +276,16 @@ public static String commaSeparated(Collection values) { } public static List valuesFromCommaSeparated(String value) { + return parseStringAsList(value, Function.identity()); + } + + public static List parseStringAsList(String value, Function transformation) { if (value == null || value.isEmpty()) { return Collections.emptyList(); } - - return Arrays.stream(value.split("(? s.replaceAll("\\\\,", ",")) + return Arrays.stream(value.split("(? s.replaceAll("\\\\,", ",")) + .map(transformation) .collect(Collectors.toList()); } @@ -294,7 +313,7 @@ public Object parseValue(String value) { } if (valueType.equals(List.class)) { - return valuesFromCommaSeparated(value); + return parseStringAsList(value, Function.identity()); } if (valueType.isEnum()) { @@ -461,4 +480,18 @@ public static Map> translateTypeHintMapping(String } 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; + } } diff --git a/client-v2/src/main/java/com/clickhouse/client/api/http/CrossOriginAwareRedirectStrategy.java b/client-v2/src/main/java/com/clickhouse/client/api/http/CrossOriginAwareRedirectStrategy.java new file mode 100644 index 000000000..b24250a64 --- /dev/null +++ b/client-v2/src/main/java/com/clickhouse/client/api/http/CrossOriginAwareRedirectStrategy.java @@ -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: + *

    + *
  • restricting redirect status codes
  • + *
  • optionally allowing cross-origin redirects
  • + *
  • always blocking HTTP -> HTTPS redirects
  • + *
+ */ +public class CrossOriginAwareRedirectStrategy implements HttpRedirectPolicy, RedirectStrategy { + private final RedirectStrategy delegate = LaxRedirectStrategy.INSTANCE; + private final Set allowedRedirectStatusCodes; + private final boolean allowCrossOriginRedirects; + + public CrossOriginAwareRedirectStrategy(boolean allowCrossOriginRedirects) { + this(Collections.emptyList(), allowCrossOriginRedirects); + } + + public CrossOriginAwareRedirectStrategy(Collection allowedRedirectStatusCodes, boolean allowCrossOriginRedirects) { + this.allowedRedirectStatusCodes = Collections.unmodifiableSet(new HashSet(allowedRedirectStatusCodes)); + this.allowCrossOriginRedirects = allowCrossOriginRedirects; + } + + public CrossOriginAwareRedirectStrategy withAllowedRedirectStatusCodes(Collection 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)) { + 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()); + } +} diff --git a/client-v2/src/main/java/com/clickhouse/client/api/http/HttpRedirectPolicy.java b/client-v2/src/main/java/com/clickhouse/client/api/http/HttpRedirectPolicy.java new file mode 100644 index 000000000..9aa5fcf21 --- /dev/null +++ b/client-v2/src/main/java/com/clickhouse/client/api/http/HttpRedirectPolicy.java @@ -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(); +} diff --git a/client-v2/src/main/java/com/clickhouse/client/api/internal/HttpAPIClientHelper.java b/client-v2/src/main/java/com/clickhouse/client/api/internal/HttpAPIClientHelper.java index 76e2dec93..8dd811093 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/internal/HttpAPIClientHelper.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/internal/HttpAPIClientHelper.java @@ -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; @@ -131,9 +133,10 @@ public class HttpAPIClientHelper { LZ4Factory lz4Factory; - public HttpAPIClientHelper(Map configuration, Object metricsRegistry, boolean initSslContext, LZ4Factory lz4Factory) { + public HttpAPIClientHelper(Map 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; @@ -266,7 +269,8 @@ private HttpClientConnectionManager poolConnectionManager(LayeredConnectionSocke return phccm; } - public CloseableHttpClient createHttpClient(boolean initSslContext, Map configuration) { + public CloseableHttpClient createHttpClient(boolean initSslContext, Map configuration, + HttpRedirectPolicy httpRedirectPolicy) { // Top Level builders HttpClientBuilder clientBuilder = HttpClientBuilder.create(); SSLContext sslContext = initSslContext ? createSSLContext(configuration) : null; @@ -338,6 +342,15 @@ public CloseableHttpClient createHttpClient(boolean initSslContext, Map 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(); } @@ -1016,4 +1029,5 @@ protected void prepareSocket(SSLSocket socket, HttpContext context) throws IOExc } } } + } diff --git a/client-v2/src/test/java/com/clickhouse/client/ClientTests.java b/client-v2/src/test/java/com/clickhouse/client/ClientTests.java index d63f9b2cb..9a2450ef4 100644 --- a/client-v2/src/test/java/com/clickhouse/client/ClientTests.java +++ b/client-v2/src/test/java/com/clickhouse/client/ClientTests.java @@ -260,7 +260,7 @@ public void testDefaultSettings() { Assert.assertEquals(config.get(p.getKey()), p.getDefaultValue(), "Default value doesn't match"); } } - Assert.assertEquals(config.size(), 34); // to check everything is set. Increment when new added. + Assert.assertEquals(config.size(), 35); // to check everything is set. Increment when new added. } try (Client client = new Client.Builder() @@ -288,12 +288,13 @@ public void testDefaultSettings() { .compressServerResponse(false) .useHttpCompression(true) .appCompressedData(true) + .setHttpAllowedRedirectCodes(301, 302, 307) .setSocketTimeout(20, SECONDS) .setSocketRcvbuf(100000) .setSocketSndbuf(100000) .build()) { Map config = client.getConfiguration(); - Assert.assertEquals(config.size(), 35); // to check everything is set. Increment when new added. + Assert.assertEquals(config.size(), 36); // to check everything is set. Increment when new added. Assert.assertEquals(config.get(ClientConfigProperties.DATABASE.getKey()), "mydb"); Assert.assertEquals(config.get(ClientConfigProperties.MAX_EXECUTION_TIME.getKey()), "10"); Assert.assertEquals(config.get(ClientConfigProperties.COMPRESSION_LZ4_UNCOMPRESSED_BUF_SIZE.getKey()), "300000"); @@ -314,6 +315,7 @@ public void testDefaultSettings() { Assert.assertEquals(config.get(ClientConfigProperties.COMPRESS_SERVER_RESPONSE.getKey()), "false"); Assert.assertEquals(config.get(ClientConfigProperties.USE_HTTP_COMPRESSION.getKey()), "true"); Assert.assertEquals(config.get(ClientConfigProperties.APP_COMPRESSED_DATA.getKey()), "true"); + Assert.assertEquals(config.get(ClientConfigProperties.HTTP_ALLOWED_REDIRECT_CODES.getKey()), "301,302,307"); Assert.assertEquals(config.get(ClientConfigProperties.SOCKET_OPERATION_TIMEOUT.getKey()), "20000"); Assert.assertEquals(config.get(ClientConfigProperties.SOCKET_RCVBUF_OPT.getKey()), "100000"); Assert.assertEquals(config.get(ClientConfigProperties.SOCKET_SNDBUF_OPT.getKey()), "100000"); @@ -360,7 +362,34 @@ public void testWithOldDefaults() { Assert.assertEquals(config.get(p.getKey()), p.getDefaultValue(), "Default value doesn't match"); } } - Assert.assertEquals(config.size(), 34); // to check everything is set. Increment when new added. + Assert.assertEquals(config.size(), 35); // to check everything is set. Increment when new added. + } + } + + @Test(groups = {"integration"}) + public void testRedirectOptionsValidation() { + try { + newClient().setHttpAllowedRedirectCodes(305); + Assert.fail("Exception expected"); + } catch (IllegalArgumentException ex) { + Assert.assertTrue(ex.getMessage().contains("Unsupported HTTP redirect status code")); + } + + try { + newClient().setOption(ClientConfigProperties.HTTP_ALLOWED_REDIRECT_CODES.getKey(), "301,999").build(); + Assert.fail("Exception expected"); + } catch (IllegalArgumentException ex) { + Assert.assertTrue(ex.getMessage().contains("Unsupported HTTP redirect status code")); + } + } + + @Test(groups = {"integration"}) + public void testRedirectOptionsPositive() { + try (Client client = newClient() + .setHttpAllowedRedirectCodes(301, 303, 308) + .build()) { + Map config = client.getConfiguration(); + Assert.assertEquals(config.get(ClientConfigProperties.HTTP_ALLOWED_REDIRECT_CODES.getKey()), "301,303,308"); } } diff --git a/client-v2/src/test/java/com/clickhouse/client/HttpTransportTests.java b/client-v2/src/test/java/com/clickhouse/client/HttpTransportTests.java index fc2d13a86..9e0916f47 100644 --- a/client-v2/src/test/java/com/clickhouse/client/HttpTransportTests.java +++ b/client-v2/src/test/java/com/clickhouse/client/HttpTransportTests.java @@ -11,6 +11,8 @@ import com.clickhouse.client.api.command.CommandSettings; import com.clickhouse.client.api.enums.Protocol; import com.clickhouse.client.api.enums.ProxyType; +import com.clickhouse.client.api.http.ClickHouseHttpProto; +import com.clickhouse.client.api.http.CrossOriginAwareRedirectStrategy; import com.clickhouse.client.api.insert.InsertResponse; import com.clickhouse.client.api.internal.DataTypeConverter; import com.clickhouse.client.api.internal.ServerSettings; @@ -1188,6 +1190,183 @@ public void testEndpointUrlPathIsPreserved() throws Exception { } } + @Test(groups = {"integration"}, dataProvider = "testHttpRedirectOptionsProvider") + public void testHttpRedirectOptions(int[] allowedCodes, boolean shouldRedirect) throws Exception { + if (isCloud()) { + return; // mocked server + } + + int serverPort = new Random().nextInt(1000) + 10000; + WireMockServer mockServer = new WireMockServer(WireMockConfiguration + .options().port(serverPort) + .notifier(new Slf4jNotifier(true))); + mockServer.start(); + + final String proxyName = "db-proxy"; + final String dbServer = "db-server"; + try { + mockServer.addStubMapping(WireMock.post(WireMock.urlPathEqualTo("/")) + .willReturn(WireMock.aResponse() + .withStatus(HttpStatus.SC_TEMPORARY_REDIRECT) + .withHeader(ClickHouseHttpProto.HEADER_SRV_DISPLAY_NAME, proxyName) + .withHeader(HttpHeaders.LOCATION, "/redirected")) + .build()); + + mockServer.addStubMapping(WireMock.post(WireMock.urlPathEqualTo("/redirected")) + .willReturn(WireMock.aResponse() + .withStatus(HttpStatus.SC_OK) + .withHeader(ClickHouseHttpProto.HEADER_SRV_DISPLAY_NAME, dbServer) + .withHeader(ClickHouseHttpProto.HEADER_SRV_SUMMARY, "{ \"read_bytes\": \"10\", \"read_rows\": \"1\"}") + .withBody("Ok.\n")) + .build()); + + Client.Builder builder = new Client.Builder() + .addEndpoint("http://localhost:" + serverPort + "/") + .setUsername("default") + .setPassword(ClickHouseServerForTest.getPassword()) + .compressServerResponse(false); + + if (allowedCodes != null) { + builder.setHttpAllowedRedirectCodes(allowedCodes); + } + + try (Client client = builder.build()) { + try (CommandResponse response = client.execute("SELECT 1").get(10, TimeUnit.SECONDS)) { + Assert.assertEquals(response.getResponseHeaders().get(ClickHouseHttpProto.HEADER_SRV_DISPLAY_NAME), + shouldRedirect ? dbServer : proxyName); + } + } + + mockServer.verify(shouldRedirect ? 1 : 0, WireMock.postRequestedFor(WireMock.urlPathEqualTo("/redirected"))); + } finally { + mockServer.stop(); + } + } + + @DataProvider(name = "testHttpRedirectOptionsProvider") + public static Object[][] testHttpRedirectOptionsProvider() { + return new Object[][]{ + {null, false}, + {new int[]{307}, true}, + {new int[]{308}, false}, + }; + } + + @Test(groups = {"integration"}) + public void testCrossOriginRedirectPolicy() throws Exception { + if (isCloud()) { + return; // mocked server + } + + final String proxyName = "db-proxy"; + final String dbServer = "db-server"; + WireMockServer sourceServer = new WireMockServer(WireMockConfiguration + .options().dynamicPort() + .notifier(new Slf4jNotifier(true))); + WireMockServer targetServer = new WireMockServer(WireMockConfiguration + .options().dynamicPort() + .notifier(new Slf4jNotifier(true))); + sourceServer.start(); + targetServer.start(); + + try { + sourceServer.addStubMapping(WireMock.post(WireMock.anyUrl()) + .willReturn(WireMock.aResponse() + .withStatus(HttpStatus.SC_TEMPORARY_REDIRECT) + .withHeader(ClickHouseHttpProto.HEADER_SRV_DISPLAY_NAME, proxyName) + .withHeader(HttpHeaders.LOCATION, "http://127.0.0.1:" + targetServer.port() + "/redirected")) + .build()); + targetServer.addStubMapping(WireMock.post(WireMock.urlPathEqualTo("/redirected")) + .willReturn(WireMock.aResponse() + .withStatus(HttpStatus.SC_OK) + .withHeader(ClickHouseHttpProto.HEADER_SRV_DISPLAY_NAME, dbServer) + .withHeader(ClickHouseHttpProto.HEADER_SRV_SUMMARY, "{ \"read_bytes\": \"10\", \"read_rows\": \"1\"}") + .withBody("Ok.\n")) + .build()); + + Object[][] scenarios = new Object[][]{ + {null, proxyName, 0}, + {Boolean.FALSE, proxyName, 0}, + {Boolean.TRUE, dbServer, 1} + }; + for (Object[] scenario : scenarios) { + Boolean allowCrossOrigin = (Boolean) scenario[0]; + String expectedDisplayName = (String) scenario[1]; + int expectedRedirectCalls = (int) scenario[2]; + + Client.Builder builder = new Client.Builder() + .addEndpoint("http://localhost:" + sourceServer.port() + "/") + .setUsername("default") + .setPassword(ClickHouseServerForTest.getPassword()) + .compressServerResponse(false) + .setHttpAllowedRedirectCodes(307); + if (allowCrossOrigin != null) { + builder.setHttpRedirectStrategy(new CrossOriginAwareRedirectStrategy(allowCrossOrigin.booleanValue())); + } + + try (Client client = builder.build()) { + try (CommandResponse response = client.execute("SELECT 1").get(10, TimeUnit.SECONDS)) { + Assert.assertEquals(response.getResponseHeaders().get(ClickHouseHttpProto.HEADER_SRV_DISPLAY_NAME), + expectedDisplayName); + } + } + targetServer.verify(expectedRedirectCalls, WireMock.postRequestedFor(WireMock.urlPathEqualTo("/redirected"))); + } + } finally { + sourceServer.stop(); + targetServer.stop(); + } + } + + @Test(groups = {"integration"}) + public void testCrossOriginRedirectPolicyWithCustomStrategyAndNoAllowedCodes() throws Exception { + if (isCloud()) { + return; // mocked server + } + + final String proxyName = "db-proxy"; + WireMockServer sourceServer = new WireMockServer(WireMockConfiguration + .options().dynamicPort() + .notifier(new Slf4jNotifier(true))); + WireMockServer targetServer = new WireMockServer(WireMockConfiguration + .options().dynamicPort() + .notifier(new Slf4jNotifier(true))); + sourceServer.start(); + targetServer.start(); + + try { + sourceServer.addStubMapping(WireMock.post(WireMock.anyUrl()) + .willReturn(WireMock.aResponse() + .withStatus(HttpStatus.SC_TEMPORARY_REDIRECT) + .withHeader(ClickHouseHttpProto.HEADER_SRV_DISPLAY_NAME, proxyName) + .withHeader(HttpHeaders.LOCATION, "http://127.0.0.1:" + targetServer.port() + "/redirected")) + .build()); + targetServer.addStubMapping(WireMock.post(WireMock.urlPathEqualTo("/redirected")) + .willReturn(WireMock.aResponse() + .withStatus(HttpStatus.SC_OK) + .withHeader(ClickHouseHttpProto.HEADER_SRV_DISPLAY_NAME, "db-server") + .withHeader(ClickHouseHttpProto.HEADER_SRV_SUMMARY, "{ \"read_bytes\": \"10\", \"read_rows\": \"1\"}") + .withBody("Ok.\n")) + .build()); + + try (Client client = new Client.Builder() + .addEndpoint("http://localhost:" + sourceServer.port() + "/") + .setUsername("default") + .setPassword(ClickHouseServerForTest.getPassword()) + .compressServerResponse(false) + .setHttpRedirectStrategy(new CrossOriginAwareRedirectStrategy(true)) + .build()) { + try (CommandResponse response = client.execute("SELECT 1").get(10, TimeUnit.SECONDS)) { + Assert.assertEquals(response.getResponseHeaders().get(ClickHouseHttpProto.HEADER_SRV_DISPLAY_NAME), proxyName); + } + } + targetServer.verify(0, WireMock.postRequestedFor(WireMock.urlPathEqualTo("/redirected"))); + } finally { + sourceServer.stop(); + targetServer.stop(); + } + } + @Test(groups = {"integration"}) public void testMultiPartRequest() { final Map params = new HashMap<>();