package dbProcs; import com.mongodb.BasicDBObject; import com.mongodb.CommandResult; import com.mongodb.DB; import com.mongodb.DBObject; import com.mongodb.MongoClient; import com.mongodb.MongoClientOptions; import com.mongodb.MongoCredential; import com.mongodb.MongoException; import com.mongodb.MongoSocketException; import com.mongodb.MongoSocketOpenException; import com.mongodb.MongoTimeoutException; import com.mongodb.ServerAddress; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.nio.charset.Charset; import java.nio.file.Files; import java.nio.file.Paths; import java.util.Arrays; import java.util.Objects; import java.util.Properties; import java.util.concurrent.ConcurrentHashMap; import org.apache.commons.io.FileUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; /** * Used to create MongoDb connections using a singleton pattern for connection reuse. MongoClient * already includes internal connection pooling, so we only need one instance.
*
* This file is part of the Security Shepherd Project. * *

The Security Shepherd project is free software: you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free Software Foundation, either * version 3 of the License, or (at your option) any later version.
* *

The Security Shepherd project is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR * PURPOSE. See the GNU General Public License for more details.
* *

You should have received a copy of the GNU General Public License along with the Security * Shepherd project. If not, see . * * @author Paul */ public class MongoDatabase { private static final Logger log = LogManager.getLogger(MongoDatabase.class); // Singleton instance for the base MongoClient (no credentials) private static volatile MongoClient baseMongoClient; // Map of credential-based MongoClients (keyed by credential source/database) private static final ConcurrentHashMap credentialClients = new ConcurrentHashMap<>(); // Lock object for thread-safe initialization private static final Object lock = new Object(); // Default pool configuration private static final int DEFAULT_CONNECTIONS_PER_HOST = 10; private static final int DEFAULT_MIN_CONNECTIONS_PER_HOST = 2; /** * Method to close a MongoDb connection. Note: With singleton pattern, individual connections * should NOT be closed by callers. This method is kept for API compatibility but does nothing for * singleton clients. Use shutdown() to close all connections when the application is shutting * down. * * @param conn The connection (ignored for singleton clients) * @deprecated Use shutdown() instead when shutting down the application */ @Deprecated public static void closeConnection(MongoClient conn) { // Do not close singleton connections - they are managed by the pool // Only close if it's not one of our managed singletons if (conn != null && conn != baseMongoClient && !credentialClients.containsValue(conn)) { conn.close(); } else { log.debug("Ignoring close request for singleton MongoClient - use shutdown() instead"); } } /** * Method to get a MongoDb Challenge collection * * @param ApplicationRoot The running context of the application. * @param path The path to the properties file to use for this connection (filtered for path * traversal attacks) * @return A MongoDb Collection * @throws IOException */ public static MongoCredential getMongoChallengeCredentials(String ApplicationRoot, String path) throws IOException { // Some over paranoid input validation never hurts. Properties prop = new Properties(); path = path.replaceAll("\\.", "").replaceAll("/", ""); log.debug("Path = " + path); MongoCredential credential; String mongo_props = new File( Objects.requireNonNull( MongoDatabase.class.getResource("/challenges/" + path + ".properties")) .getFile()) .getAbsolutePath(); log.debug("Level Properties File = " + path + ".properties"); try (InputStream mongo_input = Files.newInputStream(Paths.get(mongo_props))) { prop.load(mongo_input); } String errorBase = "Missing property: "; String dbname = prop.getProperty("databaseName"); if (dbname == null) { throw new RuntimeException(errorBase + "databaseName"); } String username = prop.getProperty("databaseUsername"); if (username == null) { throw new RuntimeException(errorBase + "databaseUsername"); } String pw_property = prop.getProperty("databasePassword"); if (pw_property == null) { throw new RuntimeException(errorBase + "databasePassword"); } char[] password = pw_property.toCharArray(); credential = MongoCredential.createScramSha1Credential(username, dbname, password); return credential; } /** * Method to get a MongoDb collection name from property file * * @return A MongoDb collection name */ public static String getMongoChallengeCollName(String ApplicationRoot, String path) { Properties prop = new Properties(); // Some over paranoid input validation never hurts. path = path.replaceAll("\\.", "").replaceAll("/", ""); log.debug("Path = " + path); String mongo_props = new File( Objects.requireNonNull( Database.class.getResource("/challenges/" + path + ".properties")) .getFile()) .getAbsolutePath(); log.debug("Properties File: " + mongo_props); try (InputStream mongo_input = Files.newInputStream(Paths.get(mongo_props))) { prop.load(mongo_input); } catch (IOException e) { log.error("Could not load properties file: " + e.toString()); throw new RuntimeException(e); } String dbCollectionName = prop.getProperty("databaseCollection"); if (dbCollectionName == null) { throw new RuntimeException("Missing property : databaseCollection"); } return dbCollectionName; } /** * Method to get a singleton MongoDb Connection. MongoClient has internal connection pooling, so * the same instance is reused. * * @param ApplicationRoot The running context of the application (kept for API compatibility) * @return A singleton MongoDb Connection */ public static MongoClient getMongoDbConnection(String ApplicationRoot) { if (baseMongoClient == null) { synchronized (lock) { if (baseMongoClient == null) { baseMongoClient = createMongoClient(null); log.info("Created singleton MongoClient instance"); } } } return baseMongoClient; } /** * Creates a MongoClient with the configuration from properties file. * * @param credential Optional credential for authenticated connections * @return A new MongoClient instance */ private static MongoClient createMongoClient(MongoCredential credential) { Properties prop = loadMongoProperties(); String errorBase = "Missing property :"; String connectionHost = prop.getProperty("connectionHost"); if (connectionHost == null) { throw new RuntimeException(errorBase + "connectionHost"); } String connectionPort = prop.getProperty("connectionPort"); if (connectionPort == null) { throw new RuntimeException(errorBase + "connectionPort"); } String connectTimeout = prop.getProperty("connectTimeout"); if (connectTimeout == null) { throw new RuntimeException(errorBase + "connectTimeout"); } String socketTimeout = prop.getProperty("socketTimeout"); if (socketTimeout == null) { throw new RuntimeException(errorBase + "socketTimeout"); } String serverSelectionTimeout = prop.getProperty("serverSelectionTimeout"); if (serverSelectionTimeout == null) { throw new RuntimeException(errorBase + "serverSelectionTimeout"); } // Configure connection pool options MongoClientOptions.Builder optionsBuilder = MongoClientOptions.builder(); optionsBuilder.connectTimeout(Integer.parseInt(connectTimeout)); optionsBuilder.socketTimeout(Integer.parseInt(socketTimeout)); optionsBuilder.serverSelectionTimeout(Integer.parseInt(serverSelectionTimeout)); // Connection pool settings int connectionsPerHost = getIntProperty(prop, "pool.connectionsPerHost", DEFAULT_CONNECTIONS_PER_HOST); int minConnectionsPerHost = getIntProperty(prop, "pool.minConnectionsPerHost", DEFAULT_MIN_CONNECTIONS_PER_HOST); optionsBuilder.connectionsPerHost(connectionsPerHost); optionsBuilder.minConnectionsPerHost(minConnectionsPerHost); MongoClientOptions mongoOptions = optionsBuilder.build(); ServerAddress serverAddress = new ServerAddress(connectionHost, Integer.parseInt(connectionPort)); try { MongoClient mongoClient; if (credential != null) { mongoClient = new MongoClient(serverAddress, Arrays.asList(credential), mongoOptions); log.debug("Created MongoClient with credentials for: " + credential.getSource()); } else { mongoClient = new MongoClient(serverAddress, mongoOptions); log.debug("Created MongoClient without credentials"); } log.debug("Connection Host: " + connectionHost + ", Port: " + connectionPort); return mongoClient; } catch (NumberFormatException e) { log.fatal("The port in the properties file is not a number: " + e); throw new RuntimeException(e); } catch (MongoSocketOpenException e) { log.fatal("Mongo Doesn't seem to be running: " + e); throw new RuntimeException(e); } catch (MongoSocketException e) { log.fatal("Unable to get Mongodb connection (Is it on?): " + e); throw new RuntimeException(e); } catch (MongoException e) { log.fatal("Something went wrong with Mongo: " + e); throw new RuntimeException(e); } } /** * Loads MongoDB properties from the configuration file. * * @return Properties object containing MongoDB configuration */ private static Properties loadMongoProperties() { Properties prop = new Properties(); String mongo_props = Constants.MONGO_DB_PROP; try (InputStream mongo_input = new FileInputStream(mongo_props)) { prop.load(mongo_input); } catch (IOException e) { log.error("Could not load properties file: " + e.toString()); throw new RuntimeException(e); } return prop; } /** Helper method to get an integer property with a default value. */ private static int getIntProperty(Properties prop, String key, int defaultValue) { String value = prop.getProperty(key); if (value != null) { try { return Integer.parseInt(value); } catch (NumberFormatException e) { log.warn( "Invalid integer value for property '{}': {}, using default: {}", key, value, defaultValue); } } return defaultValue; } /** * Method to get a singleton MongoDb Connection with credentials. Each unique credential gets its * own MongoClient instance (with internal pooling). * * @param ApplicationRoot The running context of the application (kept for API compatibility) * @param credential The credential to connect to MongoDB * @return A singleton MongoDb Connection for the given credential */ public static MongoClient getMongoDbConnection( String ApplicationRoot, MongoCredential credential) { if (credential == null) { return getMongoDbConnection(ApplicationRoot); } // Use the credential source (database name) as the key String credentialKey = credential.getSource() + ":" + credential.getUserName(); return credentialClients.computeIfAbsent( credentialKey, key -> { log.debug("Creating new MongoClient for credential: " + credentialKey); return createMongoClient(credential); }); } /** * Method to get a MongoDb Database * * @param mongoClient mongoDb connection * @return A MongoDb Database */ public static DB getMongoDatabase(MongoClient mongoClient) { DB mongoDb = null; Properties prop = loadMongoProperties(); String dbname = prop.getProperty("databaseName"); if (dbname == null) { throw new RuntimeException("Missing property : databaseName"); } try { mongoDb = mongoClient.getDB(dbname); } catch (MongoSocketException | MongoTimeoutException e) { log.fatal("Unable to get Mongodb connection (Is it on?): " + e); } catch (MongoException e) { log.fatal("Something went wrong with Mongo: " + e); e.printStackTrace(); } catch (Exception e) { log.fatal("Something went wrong: " + e); e.printStackTrace(); } return mongoDb; } /** * Method to execute a mongo database JS file in a Mongo Database * * @param file the file to run * @param mongoClient to get connection to db */ public static void executeMongoScript(File file, MongoClient mongoClient) throws IOException { String data = FileUtils.readFileToString(file, Charset.defaultCharset()); DB db = MongoDatabase.getMongoDatabase(mongoClient); DBObject script = new BasicDBObject(); script.put("eval", String.format(data)); CommandResult result = db.command(script); log.debug("Mongo Result: " + result); } /** * Shuts down all MongoDB connections. This should be called when the application is shutting * down. */ public static void shutdown() { synchronized (lock) { log.info("Shutting down MongoDB connections..."); // Close base client if (baseMongoClient != null) { baseMongoClient.close(); baseMongoClient = null; log.debug("Base MongoClient closed"); } // Close all credential-based clients for (MongoClient client : credentialClients.values()) { client.close(); } credentialClients.clear(); log.info("All MongoDB connections shut down successfully"); } } /** Resets the singleton instances. This is primarily for testing purposes. */ public static void resetInstance() { shutdown(); } /** * Checks if the base MongoClient has been initialized. * * @return true if initialized, false otherwise */ public static boolean isInitialized() { return baseMongoClient != null; } }