Skip to content

Commit d491453

Browse files
authored
Merge pull request #105 from BenCodez/codex/mariadb-driver-fallback-url
Correct MariaDB fallback JDBC URL
2 parents fbec58c + 43c2f32 commit d491453

6 files changed

Lines changed: 404 additions & 2 deletions

File tree

‎.github/workflows/maven.yml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,6 @@ jobs:
2727

2828
- name: Build with Maven
2929
run: mvn -B -f SimpleAPI/pom.xml package
30+
31+
- name: Verify legacy Connector/J fallback TLS
32+
run: mvn -B -f SimpleAPI/pom.xml -Plegacy-mysql-tls-test -Dtest=MariaDbFallbackTlsTest test

‎SimpleAPI/pom.xml‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
<timestamp>${maven.build.timestamp}</timestamp>
1414
<maven.build.timestamp.format>yyyy-MM-dd HH:mm</maven.build.timestamp.format>
1515
<build.number>NOTSET</build.number>
16+
<mysql.connector.test.group>com.mysql</mysql.connector.test.group>
17+
<mysql.connector.test.artifact>mysql-connector-j</mysql.connector.test.artifact>
18+
<mysql.connector.test.version>9.5.0</mysql.connector.test.version>
1619
<maven.compiler.source>21</maven.compiler.source>
1720
<maven.compiler.target>21</maven.compiler.target>
1821
<maven.compiler.release>21</maven.compiler.release>
@@ -298,6 +301,12 @@
298301
<version>6.1.2</version>
299302
<scope>test</scope>
300303
</dependency>
304+
<dependency>
305+
<groupId>${mysql.connector.test.group}</groupId>
306+
<artifactId>${mysql.connector.test.artifact}</artifactId>
307+
<version>${mysql.connector.test.version}</version>
308+
<scope>test</scope>
309+
</dependency>
301310
<dependency>
302311
<groupId>org.mockito</groupId>
303312
<artifactId>mockito-core</artifactId>
@@ -354,6 +363,14 @@
354363
</dependency>
355364
</dependencies>
356365
<profiles>
366+
<profile>
367+
<id>legacy-mysql-tls-test</id>
368+
<properties>
369+
<mysql.connector.test.group>mysql</mysql.connector.test.group>
370+
<mysql.connector.test.artifact>mysql-connector-java</mysql.connector.test.artifact>
371+
<mysql.connector.test.version>8.0.12</mysql.connector.test.version>
372+
</properties>
373+
</profile>
357374
<profile>
358375
<id>normal</id>
359376
<properties>

‎SimpleAPI/src/main/java/com/bencodez/simpleapi/sql/mysql/ConnectionManager.java‎

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -297,16 +297,72 @@ String buildJdbcUrl(String driverClassName) {
297297
if (postgreSqlTlsMode != PostgreSqlTlsMode.LEGACY) {
298298
throw new IllegalArgumentException("PostgreSqlTlsMode applies only to PostgreSQL connections");
299299
}
300-
boolean maria = (dbType == DbType.MARIADB) || "org.mariadb.jdbc.Driver".equals(driverClassName);
300+
boolean maria = !"com.mysql.cj.jdbc.Driver".equals(driverClassName)
301+
&& (dbType == DbType.MARIADB || "org.mariadb.jdbc.Driver".equals(driverClassName));
301302
String base = maria ? String.format("jdbc:mariadb://%s:%s/%s", host, port, database)
302303
: String.format("jdbc:mysql://%s:%s/%s", host, port, database);
303304

305+
String fallbackTls = !maria && dbType == DbType.MARIADB && useSSL
306+
? requiredMysqlFallbackTls(extra) : "";
304307
return base + "?useSSL=" + useSSL + "&allowMultiQueries=true" + "&rewriteBatchedStatements=true"
305308
+ "&useDynamicCharsetInfo=false" + "&allowPublicKeyRetrieval=" + publicKeyRetrieval
306309
+ "&tcpKeepAlive=true" + "&connectTimeout=10000" + "&socketTimeout=30000" + "&serverTimezone=UTC"
307-
+ extra;
310+
+ extra + fallbackTls;
308311
}
309312

313+
/** Require TLS on both modern and pre-8.0.13 Connector/J when MariaDB SSL is enabled. */
314+
private String requiredMysqlFallbackTls(String extra) {
315+
String mode = null;
316+
boolean verifyCertificate = false;
317+
String options = extra.startsWith("?") || extra.startsWith("&") ? extra.substring(1) : extra;
318+
for (String parameter : options.split("&", -1)) {
319+
String[] pair = parameter.split("=", 2);
320+
String key;
321+
String value;
322+
try {
323+
key = URLDecoder.decode(pair[0], StandardCharsets.UTF_8).trim().toLowerCase(Locale.ROOT);
324+
value = pair.length == 2 ? URLDecoder.decode(pair[1], StandardCharsets.UTF_8).trim() : "";
325+
} catch (IllegalArgumentException invalid) {
326+
throw new IllegalArgumentException("Invalid encoded parameter in MariaDB fallback Line");
327+
}
328+
if (key.equals("sslmode")) {
329+
String selected = value.toUpperCase(Locale.ROOT);
330+
if (mode != null || !(selected.equals("REQUIRED") || selected.equals("VERIFY_CA")
331+
|| selected.equals("VERIFY_IDENTITY"))) {
332+
throw new IllegalArgumentException("MariaDB UseSSL requires a single mandatory Connector/J TLS mode");
333+
}
334+
mode = selected;
335+
} else if (key.equals("usessl") || key.equals("requiressl")) {
336+
if (!value.equalsIgnoreCase("true")) {
337+
throw new IllegalArgumentException("MariaDB UseSSL forbids disabling Connector/J TLS");
338+
}
339+
} else if (key.equals("verifyservercertificate") && value.equalsIgnoreCase("true")) {
340+
verifyCertificate = true;
341+
}
342+
}
343+
String requiredMode = mode != null ? mode : verifyCertificate ? "VERIFY_CA" : "REQUIRED";
344+
if (verifyCertificate && requiredMode.equals("REQUIRED")) requiredMode = "VERIFY_CA";
345+
if (requiredMode.equals("VERIFY_IDENTITY") && !mysqlDriverSupportsSslMode()) {
346+
throw new IllegalArgumentException("MariaDB VERIFY_IDENTITY requires Connector/J hostname verification support");
347+
}
348+
return "&sslMode=" + requiredMode + "&requireSSL=true"
349+
+ (requiredMode.startsWith("VERIFY_") ? "&verifyServerCertificate=true" : "");
350+
}
351+
352+
/** Query the driver's supported JDBC properties without opening a connection. */
353+
private boolean mysqlDriverSupportsSslMode() {
354+
try {
355+
java.sql.Driver driver = (java.sql.Driver) Class.forName("com.mysql.cj.jdbc.Driver")
356+
.getDeclaredConstructor().newInstance();
357+
for (java.sql.DriverPropertyInfo property : driver.getPropertyInfo("jdbc:mysql://localhost/", null)) {
358+
if ("sslMode".equals(property.name)) return true;
359+
}
360+
return false;
361+
} catch (ReflectiveOperationException | SQLException | LinkageError failure) {
362+
throw new IllegalArgumentException("Unable to determine Connector/J TLS verification support", failure);
363+
}
364+
}
365+
310366
private void rejectConflictingPostgreSqlTlsOptions(String extra) {
311367
String options = extra.startsWith("?") || extra.startsWith("&") ? extra.substring(1) : extra;
312368
for (String parameter : options.split("&", -1)) {
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package com.bencodez.simpleapi.sql.mysql;
2+
3+
import static org.junit.jupiter.api.Assertions.*;
4+
5+
import java.lang.reflect.InvocationTargetException;
6+
import java.nio.file.Files;
7+
import java.nio.file.Path;
8+
import javax.tools.ToolProvider;
9+
import org.junit.jupiter.api.Test;
10+
import org.junit.jupiter.api.io.TempDir;
11+
import com.bencodez.simpleapi.tests.shared.SharedRuntimeClasspath;
12+
13+
class MariaDbDriverFallbackTest {
14+
@TempDir Path directory;
15+
16+
@Test void mysqlFallbackUsesMysqlSchemeAndKeepsConfiguredOptions() {
17+
ConnectionManager manager = manager(DbType.MARIADB);
18+
manager.setUseSSL(true);
19+
manager.setPublicKeyRetrieval(true);
20+
manager.setStr("&customOption=retained");
21+
String url = manager.buildJdbcUrl("com.mysql.cj.jdbc.Driver");
22+
assertTrue(url.startsWith("jdbc:mysql://localhost:3306/votes?"));
23+
assertTrue(url.contains("useSSL=true"));
24+
assertTrue(url.contains("allowPublicKeyRetrieval=true"));
25+
assertTrue(url.contains("&customOption=retained"));
26+
assertTrue(url.endsWith("&sslMode=REQUIRED&requireSSL=true"));
27+
assertEquals(DbType.MARIADB, manager.getDbType());
28+
}
29+
30+
@Test void nativeMariaDbDriverRetainsMariaDbScheme() {
31+
assertTrue(manager(DbType.MARIADB).buildJdbcUrl("org.mariadb.jdbc.Driver").startsWith("jdbc:mariadb:"));
32+
}
33+
34+
@Test void explicitMariaDbDriverOverridesMysqlTypeAsBefore() {
35+
assertTrue(manager(DbType.MYSQL).buildJdbcUrl("org.mariadb.jdbc.Driver").startsWith("jdbc:mariadb:"));
36+
}
37+
38+
@Test void customMariaDbDriverRetainsConfiguredScheme() {
39+
assertTrue(manager(DbType.MARIADB).buildJdbcUrl("example.CustomDriver").startsWith("jdbc:mariadb:"));
40+
}
41+
42+
@Test void mysqlAndPostgresqlSelectionsRemainUnchanged() {
43+
assertTrue(manager(DbType.MYSQL).buildJdbcUrl("com.mysql.cj.jdbc.Driver").startsWith("jdbc:mysql:"));
44+
assertTrue(manager(DbType.POSTGRESQL).buildJdbcUrl("org.postgresql.Driver").startsWith("jdbc:postgresql:"));
45+
}
46+
47+
@Test void absentMariaDbDriverResolvesToDriverThatAcceptsGeneratedUrl() throws Exception {
48+
try (var loader = isolatedMysqlDriver()) {
49+
assertThrows(ClassNotFoundException.class, () -> loader.loadClass("org.mariadb.jdbc.Driver"));
50+
Class<?> type = loader.loadClass(ConnectionManager.class.getName());
51+
Object manager = type.getConstructor(String.class, String.class, String.class, String.class, String.class)
52+
.newInstance("localhost", "3306", "test", "", "votes");
53+
Class<?> dbType = loader.loadClass(DbType.class.getName());
54+
type.getMethod("setDbType", dbType).invoke(manager, dbType.getField("MARIADB").get(null));
55+
var resolve = type.getDeclaredMethod("resolveDriver"); resolve.setAccessible(true);
56+
assertEquals("com.mysql.cj.jdbc.Driver", resolve.invoke(manager));
57+
var build = type.getDeclaredMethod("buildJdbcUrl", String.class); build.setAccessible(true);
58+
String url = (String) build.invoke(manager, resolve.invoke(manager));
59+
java.sql.Driver driver = (java.sql.Driver) loader.loadClass("com.mysql.cj.jdbc.Driver")
60+
.getConstructor().newInstance();
61+
assertTrue(driver.acceptsURL(url));
62+
type.getMethod("setMariadbFallbackToMysqlDriver", boolean.class).invoke(manager, false);
63+
InvocationTargetException rejected = assertThrows(InvocationTargetException.class, () -> resolve.invoke(manager));
64+
assertInstanceOf(ClassNotFoundException.class, rejected.getCause());
65+
}
66+
}
67+
68+
private java.net.URLClassLoader isolatedMysqlDriver() throws Exception {
69+
Path source = directory.resolve("com/mysql/cj/jdbc/Driver.java");
70+
Files.createDirectories(source.getParent());
71+
Files.writeString(source, """
72+
package com.mysql.cj.jdbc;
73+
public class Driver implements java.sql.Driver {
74+
public boolean acceptsURL(String url) { return url.startsWith("jdbc:mysql:"); }
75+
public java.sql.Connection connect(String url, java.util.Properties p) { return null; }
76+
public java.sql.DriverPropertyInfo[] getPropertyInfo(String u, java.util.Properties p) { return new java.sql.DriverPropertyInfo[0]; }
77+
public int getMajorVersion() { return 1; }
78+
public int getMinorVersion() { return 0; }
79+
public boolean jdbcCompliant() { return false; }
80+
public java.util.logging.Logger getParentLogger() { return java.util.logging.Logger.getGlobal(); }
81+
}
82+
""");
83+
assertEquals(0, ToolProvider.getSystemJavaCompiler().run(null, null, null,
84+
"-proc:none", "-d", directory.toString(), source.toString()));
85+
return SharedRuntimeClasspath.open(ConnectionManager.class.getProtectionDomain().getCodeSource().getLocation(),
86+
directory.toUri().toURL());
87+
}
88+
89+
private ConnectionManager manager(DbType type) {
90+
ConnectionManager result = new ConnectionManager("localhost", "3306", "test", "", "votes");
91+
result.setDbType(type);
92+
return result;
93+
}
94+
}

0 commit comments

Comments
 (0)