<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>
--- /dev/null
+/* 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;
+}
--- /dev/null
+/* 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);
#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"
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);
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);
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);
}
{ "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 },
{}
systemd_journal_upload_sources = files(
'journal-compression-util.c',
+ 'journal-header-util.c',
'journal-upload-journal.c',
'journal-upload.c',
)
},
]
+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],
--- /dev/null
+/* 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);
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