diff --git a/man/journal-upload.conf.xml b/man/journal-upload.conf.xml
index 3de7044fd6..704ba5eea2 100644
--- a/man/journal-upload.conf.xml
+++ b/man/journal-upload.conf.xml
@@ -129,6 +129,28 @@
Takes a boolean value, enforces using compression without content encoding negotiation.
Defaults to false.
+
+
+
+ Header=
+
+ 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(:),
+ e.g. Name:Value.
+ Header name can contain alphanumeric values, _ and - 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 Header= to empty string clears all previous assignments.
+
+
+ Example:
+ Header=HeaderName: HeaderValue
+Header=HeaderName: NewValue
+Header=HeaderName: HeaderValue
+
+ adds HeaderName header with HeaderValue, NewValue to each HTTP request.
+
+
diff --git a/src/journal-remote/journal-header-util.c b/src/journal-remote/journal-header-util.c
new file mode 100644
index 0000000000..e5a94e74b3
--- /dev/null
+++ b/src/journal-remote/journal-header-util.c
@@ -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;
+}
diff --git a/src/journal-remote/journal-header-util.h b/src/journal-remote/journal-header-util.h
new file mode 100644
index 0000000000..a71a84fd7f
--- /dev/null
+++ b/src/journal-remote/journal-header-util.h
@@ -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);
diff --git a/src/journal-remote/journal-upload.c b/src/journal-remote/journal-upload.c
index f0af903d8a..7e866aea0a 100644
--- a/src/journal-remote/journal-upload.c
+++ b/src/journal-remote/journal-upload.c
@@ -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 },
{}
diff --git a/src/journal-remote/meson.build b/src/journal-remote/meson.build
index 0f3a91a621..12d7633364 100644
--- a/src/journal-remote/meson.build
+++ b/src/journal-remote/meson.build
@@ -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],
diff --git a/src/journal-remote/test-journal-header-util.c b/src/journal-remote/test-journal-header-util.c
new file mode 100644
index 0000000000..e88bb164ef
--- /dev/null
+++ b/src/journal-remote/test-journal-header-util.c
@@ -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);
diff --git a/test/units/TEST-04-JOURNAL.journal-remote.sh b/test/units/TEST-04-JOURNAL.journal-remote.sh
index df39a50b04..094fb3f441 100755
--- a/test/units/TEST-04-JOURNAL.journal-remote.sh
+++ b/test/units/TEST-04-JOURNAL.journal-remote.sh
@@ -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 </run/systemd/journal-upload.conf.d/99-test.conf <