diff --git a/CMakeLists.txt b/CMakeLists.txt index 56d07e3ff8..03300e9f8f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -98,6 +98,7 @@ set(SOURCE_FILES include/snowflake/logger.h include/snowflake/version.h include/snowflake/platform.h + include/snowflake/client_config_parser.h lib/client.c lib/constants.h lib/cJSON.h @@ -213,6 +214,7 @@ set(SOURCE_FILES_CPP_WRAPPER include/snowflake/SFURL.hpp include/snowflake/CurlDesc.hpp include/snowflake/CurlDescPool.hpp + include/snowflake/ClientConfigParser.hpp cpp/lib/Exceptions.cpp cpp/lib/Connection.cpp cpp/lib/Statement.cpp @@ -235,6 +237,7 @@ set(SOURCE_FILES_CPP_WRAPPER cpp/lib/ResultSetJson.hpp cpp/lib/Authenticator.cpp cpp/lib/Authenticator.hpp + cpp/lib/ClientConfigParser.cpp cpp/jwt/jwtWrapper.cpp cpp/util/SnowflakeCommon.cpp cpp/util/SFURL.cpp @@ -406,6 +409,7 @@ if (LINUX) deps-build/${PLATFORM}/${CMAKE_BUILD_TYPE}/azure/include deps-build/${PLATFORM}/${CMAKE_BUILD_TYPE}/cmocka/include deps-build/${PLATFORM}/${CMAKE_BUILD_TYPE}/uuid/include + deps-build/${PLATFORM}/${CMAKE_BUILD_TYPE}/picojson/include include lib) endif() @@ -421,6 +425,7 @@ if (APPLE) deps-build/${PLATFORM}/${CMAKE_BUILD_TYPE}/aws/include deps-build/${PLATFORM}/${CMAKE_BUILD_TYPE}/azure/include deps-build/${PLATFORM}/${CMAKE_BUILD_TYPE}/cmocka/include + deps-build/${PLATFORM}/${CMAKE_BUILD_TYPE}/picojson/include include lib) endif() @@ -435,6 +440,7 @@ if (WIN32) deps-build/${PLATFORM}/${VSDIR}/${CMAKE_BUILD_TYPE}/aws/include deps-build/${PLATFORM}/${VSDIR}/${CMAKE_BUILD_TYPE}/azure/include deps-build/${PLATFORM}/${VSDIR}/${CMAKE_BUILD_TYPE}/cmocka/include + deps-build/${PLATFORM}/${VSDIR}/${CMAKE_BUILD_TYPE}/picojson/include include lib) if (CMAKE_SIZEOF_VOID_P EQUAL 8) diff --git a/cpp/lib/ClientConfigParser.cpp b/cpp/lib/ClientConfigParser.cpp new file mode 100644 index 0000000000..aa02d89f3b --- /dev/null +++ b/cpp/lib/ClientConfigParser.cpp @@ -0,0 +1,292 @@ +/** + * Copyright (c) 2024 Snowflake Computing + */ + +#include "snowflake/ClientConfigParser.hpp" +#include "snowflake/Exceptions.hpp" +#include "snowflake/client_config_parser.h" +#include "../logger/SFLogger.hpp" +#include "memory.h" + +#include +#include +#include + +#undef snprintf +#include + +#ifndef _WIN32 +#include +#endif + +using namespace Snowflake::Client; +using namespace picojson; + +namespace +{ + // constants + const std::string SF_CLIENT_CONFIG_FILE_NAME("sf_client_config.json"); + const std::string SF_CLIENT_CONFIG_ENV_NAME("SF_CLIENT_CONFIG_FILE"); + std::set KnownCommonEntries = {"log_level", "log_path"}; +} + +//////////////////////////////////////////////////////////////////////////////// +sf_bool load_client_config( + const char* in_configFilePath, + client_config* out_clientConfig) +{ +// Disable easy logging for 32-bit windows debug build due to linking issues +// with _osfile causing hanging/assertions until dynamic linking is available +#if !defined(_WIN32) && !defined(_DEBUG) + try { + EasyLoggingConfigParser configParser; + configParser.loadClientConfig(in_configFilePath, *out_clientConfig); + } catch (std::exception e) { + CXX_LOG_ERROR("Error loading client configuration: %s", e.what()); + return false; + } +#endif + return true; +} + +//////////////////////////////////////////////////////////////////////////////// +void free_client_config(client_config* clientConfig) +{ + if (clientConfig->logLevel != NULL) + { + SF_FREE(clientConfig->logLevel); + } + if (clientConfig->logPath != NULL) + { + SF_FREE(clientConfig->logPath); + } +} + +// Public ====================================================================== +//////////////////////////////////////////////////////////////////////////////// +EasyLoggingConfigParser::EasyLoggingConfigParser() +{ + ; // Do nothing. +} + +//////////////////////////////////////////////////////////////////////////////// +EasyLoggingConfigParser::~EasyLoggingConfigParser() +{ + ; // Do nothing. +} + +//////////////////////////////////////////////////////////////////////////////// +void EasyLoggingConfigParser::loadClientConfig( + const std::string& in_configFilePath, + client_config& out_clientConfig) +{ + std::string derivedConfigPath = resolveClientConfigPath(in_configFilePath); + + if (!derivedConfigPath.empty()) + { + parseConfigFile(derivedConfigPath, out_clientConfig); + } +} + +//////////////////////////////////////////////////////////////////////////////// +std::string EasyLoggingConfigParser::resolveClientConfigPath( + const std::string& in_configFilePath) +{ + char envbuf[MAX_PATH + 1]; + if (!in_configFilePath.empty()) + { + // 1. Try config file if it was passed in + CXX_LOG_INFO("Using client configuration path from a connection string: %s", in_configFilePath.c_str()); + return in_configFilePath; + } + else if (const char* clientConfigEnv = sf_getenv_s(SF_CLIENT_CONFIG_ENV_NAME.c_str(), envbuf, sizeof(envbuf))) + { + // 2. Try environment variable SF_CLIENT_CONFIG_ENV_NAME + CXX_LOG_INFO("Using client configuration path from an environment variable: %s", clientConfigEnv); + return clientConfigEnv; + } + else + { + // 3. Try DLL binary dir + std::string binaryDir = getBinaryPath(); + std::string binaryDirFilePath = binaryDir + SF_CLIENT_CONFIG_FILE_NAME; + if (boost::filesystem::is_regular_file(binaryDirFilePath)) + { + CXX_LOG_INFO("Using client configuration path from binary directory: %s", binaryDirFilePath.c_str()); + return binaryDirFilePath; + } + else + { + // 4. Try user home dir + return resolveHomeDirConfigPath(); + } + } +} + +// Private ===================================================================== +//////////////////////////////////////////////////////////////////////////////// +std::string EasyLoggingConfigParser::resolveHomeDirConfigPath() +{ + char envbuf[MAX_PATH + 1]; +#if defined(WIN32) || defined(_WIN64) + std::string homeDirFilePath; + char* homeDir; + if ((homeDir = sf_getenv_s("USERPROFILE", envbuf, sizeof(envbuf))) && strlen(homeDir) != 0) + { + homeDirFilePath = homeDir + PATH_SEP + SF_CLIENT_CONFIG_FILE_NAME; + } else { + // USERPROFILE is empty, try HOMEDRIVE and HOMEPATH + char* homeDriveEnv = sf_getenv_s("HOMEDRIVE", envbuf, sizeof(envbuf)); + char* homePathEnv = sf_getenv_s("HOMEPATH", envbuf, sizeof(envbuf)); + if (homeDriveEnv && strlen(homeDriveEnv) != 0 && homePathEnv && strlen(homePathEnv) != 0) + { + homeDirFilePath = std::string(homeDriveEnv) + homePathEnv + PATH_SEP + SF_CLIENT_CONFIG_FILE_NAME; + } + } + if (boost::filesystem::is_regular_file(homeDirFilePath)) + { + CXX_LOG_INFO("Using client configuration path from home directory: %s", homeDirFilePath.c_str()); + return homeDirFilePath; + } +#else + char* homeDir; + if ((homeDir = sf_getenv_s("HOME", envbuf, sizeof(envbuf))) && (strlen(homeDir) != 0)) + { + std::string homeDirFilePath = std::string(homeDir) + PATH_SEP + SF_CLIENT_CONFIG_FILE_NAME; + if (boost::filesystem::is_regular_file(homeDirFilePath)) + { + CXX_LOG_INFO("Using client configuration path from home directory: %s", homeDirFilePath.c_str()); + return homeDirFilePath; + } + } +#endif + return ""; +} + +//////////////////////////////////////////////////////////////////////////////// +void EasyLoggingConfigParser::parseConfigFile( + const std::string& in_filePath, + client_config& out_clientConfig) +{ + value jsonConfig; + std::string err; + std::ifstream configFile; + try + { + configFile.open(in_filePath, std::fstream::in | std::ios::binary); + if (!configFile) + { + CXX_LOG_INFO("Could not open a file. The file may not exist: %s", + in_filePath.c_str()); + std::string errMsg = "Error finding client configuration file: " + in_filePath; + throw ClientConfigException(errMsg.c_str()); + } +#if !defined(WIN32) && !defined(_WIN64) + checkIfValidPermissions(in_filePath); +#endif + err = parse(jsonConfig, configFile); + + if (!err.empty()) + { + CXX_LOG_ERROR("Error in parsing JSON: %s, err: %s", in_filePath.c_str(), err.c_str()); + std::string errMsg = "Error parsing client configuration file: " + in_filePath; + throw ClientConfigException(errMsg.c_str()); + } + } + catch (std::exception& e) + { + configFile.close(); + throw; + } + + value commonProps = jsonConfig.get("common"); + checkUnknownEntries(commonProps); + if (commonProps.is()) + { + if (commonProps.contains("log_level") && commonProps.get("log_level").is()) + { + const char* logLevel = commonProps.get("log_level").get().c_str(); + size_t logLevelSize = strlen(logLevel) + 1; + out_clientConfig.logLevel = (char*)SF_CALLOC(1, logLevelSize); + sf_strcpy(out_clientConfig.logLevel, logLevelSize, logLevel); + } + if (commonProps.contains("log_path") && commonProps.get("log_path").is()) + { + const char* logPath = commonProps.get("log_path").get().c_str(); + size_t logPathSize = strlen(logPath) + 1; + out_clientConfig.logPath = (char*)SF_CALLOC(1, logPathSize); + sf_strcpy(out_clientConfig.logPath, logPathSize, logPath); + } + } + configFile.close(); +} + +//////////////////////////////////////////////////////////////////////////////// +void EasyLoggingConfigParser::checkIfValidPermissions(const std::string& in_filePath) +{ + boost::filesystem::file_status fileStatus = boost::filesystem::status(in_filePath); + boost::filesystem::perms permissions = fileStatus.permissions(); + if (permissions & boost::filesystem::group_write || + permissions & boost::filesystem::others_write) + { + CXX_LOG_ERROR("Error due to other users having permission to modify the config file: %s", + in_filePath.c_str()); + std::string errMsg = "Error due to other users having permission to modify the config file: " + in_filePath; + throw ClientConfigException(errMsg.c_str()); + } +} + +//////////////////////////////////////////////////////////////////////////////// +void EasyLoggingConfigParser::checkUnknownEntries(value& in_config) +{ + if (in_config.is()) + { + const value::object& configObj = in_config.get(); + for (value::object::const_iterator i = configObj.begin(); i != configObj.end(); ++i) + { + std::string key = i->first; + bool found = false; + for (std::string knownEntry : KnownCommonEntries) + { + if (sf_strncasecmp(key.c_str(), knownEntry.c_str(), knownEntry.length()) == 0) + { + found = true; + } + } + if (!found) + { + std::string warnMsg = + "Unknown configuration entry: " + key + " with value:" + i->second.to_str().c_str(); + CXX_LOG_WARN(warnMsg.c_str()); + } + } + } +} + +//////////////////////////////////////////////////////////////////////////////// +std::string EasyLoggingConfigParser::getBinaryPath() +{ + std::string binaryFullPath; +#if defined(WIN32) || defined(_WIN64) + std::wstring path; + HMODULE hm = NULL; + wchar_t appName[256]; + GetModuleFileNameW(hm, appName, 256); + path = appName; + binaryFullPath = std::string(path.begin(), path.end()); +#else + Dl_info info; + int result = dladdr((void*)load_client_config, &info); + if (result) + { + binaryFullPath = std::string(info.dli_fname); + } +#endif + size_t pos = binaryFullPath.find_last_of(PATH_SEP); + if (pos == std::string::npos) + { + return ""; + } + std::string binaryPath = binaryFullPath.substr(0, pos + 1); + return binaryPath; +} diff --git a/include/snowflake/ClientConfigParser.hpp b/include/snowflake/ClientConfigParser.hpp new file mode 100644 index 0000000000..b84dbe9e65 --- /dev/null +++ b/include/snowflake/ClientConfigParser.hpp @@ -0,0 +1,106 @@ +/** + * Copyright (c) 2024 Snowflake Computing + */ + +#ifndef SNOWFLAKE_EASYLOGGINGCONFIGPARSER_HPP +#define SNOWFLAKE_EASYLOGGINGCONFIGPARSER_HPP + +#include +#include "client_config_parser.h" +#include "picojson.h" + +namespace Snowflake +{ +namespace Client +{ + struct ClientConfigException : public std::exception + { + ClientConfigException(const std::string& message) : message_(message) {} + const char* what() const noexcept + { + return message_.c_str(); + } + + std::string message_; + }; + + class EasyLoggingConfigParser + { + // Public ================================================================== + public: + /** + * Constructor for client config + */ + EasyLoggingConfigParser(); + + /// @brief Destructor. + ~EasyLoggingConfigParser(); + + /** + * Construct SFClientConfig from client config file passed by user. This method searches the + * config file in following order: 1. configFilePath param which is read from connection URL or + * connection property. 2. Environment variable: SF_CLIENT_CONFIG_FILE containing full path to + * sf_client_config file. 3. Searches for default config file name(sf_client_config.json under the + * driver directory from where the driver gets loaded. 4. Searches for default config file + * name(sf_client_config.json) under user home directory 5. Searches for default config file + * name(sf_client_config.json) under tmp directory + * + * @param in_configFilePath The config file path passed in by the user. + * @param out_clientConfig The SFClientConfig object to be filled. + */ + void loadClientConfig( + const std::string& in_configFilePath, + client_config& out_clientConfig); + + // Private ================================================================= + private: + /** + * @brief Resolve the client config path. + * + * @param in_configFilePath The config file path passed in by the user. + * + * @return The client config path + */ + std::string resolveClientConfigPath(const std::string& in_configFilePath); + + /** + * @brief Resolve home directory config path. + * + * @return The home directory client config path if exist, else empty. + */ + std::string resolveHomeDirConfigPath(); + + /** + * @brief Parse JSON string. + * + * @param in_filePath The filePath of the config file to parse. + * @param out_clientConfig The SFClientConfig object to be filled. + */ + void parseConfigFile( + const std::string& in_filePath, + client_config& out_clientConfig); + + /** + * @ brief Check if other have permission to modify file + * + * @param in_filePath The file path of the config file to check permissions. + */ + void checkIfValidPermissions(const std::string& in_filePath); + + /** + * @ brief Check if there are unknown entries in config file + * + * @param in_jsonString The json object to check in json config file. + */ + void checkUnknownEntries(picojson::value& in_config); + + /** + * @ brief Get the path to the binary file + */ + std::string getBinaryPath(); + }; + +} // namespace Client +} // namespace Snowflake + +#endif //SNOWFLAKE_EASYLOGGINGCONFIGPARSER_HPP diff --git a/include/snowflake/client.h b/include/snowflake/client.h index 1dd5f53b30..883d6f31bf 100644 --- a/include/snowflake/client.h +++ b/include/snowflake/client.h @@ -15,9 +15,6 @@ extern "C" { #include "version.h" #include "logger.h" -/** - * API Name - */ #define SF_API_NAME "C API" /** @@ -284,7 +281,10 @@ typedef enum SF_GLOBAL_ATTRIBUTE { SF_GLOBAL_CA_BUNDLE_FILE, SF_GLOBAL_SSL_VERSION, SF_GLOBAL_DEBUG, - SF_GLOBAL_OCSP_CHECK + SF_GLOBAL_OCSP_CHECK, + SF_GLOBAL_CLIENT_CONFIG_FILE, + SF_GLOBAL_LOG_LEVEL, + SF_GLOBAL_LOG_PATH } SF_GLOBAL_ATTRIBUTE; /** diff --git a/include/snowflake/client_config_parser.h b/include/snowflake/client_config_parser.h new file mode 100644 index 0000000000..861272b6c7 --- /dev/null +++ b/include/snowflake/client_config_parser.h @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2024 Snowflake Computing + */ + +#ifndef SNOWFLAKE_CONFIGPARSER_H +#define SNOWFLAKE_CONFIGPARSER_H + +#include "snowflake/client.h" +#include "cJSON.h" + +#ifdef __cplusplus +extern "C" { +#endif + + typedef struct client_config + { + // The log level + char *logLevel; + + // The log path + char *logPath; + } client_config; + + /** + * Construct client_config from client config file passed by user. This method searches the + * config file in following order: 1. configFilePath param which is read from connection URL or + * connection property. 2. Environment variable: SF_CLIENT_CONFIG_FILE containing full path to + * sf_client_config file. 3. Searches for default config file name(sf_client_config.json under the + * driver directory from where the driver gets loaded. 4. Searches for default config file + * name(sf_client_config.json) under user home directory 5. Searches for default config file + * name(sf_client_config.json) under tmp directory + * + * @param in_configFilePath The config file path passed in by the user. + * @param out_clientConfig The client_config object to be filled. + * + * @return true if successful + */ + sf_bool load_client_config( + const char* in_configFilePath, + client_config* out_clientConfig); + + /** + * Free client config memory + * + * @param clientConfig The client_config object to be freed. + */ + void free_client_config(client_config* clientConfig); + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif //SNOWFLAKE_CONFIGPARSER_H diff --git a/include/snowflake/logger.h b/include/snowflake/logger.h index b11ec1a14c..3304ed9fdc 100644 --- a/include/snowflake/logger.h +++ b/include/snowflake/logger.h @@ -47,7 +47,8 @@ typedef enum SF_LOG_LEVEL { SF_LOG_INFO, SF_LOG_WARN, SF_LOG_ERROR, - SF_LOG_FATAL + SF_LOG_FATAL, + SF_LOG_DEFAULT } SF_LOG_LEVEL; #define CXX_LOG_NS "C++" @@ -94,6 +95,8 @@ void log_masked_va_list(FILE* fp, const char *fmt, va_list args); SF_LOG_LEVEL log_from_str_to_level(const char *level_in_str); +const char* log_from_level_to_str(SF_LOG_LEVEL level); + void log_set_path(const char* path); void log_close(); diff --git a/lib/client.c b/lib/client.c index 587cdb1692..8ee4b58567 100644 --- a/lib/client.c +++ b/lib/client.c @@ -9,6 +9,7 @@ #include #include #include +#include #include "constants.h" #include "client_int.h" #include "connection.h" @@ -36,6 +37,7 @@ sf_bool DEBUG; sf_bool SF_OCSP_CHECK; char *SF_HEADER_USER_AGENT = NULL; +static char* CLIENT_CONFIG_FILE = NULL; static char *LOG_PATH = NULL; static FILE *LOG_FP = NULL; @@ -306,18 +308,38 @@ static sf_bool STDCALL log_init(const char *log_path, SF_LOG_LEVEL log_level) { SF_LOG_LEVEL sf_log_level = log_level; char strerror_buf[SF_ERROR_BUFSIZE]; + char client_config_file[MAX_PATH] = { 0 }; + snowflake_global_get_attribute( + SF_GLOBAL_CLIENT_CONFIG_FILE, client_config_file, sizeof(client_config_file)); + client_config clientConfig = { 0 }; + load_client_config(client_config_file, &clientConfig); + size_t log_path_size = 1; //Start with 1 to include null terminator log_path_size += strlen(time_str); - /* The environment variables takes precedence over the specified parameters */ + /* The environment variables takes precedence over the specified parameters. + Specified parameters takes precedence over client config */ sf_log_path = sf_getenv_s("SNOWFLAKE_LOG_PATH", log_path_buf, sizeof(log_path_buf)); - if (sf_log_path == NULL && log_path) { + if (sf_log_path == NULL) { + if (log_path && strlen(log_path) != 0) { sf_log_path = log_path; + } + else if (clientConfig.logPath != NULL) { + sf_log_path = clientConfig.logPath; + } } sf_log_level_str = sf_getenv_s("SNOWFLAKE_LOG_LEVEL", log_level_buf, sizeof(log_level_buf)); if (sf_log_level_str != NULL) { - sf_log_level = log_from_str_to_level(sf_log_level_str); + sf_log_level = log_from_str_to_level(sf_log_level_str); + } + else if (sf_log_level == SF_LOG_DEFAULT) { + if (clientConfig.logLevel != NULL) { + sf_log_level = log_from_str_to_level(clientConfig.logLevel); + } + else { + sf_log_level = SF_LOG_FATAL; + } } // Set logging level @@ -350,9 +372,12 @@ static sf_bool STDCALL log_init(const char *log_path, SF_LOG_LEVEL log_level) { goto cleanup; } + snowflake_global_set_attribute(SF_GLOBAL_LOG_LEVEL, log_from_level_to_str(sf_log_level)); + ret = SF_BOOLEAN_TRUE; -cleanup: + cleanup: + free_client_config(&clientConfig); return ret; } @@ -635,6 +660,9 @@ snowflake_global_set_attribute(SF_GLOBAL_ATTRIBUTE type, const void *value) { case SF_GLOBAL_OCSP_CHECK: SF_OCSP_CHECK = *(sf_bool *) value; break; + case SF_GLOBAL_CLIENT_CONFIG_FILE: + alloc_buffer_and_copy(&CLIENT_CONFIG_FILE, value); + break; default: break; } @@ -664,6 +692,30 @@ snowflake_global_get_attribute(SF_GLOBAL_ATTRIBUTE type, void *value, size_t siz case SF_GLOBAL_OCSP_CHECK: *((sf_bool *) value) = SF_OCSP_CHECK; break; + case SF_GLOBAL_CLIENT_CONFIG_FILE: + if (CLIENT_CONFIG_FILE) { + if (strlen(CLIENT_CONFIG_FILE) > size - 1) { + return SF_STATUS_ERROR_BUFFER_TOO_SMALL; + } + sf_strncpy(value, size, CLIENT_CONFIG_FILE, size); + } + break; + case SF_GLOBAL_LOG_LEVEL: + { + if (strlen(log_from_level_to_str(log_get_level())) > size - 1) { + return SF_STATUS_ERROR_BUFFER_TOO_SMALL; + } + sf_strncpy(value, size, log_from_level_to_str(log_get_level()), size); + break; + } + case SF_GLOBAL_LOG_PATH: + if (LOG_PATH) { + if (strlen(LOG_PATH) > size - 1) { + return SF_STATUS_ERROR_BUFFER_TOO_SMALL; + } + sf_strncpy(value, size, LOG_PATH, size); + } + break; default: break; } diff --git a/lib/logger.c b/lib/logger.c index 9d43929aaa..5bb5d6d72a 100644 --- a/lib/logger.c +++ b/lib/logger.c @@ -216,6 +216,10 @@ SF_LOG_LEVEL log_from_str_to_level(const char *level_in_str) { return SF_LOG_FATAL; } +const char* log_from_level_to_str(SF_LOG_LEVEL level) { + return level_names[level]; +} + void log_set_path(const char *path) { L.path = path; } diff --git a/tests/test_unit_logger.c b/tests/test_unit_logger.c index 1e756b618d..0b4795b08e 100644 --- a/tests/test_unit_logger.c +++ b/tests/test_unit_logger.c @@ -1,149 +1,272 @@ -/* - * Copyright (c) 2018-2019 Snowflake Computing, Inc. All rights reserved. - */ - - -#include "utils/test_setup.h" -#ifndef _WIN32 -#include -#endif - -/** - * Tests converting a string representation of log level to the log level enum - */ -void test_log_str_to_level(void **unused) { - assert_int_equal(log_from_str_to_level("TRACE"), SF_LOG_TRACE); - assert_int_equal(log_from_str_to_level("DEBUG"), SF_LOG_DEBUG); - assert_int_equal(log_from_str_to_level("INFO"), SF_LOG_INFO); - assert_int_equal(log_from_str_to_level("wArN"), SF_LOG_WARN); - assert_int_equal(log_from_str_to_level("erroR"), SF_LOG_ERROR); - assert_int_equal(log_from_str_to_level("fatal"), SF_LOG_FATAL); - - /* negative */ - assert_int_equal(log_from_str_to_level("hahahaha"), SF_LOG_FATAL); - assert_int_equal(log_from_str_to_level(NULL), SF_LOG_FATAL); -} - -#ifndef _WIN32 -/** - * Tests timing of log file creation - */ -void test_log_creation(void **unused) { - char logname[] = "dummy.log"; - - // ensure the log file doesn't exist at the beginning - remove(logname); - assert_int_not_equal(access(logname, F_OK), 0); - - log_set_lock(NULL); - log_set_level(SF_LOG_WARN); - log_set_quiet(1); - log_set_path(logname); - - // info log won't trigger the log file creation since log level is set to warning - log_info("dummy info log"); - assert_int_not_equal(access(logname, F_OK), 0); - - // warning log will trigger the log file creation - log_warn("dummy warning log"); - assert_int_equal(access(logname, F_OK), 0); - - remove(logname); -} - -/** - * Tests masking secret information in log - */ -void test_mask_secret_log(void **unused) { - FILE* fp = fopen("dummy.log", "w+"); - assert_non_null(fp); - log_set_lock(NULL); - log_set_level(SF_LOG_TRACE); - log_set_quiet(1); - log_set_fp(fp); - - const char * logtext[][2] = { - {//0 - "Secure log record!", - "Secure log record!" - }, - {//1 - "Token =ETMsDgAAAXI0IS9NABRBRVMvQ0JDL1BLQ1M1UGFkZGluZwCAABAAEEb/xAQlmT+mwIx9G32E+ikAAACA/CPlEkq//+jWZnQkOj5VhjayruDsCVRGS/B6GzHUugXLc94EfEwuto94gS/oKSVrUg/JRPekypLAx4Afa1KW8n1RqXRF9Hzy1VVLmVEBMtei3yFJPNSHtfbeFHSr9eVB/OL8dOGbxQluGCh6XmaqTjyrh3fqUTWz7+n74+gu2ugAFFZ18iT+DStK0TTdmy4vBC6xUcHQ", - "Token =****" - }, - {//2 - "idToken : ETMsDgAAAXI0IS9NABRBRVMvQ0JDL1BLQ1M1UGFkZGluZwCAABAAEEb/xAQlmT+mwIx9G32E+ikAAACA/CPlEkq//+jWZnQkOj5VhjayruDsCVRGS/B6GzHUugXLc94EfEwuto94gS/oKSVrUg/JRPekypLAx4Afa1KW8n1RqXRF9Hzy1VVLmVEBMtei3yFJPNSHtfbeFHSr9eVB/OL8dOGbxQluGCh6XmaqTjyrh3fqUTWz7+n74+gu2ugAFFZ18iT+DStK0TTdmy4vBC6xUcHQ", - "idToken : ****" - }, - {//3 - "sessionToken:ETMsDgAAAXI0IS9NABRBRVMvQ0JDL1BLQ1M1UGFkZGluZwCAABAAEEb/xAQlmT+mwIx9G32E+ikAAACA/CPlEkq//+jWZnQkOj5VhjayruDsCVRGS/B6GzHUugXLc94EfEwuto94gS/oKSVrUg/JRPekypLAx4Afa1KW8n1RqXRF9Hzy1VVLmVEBMtei3yFJPNSHtfbeFHSr9eVB/OL8dOGbxQluGCh6XmaqTjyrh3fqUTWz7+n74+gu2ugAFFZ18iT+DStK0TTdmy4vBC6xUcHQ", - "sessionToken:****" - }, - {//4 - "masterToken : 'ETMsDgAAAXI0IS9NABRBRVMvQ0JDL1BLQ1M1UGFkZGluZwCAABAAEEb/xAQlmT+mwIx9G32E+ikAAACA/CPlEkq//+jWZnQkOj5VhjayruDsCVRGS/B6GzHUugXLc94EfEwuto94gS/oKSVrUg/JRPekypLAx4Afa1KW8n1RqXRF9Hzy1VVLmVEBMtei3yFJPNSHtfbeFHSr9eVB/OL8dOGbxQluGCh6XmaqTjyrh3fqUTWz7+n74+gu2ugAFFZ18iT+DStK0TTdmy4vBC6xUcHQ'", - "masterToken : '****'" - }, - {//5 - "assertion content:\"ETMsDgAAAXI0IS9NABRBRVMvQ0JDL1BLQ1M1UGFkZGluZwCAABAAEEb/xAQlmT+mwIx9G32E+ikAAACA/CPlEkq//+jWZnQkOj5VhjayruDsCVRGS/B6GzHUugXLc94EfEwuto94gS/oKSVrUg/JRPekypLAx4Afa1KW8n1RqXRF9Hzy1VVLmVEBMtei3yFJPNSHtfbeFHSr9eVB/OL8dOGbxQluGCh6XmaqTjyrh3fqUTWz7+n74+gu2ugAFFZ18iT+DStK0TTdmy4vBC6xUcHQ\"", - "assertion content:\"****\"" - }, - {//6 - "password: random!TEST/-pwd=123++#", - "password: ****" - }, - {//7 - "pwd =\"random!TEST/-pwd=123++#", - "pwd =\"****" - }, - {//8 - "AWSAccessKeyId=ABCD%efg+1234/567", - "AWSAccessKeyId=****" - }, - {//9 - "https://sfc-fake.s3.fakeamazon.com/012345xx-012x-012x-0123-1a2b3c4d/fake/data_fake?x-amz-server-side-encryption-customer-algorithm=fakealgo&response-content-encoding=fakezip&AWSAccessKeyId=ABCD%efg+1234/567&Expires=123456789&Signature=ABCD%efg+1234/567ABCD%efg+1234/567", - "https://sfc-fake.s3.fakeamazon.com/012345xx-012x-012x-0123-1a2b3c4d/fake/data_fake?x-amz-server-side-encryption-customer-algorithm=fakealgo&response-content-encoding=fakezip&AWSAccessKeyId=****&Expires=123456789&Signature=****" - }, - {//10 - "aws_key_id='afhl124lomsafho0582'", - "aws_key_id='****'" - }, - {//11 - "aws_secret_key = 'dfhuwaojm753omsdfh30oi+fj'", - "aws_secret_key = '****'" - }, - {//12 - "\"privateKeyData\": \"abcdefghijk\"", - "\"privateKeyData\": \"XXXX\"" - }, - }; - - char * line = NULL; - size_t len = 0; - for (int i = 0; i < 13; i++) - { - fseek(fp, 0, SEEK_SET); - log_trace("%s", logtext[i][0]); - fseek(fp, 0, SEEK_SET); - getline(&line, &len, fp); - if (i != 0) - { - assert_null(strstr(line, logtext[i][0])); - } - assert_non_null(strstr(line, logtext[i][1])); - } - - free(line); - fclose(fp); -} -#endif - -int main(void) { - const struct CMUnitTest tests[] = { - cmocka_unit_test(test_log_str_to_level), -#ifndef _WIN32 - cmocka_unit_test(test_log_creation), - cmocka_unit_test(test_mask_secret_log), -#endif - }; - return cmocka_run_group_tests(tests, NULL, NULL); -} +/* + * Copyright (c) 2018-2019 Snowflake Computing, Inc. All rights reserved. + */ + + +#include "utils/test_setup.h" +#include +#include "memory.h" + +#ifndef _WIN32 +#include +#endif + +/** + * Tests converting a string representation of log level to the log level enum + */ +void test_log_str_to_level(void **unused) { + assert_int_equal(log_from_str_to_level("TRACE"), SF_LOG_TRACE); + assert_int_equal(log_from_str_to_level("DEBUG"), SF_LOG_DEBUG); + assert_int_equal(log_from_str_to_level("INFO"), SF_LOG_INFO); + assert_int_equal(log_from_str_to_level("wArN"), SF_LOG_WARN); + assert_int_equal(log_from_str_to_level("erroR"), SF_LOG_ERROR); + assert_int_equal(log_from_str_to_level("fatal"), SF_LOG_FATAL); + + /* negative */ + assert_int_equal(log_from_str_to_level("hahahaha"), SF_LOG_FATAL); + assert_int_equal(log_from_str_to_level(NULL), SF_LOG_FATAL); +} + +/** + * Tests log settings with invalid client config filepath + */ +void test_invalid_client_config_path(void** unused) { + char configFilePath[] = "fakePath.json"; + + // Parse client config for log details + client_config clientConfig = { 0 }; + sf_bool result = load_client_config(configFilePath, &clientConfig); +#if !defined(_WIN32) && !defined(_DEBUG) + assert_false(result); +#else + assert_true(result); +#endif +} + +/** + * Tests log settings from client config file with invalid json + */ +void test_client_config_log_invalid_json(void** unused) { + char clientConfigJSON[] = "{{{\"invalid json\"}"; + char configFilePath[] = "sf_client_config.json"; + FILE* file; + file = fopen(configFilePath, "w"); + fprintf(file, "%s", clientConfigJSON); + fclose(file); + + // Parse client config for log details + client_config clientConfig = { 0 }; + sf_bool result = load_client_config(configFilePath, &clientConfig); +#if !defined(_WIN32) && !defined(_DEBUG) + assert_false(result); +#else + assert_true(result); +#endif + + // Cleanup + remove(configFilePath); +} + +#ifndef _WIN32 +/** + * Tests log settings from client config file + */ +void test_client_config_log(void **unused) { + char clientConfigJSON[] = "{\"common\":{\"log_level\":\"warn\",\"log_path\":\"./test/\"}}"; + char configFilePath[] = "sf_client_config.json"; + FILE *file; + file = fopen(configFilePath,"w"); + fprintf(file, "%s", clientConfigJSON); + fclose(file); + + // Parse client config for log details + client_config clientConfig = { 0 }; + load_client_config(configFilePath, &clientConfig); + + // Set log name and level + char logname[] = "%s/dummy.log"; + size_t log_path_size = 1 + strlen(logname); + log_path_size += strlen(clientConfig.logPath); + char* LOG_PATH = (char*)SF_CALLOC(1, log_path_size); + sf_sprintf(LOG_PATH, log_path_size, logname, clientConfig.logPath); + log_set_level(log_from_str_to_level(clientConfig.logLevel)); + log_set_path(LOG_PATH); + + // Ensure the log file doesn't exist at the beginning + remove(LOG_PATH); + + // Info log won't trigger the log file creation since log level is set to warn in config + log_info("dummy info log"); + assert_int_not_equal(access(LOG_PATH, F_OK), 0); + + // Warning log will trigger the log file creation + log_warn("dummy warning log"); + assert_int_equal(access(LOG_PATH, F_OK), 0); + log_close(); + + // Cleanup + remove(configFilePath); + remove(LOG_PATH); +} + +/** + * Tests log settings from client config file via global init + */ +void test_client_config_log_init(void** unused) { + char LOG_PATH[MAX_PATH] = { 0 }; + char clientConfigJSON[] = "{\"common\":{\"log_level\":\"warn\",\"log_path\":\"./test/\"}}"; + char configFilePath[] = "sf_client_config.json"; + FILE* file; + file = fopen(configFilePath, "w"); + fprintf(file, "%s", clientConfigJSON); + fclose(file); + + snowflake_global_set_attribute(SF_GLOBAL_CLIENT_CONFIG_FILE, configFilePath); + snowflake_global_init("./logs", SF_LOG_DEFAULT, NULL); + + // Get the log path determined by libsnowflakeclient + snowflake_global_get_attribute(SF_GLOBAL_LOG_PATH, LOG_PATH, MAX_PATH); + // Ensure the log file doesn't exist at the beginning + remove(LOG_PATH); + + // Info log won't trigger the log file creation since log level is set to warn in config + log_info("dummy info log"); + assert_int_not_equal(access(LOG_PATH, F_OK), 0); + + // Warning log will trigger the log file creation + log_warn("dummy warning log"); + assert_int_equal(access(LOG_PATH, F_OK), 0); + log_close(); + + // Cleanup + remove(configFilePath); + remove(LOG_PATH); +} + +/** + * Tests timing of log file creation + */ +void test_log_creation(void **unused) { + char logname[] = "dummy.log"; + + // ensure the log file doesn't exist at the beginning + remove(logname); + assert_int_not_equal(access(logname, F_OK), 0); + + log_set_lock(NULL); + log_set_level(SF_LOG_WARN); + log_set_quiet(1); + log_set_path(logname); + + // info log won't trigger the log file creation since log level is set to warning + log_info("dummy info log"); + assert_int_not_equal(access(logname, F_OK), 0); + + // warning log will trigger the log file creation + log_warn("dummy warning log"); + assert_int_equal(access(logname, F_OK), 0); + log_close(); + + remove(logname); +} + +/** + * Tests masking secret information in log + */ +void test_mask_secret_log(void **unused) { + FILE* fp = fopen("dummy.log", "w+"); + assert_non_null(fp); + log_set_lock(NULL); + log_set_level(SF_LOG_TRACE); + log_set_quiet(1); + log_set_fp(fp); + + const char * logtext[][2] = { + {//0 + "Secure log record!", + "Secure log record!" + }, + {//1 + "Token =ETMsDgAAAXI0IS9NABRBRVMvQ0JDL1BLQ1M1UGFkZGluZwCAABAAEEb/xAQlmT+mwIx9G32E+ikAAACA/CPlEkq//+jWZnQkOj5VhjayruDsCVRGS/B6GzHUugXLc94EfEwuto94gS/oKSVrUg/JRPekypLAx4Afa1KW8n1RqXRF9Hzy1VVLmVEBMtei3yFJPNSHtfbeFHSr9eVB/OL8dOGbxQluGCh6XmaqTjyrh3fqUTWz7+n74+gu2ugAFFZ18iT+DStK0TTdmy4vBC6xUcHQ", + "Token =****" + }, + {//2 + "idToken : ETMsDgAAAXI0IS9NABRBRVMvQ0JDL1BLQ1M1UGFkZGluZwCAABAAEEb/xAQlmT+mwIx9G32E+ikAAACA/CPlEkq//+jWZnQkOj5VhjayruDsCVRGS/B6GzHUugXLc94EfEwuto94gS/oKSVrUg/JRPekypLAx4Afa1KW8n1RqXRF9Hzy1VVLmVEBMtei3yFJPNSHtfbeFHSr9eVB/OL8dOGbxQluGCh6XmaqTjyrh3fqUTWz7+n74+gu2ugAFFZ18iT+DStK0TTdmy4vBC6xUcHQ", + "idToken : ****" + }, + {//3 + "sessionToken:ETMsDgAAAXI0IS9NABRBRVMvQ0JDL1BLQ1M1UGFkZGluZwCAABAAEEb/xAQlmT+mwIx9G32E+ikAAACA/CPlEkq//+jWZnQkOj5VhjayruDsCVRGS/B6GzHUugXLc94EfEwuto94gS/oKSVrUg/JRPekypLAx4Afa1KW8n1RqXRF9Hzy1VVLmVEBMtei3yFJPNSHtfbeFHSr9eVB/OL8dOGbxQluGCh6XmaqTjyrh3fqUTWz7+n74+gu2ugAFFZ18iT+DStK0TTdmy4vBC6xUcHQ", + "sessionToken:****" + }, + {//4 + "masterToken : 'ETMsDgAAAXI0IS9NABRBRVMvQ0JDL1BLQ1M1UGFkZGluZwCAABAAEEb/xAQlmT+mwIx9G32E+ikAAACA/CPlEkq//+jWZnQkOj5VhjayruDsCVRGS/B6GzHUugXLc94EfEwuto94gS/oKSVrUg/JRPekypLAx4Afa1KW8n1RqXRF9Hzy1VVLmVEBMtei3yFJPNSHtfbeFHSr9eVB/OL8dOGbxQluGCh6XmaqTjyrh3fqUTWz7+n74+gu2ugAFFZ18iT+DStK0TTdmy4vBC6xUcHQ'", + "masterToken : '****'" + }, + {//5 + "assertion content:\"ETMsDgAAAXI0IS9NABRBRVMvQ0JDL1BLQ1M1UGFkZGluZwCAABAAEEb/xAQlmT+mwIx9G32E+ikAAACA/CPlEkq//+jWZnQkOj5VhjayruDsCVRGS/B6GzHUugXLc94EfEwuto94gS/oKSVrUg/JRPekypLAx4Afa1KW8n1RqXRF9Hzy1VVLmVEBMtei3yFJPNSHtfbeFHSr9eVB/OL8dOGbxQluGCh6XmaqTjyrh3fqUTWz7+n74+gu2ugAFFZ18iT+DStK0TTdmy4vBC6xUcHQ\"", + "assertion content:\"****\"" + }, + {//6 + "password: random!TEST/-pwd=123++#", + "password: ****" + }, + {//7 + "pwd =\"random!TEST/-pwd=123++#", + "pwd =\"****" + }, + {//8 + "AWSAccessKeyId=ABCD%efg+1234/567", + "AWSAccessKeyId=****" + }, + {//9 + "https://sfc-fake.s3.fakeamazon.com/012345xx-012x-012x-0123-1a2b3c4d/fake/data_fake?x-amz-server-side-encryption-customer-algorithm=fakealgo&response-content-encoding=fakezip&AWSAccessKeyId=ABCD%efg+1234/567&Expires=123456789&Signature=ABCD%efg+1234/567ABCD%efg+1234/567", + "https://sfc-fake.s3.fakeamazon.com/012345xx-012x-012x-0123-1a2b3c4d/fake/data_fake?x-amz-server-side-encryption-customer-algorithm=fakealgo&response-content-encoding=fakezip&AWSAccessKeyId=****&Expires=123456789&Signature=****" + }, + {//10 + "aws_key_id='afhl124lomsafho0582'", + "aws_key_id='****'" + }, + {//11 + "aws_secret_key = 'dfhuwaojm753omsdfh30oi+fj'", + "aws_secret_key = '****'" + }, + {//12 + "\"privateKeyData\": \"abcdefghijk\"", + "\"privateKeyData\": \"XXXX\"" + }, + }; + + char * line = NULL; + size_t len = 0; + for (int i = 0; i < 13; i++) + { + fseek(fp, 0, SEEK_SET); + log_trace("%s", logtext[i][0]); + fseek(fp, 0, SEEK_SET); + getline(&line, &len, fp); + if (i != 0) + { + assert_null(strstr(line, logtext[i][0])); + } + assert_non_null(strstr(line, logtext[i][1])); + } + + free(line); + fclose(fp); +} +#endif + +int main(void) { + const struct CMUnitTest tests[] = { + cmocka_unit_test(test_log_str_to_level), + cmocka_unit_test(test_invalid_client_config_path), + cmocka_unit_test(test_client_config_log_invalid_json), +#ifndef _WIN32 + cmocka_unit_test(test_client_config_log), + cmocka_unit_test(test_client_config_log_init), + cmocka_unit_test(test_log_creation), + cmocka_unit_test(test_mask_secret_log), +#endif + }; + return cmocka_run_group_tests(tests, NULL, NULL); +}