1
0
mirror of https://github.com/systemd/systemd.git synced 2024-11-12 15:21:19 +03:00
systemd/src/journal-remote/journal-remote.c
Zbigniew Jędrzejewski-Szmek 8201af08fa journal-remote: allow splitting incoming logs by source host
Previously existing scheme where the file name would be based on
the source was just too ugly and unpredicatable. Now there are
only two options:
  1. just one file (until rotation),
  2. one file per source host, using the hostname as filename part.
For the cases where the source is specified by the user, only
option one is allowed, and the full of the file must be specified.
2014-07-15 22:31:41 -04:00

1591 lines
51 KiB
C

/*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/
/***
This file is part of systemd.
Copyright 2012 Zbigniew Jędrzejewski-Szmek
systemd is free software; you can redistribute it and/or modify it
under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 2.1 of the License, or
(at your option) any later version.
systemd is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with systemd; If not, see <http://www.gnu.org/licenses/>.
***/
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/prctl.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <getopt.h>
#include "sd-daemon.h"
#include "sd-event.h"
#include "journal-file.h"
#include "journald-native.h"
#include "socket-util.h"
#include "mkdir.h"
#include "build.h"
#include "macro.h"
#include "strv.h"
#include "hashmap.h"
#include "fileio.h"
#include "conf-parser.h"
#include "microhttpd-util.h"
#include "siphash24.h"
#ifdef HAVE_GNUTLS
#include <gnutls/gnutls.h>
#endif
#include "journal-remote-parse.h"
#include "journal-remote-write.h"
#define REMOTE_JOURNAL_PATH "/var/log/journal/remote"
#define KEY_FILE CERTIFICATE_ROOT "/private/journal-remote.pem"
#define CERT_FILE CERTIFICATE_ROOT "/certs/journal-remote.pem"
#define TRUST_FILE CERTIFICATE_ROOT "/ca/trusted.pem"
static char* arg_url = NULL;
static char* arg_getter = NULL;
static char* arg_listen_raw = NULL;
static char* arg_listen_http = NULL;
static char* arg_listen_https = NULL;
static char** arg_files = NULL;
static int arg_compress = true;
static int arg_seal = false;
static int http_socket = -1, https_socket = -1;
static char** arg_gnutls_log = NULL;
static JournalWriteSplitMode arg_split_mode = JOURNAL_WRITE_SPLIT_NONE;
static char* arg_output = NULL;
static char *arg_key = NULL;
static char *arg_cert = NULL;
static char *arg_trust = NULL;
static bool arg_trust_all = false;
/**********************************************************************
**********************************************************************
**********************************************************************/
static int spawn_child(const char* child, char** argv) {
int fd[2];
pid_t parent_pid, child_pid;
int r;
if (pipe(fd) < 0) {
log_error("Failed to create pager pipe: %m");
return -errno;
}
parent_pid = getpid();
child_pid = fork();
if (child_pid < 0) {
r = -errno;
log_error("Failed to fork: %m");
safe_close_pair(fd);
return r;
}
/* In the child */
if (child_pid == 0) {
r = dup2(fd[1], STDOUT_FILENO);
if (r < 0) {
log_error("Failed to dup pipe to stdout: %m");
_exit(EXIT_FAILURE);
}
safe_close_pair(fd);
/* Make sure the child goes away when the parent dies */
if (prctl(PR_SET_PDEATHSIG, SIGTERM) < 0)
_exit(EXIT_FAILURE);
/* Check whether our parent died before we were able
* to set the death signal */
if (getppid() != parent_pid)
_exit(EXIT_SUCCESS);
execvp(child, argv);
log_error("Failed to exec child %s: %m", child);
_exit(EXIT_FAILURE);
}
r = close(fd[1]);
if (r < 0)
log_warning("Failed to close write end of pipe: %m");
return fd[0];
}
static int spawn_curl(const char* url) {
char **argv = STRV_MAKE("curl",
"-HAccept: application/vnd.fdo.journal",
"--silent",
"--show-error",
url);
int r;
r = spawn_child("curl", argv);
if (r < 0)
log_error("Failed to spawn curl: %m");
return r;
}
static int spawn_getter(const char *getter, const char *url) {
int r;
_cleanup_strv_free_ char **words = NULL;
assert(getter);
words = strv_split_quoted(getter);
if (!words)
return log_oom();
r = strv_extend(&words, url);
if (r < 0) {
log_error("Failed to create command line: %s", strerror(-r));
return r;
}
r = spawn_child(words[0], words);
if (r < 0)
log_error("Failed to spawn getter %s: %m", getter);
return r;
}
#define filename_escape(s) xescape((s), "./ ")
static int open_output(Writer *w, const char* host) {
_cleanup_free_ char *_output = NULL;
const char *output;
int r;
switch (arg_split_mode) {
case JOURNAL_WRITE_SPLIT_NONE:
output = arg_output ?: REMOTE_JOURNAL_PATH "/remote.journal";
break;
case JOURNAL_WRITE_SPLIT_HOST: {
_cleanup_free_ char *name;
assert(host);
name = filename_escape(host);
if (!name)
return log_oom();
r = asprintf(&_output, "%s/remote-%s.journal",
arg_output ?: REMOTE_JOURNAL_PATH,
name);
if (r < 0)
return log_oom();
output = _output;
break;
}
default:
assert_not_reached("what?");
}
r = journal_file_open_reliably(output,
O_RDWR|O_CREAT, 0640,
arg_compress, arg_seal,
&w->metrics,
w->mmap,
NULL, &w->journal);
if (r < 0)
log_error("Failed to open output journal %s: %s",
output, strerror(-r));
else
log_info("Opened output file %s", w->journal->path);
return r;
}
/**********************************************************************
**********************************************************************
**********************************************************************/
typedef struct MHDDaemonWrapper {
uint64_t fd;
struct MHD_Daemon *daemon;
sd_event_source *event;
} MHDDaemonWrapper;
typedef struct RemoteServer {
RemoteSource **sources;
size_t sources_size;
size_t active;
sd_event *events;
sd_event_source *sigterm_event, *sigint_event, *listen_event;
Hashmap *writers;
bool check_trust;
Hashmap *daemons;
} RemoteServer;
/* This should go away as soon as µhttpd allows state to be passed around. */
static RemoteServer *server;
static int dispatch_raw_source_event(sd_event_source *event,
int fd,
uint32_t revents,
void *userdata);
static int dispatch_raw_connection_event(sd_event_source *event,
int fd,
uint32_t revents,
void *userdata);
static int dispatch_http_event(sd_event_source *event,
int fd,
uint32_t revents,
void *userdata);
static int get_source_for_fd(RemoteServer *s, int fd, RemoteSource **source) {
assert(fd >= 0);
assert(source);
if (!GREEDY_REALLOC0(s->sources, s->sources_size, fd + 1))
return log_oom();
if (s->sources[fd] == NULL) {
s->sources[fd] = new0(RemoteSource, 1);
if (!s->sources[fd])
return log_oom();
s->sources[fd]->fd = -1;
s->active++;
}
*source = s->sources[fd];
return 0;
}
static int remove_source(RemoteServer *s, int fd) {
RemoteSource *source;
assert(s);
assert(fd >= 0 && fd < (ssize_t) s->sources_size);
source = s->sources[fd];
if (source) {
/* this closes fd too */
source_free(source);
s->sources[fd] = NULL;
s->active--;
}
return 0;
}
static int add_source(RemoteServer *s, int fd, const char* _name) {
RemoteSource *source;
char *name;
int r;
assert(s);
assert(fd >= 0);
assert(_name);
log_debug("Creating source for fd:%d (%s)", fd, _name);
name = strdup(_name);
if (!name)
return log_oom();
r = get_source_for_fd(s, fd, &source);
if (r < 0) {
log_error("Failed to create source for fd:%d (%s)", fd, name);
free(name);
return r;
}
assert(source);
assert(source->fd < 0);
assert(!source->name);
source->fd = fd;
source->name = name;
r = sd_event_add_io(s->events, &source->event,
fd, EPOLLIN|EPOLLRDHUP|EPOLLPRI,
dispatch_raw_source_event, s);
if (r < 0) {
log_error("Failed to register event source for fd:%d: %s",
fd, strerror(-r));
goto error;
}
return 1; /* work to do */
error:
remove_source(s, fd);
return r;
}
static int add_raw_socket(RemoteServer *s, int fd) {
int r;
r = sd_event_add_io(s->events, &s->listen_event,
fd, EPOLLIN,
dispatch_raw_connection_event, s);
if (r < 0) {
close(fd);
return r;
}
s->active ++;
return 0;
}
static int setup_raw_socket(RemoteServer *s, const char *address) {
int fd;
fd = make_socket_fd(LOG_INFO, address, SOCK_STREAM | SOCK_CLOEXEC);
if (fd < 0)
return fd;
return add_raw_socket(s, fd);
}
/**********************************************************************
**********************************************************************
**********************************************************************/
static int init_writer_hashmap(RemoteServer *s) {
static const struct {
hash_func_t hash_func;
compare_func_t compare_func;
} functions[] = {
[JOURNAL_WRITE_SPLIT_NONE] = {trivial_hash_func,
trivial_compare_func},
[JOURNAL_WRITE_SPLIT_HOST] = {string_hash_func,
string_compare_func},
};
assert(arg_split_mode >= 0 && arg_split_mode < (int) ELEMENTSOF(functions));
s->writers = hashmap_new(functions[arg_split_mode].hash_func,
functions[arg_split_mode].compare_func);
if (!s->writers)
return log_oom();
return 0;
}
static int get_writer(RemoteServer *s, const char *host, sd_id128_t *machine,
Writer **writer) {
const void *key;
Writer *w;
int r;
switch(arg_split_mode) {
case JOURNAL_WRITE_SPLIT_NONE:
key = "one and only";
break;
case JOURNAL_WRITE_SPLIT_HOST:
assert(host);
key = host;
break;
default:
assert_not_reached("what split mode?");
}
w = hashmap_get(s->writers, key);
if (!w) {
w = new0(Writer, 1);
if (!w)
return -ENOMEM;
r = writer_init(w);
if (r < 0) {
free(w);
return r;
}
r = hashmap_put(s->writers, key, w);
if (r < 0) {
writer_close(w);
free(w);
return r;
}
r = open_output(w, host);
if (r < 0)
return r;
}
*writer = w;
return 0;
}
/**********************************************************************
**********************************************************************
**********************************************************************/
static RemoteSource *request_meta(void **connection_cls, int fd, char *hostname) {
RemoteSource *source;
assert(connection_cls);
if (*connection_cls) {
free(hostname);
return *connection_cls;
}
source = new0(RemoteSource, 1);
if (!source) {
free(hostname);
return NULL;
}
source->fd = -1; /* fd */
source->name = hostname;
log_debug("Added RemoteSource as connection metadata %p", source);
*connection_cls = source;
return source;
}
static void request_meta_free(void *cls,
struct MHD_Connection *connection,
void **connection_cls,
enum MHD_RequestTerminationCode toe) {
RemoteSource *s;
assert(connection_cls);
s = *connection_cls;
log_debug("Cleaning up connection metadata %p", s);
source_free(s);
*connection_cls = NULL;
}
static int process_http_upload(
struct MHD_Connection *connection,
const char *upload_data,
size_t *upload_data_size,
RemoteSource *source) {
Writer *w;
int r;
bool finished = false;
assert(source);
log_debug("request_handler_upload: connection %p, %zu bytes",
connection, *upload_data_size);
r = get_writer(server, source->name, NULL, &w);
if (r < 0) {
log_warning("Failed to get writer for source %s (%s): %s",
source->name, source->name, strerror(-r));
return mhd_respondf(connection,
MHD_HTTP_SERVICE_UNAVAILABLE,
"Failed to get writer for connection: %s.\n",
strerror(-r));
}
if (*upload_data_size) {
log_info("Received %zu bytes", *upload_data_size);
r = push_data(source, upload_data, *upload_data_size);
if (r < 0)
return mhd_respond_oom(connection);
*upload_data_size = 0;
} else
finished = true;
while (true) {
r = process_source(source, w, arg_compress, arg_seal);
if (r == -EAGAIN || r == -EWOULDBLOCK)
break;
else if (r < 0) {
log_warning("Failed to process data for connection %p", connection);
if (r == -E2BIG)
return mhd_respondf(connection,
MHD_HTTP_REQUEST_ENTITY_TOO_LARGE,
"Entry is too large, maximum is %u bytes.\n",
DATA_SIZE_MAX);
else
return mhd_respondf(connection,
MHD_HTTP_UNPROCESSABLE_ENTITY,
"Processing failed: %s.", strerror(-r));
}
}
if (!finished)
return MHD_YES;
/* The upload is finished */
if (source_non_empty(source)) {
log_warning("EOF reached with incomplete data");
return mhd_respond(connection, MHD_HTTP_EXPECTATION_FAILED,
"Trailing data not processed.");
}
return mhd_respond(connection, MHD_HTTP_ACCEPTED, "OK.\n");
};
static int request_handler(
void *cls,
struct MHD_Connection *connection,
const char *url,
const char *method,
const char *version,
const char *upload_data,
size_t *upload_data_size,
void **connection_cls) {
const char *header;
int r, code, fd;
char *hostname;
assert(connection);
assert(connection_cls);
assert(url);
assert(method);
log_debug("Handling a connection %s %s %s", method, url, version);
if (*connection_cls)
return process_http_upload(connection,
upload_data, upload_data_size,
*connection_cls);
if (!streq(method, "POST"))
return mhd_respond(connection, MHD_HTTP_METHOD_NOT_ACCEPTABLE,
"Unsupported method.\n");
if (!streq(url, "/upload"))
return mhd_respond(connection, MHD_HTTP_NOT_FOUND,
"Not found.\n");
header = MHD_lookup_connection_value(connection,
MHD_HEADER_KIND, "Content-Type");
if (!header || !streq(header, "application/vnd.fdo.journal"))
return mhd_respond(connection, MHD_HTTP_UNSUPPORTED_MEDIA_TYPE,
"Content-Type: application/vnd.fdo.journal"
" is required.\n");
{
const union MHD_ConnectionInfo *ci;
ci = MHD_get_connection_info(connection,
MHD_CONNECTION_INFO_CONNECTION_FD);
if (!ci) {
log_error("MHD_get_connection_info failed: cannot get remote fd");
return mhd_respond(connection, MHD_HTTP_INTERNAL_SERVER_ERROR,
"Cannot check remote address");
return code;
}
fd = ci->connect_fd;
assert(fd >= 0);
}
if (server->check_trust) {
r = check_permissions(connection, &code, &hostname);
if (r < 0)
return code;
} else {
r = getnameinfo_pretty(fd, &hostname);
if (r < 0) {
return mhd_respond(connection, MHD_HTTP_INTERNAL_SERVER_ERROR,
"Cannot check remote hostname");
}
}
assert(hostname);
if (!request_meta(connection_cls, fd, hostname))
return respond_oom(connection);
return MHD_YES;
}
static int setup_microhttpd_server(RemoteServer *s,
int fd,
const char *key,
const char *cert,
const char *trust) {
struct MHD_OptionItem opts[] = {
{ MHD_OPTION_NOTIFY_COMPLETED, (intptr_t) request_meta_free},
{ MHD_OPTION_EXTERNAL_LOGGER, (intptr_t) microhttpd_logger},
{ MHD_OPTION_LISTEN_SOCKET, fd},
{ MHD_OPTION_END},
{ MHD_OPTION_END},
{ MHD_OPTION_END},
{ MHD_OPTION_END}};
int opts_pos = 3;
int flags =
MHD_USE_DEBUG |
MHD_USE_PEDANTIC_CHECKS |
MHD_USE_EPOLL_LINUX_ONLY |
MHD_USE_DUAL_STACK;
const union MHD_DaemonInfo *info;
int r, epoll_fd;
MHDDaemonWrapper *d;
assert(fd >= 0);
r = fd_nonblock(fd, true);
if (r < 0) {
log_error("Failed to make fd:%d nonblocking: %s", fd, strerror(-r));
return r;
}
if (key) {
assert(cert);
opts[opts_pos++] = (struct MHD_OptionItem)
{MHD_OPTION_HTTPS_MEM_KEY, 0, (char*) key};
opts[opts_pos++] = (struct MHD_OptionItem)
{MHD_OPTION_HTTPS_MEM_CERT, 0, (char*) cert};
flags |= MHD_USE_SSL;
if (trust)
opts[opts_pos++] = (struct MHD_OptionItem)
{MHD_OPTION_HTTPS_MEM_TRUST, 0, (char*) trust};
}
d = new(MHDDaemonWrapper, 1);
if (!d)
return log_oom();
d->fd = (uint64_t) fd;
d->daemon = MHD_start_daemon(flags, 0,
NULL, NULL,
request_handler, NULL,
MHD_OPTION_ARRAY, opts,
MHD_OPTION_END);
if (!d->daemon) {
log_error("Failed to start µhttp daemon");
r = -EINVAL;
goto error;
}
log_debug("Started MHD %s daemon on fd:%d (wrapper @ %p)",
key ? "HTTPS" : "HTTP", fd, d);
info = MHD_get_daemon_info(d->daemon, MHD_DAEMON_INFO_EPOLL_FD_LINUX_ONLY);
if (!info) {
log_error("µhttp returned NULL daemon info");
r = -ENOTSUP;
goto error;
}
epoll_fd = info->listen_fd;
if (epoll_fd < 0) {
log_error("µhttp epoll fd is invalid");
r = -EUCLEAN;
goto error;
}
r = sd_event_add_io(s->events, &d->event,
epoll_fd, EPOLLIN,
dispatch_http_event, d);
if (r < 0) {
log_error("Failed to add event callback: %s", strerror(-r));
goto error;
}
r = hashmap_ensure_allocated(&s->daemons, uint64_hash_func, uint64_compare_func);
if (r < 0) {
log_oom();
goto error;
}
r = hashmap_put(s->daemons, &d->fd, d);
if (r < 0) {
log_error("Failed to add daemon to hashmap: %s", strerror(-r));
goto error;
}
s->active ++;
return 0;
error:
MHD_stop_daemon(d->daemon);
free(d->daemon);
free(d);
return r;
}
static int setup_microhttpd_socket(RemoteServer *s,
const char *address,
const char *key,
const char *cert,
const char *trust) {
int fd;
fd = make_socket_fd(LOG_INFO, address, SOCK_STREAM | SOCK_CLOEXEC);
if (fd < 0)
return fd;
return setup_microhttpd_server(s, fd, key, cert, trust);
}
static int dispatch_http_event(sd_event_source *event,
int fd,
uint32_t revents,
void *userdata) {
MHDDaemonWrapper *d = userdata;
int r;
assert(d);
r = MHD_run(d->daemon);
if (r == MHD_NO) {
log_error("MHD_run failed!");
// XXX: unregister daemon
return -EINVAL;
}
return 1; /* work to do */
}
/**********************************************************************
**********************************************************************
**********************************************************************/
static int dispatch_sigterm(sd_event_source *event,
const struct signalfd_siginfo *si,
void *userdata) {
RemoteServer *s = userdata;
assert(s);
log_received_signal(LOG_INFO, si);
sd_event_exit(s->events, 0);
return 0;
}
static int setup_signals(RemoteServer *s) {
sigset_t mask;
int r;
assert(s);
assert_se(sigemptyset(&mask) == 0);
sigset_add_many(&mask, SIGINT, SIGTERM, -1);
assert_se(sigprocmask(SIG_SETMASK, &mask, NULL) == 0);
r = sd_event_add_signal(s->events, &s->sigterm_event, SIGTERM, dispatch_sigterm, s);
if (r < 0)
return r;
r = sd_event_add_signal(s->events, &s->sigint_event, SIGINT, dispatch_sigterm, s);
if (r < 0)
return r;
return 0;
}
static int fd_fd(const char *spec) {
int fd, r;
r = safe_atoi(spec, &fd);
if (r < 0)
return r;
return -1;
}
static int remoteserver_init(RemoteServer *s,
const char* key,
const char* cert,
const char* trust) {
int r, n, fd;
const char *output_name = NULL;
char **file;
assert(s);
if ((arg_listen_raw || arg_listen_http) && trust) {
log_error("Option --trust makes all non-HTTPS connections untrusted.");
return -EINVAL;
}
sd_event_default(&s->events);
setup_signals(s);
assert(server == NULL);
server = s;
n = sd_listen_fds(true);
if (n < 0) {
log_error("Failed to read listening file descriptors from environment: %s",
strerror(-n));
return n;
} else
log_info("Received %d descriptors", n);
if (MAX(http_socket, https_socket) >= SD_LISTEN_FDS_START + n) {
log_error("Received fewer sockets than expected");
return -EBADFD;
}
for (fd = SD_LISTEN_FDS_START; fd < SD_LISTEN_FDS_START + n; fd++) {
if (sd_is_socket(fd, AF_UNSPEC, 0, false)) {
log_info("Received a listening socket (fd:%d)", fd);
if (fd == http_socket)
r = setup_microhttpd_server(s, fd, NULL, NULL, NULL);
else if (fd == https_socket)
r = setup_microhttpd_server(s, fd, key, cert, trust);
else
r = add_raw_socket(s, fd);
} else if (sd_is_socket(fd, AF_UNSPEC, 0, true)) {
_cleanup_free_ char *hostname = NULL;
r = getnameinfo_pretty(fd, &hostname);
if (r < 0) {
log_error("Failed to retrieve remote name: %s", strerror(-r));
return r;
}
log_info("Received a connection socket (fd:%d) from %s", fd, hostname);
r = add_source(s, fd, hostname);
} else {
log_error("Unknown socket passed on fd:%d", fd);
return -EINVAL;
}
if(r < 0) {
log_error("Failed to register socket (fd:%d): %s",
fd, strerror(-r));
return r;
}
output_name = "socket";
}
if (arg_url) {
const char *url, *hostname;
url = strappenda(arg_url, "/entries");
if (arg_getter) {
log_info("Spawning getter %s...", url);
fd = spawn_getter(arg_getter, url);
} else {
log_info("Spawning curl %s...", url);
fd = spawn_curl(url);
}
if (fd < 0)
return fd;
hostname =
startswith(arg_url, "https://") ?:
startswith(arg_url, "http://") ?:
arg_url;
r = add_source(s, fd, hostname);
if (r < 0)
return r;
output_name = arg_url;
}
if (arg_listen_raw) {
log_info("Listening on a socket...");
r = setup_raw_socket(s, arg_listen_raw);
if (r < 0)
return r;
output_name = arg_listen_raw;
}
if (arg_listen_http) {
r = setup_microhttpd_socket(s, arg_listen_http, NULL, NULL, NULL);
if (r < 0)
return r;
output_name = arg_listen_http;
}
if (arg_listen_https) {
r = setup_microhttpd_socket(s, arg_listen_https, key, cert, trust);
if (r < 0)
return r;
output_name = arg_listen_https;
}
STRV_FOREACH(file, arg_files) {
if (streq(*file, "-")) {
log_info("Reading standard input...");
fd = STDIN_FILENO;
output_name = "stdin";
} else {
log_info("Reading file %s...", *file);
fd = open(*file, O_RDONLY|O_CLOEXEC|O_NOCTTY|O_NONBLOCK);
if (fd < 0) {
log_error("Failed to open %s: %m", *file);
return -errno;
}
output_name = *file;
}
r = add_source(s, fd, output_name);
if (r < 0)
return r;
}
if (s->active == 0) {
log_error("Zarro sources specified");
return -EINVAL;
}
if (!!n + !!arg_url + !!arg_listen_raw + !!arg_files)
output_name = "multiple";
r = init_writer_hashmap(s);
if (r < 0)
return r;
if (arg_split_mode == JOURNAL_WRITE_SPLIT_NONE) {
/* In this case we know what the writer will be
called, so we can create it and verify that we can
create output as expected. */
Writer *w;
r = get_writer(s, NULL, NULL, &w);
if (r < 0)
return r;
}
return 0;
}
static int server_destroy(RemoteServer *s, uint64_t *event_count) {
int r;
size_t i;
Writer *w;
MHDDaemonWrapper *d;
*event_count = 0;
while ((w = hashmap_steal_first(s->writers))) {
*event_count += w->seqnum;
r = writer_close(w);
if (r < 0)
log_warning("Failed to close writer: %s", strerror(-r));
free(w);
}
hashmap_free(s->writers);
while ((d = hashmap_steal_first(s->daemons))) {
MHD_stop_daemon(d->daemon);
sd_event_source_unref(d->event);
free(d);
}
hashmap_free(s->daemons);
assert(s->sources_size == 0 || s->sources);
for (i = 0; i < s->sources_size; i++)
remove_source(s, i);
free(s->sources);
sd_event_source_unref(s->sigterm_event);
sd_event_source_unref(s->sigint_event);
sd_event_source_unref(s->listen_event);
sd_event_unref(s->events);
/* fds that we're listening on remain open... */
return r;
}
/**********************************************************************
**********************************************************************
**********************************************************************/
static int dispatch_raw_source_event(sd_event_source *event,
int fd,
uint32_t revents,
void *userdata) {
Writer *w;
RemoteServer *s = userdata;
RemoteSource *source;
int r;
assert(fd >= 0 && fd < (ssize_t) s->sources_size);
source = s->sources[fd];
assert(source->fd == fd);
r = get_writer(s, source->name, NULL, &w);
if (r < 0) {
log_warning("Failed to get writer for source %s (%s): %s",
source->name, source->name, strerror(-r));
return r;
}
r = process_source(source, w, arg_compress, arg_seal);
if (source->state == STATE_EOF) {
log_info("EOF reached with source fd:%d (%s)",
source->fd, source->name);
if (source_non_empty(source))
log_warning("EOF reached with incomplete data");
remove_source(s, source->fd);
log_info("%zd active source remaining", s->active);
return 0;
} else if (r == -E2BIG) {
log_error("Entry too big, skipped");
return 1;
} else if (r == -EAGAIN) {
return 0;
} else if (r < 0) {
log_info("Closing connection: %s", strerror(-r));
remove_source(server, fd);
return 0;
} else
return 1;
}
static int accept_connection(const char* type, int fd,
SocketAddress *addr, char **hostname) {
int fd2, r;
log_debug("Accepting new %s connection on fd:%d", type, fd);
fd2 = accept4(fd, &addr->sockaddr.sa, &addr->size, SOCK_NONBLOCK|SOCK_CLOEXEC);
if (fd2 < 0) {
log_error("accept() on fd:%d failed: %m", fd);
return -errno;
}
switch(socket_address_family(addr)) {
case AF_INET:
case AF_INET6: {
_cleanup_free_ char *a = NULL;
char *b;
r = socket_address_print(addr, &a);
if (r < 0) {
log_error("socket_address_print(): %s", strerror(-r));
close(fd2);
return r;
}
r = socknameinfo_pretty(&addr->sockaddr, addr->size, &b);
if (r < 0) {
close(fd2);
return r;
}
log_info("Accepted %s %s connection from %s",
type,
socket_address_family(addr) == AF_INET ? "IP" : "IPv6",
a);
*hostname = b;
return fd2;
};
default:
log_error("Rejected %s connection with unsupported family %d",
type, socket_address_family(addr));
close(fd2);
return -EINVAL;
}
}
static int dispatch_raw_connection_event(sd_event_source *event,
int fd,
uint32_t revents,
void *userdata) {
RemoteServer *s = userdata;
int fd2;
SocketAddress addr = {
.size = sizeof(union sockaddr_union),
.type = SOCK_STREAM,
};
char *hostname;
fd2 = accept_connection("raw", fd, &addr, &hostname);
if (fd2 < 0)
return fd2;
return add_source(s, fd2, hostname);
}
/**********************************************************************
**********************************************************************
**********************************************************************/
static const char* const journal_write_split_mode_table[_JOURNAL_WRITE_SPLIT_MAX] = {
[JOURNAL_WRITE_SPLIT_NONE] = "none",
[JOURNAL_WRITE_SPLIT_HOST] = "host",
};
DEFINE_PRIVATE_STRING_TABLE_LOOKUP(journal_write_split_mode, JournalWriteSplitMode);
static DEFINE_CONFIG_PARSE_ENUM(config_parse_write_split_mode,
journal_write_split_mode,
JournalWriteSplitMode,
"Failed to parse split mode setting");
static int parse_config(void) {
const ConfigTableItem items[] = {
{ "Remote", "SplitMode", config_parse_write_split_mode, 0, &arg_split_mode },
{ "Remote", "ServerKeyFile", config_parse_path, 0, &arg_key },
{ "Remote", "ServerCertificateFile", config_parse_path, 0, &arg_cert },
{ "Remote", "TrustedCertificateFile", config_parse_path, 0, &arg_trust },
{}};
int r;
r = config_parse(NULL, PKGSYSCONFDIR "/journal-remote.conf", NULL,
"Remote\0",
config_item_table_lookup, items,
false, false, NULL);
if (r < 0)
log_error("Failed to parse configuration file: %s", strerror(-r));
return r;
}
static void help(void) {
printf("%s [OPTIONS...] {FILE|-}...\n\n"
"Write external journal events to a journal file.\n\n"
"Options:\n"
" --url=URL Read events from systemd-journal-gatewayd at URL\n"
" --getter=COMMAND Read events from the output of COMMAND\n"
" --listen-raw=ADDR Listen for connections at ADDR\n"
" --listen-http=ADDR Listen for HTTP connections at ADDR\n"
" --listen-https=ADDR Listen for HTTPS connections at ADDR\n"
" -o --output=FILE|DIR Write output to FILE or DIR/external-*.journal\n"
" --[no-]compress Use XZ-compression in the output journal (default: yes)\n"
" --[no-]seal Use Event sealing in the output journal (default: no)\n"
" --key=FILENAME Specify key in PEM format (default:\n"
" \"" KEY_FILE "\")\n"
" --cert=FILENAME Specify certificate in PEM format (default:\n"
" \"" CERT_FILE "\")\n"
" --trust=FILENAME|all Specify CA certificate or disable checking (default:\n"
" \"" TRUST_FILE "\")\n"
" --gnutls-log=CATEGORY...\n"
" Specify a list of gnutls logging categories\n"
" -h --help Show this help and exit\n"
" --version Print version string and exit\n"
"\n"
"Note: file descriptors from sd_listen_fds() will be consumed, too.\n"
, program_invocation_short_name);
}
static int parse_argv(int argc, char *argv[]) {
enum {
ARG_VERSION = 0x100,
ARG_URL,
ARG_LISTEN_RAW,
ARG_LISTEN_HTTP,
ARG_LISTEN_HTTPS,
ARG_GETTER,
ARG_SPLIT_MODE,
ARG_COMPRESS,
ARG_NO_COMPRESS,
ARG_SEAL,
ARG_NO_SEAL,
ARG_KEY,
ARG_CERT,
ARG_TRUST,
ARG_GNUTLS_LOG,
};
static const struct option options[] = {
{ "help", no_argument, NULL, 'h' },
{ "version", no_argument, NULL, ARG_VERSION },
{ "url", required_argument, NULL, ARG_URL },
{ "getter", required_argument, NULL, ARG_GETTER },
{ "listen-raw", required_argument, NULL, ARG_LISTEN_RAW },
{ "listen-http", required_argument, NULL, ARG_LISTEN_HTTP },
{ "listen-https", required_argument, NULL, ARG_LISTEN_HTTPS },
{ "output", required_argument, NULL, 'o' },
{ "split-mode", required_argument, NULL, ARG_SPLIT_MODE },
{ "compress", no_argument, NULL, ARG_COMPRESS },
{ "no-compress", no_argument, NULL, ARG_NO_COMPRESS },
{ "seal", no_argument, NULL, ARG_SEAL },
{ "no-seal", no_argument, NULL, ARG_NO_SEAL },
{ "key", required_argument, NULL, ARG_KEY },
{ "cert", required_argument, NULL, ARG_CERT },
{ "trust", required_argument, NULL, ARG_TRUST },
{ "gnutls-log", required_argument, NULL, ARG_GNUTLS_LOG },
{}
};
int c, r;
bool type_a, type_b;
assert(argc >= 0);
assert(argv);
while ((c = getopt_long(argc, argv, "ho:", options, NULL)) >= 0)
switch(c) {
case 'h':
help();
return 0 /* done */;
case ARG_VERSION:
puts(PACKAGE_STRING);
puts(SYSTEMD_FEATURES);
return 0 /* done */;
case ARG_URL:
if (arg_url) {
log_error("cannot currently set more than one --url");
return -EINVAL;
}
arg_url = optarg;
break;
case ARG_GETTER:
if (arg_getter) {
log_error("cannot currently use --getter more than once");
return -EINVAL;
}
arg_getter = optarg;
break;
case ARG_LISTEN_RAW:
if (arg_listen_raw) {
log_error("cannot currently use --listen-raw more than once");
return -EINVAL;
}
arg_listen_raw = optarg;
break;
case ARG_LISTEN_HTTP:
if (arg_listen_http || http_socket >= 0) {
log_error("cannot currently use --listen-http more than once");
return -EINVAL;
}
r = fd_fd(optarg);
if (r >= 0)
http_socket = r;
else
arg_listen_http = optarg;
break;
case ARG_LISTEN_HTTPS:
if (arg_listen_https || https_socket >= 0) {
log_error("cannot currently use --listen-https more than once");
return -EINVAL;
}
r = fd_fd(optarg);
if (r >= 0)
https_socket = r;
else
arg_listen_https = optarg;
break;
case ARG_KEY:
if (arg_key) {
log_error("Key file specified twice");
return -EINVAL;
}
arg_key = strdup(optarg);
if (!arg_key)
return log_oom();
break;
case ARG_CERT:
if (arg_cert) {
log_error("Certificate file specified twice");
return -EINVAL;
}
arg_cert = strdup(optarg);
if (!arg_cert)
return log_oom();
break;
case ARG_TRUST:
if (arg_trust || arg_trust_all) {
log_error("Confusing trusted CA configuration");
return -EINVAL;
}
if (streq(optarg, "all"))
arg_trust_all = true;
else {
#ifdef HAVE_GNUTLS
arg_trust = strdup(optarg);
if (!arg_trust)
return log_oom();
#else
log_error("Option --trust is not available.");
return -EINVAL;
#endif
}
break;
case 'o':
if (arg_output) {
log_error("cannot use --output/-o more than once");
return -EINVAL;
}
arg_output = optarg;
break;
case ARG_SPLIT_MODE:
arg_split_mode = journal_write_split_mode_from_string(optarg);
if (arg_split_mode == _JOURNAL_WRITE_SPLIT_INVALID) {
log_error("Invalid split mode: %s", optarg);
return -EINVAL;
}
break;
case ARG_COMPRESS:
arg_compress = true;
break;
case ARG_NO_COMPRESS:
arg_compress = false;
break;
case ARG_SEAL:
arg_seal = true;
break;
case ARG_NO_SEAL:
arg_seal = false;
break;
case ARG_GNUTLS_LOG: {
#ifdef HAVE_GNUTLS
char *word, *state;
size_t size;
FOREACH_WORD_SEPARATOR(word, size, optarg, ",", state) {
char *cat;
cat = strndup(word, size);
if (!cat)
return log_oom();
if (strv_consume(&arg_gnutls_log, cat) < 0)
return log_oom();
}
break;
#else
log_error("Option --gnutls-log is not available.");
return -EINVAL;
#endif
}
case '?':
return -EINVAL;
default:
log_error("Unknown option code %c", c);
return -EINVAL;
}
if (optind < argc)
arg_files = argv + optind;
type_a = arg_getter || !strv_isempty(arg_files);
type_b = arg_url
|| arg_listen_raw
|| arg_listen_http || arg_listen_https
|| sd_listen_fds(false) > 0;
if (type_a && type_b) {
log_error("Cannot use file input or --getter with "
"--arg-listen-... or socket activation.");
return -EINVAL;
}
if (type_a) {
if (!arg_output) {
log_error("Option --output must be specified with file input or --getter.");
return -EINVAL;
}
arg_split_mode = JOURNAL_WRITE_SPLIT_NONE;
}
if (arg_split_mode == JOURNAL_WRITE_SPLIT_NONE
&& arg_output && is_dir(arg_output, true) > 0) {
log_error("For SplitMode=none, output must be a file.");
return -EINVAL;
}
if (arg_split_mode == JOURNAL_WRITE_SPLIT_HOST
&& arg_output && is_dir(arg_output, true) <= 0) {
log_error("For SplitMode=host, output must be a directory.");
return -EINVAL;
}
log_debug("Full config: SplitMode=%s Key=%s Cert=%s Trust=%s",
journal_write_split_mode_to_string(arg_split_mode),
strna(arg_key),
strna(arg_cert),
strna(arg_trust));
return 1 /* work to do */;
}
static int load_certificates(char **key, char **cert, char **trust) {
int r;
r = read_full_file(arg_key ?: KEY_FILE, key, NULL);
if (r < 0) {
log_error("Failed to read key from file '%s': %s",
arg_key ?: KEY_FILE, strerror(-r));
return r;
}
r = read_full_file(arg_cert ?: CERT_FILE, cert, NULL);
if (r < 0) {
log_error("Failed to read certificate from file '%s': %s",
arg_cert ?: CERT_FILE, strerror(-r));
return r;
}
if (arg_trust_all)
log_info("Certificate checking disabled.");
else {
r = read_full_file(arg_trust ?: TRUST_FILE, trust, NULL);
if (r < 0) {
log_error("Failed to read CA certificate file '%s': %s",
arg_trust ?: TRUST_FILE, strerror(-r));
return r;
}
}
return 0;
}
static int setup_gnutls_logger(char **categories) {
if (!arg_listen_http && !arg_listen_https)
return 0;
#ifdef HAVE_GNUTLS
{
char **cat;
int r;
gnutls_global_set_log_function(log_func_gnutls);
if (categories)
STRV_FOREACH(cat, categories) {
r = log_enable_gnutls_category(*cat);
if (r < 0)
return r;
}
else
log_reset_gnutls_level();
}
#endif
return 0;
}
int main(int argc, char **argv) {
RemoteServer s = {};
int r, r2;
_cleanup_free_ char *key = NULL, *cert = NULL, *trust = NULL;
uint64_t entry_count;
log_show_color(true);
log_parse_environment();
r = parse_config();
if (r < 0)
return EXIT_FAILURE;
r = parse_argv(argc, argv);
if (r <= 0)
return r == 0 ? EXIT_SUCCESS : EXIT_FAILURE;
r = setup_gnutls_logger(arg_gnutls_log);
if (r < 0)
return EXIT_FAILURE;
if (arg_listen_https || https_socket >= 0)
if (load_certificates(&key, &cert, &trust) < 0)
return EXIT_FAILURE;
if (remoteserver_init(&s, key, cert, trust) < 0)
return EXIT_FAILURE;
sd_event_set_watchdog(s.events, true);
log_debug("%s running as pid "PID_FMT,
program_invocation_short_name, getpid());
sd_notify(false,
"READY=1\n"
"STATUS=Processing requests...");
while (s.active) {
r = sd_event_get_state(s.events);
if (r < 0)
break;
if (r == SD_EVENT_FINISHED)
break;
r = sd_event_run(s.events, -1);
if (r < 0) {
log_error("Failed to run event loop: %s", strerror(-r));
break;
}
}
r2 = server_destroy(&s, &entry_count);
log_info("Finishing after writing %" PRIu64 " entries", entry_count);
sd_notify(false, "STATUS=Shutting down...");
free(arg_key);
free(arg_cert);
free(arg_trust);
return r >= 0 && r2 >= 0 ? EXIT_SUCCESS : EXIT_FAILURE;
}