UNPKG

snowflake-sdk

Version:
291 lines 13 kB
"use strict"; const os = require('os'); const path = require('path'); const fs = require('fs'); const { isString, exists, getDriverDirectory, isWindows } = require('../util'); const Logger = require('../logger'); const { IsFileExisted } = require('../file_util'); const clientConfigFileName = 'sf_client_config.json'; const Levels = Object.freeze({ Off: 'OFF', Error: 'ERROR', Warn: 'WARN', Info: 'INFO', Debug: 'DEBUG', Trace: 'TRACE', }); const defaultDirectories = getDefaultDirectories(); function getDefaultDirectories() { const directories = []; const driverDirectory = getDriverDirectory(); Logger.getInstance().debug(`Detected driver directory: ${driverDirectory}`); if (driverDirectory) { directories.push({ dir: driverDirectory, dirDescription: 'driver', }); } else { Logger.getInstance().warn('Driver directory is not defined'); } const homedir = os.homedir(); Logger.getInstance().debug(`Detected home directory: ${homedir}`); if (exists(homedir)) { directories.push({ dir: homedir, dirDescription: 'home', }); } else { Logger.getInstance().warn('Home directory of the user is not defined'); } Logger.getInstance().debug(`Detected default directories: ${driverDirectory}`); return directories; } const knownCommonEntries = ['log_level', 'log_path']; const allLevels = Object.values(Levels); class ClientConfig { constructor(filePath, loggingConfig) { this.configPath = filePath; this.loggingConfig = loggingConfig; } } class ClientLoggingConfig { constructor(logLevel, logPath) { this.logLevel = logLevel; this.logPath = logPath; } } class ConfigurationError extends Error { name = 'ConfigurationError'; constructor(message, cause) { super(message); this.cause = cause; Error.captureStackTrace(this, this.constructor); } toString() { return this.message + ': ' + this.cause.toString(); } } /** * @param value {String} Log level. * @return {String} normalized log level value. * @throws {Error} Error for unknown value. */ function levelFromString(value) { const level = value.toUpperCase(); if (!allLevels.includes(level)) { Logger.getInstance().error(`Tried to create unsupported log level from string: ${value}`); throw new Error('Unknown log level: ' + value); } return level; } /** * @param fsPromisesModule {module} filestream module * @param processModule {processModule} process module */ function ConfigurationUtil(fsPromisesModule, processModule) { const fsPromises = typeof fsPromisesModule !== 'undefined' ? fsPromisesModule : require('fs/promises'); const process = typeof processModule !== 'undefined' ? processModule : require('process'); let configFileContents = null; let fd = null; /** * @param configFilePath {String} A path to a client config file. * @return {Promise<ClientConfig>} Client configuration. */ this.getClientConfig = async function (configFilePath, mock = false, delay = 0) { Logger.getInstance().debug('Retrieving client config'); const path = await findConfig(configFilePath); if (!exists(path) || path === '') { Logger.getInstance().info('No config file path found. Client config will not be used.'); return null; } const isFileExist = mock ? mock : IsFileExisted(path); if (!isFileExist) { Logger.getInstance().info(`No config file not found on ${path}. Client config will not be used.`); return null; } try { fd = await openFileSafely(path); if (!isWindows()) { const openStats = await fd.stat(); const mode = openStats.mode & 0o777; if (!isFilePermissionValid(mode)) { Logger.getInstance().warn(`Config file path permissions are invalid. File: ${path} can be modified by group or others. Client config will not be used.`); throw new ConfigurationError(`Configuration file: ${path} can be modified by group or others`, 'IncorrectPerms'); } if (!validateOwnership(openStats)) { Logger.getInstance().warn('This config file is not owned by the current user. Client config will not be used.'); throw new ConfigurationError('Configuration file: not owned by the current user', 'Invalid Ownership'); } Logger.getInstance().debug(`Config file path permissions are valid. Path: ${path}`); if (mock) { await new Promise((resolve) => setTimeout(resolve, delay)); } configFileContents = await readFileConfig(fd).catch((err) => { Logger.getInstance().debug(`Reading configuration from the file failed. Path: ${path}`); throw new ConfigurationError('Finding client configuration failed', err); }); //Compare the modification time from the 'open' call with the modification time after reading to validate whether the file has been modified. const currentStat = await fsPromises.stat(path); if (!isFileModified(openStats, currentStat)) { Logger.getInstance().error('The file was modified after the driver opened the config file and can no longer be used.'); throw new ConfigurationError('The config file has been modified', 'InvalidConfigFile'); } } else { configFileContents = await readFileConfig(fd).catch((err) => { Logger.getInstance().debug(`Reading configuration from the file failed. Path: ${path}`); throw new ConfigurationError('Finding client configuration failed', err); }); } Logger.getInstance().info('Using client configuration from path: %s', path); } catch (err) { if (err.syscall === 'open') { Logger.getInstance().debug(`Fail to open the configuration file from. Path: ${path}. If the file is a symlink, please change the path to the real path`); throw new ConfigurationError('Fail to open the configuration file', err); } else { throw err; } } finally { await fd?.close(); } return configFileContents == null ? null : parseConfigFile(path, configFileContents); }; function isFilePermissionValid(mode) { return (mode & (1 << 4)) === 0 && (mode & (1 << 1)) === 0; } function validateOwnership(stats) { const currentUser = os.userInfo(); return stats.uid === currentUser.uid && stats.gid === currentUser.gid; } function isFileModified(openStat, newStat) { const keys = ['uid', 'mtimeMs', 'mode', 'birthtimeMs', 'ctimeMs']; return keys.every((key) => openStat[key] === newStat[key]); } async function openFileSafely(filePath) { return fsPromises.open(filePath, fs.constants.O_NOFOLLOW | fs.constants.O_RDONLY); } async function readFileConfig(fd) { return fd.readFile({ encoding: 'utf8' }); } function parseConfigFile(path, configurationJson) { Logger.getInstance().debug('Parsing config file: %s', path); try { const parsedConfiguration = JSON.parse(configurationJson); Logger.getInstance().trace('Config file contains correct JSON structure. Validating the input.'); checkUnknownEntries(parsedConfiguration); validate(parsedConfiguration); Logger.getInstance().debug('Config file contains valid configuration input.'); const clientConfig = new ClientConfig(path, new ClientLoggingConfig(getLogLevel(parsedConfiguration), getLogPath(parsedConfiguration))); Logger.getInstance().info('Client Configuration created with Log Level: %s and Log Path: %s', clientConfig.loggingConfig.logLevel, clientConfig.loggingConfig.logPath); return clientConfig; } catch (err) { Logger.getInstance().error('Parsing client configuration failed. Used config file from path: %s', path); throw new ConfigurationError('Parsing client configuration failed', err); } } function checkUnknownEntries(config) { for (const key in config.common) { if (!knownCommonEntries.includes(key.toLowerCase())) { Logger.getInstance().warn('Unknown configuration entry: %s with value: %s', key, config.common[key]); } } } function validate(configuration) { validateLogLevel(configuration); validateLogPath(configuration); } function validateLogLevel(configuration) { const logLevel = getLogLevel(configuration); if (logLevel == null) { Logger.getInstance().debug('Log level is not specified.'); return; } if (!isString(logLevel)) { const errorMessage = 'Log level is not a string.'; Logger.getInstance().error(errorMessage); throw new Error(errorMessage); } levelFromString(logLevel); } function validateLogPath(configuration) { const logPath = getLogPath(configuration); if (logPath == null) { Logger.getInstance().debug('Log path is not specified'); return; } if (!isString(logPath)) { const errorMessage = 'Log path is not a string.'; Logger.getInstance().error(errorMessage); throw new Error(errorMessage); } } function getLogLevel(configuration) { return configuration.common.log_level; } function getLogPath(configuration) { return configuration.common.log_path; } async function findConfig(filePathFromConnectionString) { Logger.getInstance().trace(`findConfig() called with param: ${filePathFromConnectionString}`); if (exists(filePathFromConnectionString)) { Logger.getInstance().info('Found client configuration path in a connection string. Path: %s', filePathFromConnectionString); return filePathFromConnectionString; } const filePathFromEnvVariable = await getFilePathFromEnvironmentVariable(); if (exists(filePathFromEnvVariable)) { Logger.getInstance().info('Found client configuration path in an environment variable. Path: %s', filePathFromEnvVariable); return filePathFromEnvVariable; } const fileFromDefDirs = await searchForConfigInDefaultDirectories(); if (exists(fileFromDefDirs)) { Logger.getInstance().info('Found client configuration path in %s directory. Path: %s', fileFromDefDirs.dirDescription, fileFromDefDirs.configPath); return fileFromDefDirs.configPath; } Logger.getInstance().info('No client config detected.'); return null; } async function verifyNotEmpty(filePath) { return filePath ? filePath : null; } function getFilePathFromEnvironmentVariable() { return verifyNotEmpty(process.env.SF_CLIENT_CONFIG_FILE); } async function searchForConfigInDefaultDirectories() { Logger.getInstance().debug(`Searching for config in default directories: ${JSON.stringify(defaultDirectories)}`); for (const directory of defaultDirectories) { const configPath = await searchForConfigInDictionary(directory.dir, directory.dirDescription); if (exists(configPath)) { Logger.getInstance().debug(`Config found in the default directory: ${directory.dir}. Path: ${configPath}`); return { configPath: configPath, dirDescription: directory.dirDescription }; } } Logger.getInstance().debug('Unable to find config in any default directory.'); return null; } async function searchForConfigInDictionary(directory, directoryDescription) { try { const filePath = path.join(directory, clientConfigFileName); return await onlyIfFileExists(filePath); } catch (e) { Logger.getInstance().error('Error while searching for the client config in %s directory: %s', directoryDescription, e); return null; } } async function onlyIfFileExists(filePath) { return await fsPromises .access(filePath, fs.constants.F_OK) .then(() => filePath) .catch(() => null); } } exports.Levels = Levels; exports.levelFromString = levelFromString; exports.ConfigurationUtil = ConfigurationUtil; //# sourceMappingURL=client_configuration.js.map