snowflake-sdk
Version:
Node.js driver for Snowflake
291 lines • 13 kB
JavaScript
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
;