slog.c (562 lines of code) (raw):
/*
 * Copyright 2004-present Facebook. All Rights Reserved.
 */
 /* When using slogctxt in any module perform a NULL pointer check */
#include <sys/types.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <syslog.h>
#include <pwd.h>
#include <time.h>
#include "includes.h"
#include "slog.h"
#include "log.h"
#include "misc.h"
#include "servconf.h"
#include "xmalloc.h"
typedef struct Structuredlogctxt Structuredlogctxt;
struct Structuredlogctxt { /* items we care about logging */
	char		server_ip[SLOG_SHORT_STRING_LEN];		// server_ip
	int		server_port;		// server_port
	char		remote_ip[SLOG_SHORT_STRING_LEN];		// remote_ip
	int		remote_port;		// remote_port
	pid_t		pam_process_pid;		// pam_pid
	char		session_id[SLOG_STRING_LEN];		// session_id
	char		method[SLOG_STRING_LEN];		// method
	char		cert_id[SLOG_STRING_LEN];		// cert_id
	unsigned long long		cert_serial;		// cert_serial
	char		principal[SLOG_STRING_LEN];		// principal
	char		user[SLOG_STRING_LEN];		// user
	char		command[SLOG_LONG_STRING_LEN];		// command
	SLOG_SESSION_STATE		session_state;		// session_state
	SLOG_AUTHENTICATED		auth_successful;		// auth_successful
	time_t		start_time;		// start_time
	time_t		end_time;		// end_time
	int		duration;		// duration
	pid_t		main_pid;		// main_pid
	char		auth_info[SLOG_MEDIUM_STRING_LEN];		// auth_info
	char		client_version[SLOG_STRING_LEN];		// client_version
	struct timeval		auth_start_time;
	struct timeval		last_partial_auth_time;
	struct timeval		auth_end_time;
	double		auth_duration;		// auth_duration
	double		last_partial_auth_duration;		// last_partial_auth_duration
};
Structuredlogctxt *slogctxt;
extern ServerOptions options;
// Define token constants and default syntax format
static const char *server_ip_token         = "server_ip";
static const char *server_port_token       = "server_port";
static const char *remote_ip_token         = "remote_ip";
static const char *remote_port_token       = "remote_port";
static const char *pam_pid_token           = "pam_pid";
static const char *process_pid_token       = "pid";
static const char *session_id_token        = "session_id";
static const char *method_token            = "method";
static const char *cert_id_token           = "cert_id";
static const char *cert_serial_token       = "cert_serial";
static const char *principal_token         = "principal";
static const char *user_token              = "user";
static const char *command_token           = "command";
static const char *session_state_token     = "session_state";
static const char *auth_successful_token   = "auth_successful";
static const char *start_time_token        = "start_time";
static const char *end_time_token          = "end_time";
static const char *duration_token          = "duration";
static const char *main_pid_token          = "main_pid";
static const char *auth_info_token         = "auth_info";
static const char *client_version          = "client_version";
static const char *auth_duration_token     = "auth_duration";
static const char *last_partial_auth_duration_token	=
	"last_partial_auth_duration";
/* Example log format sshd_config
 * LogFormatPrefix sshd_auth_msg:
 * LogFormatKeys server_ip server_port remote_ip remote_port pid session_id /
   method cert_id cert_serial principal user session_state auth_successful /
   start_time command # NO LINE BREAKS
 * LogFormatJson yes # no makes this output an json array vs json object
 */
// Set a arbitrary large size so we can feed a potentially
// large json object to the logger
#define SLOG_BUF_SIZE 8192
#define SLOG_TRUNCATED_MESSAGE_JSON ", \"incomplete\": \"true\"}"
#define SLOG_TRUNCATED_MESSAGE_ARRAY ", \"incomplete\"]"
#define SLOG_TRUNCATED_SIZE 25
/* size of format for JSON for quotes_colon_space around key and comma_space
   after value or closure_null */
#define SLOG_JSON_FORMAT_SIZE 6
#define SLOG_BUF_CALC_SIZE  SLOG_BUF_SIZE - SLOG_TRUNCATED_SIZE
/* private declarations */
static void slog_log(void);
static void slog_cleanup(void);
static void slog_generate_auth_payload(char *);
static void slog_escape_value(char *, char *, size_t);
static void slog_get_safe_from_token(char *, const char *);
static const char* slog_get_state_text(void);
/* public functions */
void
slog_init(void)
{
	/* initialize only if we have log_format_keys */
	if (options.num_log_format_keys != 0) {
		slogctxt = xcalloc(1, sizeof(Structuredlogctxt));
		if (slogctxt != NULL)
			slog_cleanup();
	}
}
void
slog_pam_session_opened(void)
{
	if (slogctxt != NULL) {
		slogctxt->session_state = SLOG_SESSION_OPEN;
		slogctxt->pam_process_pid = getpid();
	}
}
void
slog_set_auth_start(void)
{
	if (slogctxt != NULL) {
		gettimeofday(&slogctxt->auth_start_time, NULL);
	}
}
void
slog_set_last_partial_auth_time(void)
{
	if (slogctxt != NULL) {
		if (gettimeofday(&slogctxt->last_partial_auth_time, NULL) == 0) {
			struct timeval res;
			timersub(&slogctxt->last_partial_auth_time, &slogctxt->auth_start_time,
			    &res);
			slogctxt->last_partial_auth_duration = res.tv_sec +
			    (res.tv_usec / 1000000.0);
		}
	}
}
void
slog_set_auth_end(void)
{
	if (slogctxt != NULL) {
		if (gettimeofday(&slogctxt->auth_end_time, NULL) == 0) {
			struct timeval res;
			timersub(&slogctxt->auth_end_time, &slogctxt->auth_start_time, &res);
			slogctxt->auth_duration = res.tv_sec + (res.tv_usec / 1000000.0);
		}
	}
}
void
slog_set_auth_data(int authenticated, const char *method, const char *user)
{
	if (slogctxt != NULL) {
		slogctxt->auth_successful =
		    authenticated ? SLOG_AUTHORIZED : SLOG_UNAUTHORIZED;
        if (slogctxt->method[0] != '\0') {
            strlcat(slogctxt->method, ",", SLOG_SHORT_STRING_LEN);
        }
        strlcat(slogctxt->method, method, SLOG_SHORT_STRING_LEN);
		strlcpy(slogctxt->user, user, SLOG_STRING_LEN);
	}
}
void
slog_set_cert_id(const char *id)
{
	if (slogctxt != NULL)
		strlcpy(slogctxt->cert_id, id, SLOG_STRING_LEN);
}
void
slog_set_cert_serial(unsigned long long serial)
{
	if (slogctxt != NULL)
		slogctxt->cert_serial = serial;
}
void
slog_set_connection(const char *remote_ip, int remote_port,
    const char *server_ip, int server_port, const char *session)
{
	if (slogctxt != NULL) {
		strlcpy(slogctxt->remote_ip, remote_ip, SLOG_SHORT_STRING_LEN);
		slogctxt->remote_port = remote_port;
		strlcpy(slogctxt->server_ip, server_ip, SLOG_SHORT_STRING_LEN);
		slogctxt->server_port = server_port;
		strlcpy(slogctxt->session_id, session, SLOG_STRING_LEN);
		slogctxt->start_time = time(NULL);
		slogctxt->main_pid = getpid();
	}
}
void
slog_set_client_version(const char *version)
{
	if (slogctxt != NULL) {
		if (strlen(version) < SLOG_STRING_LEN)
			strlcpy(slogctxt->client_version, version, SLOG_STRING_LEN);
		else {
			// version can be up to 256 bytes, truncate to 95 and add ' ...'
			// which will fit in SLOG_STRING_LEN
			snprintf(slogctxt->client_version, SLOG_STRING_LEN, "%.95s ...", version);
		}
	}
}
void
slog_set_command(const char *command)
{
	if (slogctxt != NULL) {
		if (strlen(command) < SLOG_LONG_STRING_LEN)
			strlcpy(slogctxt->command, command, SLOG_LONG_STRING_LEN);
		else {
			// If command is longer than allowed we truncate it to
			// 1995 (SLOG_LONG_STRING_LEN - 5) characters and add ' ...\0' to
			// the end of the command.
			snprintf(slogctxt->command, SLOG_LONG_STRING_LEN, "%.1995s ...", command);
		}
	}
}
void
slog_set_principal(const char *principal)
{
	if (slogctxt != NULL)
		strlcpy(slogctxt->principal, principal, SLOG_STRING_LEN);
}
void
slog_set_user(const char *user)
{
	if (slogctxt != NULL)
		strlcpy(slogctxt->user, user, SLOG_STRING_LEN);
}
void
slog_set_auth_info(const char *auth_info)
{
	if (slogctxt != NULL)
		strlcpy(slogctxt->auth_info, auth_info, SLOG_MEDIUM_STRING_LEN);
}
void
slog_exit_handler(void)
{
	/* to prevent duplicate logging we only log based on the pid set */
	if (slogctxt != NULL) {
		if (slogctxt->server_ip[0] == 0)
			return; // not initialized
		if (slogctxt->main_pid != getpid())
			return; // not main process
		if (slogctxt->session_state == SLOG_SESSION_INIT)
			slog_log();
		else {
			slogctxt->session_state = SLOG_SESSION_CLOSED;
			slogctxt->end_time = time(NULL);
			slogctxt->duration = slogctxt->end_time - slogctxt->start_time;
			slog_log();
			slog_cleanup();
		}
  }
}
void
slog_log_session(void)
{
	if (slogctxt != NULL) {
		slogctxt->session_state = SLOG_SESSION_OPEN;
		slog_log();
	}
}
/* private function scope begin */
static void
slog_log(void)
{
	char *buffer = xmalloc(SLOG_BUF_SIZE);
	if (buffer == NULL)
		return;
	memset(buffer, 0, SLOG_BUF_SIZE);
	if (options.num_log_format_keys > 0
	    && slogctxt != NULL
	    && slogctxt->server_ip[0] != 0
	    && slogctxt->user[0] != 0) {
		slog_generate_auth_payload(buffer);
		do_log_slog_payload(buffer);
	}
	free(buffer);
}
static void
slog_cleanup(void)
{
	// Reset the log context values
	if (slogctxt != NULL) {
		memset(slogctxt, 0, sizeof(Structuredlogctxt));
		slogctxt->session_state = SLOG_SESSION_INIT;
		slogctxt->auth_successful = SLOG_UNAUTHORIZED;
	}
}
/* We use debug3 since the debug is very noisy */
static void
slog_generate_auth_payload(char *buf)
{
	if (buf == NULL)
		return;
	// Create large buffer so don't risk overflow
	char *safe = xmalloc(SLOG_BUF_SIZE);
	memset(safe, 0, SLOG_BUF_SIZE);
	if (safe == NULL)
		return;
	int i;
	size_t remaining;
	int json = options.log_format_json;
	int keys = options.num_log_format_keys;
	int truncated = 0;
	char *tmpbuf = buf;
	char *key;
	debug3("JSON format is %d with %d tokens.", json, keys);
	if (options.log_format_prefix != NULL
	    && strlen(options.log_format_prefix) < SLOG_BUF_CALC_SIZE-1) {
		tmpbuf += snprintf(tmpbuf, SLOG_BUF_CALC_SIZE,
	    "%s ", options.log_format_prefix);
	}
	*tmpbuf++ = (json) ? '{' : '[';
	debug3("current buffer after prefix: %s", buf);
	// Loop through the keys filling out the output string
	for (i = 0; i < keys; i++) {
		safe[0] = 0;  // clear safe string
		key = options.log_format_keys[i];
		remaining = SLOG_BUF_CALC_SIZE - (tmpbuf - buf);
		if (key == NULL)
			continue;  // Shouldn't happen but if null go to next key
		slog_get_safe_from_token(safe, key);
		debug3("token: %s, value: %s", key, safe);
		if (json) {
			if (*safe == '\0')
				continue; // No value since we are using key pairs skip
			if (remaining <= SLOG_JSON_FORMAT_SIZE + strlen(key) + strlen(safe)) {
				debug("Log would exceed buffer size %u, %zu, %zu at key: %s",
				    (unsigned int)remaining, strlen(key), strlen(safe), key);
				truncated = 1;
				break;
			}
			tmpbuf += snprintf(tmpbuf, remaining, "%s\"%s\": %s",
			    i > 0 ? ", " : "", key, safe);
		} else {
			if (*safe == '\0')
				strlcpy(safe, "\"\"", SLOG_SHORT_STRING_LEN);
			if (remaining < SLOG_JSON_FORMAT_SIZE + strlen(safe)) {
				debug("Log would exceed remaining buffer size %d, %zu, at key: %s",
				    (unsigned int)remaining, strlen(safe), key);
				truncated = 1;
				break;
			}
			tmpbuf += snprintf(tmpbuf, remaining, "%s%s", i > 0 ? ", " : "", safe);
		}
		debug3("current buffer after token: %s", buf);
		debug3("end of loop key: %s, %d out of %d keys", key, i + 1, keys);
	}
	// Close the log string. If truncated set truncated message and close string
	if (truncated == 1)
		strlcpy(tmpbuf, json ? SLOG_TRUNCATED_MESSAGE_JSON :
		    SLOG_TRUNCATED_MESSAGE_ARRAY, SLOG_TRUNCATED_SIZE);
	else {
		*tmpbuf++ = (json) ? '}' : ']';
	}
	free(safe);
}
// buffer size is input string * 2 +1
static void
slog_escape_value(char *output, char *input, size_t buffer_size)
{
	int i;
	buffer_size -= 2;
	if (input != NULL) {
		int input_size = strlen(input);
		char *temp = output;
		*temp++ = '"';
		buffer_size--;
		for (i = 0; i < input_size && buffer_size > 0; i++) {
			switch(input[i]) {
			// characters escaped are the same as folly::json::escapeString
			case 27: // <escape> ascii control character
				if (buffer_size > 6) {
					*temp++ = '\\';
					*temp++ = 'u';
					*temp++ = '0';
					*temp++ = '0';
					*temp++ = '1';
					*temp++ = 'b';
					buffer_size -= 6;
				}
			case '\b':
				if (buffer_size > 1) {
					*temp++ = '\\';
					*temp++ = 'b';
					buffer_size -= 2;
				}
				break;
			case '\f':
				if (buffer_size > 1) {
					*temp++ = '\\';
					*temp++ = 'f';
					buffer_size -= 2;
				}
				break;
			case '\n':
				if (buffer_size > 1) {
					*temp++ = '\\';
					*temp++ = 'n';
					buffer_size -= 2;
					}
				break;
			case '\r':
				if (buffer_size > 1) {
					*temp++ = '\\';
					*temp++ = 'r';
					buffer_size -= 2;
				}
				break;
			case '\t':
				if (buffer_size > 1) {
					*temp++ = '\\';
					*temp++ = 't';
					buffer_size -= 2;
				}
				break;
			case '\"':
			case '\\':
				if (buffer_size > 1) {
					*temp++ = '\\';
					buffer_size--;
			}
			default:  // Non-escape char
				*temp++ = input[i];
				buffer_size--;
			}
		}
		*temp++ = '"';
		*temp++ = '\0';
	}
}
static void
slog_get_safe_from_token(char *output, const char *token)
{
	if (output == NULL || token == NULL || slogctxt == NULL)
		return;
	if (strcmp(token, server_ip_token) == 0) {
		if (slogctxt->server_ip[0] != 0) {
			snprintf(output, SLOG_SHORT_STRING_LEN, "\"%s\"",
			    slogctxt->server_ip);
		}
		return;
	}
	if (strcmp(token, server_port_token) == 0) {
		if (slogctxt->server_port > 0) {
			snprintf(output, SLOG_SHORT_STRING_LEN, "\"%d\"",
			slogctxt->server_port);
		}
		return;
	}
	if (strcmp(token, remote_ip_token) == 0) {
		if (slogctxt->remote_ip[0] != 0) {
			snprintf(output, SLOG_SHORT_STRING_LEN, "\"%s\"",
			    slogctxt->remote_ip);
		}
		return;
	}
	if (strcmp(token, remote_port_token) == 0) {
		if (slogctxt->remote_port > 0) {
			snprintf(output, SLOG_SHORT_STRING_LEN, "\"%d\"",
			    slogctxt->remote_port);
		}
		return;
	}
	if (strcmp(token, pam_pid_token) == 0) {
		if (slogctxt->pam_process_pid > 0) {
			snprintf(output, SLOG_SHORT_STRING_LEN, "\"%ld\"",
			    (long)slogctxt->pam_process_pid);
		}
		return;
	}
	if (strcmp(token, process_pid_token) == 0) {
		snprintf(output, SLOG_SHORT_STRING_LEN, "\"%ld\"", (long)getpid());
		return;
	}
	if (strcmp(token, session_id_token) == 0) {
		if (slogctxt->session_id[0] != 0) {
				snprintf(output, SLOG_STRING_LEN, "\"%s\"",
				    slogctxt->session_id);
		}
		return;
	}
	if (strcmp(token, method_token) == 0) {
		if (slogctxt->method[0] != 0) {
				snprintf(output, SLOG_STRING_LEN, "\"%s\"",
				    slogctxt->method);
		}
		return;
	}
	// Arbitrary input
	if (strcmp(token, cert_id_token) == 0) {
		if (slogctxt->cert_id[0] != 0) {
			slog_escape_value(output, slogctxt->cert_id,
			    SLOG_STRING_LEN * 2 + 1);
		}
		return;
	}
	if (strcmp(token, cert_serial_token) == 0) {
		if (slogctxt->cert_serial > 0) {
            snprintf(output, SLOG_SHORT_STRING_LEN, "\"%llu\"",
                slogctxt->cert_serial);
		}
		return;
	}
	// Arbitrary input
	if (strcmp(token, principal_token) == 0) {
		if (slogctxt->principal[0] != 0) {
			slog_escape_value(output, slogctxt->principal,
			    SLOG_STRING_LEN * 2 + 1);
		}
	return;
	}
	// Arbitrary input
	if (strcmp(token, user_token) == 0) {
		if (slogctxt->user[0] != 0) {
			slog_escape_value(output, slogctxt->user,
		    SLOG_STRING_LEN * 2 + 1);
		}
		return;
	}
	// Arbitrary input
	if (strcmp(token, auth_info_token) == 0) {
		if (slogctxt->auth_info[0] != 0) {
			slog_escape_value(output, slogctxt->auth_info,
			    SLOG_MEDIUM_STRING_LEN * 2 + 1);
		}
		return;
	}
	// Arbitrary input
	if (strcmp(token, command_token) == 0) {
		if (slogctxt->command[0] != 0) {
			slog_escape_value(output, slogctxt->command,
			    SLOG_LONG_STRING_LEN * 2 + 1);
		}
		return;
	}
	if (strcmp(token, auth_successful_token) == 0) {
		snprintf(output, SLOG_SHORT_STRING_LEN, "\"%s\"",
		    slogctxt->auth_successful == SLOG_AUTHORIZED ? "true" : "false");
		return;
	}
	if (strcmp(token, session_state_token) == 0) {
		snprintf(output, SLOG_SHORT_STRING_LEN, "\"%s\"",
		    slog_get_state_text());
		return;
	}
	if (strcmp(token, start_time_token) == 0) {
		snprintf(output, SLOG_SHORT_STRING_LEN, "\"%d\"",
		    (int)slogctxt->start_time);
		return;
	}
	if (strcmp(token, end_time_token) == 0 && slogctxt->end_time > 0) {
		snprintf(output, SLOG_SHORT_STRING_LEN, "\"%d\"",
		    (int)slogctxt->end_time);
		return;
	}
	if (strcmp(token, duration_token) == 0 && slogctxt->end_time > 0) {
		snprintf(output, SLOG_SHORT_STRING_LEN, "\"%d\"", slogctxt->duration);
		return;
	}
	if (strcmp(token, auth_duration_token) == 0 &&
		slogctxt->auth_end_time.tv_sec > 0) {
			snprintf(output, SLOG_SHORT_STRING_LEN, "\"%f\"", slogctxt->auth_duration);
			return;
	}
	if (strcmp(token, last_partial_auth_duration_token) == 0 &&
		timerisset(&slogctxt->last_partial_auth_time)) {
			snprintf(output, SLOG_SHORT_STRING_LEN, "\"%f\"",
			    slogctxt->last_partial_auth_duration);
			return;
	}
	if (strcmp(token, main_pid_token) == 0) {
		if (slogctxt->main_pid > 0) {
			snprintf(output, SLOG_SHORT_STRING_LEN, "\"%ld\"",
			    (long)slogctxt->main_pid);
		}
		return;
	}
	// Arbitrary input
	if (strncmp(token, client_version, strlen(client_version)) == 0) {
		if (slogctxt->client_version[0] != 0) {
			slog_escape_value(output, slogctxt->client_version,
			    SLOG_STRING_LEN + 2);
		}
		return;
	}
}
static const char *
slog_get_state_text(void)
{
	if (slogctxt == NULL)
		return "";
	switch (slogctxt->session_state) {
		case SLOG_SESSION_INIT:
			return "Session failed";
		case SLOG_SESSION_OPEN:
			return "Session opened";
		case SLOG_SESSION_CLOSED:
			return "Session closed";
		default:
			return "Unknown session state";  // Should never happen
	}
}