diff --git a/CHANGELOG.md b/CHANGELOG.md index db00676ad..1973f4ca7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,26 @@ # Change Log +## [3.5.0](https://github.com/mariadb-corporation/mariadb-connector-j/tree/3.5.0) (Oct 2024) +[Full Changelog](https://github.com/mariadb-corporation/mariadb-connector-j/compare/3.4.1...3.5.0) + +#### Notable changes + +* CONJ-1193 Parsec authentication implementation +* CONJ-1183 permit setting specific truststore + +#### Bugs Fixed + +* CONJ-1202 Session variable setting must be executed last +* CONJ-1201 incorrect default behavior for forceConnectionTimeZoneToSession +* CONJ-1200 Batch import fails with exception "Unknown command" +* CONJ-1199 option `connectionCollation` addition in order to force collation +* CONJ-1187 Use different exception type for connection timeouts + ## [3.4.1](https://github.com/mariadb-corporation/mariadb-connector-j/tree/3.4.1) (Jul 2024) [Full Changelog](https://github.com/mariadb-corporation/mariadb-connector-j/compare/3.4.0...3.4.1) ##### Bugs Fixed + * CONJ-1181 Ensure Prepare cache use schema * CONJ-1178 DatabaseMetaData.getImportedKeys return different PK_NAME value than getExportedKeys. * CONJ-1180 Correct DatabaseMeta.getExportedKeys() performances diff --git a/README.md b/README.md index 9a93714ad..d2cd12c83 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,8 @@ Tracker link https://ji ## Status -[![Linux Build](https://travis-ci.com/mariadb-corporation/mariadb-connector-j.svg?branch=master)](https://app.travis-ci.com/github/mariadb-corporation/mariadb-connector-j) -[![Maven Central](https://maven-badges.herokuapp.com/maven-central/org.mariadb.jdbc/mariadb-java-client/badge.svg)](https://maven-badges.herokuapp.com/maven-central/org.mariadb.jdbc/mariadb-java-client) +[![Linux Build](https://app.travis-ci.com/mariadb-corporation/mariadb-connector-j.svg?branch=master)](https://app.travis-ci.com/github/mariadb-corporation/mariadb-connector-j) +[![Maven Central](https://img.shields.io/maven-central/v/org.mariadb.jdbc/mariadb-java-client.svg)](https://maven-badges.herokuapp.com/maven-central/org.mariadb.jdbc/mariadb-java-client) [![License (LGPL version 2.1)](https://img.shields.io/badge/license-GNU%20LGPL%20version%202.1-green.svg?style=flat-square)](http://opensource.org/licenses/LGPL-2.1) [![codecov][codecov-image]][codecov-url] diff --git a/pom.xml b/pom.xml index 17107212b..23aaa9bfe 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ mariadb-java-client jar mariadb-java-client - 3.4.1 + 3.5.0 JDBC driver for MariaDB and MySQL https://mariadb.com/kb/en/mariadb/about-mariadb-connector-j/ diff --git a/src/main/java/org/mariadb/jdbc/BasePreparedStatement.java b/src/main/java/org/mariadb/jdbc/BasePreparedStatement.java index 7899777f8..c3be4827e 100644 --- a/src/main/java/org/mariadb/jdbc/BasePreparedStatement.java +++ b/src/main/java/org/mariadb/jdbc/BasePreparedStatement.java @@ -155,15 +155,6 @@ public void updateMeta(ColumnDecoder[] ci) { public abstract ParameterMetaData getParameterMetaData() throws SQLException; - /** - * Set all parameters - * - * @param parameters parameters - */ - public void setParameters(Parameters parameters) { - this.parameters = parameters; - } - /** * Set parameter * diff --git a/src/main/java/org/mariadb/jdbc/Configuration.java b/src/main/java/org/mariadb/jdbc/Configuration.java index 5133d5f25..c02f95df4 100644 --- a/src/main/java/org/mariadb/jdbc/Configuration.java +++ b/src/main/java/org/mariadb/jdbc/Configuration.java @@ -63,6 +63,7 @@ public class Configuration { // various private String timezone = null; + private String connectionCollation = null; private String connectionTimeZone = null; private Boolean forceConnectionTimeZoneToSession = null; private boolean preserveInstants; @@ -105,7 +106,9 @@ public class Configuration { private SslMode sslMode = SslMode.DISABLE; private String serverSslCert = null; private String keyStore = null; + private String trustStore = null; private String keyStorePassword = null; + private String trustStorePassword = null; private String keyPassword = null; private String keyStoreType = null; private String trustStoreType = null; @@ -174,6 +177,7 @@ private Configuration( Properties nonMappedOptions, String timezone, String connectionTimeZone, + String connectionCollation, boolean forceConnectionTimeZoneToSession, boolean preserveInstants, Boolean autocommit, @@ -210,7 +214,9 @@ private Configuration( SslMode sslMode, String serverSslCert, String keyStore, + String trustStore, String keyStorePassword, + String trustStorePassword, String keyPassword, String keyStoreType, String trustStoreType, @@ -261,6 +267,7 @@ private Configuration( this.nonMappedOptions = nonMappedOptions; this.timezone = timezone; this.connectionTimeZone = connectionTimeZone; + this.connectionCollation = connectionCollation; this.forceConnectionTimeZoneToSession = forceConnectionTimeZoneToSession; this.preserveInstants = preserveInstants; this.autocommit = autocommit; @@ -297,7 +304,9 @@ private Configuration( this.sslMode = sslMode; this.serverSslCert = serverSslCert; this.keyStore = keyStore; + this.trustStore = trustStore; this.keyStorePassword = keyStorePassword; + this.trustStorePassword = trustStorePassword; this.keyPassword = keyPassword; this.keyStoreType = keyStoreType; this.trustStoreType = trustStoreType; @@ -378,6 +387,7 @@ private Configuration( Boolean yearIsDateType, String timezone, String connectionTimeZone, + String connectionCollation, Boolean forceConnectionTimeZoneToSession, Boolean preserveInstants, Boolean dumpQueriesOnException, @@ -419,7 +429,9 @@ private Configuration( Boolean allowPublicKeyRetrieval, String serverSslCert, String keyStore, + String trustStore, String keyStorePassword, + String trustStorePassword, String keyPassword, String keyStoreType, String trustStoreType, @@ -475,6 +487,7 @@ private Configuration( if (yearIsDateType != null) this.yearIsDateType = yearIsDateType; this.timezone = timezone; if (connectionTimeZone != null) this.connectionTimeZone = connectionTimeZone; + if (connectionCollation != null) this.connectionCollation = connectionCollation; if (forceConnectionTimeZoneToSession != null) this.forceConnectionTimeZoneToSession = forceConnectionTimeZoneToSession; if (preserveInstants != null) this.preserveInstants = preserveInstants; @@ -555,7 +568,9 @@ private Configuration( if (initSql != null) this.initSql = initSql; if (serverSslCert != null) this.serverSslCert = serverSslCert; if (keyStore != null) this.keyStore = keyStore; + if (trustStore != null) this.trustStore = trustStore; if (keyStorePassword != null) this.keyStorePassword = keyStorePassword; + if (trustStorePassword != null) this.trustStorePassword = trustStorePassword; if (keyPassword != null) this.keyPassword = keyPassword; if (keyStoreType != null) this.keyStoreType = keyStoreType; if (trustStoreType != null) this.trustStoreType = trustStoreType; @@ -618,6 +633,24 @@ private Configuration( // option value verification // ************************************************************* + // ensure connection collation format + if (connectionCollation != null) { + if ("".equals(connectionCollation.trim())) { + this.connectionCollation = null; + } else { + // ensure this is an utf8mb4 collation + if (!connectionCollation.toLowerCase(Locale.ROOT).startsWith("utf8mb4_")) { + throw new SQLException( + String.format( + "wrong connection collation '%s' only utf8mb4 collation are accepted", + connectionCollation)); + } else if (!connectionCollation.matches("^[a-zA-Z0-9_]+$")) { + throw new SQLException( + String.format("wrong connection collation '%s' name", connectionCollation)); + } + } + } + // int fields must all be positive Field[] fields = Configuration.class.getDeclaredFields(); try { @@ -651,6 +684,7 @@ public Builder toBuilder() { .haMode(this.haMode) .timezone(this.timezone) .connectionTimeZone(this.connectionTimeZone) + .connectionCollation(this.connectionCollation) .forceConnectionTimeZoneToSession(this.forceConnectionTimeZoneToSession) .preserveInstants(this.preserveInstants) .autocommit(this.autocommit) @@ -689,8 +723,10 @@ public Builder toBuilder() { .sslMode(this.sslMode.name()) .serverSslCert(this.serverSslCert) .keyStore(this.keyStore) + .trustStore(this.trustStore) .keyStoreType(this.keyStoreType) .keyStorePassword(this.keyStorePassword) + .trustStorePassword(this.trustStorePassword) .keyPassword(this.keyPassword) .trustStoreType(this.trustStoreType) .enabledSslCipherSuites(this.enabledSslCipherSuites) @@ -799,7 +835,12 @@ private static Configuration parseInternal(String url, Properties properties) int skipPos; int posToSkip = 0; while ((skipPos = urlSecondPart.indexOf("address=(", posToSkip)) > -1) { - posToSkip = urlSecondPart.indexOf(")", skipPos); + posToSkip = urlSecondPart.indexOf(")", skipPos) + 1; + while (urlSecondPart.startsWith("(", posToSkip)) { + int endingBraceIndex = urlSecondPart.indexOf(")", posToSkip); + if (endingBraceIndex == -1) break; + posToSkip = endingBraceIndex + 1; + } } int dbIndex = urlSecondPart.indexOf("/", posToSkip); int paramIndex = urlSecondPart.indexOf("?"); @@ -1043,13 +1084,18 @@ public static String toConf(String url) throws SQLException { case "Integer": case "SslMode": case "CatalogTerm": - (Objects.equals(fieldValue, field.get(defaultConf)) + StringBuilder sbb = + (Objects.equals(fieldValue, field.get(defaultConf)) ? sbDefaultOpts - : sbDifferentOpts) - .append("\n * ") - .append(field.getName()) - .append(" : ") - .append(fieldValue); + : sbDifferentOpts); + + sbb.append("\n * ").append(field.getName()).append(" : "); + if ("password".equals(field.getName()) + || "keyStorePassword".equals(field.getName()) + || "trustStorePassword".equals(field.getName())) { + sbb.append("***"); + } else sbb.append(fieldValue); + break; case "ArrayList": (Objects.equals(fieldValue.toString(), field.get(defaultConf).toString()) @@ -1144,7 +1190,9 @@ protected static String buildUrl(Configuration conf) { if (obj != null && (!(obj instanceof Properties) || ((Properties) obj).size() > 0)) { - if ("password".equals(field.getName())) { + if ("password".equals(field.getName()) + || "keyStorePassword".equals(field.getName()) + || "trustStorePassword".equals(field.getName())) { sb.append(first ? '?' : '&'); first = false; sb.append(field.getName()).append('='); @@ -1335,6 +1383,15 @@ public String keyStore() { return keyStore; } + /** + * trust store + * + * @return trust store + */ + public String trustStore() { + return trustStore; + } + /** * key store password * @@ -1344,6 +1401,15 @@ public String keyStorePassword() { return keyStorePassword; } + /** + * trust store password + * + * @return trust store password + */ + public String trustStorePassword() { + return trustStorePassword; + } + /** * key store alias password * @@ -1635,6 +1701,15 @@ public String connectionTimeZone() { return connectionTimeZone; } + /** + * get connectionCollation + * + * @return connectionCollation + */ + public String connectionCollation() { + return connectionCollation; + } + /** * forceConnectionTimeZoneToSession must connection timezone be forced * @@ -2124,6 +2199,7 @@ public static final class Builder implements Cloneable { // various private String timezone; private String connectionTimeZone; + private String connectionCollation; private Boolean forceConnectionTimeZoneToSession; private Boolean preserveInstants; private Boolean autocommit; @@ -2164,7 +2240,9 @@ public static final class Builder implements Cloneable { private String sslMode; private String serverSslCert; private String keyStore; + private String trustStore; private String keyStorePassword; + private String trustStorePassword; private String keyPassword; private String keyStoreType; private String trustStoreType; @@ -2256,6 +2334,18 @@ public Builder keyStore(String keyStore) { return this; } + /** + * File path of the trustStore file that contain trusted certificates (similar to java System + * property \"javax.net.ssl.trustStore\") + * + * @param trustStore client trust store certificates + * @return this {@link Builder} + */ + public Builder trustStore(String trustStore) { + this.trustStore = nullOrEmpty(trustStore); + return this; + } + /** * Client keystore password * @@ -2267,6 +2357,17 @@ public Builder keyStorePassword(String keyStorePassword) { return this; } + /** + * Client truststore password + * + * @param trustStorePassword client truststore password + * @return this {@link Builder} + */ + public Builder trustStorePassword(String trustStorePassword) { + this.trustStorePassword = nullOrEmpty(trustStorePassword); + return this; + } + /** * Client keystore alias password * @@ -2803,6 +2904,18 @@ public Builder connectionTimeZone(String connectionTimeZone) { return this; } + /** + * indicate what utf8mb4 collation to use. if not set, server default collation for utf8mb4 will + * be used + * + * @param connectionCollation utf8mb4 collation to use + * @return this {@link Builder} + */ + public Builder connectionCollation(String connectionCollation) { + this.connectionCollation = nullOrEmpty(connectionCollation); + return this; + } + /** * Indicate if connectionTimeZone must be forced to session * @@ -3345,6 +3458,7 @@ public Configuration build() throws SQLException { this.yearIsDateType, this.timezone, this.connectionTimeZone, + this.connectionCollation, this.forceConnectionTimeZoneToSession, this.preserveInstants, this.dumpQueriesOnException, @@ -3386,7 +3500,9 @@ public Configuration build() throws SQLException { this.allowPublicKeyRetrieval, this.serverSslCert, this.keyStore, + this.trustStore, this.keyStorePassword, + this.trustStorePassword, this.keyPassword, this.keyStoreType, this.trustStoreType, diff --git a/src/main/java/org/mariadb/jdbc/MariaDbPoolConnection.java b/src/main/java/org/mariadb/jdbc/MariaDbPoolConnection.java index 28a4506bd..0a42fe1fa 100644 --- a/src/main/java/org/mariadb/jdbc/MariaDbPoolConnection.java +++ b/src/main/java/org/mariadb/jdbc/MariaDbPoolConnection.java @@ -175,12 +175,8 @@ protected static XAException mapXaException(SQLException sqle) { xaErrorCode = 0; break; } - XAException xaException; - if (xaErrorCode != 0) { - xaException = new XAException(xaErrorCode); - } else { - xaException = new XAException(sqle.getMessage()); - } + XAException xaException = new XAException(sqle.getMessage()); + xaException.errorCode = xaErrorCode; xaException.initCause(sqle); return xaException; } diff --git a/src/main/java/org/mariadb/jdbc/client/impl/ConnectionHelper.java b/src/main/java/org/mariadb/jdbc/client/impl/ConnectionHelper.java index ec96f6eca..82b22e735 100644 --- a/src/main/java/org/mariadb/jdbc/client/impl/ConnectionHelper.java +++ b/src/main/java/org/mariadb/jdbc/client/impl/ConnectionHelper.java @@ -7,8 +7,10 @@ import java.lang.reflect.Constructor; import java.net.InetSocketAddress; import java.net.Socket; +import java.net.SocketTimeoutException; import java.sql.SQLException; import java.sql.SQLNonTransientConnectionException; +import java.sql.SQLTimeoutException; import java.util.Arrays; import java.util.List; import javax.net.SocketFactory; @@ -117,7 +119,14 @@ public static Socket connectSocket(final Configuration conf, final HostAddress h } return socket; + } catch (SocketTimeoutException ste) { + throw new SQLTimeoutException( + String.format("Socket timeout when connecting to %s. %s", hostAddress, ste.getMessage()), + "08000", + ste); + } catch (IOException ioe) { + throw new SQLNonTransientConnectionException( String.format("Socket fail to connect to %s. %s", hostAddress, ioe.getMessage()), "08000", @@ -154,6 +163,11 @@ public static long initializeClientCapabilities( capabilities |= Capabilities.BULK_UNIT_RESULTS; } + if (Boolean.parseBoolean( + configuration.nonMappedOptions().getProperty("disableSessionTracking", "true"))) { + capabilities &= ~Capabilities.CLIENT_SESSION_TRACK; + } + // since skipping metadata is only available when using binary protocol, // only set it when server permit it and using binary protocol if (configuration.useServerPrepStmts() diff --git a/src/main/java/org/mariadb/jdbc/client/impl/StandardClient.java b/src/main/java/org/mariadb/jdbc/client/impl/StandardClient.java index dbaeaf019..4b54b85a6 100644 --- a/src/main/java/org/mariadb/jdbc/client/impl/StandardClient.java +++ b/src/main/java/org/mariadb/jdbc/client/impl/StandardClient.java @@ -19,6 +19,7 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.sql.SQLNonTransientConnectionException; +import java.sql.SQLTimeoutException; import java.time.DateTimeException; import java.time.Instant; import java.time.ZoneId; @@ -243,17 +244,21 @@ public StandardClient( postConnectionQueries(); } setSocketTimeout(conf.socketTimeout()); - - } catch (IOException ioException) { - destroySocket(); - - String errorMsg = - String.format("Could not connect to %s : %s", hostAddress, ioException.getMessage()); - - throw exceptionFactory.create(errorMsg, "08000", ioException); } catch (SQLException sqlException) { destroySocket(); throw sqlException; + } catch (SocketTimeoutException ste) { + destroySocket(); + throw new SQLTimeoutException( + String.format("Socket timeout when connecting to %s. %s", hostAddress, ste.getMessage()), + "08000", + ste); + } catch (IOException ioException) { + destroySocket(); + throw exceptionFactory.create( + String.format("Could not connect to %s : %s", hostAddress, ioException.getMessage()), + "08000", + ioException); } } @@ -771,14 +776,8 @@ public String createSessionVariableQuery(Context context) { + "')"); } - // add configured session variable if configured - if (conf.sessionVariables() != null) { - sessionCommands.add(Security.parseSessionVariables(conf.sessionVariables())); - } - // force client timezone to connection to ensure result of now(), ... - if (conf.forceConnectionTimeZoneToSession() == null - || conf.forceConnectionTimeZoneToSession()) { + if (Boolean.TRUE.equals(conf.forceConnectionTimeZoneToSession())) { TimeZone connectionTz = context.getConnectionTimeZone(); ZoneId connectionZoneId = connectionTz.toZoneId(); @@ -817,9 +816,21 @@ public String createSessionVariableQuery(Context context) { context.canUseTransactionIsolation() ? "transaction_read_only" : "tx_read_only")); } - if (context.getCharset() == null || !"utf8mb4".equals(context.getCharset())) { - sessionCommands.add("NAMES utf8mb4"); + if (context.getCharset() == null + || !"utf8mb4".equals(context.getCharset()) + || conf.connectionCollation() != null) { + String defaultCharsetSet = "NAMES utf8mb4"; + if (conf.connectionCollation() != null) { + defaultCharsetSet += " COLLATE " + conf.connectionCollation(); + } + sessionCommands.add(defaultCharsetSet); + } + + // add configured session variable if configured + if (conf.sessionVariables() != null) { + sessionCommands.add(Security.parseSessionVariables(conf.sessionVariables())); } + if (!sessionCommands.isEmpty()) { return "set " + sessionCommands.stream().collect(Collectors.joining(",")); } diff --git a/src/main/java/org/mariadb/jdbc/client/socket/impl/PacketWriter.java b/src/main/java/org/mariadb/jdbc/client/socket/impl/PacketWriter.java index 57cf69e04..2aa36d11b 100644 --- a/src/main/java/org/mariadb/jdbc/client/socket/impl/PacketWriter.java +++ b/src/main/java/org/mariadb/jdbc/client/socket/impl/PacketWriter.java @@ -673,12 +673,8 @@ private void growBuffer(int len) throws IOException { return; } - // need to keep all data, buf can grow more than maxPacketLength - // grow buf if needed - if (bufLength == maxPacketLength) return; - if (len + pos > newCapacity) { - newCapacity = Math.min(maxPacketLength, len + pos); - } + growBuffer(len); + return; } } @@ -808,8 +804,7 @@ public void flushBufferStopAtMark() throws IOException { pos = mark; writeSocket(true); out.flush(); - initPacket(); - + cmdLength = 0; System.arraycopy(buf, mark, buf, pos, end - mark); pos += end - mark; mark = -1; diff --git a/src/main/java/org/mariadb/jdbc/message/client/BulkExecutePacket.java b/src/main/java/org/mariadb/jdbc/message/client/BulkExecutePacket.java index 03ab9ea26..1d6223c90 100644 --- a/src/main/java/org/mariadb/jdbc/message/client/BulkExecutePacket.java +++ b/src/main/java/org/mariadb/jdbc/message/client/BulkExecutePacket.java @@ -126,7 +126,7 @@ public int encode(Writer writer, Context context, Prepare newPrepareResult) } } - if (!writer.isMarked() && writer.hasFlushed()) { + if (!writer.bufIsDataAfterMark() && !writer.isMarked() && writer.hasFlushed()) { // parameter were too big to fit in a MySQL packet // need to finish the packet separately writer.flush(); diff --git a/src/main/java/org/mariadb/jdbc/plugin/tls/main/DefaultTlsSocketPlugin.java b/src/main/java/org/mariadb/jdbc/plugin/tls/main/DefaultTlsSocketPlugin.java index 88032529f..0b9924142 100644 --- a/src/main/java/org/mariadb/jdbc/plugin/tls/main/DefaultTlsSocketPlugin.java +++ b/src/main/java/org/mariadb/jdbc/plugin/tls/main/DefaultTlsSocketPlugin.java @@ -5,9 +5,12 @@ import java.io.*; import java.net.URI; +import java.net.URL; import java.security.GeneralSecurityException; import java.security.KeyStore; +import java.security.NoSuchAlgorithmException; import java.security.cert.Certificate; +import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.sql.SQLException; @@ -100,7 +103,7 @@ public TrustManager[] getTrustManager( trustManager = new X509TrustManager[] {new MariaDbX509TrustingManager()}; } else { // if certificate is provided, load it. - if (conf.serverSslCert() != null) { + if (conf.serverSslCert() != null || conf.trustStore() != null) { KeyStore ks; try { ks = @@ -112,19 +115,57 @@ public TrustManager[] getTrustManager( throw exceptionFactory.create( "Failed to create keystore instance", "08000", generalSecurityEx); } + if (conf.trustStore() != null) { + InputStream inStream; + try { + inStream = new URL(conf.trustStore()).openStream(); + } catch (IOException ioexception) { + try { + inStream = new FileInputStream(conf.trustStore()); + } catch (FileNotFoundException fileNotFoundEx) { + throw new SQLException( + "Failed to find trustStore file. trustStore=" + conf.trustStore(), + "08000", + fileNotFoundEx); + } + } + try { + ks.load( + inStream, + conf.trustStorePassword() == null ? null : conf.trustStorePassword().toCharArray()); + } catch (IOException | NoSuchAlgorithmException | CertificateException ioEx) { + throw exceptionFactory.create("Failed load keyStore", "08000", ioEx); + } finally { + try { + inStream.close(); + } catch (IOException e) { + // eat + } + } + } else { + try (InputStream inStream = getInputStreamFromPath(conf.serverSslCert())) { + // generate a keyStore from the provided cert - try (InputStream inStream = getInputStreamFromPath(conf.serverSslCert())) { - // generate a keyStore from the provided cert + // Note: KeyStore requires it be loaded even if you don't load anything into it + // (will be initialized with "javax.net.ssl.trustStore") values. + ks.load(null); + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + Collection caList = cf.generateCertificates(inStream); + for (Certificate ca : caList) { + ks.setCertificateEntry(UUID.randomUUID().toString(), ca); + } - // Note: KeyStore requires it be loaded even if you don't load anything into it - // (will be initialized with "javax.net.ssl.trustStore") values. - ks.load(null); - CertificateFactory cf = CertificateFactory.getInstance("X.509"); - Collection caList = cf.generateCertificates(inStream); - for (Certificate ca : caList) { - ks.setCertificateEntry(UUID.randomUUID().toString(), ca); + } catch (IOException ioEx) { + throw exceptionFactory.create("Failed load keyStore", "08000", ioEx); + } catch (GeneralSecurityException generalSecurityEx) { + throw exceptionFactory.create( + "Failed to store certificate from serverSslCert into a keyStore", + "08000", + generalSecurityEx); } + } + try { TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); tmf.init(ks); @@ -135,11 +176,16 @@ public TrustManager[] getTrustManager( } } - } catch (IOException ioEx) { - throw exceptionFactory.create("Failed load keyStore", "08000", ioEx); + for (TrustManager tm : tmf.getTrustManagers()) { + if (tm instanceof X509TrustManager) { + trustManager = new X509TrustManager[] {(X509TrustManager) tm}; + break; + } + } + } catch (GeneralSecurityException generalSecurityEx) { throw exceptionFactory.create( - "Failed to store certificate from serverSslCert into a keyStore", + "Failed to load certificates from serverSslCert/trustStore", "08000", generalSecurityEx); } diff --git a/src/main/java/org/mariadb/jdbc/util/options/OptionAliases.java b/src/main/java/org/mariadb/jdbc/util/options/OptionAliases.java index e6c4ff140..d4e590842 100644 --- a/src/main/java/org/mariadb/jdbc/util/options/OptionAliases.java +++ b/src/main/java/org/mariadb/jdbc/util/options/OptionAliases.java @@ -17,6 +17,11 @@ public final class OptionAliases { OPTIONS_ALIASES.put("clientcertificatekeystoreurl", "keyStore"); OPTIONS_ALIASES.put("clientcertificatekeystorepassword", "keyStorePassword"); OPTIONS_ALIASES.put("clientcertificatekeystoretype", "keyStoreType"); + + OPTIONS_ALIASES.put("trustcertificatekeystoreurl", "trustStore"); + OPTIONS_ALIASES.put("trustcertificatekeystorepassword", "keyStorePassword"); + OPTIONS_ALIASES.put("trustcertificatekeystoretype", "trustStoreType"); + OPTIONS_ALIASES.put("nullcatalogmeanscurrent", "nullDatabaseMeansCurrent"); OPTIONS_ALIASES.put("databaseterm", "useCatalogTerm"); } diff --git a/src/main/resources/driver.properties b/src/main/resources/driver.properties index bb6ab2446..c57cb2e1d 100644 --- a/src/main/resources/driver.properties +++ b/src/main/resources/driver.properties @@ -81,4 +81,7 @@ nullDatabaseMeansCurrent=When enable, in DatabaseMetadata, will handle null data preserveInstants= This option controls whether the connector converts Timestamp values to the connection's time zone. connectionTimeZone=This option defines the connection's time zone. LOCAL retrieves the JVM's default time zone, SERVER fetches the server's global time zone upon connection creation, and allows specifying a server time zone without requesting it during connection establishment. forceConnectionTimeZoneToSession=This setting dictates whether the connector enforces the connection time zone for the session. -pinGlobalTxToPhysicalConnection=When set, will reuse previous connection used for XID. \ No newline at end of file +pinGlobalTxToPhysicalConnection=When set, will reuse previous connection used for XID. +connectionCollation=indicate what utf8mb4 collation to use. if not set, server default collation for utf8mb4 will be used +trustStore=File path of the trustStore file (similar to java System property \"javax.net.ssl.trustStore\". (legacy alias trustCertificateKeyStoreUrl). Use the specified file for trusted root certificates. When set, overrides serverSslCert. +trustStorePassword=Password for the trusted root certificate file (similar to java System property \"javax.net.ssl.trustStorePassword\").(legacy alias trustCertificateKeyStorePassword). diff --git a/src/test/java/org/mariadb/jdbc/integration/BatchTest.java b/src/test/java/org/mariadb/jdbc/integration/BatchTest.java index 70cf11c4e..b839a269a 100644 --- a/src/test/java/org/mariadb/jdbc/integration/BatchTest.java +++ b/src/test/java/org/mariadb/jdbc/integration/BatchTest.java @@ -22,6 +22,9 @@ public static void beforeAll2() throws SQLException { "CREATE TABLE BatchTest (t1 int not null primary key auto_increment, t2 LONGTEXT)"); createSequenceTables(); stmt.execute("CREATE TABLE timestampCal(id int, val TIMESTAMP)"); + stmt.execute( + "CREATE TABLE batchParamTest (sn_key INTEGER, sn_name VARCHAR(255), sn_description" + + " VARCHAR(255), sn_object LONGBLOB, CONSTRAINT pk_test_i PRIMARY KEY (sn_key) ) "); } @AfterAll @@ -29,6 +32,7 @@ public static void after2() throws SQLException { Statement stmt = sharedConn.createStatement(); stmt.execute("DROP TABLE IF EXISTS timestampCal"); stmt.execute("DROP TABLE IF EXISTS BatchTest"); + stmt.execute("DROP TABLE IF EXISTS batchParamTest"); } @Test @@ -657,6 +661,38 @@ private void batchWithError(Connection con) throws SQLException { } } + @Test + public void bigParameterFlushTest() throws SQLException { + int maxAllowedPacket = getMaxAllowedPacket(); + Assumptions.assumeTrue(maxAllowedPacket > 22 * 1024 * 1024); + String insertStatement = + "INSERT INTO batchParamTest (sn_key, sn_name, sn_description, sn_object) VALUES (?, ?, ?," + + " ?)"; + try (PreparedStatement prep = sharedConn.prepareStatement(insertStatement)) { + prep.setInt(1, 1000); + prep.setString(2, "name1"); + prep.setString(3, "desc1"); + prep.setBytes(4, new byte[334004]); + prep.addBatch(); + + prep.setInt(1, 1001); + prep.setString(2, "name2"); + prep.setString(3, "desc2"); + prep.setBytes(4, new byte[21963743]); + prep.addBatch(); + + prep.executeBatch(); + prep.clearBatch(); + sharedConn.commit(); + } + Statement stmt = sharedConn.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT * FROM batchParamTest"); + assertTrue(rs.next()); + assertEquals(334004, rs.getBytes(4).length); + assertTrue(rs.next()); + assertEquals(21963743, rs.getBytes(4).length); + } + @Test public void ensureCalendarSync() throws SQLException { Assumptions.assumeTrue(isMariaDBServer() && !isXpand()); diff --git a/src/test/java/org/mariadb/jdbc/integration/Common.java b/src/test/java/org/mariadb/jdbc/integration/Common.java index 8f8787863..7f4828fa7 100644 --- a/src/test/java/org/mariadb/jdbc/integration/Common.java +++ b/src/test/java/org/mariadb/jdbc/integration/Common.java @@ -269,7 +269,10 @@ public Connection createProxyCon(HaMode mode, String opts) throws SQLException { throw new SQLException("proxy error", i); } - String url = mDefUrl.replaceAll("//([^/]*)/", "//localhost:" + proxy.getLocalPort() + "/"); + String url = + mDefUrl.replaceAll( + "//(" + hostname + "|" + hostname + ":" + port + ")/", + "//localhost:" + proxy.getLocalPort() + "/"); if (mode != HaMode.NONE) { url = url.replaceAll( diff --git a/src/test/java/org/mariadb/jdbc/integration/ConfigurationTest.java b/src/test/java/org/mariadb/jdbc/integration/ConfigurationTest.java index f41ada7ed..b82456974 100644 --- a/src/test/java/org/mariadb/jdbc/integration/ConfigurationTest.java +++ b/src/test/java/org/mariadb/jdbc/integration/ConfigurationTest.java @@ -155,4 +155,47 @@ public void jdbcCompliantTruncation() throws SQLException { stmt.execute("SET @@global.sql_mode = '" + sqlMode + "'"); } } + + @Test + public void connectionCollationTest() throws SQLException { + try (org.mariadb.jdbc.Connection conn = + createCon("&connectionCollation=utf8mb4_vietnamese_ci")) { + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT @@session.COLLATION_CONNECTION"); + rs.next(); + assertEquals("utf8mb4_vietnamese_ci", rs.getString(1)); + } + + try (org.mariadb.jdbc.Connection conn = createCon("&connectionCollation=")) { + Statement stmt = conn.createStatement(); + ResultSet rs = + stmt.executeQuery("SELECT @@global.COLLATION_CONNECTION, @@session.COLLATION_CONNECTION"); + rs.next(); + if (rs.getString(1).startsWith("utf8mb4")) { + // could be different, because charset default might differ from server default + assertTrue( + rs.getString(2).equals(rs.getString(1)) + || "utf8mb4_unicode_ci".equals(rs.getString(1))); + } + } + + Statement stmt = sharedConn.createStatement(); + ResultSet rs = + stmt.executeQuery("SELECT @@global.COLLATION_CONNECTION, @@session.COLLATION_CONNECTION"); + rs.next(); + // could be different, because charset default might differ from server default + if (rs.getString(1).startsWith("utf8mb4")) { + assertTrue( + rs.getString(2).equals(rs.getString(1)) || "utf8mb4_unicode_ci".equals(rs.getString(1))); + } + + assertThrowsContains( + SQLException.class, + () -> createCon("&connectionCollation=utf8_vietnamese_ci"), + "wrong connection collation 'utf8_vietnamese_ci' only utf8mb4 collation are accepted"); + assertThrowsContains( + SQLException.class, + () -> createCon("&connectionCollation=utf8mb4_vietnamese_ci;SELECT"), + "wrong connection collation 'utf8mb4_vietnamese_ci;SELECT' name"); + } } diff --git a/src/test/java/org/mariadb/jdbc/integration/ConnectionTest.java b/src/test/java/org/mariadb/jdbc/integration/ConnectionTest.java index 695fdda5f..4998c9e85 100644 --- a/src/test/java/org/mariadb/jdbc/integration/ConnectionTest.java +++ b/src/test/java/org/mariadb/jdbc/integration/ConnectionTest.java @@ -344,6 +344,21 @@ public void databaseStateChange() throws SQLException { } } + @Test + public void databaseNoStateChange() throws SQLException { + try (Connection connection = createCon("&disableSessionTracking=true")) { + assertEquals(database, connection.getCatalog()); + + try (Statement stmt = connection.createStatement()) { + stmt.execute("drop database if exists _test_db"); + stmt.execute("create database _test_db"); + stmt.execute("USE _test_db"); + assertEquals("_test_db", connection.getCatalog()); + stmt.execute("drop database _test_db"); + } + } + } + @Test public void catalog() throws SQLException { Assumptions.assumeTrue( diff --git a/src/test/java/org/mariadb/jdbc/integration/ErrorTest.java b/src/test/java/org/mariadb/jdbc/integration/ErrorTest.java index 94cd132fc..140b5ae49 100644 --- a/src/test/java/org/mariadb/jdbc/integration/ErrorTest.java +++ b/src/test/java/org/mariadb/jdbc/integration/ErrorTest.java @@ -106,6 +106,9 @@ private void testPre41ErrorFormat(Connection con) throws Exception { @Test public void deadLockInformation() throws SQLException { + // skip for mysql, since idle_transaction_timeout doesn't exist, so test takes too much time + Assumptions.assumeTrue(isMariaDBServer()); + Statement stmt = sharedConn.createStatement(); stmt.execute("insert into deadlock(a) values(0), (1)"); @@ -132,6 +135,7 @@ public void deadLockInformation() throws SQLException { try { stmt2.execute("SET SESSION idle_transaction_timeout=2, innodb_lock_wait_timeout=2"); } catch (SQLException e) { + e.printStackTrace(); // eat ( for mariadb >= 10.3) } stmt2.execute("start transaction"); diff --git a/src/test/java/org/mariadb/jdbc/integration/MultiHostTest.java b/src/test/java/org/mariadb/jdbc/integration/MultiHostTest.java index a1421b419..7428e4cb0 100644 --- a/src/test/java/org/mariadb/jdbc/integration/MultiHostTest.java +++ b/src/test/java/org/mariadb/jdbc/integration/MultiHostTest.java @@ -58,7 +58,7 @@ public void ensureReadOnlyOnReplica() throws Exception { HostAddress hostAddress = conf.addresses().get(0); String url = mDefUrl.replaceAll( - "//([^/]*)/", + "//(" + hostname + "|" + hostname + ":" + port + ")/", String.format( "//mariadb1.example.com:%s,mariadb2.example.com:%s,mariadb3.example.com:%s/", hostAddress.port, hostAddress.port, hostAddress.port)); @@ -170,7 +170,7 @@ public void closedConnectionMulti() throws Exception { HostAddress hostAddress = conf.addresses().get(0); String url = mDefUrl.replaceAll( - "//([^/]*)/", + "//(" + hostname + "|" + hostname + ":" + port + ")/", String.format( "//address=(host=localhost)(port=9999)(type=master),address=(host=%s)(port=%s)(type=master)/", hostAddress.host, hostAddress.port)); @@ -188,7 +188,7 @@ public void closedConnectionMulti() throws Exception { url = mDefUrl.replaceAll( - "//([^/]*)/", + "//(" + hostname + "|" + hostname + ":" + port + ")/", String.format( "//%s:%s,%s,%s/", hostAddress.host, hostAddress.port, hostAddress.host, hostAddress.port)); @@ -250,7 +250,7 @@ public void masterFailover() throws Exception { String url = mDefUrl.replaceAll( - "//([^/]*)/" + database, + "//(" + hostname + "|" + hostname + ":" + port + ")/" + database, String.format( "//address=(host=localhost)(port=9999)(type=master),address=(host=localhost)(port=%s)(type=master),address=(host=%s)(port=%s)(type=master)/" + database, @@ -266,10 +266,10 @@ public void masterFailover() throws Exception { (Connection) DriverManager.getConnection( url - + "&deniedListTimeout=300&retriesAllDown=4&connectTimeout=20&deniedListTimeout=20")) { + + "&deniedListTimeout=300&retriesAllDown=4&connectTimeout=50&deniedListTimeout=50")) { Statement stmt = con.createStatement(); stmt.execute("SET @con=1"); - proxy.restart(50); + proxy.restart(100); con.isValid(1000); } @@ -279,7 +279,7 @@ public void masterFailover() throws Exception { (Connection) DriverManager.getConnection( url - + "&waitReconnectTimeout=300&retriesAllDown=10&connectTimeout=20&deniedListTimeout=20&socketTimeout=100")) { + + "&waitReconnectTimeout=300&retriesAllDown=10&connectTimeout=50&deniedListTimeout=50&socketTimeout=100")) { Statement stmt = con.createStatement(); stmt.execute("START TRANSACTION"); stmt.execute("SET @con=1"); @@ -300,7 +300,7 @@ public void masterFailover() throws Exception { try (Connection con = (Connection) DriverManager.getConnection( - url + "&retriesAllDown=4&connectTimeout=20&deniedListTimeout=20")) { + url + "&retriesAllDown=4&connectTimeout=50&deniedListTimeout=50")) { Statement stmt = con.createStatement(); con.setAutoCommit(false); stmt.execute("START TRANSACTION"); @@ -321,7 +321,7 @@ public void masterFailover() throws Exception { (Connection) DriverManager.getConnection( url - + "&transactionReplay=true&waitReconnectTimeout=300&deniedListTimeout=300&retriesAllDown=4&connectTimeout=20")) { + + "&transactionReplay=true&waitReconnectTimeout=300&deniedListTimeout=300&retriesAllDown=4&connectTimeout=50")) { Statement stmt = con.createStatement(); stmt.execute("DROP TABLE IF EXISTS testReplay"); stmt.execute("CREATE TABLE testReplay(id INT)"); @@ -373,7 +373,7 @@ public void masterStreamingFailover() throws Exception { String url = mDefUrl.replaceAll( - "//([^/]*)/", + "//(" + hostname + "|" + hostname + ":" + port + ")/", String.format( "//address=(host=localhost)(port=%s)(type=master)/", proxy.getLocalPort())); url = url.replaceAll("jdbc:mariadb:", "jdbc:mariadb:sequential:"); @@ -440,7 +440,7 @@ public void masterReplicationFailover() throws Exception { String url = mDefUrl.replaceAll( - "//([^/]*)/", + "//(" + hostname + "|" + hostname + ":" + port + ")/", String.format( "//localhost:%s,%s:%s/", proxy.getLocalPort(), hostAddress.host, hostAddress.port)); url = url.replaceAll("jdbc:mariadb:", "jdbc:mariadb:replication:"); @@ -502,7 +502,7 @@ public void masterReplicationStreamingFailover() throws Exception { String url = mDefUrl.replaceAll( - "//([^/]*)/", + "//(" + hostname + "|" + hostname + ":" + port + ")/", String.format( "//address=(host=localhost)(port=%s)(type=primary),address=(host=%s)(port=%s)(type=replica)/", proxy.getLocalPort(), hostAddress.host, hostAddress.port)); @@ -566,7 +566,7 @@ public Connection createProxyConKeep(String opts) throws SQLException { String url = mDefUrl.replaceAll( - "//([^/]*)/", + "//(" + hostname + "|" + hostname + ":" + port + ")/", String.format( "//%s:%s,localhost:%s/", hostAddress.host, hostAddress.port, proxy.getLocalPort())); url = url.replaceAll("jdbc:mariadb:", "jdbc:mariadb:replication:"); diff --git a/src/test/java/org/mariadb/jdbc/integration/ParameterMetaDataTest.java b/src/test/java/org/mariadb/jdbc/integration/ParameterMetaDataTest.java index a34560627..7443050b9 100644 --- a/src/test/java/org/mariadb/jdbc/integration/ParameterMetaDataTest.java +++ b/src/test/java/org/mariadb/jdbc/integration/ParameterMetaDataTest.java @@ -65,6 +65,11 @@ public void parameterMetaDataNotPreparable() throws Exception { ParameterMetaData meta = pstmt.getParameterMetaData(); assertEquals(1, meta.getParameterCount()); assertEquals(ParameterMetaData.parameterModeIn, meta.getParameterMode(1)); + + assertEquals(ParameterMetaData.parameterNullable, meta.isNullable(1)); + assertTrue(meta.isSigned(1)); + meta.unwrap(java.sql.ParameterMetaData.class); + meta.isWrapperFor(java.sql.ParameterMetaData.class); Common.assertThrowsContains( SQLSyntaxErrorException.class, () -> meta.getParameterTypeName(1), diff --git a/src/test/java/org/mariadb/jdbc/integration/PoolDataSourceTest.java b/src/test/java/org/mariadb/jdbc/integration/PoolDataSourceTest.java index 6620af926..d78f19af3 100644 --- a/src/test/java/org/mariadb/jdbc/integration/PoolDataSourceTest.java +++ b/src/test/java/org/mariadb/jdbc/integration/PoolDataSourceTest.java @@ -609,7 +609,7 @@ public void ensureUsingPool() throws Exception { stmt.execute("SELECT 1"); } catch (SQLException e) { - e.printStackTrace(); + // e.printStackTrace(); } }); } diff --git a/src/test/java/org/mariadb/jdbc/integration/PooledConnectionTest.java b/src/test/java/org/mariadb/jdbc/integration/PooledConnectionTest.java index 86de93401..639415cc0 100644 --- a/src/test/java/org/mariadb/jdbc/integration/PooledConnectionTest.java +++ b/src/test/java/org/mariadb/jdbc/integration/PooledConnectionTest.java @@ -72,7 +72,10 @@ public void testPoolFailover() throws Exception { throw new SQLException("proxy error", i); } - String url = mDefUrl.replaceAll("//([^/]*)/", "//localhost:" + proxy.getLocalPort() + "/"); + String url = + mDefUrl.replaceAll( + "//(" + hostname + "|" + hostname + ":" + port + ")/", + "//localhost:" + proxy.getLocalPort() + "/"); if (conf.sslMode() == SslMode.VERIFY_FULL) { url = url.replaceAll("sslMode=verify-full", "sslMode=verify-ca"); } diff --git a/src/test/java/org/mariadb/jdbc/integration/SslTest.java b/src/test/java/org/mariadb/jdbc/integration/SslTest.java index cfa5f0f00..553b39da0 100644 --- a/src/test/java/org/mariadb/jdbc/integration/SslTest.java +++ b/src/test/java/org/mariadb/jdbc/integration/SslTest.java @@ -7,11 +7,12 @@ import java.io.*; import java.nio.file.Paths; +import java.security.KeyStore; import java.security.NoSuchAlgorithmException; +import java.security.cert.Certificate; +import java.security.cert.CertificateFactory; import java.sql.*; -import java.util.Arrays; -import java.util.List; -import java.util.Locale; +import java.util.*; import javax.net.ssl.SSLContext; import org.junit.jupiter.api.*; import org.mariadb.jdbc.*; @@ -103,6 +104,18 @@ public static String retrieveCertificatePath() throws Exception { return serverCertificatePath; } + public static String retrieveCaCertificatePath() throws Exception { + String serverCertificatePath = checkFileExists(System.getProperty("serverCertificatePath")); + if (serverCertificatePath == null) { + serverCertificatePath = checkFileExists(System.getenv("TEST_DB_SERVER_CA_CERT")); + } + + if (serverCertificatePath == null) { + serverCertificatePath = checkFileExists("../../ssl/ca.crt"); + } + return serverCertificatePath; + } + private static String checkFileExists(String path) throws IOException { if (path == null) return null; File f = new File(path); @@ -127,7 +140,9 @@ private String getSslVersion(Connection con) throws SQLException { @Test public void simpleSsl() throws SQLException { - try (Connection con = createCon("sslMode=trust", sslPort)) { + try (Connection con = + createCon( + "sslMode=trust&enabledSslCipherSuites=TLS_DHE_RSA_WITH_AES_256_GCM_SHA384", sslPort)) { assertNotNull(getSslVersion(con)); } try (Connection con = createCon("sslMode=trust&useReadAheadInput=false", sslPort)) { @@ -252,7 +267,7 @@ void ensureUnixSocketSsl() throws SQLException { String path = rs.getString(2); String url = mDefUrl.replaceAll( - "//([^/]*)/", + "//(" + hostname + "|" + hostname + ":" + port + ")/", "//address=(localSocket=" + path + "),address=(host=" @@ -345,14 +360,19 @@ public void mutualAuthSsl() throws Exception { Assumptions.assumeTrue(System.getenv("TEST_DB_CLIENT_PKCS") != null); // without password - assertThrows( - SQLInvalidAuthorizationSpecException.class, - () -> - createCon( - baseMutualOptions - + "&sslMode=trust&keyStore=" - + System.getenv("TEST_DB_CLIENT_PKCS"), - sslPort)); + try { + assertThrows( + SQLInvalidAuthorizationSpecException.class, + () -> + createCon( + baseMutualOptions + + "&sslMode=trust&keyStore=" + + System.getenv("TEST_DB_CLIENT_PKCS"), + sslPort)); + } catch (Throwable e) { + e.printStackTrace(); + throw e; + } // with password try (Connection con = createCon( @@ -495,7 +515,10 @@ public void certificateMandatorySsl() throws Throwable { throw new SQLException("proxy error", i); } - String url = mDefUrl.replaceAll("//([^/]*)/", "//localhost:" + proxy.getLocalPort() + "/"); + String url = + mDefUrl.replaceAll( + "//(" + hostname + "|" + hostname + ":" + port + ")/", + "//localhost:" + proxy.getLocalPort() + "/"); Common.assertThrowsContains( SQLException.class, () -> @@ -546,6 +569,113 @@ && minVersion(11, 4, 1) } } + @Test + public void trustStoreParameter() throws Throwable { + Assumptions.assumeTrue(!"maxscale".equals(System.getenv("srv"))); + String serverCertPath = retrieveCertificatePath(); + String caCertPath = retrieveCaCertificatePath(); + Assumptions.assumeTrue(serverCertPath != null, "Canceled, server certificate not provided"); + + KeyStore ks = KeyStore.getInstance("jks"); + char[] pwdArray = "myPwd0".toCharArray(); + ks.load(null, pwdArray); + + File temptrustStoreFile = File.createTempFile("newKeyStoreFileName", ".jks"); + + KeyStore ks2 = KeyStore.getInstance("pkcs12"); + ks2.load(null, pwdArray); + File temptrustStoreFile2 = File.createTempFile("newKeyStoreFileName", ".pkcs12"); + + try (InputStream inStream = new File(serverCertPath).toURI().toURL().openStream()) { + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + Collection serverCertList = cf.generateCertificates(inStream); + List certs = new ArrayList<>(); + for (Iterator iter = serverCertList.iterator(); iter.hasNext(); ) { + certs.add(iter.next()); + } + if (caCertPath != null) { + try (InputStream inStream2 = new File(caCertPath).toURI().toURL().openStream()) { + CertificateFactory cf2 = CertificateFactory.getInstance("X.509"); + Collection caCertList = cf.generateCertificates(inStream); + for (Iterator iter = caCertList.iterator(); iter.hasNext(); ) { + certs.add(iter.next()); + } + } + } + for (Certificate cert : certs) { + ks.setCertificateEntry(hostname, cert); + ks2.setCertificateEntry(hostname, cert); + } + + try (FileOutputStream fos = new FileOutputStream(temptrustStoreFile.getPath())) { + ks.store(fos, pwdArray); + } + try (FileOutputStream fos = new FileOutputStream(temptrustStoreFile2.getPath())) { + ks2.store(fos, pwdArray); + } + } + + // certificate path, like /path/certificate.crt + try (Connection con = + createCon( + baseOptions + + "&sslMode=VERIFY_CA&trustStore=" + + temptrustStoreFile + + "&trustStoreType=jks&trustStorePassword=myPwd0", + sslPort)) { + assertNotNull(getSslVersion(con)); + } + + try (Connection con = + createCon( + baseOptions + + "&sslMode=VERIFY_CA&trustStore=" + + temptrustStoreFile + + "&trustStoreType=jks&trustStorePassword=myPwd0", + sslPort)) { + assertNotNull(getSslVersion(con)); + } + // with alias + try (Connection con = + createCon( + baseOptions + + "&sslMode=VERIFY_CA&trustCertificateKeystoreUrl=" + + temptrustStoreFile + + "&trustCertificateKeyStoretype=jks&trustCertificateKeystorePassword=myPwd0", + sslPort)) { + assertNotNull(getSslVersion(con)); + } + assertThrowsContains( + SQLException.class, + () -> + createCon( + baseOptions + + "&sslMode=VERIFY_CA&trustStore=" + + temptrustStoreFile + + "&trustStoreType=jks&trustStorePassword=wrongPwd", + sslPort), + "Failed load keyStore"); + try (Connection con = + createCon( + baseOptions + + "&sslMode=VERIFY_CA&trustStore=" + + temptrustStoreFile2 + + "&trustStoreType=pkcs12&trustStorePassword=myPwd0", + sslPort)) { + assertNotNull(getSslVersion(con)); + } + assertThrowsContains( + SQLException.class, + () -> + createCon( + baseOptions + + "&sslMode=VERIFY_CA&trustStore=" + + temptrustStoreFile2 + + "&trustStoreType=pkcs12&trustStorePassword=wrongPwd", + sslPort), + "Failed load keyStore"); + } + private String getServerCertificate(String serverCertPath) throws SQLException { try (BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(serverCertPath)))) { diff --git a/src/test/java/org/mariadb/jdbc/integration/XaTest.java b/src/test/java/org/mariadb/jdbc/integration/XaTest.java index 1e59ffdd8..970076a50 100644 --- a/src/test/java/org/mariadb/jdbc/integration/XaTest.java +++ b/src/test/java/org/mariadb/jdbc/integration/XaTest.java @@ -21,12 +21,15 @@ public class XaTest extends Common { private static MariaDbDataSource dataSource; private static MariaDbPoolDataSource poolDataSource; + private static MariaDbDataSource pinnedDataSource; + private static MariaDbPoolDataSource pinnedPoolDataSource; @AfterAll public static void drop() throws SQLException { Statement stmt = sharedConn.createStatement(); stmt.execute("DROP TABLE IF EXISTS xatable"); if (poolDataSource != null) poolDataSource.close(); + if (pinnedPoolDataSource != null) pinnedPoolDataSource.close(); } @BeforeAll @@ -42,6 +45,8 @@ public static void beforeAll2() throws SQLException { stmt.execute("FLUSH TABLES"); dataSource = new MariaDbDataSource(mDefUrl); poolDataSource = new MariaDbPoolDataSource(mDefUrl); + pinnedDataSource = new MariaDbDataSource(mDefUrl + "&pinGlobalTxToPhysicalConnection"); + pinnedPoolDataSource = new MariaDbPoolDataSource(mDefUrl + "&pinGlobalTxToPhysicalConnection"); } @Test @@ -79,6 +84,9 @@ public void testPinGlobalTxToPhysicalConnection() throws Exception { XAConnection c2 = xads.getXAConnection(); c2.getXAResource().prepare(txid); c2.getXAResource().commit(txid, false); + + c1.close(); + c2.close(); } @Test @@ -95,12 +103,24 @@ public void testPinGlobalTxToPhysicalConnection2() throws Exception { c2.getXAResource().start(txid, XAResource.TMJOIN); c2.getConnection().createStatement().executeQuery("SELECT 1"); c2.getXAResource().end(txid, XAResource.TMSUCCESS); + + c1.close(); + c2.close(); } @Test public void xaRmTest() throws Exception { - MariaDbDataSource dataSource1 = new MariaDbDataSource(mDefUrl); - MariaDbDataSource dataSource2 = new MariaDbDataSource(mDefUrl + "&test=t"); + xaRmTest(false); + xaRmTest(true); + } + + private void xaRmTest(boolean pinnedConnection) throws Exception { + MariaDbDataSource dataSource1 = + new MariaDbDataSource( + mDefUrl + (pinnedConnection ? "&pinGlobalTxToPhysicalConnection" : "")); + MariaDbDataSource dataSource2 = + new MariaDbDataSource( + mDefUrl + (pinnedConnection ? "&pinGlobalTxToPhysicalConnection" : "") + "&test=t"); XAConnection con1 = dataSource1.getXAConnection(); XAConnection con2 = dataSource1.getXAConnection(); XAConnection con3 = dataSource2.getXAConnection(); @@ -219,6 +239,9 @@ public void testCommit() throws Exception { Assumptions.assumeFalse("galera".equals(System.getenv("srv"))); testCommit(dataSource); testCommit(poolDataSource); + + testCommit(pinnedDataSource); + testCommit(pinnedPoolDataSource); } public void testCommit(XADataSource dataSource) throws Exception { @@ -239,6 +262,9 @@ public void testRollback() throws Exception { Assumptions.assumeFalse("galera".equals(System.getenv("srv"))); testRollback(dataSource); testRollback(poolDataSource); + + testRollback(pinnedDataSource); + testRollback(pinnedPoolDataSource); } public void testRollback(XADataSource dataSource) throws Exception { @@ -252,6 +278,11 @@ public void testRollback(XADataSource dataSource) throws Exception { @Test public void testRecover() throws Exception { + testRecover(dataSource); + testRecover(pinnedDataSource); + } + + private void testRecover(XADataSource dataSource) throws Exception { Assumptions.assumeFalse("galera".equals(System.getenv("srv"))); XAConnection xaConnection = dataSource.getXAConnection(); try { @@ -281,9 +312,17 @@ public void testRecover() throws Exception { @Test public void resumeAndJoinTest() throws Exception { + resumeAndJoinTest(false); + resumeAndJoinTest(true); + } + + private void resumeAndJoinTest(boolean pinnedConnection) throws Exception { + Assumptions.assumeFalse("galera".equals(System.getenv("srv"))); Connection conn1; - MariaDbDataSource ds = new MariaDbDataSource(mDefUrl); + MariaDbDataSource ds = + new MariaDbDataSource( + mDefUrl + (pinnedConnection ? "&pinGlobalTxToPhysicalConnection" : "")); XAConnection xaConn1 = null; Xid xid = newXid(); @@ -306,11 +345,15 @@ public void resumeAndJoinTest() throws Exception { xaRes1.start(xid, XAResource.TMNOFLAGS); conn1.createStatement().executeQuery("SELECT 1"); xaRes1.end(xid, XAResource.TMSUCCESS); - try { + if (pinnedConnection) { xaRes1.start(xid, XAResource.TMJOIN); - fail(); // without pinGlobalTxToPhysicalConnection=true - } catch (XAException xaex) { - xaConn1.close(); + } else { + try { + xaRes1.start(xid, XAResource.TMJOIN); + fail(); // without pinGlobalTxToPhysicalConnection=true + } catch (XAException xaex) { + xaConn1.close(); + } } } finally { @@ -319,4 +362,81 @@ public void resumeAndJoinTest() throws Exception { } } } + + @Test + public void errorCodeTest() throws SQLException, XAException { + XAConnection con = poolDataSource.getXAConnection(); + try { + Xid xid = newXid(); + Xid xid2 = newXid(); + XAResource xaResource = con.getXAResource(); + try { + xaResource.prepare(xid); + xaResource.commit(xid, true); + fail(); + } catch (XAException xae) { + assertEquals(XAException.XAER_RMFAIL, xae.errorCode); // 1399 + } + try { + xaResource.forget(xid); + xaResource.rollback(xid); + fail(); + } catch (XAException xae) { + assertEquals(XAException.XAER_NOTA, xae.errorCode); // 1397 + } + try { + xaResource.commit(xid, true); + fail(); + } catch (XAException xae) { + assertEquals(XAException.XAER_INVAL, xae.errorCode); // 1398 + } + try { + xaResource.commit(xid, true); + fail(); + } catch (XAException xae) { + assertEquals(XAException.XAER_INVAL, xae.errorCode); // 1398 + } + xaResource.start(xid2, XAResource.TMNOFLAGS); + xaResource.end(xid2, XAResource.TMSUCCESS); + try { + xaResource.start(xid2, XAResource.TMRESUME); + } catch (XAException e) { + // eat + } + try { + xaResource.end(xid2, XAResource.TMSUSPEND); + } catch (XAException e) { + // eat + } + + try { + xaResource.start(xid2, XAResource.TMJOIN); + } catch (XAException e) { + // eat + } + + try { + xaResource.end(xid2, XAResource.TMFAIL); + } catch (XAException e) { + // eat + } + xaResource.recover(XAResource.TMSTARTRSCAN | XAResource.TMENDRSCAN); + try { + xaResource.recover(XAResource.TMFAIL); + fail(); + } catch (XAException e) { + // eat + } + xaResource.recover(XAResource.TMNOFLAGS); + con.close(); + try { + xaResource.recover(XAResource.TMSTARTRSCAN); + fail(); + } catch (XAException e) { + // eat + } + } finally { + con.close(); + } + } } diff --git a/src/test/java/org/mariadb/jdbc/integration/codec/DateTimeCodecTest.java b/src/test/java/org/mariadb/jdbc/integration/codec/DateTimeCodecTest.java index 5563ecb8c..ced3b577f 100644 --- a/src/test/java/org/mariadb/jdbc/integration/codec/DateTimeCodecTest.java +++ b/src/test/java/org/mariadb/jdbc/integration/codec/DateTimeCodecTest.java @@ -534,6 +534,25 @@ public void getDate(ResultSet rs) throws SQLException { public void getDateTimezoneTest() throws SQLException { TimeZone initialTz = Calendar.getInstance().getTimeZone(); + TimeZone.setDefault(TimeZone.getTimeZone("GMT+11")); + try (Connection con = createCon()) { + // expect server tz to be different. + ResultSet rs = con.createStatement().executeQuery("SELECT @@session.time_zone"); + rs.next(); + String zoneId = rs.getString(1); + TimeZone serverTz = null; + try { + serverTz = TimeZone.getTimeZone(ZoneId.of(zoneId).normalized()); + } catch (DateTimeException e) { + try { + serverTz = TimeZone.getTimeZone(ZoneId.of(zoneId, ZoneId.SHORT_IDS).normalized()); + } catch (DateTimeException e2) { + // unknown zone id + } + } + assertNotEquals(TimeZone.getDefault(), serverTz); + } + TimeZone.setDefault(TimeZone.getTimeZone("GMT+8")); try (Connection conGmt8 = createCon("timezone=auto")) { getDateTimezoneTestGmt8(conGmt8, getPrepare(conGmt8), TimeZone.getTimeZone("GMT+8")); diff --git a/src/test/java/org/mariadb/jdbc/integration/codec/LongCodecTest.java b/src/test/java/org/mariadb/jdbc/integration/codec/LongCodecTest.java index b51320436..f7979b366 100644 --- a/src/test/java/org/mariadb/jdbc/integration/codec/LongCodecTest.java +++ b/src/test/java/org/mariadb/jdbc/integration/codec/LongCodecTest.java @@ -975,6 +975,8 @@ private void sendParam(Connection con) throws SQLException { prep.execute(); prep.setObject(1, 2L); prep.execute(); + prep.setObject(1, 3L, Types.DECIMAL); + prep.execute(); prep.setObject(1, BigInteger.TEN); prep.execute(); prep.setObject(1, null); @@ -985,6 +987,10 @@ private void sendParam(Connection con) throws SQLException { prep.execute(); prep.setObject(1, null, Types.BIGINT); prep.execute(); + assertThrowsContains( + SQLSyntaxErrorException.class, + () -> prep.setObject(1, 3L, Types.BLOB), + "Could not convert"); } ResultSet rs = con.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE) @@ -1000,6 +1006,7 @@ private void sendParam(Connection con) throws SQLException { rs.updateLong("t1", 60L); rs.updateRow(); assertEquals(60L, rs.getLong(2)); + assertTrue(rs.next()); assertTrue(rs.next()); assertEquals(10, rs.getLong(2)); @@ -1044,6 +1051,7 @@ private void sendParam(Connection con) throws SQLException { assertTrue(rs.next()); assertEquals(60L, rs.getLong(2)); + assertTrue(rs.next()); assertTrue(rs.next()); assertEquals(0L, rs.getLong(2)); diff --git a/src/test/java/org/mariadb/jdbc/integration/codec/VarcharCodecTest.java b/src/test/java/org/mariadb/jdbc/integration/codec/VarcharCodecTest.java index 439a299f2..6c4bf8109 100644 --- a/src/test/java/org/mariadb/jdbc/integration/codec/VarcharCodecTest.java +++ b/src/test/java/org/mariadb/jdbc/integration/codec/VarcharCodecTest.java @@ -896,6 +896,8 @@ private void sendParam(Connection con) throws SQLException { prep.execute(); prep.setObject(1, "e🌟2"); prep.execute(); + prep.setObject(1, '0', Types.BOOLEAN); + prep.execute(); prep.setObject(1, null); prep.execute(); prep.setObject(1, "e🌟3", Types.VARCHAR); @@ -922,6 +924,14 @@ private void sendParam(Connection con) throws SQLException { prep.execute(); prep.setNString(1, "e🌟12"); prep.execute(); + assertThrowsContains( + SQLSyntaxErrorException.class, + () -> prep.setObject(1, "e🌟12", Types.BLOB), + "Cannot convert"); + assertThrowsContains( + SQLSyntaxErrorException.class, + () -> prep.setObject(1, "e🌟12", Types.OTHER), + "Could not convert"); } ResultSet rs = @@ -945,6 +955,7 @@ private void sendParam(Connection con) throws SQLException { rs.updateString(2, null); rs.updateRow(); assertNull(rs.getString(2)); + assertTrue(rs.next()); assertTrue(rs.next()); assertNull(rs.getString(2)); @@ -1020,6 +1031,7 @@ private void sendParam(Connection con) throws SQLException { assertTrue(rs.next()); assertNull(rs.getString(2)); + assertTrue(rs.next()); assertTrue(rs.next()); assertEquals("f🌟12", rs.getString(2)); diff --git a/src/test/java/org/mariadb/jdbc/unit/util/ConfigurationTest.java b/src/test/java/org/mariadb/jdbc/unit/util/ConfigurationTest.java index 053b036ce..fa2c9cf57 100644 --- a/src/test/java/org/mariadb/jdbc/unit/util/ConfigurationTest.java +++ b/src/test/java/org/mariadb/jdbc/unit/util/ConfigurationTest.java @@ -904,10 +904,14 @@ public void builder() throws SQLException { .preserveInstants(true) .connectionTimeZone("SERVER") .forceConnectionTimeZoneToSession(false) + .connectionCollation("utf8mb4_vietnamese_ci") + .trustStore("/tmp/file") + .trustStorePassword("PWD") + .trustStoreType("jks") .build(); String expected = - "jdbc:mariadb://host1:3305,address=(host=host2)(port=3307)(type=replica)/db?user=me&password=***&timezone=UTC&connectionTimeZone=SERVER&forceConnectionTimeZoneToSession=false&preserveInstants=true&autocommit=false&nullDatabaseMeansCurrent=true&useCatalogTerm=SCHEMA&createDatabaseIfNotExist=true&useLocalSessionState=true&returnMultiValuesGeneratedIds=true&permitRedirect=false&transactionIsolation=REPEATABLE_READ&defaultFetchSize=10&maxQuerySizeToLog=100&maxAllowedPacket=8000&geometryDefaultType=default&restrictedAuth=mysql_native_password,client_ed25519&initSql=SET" - + " @@a='10'&socketFactory=someSocketFactory&connectTimeout=22&uuidAsString=true&tcpKeepAlive=false&tcpKeepIdle=10&tcpKeepCount=50&tcpKeepInterval=50&tcpAbortiveClose=true&localSocketAddress=localSocketAddress&socketTimeout=1000&useReadAheadInput=true&tlsSocketType=TLStype&sslMode=TRUST&serverSslCert=mycertPath&keyStore=/tmp&keyStorePassword=MyPWD&keyStoreType=JKS&trustStoreType=JKS&enabledSslCipherSuites=myCipher,cipher2&enabledSslProtocolSuites=TLSv1.2&fallbackToSystemKeyStore=false&fallbackToSystemTrustStore=false&allowMultiQueries=true&allowLocalInfile=false&useCompression=true&useAffectedRows=true&useBulkStmts=true&disablePipeline=true&cachePrepStmts=false&prepStmtCacheSize=2&useServerPrepStmts=true&credentialType=ENV&sessionVariables=blabla&connectionAttributes=bla=bla&servicePrincipalName=SPN&blankTableNameMeta=true&tinyInt1isBit=false&yearIsDateType=false&dumpQueriesOnException=true&includeInnodbStatusInDeadlockExceptions=true&includeThreadDumpInDeadlockExceptions=true&retriesAllDown=10&galeraAllowedState=A,B&transactionReplay=true&pool=true&poolName=myPool&maxPoolSize=16&minPoolSize=12&maxIdleTime=25000®isterJmxPool=false&poolValidMinDelay=260&useResetConnection=true&serverRsaPublicKeyFile=RSAPath&allowPublicKeyRetrieval=true"; + "jdbc:mariadb://host1:3305,address=(host=host2)(port=3307)(type=replica)/db?user=me&password=***&timezone=UTC&connectionCollation=utf8mb4_vietnamese_ci&connectionTimeZone=SERVER&forceConnectionTimeZoneToSession=false&preserveInstants=true&autocommit=false&nullDatabaseMeansCurrent=true&useCatalogTerm=SCHEMA&createDatabaseIfNotExist=true&useLocalSessionState=true&returnMultiValuesGeneratedIds=true&permitRedirect=false&transactionIsolation=REPEATABLE_READ&defaultFetchSize=10&maxQuerySizeToLog=100&maxAllowedPacket=8000&geometryDefaultType=default&restrictedAuth=mysql_native_password,client_ed25519&initSql=SET" + + " @@a='10'&socketFactory=someSocketFactory&connectTimeout=22&uuidAsString=true&tcpKeepAlive=false&tcpKeepIdle=10&tcpKeepCount=50&tcpKeepInterval=50&tcpAbortiveClose=true&localSocketAddress=localSocketAddress&socketTimeout=1000&useReadAheadInput=true&tlsSocketType=TLStype&sslMode=TRUST&serverSslCert=mycertPath&keyStore=/tmp&trustStore=/tmp/file&keyStorePassword=***&trustStorePassword=***&keyStoreType=JKS&trustStoreType=jks&enabledSslCipherSuites=myCipher,cipher2&enabledSslProtocolSuites=TLSv1.2&fallbackToSystemKeyStore=false&fallbackToSystemTrustStore=false&allowMultiQueries=true&allowLocalInfile=false&useCompression=true&useAffectedRows=true&useBulkStmts=true&disablePipeline=true&cachePrepStmts=false&prepStmtCacheSize=2&useServerPrepStmts=true&credentialType=ENV&sessionVariables=blabla&connectionAttributes=bla=bla&servicePrincipalName=SPN&blankTableNameMeta=true&tinyInt1isBit=false&yearIsDateType=false&dumpQueriesOnException=true&includeInnodbStatusInDeadlockExceptions=true&includeThreadDumpInDeadlockExceptions=true&retriesAllDown=10&galeraAllowedState=A,B&transactionReplay=true&pool=true&poolName=myPool&maxPoolSize=16&minPoolSize=12&maxIdleTime=25000®isterJmxPool=false&poolValidMinDelay=260&useResetConnection=true&serverRsaPublicKeyFile=RSAPath&allowPublicKeyRetrieval=true"; assertEquals(expected, conf.toString()); assertEquals(expected, conf.toBuilder().build().toString()); } @@ -1011,13 +1015,13 @@ public void toConf() throws SQLException { .startsWith( "Configuration:\n" + " * resulting Url :" - + " jdbc:mariadb://localhost/test?user=root&sslMode=VERIFY_CA&serverSslCert=/tmp/t.pem&keyStore=/tmp/keystore&keyStorePassword=kspass&trustStoreType=JKS\n" + + " jdbc:mariadb://localhost/test?user=root&sslMode=VERIFY_CA&serverSslCert=/tmp/t.pem&keyStore=/tmp/keystore&keyStorePassword=***&trustStoreType=JKS\n" + "Unknown options : None\n" + "\n" + "Non default options : \n" + " * database : test\n" + " * keyStore : /tmp/keystore\n" - + " * keyStorePassword : kspass\n" + + " * keyStorePassword : ***\n" + " * serverSslCert : /tmp/t.pem\n" + " * sslMode : VERIFY_CA\n" + " * trustStoreType : JKS\n"