report: add ability to upload reports over http (#41440)

This commit is contained in:
Zbigniew Jędrzejewski-Szmek
2026-04-16 22:35:05 +02:00
committed by GitHub
24 changed files with 950 additions and 266 deletions
+1
View File
@@ -45,6 +45,7 @@ wrap=(
/usr/lib/systemd/tests/testdata/TEST-74-AUX-UTILS.units/proxy-echo.py
/usr/libexec/polkit-1/polkitd
/usr/lib/systemd/tests/integration-tests/TEST-74-AUX-UTILS/TEST-74-AUX-UTILS.units/fake-imds.py
/usr/lib/systemd/tests/integration-tests/TEST-74-AUX-UTILS/TEST-74-AUX-UTILS.units/fake-report-server.py
agetty
btrfs
capsh
+53 -23
View File
@@ -7,10 +7,6 @@
#include "iovec-wrapper.h"
#include "string-util.h"
struct iovec_wrapper *iovw_new(void) {
return new0(struct iovec_wrapper, 1);
}
void iovw_done(struct iovec_wrapper *iovw) {
assert(iovw);
@@ -27,22 +23,6 @@ void iovw_done_free(struct iovec_wrapper *iovw) {
iovw_done(iovw);
}
struct iovec_wrapper *iovw_free_free(struct iovec_wrapper *iovw) {
if (!iovw)
return NULL;
iovw_done_free(iovw);
return mfree(iovw);
}
struct iovec_wrapper *iovw_free(struct iovec_wrapper *iovw) {
if (!iovw)
return NULL;
iovw_done(iovw);
return mfree(iovw);
}
int iovw_put(struct iovec_wrapper *iovw, void *data, size_t len) {
assert(iovw);
@@ -61,6 +41,28 @@ int iovw_put(struct iovec_wrapper *iovw, void *data, size_t len) {
return 0;
}
int iovw_append(struct iovec_wrapper *iovw, const void *data, size_t len) {
if (len == 0)
return 0;
void *c = memdup(data, len);
if (!c)
return -ENOMEM;
return iovw_put(iovw, c, len);
}
int iovw_consume(struct iovec_wrapper *iovw, void *data, size_t len) {
/* Move data into iovw or free on error */
int r;
r = iovw_put(iovw, data, len);
if (r < 0)
free(data);
return r;
}
int iovw_put_string_field_full(struct iovec_wrapper *iovw, bool replace, const char *field, const char *value) {
_cleanup_free_ char *x = NULL;
int r;
@@ -118,13 +120,12 @@ void iovw_rebase(struct iovec_wrapper *iovw, void *old, void *new) {
}
size_t iovw_size(const struct iovec_wrapper *iovw) {
if (!iovw)
return 0;
assert(iovw);
return iovec_total_size(iovw->iovec, iovw->count);
}
int iovw_append(struct iovec_wrapper *target, const struct iovec_wrapper *source) {
int iovw_append_iovw(struct iovec_wrapper *target, const struct iovec_wrapper *source) {
size_t original_count;
int r;
@@ -160,3 +161,32 @@ rollback:
target->count = original_count;
return r;
}
char* iovw_to_cstring(const struct iovec_wrapper *iovw) {
size_t size;
char *p, *ans;
assert(iovw);
/* Squish a series of iovecs into a C string. Embedded NULs are not allowed.
* The caller is expected to filter them out when populating the data. */
size = iovw_size(iovw);
if (size == SIZE_MAX)
return NULL; /* Prevent theoretical overflow */
size ++;
p = ans = new(char, size);
if (!ans)
return NULL;
FOREACH_ARRAY(iovec, iovw->iovec, iovw->count) {
assert(!memchr(iovec->iov_base, 0, iovec->iov_len));
p = mempcpy(p, iovec->iov_base, iovec->iov_len);
}
*p = '\0';
return ans;
}
+4 -17
View File
@@ -8,26 +8,12 @@ struct iovec_wrapper {
size_t count;
};
struct iovec_wrapper *iovw_new(void);
struct iovec_wrapper *iovw_free(struct iovec_wrapper *iovw);
struct iovec_wrapper *iovw_free_free(struct iovec_wrapper *iovw);
DEFINE_TRIVIAL_CLEANUP_FUNC(struct iovec_wrapper*, iovw_free_free);
void iovw_done_free(struct iovec_wrapper *iovw);
void iovw_done(struct iovec_wrapper *iovw);
int iovw_put(struct iovec_wrapper *iovw, void *data, size_t len);
static inline int iovw_consume(struct iovec_wrapper *iovw, void *data, size_t len) {
/* Move data into iovw or free on error */
int r;
r = iovw_put(iovw, data, len);
if (r < 0)
free(data);
return r;
}
int iovw_append(struct iovec_wrapper *iovw, const void *data, size_t len);
int iovw_consume(struct iovec_wrapper *iovw, void *data, size_t len);
static inline bool iovw_isempty(const struct iovec_wrapper *iovw) {
return !iovw || iovw->count == 0;
@@ -46,4 +32,5 @@ int iovw_put_string_fieldf_full(struct iovec_wrapper *iovw, bool replace, const
int iovw_put_string_field_free(struct iovec_wrapper *iovw, const char *field, char *value);
void iovw_rebase(struct iovec_wrapper *iovw, void *old, void *new);
size_t iovw_size(const struct iovec_wrapper *iovw);
int iovw_append(struct iovec_wrapper *target, const struct iovec_wrapper *source);
int iovw_append_iovw(struct iovec_wrapper *target, const struct iovec_wrapper *source);
char* iovw_to_cstring(const struct iovec_wrapper *iovw);
+1 -1
View File
@@ -50,7 +50,7 @@ int coredump_backtrace(int argc, char *argv[]) {
} else {
/* The imported iovecs are not supposed to be freed by us so let's copy and merge them at the
* end of the array. */
r = iovw_append(&context.iovw, &importer.iovw);
r = iovw_append_iovw(&context.iovw, &importer.iovw);
if (r < 0)
return r;
}
+7 -18
View File
@@ -18,6 +18,7 @@
#include "chase.h"
#include "copy.h"
#include "creds-util.h"
#include "curl-util.h"
#include "device-private.h"
#include "dns-rr.h"
#include "errno-util.h"
@@ -53,8 +54,6 @@
#include "web-util.h"
#include "xattr-util.h"
#include "../import/curl-util.h"
/* This implements a client to the AWS' and Azure's "Instance Metadata Service", as well as GCP's "VM
* Metadata", i.e.:
*
@@ -1113,20 +1112,14 @@ static int context_acquire_data(Context *c) {
if (!token_header)
return context_log_oom(c);
struct curl_slist *n = curl_slist_append(c->request_header_data, token_header);
if (!n)
return context_log_oom(c);
c->request_header_data = n;
r = curl_append_to_header(&c->request_header_data, STRV_MAKE(token_header));
if (r < 0)
return context_log_errno(c, LOG_ERR, r, "Failed to create curl header: %m");
}
STRV_FOREACH(i, arg_extra_header) {
struct curl_slist *n = curl_slist_append(c->request_header_data, *i);
if (!n)
return context_log_oom(c);
c->request_header_data = n;
}
r = curl_append_to_header(&c->request_header_data, arg_extra_header);
if (r < 0)
return context_log_errno(c, LOG_ERR, r, "Failed to create curl header: %m");
if (c->request_header_data)
if (curl_easy_setopt(c->curl_data, CURLOPT_HTTPHEADER, c->request_header_data) != CURLE_OK)
@@ -2250,10 +2243,6 @@ static bool http_header_name_valid(const char *a) {
return a && ascii_is_valid(a) && !string_has_cc(a, /* ok= */ NULL) && !strchr(a, ':');
}
static bool http_header_valid(const char *a) {
return a && ascii_is_valid(a) && !string_has_cc(a, /* ok= */ NULL) && strchr(a, ':');
}
static int parse_argv(int argc, char *argv[]) {
int r;
+3 -2
View File
@@ -11,8 +11,9 @@ executables += [
'sources' : files(
'imdsd.c',
'imds-util.c'
) + import_curl_util_c,
'dependencies' : [ libcurl ],
),
'link_with' : [libcurlutil_static, libshared],
'dependencies' : [libcurl],
},
libexec_template + {
'name' : 'systemd-imds',
+2 -3
View File
@@ -1,7 +1,5 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
import_curl_util_c = files('curl-util.c')
if conf.get('ENABLE_IMPORTD') != 1
subdir_done()
endif
@@ -35,8 +33,9 @@ executables += [
'pull-oci.c',
'pull-raw.c',
'pull-tar.c',
) + import_curl_util_c,
),
'objects' : ['systemd-importd'],
'link_with' : [libcurlutil_static, libshared],
'dependencies' : common_deps + [
libopenssl,
],
+49 -67
View File
@@ -11,6 +11,7 @@
#include "alloc-util.h"
#include "build.h"
#include "conf-parser.h"
#include "curl-util.h"
#include "daemon-util.h"
#include "env-file.h"
#include "extract-word.h"
@@ -81,20 +82,6 @@ static void close_fd_input(Uploader *u);
#define STATE_FILE "/var/lib/systemd/journal-upload/state"
#define easy_setopt(curl, opt, value, level, cmd) \
do { \
code = curl_easy_setopt(curl, opt, value); \
if (code) { \
log_full(level, \
"curl_easy_setopt " #opt " failed: %s", \
curl_easy_strerror(code)); \
cmd; \
} \
} while (0)
DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(CURL*, curl_easy_cleanup, NULL);
DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(struct curl_slist*, curl_slist_free_all, NULL);
static size_t output_callback(char *buf,
size_t size,
size_t nmemb,
@@ -194,8 +181,6 @@ int start_upload(Uploader *u,
size_t nmemb,
void *userdata),
void *data) {
CURLcode code;
assert(u);
assert(input_callback);
@@ -262,64 +247,63 @@ int start_upload(Uploader *u,
"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, CURLOPT_TIMEOUT,
(long) DIV_ROUND_UP(arg_network_timeout_usec, USEC_PER_SEC),
LOG_ERR, return -EXFULL);
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 */
easy_setopt(curl, CURLOPT_POST, 1L,
LOG_ERR, return -EXFULL);
if (!easy_setopt(curl, LOG_ERR, CURLOPT_POST, 1L))
return -EXFULL;
easy_setopt(curl, CURLOPT_ERRORBUFFER, u->error,
LOG_ERR, return -EXFULL);
if (!easy_setopt(curl, LOG_ERR, CURLOPT_ERRORBUFFER, u->error))
return -EXFULL;
/* set where to write to */
easy_setopt(curl, CURLOPT_WRITEFUNCTION, output_callback,
LOG_ERR, return -EXFULL);
if (!easy_setopt(curl, LOG_ERR, CURLOPT_WRITEFUNCTION, output_callback))
return -EXFULL;
easy_setopt(curl, CURLOPT_WRITEDATA, data,
LOG_ERR, return -EXFULL);
if (!easy_setopt(curl, LOG_ERR, CURLOPT_WRITEDATA, data))
return -EXFULL;
/* set where to read from */
easy_setopt(curl, CURLOPT_READFUNCTION, input_callback,
LOG_ERR, return -EXFULL);
if (!easy_setopt(curl, LOG_ERR, CURLOPT_READFUNCTION, input_callback))
return -EXFULL;
easy_setopt(curl, CURLOPT_READDATA, data,
LOG_ERR, return -EXFULL);
if (!easy_setopt(curl, LOG_ERR, CURLOPT_READDATA, data))
return -EXFULL;
/* use our special own mime type and chunked transfer */
easy_setopt(curl, CURLOPT_HTTPHEADER, u->header,
LOG_ERR, return -EXFULL);
if (!easy_setopt(curl, LOG_ERR, CURLOPT_HTTPHEADER, u->header))
return -EXFULL;
if (DEBUG_LOGGING)
/* enable verbose for easier tracing */
easy_setopt(curl, CURLOPT_VERBOSE, 1L, LOG_WARNING, );
(void) easy_setopt(curl, LOG_WARNING, CURLOPT_VERBOSE, 1L);
easy_setopt(curl, CURLOPT_USERAGENT,
"systemd-journal-upload " GIT_VERSION,
LOG_WARNING, );
(void) easy_setopt(curl, LOG_WARNING,
CURLOPT_USERAGENT, "systemd-journal-upload " GIT_VERSION);
if (!streq_ptr(arg_key, "-") && (arg_key || startswith(u->url, "https://"))) {
easy_setopt(curl, CURLOPT_SSLKEY, arg_key ?: PRIV_KEY_FILE,
LOG_ERR, return -EXFULL);
easy_setopt(curl, CURLOPT_SSLCERT, arg_cert ?: CERT_FILE,
LOG_ERR, return -EXFULL);
if (!easy_setopt(curl, LOG_ERR, CURLOPT_SSLKEY, arg_key ?: PRIV_KEY_FILE))
return -EXFULL;
if (!easy_setopt(curl, LOG_ERR, CURLOPT_SSLCERT, arg_cert ?: CERT_FILE))
return -EXFULL;
}
if (STRPTR_IN_SET(arg_trust, "-", "all")) {
log_info("Server certificate verification disabled.");
easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L,
LOG_ERR, return -EUCLEAN);
easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L,
LOG_ERR, return -EUCLEAN);
} else if (arg_trust || startswith(u->url, "https://"))
easy_setopt(curl, CURLOPT_CAINFO, arg_trust ?: TRUST_FILE,
LOG_ERR, return -EXFULL);
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(u->url, "https://")) {
if (!easy_setopt(curl, LOG_ERR, CURLOPT_CAINFO, arg_trust ?: TRUST_FILE))
return -EXFULL;
}
if (arg_key || arg_trust)
easy_setopt(curl, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1,
LOG_WARNING, );
if (startswith(u->url, "https://"))
(void) easy_setopt(curl, LOG_WARNING, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1_2);
u->easy = TAKE_PTR(curl);
} else {
@@ -330,11 +314,8 @@ int start_upload(Uploader *u,
}
/* upload to this place */
code = curl_easy_setopt(u->easy, CURLOPT_URL, u->url);
if (code)
return log_error_errno(SYNTHETIC_ERRNO(EXFULL),
"curl_easy_setopt CURLOPT_URL failed: %s",
curl_easy_strerror(code));
if (!easy_setopt(u->easy, LOG_ERR, CURLOPT_URL, u->url))
return -EXFULL;
u->uploading = true;
@@ -567,15 +548,15 @@ static int update_content_encoding_header(Uploader *u, const CompressionConfig *
break;
}
if (update_header) {
CURLcode code;
easy_setopt(u->easy, CURLOPT_HTTPHEADER, u->header, LOG_WARNING, return -EXFULL);
}
if (update_header &&
!easy_setopt(u->easy, LOG_WARNING, CURLOPT_HTTPHEADER, u->header))
return -EXFULL;
u->compression = cc;
if (cc)
log_debug("Using compression algorithm %s with compression level %i.", compression_to_string(cc->algorithm), cc->level);
log_debug("Using compression algorithm %s with compression level %i.",
compression_to_string(cc->algorithm), cc->level);
else
log_debug("Disabled compression algorithm.");
return 0;
@@ -644,12 +625,13 @@ static int perform_upload(Uploader *u) {
code = curl_easy_perform(u->easy);
if (code) {
if (u->error[0])
log_error("Upload to %s failed: %.*s",
u->url, (int) sizeof(u->error), u->error);
return log_error_errno(SYNTHETIC_ERRNO(EIO),
"Upload to %s failed: %.*s",
u->url, (int) sizeof(u->error), u->error);
else
log_error("Upload to %s failed: %s",
u->url, curl_easy_strerror(code));
return -EIO;
return log_error_errno(SYNTHETIC_ERRNO(EIO),
"Upload to %s failed: %s",
u->url, curl_easy_strerror(code));
}
code = curl_easy_getinfo(u->easy, CURLINFO_RESPONSE_CODE, &status);
+6 -1
View 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 + {
+213
View File
@@ -0,0 +1,213 @@
/* 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. */
#define SERVER_ANSWER_MAX (1*1024*1024u)
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) {
size_t new_size = size_add(iovw_size(&context->upload_answer), nmemb);
if (new_size > SERVER_ANSWER_MAX) {
log_warning("Server answer too long (%zu > %u), refusing.", new_size, SERVER_ANSWER_MAX);
return 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");
r = curl_append_to_header(&header, arg_extra_headers);
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
}
+136 -109
View File
@@ -1,7 +1,5 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include <getopt.h>
#include "sd-event.h"
#include "sd-varlink.h"
@@ -13,10 +11,12 @@
#include "format-table.h"
#include "log.h"
#include "main-func.h"
#include "options.h"
#include "parse-argument.h"
#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"
@@ -26,6 +26,7 @@
#include "time-util.h"
#include "varlink-idl-util.h"
#include "verbs.h"
#include "web-util.h"
#define METRICS_OR_FACTS_MAX 4096U
#define METRICS_OR_FACTS_LINKS_MAX 128U
@@ -36,27 +37,19 @@ 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;
char **arg_extra_headers = 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);
STATIC_DESTRUCTOR_REGISTER(arg_extra_headers, strv_freep);
typedef struct LinkInfo {
Context *context;
@@ -77,9 +70,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);
@@ -570,6 +566,7 @@ static int output_collected(Context *context) {
}
_cleanup_(table_unrefp) Table *table = NULL;
switch(context->action) {
case ACTION_LIST_METRICS:
@@ -708,6 +705,10 @@ static int readdir_sources(char **ret_directory, DirectoryEntries **ret) {
return m > 0;
}
VERB_FULL(verb_metrics, "metrics", "[MATCH…]", VERB_ANY, VERB_ANY, 0, ACTION_LIST_METRICS,
"Acquire list of metrics and their values");
VERB_FULL(verb_metrics, "describe-metrics", "[MATCH…]", VERB_ANY, VERB_ANY, 0, ACTION_DESCRIBE_METRICS,
"Describe available metrics");
static int verb_metrics(int argc, char *argv[], uintptr_t data, void *userdata) {
Action action = data;
int r;
@@ -768,7 +769,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;
}
@@ -788,6 +792,10 @@ static int verb_metrics(int argc, char *argv[], uintptr_t data, void *userdata)
return 0;
}
VERB_FULL(verb_facts, "facts", "[MATCH…]", VERB_ANY, VERB_ANY, 0, ACTION_LIST_FACTS,
"Acquire list of facts and their values");
VERB_FULL(verb_facts, "describe-facts", "[MATCH…]", VERB_ANY, VERB_ANY, 0, ACTION_DESCRIBE_FACTS,
"Describe available facts");
static int verb_facts(int argc, char *argv[], uintptr_t data, void *userdata) {
Action action = data;
int r;
@@ -848,7 +856,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;
}
@@ -868,6 +879,7 @@ static int verb_facts(int argc, char *argv[], uintptr_t data, void *userdata) {
return 0;
}
VERB_NOARG(verb_list_sources, "list-sources", "Show list of known metrics sources");
static int verb_list_sources(int argc, char *argv[], uintptr_t _data, void *userdata) {
int r;
@@ -925,143 +937,158 @@ static int verb_list_sources(int argc, char *argv[], uintptr_t _data, void *user
static int help(void) {
_cleanup_free_ char *link = NULL;
_cleanup_(table_unrefp) Table *verbs = NULL, *options = NULL;
int r;
r = terminal_urlify_man("systemd-report", "1", &link);
if (r < 0)
return log_oom();
printf("%1$s [OPTIONS...] COMMAND ...\n"
"\n%5$sAcquire metrics and facts from local sources.%6$s\n"
"\n%3$sCommands:%4$s\n"
" metrics [MATCH...] Acquire list of metrics and their values\n"
" describe-metrics [MATCH...]\n"
" Describe available metrics\n"
" facts [MATCH...] Acquire list of facts and their values\n"
" describe-facts [MATCH...]\n"
" Describe available facts\n"
" list-sources Show list of known metrics sources\n"
"\n%3$sOptions:%4$s\n"
" -h --help Show this help\n"
" --version Show package version\n"
" --no-pager Do not pipe output into a pager\n"
" --no-legend Do not show the headers and footers\n"
" --user Connect to user service manager\n"
" --system Connect to system service manager (default)\n"
" --json=pretty|short\n"
" Configure JSON output\n"
" -j Equivalent to --json=pretty (on TTY) or --json=short\n"
" (otherwise)\n"
"\nSee the %2$s for details.\n",
program_invocation_short_name,
link,
ansi_underline(),
ansi_normal(),
ansi_highlight(),
ansi_normal());
r = verbs_get_help_table(&verbs);
if (r < 0)
return r;
r = option_parser_get_help_table(&options);
if (r < 0)
return r;
(void) table_sync_column_widths(0, options, verbs);
printf("%s [OPTIONS...] COMMAND ...\n"
"\n%sAcquire metrics and facts from local sources.%s\n"
"\n%sCommands:%s\n",
program_invocation_short_name,
ansi_highlight(),
ansi_normal(),
ansi_underline(),
ansi_normal());
r = table_print_or_warn(verbs);
if (r < 0)
return r;
printf("\n%sOptions:%s\n",
ansi_underline(),
ansi_normal());
r = table_print_or_warn(options);
if (r < 0)
return r;
printf("\nSee the %s for details.\n", link);
return 0;
}
static int verb_help(int argc, char *argv[], uintptr_t _data, void *userdata) {
return help();
}
VERB_COMMON_HELP_HIDDEN(help);
static int parse_argv(int argc, char *argv[]) {
enum {
ARG_VERSION = 0x100,
ARG_NO_PAGER,
ARG_NO_LEGEND,
ARG_USER,
ARG_SYSTEM,
ARG_JSON,
};
static const struct option options[] = {
{ "help", no_argument, NULL, 'h' },
{ "version", no_argument, NULL, ARG_VERSION },
{ "no-pager", no_argument, NULL, ARG_NO_PAGER },
{ "no-legend", no_argument, NULL, ARG_NO_LEGEND },
{ "user", no_argument, NULL, ARG_USER },
{ "system", no_argument, NULL, ARG_SYSTEM },
{ "json", required_argument, NULL, ARG_JSON },
{}
};
int c, r;
static int parse_argv(int argc, char *argv[], char ***ret_args) {
int r;
assert(argc >= 0);
assert(argv);
while ((c = getopt_long(argc, argv, "hj", options, NULL)) >= 0)
OptionParser state = { argc, argv };
const char *arg;
FOREACH_OPTION(&state, c, &arg, /* on_error= */ return c)
switch (c) {
case 'h':
OPTION_COMMON_HELP:
return help();
case ARG_VERSION:
OPTION_COMMON_VERSION:
return version();
case ARG_NO_PAGER:
OPTION_COMMON_NO_PAGER:
arg_pager_flags |= PAGER_DISABLE;
break;
case ARG_NO_LEGEND:
OPTION_COMMON_NO_LEGEND:
arg_legend = false;
break;
case ARG_USER:
OPTION_LONG("user", NULL, "Connect to user service manager"):
arg_runtime_scope = RUNTIME_SCOPE_USER;
break;
case ARG_SYSTEM:
OPTION_LONG("system", NULL, "Connect to system service manager (default)"):
arg_runtime_scope = RUNTIME_SCOPE_SYSTEM;
break;
case ARG_JSON:
r = parse_json_argument(optarg, &arg_json_format_flags);
OPTION_COMMON_JSON:
r = parse_json_argument(arg, &arg_json_format_flags);
if (r <= 0)
return r;
break;
case 'j':
OPTION_COMMON_LOWERCASE_J:
arg_json_format_flags = SD_JSON_FORMAT_PRETTY_AUTO|SD_JSON_FORMAT_COLOR_AUTO;
break;
case '?':
return -EINVAL;
OPTION_LONG("url", "URL",
"Upload to this address"):
r = free_and_strdup_warn(&arg_url, arg);
if (r < 0)
return r;
break;
default:
assert_not_reached();
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;
OPTION_LONG("extra-header", "NAME: VALUE",
"Inject additional header into the upload request"):
if (isempty(arg)) {
arg_extra_headers = strv_free(arg_extra_headers);
break;
}
if (!http_header_valid(arg))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid HTTP header: %s", arg);
if (strv_extend(&arg_extra_headers, arg) < 0)
return log_oom();
break;
}
if ((arg_url || arg_key || arg_cert || arg_trust || arg_extra_headers) && !HAVE_LIBCURL)
return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Compiled without libcurl.");
*ret_args = option_parser_get_args(&state);
return 1;
}
static int report_main(int argc, char *argv[]) {
static const Verb verbs[] = {
{ "help", VERB_ANY, 1, 0, verb_help },
{ "metrics", VERB_ANY, VERB_ANY, 0, verb_metrics, ACTION_LIST_METRICS },
{ "describe-metrics", VERB_ANY, VERB_ANY, 0, verb_metrics, ACTION_DESCRIBE_METRICS },
{ "facts", VERB_ANY, VERB_ANY, 0, verb_facts, ACTION_LIST_FACTS },
{ "describe-facts", VERB_ANY, VERB_ANY, 0, verb_facts, ACTION_DESCRIBE_FACTS },
{ "list-sources", VERB_ANY, 1, 0, verb_list_sources },
{}
};
return dispatch_verb(argc, argv, verbs, NULL);
}
static int run(int argc, char *argv[]) {
char **args = NULL;
int r;
log_setup();
r = parse_argv(argc, argv);
r = parse_argv(argc, argv, &args);
if (r <= 0)
return r;
return report_main(argc, argv);
return dispatch_verb_with_args(args, NULL);
}
DEFINE_MAIN_FUNCTION(run);
+36
View File
@@ -0,0 +1,36 @@
/* 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 char **arg_extra_headers;
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);
@@ -8,6 +8,7 @@
#include "hashmap.h"
#include "log.h"
#include "string-util.h"
#include "strv.h"
#include "time-util.h"
#include "version.h"
@@ -407,3 +408,19 @@ int curl_parse_http_time(const char *t, usec_t *ret) {
return 0;
}
int curl_append_to_header(struct curl_slist **list, char **headers) {
/* This function leaves 'list' modified on partial failure.
* Input/output param list may point to NULL. */
assert(list);
STRV_FOREACH(h, headers) {
struct curl_slist *l = curl_slist_append(*list, *h);
if (!l)
return -ENOMEM;
*list = l;
}
return 0;
}
@@ -5,6 +5,19 @@
#include "shared-forward.h"
#define easy_setopt(curl, log_level, opt, value) ({ \
CURLcode code = curl_easy_setopt(ASSERT_PTR(curl), opt, value); \
if (code) \
log_full(log_level, \
"curl_easy_setopt %s failed: %s", \
#opt, curl_easy_strerror(code)); \
code == CURLE_OK; \
})
DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(CURL*, curl_easy_cleanup, NULL);
DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(CURLM*, curl_multi_cleanup, NULL);
DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(struct curl_slist*, curl_slist_free_all, NULL);
typedef struct CurlGlue CurlGlue;
typedef struct CurlGlue {
@@ -30,7 +43,4 @@ void curl_glue_remove_and_free(CurlGlue *g, CURL *c);
struct curl_slist *curl_slist_new(const char *first, ...) _sentinel_;
int curl_header_strdup(const void *contents, size_t sz, const char *field, char **value);
int curl_parse_http_time(const char *t, usec_t *ret);
DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(CURL*, curl_easy_cleanup, NULL);
DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(CURLM*, curl_multi_cleanup, NULL);
DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(struct curl_slist*, curl_slist_free_all, NULL);
int curl_append_to_header(struct curl_slist **list, char **headers);
+1 -1
View File
@@ -123,7 +123,7 @@ static int fact_build_send(FactFamilyContext *context, const char *object, sd_js
return sd_varlink_replybo(context->link,
SD_JSON_BUILD_PAIR_STRING("name", context->fact_family->name),
JSON_BUILD_PAIR_STRING_NON_EMPTY("object", object),
SD_JSON_BUILD_PAIR("value", SD_JSON_BUILD_VARIANT(value)));
SD_JSON_BUILD_PAIR_VARIANT("value", value));
}
int fact_build_send_string(FactFamilyContext *context, const char *object, const char *value) {
+15
View File
@@ -459,3 +459,18 @@ libshared_fdisk = static_library(
userspace],
c_args : ['-fvisibility=default'],
build_by_default : false)
# A small shared file that is linked into a few places.
# It is not part of libshared because this code needs libcurl and
# we don't want to link libshared to libcurl.
if conf.get('HAVE_LIBCURL') == 1
libcurlutil_static = static_library(
'curl-util',
'curl-util.c',
implicit_include_directories : false,
dependencies : [userspace, libcurl],
include_directories : includes,
build_by_default : false)
else
libcurlutil_static = []
endif
+20 -13
View File
@@ -5,19 +5,6 @@
#include "utf8.h"
#include "web-util.h"
bool http_etag_is_valid(const char *etag) {
if (isempty(etag))
return false;
if (!endswith(etag, "\""))
return false;
if (!STARTSWITH_SET(etag, "\"", "W/\""))
return false;
return true;
}
bool http_url_is_valid(const char *url) {
const char *p;
@@ -62,3 +49,23 @@ bool documentation_url_is_valid(const char *url) {
return ascii_is_valid(p);
}
bool http_header_valid(const char *header) {
return header &&
ascii_is_valid(header) &&
!string_has_cc(header, /* ok= */ NULL) &&
strchr(header, ':');
}
bool http_etag_is_valid(const char *etag) {
if (isempty(etag))
return false;
if (!endswith(etag, "\""))
return false;
if (!STARTSWITH_SET(etag, "\"", "W/\""))
return false;
return true;
}
+2 -3
View File
@@ -5,7 +5,6 @@
bool http_url_is_valid(const char *url) _pure_;
bool file_url_is_valid(const char *url) _pure_;
bool documentation_url_is_valid(const char *url) _pure_;
bool http_etag_is_valid(const char *etag);
bool http_header_valid(const char *header) _pure_;
bool http_etag_is_valid(const char *etag) _pure_;
+1
View File
@@ -130,6 +130,7 @@ simple_tests += files(
'test-install-root.c',
'test-io-util.c',
'test-iovec-util.c',
'test-iovec-wrapper.c',
'test-journal-importer.c',
'test-kbd-util.c',
'test-label.c',
+245
View File
@@ -0,0 +1,245 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include <sys/uio.h>
#include "alloc-util.h"
#include "iovec-wrapper.h"
#include "tests.h"
TEST(iovw_put) {
_cleanup_(iovw_done) struct iovec_wrapper iovw = {};
/* Zero-length insertions are no-ops and do not touch the data pointer */
ASSERT_OK_ZERO(iovw_put(&iovw, NULL, 0));
ASSERT_OK_ZERO(iovw_put(&iovw, (char*) "foo", 0));
ASSERT_EQ(iovw.count, 0U);
ASSERT_OK(iovw_put(&iovw, (char*) "foo", 3));
ASSERT_OK(iovw_put(&iovw, (char*) "barbar", 6));
ASSERT_OK(iovw_put(&iovw, (char*) "q", 1));
ASSERT_EQ(iovw.count, 3U);
ASSERT_EQ(iovw.iovec[0].iov_len, 3U);
ASSERT_EQ(memcmp(iovw.iovec[0].iov_base, "foo", 3), 0);
ASSERT_EQ(iovw.iovec[1].iov_len, 6U);
ASSERT_EQ(memcmp(iovw.iovec[1].iov_base, "barbar", 6), 0);
ASSERT_EQ(iovw.iovec[2].iov_len, 1U);
ASSERT_EQ(memcmp(iovw.iovec[2].iov_base, "q", 1), 0);
}
TEST(iovw_append) {
_cleanup_(iovw_done_free) struct iovec_wrapper iovw = {};
/* iovw_append copies the data; the wrapper owns the copies. */
char buf[4] = { 'o', 'n', 'e', '\0' };
ASSERT_OK(iovw_append(&iovw, buf, 3));
ASSERT_EQ(iovw.count, 1U);
ASSERT_EQ(iovw.iovec[0].iov_len, 3U);
ASSERT_EQ(memcmp(iovw.iovec[0].iov_base, "one", 3), 0);
/* Insert with a NUL */
ASSERT_OK_ZERO(iovw_append(&iovw, buf, 4));
ASSERT_EQ(iovw.count, 2U);
ASSERT_EQ(iovw.iovec[1].iov_len, 4U);
ASSERT_EQ(memcmp(iovw.iovec[1].iov_base, "one\0", 4), 0);
/* Mutating the caller's buffer does not affect what's stored */
memset(buf, 'X', sizeof buf);
ASSERT_EQ(memcmp(iovw.iovec[0].iov_base, "one", 3), 0);
}
TEST(iovw_consume) {
_cleanup_(iovw_done_free) struct iovec_wrapper iovw = {};
char *p = strdup("consumed");
ASSERT_NOT_NULL(p);
ASSERT_OK(iovw_consume(&iovw, p, strlen(p)));
ASSERT_EQ(iovw.count, 1U);
/* iovw_consume moves ownership in place, no copy */
ASSERT_PTR_EQ(iovw.iovec[0].iov_base, p);
/* Zero-length: iovw_put returns 0 without adding anything, and does not free the payload.
* Confirm by strdup'ing something and explicitly freeing it afterwards. */
_cleanup_free_ char *q = strdup("");
ASSERT_NOT_NULL(q);
ASSERT_OK_ZERO(iovw_consume(&iovw, q, 0));
ASSERT_EQ(iovw.count, 1U);
}
TEST(iovw_isempty) {
ASSERT_TRUE(iovw_isempty(NULL));
_cleanup_(iovw_done) struct iovec_wrapper iovw = {};
ASSERT_TRUE(iovw_isempty(&iovw));
ASSERT_OK(iovw_put(&iovw, (char*) "x", 1));
ASSERT_FALSE(iovw_isempty(&iovw));
}
TEST(iovw_put_string_field) {
_cleanup_(iovw_done_free) struct iovec_wrapper iovw = {};
ASSERT_OK(iovw_put_string_field(&iovw, "FOO=", "bar"));
ASSERT_OK(iovw_put_string_field(&iovw, "BAZ=", "quux"));
ASSERT_EQ(iovw.count, 2U);
ASSERT_EQ(iovw.iovec[0].iov_len, strlen("FOO=bar"));
ASSERT_EQ(memcmp(iovw.iovec[0].iov_base, "FOO=bar", strlen("FOO=bar")), 0);
ASSERT_EQ(iovw.iovec[1].iov_len, strlen("BAZ=quux"));
ASSERT_EQ(memcmp(iovw.iovec[1].iov_base, "BAZ=quux", strlen("BAZ=quux")), 0);
/* Non-replacing put: a second FOO= just appends rather than replacing */
ASSERT_OK(iovw_put_string_field(&iovw, "FOO=", "second"));
ASSERT_EQ(iovw.count, 3U);
ASSERT_EQ(iovw.iovec[0].iov_len, strlen("FOO=bar"));
ASSERT_EQ(memcmp(iovw.iovec[0].iov_base, "FOO=bar", strlen("FOO=bar")), 0);
ASSERT_EQ(iovw.iovec[2].iov_len, strlen("FOO=second"));
ASSERT_EQ(memcmp(iovw.iovec[2].iov_base, "FOO=second", strlen("FOO=second")), 0);
}
TEST(iovw_replace_string_field) {
_cleanup_(iovw_done_free) struct iovec_wrapper iovw = {};
/* If the field does not exist yet, replace acts like put */
ASSERT_OK(iovw_replace_string_field(&iovw, "A=", "1"));
ASSERT_EQ(iovw.count, 1U);
ASSERT_EQ(iovw.iovec[0].iov_len, strlen("A=1"));
ASSERT_EQ(memcmp(iovw.iovec[0].iov_base, "A=1", strlen("A=1")), 0);
/* Replacing an existing field updates it in place */
ASSERT_OK(iovw_replace_string_field(&iovw, "A=", "twentytwo"));
ASSERT_EQ(iovw.count, 1U);
ASSERT_EQ(iovw.iovec[0].iov_len, strlen("A=twentytwo"));
ASSERT_EQ(memcmp(iovw.iovec[0].iov_base, "A=twentytwo", strlen("A=twentytwo")), 0);
/* Distinct field still appends */
ASSERT_OK(iovw_replace_string_field(&iovw, "B=", "x"));
ASSERT_EQ(iovw.count, 2U);
}
TEST(iovw_put_string_fieldf) {
_cleanup_(iovw_done_free) struct iovec_wrapper iovw = {};
ASSERT_OK(iovw_put_string_fieldf(&iovw, "N=", "%d-%s", 42, "answer"));
ASSERT_EQ(iovw.count, 1U);
ASSERT_EQ(iovw.iovec[0].iov_len, strlen("N=42-answer"));
ASSERT_EQ(memcmp(iovw.iovec[0].iov_base, "N=42-answer", strlen("N=42-answer")), 0);
/* Replacing variant */
ASSERT_OK(iovw_replace_string_fieldf(&iovw, "N=", "%d", 7));
ASSERT_EQ(iovw.count, 1U);
ASSERT_EQ(iovw.iovec[0].iov_len, strlen("N=7"));
ASSERT_EQ(memcmp(iovw.iovec[0].iov_base, "N=7", strlen("N=7")), 0);
}
TEST(iovw_put_string_field_free) {
_cleanup_(iovw_done_free) struct iovec_wrapper iovw = {};
/* iovw_put_string_field_free takes ownership of the value string (frees it on return). */
char *v = strdup("hello");
ASSERT_NOT_NULL(v);
ASSERT_OK(iovw_put_string_field_free(&iovw, "K=", v));
ASSERT_EQ(iovw.count, 1U);
ASSERT_EQ(iovw.iovec[0].iov_len, strlen("K=hello"));
ASSERT_EQ(memcmp(iovw.iovec[0].iov_base, "K=hello", strlen("K=hello")), 0);
}
TEST(iovw_rebase) {
/* iovw_rebase shifts all iov_base pointers from an old base to a new base. Fabricate a
* stand-in "old base" and "new base" and a wrapper with offsets pointing into the old
* base, then verify they get rewritten to point into the new base. */
uint8_t old_base[64] = {}, new_base[64] = {};
for (size_t i = 0; i < sizeof old_base; i++) {
old_base[i] = (uint8_t) i;
new_base[i] = (uint8_t) (100 + i);
}
_cleanup_(iovw_done) struct iovec_wrapper iovw = {};
ASSERT_OK(iovw_put(&iovw, old_base + 0, 4));
ASSERT_OK(iovw_put(&iovw, old_base + 10, 2));
ASSERT_OK(iovw_put(&iovw, old_base + 30, 8));
ASSERT_EQ(iovw.count, 3U);
iovw_rebase(&iovw, old_base, new_base);
ASSERT_PTR_EQ(iovw.iovec[0].iov_base, new_base + 0);
ASSERT_PTR_EQ(iovw.iovec[1].iov_base, new_base + 10);
ASSERT_PTR_EQ(iovw.iovec[2].iov_base, new_base + 30);
/* Lengths are preserved */
ASSERT_EQ(iovw.iovec[0].iov_len, 4U);
ASSERT_EQ(iovw.iovec[1].iov_len, 2U);
ASSERT_EQ(iovw.iovec[2].iov_len, 8U);
/* And the contents through the new base match what we staged there */
ASSERT_EQ(memcmp(iovw.iovec[0].iov_base, new_base + 0, 4), 0);
ASSERT_EQ(memcmp(iovw.iovec[1].iov_base, new_base + 10, 2), 0);
ASSERT_EQ(memcmp(iovw.iovec[2].iov_base, new_base + 30, 8), 0);
}
TEST(iovw_size) {
_cleanup_(iovw_done) struct iovec_wrapper iovw = {};
ASSERT_EQ(iovw_size(&iovw), 0U);
ASSERT_OK(iovw_put(&iovw, (char*) "abcd", 4));
ASSERT_OK(iovw_put(&iovw, (char*) "efghij", 6));
ASSERT_OK(iovw_put(&iovw, (char*) "kl", 2));
ASSERT_EQ(iovw_size(&iovw), 12U);
}
TEST(iovw_append_iovw) {
_cleanup_(iovw_done_free) struct iovec_wrapper target = {};
_cleanup_(iovw_done) struct iovec_wrapper source = {};
/* Appending an empty/NULL source is a no-op */
ASSERT_OK_ZERO(iovw_append_iovw(&target, NULL));
ASSERT_OK_ZERO(iovw_append_iovw(&target, &source));
ASSERT_EQ(target.count, 0U);
ASSERT_OK(iovw_put(&source, (char*) "one", 3));
ASSERT_OK(iovw_put(&source, (char*) "twotwo", 6));
ASSERT_EQ(source.count, 2U);
/* Pre-seed target with one entry to check that append adds on top rather than replacing */
char *seed = strdup("zero");
ASSERT_NOT_NULL(seed);
ASSERT_OK(iovw_put(&target, seed, strlen(seed)));
ASSERT_OK(iovw_append_iovw(&target, &source));
ASSERT_EQ(target.count, 3U);
/* Appended entries must be fresh copies, not aliases of the source entries */
ASSERT_TRUE(target.iovec[1].iov_base != source.iovec[0].iov_base);
ASSERT_TRUE(target.iovec[2].iov_base != source.iovec[1].iov_base);
ASSERT_EQ(target.iovec[1].iov_len, 3U);
ASSERT_EQ(memcmp(target.iovec[1].iov_base, "one", 3), 0);
ASSERT_EQ(target.iovec[2].iov_len, 6U);
ASSERT_EQ(memcmp(target.iovec[2].iov_base, "twotwo", 6), 0);
/* Source is unchanged */
ASSERT_EQ(source.count, 2U);
}
TEST(iovw_to_cstring) {
_cleanup_(iovw_done) struct iovec_wrapper iovw = {};
_cleanup_free_ char *s;
/* Empty wrapper → empty string */
s = iovw_to_cstring(&iovw);
ASSERT_NOT_NULL(s);
ASSERT_STREQ(s, "");
s = mfree(s);
ASSERT_OK(iovw_put(&iovw, (char*) "foo", 3));
ASSERT_OK(iovw_put(&iovw, (char*) "/", 1));
ASSERT_OK(iovw_put(&iovw, (char*) "bar", 3));
s = iovw_to_cstring(&iovw);
ASSERT_NOT_NULL(s);
ASSERT_STREQ(s, "foo/bar");
}
DEFINE_TEST_MAIN(LOG_INFO);
+3 -4
View File
@@ -261,13 +261,12 @@ static void test_log_context(void) {
IOVEC_MAKE_STRING("ABC=def"),
IOVEC_MAKE_STRING("GHI=jkl"),
};
_cleanup_free_ struct iovec_wrapper *iovw = NULL;
ASSERT_NOT_NULL(iovw = iovw_new());
ASSERT_OK(iovw_consume(iovw, strdup("MNO=pqr"), STRLEN("MNO=pqr") + 1));
struct iovec_wrapper iovw = {};
ASSERT_OK(iovw_consume(&iovw, strdup("MNO=pqr"), STRLEN("MNO=pqr") + 1));
LOG_CONTEXT_PUSH_IOV(iov, ELEMENTSOF(iov));
LOG_CONTEXT_PUSH_IOV(iov, ELEMENTSOF(iov));
LOG_CONTEXT_CONSUME_IOV(iovw->iovec, iovw->count);
LOG_CONTEXT_CONSUME_IOV(iovw.iovec, iovw.count);
LOG_CONTEXT_PUSH("STU=vwx");
ASSERT_EQ(log_context_num_contexts(), 3U);
+22
View File
@@ -617,6 +617,28 @@ TEST(split_pair) {
ASSERT_STREQ(b, "=");
}
TEST(empty_to_null) {
const char *s = "asdf", *n = NULL, *e = "";
char *t = (char*) "asdf";
const char p[] = "asdf";
char q[] = "asdf";
/* empty_to_null cannot be used with constant strings, e.g.
* empty_to_null("") fails with 'error: cast specifies array type'. */
ASSERT_NULL(empty_to_null(NULL));
ASSERT_NULL(empty_to_null(n));
ASSERT_NULL(empty_to_null(e));
ASSERT_STREQ(empty_to_null(s), "asdf");
ASSERT_NULL(empty_to_null(s + 4));
ASSERT_STREQ(empty_to_null(t), "asdf");
ASSERT_NULL(empty_to_null(t + 4));
ASSERT_STREQ(empty_to_null(&p[0]), "asdf");
ASSERT_NULL(empty_to_null(&p[0] + 4));
ASSERT_STREQ(empty_to_null(&q[0]), "asdf");
ASSERT_NULL(empty_to_null(&q[0] + 4));
}
TEST(first_word) {
assert_se(first_word("Hello", ""));
assert_se(first_word("Hello", "Hello"));
@@ -0,0 +1,70 @@
#!/usr/bin/python3
# SPDX-License-Identifier: LGPL-2.1-or-later
import argparse, json, os, socket, ssl
from http.server import BaseHTTPRequestHandler, HTTPServer
def sd_notify(state: str) -> bool:
notify_socket = os.environ.get("NOTIFY_SOCKET")
if not notify_socket:
return False
if notify_socket.startswith("@"):
notify_socket = "\0" + notify_socket[1:]
try:
with socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) as sock:
sock.sendto(state.encode(), notify_socket)
except OSError:
return False
return True
class Handler(BaseHTTPRequestHandler):
def do_POST(self):
length = int(self.headers.get("Content-Length", 0))
body = self.rfile.read(length)
# Check optional attribute
if auth := self.headers.get("Authorization"):
print(f"Authorization: {auth}")
# Validate JSON structure
try:
data = json.loads(body)
except json.JSONDecodeError:
self.send_error(400, "Invalid JSON")
return
print(f"JSON: {s if len(s := str(data)) < 80 else s[:40] + '…' + s[-40:]}")
if "metrics" not in data and "facts" not in data:
self.send_error(400, "Missing 'metrics' or 'facts' field")
return
response = json.dumps({"status": "ok"}).encode()
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", len(response))
self.end_headers()
self.wfile.write(response)
def log_message(self, fmt, *args):
print(f"{self.address_string()} - {fmt % args}")
parser = argparse.ArgumentParser()
parser.add_argument("--port", type=int, default=8089)
parser.add_argument("--cert", help="TLS certificate file")
parser.add_argument("--key", help="TLS private key file")
args = parser.parse_args()
server = HTTPServer(("", args.port), Handler)
if args.cert and args.key:
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ctx.load_cert_chain(args.cert, args.key)
server.socket = ctx.wrap_socket(server.socket, server_side=True)
print(f"Serving on https://localhost:{args.port}/")
else:
print(f"Serving on http://localhost:{args.port}/")
try:
sd_notify("READY=1")
server.serve_forever()
except KeyboardInterrupt:
print("\nStopped.")
+29
View File
@@ -65,3 +65,32 @@ systemctl start systemd-report-basic.socket
# Test facts via direct Varlink call on existing socket
varlinkctl --more call /run/systemd/report/io.systemd.Basic io.systemd.Facts.List {}
varlinkctl --more call /run/systemd/report/io.systemd.Basic io.systemd.Facts.Describe {}
# Test HTTP upload (plain http)
FAKE_SERVER=/usr/lib/systemd/tests/integration-tests/TEST-74-AUX-UTILS/TEST-74-AUX-UTILS.units/fake-report-server.py
CERTDIR=$(mktemp -d)
at_exit() {
set +e
systemctl stop fake-report-server fake-report-server-tls
rm -rf "$CERTDIR"
}
trap at_exit EXIT
systemd-run -p Type=notify --unit=fake-report-server "$FAKE_SERVER"
systemctl status fake-report-server
"$REPORT" metrics --url=http://localhost:8089/
"$REPORT" facts --url=http://localhost:8089/
# Test HTTPS upload with generated TLS certificates
openssl req -x509 -newkey rsa:2048 -keyout "$CERTDIR/server.key" -out "$CERTDIR/server.crt" \
-days 1 -nodes -subj "/CN=localhost" 2>/dev/null
systemd-run -p Type=notify --unit=fake-report-server-tls \
"$FAKE_SERVER" --cert="$CERTDIR/server.crt" --key="$CERTDIR/server.key" --port=8090
systemctl status fake-report-server-tls
"$REPORT" metrics --url=https://localhost:8090/ --key=- --trust="$CERTDIR/server.crt"
"$REPORT" facts --url=https://localhost:8090/ --key=- --trust="$CERTDIR/server.crt" \
--extra-header='Authorization: Bearer magic string'