SSLproxy/log.c
2014-11-14 15:31:30 +01:00

682 lines
15 KiB
C

/*
* SSLsplit - transparent and scalable SSL/TLS interception
* Copyright (c) 2009-2014, Daniel Roethlisberger <daniel@roe.ch>
* All rights reserved.
* http://www.roe.ch/SSLsplit
*
* 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 unmodified, 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.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
* IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "log.h"
#include "logger.h"
#include "sys.h"
#include "attrib.h"
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdarg.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <syslog.h>
#include <assert.h>
#include <sys/stat.h>
/*
* Centralized logging code multiplexing thread access to the logger based
* logging in separate threads. Some log types are switchable to different
* backends, such as syslog and stderr.
*/
/*
* Error log.
* Switchable between stderr and syslog.
* Uses logger thread.
*/
static logger_t *err_log = NULL;
static int err_started = 0; /* while 0, shortcut the thrqueue */
static int err_mode = LOG_ERR_MODE_STDERR;
static ssize_t
log_err_writecb(UNUSED int fd, const void *buf, size_t sz)
{
switch (err_mode) {
case LOG_ERR_MODE_STDERR:
return fwrite(buf, sz - 1, 1, stderr);
case LOG_ERR_MODE_SYSLOG:
syslog(LOG_ERR, "%s", (const char *)buf);
return 0;
}
return -1;
}
int
log_err_printf(const char *fmt, ...)
{
va_list ap;
char *buf;
int rv;
va_start(ap, fmt);
rv = vasprintf(&buf, fmt, ap);
va_end(ap);
if (rv == -1)
return -1;
if (err_started) {
return logger_write_freebuf(err_log, 0, buf, strlen(buf) + 1);
} else {
log_err_writecb(0, (unsigned char*)buf, strlen(buf) + 1);
free(buf);
}
return 0;
}
void
log_err_mode(int mode)
{
err_mode = mode;
}
/*
* Debug log. Redirects logging to error log.
* Switchable between error log or no logging.
* Uses the error log logger thread.
*/
static int dbg_mode = LOG_DBG_MODE_NONE;
int
log_dbg_write_free(void *buf, size_t sz)
{
if (dbg_mode == LOG_DBG_MODE_NONE)
return 0;
if (err_started) {
return logger_write_freebuf(err_log, 0, buf, sz);
} else {
log_err_writecb(0, buf, sz);
free(buf);
}
return 0;
}
int
log_dbg_print_free(char *s)
{
return log_dbg_write_free(s, strlen(s) + 1);
}
int
log_dbg_printf(const char *fmt, ...)
{
va_list ap;
char *buf;
int rv;
if (dbg_mode == LOG_DBG_MODE_NONE)
return 0;
va_start(ap, fmt);
rv = vasprintf(&buf, fmt, ap);
va_end(ap);
if (rv == -1)
return -1;
return log_dbg_print_free(buf);
}
void
log_dbg_mode(int mode)
{
dbg_mode = mode;
}
/*
* Connection log. Logs a one-liner to a file-based connection log.
* Uses a logger thread.
*/
logger_t *connect_log = NULL;
static int connect_fd = -1;
static int
log_connect_open(const char *logfile)
{
connect_fd = open(logfile, O_WRONLY|O_APPEND|O_CREAT, 0660);
if (connect_fd == -1) {
log_err_printf("Failed to open '%s' for writing: %s\n",
logfile, strerror(errno));
return -1;
}
return 0;
}
/*
* Do the actual write to the open connection log file descriptor.
* We prepend a timestamp here, which means that timestamps are slightly
* delayed from the time of actual logging. Since we only have second
* resolution that should not make any difference.
*/
static ssize_t
log_connect_writecb(UNUSED int fd, const void *buf, size_t sz)
{
char timebuf[32];
time_t epoch;
struct tm *utc;
size_t n;
time(&epoch);
utc = gmtime(&epoch);
n = strftime(timebuf, sizeof(timebuf), "%Y-%m-%d %H:%M:%S UTC ", utc);
if (n == 0) {
log_err_printf("Error from strftime(): buffer too small\n");
return -1;
}
if ((write(connect_fd, timebuf, n) == -1) ||
(write(connect_fd, buf, sz) == -1)) {
log_err_printf("Warning: Failed to write to connect log: %s\n",
strerror(errno));
}
return 0;
}
static void
log_connect_close(void)
{
close(connect_fd);
}
/*
* Content log.
* Logs connection content to either a single file or a directory containing
* per-connection logs.
* Uses a logger thread.
*/
logger_t *content_log = NULL;
static int content_fd = -1; /* if set, we are in single file mode */
static const char *content_basedir = NULL;
static const char *content_logspec = NULL;
static int
log_content_open_singlefile(const char *logfile)
{
content_fd = open(logfile, O_WRONLY|O_APPEND|O_CREAT, 0660);
if (content_fd == -1) {
log_err_printf("Failed to open '%s' for writing: %s\n",
logfile, strerror(errno));
return -1;
}
return 0;
}
static int
log_content_open_logdir(const char *basedir)
{
content_basedir = basedir;
return 0;
}
static int
log_content_open_logspec(const char *logspec)
{
content_logspec = logspec;
return 0;
}
static void
log_content_close_singlefile(void)
{
if (content_fd != -1) {
close(content_fd);
content_fd = -1;
}
}
/*
* Generate a log path based on the given log spec.
* Returns an allocated buffer which must be freed by caller, or NULL on error.
*/
#define PATH_BUF_INC 1024
static char *
log_content_format_pathspec(const char *logspec, char *srcaddr, char *dstaddr,
char *exec_path, char *user, char *group)
WUNRES MALLOC NONNULL(1,2,3);
static char *
log_content_format_pathspec(const char *logspec, char *srcaddr, char *dstaddr,
char *exec_path, char *user, char *group)
{
/* set up buffer to hold our generated file path */
size_t path_buflen = PATH_BUF_INC;
char *path_buf = malloc(path_buflen);
if (path_buf == NULL) {
log_err_printf("failed to allocate path buffer\n");
return NULL;
}
/* initialize the buffer as an empty C string */
path_buf[0] = '\0';
/* iterate over format specifiers */
size_t path_len = 0;
for (const char *p = logspec; *p != '\0'; p++) {
const char *elem = NULL;
size_t elem_len = 0;
const char iso8601[] = "%Y%m%dT%H%M%SZ";
char timebuf[24]; /* sized for ISO 8601 format */
/* parse the format string and generate the next path element */
switch (*p) {
case '%':
p++;
/* handle format specifiers. */
switch (*p) {
case '\0':
/* unexpected eof; backtrack and discard
* invalid format spec */
p--;
elem_len = 0;
break;
case '%':
elem = p;
elem_len = 1;
break;
case 'd':
elem = dstaddr;
elem_len = strlen(dstaddr);
break;
case 's':
elem = srcaddr;
elem_len = strlen(srcaddr);
break;
case 'x':
if (exec_path) {
char *match = exec_path;
while ((match = strchr(match, '/')) != NULL) {
match++;
elem = match;
}
elem_len = elem ? strlen(elem) : 0;
} else {
elem_len = 0;
}
break;
case 'X':
elem = exec_path;
elem_len = exec_path ? strlen(exec_path) : 0;
break;
case 'u':
elem = user;
elem_len = user ? strlen(user) : 0;
break;
case 'g':
elem = group;
elem_len = group ? strlen(group) : 0;
break;
case 'T': {
time_t epoch;
struct tm *utc;
time(&epoch);
utc = gmtime(&epoch);
strftime(timebuf, sizeof(timebuf), iso8601, utc);
elem = timebuf;
elem_len = sizeof(timebuf);
break;
}}
break;
default:
elem = p;
elem_len = 1;
break;
}
if (elem_len > 0) {
/* growing the buffer to fit elem_len + terminating \0 */
if (path_buflen - path_len < elem_len + 1) {
/* Grow in PATH_BUF_INC chunks.
* Note that the use of `PATH_BUF_INC' provides
* our guaranteed space for a trailing '\0' */
path_buflen += elem_len + PATH_BUF_INC;
char *newbuf = realloc(path_buf, path_buflen);
if (newbuf == NULL) {
log_err_printf("failed to reallocate"
" path buffer\n");
free(path_buf);
return NULL;
}
path_buf = newbuf;
}
strncat(path_buf, elem, elem_len);
path_len += elem_len;
}
}
/* apply terminating NUL */
assert(path_buflen > path_len);
path_buf[path_len] = '\0';
return path_buf;
}
#undef PATH_BUF_INC
void
log_content_open(log_content_ctx_t *ctx, char *srcaddr, char *dstaddr,
char *exec_path, char *user, char *group)
{
if (ctx->open)
return;
if (content_fd != -1) {
/* single-file content log (-L) */
ctx->fd = content_fd;
asprintf(&ctx->header_in, "%s -> %s", srcaddr, dstaddr);
asprintf(&ctx->header_out, "%s -> %s", dstaddr, srcaddr);
} else if (content_logspec) {
/* per-connection-file content log with logspec (-F) */
char *filename;
filename = log_content_format_pathspec(content_logspec,
srcaddr, dstaddr,
exec_path, user, group);
if (!filename) {
ctx->fd = -1;
return;
}
/* statefully create parent directories by iteratively rewriting
* the path at each directory seperator */
char parent[strlen(filename)+1];
char *p;
memcpy(parent, filename, sizeof(parent));
/* skip leading '/' characters */
p = parent;
while (*p == '/') p++;
while ((p = strchr(p, '/')) != NULL) {
/* overwrite '/' to terminate the string at the next
* parent directory */
*p = '\0';
struct stat sbuf;
if (stat(parent, &sbuf) != 0) {
if (mkdir(parent, 0755) != 0) {
log_err_printf("Could not create "
"directory '%s': %s\n",
parent, strerror(errno));
ctx->fd = -1;
free(filename);
return;
}
} else if (!S_ISDIR(sbuf.st_mode)) {
log_err_printf("Failed to open '%s': "
"%s is not a directory\n",
filename, parent);
ctx->fd = -1;
free(filename);
return;
}
/* replace the overwritten slash */
*p = '/';
p++;
/* skip leading '/' characters */
while (*p == '/') p++;
}
ctx->fd = open(filename, O_WRONLY|O_APPEND|O_CREAT, 0660);
if (ctx->fd == -1) {
log_err_printf("Failed to open '%s': %s\n",
filename, strerror(errno));
}
free(filename);
} else {
/* per-connection-file content log (-S) */
char filename[1024];
char timebuf[24];
time_t epoch;
struct tm *utc;
time(&epoch);
utc = gmtime(&epoch);
strftime(timebuf, sizeof(timebuf), "%Y%m%dT%H%M%SZ", utc);
snprintf(filename, sizeof(filename), "%s/%s-%s-%s.log",
content_basedir, timebuf, srcaddr, dstaddr);
ctx->fd = open(filename, O_WRONLY|O_APPEND|O_CREAT, 0660);
if (ctx->fd == -1) {
log_err_printf("Failed to open '%s': %s\n",
filename, strerror(errno));
}
}
ctx->open = 1;
}
void
log_content_submit(log_content_ctx_t *ctx, logbuf_t *lb, int direction)
{
logbuf_t *head;
time_t epoch;
struct tm *utc;
char *header;
if (!ctx->open) {
log_err_printf("log_content_submit called on closed ctx\n");
return;
}
if (!(header = direction ? ctx->header_out : ctx->header_in))
goto out;
/* prepend size tag and newline */
head = logbuf_new_printf(lb->fd, lb, " (%zu):\n", logbuf_size(lb));
if (!head) {
log_err_printf("Failed to allocate memory\n");
logbuf_free(lb);
return;
}
lb = head;
/* prepend header */
head = logbuf_new_copy(header, strlen(header), lb->fd, lb);
if (!head) {
log_err_printf("Failed to allocate memory\n");
logbuf_free(lb);
return;
}
lb = head;
/* prepend timestamp */
head = logbuf_new_alloc(32, lb->fd, lb);
if (!head) {
log_err_printf("Failed to allocate memory\n");
logbuf_free(lb);
return;
}
lb = head;
time(&epoch);
utc = gmtime(&epoch);
lb->sz = strftime((char*)lb->buf, lb->sz, "%Y-%m-%d %H:%M:%S UTC ",
utc);
out:
lb->fd = ctx->fd;
logger_submit(content_log, lb);
}
void
log_content_close(log_content_ctx_t *ctx)
{
if (!ctx->open)
return;
if (content_fd == -1) {
logger_write_freebuf(content_log, ctx->fd, NULL, 0);
}
if (ctx->header_in) {
free(ctx->header_in);
}
if (ctx->header_out) {
free(ctx->header_out);
}
ctx->open = 0;
}
/*
* Do the actual write to the open connection log file descriptor.
* We prepend a timestamp here, which means that timestamps are slightly
* delayed from the time of actual logging. Since we only have second
* resolution that should not make any difference.
*/
static ssize_t
log_content_writecb(int fd, const void *buf, size_t sz)
{
if (!buf) {
close(fd);
return 0;
}
if (write(fd, buf, sz) == -1) {
log_err_printf("Warning: Failed to write to content log: %s\n",
strerror(errno));
return -1;
}
return 0;
}
/*
* Initialization and destruction.
*/
/*
* Log pre-init: open all log files but don't start any threads, since we may
* fork() after pre-initialization.
* Return -1 on errors, 0 otherwise.
*/
int
log_preinit(opts_t *opts)
{
if (opts->contentlog) {
if (opts->contentlogdir) {
if (log_content_open_logdir(opts->contentlog) == -1)
goto out;
} else if (opts->contentlogspec) {
if (log_content_open_logspec(opts->contentlog) == -1)
goto out;
} else {
if (log_content_open_singlefile(opts->contentlog) == -1)
goto out;
}
if (!(content_log = logger_new(log_content_writecb))) {
log_content_close_singlefile();
goto out;
}
}
if (opts->connectlog) {
if (log_connect_open(opts->connectlog) == -1)
goto out;
if (!(connect_log = logger_new(log_connect_writecb))) {
log_connect_close();
goto out;
}
}
if (!(err_log = logger_new(log_err_writecb)))
goto out;
return 0;
out:
if (content_log) {
log_content_close_singlefile();
logger_free(content_log);
}
if (connect_log) {
log_connect_close();
logger_free(connect_log);
}
return -1;
}
/*
* Log post-init: start logging threads.
* Return -1 on errors, 0 otherwise.
*/
int
log_init(opts_t *opts)
{
if (err_log)
if (logger_start(err_log) == -1)
return -1;
if (!opts->debug) {
err_started = 1;
}
if (connect_log)
if (logger_start(connect_log) == -1)
return -1;
if (content_log)
if (logger_start(content_log) == -1)
return -1;
return 0;
}
/*
* Drain and cleanup. Tell all loggers to leave, then join all logger threads,
* and finally free resources and close log files.
*/
void
log_fini(void)
{
if (content_log)
logger_leave(content_log);
if (connect_log)
logger_leave(connect_log);
logger_leave(err_log);
if (content_log)
logger_join(content_log);
if (connect_log)
logger_join(connect_log);
logger_join(err_log);
if (content_log)
logger_free(content_log);
if (connect_log)
logger_free(connect_log);
logger_free(err_log);
if (content_log)
log_content_close_singlefile();
if (connect_log)
log_connect_close();
}
/* vim: set noet ft=c: */