server/imports/core/connection/index.js
import { Database, Logger, Error } from '/server/imports/modules';
import ConnectionHelper from './helper';
const fs = require('fs');
const mongodbUrlParser = require('mongodb-url');
const jsonParser = require('json5');
const Connection = function () {
};
const addSSLOptions = function (obj, result) {
if (obj.rootCAFile) {
result.sslValidate = true;
result.sslCA = Buffer.from(obj.rootCAFile);
}
if (obj.certificateFile) result.sslCert = Buffer.from(obj.certificateFile);
if (obj.certificateKeyFile) result.sslKey = Buffer.from(obj.certificateKeyFile);
if (obj.passPhrase) result.sslPass = obj.passPhrase;
if (obj.disableHostnameVerification) result.checkServerIdentity = false;
};
const setOptionsToConnectionFromParsedUrl = function (connection, parsedUrl) {
if (parsedUrl.server_options) {
connection.options.connectionTimeout = (parsedUrl.server_options.socketOptions && parsedUrl.server_options.socketOptions.connectTimeoutMS)
? parsedUrl.server_options.socketOptions.connectTimeoutMS : '';
connection.options.socketTimeout = (parsedUrl.server_options.socketOptions && parsedUrl.server_options.socketOptions.socketTimeoutMS)
? parsedUrl.server_options.socketOptions.socketTimeoutMS : '';
connection.ssl.enabled = !!parsedUrl.server_options.ssl;
}
connection.options.replicaSetName = (parsedUrl.rs_options && parsedUrl.rs_options.rs_name) ? parsedUrl.rs_options.rs_name : '';
connection.options.readPreference = parsedUrl.db_options.read_preference;
};
const setAuthToConnectionFromParsedUrl = function (connection, parsedUrl) {
connection.authenticationType = parsedUrl.db_options.authMechanism ? parsedUrl.db_options.authMechanism.toLowerCase().replace(new RegExp('-', 'g'), '_') : '';
if (connection.authenticationType) connection[connection.authenticationType] = {};
if (parsedUrl.db_options.gssapiServiceName && connection.authenticationType === 'gssapi') connection.gssapi.serviceName = parsedUrl.db_options.gssapiServiceName;
if (connection.authenticationType === 'mongodb_x509') delete connection.ssl;
if (parsedUrl.auth) {
// if auth exists there should be an authentication, even there's no authMechanism set
connection.authenticationType = connection.authenticationType || 'scram_sha_1';
connection[connection.authenticationType] = connection[connection.authenticationType] || {};
connection[connection.authenticationType].username = parsedUrl.auth.user ? parsedUrl.auth.user : '';
connection[connection.authenticationType].password = parsedUrl.auth.password ? parsedUrl.auth.password : '';
}
if (connection.authenticationType === 'mongodb_cr' || connection.authenticationType === 'scram_sha_1' || connection.authenticationType === 'scram_sha_256') {
connection[connection.authenticationType].authSource = parsedUrl.db_options.authSource ? parsedUrl.db_options.authSource : connection.databaseName;
}
};
const checkAuthenticationOfConnection = function (connection) {
if (connection.authenticationType !== 'scram_sha_1') delete connection.scram_sha_1;
if (connection.authenticationType !== 'scram_sha_256') delete connection.scram_sha_256;
if (connection.authenticationType !== 'mongodb_cr') delete connection.mongodb_cr;
if (connection.authenticationType !== 'mongodb_x509') delete connection.mongodb_x509;
if (connection.authenticationType !== 'gssapi') delete connection.gssapi;
if (connection.authenticationType !== 'plain') delete connection.plain;
if (connection.mongodb_x509) delete connection.ssl;
if (connection.ssl && !connection.ssl.enabled) delete connection.ssl;
if (connection.gssapi && !connection.gssapi.serviceName) Error.create({ type: Error.types.MissingParameter, formatters: ['service-name', 'gssapi'], metadataToLog: connection });
};
const checkSSHOfConnection = function (connection) {
if (connection.ssh) {
if (!connection.ssh.enabled) delete connection.ssh;
if (!connection.ssh.destinationPort) Error.create({ type: Error.types.MissingParameter, formatters: ['destination-port', 'ssh'], metadataToLog: connection });
if (!connection.ssh.username) Error.create({ type: Error.types.MissingParameter, formatters: ['username', 'ssh'], metadataToLog: connection });
if (!connection.ssh.host) Error.create({ type: Error.types.MissingParameter, formatters: ['host', 'ssh'], metadataToLog: connection });
if (!connection.ssh.port) Error.create({ type: Error.types.MissingParameter, formatters: ['port', 'ssh'], metadataToLog: connection });
if (!connection.ssh.certificateFileName && !connection.ssh.password) {
Error.create({ type: Error.types.MissingParameter, formatters: ['certificate-or-password', 'ssh'], metadataToLog: connection });
}
}
};
const migrateSSHPart = function (oldConnection, connection) {
if (oldConnection.sshAddress) {
connection.ssh = {
enabled: true,
host: oldConnection.sshAddress,
port: oldConnection.sshPort,
username: oldConnection.sshUser,
destinationPort: oldConnection.sshPort,
};
if (oldConnection.sshPassword) connection.ssh.password = oldConnection.sshPassword;
else {
connection.ssh.certificateFile = oldConnection.sshCertificate;
connection.ssh.certificateFileName = oldConnection.sshCertificatePath;
connection.ssh.passPhrase = oldConnection.sshPassPhrase;
}
}
};
const migrateSSLPart = function (oldConnection, connection) {
if (oldConnection.sslCertificatePath) {
const objToChange = oldConnection.x509Username ? connection.mongodb_x509 : connection.ssl;
objToChange.certificateFile = oldConnection.sslCertificate;
objToChange.certificateFileName = oldConnection.sslCertificatePath;
objToChange.passPhrase = oldConnection.passPhrase;
if (oldConnection.rootCACertificatePath) {
objToChange.rootCAFile = oldConnection.rootCACertificate;
objToChange.rootCAFileName = oldConnection.rootCACertificatePath;
}
if (oldConnection.certificateKeyPath) {
objToChange.certificateKeyFile = oldConnection.certificateKey;
objToChange.certificateKeyFileName = oldConnection.certificateKeyPath;
}
}
};
const getRoundedMilisecondsFromSeconds = function (sec) {
if (sec) return Math.round(sec * 100 * 1000) / 100;
return '30000';
};
Connection.prototype = {
importConnections(file) {
Logger.info({ message: 'import-mongoclient-connections', metadataToLog: { file } });
try {
const mongoclientData = JSON.parse(file);
if (mongoclientData.connections) {
for (let i = 0; i < mongoclientData.connections.length; i += 1) {
delete mongoclientData.connections[i]._id;
Database.insert({ type: Database.types.Connections, document: mongoclientData.connections[i] });
}
this.migrateConnectionsIfExist();
}
} catch (exception) {
Error.create({ type: Error.types.InternalError, externalError: exception, metadataToLog: file });
}
},
save(connection) {
Logger.info({ message: 'save-connection', metadataToLog: { connection } });
if (connection._id) Database.remove({ type: Database.types.Connections, selector: { _id: connection._id } });
Database.create({ type: Database.types.Connections, document: connection });
},
checkAndClear(connection) {
Logger.info({ message: 'check-connection', metadataToLog: { connection } });
if (connection.url) connection = this.parseUrl(connection);
if (connection.servers.length === 0) Error.create({ type: Error.types.MissingParameter, formatters: ['one-server', 'servers'] });
connection.servers.forEach((server) => { if (!server.host || !server.port) Error.create({ type: Error.types.MissingParameter, formatters: ['host-and-port', 'server'] }); });
checkAuthenticationOfConnection(connection);
checkSSHOfConnection(connection);
},
parseUrl(connection) {
try {
Logger.info({ message: 'parse-url', metadataToLog: { connection } });
const parsedUrl = mongodbUrlParser(connection.url);
connection.options = connection.options || {};
connection.ssl = connection.ssl || {};
connection.databaseName = parsedUrl.dbName || 'admin';
connection.servers = parsedUrl.servers;
Logger.info({ message: 'parsed-url', metadataToLog: { parsedUrl } });
setOptionsToConnectionFromParsedUrl(connection, parsedUrl);
setAuthToConnectionFromParsedUrl(connection, parsedUrl);
return connection;
} catch (exception) {
Error.create({ type: Error.types.ParseUrlError, externalError: exception, metadataToLog: connection });
}
},
remove(connectionId) {
Logger.info({ message: 'remove-connection', metadataToLog: { connectionId } });
Database.remove({ type: Database.types.Connections, selector: { _id: connectionId } });
Database.remove({ type: Database.types.QueryHistory, selector: { _id: connectionId } });
},
/* Migrates 1.x version connections to 2.x */
migrateConnectionsIfExist() {
Logger.info({ message: 'migrate-connections' });
const settings = Database.readOne({ type: Database.types.Settings, query: {} });
if (settings.isMigrationDone) return;
const connectionsAfterMigration = [];
Database.read({ type: Database.types.Connections }).forEach((oldConnection) => {
// if there's a name (was mandatory) property it's old.
if (oldConnection.name) {
let connection = { options: {} };
if (oldConnection.url) {
connection = this.parseUrl({ url: oldConnection.url });
connection.url = oldConnection.url;
}
connection._id = oldConnection._id;
connection.connectionName = oldConnection.name;
migrateSSHPart(oldConnection, connection);
if (oldConnection.host && oldConnection.port) {
connection.servers = [{
host: oldConnection.host,
port: oldConnection.port,
}];
}
if (oldConnection.readFromSecondary) connection.options.readPreference = 'secondary';
else connection.options.readPreference = 'primary';
if (oldConnection.databaseName) connection.databaseName = oldConnection.databaseName;
if (oldConnection.user && oldConnection.password) {
connection.scram_sha_1 = {
username: oldConnection.user,
password: oldConnection.password,
};
if (oldConnection.authDatabaseName) connection.scram_sha_1.authSource = oldConnection.authDatabaseName;
connection.authenticationType = 'scram_sha_1';
}
if (oldConnection.useSsl || oldConnection.sslCertificatePath) connection.ssl = { enabled: true };
if (oldConnection.x509Username) {
connection.authenticationType = 'mongodb_x509';
connection.mongodb_x509 = { username: oldConnection.x509Username };
delete connection.ssl;
}
migrateSSLPart(oldConnection, connection);
connectionsAfterMigration.push(connection);
}
});
Database.remove({ type: Database.types.Connections });
connectionsAfterMigration.forEach(conn => Database.create({ type: Database.types.Connections, document: conn }));
Database.update({ type: Database.types.Settings, selector: {}, modifier: { $set: { isMigrationDone: true } } });
},
tryInjectDefaultConnection() {
const DEFAULT_CONNECTION_NAME = 'Default (preconfigured)';
const defaultConnection = process.env.MONGOCLIENT_DEFAULT_CONNECTION_URL;
if (!defaultConnection) return;
Logger.info({ message: 'inject-default-connection', metadataToLog: { defaultConnection } });
const connection = this.parseUrl({ url: defaultConnection });
connection.url = defaultConnection;
connection.connectionName = DEFAULT_CONNECTION_NAME;
// delete existing connection after we parsed the new one
const existingConnection = Database.readOne({ type: Database.types.Connections, query: { connectionName: DEFAULT_CONNECTION_NAME } });
if (existingConnection) {
Database.remove({ type: Database.types.Connections, selector: { _id: existingConnection._id } });
connection._id = existingConnection._id;
}
Database.create({ type: Database.types.Connections, document: connection });
},
savePredefinedConnections() {
const filePath = process.env.MONGOCLIENT_CONNECTIONS_FILE_PATH;
if (!filePath || !fs.existsSync(filePath)) return;
const fileContent = fs.readFileSync(filePath, 'utf8');
if (!fileContent || fileContent.replace(/\s/g, '').length === 0) return;
Logger.info({ message: 'predefined-connections', metadataToLog: { connections: fileContent } });
try {
const connections = jsonParser.parse(fileContent);
if (Array.isArray(connections) && connections.length > 0) {
// clear existing connections
Database.remove({ type: Database.types.Connections, selector: {} });
// insert new connection URLs.
connections.forEach((connectionObj) => {
Logger.info({ message: 'import-predefined-connection', metadataToLog: { connection: connectionObj } });
const connection = this.parseUrl({ url: connectionObj.url });
connection.url = connectionObj.url;
connection.connectionName = connectionObj.name;
Database.create({ type: Database.types.Connections, document: connection });
});
}
} catch (exception) {
Logger.error({ message: 'predefined-connections', metadataToLog: { exception } });
}
},
getConnectionUrl(connection, username, password, addAuthSource, keepDB) {
if (connection.url) {
if (username || password) ConnectionHelper.changeUsernameAndPasswordFromConnectionUrl(connection, username, password);
if (!keepDB) ConnectionHelper.extractDBFromConnectionUrl(connection);
else ConnectionHelper.putCorrectDBToConnectionUrl(connection);
if (addAuthSource) ConnectionHelper.addAuthSourceToConnectionUrl(connection);
return connection.url;
}
const settings = Database.readOne({ type: Database.types.Settings, query: {} });
// url
let connectionUrl = 'mongodb://';
if (connection.authenticationType) {
if (username) connectionUrl += encodeURIComponent(username);
else if (connection[connection.authenticationType].username) connectionUrl += encodeURIComponent(connection[connection.authenticationType].username);
if (password) connectionUrl += `:${encodeURIComponent(password)}`;
else if (connection[connection.authenticationType].password) connectionUrl += `:${encodeURIComponent(connection[connection.authenticationType].password)}`;
connectionUrl += '@';
}
connection.servers.forEach((server) => { connectionUrl += `${encodeURIComponent(server.host)}:${server.port},`; });
if (connectionUrl.endsWith(',')) connectionUrl = connectionUrl.substring(0, connectionUrl.length - 1);
connectionUrl += '/';
if (keepDB) connectionUrl += connection.databaseName;
// options
if (connection.authenticationType === 'mongodb_cr' || connection.authenticationType === 'scram_sha_1' || connection.authenticationType === 'scram_sha_256') {
connectionUrl += ConnectionHelper.addOptionToUrl(connectionUrl, 'authSource', connection[connection.authenticationType].authSource);
} else if (connection.authenticationType === 'mongodb_x509') {
connectionUrl += ConnectionHelper.addOptionToUrl(connectionUrl, 'ssl', 'true');
} else if (connection.authenticationType === 'gssapi' || connection.authenticationType === 'plain') {
if (connection.authenticationType === 'gssapi') connectionUrl += ConnectionHelper.addOptionToUrl(connectionUrl, 'gssapiServiceName', connection.gssapi.serviceName);
connectionUrl += ConnectionHelper.addOptionToUrl(connectionUrl, 'authSource', '$external');
}
if (connection.options) {
if (connection.options.readPreference) connectionUrl += ConnectionHelper.addOptionToUrl(connectionUrl, 'readPreference', connection.options.readPreference);
if (connection.options.connectionTimeout) {
connectionUrl += ConnectionHelper.addOptionToUrl(connectionUrl, 'connectTimeoutMS', getRoundedMilisecondsFromSeconds(connection.options.connectionTimeout));
} else {
connectionUrl += ConnectionHelper.addOptionToUrl(connectionUrl, 'connectTimeoutMS', getRoundedMilisecondsFromSeconds(settings.connectionTimeoutInSeconds));
}
if (connection.options.socketTimeout) connectionUrl += ConnectionHelper.addOptionToUrl(connectionUrl, 'socketTimeoutMS', getRoundedMilisecondsFromSeconds(connection.options.socketTimeout));
else connectionUrl += ConnectionHelper.addOptionToUrl(connectionUrl, 'socketTimeoutMS', getRoundedMilisecondsFromSeconds(settings.socketTimeoutInSeconds));
if (connection.options.replicaSetName) connectionUrl += ConnectionHelper.addOptionToUrl(connectionUrl, 'replicaSet', connection.options.replicaSetName);
}
if (connection.ssl && connection.ssl.enabled) connectionUrl += ConnectionHelper.addOptionToUrl(connectionUrl, 'ssl', 'true');
if (connection.authenticationType) connectionUrl += ConnectionHelper.addOptionToUrl(connectionUrl, 'authMechanism', connection.authenticationType.toUpperCase().replace(new RegExp('_', 'g'), '-'));
if (addAuthSource) {
if (connection.authenticationType === 'mongodb_cr' || connection.authenticationType === 'scram_sha_1' || connection.authenticationType === 'scram_sha_256') {
if (connection[connection.authenticationType].authSource) connectionUrl += ConnectionHelper.addOptionToUrl(connectionUrl, 'authSource', connection[connection.authenticationType].authSource);
else connectionUrl += ConnectionHelper.addOptionToUrl(connectionUrl, 'authSource', connection.databaseName);
} else if (connection.authenticationType === 'gssapi' || connection.authenticationType === 'plain') {
connectionUrl += ConnectionHelper.addOptionToUrl(connectionUrl, 'authSource', '$external');
}
}
return connectionUrl;
},
getConnectionOptions(connection) {
const result = { useNewUrlParser: true, useUnifiedTopology: true };
if (connection.authenticationType === 'mongodb_x509') addSSLOptions(connection.mongodb_x509, result);
if (connection.ssl && connection.ssl.enabled) addSSLOptions(connection.ssl, result);
if (connection.options && connection.options.connectWithNoPrimary) result.connectWithNoPrimary = true;
// added authSource to here to provide same authSource as DB name if it's not provided when connection is being used by URL
if (connection.authenticationType === 'mongodb_cr' || connection.authenticationType === 'scram_sha_1' || connection.authenticationType === 'scram_sha_256') {
if (connection[connection.authenticationType].authSource) result.authSource = connection[connection.authenticationType].authSource;
else result.authSource = connection.databaseName;
} else if (connection.authenticationType === 'gssapi' || connection.authenticationType === 'plain') {
result.authSource = '$external';
}
return result;
}
};
export default new Connection();