]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
report: add basic upload functionality
authorZbigniew Jędrzejewski-Szmek <zbyszek@amutable.com>
Fri, 6 Mar 2026 10:36:13 +0000 (11:36 +0100)
committerZbigniew Jędrzejewski-Szmek <zbyszek@amutable.com>
Thu, 16 Apr 2026 19:12:20 +0000 (21:12 +0200)
src/report/meson.build
src/report/report-upload.c [new file with mode: 0644]
src/report/report.c
src/report/report.h [new file with mode: 0644]

index 36227bed9f56bb5170caf3c8c7455dee1ec5d935..6e0c231be3245aaa68371c6b25caa13004a3b1de 100644 (file)
@@ -4,7 +4,12 @@ executables += [
         libexec_template + {
                 'name' : 'systemd-report',
                 'public' : true,
-                'sources' : files('report.c'),
+                'sources' : files(
+                        'report.c',
+                        'report-upload.c',
+                ),
+                'link_with' : [libcurlutil_static, libshared],
+                'dependencies' : [libcurl],
         },
 
         libexec_template + {
diff --git a/src/report/report-upload.c b/src/report/report-upload.c
new file mode 100644 (file)
index 0000000..897d7d1
--- /dev/null
@@ -0,0 +1,200 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "sd-json.h"
+
+#include "log.h"
+#include "report.h"
+#include "string-util.h"
+#include "strv.h"
+#include "time-util.h"
+#include "utf8.h"
+#include "version.h"
+
+#if HAVE_LIBCURL
+#include "curl-util.h"
+#include <curl/easy.h>   /* Sadly this fails if ordered first. */
+
+static size_t output_callback(char *buf,
+                              size_t size,
+                              size_t nmemb,
+                              void *userp) {
+
+        Context *context = ASSERT_PTR(userp);
+        int r;
+
+        assert(size == 1);  /* The docs say that this is always true. */
+
+        log_debug("Got an answer from the server (%zu bytes)", nmemb);
+
+        if (nmemb != 0) {
+                if (memchr(buf, 0, nmemb)) {
+                        log_warning("Server answer contains an embedded NUL, refusing.");
+                        return 0;
+                }
+
+                r = iovw_append(&context->upload_answer, buf, nmemb);
+                if (r < 0) {
+                        log_warning("Failed to store server answer (%zu bytes): out of memory", nmemb);
+                        return 0;  /* Returning < nmemb signals failure */
+                }
+        }
+
+        return nmemb;
+}
+
+static int build_json_report(Context *context, sd_json_variant **ret) {
+        /* Convert the variant array to a JSON report. */
+
+        assert(context);
+        assert(ret);
+
+        usec_t ts = now(CLOCK_REALTIME);
+        int r;
+
+        const char *ident;
+        if (IN_SET(context->action, ACTION_LIST_METRICS, ACTION_DESCRIBE_METRICS))
+                ident = "metrics";
+        else if (IN_SET(context->action, ACTION_LIST_FACTS, ACTION_DESCRIBE_FACTS))
+                ident = "facts";
+        else
+                assert_not_reached();
+
+        r = sd_json_buildo(ret,
+                           SD_JSON_BUILD_PAIR("timestamp",
+                                              SD_JSON_BUILD_STRING(FORMAT_TIMESTAMP_STYLE(ts, TIMESTAMP_UTC))),
+                           SD_JSON_BUILD_PAIR(ident,
+                                              SD_JSON_BUILD_VARIANT_ARRAY(context->metrics, context->n_metrics)));
+        if (r < 0)
+                return log_error_errno(r, "Failed to build JSON data: %m");
+        return 0;
+}
+#endif
+
+int upload_collected(Context *context) {
+#if HAVE_LIBCURL
+        _cleanup_(curl_slist_free_allp) struct curl_slist *header = NULL;
+        char error[CURL_ERROR_SIZE] = {};
+        _cleanup_free_ char *json = NULL;
+        int r;
+
+        {
+                /* Convert our variant array to a JSON report.
+                 * We won't need the JSON structure again, so free it quickly. */
+
+                _cleanup_(sd_json_variant_unrefp) sd_json_variant *vl = NULL;
+                r = build_json_report(context, &vl);
+                if (r < 0)
+                        return r;
+
+                r = sd_json_variant_format(vl, /* flags= */ 0, &json);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to format JSON data: %m");
+        }
+
+        r = curl_append_to_header(&header,
+                                  STRV_MAKE("Content-Type: application/json",
+                                            "Accept: application/json"));
+        if (r < 0)
+                return log_error_errno(r, "Failed to create curl header: %m");
+
+        _cleanup_(curl_easy_cleanupp) CURL *curl = curl_easy_init();
+        if (!curl)
+                return log_error_errno(SYNTHETIC_ERRNO(ENOSR),
+                                       "Call to curl_easy_init failed.");
+
+        /* If configured, set a timeout for the curl operation. */
+        if (arg_network_timeout_usec != USEC_INFINITY &&
+            !easy_setopt(curl, LOG_ERR, CURLOPT_TIMEOUT,
+                         (long) DIV_ROUND_UP(arg_network_timeout_usec, USEC_PER_SEC)))
+                return -EXFULL;
+
+        /* Tell it to POST to the URL */
+        if (!easy_setopt(curl, LOG_ERR, CURLOPT_POST, 1L))
+                return -EXFULL;
+
+        if (!easy_setopt(curl, LOG_ERR, CURLOPT_ERRORBUFFER, error))
+                return -EXFULL;
+
+        /* Where to write to */
+        if (!easy_setopt(curl, LOG_ERR, CURLOPT_WRITEFUNCTION, output_callback))
+                return -EXFULL;
+
+        if (!easy_setopt(curl, LOG_ERR, CURLOPT_WRITEDATA, context))
+                return -EXFULL;
+
+        if (!easy_setopt(curl, LOG_ERR, CURLOPT_HTTPHEADER, header))
+                return -EXFULL;
+
+        if (DEBUG_LOGGING)
+                /* enable verbose for easier tracing */
+                (void) easy_setopt(curl, LOG_WARNING, CURLOPT_VERBOSE, 1L);
+
+        (void) easy_setopt(curl, LOG_WARNING,
+                           CURLOPT_USERAGENT, "systemd-report " GIT_VERSION);
+
+        if (!streq_ptr(arg_key, "-") && (arg_key || startswith(arg_url, "https://"))) {
+                if (!easy_setopt(curl, LOG_ERR, CURLOPT_SSLKEY, arg_key ?: REPORT_PRIV_KEY_FILE))
+                        return -EXFULL;
+                if (!easy_setopt(curl, LOG_ERR, CURLOPT_SSLCERT, arg_cert ?: REPORT_CERT_FILE))
+                        return -EXFULL;
+        }
+
+        if (STRPTR_IN_SET(arg_trust, "-", "all")) {
+                log_info("Server certificate verification disabled.");
+                if (!easy_setopt(curl, LOG_ERR, CURLOPT_SSL_VERIFYPEER, 0L))
+                        return -EUCLEAN;
+                if (!easy_setopt(curl, LOG_ERR, CURLOPT_SSL_VERIFYHOST, 0L))
+                        return -EUCLEAN;
+        } else if (arg_trust || startswith(arg_url, "https://")) {
+                if (!easy_setopt(curl, LOG_ERR, CURLOPT_CAINFO, arg_trust ?: REPORT_TRUST_FILE))
+                        return -EXFULL;
+        }
+
+        if (startswith(arg_url, "https://"))
+                (void) easy_setopt(curl, LOG_WARNING, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1_2);
+
+        /* Upload to this place */
+        if (!easy_setopt(curl, LOG_ERR, CURLOPT_URL, arg_url))
+                return -EXFULL;
+
+        if (!easy_setopt(curl, LOG_ERR, CURLOPT_POSTFIELDS, json))
+                return -EXFULL;
+
+        CURLcode code = curl_easy_perform(curl);
+        if (code != CURLE_OK)
+                return log_error_errno(SYNTHETIC_ERRNO(EIO),
+                                       "Upload to %s failed: %s", arg_url,
+                                       empty_to_null(&error[0]) ?: curl_easy_strerror(code));
+
+        long status;
+        code = curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status);
+        if (code != CURLE_OK)
+                return log_error_errno(SYNTHETIC_ERRNO(EUCLEAN),
+                                       "Failed to retrieve response code: %s",
+                                       curl_easy_strerror(code));
+
+        _cleanup_free_ char *ans = iovw_to_cstring(&context->upload_answer);
+        if (!ans)
+                return log_oom();
+
+        if (!utf8_is_valid(ans))
+                return log_error_errno(SYNTHETIC_ERRNO(EUCLEAN),
+                                       "Upload to %s failed with code %ld and an invalid UTF-8 answer.",
+                                       arg_url, status);
+
+        if (status >= 300)
+                return log_error_errno(SYNTHETIC_ERRNO(EIO),
+                                       "Upload to %s failed with code %ld: %s",
+                                       arg_url, status, strna(ans));
+        if (status < 200)
+                return log_error_errno(SYNTHETIC_ERRNO(EIO),
+                                       "Upload to %s finished with unexpected code %ld: %s",
+                                       arg_url, status, strna(ans));
+        log_info("Upload to %s finished successfully with code %ld: %s",
+                 arg_url, status, strna(ans));
+        return 0;
+#else
+        return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
+                               "Compiled without libcurl.");
+#endif
+}
index 96ff28dccd9b30a61f8e0ab105f8b5f78e2386aa..b02a48b28b46a246539e51ada1966313edabdf7d 100644 (file)
@@ -16,6 +16,7 @@
 #include "path-lookup.h"
 #include "pretty-print.h"
 #include "recurse-dir.h"
+#include "report.h"
 #include "runtime-scope.h"
 #include "set.h"
 #include "sort-util.h"
@@ -35,27 +36,17 @@ static bool arg_legend = true;
 static RuntimeScope arg_runtime_scope = RUNTIME_SCOPE_SYSTEM;
 static sd_json_format_flags_t arg_json_format_flags = SD_JSON_FORMAT_OFF|SD_JSON_FORMAT_PRETTY_AUTO|SD_JSON_FORMAT_COLOR_AUTO;
 static char **arg_matches = NULL;
+char *arg_url = NULL;
+char *arg_key = NULL;
+char *arg_cert = NULL;
+char *arg_trust = NULL;
+usec_t arg_network_timeout_usec = TIMEOUT_USEC;
 
 STATIC_DESTRUCTOR_REGISTER(arg_matches, strv_freep);
-
-typedef enum Action {
-        ACTION_LIST_METRICS,
-        ACTION_DESCRIBE_METRICS,
-        ACTION_LIST_FACTS,
-        ACTION_DESCRIBE_FACTS,
-        _ACTION_MAX,
-        _ACTION_INVALID = -EINVAL,
-} Action;
-
-/* The structure for collected "metrics" or "facts". The fields
- * are prefixed with just "metrics" for brevity. */
-typedef struct Context {
-        Action action;
-        sd_event *event;
-        Set *link_infos;
-        sd_json_variant **metrics;  /* Collected metrics or facts for sorting */
-        size_t n_metrics, n_skipped_metrics, n_invalid_metrics;
-} Context;
+STATIC_DESTRUCTOR_REGISTER(arg_url, freep);
+STATIC_DESTRUCTOR_REGISTER(arg_key, freep);
+STATIC_DESTRUCTOR_REGISTER(arg_cert, freep);
+STATIC_DESTRUCTOR_REGISTER(arg_trust, freep);
 
 typedef struct LinkInfo {
         Context *context;
@@ -76,9 +67,12 @@ static void context_done(Context *context) {
         if (!context)
                 return;
 
-        set_free(context->link_infos);
+        context->event = sd_event_unref(context->event);
+        context->link_infos = set_free(context->link_infos);
         sd_json_variant_unref_many(context->metrics, context->n_metrics);
-        sd_event_unref(context->event);
+        context->metrics = NULL;
+        context->n_metrics = 0;
+        iovw_done_free(&context->upload_answer);
 }
 
 DEFINE_TRIVIAL_CLEANUP_FUNC(LinkInfo*, link_info_free);
@@ -569,6 +563,7 @@ static int output_collected(Context *context) {
         }
 
         _cleanup_(table_unrefp) Table *table = NULL;
+
         switch(context->action) {
 
         case ACTION_LIST_METRICS:
@@ -771,7 +766,10 @@ static int verb_metrics(int argc, char *argv[], uintptr_t data, void *userdata)
                 if (r < 0)
                         return log_error_errno(r, "Failed to run event loop: %m");
 
-                r = output_collected(&context);
+                if (arg_url)
+                        r = upload_collected(&context);
+                else
+                        r = output_collected(&context);
                 if (r < 0)
                         return r;
         }
@@ -855,7 +853,10 @@ static int verb_facts(int argc, char *argv[], uintptr_t data, void *userdata) {
                 if (r < 0)
                         return log_error_errno(r, "Failed to run event loop: %m");
 
-                r = output_collected(&context);
+                if (arg_url)
+                        r = upload_collected(&context);
+                else
+                        r = output_collected(&context);
                 if (r < 0)
                         return r;
         }
@@ -1017,8 +1018,45 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) {
                 OPTION_COMMON_LOWERCASE_J:
                         arg_json_format_flags = SD_JSON_FORMAT_PRETTY_AUTO|SD_JSON_FORMAT_COLOR_AUTO;
                         break;
+
+                OPTION_LONG("url", "URL",
+                            "Upload to this address"):
+                        r = free_and_strdup_warn(&arg_url, arg);
+                        if (r < 0)
+                                return r;
+                        break;
+
+                OPTION_LONG("key", "FILENAME",
+                            "Specify key in PEM format (default: \"" REPORT_PRIV_KEY_FILE "\")"):
+                        r = free_and_strdup_warn(&arg_key, arg);
+                        if (r < 0)
+                                return r;
+                        break;
+
+                OPTION_LONG("cert", "FILENAME",
+                            "Specify certificate in PEM format (default: \"" REPORT_CERT_FILE "\")"):
+                        r = free_and_strdup_warn(&arg_cert, arg);
+                        if (r < 0)
+                                return r;
+                        break;
+
+                OPTION_LONG("trust", "FILENAME|all",
+                            "Specify CA certificate or disable checking (default: \"" REPORT_TRUST_FILE "\")"):
+                        r = free_and_strdup_warn(&arg_trust, arg);
+                        if (r < 0)
+                                return r;
+                        break;
+
+                OPTION_LONG("network-timeout", "SEC", "Specify timeout for network upload operation"):
+                        r = parse_sec(arg, &arg_network_timeout_usec);
+                        if (r < 0)
+                                return log_error_errno(r, "Failed to parse --network-timeout value: %s", arg);
+                        break;
                 }
 
+        if ((arg_url || arg_key || arg_cert || arg_trust) && !HAVE_LIBCURL)
+                return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Compiled without libcurl.");
+
         *ret_args = option_parser_get_args(&state);
         return 1;
 }
diff --git a/src/report/report.h b/src/report/report.h
new file mode 100644 (file)
index 0000000..69c9c58
--- /dev/null
@@ -0,0 +1,35 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "shared-forward.h"
+
+#include "iovec-wrapper.h"
+
+#define REPORT_PRIV_KEY_FILE CERTIFICATE_ROOT "/private/systemd-report.pem"
+#define REPORT_CERT_FILE     CERTIFICATE_ROOT "/certs/systemd-report.pem"
+#define REPORT_TRUST_FILE    CERTIFICATE_ROOT "/ca/trusted.pem"
+
+extern char *arg_url, *arg_key, *arg_cert, *arg_trust;
+extern usec_t arg_network_timeout_usec;
+
+typedef enum Action {
+        ACTION_LIST_METRICS,
+        ACTION_DESCRIBE_METRICS,
+        ACTION_LIST_FACTS,
+        ACTION_DESCRIBE_FACTS,
+        _ACTION_MAX,
+        _ACTION_INVALID = -EINVAL,
+} Action;
+
+/* The structure for collected "metrics" or "facts". The fields
+ * are prefixed with just "metrics" for brevity. */
+typedef struct Context {
+        Action action;
+        sd_event *event;
+        Set *link_infos;
+        sd_json_variant **metrics;  /* Collected metrics or facts for sorting */
+        size_t n_metrics, n_skipped_metrics, n_invalid_metrics;
+        struct iovec_wrapper upload_answer;
+} Context;
+
+int upload_collected(Context *context);