diff --git a/TODO b/TODO index 8111703d16..fde2013c83 100644 --- a/TODO +++ b/TODO @@ -148,6 +148,24 @@ Features: hostname pattern and has a ProxyCommand set that allows connecting to any local VM/container that way without any networking configured. +* Varlinkification of the following command line tools, to open them up to + other programs via IPC: + - bootctl + - journalctl (allowing journal read access via IPC) + - coredumpcl + - systemd-bless-boot + - systemd-measure + - systemd-creds (allowing clients to encrypt credentials locally) + - systemd-cryptenroll (to allow UIs to enroll FIDO2 keys and such) + - systemd-dissect + - systemd-sysext + - systemd-sysupdate + - systemd-analyze + - kernel-install + +* Varlink: add glue code to allow varlink clients to be authenticated via + Polkit by passing client pidfd over. + * in the service manager, pick up ERRNO= + BUSERROR= + VARLINKERROR= error identifiers, and store them along with the exit status of a server and report via "systemctl status". @@ -159,32 +177,15 @@ Features: * automatically mount one virtiofs during early boot phase to /run/host/, similar to how we do that for nspawn, based on some clear tag. -* make systemd-pcrextend accessible via varlink so that clients can measure - things reasonably freely, and get a proper entry in the TPM event log we - maintain, correctly synchronized - * add some service that makes an atomic snapshot of PCR state and event log up to that point available, possibly even with quote by the TPM. * encode type1 entries in some UKI section to add additional entries to the menu. -* extend the various CLI tools we have that output JSON already to also read - their command to execute from JSON in varlink format, so that you can fork - them off and talk to them fully via varlink. - -* add a "varlinkctl" tool that allows interacting with varlink services from - the shell. In particular add a "--exec=" switch, which allows specifying a - binary to invoke to which to talk via stdin/stdout - -* make tools that speak varlink over stdin/stdout trivially sockect - activatable. i.e. once bootctl, kernel-install, systemd-measure and similar - speak varlink make them available via a .socket unit with Accept=yes, so that - they can be talked to via IPC out-of-process - -* beef up .service units that are socket activated with Accept=yes with options - AllowPeerUser= + AllowPeerGroup= to allow trivially simple access control - when invoked via socket as IPC services +* Add ACL-based access management to .socket units. i.e. add AllowPeerUser= + + AllowPeerGroup= that installs additional user/group ACL entries on AF_UNIX + sockets. * systemd-tpm2-setup should probably have a factory reset logic, i.e. when some kernel command line option is set we reset the TPM (equivalent of tpm2_clear @@ -929,8 +930,6 @@ Features: 2. systemd-gpt-auto-generator should auto-mount /var if it finds it on disk 3. mount.x-initrd mount option in fstab should be implied for /var -* implement varlink introspection - * make persistent restarts easier by adding a new setting OpenPersistentFile= or so, which allows opening one or more files that is "persistent" across service restarts, hot reboot, cold reboots (depending on configuration): the diff --git a/man/busctl.xml b/man/busctl.xml index ce23dd1b33..cc404529ae 100644 --- a/man/busctl.xml +++ b/man/busctl.xml @@ -582,6 +582,7 @@ o "/org/freedesktop/systemd1/job/42684" dbus-daemon1, D-Bus, sd-bus3, + varlinkctl1, systemd1, machinectl1, wireshark1 diff --git a/man/rules/meson.build b/man/rules/meson.build index 3454668cab..3265db9c2d 100644 --- a/man/rules/meson.build +++ b/man/rules/meson.build @@ -1249,6 +1249,7 @@ manpages = [ ['systemd-user-runtime-dir', 'user-runtime-dir@.service'], ''], ['userdbctl', '1', [], 'ENABLE_USERDB'], + ['varlinkctl', '1', [], ''], ['vconsole.conf', '5', [], 'ENABLE_VCONSOLE'], ['veritytab', '5', [], 'HAVE_LIBCRYPTSETUP'] ] diff --git a/man/varlinkctl.xml b/man/varlinkctl.xml new file mode 100644 index 0000000000..5d0ec6ace7 --- /dev/null +++ b/man/varlinkctl.xml @@ -0,0 +1,315 @@ + + + + + + + + varlinkctl + systemd + + + + varlinkctl + 1 + + + + varlinkctl + Introspect with and invoke Varlink services + + + + + varlinkctl + OPTIONS + info + ADDRESS + + + + varlinkctl + OPTIONS + list-interfaces + ADDRESS + + + + varlinkctl + OPTIONS + introspect + ADDRESS + INTERFACE + + + + varlinkctl + OPTIONS + call + ADDRESS + METHOD + PARAMETERS + + + + varlinkctl + OPTIONS + validate-idl + FILE + + + + + Description + + varlinkctl may be used to introspect and invoke Varlink services. + + Services are referenced by one of the following: + + + A Varlink service reference starting with the unix: string, followed + by an absolute AF_UNIX path, or by @ and an arbitrary string + (the latter for referencing sockets in the abstract namespace). + + A Varlink service reference starting with the exec: string, followed + by an absolute path of a binary to execute. + + + For convenience these two simpler (redundant) service address syntaxes are also supported: + + + A file system path to an AF_UNIX socket, either absolute + (i.e. begins with /) or relative (in which case it must begin with + ./). + + A file system path to an executable, either absolute or relative (as above, must begin + with /, resp. ./). + + + + + Commands + + The following commands are understood: + + + + info ADDRESS + + Show brief information about the specified service, including vendor name and list of + implemented interfaces. Expects a service address in the formats described above. + + + + + + list-interfaces ADDRESS + + Show list of interfaces implemented by the specified service. Expects a service + address in the formats described above. + + + + + + introspect ADDRESS INTERFACE + + Show interface definition of the specified interface provided by the specified + service. Expects a service address in the formats described above and a Varlink interface + name. + + + + + + call ADDRESS METHOD [ARGUMENTS] + + Call the specified method of the specified service. Expects a service address in the + format described above, a fully qualified Varlink method name, and a JSON arguments object. If the + arguments object is not specified, it is read from STDIN instead. To pass an empty list of + parameters, specify the empty object {}. + + The reply parameters are written as JSON object to STDOUT. + + + + + + validate-idl [FILE] + + Reads a Varlink interface definition file, parses and validates it, then outputs it + with syntax highlighting. This checks for syntax and internal consistency of the interface. Expects a + file name to read the interface definition from. If omitted reads the interface definition from + STDIN. + + + + + + help + + Show command syntax help. + + + + + + + + Options + + The following options are understood: + + + + + + When used with call: expect multiple method replies. If this flag is + set the method call is sent with the more flag set, which tells the service to + generate multiple replies, if needed. The command remains running until the service sends a reply + message that indicates it is the last in the series. This flag should be set only for method calls + that support this mechanism. + + If this mode is enabled output is automatically switched to JSON-SEQ mode, so that individual + reply objects can be easily discerned. + + + + + + + + When used with call: do not expect a method reply. If this flag + is set the method call is sent with the oneway flag set (the command exits + immediately after), which tells the service not to generate a reply. + + + + + + MODE + + + Selects the JSON output formatting, one of pretty (for nicely indented, + colorized output) or short (for terse output with minimal whitespace and no + newlines), defaults to short. + + + + + + + + + + Equivalent to when invoked interactively from a terminal. Otherwise + equivalent to , in particular when the output is piped to some other + program. + + + + + + + + + + + + + Examples + + + Investigating a Service + + The following three commands inspect the io.systemd.Resolve service + implemented by + systemd-resolved.service8, + listing general service information and implemented interfaces, and then displaying the interface + definition of its primary interface: + + $ varlinkctl info /run/systemd/resolve/io.systemd.Resolve + Vendor: The systemd Project + Product: systemd (systemd-resolved) + Version: 254 (254-1522-g4790521^) + URL: https://systemd.io/ +Interfaces: io.systemd + io.systemd.Resolve + org.varlink.service +$ varlinkctl list-interfaces /run/systemd/resolve/io.systemd.Resolve +io.systemd +io.systemd.Resolve +org.varlink.service +$ varlinkctl introspect /run/systemd/resolve/io.systemd.Resolve io.systemd.Resolve +interface io.systemd.Resolve +type ResolvedAddress( + ifindex: ?int, + … + + (Interface definition has been truncated in the example above, in the interest of brevity.) + + + + Invoking a Method + + The following command resolves a hostname via systemd-resolved.service8's ResolveHostname method call. + + $ varlinkctl call /run/systemd/resolve/io.systemd.Resolve io.systemd.Resolve.ResolveHostname '{"name":"systemd.io","family":2}' -j +{ + "addresses" : [ + { + "ifindex" : 2, + "family" : 2, + "address" : [ + 185, + 199, + 111, + 153 + ] + } + ], + "name" : "systemd.io", + "flags" : 1048577 +} + + + + Investigating a Service Executable + + The following comand inspects the /usr/lib/systemd/systemd-pcrextend + executable and the IPC APIs it provides. It then invokes a method on it: + + # varlinkctl info /usr/lib/systemd/systemd-pcrextend + Vendor: The systemd Project + Product: systemd (systemd-pcrextend) + Version: 254 (254-1536-g97734fb) + URL: https://systemd.io/ +Interfaces: io.systemd + io.systemd.PCRExtend + org.varlink.service +# varlinkctl introspect /usr/lib/systemd/systemd-pcrextend io.systemd.PCRExtend +interface io.systemd.PCRExtend + +method Extend( + pcr: int, + text: ?string, + data: ?string +) -> () +# varlinkctl call /usr/lib/systemd/systemd-pcrextend io.systemd.PCRExtend.Extend '{"pcr":15,"text":"foobar"}' +{} + + + + + + See Also + + + busctl1, + Varlink + + + diff --git a/meson.build b/meson.build index 88bc1367e0..5e9ca285ac 100644 --- a/meson.build +++ b/meson.build @@ -2193,6 +2193,7 @@ subdir('src/update-done') subdir('src/update-utmp') subdir('src/user-sessions') subdir('src/userdb') +subdir('src/varlinkctl') subdir('src/vconsole') subdir('src/veritysetup') subdir('src/volatile-root') diff --git a/src/basic/alloc-util.h b/src/basic/alloc-util.h index 156c973e69..3ef834acee 100644 --- a/src/basic/alloc-util.h +++ b/src/basic/alloc-util.h @@ -254,4 +254,24 @@ static inline void free_many_charp(char **c, size_t n) { free_many((void**) c, n); } +_alloc_(2) static inline void *realloc0(void *p, size_t new_size) { + size_t old_size; + void *q; + + /* Like realloc(), but initializes anything appended to zero */ + + old_size = MALLOC_SIZEOF_SAFE(p); + + q = realloc(p, new_size); + if (!q) + return NULL; + + new_size = MALLOC_SIZEOF_SAFE(q); /* Update with actually allocated space */ + + if (new_size > old_size) + memset((uint8_t*) q + old_size, 0, new_size - old_size); + + return q; +} + #include "memory-util.h" diff --git a/src/core/core-varlink.c b/src/core/core-varlink.c index 1ff4a93cce..71714e9473 100644 --- a/src/core/core-varlink.c +++ b/src/core/core-varlink.c @@ -5,6 +5,8 @@ #include "strv.h" #include "user-util.h" #include "varlink.h" +#include "varlink-io.systemd.UserDatabase.h" +#include "varlink-io.systemd.ManagedOOM.h" typedef struct LookupParameters { const char *user_name; @@ -232,7 +234,7 @@ static int vl_method_subscribe_managed_oom_cgroups( /* We only take one subscriber for this method so return an error if there's already an existing one. * This shouldn't happen since systemd-oomd is the only client of this method. */ if (FLAGS_SET(flags, VARLINK_METHOD_MORE) && m->managed_oom_varlink) - return varlink_error(link, VARLINK_ERROR_SUBSCRIPTION_TAKEN, NULL); + return varlink_error(link, "io.systemd.ManagedOOM.SubscriptionTaken", NULL); r = build_managed_oom_cgroups_json(m, &v); if (r < 0) @@ -600,6 +602,13 @@ int manager_setup_varlink_server(Manager *m, VarlinkServer **ret) { varlink_server_set_userdata(s, m); + r = varlink_server_add_interface_many( + s, + &vl_interface_io_systemd_UserDatabase, + &vl_interface_io_systemd_ManagedOOM); + if (r < 0) + return log_error_errno(r, "Failed to add interfaces to varlink server: %m"); + r = varlink_server_bind_method_many( s, "io.systemd.UserDatabase.GetUserRecord", vl_method_get_user_record, diff --git a/src/home/homed-manager.c b/src/home/homed-manager.c index 903207b50f..c4525310fc 100644 --- a/src/home/homed-manager.c +++ b/src/home/homed-manager.c @@ -53,6 +53,7 @@ #include "user-record-util.h" #include "user-record.h" #include "user-util.h" +#include "varlink-io.systemd.UserDatabase.h" /* Where to look for private/public keys that are used to sign the user records. We are not using * CONF_PATHS_NULSTR() here since we want to insert /var/lib/systemd/home/ in the middle. And we insert that @@ -1003,6 +1004,10 @@ static int manager_bind_varlink(Manager *m) { varlink_server_set_userdata(m->varlink_server, m); + r = varlink_server_add_interface(m->varlink_server, &vl_interface_io_systemd_UserDatabase); + if (r < 0) + return log_error_errno(r, "Failed to add UserDatabase interface to varlink server: %m"); + r = varlink_server_bind_method_many( m->varlink_server, "io.systemd.UserDatabase.GetUserRecord", vl_method_get_user_record, diff --git a/src/journal/journald-server.c b/src/journal/journald-server.c index 615b3c6c6b..371338211a 100644 --- a/src/journal/journald-server.c +++ b/src/journal/journald-server.c @@ -58,6 +58,7 @@ #include "syslog-util.h" #include "uid-alloc-range.h" #include "user-util.h" +#include "varlink-io.systemd.Journal.h" #define USER_JOURNALS_MAX 1024 @@ -2224,6 +2225,10 @@ static int server_open_varlink(Server *s, const char *socket, int fd) { varlink_server_set_userdata(s->varlink_server, s); + r = varlink_server_add_interface(s->varlink_server, &vl_interface_io_systemd_Journal); + if (r < 0) + return log_error_errno(r, "Failed to add Journal interface to varlink server: %m"); + r = varlink_server_bind_method_many( s->varlink_server, "io.systemd.Journal.Synchronize", vl_method_synchronize, diff --git a/src/machine/machined-varlink.c b/src/machine/machined-varlink.c index ec625ad7b4..4b72ee9b0a 100644 --- a/src/machine/machined-varlink.c +++ b/src/machine/machined-varlink.c @@ -5,6 +5,7 @@ #include "mkdir.h" #include "user-util.h" #include "varlink.h" +#include "varlink-io.systemd.UserDatabase.h" typedef struct LookupParameters { const char *user_name; @@ -392,6 +393,10 @@ int manager_varlink_init(Manager *m) { varlink_server_set_userdata(s, m); + r = varlink_server_add_interface(s, &vl_interface_io_systemd_UserDatabase); + if (r < 0) + return log_error_errno(r, "Failed to add UserDatabase interface to varlink server: %m"); + r = varlink_server_bind_method_many( s, "io.systemd.UserDatabase.GetUserRecord", vl_method_get_user_record, diff --git a/src/oom/oomd-manager.c b/src/oom/oomd-manager.c index 49896ea7f1..75918a5fe7 100644 --- a/src/oom/oomd-manager.c +++ b/src/oom/oomd-manager.c @@ -15,6 +15,7 @@ #include "oomd-manager.h" #include "path-util.h" #include "percent-util.h" +#include "varlink-io.systemd.oom.h" typedef struct ManagedOOMMessage { ManagedOOMMode mode; @@ -733,6 +734,10 @@ static int manager_varlink_init(Manager *m, int fd) { varlink_server_set_userdata(s, m); + r = varlink_server_add_interface(s, &vl_interface_io_systemd_oom); + if (r < 0) + return log_error_errno(r, "Failed to add oom interface to varlink server: %m"); + r = varlink_server_bind_method(s, "io.systemd.oom.ReportManagedOOMCGroups", process_managed_oom_request); if (r < 0) return log_error_errno(r, "Failed to register varlink method: %m"); diff --git a/src/pcrextend/pcrextend.c b/src/pcrextend/pcrextend.c index 378d1b0e8f..c44e78c4ca 100644 --- a/src/pcrextend/pcrextend.c +++ b/src/pcrextend/pcrextend.c @@ -21,6 +21,8 @@ #include "pretty-print.h" #include "tpm2-pcr.h" #include "tpm2-util.h" +#include "varlink.h" +#include "varlink-io.systemd.PCRExtend.h" static bool arg_graceful = false; static char *arg_tpm2_device = NULL; @@ -28,11 +30,14 @@ static char **arg_banks = NULL; static char *arg_file_system = NULL; static bool arg_machine_id = false; static unsigned arg_pcr_index = UINT_MAX; +static bool arg_varlink = false; STATIC_DESTRUCTOR_REGISTER(arg_banks, strv_freep); STATIC_DESTRUCTOR_REGISTER(arg_tpm2_device, freep); STATIC_DESTRUCTOR_REGISTER(arg_file_system, freep); +#define EXTENSION_STRING_SAFE_LIMIT 1024 + static int help(int argc, char *argv[], void *userdata) { _cleanup_free_ char *link = NULL; int r; @@ -165,7 +170,12 @@ static int parse_argv(int argc, char *argv[]) { if (arg_file_system && arg_machine_id) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--file-system= and --machine-id may not be combined."); - if (arg_pcr_index == UINT_MAX) + r = varlink_invocation(VARLINK_ALLOW_ACCEPT); + if (r < 0) + return log_error_errno(r, "Failed to check if invoked in Varlink mode: %m"); + if (r > 0) + arg_varlink = true; + else if (arg_pcr_index == UINT_MAX) arg_pcr_index = (arg_file_system || arg_machine_id) ? TPM2_PCR_SYSTEM_IDENTITY : /* → PCR 15 */ TPM2_PCR_KERNEL_BOOT; /* → PCR 11 */ @@ -257,10 +267,119 @@ static int get_file_system_word( return 0; } +static int extend_now(unsigned pcr, const void *data, size_t size, Tpm2UserspaceEventType event) { + _cleanup_(tpm2_context_unrefp) Tpm2Context *c = NULL; + int r; + + r = tpm2_context_new(arg_tpm2_device, &c); + if (r < 0) + return r; + + r = determine_banks(c, pcr); + if (r < 0) + return r; + if (strv_isempty(arg_banks)) /* Still none? */ + return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Found a TPM2 without enabled PCR banks. Can't operate."); + + _cleanup_free_ char *joined_banks = NULL; + joined_banks = strv_join(arg_banks, ", "); + if (!joined_banks) + return log_oom(); + + _cleanup_free_ char *safe = NULL; + if (size > EXTENSION_STRING_SAFE_LIMIT) { + safe = cescape_length(data, EXTENSION_STRING_SAFE_LIMIT); + if (!safe) + return log_oom(); + + if (!strextend(&safe, "...")) + return log_oom(); + } else { + safe = cescape_length(data, size); + if (!safe) + return log_oom(); + } + + log_debug("Measuring '%s' into PCR index %u, banks %s.", safe, pcr, joined_banks); + + r = tpm2_extend_bytes(c, arg_banks, pcr, data, size, /* secret= */ NULL, /* secret_size= */ 0, event, safe); + if (r < 0) + return log_error_errno(r, "Could not extend PCR: %m"); + + log_struct(LOG_INFO, + "MESSAGE_ID=" SD_MESSAGE_TPM_PCR_EXTEND_STR, + LOG_MESSAGE("Extended PCR index %u with '%s' (banks %s).", pcr, safe, joined_banks), + "MEASURING=%s", safe, + "PCR=%u", pcr, + "BANKS=%s", joined_banks); + + return 0; +} + +typedef struct MethodExtendParameters { + unsigned pcr; + const char *text; + void *data; + size_t data_size; +} MethodExtendParameters; + +static int json_dispatch_binary_data(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + MethodExtendParameters *p = ASSERT_PTR(userdata); + _cleanup_free_ void *d = NULL; + size_t l; + int r; + + r = json_variant_unbase64(variant, &d, &l); + if (r < 0) + return json_log(variant, flags, r, "JSON variant is not a base64 string."); + + free_and_replace(p->data, d); + p->data_size = l; + + return 0; +} + +static int vl_method_extend(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) { + + static const JsonDispatch dispatch_table[] = { + { "pcr", JSON_VARIANT_UNSIGNED, json_dispatch_uint, offsetof(MethodExtendParameters, pcr), JSON_MANDATORY }, + { "text", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(MethodExtendParameters, text), 0 }, + { "data", JSON_VARIANT_STRING, json_dispatch_binary_data, 0, 0 }, + {} + }; + MethodExtendParameters p = { + .pcr = UINT_MAX, + }; + int r; + + assert(link); + + r = json_dispatch(parameters, dispatch_table, NULL, 0, &p); + if (r < 0) + return r; + + if (!TPM2_PCR_INDEX_VALID(p.pcr)) + return varlink_errorb(link, VARLINK_ERROR_INVALID_PARAMETER, JSON_BUILD_OBJECT(JSON_BUILD_PAIR_STRING("parameter", "pcr"))); + + if (p.text) { + /* Specifying both the text string and the binary data is not allowed */ + if (p.data) + return varlink_errorb(link, VARLINK_ERROR_INVALID_PARAMETER, JSON_BUILD_OBJECT(JSON_BUILD_PAIR_STRING("parameter", "data"))); + + r = extend_now(p.pcr, p.text, strlen(p.text), _TPM2_USERSPACE_EVENT_TYPE_INVALID); + } else if (p.data) + r = extend_now(p.pcr, p.data, p.data_size, _TPM2_USERSPACE_EVENT_TYPE_INVALID); + else + return varlink_errorb(link, VARLINK_ERROR_INVALID_PARAMETER, JSON_BUILD_OBJECT(JSON_BUILD_PAIR_STRING("parameter", "text"))); + if (r < 0) + return r; + + return varlink_reply(link, NULL); +} + static int run(int argc, char *argv[]) { - _cleanup_free_ char *joined = NULL, *word = NULL; + _cleanup_free_ char *word = NULL; Tpm2UserspaceEventType event; - size_t length; int r; log_setup(); @@ -269,6 +388,30 @@ static int run(int argc, char *argv[]) { if (r <= 0) return r; + if (arg_varlink) { + _cleanup_(varlink_server_unrefp) VarlinkServer *varlink_server = NULL; + + /* Invocation as Varlink service */ + + r = varlink_server_new(&varlink_server, VARLINK_SERVER_ROOT_ONLY); + if (r < 0) + return log_error_errno(r, "Failed to allocate Varlink server: %m"); + + r = varlink_server_add_interface(varlink_server, &vl_interface_io_systemd_PCRExtend); + if (r < 0) + return log_error_errno(r, "Failed to add Varlink interface: %m"); + + r = varlink_server_bind_method(varlink_server, "io.systemd.PCRExtend.Extend", vl_method_extend); + if (r < 0) + return log_error_errno(r, "Failed to bind Varlink method: %m"); + + r = varlink_server_loop_auto(varlink_server); + if (r < 0) + return log_error_errno(r, "Failed to run Varlink event loop: %m"); + + return EXIT_SUCCESS; + } + if (arg_file_system) { _cleanup_free_ char *normalized = NULL, *normalized_escaped = NULL; _cleanup_(sd_device_unrefp) sd_device *d = NULL; @@ -348,8 +491,6 @@ static int run(int argc, char *argv[]) { return EXIT_SUCCESS; } - length = strlen(word); - /* Skip logic if sd-stub is not used, after all PCR 11 might have a very different purpose then. */ r = efi_measured_uki(LOG_ERR); if (r < 0) @@ -359,34 +500,10 @@ static int run(int argc, char *argv[]) { return EXIT_SUCCESS; } - _cleanup_(tpm2_context_unrefp) Tpm2Context *c = NULL; - r = tpm2_context_new(arg_tpm2_device, &c); + r = extend_now(arg_pcr_index, word, strlen(word), event); if (r < 0) return log_error_errno(r, "Failed to create TPM2 context: %m"); - r = determine_banks(c, arg_pcr_index); - if (r < 0) - return r; - if (strv_isempty(arg_banks)) /* Still none? */ - return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Found a TPM2 without enabled PCR banks. Can't operate."); - - joined = strv_join(arg_banks, ", "); - if (!joined) - return log_oom(); - - log_debug("Measuring '%s' into PCR index %u, banks %s.", word, arg_pcr_index, joined); - - r = tpm2_extend_bytes(c, arg_banks, arg_pcr_index, word, length, NULL, 0, event, word); - if (r < 0) - return log_error_errno(r, "Could not extend PCR: %m"); - - log_struct(LOG_INFO, - "MESSAGE_ID=" SD_MESSAGE_TPM_PCR_EXTEND_STR, - LOG_MESSAGE("Extended PCR index %u with '%s' (banks %s).", arg_pcr_index, word, joined), - "MEASURING=%s", word, - "PCR=%u", arg_pcr_index, - "BANKS=%s", joined); - return EXIT_SUCCESS; } diff --git a/src/resolve/resolved-varlink.c b/src/resolve/resolved-varlink.c index 83b8e52807..c9bb616acb 100644 --- a/src/resolve/resolved-varlink.c +++ b/src/resolve/resolved-varlink.c @@ -5,6 +5,8 @@ #include "resolved-dns-synthesize.h" #include "resolved-varlink.h" #include "socket-netlink.h" +#include "varlink-io.systemd.Resolve.h" +#include "varlink-io.systemd.Resolve.Monitor.h" typedef struct LookupParameters { int ifindex; @@ -699,6 +701,10 @@ static int varlink_monitor_server_init(Manager *m) { varlink_server_set_userdata(server, m); + r = varlink_server_add_interface(server, &vl_interface_io_systemd_Resolve_Monitor); + if (r < 0) + return log_error_errno(r, "Failed to add Resolve.Monitor interface to varlink server: %m"); + r = varlink_server_bind_method_many( server, "io.systemd.Resolve.Monitor.SubscribeQueryResults", vl_method_subscribe_query_results, @@ -741,6 +747,10 @@ static int varlink_main_server_init(Manager *m) { varlink_server_set_userdata(s, m); + r = varlink_server_add_interface(s, &vl_interface_io_systemd_Resolve); + if (r < 0) + return log_error_errno(r, "Failed to add Resolve interface to varlink server: %m"); + r = varlink_server_bind_method_many( s, "io.systemd.Resolve.ResolveHostname", vl_method_resolve_hostname, diff --git a/src/shared/meson.build b/src/shared/meson.build index 3df178bfa1..569b865636 100644 --- a/src/shared/meson.build +++ b/src/shared/meson.build @@ -166,6 +166,16 @@ shared_sources = files( 'userdb-dropin.c', 'userdb.c', 'varlink.c', + 'varlink-idl.c', + 'varlink-io.systemd.c', + 'varlink-io.systemd.Journal.c', + 'varlink-io.systemd.ManagedOOM.c', + 'varlink-io.systemd.PCRExtend.c', + 'varlink-io.systemd.Resolve.Monitor.c', + 'varlink-io.systemd.Resolve.c', + 'varlink-io.systemd.UserDatabase.c', + 'varlink-io.systemd.oom.c', + 'varlink-org.varlink.service.c', 'verb-log-control.c', 'verbs.c', 'vlan-util.c', diff --git a/src/shared/varlink-idl.c b/src/shared/varlink-idl.c new file mode 100644 index 0000000000..157474facf --- /dev/null +++ b/src/shared/varlink-idl.c @@ -0,0 +1,1582 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "memstream-util.h" +#include "strv.h" +#include "terminal-util.h" +#include "varlink-idl.h" +#include "set.h" + +enum { + COLOR_SYMBOL_TYPE, /* interface, method, type, error */ + COLOR_FIELD_TYPE, /* string, bool, … */ + COLOR_IDENTIFIER, + COLOR_MARKS, /* [], ->, ?, … */ + COLOR_RESET, + _COLOR_MAX, +}; + +static int varlink_idl_format_all_fields(FILE *f, const VarlinkSymbol *symbol, VarlinkFieldDirection direction, const char *indent, const char *const colors[static _COLOR_MAX]); + +static int varlink_idl_format_enum_values( + FILE *f, + const VarlinkSymbol *symbol, + const char *indent, + const char *const colors[static _COLOR_MAX]) { + + bool first = true; + + assert(f); + assert(symbol); + assert(symbol->symbol_type == VARLINK_ENUM_TYPE); + + for (const VarlinkField *field = symbol->fields; field->field_type != _VARLINK_FIELD_TYPE_END_MARKER; field++) { + + if (first) { + first = false; + fputs("(\n", f); + } else + fputs(",\n", f); + + fputs(strempty(indent), f); + fputs("\t", f); + fputs(colors[COLOR_IDENTIFIER], f); + fputs(field->name, f); + fputs(colors[COLOR_RESET], f); + } + + if (first) + fputs("()", f); + else { + fputs("\n", f); + fputs(strempty(indent), f); + fputs(")", f); + } + + return 0; +} + +static int varlink_idl_format_field( + FILE *f, + const VarlinkField *field, + const char *indent, + const char *const colors[static _COLOR_MAX]) { + + assert(f); + assert(field); + + fputs(strempty(indent), f); + fputs(colors[COLOR_IDENTIFIER], f); + fputs(field->name, f); + fputs(colors[COLOR_RESET], f); + fputs(": ", f); + + if (FLAGS_SET(field->field_flags, VARLINK_NULLABLE)) { + fputs(colors[COLOR_MARKS], f); + fputs("?", f); + fputs(colors[COLOR_RESET], f); + } + + switch (field->field_flags & (VARLINK_MAP|VARLINK_ARRAY)) { + + case VARLINK_MAP: + fputs(colors[COLOR_MARKS], f); + fputs("[", f); + fputs(colors[COLOR_FIELD_TYPE], f); + fputs("string", f); + fputs(colors[COLOR_MARKS], f); + fputs("]", f); + fputs(colors[COLOR_RESET], f); + break; + + case VARLINK_ARRAY: + fputs(colors[COLOR_MARKS], f); + fputs("[]", f); + fputs(colors[COLOR_RESET], f); + break; + + case 0: + break; + + default: + assert_not_reached(); + } + + switch (field->field_type) { + + case VARLINK_BOOL: + fputs(colors[COLOR_FIELD_TYPE], f); + fputs("bool", f); + fputs(colors[COLOR_RESET], f); + break; + + case VARLINK_INT: + fputs(colors[COLOR_FIELD_TYPE], f); + fputs("int", f); + fputs(colors[COLOR_RESET], f); + break; + + case VARLINK_FLOAT: + fputs(colors[COLOR_FIELD_TYPE], f); + fputs("float", f); + fputs(colors[COLOR_RESET], f); + break; + + case VARLINK_STRING: + fputs(colors[COLOR_FIELD_TYPE], f); + fputs("string", f); + fputs(colors[COLOR_RESET], f); + break; + + case VARLINK_OBJECT: + fputs(colors[COLOR_FIELD_TYPE], f); + fputs("object", f); + fputs(colors[COLOR_RESET], f); + break; + + case VARLINK_NAMED_TYPE: + fputs(colors[COLOR_IDENTIFIER], f); + fputs(ASSERT_PTR(field->named_type), f); + fputs(colors[COLOR_RESET], f); + break; + + case VARLINK_STRUCT: + return varlink_idl_format_all_fields(f, ASSERT_PTR(field->symbol), VARLINK_REGULAR, indent, colors); + + case VARLINK_ENUM: + return varlink_idl_format_enum_values(f, ASSERT_PTR(field->symbol), indent, colors); + + default: + assert_not_reached(); + } + + return 0; +} + +static int varlink_idl_format_all_fields( + FILE *f, + const VarlinkSymbol *symbol, + VarlinkFieldDirection filter_direction, + const char *indent, + const char *const colors[static _COLOR_MAX]) { + + _cleanup_free_ char *indent2 = NULL; + bool first = true; + int r; + + assert(f); + assert(symbol); + assert(IN_SET(symbol->symbol_type, VARLINK_STRUCT_TYPE, VARLINK_METHOD, VARLINK_ERROR)); + + indent2 = strjoin(strempty(indent), "\t"); + if (!indent2) + return -ENOMEM; + + for (const VarlinkField *field = symbol->fields; field->field_type != _VARLINK_FIELD_TYPE_END_MARKER; field++) { + + if (field->field_direction != filter_direction) + continue; + + if (first) { + first = false; + fputs("(\n", f); + } else + fputs(",\n", f); + + r = varlink_idl_format_field(f, field, indent2, colors); + if (r < 0) + return r; + } + + if (first) + fputs("()", f); + else { + fputs("\n", f); + fputs(strempty(indent), f); + fputs(")", f); + } + + return 0; +} + +static int varlink_idl_format_symbol( + FILE *f, + const VarlinkSymbol *symbol, + const char *const colors[static _COLOR_MAX]) { + int r; + + assert(f); + assert(symbol); + + switch (symbol->symbol_type) { + + case VARLINK_ENUM_TYPE: + fputs(colors[COLOR_SYMBOL_TYPE], f); + fputs("type ", f); + fputs(colors[COLOR_IDENTIFIER], f); + fputs(symbol->name, f); + fputs(colors[COLOR_RESET], f); + + r = varlink_idl_format_enum_values(f, symbol, /* indent= */ NULL, colors); + break; + + case VARLINK_STRUCT_TYPE: + fputs(colors[COLOR_SYMBOL_TYPE], f); + fputs("type ", f); + fputs(colors[COLOR_IDENTIFIER], f); + fputs(symbol->name, f); + fputs(colors[COLOR_RESET], f); + + r = varlink_idl_format_all_fields(f, symbol, VARLINK_REGULAR, /* indent= */ NULL, colors); + break; + + case VARLINK_METHOD: + fputs(colors[COLOR_SYMBOL_TYPE], f); + fputs("method ", f); + fputs(colors[COLOR_IDENTIFIER], f); + fputs(symbol->name, f); + fputs(colors[COLOR_RESET], f); + + r = varlink_idl_format_all_fields(f, symbol, VARLINK_INPUT, /* indent= */ NULL, colors); + if (r < 0) + return r; + + fputs(colors[COLOR_MARKS], f); + fputs(" -> ", f); + fputs(colors[COLOR_RESET], f); + + r = varlink_idl_format_all_fields(f, symbol, VARLINK_OUTPUT, /* indent= */ NULL, colors); + break; + + case VARLINK_ERROR: + fputs(colors[COLOR_SYMBOL_TYPE], f); + fputs("error ", f); + fputs(colors[COLOR_IDENTIFIER], f); + fputs(symbol->name, f); + fputs(colors[COLOR_RESET], f); + + r = varlink_idl_format_all_fields(f, symbol, VARLINK_REGULAR, /* indent= */ NULL, colors); + break; + + default: + assert_not_reached(); + } + if (r < 0) + return r; + + fputs("\n", f); + return 0; +} + +static int varlink_idl_format_all_symbols( + FILE *f, + const VarlinkInterface *interface, + VarlinkSymbolType filter_type, + const char *const colors[static _COLOR_MAX]) { + + int r; + + assert(f); + assert(interface); + + for (const VarlinkSymbol *const*symbol = interface->symbols; *symbol; symbol++) { + + if ((*symbol)->symbol_type != filter_type) + continue; + + fputs("\n", f); + + r = varlink_idl_format_symbol(f, *symbol, colors); + if (r < 0) + return r; + } + + return 0; +} + +int varlink_idl_dump(FILE *f, int use_colors, const VarlinkInterface *interface) { + static const char* const color_table[_COLOR_MAX] = { + [COLOR_SYMBOL_TYPE] = ANSI_HIGHLIGHT_GREEN, + [COLOR_FIELD_TYPE] = ANSI_HIGHLIGHT_BLUE, + [COLOR_IDENTIFIER] = ANSI_NORMAL, + [COLOR_MARKS] = ANSI_HIGHLIGHT_MAGENTA, + [COLOR_RESET] = ANSI_NORMAL, + }; + + static const char* const color_off[_COLOR_MAX] = { + "", "", "", "", "", + }; + + int r; + + assert(interface); + + if (!f) + f = stdout; + + if (use_colors < 0) + use_colors = colors_enabled(); + + const char *const *colors = use_colors ? color_table : color_off; + + fputs(colors[COLOR_SYMBOL_TYPE], f); + fputs("interface ", f); + fputs(colors[COLOR_IDENTIFIER], f); + fputs(ASSERT_PTR(interface->name), f); + fputs(colors[COLOR_RESET], f); + fputs("\n", f); + + for (VarlinkSymbolType t = 0; t < _VARLINK_SYMBOL_TYPE_MAX; t++) { + r = varlink_idl_format_all_symbols(f, interface, t, colors); + if (r < 0) + return r; + } + + return 0; +} + +int varlink_idl_format(const VarlinkInterface *interface, char **ret) { + _cleanup_(memstream_done) MemStream memstream = {}; + int r; + + if (!memstream_init(&memstream)) + return -errno; + + r = varlink_idl_dump(memstream.f, /* use_colors= */ false, interface); + if (r < 0) + return r; + + return memstream_finalize(&memstream, ret, NULL); +} + +static VarlinkSymbol *varlink_symbol_free(VarlinkSymbol *symbol) { + if (!symbol) + return NULL; + + /* See comment in varlink_interface_free() regarding the casting away of `const` */ + + free((char*) symbol->name); + + for (size_t i = 0; symbol->fields[i].field_type != _VARLINK_FIELD_TYPE_END_MARKER; i++) { + VarlinkField *field = symbol->fields + i; + + free((void*) field->name); + free((void*) field->named_type); + + /* The symbol pointer might either point to a named symbol, in which case that symbol is + * owned by the interface, or by an anomyous symbol, in which case it is owned by us, and we + * need to free it */ + if (field->symbol && field->field_type != VARLINK_NAMED_TYPE) + varlink_symbol_free((VarlinkSymbol*) field->symbol); + } + + return mfree(symbol); +} + +VarlinkInterface* varlink_interface_free(VarlinkInterface *interface) { + if (!interface) + return NULL; + + /* So here's the thing: in most cases we want that users of this define their interface descriptions + * in C code, and hence the definitions are constant and immutable during the life-cycle of the + * system. Because of that we define all structs with const* pointers. It makes it very nice and + * straight-forward to populate these structs with literal C strings. However, in some not so common + * cases we also want to allocate these structures dynamically on the heap, when parsing interface + * descriptions. But given this should be the exceptional and not the common case, we decided to + * simple cast away the 'const' where needed, even if it is ugly. */ + + free((char*) interface->name); + + for (size_t i = 0; interface->symbols[i]; i++) + varlink_symbol_free((VarlinkSymbol*) interface->symbols[i]); + + return mfree(interface); +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(VarlinkSymbol*, varlink_symbol_free); + +static int varlink_interface_realloc(VarlinkInterface **interface, size_t n_symbols) { + VarlinkInterface *n; + + assert(interface); + + n_symbols ++; /* Space for trailing NULL end marker symbol */ + + /* Overflow check */ + if (n_symbols > (SIZE_MAX - offsetof(VarlinkInterface, symbols)) / sizeof(VarlinkSymbol*)) + return -ENOMEM; + + n = realloc0(*interface, offsetof(VarlinkInterface, symbols) + sizeof(VarlinkSymbol*) * n_symbols); + if (!n) + return -ENOMEM; + + *interface = n; + return 0; +} + +static int varlink_symbol_realloc(VarlinkSymbol **symbol, size_t n_fields) { + VarlinkSymbol *n; + + assert(symbol); + + n_fields ++; /* Space for trailing end marker field */ + + /* Overflow check */ + if (n_fields > (SIZE_MAX - offsetof(VarlinkSymbol, fields)) / sizeof(VarlinkField)) + return -ENOMEM; + + n = realloc0(*symbol, offsetof(VarlinkSymbol, fields) + sizeof(VarlinkField) * n_fields); + if (!n) + return -ENOMEM; + + *symbol = n; + return 0; +} + +#define VALID_CHARS_IDENTIFIER ALPHANUMERICAL "_" +#define VALID_CHARS_RESERVED LOWERCASE_LETTERS +#define VALID_CHARS_INTERFACE_NAME ALPHANUMERICAL ".-" + +static void advance_line_column(const char *p, size_t n, unsigned *line, unsigned *column) { + + assert(p); + assert(line); + assert(column); + + for (; n > 0; p++, n--) { + + if (*p == '\n') { + (*line)++; + *column = 1; + } else + (*column)++; + } +} + +static size_t token_match( + const char *p, + const char *allowed_delimiters, + const char *allowed_chars) { + + /* Checks if the string p begins either with one of the token characters in allowed_delimiters or + * with a string consisting of allowed_chars. */ + + assert(p); + + if (allowed_delimiters && strchr(allowed_delimiters, *p)) + return 1; + + if (!allowed_chars) + return 0; + + return strspn(p, allowed_chars); +} + +static int varlink_idl_subparse_token( + const char **p, + unsigned *line, + unsigned *column, + const char *allowed_delimiters, + const char *allowed_chars, + char **ret_token) { + + _cleanup_free_ char *t = NULL; + size_t l; + + assert(p); + assert(*p); + assert(line); + assert(column); + assert(ret_token); + + if (**p == '\0') { /* eof */ + *ret_token = NULL; + return 0; + } + + l = token_match(*p, allowed_delimiters, allowed_chars); + + /* No token of the permitted character set found? Then let's try to skip over whitespace and try again */ + if (l == 0) { + size_t ll; + + ll = strspn(*p, WHITESPACE); + advance_line_column(*p, ll, line, column); + *p += ll; + + if (**p == '\0') { /* eof */ + *ret_token = NULL; + return 0; + } + + l = token_match(*p, allowed_delimiters, allowed_chars); + if (l == 0) + return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "Couldn't find token of allowed chars '%s' or allowed delimiters '%s'.", strempty(allowed_chars), strempty(allowed_delimiters)); + } + + t = strndup(*p, l); + if (!t) + return -ENOMEM; + + advance_line_column(*p, l, line, column); + *p += l; + + *ret_token = TAKE_PTR(t); + return 1; +} + +static int varlink_idl_subparse_comment( + const char **p, + unsigned *line, + unsigned *column) { + + size_t l; + + assert(p); + assert(*p); + assert(line); + assert(column); + + l = strcspn(*p, NEWLINE); + advance_line_column(*p, l + 1, line, column); + *p += l; + + return 1; +} + +static int varlink_idl_subparse_whitespace( + const char **p, + unsigned *line, + unsigned *column) { + + size_t l; + + assert(p); + assert(*p); + assert(line); + assert(column); + + l = strspn(*p, WHITESPACE); + advance_line_column(*p, l, line, column); + *p += l; + + return 1; +} + +static int varlink_idl_subparse_struct_or_enum(const char **p, unsigned *line, unsigned *column, VarlinkSymbol **symbol, size_t *n_fields, VarlinkFieldDirection direction); + +static int varlink_idl_subparse_field_type( + const char **p, + unsigned *line, + unsigned *column, + VarlinkField *field) { + + size_t l; + int r; + + assert(p); + assert(*p); + assert(line); + assert(field); + + r = varlink_idl_subparse_whitespace(p, line, column); + if (r < 0) + return r; + + if (startswith(*p, "?")) { + field->field_flags |= VARLINK_NULLABLE; + l = 1; + } else { + field->field_flags &= ~VARLINK_NULLABLE; + l = 0; + } + + advance_line_column(*p, l, line, column); + *p += l; + + if (startswith(*p, "[]")) { + l = 2; + field->field_flags = (field->field_flags & ~VARLINK_MAP) | VARLINK_ARRAY; + } else if (startswith(*p, "[string]")) { + l = 8; + field->field_flags = (field->field_flags & ~VARLINK_ARRAY) | VARLINK_MAP; + } else { + l = 0; + field->field_flags = field->field_flags & ~(VARLINK_MAP | VARLINK_ARRAY); + } + + advance_line_column(*p, l, line, column); + *p += l; + + if (startswith(*p, "bool")) { + l = 4; + field->field_type = VARLINK_BOOL; + } else if (startswith(*p, "int")) { + l = 3; + field->field_type = VARLINK_INT; + } else if (startswith(*p, "float")) { + l = 5; + field->field_type = VARLINK_FLOAT; + } else if (startswith(*p, "string")) { + l = 6; + field->field_type = VARLINK_STRING; + } else if (startswith(*p, "object")) { + l = 6; + field->field_type = VARLINK_OBJECT; + } else if (**p == '(') { + _cleanup_(varlink_symbol_freep) VarlinkSymbol *symbol = NULL; + size_t n_fields = 0; + + r = varlink_symbol_realloc(&symbol, n_fields); + if (r < 0) + return r; + + symbol->symbol_type = _VARLINK_SYMBOL_TYPE_INVALID; + + r = varlink_idl_subparse_struct_or_enum( + p, + line, + column, + &symbol, + &n_fields, + VARLINK_REGULAR); + if (r < 0) + return r; + + if (symbol->symbol_type == VARLINK_STRUCT_TYPE) + field->field_type = VARLINK_STRUCT; + else { + assert(symbol->symbol_type == VARLINK_ENUM_TYPE); + field->field_type = VARLINK_ENUM; + } + + field->symbol = TAKE_PTR(symbol); + l = 0; + } else { + _cleanup_free_ char *token = NULL; + + r = varlink_idl_subparse_token(p, line, column, /* valid_tokens= */ NULL, VALID_CHARS_IDENTIFIER, &token); + if (r < 0) + return r; + if (!token) + return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "%u:%u: Premature EOF: %m", *line, *column); + + field->named_type = TAKE_PTR(token); + field->field_type = VARLINK_NAMED_TYPE; + l = 0; + } + + advance_line_column(*p, l, line, column); + *p += l; + + return 0; +} + +static int varlink_idl_subparse_struct_or_enum( + const char **p, + unsigned *line, + unsigned *column, + VarlinkSymbol **symbol, + size_t *n_fields, + VarlinkFieldDirection direction) { + + enum { + STATE_OPEN, + STATE_NAME, + STATE_COLON, + STATE_COMMA, + STATE_DONE, + } state = STATE_OPEN; + _cleanup_free_ char *field_name = NULL; + const char *allowed_delimiters = "(", *allowed_chars = NULL; + int r; + + assert(p); + assert(*p); + assert(line); + assert(column); + assert(symbol); + assert(*symbol); + assert(n_fields); + + while (state != STATE_DONE) { + _cleanup_free_ char *token = NULL; + + r = varlink_idl_subparse_token( + p, + line, + column, + allowed_delimiters, + allowed_chars, + &token); + if (r < 0) + return r; + + switch (state) { + + case STATE_OPEN: + if (!token) + return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "%u:%u: Premature EOF: %m", *line, *column); + if (!streq(token, "(")) + return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "%u:%u: Unexpected token '%s': %m", *line, *column, token); + + state = STATE_NAME; + allowed_delimiters = ")"; + allowed_chars = VALID_CHARS_IDENTIFIER; + break; + + case STATE_NAME: + assert(!field_name); + + if (!token) + return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "%u:%u: Premature EOF: %m", *line, *column); + if (streq(token, "#")) { + r = varlink_idl_subparse_comment(p, line, column); + if (r < 0) + return r; + } else if (streq(token, ")")) + state = STATE_DONE; + else { + field_name = TAKE_PTR(token); + state = STATE_COLON; + allowed_delimiters = ":,)"; + allowed_chars = NULL; + } + + break; + + case STATE_COLON: + assert(field_name); + + if (!token) + return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "%u:%u: Premature EOF: %m", *line, *column); + + if (streq(token, ":")) { + VarlinkField *field; + + if ((*symbol)->symbol_type < 0) + (*symbol)->symbol_type = VARLINK_STRUCT_TYPE; + if ((*symbol)->symbol_type == VARLINK_ENUM_TYPE) + return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "%u:%u: Enum with struct fields, refusing: %m", *line, *column); + + r = varlink_symbol_realloc(symbol, *n_fields + 1); + if (r < 0) + return r; + + field = (*symbol)->fields + (*n_fields)++; + *field = (VarlinkField) { + .name = TAKE_PTR(field_name), + .field_type = _VARLINK_FIELD_TYPE_INVALID, + .field_direction = direction, + }; + + r = varlink_idl_subparse_field_type(p, line, column, field); + if (r < 0) + return r; + + state = STATE_COMMA; + allowed_delimiters = ",)"; + allowed_chars = NULL; + + } else if (STR_IN_SET(token, ",", ")")) { + VarlinkField *field; + + if ((*symbol)->symbol_type < 0) + (*symbol)->symbol_type = VARLINK_ENUM_TYPE; + if ((*symbol)->symbol_type != VARLINK_ENUM_TYPE) + return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "%u:%u: Struct with enum fields, refusing: %m", *line, *column); + + r = varlink_symbol_realloc(symbol, *n_fields + 1); + if (r < 0) + return r; + + field = (*symbol)->fields + (*n_fields)++; + *field = (VarlinkField) { + .name = TAKE_PTR(field_name), + .field_type = VARLINK_ENUM_VALUE, + }; + + if (streq(token, ",")) { + state = STATE_NAME; + allowed_delimiters = NULL; + allowed_chars = VALID_CHARS_IDENTIFIER; + } else { + assert(streq(token, ")")); + state = STATE_DONE; + } + } else + return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "%u:%u: Unexpected token '%s': %m", *line, *column, token); + + break; + + case STATE_COMMA: + assert(!field_name); + + if (!token) + return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "%u:%u: Premature EOF: %m", *line, *column); + if (streq(token, ",")) { + state = STATE_NAME; + allowed_delimiters = NULL; + allowed_chars = VALID_CHARS_IDENTIFIER; + } else if (streq(token, ")")) + state = STATE_DONE; + else + return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "%u:%u: Unexpected token '%s': %m", *line, *column, token); + break; + + default: + assert_not_reached(); + } + } + + return 0; +} + +static int varlink_idl_resolve_symbol_types(VarlinkInterface *interface, VarlinkSymbol *symbol) { + assert(interface); + assert(symbol); + + for (VarlinkField *field = symbol->fields; field->field_type != _VARLINK_FIELD_TYPE_END_MARKER; field++) { + const VarlinkSymbol *found; + + if (field->field_type != VARLINK_NAMED_TYPE) + continue; + + if (field->symbol) /* Already resolved */ + continue; + + if (!field->named_type) + return log_debug_errno(SYNTHETIC_ERRNO(ENETUNREACH), "Named type field lacking a type name."); + + found = varlink_idl_find_symbol(interface, _VARLINK_SYMBOL_TYPE_INVALID, field->named_type); + if (!found) + return log_debug_errno(SYNTHETIC_ERRNO(ENETUNREACH), "Failed to find type '%s'.", field->named_type); + + if (!IN_SET(found->symbol_type, VARLINK_STRUCT_TYPE, VARLINK_ENUM_TYPE)) + return log_debug_errno(SYNTHETIC_ERRNO(ENETUNREACH), "Symbol '%s' is referenced as type but is not a type.", field->named_type); + + field->symbol = found; + } + + return 0; +} + +static int varlink_idl_resolve_types(VarlinkInterface *interface) { + int r; + + assert(interface); + + for (VarlinkSymbol **symbol = (VarlinkSymbol**) interface->symbols; *symbol; symbol++) { + r = varlink_idl_resolve_symbol_types(interface, *symbol); + if (r < 0) + return r; + } + + return 0; +} + +int varlink_idl_parse( + const char *text, + unsigned *line, + unsigned *column, + VarlinkInterface **ret) { + + _cleanup_(varlink_interface_freep) VarlinkInterface *interface = NULL; + _cleanup_(varlink_symbol_freep) VarlinkSymbol *symbol = NULL; + enum { + STATE_PRE_INTERFACE, + STATE_INTERFACE, + STATE_PRE_SYMBOL, + STATE_METHOD, + STATE_METHOD_NAME, + STATE_METHOD_ARROW, + STATE_TYPE, + STATE_TYPE_NAME, + STATE_ERROR, + STATE_ERROR_NAME, + STATE_DONE, + } state = STATE_PRE_INTERFACE; + const char *allowed_delimiters = "#", *allowed_chars = VALID_CHARS_RESERVED; + size_t n_symbols = 0, n_fields = 1; + unsigned _line = 0, _column = 1; + const char **p = &text; + int r; + + if (!line) + line = &_line; + if (!column) + column = &_column; + + while (state != STATE_DONE) { + _cleanup_free_ char *token = NULL; + + r = varlink_idl_subparse_token( + p, + line, + column, + allowed_delimiters, + allowed_chars, + &token); + if (r < 0) + return r; + + switch (state) { + + case STATE_PRE_INTERFACE: + if (!token) + return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "%u:%u: Premature EOF: %m", *line, *column); + if (streq(token, "#")) { + r = varlink_idl_subparse_comment(&text, line, column); + if (r < 0) + return r; + } else if (streq(token, "interface")) { + state = STATE_INTERFACE; + allowed_delimiters = NULL; + allowed_chars = VALID_CHARS_INTERFACE_NAME; + } else + return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "%u:%u: Unexpected token '%s': %m", *line, *column, token); + break; + + case STATE_INTERFACE: + assert(!interface); + assert(n_symbols == 0); + + if (!token) + return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "%u:%u: Premature EOF: %m", *line, *column); + + r = varlink_interface_realloc(&interface, n_symbols); + if (r < 0) + return r; + + interface->name = TAKE_PTR(token); + state = STATE_PRE_SYMBOL; + allowed_delimiters = "#"; + allowed_chars = VALID_CHARS_RESERVED; + break; + + case STATE_PRE_SYMBOL: + if (!token) { + state = STATE_DONE; + break; + } + + if (streq(token, "#")) { + r = varlink_idl_subparse_comment(&text, line, column); + if (r < 0) + return r; + } else if (streq(token, "method")) { + state = STATE_METHOD; + allowed_chars = VALID_CHARS_IDENTIFIER; + } else if (streq(token, "type")) { + state = STATE_TYPE; + allowed_chars = VALID_CHARS_IDENTIFIER; + } else if (streq(token, "error")) { + state = STATE_ERROR; + allowed_chars = VALID_CHARS_IDENTIFIER; + } else + return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "%u:%u: Unexpected token '%s': %m", *line, *column, token); + + break; + + case STATE_METHOD: + assert(!symbol); + n_fields = 0; + + r = varlink_symbol_realloc(&symbol, n_fields); + if (r < 0) + return r; + + symbol->symbol_type = VARLINK_METHOD; + symbol->name = TAKE_PTR(token); + + r = varlink_idl_subparse_struct_or_enum(&text, line, column, &symbol, &n_fields, VARLINK_INPUT); + if (r < 0) + return r; + + state = STATE_METHOD_ARROW; + allowed_chars = "->"; + break; + + case STATE_METHOD_ARROW: + assert(symbol); + + if (!streq(token, "->")) + return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "%u:%u: Unexpected token '%s': %m", *line, *column, token); + + r = varlink_idl_subparse_struct_or_enum(&text, line, column, &symbol, &n_fields, VARLINK_OUTPUT); + if (r < 0) + return r; + + r = varlink_interface_realloc(&interface, n_symbols + 1); + if (r < 0) + return r; + + interface->symbols[n_symbols++] = TAKE_PTR(symbol); + + state = STATE_PRE_SYMBOL; + allowed_chars = VALID_CHARS_RESERVED "#"; + break; + + case STATE_TYPE: + assert(!symbol); + n_fields = 0; + + r = varlink_symbol_realloc(&symbol, n_fields); + if (r < 0) + return r; + + symbol->symbol_type = _VARLINK_SYMBOL_TYPE_INVALID; /* don't know yet if enum or struct, will be fileld in by varlink_idl_subparse_struct_or_enum() */ + symbol->name = TAKE_PTR(token); + + r = varlink_idl_subparse_struct_or_enum(&text, line, column, &symbol, &n_fields, VARLINK_REGULAR); + if (r < 0) + return r; + + r = varlink_interface_realloc(&interface, n_symbols + 1); + if (r < 0) + return r; + + interface->symbols[n_symbols++] = TAKE_PTR(symbol); + + state = STATE_PRE_SYMBOL; + allowed_chars = VALID_CHARS_RESERVED "#"; + break; + + case STATE_ERROR: + assert(!symbol); + n_fields = 0; + + r = varlink_symbol_realloc(&symbol, n_fields); + if (r < 0) + return r; + + symbol->symbol_type = VARLINK_ERROR; + symbol->name = TAKE_PTR(token); + + r = varlink_idl_subparse_struct_or_enum(&text, line, column, &symbol, &n_fields, VARLINK_REGULAR); + if (r < 0) + return r; + + r = varlink_interface_realloc(&interface, n_symbols + 1); + if (r < 0) + return r; + + interface->symbols[n_symbols++] = TAKE_PTR(symbol); + + state = STATE_PRE_SYMBOL; + allowed_chars = VALID_CHARS_RESERVED "#"; + break; + + default: + assert_not_reached(); + } + } + + r = varlink_idl_resolve_types(interface); + if (r < 0) + return r; + + *ret = TAKE_PTR(interface); + return 0; +} + +bool varlink_idl_field_name_is_valid(const char *name) { + if (isempty(name)) + return false; + + /* Field names may start with lower or uppercase char, but no numerals or underscore */ + if (!strchr(LETTERS, name[0])) + return false; + + /* Otherwise fields may be alphanumerical or underscore, but no two underscore may immediately follow + * each other or be trailing */ + bool underscore = false; + for (const char *c = name + 1; *c; c++) { + if (*c == '_') { + if (underscore) + return false; + + underscore = true; + continue; + } + + if (!strchr(ALPHANUMERICAL, *c)) + return false; + + underscore = false; + } + + if (underscore) + return false; + + return true; +} + +bool varlink_idl_symbol_name_is_valid(const char *name) { + if (isempty(name)) + return false; + + /* We might want to reference VARLINK_STRUCT_TYPE and VARLINK_ENUM_TYPE symbols where we also + * reference native types, hence make sure the native type names are refused as symbol names. */ + if (STR_IN_SET(name, "bool", "int", "float", "string", "object")) + return false; + + /* Symbols must be named with an uppercase letter as first character */ + if (!strchr(UPPERCASE_LETTERS, name[0])) + return false; + + for (const char *c = name + 1; *c; c++) + if (!strchr(ALPHANUMERICAL, *c)) + return false; + + return true; +} + +bool varlink_idl_interface_name_is_valid(const char *name) { + if (isempty(name)) + return false; + + /* Interface names must start with a letter, uppercase or lower case, but nothing else */ + if (!strchr(LETTERS, name[0])) + return false; + + /* Otherwise it may be a series of non-empty dot separated labels, which are alphanumerical and may + * contain single dashes in the middle */ + bool dot = false, dash = false; + for (const char *c = name + 1; *c; c++) { + switch (*c) { + + case '.': + if (dot || dash) + return false; + + dot = true; + break; + + case '-': + if (dot || dash) + return false; + + dash = true; + break; + + default: + if (!strchr(ALPHANUMERICAL, *c)) + return false; + + dot = dash = false; + } + } + + if (dot || dash) + return false; + + return true; +} + +static int varlink_idl_symbol_consistent(const VarlinkInterface *interface, const VarlinkSymbol *symbol, bool level); + +static int varlink_idl_field_consistent( + const VarlinkInterface *interface, + const VarlinkSymbol *symbol, + const VarlinkField *field, + bool level) { + + const char *symbol_name; + int r; + + assert(interface); + assert(symbol); + assert(field); + assert(field->name); + + symbol_name = symbol->name ?: ""; + + if (field->field_type <= 0 || field->field_type >= _VARLINK_FIELD_TYPE_MAX) + return log_full_errno(level, SYNTHETIC_ERRNO(EUCLEAN), "Field type for '%s' in symbol '%s' is not valid, refusing.", field->name, symbol_name); + + if (field->field_type == VARLINK_ENUM_VALUE) { + + if (symbol->symbol_type != VARLINK_ENUM_TYPE) + return log_full_errno(level, SYNTHETIC_ERRNO(EUCLEAN), "Enum field type for '%s' in non-enum symbol '%s', refusing.", field->name, symbol_name); + + if (field->field_flags != 0) + return log_full_errno(level, SYNTHETIC_ERRNO(EUCLEAN), "Enum field '%s' in symbol '%s' has non-zero flags set, refusing.", field->name, symbol_name); + } else { + if (symbol->symbol_type == VARLINK_ENUM_TYPE) + return log_full_errno(level, SYNTHETIC_ERRNO(EUCLEAN), "Non-enum field type for '%s' in enum symbol '%s', refusing.", field->name, symbol_name); + + if (!IN_SET(field->field_flags & ~VARLINK_NULLABLE, 0, VARLINK_ARRAY, VARLINK_MAP)) + return log_full_errno(level, SYNTHETIC_ERRNO(EUCLEAN), "Flags of field '%s' in symbol '%s' is invalid, refusing.", field->name, symbol_name); + } + + if (symbol->symbol_type != VARLINK_METHOD) { + if (field->field_direction != VARLINK_REGULAR) + return log_full_errno(level, SYNTHETIC_ERRNO(EUCLEAN), "Direction of '%s' in non-method symbol '%s' not regular, refusing.", field->name, symbol_name); + } else { + if (!IN_SET(field->field_direction, VARLINK_INPUT, VARLINK_OUTPUT)) + return log_full_errno(level, SYNTHETIC_ERRNO(EUCLEAN), "Direction of '%s' in method symbol '%s' is not input or output, refusing.", field->name, symbol_name); + } + + if (field->symbol) { + if (!IN_SET(field->field_type, VARLINK_STRUCT, VARLINK_ENUM, VARLINK_NAMED_TYPE)) + return log_full_errno(level, SYNTHETIC_ERRNO(EUCLEAN), "Target symbol for field '%s' in symbol '%s' defined for elemental field, refusing.", field->name, symbol_name); + + if (field->field_type == VARLINK_NAMED_TYPE) { + const VarlinkSymbol *found; + + if (!field->symbol->name || !field->named_type || !streq(field->symbol->name, field->named_type)) + return log_full_errno(level, SYNTHETIC_ERRNO(EUCLEAN), "Resolved symbol name and named type of field '%s' in symbol '%s' do do not match, refusing.", field->name, symbol_name); + + /* If this is a named type, then check if it's properly part of the interface */ + found = varlink_idl_find_symbol(interface, _VARLINK_SYMBOL_TYPE_INVALID, field->symbol->name); + if (!found) + return log_full_errno(level, SYNTHETIC_ERRNO(EUCLEAN), "Resolved symbol of named type of field '%s' in symbol '%s' is not part of the interface, refusing.", field->name, symbol_name); + + if (!IN_SET(found->symbol_type, VARLINK_ENUM_TYPE, VARLINK_STRUCT_TYPE)) + return log_full_errno(level, SYNTHETIC_ERRNO(EUCLEAN), "Resolved symbol of named type of field '%s' in symbol '%s' is not a type, refusing.", field->name, symbol_name); + } else { + /* If this is an anonymous type, then we recursively check if it's consistent, since + * it's not part of the interface, and hence we won't validate it from there. */ + + r = varlink_idl_symbol_consistent(interface, field->symbol, level); + if (r < 0) + return r; + } + + } else { + if (IN_SET(field->field_type, VARLINK_STRUCT, VARLINK_ENUM, VARLINK_NAMED_TYPE)) + return log_full_errno(level, SYNTHETIC_ERRNO(EUCLEAN), "No target symbol for field '%s' in symbol '%s' defined for elemental field, refusing.", field->name, symbol_name); + + if (field->named_type) + return log_full_errno(level, SYNTHETIC_ERRNO(EUCLEAN), "Unresolved symbol in field '%s' in symbol '%s', refusing.", field->name, symbol_name); + } + + if (field->named_type) { + if (field->field_type != VARLINK_NAMED_TYPE) + return log_full_errno(level, SYNTHETIC_ERRNO(EUCLEAN), "Named type set for field '%s' in symbol '%s' but not a named type field, refusing.", field->name, symbol_name); + } else { + if (field->field_type == VARLINK_NAMED_TYPE) + return log_full_errno(level, SYNTHETIC_ERRNO(EUCLEAN), "No named type set for field '%s' in symbol '%s' but field is a named type field, refusing.", field->name, symbol_name); + } + + return 0; +} + +static bool varlink_symbol_is_empty(const VarlinkSymbol *symbol) { + assert(symbol); + + return symbol->fields[0].field_type == _VARLINK_FIELD_TYPE_END_MARKER; +} + +static int varlink_idl_symbol_consistent( + const VarlinkInterface *interface, + const VarlinkSymbol *symbol, + bool level) { + + _cleanup_(set_freep) Set *input_set = NULL, *output_set = NULL; + const char *symbol_name; + int r; + + assert(interface); + assert(symbol); + + symbol_name = symbol->name ?: ""; + + if (symbol->symbol_type < 0 || symbol->symbol_type >= _VARLINK_SYMBOL_TYPE_MAX) + return log_full_errno(level, SYNTHETIC_ERRNO(EUCLEAN), "Symbol type for '%s' is not valid, refusing.", symbol_name); + + if (IN_SET(symbol->symbol_type, VARLINK_STRUCT_TYPE, VARLINK_ENUM_TYPE) && varlink_symbol_is_empty(symbol)) + return log_full_errno(level, SYNTHETIC_ERRNO(EUCLEAN), "Symbol '%s' is empty, refusing.", symbol_name); + + for (const VarlinkField *field = symbol->fields; field->field_type != _VARLINK_FIELD_TYPE_END_MARKER; field++) { + Set **name_set = field->field_direction == VARLINK_OUTPUT ? &output_set : &input_set; /* for the method case we need two separate sets, otherwise we use the same */ + + if (!varlink_idl_field_name_is_valid(field->name)) + return log_full_errno(level, SYNTHETIC_ERRNO(EUCLEAN), "Field name '%s' in symbol '%s' not valid, refusing.", field->name, symbol_name); + + if (set_contains(*name_set, field->name)) + return log_full_errno(level, SYNTHETIC_ERRNO(ENOTUNIQ), "Field '%s' defined twice in symbol '%s', refusing.", field->name, symbol_name); + + if (set_ensure_put(name_set, &string_hash_ops, field->name) < 0) + return log_oom(); + + r = varlink_idl_field_consistent(interface, symbol, field, level); + if (r < 0) + return r; + } + + return 0; +} + +int varlink_idl_consistent(const VarlinkInterface *interface, bool level) { + _cleanup_(set_freep) Set *name_set = NULL; + int r; + + assert(interface); + + if (!varlink_idl_interface_name_is_valid(interface->name)) + return log_full_errno(level, SYNTHETIC_ERRNO(EUCLEAN), "Interface name '%s' is not valid, refusing.", interface->name); + + for (const VarlinkSymbol *const *symbol = interface->symbols; *symbol; symbol++) { + + if (!varlink_idl_symbol_name_is_valid((*symbol)->name)) + return log_full_errno(level, SYNTHETIC_ERRNO(EUCLEAN), "Symbol name '%s' is not valid, refusing.", strempty((*symbol)->name)); + + if (set_contains(name_set, (*symbol)->name)) + return log_full_errno(level, SYNTHETIC_ERRNO(ENOTUNIQ), "Symbol '%s' defined twice in interface, refusing.", (*symbol)->name); + + if (set_ensure_put(&name_set, &string_hash_ops, (*symbol)->name) < 0) + return log_oom(); + + r = varlink_idl_symbol_consistent(interface, *symbol, level); + if (r < 0) + return r; + } + + return 0; +} + +static int varlink_idl_validate_symbol(const VarlinkSymbol *symbol, JsonVariant *v, VarlinkFieldDirection direction, const char **bad_field); + +static int varlink_idl_validate_field_element_type(const VarlinkField *field, JsonVariant *v) { + assert(field); + + switch (field->field_type) { + + case VARLINK_STRUCT: + case VARLINK_ENUM: + case VARLINK_NAMED_TYPE: + return varlink_idl_validate_symbol(field->symbol, v, VARLINK_REGULAR, NULL); + + case VARLINK_BOOL: + if (!json_variant_is_boolean(v)) + return log_debug_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "Field '%s' should be a bool, but it is not, refusing.", strna(field->name)); + + break; + + case VARLINK_INT: + if (!json_variant_is_integer(v) && !json_variant_is_unsigned(v)) + return log_debug_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "Field '%s' should be an int, but it is not, refusing.", strna(field->name)); + + break; + + case VARLINK_FLOAT: + if (!json_variant_is_number(v)) + return log_debug_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "Field '%s' should be a float, but it is not, refusing.", strna(field->name)); + + break; + + case VARLINK_STRING: + if (!json_variant_is_string(v)) + return log_debug_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "Field '%s' should be a string, but it is not, refusing.", strna(field->name)); + + break; + + case VARLINK_OBJECT: + if (!json_variant_is_object(v)) + return log_debug_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "Field '%s' should be an object, but it is not, refusing.", strna(field->name)); + + break; + + default: + assert_not_reached(); + } + + return 0; +} + +static int varlink_idl_validate_field(const VarlinkField *field, JsonVariant *v) { + int r; + + assert(field); + + if (!v || json_variant_is_null(v)) { + + if (!FLAGS_SET(field->field_flags, VARLINK_NULLABLE)) + return log_debug_errno(SYNTHETIC_ERRNO(ENOANO), "Mandatory field '%s' is null or missing on object, refusing.", strna(field->name)); + + } else if (FLAGS_SET(field->field_flags, VARLINK_ARRAY)) { + JsonVariant *i; + + if (!json_variant_is_array(v)) + return log_debug_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "Field '%s' should be an array, but it is not, refusing.", strna(field->name)); + + JSON_VARIANT_ARRAY_FOREACH(i, v) { + r = varlink_idl_validate_field_element_type(field, i); + if (r < 0) + return r; + } + + } else if (FLAGS_SET(field->field_flags, VARLINK_MAP)) { + _unused_ const char *k; + JsonVariant *e; + + if (!json_variant_is_object(v)) + return log_debug_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "Field '%s' should be an object, but it is not, refusing.", strna(field->name)); + + JSON_VARIANT_OBJECT_FOREACH(k, e, v) { + r = varlink_idl_validate_field_element_type(field, e); + if (r < 0) + return r; + } + } else { + + r = varlink_idl_validate_field_element_type(field, v); + if (r < 0) + return r; + } + + return 0; +} + +static int varlink_idl_validate_symbol(const VarlinkSymbol *symbol, JsonVariant *v, VarlinkFieldDirection direction, const char **bad_field) { + int r; + + assert(symbol); + + if (!v) { + if (bad_field) + *bad_field = NULL; + return log_debug_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "Null object passed, refusing."); + } + + switch (symbol->symbol_type) { + + case VARLINK_ENUM_TYPE: { + bool found = false; + const char *s; + + if (!json_variant_is_string(v)) { + if (bad_field) + *bad_field = symbol->name; + return log_debug_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "Passed non-string to enum field '%s', refusing.", strna(symbol->name)); + } + + assert_se(s = json_variant_string(v)); + + for (const VarlinkField *field = symbol->fields; field->field_type != _VARLINK_FIELD_TYPE_END_MARKER; field++) { + + assert(field->field_type == VARLINK_ENUM_VALUE); + + if (streq_ptr(field->name, s)) { + found = true; + break; + } + } + + if (!found) { + if (bad_field) + *bad_field = s; + return log_debug_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "Passed unrecognized string '%s' to enum field '%s', refusing.", s, strna(symbol->name)); + } + + break; + } + + case VARLINK_STRUCT_TYPE: + case VARLINK_METHOD: + case VARLINK_ERROR: { + if (!json_variant_is_object(v)) { + if (bad_field) + *bad_field = symbol->name; + return log_debug_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "Passed non-object to field '%s', refusing.", strna(symbol->name)); + } + + for (const VarlinkField *field = symbol->fields; field->field_type != _VARLINK_FIELD_TYPE_END_MARKER; field++) { + + if (field->field_direction != direction) + continue; + + r = varlink_idl_validate_field(field, json_variant_by_key(v, field->name)); + if (r < 0) { + if (bad_field) + *bad_field = field->name; + return r; + } + } + + _unused_ JsonVariant *e; + const char *name; + JSON_VARIANT_OBJECT_FOREACH(name, e, v) { + if (!varlink_idl_find_field(symbol, name)) { + if (bad_field) + *bad_field = name; + return log_debug_errno(SYNTHETIC_ERRNO(EBUSY), "Field '%s' not defined for object, refusing.", name); + } + } + + break; + } + + default: + assert_not_reached(); + } + + return 1; /* validated */ +} + +static int varlink_idl_validate_method(const VarlinkSymbol *method, JsonVariant *v, VarlinkFieldDirection direction, const char **bad_field) { + assert(IN_SET(direction, VARLINK_INPUT, VARLINK_OUTPUT)); + + if (!method) + return 0; /* Can't validate */ + if (method->symbol_type != VARLINK_METHOD) + return -EBADMSG; + + return varlink_idl_validate_symbol(method, v, direction, bad_field); +} + +int varlink_idl_validate_method_call(const VarlinkSymbol *method, JsonVariant *v, const char **bad_field) { + return varlink_idl_validate_method(method, v, VARLINK_INPUT, bad_field); +} + +int varlink_idl_validate_method_reply(const VarlinkSymbol *method, JsonVariant *v, const char **bad_field) { + return varlink_idl_validate_method(method, v, VARLINK_OUTPUT, bad_field); +} + +int varlink_idl_validate_error(const VarlinkSymbol *error, JsonVariant *v, const char **bad_field) { + if (!error) + return 0; /* Can't validate */ + if (error->symbol_type != VARLINK_ERROR) + return -EBADMSG; + + return varlink_idl_validate_symbol(error, v, VARLINK_REGULAR, bad_field); +} + +const VarlinkSymbol* varlink_idl_find_symbol( + const VarlinkInterface *interface, + VarlinkSymbolType type, + const char *name) { + + assert(interface); + assert(type < _VARLINK_SYMBOL_TYPE_MAX); + + if (isempty(name)) + return NULL; + + for (const VarlinkSymbol *const*symbol = interface->symbols; *symbol; symbol++) { + if (type >= 0 && (*symbol)->symbol_type != type) + continue; + + if (streq_ptr((*symbol)->name, name)) + return *symbol; + } + + return NULL; +} + +const VarlinkField* varlink_idl_find_field( + const VarlinkSymbol *symbol, + const char *name) { + + assert(symbol); + + if (isempty(name)) + return NULL; + + for (const VarlinkField *field = symbol->fields; field->field_type != _VARLINK_FIELD_TYPE_END_MARKER; field++) + if (streq_ptr(field->name, name)) + return field; + + return NULL; +} diff --git a/src/shared/varlink-idl.h b/src/shared/varlink-idl.h new file mode 100644 index 0000000000..3a7075e76f --- /dev/null +++ b/src/shared/varlink-idl.h @@ -0,0 +1,158 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include +#include +#include + +#include "json.h" +#include "macro.h" + +/* This implements the Varlink Interface Definition Language ("Varlink IDL"), + * i.e. https://varlink.org/Interface-Definition + * + * Primarily allows encoding static interface definitions in C code, that can be converted to the textual IDL + * format on-the-fly. Can also parse the textual format back to C structures. Validates the interface + * definitions for internal consistency and validates JSON objects against the interface definitions. */ + +typedef enum VarlinkSymbolType { + VARLINK_ENUM_TYPE, + VARLINK_STRUCT_TYPE, + VARLINK_METHOD, + VARLINK_ERROR, + _VARLINK_SYMBOL_TYPE_MAX, + _VARLINK_SYMBOL_TYPE_INVALID = -EINVAL, +} VarlinkSymbolType; + +typedef enum VarlinkFieldType { + _VARLINK_FIELD_TYPE_END_MARKER = 0, /* zero type means: this is the last entry in the fields[] array of VarlinkSymbol */ + VARLINK_STRUCT, + VARLINK_ENUM, + VARLINK_NAMED_TYPE, + VARLINK_BOOL, + VARLINK_INT, + VARLINK_FLOAT, + VARLINK_STRING, + VARLINK_OBJECT, + VARLINK_ENUM_VALUE, + _VARLINK_FIELD_TYPE_MAX, + _VARLINK_FIELD_TYPE_INVALID = -EINVAL, +} VarlinkFieldType; + +typedef enum VarlinkFieldDirection { + VARLINK_REGULAR, + VARLINK_INPUT, + VARLINK_OUTPUT, + _VARLINK_FIELD_DIRECTION_MAX, + _VARLINK_FIELD_DIRECTION_INVALID = -EINVAL, +} VarlinkFieldDirection; + +typedef enum VarlinkFieldFlags { + VARLINK_ARRAY = 1 << 0, + VARLINK_MAP = 1 << 1, + VARLINK_NULLABLE = 1 << 2, + _VARLINK_FIELD_FLAGS_MAX = (1 << 3) - 1, + _VARLINK_FIELD_FLAGS_INVALID = -EINVAL, +} VarlinkFieldFlags; + +typedef struct VarlinkField VarlinkField; +typedef struct VarlinkSymbol VarlinkSymbol; +typedef struct VarlinkInterface VarlinkInterface; + +/* Fields are the components making up symbols */ +struct VarlinkField { + const char *name; + VarlinkFieldType field_type; + VarlinkFieldFlags field_flags; + VarlinkFieldDirection field_direction; /* in case of method call fields: whether input or output argument */ + const VarlinkSymbol *symbol; /* VARLINK_STRUCT, VARLINK_ENUM: anonymous symbol that carries the definitions, VARLINK_NAMED_TYPE: resolved symbol */ + const char *named_type; /* VARLINK_NAMED_TYPE */ +}; + +/* Symbols are primary named concepts in an interface, and are methods, errors or named types (either enum or struct). */ +struct VarlinkSymbol { + const char *name; /* most symbols have a name, but sometimes they are created on-the-fly for fields, in which case they are anonymous */ + VarlinkSymbolType symbol_type; + VarlinkField fields[]; +}; + +/* An interface definition has a name and consist of symbols */ +struct VarlinkInterface { + const char *name; + const VarlinkSymbol *symbols[]; +}; + +#define VARLINK_DEFINE_FIELD(_name, _field_type, _field_flags) \ + { .name = #_name, .field_type = (_field_type), .field_flags = (_field_flags) } + +#define VARLINK_DEFINE_FIELD_BY_TYPE(_name, _named_type, _field_flags) \ + { .name = #_name, .field_type = VARLINK_NAMED_TYPE, .named_type = #_named_type, .symbol = &vl_type_ ## _named_type, .field_flags = (_field_flags) } + +#define VARLINK_DEFINE_INPUT(_name, _field_type, _field_flags) \ + { .name = #_name, .field_type = (_field_type), .field_flags = (_field_flags), .field_direction = VARLINK_INPUT } + +#define VARLINK_DEFINE_INPUT_BY_TYPE(_name, _named_type, _field_flags) \ + { .name = #_name, .field_type = VARLINK_NAMED_TYPE, .named_type = #_named_type, .symbol = &vl_type_ ## _named_type, .field_flags = (_field_flags), .field_direction = VARLINK_INPUT } + +#define VARLINK_DEFINE_OUTPUT(_name, _field_type, _field_flags) \ + { .name = #_name, .field_type = (_field_type), .field_flags = (_field_flags), .field_direction = VARLINK_OUTPUT } + +#define VARLINK_DEFINE_OUTPUT_BY_TYPE(_name, _named_type, _field_flags) \ + { .name = #_name, .field_type = VARLINK_NAMED_TYPE, .named_type = #_named_type, .symbol = &vl_type_ ## _named_type, .field_flags = (_field_flags), .field_direction = VARLINK_OUTPUT } + +#define VARLINK_DEFINE_ENUM_VALUE(_name) \ + { .name = #_name, .field_type = VARLINK_ENUM_VALUE } + +#define VARLINK_DEFINE_METHOD(_name, ...) \ + const VarlinkSymbol vl_method_ ## _name = { \ + .name = #_name, \ + .symbol_type = VARLINK_METHOD, \ + .fields = { __VA_ARGS__ __VA_OPT__(,) {}}, \ + } + +#define VARLINK_DEFINE_ERROR(_name, ...) \ + const VarlinkSymbol vl_error_ ## _name = { \ + .name = #_name, \ + .symbol_type = VARLINK_ERROR, \ + .fields = { __VA_ARGS__ __VA_OPT__(,) {}}, \ + } + +#define VARLINK_DEFINE_STRUCT_TYPE(_name, ...) \ + const VarlinkSymbol vl_type_ ## _name = { \ + .name = #_name, \ + .symbol_type = VARLINK_STRUCT_TYPE, \ + .fields = { __VA_ARGS__ __VA_OPT__(,) {}}, \ + } + +#define VARLINK_DEFINE_ENUM_TYPE(_name, ...) \ + const VarlinkSymbol vl_type_ ## _name = { \ + .name = #_name, \ + .symbol_type = VARLINK_ENUM_TYPE, \ + .fields = { __VA_ARGS__ __VA_OPT__(,) {}}, \ + } + +#define VARLINK_DEFINE_INTERFACE(_name, _full_name, ...) \ + const VarlinkInterface vl_interface_ ## _name = { \ + .name = (_full_name), \ + .symbols = { __VA_ARGS__ __VA_OPT__(,) NULL}, \ + } + +int varlink_idl_dump(FILE *f, int use_colors, const VarlinkInterface *interface); +int varlink_idl_format(const VarlinkInterface *interface, char **ret); + +int varlink_idl_parse(const char *text, unsigned *ret_line, unsigned *ret_column, VarlinkInterface **ret); +VarlinkInterface* varlink_interface_free(VarlinkInterface *interface); +DEFINE_TRIVIAL_CLEANUP_FUNC(VarlinkInterface*, varlink_interface_free); + +bool varlink_idl_field_name_is_valid(const char *name); +bool varlink_idl_symbol_name_is_valid(const char *name); +bool varlink_idl_interface_name_is_valid(const char *name); + +int varlink_idl_consistent(const VarlinkInterface *interface, bool level); + +const VarlinkSymbol* varlink_idl_find_symbol(const VarlinkInterface *interface, VarlinkSymbolType type, const char *name); +const VarlinkField* varlink_idl_find_field(const VarlinkSymbol *symbol, const char *name); + +int varlink_idl_validate_method_call(const VarlinkSymbol *method, JsonVariant *v, const char **bad_field); +int varlink_idl_validate_method_reply(const VarlinkSymbol *method, JsonVariant *v, const char **bad_field); +int varlink_idl_validate_error(const VarlinkSymbol *error, JsonVariant *v, const char **bad_field); diff --git a/src/shared/varlink-io.systemd.Journal.c b/src/shared/varlink-io.systemd.Journal.c new file mode 100644 index 0000000000..b93fb72aca --- /dev/null +++ b/src/shared/varlink-io.systemd.Journal.c @@ -0,0 +1,19 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "varlink-io.systemd.Journal.h" + +static VARLINK_DEFINE_METHOD(Synchronize); +static VARLINK_DEFINE_METHOD(Rotate); +static VARLINK_DEFINE_METHOD(FlushToVar); +static VARLINK_DEFINE_METHOD(RelinquishVar); + +static VARLINK_DEFINE_ERROR(NotSupportedByNamespaces); + +VARLINK_DEFINE_INTERFACE( + io_systemd_Journal, + "io.systemd.Journal", + &vl_method_Synchronize, + &vl_method_Rotate, + &vl_method_FlushToVar, + &vl_method_RelinquishVar, + &vl_error_NotSupportedByNamespaces); diff --git a/src/shared/varlink-io.systemd.Journal.h b/src/shared/varlink-io.systemd.Journal.h new file mode 100644 index 0000000000..0bc94a775c --- /dev/null +++ b/src/shared/varlink-io.systemd.Journal.h @@ -0,0 +1,6 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "varlink-idl.h" + +extern const VarlinkInterface vl_interface_io_systemd_Journal; diff --git a/src/shared/varlink-io.systemd.ManagedOOM.c b/src/shared/varlink-io.systemd.ManagedOOM.c new file mode 100644 index 0000000000..d6414b3718 --- /dev/null +++ b/src/shared/varlink-io.systemd.ManagedOOM.c @@ -0,0 +1,23 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "varlink-io.systemd.ManagedOOM.h" + +/* Pull in vl_type_ControlGroup, since both interfaces need it */ +#include "varlink-io.systemd.oom.h" + +/* This is PID1's Varlink service, where PID 1 is the server and oomd is the client. + * + * Compare with io.systemd.oom where the client/server roles of oomd and the service manager are swapped! */ + +static VARLINK_DEFINE_METHOD( + SubscribeManagedOOMCGroups, + VARLINK_DEFINE_OUTPUT_BY_TYPE(cgroups, ControlGroup, VARLINK_ARRAY)); + +static VARLINK_DEFINE_ERROR(SubscriptionTaken); + +VARLINK_DEFINE_INTERFACE( + io_systemd_ManagedOOM, + "io.systemd.ManagedOOM", + &vl_method_SubscribeManagedOOMCGroups, + &vl_type_ControlGroup, + &vl_error_SubscriptionTaken); diff --git a/src/shared/varlink-io.systemd.ManagedOOM.h b/src/shared/varlink-io.systemd.ManagedOOM.h new file mode 100644 index 0000000000..2c8bf54c4e --- /dev/null +++ b/src/shared/varlink-io.systemd.ManagedOOM.h @@ -0,0 +1,6 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "varlink-idl.h" + +extern const VarlinkInterface vl_interface_io_systemd_ManagedOOM; diff --git a/src/shared/varlink-io.systemd.PCRExtend.c b/src/shared/varlink-io.systemd.PCRExtend.c new file mode 100644 index 0000000000..37d403f0ee --- /dev/null +++ b/src/shared/varlink-io.systemd.PCRExtend.c @@ -0,0 +1,14 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "varlink-io.systemd.PCRExtend.h" + +static VARLINK_DEFINE_METHOD( + Extend, + VARLINK_DEFINE_INPUT(pcr, VARLINK_INT, 0), + VARLINK_DEFINE_INPUT(text, VARLINK_STRING, VARLINK_NULLABLE), + VARLINK_DEFINE_INPUT(data, VARLINK_STRING, VARLINK_NULLABLE)); + +VARLINK_DEFINE_INTERFACE( + io_systemd_PCRExtend, + "io.systemd.PCRExtend", + &vl_method_Extend); diff --git a/src/shared/varlink-io.systemd.PCRExtend.h b/src/shared/varlink-io.systemd.PCRExtend.h new file mode 100644 index 0000000000..ffc075af2c --- /dev/null +++ b/src/shared/varlink-io.systemd.PCRExtend.h @@ -0,0 +1,6 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "varlink-idl.h" + +extern const VarlinkInterface vl_interface_io_systemd_PCRExtend; diff --git a/src/shared/varlink-io.systemd.Resolve.Monitor.c b/src/shared/varlink-io.systemd.Resolve.Monitor.c new file mode 100644 index 0000000000..179b9bcf1e --- /dev/null +++ b/src/shared/varlink-io.systemd.Resolve.Monitor.c @@ -0,0 +1,136 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "varlink-io.systemd.Resolve.Monitor.h" + +VARLINK_DEFINE_STRUCT_TYPE( + ResourceKey, + VARLINK_DEFINE_FIELD(class, VARLINK_INT, 0), + VARLINK_DEFINE_FIELD(type, VARLINK_INT, 0), + VARLINK_DEFINE_FIELD(name, VARLINK_STRING, 0)); + +VARLINK_DEFINE_STRUCT_TYPE( + ResourceRecord, + VARLINK_DEFINE_FIELD_BY_TYPE(key, ResourceKey, 0), + VARLINK_DEFINE_FIELD(priority, VARLINK_INT, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(weight, VARLINK_INT, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(port, VARLINK_INT, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(name, VARLINK_STRING, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(cpu, VARLINK_STRING, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(os, VARLINK_STRING, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(items, VARLINK_STRING, VARLINK_NULLABLE|VARLINK_ARRAY), + VARLINK_DEFINE_FIELD(address, VARLINK_INT, VARLINK_NULLABLE|VARLINK_ARRAY), + VARLINK_DEFINE_FIELD(mname, VARLINK_STRING, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(rname, VARLINK_STRING, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(serial, VARLINK_INT, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(refresh, VARLINK_INT, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(expire, VARLINK_INT, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(minimum, VARLINK_INT, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(exchange, VARLINK_STRING, VARLINK_NULLABLE)); + +VARLINK_DEFINE_STRUCT_TYPE( + Answer, + VARLINK_DEFINE_FIELD_BY_TYPE(rr, ResourceRecord, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(raw, VARLINK_STRING, 0), + VARLINK_DEFINE_FIELD(ifindex, VARLINK_INT, VARLINK_NULLABLE)); + +VARLINK_DEFINE_METHOD( + SubscribeQueryResults, + /* First reply */ + VARLINK_DEFINE_OUTPUT(ready, VARLINK_BOOL, VARLINK_NULLABLE), + /* Subsequent replies */ + VARLINK_DEFINE_OUTPUT(state, VARLINK_STRING, VARLINK_NULLABLE), + VARLINK_DEFINE_OUTPUT(rcode, VARLINK_INT, VARLINK_NULLABLE), + VARLINK_DEFINE_OUTPUT(errno, VARLINK_INT, VARLINK_NULLABLE), + VARLINK_DEFINE_OUTPUT_BY_TYPE(question, ResourceKey, VARLINK_NULLABLE|VARLINK_ARRAY), + VARLINK_DEFINE_OUTPUT_BY_TYPE(collectedQuestions, ResourceKey, VARLINK_NULLABLE|VARLINK_ARRAY), + VARLINK_DEFINE_OUTPUT_BY_TYPE(answer, Answer, VARLINK_NULLABLE|VARLINK_ARRAY)); + +VARLINK_DEFINE_STRUCT_TYPE( + CacheEntry, + VARLINK_DEFINE_FIELD_BY_TYPE(key, ResourceKey, 0), + VARLINK_DEFINE_FIELD_BY_TYPE(rrs, ResourceRecord, VARLINK_NULLABLE|VARLINK_ARRAY), + VARLINK_DEFINE_FIELD(type, VARLINK_STRING, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(until, VARLINK_INT, 0)); + +VARLINK_DEFINE_STRUCT_TYPE( + ScopeCache, + VARLINK_DEFINE_FIELD(protocol, VARLINK_STRING, 0), + VARLINK_DEFINE_FIELD(family, VARLINK_INT, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(ifindex, VARLINK_INT, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(ifname, VARLINK_STRING, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD_BY_TYPE(cache, CacheEntry, VARLINK_ARRAY)); + +VARLINK_DEFINE_METHOD( + DumpCache, + VARLINK_DEFINE_OUTPUT_BY_TYPE(dump, ScopeCache, VARLINK_ARRAY)); + +VARLINK_DEFINE_STRUCT_TYPE( + ServerState, + VARLINK_DEFINE_FIELD(Server, VARLINK_STRING, 0), + VARLINK_DEFINE_FIELD(Type, VARLINK_STRING, 0), + VARLINK_DEFINE_FIELD(Interface, VARLINK_STRING, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(InterfaceIndex, VARLINK_INT, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(VerifiedFeatureLevel, VARLINK_STRING, 0), + VARLINK_DEFINE_FIELD(PossibleFeatureLevel, VARLINK_STRING, 0), + VARLINK_DEFINE_FIELD(DNSSECMode, VARLINK_STRING, 0), + VARLINK_DEFINE_FIELD(DNSSECSupported, VARLINK_BOOL, 0), + VARLINK_DEFINE_FIELD(ReceivedUDPFragmentMax, VARLINK_INT, 0), + VARLINK_DEFINE_FIELD(FailedUDPAttempts, VARLINK_INT, 0), + VARLINK_DEFINE_FIELD(FailedTCPAttempts, VARLINK_INT, 0), + VARLINK_DEFINE_FIELD(PacketTruncated, VARLINK_BOOL, 0), + VARLINK_DEFINE_FIELD(PacketBadOpt, VARLINK_BOOL, 0), + VARLINK_DEFINE_FIELD(PacketRRSIGMissing, VARLINK_BOOL, 0), + VARLINK_DEFINE_FIELD(PacketInvalid, VARLINK_BOOL, 0), + VARLINK_DEFINE_FIELD(PacketDoOff, VARLINK_BOOL, 0)); + +VARLINK_DEFINE_METHOD( + DumpServerState, + VARLINK_DEFINE_OUTPUT_BY_TYPE(dump, ServerState, VARLINK_ARRAY)); + +VARLINK_DEFINE_STRUCT_TYPE( + TransactionStatistics, + VARLINK_DEFINE_FIELD(currentTransactions, VARLINK_INT, 0), + VARLINK_DEFINE_FIELD(totalTransactions, VARLINK_INT, 0), + VARLINK_DEFINE_FIELD(totalTimeouts, VARLINK_INT, 0), + VARLINK_DEFINE_FIELD(totalTimeoutsServedStale, VARLINK_INT, 0), + VARLINK_DEFINE_FIELD(totalFailedResponses, VARLINK_INT, 0), + VARLINK_DEFINE_FIELD(totalFailedResponsesServedStale, VARLINK_INT, 0)); + +VARLINK_DEFINE_STRUCT_TYPE( + CacheStatistics, + VARLINK_DEFINE_FIELD(size, VARLINK_INT, 0), + VARLINK_DEFINE_FIELD(hits, VARLINK_INT, 0), + VARLINK_DEFINE_FIELD(misses, VARLINK_INT, 0)); + +VARLINK_DEFINE_STRUCT_TYPE( + DnssecStatistics, + VARLINK_DEFINE_FIELD(secure, VARLINK_INT, 0), + VARLINK_DEFINE_FIELD(insecure, VARLINK_INT, 0), + VARLINK_DEFINE_FIELD(bogus, VARLINK_INT, 0), + VARLINK_DEFINE_FIELD(indeterminate, VARLINK_INT, 0)); + +VARLINK_DEFINE_METHOD( + DumpStatistics, + VARLINK_DEFINE_OUTPUT_BY_TYPE(transactions, TransactionStatistics, 0), + VARLINK_DEFINE_OUTPUT_BY_TYPE(cache, CacheStatistics, 0), + VARLINK_DEFINE_OUTPUT_BY_TYPE(dnssec, DnssecStatistics, 0)); + +VARLINK_DEFINE_METHOD(ResetStatistics); + +VARLINK_DEFINE_INTERFACE( + io_systemd_Resolve_Monitor, + "io.systemd.Resolve.Monitor", + &vl_method_SubscribeQueryResults, + &vl_method_DumpCache, + &vl_method_DumpServerState, + &vl_method_DumpStatistics, + &vl_method_ResetStatistics, + &vl_type_ResourceKey, + &vl_type_ResourceRecord, + &vl_type_Answer, + &vl_type_CacheEntry, + &vl_type_ScopeCache, + &vl_type_TransactionStatistics, + &vl_type_CacheStatistics, + &vl_type_DnssecStatistics, + &vl_type_ServerState); diff --git a/src/shared/varlink-io.systemd.Resolve.Monitor.h b/src/shared/varlink-io.systemd.Resolve.Monitor.h new file mode 100644 index 0000000000..a133ec30a5 --- /dev/null +++ b/src/shared/varlink-io.systemd.Resolve.Monitor.h @@ -0,0 +1,6 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "varlink-idl.h" + +extern const VarlinkInterface vl_interface_io_systemd_Resolve_Monitor; diff --git a/src/shared/varlink-io.systemd.Resolve.c b/src/shared/varlink-io.systemd.Resolve.c new file mode 100644 index 0000000000..0d8ad281fa --- /dev/null +++ b/src/shared/varlink-io.systemd.Resolve.c @@ -0,0 +1,76 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "varlink-io.systemd.Resolve.h" + +static VARLINK_DEFINE_STRUCT_TYPE( + ResolvedAddress, + VARLINK_DEFINE_FIELD(ifindex, VARLINK_INT, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(family, VARLINK_INT, 0), + VARLINK_DEFINE_FIELD(address, VARLINK_INT, VARLINK_ARRAY)); + +static VARLINK_DEFINE_METHOD( + ResolveHostname, + VARLINK_DEFINE_INPUT(ifindex, VARLINK_INT, VARLINK_NULLABLE), + VARLINK_DEFINE_INPUT(name, VARLINK_STRING, 0), + VARLINK_DEFINE_INPUT(family, VARLINK_INT, VARLINK_NULLABLE), + VARLINK_DEFINE_INPUT(flags, VARLINK_INT, VARLINK_NULLABLE), + VARLINK_DEFINE_OUTPUT_BY_TYPE(addresses, ResolvedAddress, VARLINK_ARRAY), + VARLINK_DEFINE_OUTPUT(name, VARLINK_STRING, 0), + VARLINK_DEFINE_OUTPUT(flags, VARLINK_INT, 0)); + +static VARLINK_DEFINE_STRUCT_TYPE( + ResolvedName, + VARLINK_DEFINE_FIELD(ifindex, VARLINK_INT, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(name, VARLINK_STRING, 0)); + +static VARLINK_DEFINE_METHOD( + ResolveAddress, + VARLINK_DEFINE_INPUT(ifindex, VARLINK_INT, VARLINK_NULLABLE), + VARLINK_DEFINE_INPUT(family, VARLINK_INT, 0), + VARLINK_DEFINE_INPUT(address, VARLINK_INT, VARLINK_ARRAY), + VARLINK_DEFINE_INPUT(flags, VARLINK_INT, VARLINK_NULLABLE), + VARLINK_DEFINE_OUTPUT_BY_TYPE(names, ResolvedName, VARLINK_ARRAY), + VARLINK_DEFINE_OUTPUT(flags, VARLINK_INT, 0)); + +static VARLINK_DEFINE_ERROR(NoNameServers); +static VARLINK_DEFINE_ERROR(NoSuchResourceRecord); +static VARLINK_DEFINE_ERROR(QueryTimedOut); +static VARLINK_DEFINE_ERROR(MaxAttemptsReached); +static VARLINK_DEFINE_ERROR(InvalidReply); +static VARLINK_DEFINE_ERROR(QueryAborted); +static VARLINK_DEFINE_ERROR( + DNSSECValidationFailed, + VARLINK_DEFINE_FIELD(result, VARLINK_STRING, 0)); +static VARLINK_DEFINE_ERROR(NoTrustAnchor); +static VARLINK_DEFINE_ERROR(ResourceRecordTypeUnsupported); +static VARLINK_DEFINE_ERROR(NetworkDown); +static VARLINK_DEFINE_ERROR(NoSource); +static VARLINK_DEFINE_ERROR(StubLoop); +static VARLINK_DEFINE_ERROR( + DNSError, + VARLINK_DEFINE_FIELD(rcode, VARLINK_INT, 0)); +static VARLINK_DEFINE_ERROR(CNAMELoop); +static VARLINK_DEFINE_ERROR(BadAddressSize); + +VARLINK_DEFINE_INTERFACE( + io_systemd_Resolve, + "io.systemd.Resolve", + &vl_method_ResolveHostname, + &vl_method_ResolveAddress, + &vl_type_ResolvedAddress, + &vl_type_ResolvedName, + &vl_error_NoNameServers, + &vl_error_NoSuchResourceRecord, + &vl_error_QueryTimedOut, + &vl_error_MaxAttemptsReached, + &vl_error_InvalidReply, + &vl_error_QueryAborted, + &vl_error_DNSSECValidationFailed, + &vl_error_NoTrustAnchor, + &vl_error_ResourceRecordTypeUnsupported, + &vl_error_NetworkDown, + &vl_error_NoSource, + &vl_error_StubLoop, + &vl_error_DNSError, + &vl_error_CNAMELoop, + &vl_error_BadAddressSize); diff --git a/src/shared/varlink-io.systemd.Resolve.h b/src/shared/varlink-io.systemd.Resolve.h new file mode 100644 index 0000000000..5c7ed39d0d --- /dev/null +++ b/src/shared/varlink-io.systemd.Resolve.h @@ -0,0 +1,6 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "varlink-idl.h" + +extern const VarlinkInterface vl_interface_io_systemd_Resolve; diff --git a/src/shared/varlink-io.systemd.UserDatabase.c b/src/shared/varlink-io.systemd.UserDatabase.c new file mode 100644 index 0000000000..a640d2e29a --- /dev/null +++ b/src/shared/varlink-io.systemd.UserDatabase.c @@ -0,0 +1,46 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "varlink-io.systemd.UserDatabase.h" + +static VARLINK_DEFINE_METHOD( + GetUserRecord, + VARLINK_DEFINE_INPUT(uid, VARLINK_INT, VARLINK_NULLABLE), + VARLINK_DEFINE_INPUT(userName, VARLINK_STRING, VARLINK_NULLABLE), + VARLINK_DEFINE_INPUT(service, VARLINK_STRING, 0), + VARLINK_DEFINE_OUTPUT(record, VARLINK_OBJECT, 0), + VARLINK_DEFINE_OUTPUT(incomplete, VARLINK_BOOL, 0)); + +static VARLINK_DEFINE_METHOD( + GetGroupRecord, + VARLINK_DEFINE_INPUT(gid, VARLINK_INT, VARLINK_NULLABLE), + VARLINK_DEFINE_INPUT(groupName, VARLINK_STRING, VARLINK_NULLABLE), + VARLINK_DEFINE_INPUT(service, VARLINK_STRING, 0), + VARLINK_DEFINE_OUTPUT(record, VARLINK_OBJECT, 0), + VARLINK_DEFINE_OUTPUT(incomplete, VARLINK_BOOL, 0)); + +static VARLINK_DEFINE_METHOD( + GetMemberships, + VARLINK_DEFINE_INPUT(userName, VARLINK_STRING, VARLINK_NULLABLE), + VARLINK_DEFINE_INPUT(groupName, VARLINK_STRING, VARLINK_NULLABLE), + VARLINK_DEFINE_INPUT(service, VARLINK_STRING, 0), + VARLINK_DEFINE_OUTPUT(userName, VARLINK_STRING, 0), + VARLINK_DEFINE_OUTPUT(groupName, VARLINK_STRING, 0)); + +static VARLINK_DEFINE_ERROR(NoRecordFound); +static VARLINK_DEFINE_ERROR(BadService); +static VARLINK_DEFINE_ERROR(ServiceNotAvailable); +static VARLINK_DEFINE_ERROR(ConflictingRecordNotFound); +static VARLINK_DEFINE_ERROR(EnumerationNotSupported); + +/* As per https://systemd.io/USER_GROUP_API/ */ +VARLINK_DEFINE_INTERFACE( + io_systemd_UserDatabase, + "io.systemd.UserDatabase", + &vl_method_GetUserRecord, + &vl_method_GetGroupRecord, + &vl_method_GetMemberships, + &vl_error_NoRecordFound, + &vl_error_BadService, + &vl_error_ServiceNotAvailable, + &vl_error_ConflictingRecordNotFound, + &vl_error_EnumerationNotSupported); diff --git a/src/shared/varlink-io.systemd.UserDatabase.h b/src/shared/varlink-io.systemd.UserDatabase.h new file mode 100644 index 0000000000..346ca848ac --- /dev/null +++ b/src/shared/varlink-io.systemd.UserDatabase.h @@ -0,0 +1,6 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "varlink-idl.h" + +extern const VarlinkInterface vl_interface_io_systemd_UserDatabase; diff --git a/src/shared/varlink-io.systemd.c b/src/shared/varlink-io.systemd.c new file mode 100644 index 0000000000..cdfe9ac263 --- /dev/null +++ b/src/shared/varlink-io.systemd.c @@ -0,0 +1,21 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "varlink-io.systemd.h" + +/* These are local errors that never cross the wire, and are our own invention */ +static VARLINK_DEFINE_ERROR(Disconnected); +static VARLINK_DEFINE_ERROR(TimedOut); +static VARLINK_DEFINE_ERROR(Protocol); + +/* This one we invented, and use for generically propagating system errors (errno) to clients */ +static VARLINK_DEFINE_ERROR( + System, + VARLINK_DEFINE_FIELD(errno, VARLINK_INT, 0)); + +VARLINK_DEFINE_INTERFACE( + io_systemd, + "io.systemd", + &vl_error_Disconnected, + &vl_error_TimedOut, + &vl_error_Protocol, + &vl_error_System); diff --git a/src/shared/varlink-io.systemd.h b/src/shared/varlink-io.systemd.h new file mode 100644 index 0000000000..6c17c6c4c7 --- /dev/null +++ b/src/shared/varlink-io.systemd.h @@ -0,0 +1,6 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "varlink-idl.h" + +extern const VarlinkInterface vl_interface_io_systemd; diff --git a/src/shared/varlink-io.systemd.oom.c b/src/shared/varlink-io.systemd.oom.c new file mode 100644 index 0000000000..e1da3fa527 --- /dev/null +++ b/src/shared/varlink-io.systemd.oom.c @@ -0,0 +1,25 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "varlink-io.systemd.oom.h" + +/* This is oomd's Varlink service, where oomd is server and systemd --user is the client. + * + * Compare with io.systemd.ManagedOOM where the client/server roles of the service manager and oomd are + * swapped! */ + +VARLINK_DEFINE_STRUCT_TYPE( + ControlGroup, + VARLINK_DEFINE_FIELD(mode, VARLINK_STRING, 0), + VARLINK_DEFINE_FIELD(path, VARLINK_STRING, 0), + VARLINK_DEFINE_FIELD(property, VARLINK_STRING, 0), + VARLINK_DEFINE_FIELD(limit, VARLINK_INT, VARLINK_NULLABLE)); + +static VARLINK_DEFINE_METHOD( + ReportManagedOOMCGroups, + VARLINK_DEFINE_INPUT_BY_TYPE(cgroups, ControlGroup, VARLINK_ARRAY)); + +VARLINK_DEFINE_INTERFACE( + io_systemd_oom, + "io.systemd.oom", + &vl_method_ReportManagedOOMCGroups, + &vl_type_ControlGroup); diff --git a/src/shared/varlink-io.systemd.oom.h b/src/shared/varlink-io.systemd.oom.h new file mode 100644 index 0000000000..911dbc29e6 --- /dev/null +++ b/src/shared/varlink-io.systemd.oom.h @@ -0,0 +1,7 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "varlink-idl.h" + +extern const VarlinkSymbol vl_type_ControlGroup; +extern const VarlinkInterface vl_interface_io_systemd_oom; diff --git a/src/shared/varlink-org.varlink.service.c b/src/shared/varlink-org.varlink.service.c new file mode 100644 index 0000000000..52ba99dd2b --- /dev/null +++ b/src/shared/varlink-org.varlink.service.c @@ -0,0 +1,45 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "varlink-org.varlink.service.h" + +static VARLINK_DEFINE_METHOD( + GetInfo, + VARLINK_DEFINE_OUTPUT(vendor, VARLINK_STRING, 0), + VARLINK_DEFINE_OUTPUT(product, VARLINK_STRING, 0), + VARLINK_DEFINE_OUTPUT(version, VARLINK_STRING, 0), + VARLINK_DEFINE_OUTPUT(url, VARLINK_STRING, 0), + VARLINK_DEFINE_OUTPUT(interfaces, VARLINK_STRING, VARLINK_ARRAY)); + +static VARLINK_DEFINE_METHOD( + GetInterfaceDescription, + VARLINK_DEFINE_OUTPUT(interface, VARLINK_STRING, 0)); + +static VARLINK_DEFINE_ERROR( + InterfaceNotFound, + VARLINK_DEFINE_FIELD(interface, VARLINK_STRING, 0)); + +static VARLINK_DEFINE_ERROR( + MethodNotFound, + VARLINK_DEFINE_FIELD(method, VARLINK_STRING, 0)); + +static VARLINK_DEFINE_ERROR( + MethodNotImplemented, + VARLINK_DEFINE_FIELD(method, VARLINK_STRING, 0)); + +static VARLINK_DEFINE_ERROR( + InvalidParameter, + VARLINK_DEFINE_FIELD(parameter, VARLINK_STRING, 0)); + +static VARLINK_DEFINE_ERROR(PermissionDenied); + +/* As per https://varlink.org/Service */ +VARLINK_DEFINE_INTERFACE( + org_varlink_service, + "org.varlink.service", + &vl_method_GetInfo, + &vl_method_GetInterfaceDescription, + &vl_error_InterfaceNotFound, + &vl_error_MethodNotFound, + &vl_error_MethodNotImplemented, + &vl_error_InvalidParameter, + &vl_error_PermissionDenied); diff --git a/src/shared/varlink-org.varlink.service.h b/src/shared/varlink-org.varlink.service.h new file mode 100644 index 0000000000..75c55e6e82 --- /dev/null +++ b/src/shared/varlink-org.varlink.service.h @@ -0,0 +1,6 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "varlink-idl.h" + +extern const VarlinkInterface vl_interface_org_varlink_service; diff --git a/src/shared/varlink.c b/src/shared/varlink.c index d2f649485b..f061024785 100644 --- a/src/shared/varlink.c +++ b/src/shared/varlink.c @@ -3,6 +3,8 @@ #include #include +#include + #include "alloc-util.h" #include "errno-util.h" #include "fd-util.h" @@ -10,6 +12,7 @@ #include "hashmap.h" #include "io-util.h" #include "list.h" +#include "path-util.h" #include "process-util.h" #include "selinux-util.h" #include "serialize.h" @@ -23,6 +26,9 @@ #include "user-util.h" #include "varlink.h" #include "varlink-internal.h" +#include "varlink-org.varlink.service.h" +#include "varlink-io.systemd.h" +#include "version.h" #define VARLINK_DEFAULT_CONNECTIONS_MAX 4096U #define VARLINK_DEFAULT_CONNECTIONS_PER_UID_MAX 1024U @@ -162,6 +168,7 @@ struct Varlink { VarlinkReply reply_callback; JsonVariant *current; + VarlinkSymbol *current_method; struct ucred ucred; bool ucred_acquired:1; @@ -189,6 +196,8 @@ struct Varlink { sd_event_source *time_event_source; sd_event_source *quit_event_source; sd_event_source *defer_event_source; + + pid_t exec_pid; }; typedef struct VarlinkServerSocket VarlinkServerSocket; @@ -210,7 +219,9 @@ struct VarlinkServer { LIST_HEAD(VarlinkServerSocket, sockets); - Hashmap *methods; + Hashmap *methods; /* Fully qualified symbol name of a method → VarlinkMethod */ + Hashmap *interfaces; /* Fully qualified interface name → VarlinkInterface* */ + Hashmap *symbols; /* Fully qualified symbol name of methord/error → VarlinkSymbol* */ VarlinkConnect connect_callback; VarlinkDisconnect disconnect_callback; @@ -218,13 +229,15 @@ struct VarlinkServer { int64_t event_priority; unsigned n_connections; - Hashmap *by_uid; + Hashmap *by_uid; /* UID_TO_PTR(uid) → UINT_TO_PTR(n_connections) */ void *userdata; char *description; unsigned connections_max; unsigned connections_per_uid_max; + + bool exit_on_idle; }; static const char* const varlink_state_table[_VARLINK_STATE_MAX] = { @@ -264,6 +277,7 @@ DEFINE_PRIVATE_STRING_TABLE_LOOKUP_TO_STRING(varlink_state, VarlinkState); log_debug("%s: " fmt, varlink_server_description(s), ##__VA_ARGS__) static int varlink_format_queue(Varlink *v); +static void varlink_server_test_exit_on_idle(VarlinkServer *s); static const char *varlink_description(Varlink *v) { return (v ? v->description : NULL) ?: "varlink"; @@ -398,6 +412,143 @@ int varlink_connect_address(Varlink **ret, const char *address) { return 0; } +int varlink_connect_exec(Varlink **ret, const char *_command, char **_argv) { + _cleanup_close_pair_ int pair[2] = PIPE_EBADF; + _cleanup_(sigkill_waitp) pid_t pid = 0; + _cleanup_free_ char *command = NULL; + _cleanup_strv_free_ char **argv = NULL; + int r; + + assert_return(ret, -EINVAL); + assert_return(_command, -EINVAL); + + /* Copy the strings, in case they point into our own argv[], which we'll invalidate shortly because + * we rename the child process */ + command = strdup(_command); + if (!command) + return -ENOMEM; + + if (strv_isempty(_argv)) + argv = strv_new(command); + else + argv = strv_copy(_argv); + if (!argv) + return -ENOMEM; + + log_debug("Forking off Varlink child process '%s'.", command); + + if (socketpair(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0, pair) < 0) + return log_debug_errno(errno, "Failed to allocate AF_UNIX socket pair: %m"); + + r = safe_fork_full( + "(sd-vlexec)", + /* stdio_fds= */ NULL, + /* except_fds= */ (int[]) { pair[1] }, + /* n_except_fds= */ 1, + FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG|FORK_REOPEN_LOG|FORK_LOG|FORK_RLIMIT_NOFILE_SAFE, + &pid); + if (r < 0) + return log_debug_errno(r, "Failed to spawn process: %m"); + if (r == 0) { + char spid[DECIMAL_STR_MAX(pid_t)+1]; + const char *setenv_list[] = { + "LISTEN_FDS", "1", + "LISTEN_PID", spid, + "LISTEN_FDNAMES", "varlink", + NULL, NULL, + }; + /* Child */ + + pair[0] = -EBADF; + + r = move_fd(pair[1], 3, /* cloexec= */ false); + if (r < 0) { + log_debug_errno(r, "Failed to move file descriptor to 3: %m"); + _exit(EXIT_FAILURE); + } + + xsprintf(spid, PID_FMT, pid); + + STRV_FOREACH_PAIR(a, b, setenv_list) { + if (setenv(*a, *b, /* override= */ true) < 0) { + log_debug_errno(errno, "Failed to set environment variable '%s': %m", *a); + _exit(EXIT_FAILURE); + } + } + + execvp(command, argv); + log_debug_errno(r, "Failed to invoke process '%s': %m", command); + _exit(EXIT_FAILURE); + } + + pair[1] = safe_close(pair[1]); + + Varlink *v; + r = varlink_new(&v); + if (r < 0) + return log_debug_errno(r, "Failed to create varlink object: %m"); + + v->fd = TAKE_FD(pair[0]); + v->af = AF_UNIX; + v->exec_pid = TAKE_PID(pid); + varlink_set_state(v, VARLINK_IDLE_CLIENT); + + *ret = v; + return 0; +} + +int varlink_connect_url(Varlink **ret, const char *url) { + _cleanup_free_ char *c = NULL; + const char *p; + bool exec; + + assert_return(ret, -EINVAL); + assert_return(url, -EINVAL); + + // FIXME: Add support for vsock:, ssh-exec:, ssh-unix: URL schemes here. (The latter with OpenSSH + // 9.4's -W switch for referencing remote AF_UNIX sockets.) + + /* The Varlink URL scheme is a bit underdefined. We support only the unix: transport for now, plus an + * exec: transport we made up ourselves. Strictly speaking this shouldn't even be called URL, since + * it has nothing to do with Internet URLs by RFC. */ + + p = startswith(url, "unix:"); + if (p) + exec = false; + else { + p = startswith(url, "exec:"); + if (!p) + return log_debug_errno(SYNTHETIC_ERRNO(EPROTONOSUPPORT), "URL scheme not supported."); + + exec = true; + } + + /* The varlink.org reference C library supports more than just file system paths. We might want to + * support that one day too. For now simply refuse that. */ + if (p[strcspn(p, ";?#")] != '\0') + return log_debug_errno(SYNTHETIC_ERRNO(EPROTONOSUPPORT), "URL parameterization with ';', '?', '#' not supported."); + + if (exec || p[0] != '@') { /* no validity checks for abstract namespace */ + + if (!path_is_absolute(p)) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Specified path not absolute, refusing."); + + c = strdup(p); + if (!c) + return log_oom_debug(); + + path_simplify(c); + + if (!path_is_normalized(c)) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Specified path is not normalized, refusing."); + } + + if (exec) + return varlink_connect_exec(ret, c ?: p, NULL); + + return varlink_connect_address(ret, c ?: p); +} + int varlink_connect_fd(Varlink **ret, int fd) { Varlink *v; int r; @@ -442,6 +593,7 @@ static void varlink_clear_current(Varlink *v) { /* Clears the currently processed incoming message */ v->current = json_variant_unref(v->current); + v->current_method = NULL; close_many(v->input_fds, v->n_input_fds); v->input_fds = mfree(v->input_fds); @@ -475,6 +627,11 @@ static void varlink_clear(Varlink *v) { v->output_queue_tail = NULL; v->event = sd_event_unref(v->event); + + if (v->exec_pid > 0) { + sigterm_wait(v->exec_pid); + v->exec_pid = 0; + } } static Varlink* varlink_destroy(Varlink *v) { @@ -987,10 +1144,84 @@ invalid: return 1; } +static int generic_method_get_info( + Varlink *link, + JsonVariant *parameters, + VarlinkMethodFlags flags, + void *userdata) { + + _cleanup_strv_free_ char **interfaces = NULL; + _cleanup_free_ char *product = NULL; + int r; + + assert(link); + + if (json_variant_elements(parameters) != 0) + return varlink_errorb(link, VARLINK_ERROR_INVALID_PARAMETER, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR_VARIANT("parameter", json_variant_by_index(parameters, 0)))); + + product = strjoin("systemd (", program_invocation_short_name, ")"); + if (!product) + return -ENOMEM; + + VarlinkInterface *interface; + HASHMAP_FOREACH(interface, ASSERT_PTR(link->server)->interfaces) { + r = strv_extend(&interfaces, interface->name); + if (r < 0) + return r; + } + + strv_sort(interfaces); + + return varlink_replyb(link, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR_STRING("vendor", "The systemd Project"), + JSON_BUILD_PAIR_STRING("product", product), + JSON_BUILD_PAIR_STRING("version", STRINGIFY(PROJECT_VERSION) " (" GIT_VERSION ")"), + JSON_BUILD_PAIR_STRING("url", "https://systemd.io/"), + JSON_BUILD_PAIR_STRV("interfaces", interfaces))); +} + +static int generic_method_get_interface_description( + Varlink *link, + JsonVariant *parameters, + VarlinkMethodFlags flags, + void *userdata) { + + static const struct JsonDispatch dispatch_table[] = { + { "interface", JSON_VARIANT_STRING, json_dispatch_const_string, 0, JSON_MANDATORY }, + {} + }; + _cleanup_free_ char *text = NULL; + const VarlinkInterface *interface; + const char *name = NULL; + int r; + + assert(link); + + r = json_dispatch(parameters, dispatch_table, NULL, 0, &name); + if (r < 0) + return r; + + interface = hashmap_get(ASSERT_PTR(link->server)->interfaces, name); + if (!interface) + return varlink_errorb(link, VARLINK_ERROR_INTERFACE_NOT_FOUND, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR_STRING("interface", name))); + + r = varlink_idl_format(interface, &text); + if (r < 0) + return r; + + return varlink_replyb(link, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR_STRING("description", text))); +} + static int varlink_dispatch_method(Varlink *v) { _cleanup_(json_variant_unrefp) JsonVariant *parameters = NULL; VarlinkMethodFlags flags = 0; - const char *method = NULL, *error; + const char *method = NULL; JsonVariant *e; VarlinkMethod callback; const char *k; @@ -1063,37 +1294,51 @@ static int varlink_dispatch_method(Varlink *v) { assert(v->server); - if (STR_IN_SET(method, "org.varlink.service.GetInfo", "org.varlink.service.GetInterface")) { - /* For now, we don't implement a single of varlink's own methods */ - callback = NULL; - error = VARLINK_ERROR_METHOD_NOT_IMPLEMENTED; - } else if (startswith(method, "org.varlink.service.")) { - callback = NULL; - error = VARLINK_ERROR_METHOD_NOT_FOUND; - } else { - callback = hashmap_get(v->server->methods, method); - error = VARLINK_ERROR_METHOD_NOT_FOUND; + /* First consult user supplied method implementations */ + callback = hashmap_get(v->server->methods, method); + if (!callback) { + if (streq(method, "org.varlink.service.GetInfo")) + callback = generic_method_get_info; + else if (streq(method, "org.varlink.service.GetInterfaceDescription")) + callback = generic_method_get_interface_description; } if (callback) { - r = callback(v, parameters, flags, v->userdata); - if (r < 0) { - log_debug_errno(r, "Callback for %s returned error: %m", method); + bool invalid = false; - /* We got an error back from the callback. Propagate it to the client if the method call remains unanswered. */ - if (!FLAGS_SET(flags, VARLINK_METHOD_ONEWAY)) { - r = varlink_error_errno(v, r); - if (r < 0) - return r; + v->current_method = hashmap_get(v->server->symbols, method); + if (!v->current_method) + log_debug("No interface description defined for method '%s', not validating.", method); + else { + const char *bad_field; + + r = varlink_idl_validate_method_call(v->current_method, parameters, &bad_field); + if (r < 0) { + log_debug_errno(r, "Parameters for method %s() didn't pass validation on field '%s': %m", method, strna(bad_field)); + r = varlink_errorb(v, VARLINK_ERROR_INVALID_PARAMETER, JSON_BUILD_OBJECT(JSON_BUILD_PAIR_STRING("parameter", bad_field))); + invalid = true; + } + } + + if (!invalid) { + r = callback(v, parameters, flags, v->userdata); + if (r < 0) { + log_debug_errno(r, "Callback for %s returned error: %m", method); + + /* We got an error back from the callback. Propagate it to the client if the method call remains unanswered. */ + if (!FLAGS_SET(flags, VARLINK_METHOD_ONEWAY)) { + r = varlink_error_errno(v, r); + if (r < 0) + return r; + } } } } else if (!FLAGS_SET(flags, VARLINK_METHOD_ONEWAY)) { - assert(error); - - r = varlink_errorb(v, error, JSON_BUILD_OBJECT(JSON_BUILD_PAIR("method", JSON_BUILD_STRING(method)))); + r = varlink_errorb(v, VARLINK_ERROR_METHOD_NOT_FOUND, JSON_BUILD_OBJECT(JSON_BUILD_PAIR("method", JSON_BUILD_STRING(method)))); if (r < 0) return r; - } + } else + r = 0; switch (v->state) { @@ -1113,7 +1358,6 @@ static int varlink_dispatch_method(Varlink *v) { default: assert_not_reached(); - } return r; @@ -1282,6 +1526,15 @@ int varlink_wait(Varlink *v, usec_t timeout) { return 1; } +int varlink_is_idle(Varlink *v) { + assert_return(v, -EINVAL); + + /* Returns true if there's nothing pending on the connection anymore, i.e. we processed all incoming + * or outgoing messages fully, or finished disconnection */ + + return IN_SET(v->state, VARLINK_DISCONNECTED, VARLINK_IDLE_CLIENT, VARLINK_IDLE_SERVER); +} + int varlink_get_fd(Varlink *v) { assert_return(v, -EINVAL); @@ -1406,6 +1659,7 @@ static void varlink_detach_server(Varlink *v) { if (saved_server->disconnect_callback) saved_server->disconnect_callback(saved_server, v, saved_server->userdata); + varlink_server_test_exit_on_idle(saved_server); varlink_server_unref(saved_server); varlink_unref(v); } @@ -1849,6 +2103,14 @@ int varlink_reply(Varlink *v, JsonVariant *parameters) { if (r < 0) return varlink_log_errno(v, r, "Failed to build json message: %m"); + if (v->current_method) { + const char *bad_field = NULL; + + r = varlink_idl_validate_method_reply(v->current_method, parameters, &bad_field); + if (r < 0) + log_debug_errno(r, "Return parameters for method reply %s() didn't pass validation on field '%s', ignoring: %m", v->current_method->name, strna(bad_field)); + } + r = varlink_enqueue_json(v, m); if (r < 0) return varlink_log_errno(v, r, "Failed to enqueue json message: %m"); @@ -1915,6 +2177,17 @@ int varlink_error(Varlink *v, const char *error_id, JsonVariant *parameters) { if (r < 0) return varlink_log_errno(v, r, "Failed to build json message: %m"); + VarlinkSymbol *symbol = hashmap_get(v->server->symbols, error_id); + if (!symbol) + log_debug("No interface description defined for error '%s', not validating.", error_id); + else { + const char *bad_field = NULL; + + r = varlink_idl_validate_method_reply(symbol, parameters, &bad_field); + if (r < 0) + log_debug_errno(r, "Parameters for error %s didn't pass validation on field '%s', ignoring: %m", error_id, strna(bad_field)); + } + r = varlink_enqueue_json(v, m); if (r < 0) return varlink_log_errno(v, r, "Failed to enqueue json message: %m"); @@ -2016,6 +2289,14 @@ int varlink_notify(Varlink *v, JsonVariant *parameters) { if (r < 0) return varlink_log_errno(v, r, "Failed to build json message: %m"); + if (v->current_method) { + const char *bad_field = NULL; + + r = varlink_idl_validate_method_reply(v->current_method, parameters, &bad_field); + if (r < 0) + log_debug_errno(r, "Return parameters for method reply %s() didn't pass validation on field '%s', ignoring: %m", v->current_method->name, strna(bad_field)); + } + r = varlink_enqueue_json(v, m); if (r < 0) return varlink_log_errno(v, r, "Failed to enqueue json message: %m"); @@ -2445,7 +2726,8 @@ int varlink_set_allow_fd_passing_output(Varlink *v, bool b) { } int varlink_server_new(VarlinkServer **ret, VarlinkServerFlags flags) { - VarlinkServer *s; + _cleanup_(varlink_server_unrefp) VarlinkServer *s = NULL; + int r; assert_return(ret, -EINVAL); assert_return((flags & ~_VARLINK_SERVER_FLAGS_ALL) == 0, -EINVAL); @@ -2461,7 +2743,14 @@ int varlink_server_new(VarlinkServer **ret, VarlinkServerFlags flags) { .connections_per_uid_max = varlink_server_connections_per_uid_max(NULL), }; - *ret = s; + r = varlink_server_add_interface_many( + s, + &vl_interface_io_systemd, + &vl_interface_org_varlink_service); + if (r < 0) + return r; + + *ret = TAKE_PTR(s); return 0; } @@ -2477,6 +2766,8 @@ static VarlinkServer* varlink_server_destroy(VarlinkServer *s) { free(m); hashmap_free(s->methods); + hashmap_free(s->interfaces); + hashmap_free(s->symbols); hashmap_free(s->by_uid); sd_event_unref(s->event); @@ -2772,6 +3063,49 @@ int varlink_server_listen_address(VarlinkServer *s, const char *address, mode_t return 0; } +int varlink_server_listen_auto(VarlinkServer *s) { + _cleanup_strv_free_ char **names = NULL; + int r, n = 0; + + assert_return(s, -EINVAL); + + /* Adds all passed fds marked as "varlink" to our varlink server. These fds can either refer to a + * listening socket or to a connection socket. + * + * See https://varlink.org/#activation for the environment variables this is backed by and the + * recommended "varlink" identifier in $LISTEN_FDNAMES. */ + + r = sd_listen_fds_with_names(/* unset_environment= */ false, &names); + if (r < 0) + return r; + + for (int i = 0; i < r; i++) { + int b, fd; + socklen_t l = sizeof(b); + + if (!streq(names[i], "varlink")) + continue; + + fd = SD_LISTEN_FDS_START + i; + + if (getsockopt(fd, SOL_SOCKET, SO_ACCEPTCONN, &b, &l) < 0) + return -errno; + + assert(l == sizeof(b)); + + if (b) /* Listening socket? */ + r = varlink_server_listen_fd(s, fd); + else /* Otherwise assume connection socket */ + r = varlink_server_add_connection(s, fd, NULL); + if (r < 0) + return r; + + n++; + } + + return n; +} + void* varlink_server_set_userdata(VarlinkServer *s, void *userdata) { void *ret; @@ -2789,6 +3123,34 @@ void* varlink_server_get_userdata(VarlinkServer *s) { return s->userdata; } +int varlink_server_loop_auto(VarlinkServer *server) { + _cleanup_(sd_event_unrefp) sd_event *event = NULL; + int r; + + assert_return(server, -EINVAL); + assert_return(!server->event, -EBUSY); + + /* Runs a Varlink service event loop populated with a passed fd. Exits on the last connection. */ + + r = sd_event_new(&event); + if (r < 0) + return r; + + r = varlink_server_set_exit_on_idle(server, true); + if (r < 0) + return r; + + r = varlink_server_attach_event(server, event, 0); + if (r < 0) + return r; + + r = varlink_server_listen_auto(server); + if (r < 0) + return r; + + return sd_event_loop(event); +} + static VarlinkServerSocket* varlink_server_socket_destroy(VarlinkServerSocket *ss) { if (!ss) return NULL; @@ -2813,9 +3175,23 @@ int varlink_server_shutdown(VarlinkServer *s) { return 0; } +static void varlink_server_test_exit_on_idle(VarlinkServer *s) { + assert(s); + + if (s->exit_on_idle && s->event && s->n_connections == 0) + (void) sd_event_exit(s->event, 0); +} + +int varlink_server_set_exit_on_idle(VarlinkServer *s, bool b) { + assert_return(s, -EINVAL); + + s->exit_on_idle = b; + varlink_server_test_exit_on_idle(s); + return 0; +} + static int varlink_server_add_socket_event_source(VarlinkServer *s, VarlinkServerSocket *ss, int64_t priority) { _cleanup_(sd_event_source_unrefp) sd_event_source *es = NULL; - int r; assert(s); @@ -2880,6 +3256,22 @@ sd_event *varlink_server_get_event(VarlinkServer *s) { return s->event; } +static bool varlink_symbol_in_interface(const char *method, const char *interface) { + const char *p; + + assert(method); + assert(interface); + + p = startswith(method, interface); + if (!p) + return false; + + if (*p != '.') + return false; + + return !strchr(p+1, '.'); +} + int varlink_server_bind_method(VarlinkServer *s, const char *method, VarlinkMethod callback) { _cleanup_free_ char *m = NULL; int r; @@ -2888,7 +3280,8 @@ int varlink_server_bind_method(VarlinkServer *s, const char *method, VarlinkMeth assert_return(method, -EINVAL); assert_return(callback, -EINVAL); - if (startswith(method, "org.varlink.service.")) + if (varlink_symbol_in_interface(method, "org.varlink.service") || + varlink_symbol_in_interface(method, "io.systemd")) return log_debug_errno(SYNTHETIC_ERRNO(EEXIST), "Cannot bind server to '%s'.", method); m = strdup(method); @@ -2952,6 +3345,63 @@ int varlink_server_bind_disconnect(VarlinkServer *s, VarlinkDisconnect callback) return 0; } +int varlink_server_add_interface(VarlinkServer *s, const VarlinkInterface *interface) { + int r; + + assert_return(s, -EINVAL); + assert_return(interface, -EINVAL); + assert_return(interface->name, -EINVAL); + + if (hashmap_contains(s->interfaces, interface->name)) + return log_debug_errno(SYNTHETIC_ERRNO(EEXIST), "Duplicate registration of interface '%s'.", interface->name); + + r = hashmap_ensure_put(&s->interfaces, &string_hash_ops, interface->name, (void*) interface); + if (r < 0) + return r; + + for (const VarlinkSymbol *const*symbol = interface->symbols; *symbol; symbol++) { + _cleanup_free_ char *j = NULL; + + /* We only ever want to validate method calls/replies and errors against the interface + * definitions, hence don't bother with the type symbols */ + if (!IN_SET((*symbol)->symbol_type, VARLINK_METHOD, VARLINK_ERROR)) + continue; + + j = strjoin(interface->name, ".", (*symbol)->name); + if (!j) + return -ENOMEM; + + r = hashmap_ensure_put(&s->symbols, &string_hash_ops_free, j, (void*) *symbol); + if (r < 0) + return r; + + TAKE_PTR(j); + } + + return 0; +} + +int varlink_server_add_interface_many_internal(VarlinkServer *s, ...) { + va_list ap; + int r = 0; + + assert_return(s, -EINVAL); + + va_start(ap, s); + for (;;) { + const VarlinkInterface *interface = va_arg(ap, const VarlinkInterface*); + if (!interface) + break; + + r = varlink_server_add_interface(s, interface); + if (r < 0) + break; + } + va_end(ap); + + return r; +} + unsigned varlink_server_connections_max(VarlinkServer *s) { int dts; @@ -3090,3 +3540,38 @@ int varlink_server_deserialize_one(VarlinkServer *s, const char *value, FDSet *f LIST_PREPEND(sockets, s->sockets, TAKE_PTR(ss)); return 0; } + +int varlink_invocation(VarlinkInvocationFlags flags) { + _cleanup_strv_free_ char **names = NULL; + int r, b; + socklen_t l = sizeof(b); + + /* Returns true if this is a "pure" varlink server invocation, i.e. with one fd passed. */ + + r = sd_listen_fds_with_names(/* unset_environment= */ false, &names); + if (r < 0) + return r; + if (r == 0) + return false; + if (r > 1) + return -ETOOMANYREFS; + + if (!strv_equal(names, STRV_MAKE("varlink"))) + return false; + + if (FLAGS_SET(flags, VARLINK_ALLOW_LISTEN|VARLINK_ALLOW_ACCEPT)) /* Both flags set? Then allow everything */ + return true; + + if ((flags & (VARLINK_ALLOW_LISTEN|VARLINK_ALLOW_ACCEPT)) == 0) /* Neither is set, then fail */ + return -EISCONN; + + if (getsockopt(SD_LISTEN_FDS_START, SOL_SOCKET, SO_ACCEPTCONN, &b, &l) < 0) + return -errno; + + assert(l == sizeof(b)); + + if (!FLAGS_SET(flags, b ? VARLINK_ALLOW_LISTEN : VARLINK_ALLOW_ACCEPT)) + return -EISCONN; + + return true; +} diff --git a/src/shared/varlink.h b/src/shared/varlink.h index 0984222c96..5fe529c853 100644 --- a/src/shared/varlink.h +++ b/src/shared/varlink.h @@ -5,6 +5,7 @@ #include "json.h" #include "time-util.h" +#include "varlink-idl.h" /* A minimal Varlink implementation. We only implement the minimal, obvious bits here though. No validation, * no introspection, no name service, just the stuff actually needed. @@ -45,7 +46,6 @@ typedef enum VarlinkServerFlags { VARLINK_SERVER_MYSELF_ONLY = 1 << 1, /* Only accessible by our own UID */ VARLINK_SERVER_ACCOUNT_UID = 1 << 2, /* Do per user accounting */ VARLINK_SERVER_INHERIT_USERDATA = 1 << 3, /* Initialize Varlink connection userdata from VarlinkServer userdata */ - _VARLINK_SERVER_FLAGS_ALL = (1 << 4) - 1, } VarlinkServerFlags; @@ -55,6 +55,8 @@ typedef int (*VarlinkConnect)(VarlinkServer *server, Varlink *link, void *userda typedef void (*VarlinkDisconnect)(VarlinkServer *server, Varlink *link, void *userdata); int varlink_connect_address(Varlink **ret, const char *address); +int varlink_connect_exec(Varlink **ret, const char *command, char **argv); +int varlink_connect_url(Varlink **ret, const char *url); int varlink_connect_fd(Varlink **ret, int fd); Varlink* varlink_ref(Varlink *link); @@ -71,6 +73,8 @@ sd_event *varlink_get_event(Varlink *v); int varlink_process(Varlink *v); int varlink_wait(Varlink *v, usec_t timeout); +int varlink_is_idle(Varlink *v); + int varlink_flush(Varlink *v); int varlink_close(Varlink *v); @@ -142,6 +146,7 @@ VarlinkServer *varlink_server_unref(VarlinkServer *s); /* Add addresses or fds to listen on */ int varlink_server_listen_address(VarlinkServer *s, const char *address, mode_t mode); int varlink_server_listen_fd(VarlinkServer *s, int fd); +int varlink_server_listen_auto(VarlinkServer *s); int varlink_server_add_connection(VarlinkServer *s, int fd, Varlink **ret); /* Bind callbacks */ @@ -151,6 +156,11 @@ int varlink_server_bind_method_many_internal(VarlinkServer *s, ...); int varlink_server_bind_connect(VarlinkServer *s, VarlinkConnect connect); int varlink_server_bind_disconnect(VarlinkServer *s, VarlinkDisconnect disconnect); +/* Add interface definition */ +int varlink_server_add_interface(VarlinkServer *s, const VarlinkInterface *interface); +int varlink_server_add_interface_many_internal(VarlinkServer *s, ...); +#define varlink_server_add_interface_many(s, ...) varlink_server_add_interface_many_internal(s, __VA_ARGS__, NULL) + void* varlink_server_set_userdata(VarlinkServer *s, void *userdata); void* varlink_server_get_userdata(VarlinkServer *s); @@ -158,8 +168,12 @@ int varlink_server_attach_event(VarlinkServer *v, sd_event *e, int64_t priority) int varlink_server_detach_event(VarlinkServer *v); sd_event *varlink_server_get_event(VarlinkServer *v); +int varlink_server_loop_auto(VarlinkServer *server); + int varlink_server_shutdown(VarlinkServer *server); +int varlink_server_set_exit_on_idle(VarlinkServer *s, bool b); + unsigned varlink_server_connections_max(VarlinkServer *s); unsigned varlink_server_connections_per_uid_max(VarlinkServer *s); @@ -170,6 +184,15 @@ unsigned varlink_server_current_connections(VarlinkServer *s); int varlink_server_set_description(VarlinkServer *s, const char *description); +typedef enum VarlinkInvocationFlags { + VARLINK_ALLOW_LISTEN = 1 << 0, + VARLINK_ALLOW_ACCEPT = 1 << 1, + _VARLINK_SERVER_INVOCATION_FLAGS_MAX = (1 << 2) - 1, + _VARLINK_SERVER_INVOCATION_FLAGS_INVALID = -EINVAL, +} VarlinkInvocationFlags; + +int varlink_invocation(VarlinkInvocationFlags flags); + DEFINE_TRIVIAL_CLEANUP_FUNC(Varlink *, varlink_unref); DEFINE_TRIVIAL_CLEANUP_FUNC(Varlink *, varlink_close_unref); DEFINE_TRIVIAL_CLEANUP_FUNC(Varlink *, varlink_flush_close_unref); @@ -179,6 +202,8 @@ DEFINE_TRIVIAL_CLEANUP_FUNC(VarlinkServer *, varlink_server_unref); #define VARLINK_ERROR_DISCONNECTED "io.systemd.Disconnected" #define VARLINK_ERROR_TIMEOUT "io.systemd.TimedOut" #define VARLINK_ERROR_PROTOCOL "io.systemd.Protocol" + +/* This one we invented, and use for generically propagating system errors (errno) to clients */ #define VARLINK_ERROR_SYSTEM "io.systemd.System" /* These are errors defined in the Varlink spec */ @@ -188,5 +213,4 @@ DEFINE_TRIVIAL_CLEANUP_FUNC(VarlinkServer *, varlink_server_unref); #define VARLINK_ERROR_INVALID_PARAMETER "org.varlink.service.InvalidParameter" /* These are errors we came up with and squatted the namespace with */ -#define VARLINK_ERROR_SUBSCRIPTION_TAKEN "org.varlink.service.SubscriptionTaken" #define VARLINK_ERROR_PERMISSION_DENIED "org.varlink.service.PermissionDenied" diff --git a/src/test/meson.build b/src/test/meson.build index 7e25fcdbaa..3f1b7afbf7 100644 --- a/src/test/meson.build +++ b/src/test/meson.build @@ -417,6 +417,10 @@ executables += [ 'sources' : files('test-varlink.c'), 'dependencies' : threads, }, + test_template + { + 'sources' : files('test-varlink-idl.c'), + 'dependencies' : threads, + }, test_template + { 'sources' : files('test-watchdog.c'), 'type' : 'unsafe', diff --git a/src/test/test-varlink-idl.c b/src/test/test-varlink-idl.c new file mode 100644 index 0000000000..a93d717f3f --- /dev/null +++ b/src/test/test-varlink-idl.c @@ -0,0 +1,344 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include + +#include "fd-util.h" +#include "pretty-print.h" +#include "tests.h" +#include "varlink.h" +#include "varlink-idl.h" +#include "varlink-io.systemd.h" +#include "varlink-io.systemd.Journal.h" +#include "varlink-io.systemd.ManagedOOM.h" +#include "varlink-io.systemd.PCRExtend.h" +#include "varlink-io.systemd.Resolve.Monitor.h" +#include "varlink-io.systemd.Resolve.h" +#include "varlink-io.systemd.UserDatabase.h" +#include "varlink-io.systemd.oom.h" +#include "varlink-org.varlink.service.h" + +static VARLINK_DEFINE_ENUM_TYPE( + EnumTest, + VARLINK_DEFINE_ENUM_VALUE(foo), + VARLINK_DEFINE_ENUM_VALUE(bar), + VARLINK_DEFINE_ENUM_VALUE(baz)); + +static VARLINK_DEFINE_STRUCT_TYPE( + NestedStructTest, + VARLINK_DEFINE_FIELD(x, VARLINK_INT, 0)); + +static VARLINK_DEFINE_STRUCT_TYPE( + StructTest, + + VARLINK_DEFINE_FIELD(bbb, VARLINK_BOOL, 0), + VARLINK_DEFINE_FIELD(bbbn, VARLINK_BOOL, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(bbba, VARLINK_BOOL, VARLINK_ARRAY), + VARLINK_DEFINE_FIELD(bbbna, VARLINK_BOOL, VARLINK_NULLABLE|VARLINK_ARRAY), + VARLINK_DEFINE_FIELD(bbbm, VARLINK_BOOL, VARLINK_MAP), + VARLINK_DEFINE_FIELD(bbbnm, VARLINK_BOOL, VARLINK_NULLABLE|VARLINK_MAP), + + VARLINK_DEFINE_FIELD(iii, VARLINK_INT, 0), + VARLINK_DEFINE_FIELD(iiin, VARLINK_INT, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(iiia, VARLINK_INT, VARLINK_ARRAY), + VARLINK_DEFINE_FIELD(iiina, VARLINK_INT, VARLINK_NULLABLE|VARLINK_ARRAY), + VARLINK_DEFINE_FIELD(iiim, VARLINK_INT, VARLINK_MAP), + VARLINK_DEFINE_FIELD(iiinm, VARLINK_INT, VARLINK_NULLABLE|VARLINK_MAP), + + VARLINK_DEFINE_FIELD(fff, VARLINK_FLOAT, 0), + VARLINK_DEFINE_FIELD(fffn, VARLINK_FLOAT, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(fffa, VARLINK_FLOAT, VARLINK_ARRAY), + VARLINK_DEFINE_FIELD(fffna, VARLINK_FLOAT, VARLINK_NULLABLE|VARLINK_ARRAY), + VARLINK_DEFINE_FIELD(fffm, VARLINK_FLOAT, VARLINK_MAP), + VARLINK_DEFINE_FIELD(fffnm, VARLINK_FLOAT, VARLINK_NULLABLE|VARLINK_MAP), + + VARLINK_DEFINE_FIELD(sss, VARLINK_STRING, 0), + VARLINK_DEFINE_FIELD(sssn, VARLINK_STRING, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(sssa, VARLINK_STRING, VARLINK_ARRAY), + VARLINK_DEFINE_FIELD(sssna, VARLINK_STRING, VARLINK_NULLABLE|VARLINK_ARRAY), + VARLINK_DEFINE_FIELD(sssm, VARLINK_STRING, VARLINK_MAP), + VARLINK_DEFINE_FIELD(sssnm, VARLINK_STRING, VARLINK_NULLABLE|VARLINK_MAP), + + VARLINK_DEFINE_FIELD(ooo, VARLINK_OBJECT, 0), + VARLINK_DEFINE_FIELD(ooon, VARLINK_OBJECT, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(oooa, VARLINK_OBJECT, VARLINK_ARRAY), + VARLINK_DEFINE_FIELD(ooona, VARLINK_OBJECT, VARLINK_NULLABLE|VARLINK_ARRAY), + VARLINK_DEFINE_FIELD(ooom, VARLINK_OBJECT, VARLINK_MAP), + VARLINK_DEFINE_FIELD(ooonm, VARLINK_OBJECT, VARLINK_NULLABLE|VARLINK_MAP), + + VARLINK_DEFINE_FIELD_BY_TYPE(eee, EnumTest, 0), + VARLINK_DEFINE_FIELD_BY_TYPE(eeen, EnumTest, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD_BY_TYPE(eeea, EnumTest, VARLINK_ARRAY), + VARLINK_DEFINE_FIELD_BY_TYPE(eeena, EnumTest, VARLINK_NULLABLE|VARLINK_ARRAY), + VARLINK_DEFINE_FIELD_BY_TYPE(eeem, EnumTest, VARLINK_MAP), + VARLINK_DEFINE_FIELD_BY_TYPE(eeenm, EnumTest, VARLINK_NULLABLE|VARLINK_MAP), + + VARLINK_DEFINE_FIELD_BY_TYPE(nnn, NestedStructTest, 0), + VARLINK_DEFINE_FIELD_BY_TYPE(nnnn, NestedStructTest, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD_BY_TYPE(nnna, NestedStructTest, VARLINK_ARRAY), + VARLINK_DEFINE_FIELD_BY_TYPE(nnnna, NestedStructTest, VARLINK_NULLABLE|VARLINK_ARRAY), + VARLINK_DEFINE_FIELD_BY_TYPE(nnnm, NestedStructTest, VARLINK_MAP), + VARLINK_DEFINE_FIELD_BY_TYPE(nnnnm, NestedStructTest, VARLINK_NULLABLE|VARLINK_MAP)); + +static VARLINK_DEFINE_METHOD( + MethodTest, + VARLINK_DEFINE_INPUT(x, VARLINK_BOOL, 0), + VARLINK_DEFINE_INPUT_BY_TYPE(y, EnumTest, 0), + VARLINK_DEFINE_INPUT_BY_TYPE(z, StructTest, 0), + VARLINK_DEFINE_OUTPUT(x, VARLINK_BOOL, 0), + VARLINK_DEFINE_OUTPUT_BY_TYPE(y, EnumTest, 0), + VARLINK_DEFINE_OUTPUT_BY_TYPE(z, StructTest, 0)); + +static VARLINK_DEFINE_ERROR( + ErrorTest, + VARLINK_DEFINE_FIELD(x, VARLINK_BOOL, 0), + VARLINK_DEFINE_FIELD_BY_TYPE(y, EnumTest, 0), + VARLINK_DEFINE_FIELD_BY_TYPE(z, StructTest, 0)); + +static VARLINK_DEFINE_INTERFACE( + xyz_test, + "xyz.test", + &vl_type_EnumTest, + &vl_type_NestedStructTest, + &vl_type_StructTest, + &vl_method_MethodTest, + &vl_error_ErrorTest); + +static void test_parse_format_one(const VarlinkInterface *iface) { + _cleanup_(varlink_interface_freep) VarlinkInterface *parsed = NULL; + _cleanup_free_ char *text = NULL, *text2 = NULL; + + assert_se(iface); + + assert_se(varlink_idl_dump(stdout, /* use_colors=*/ true, iface) >= 0); + assert_se(varlink_idl_consistent(iface, LOG_ERR) >= 0); + assert_se(varlink_idl_format(iface, &text) >= 0); + assert_se(varlink_idl_parse(text, NULL, NULL, &parsed) >= 0); + assert_se(varlink_idl_consistent(parsed, LOG_ERR) >= 0); + assert_se(varlink_idl_format(parsed, &text2) >= 0); + assert_se(streq(text, text2)); +} + +TEST(parse_format) { + test_parse_format_one(&vl_interface_org_varlink_service); + print_separator(); + test_parse_format_one(&vl_interface_io_systemd_UserDatabase); + print_separator(); + test_parse_format_one(&vl_interface_io_systemd_Journal); + print_separator(); + test_parse_format_one(&vl_interface_io_systemd_Resolve); + print_separator(); + test_parse_format_one(&vl_interface_io_systemd_Resolve_Monitor); + print_separator(); + test_parse_format_one(&vl_interface_io_systemd_ManagedOOM); + print_separator(); + test_parse_format_one(&vl_interface_io_systemd_oom); + print_separator(); + test_parse_format_one(&vl_interface_io_systemd); + print_separator(); + test_parse_format_one(&vl_interface_io_systemd_PCRExtend); + print_separator(); + test_parse_format_one(&vl_interface_xyz_test); +} + +TEST(parse) { + _cleanup_(varlink_interface_freep) VarlinkInterface *parsed = NULL; + + /* This one has (nested) enonymous enums and structs */ + static const char text[] = + "interface quu.waa\n" + "type Fooenum ( a, b, c )\n" + "type Barstruct ( a : (x, y, z), b : (x : int), c: (f, ff, fff), d: object, e : (sub : (subsub: (subsubsub: string, subsubsub2: (iii, ooo)))))" + ; + + assert_se(varlink_idl_parse(text, NULL, NULL, &parsed) >= 0); + test_parse_format_one(parsed); +} + +TEST(interface_name_is_valid) { + assert_se(!varlink_idl_interface_name_is_valid(NULL)); + assert_se(!varlink_idl_interface_name_is_valid("")); + assert_se(!varlink_idl_interface_name_is_valid(",")); + assert_se(!varlink_idl_interface_name_is_valid(".")); + assert_se(!varlink_idl_interface_name_is_valid("-")); + assert_se(varlink_idl_interface_name_is_valid("a")); + assert_se(varlink_idl_interface_name_is_valid("a.a")); + assert_se(!varlink_idl_interface_name_is_valid("-.a")); + assert_se(!varlink_idl_interface_name_is_valid("-a.a")); + assert_se(!varlink_idl_interface_name_is_valid("a-.a")); + assert_se(varlink_idl_interface_name_is_valid("a-a.a")); + assert_se(!varlink_idl_interface_name_is_valid("a-a.a-")); + assert_se(!varlink_idl_interface_name_is_valid("a-a.-a")); + assert_se(!varlink_idl_interface_name_is_valid("a-a.-")); + assert_se(varlink_idl_interface_name_is_valid("a-a.a-a")); + assert_se(varlink_idl_interface_name_is_valid("io.systemd.Foobar")); +} + +TEST(symbol_name_is_valid) { + assert_se(!varlink_idl_symbol_name_is_valid(NULL)); + assert_se(!varlink_idl_symbol_name_is_valid("")); + assert_se(!varlink_idl_symbol_name_is_valid("_")); + assert_se(!varlink_idl_symbol_name_is_valid("_foo")); + assert_se(varlink_idl_symbol_name_is_valid("Foofoo")); + assert_se(varlink_idl_symbol_name_is_valid("Foo")); + assert_se(varlink_idl_symbol_name_is_valid("Foo0")); + assert_se(!varlink_idl_symbol_name_is_valid("0Foo")); + assert_se(!varlink_idl_symbol_name_is_valid("foo")); + assert_se(varlink_idl_symbol_name_is_valid("Foo0foo")); + assert_se(!varlink_idl_symbol_name_is_valid("bool")); + assert_se(!varlink_idl_symbol_name_is_valid("int")); + assert_se(!varlink_idl_symbol_name_is_valid("float")); + assert_se(!varlink_idl_symbol_name_is_valid("string")); + assert_se(!varlink_idl_symbol_name_is_valid("object")); +} + +TEST(field_name_is_valid) { + assert_se(!varlink_idl_field_name_is_valid(NULL)); + assert_se(!varlink_idl_field_name_is_valid("")); + assert_se(!varlink_idl_field_name_is_valid("_")); + assert_se(!varlink_idl_field_name_is_valid("_foo")); + assert_se(!varlink_idl_field_name_is_valid("_foo_")); + assert_se(!varlink_idl_field_name_is_valid("foo_")); + assert_se(varlink_idl_field_name_is_valid("foo_foo")); + assert_se(varlink_idl_field_name_is_valid("f_o_o_f_o_o")); + assert_se(!varlink_idl_field_name_is_valid("foo__foo")); + assert_se(varlink_idl_field_name_is_valid("Foofoo")); + assert_se(varlink_idl_field_name_is_valid("Foo")); + assert_se(varlink_idl_field_name_is_valid("Foo0")); + assert_se(!varlink_idl_field_name_is_valid("0Foo")); + assert_se(varlink_idl_field_name_is_valid("foo")); + assert_se(varlink_idl_field_name_is_valid("Foo0foo")); + assert_se(varlink_idl_field_name_is_valid("foo0foo")); +} + +TEST(validate_json) { + + _cleanup_(varlink_interface_freep) VarlinkInterface *parsed = NULL; + + /* This one has (nested) enonymous enums and structs */ + static const char text[] = + "interface validate.test\n" + "method Mymethod ( a:string, b:int, c:?bool, d:[]int, e:?[string]bool, f:?(piff, paff), g:(f:float) ) -> ()\n"; + + assert_se(varlink_idl_parse(text, NULL, NULL, &parsed) >= 0); + test_parse_format_one(parsed); + + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + + assert_se(json_build(&v, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("a", JSON_BUILD_STRING("x")), + JSON_BUILD_PAIR("b", JSON_BUILD_UNSIGNED(44)), + JSON_BUILD_PAIR("d", JSON_BUILD_ARRAY(JSON_BUILD_UNSIGNED(5), JSON_BUILD_UNSIGNED(7), JSON_BUILD_UNSIGNED(107))), + JSON_BUILD_PAIR("g", JSON_BUILD_OBJECT(JSON_BUILD_PAIR("f", JSON_BUILD_REAL(0.5f)))))) >= 0); + + json_variant_dump(v, JSON_FORMAT_PRETTY_AUTO|JSON_FORMAT_COLOR_AUTO, stdout, NULL); + + const VarlinkSymbol* symbol = ASSERT_PTR(varlink_idl_find_symbol(parsed, VARLINK_METHOD, "Mymethod")); + + assert_se(varlink_idl_validate_method_call(symbol, v, NULL) >= 0); +} + +static int test_method(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) { + JsonVariant *foo = json_variant_by_key(parameters, "foo"), *bar = json_variant_by_key(parameters, "bar"); + + return varlink_replyb(link, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR_UNSIGNED("waldo", json_variant_unsigned(foo) * json_variant_unsigned(bar)), + JSON_BUILD_PAIR_UNSIGNED("quux", json_variant_unsigned(foo) + json_variant_unsigned(bar)))); +} + +static int done_method(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) { + assert_se(sd_event_exit(varlink_get_event(link), 0) >= 0); + return 0; +} + +static VARLINK_DEFINE_METHOD( + TestMethod, + VARLINK_DEFINE_INPUT(foo, VARLINK_INT, 0), + VARLINK_DEFINE_INPUT(bar, VARLINK_INT, 0), + VARLINK_DEFINE_INPUT(optional, VARLINK_STRING, VARLINK_NULLABLE), + VARLINK_DEFINE_OUTPUT(waldo, VARLINK_INT, 0), + VARLINK_DEFINE_OUTPUT(quux, VARLINK_INT, 0)); + +static VARLINK_DEFINE_METHOD(Done); + +static VARLINK_DEFINE_INTERFACE( + xyz, + "xyz", + &vl_method_TestMethod, + &vl_method_Done); + + +static void* server_thread(void *userdata) { + _cleanup_(varlink_server_unrefp) VarlinkServer *server = NULL; + _cleanup_(sd_event_unrefp) sd_event *event = NULL; + + assert_se(varlink_server_new(&server, 0) >= 0); + assert_se(varlink_server_add_interface(server, &vl_interface_xyz) >= 0); + assert_se(varlink_server_bind_method(server, "xyz.TestMethod", test_method) >= 0); + assert_se(varlink_server_bind_method(server, "xyz.Done", done_method) >= 0); + + assert_se(sd_event_new(&event) >= 0); + assert_se(varlink_server_attach_event(server, event, 0) >= 0); + + assert_se(varlink_server_add_connection(server, PTR_TO_FD(userdata), NULL) >= 0); + + assert_se(sd_event_loop(event) >= 0); + return NULL; +} + +TEST(validate_method_call) { + _cleanup_close_pair_ int fd[2] = PIPE_EBADF; + _cleanup_(varlink_unrefp) Varlink *v = NULL; + pthread_t t; + + assert_se(socketpair(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0, fd) >= 0); + assert_se(pthread_create(&t, NULL, server_thread, FD_TO_PTR(TAKE_FD(fd[1]))) == 0); + assert_se(varlink_connect_fd(&v, TAKE_FD(fd[0])) >= 0); + + JsonVariant *reply = NULL; + const char *error_id = NULL; + assert_se(varlink_callb(v, "xyz.TestMethod", &reply, &error_id, NULL, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR_UNSIGNED("foo", 8), + JSON_BUILD_PAIR_UNSIGNED("bar", 9))) >= 0); + + _cleanup_(json_variant_unrefp) JsonVariant *expected_reply = NULL; + assert_se(json_build(&expected_reply, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR_UNSIGNED("waldo", 8*9), + JSON_BUILD_PAIR_UNSIGNED("quux", 8+9))) >= 0); + + assert_se(!error_id); + + json_variant_dump(reply, JSON_FORMAT_PRETTY_AUTO|JSON_FORMAT_COLOR_AUTO, NULL, NULL); + json_variant_dump(expected_reply, JSON_FORMAT_PRETTY_AUTO|JSON_FORMAT_COLOR_AUTO, NULL, NULL); + assert_se(json_variant_equal(reply, expected_reply)); + + assert_se(varlink_callb(v, "xyz.TestMethod", &reply, &error_id, NULL, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR_UNSIGNED("foo", 9), + JSON_BUILD_PAIR_UNSIGNED("bar", 8), + JSON_BUILD_PAIR_STRING("optional", "pfft"))) >= 0); + + assert_se(!error_id); + assert_se(json_variant_equal(reply, expected_reply)); + + assert_se(varlink_callb(v, "xyz.TestMethod", &reply, &error_id, NULL, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR_UNSIGNED("foo", 8), + JSON_BUILD_PAIR_UNSIGNED("bar", 9), + JSON_BUILD_PAIR_STRING("zzz", "pfft"))) >= 0); + assert_se(streq_ptr(error_id, VARLINK_ERROR_INVALID_PARAMETER)); + + assert_se(varlink_callb(v, "xyz.TestMethod", &reply, &error_id, NULL, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR_STRING("foo", "wuff"), + JSON_BUILD_PAIR_UNSIGNED("bar", 9))) >= 0); + assert_se(streq_ptr(error_id, VARLINK_ERROR_INVALID_PARAMETER)); + + assert_se(varlink_send(v, "xyz.Done", NULL) >= 0); + assert_se(varlink_flush(v) >= 0); + assert_se(pthread_join(t, NULL) == 0); +} + +DEFINE_TEST_MAIN(LOG_DEBUG); diff --git a/src/userdb/userwork.c b/src/userdb/userwork.c index 9fccadc950..7413378bcb 100644 --- a/src/userdb/userwork.c +++ b/src/userdb/userwork.c @@ -18,6 +18,7 @@ #include "user-util.h" #include "userdb.h" #include "varlink.h" +#include "varlink-io.systemd.UserDatabase.h" #define ITERATIONS_MAX 64U #define RUNTIME_MAX_USEC (5 * USEC_PER_MINUTE) @@ -483,6 +484,10 @@ static int run(int argc, char *argv[]) { if (r < 0) return log_error_errno(r, "Failed to allocate server: %m"); + r = varlink_server_add_interface(server, &vl_interface_io_systemd_UserDatabase); + if (r < 0) + return log_error_errno(r, "Failed to add UserDatabase interface to varlink server: %m"); + r = varlink_server_bind_method_many( server, "io.systemd.UserDatabase.GetUserRecord", vl_method_get_user_record, diff --git a/src/varlinkctl/meson.build b/src/varlinkctl/meson.build new file mode 100644 index 0000000000..c1074dc50a --- /dev/null +++ b/src/varlinkctl/meson.build @@ -0,0 +1,13 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +varlinkctl_sources = files( + 'varlinkctl.c', +) + +executables += [ + executable_template + { + 'name' : 'varlinkctl', + 'public' : true, + 'sources' : varlinkctl_sources, + }, +] diff --git a/src/varlinkctl/varlinkctl.c b/src/varlinkctl/varlinkctl.c new file mode 100644 index 0000000000..a656bfcaf0 --- /dev/null +++ b/src/varlinkctl/varlinkctl.c @@ -0,0 +1,520 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include + +#include "build.h" +#include "fd-util.h" +#include "fileio.h" +#include "format-table.h" +#include "main-func.h" +#include "pager.h" +#include "parse-argument.h" +#include "path-util.h" +#include "pretty-print.h" +#include "terminal-util.h" +#include "varlink.h" +#include "verbs.h" +#include "version.h" + +static JsonFormatFlags arg_json_format_flags = JSON_FORMAT_OFF; +static PagerFlags arg_pager_flags = 0; +static VarlinkMethodFlags arg_method_flags = 0; + +static int help(void) { + _cleanup_free_ char *link = NULL; + int r; + + r = terminal_urlify_man("varlinkctl", "1", &link); + if (r < 0) + return log_oom(); + + pager_open(arg_pager_flags); + + printf("%1$s [OPTIONS...] COMMAND ...\n\n" + "%5$sIntrospect Varlink Services.%6$s\n" + "\n%3$sCommands:%4$s\n" + " info ADDRESS Show service information\n" + " list-interfaces ADDRESS\n" + " List interfaces implemented by service\n" + " introspect ADDRESS INTERFACE\n" + " Show interface definition\n" + " call ADDRESS METHOD [PARAMS]\n" + " Invoke method\n" + " validate-idl [FILE] Validate interface description\n" + " help Show this help\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" + " --more Request multiple responses\n" + " --oneway Do not request response\n" + " --json=MODE Output as JSON\n" + " -j Same as --json=pretty on tty, --json=short otherwise\n" + "\nSee the %2$s for details.\n", + program_invocation_short_name, + link, + ansi_underline(), + ansi_normal(), + ansi_highlight(), + ansi_normal()); + + return 0; +} + +static int verb_help(int argc, char **argv, void *userdata) { + return help(); +} + +static int parse_argv(int argc, char *argv[]) { + + enum { + ARG_VERSION = 0x100, + ARG_NO_PAGER, + ARG_MORE, + ARG_ONEWAY, + 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 }, + { "more", no_argument, NULL, ARG_MORE }, + { "oneway", no_argument, NULL, ARG_ONEWAY }, + { "json", required_argument, NULL, ARG_JSON }, + {}, + }; + + int c, r; + + assert(argc >= 0); + assert(argv); + + while ((c = getopt_long(argc, argv, "hj", options, NULL)) >= 0) + + switch (c) { + + case 'h': + return help(); + + case ARG_VERSION: + return version(); + + case ARG_NO_PAGER: + arg_pager_flags |= PAGER_DISABLE; + break; + + case ARG_MORE: + arg_method_flags = (arg_method_flags & ~VARLINK_METHOD_ONEWAY) | VARLINK_METHOD_MORE; + break; + + case ARG_ONEWAY: + arg_method_flags = (arg_method_flags & ~VARLINK_METHOD_MORE) | VARLINK_METHOD_ONEWAY; + break; + + case ARG_JSON: + r = parse_json_argument(optarg, &arg_json_format_flags); + if (r <= 0) + return r; + + break; + + case 'j': + arg_json_format_flags = JSON_FORMAT_PRETTY_AUTO|JSON_FORMAT_COLOR_AUTO; + break; + + case '?': + return -EINVAL; + + default: + assert_not_reached(); + } + + /* If more than one reply is expected, imply JSON-SEQ output */ + if (FLAGS_SET(arg_method_flags, VARLINK_METHOD_MORE)) + arg_json_format_flags |= JSON_FORMAT_SEQ; + + return 1; +} + +static int varlink_connect_auto(Varlink **ret, const char *where) { + int r; + + assert(ret); + assert(where); + + if (STARTSWITH_SET(where, "/", "./")) { /* If the string starts with a slash or dot slash we use it as a file system path */ + _cleanup_close_ int fd = -EBADF; + struct stat st; + + fd = open(where, O_PATH|O_CLOEXEC); + if (fd < 0) + return log_error_errno(errno, "Failed to open '%s': %m", where); + + if (fstat(fd, &st) < 0) + return log_error_errno(errno, "Failed to stat '%s': %m", where); + + /* Is this a socket in the fs? Then connect() to it. */ + if (S_ISSOCK(st.st_mode)) { + r = varlink_connect_address(ret, FORMAT_PROC_FD_PATH(fd)); + if (r < 0) + return log_error_errno(r, "Failed to connect to '%s': %m", where); + + return 0; + } + + /* Is this an executable binary? Then fork it off. */ + if (S_ISREG(st.st_mode) && (st.st_mode & 0111)) { + r = varlink_connect_exec(ret, where, STRV_MAKE(where)); /* Ideally we'd use FORMAT_PROC_FD_PATH(fd) here too, but that breaks the #! logic */ + if (r < 0) + return log_error_errno(r, "Failed to spawn '%s' process: %m", where); + + return 0; + } + + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unrecognized path '%s' is neither an AF_UNIX socket, nor an executable binary.", where); + } + + /* Otherwise assume this is an URL */ + r = varlink_connect_url(ret, where); + if (r < 0) + return log_error_errno(r, "Failed to connect to URL '%s': %m", where); + + return 0; +} + +typedef struct GetInfoData { + const char *vendor; + const char *product; + const char *version; + const char *url; + char **interfaces; +} GetInfoData; + +static void get_info_data_done(GetInfoData *d) { + assert(d); + + d->interfaces = strv_free(d->interfaces); +} + +static int verb_info(int argc, char *argv[], void *userdata) { + _cleanup_(varlink_unrefp) Varlink *vl = NULL; + const char *url; + int r; + + assert(argc == 2); + url = argv[1]; + + r = varlink_connect_auto(&vl, url); + if (r < 0) + return r; + + JsonVariant *reply = NULL; + const char *error = NULL; + r = varlink_call(vl, "org.varlink.service.GetInfo", NULL, &reply, &error, NULL); + if (r < 0) + return log_error_errno(r, "Failed to issue GetInfo() call: %m"); + if (error) + return log_error_errno(SYNTHETIC_ERRNO(EBADE), "Method call GetInfo() failed: %s", error); + + pager_open(arg_pager_flags); + + if (FLAGS_SET(arg_json_format_flags, JSON_FORMAT_OFF)) { + static const struct JsonDispatch dispatch_table[] = { + { "vendor", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(GetInfoData, vendor), JSON_MANDATORY }, + { "product", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(GetInfoData, product), JSON_MANDATORY }, + { "version", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(GetInfoData, version), JSON_MANDATORY }, + { "url", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(GetInfoData, url), JSON_MANDATORY }, + { "interfaces", JSON_VARIANT_ARRAY, json_dispatch_strv, offsetof(GetInfoData, interfaces), JSON_MANDATORY }, + {} + }; + _cleanup_(get_info_data_done) GetInfoData data = {}; + + r = json_dispatch(reply, dispatch_table, NULL, JSON_LOG, &data); + if (r < 0) + return r; + + strv_sort(data.interfaces); + + if (streq_ptr(argv[0], "list-interfaces")) { + STRV_FOREACH(i, data.interfaces) + puts(*i); + } else { + _cleanup_(table_unrefp) Table *t = NULL; + + t = table_new_vertical(); + if (!t) + return log_oom(); + + r = table_add_many( + t, + TABLE_FIELD, "Vendor", + TABLE_STRING, data.vendor, + TABLE_FIELD, "Product", + TABLE_STRING, data.product, + TABLE_FIELD, "Version", + TABLE_STRING, data.version, + TABLE_FIELD, "URL", + TABLE_STRING, data.url, + TABLE_SET_URL, data.url, + TABLE_FIELD, "Interfaces", + TABLE_STRV, data.interfaces); + if (r < 0) + return table_log_add_error(r); + + r = table_print(t, NULL); + if (r < 0) + return table_log_print_error(r); + } + } else { + JsonVariant *v; + + v = streq_ptr(argv[0], "list-interfaces") ? + json_variant_by_key(reply, "interfaces") : reply; + + json_variant_dump(v, arg_json_format_flags, stdout, NULL); + } + + return 0; +} + +typedef struct GetInterfaceDescriptionData { + const char *description; +} GetInterfaceDescriptionData; + +static int verb_introspect(int argc, char *argv[], void *userdata) { + _cleanup_(varlink_unrefp) Varlink *vl = NULL; + const char *url, *interface; + int r; + + assert(argc == 3); + url = argv[1]; + interface = argv[2]; + + r = varlink_connect_auto(&vl, url); + if (r < 0) + return r; + + JsonVariant *reply = NULL; + const char *error = NULL; + r = varlink_callb(vl, "org.varlink.service.GetInterfaceDescription", &reply, &error, NULL, JSON_BUILD_OBJECT(JSON_BUILD_PAIR_STRING("interface", interface))); + if (r < 0) + return log_error_errno(r, "Failed to issue GetInterfaceDescription() call: %m"); + if (error) + return log_error_errno(SYNTHETIC_ERRNO(EBADE), "Method call GetInterfaceDescription() failed: %s", error); + + pager_open(arg_pager_flags); + + if (FLAGS_SET(arg_json_format_flags, JSON_FORMAT_OFF)) { + static const struct JsonDispatch dispatch_table[] = { + { "description", JSON_VARIANT_STRING, json_dispatch_const_string, 0, JSON_MANDATORY }, + {} + }; + _cleanup_(varlink_interface_freep) VarlinkInterface *vi = NULL; + const char *description = NULL; + unsigned line = 0, column = 0; + + r = json_dispatch(reply, dispatch_table, NULL, JSON_LOG, &description); + if (r < 0) + return r; + + /* Try to parse the returned description, so that we can add syntax highlighting */ + r = varlink_idl_parse(ASSERT_PTR(description), &line, &column, &vi); + if (r < 0) { + log_warning_errno(r, "Failed to parse returned interface description at %u:%u, showing raw interface description: %m", line, column); + + fputs(description, stdout); + if (!endswith(description, "\n")) + fputs("\n", stdout); + } else { + r = varlink_idl_dump(stdout, /* use_colors= */ -1, vi); + if (r < 0) + return log_error_errno(r, "Failed to format parsed interface description: %m"); + } + } else + json_variant_dump(reply, arg_json_format_flags, stdout, NULL); + + return 0; +} + +static int reply_callback( + Varlink *link, + JsonVariant *parameters, + const char *error, + VarlinkReplyFlags flags, + void *userdata) { + + int r; + + assert(link); + + if (error) { + /* Propagate the error we received via sd_notify() */ + (void) sd_notifyf(/* unset_environment= */ false, "VARLINKERROR=%s", error); + + r = log_error_errno(SYNTHETIC_ERRNO(EBADE), "Method call failed: %s", error); + } else + r = 0; + + json_variant_dump(parameters, arg_json_format_flags, stdout, NULL); + return r; +} + +static int verb_call(int argc, char *argv[], void *userdata) { + _cleanup_(json_variant_unrefp) JsonVariant *jp = NULL; + _cleanup_(varlink_unrefp) Varlink *vl = NULL; + const char *url, *method, *parameter; + unsigned line = 0, column = 0; + int r; + + assert(argc >= 3); + assert(argc <= 4); + url = argv[1]; + method = argv[2]; + parameter = argc > 3 && !streq(argv[3], "-") ? argv[3] : NULL; + + arg_json_format_flags &= ~JSON_FORMAT_OFF; + + if (parameter) { + r = json_parse_with_source(parameter, "", 0, &jp, &line, &column); + if (r < 0) + return log_error_errno(r, "Failed to parse parameters at :%u:%u: %m", line, column); + } else { + r = json_parse_file_at(stdin, AT_FDCWD, "", 0, &jp, &line, &column); + if (r < 0) + return log_error_errno(r, "Failed to parse parameters at :%u:%u: %m", line, column); + } + + r = varlink_connect_auto(&vl, url); + if (r < 0) + return r; + + if (arg_method_flags & VARLINK_METHOD_ONEWAY) { + r = varlink_send(vl, method, jp); + if (r < 0) + return log_error_errno(r, "Failed to issue %s() call: %m", method); + + r = varlink_flush(vl); + if (r < 0) + return log_error_errno(r, "Failed to flush Varlink connection: %m"); + + } else if (arg_method_flags & VARLINK_METHOD_MORE) { + + varlink_set_userdata(vl, (void*) method); + + r = varlink_bind_reply(vl, reply_callback); + if (r < 0) + return log_error_errno(r, "Failed to bind reply callback: %m"); + + r = varlink_observe(vl, method, jp); + if (r < 0) + return log_error_errno(r, "Failed to issue %s() call: %m", method); + + for (;;) { + r = varlink_is_idle(vl); + if (r < 0) + return log_error_errno(r, "Failed to check if varlink connection is idle: %m"); + if (r > 0) + break; + + r = varlink_process(vl); + if (r < 0) + return log_error_errno(r, "Failed to process varlink connection: %m"); + if (r != 0) + continue; + + r = varlink_wait(vl, USEC_INFINITY); + if (r < 0) + return log_error_errno(r, "Failed to wait for varlink connection events: %m"); + } + } else { + JsonVariant *reply = NULL; + const char *error = NULL; + + r = varlink_call(vl, method, jp, &reply, &error, NULL); + if (r < 0) + return log_error_errno(r, "Failed to issue %s() call: %m", method); + + /* If the server returned an error to us, then fail, but first output the associated parameters */ + if (error) { + /* Propagate the error we received via sd_notify() */ + (void) sd_notifyf(/* unset_environment= */ false, "VARLINKERROR=%s", error); + + r = log_error_errno(SYNTHETIC_ERRNO(EBADE), "Method call %s() failed: %s", method, error); + } else + r = 0; + + pager_open(arg_pager_flags); + + json_variant_dump(reply, arg_json_format_flags, stdout, NULL); + return r; + } + + return 0; +} + +static int verb_validate_idl(int argc, char *argv[], void *userdata) { + _cleanup_(varlink_interface_freep) VarlinkInterface *vi = NULL; + _cleanup_free_ char *text = NULL; + const char *fname; + unsigned line = 1, column = 1; + int r; + + fname = argc > 1 ? argv[1] : NULL; + + if (fname) { + r = read_full_file(fname, &text, NULL); + if (r < 0) + return log_error_errno(r, "Failed to read interface description file '%s': %m", fname); + } else { + r = read_full_stream(stdin, &text, NULL); + if (r < 0) + return log_error_errno(r, "Failed to read interface description from stdin: %m"); + + fname = ""; + } + + r = varlink_idl_parse(text, &line, &column, &vi); + if (r < 0) + return log_error_errno(r, "%s:%u:%u: Failed to parse interface description: %m", fname, line, column); + + r = varlink_idl_consistent(vi, LOG_ERR); + if (r < 0) + return r; + + pager_open(arg_pager_flags); + + r = varlink_idl_dump(stdout, /* use_colors= */ -1, vi); + if (r < 0) + return log_error_errno(r, "Failed to format parsed interface description: %m"); + + return 0; +} + +static int varlinkctl_main(int argc, char *argv[]) { + static const Verb verbs[] = { + { "info", 2, 2, 0, verb_info }, + { "list-interfaces", 2, 2, 0, verb_info }, + { "introspect", 3, 3, 0, verb_introspect }, + { "call", 3, 4, 0, verb_call }, + { "validate-idl", 1, 2, 0, verb_validate_idl }, + { "help", VERB_ANY, VERB_ANY, 0, verb_help }, + {} + }; + + return dispatch_verb(argc, argv, verbs, NULL); +} + +static int run(int argc, char *argv[]) { + int r; + + log_setup(); + + r = parse_argv(argc, argv); + if (r <= 0) + return r; + + return varlinkctl_main(argc, argv); +} + +DEFINE_MAIN_FUNCTION(run); diff --git a/test/units/testsuite-04.journal.sh b/test/units/testsuite-04.journal.sh index 1e105a0566..3efac616e0 100755 --- a/test/units/testsuite-04.journal.sh +++ b/test/units/testsuite-04.journal.sh @@ -10,8 +10,12 @@ for ((i = 0; i < ITERATIONS; i++)); do dd if=/dev/urandom bs=1M count=1 | base64 | systemd-cat done journalctl --rotate +# Let's test varlinkctl a bit, i.e. implement the equivalent of 'journalctl --rotate' via varlinkctl +varlinkctl call /run/systemd/journal/io.systemd.journal io.systemd.Journal.Rotate '{}' journalctl --flush +varlinkctl call /run/systemd/journal/io.systemd.journal io.systemd.Journal.FlushToVar '{}' journalctl --sync +varlinkctl call /run/systemd/journal/io.systemd.journal io.systemd.Journal.Synchronize '{}' journalctl --rotate --vacuum-size=8M # Reset the ratelimit buckets for the subsequent tests below. @@ -42,7 +46,8 @@ write_and_match "<5> \t Leading spaces\n" " \t Leading spaces\n" --level-prefix # --output-fields restricts output ID="$(systemd-id128 new)" echo -ne "foo" | systemd-cat -t "$ID" --level-prefix false -journalctl --sync +# Let's test varlinkctl a bit, i.e. implement the equivalent of 'journalctl --sync' via varlinkctl +varlinkctl call /run/systemd/journal/io.systemd.journal io.systemd.Journal.Synchronize '{}' journalctl -b -o export --output-fields=MESSAGE,FOO --output-fields=PRIORITY,MESSAGE -t "$ID" >/tmp/output [[ $(wc -l /run/systemd/system/systemd-pcrextend.socket.d/50-no-condition.conf <