diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-06 00:47:26 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-06 00:47:26 +0000 |
commit | 96b619cc129afed52411b9fad3407037a1cb7207 (patch) | |
tree | e453a74cc9ae39fbfcb3ac55a347e880413e4a06 /src/log.c | |
parent | Initial commit. (diff) | |
download | exim4-upstream.tar.xz exim4-upstream.zip |
Adding upstream version 4.92.upstream/4.92upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/log.c')
-rw-r--r-- | src/log.c | 1462 |
1 files changed, 1462 insertions, 0 deletions
diff --git a/src/log.c b/src/log.c new file mode 100644 index 0000000..d082000 --- /dev/null +++ b/src/log.c @@ -0,0 +1,1462 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2018 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* Functions for writing log files. The code for maintaining datestamped +log files was originally contributed by Tony Sheen. */ + + +#include "exim.h" + +#define LOG_NAME_SIZE 256 +#define MAX_SYSLOG_LEN 870 + +#define LOG_MODE_FILE 1 +#define LOG_MODE_SYSLOG 2 + +enum { lt_main, lt_reject, lt_panic, lt_debug }; + +static uschar *log_names[] = { US"main", US"reject", US"panic", US"debug" }; + + + +/************************************************* +* Local static variables * +*************************************************/ + +static uschar mainlog_name[LOG_NAME_SIZE]; +static uschar rejectlog_name[LOG_NAME_SIZE]; +static uschar debuglog_name[LOG_NAME_SIZE]; + +static uschar *mainlog_datestamp = NULL; +static uschar *rejectlog_datestamp = NULL; + +static int mainlogfd = -1; +static int rejectlogfd = -1; +static ino_t mainlog_inode = 0; +static ino_t rejectlog_inode = 0; + +static uschar *panic_save_buffer = NULL; +static BOOL panic_recurseflag = FALSE; + +static BOOL syslog_open = FALSE; +static BOOL path_inspected = FALSE; +static int logging_mode = LOG_MODE_FILE; +static uschar *file_path = US""; + +static size_t pid_position[2]; + + +/* These should be kept in-step with the private delivery error +number definitions in macros.h */ + +static const uschar * exim_errstrings[] = { + US"", + US"unknown error", + US"user slash", + US"exist race", + US"not regular", + US"not directory", + US"bad ugid", + US"bad mode", + US"inode changed", + US"lock failed", + US"bad address2", + US"forbid pipe", + US"forbid file", + US"forbid reply", + US"missing pipe", + US"missing file", + US"missing reply", + US"bad redirect", + US"smtp closed", + US"smtp format", + US"spool format", + US"not absolute", + US"Exim-imposed quota", + US"held", + US"Delivery filter process failure", + US"Delivery add/remove header failure", + US"Delivery write incomplete error", + US"Some expansion failed", + US"Failed to get gid", + US"Failed to get uid", + US"Unset or non-existent transport", + US"MBX length mismatch", + US"Lookup failed routing or in smtp tpt", + US"Can't match format in appendfile", + US"Creation outside home in appendfile", + US"Can't check a list; lookup defer", + US"DNS lookup defer", + US"Failed to start TLS session", + US"Mandatory TLS session not started", + US"Failed to chown a file", + US"Failed to create a pipe", + US"When verifying", + US"When required by client", + US"Used internally in smtp transport", + US"RCPT gave 4xx error", + US"MAIL gave 4xx error", + US"DATA gave 4xx error", + US"Negotiation failed for proxy configured host", + US"Authenticator 'other' failure", + US"target not supporting SMTPUTF8", + US"", + + US"Not time for routing", + US"Not time for local delivery", + US"Not time for any remote host", + US"Local-only delivery", + US"Domain in queue_domains", + US"Transport concurrency limit", +}; + + +/************************************************/ +const uschar * +exim_errstr(int err) +{ +return err < 0 ? exim_errstrings[-err] : CUS strerror(err); +} + +/************************************************* +* Write to syslog * +*************************************************/ + +/* The given string is split into sections according to length, or at embedded +newlines, and syslogged as a numbered sequence if it is overlong or if there is +more than one line. However, if we are running in the test harness, do not do +anything. (The test harness doesn't use syslog - for obvious reasons - but we +can get here if there is a failure to open the panic log.) + +Arguments: + priority syslog priority + s the string to be written + +Returns: nothing +*/ + +static void +write_syslog(int priority, const uschar *s) +{ +int len, pass; +int linecount = 0; + +if (!syslog_pid && LOGGING(pid)) + s = string_sprintf("%.*s%s", (int)pid_position[0], s, s + pid_position[1]); +if (!syslog_timestamp) + { + len = log_timezone ? 26 : 20; + if (LOGGING(millisec)) len += 4; + s += len; + } + +len = Ustrlen(s); + +#ifndef NO_OPENLOG +if (!syslog_open && !f.running_in_test_harness) + { +# ifdef SYSLOG_LOG_PID + openlog(CS syslog_processname, LOG_PID|LOG_CONS, syslog_facility); +# else + openlog(CS syslog_processname, LOG_CONS, syslog_facility); +# endif + syslog_open = TRUE; + } +#endif + +/* First do a scan through the message in order to determine how many lines +it is going to end up as. Then rescan to output it. */ + +for (pass = 0; pass < 2; pass++) + { + int i; + int tlen; + const uschar * ss = s; + for (i = 1, tlen = len; tlen > 0; i++) + { + int plen = tlen; + uschar *nlptr = Ustrchr(ss, '\n'); + if (nlptr != NULL) plen = nlptr - ss; +#ifndef SYSLOG_LONG_LINES + if (plen > MAX_SYSLOG_LEN) plen = MAX_SYSLOG_LEN; +#endif + tlen -= plen; + if (ss[plen] == '\n') tlen--; /* chars left */ + + if (pass == 0) + linecount++; + else if (f.running_in_test_harness) + if (linecount == 1) + fprintf(stderr, "SYSLOG: '%.*s'\n", plen, ss); + else + fprintf(stderr, "SYSLOG: '[%d%c%d] %.*s'\n", i, + ss[plen] == '\n' && tlen != 0 ? '\\' : '/', + linecount, plen, ss); + else + if (linecount == 1) + syslog(priority, "%.*s", plen, ss); + else + syslog(priority, "[%d%c%d] %.*s", i, + ss[plen] == '\n' && tlen != 0 ? '\\' : '/', + linecount, plen, ss); + + ss += plen; + if (*ss == '\n') ss++; + } + } +} + + + +/************************************************* +* Die tidily * +*************************************************/ + +/* This is called when Exim is dying as a result of something going wrong in +the logging, or after a log call with LOG_PANIC_DIE set. Optionally write a +message to debug_file or a stderr file, if they exist. Then, if in the middle +of accepting a message, throw it away tidily by calling receive_bomb_out(); +this will attempt to send an SMTP response if appropriate. Passing NULL as the +first argument stops it trying to run the NOTQUIT ACL (which might try further +logging and thus cause problems). Otherwise, try to close down an outstanding +SMTP call tidily. + +Arguments: + s1 Error message to write to debug_file and/or stderr and syslog + s2 Error message for any SMTP call that is in progress +Returns: The function does not return +*/ + +static void +die(uschar *s1, uschar *s2) +{ +if (s1) + { + write_syslog(LOG_CRIT, s1); + if (debug_file) debug_printf("%s\n", s1); + if (log_stderr && log_stderr != debug_file) + fprintf(log_stderr, "%s\n", s1); + } +if (f.receive_call_bombout) receive_bomb_out(NULL, s2); /* does not return */ +if (smtp_input) smtp_closedown(s2); +exim_exit(EXIT_FAILURE, NULL); +} + + + +/************************************************* +* Create a log file * +*************************************************/ + +/* This function is called to create and open a log file. It may be called in a +subprocess when the original process is root. + +Arguments: + name the file name + +The file name has been build in a working buffer, so it is permissible to +overwrite it temporarily if it is necessary to create the directory. + +Returns: a file descriptor, or < 0 on failure (errno set) +*/ + +int +log_create(uschar *name) +{ +int fd = Uopen(name, +#ifdef O_CLOEXEC + O_CLOEXEC | +#endif + O_CREAT|O_APPEND|O_WRONLY, LOG_MODE); + +/* If creation failed, attempt to build a log directory in case that is the +problem. */ + +if (fd < 0 && errno == ENOENT) + { + BOOL created; + uschar *lastslash = Ustrrchr(name, '/'); + *lastslash = 0; + created = directory_make(NULL, name, LOG_DIRECTORY_MODE, FALSE); + DEBUG(D_any) debug_printf("%s log directory %s\n", + created ? "created" : "failed to create", name); + *lastslash = '/'; + if (created) fd = Uopen(name, +#ifdef O_CLOEXEC + O_CLOEXEC | +#endif + O_CREAT|O_APPEND|O_WRONLY, LOG_MODE); + } + +return fd; +} + + + +/************************************************* +* Create a log file as the exim user * +*************************************************/ + +/* This function is called when we are root to spawn an exim:exim subprocess +in which we can create a log file. It must be signal-safe since it is called +by the usr1_handler(). + +Arguments: + name the file name + +Returns: a file descriptor, or < 0 on failure (errno set) +*/ + +int +log_create_as_exim(uschar *name) +{ +pid_t pid = fork(); +int status = 1; +int fd = -1; + +/* In the subprocess, change uid/gid and do the creation. Return 0 from the +subprocess on success. If we don't check for setuid failures, then the file +can be created as root, so vulnerabilities which cause setuid to fail mean +that the Exim user can use symlinks to cause a file to be opened/created as +root. We always open for append, so can't nuke existing content but it would +still be Rather Bad. */ + +if (pid == 0) + { + if (setgid(exim_gid) < 0) + die(US"exim: setgid for log-file creation failed, aborting", + US"Unexpected log failure, please try later"); + if (setuid(exim_uid) < 0) + die(US"exim: setuid for log-file creation failed, aborting", + US"Unexpected log failure, please try later"); + _exit((log_create(name) < 0)? 1 : 0); + } + +/* If we created a subprocess, wait for it. If it succeeded, try the open. */ + +while (pid > 0 && waitpid(pid, &status, 0) != pid); +if (status == 0) fd = Uopen(name, +#ifdef O_CLOEXEC + O_CLOEXEC | +#endif + O_APPEND|O_WRONLY, LOG_MODE); + +/* If we failed to create a subprocess, we are in a bad way. We return +with fd still < 0, and errno set, letting the caller handle the error. */ + +return fd; +} + + + + +/************************************************* +* Open a log file * +*************************************************/ + +/* This function opens one of a number of logs, creating the log directory if +it does not exist. This may be called recursively on failure, in order to open +the panic log. + +The directory is in the static variable file_path. This is static so that it +the work of sorting out the path is done just once per Exim process. + +Exim is normally configured to avoid running as root wherever possible, the log +files must be owned by the non-privileged exim user. To ensure this, first try +an open without O_CREAT - most of the time this will succeed. If it fails, try +to create the file; if running as root, this must be done in a subprocess to +avoid races. + +Arguments: + fd where to return the resulting file descriptor + type lt_main, lt_reject, lt_panic, or lt_debug + tag optional tag to include in the name (only hooked up for debug) + +Returns: nothing +*/ + +static void +open_log(int *fd, int type, uschar *tag) +{ +uid_t euid; +BOOL ok, ok2; +uschar buffer[LOG_NAME_SIZE]; + +/* The names of the log files are controlled by file_path. The panic log is +written to the same directory as the main and reject logs, but its name does +not have a datestamp. The use of datestamps is indicated by %D/%M in file_path. +When opening the panic log, if %D or %M is present, we remove the datestamp +from the generated name; if it is at the start, remove a following +non-alphanumeric character as well; otherwise, remove a preceding +non-alphanumeric character. This is definitely kludgy, but it sort of does what +people want, I hope. */ + +ok = string_format(buffer, sizeof(buffer), CS file_path, log_names[type]); + +/* Save the name of the mainlog for rollover processing. Without a datestamp, +it gets statted to see if it has been cycled. With a datestamp, the datestamp +will be compared. The static slot for saving it is the same size as buffer, +and the text has been checked above to fit, so this use of strcpy() is OK. */ + +if (type == lt_main && string_datestamp_offset >= 0) + { + Ustrcpy(mainlog_name, buffer); + mainlog_datestamp = mainlog_name + string_datestamp_offset; + } + +/* Ditto for the reject log */ + +else if (type == lt_reject && string_datestamp_offset >= 0) + { + Ustrcpy(rejectlog_name, buffer); + rejectlog_datestamp = rejectlog_name + string_datestamp_offset; + } + +/* and deal with the debug log (which keeps the datestamp, but does not +update it) */ + +else if (type == lt_debug) + { + Ustrcpy(debuglog_name, buffer); + if (tag) + { + /* this won't change the offset of the datestamp */ + ok2 = string_format(buffer, sizeof(buffer), "%s%s", + debuglog_name, tag); + if (ok2) + Ustrcpy(debuglog_name, buffer); + } + } + +/* Remove any datestamp if this is the panic log. This is rare, so there's no +need to optimize getting the datestamp length. We remove one non-alphanumeric +char afterwards if at the start, otherwise one before. */ + +else if (string_datestamp_offset >= 0) + { + uschar * from = buffer + string_datestamp_offset; + uschar * to = from + string_datestamp_length; + + if (from == buffer || from[-1] == '/') + { + if (!isalnum(*to)) to++; + } + else + if (!isalnum(from[-1])) from--; + + /* This copy is ok, because we know that to is a substring of from. But + due to overlap we must use memmove() not Ustrcpy(). */ + memmove(from, to, Ustrlen(to)+1); + } + +/* If the file name is too long, it is an unrecoverable disaster */ + +if (!ok) + die(US"exim: log file path too long: aborting", + US"Logging failure; please try later"); + +/* We now have the file name. Try to open an existing file. After a successful +open, arrange for automatic closure on exec(), and then return. */ + +*fd = Uopen(buffer, +#ifdef O_CLOEXEC + O_CLOEXEC | +#endif + O_APPEND|O_WRONLY, LOG_MODE); + +if (*fd >= 0) + { +#ifndef O_CLOEXEC + (void)fcntl(*fd, F_SETFD, fcntl(*fd, F_GETFD) | FD_CLOEXEC); +#endif + return; + } + +/* Open was not successful: try creating the file. If this is a root process, +we must do the creating in a subprocess set to exim:exim in order to ensure +that the file is created with the right ownership. Otherwise, there can be a +race if another Exim process is trying to write to the log at the same time. +The use of SIGUSR1 by the exiwhat utility can provoke a lot of simultaneous +writing. */ + +euid = geteuid(); + +/* If we are already running as the Exim user (even if that user is root), +we can go ahead and create in the current process. */ + +if (euid == exim_uid) *fd = log_create(buffer); + +/* Otherwise, if we are root, do the creation in an exim:exim subprocess. If we +are neither exim nor root, creation is not attempted. */ + +else if (euid == root_uid) *fd = log_create_as_exim(buffer); + +/* If we now have an open file, set the close-on-exec flag and return. */ + +if (*fd >= 0) + { +#ifndef O_CLOEXEC + (void)fcntl(*fd, F_SETFD, fcntl(*fd, F_GETFD) | FD_CLOEXEC); +#endif + return; + } + +/* Creation failed. There are some circumstances in which we get here when +the effective uid is not root or exim, which is the problem. (For example, a +non-setuid binary with log_arguments set, called in certain ways.) Rather than +just bombing out, force the log to stderr and carry on if stderr is available. +*/ + +if (euid != root_uid && euid != exim_uid && log_stderr != NULL) + { + *fd = fileno(log_stderr); + return; + } + +/* Otherwise this is a disaster. This call is deliberately ONLY to the panic +log. If possible, save a copy of the original line that was being logged. If we +are recursing (can't open the panic log either), the pointer will already be +set. */ + +if (!panic_save_buffer) + if ((panic_save_buffer = US malloc(LOG_BUFFER_SIZE))) + memcpy(panic_save_buffer, log_buffer, LOG_BUFFER_SIZE); + +log_write(0, LOG_PANIC_DIE, "Cannot open %s log file \"%s\": %s: " + "euid=%d egid=%d", log_names[type], buffer, strerror(errno), euid, getegid()); +/* Never returns */ +} + + +static void +unlink_log(int type) +{ +if (type == lt_debug) unlink(CS debuglog_name); +} + + + +/************************************************* +* Add configuration file info to log line * +*************************************************/ + +/* This is put in a function because it's needed twice (once for debugging, +once for real). + +Arguments: + ptr pointer to the end of the line we are building + flags log flags + +Returns: updated pointer +*/ + +static gstring * +log_config_info(gstring * g, int flags) +{ +g = string_cat(g, US"Exim configuration error"); + +if (flags & (LOG_CONFIG_FOR & ~LOG_CONFIG)) + return string_cat(g, US" for "); + +if (flags & (LOG_CONFIG_IN & ~LOG_CONFIG)) + g = string_fmt_append(g, " in line %d of %s", config_lineno, config_filename); + +return string_catn(g, US":\n ", 4); +} + + +/************************************************* +* A write() operation failed * +*************************************************/ + +/* This function is called when write() fails on anything other than the panic +log, which can happen if a disk gets full or a file gets too large or whatever. +We try to save the relevant message in the panic_save buffer before crashing +out. + +The potential invoker should probably not call us for EINTR -1 writes. But +otherwise, short writes are bad as we don't do non-blocking writes to fds +subject to flow control. (If we do, that's new and the logic of this should +be reconsidered). + +Arguments: + name the name of the log being written + length the string length being written + rc the return value from write() + +Returns: does not return +*/ + +static void +log_write_failed(uschar *name, int length, int rc) +{ +int save_errno = errno; + +if (!panic_save_buffer) + if ((panic_save_buffer = US malloc(LOG_BUFFER_SIZE))) + memcpy(panic_save_buffer, log_buffer, LOG_BUFFER_SIZE); + +log_write(0, LOG_PANIC_DIE, "failed to write to %s: length=%d result=%d " + "errno=%d (%s)", name, length, rc, save_errno, + (save_errno == 0)? "write incomplete" : strerror(save_errno)); +/* Never returns */ +} + + + +/************************************************* +* Write to an fd, retrying after signals * +*************************************************/ + +/* Basic write to fd for logs, handling EINTR. + +Arguments: + fd the fd to write to + buf the string to write + length the string length being written + +Returns: + length actually written, persisting an errno from write() +*/ +ssize_t +write_to_fd_buf(int fd, const uschar *buf, size_t length) +{ +ssize_t wrote; +size_t total_written = 0; +const uschar *p = buf; +size_t left = length; + +while (1) + { + wrote = write(fd, p, left); + if (wrote == (ssize_t)-1) + { + if (errno == EINTR) continue; + return wrote; + } + total_written += wrote; + if (wrote == left) + break; + else + { + p += wrote; + left -= wrote; + } + } +return total_written; +} + + + +static void +set_file_path(void) +{ +int sep = ':'; /* Fixed separator - outside use */ +uschar *t; +const uschar *tt = US LOG_FILE_PATH; +while ((t = string_nextinlist(&tt, &sep, log_buffer, LOG_BUFFER_SIZE))) + { + if (Ustrcmp(t, "syslog") == 0 || t[0] == 0) continue; + file_path = string_copy(t); + break; + } +} + + +void +mainlog_close(void) +{ +if (mainlogfd < 0) return; +(void)close(mainlogfd); +mainlogfd = -1; +mainlog_inode = 0; +} + +/************************************************* +* Write message to log file * +*************************************************/ + +/* Exim can be configured to log to local files, or use syslog, or both. This +is controlled by the setting of log_file_path. The following cases are +recognized: + + log_file_path = "" write files in the spool/log directory + log_file_path = "xxx" write files in the xxx directory + log_file_path = "syslog" write to syslog + log_file_path = "syslog : xxx" write to syslog and to files (any order) + +The message always gets '\n' added on the end of it, since more than one +process may be writing to the log at once and we don't want intermingling to +happen in the middle of lines. To be absolutely sure of this we write the data +into a private buffer and then put it out in a single write() call. + +The flags determine which log(s) the message is written to, or for syslogging, +which priority to use, and in the case of the panic log, whether the process +should die afterwards. + +The variable really_exim is TRUE only when exim is running in privileged state +(i.e. not with a changed configuration or with testing options such as -brw). +If it is not, don't try to write to the log because permission will probably be +denied. + +Avoid actually writing to the logs when exim is called with -bv or -bt to +test an address, but take other actions, such as panicking. + +In Exim proper, the buffer for building the message is got at start-up, so that +nothing gets done if it can't be got. However, some functions that are also +used in utilities occasionally obey log_write calls in error situations, and it +is simplest to put a single malloc() here rather than put one in each utility. +Malloc is used directly because the store functions may call log_write(). + +If a message_id exists, we include it after the timestamp. + +Arguments: + selector write to main log or LOG_INFO only if this value is zero, or if + its bit is set in log_selector[0] + flags each bit indicates some independent action: + LOG_SENDER add raw sender to the message + LOG_RECIPIENTS add raw recipients list to message + LOG_CONFIG add "Exim configuration error" + LOG_CONFIG_FOR add " for " instead of ":\n " + LOG_CONFIG_IN add " in line x[ of file y]" + LOG_MAIN write to main log or syslog LOG_INFO + LOG_REJECT write to reject log or syslog LOG_NOTICE + LOG_PANIC write to panic log or syslog LOG_ALERT + LOG_PANIC_DIE write to panic log or LOG_ALERT and then crash + format a printf() format + ... arguments for format + +Returns: nothing +*/ + +void +log_write(unsigned int selector, int flags, const char *format, ...) +{ +int paniclogfd; +ssize_t written_len; +gstring gs = { .size = LOG_BUFFER_SIZE-1, .ptr = 0, .s = log_buffer }; +gstring * g; +va_list ap; + +/* If panic_recurseflag is set, we have failed to open the panic log. This is +the ultimate disaster. First try to write the message to a debug file and/or +stderr and also to syslog. If panic_save_buffer is not NULL, it contains the +original log line that caused the problem. Afterwards, expire. */ + +if (panic_recurseflag) + { + uschar *extra = panic_save_buffer ? panic_save_buffer : US""; + if (debug_file) debug_printf("%s%s", extra, log_buffer); + if (log_stderr && log_stderr != debug_file) + fprintf(log_stderr, "%s%s", extra, log_buffer); + if (*extra) write_syslog(LOG_CRIT, extra); + write_syslog(LOG_CRIT, log_buffer); + die(US"exim: could not open panic log - aborting: see message(s) above", + US"Unexpected log failure, please try later"); + } + +/* Ensure we have a buffer (see comment above); this should never be obeyed +when running Exim proper, only when running utilities. */ + +if (!log_buffer) + if (!(log_buffer = US malloc(LOG_BUFFER_SIZE))) + { + fprintf(stderr, "exim: failed to get store for log buffer\n"); + exim_exit(EXIT_FAILURE, NULL); + } + +/* If we haven't already done so, inspect the setting of log_file_path to +determine whether to log to files and/or to syslog. Bits in logging_mode +control this, and for file logging, the path must end up in file_path. This +variable must be in permanent store because it may be required again later in +the process. */ + +if (!path_inspected) + { + BOOL multiple = FALSE; + int old_pool = store_pool; + + store_pool = POOL_PERM; + + /* If nothing has been set, don't waste effort... the default values for the + statics are file_path="" and logging_mode = LOG_MODE_FILE. */ + + if (*log_file_path) + { + int sep = ':'; /* Fixed separator - outside use */ + uschar *s; + const uschar *ss = log_file_path; + + logging_mode = 0; + while ((s = string_nextinlist(&ss, &sep, log_buffer, LOG_BUFFER_SIZE))) + { + if (Ustrcmp(s, "syslog") == 0) + logging_mode |= LOG_MODE_SYSLOG; + else if (logging_mode & LOG_MODE_FILE) + multiple = TRUE; + else + { + logging_mode |= LOG_MODE_FILE; + + /* If a non-empty path is given, use it */ + + if (*s) + file_path = string_copy(s); + + /* If the path is empty, we want to use the first non-empty, non- + syslog item in LOG_FILE_PATH, if there is one, since the value of + log_file_path may have been set at runtime. If there is no such item, + use the ultimate default in the spool directory. */ + + else + set_file_path(); /* Empty item in log_file_path */ + } /* First non-syslog item in log_file_path */ + } /* Scan of log_file_path */ + } + + /* If no modes have been selected, it is a major disaster */ + + if (logging_mode == 0) + die(US"Neither syslog nor file logging set in log_file_path", + US"Unexpected logging failure"); + + /* Set up the ultimate default if necessary. Then revert to the old store + pool, and record that we've sorted out the path. */ + + if (logging_mode & LOG_MODE_FILE && !file_path[0]) + file_path = string_sprintf("%s/log/%%slog", spool_directory); + store_pool = old_pool; + path_inspected = TRUE; + + /* If more than one file path was given, log a complaint. This recursive call + should work since we have now set up the routing. */ + + if (multiple) + log_write(0, LOG_MAIN|LOG_PANIC, + "More than one path given in log_file_path: using %s", file_path); + } + +/* If debugging, show all log entries, but don't show headers. Do it all +in one go so that it doesn't get split when multi-processing. */ + +DEBUG(D_any|D_v) + { + int i; + + g = string_catn(&gs, US"LOG:", 4); + + /* Show the selector that was passed into the call. */ + + for (i = 0; i < log_options_count; i++) + { + unsigned int bitnum = log_options[i].bit; + if (bitnum < BITWORDSIZE && selector == BIT(bitnum)) + g = string_fmt_append(g, " %s", log_options[i].name); + } + + g = string_fmt_append(g, "%s%s%s%s\n ", + flags & LOG_MAIN ? " MAIN" : "", + flags & LOG_PANIC ? " PANIC" : "", + (flags & LOG_PANIC_DIE) == LOG_PANIC_DIE ? " DIE" : "", + flags & LOG_REJECT ? " REJECT" : ""); + + if (flags & LOG_CONFIG) g = log_config_info(g, flags); + + va_start(ap, format); + i = g->ptr; + if (!string_vformat(g, FALSE, format, ap)) + { + g->ptr = i; + g = string_cat(g, US"**** log string overflowed log buffer ****"); + } + va_end(ap); + + g->size = LOG_BUFFER_SIZE; + g = string_catn(g, US"\n", 1); + debug_printf("%s", string_from_gstring(g)); + + gs.size = LOG_BUFFER_SIZE-1; /* Having used the buffer for debug output, */ + gs.ptr = 0; /* reset it for the real use. */ + gs.s = log_buffer; + } +/* If no log file is specified, we are in a mess. */ + +if (!(flags & (LOG_MAIN|LOG_PANIC|LOG_REJECT))) + log_write(0, LOG_MAIN|LOG_PANIC_DIE, "log_write called with no log " + "flags set"); + +/* There are some weird circumstances in which logging is disabled. */ + +if (f.disable_logging) + { + DEBUG(D_any) debug_printf("log writing disabled\n"); + return; + } + +/* Handle disabled reject log */ + +if (!write_rejectlog) flags &= ~LOG_REJECT; + +/* Create the main message in the log buffer. Do not include the message id +when called by a utility. */ + +g = string_fmt_append(&gs, "%s ", tod_stamp(tod_log)); + +if (LOGGING(pid)) + { + if (!syslog_pid) pid_position[0] = g->ptr; /* remember begin … */ + g = string_fmt_append(g, "[%d] ", (int)getpid()); + if (!syslog_pid) pid_position[1] = g->ptr; /* … and end+1 of the PID */ + } + +if (f.really_exim && message_id[0] != 0) + g = string_fmt_append(g, "%s ", message_id); + +if (flags & LOG_CONFIG) + g = log_config_info(g, flags); + +va_start(ap, format); + { + int i = g->ptr; + if (!string_vformat(g, FALSE, format, ap)) + { + g->ptr = i; + g = string_cat(g, US"**** log string overflowed log buffer ****\n"); + } + } +va_end(ap); + +/* Add the raw, unrewritten, sender to the message if required. This is done +this way because it kind of fits with LOG_RECIPIENTS. */ + +if ( flags & LOG_SENDER + && g->ptr < LOG_BUFFER_SIZE - 10 - Ustrlen(raw_sender)) + g = string_fmt_append(g, " from <%s>", raw_sender); + +/* Add list of recipients to the message if required; the raw list, +before rewriting, was saved in raw_recipients. There may be none, if an ACL +discarded them all. */ + +if ( flags & LOG_RECIPIENTS + && g->ptr < LOG_BUFFER_SIZE - 6 + && raw_recipients_count > 0) + { + int i; + g = string_fmt_append(g, " for"); + for (i = 0; i < raw_recipients_count; i++) + { + uschar * s = raw_recipients[i]; + if (LOG_BUFFER_SIZE - g->ptr < Ustrlen(s) + 3) break; + g = string_fmt_append(g, " %s", s); + } + } + +g = string_catn(g, US"\n", 1); +string_from_gstring(g); + +/* Handle loggable errors when running a utility, or when address testing. +Write to log_stderr unless debugging (when it will already have been written), +or unless there is no log_stderr (expn called from daemon, for example). */ + +if (!f.really_exim || f.log_testing_mode) + { + if ( !debug_selector + && log_stderr + && (selector == 0 || (selector & log_selector[0]) != 0) + ) + if (host_checking) + fprintf(log_stderr, "LOG: %s", CS(log_buffer + 20)); /* no timestamp */ + else + fprintf(log_stderr, "%s", CS log_buffer); + + if ((flags & LOG_PANIC_DIE) == LOG_PANIC_DIE) exim_exit(EXIT_FAILURE, US""); + return; + } + +/* Handle the main log. We know that either syslog or file logging (or both) is +set up. A real file gets left open during reception or delivery once it has +been opened, but we don't want to keep on writing to it for too long after it +has been renamed. Therefore, do a stat() and see if the inode has changed, and +if so, re-open. */ + +if ( flags & LOG_MAIN + && (!selector || selector & log_selector[0])) + { + if ( logging_mode & LOG_MODE_SYSLOG + && (syslog_duplication || !(flags & (LOG_REJECT|LOG_PANIC)))) + write_syslog(LOG_INFO, log_buffer); + + if (logging_mode & LOG_MODE_FILE) + { + struct stat statbuf; + + /* Check for a change to the mainlog file name when datestamping is in + operation. This happens at midnight, at which point we want to roll over + the file. Closing it has the desired effect. */ + + if (mainlog_datestamp) + { + uschar *nowstamp = tod_stamp(string_datestamp_type); + if (Ustrncmp (mainlog_datestamp, nowstamp, Ustrlen(nowstamp)) != 0) + { + (void)close(mainlogfd); /* Close the file */ + mainlogfd = -1; /* Clear the file descriptor */ + mainlog_inode = 0; /* Unset the inode */ + mainlog_datestamp = NULL; /* Clear the datestamp */ + } + } + + /* Otherwise, we want to check whether the file has been renamed by a + cycling script. This could be "if else", but for safety's sake, leave it as + "if" so that renaming the log starts a new file even when datestamping is + happening. */ + + if (mainlogfd >= 0) + if (Ustat(mainlog_name, &statbuf) < 0 || statbuf.st_ino != mainlog_inode) + mainlog_close(); + + /* If the log is closed, open it. Then write the line. */ + + if (mainlogfd < 0) + { + open_log(&mainlogfd, lt_main, NULL); /* No return on error */ + if (fstat(mainlogfd, &statbuf) >= 0) mainlog_inode = statbuf.st_ino; + } + + /* Failing to write to the log is disastrous */ + + written_len = write_to_fd_buf(mainlogfd, g->s, g->ptr); + if (written_len != g->ptr) + { + log_write_failed(US"main log", g->ptr, written_len); + /* That function does not return */ + } + } + } + +/* Handle the log for rejected messages. This can be globally disabled, in +which case the flags are altered above. If there are any header lines (i.e. if +the rejection is happening after the DATA phase), log the recipients and the +headers. */ + +if (flags & LOG_REJECT) + { + header_line *h; + + if (header_list && LOGGING(rejected_header)) + { + uschar * p = g->s + g->ptr; + int i; + + if (recipients_count > 0) + { + /* List the sender */ + + string_format(p, LOG_BUFFER_SIZE - g->ptr, + "Envelope-from: <%s>\n", sender_address); + while (*p) p++; + g->ptr = p - g->s; + + /* List up to 5 recipients */ + + string_format(p, LOG_BUFFER_SIZE - g->ptr, + "Envelope-to: <%s>\n", recipients_list[0].address); + while (*p) p++; + g->ptr = p - g->s; + + for (i = 1; i < recipients_count && i < 5; i++) + { + string_format(p, LOG_BUFFER_SIZE - g->ptr, " <%s>\n", + recipients_list[i].address); + while (*p) p++; + g->ptr = p - g->s; + } + + if (i < recipients_count) + { + string_format(p, LOG_BUFFER_SIZE - g->ptr, + " ...\n"); + while (*p) p++; + g->ptr = p - g->s; + } + } + + /* A header with a NULL text is an unfilled in Received: header */ + + for (h = header_list; h; h = h->next) if (h->text) + { + BOOL fitted = string_format(p, LOG_BUFFER_SIZE - g->ptr, + "%c %s", h->type, h->text); + while (*p) p++; + g->ptr = p - g->s; + if (!fitted) /* Buffer is full; truncate */ + { + g->ptr -= 100; /* For message and separator */ + if (g->s[g->ptr-1] == '\n') g->ptr--; + g = string_cat(g, US"\n*** truncated ***\n"); + break; + } + } + } + + /* Write to syslog or to a log file */ + + if ( logging_mode & LOG_MODE_SYSLOG + && (syslog_duplication || !(flags & LOG_PANIC))) + write_syslog(LOG_NOTICE, string_from_gstring(g)); + + /* Check for a change to the rejectlog file name when datestamping is in + operation. This happens at midnight, at which point we want to roll over + the file. Closing it has the desired effect. */ + + if (logging_mode & LOG_MODE_FILE) + { + struct stat statbuf; + + if (rejectlog_datestamp) + { + uschar *nowstamp = tod_stamp(string_datestamp_type); + if (Ustrncmp (rejectlog_datestamp, nowstamp, Ustrlen(nowstamp)) != 0) + { + (void)close(rejectlogfd); /* Close the file */ + rejectlogfd = -1; /* Clear the file descriptor */ + rejectlog_inode = 0; /* Unset the inode */ + rejectlog_datestamp = NULL; /* Clear the datestamp */ + } + } + + /* Otherwise, we want to check whether the file has been renamed by a + cycling script. This could be "if else", but for safety's sake, leave it as + "if" so that renaming the log starts a new file even when datestamping is + happening. */ + + if (rejectlogfd >= 0) + if (Ustat(rejectlog_name, &statbuf) < 0 || + statbuf.st_ino != rejectlog_inode) + { + (void)close(rejectlogfd); + rejectlogfd = -1; + rejectlog_inode = 0; + } + + /* Open the file if necessary, and write the data */ + + if (rejectlogfd < 0) + { + open_log(&rejectlogfd, lt_reject, NULL); /* No return on error */ + if (fstat(rejectlogfd, &statbuf) >= 0) rejectlog_inode = statbuf.st_ino; + } + + written_len = write_to_fd_buf(rejectlogfd, g->s, g->ptr); + if (written_len != g->ptr) + { + log_write_failed(US"reject log", g->ptr, written_len); + /* That function does not return */ + } + } + } + + +/* Handle the panic log, which is not kept open like the others. If it fails to +open, there will be a recursive call to log_write(). We detect this above and +attempt to write to the system log as a last-ditch try at telling somebody. In +all cases except mua_wrapper, try to write to log_stderr. */ + +if (flags & LOG_PANIC) + { + if (log_stderr && log_stderr != debug_file && !mua_wrapper) + fprintf(log_stderr, "%s", CS string_from_gstring(g)); + + if (logging_mode & LOG_MODE_SYSLOG) + write_syslog(LOG_ALERT, log_buffer); + + /* If this panic logging was caused by a failure to open the main log, + the original log line is in panic_save_buffer. Make an attempt to write it. */ + + if (logging_mode & LOG_MODE_FILE) + { + panic_recurseflag = TRUE; + open_log(&paniclogfd, lt_panic, NULL); /* Won't return on failure */ + panic_recurseflag = FALSE; + + if (panic_save_buffer) + { + int i = write(paniclogfd, panic_save_buffer, Ustrlen(panic_save_buffer)); + i = i; /* compiler quietening */ + } + + written_len = write_to_fd_buf(paniclogfd, g->s, g->ptr); + if (written_len != g->ptr) + { + int save_errno = errno; + write_syslog(LOG_CRIT, log_buffer); + sprintf(CS log_buffer, "write failed on panic log: length=%d result=%d " + "errno=%d (%s)", g->ptr, (int)written_len, save_errno, strerror(save_errno)); + write_syslog(LOG_CRIT, string_from_gstring(g)); + flags |= LOG_PANIC_DIE; + } + + (void)close(paniclogfd); + } + + /* Give up if the DIE flag is set */ + + if ((flags & LOG_PANIC_DIE) != LOG_PANIC) + die(NULL, US"Unexpected failure, please try later"); + } +} + + + +/************************************************* +* Close any open log files * +*************************************************/ + +void +log_close_all(void) +{ +if (mainlogfd >= 0) + { (void)close(mainlogfd); mainlogfd = -1; } +if (rejectlogfd >= 0) + { (void)close(rejectlogfd); rejectlogfd = -1; } +closelog(); +syslog_open = FALSE; +} + + + +/************************************************* +* Multi-bit set or clear * +*************************************************/ + +/* These functions take a list of bit indexes (terminated by -1) and +clear or set the corresponding bits in the selector. + +Arguments: + selector address of the bit string + selsize number of words in the bit string + bits list of bits to set +*/ + +void +bits_clear(unsigned int *selector, size_t selsize, int *bits) +{ +for(; *bits != -1; ++bits) + BIT_CLEAR(selector, selsize, *bits); +} + +void +bits_set(unsigned int *selector, size_t selsize, int *bits) +{ +for(; *bits != -1; ++bits) + BIT_SET(selector, selsize, *bits); +} + + + +/************************************************* +* Decode bit settings for log/debug * +*************************************************/ + +/* This function decodes a string containing bit settings in the form of +name +and/or -name sequences, and sets/unsets bits in a bit string accordingly. It +also recognizes a numeric setting of the form =<number>, but this is not +intended for user use. It's an easy way for Exim to pass the debug settings +when it is re-exec'ed. + +The option table is a list of names and bit indexes. The index -1 +means "set all bits, except for those listed in notall". The notall +list is terminated by -1. + +The action taken for bad values varies depending upon why we're here. +For log messages, or if the debugging is triggered from config, then we write +to the log on the way out. For debug setting triggered from the command-line, +we treat it as an unknown option: error message to stderr and die. + +Arguments: + selector address of the bit string + selsize number of words in the bit string + notall list of bits to exclude from "all" + string the configured string + options the table of option names + count size of table + which "log" or "debug" + flags DEBUG_FROM_CONFIG + +Returns: nothing on success - bomb out on failure +*/ + +void +decode_bits(unsigned int *selector, size_t selsize, int *notall, + uschar *string, bit_table *options, int count, uschar *which, int flags) +{ +uschar *errmsg; +if (string == NULL) return; + +if (*string == '=') + { + char *end; /* Not uschar */ + memset(selector, 0, sizeof(*selector)*selsize); + *selector = strtoul(CS string+1, &end, 0); + if (*end == 0) return; + errmsg = string_sprintf("malformed numeric %s_selector setting: %s", which, + string); + goto ERROR_RETURN; + } + +/* Handle symbolic setting */ + +else for(;;) + { + BOOL adding; + uschar *s; + int len; + bit_table *start, *end; + + while (isspace(*string)) string++; + if (*string == 0) return; + + if (*string != '+' && *string != '-') + { + errmsg = string_sprintf("malformed %s_selector setting: " + "+ or - expected but found \"%s\"", which, string); + goto ERROR_RETURN; + } + + adding = *string++ == '+'; + s = string; + while (isalnum(*string) || *string == '_') string++; + len = string - s; + + start = options; + end = options + count; + + while (start < end) + { + bit_table *middle = start + (end - start)/2; + int c = Ustrncmp(s, middle->name, len); + if (c == 0) + { + if (middle->name[len] != 0) c = -1; else + { + unsigned int bit = middle->bit; + + if (bit == -1) + { + if (adding) + { + memset(selector, -1, sizeof(*selector)*selsize); + bits_clear(selector, selsize, notall); + } + else + memset(selector, 0, sizeof(*selector)*selsize); + } + else if (adding) + BIT_SET(selector, selsize, bit); + else + BIT_CLEAR(selector, selsize, bit); + + break; /* Out of loop to match selector name */ + } + } + if (c < 0) end = middle; else start = middle + 1; + } /* Loop to match selector name */ + + if (start >= end) + { + errmsg = string_sprintf("unknown %s_selector setting: %c%.*s", which, + adding? '+' : '-', len, s); + goto ERROR_RETURN; + } + } /* Loop for selector names */ + +/* Handle disasters */ + +ERROR_RETURN: +if (Ustrcmp(which, "debug") == 0) + { + if (flags & DEBUG_FROM_CONFIG) + { + log_write(0, LOG_CONFIG|LOG_PANIC, "%s", errmsg); + return; + } + fprintf(stderr, "exim: %s\n", errmsg); + exit(EXIT_FAILURE); + } +else log_write(0, LOG_CONFIG|LOG_PANIC_DIE, "%s", errmsg); +} + + + +/************************************************* +* Activate a debug logfile (late) * +*************************************************/ + +/* Normally, debugging is activated from the command-line; it may be useful +within the configuration to activate debugging later, based on certain +conditions. If debugging is already in progress, we return early, no action +taken (besides debug-logging that we wanted debug-logging). + +Failures in options are not fatal but will result in paniclog entries for the +misconfiguration. + +The first use of this is in ACL logic, "control = debug/tag=foo/opts=+expand" +which can be combined with conditions, etc, to activate extra logging only +for certain sources. The second use is inetd wait mode debug preservation. */ + +void +debug_logging_activate(uschar *tag_name, uschar *opts) +{ +int fd = -1; + +if (debug_file) + { + debug_printf("DEBUGGING ACTIVATED FROM WITHIN CONFIG.\n" + "DEBUG: Tag=\"%s\" opts=\"%s\"\n", tag_name, opts ? opts : US""); + return; + } + +if (tag_name != NULL && (Ustrchr(tag_name, '/') != NULL)) + { + log_write(0, LOG_MAIN|LOG_PANIC, "debug tag may not contain a '/' in: %s", + tag_name); + return; + } + +debug_selector = D_default; +if (opts) + decode_bits(&debug_selector, 1, debug_notall, opts, + debug_options, debug_options_count, US"debug", DEBUG_FROM_CONFIG); + +/* When activating from a transport process we may never have logged at all +resulting in certain setup not having been done. Hack this for now so we +do not segfault; note that nondefault log locations will not work */ + +if (!*file_path) set_file_path(); + +open_log(&fd, lt_debug, tag_name); + +if (fd != -1) + debug_file = fdopen(fd, "w"); +else + log_write(0, LOG_MAIN|LOG_PANIC, "unable to open debug log"); +} + + +void +debug_logging_stop(void) +{ +if (!debug_file || !debuglog_name[0]) return; + +debug_selector = 0; +fclose(debug_file); +debug_file = NULL; +unlink_log(lt_debug); +} + + +/* End of log.c */ |