]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
journal-remote: added custom headers support
authorAndrii Chubatiuk <andrew.chubatiuk@gmail.com>
Wed, 16 Oct 2024 12:06:19 +0000 (15:06 +0300)
committerYu Watanabe <watanabe.yu+github@gmail.com>
Fri, 14 Mar 2025 22:27:38 +0000 (07:27 +0900)
man/journal-upload.conf.xml
src/journal-remote/journal-header-util.c [new file with mode: 0644]
src/journal-remote/journal-header-util.h [new file with mode: 0644]
src/journal-remote/journal-upload.c
src/journal-remote/meson.build
src/journal-remote/test-journal-header-util.c [new file with mode: 0644]
test/units/TEST-04-JOURNAL.journal-remote.sh

index 3de7044fd665ead042b15506ed08af2f867f051e..704ba5eea20b21742fcb4986f4a33d457f87b247 100644 (file)
         <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>
diff --git a/src/journal-remote/journal-header-util.c b/src/journal-remote/journal-header-util.c
new file mode 100644 (file)
index 0000000..e5a94e7
--- /dev/null
@@ -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 (file)
index 0000000..a71a84f
--- /dev/null
@@ -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);
index f0af903d8a5c86cf298a7f8d2c7dcb094bf30d6c..7e866aea0a6656cf2db0b79ed953f8904585fff7 100644 (file)
 #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    },
                 {}
index 0f3a91a621dc11faa29cca6bfb0d7368c2b87959..12d763336466d09c5ed3b024e8d315ee220912c4 100644 (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],
diff --git a/src/journal-remote/test-journal-header-util.c b/src/journal-remote/test-journal-header-util.c
new file mode 100644 (file)
index 0000000..e88bb16
--- /dev/null
@@ -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);
index df39a50b049443c5bb30e4c03a37cc91f3294c2d..094fb3f4411f6f48e95fd4de0175c3c1fc9623ad 100755 (executable)
@@ -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