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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/maven.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,6 @@ jobs:

- name: Build with Maven
run: mvn -B -f SimpleAPI/pom.xml package

- name: Verify legacy Connector/J fallback TLS
run: mvn -B -f SimpleAPI/pom.xml -Plegacy-mysql-tls-test -Dtest=MariaDbFallbackTlsTest test
17 changes: 17 additions & 0 deletions SimpleAPI/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
<timestamp>${maven.build.timestamp}</timestamp>
<maven.build.timestamp.format>yyyy-MM-dd HH:mm</maven.build.timestamp.format>
<build.number>NOTSET</build.number>
<mysql.connector.test.group>com.mysql</mysql.connector.test.group>
<mysql.connector.test.artifact>mysql-connector-j</mysql.connector.test.artifact>
<mysql.connector.test.version>9.5.0</mysql.connector.test.version>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<maven.compiler.release>21</maven.compiler.release>
Expand Down Expand Up @@ -298,6 +301,12 @@
<version>6.1.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>${mysql.connector.test.group}</groupId>
<artifactId>${mysql.connector.test.artifact}</artifactId>
<version>${mysql.connector.test.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
Expand Down Expand Up @@ -354,6 +363,14 @@
</dependency>
</dependencies>
<profiles>
<profile>
<id>legacy-mysql-tls-test</id>
<properties>
<mysql.connector.test.group>mysql</mysql.connector.test.group>
<mysql.connector.test.artifact>mysql-connector-java</mysql.connector.test.artifact>
<mysql.connector.test.version>8.0.12</mysql.connector.test.version>
</properties>
</profile>
<profile>
<id>normal</id>
<properties>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -297,16 +297,72 @@ String buildJdbcUrl(String driverClassName) {
if (postgreSqlTlsMode != PostgreSqlTlsMode.LEGACY) {
throw new IllegalArgumentException("PostgreSqlTlsMode applies only to PostgreSQL connections");
}
boolean maria = (dbType == DbType.MARIADB) || "org.mariadb.jdbc.Driver".equals(driverClassName);
boolean maria = !"com.mysql.cj.jdbc.Driver".equals(driverClassName)
&& (dbType == DbType.MARIADB || "org.mariadb.jdbc.Driver".equals(driverClassName));
Comment thread
BenCodez marked this conversation as resolved.
Resolved
String base = maria ? String.format("jdbc:mariadb://%s:%s/%s", host, port, database)
: String.format("jdbc:mysql://%s:%s/%s", host, port, database);

String fallbackTls = !maria && dbType == DbType.MARIADB && useSSL
? requiredMysqlFallbackTls(extra) : "";
return base + "?useSSL=" + useSSL + "&allowMultiQueries=true" + "&rewriteBatchedStatements=true"
+ "&useDynamicCharsetInfo=false" + "&allowPublicKeyRetrieval=" + publicKeyRetrieval
+ "&tcpKeepAlive=true" + "&connectTimeout=10000" + "&socketTimeout=30000" + "&serverTimezone=UTC"
+ extra;
+ extra + fallbackTls;
}

/** Require TLS on both modern and pre-8.0.13 Connector/J when MariaDB SSL is enabled. */
private String requiredMysqlFallbackTls(String extra) {
String mode = null;
boolean verifyCertificate = false;
String options = extra.startsWith("?") || extra.startsWith("&") ? extra.substring(1) : extra;
for (String parameter : options.split("&", -1)) {
String[] pair = parameter.split("=", 2);
String key;
String value;
try {
key = URLDecoder.decode(pair[0], StandardCharsets.UTF_8).trim().toLowerCase(Locale.ROOT);
value = pair.length == 2 ? URLDecoder.decode(pair[1], StandardCharsets.UTF_8).trim() : "";
} catch (IllegalArgumentException invalid) {
throw new IllegalArgumentException("Invalid encoded parameter in MariaDB fallback Line");
}
if (key.equals("sslmode")) {
String selected = value.toUpperCase(Locale.ROOT);
if (mode != null || !(selected.equals("REQUIRED") || selected.equals("VERIFY_CA")
|| selected.equals("VERIFY_IDENTITY"))) {
throw new IllegalArgumentException("MariaDB UseSSL requires a single mandatory Connector/J TLS mode");
}
mode = selected;
} else if (key.equals("usessl") || key.equals("requiressl")) {
if (!value.equalsIgnoreCase("true")) {
throw new IllegalArgumentException("MariaDB UseSSL forbids disabling Connector/J TLS");
}
} else if (key.equals("verifyservercertificate") && value.equalsIgnoreCase("true")) {
verifyCertificate = true;
}
}
String requiredMode = mode != null ? mode : verifyCertificate ? "VERIFY_CA" : "REQUIRED";
if (verifyCertificate && requiredMode.equals("REQUIRED")) requiredMode = "VERIFY_CA";
if (requiredMode.equals("VERIFY_IDENTITY") && !mysqlDriverSupportsSslMode()) {
throw new IllegalArgumentException("MariaDB VERIFY_IDENTITY requires Connector/J hostname verification support");
}
return "&sslMode=" + requiredMode + "&requireSSL=true"
+ (requiredMode.startsWith("VERIFY_") ? "&verifyServerCertificate=true" : "");
}

/** Query the driver's supported JDBC properties without opening a connection. */
private boolean mysqlDriverSupportsSslMode() {
try {
java.sql.Driver driver = (java.sql.Driver) Class.forName("com.mysql.cj.jdbc.Driver")
.getDeclaredConstructor().newInstance();
for (java.sql.DriverPropertyInfo property : driver.getPropertyInfo("jdbc:mysql://localhost/", null)) {
if ("sslMode".equals(property.name)) return true;
}
return false;
} catch (ReflectiveOperationException | SQLException | LinkageError failure) {
throw new IllegalArgumentException("Unable to determine Connector/J TLS verification support", failure);
}
}

private void rejectConflictingPostgreSqlTlsOptions(String extra) {
String options = extra.startsWith("?") || extra.startsWith("&") ? extra.substring(1) : extra;
for (String parameter : options.split("&", -1)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package com.bencodez.simpleapi.sql.mysql;

import static org.junit.jupiter.api.Assertions.*;

import java.lang.reflect.InvocationTargetException;
import java.nio.file.Files;
import java.nio.file.Path;
import javax.tools.ToolProvider;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import com.bencodez.simpleapi.tests.shared.SharedRuntimeClasspath;

class MariaDbDriverFallbackTest {
@TempDir Path directory;

@Test void mysqlFallbackUsesMysqlSchemeAndKeepsConfiguredOptions() {
ConnectionManager manager = manager(DbType.MARIADB);
manager.setUseSSL(true);
manager.setPublicKeyRetrieval(true);
manager.setStr("&customOption=retained");
String url = manager.buildJdbcUrl("com.mysql.cj.jdbc.Driver");
assertTrue(url.startsWith("jdbc:mysql://localhost:3306/votes?"));
assertTrue(url.contains("useSSL=true"));
assertTrue(url.contains("allowPublicKeyRetrieval=true"));
assertTrue(url.contains("&customOption=retained"));
assertTrue(url.endsWith("&sslMode=REQUIRED&requireSSL=true"));
assertEquals(DbType.MARIADB, manager.getDbType());
}

@Test void nativeMariaDbDriverRetainsMariaDbScheme() {
assertTrue(manager(DbType.MARIADB).buildJdbcUrl("org.mariadb.jdbc.Driver").startsWith("jdbc:mariadb:"));
}

@Test void explicitMariaDbDriverOverridesMysqlTypeAsBefore() {
assertTrue(manager(DbType.MYSQL).buildJdbcUrl("org.mariadb.jdbc.Driver").startsWith("jdbc:mariadb:"));
}

@Test void customMariaDbDriverRetainsConfiguredScheme() {
assertTrue(manager(DbType.MARIADB).buildJdbcUrl("example.CustomDriver").startsWith("jdbc:mariadb:"));
}

@Test void mysqlAndPostgresqlSelectionsRemainUnchanged() {
assertTrue(manager(DbType.MYSQL).buildJdbcUrl("com.mysql.cj.jdbc.Driver").startsWith("jdbc:mysql:"));
assertTrue(manager(DbType.POSTGRESQL).buildJdbcUrl("org.postgresql.Driver").startsWith("jdbc:postgresql:"));
}

@Test void absentMariaDbDriverResolvesToDriverThatAcceptsGeneratedUrl() throws Exception {
try (var loader = isolatedMysqlDriver()) {
assertThrows(ClassNotFoundException.class, () -> loader.loadClass("org.mariadb.jdbc.Driver"));
Class<?> type = loader.loadClass(ConnectionManager.class.getName());
Object manager = type.getConstructor(String.class, String.class, String.class, String.class, String.class)
.newInstance("localhost", "3306", "test", "", "votes");
Class<?> dbType = loader.loadClass(DbType.class.getName());
type.getMethod("setDbType", dbType).invoke(manager, dbType.getField("MARIADB").get(null));
var resolve = type.getDeclaredMethod("resolveDriver"); resolve.setAccessible(true);
assertEquals("com.mysql.cj.jdbc.Driver", resolve.invoke(manager));
var build = type.getDeclaredMethod("buildJdbcUrl", String.class); build.setAccessible(true);
String url = (String) build.invoke(manager, resolve.invoke(manager));
java.sql.Driver driver = (java.sql.Driver) loader.loadClass("com.mysql.cj.jdbc.Driver")
.getConstructor().newInstance();
assertTrue(driver.acceptsURL(url));
type.getMethod("setMariadbFallbackToMysqlDriver", boolean.class).invoke(manager, false);
InvocationTargetException rejected = assertThrows(InvocationTargetException.class, () -> resolve.invoke(manager));
assertInstanceOf(ClassNotFoundException.class, rejected.getCause());
}
}

private java.net.URLClassLoader isolatedMysqlDriver() throws Exception {
Path source = directory.resolve("com/mysql/cj/jdbc/Driver.java");
Files.createDirectories(source.getParent());
Files.writeString(source, """
package com.mysql.cj.jdbc;
public class Driver implements java.sql.Driver {
public boolean acceptsURL(String url) { return url.startsWith("jdbc:mysql:"); }
public java.sql.Connection connect(String url, java.util.Properties p) { return null; }
public java.sql.DriverPropertyInfo[] getPropertyInfo(String u, java.util.Properties p) { return new java.sql.DriverPropertyInfo[0]; }
public int getMajorVersion() { return 1; }
public int getMinorVersion() { return 0; }
public boolean jdbcCompliant() { return false; }
public java.util.logging.Logger getParentLogger() { return java.util.logging.Logger.getGlobal(); }
}
""");
assertEquals(0, ToolProvider.getSystemJavaCompiler().run(null, null, null,
"-proc:none", "-d", directory.toString(), source.toString()));
return SharedRuntimeClasspath.open(ConnectionManager.class.getProtectionDomain().getCodeSource().getLocation(),
directory.toUri().toURL());
}

private ConnectionManager manager(DbType type) {
ConnectionManager result = new ConnectionManager("localhost", "3306", "test", "", "votes");
result.setDbType(type);
return result;
}
}
Loading
Loading