/* sync_log.c -- Cyrus synchronization logging functions * * Copyright (c) 1994-2008 Carnegie Mellon University. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * 3. The name "Carnegie Mellon University" must not be used to * endorse or promote products derived from this software without * prior written permission. For permission or any legal * details, please contact * Carnegie Mellon University * Center for Technology Transfer and Enterprise Creation * 4615 Forbes Avenue * Suite 302 * Pittsburgh, PA 15213 * (412) 268-7393, fax: (412) 268-7395 * innovation@andrew.cmu.edu * * 4. Redistributions of any form whatsoever must retain the following * acknowledgment: * "This product includes software developed by Computing Services * at Carnegie Mellon University (http://www.cmu.edu/computing/)." * * CARNEGIE MELLON UNIVERSITY DISCLAIMS ALL WARRANTIES WITH REGARD TO * THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY * AND FITNESS, IN NO EVENT SHALL CARNEGIE MELLON UNIVERSITY BE LIABLE * FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * * Original version written by David Carter * Rewritten and integrated into Cyrus by Ken Murchison */ /* YYY Need better quoting for obscure filenames: use literals? */ #include #ifdef HAVE_UNISTD_H #include #endif #include #include #include #include #include #include #include #include "assert.h" #include "command.h" #include "sync_log.h" #include "global.h" #include "cyr_lock.h" #include "mailbox.h" #include "retry.h" #include "util.h" #include "xmalloc.h" #include "xstrlcpy.h" /* generated headers are not necessarily in current directory */ #include "imap/imap_err.h" static int sync_log_suppressed = 0; static strarray_t *channels = NULL; static strarray_t *unsuppressable = NULL; static struct buf *rightnow_log = NULL; static int sync_log_initialized = 0; static void done_cb(void *rock __attribute__((unused))) { sync_log_done(); } static void init_internal() { if (!sync_log_initialized) { sync_log_init(); cyrus_modules_add(done_cb, NULL); } } EXPORTED void sync_log_init(void) { const char *conf; int i; /* sync_log_init() may be called more than once */ if (channels) strarray_free(channels); conf = config_getstring(IMAPOPT_SYNC_LOG_CHANNELS); if (!conf) conf = "\"\""; channels = strarray_split(conf, " ", 0); /* * The sysadmin can specify "" in the value of sync_log_channels to * mean the default channel name - this will be useful for sysadmins * who want to start using a sync log channel for squatter but who * have been using the default sync log channel for sync_client. */ i = strarray_find(channels, "\"\"", 0); if (i >= 0) strarray_set(channels, i, NULL); strarray_free(unsuppressable); unsuppressable = NULL; conf = config_getstring(IMAPOPT_SYNC_LOG_UNSUPPRESSABLE_CHANNELS); if (conf) { unsuppressable = strarray_split(conf, " ", 0); i = strarray_find(unsuppressable, "\"\"", 0); if (i >= 0) strarray_set(unsuppressable, i, NULL); } conf = config_getstring(IMAPOPT_SYNC_RIGHTNOW_CHANNEL); if (conf) { rightnow_log = buf_new(); } sync_log_initialized = 1; } EXPORTED void sync_log_suppress(void) { sync_log_suppressed = 1; } EXPORTED void sync_log_done(void) { sync_log_reset(); if (rightnow_log) { buf_free(rightnow_log); rightnow_log = NULL; } strarray_free(channels); channels = NULL; strarray_free(unsuppressable); unsuppressable = NULL; sync_log_initialized = 0; } static char *sync_log_fname(const char *channel) { static char buf[MAX_MAILBOX_PATH]; if (channel) snprintf(buf, MAX_MAILBOX_PATH, "%s/sync/%s/log", config_dir, channel); else snprintf(buf, MAX_MAILBOX_PATH, "%s/sync/log", config_dir); return buf; } static int sync_log_enabled(const char *channel) { if (!config_getswitch(IMAPOPT_SYNC_LOG)) return 0; /* entire mechanism is disabled */ if (!sync_log_suppressed) return 1; /* _suppress() wasn't called */ if (unsuppressable && strarray_find(unsuppressable, channel, 0) >= 0) return 1; /* channel is unsuppressable */ return 0; /* suppressed */ } static void sync_log_base(const char *channel, const char *string) { int fd; struct stat sbuffile, sbuffd; int retries = 0; const char *fname; fname = sync_log_fname(channel); while (retries++ < SYNC_LOG_RETRIES) { fd = open(fname, O_WRONLY|O_APPEND|O_CREAT, 0640); if (fd < 0 && errno == ENOENT) { if (!cyrus_mkdir(fname, 0755)) { fd = open(fname, O_WRONLY|O_APPEND|O_CREAT, 0640); } } if (fd < 0) { syslog(LOG_ERR, "sync_log(): Unable to write to log file %s: %s", fname, strerror(errno)); return; } if (lock_blocking(fd, fname) == -1) { syslog(LOG_ERR, "sync_log(): Failed to lock %s for %s: %m", fname, string); xclose(fd); return; } /* Check that the file wasn't renamed after it was opened above */ if ((fstat(fd, &sbuffd) == 0) && (stat(fname, &sbuffile) == 0) && (sbuffd.st_ino == sbuffile.st_ino)) break; lock_unlock(fd, fname); xclose(fd); } if (retries >= SYNC_LOG_RETRIES) { xclose(fd); syslog(LOG_ERR, "sync_log(): Failed to lock %s for %s after %d attempts", fname, string, retries); return; } if (retry_write(fd, string, strlen(string)) < 0) syslog(LOG_ERR, "write() to %s failed: %s", fname, strerror(errno)); (void)fsync(fd); /* paranoia */ lock_unlock(fd, fname); xclose(fd); } EXPORTED struct buf *sync_log_rightnow_buf() { if (!channels) return NULL; if (!rightnow_log) return NULL; if (!buf_len(rightnow_log)) return NULL; return rightnow_log; } EXPORTED void sync_log_reset() { if (!channels) return; if (!rightnow_log) return; if (!buf_len(rightnow_log)) return; syslog(LOG_NOTICE, "SYNCNOTICE: rightnow log leaked %s", buf_cstring(rightnow_log)); buf_reset(rightnow_log); } static const char *sync_quote_name(const char *name) { static char buf[MAX_MAILBOX_BUFFER+3]; /* "x2 plus \0 */ char c; int src; int dst = 0; int need_quote = 0; /* initial quote */ buf[dst++] = '"'; /* degenerate case - no name is the empty string, quote it */ if (!name || !*name) { need_quote = 1; goto end; } for (src = 0; name[src]; src++) { c = name[src]; if ((c == '\r') || (c == '\n')) fatal("Illegal line break in folder name", EX_IOERR); /* quoteable characters */ if ((c == '\\') || (c == '\"') || (c == '{') || (c == '}')) { need_quote = 1; buf[dst++] = '\\'; } /* non-atom characters */ else if ((c == ' ') || (c == '\t') || (c == '(') || (c == ')')) { need_quote = 1; } buf[dst++] = c; if (dst > MAX_MAILBOX_BUFFER) fatal("word too long", EX_IOERR); } end: if (need_quote) { buf[dst++] = '\"'; buf[dst] = '\0'; return buf; } else { buf[dst] = '\0'; return buf + 1; /* skip initial quote */ } } #define BUFSIZE 4096 static char *va_format(const char *fmt, va_list ap) { static char buf[BUFSIZE+1]; size_t len; int ival; const char *sval; const char *p; for (len = 0, p = fmt; *p && len < BUFSIZE; p++) { if (*p != '%') { buf[len++] = *p; continue; } switch (*++p) { case 'd': ival = va_arg(ap, int); len += snprintf(buf+len, BUFSIZE-len, "%d", ival); break; case 's': sval = va_arg(ap, const char *); sval = sync_quote_name(sval); strlcpy(buf+len, sval, BUFSIZE-len); len += strlen(sval); break; default: buf[len++] = *p; break; } } if (buf[len-1] != '\n') buf[len++] = '\n'; buf[len] = '\0'; return buf; } EXPORTED void sync_log(const char *fmt, ...) { va_list ap; const char *val; int i; init_internal(); if (!channels) return; va_start(ap, fmt); val = va_format(fmt, ap); va_end(ap); if (rightnow_log) buf_appendcstr(rightnow_log, val); for (i = 0 ; i < channels->count ; i++) { const char *channel = channels->data[i]; if (sync_log_enabled(channel)) sync_log_base(channel, val); } } EXPORTED void sync_log_channel(const char *channel, const char *fmt, ...) { va_list ap; const char *val; init_internal(); va_start(ap, fmt); val = va_format(fmt, ap); va_end(ap); sync_log_base(channel, val); } /* * Read-side sync log code */ struct sync_log_reader { /* * This object works in four modes: * * - initialised with a sync log channel * - standard mode used by sync_client * - slr->log_file != NULL * - slr->work_file is the name of a rename()d * file that needs to be unlink()ed. * - slr->content_buf is empty * * - initialised with a saved file name * - used by the sync_client -f option * - slr->log_file = NULL * - slr->work_file is the file given us by the user * which it's important that we do not unlink() * - slr->content_buf is empty * * - initialised with a file descriptor * - slr->log_file = NULL * - slr->work_file = NULL * - slr->fd is a file descriptor, probably stdin, * and possibly a pipe * - slr->content_buf is empty * - we cannot unlink() anything even if we wanted to. * * - initialised with the content of a file * - slr->log_file = NULL * - slr->work_file = NULL * - slr->content_buf has a length * - we cannot unlink() anything even if we wanted to. */ char *log_file; char *work_file; int fd; int fd_is_ours; struct protstream *input; struct buf type; struct buf arg1; struct buf arg2; struct buf contentbuf; }; static sync_log_reader_t *sync_log_reader_alloc(void) { sync_log_reader_t *slr = xzmalloc(sizeof(sync_log_reader_t)); slr->fd = -1; return slr; } /* * Create a sync log reader object which will read from the given sync log * channel 'channel'. The channel may be NULL for the default channel. * Returns a new object which must be freed with sync_log_reader_free(). * Does not return NULL. */ EXPORTED sync_log_reader_t *sync_log_reader_create_with_channel(const char *channel) { sync_log_reader_t *slr = sync_log_reader_alloc(); struct buf buf = BUF_INITIALIZER; slr->log_file = xstrdup(sync_log_fname(channel)); /* Create a work log filename. We will process this * first if it exists */ buf_printf(&buf, "%s-run", slr->log_file); slr->work_file = buf_release(&buf); return slr; } /* * Create a sync log reader object which will read from the given file * 'filename'. Returns a new object which must be freed with * sync_log_reader_free(). Does not return NULL. */ EXPORTED sync_log_reader_t *sync_log_reader_create_with_filename(const char *filename) { sync_log_reader_t *slr = sync_log_reader_alloc(); slr->work_file = xstrdup(filename); /* slr->log_file remain NULL, which matters later */ return slr; } EXPORTED sync_log_reader_t *sync_log_reader_create_with_content(const char *content) { sync_log_reader_t *slr = sync_log_reader_alloc(); buf_init_ro_cstr(&slr->contentbuf, content); return slr; } /* * Create a sync log reader object which will read from the given file * descriptor 'fd'. The file descriptor must be open for reading and * is not closed. Returns a new object which must be freed with * sync_log_reader_free(). Does not return NULL. */ EXPORTED sync_log_reader_t *sync_log_reader_create_with_fd(int fd) { sync_log_reader_t *slr = sync_log_reader_alloc(); slr->fd = fd; slr->fd_is_ours = 0; /* slr->log_file remain NULL, which matters later */ return slr; } /* * Free a sync log reader object. */ EXPORTED void sync_log_reader_free(sync_log_reader_t *slr) { if (!slr) return; if (slr->input) prot_free(slr->input); if (slr->fd_is_ours && slr->fd >= 0) close(slr->fd); free(slr->log_file); free(slr->work_file); buf_free(&slr->type); buf_free(&slr->arg1); buf_free(&slr->arg2); buf_free(&slr->contentbuf); free(slr); } /* * Begin reading a sync log file. If the reader is reading from a * channel, rename the current log file so it will not be appended to by * the write side code, and open the file. Otherwise, just open the file * (note this is still necessary even when the reader is reading from a * file descriptor). * * When sync_log_reader_begin() returns success, you should loop calling * sync_log_reader_getitem() and handling the items, until it returns * EOF, and then call sync_log_reader_end(). * * Returns zero on success, IMAP_AGAIN if reading from a channel and * there is no current log file, or an IMAP error code on failure. */ EXPORTED int sync_log_reader_begin(sync_log_reader_t *slr) { struct stat sbuf; int r; if (slr->input) { r = sync_log_reader_end(slr); if (r) return r; } if (buf_len(&slr->contentbuf)) { slr->input = prot_readmap(buf_base(&slr->contentbuf), buf_len(&slr->contentbuf)); return 0; } if (stat(slr->work_file, &sbuf) == 0) { /* Existing work log file - process this first */ syslog(LOG_NOTICE, "Reprocessing sync log file %s", slr->work_file); } else if (!slr->log_file) { syslog(LOG_ERR, "No sync log filename"); return IMAP_IOERROR; } else { /* Check for sync_log file */ if (stat(slr->log_file, &sbuf) < 0) { if (errno == ENOENT) return IMAP_AGAIN; /* no problem, try again later */ syslog(LOG_ERR, "Failed to stat %s: %m", slr->log_file); return IMAP_IOERROR; } /* Move sync_log to our work file */ if (rename(slr->log_file, slr->work_file) < 0) { syslog(LOG_ERR, "Rename %s -> %s failed: %m", slr->log_file, slr->work_file); return IMAP_IOERROR; } } if (slr->fd < 0) { int fd = open(slr->work_file, O_RDWR, 0); if (fd < 0) { syslog(LOG_ERR, "Failed to open %s: %m", slr->work_file); return IMAP_IOERROR; } if (lock_blocking(fd, slr->work_file) < 0) { syslog(LOG_ERR, "Failed to lock %s: %m", slr->work_file); close(fd); return IMAP_IOERROR; } slr->fd = fd; slr->fd_is_ours = 1; /* we can unlock immediately, since we have serialised * any process which held the lock over the rename. All * future attempts to lock this inode will stat and notice * the rename, so they won't write any more */ lock_unlock(slr->fd, slr->work_file); } slr->input = prot_new(slr->fd, /*write*/0); return 0; } EXPORTED const char *sync_log_reader_get_file_name(const sync_log_reader_t *slr) { return slr->work_file; } /* * Finish reading a sync log file. Closes the file (and, if the reader * is reading from a channel, unlinks the work file and prepares for the * next file). Returns 0 on success or an IMAP error code on failure. */ EXPORTED int sync_log_reader_end(sync_log_reader_t *slr) { if (!slr->input) return 0; if (slr->input) { prot_free(slr->input); slr->input = NULL; } if (slr->fd_is_ours && slr->fd >= 0) { lock_unlock(slr->fd, slr->work_file); close(slr->fd); slr->fd = -1; } if (slr->log_file) { /* We were initialised with a sync log channel, whose * log file we rename()d to the work file. Now that * we've done with the work file we can unlink it. * Further checks at this point are just paranoia. */ if (slr->work_file && unlink(slr->work_file) < 0) { syslog(LOG_ERR, "Unlink %s failed: %m", slr->work_file); return IMAP_IOERROR; } } return 0; } /* * Read a single log item from a sync log file. The item will be * returned as three constant strings. The first string is the type of * the item (e.g. "MAILBOX") and is always capitalised. The second and * third strings are arguments. * * Returns 0 on success, EOF when the end of the file is reached, or an * IMAP error code on failure. */ EXPORTED int sync_log_reader_getitem(sync_log_reader_t *slr, const char *args[3]) { int c; const char *arg1s = NULL; const char *arg2s = NULL; if (!slr->input) return EOF; for (;;) { if ((c = getword(slr->input, &slr->type)) == EOF) return EOF; /* Ignore blank lines */ if (c == '\r') c = prot_getc(slr->input); if (c == '\n') continue; if (c != ' ') { syslog(LOG_ERR, "Invalid input"); eatline(slr->input, c); continue; } if ((c = getastring(slr->input, 0, &slr->arg1)) == EOF) return EOF; arg1s = slr->arg1.s; arg2s = NULL; if (c == ' ') { if ((c = getastring(slr->input, 0, &slr->arg2)) == EOF) return EOF; arg2s = slr->arg2.s; } if (c == '\r') c = prot_getc(slr->input); if (c != '\n') { syslog(LOG_ERR, "Garbage at end of input line"); eatline(slr->input, c); continue; } break; } ucase(slr->type.s); args[0] = slr->type.s; args[1] = arg1s; args[2] = arg2s; return 0; }