/* * Copyright 2021, João Valverde * * Wireshark - Network traffic analyzer * By Gerald Combs * Copyright 1998 Gerald Combs * * SPDX-License-Identifier: GPL-2.0-or-later */ #include "config.h" #if defined(WS_DISABLE_ASSERT) && !defined(NDEBUG) #define NDEBUG #endif #include "wslog.h" #include #include #include #include #include #ifdef HAVE_UNISTD_H #include #endif #ifdef _WIN32 #include #include #endif #include "file_util.h" #include "to_str.h" #include "strtoi.h" /* Runtime log level. */ #define ENV_VAR_LEVEL "WIRESHARK_LOG_LEVEL" /* Log domains enabled/disabled. */ #define ENV_VAR_DOMAIN "WIRESHARK_LOG_DOMAIN" /* Alias "domain" and "domains". */ #define ENV_VAR_DOMAIN_S "WIRESHARK_LOG_DOMAINS" /* Log level that generates a trap and aborts. Can be "critical" * or "warning". */ #define ENV_VAR_FATAL "WIRESHARK_LOG_FATAL" /* Domains that will produce debug output, regardless of log level or * domain filter. */ #define ENV_VAR_DEBUG "WIRESHARK_LOG_DEBUG" /* Domains that will produce noisy output, regardless of log level or * domain filter. */ #define ENV_VAR_NOISY "WIRESHARK_LOG_NOISY" #define DEFAULT_LOG_LEVEL LOG_LEVEL_MESSAGE #define DEFAULT_PROGNAME "PID" #define DOMAIN_UNDEFED(domain) ((domain) == NULL || *(domain) == '\0') #define DOMAIN_DEFINED(domain) (!DOMAIN_UNDEFED(domain)) #define VALID_FATAL_LEVEL(level) \ (level >= LOG_LEVEL_WARNING && level <= LOG_LEVEL_ERROR) /* * Note: I didn't measure it but I assume using a string array is faster than * a GHashTable for small number N of domains. */ typedef struct { char **domainv; gboolean positive; /* positive or negative match */ enum ws_log_level min_level; /* for level filters */ } log_filter_t; /* If the module is not initialized by calling ws_log_init() all messages * will be printed regardless of log level. This is a feature, not a bug. */ static enum ws_log_level current_log_level = LOG_LEVEL_NONE; static gboolean stdout_color_enabled = FALSE; static gboolean stderr_color_enabled = FALSE; /* Use stderr for levels "info" and below. */ static gboolean stderr_debug_enabled = FALSE; static const char *registered_progname = DEFAULT_PROGNAME; /* List of domains to filter. */ static log_filter_t *domain_filter = NULL; /* List of domains to output debug level unconditionally. */ static log_filter_t *debug_filter = NULL; /* List of domains to output noisy level unconditionally. */ static log_filter_t *noisy_filter = NULL; static ws_log_writer_cb *registered_log_writer = NULL; static void *registered_log_writer_data = NULL; static ws_log_writer_free_data_cb *registered_log_writer_data_free = NULL; static FILE *custom_log = NULL; static enum ws_log_level fatal_log_level = LOG_LEVEL_ERROR; #ifndef WS_DISABLE_DEBUG static gboolean init_complete = FALSE; #endif static void print_err(void (*vcmdarg_err)(const char *, va_list ap), int exit_failure, const char *fmt, ...) G_GNUC_PRINTF(3,4); static void ws_log_cleanup(void); const char *ws_log_level_to_string(enum ws_log_level level) { switch (level) { case LOG_LEVEL_NONE: return "(zero)"; case LOG_LEVEL_ERROR: return "ERROR"; case LOG_LEVEL_CRITICAL: return "CRITICAL"; case LOG_LEVEL_WARNING: return "WARNING"; case LOG_LEVEL_MESSAGE: return "MESSAGE"; case LOG_LEVEL_INFO: return "INFO"; case LOG_LEVEL_DEBUG: return "DEBUG"; case LOG_LEVEL_NOISY: return "NOISY"; default: return "(BOGUS LOG LEVEL)"; } } static enum ws_log_level string_to_log_level(const char *str_level) { if (!str_level) return LOG_LEVEL_NONE; if (g_ascii_strcasecmp(str_level, "noisy") == 0) return LOG_LEVEL_NOISY; else if (g_ascii_strcasecmp(str_level, "debug") == 0) return LOG_LEVEL_DEBUG; else if (g_ascii_strcasecmp(str_level, "info") == 0) return LOG_LEVEL_INFO; else if (g_ascii_strcasecmp(str_level, "message") == 0) return LOG_LEVEL_MESSAGE; else if (g_ascii_strcasecmp(str_level, "warning") == 0) return LOG_LEVEL_WARNING; else if (g_ascii_strcasecmp(str_level, "critical") == 0) return LOG_LEVEL_CRITICAL; else if (g_ascii_strcasecmp(str_level, "error") == 0) return LOG_LEVEL_ERROR; else return LOG_LEVEL_NONE; } WS_RETNONNULL static inline const char *domain_to_string(const char *domain) { return DOMAIN_UNDEFED(domain) ? "(none)" : domain; } static inline gboolean filter_contains(log_filter_t *filter, const char *domain) { if (filter == NULL || DOMAIN_UNDEFED(domain)) return FALSE; for (char **domv = filter->domainv; *domv != NULL; domv++) { if (g_ascii_strcasecmp(*domv, domain) == 0) { return TRUE; } } return FALSE; } static inline gboolean level_filter_matches(log_filter_t *filter, const char *domain, enum ws_log_level level, gboolean *active_ptr) { if (filter == NULL || DOMAIN_UNDEFED(domain)) return FALSE; if (!filter_contains(filter, domain)) return FALSE; if (filter->positive) { if (active_ptr) *active_ptr = level >= filter->min_level; return TRUE; } /* negative match */ if (level <= filter->min_level) { if (active_ptr) *active_ptr = FALSE; return TRUE; } return FALSE; } gboolean ws_log_msg_is_active(const char *domain, enum ws_log_level level) { /* * Higher numerical levels have higher priority. Critical and above * are always enabled. */ if (level >= LOG_LEVEL_CRITICAL) return TRUE; /* * Check if the level has been configured as fatal. */ if (level >= fatal_log_level) return TRUE; /* * The debug/noisy filter overrides the other parameters. */ if (DOMAIN_DEFINED(domain)) { gboolean active; if (level_filter_matches(noisy_filter, domain, level, &active)) return active; if (level_filter_matches(debug_filter, domain, level, &active)) return active; } /* * If the priority is lower than the current minimum drop the * message. */ if (level < current_log_level) return FALSE; /* * If we don't have domain filtering enabled we are done. */ if (domain_filter == NULL) return TRUE; /* * We have a filter but we don't use it with the undefined domain, * pretty much every permanent call to ws_log should be using a * chosen domain. */ if (DOMAIN_UNDEFED(domain)) return TRUE; /* Check if the domain filter matches. */ if (filter_contains(domain_filter, domain)) return domain_filter->positive; /* We have a domain filter but it didn't match. */ return !domain_filter->positive; } enum ws_log_level ws_log_get_level(void) { return current_log_level; } void ws_log_set_level(enum ws_log_level level) { if (level <= LOG_LEVEL_NONE || level >= _LOG_LEVEL_LAST) return; current_log_level = level; } enum ws_log_level ws_log_set_level_str(const char *str_level) { enum ws_log_level level; level = string_to_log_level(str_level); if (level == LOG_LEVEL_NONE) return LOG_LEVEL_NONE; current_log_level = level; return current_log_level; } static const char *opt_level = "--log-level"; /* Alias "domain" and "domains". */ static const char *opt_domain = "--log-domain"; static const char *opt_file = "--log-file"; static const char *opt_fatal = "--log-fatal"; static const char *opt_debug = "--log-debug"; static const char *opt_noisy = "--log-noisy"; static void print_err(void (*vcmdarg_err)(const char *, va_list ap), int exit_failure, const char *fmt, ...) { va_list ap; if (vcmdarg_err == NULL) return; va_start(ap, fmt); vcmdarg_err(fmt, ap); va_end(ap); if (exit_failure != LOG_ARGS_NOEXIT) exit(exit_failure); } /* * This tries to convert old log level preference to a wslog * configuration. The string must start with "console.log.level:" * It receives an argv for { '-o', 'console.log.level:nnn', ...} or * { '-oconsole.log.level:nnn', ...}. */ static void parse_console_compat_option(char *argv[], void (*vcmdarg_err)(const char *, va_list ap), int exit_failure) { const char *mask_str; guint32 mask; enum ws_log_level level; ws_assert(argv != NULL); if (argv[0] == NULL) return; if (strcmp(argv[0], "-o") == 0) { if (argv[1] == NULL || !g_str_has_prefix(argv[1], "console.log.level:")) { /* Not what we were looking for. */ return; } mask_str = argv[1] + strlen("console.log.level:"); } else if (g_str_has_prefix(argv[0], "-oconsole.log.level:")) { mask_str = argv[0] + strlen("-oconsole.log.level:"); } else { /* Not what we were looking for. */ return; } print_err(vcmdarg_err, LOG_ARGS_NOEXIT, "Option 'console.log.level' is deprecated, consult '--help' " "for diagnostic message options."); if (*mask_str == '\0') { print_err(vcmdarg_err, exit_failure, "Missing value to 'console.log.level' option."); return; } if (!ws_basestrtou32(mask_str, NULL, &mask, 10)) { print_err(vcmdarg_err, exit_failure, "%s is not a valid decimal number.", mask_str); return; } /* * The lowest priority bit in the mask defines the level. */ if (mask & G_LOG_LEVEL_DEBUG) level = LOG_LEVEL_DEBUG; else if (mask & G_LOG_LEVEL_INFO) level = LOG_LEVEL_INFO; else if (mask & G_LOG_LEVEL_MESSAGE) level = LOG_LEVEL_MESSAGE; else if (mask & G_LOG_LEVEL_WARNING) level = LOG_LEVEL_WARNING; else if (mask & G_LOG_LEVEL_CRITICAL) level = LOG_LEVEL_CRITICAL; else if (mask & G_LOG_LEVEL_ERROR) level = LOG_LEVEL_ERROR; else level = LOG_LEVEL_NONE; if (level == LOG_LEVEL_NONE) { /* Some values (like zero) might not contain any meaningful bits. * Throwing an error in that case seems appropriate. */ print_err(vcmdarg_err, exit_failure, "Value %s is not a valid log mask.", mask_str); return; } ws_log_set_level(level); } int ws_log_parse_args(int *argc_ptr, char *argv[], void (*vcmdarg_err)(const char *, va_list ap), int exit_failure) { char **ptr = argv; int count = *argc_ptr; int ret = 0; size_t optlen; const char *option, *value; int extra; if (argc_ptr == NULL || argv == NULL) return -1; /* Configure from command line. */ while (*ptr != NULL) { if (g_str_has_prefix(*ptr, opt_level)) { option = opt_level; optlen = strlen(opt_level); } else if (g_str_has_prefix(*ptr, opt_domain)) { option = opt_domain; optlen = strlen(opt_domain); /* Alias "domain" and "domains". Last form wins. */ if (*(*ptr + optlen) == 's') { optlen += 1; } } else if (g_str_has_prefix(*ptr, opt_file)) { option = opt_file; optlen = strlen(opt_file); } else if (g_str_has_prefix(*ptr, opt_fatal)) { option = opt_fatal; optlen = strlen(opt_fatal); } else if (g_str_has_prefix(*ptr, opt_debug)) { option = opt_debug; optlen = strlen(opt_debug); } else if (g_str_has_prefix(*ptr, opt_noisy)) { option = opt_noisy; optlen = strlen(opt_noisy); } else { /* Check is we have the old '-o console.log.level' flag, * or '-oconsole.log.level', for backward compatibility. * Then if we do ignore it after processing and let the * preferences module handle it later. */ if (*(*ptr + 0) == '-' && *(*ptr + 1) == 'o') { parse_console_compat_option(ptr, vcmdarg_err, exit_failure); } ptr += 1; count -= 1; continue; } value = *ptr + optlen; /* Two possibilities: * --