diff --git a/chat2db-community-server/chat2db-community-domain/chat2db-community-domain-api/src/main/java/ai/chat2db/community/domain/api/config/DBConfig.java b/chat2db-community-server/chat2db-community-domain/chat2db-community-domain-api/src/main/java/ai/chat2db/community/domain/api/config/DBConfig.java index ae21d15c29..4d4be34e97 100644 --- a/chat2db-community-server/chat2db-community-domain/chat2db-community-domain-api/src/main/java/ai/chat2db/community/domain/api/config/DBConfig.java +++ b/chat2db-community-server/chat2db-community-domain/chat2db-community-domain-api/src/main/java/ai/chat2db/community/domain/api/config/DBConfig.java @@ -142,6 +142,10 @@ public String getDbType() { public void setDbType(String dbType) { this.dbType = dbType; + applyDbType(defaultDriverConfig); + if (!CollectionUtils.isEmpty(driverConfigList)) { + driverConfigList.forEach(this::applyDbType); + } } public String getName() { @@ -202,6 +206,7 @@ public DriverConfig getDefaultDriverConfig() { public void setDefaultDriverConfig(DriverConfig defaultDriverConfig) { this.defaultDriverConfig = defaultDriverConfig; + applyDbType(defaultDriverConfig); } public List getDriverConfigList() { @@ -211,12 +216,22 @@ public List getDriverConfigList() { public void setDriverConfigList(List driverConfigList) { this.driverConfigList = driverConfigList; if (!CollectionUtils.isEmpty(driverConfigList)) { + DriverConfig selectedDefault = null; for (DriverConfig driverConfig : driverConfigList) { - if (driverConfig.isDefaultDriver()) { - this.defaultDriverConfig = driverConfig; - break; + applyDbType(driverConfig); + if (driverConfig.isDefaultDriver() && selectedDefault == null) { + selectedDefault = driverConfig; } } + if (selectedDefault != null) { + this.defaultDriverConfig = selectedDefault; + } + } + } + + private void applyDbType(DriverConfig driverConfig) { + if (driverConfig != null && StringUtils.isBlank(driverConfig.getDbType())) { + driverConfig.setDbType(dbType); } } diff --git a/chat2db-community-server/chat2db-community-domain/chat2db-community-domain-core/src/main/java/ai/chat2db/community/domain/core/impl/db/DbJdbcDriverServiceImpl.java b/chat2db-community-server/chat2db-community-domain/chat2db-community-domain-core/src/main/java/ai/chat2db/community/domain/core/impl/db/DbJdbcDriverServiceImpl.java index 0ffa280bbe..d7d0bd4e57 100644 --- a/chat2db-community-server/chat2db-community-domain/chat2db-community-domain-core/src/main/java/ai/chat2db/community/domain/core/impl/db/DbJdbcDriverServiceImpl.java +++ b/chat2db-community-server/chat2db-community-domain/chat2db-community-domain-core/src/main/java/ai/chat2db/community/domain/core/impl/db/DbJdbcDriverServiceImpl.java @@ -21,6 +21,7 @@ import java.io.File; import java.io.IOException; +import java.io.UncheckedIOException; import java.nio.file.StandardCopyOption; import java.util.ArrayList; import java.util.LinkedHashMap; @@ -187,7 +188,12 @@ public String copyDrivers(List driverPaths) { exists = false; break; } - File target = new File(JdbcDriverConstants.DRIVER_LIB_PATH + file.getName()); + File target; + try { + target = new File(JdbcDriverConstants.createDriverLibDirectory(), file.getName()); + } catch (IOException e) { + throw new UncheckedIOException("Unable to create JDBC driver directory", e); + } FileUtil.copyFile(file, target, StandardCopyOption.REPLACE_EXISTING); driverNames.append(file.getName()).append(","); } @@ -249,7 +255,7 @@ public void deleteUnreferencedDriverJars(String jdbcDriver) { if (StringUtils.isBlank(jar) || isJarReferenced(jar)) { continue; } - File file = new File(JdbcDriverConstants.DRIVER_LIB_PATH + jar); + File file = new File(JdbcDriverConstants.getDriverLibPath() + jar); if (file.exists()) { try { FileUtil.del(file); @@ -300,7 +306,7 @@ private boolean driverExists(DriverConfig driverConfig) { return false; } for (String jarPath : driverConfig.getJdbcDriver().split(",")) { - File file = new File(JdbcDriverConstants.DRIVER_LIB_PATH + jarPath); + File file = new File(JdbcDriverConstants.getDriverLibPath() + jarPath); if (!file.exists()) { return false; } diff --git a/chat2db-community-server/chat2db-community-domain/chat2db-community-domain-core/src/test/java/ai/chat2db/community/domain/core/impl/db/DbJdbcDriverServiceImplDriverCopyTest.java b/chat2db-community-server/chat2db-community-domain/chat2db-community-domain-core/src/test/java/ai/chat2db/community/domain/core/impl/db/DbJdbcDriverServiceImplDriverCopyTest.java new file mode 100644 index 0000000000..f5e76423c5 --- /dev/null +++ b/chat2db-community-server/chat2db-community-domain/chat2db-community-domain-core/src/test/java/ai/chat2db/community/domain/core/impl/db/DbJdbcDriverServiceImplDriverCopyTest.java @@ -0,0 +1,54 @@ +package ai.chat2db.community.domain.core.impl.db; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +class DbJdbcDriverServiceImplDriverCopyTest { + + @TempDir + private Path temporaryDirectory; + + @Test + void copyDriversCreatesDirectoryUnderTheCurrentUserHome() throws Exception { + String originalHome = System.getProperty("user.home"); + String originalRuntimeMode = System.getProperty("chat2db.runtime.mode"); + byte[] driverBytes = "custom-driver".getBytes(StandardCharsets.UTF_8); + Path source = temporaryDirectory.resolve("custom-driver.jar"); + Files.write(source, driverBytes); + Path initializedHome = temporaryDirectory.resolve("initialized-home"); + Path activeHome = temporaryDirectory.resolve("active-home"); + try { + System.setProperty("chat2db.runtime.mode", "community"); + System.setProperty("user.home", initializedHome.toString()); + DbJdbcDriverServiceImpl service = new DbJdbcDriverServiceImpl(); + + System.setProperty("user.home", activeHome.toString()); + Path target = activeHome.resolve(".chat2db-community").resolve("jdbc-lib") + .resolve("custom-driver.jar"); + assertFalse(Files.exists(target.getParent())); + + assertEquals("custom-driver.jar", service.copyDrivers(List.of(source.toString()))); + assertArrayEquals(driverBytes, Files.readAllBytes(target)); + } finally { + restoreProperty("user.home", originalHome); + restoreProperty("chat2db.runtime.mode", originalRuntimeMode); + } + } + + private void restoreProperty(String name, String value) { + if (value == null) { + System.clearProperty(name); + } else { + System.setProperty(name, value); + } + } +} diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-generic/src/test/java/ai/chat2db/spi/sql/HsqldbTrustedDriverDownloadTest.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-generic/src/test/java/ai/chat2db/spi/sql/HsqldbTrustedDriverDownloadTest.java new file mode 100644 index 0000000000..31b4283705 --- /dev/null +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-generic/src/test/java/ai/chat2db/spi/sql/HsqldbTrustedDriverDownloadTest.java @@ -0,0 +1,97 @@ +package ai.chat2db.spi.sql; + +import ai.chat2db.community.domain.api.config.DriverConfig; +import ai.chat2db.community.domain.api.model.request.datasource.DbDataSourcePreConnectRequest; +import ai.chat2db.community.tools.constant.JdbcDriverConstants; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.sql.Connection; +import java.sql.DriverPropertyInfo; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class HsqldbTrustedDriverDownloadTest { + + private static final String HSQLDB_JAR = "hsqldb-2.7.3.jar"; + private static final String HSQLDB_DRIVER_CLASS = "org.hsqldb.jdbc.JDBCDriver"; + private static final String TRUSTED_HSQLDB_URL = + "https://repo1.maven.org/maven2/org/hsqldb/hsqldb/2.7.3/hsqldb-2.7.3.jar"; + private static final String MALICIOUS_URL = "http://127.0.0.1:1/hsqldb-2.7.3.jar"; + + @TempDir + private Path temporaryDirectory; + + @Test + void preConnectRequestCannotOverrideTheTrustedDriverDownloadUrl() { + DriverConfig requestDriver = new DriverConfig(); + requestDriver.setJdbcDriver(HSQLDB_JAR); + requestDriver.setJdbcDriverClass(HSQLDB_DRIVER_CLASS); + requestDriver.setDownloadJdbcDriverUrls(List.of(MALICIOUS_URL)); + DbDataSourcePreConnectRequest request = new DbDataSourcePreConnectRequest(); + request.setType("HSQLDB"); + request.setDriverConfig(requestDriver); + + List selectedUrls = JdbcDriverManager.resolveTrustedDownloadUrls( + request.getType(), request.getDriverConfig()); + + assertEquals(List.of(TRUSTED_HSQLDB_URL), selectedUrls); + assertFalse(selectedUrls.contains(MALICIOUS_URL)); + } + + @Test + void requestThatDoesNotExactlyMatchBuiltInDriverGetsNoTrustedUrls() { + DriverConfig requestDriver = new DriverConfig(); + requestDriver.setJdbcDriver(HSQLDB_JAR); + requestDriver.setJdbcDriverClass("attacker.Driver"); + requestDriver.setDownloadJdbcDriverUrls(List.of(MALICIOUS_URL)); + + assertEquals(List.of(), JdbcDriverManager.resolveTrustedDownloadUrls("HSQLDB", requestDriver)); + } + + @Test + void legacyPublicApisKeepUsingTheTrustedBuiltInDriverConfig() throws Exception { + String originalHome = System.getProperty("user.home"); + String originalRuntimeMode = System.getProperty("chat2db.runtime.mode"); + DriverConfig builtInDriver = Chat2DBContext.getDefaultDriverConfig("HSQLDB"); + try { + System.setProperty("chat2db.runtime.mode", "community"); + System.setProperty("user.home", temporaryDirectory.toString()); + Path sourceJar = Path.of(new URI(org.hsqldb.jdbc.JDBCDriver.class.getProtectionDomain() + .getCodeSource().getLocation().toString())); + Path targetJar = JdbcDriverConstants.createDriverLibDirectory().toPath().resolve(HSQLDB_JAR); + Files.copy(sourceJar, targetJar, StandardCopyOption.REPLACE_EXISTING); + JdbcDriverManager.unload(HSQLDB_JAR); + + assertEquals("HSQLDB", builtInDriver.getDbType()); + assertEquals(List.of(TRUSTED_HSQLDB_URL), JdbcDriverManager.resolveTrustedDownloadUrls( + builtInDriver.getDbType(), builtInDriver)); + assertNotNull(JdbcDriverManager.getClassLoader(builtInDriver).loadClass(HSQLDB_DRIVER_CLASS)); + DriverPropertyInfo[] properties = JdbcDriverManager.getProperty(builtInDriver); + assertNotNull(properties); + try (Connection connection = JdbcDriverManager.getConnection( + "jdbc:hsqldb:mem:legacy_driver_api;shutdown=true", builtInDriver)) { + assertFalse(connection.isClosed()); + } + } finally { + JdbcDriverManager.unload(HSQLDB_JAR); + restoreProperty("user.home", originalHome); + restoreProperty("chat2db.runtime.mode", originalRuntimeMode); + } + } + + private void restoreProperty(String name, String value) { + if (value == null) { + System.clearProperty(name); + } else { + System.setProperty(name, value); + } + } +} diff --git a/chat2db-community-server/chat2db-community-spi/src/main/java/ai/chat2db/spi/DefaultDBManager.java b/chat2db-community-server/chat2db-community-spi/src/main/java/ai/chat2db/spi/DefaultDBManager.java index 87ff471980..53d3137006 100644 --- a/chat2db-community-server/chat2db-community-spi/src/main/java/ai/chat2db/spi/DefaultDBManager.java +++ b/chat2db-community-server/chat2db-community-spi/src/main/java/ai/chat2db/spi/DefaultDBManager.java @@ -146,7 +146,7 @@ public Connection getConnection(ConnectInfo connectInfo) { driverConfig = Chat2DBContext.getDefaultDriverConfig(connectInfo.getDbType()); } connection = JdbcDriverManager.getConnection(url, connectInfo.getUser(), connectInfo.getPassword(), - driverConfig, connectInfo.getExtendMap()); + connectInfo.getDbType(), driverConfig, connectInfo.getExtendMap()); } catch (Exception e1) { close(connection, session, ssh); diff --git a/chat2db-community-server/chat2db-community-spi/src/main/java/ai/chat2db/spi/sql/JdbcDriverManager.java b/chat2db-community-server/chat2db-community-spi/src/main/java/ai/chat2db/spi/sql/JdbcDriverManager.java index 901af4580f..c20c64fad6 100644 --- a/chat2db-community-server/chat2db-community-spi/src/main/java/ai/chat2db/spi/sql/JdbcDriverManager.java +++ b/chat2db-community-server/chat2db-community-spi/src/main/java/ai/chat2db/spi/sql/JdbcDriverManager.java @@ -1,9 +1,11 @@ package ai.chat2db.spi.sql; -import ai.chat2db.community.tools.exception.ConnectionException; +import ai.chat2db.community.domain.api.config.DBConfig; import ai.chat2db.community.domain.api.config.DriverConfig; +import ai.chat2db.community.tools.exception.ConnectionException; import ai.chat2db.spi.model.datasource.DriverEntry; +import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -31,7 +33,7 @@ public class JdbcDriverManager { public static Connection getConnection(String url, DriverConfig driver) throws SQLException { Properties info = new Properties(); - return getConnection(url, info, driver); + return getConnection(url, info, driver.getDbType(), driver); } public static Connection getConnection(String url, String user, String password, DriverConfig driver) @@ -45,12 +47,18 @@ public static Connection getConnection(String url, String user, String password, info.put("password", password); } - return getConnection(url, info, driver); + return getConnection(url, info, driver.getDbType(), driver); } public static Connection getConnection(String url, String user, String password, DriverConfig driver, Map properties) throws SQLException { + return getConnection(url, user, password, driver.getDbType(), driver, properties); + } + + public static Connection getConnection(String url, String user, String password, String dbType, + DriverConfig driver, Map properties) + throws SQLException { Properties info = new Properties(); if (StringUtils.isNotEmpty(user)) { info.put("user", user); @@ -66,18 +74,23 @@ public static Connection getConnection(String url, String user, String password, } } } - return getConnection(url, info, driver); + return getConnection(url, info, dbType, driver); } public static Connection getConnection(String url, Properties info, DriverConfig driver) throws SQLException { + return getConnection(url, info, driver.getDbType(), driver); + } + + private static Connection getConnection(String url, Properties info, String dbType, DriverConfig driver) + throws SQLException { if (Objects.isNull(url)) { throw new SQLException("The url cannot be null", SQL_STATE_CODE); } DriverEntry driverEntry = DRIVER_ENTRY_MAP.get(driver.getJdbcDriver()); if (Objects.isNull(driverEntry)) { - driverEntry = getJDBCDriver(driver); + driverEntry = getJDBCDriver(dbType, driver); } Connection connection; try { @@ -107,7 +120,7 @@ public static DriverPropertyInfo[] getProperty(DriverConfig driver) DriverEntry driverEntry = DRIVER_ENTRY_MAP.get(driver.getJdbcDriver()); try { if (driverEntry == null) { - driverEntry = getJDBCDriver(driver); + driverEntry = getJDBCDriver(driver.getDbType(), driver); } String url = Objects.isNull(driver.getUrl()) ? "" : driver.getUrl(); return driverEntry.getDriver().getPropertyInfo(url, null); @@ -128,14 +141,14 @@ private static Connection tryConnectionAgain(DriverEntry driverEntry, String url return null; } - private static DriverEntry getJDBCDriver(DriverConfig driver) + private static DriverEntry getJDBCDriver(String dbType, DriverConfig driver) throws SQLException { synchronized (driver) { try { if (DRIVER_ENTRY_MAP.containsKey(driver.getJdbcDriver())) { return DRIVER_ENTRY_MAP.get(driver.getJdbcDriver()); } - ClassLoader cl = getClassLoader(driver); + ClassLoader cl = getClassLoader(dbType, driver); Driver d = (Driver) cl.loadClass(driver.getJdbcDriverClass()).newInstance(); DriverEntry driverEntry = DriverEntry.builder().driverConfig(driver).driver(d).build(); DRIVER_ENTRY_MAP.put(driver.getJdbcDriver(), driverEntry); @@ -148,6 +161,11 @@ private static DriverEntry getJDBCDriver(DriverConfig driver) } public static ClassLoader getClassLoader(DriverConfig driverConfig) throws IOException, ClassNotFoundException { + return getClassLoader(driverConfig.getDbType(), driverConfig); + } + + public static ClassLoader getClassLoader(String dbType, DriverConfig driverConfig) + throws IOException, ClassNotFoundException { String jarPath = driverConfig.getJdbcDriver(); if (CLASS_LOADER_MAP.containsKey(jarPath)) { return CLASS_LOADER_MAP.get(jarPath); @@ -158,9 +176,9 @@ public static ClassLoader getClassLoader(DriverConfig driverConfig) throws IOExc } URLClassLoader cl; try { - cl = getURLClassLoader(jarPath, driverConfig.getJdbcDriverClass(), false); + cl = getURLClassLoader(dbType, driverConfig, false); } catch (Exception e) { - cl = getURLClassLoader(jarPath, driverConfig.getJdbcDriverClass(), true); + cl = getURLClassLoader(dbType, driverConfig, true); } CLASS_LOADER_MAP.put(jarPath, cl); return cl; @@ -168,13 +186,14 @@ public static ClassLoader getClassLoader(DriverConfig driverConfig) throws IOExc } } - private static String getFilePath(String jarPath, boolean clean) { - return clean ? getNewFullPath(jarPath) : getFullPath(jarPath); + private static String getFilePath(String jarPath, boolean clean, List downloadUrls) { + return clean ? getNewFullPath(jarPath, downloadUrls) : getFullPath(jarPath, downloadUrls); } - private static List getJarUrlsFromZip(String zipFilePath, boolean clean) throws IOException { + private static List getJarUrlsFromZip(String zipFilePath, boolean clean, + List downloadUrls) throws IOException { List jarUrls = new ArrayList<>(); - String file = getFilePath(zipFilePath, clean); + String file = getFilePath(zipFilePath, clean, downloadUrls); File unzipFile = new File(file); File[] files = unzipFile.listFiles(); for (File f : files) { @@ -185,10 +204,11 @@ private static List getJarUrlsFromZip(String zipFilePath, boolean clean) th return jarUrls; } - private static List getJarUrlsFromPaths(String[] jarPaths, boolean clean) throws IOException { + private static List getJarUrlsFromPaths(String[] jarPaths, boolean clean, + List downloadUrls) throws IOException { List jarUrls = new ArrayList<>(); for (String jarPath : jarPaths) { - String file = getFilePath(jarPath, clean); + String file = getFilePath(jarPath, clean, downloadUrls); File driverFile = new File(file); if (!driverFile.exists()) { throw new IOException("Driver jar file not found: " + jarPath @@ -215,19 +235,48 @@ public static void unload(String jdbcDriver) { } } - private static URLClassLoader getURLClassLoader(String jarPath, String clazz, boolean clean) throws IOException, ClassNotFoundException { + private static URLClassLoader getURLClassLoader(String dbType, DriverConfig driverConfig, boolean clean) + throws IOException, ClassNotFoundException { + String jarPath = driverConfig.getJdbcDriver(); + List downloadUrls = resolveTrustedDownloadUrls(dbType, driverConfig); String[] jarPaths = jarPath.split(","); List jarUrls; if (jarPath.endsWith(".zip")) { - jarUrls = getJarUrlsFromZip(jarPath, clean); + jarUrls = getJarUrlsFromZip(jarPath, clean, downloadUrls); } else { - jarUrls = getJarUrlsFromPaths(jarPaths, clean); + jarUrls = getJarUrlsFromPaths(jarPaths, clean, downloadUrls); } URL[] urls = jarUrls.toArray(new URL[0]); URLClassLoader classLoader = new URLClassLoader(urls, ClassLoader.getSystemClassLoader()); - classLoader.loadClass(clazz); + classLoader.loadClass(driverConfig.getJdbcDriverClass()); return classLoader; } + static List resolveTrustedDownloadUrls(String dbType, DriverConfig requestedDriver) { + if (StringUtils.isBlank(dbType) || requestedDriver == null + || StringUtils.isBlank(requestedDriver.getJdbcDriver()) + || StringUtils.isBlank(requestedDriver.getJdbcDriverClass())) { + return List.of(); + } + DBConfig dbConfig; + try { + dbConfig = Chat2DBContext.getDBConfig(dbType); + } catch (IllegalArgumentException e) { + return List.of(); + } + if (dbConfig == null || CollectionUtils.isEmpty(dbConfig.getDriverConfigList())) { + return List.of(); + } + for (DriverConfig trustedDriver : dbConfig.getDriverConfigList()) { + if (trustedDriver != null + && StringUtils.equals(requestedDriver.getJdbcDriver(), trustedDriver.getJdbcDriver()) + && StringUtils.equals(requestedDriver.getJdbcDriverClass(), trustedDriver.getJdbcDriverClass())) { + List trustedUrls = trustedDriver.getDownloadJdbcDriverUrls(); + return CollectionUtils.isEmpty(trustedUrls) ? List.of() : List.copyOf(trustedUrls); + } + } + return List.of(); + } + } diff --git a/chat2db-community-server/chat2db-community-spi/src/main/java/ai/chat2db/spi/util/JdbcUtils.java b/chat2db-community-server/chat2db-community-spi/src/main/java/ai/chat2db/spi/util/JdbcUtils.java index 686be3423d..ab98de6980 100644 --- a/chat2db-community-server/chat2db-community-spi/src/main/java/ai/chat2db/spi/util/JdbcUtils.java +++ b/chat2db-community-server/chat2db-community-spi/src/main/java/ai/chat2db/spi/util/JdbcUtils.java @@ -173,7 +173,7 @@ public static DataSourceConnect testConnect(String url, String host, String port url = replaceUrlHostAndPortForSsh(url, host, port, ssh.getLocalPort()); } connection = JdbcDriverManager.getConnection(url, userName, password, - driverConfig, properties); + dbType, driverConfig, properties); if (DataSourceTypeEnum.MONGODB.name().equals(dbType)) { statement = connection.prepareStatement(MONGODB_TEST_CONNECT_COMMAND); diff --git a/chat2db-community-server/chat2db-community-start/src/test/java/ai/chat2db/community/start/test/api/ConfigOnlyDatabaseApiFlowTest.java b/chat2db-community-server/chat2db-community-start/src/test/java/ai/chat2db/community/start/test/api/ConfigOnlyDatabaseApiFlowTest.java index da30ef1149..8d8b762717 100644 --- a/chat2db-community-server/chat2db-community-start/src/test/java/ai/chat2db/community/start/test/api/ConfigOnlyDatabaseApiFlowTest.java +++ b/chat2db-community-server/chat2db-community-start/src/test/java/ai/chat2db/community/start/test/api/ConfigOnlyDatabaseApiFlowTest.java @@ -56,7 +56,7 @@ public class ConfigOnlyDatabaseApiFlowTest extends BaseTest { Path m2Jar = Path.of(originalHome, ".m2", "repository", "org", "hsqldb", "hsqldb", "2.7.3", "hsqldb-2.7.3.jar"); if (Files.isRegularFile(m2Jar)) { - Path libDir = Path.of(JdbcDriverConstants.DRIVER_LIB_PATH).toAbsolutePath().normalize(); + Path libDir = Path.of(JdbcDriverConstants.getDriverLibPath()).toAbsolutePath().normalize(); if (!libDir.startsWith(home.toAbsolutePath().normalize())) { throw new IOException("test JDBC directory escaped the temporary user home: " + libDir); } diff --git a/chat2db-community-server/chat2db-community-tools/src/main/java/ai/chat2db/community/tools/constant/JdbcDriverConstants.java b/chat2db-community-server/chat2db-community-tools/src/main/java/ai/chat2db/community/tools/constant/JdbcDriverConstants.java index 0f884642b9..20ed4e3945 100644 --- a/chat2db-community-server/chat2db-community-tools/src/main/java/ai/chat2db/community/tools/constant/JdbcDriverConstants.java +++ b/chat2db-community-server/chat2db-community-tools/src/main/java/ai/chat2db/community/tools/constant/JdbcDriverConstants.java @@ -3,13 +3,30 @@ import ai.chat2db.community.tools.util.ConfigUtils; import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; public final class JdbcDriverConstants { - public static final String DRIVER_LIB_PATH = ConfigUtils.getBasePath() + File.separator - + "jdbc-lib" + File.separator; + /** + * @deprecated Use {@link #getDriverLibPath()} so the path reflects the active runtime home. + */ + @Deprecated + public static final String DRIVER_LIB_PATH = getDriverLibPath(); + public static final String DOWNLOAD_URL_HOST = "https://cdn.chat2db-ai.com/lib/"; private JdbcDriverConstants() { } + + public static String getDriverLibPath() { + return ConfigUtils.getBasePath() + File.separator + "jdbc-lib" + File.separator; + } + + public static File createDriverLibDirectory() throws IOException { + Path directory = Path.of(getDriverLibPath()); + Files.createDirectories(directory); + return directory.toFile(); + } } diff --git a/chat2db-community-server/chat2db-community-tools/src/main/java/ai/chat2db/community/tools/util/JdbcJarUtils.java b/chat2db-community-server/chat2db-community-tools/src/main/java/ai/chat2db/community/tools/util/JdbcJarUtils.java index fa81a812b6..9ccfdf56b5 100644 --- a/chat2db-community-server/chat2db-community-tools/src/main/java/ai/chat2db/community/tools/util/JdbcJarUtils.java +++ b/chat2db-community-server/chat2db-community-tools/src/main/java/ai/chat2db/community/tools/util/JdbcJarUtils.java @@ -34,13 +34,6 @@ public class JdbcJarUtils { private static final OkHttpClient client = new OkHttpClient(); - static { - File file = new File(JdbcDriverConstants.DRIVER_LIB_PATH); - if (!file.exists()) { - file.mkdirs(); - } - } - public static void asyncDownload(List urls) throws Exception { for (String url : urls) { File file = outputFile(url); @@ -101,10 +94,6 @@ public void onResponse(Call call, Response response) { } public static void download(String url) throws IOException { - File pathfile = new File(JdbcDriverConstants.DRIVER_LIB_PATH); - if (!pathfile.exists()) { - pathfile.mkdirs(); - } File file = outputFile(url); deleteIfExists(file); String safeUrl = sanitizeUrl(url); @@ -163,7 +152,7 @@ private static File outputFile(String url) throws IOException { if (fileName.isBlank()) { throw downloadFailure(sanitizeUrl(url), "missing file name"); } - return new File(JdbcDriverConstants.DRIVER_LIB_PATH, fileName); + return new File(JdbcDriverConstants.createDriverLibDirectory(), fileName); } catch (URISyntaxException e) { throw downloadFailure(sanitizeUrl(url), e.getClass().getSimpleName()); } @@ -198,22 +187,28 @@ private static void deleteIfExists(File file) { } public static String getNewFullPath(String jarPath) { - String path = JdbcDriverConstants.DRIVER_LIB_PATH + jarPath; - File file = new File(path); + return getNewFullPath(jarPath, null); + } + + public static String getNewFullPath(String jarPath, List downloadUrls) { + File file = driverFile(jarPath); if (file.exists()) { file.delete(); } - return getFullPath(jarPath); + return getFullPath(jarPath, downloadUrls); } public static String getFullPath(String jarPath) { + return getFullPath(jarPath, null); + } + + public static String getFullPath(String jarPath, List downloadUrls) { if(jarPath.endsWith(".zip")){ - return getFullPathZip(jarPath); + return getFullPathZip(jarPath, downloadUrls); } - String path = JdbcDriverConstants.DRIVER_LIB_PATH + jarPath; - File file = new File(path); + File file = driverFile(jarPath); if (!file.exists()) { - String url = getDownloadUrl(jarPath); + String url = getDownloadUrl(jarPath, downloadUrls); try { download(url); } catch (IOException e) { @@ -224,15 +219,14 @@ public static String getFullPath(String jarPath) { } } } - return path; + return file.getPath(); } - private static String getFullPathZip(String jarPath) { - String path = JdbcDriverConstants.DRIVER_LIB_PATH + jarPath; - File file = new File(path); + private static String getFullPathZip(String jarPath, List downloadUrls) { + File file = driverFile(jarPath); File destDir = FileUtil.file(file.getParentFile(), FileUtil.mainName(file)); if (!file.exists()) { - String url = getDownloadUrl(jarPath); + String url = getDownloadUrl(jarPath, downloadUrls); try { download(url); return ZipUtil.unzip(file,destDir).getAbsolutePath(); @@ -253,4 +247,28 @@ private static String getFullPathZip(String jarPath) { private static String getDownloadUrl(String jarPath) { return JdbcDriverConstants.DOWNLOAD_URL_HOST + jarPath; } + + static String getDownloadUrl(String jarPath, List downloadUrls) { + if (downloadUrls != null) { + for (String downloadUrl : downloadUrls) { + if (jarPath.equals(fileName(downloadUrl))) { + return downloadUrl; + } + } + } + return getDownloadUrl(jarPath); + } + + private static String fileName(String url) { + try { + String path = new URI(url).getPath(); + return path == null ? null : new File(path).getName(); + } catch (URISyntaxException | RuntimeException e) { + return null; + } + } + + private static File driverFile(String jarPath) { + return new File(JdbcDriverConstants.getDriverLibPath(), jarPath); + } } diff --git a/chat2db-community-server/chat2db-community-tools/src/test/java/ai/chat2db/community/tools/util/JdbcJarUtilsTest.java b/chat2db-community-server/chat2db-community-tools/src/test/java/ai/chat2db/community/tools/util/JdbcJarUtilsTest.java index 253e1eec46..e336cd1edc 100644 --- a/chat2db-community-server/chat2db-community-tools/src/test/java/ai/chat2db/community/tools/util/JdbcJarUtilsTest.java +++ b/chat2db-community-server/chat2db-community-tools/src/test/java/ai/chat2db/community/tools/util/JdbcJarUtilsTest.java @@ -3,6 +3,8 @@ import java.io.File; import java.io.IOException; import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.UUID; @@ -14,7 +16,9 @@ import com.sun.net.httpserver.HttpServer; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -26,6 +30,9 @@ class JdbcJarUtilsTest { private final List filesToDelete = new ArrayList<>(); private HttpServer server; + @TempDir + private Path temporaryDirectory; + @AfterEach void cleanUp() { if (server != null) { @@ -114,13 +121,75 @@ void sanitizeUrlRemovesUserInfoQueryAndFragment() { assertFalse(sanitized.contains("token=secret")); } + @Test + void configuredDriverUrlIsUsedWhenTheJarIsMissing() throws Exception { + String fileName = uniqueFileName(); + File output = outputFile(fileName); + byte[] driverBytes = "configured-driver".getBytes(StandardCharsets.UTF_8); + server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0); + server.createContext("/maven/" + fileName, exchange -> { + exchange.sendResponseHeaders(200, driverBytes.length); + try (var responseBody = exchange.getResponseBody()) { + responseBody.write(driverBytes); + } + }); + server.start(); + String configuredUrl = "http://127.0.0.1:" + server.getAddress().getPort() + + "/maven/" + fileName; + + String path = JdbcJarUtils.getFullPath(fileName, List.of(configuredUrl)); + + assertEquals(output.getAbsolutePath(), path); + assertArrayEquals(driverBytes, java.nio.file.Files.readAllBytes(output.toPath())); + } + + @Test + void hsqldbJarResolvesToItsConfiguredMavenUrl() { + String mavenUrl = "https://repo1.maven.org/maven2/org/hsqldb/hsqldb/2.7.3/hsqldb-2.7.3.jar"; + + assertEquals(mavenUrl, + JdbcJarUtils.getDownloadUrl("hsqldb-2.7.3.jar", List.of(mavenUrl))); + } + + @Test + void driverLibPathTracksUserHomeAfterClassInitialization() { + JdbcDriverConstants.getDriverLibPath(); + String originalHome = System.getProperty("user.home"); + String originalRuntimeMode = System.getProperty("chat2db.runtime.mode"); + Path firstHome = temporaryDirectory.resolve("first-home"); + Path secondHome = temporaryDirectory.resolve("second-home"); + try { + System.setProperty("chat2db.runtime.mode", "community"); + System.setProperty("user.home", firstHome.toString()); + assertEquals(expectedDriverPath(firstHome), JdbcDriverConstants.getDriverLibPath()); + + System.setProperty("user.home", secondHome.toString()); + assertEquals(expectedDriverPath(secondHome), JdbcDriverConstants.getDriverLibPath()); + } finally { + restoreProperty("user.home", originalHome); + restoreProperty("chat2db.runtime.mode", originalRuntimeMode); + } + } + private String uniqueFileName() { return "jdbc-driver-test-" + UUID.randomUUID() + ".jar"; } private File outputFile(String fileName) { - File output = new File(JdbcDriverConstants.DRIVER_LIB_PATH, fileName); + File output = new File(JdbcDriverConstants.getDriverLibPath(), fileName); filesToDelete.add(output); return output; } + + private String expectedDriverPath(Path home) { + return home.resolve(".chat2db-community").resolve("jdbc-lib") + File.separator; + } + + private void restoreProperty(String name, String value) { + if (value == null) { + System.clearProperty(name); + } else { + System.setProperty(name, value); + } + } } diff --git a/chat2db-community-server/chat2db-community-web/src/main/java/ai/chat2db/community/web/api/adapter/db/MultipartJdbcDriverUploadAdapter.java b/chat2db-community-server/chat2db-community-web/src/main/java/ai/chat2db/community/web/api/adapter/db/MultipartJdbcDriverUploadAdapter.java index e589277898..270f813f92 100644 --- a/chat2db-community-server/chat2db-community-web/src/main/java/ai/chat2db/community/web/api/adapter/db/MultipartJdbcDriverUploadAdapter.java +++ b/chat2db-community-server/chat2db-community-web/src/main/java/ai/chat2db/community/web/api/adapter/db/MultipartJdbcDriverUploadAdapter.java @@ -17,10 +17,10 @@ public class MultipartJdbcDriverUploadAdapter implements IDbJdbcDriverUploadServ @Override public List upload(MultipartFile[] files) throws IOException { List uploadedFiles = new ArrayList<>(); + File driverDirectory = JdbcDriverConstants.createDriverLibDirectory(); for (MultipartFile file : files) { String originalFilename = FilenameUtils.getName(file.getOriginalFilename()); - String location = JdbcDriverConstants.DRIVER_LIB_PATH + originalFilename; - file.transferTo(new File(location)); + file.transferTo(new File(driverDirectory, originalFilename)); uploadedFiles.add(originalFilename); } return uploadedFiles; diff --git a/chat2db-community-server/chat2db-community-web/src/test/java/ai/chat2db/community/web/api/adapter/db/MultipartJdbcDriverUploadAdapterTest.java b/chat2db-community-server/chat2db-community-web/src/test/java/ai/chat2db/community/web/api/adapter/db/MultipartJdbcDriverUploadAdapterTest.java new file mode 100644 index 0000000000..151500c11e --- /dev/null +++ b/chat2db-community-server/chat2db-community-web/src/test/java/ai/chat2db/community/web/api/adapter/db/MultipartJdbcDriverUploadAdapterTest.java @@ -0,0 +1,103 @@ +package ai.chat2db.community.web.api.adapter.db; + +import ai.chat2db.community.tools.constant.JdbcDriverConstants; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +class MultipartJdbcDriverUploadAdapterTest { + + @TempDir + private Path temporaryDirectory; + + @Test + void uploadCreatesDirectoryUnderTheCurrentUserHome() throws Exception { + String originalHome = System.getProperty("user.home"); + String originalRuntimeMode = System.getProperty("chat2db.runtime.mode"); + byte[] driverBytes = "uploaded-driver".getBytes(StandardCharsets.UTF_8); + Path initializedHome = temporaryDirectory.resolve("initialized-home"); + Path activeHome = temporaryDirectory.resolve("active-home"); + try { + System.setProperty("chat2db.runtime.mode", "community"); + System.setProperty("user.home", initializedHome.toString()); + JdbcDriverConstants.getDriverLibPath(); + MultipartJdbcDriverUploadAdapter adapter = new MultipartJdbcDriverUploadAdapter(); + + System.setProperty("user.home", activeHome.toString()); + Path target = activeHome.resolve(".chat2db-community").resolve("jdbc-lib") + .resolve("uploaded-driver.jar"); + assertFalse(Files.exists(target.getParent())); + + assertEquals(List.of("uploaded-driver.jar"), + adapter.upload(new MultipartFile[]{multipartFile("uploaded-driver.jar", driverBytes)})); + assertArrayEquals(driverBytes, Files.readAllBytes(target)); + } finally { + restoreProperty("user.home", originalHome); + restoreProperty("chat2db.runtime.mode", originalRuntimeMode); + } + } + + private MultipartFile multipartFile(String fileName, byte[] contents) { + return new MultipartFile() { + @Override + public String getName() { + return fileName; + } + + @Override + public String getOriginalFilename() { + return fileName; + } + + @Override + public String getContentType() { + return "application/java-archive"; + } + + @Override + public boolean isEmpty() { + return contents.length == 0; + } + + @Override + public long getSize() { + return contents.length; + } + + @Override + public byte[] getBytes() { + return contents.clone(); + } + + @Override + public InputStream getInputStream() { + return new java.io.ByteArrayInputStream(contents); + } + + @Override + public void transferTo(File destination) throws IOException { + Files.write(destination.toPath(), contents); + } + }; + } + + private void restoreProperty(String name, String value) { + if (value == null) { + System.clearProperty(name); + } else { + System.setProperty(name, value); + } + } +}