From: Zbigniew Jędrzejewski-Szmek Date: Fri, 6 Mar 2026 10:36:13 +0000 (+0100) Subject: report: add basic upload functionality X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=5bbbe210a4e3856385d95e16074d8aa98cff909b;p=thirdparty%2Fsystemd.git report: add basic upload functionality --- diff --git a/src/report/meson.build b/src/report/meson.build index 36227bed9f5..6e0c231be32 100644 --- a/src/report/meson.build +++ b/src/report/meson.build @@ -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 index 00000000000..897d7d1159f --- /dev/null +++ b/src/report/report-upload.c @@ -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 /* 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 +} diff --git a/src/report/report.c b/src/report/report.c index 96ff28dccd9..b02a48b28b4 100644 --- a/src/report/report.c +++ b/src/report/report.c @@ -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 index 00000000000..69c9c5851b7 --- /dev/null +++ b/src/report/report.h @@ -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);