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 e4bb92590..94af7a11e 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 @@ -1,7 +1,16 @@ package com.clickhouse.client.api.internal; import com.clickhouse.client.ClickHouseSslContextProvider; -import com.clickhouse.client.api.*; +import com.clickhouse.client.api.ClickHouseException; +import com.clickhouse.client.api.Client; +import com.clickhouse.client.api.ClientConfigProperties; +import com.clickhouse.client.api.ClientException; +import com.clickhouse.client.api.ClientFaultCause; +import com.clickhouse.client.api.ClientMisconfigurationException; +import com.clickhouse.client.api.ConnectionInitiationException; +import com.clickhouse.client.api.ConnectionReuseStrategy; +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.ClickHouseHttpProto; import com.clickhouse.client.api.transport.Endpoint; @@ -63,6 +72,7 @@ import java.net.SocketTimeoutException; import java.net.URI; import java.net.URISyntaxException; +import java.net.URLEncoder; import java.net.UnknownHostException; import java.nio.charset.StandardCharsets; import java.security.NoSuchAlgorithmException; @@ -79,12 +89,16 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; import java.util.function.Function; +import java.util.regex.Pattern; public class HttpAPIClientHelper { private static final Logger LOG = LoggerFactory.getLogger(Client.class); private static final int ERROR_BODY_BUFFER_SIZE = 1024; // Error messages are usually small + private static final Pattern PATTERN_HEADER_VALUE_ASCII = Pattern.compile( + "\\p{Graph}+(?:[ ]\\p{Graph}+)*"); + private final CloseableHttpClient httpClient; private final RequestConfig baseRequestConfig; @@ -287,7 +301,7 @@ public CloseableHttpClient createHttpClient(boolean initSslContext, MapgetOrDefault(configuration)) { clientBuilder.setConnectionManager(poolConnectionManager(sslConnectionSocketFactory, socketConfig, configuration)); } else { clientBuilder.setConnectionManager(basicConnectionManager(sslConnectionSocketFactory, socketConfig, configuration)); @@ -430,36 +444,55 @@ public ClassicHttpResponse executeRequest(Endpoint server, Map r private static final ContentType CONTENT_TYPE = ContentType.create(ContentType.TEXT_PLAIN.getMimeType(), "UTF-8"); private void addHeaders(HttpPost req, Map requestConfig) { - req.addHeader(HttpHeaders.CONTENT_TYPE, CONTENT_TYPE.getMimeType()); + addHeader(req, HttpHeaders.CONTENT_TYPE, CONTENT_TYPE.getMimeType()); if (requestConfig.containsKey(ClientConfigProperties.INPUT_OUTPUT_FORMAT.getKey())) { - req.addHeader(ClickHouseHttpProto.HEADER_FORMAT, requestConfig.get(ClientConfigProperties.INPUT_OUTPUT_FORMAT.getKey())); + addHeader( + req, + ClickHouseHttpProto.HEADER_FORMAT, + requestConfig.get(ClientConfigProperties.INPUT_OUTPUT_FORMAT.getKey())); } - if (requestConfig.containsKey(ClientConfigProperties.QUERY_ID.getKey())) { - req.addHeader(ClickHouseHttpProto.HEADER_QUERY_ID, requestConfig.get(ClientConfigProperties.QUERY_ID.getKey()).toString()); - } - - - req.addHeader(ClickHouseHttpProto.HEADER_DATABASE, ClientConfigProperties.DATABASE.getOrDefault(requestConfig)); - - - if (ClientConfigProperties.SSL_AUTH.getOrDefault(requestConfig)) { - req.addHeader(ClickHouseHttpProto.HEADER_DB_USER, ClientConfigProperties.USER.getOrDefault(requestConfig)); - req.addHeader(ClickHouseHttpProto.HEADER_SSL_CERT_AUTH, "on"); - } else if (ClientConfigProperties.HTTP_USE_BASIC_AUTH.getOrDefault(requestConfig)) { + addHeader( + req, + ClickHouseHttpProto.HEADER_QUERY_ID, + requestConfig.get(ClientConfigProperties.QUERY_ID.getKey())); + } + addHeader( + req, + ClickHouseHttpProto.HEADER_DATABASE, + ClientConfigProperties.DATABASE.getOrDefault(requestConfig)); + + if (ClientConfigProperties.SSL_AUTH.getOrDefault(requestConfig).booleanValue()) { + addHeader( + req, + ClickHouseHttpProto.HEADER_DB_USER, + ClientConfigProperties.USER.getOrDefault(requestConfig)); + addHeader( + req, + ClickHouseHttpProto.HEADER_SSL_CERT_AUTH, + "on"); + } else if (ClientConfigProperties.HTTP_USE_BASIC_AUTH.getOrDefault(requestConfig).booleanValue()) { String user = ClientConfigProperties.USER.getOrDefault(requestConfig); String password = ClientConfigProperties.PASSWORD.getOrDefault(requestConfig); - - req.addHeader(HttpHeaders.AUTHORIZATION, "Basic " + Base64.getEncoder().encodeToString( - (user + ":" + password).getBytes(StandardCharsets.UTF_8)) - ); + // Use as-is, no encoding allowed + req.addHeader( + HttpHeaders.AUTHORIZATION, + "Basic " + Base64.getEncoder().encodeToString( + (user + ":" + password).getBytes(StandardCharsets.UTF_8))); } else { - req.addHeader(ClickHouseHttpProto.HEADER_DB_USER, ClientConfigProperties.USER.getOrDefault(requestConfig)); - req.addHeader(ClickHouseHttpProto.HEADER_DB_PASSWORD, ClientConfigProperties.PASSWORD.getOrDefault(requestConfig)); - + addHeader( + req, + ClickHouseHttpProto.HEADER_DB_USER, + ClientConfigProperties.USER.getOrDefault(requestConfig)); + addHeader( + req, + ClickHouseHttpProto.HEADER_DB_PASSWORD, + ClientConfigProperties.PASSWORD.getOrDefault(requestConfig)); } if (proxyAuthHeaderValue != null) { - req.addHeader(HttpHeaders.PROXY_AUTHORIZATION, proxyAuthHeaderValue); + req.addHeader( + HttpHeaders.PROXY_AUTHORIZATION, + proxyAuthHeaderValue); } boolean clientCompression = ClientConfigProperties.COMPRESS_CLIENT_REQUEST.getOrDefault(requestConfig); @@ -469,10 +502,10 @@ private void addHeaders(HttpPost req, Map requestConfig) { if (useHttpCompression) { if (serverCompression) { - req.addHeader(HttpHeaders.ACCEPT_ENCODING, "lz4"); + addHeader(req, HttpHeaders.ACCEPT_ENCODING, "lz4"); } if (clientCompression && !appCompressedData) { - req.addHeader(HttpHeaders.CONTENT_ENCODING, "lz4"); + addHeader(req, HttpHeaders.CONTENT_ENCODING, "lz4"); } } @@ -480,15 +513,19 @@ private void addHeaders(HttpPost req, Map requestConfig) { if (key.startsWith(ClientConfigProperties.HTTP_HEADER_PREFIX)) { Object val = requestConfig.get(key); if (val != null) { - req.setHeader(key.substring(ClientConfigProperties.HTTP_HEADER_PREFIX.length()), String.valueOf(val)); + addHeader( + req, + key.substring(ClientConfigProperties.HTTP_HEADER_PREFIX.length()), + String.valueOf(val)); } } } - // Special cases - if (req.containsHeader(HttpHeaders.AUTHORIZATION) && (req.containsHeader(ClickHouseHttpProto.HEADER_DB_USER) || - req.containsHeader(ClickHouseHttpProto.HEADER_DB_PASSWORD))) { + if (req.containsHeader(HttpHeaders.AUTHORIZATION) + && (req.containsHeader(ClickHouseHttpProto.HEADER_DB_USER) || + req.containsHeader(ClickHouseHttpProto.HEADER_DB_PASSWORD))) + { // user has set auth header for purpose, lets remove ours req.removeHeaders(ClickHouseHttpProto.HEADER_DB_USER); req.removeHeaders(ClickHouseHttpProto.HEADER_DB_PASSWORD); @@ -668,7 +705,6 @@ private void correctUserAgentHeader(HttpRequest request, Map req } else if (userAgentHeader != null) { userAgentValue = userAgentHeader.getValue() + " " + defaultUserAgent; } - request.setHeader(HttpHeaders.USER_AGENT, userAgentValue); } @@ -720,6 +756,25 @@ public void close() { httpClient.close(CloseMode.IMMEDIATE); } + private static void addHeader(HttpRequest req, String headerName, + T value) + { + if (value == null) { + return; + } + String tString = value.toString(); + if (tString.isBlank()) { + return; + } + if (PATTERN_HEADER_VALUE_ASCII.matcher(tString).matches()) { + req.addHeader(headerName, tString); + } else { + req.addHeader( + headerName + "*", + "UTF-8''" + URLEncoder.encode(tString, StandardCharsets.UTF_8)); + } + } + /** * This factory is used only when no ssl connections are required (no https endpoints). * Internally http client would create factory and spend time if no supplied. diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/JdbcConfiguration.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/JdbcConfiguration.java index fdc5f4f16..90f50e0cf 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/JdbcConfiguration.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/JdbcConfiguration.java @@ -6,6 +6,9 @@ import com.google.common.collect.ImmutableMap; import java.net.URI; +import java.net.URISyntaxException; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; import java.sql.DriverPropertyInfo; import java.sql.SQLException; import java.util.Comparator; @@ -13,18 +16,20 @@ import java.util.List; import java.util.Map; import java.util.Properties; -import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; public class JdbcConfiguration { - private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(JdbcConfiguration.class); - public static final String PREFIX_CLICKHOUSE = "jdbc:clickhouse:"; - public static final String PREFIX_CLICKHOUSE_SHORT = "jdbc:ch:"; - public static final String USE_SSL_PROP = "ssl"; + private static final String PREFIX_CLICKHOUSE = "jdbc:clickhouse:"; + private static final String PREFIX_CLICKHOUSE_SHORT = "jdbc:ch:"; + static final String USE_SSL_PROP = "ssl"; - final boolean disableFrameworkDetection; + private static final String PARSE_URL_CONN_URL_PROP = "connection_url"; + private static final Pattern PATTERN_HTTP_TOKEN = Pattern.compile( + "[A-Za-z0-9!#$%&'*+\\.\\^_`\\|~-]+"); + + private final boolean disableFrameworkDetection; final Map clientProperties; public Map getClientProperties() { @@ -70,12 +75,38 @@ public JdbcConfiguration(String url, Properties info) throws SQLException { } this.connectionUrl = createConnectionURL(tmpConnectionUrl, useSSL); - this.isIgnoreUnsupportedRequests= Boolean.parseBoolean(getDriverProperty(DriverProperties.IGNORE_UNSUPPORTED_VALUES.getKey(), "false")); + this.isIgnoreUnsupportedRequests = Boolean.parseBoolean(getDriverProperty(DriverProperties.IGNORE_UNSUPPORTED_VALUES.getKey(), "false")); } - public static boolean acceptsURL(String url) { - // TODO: should be also checked for http/https - return url.startsWith(PREFIX_CLICKHOUSE) || url.startsWith(PREFIX_CLICKHOUSE_SHORT); + /** + * This method (only) checks if this driver is probably responsible for the + * connection as given in {@code url}, no further sanity checks are + * performed. + * + * @param url + * the JDBC connection URL + * @return {@link true} if ClickHouse JDBC driver is responsible for + * connection, {@code false} else + * @throws SQLException + * if there is a technical error parsing the {@code url} + */ + public static boolean acceptsURL(String url) throws SQLException { + if (url == null) { + throw new SQLException("URL is null"); + } + if (!url.startsWith(PREFIX_CLICKHOUSE) + && !url.startsWith(PREFIX_CLICKHOUSE_SHORT)) + { + return false; + } + try { + URI uri = new URI(url); + // make sure uri is used + return "jdbc".equals(uri.getScheme()); + } catch (URISyntaxException urise) { + throw new SQLException( + "Not a valid URL '" + url + "'. ", urise); + } } public String getConnectionUrl() { @@ -88,19 +119,18 @@ public String getConnectionUrl() { * JDBC URL should have only a single path parameter to specify database name. * Note: Some BI tools do not let pass JDBC URL, so ssl is passed as property. * @param url - JDBC url - * @param ssl - if SSL protocol should be used when protocol is not specified + * @param ssl - if SSL protocol should be used * @return URL without JDBC prefix */ - static String createConnectionURL(String url, boolean ssl) throws SQLException { - if (url.startsWith("//")) { - url = (ssl ? "https:" : "http:") + url; - } - + private static String createConnectionURL(String url, boolean ssl) throws SQLException { + String adjustedURL = ssl && url.startsWith("http://") + ? "https://" + url.substring(7) + : url; try { - URI tmp = URI.create(url); - return tmp.getScheme() + "://" + tmp.getAuthority(); - } catch (Exception e) { - throw new SQLException("Failed to parse url", e); + URI tmp = URI.create(adjustedURL); + return tmp.toASCIIString(); + } catch (IllegalArgumentException iae) { + throw new SQLException("Failed to parse URL '" + url + "'", iae); } } @@ -110,77 +140,66 @@ private static String stripJDBCPrefix(String url) { } else if (url.startsWith(PREFIX_CLICKHOUSE_SHORT)) { return url.substring(PREFIX_CLICKHOUSE_SHORT.length()); } else { - throw new IllegalArgumentException("Specified URL doesn't have jdbc any of prefixes: [ " + PREFIX_CLICKHOUSE + ", " + PREFIX_CLICKHOUSE_SHORT + " ]"); + throw new IllegalArgumentException("Specified JDBC URL doesn't have any of prefixes: [ " + + PREFIX_CLICKHOUSE + ", " + PREFIX_CLICKHOUSE_SHORT + " ]"); } } - List listOfProperties; - - /** - * RegExp that extracts main parts: - *
    - *
  • 1 - protocol (ex.: {@code http:}) (optional)
  • - *
  • 2 - host (ex.: {@code localhost} (required)
  • - *
  • 3 - port (ex.: {@code 8123 } (optional)
  • - *
  • 4 - database name (optional)
  • - *
  • 5 - query parameters as is (optional)
  • - *
- */ - private static final Pattern URL_REGEXP = Pattern.compile("(https?:)?\\/\\/([\\w\\.\\-]+|\\[[0-9a-fA-F:]+\\]):?([\\d]*)(?:\\/([\\w]+))?\\/?\\??(.*)$"); + private List listOfProperties; - /** - * Extracts positions of parameters names. - * Match will be {@code param1=} or {@code ¶m2=}. - * There is limitation to not have '=' in values. - */ - private static final Pattern PARAM_EXTRACT_REGEXP = Pattern.compile("(?:&?[\\w\\.]+)=(?:[\\\\w])*"); private Map parseUrl(String url) throws SQLException { Map properties = new HashMap<>(); - - // process host and protocol - url = stripJDBCPrefix(url); - Matcher m = URL_REGEXP.matcher(url); - if (!m.find()) { - throw new SQLException("Invalid url " + url); + String myURL = null; + try { + myURL = stripJDBCPrefix(url); + } catch (Exception e) { + throw new SQLException( + "Error determining JDBC prefix from URL '" + url + "'", e); } - String proto = m.group(1); - String host = m.group(2); - String port = m.group(3); - - String connectionUrl = (proto == null ? "" : proto) + "//" + host + (port.isEmpty() ? "" : ":" + port); - properties.put(PARSE_URL_CONN_URL_PROP, connectionUrl); - - // Set database if present - String database = m.group(4); - if (database != null && !database.isEmpty()) { - properties.put(ClientConfigProperties.DATABASE.getKey(), database); + if (myURL.startsWith("//")) { + myURL = "http://" + myURL.substring(2); } - - // Parse query string - String queryStr = m.group(5); - if (queryStr != null && !queryStr.isEmpty()) { - Matcher qm = PARAM_EXTRACT_REGEXP.matcher(queryStr); - - if (qm.find()) { - String name = queryStr.substring(qm.start() + (queryStr.charAt(qm.start()) == '&' ? 1 : 0), qm.end() - 1); - int valStartPos = qm.end(); - while (qm.find()) { - String value = queryStr.substring(valStartPos, qm.start()); - properties.put(name, value); - name = queryStr.substring(qm.start() + (queryStr.charAt(qm.start()) == '&' ? 1 : 0), qm.end() - 1); - valStartPos = qm.end(); + URI uri = null; + try { + uri = new URI(myURL); + } catch (URISyntaxException urise) { + throw new SQLException( + "Invalid JDBC URL '" + url + "'", urise); + } + if (uri.getAuthority() == null) { + throw new SQLException( + "Invalid authority part JDBC URL '" + url + "'"); + } + properties.put(PARSE_URL_CONN_URL_PROP, uri.getScheme() + "://" + + uri.getRawAuthority()); // will be parsed again later + if (uri.getPath() != null + && !uri.getPath().isBlank() + && !"/".equals(uri.getPath())) + { + properties.put( + ClientConfigProperties.DATABASE.getKey(), + uri.getPath().substring(1)); + } + if (uri.getQuery() != null && !uri.getQuery().isBlank()) { + for (String pair : uri.getRawQuery().split("&")) { + String[] p = pair.split("=", 2); + if (p.length != 2 || p[0] == null || p[1] == null) { + throw new SQLException("Invalid query parameter '" + pair + "'"); } - - String value = queryStr.substring(valStartPos); - properties.put(name, value); + String key = URLDecoder.decode(p[0], StandardCharsets.UTF_8); + if (key == null || key.isBlank() || !PATTERN_HTTP_TOKEN.matcher(key).matches()) { + throw new SQLException("Invalid query parameter key in pair'" + pair + "'"); + } + String value = URLDecoder.decode(p[1], StandardCharsets.UTF_8); + if (value == null || value.isBlank() || "=".equals(value)) { + throw new SQLException("Invalid query parameter value in pair '" + pair + "'"); + } + properties.put(key.trim(), value); } } - return properties; } - private static final String PARSE_URL_CONN_URL_PROP = "connection_url"; - private void initProperties(Map urlProperties, Properties providedProperties) { // Copy provided properties @@ -268,4 +287,5 @@ public boolean isBetaFeatureEnabled(DriverProperties prop) { String value = driverProperties.getOrDefault(prop.getKey(), prop.getDefaultValue()); return Boolean.parseBoolean(value); } + } diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/ConnectionTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/ConnectionTest.java index 7d3e9e26e..f26d40d9d 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/ConnectionTest.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/ConnectionTest.java @@ -284,6 +284,7 @@ public void influenceUserAgentClientNameTest() throws SQLException { influenceUserAgentTest(clientName, "?" + ClientConfigProperties.CLIENT_NAME.getKey() + "=" + clientName); influenceUserAgentTest(clientName, "?" + ClientConfigProperties.PRODUCT_NAME.getKey() + "=" + clientName); } + private void influenceUserAgentTest(String clientName, String urlParam) throws SQLException { Properties info = new Properties(); info.setProperty("user", "default"); @@ -549,4 +550,36 @@ public void testDisableExtraCallToServer() throws Exception { } + @Test(groups = { "integration" }, dataProvider = "validDatabaseNames") + public void testConnectionWithValidDatabaseName(String dbName) throws Exception { + if (isCloud()) { + return; + } + Connection connCreate = this.getJdbcConnection(); + connCreate.createStatement().executeUpdate("CREATE DATABASE `" + dbName + "`"); + Properties properties = new Properties(); + properties.put(ClientConfigProperties.DATABASE.getKey(), dbName); + Connection connCheck = this.getJdbcConnection(properties); + ResultSet rs = connCheck.createStatement().executeQuery("SELECT 1"); + rs.next(); + Assert.assertEquals(rs.getInt(1), Integer.valueOf(1)); + Assert.assertEquals(dbName, rs.getMetaData().getSchemaName(1)); + connCreate.createStatement().executeUpdate("DROP DATABASE `" + dbName + "`"); + connCreate.close(); + connCheck.close(); + } + + @DataProvider(name = "validDatabaseNames") + public Object[][] createValidDatabaseNames() { + return new Object[][] { + { "foo" }, + { "with-dashes" }, + { "☺" }, + { "foo/bar" }, + { "foobar 20" }, + { " leading_and_trailing_spaces " }, + { "multi\nline\r\ndos" }, + }; + } + } diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/JdbcIntegrationTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/JdbcIntegrationTest.java index 6b80b9d55..b54e3a644 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/JdbcIntegrationTest.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/JdbcIntegrationTest.java @@ -1,9 +1,8 @@ package com.clickhouse.jdbc; -import com.clickhouse.client.ClickHouseServerForTest; - import com.clickhouse.client.BaseIntegrationTest; import com.clickhouse.client.ClickHouseProtocol; +import com.clickhouse.client.ClickHouseServerForTest; import com.clickhouse.client.api.ClientConfigProperties; import com.clickhouse.client.api.query.GenericRecord; import com.clickhouse.logging.Logger; @@ -20,6 +19,7 @@ public abstract class JdbcIntegrationTest extends BaseIntegrationTest { public String getEndpointString() { return getEndpointString(isCloud()); } + public String getEndpointString(boolean includeDbName) { return "jdbc:ch:" + (isCloud() ? "" : "http://") + ClickHouseServerForTest.getClickHouseAddress(ClickHouseProtocol.HTTP, false) + "/" + (includeDbName ? ClickHouseServerForTest.getDatabase() : ""); @@ -38,7 +38,7 @@ public Connection getJdbcConnection(Properties properties) throws SQLException { info.putAll(properties); } - info.setProperty(ClientConfigProperties.DATABASE.getKey(), ClickHouseServerForTest.getDatabase()); + info.putIfAbsent(ClientConfigProperties.DATABASE.getKey(), getDatabase()); return new ConnectionImpl(getEndpointString(), info); } @@ -47,6 +47,7 @@ protected static String getDatabase() { return ClickHouseServerForTest.getDatabase(); } + @Override protected boolean runQuery(String query) { return runQuery(query, new Properties()); } diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/internal/JdbcConfigurationTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/internal/JdbcConfigurationTest.java index 52b74ef89..eb6cfb26c 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/internal/JdbcConfigurationTest.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/internal/JdbcConfigurationTest.java @@ -6,65 +6,248 @@ import org.testng.annotations.Test; import java.sql.DriverPropertyInfo; +import java.sql.SQLException; import java.util.Arrays; +import java.util.HashMap; import java.util.Map; +import java.util.Objects; import java.util.Properties; -import java.util.stream.Collectors; import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertThrows; public class JdbcConfigurationTest { + private static final JdbcConfigurationTestData[] VALID_URLs = new JdbcConfigurationTestData[] { + new JdbcConfigurationTestData("jdbc:ch://localhost"), + new JdbcConfigurationTestData("jdbc:clickhouse://localhost"), + new JdbcConfigurationTestData("jdbc:clickhouse:http://localhost"), + new JdbcConfigurationTestData("jdbc:clickhouse:https://localhost") + .withExpectedConnectionURL("https://localhost"), + new JdbcConfigurationTestData("jdbc:clickhouse://localhost") + .withAdditionalConnectionParameters( + Map.of(JdbcConfiguration.USE_SSL_PROP, "true")) + .withExpectedConnectionURL("https://localhost") + .withAdditionalExpectedClientProperties( + Map.of("ssl", "true")), + new JdbcConfigurationTestData("jdbc:clickhouse://[::1]") + .withExpectedConnectionURL("http://[::1]"), + new JdbcConfigurationTestData("jdbc:clickhouse://[::1]:8123") + .withExpectedConnectionURL("http://[::1]:8123"), + new JdbcConfigurationTestData("jdbc:clickhouse://localhost:8443") + .withExpectedConnectionURL("http://localhost:8443"), + new JdbcConfigurationTestData("jdbc:clickhouse://localhost/database") + .withAdditionalExpectedClientProperties( + Map.of("database", "database")), + new JdbcConfigurationTestData("jdbc:clickhouse://localhost:42/database") + .withExpectedConnectionURL("http://localhost:42") + .withAdditionalExpectedClientProperties( + Map.of("database", "database")), + new JdbcConfigurationTestData("jdbc:clickhouse://localhost/data-base") + .withAdditionalExpectedClientProperties( + Map.of("database", "data-base")), + new JdbcConfigurationTestData("jdbc:clickhouse://localhost/data%20base") + .withAdditionalExpectedClientProperties( + Map.of("database", "data base")), + new JdbcConfigurationTestData("jdbc:clickhouse://localhost/data%2Fbase") + .withAdditionalExpectedClientProperties( + Map.of("database", "data/base")), + new JdbcConfigurationTestData("jdbc:clickhouse://localhost/☺") + .withAdditionalExpectedClientProperties( + Map.of("database", "☺")), + new JdbcConfigurationTestData("jdbc:clickhouse://localhost/db?key1=val1&key2=val2") + .withAdditionalExpectedClientProperties( + Map.of( + "database", "db", + "key1", "val1", + "key2", "val2" + )), + new JdbcConfigurationTestData("jdbc:clickhouse://localhost/db?key1=val%201") + .withAdditionalExpectedClientProperties( + Map.of( + "database", "db", + "key1", "val 1" + )), + new JdbcConfigurationTestData("jdbc:clickhouse://localhost/?key1=val1") + .withAdditionalExpectedClientProperties( + Map.of( + "key1", "val1" + )), + new JdbcConfigurationTestData("jdbc:clickhouse://localhost?key1=val1") + .withAdditionalExpectedClientProperties( + Map.of( + "key1", "val1" + )), + new JdbcConfigurationTestData("jdbc:clickhouse://localhost:8123?key1=val1") + .withExpectedConnectionURL("http://localhost:8123") + .withAdditionalExpectedClientProperties( + Map.of( + "key1", "val1" + )), + new JdbcConfigurationTestData("jdbc:clickhouse://localhost:8123/?key1=val1") + .withExpectedConnectionURL("http://localhost:8123") + .withAdditionalExpectedClientProperties( + Map.of( + "key1", "val1" + )), + new JdbcConfigurationTestData("jdbc:clickhouse://localhost?key1=☺") + .withAdditionalExpectedClientProperties( + Map.of( + "key1", "☺" + )), + new JdbcConfigurationTestData("jdbc:clickhouse://localhost?key1=val1,val2") + .withAdditionalExpectedClientProperties( + Map.of( + "key1", "val1,val2" + )), + new JdbcConfigurationTestData( + "jdbc:clickhouse://localhost:8443/default?custom_header1=%22role%201,3,4%22,%27val2%27,val3¶m1=value1") + .withExpectedConnectionURL("http://localhost:8443") + .withAdditionalExpectedClientProperties( + Map.of( + "database", "default", + "custom_header1", "\"role 1,3,4\",'val2',val3", + "param1", "value1" + )) + }; - @Test(dataProvider = "testConnectionUrlDataProvider") - public void testConnectionUrl(String jdbcUrl, String connectionUrl, Properties properties, Map expectedClientProps) throws Exception { - JdbcConfiguration configuration = new JdbcConfiguration(jdbcUrl, properties); + @Test(dataProvider = "validURLTestData") + public void testParseURLValid(String jdbcURL, Properties properties, + String connectionUrl, Map expectedClientProps) + throws Exception + { + JdbcConfiguration configuration = new JdbcConfiguration(jdbcURL, properties); assertEquals(configuration.getConnectionUrl(), connectionUrl); assertEquals(configuration.clientProperties, expectedClientProps); } - @DataProvider(name = "testConnectionUrlDataProvider") - public static Object[][] testConnectionUrlDataProvider() { - Properties defaultProps = new Properties(); - defaultProps.setProperty(ClientConfigProperties.USER.getKey(), "default"); - defaultProps.setProperty(ClientConfigProperties.PASSWORD.getKey(), ""); - Properties useSSL = new Properties(); - useSSL.put(JdbcConfiguration.USE_SSL_PROP, "true"); - - Map defaultParams = Map.of( "user", "default", "password", ""); - Map simpleParams = Map.of( "database", "clickhouse", "param1", "value1", "param2", "value2", "user", "default", "password", ""); - Map useSSLParams = Map.of("ssl", "true"); - Map withListParams = Map.of("database", "default", "param1", "value1", "custom_header1", "val1,val2,val3", "user", "default", "password", ""); - Map withListParamsQuotes = Map.of("database", "default", "param1", "value1", "custom_header1", "\"role 1,3,4\",'val2',val3", "user", "default", "password", ""); - Map useDatabaseSSLParams = Map.of("database", "clickhouse", "ssl", "true", "user", "default", "password", ""); - - return new Object[][] { - {"jdbc:clickhouse://localhost:8123/", "http://localhost:8123", defaultProps, defaultParams}, - {"jdbc:clickhouse://127.0.0.1:8123/", "http://127.0.0.1:8123", defaultProps, defaultParams}, - {"jdbc:clickhouse://[::1]:8123/", "http://[::1]:8123", defaultProps, defaultParams}, - {"jdbc:clickhouse://[::1]/", "http://[::1]", defaultProps, defaultParams}, - {"jdbc:clickhouse://localhost:8443/clickhouse?param1=value1¶m2=value2", "http://localhost:8443", defaultProps, simpleParams}, - {"jdbc:clickhouse:https://localhost:8123/clickhouse?param1=value1¶m2=value2", "https://localhost:8123", defaultProps, simpleParams}, - {"jdbc:clickhouse://localhost:8443/", "https://localhost:8443", useSSL, useSSLParams}, - {"jdbc:clickhouse://localhost:8443/default?param1=value1&custom_header1=val1,val2,val3", "http://localhost:8443", defaultProps, withListParams}, - {"jdbc:clickhouse://localhost:8443/default?custom_header1=\"role 1,3,4\",'val2',val3¶m1=value1", "http://localhost:8443", defaultProps, withListParamsQuotes}, - {"jdbc:clickhouse://localhost:8443/clickhouse?ssl=true", "https://localhost:8443", defaultProps, useDatabaseSSLParams}, - }; + @Test(dataProvider = "invalidURLs") + public void testParseURLInvalid(String jdbcURL) { + assertThrows( + SQLException.class, + () -> new JdbcConfiguration(jdbcURL, new Properties())); + + } + + @Test(dataProvider = "validURLs") + public void testAcceptsURLValid(String url) throws Exception { + Assert.assertTrue(JdbcConfiguration.acceptsURL(url)); } @Test public void testConfigurationProperties() throws Exception { - Properties properties = new Properties(); properties.setProperty(ClientConfigProperties.DATABASE.getKey(), "default2"); properties.setProperty(DriverProperties.DEFAULT_QUERY_SETTINGS.getKey(), - ClientConfigProperties.commaSeparated(Arrays.asList("http_headers=header1=3,header2=4"))); + ClientConfigProperties.commaSeparated(Arrays.asList("http_headers=header1=3,header2=4"))); String url = "jdbc:clickhouse://localhost:8123/clickhouse?client_name=test_application&database=default1"; JdbcConfiguration configuration = new JdbcConfiguration(url, properties); - Assert.assertEquals(configuration.getConnectionUrl(), "http://localhost:8123"); - Map infos = configuration.getDriverPropertyInfo().stream().collect(Collectors.toMap(d -> d.name, d -> d)); + assertEquals(configuration.getConnectionUrl(), "http://localhost:8123"); + DriverPropertyInfo p = configuration.getDriverPropertyInfo().stream() + .filter(dpi -> ClientConfigProperties.DATABASE.getKey().equals(dpi.name)) + .findAny() + .orElseThrow(); + assertEquals(p.value, "default1"); + } - DriverPropertyInfo p = infos.get(ClientConfigProperties.DATABASE.getKey()); - Assert.assertEquals(p.value, "default1"); + @DataProvider(name = "validURLTestData") + public Object[][] createValidConnectionURLTestData() { + return Arrays.stream(VALID_URLs) + .map(d -> new Object[] { + d.url, + d.connectionParameters, + d.expectedConnectionURL, + d.expectedClientProperties + }) + .toArray(Object[][]::new); } + + @DataProvider(name = "validURLs") + public Object[][] createValidConnectionURLs() { + return Arrays.stream(VALID_URLs) + .map(d -> new Object[] { + d.url + }) + .toArray(Object[][]::new); + } + + @DataProvider(name = "invalidURLs") + public Object[][] createInvalidConnectionURLs() { + return new String[][] { + { null }, + { "" }, + { " " }, + { " \r\n \t " }, + { "jdbc:error://foo.bar" }, + { "https://clickhouse.com" }, + { "jdbc:clickhouse: //foo.bar" }, + { "jdbc:ch" }, + { "jdbc:clickhouse://" }, + { "jdbc:clickhouse:// /" }, + { "jdbc:clickhouse:////foo.bar" }, + { "jdbc:clickhouse://local host" }, + { "jdbc:clickhouse://foo.bar/data base" }, + { "jdbc:clickhouse://foo.bar? = " }, + { "jdbc:clickhouse://foo.bar?x= " }, + { "jdbc:clickhouse://foo.bar? =x" }, + { "jdbc:clickhouse://foo.bar?%20=" }, + { "jdbc:clickhouse://foo.bar?%20=%20" }, + { "jdbc:clickhouse://foo.bar?x=%20%20" }, + { "jdbc:clickhouse://localhost/?key%201=val1" }, + { "jdbc:clickhouse://localhost/db?%20key1%20=%20val%20" }, + { "jdbc:clickhouse://foo.bar?x&y=z" }, + { "jdbc:clickhouse://foo.bar?x==&y=z" }, + { "jdbc:clickhouse://localhost?☺=value1" }, + }; + } + + private static final class JdbcConfigurationTestData { + + private static final Map DEFAULT_CONNECTION_PARAMS = + Map.of( "user", "default", "password", ""); + + private static final Map DEFAULT_EXPECTED_CLIENT_PROPERTIES = + Map.of( "user", "default", "password", ""); + + private static final String DEFAULT_EXPECTED_CONNECTION_URL = + "http://localhost"; + + private final String url; + private final Properties connectionParameters; + private String expectedConnectionURL; + private final Map expectedClientProperties; + + JdbcConfigurationTestData(String url) { + this.url = Objects.requireNonNull(url); + this.connectionParameters = new Properties(); + this.connectionParameters.putAll(DEFAULT_CONNECTION_PARAMS); + this.expectedConnectionURL = DEFAULT_EXPECTED_CONNECTION_URL; + this.expectedClientProperties = new HashMap<>( + DEFAULT_EXPECTED_CLIENT_PROPERTIES); // modifiable + } + + JdbcConfigurationTestData withAdditionalConnectionParameters( + Map additionalConnectionParameters) + { + this.connectionParameters.putAll(additionalConnectionParameters); + return this; + } + + JdbcConfigurationTestData withExpectedConnectionURL( + String expectedConnectionURL) + { + this.expectedConnectionURL = Objects.requireNonNull(expectedConnectionURL); + return this; + } + + JdbcConfigurationTestData withAdditionalExpectedClientProperties( + Map additionalExpectedClientProperties) + { + this.expectedClientProperties.putAll( + additionalExpectedClientProperties); + return this; + } + + } + }