journal-remote: added custom headers support

This commit is contained in:
Andrii Chubatiuk
2024-10-16 15:06:19 +03:00
committed by Yu Watanabe
parent e5dfe2cd8d
commit 5209e9cb05
7 changed files with 240 additions and 0 deletions

View File

@@ -129,6 +129,28 @@
<listitem><para>Takes a boolean value, enforces using compression without content encoding negotiation.
Defaults to <literal>false</literal>.</para>
<xi:include href="version-info.xml" xpointer="v258"/></listitem>
</varlistentry>
<varlistentry>
<term><varname>Header=</varname></term>
<listitem><para>Specifies an additional HTTP header to be added to each request to a URL.
Takes a pair of header name and value separated with a colon(<literal>:</literal>),
e.g. <literal>Name:Value</literal>.
Header name can contain alphanumeric values, <literal>_</literal> and <literal>-</literal> symbols additionally.
This option may be specified more than once, in which case all listed headers will be set.
If the same header name is listed more than once, all its unique values will be concatenated with comma.
Setting <varname>Header=</varname> to empty string clears all previous assignments.
</para>
<para>Example:
<programlisting>Header=HeaderName: HeaderValue
Header=HeaderName: NewValue
Header=HeaderName: HeaderValue</programlisting>
adds <literal>HeaderName</literal> header with <literal>HeaderValue, NewValue</literal> to each HTTP request.
</para>
<xi:include href="version-info.xml" xpointer="v258"/></listitem>
</varlistentry>
</variablelist>

View File

@@ -0,0 +1,112 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include "escape.h"
#include "journal-header-util.h"
#include "string-util.h"
#include "strv.h"
/* According to https://developers.cloudflare.com/rules/transform/request-header-modification/reference/header-format/
* HTTP header name can contain:
* - Alphanumeric characters: a-z, A-Z, and 0-9
* - The following special characters: - and _
*/
#define VALID_HEADER_NAME_CHARS \
ALPHANUMERICAL "_-"
#define HEADER_NAME_LENGTH_MAX 40
/* No RFC defines this limit, added for safety */
#define HEADER_VALUE_LENGTH_MAX 8000
/* According to https://developers.cloudflare.com/rules/transform/request-header-modification/reference/header-format/
* HTTP header value can contain:
* - Alphanumeric characters: a-z, A-Z, and 0-9
* - The following special characters: _ :;.,\/"'?!(){}[]@<>=-+*#$&`|~^%
*/
#define VALID_HEADER_VALUE_CHARS \
ALPHANUMERICAL "_ :;.,\\/'\"?!(){}[]@<>=-+*#$&`|~^%"
bool header_name_is_valid(const char *e) {
if (isempty(e))
return false;
if (strlen(e) > HEADER_NAME_LENGTH_MAX)
return false;
return in_charset(e, VALID_HEADER_NAME_CHARS);
}
bool header_value_is_valid(const char *e) {
if (!e)
return false;
if (strlen(e) > HEADER_VALUE_LENGTH_MAX)
return false;
return in_charset(e, VALID_HEADER_VALUE_CHARS);
}
int header_put(OrderedHashmap **headers, const char *name, const char *value) {
assert(headers);
if (!header_value_is_valid(value))
return -EINVAL;
if (!header_name_is_valid(name))
return -EINVAL;
return string_strv_ordered_hashmap_put(headers, name, value);
}
int config_parse_header(
const char *unit,
const char *filename,
unsigned line,
const char *section,
unsigned section_line,
const char *lvalue,
int ltype,
const char *rvalue,
void *data,
void *userdata) {
OrderedHashmap **headers = ASSERT_PTR(data);
_cleanup_free_ char *unescaped = NULL;
char *t;
int r;
assert(filename);
assert(lvalue);
assert(rvalue);
if (isempty(rvalue)) {
/* an empty string clears the previous assignments. */
*headers = ordered_hashmap_free(*headers);
return 1;
}
r = cunescape(rvalue, 0, &unescaped);
if (r < 0) {
log_syntax(unit, LOG_WARNING, filename, line, r,
"Failed to unescape headers, ignoring: %s", rvalue);
return 0;
}
t = strchr(unescaped, ':');
if (!t) {
log_syntax(unit, LOG_WARNING, filename, line, 0,
"Failed to parse header, name: value separator was not found, ignoring: %s", unescaped);
return 0;
}
*t++ = '\0';
r = header_put(headers, strstrip(unescaped), skip_leading_chars(t, WHITESPACE));
if (r < 0) {
log_syntax(unit, LOG_WARNING, filename, line, r,
"Failed to update headers, ignoring: %s", rvalue);
return 0;
}
return 1;
}

View File

@@ -0,0 +1,13 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#pragma once
#include "conf-parser.h"
#include "hashmap.h"
bool header_value_is_valid(const char *value);
bool header_name_is_valid(const char *value);
int header_put(OrderedHashmap **headers, const char *name, const char *value);
CONFIG_PARSER_PROTOTYPE(config_parse_header);

View File

@@ -15,11 +15,13 @@
#include "constants.h"
#include "daemon-util.h"
#include "env-file.h"
#include "escape.h"
#include "fd-util.h"
#include "fileio.h"
#include "format-util.h"
#include "fs-util.h"
#include "glob-util.h"
#include "journal-header-util.h"
#include "journal-upload.h"
#include "journal-util.h"
#include "log.h"
@@ -59,6 +61,7 @@ static int arg_follow = -1;
static char *arg_save_state = NULL;
static usec_t arg_network_timeout_usec = USEC_INFINITY;
static OrderedHashmap *arg_compression = NULL;
static OrderedHashmap *arg_headers = NULL;
static bool arg_force_compression = false;
STATIC_DESTRUCTOR_REGISTER(arg_url, freep);
@@ -72,6 +75,7 @@ STATIC_DESTRUCTOR_REGISTER(arg_machine, freep);
STATIC_DESTRUCTOR_REGISTER(arg_namespace, freep);
STATIC_DESTRUCTOR_REGISTER(arg_save_state, freep);
STATIC_DESTRUCTOR_REGISTER(arg_compression, ordered_hashmap_freep);
STATIC_DESTRUCTOR_REGISTER(arg_headers, ordered_hashmap_freep);
static void close_fd_input(Uploader *u);
@@ -226,6 +230,28 @@ int start_upload(Uploader *u,
h = l;
}
char **values;
const char *name;
ORDERED_HASHMAP_FOREACH_KEY(values, name, arg_headers) {
_cleanup_free_ char *joined = strv_join(values, ", ");
if (!joined)
return log_oom();
if (!header_value_is_valid(joined)) {
log_warning("Concatenated header value for %s is invalid, ignoring", name);
continue;
}
_cleanup_free_ char *header = strjoin(name, ": ", joined);
if (!header)
return log_oom();
l = curl_slist_append(h, header);
if (!l)
return log_oom();
h = l;
}
u->header = TAKE_PTR(h);
}
@@ -657,6 +683,7 @@ static int parse_config(void) {
{ "Upload", "ServerCertificateFile", config_parse_path_or_ignore, 0, &arg_cert },
{ "Upload", "TrustedCertificateFile", config_parse_path_or_ignore, 0, &arg_trust },
{ "Upload", "NetworkTimeoutSec", config_parse_sec, 0, &arg_network_timeout_usec },
{ "Upload", "Header", config_parse_header, 0, &arg_headers },
{ "Upload", "Compression", config_parse_compression, /* with_level */ true, &arg_compression },
{ "Upload", "ForceCompression", config_parse_bool, 0, &arg_force_compression },
{}

View File

@@ -2,6 +2,7 @@
systemd_journal_upload_sources = files(
'journal-compression-util.c',
'journal-header-util.c',
'journal-upload-journal.c',
'journal-upload.c',
)
@@ -90,6 +91,12 @@ executables += [
},
]
executables += [
test_template + {
'sources' : files('test-journal-header-util.c', 'journal-header-util.c'),
},
]
in_files = [
['journal-upload.conf',
conf.get('ENABLE_REMOTE') == 1 and conf.get('HAVE_LIBCURL') == 1 and install_sysconfdir_samples],

View File

@@ -0,0 +1,21 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include "hashmap.h"
#include "journal-header-util.h"
#include "tests.h"
TEST(header_put) {
_cleanup_ordered_hashmap_free_ OrderedHashmap *headers = NULL;
ASSERT_OK_POSITIVE(header_put(&headers, "NewName", "Val"));
ASSERT_OK_POSITIVE(header_put(&headers, "Name", "FirstName"));
ASSERT_OK_POSITIVE(header_put(&headers, "Name", "Override"));
ASSERT_OK_ZERO(header_put(&headers, "Name", "FirstName"));
ASSERT_ERROR(header_put(&headers, "InvalidN@me", "test"), EINVAL);
ASSERT_ERROR(header_put(&headers, "Name", NULL), EINVAL);
ASSERT_ERROR(header_put(&headers, NULL, "Value"), EINVAL);
ASSERT_OK_POSITIVE(header_put(&headers, "Name", ""));
ASSERT_ERROR(header_put(&headers, "", "Value"), EINVAL);
}
DEFINE_TEST_MAIN(LOG_DEBUG);

View File

@@ -272,3 +272,41 @@ EOF
rm /run/systemd/journal-upload.conf.d/99-test.conf
rm /run/systemd/journal-remote.conf.d/99-test.conf
done
# Let's test sending data with custom headers
echo "$TEST_MESSAGE" | systemd-cat -t "$TEST_TAG"
journalctl --sync
cat >/run/systemd/journal-remote.conf.d/99-test.conf <<EOF
[Remote]
SplitMode=host
ServerKeyFile=/run/systemd/remote-pki/server.key
ServerCertificateFile=/run/systemd/remote-pki/server.crt
TrustedCertificateFile=/run/systemd/remote-pki/ca.crt
EOF
cat >/run/systemd/journal-upload.conf.d/99-test.conf <<EOF
[Upload]
URL=https://localhost:19532
Header=TestHeader: TestValue
ServerKeyFile=/run/systemd/remote-pki/client.key
ServerCertificateFile=/run/systemd/remote-pki/client.crt
TrustedCertificateFile=/run/systemd/remote-pki/ca.crt
EOF
systemd-analyze cat-config systemd/journal-remote.conf
systemd-analyze cat-config systemd/journal-upload.conf
systemctl restart systemd-journal-remote.socket
systemctl restart systemd-journal-upload
timeout 15 bash -xec 'until systemctl -q is-active systemd-journal-remote.service; do sleep 1; done'
systemctl status systemd-journal-{remote,upload}
# It may take a bit until the whole journal is transferred
timeout 30 bash -xec "until journalctl --directory=/var/log/journal/remote --identifier='$TEST_TAG' --grep='$TEST_MESSAGE'; do sleep 1; done"
systemctl stop systemd-journal-upload
systemctl stop systemd-journal-remote.{socket,service}
rm -rf /var/log/journal/remote/*
rm /run/systemd/journal-upload.conf.d/99-test.conf
rm /run/systemd/journal-remote.conf.d/99-test.conf