diff options
Diffstat (limited to 'src/libnmc-base')
-rw-r--r-- | src/libnmc-base/meson.build | 15 | ||||
-rw-r--r-- | src/libnmc-base/nm-client-utils.c | 886 | ||||
-rw-r--r-- | src/libnmc-base/nm-client-utils.h | 52 | ||||
-rw-r--r-- | src/libnmc-base/nm-polkit-listener.c | 910 | ||||
-rw-r--r-- | src/libnmc-base/nm-polkit-listener.h | 33 | ||||
-rw-r--r-- | src/libnmc-base/nm-secret-agent-simple.c | 1402 | ||||
-rw-r--r-- | src/libnmc-base/nm-secret-agent-simple.h | 61 | ||||
-rw-r--r-- | src/libnmc-base/nm-vpn-helpers.c | 821 | ||||
-rw-r--r-- | src/libnmc-base/nm-vpn-helpers.h | 31 | ||||
-rw-r--r-- | src/libnmc-base/qrcodegen.c | 1141 | ||||
-rw-r--r-- | src/libnmc-base/qrcodegen.h | 312 |
11 files changed, 5664 insertions, 0 deletions
diff --git a/src/libnmc-base/meson.build b/src/libnmc-base/meson.build new file mode 100644 index 0000000000..adb71531df --- /dev/null +++ b/src/libnmc-base/meson.build @@ -0,0 +1,15 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +libnmc_base = static_library( + 'nmc-base', + sources: files( + 'nm-client-utils.c', + 'nm-secret-agent-simple.c', + 'nm-vpn-helpers.c', + 'nm-polkit-listener.c', + ), + dependencies: [ + libnm_dep, + glib_dep, + ], +) diff --git a/src/libnmc-base/nm-client-utils.c b/src/libnmc-base/nm-client-utils.c new file mode 100644 index 0000000000..701f8e1834 --- /dev/null +++ b/src/libnmc-base/nm-client-utils.c @@ -0,0 +1,886 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2010 - 2017 Red Hat, Inc. + */ + +#include "libnm-client-aux-extern/nm-default-client.h" + +#include "nm-client-utils.h" + +#include "libnm-glib-aux/nm-secret-utils.h" +#include "libnm-glib-aux/nm-io-utils.h" +#include "nm-utils.h" +#include "nm-device-bond.h" +#include "nm-device-bridge.h" +#include "nm-device-team.h" + +/*****************************************************************************/ + +static int +_nmc_objects_sort_by_path_cmp(gconstpointer pa, gconstpointer pb, gpointer user_data) +{ + NMObject *a = *((NMObject **) pa); + NMObject *b = *((NMObject **) pb); + + NM_CMP_SELF(a, b); + NM_CMP_RETURN(nm_utils_dbus_path_cmp(nm_object_get_path(a), nm_object_get_path(b))); + return 0; +} + +const NMObject ** +nmc_objects_sort_by_path(const NMObject *const *objs, gssize len) +{ + const NMObject **arr; + gsize i, l; + + if (len < 0) + l = NM_PTRARRAY_LEN(objs); + else + l = len; + + arr = g_new(const NMObject *, l + 1); + for (i = 0; i < l; i++) + arr[i] = objs[i]; + arr[l] = NULL; + + if (l > 1) { + g_qsort_with_data(arr, l, sizeof(gpointer), _nmc_objects_sort_by_path_cmp, NULL); + } + return arr; +} + +/*****************************************************************************/ +/* + * Convert string to unsigned integer. + * If required, the resulting number is checked to be in the <min,max> range. + */ +static gboolean +nmc_string_to_uint_base(const char * str, + int base, + gboolean range_check, + unsigned long int min, + unsigned long int max, + unsigned long int *value) +{ + char * end; + unsigned long int tmp; + + if (!str || !str[0]) + return FALSE; + + /* FIXME: don't use this function, replace by _nm_utils_ascii_str_to_int64() */ + errno = 0; + tmp = strtoul(str, &end, base); + if (errno || *end != '\0' || (range_check && (tmp < min || tmp > max))) { + return FALSE; + } + *value = tmp; + return TRUE; +} + +gboolean +nmc_string_to_uint(const char * str, + gboolean range_check, + unsigned long int min, + unsigned long int max, + unsigned long int *value) +{ + return nmc_string_to_uint_base(str, 10, range_check, min, max, value); +} + +gboolean +nmc_string_to_bool(const char *str, gboolean *val_bool, GError **error) +{ + const char *s_true[] = {"true", "yes", "on", "1", NULL}; + const char *s_false[] = {"false", "no", "off", "0", NULL}; + + g_return_val_if_fail(error == NULL || *error == NULL, FALSE); + + if (g_strcmp0(str, "o") == 0) { + g_set_error(error, + 1, + 0, + /* TRANSLATORS: the first %s is the partial value entered by + * the user, the second %s a list of compatible values. + */ + _("'%s' is ambiguous (%s)"), + str, + "on x off"); + return FALSE; + } + + if (nmc_string_is_valid(str, s_true, NULL)) + *val_bool = TRUE; + else if (nmc_string_is_valid(str, s_false, NULL)) + *val_bool = FALSE; + else { + g_set_error(error, + 1, + 0, + _("'%s' is not valid; use [%s] or [%s]"), + str, + "true, yes, on", + "false, no, off"); + return FALSE; + } + return TRUE; +} + +gboolean +nmc_string_to_ternary(const char *str, NMTernary *val, GError **error) +{ + const char *s_true[] = {"true", "yes", "on", NULL}; + const char *s_false[] = {"false", "no", "off", NULL}; + const char *s_unknown[] = {"unknown", NULL}; + + g_return_val_if_fail(error == NULL || *error == NULL, FALSE); + + if (g_strcmp0(str, "o") == 0) { + g_set_error(error, + 1, + 0, + /* TRANSLATORS: the first %s is the partial value entered by + * the user, the second %s a list of compatible values. + */ + _("'%s' is ambiguous (%s)"), + str, + "on x off"); + return FALSE; + } + + if (nmc_string_is_valid(str, s_true, NULL)) + *val = NM_TERNARY_TRUE; + else if (nmc_string_is_valid(str, s_false, NULL)) + *val = NM_TERNARY_FALSE; + else if (nmc_string_is_valid(str, s_unknown, NULL)) + *val = NM_TERNARY_DEFAULT; + else { + g_set_error(error, + 1, + 0, + _("'%s' is not valid; use [%s], [%s] or [%s]"), + str, + "true, yes, on", + "false, no, off", + "unknown"); + return FALSE; + } + return TRUE; +} + +/* + * Check whether 'input' is contained in 'allowed' array. It performs case + * insensitive comparison and supports shortcut strings if they are unique. + * Returns: a pointer to found string in allowed array on success or NULL. + * On failure: error->code : 0 - string not found; 1 - string is ambiguous + */ +const char * +nmc_string_is_valid(const char *input, const char **allowed, GError **error) +{ + const char **p; + size_t input_ln, p_len; + const char * partial_match = NULL; + gboolean ambiguous = FALSE; + + g_return_val_if_fail(!error || !*error, NULL); + + if (!input || !*input) + goto finish; + + input_ln = strlen(input); + for (p = allowed; p && *p; p++) { + p_len = strlen(*p); + if (g_ascii_strncasecmp(input, *p, input_ln) == 0) { + if (input_ln == p_len) + return *p; + if (!partial_match) + partial_match = *p; + else + ambiguous = TRUE; + } + } + + if (ambiguous) { + GString *candidates = g_string_new(""); + + for (p = allowed; *p; p++) { + if (g_ascii_strncasecmp(input, *p, input_ln) == 0) { + if (candidates->len > 0) + g_string_append(candidates, ", "); + g_string_append(candidates, *p); + } + } + g_set_error(error, 1, 1, _("'%s' is ambiguous: %s"), input, candidates->str); + g_string_free(candidates, TRUE); + return NULL; + } +finish: + if (!partial_match) { + char *valid_vals = g_strjoinv(", ", (char **) allowed); + + if (!input || !*input) + g_set_error(error, 1, 0, _("missing name, try one of [%s]"), valid_vals); + else + g_set_error(error, 1, 0, _("'%s' not among [%s]"), input, valid_vals); + + g_free(valid_vals); + } + + return partial_match; +} + +gboolean +matches(const char *cmd, const char *pattern) +{ + size_t len = strlen(cmd); + if (!len || len > strlen(pattern)) + return FALSE; + return memcmp(pattern, cmd, len) == 0; +} + +const char * +nmc_bond_validate_mode(const char *mode, GError **error) +{ + unsigned long mode_int; + static const char *valid_modes[] = {"balance-rr", + "active-backup", + "balance-xor", + "broadcast", + "802.3ad", + "balance-tlb", + "balance-alb", + NULL}; + if (nmc_string_to_uint(mode, TRUE, 0, 6, &mode_int)) { + /* Translate bonding mode numbers to mode names: + * https://www.kernel.org/doc/Documentation/networking/bonding.txt + */ + return valid_modes[mode_int]; + } else + return nmc_string_is_valid(mode, valid_modes, error); +} + +NM_UTILS_LOOKUP_STR_DEFINE( + nmc_device_state_to_string, + NMDeviceState, + NM_UTILS_LOOKUP_DEFAULT(N_("unknown")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_UNMANAGED, N_("unmanaged")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_UNAVAILABLE, N_("unavailable")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_DISCONNECTED, N_("disconnected")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_PREPARE, N_("connecting (prepare)")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_CONFIG, N_("connecting (configuring)")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_NEED_AUTH, N_("connecting (need authentication)")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_IP_CONFIG, N_("connecting (getting IP configuration)")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_IP_CHECK, N_("connecting (checking IP connectivity)")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_SECONDARIES, + N_("connecting (starting secondary connections)")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_ACTIVATED, N_("connected")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_DEACTIVATING, N_("deactivating")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_FAILED, N_("connection failed")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_UNKNOWN, N_("unknown")), ); + +static NM_UTILS_LOOKUP_STR_DEFINE( + _device_state_to_string, + NMDeviceState, + NM_UTILS_LOOKUP_DEFAULT(NULL), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_PREPARE, N_("connecting (externally)")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_CONFIG, N_("connecting (externally)")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_NEED_AUTH, N_("connecting (externally)")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_IP_CONFIG, N_("connecting (externally)")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_IP_CHECK, N_("connecting (externally)")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_SECONDARIES, N_("connecting (externally)")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_ACTIVATED, N_("connected (externally)")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_DEACTIVATING, N_("deactivating (externally)")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_FAILED, N_("deactivating (externally)")), + NM_UTILS_LOOKUP_ITEM_IGNORE_OTHER(), ); + +const char * +nmc_device_state_to_string_with_external(NMDevice *device) +{ + NMActiveConnection *ac; + NMDeviceState state; + const char * s; + + state = nm_device_get_state(device); + + if ((ac = nm_device_get_active_connection(device)) + && NM_FLAGS_HAS(nm_active_connection_get_state_flags(ac), NM_ACTIVATION_STATE_FLAG_EXTERNAL) + && (s = _device_state_to_string(state))) + return s; + + return nmc_device_state_to_string(state); +} + +NM_UTILS_LOOKUP_STR_DEFINE(nmc_device_metered_to_string, + NMMetered, + NM_UTILS_LOOKUP_DEFAULT(N_("unknown")), + NM_UTILS_LOOKUP_ITEM(NM_METERED_YES, N_("yes")), + NM_UTILS_LOOKUP_ITEM(NM_METERED_NO, N_("no")), + NM_UTILS_LOOKUP_ITEM(NM_METERED_GUESS_YES, N_("yes (guessed)")), + NM_UTILS_LOOKUP_ITEM(NM_METERED_GUESS_NO, N_("no (guessed)")), + NM_UTILS_LOOKUP_ITEM(NM_METERED_UNKNOWN, N_("unknown")), ); + +NM_UTILS_LOOKUP_STR_DEFINE( + nmc_device_reason_to_string, + NMDeviceStateReason, + /* TRANSLATORS: Unknown reason for a device state change (NMDeviceStateReason) */ + NM_UTILS_LOOKUP_DEFAULT(N_("Unknown")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_NONE, N_("No reason given")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_UNKNOWN, N_("Unknown error")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_NOW_MANAGED, N_("Device is now managed")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_NOW_UNMANAGED, N_("Device is now unmanaged")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_CONFIG_FAILED, + N_("The device could not be readied for configuration")), + NM_UTILS_LOOKUP_ITEM( + NM_DEVICE_STATE_REASON_IP_CONFIG_UNAVAILABLE, + N_("IP configuration could not be reserved (no available address, timeout, etc.)")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_IP_CONFIG_EXPIRED, + N_("The IP configuration is no longer valid")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_NO_SECRETS, + N_("Secrets were required, but not provided")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT, + N_("802.1X supplicant disconnected")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_SUPPLICANT_CONFIG_FAILED, + N_("802.1X supplicant configuration failed")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED, N_("802.1X supplicant failed")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_SUPPLICANT_TIMEOUT, + N_("802.1X supplicant took too long to authenticate")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_PPP_START_FAILED, + N_("PPP service failed to start")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_PPP_DISCONNECT, N_("PPP service disconnected")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_PPP_FAILED, N_("PPP failed")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_DHCP_START_FAILED, + N_("DHCP client failed to start")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_DHCP_ERROR, N_("DHCP client error")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_DHCP_FAILED, N_("DHCP client failed")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_SHARED_START_FAILED, + N_("Shared connection service failed to start")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_SHARED_FAILED, + N_("Shared connection service failed")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_AUTOIP_START_FAILED, + N_("AutoIP service failed to start")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_AUTOIP_ERROR, N_("AutoIP service error")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_AUTOIP_FAILED, N_("AutoIP service failed")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_MODEM_BUSY, N_("The line is busy")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_MODEM_NO_DIAL_TONE, N_("No dial tone")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_MODEM_NO_CARRIER, + N_("No carrier could be established")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_MODEM_DIAL_TIMEOUT, + N_("The dialing request timed out")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_MODEM_DIAL_FAILED, + N_("The dialing attempt failed")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_MODEM_INIT_FAILED, + N_("Modem initialization failed")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_GSM_APN_FAILED, + N_("Failed to select the specified APN")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_GSM_REGISTRATION_NOT_SEARCHING, + N_("Not searching for networks")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_GSM_REGISTRATION_DENIED, + N_("Network registration denied")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_GSM_REGISTRATION_TIMEOUT, + N_("Network registration timed out")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_GSM_REGISTRATION_FAILED, + N_("Failed to register with the requested network")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_GSM_PIN_CHECK_FAILED, N_("PIN check failed")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_FIRMWARE_MISSING, + N_("Necessary firmware for the device may be missing")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_REMOVED, N_("The device was removed")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_SLEEPING, N_("NetworkManager went to sleep")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_CONNECTION_REMOVED, + N_("The device's active connection disappeared")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_USER_REQUESTED, + N_("Device disconnected by user or client")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_CARRIER, N_("Carrier/link changed")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_CONNECTION_ASSUMED, + N_("The device's existing connection was assumed")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_SUPPLICANT_AVAILABLE, + N_("The supplicant is now available")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_MODEM_NOT_FOUND, + N_("The modem could not be found")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_BT_FAILED, + N_("The Bluetooth connection failed or timed out")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_GSM_SIM_NOT_INSERTED, + N_("GSM Modem's SIM card not inserted")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_GSM_SIM_PIN_REQUIRED, + N_("GSM Modem's SIM PIN required")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_GSM_SIM_PUK_REQUIRED, + N_("GSM Modem's SIM PUK required")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_GSM_SIM_WRONG, N_("GSM Modem's SIM wrong")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_INFINIBAND_MODE, + N_("InfiniBand device does not support connected mode")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_DEPENDENCY_FAILED, + N_("A dependency of the connection failed")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_BR2684_FAILED, + N_("A problem with the RFC 2684 Ethernet over ADSL bridge")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_MODEM_MANAGER_UNAVAILABLE, + N_("ModemManager is unavailable")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_SSID_NOT_FOUND, + N_("The Wi-Fi network could not be found")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_SECONDARY_CONNECTION_FAILED, + N_("A secondary connection of the base connection failed")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_DCB_FCOE_FAILED, N_("DCB or FCoE setup failed")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_TEAMD_CONTROL_FAILED, N_("teamd control failed")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_MODEM_FAILED, + N_("Modem failed or no longer available")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_MODEM_AVAILABLE, + N_("Modem now ready and available")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_SIM_PIN_INCORRECT, N_("SIM PIN was incorrect")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_NEW_ACTIVATION, + N_("New connection activation was enqueued")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_PARENT_CHANGED, N_("The device's parent changed")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_PARENT_MANAGED_CHANGED, + N_("The device parent's management changed")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_OVSDB_FAILED, + N_("Open vSwitch database connection failed")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_IP_ADDRESS_DUPLICATE, + N_("A duplicate IP address was detected")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_IP_METHOD_UNSUPPORTED, + N_("The selected IP method is not supported")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_SRIOV_CONFIGURATION_FAILED, + N_("Failed to configure SR-IOV parameters")), + NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_PEER_NOT_FOUND, + N_("The Wi-Fi P2P peer could not be found")), ); + +NM_UTILS_LOOKUP_STR_DEFINE( + nm_active_connection_state_reason_to_string, + NMActiveConnectionStateReason, + /* TRANSLATORS: Unknown reason for a connection state change (NMActiveConnectionStateReason) */ + NM_UTILS_LOOKUP_DEFAULT(N_("Unknown")), + NM_UTILS_LOOKUP_ITEM(NM_ACTIVE_CONNECTION_STATE_REASON_UNKNOWN, N_("Unknown reason")), + NM_UTILS_LOOKUP_ITEM(NM_ACTIVE_CONNECTION_STATE_REASON_NONE, + N_("The connection was disconnected")), + NM_UTILS_LOOKUP_ITEM(NM_ACTIVE_CONNECTION_STATE_REASON_USER_DISCONNECTED, + N_("Disconnected by user")), + NM_UTILS_LOOKUP_ITEM(NM_ACTIVE_CONNECTION_STATE_REASON_DEVICE_DISCONNECTED, + N_("The base network connection was interrupted")), + NM_UTILS_LOOKUP_ITEM(NM_ACTIVE_CONNECTION_STATE_REASON_SERVICE_STOPPED, + N_("The VPN service stopped unexpectedly")), + NM_UTILS_LOOKUP_ITEM(NM_ACTIVE_CONNECTION_STATE_REASON_IP_CONFIG_INVALID, + N_("The VPN service returned invalid configuration")), + NM_UTILS_LOOKUP_ITEM(NM_ACTIVE_CONNECTION_STATE_REASON_CONNECT_TIMEOUT, + N_("The connection attempt timed out")), + NM_UTILS_LOOKUP_ITEM(NM_ACTIVE_CONNECTION_STATE_REASON_SERVICE_START_TIMEOUT, + N_("The VPN service did not start in time")), + NM_UTILS_LOOKUP_ITEM(NM_ACTIVE_CONNECTION_STATE_REASON_SERVICE_START_FAILED, + N_("The VPN service failed to start")), + NM_UTILS_LOOKUP_ITEM(NM_ACTIVE_CONNECTION_STATE_REASON_NO_SECRETS, N_("No valid secrets")), + NM_UTILS_LOOKUP_ITEM(NM_ACTIVE_CONNECTION_STATE_REASON_LOGIN_FAILED, N_("Invalid secrets")), + NM_UTILS_LOOKUP_ITEM(NM_ACTIVE_CONNECTION_STATE_REASON_CONNECTION_REMOVED, + N_("The connection was removed")), + NM_UTILS_LOOKUP_ITEM(NM_ACTIVE_CONNECTION_STATE_REASON_DEPENDENCY_FAILED, + N_("Master connection failed")), + NM_UTILS_LOOKUP_ITEM(NM_ACTIVE_CONNECTION_STATE_REASON_DEVICE_REALIZE_FAILED, + N_("Could not create a software link")), + NM_UTILS_LOOKUP_ITEM(NM_ACTIVE_CONNECTION_STATE_REASON_DEVICE_REMOVED, + N_("The device disappeared")), ); + +NMActiveConnectionState +nmc_activation_get_effective_state(NMActiveConnection *active, + NMDevice * device, + const char ** reason) +{ + NMActiveConnectionState ac_state; + NMActiveConnectionStateReason ac_reason; + NMDeviceState dev_state = NM_DEVICE_STATE_UNKNOWN; + NMDeviceStateReason dev_reason = NM_DEVICE_STATE_REASON_UNKNOWN; + + g_return_val_if_fail(active, NM_ACTIVE_CONNECTION_STATE_UNKNOWN); + g_return_val_if_fail(reason, NM_ACTIVE_CONNECTION_STATE_UNKNOWN); + + *reason = NULL; + ac_reason = nm_active_connection_get_state_reason(active); + + if (device) { + dev_state = nm_device_get_state(device); + dev_reason = nm_device_get_state_reason(device); + } + + ac_state = nm_active_connection_get_state(active); + switch (ac_state) { + case NM_ACTIVE_CONNECTION_STATE_DEACTIVATED: + if (!device || ac_reason != NM_ACTIVE_CONNECTION_STATE_REASON_DEVICE_DISCONNECTED + || nm_device_get_active_connection(device) != active) { + /* (1) + * - we have no device, + * - or, @ac_reason is specific + * - or, @device no longer references the current @active + * >> we complete with @ac_reason. */ + *reason = gettext(nm_active_connection_state_reason_to_string(ac_reason)); + } else if (dev_state <= NM_DEVICE_STATE_DISCONNECTED + || dev_state >= NM_DEVICE_STATE_FAILED) { + /* (2) + * - not (1) + * - and, the device is no longer in an activated state, + * >> we complete with @dev_reason. */ + *reason = gettext(nmc_device_reason_to_string(dev_reason)); + } else { + /* (3) + * we wait for the device go disconnect. We will get a better + * failure reason from the device (2). */ + return NM_ACTIVE_CONNECTION_STATE_UNKNOWN; + } + break; + case NM_ACTIVE_CONNECTION_STATE_ACTIVATING: + /* activating master connection does not automatically activate any slaves, so their + * active connection state will not progress beyond ACTIVATING state. + * Monitor the device instead. */ + if (device + && (NM_IS_DEVICE_BOND(device) || NM_IS_DEVICE_TEAM(device) + || NM_IS_DEVICE_BRIDGE(device)) + && dev_state >= NM_DEVICE_STATE_IP_CONFIG && dev_state <= NM_DEVICE_STATE_ACTIVATED) { + *reason = "master waiting for slaves"; + return NM_ACTIVE_CONNECTION_STATE_ACTIVATED; + } + break; + default: + break; + } + + return ac_state; +} + +static gboolean +can_show_utf8(void) +{ + static gboolean can_show_utf8_set = FALSE; + static gboolean can_show_utf8 = TRUE; + char * locale_str; + + if (G_LIKELY(can_show_utf8_set)) + return can_show_utf8; + + if (!g_get_charset(NULL)) { + /* Non-UTF-8 locale */ + locale_str = g_locale_from_utf8("\342\226\202\342\226\204\342\226\206\342\226\210", + -1, + NULL, + NULL, + NULL); + if (locale_str) + g_free(locale_str); + else + can_show_utf8 = FALSE; + } + + return can_show_utf8; +} + +static gboolean +can_show_graphics(void) +{ + static gboolean can_show_graphics_set = FALSE; + static gboolean can_show_graphics = TRUE; + + if (G_LIKELY(can_show_graphics_set)) + return can_show_graphics; + + can_show_graphics = can_show_utf8(); + + /* The linux console font typically doesn't have characters we need */ + if (g_strcmp0(g_getenv("TERM"), "linux") == 0) + can_show_graphics = FALSE; + + return can_show_graphics; +} + +/** + * nmc_wifi_strength_bars: + * @strength: the access point strength, from 0 to 100 + * + * Converts @strength into a 4-character-wide graphical representation of + * strength suitable for printing to stdout. If the current locale and terminal + * support it, this will use unicode graphics characters to represent + * "bars". Otherwise, it will use 0 to 4 asterisks. + * + * Returns: the graphical representation of the access point strength + */ +const char * +nmc_wifi_strength_bars(guint8 strength) +{ + if (!can_show_graphics()) + return nm_utils_wifi_strength_bars(strength); + + if (strength > 80) + return /* ▂▄▆█ */ "\342\226\202\342\226\204\342\226\206\342\226\210"; + else if (strength > 55) + return /* â–‚â–„â–†_ */ "\342\226\202\342\226\204\342\226\206_"; + else if (strength > 30) + return /* â–‚â–„__ */ "\342\226\202\342\226\204__"; + else if (strength > 5) + return /* â–‚___ */ "\342\226\202___"; + else + return /* ____ */ "____"; +} + +/** + * nmc_utils_password_subst_char: + * + * Returns: the string substituted when hiding actual password glyphs + */ +const char * +nmc_password_subst_char(void) +{ + if (can_show_graphics()) + return "\u2022"; /* Bullet */ + else + return "*"; +} + +/* + * We actually use a small part of qrcodegen.c, but we'd prefer to keep it + * intact. Include it instead of linking to it to give the compiler a + * chance to optimize bits we don't need away. + */ + +#pragma GCC visibility push(hidden) +NM_PRAGMA_WARNING_DISABLE("-Wdeclaration-after-statement") +#undef NDEBUG +#define NDEBUG +#include "qrcodegen.c" +NM_PRAGMA_WARNING_REENABLE +#pragma GCC visibility pop + +void +nmc_print_qrcode(const char *str) +{ + uint8_t tempBuffer[qrcodegen_BUFFER_LEN_FOR_VERSION(qrcodegen_VERSION_MAX)]; + uint8_t qrcode[qrcodegen_BUFFER_LEN_FOR_VERSION(qrcodegen_VERSION_MAX)]; + gboolean term_linux; + int size; + int x; + int y; + + term_linux = g_strcmp0(g_getenv("TERM"), "linux") == 0; + if (!term_linux && !can_show_graphics()) + return; + + if (!qrcodegen_encodeText(str, + tempBuffer, + qrcode, + qrcodegen_Ecc_LOW, + qrcodegen_VERSION_MIN, + qrcodegen_VERSION_MAX, + qrcodegen_Mask_AUTO, + FALSE)) { + return; + } + + size = qrcodegen_getSize(qrcode); + + g_print("\n"); + + if (term_linux) { + /* G1 alternate character set on Linux console. */ + for (y = -1; y < size + 1; y += 1) { + g_print(" \033[37;40;1m\016"); + for (x = -1; x < size + 1; x++) { + g_print(qrcodegen_getModule(qrcode, x, y) ? " " : "\060\060"); + } + g_print("\017\033[0m\n"); + } + } else { + /* UTF-8 */ + for (y = -2; y < size + 2; y += 2) { + g_print(" \033[37;40m"); + for (x = -2; x < size + 2; x++) { + bool top = qrcodegen_getModule(qrcode, x, y); + bool bottom = qrcodegen_getModule(qrcode, x, y + 1); + if (top) { + g_print(bottom ? " " : "\u2584"); + } else { + g_print(bottom ? "\u2580" : "\u2588"); + } + } + g_print("\033[0m\n"); + } + } +} + +/** + * nmc_utils_read_passwd_file: + * @passwd_file: file with passwords to parse + * @out_error_line: returns in case of a syntax error in the file, the line + * on which it occurred. + * @error: location to store error, or %NULL + * + * Parse passwords given in @passwd_file and insert them into a hash table. + * Example of @passwd_file contents: + * wifi.psk:tajne heslo + * 802-1x.password:krakonos + * 802-11-wireless-security:leap-password:my leap password + * + * Returns: (transfer full): hash table with parsed passwords, or %NULL on an error + */ +GHashTable * +nmc_utils_read_passwd_file(const char *passwd_file, gssize *out_error_line, GError **error) +{ + nm_auto_clear_secret_ptr NMSecretPtr contents = {0}; + + NM_SET_OUT(out_error_line, -1); + + if (!passwd_file) + return g_hash_table_new_full(nm_str_hash, + g_str_equal, + g_free, + (GDestroyNotify) nm_free_secret); + + if (!nm_utils_file_get_contents(-1, + passwd_file, + 1024 * 1024, + NM_UTILS_FILE_GET_CONTENTS_FLAG_SECRET, + &contents.str, + &contents.len, + NULL, + error)) + return NULL; + + return nmc_utils_parse_passwd_file(contents.str, out_error_line, error); +} + +GHashTable * +nmc_utils_parse_passwd_file(char * contents /* will be modified */, + gssize * out_error_line, + GError **error) +{ + gs_unref_hashtable GHashTable *pwds_hash = NULL; + const char * contents_str; + gsize contents_line; + + pwds_hash = + g_hash_table_new_full(nm_str_hash, g_str_equal, g_free, (GDestroyNotify) nm_free_secret); + + NM_SET_OUT(out_error_line, -1); + + contents_str = contents; + contents_line = 0; + while (contents_str[0]) { + nm_auto_free_secret char *l_hash_key = NULL; + nm_auto_free_secret char *l_hash_val = NULL; + const char * l_content_line; + const char * l_setting; + const char * l_prop; + const char * l_val; + const char * s; + gsize l_hash_val_len; + + /* consume first line. As line delimiters we accept "\r\n", "\n", and "\r". */ + l_content_line = contents_str; + s = l_content_line; + while (!NM_IN_SET(s[0], '\0', '\r', '\n')) + s++; + if (s[0] != '\0') { + if (s[0] == '\r' && s[1] == '\n') { + ((char *) s)[0] = '\0'; + s += 2; + } else { + ((char *) s)[0] = '\0'; + s += 1; + } + } + contents_str = s; + contents_line++; + + l_content_line = nm_str_skip_leading_spaces(l_content_line); + if (NM_IN_SET(l_content_line[0], '\0', '#')) { + /* a comment or empty line. Ignore. */ + continue; + } + + l_setting = l_content_line; + + s = l_setting; + while (!NM_IN_SET(s[0], '\0', ':', '=')) + s++; + if (s[0] == '\0') { + NM_SET_OUT(out_error_line, contents_line); + nm_utils_error_set(error, + NM_UTILS_ERROR_UNKNOWN, + _("missing colon for \"<setting>.<property>:<secret>\" format")); + return NULL; + } + ((char *) s)[0] = '\0'; + s++; + + l_val = s; + + g_strchomp((char *) l_setting); + + nm_assert(nm_str_is_stripped(l_setting)); + + s = strchr(l_setting, '.'); + if (!s) { + NM_SET_OUT(out_error_line, contents_line); + nm_utils_error_set(error, + NM_UTILS_ERROR_UNKNOWN, + _("missing dot for \"<setting>.<property>:<secret>\" format")); + return NULL; + } else if (s == l_setting) { + NM_SET_OUT(out_error_line, contents_line); + nm_utils_error_set(error, + NM_UTILS_ERROR_UNKNOWN, + _("missing setting for \"<setting>.<property>:<secret>\" format")); + return NULL; + } + ((char *) s)[0] = '\0'; + s++; + + l_prop = s; + if (l_prop[0] == '\0') { + NM_SET_OUT(out_error_line, contents_line); + nm_utils_error_set(error, + NM_UTILS_ERROR_UNKNOWN, + _("missing property for \"<setting>.<property>:<secret>\" format")); + return NULL; + } + + /* Accept wifi-sec or wifi instead of cumbersome '802-11-wireless-security' */ + if (NM_IN_STRSET(l_setting, "wifi-sec", "wifi")) + l_setting = NM_SETTING_WIRELESS_SECURITY_SETTING_NAME; + + if (nm_setting_lookup_type(l_setting) == G_TYPE_INVALID) { + NM_SET_OUT(out_error_line, contents_line); + nm_utils_error_set(error, NM_UTILS_ERROR_UNKNOWN, _("invalid setting name")); + return NULL; + } + + if (nm_streq(l_setting, "vpn") && NM_STR_HAS_PREFIX(l_prop, "secret.")) { + /* in 1.12.0, we wrongly required the VPN secrets to be named + * "vpn.secret". It should be "vpn.secrets". Work around it + * (rh#1628833). */ + l_hash_key = g_strdup_printf("vpn.secrets.%s", &l_prop[NM_STRLEN("secret.")]); + } else + l_hash_key = g_strdup_printf("%s.%s", l_setting, l_prop); + + if (!g_utf8_validate(l_hash_key, -1, NULL)) { + NM_SET_OUT(out_error_line, contents_line); + nm_utils_error_set(error, NM_UTILS_ERROR_UNKNOWN, _("property name is not UTF-8")); + return NULL; + } + + /* Support backslash escaping in the secret value. We strip non-escaped leading/trailing whitespaces. */ + s = nm_utils_buf_utf8safe_unescape(l_val, + NM_UTILS_STR_UTF8_SAFE_UNESCAPE_STRIP_SPACES, + &l_hash_val_len, + (gpointer *) &l_hash_val); + if (!l_hash_val) + l_hash_val = g_strdup(s); + + if (!g_utf8_validate(l_hash_val, -1, NULL)) { + /* In some cases it might make sense to support binary secrets (like the WPA-PSK which has no + * defined encoding. However, all API that follows can only handle UTF-8, and no mechanism + * to escape the secrets. Reject non-UTF-8 early. */ + NM_SET_OUT(out_error_line, contents_line); + nm_utils_error_set(error, NM_UTILS_ERROR_UNKNOWN, _("secret is not UTF-8")); + return NULL; + } + + if (strlen(l_hash_val) != l_hash_val_len) { + NM_SET_OUT(out_error_line, contents_line); + nm_utils_error_set(error, NM_UTILS_ERROR_UNKNOWN, _("secret is not UTF-8")); + return NULL; + } + + g_hash_table_insert(pwds_hash, g_steal_pointer(&l_hash_key), g_steal_pointer(&l_hash_val)); + } + + return g_steal_pointer(&pwds_hash); +} diff --git a/src/libnmc-base/nm-client-utils.h b/src/libnmc-base/nm-client-utils.h new file mode 100644 index 0000000000..7017e39a75 --- /dev/null +++ b/src/libnmc-base/nm-client-utils.h @@ -0,0 +1,52 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2010 - 2017 Red Hat, Inc. + */ + +#ifndef __NM_CLIENT_UTILS_H__ +#define __NM_CLIENT_UTILS_H__ + +#include "nm-active-connection.h" +#include "nm-device.h" +#include "libnm-core-aux-intern/nm-libnm-core-utils.h" + +const NMObject **nmc_objects_sort_by_path(const NMObject *const *objs, gssize len); + +const char *nmc_string_is_valid(const char *input, const char **allowed, GError **error); + +gboolean nmc_string_to_uint(const char * str, + gboolean range_check, + unsigned long int min, + unsigned long int max, + unsigned long int *value); +gboolean nmc_string_to_bool(const char *str, gboolean *val_bool, GError **error); +gboolean nmc_string_to_ternary(const char *str, NMTernary *val, GError **error); + +gboolean matches(const char *cmd, const char *pattern); + +/* FIXME: don't expose this function on its own, at least not from this file. */ +const char *nmc_bond_validate_mode(const char *mode, GError **error); + +const char *nmc_device_state_to_string_with_external(NMDevice *device); + +const char *nm_active_connection_state_reason_to_string(NMActiveConnectionStateReason reason); +const char *nmc_device_state_to_string(NMDeviceState state); +const char *nmc_device_reason_to_string(NMDeviceStateReason reason); +const char *nmc_device_metered_to_string(NMMetered value); + +NMActiveConnectionState nmc_activation_get_effective_state(NMActiveConnection *active, + NMDevice * device, + const char ** reason); + +const char *nmc_wifi_strength_bars(guint8 strength); + +const char *nmc_password_subst_char(void); + +void nmc_print_qrcode(const char *str); + +GHashTable *nmc_utils_parse_passwd_file(char *contents, gssize *out_error_line, GError **error); + +GHashTable * +nmc_utils_read_passwd_file(const char *passwd_file, gssize *out_error_line, GError **error); + +#endif /* __NM_CLIENT_UTILS_H__ */ diff --git a/src/libnmc-base/nm-polkit-listener.c b/src/libnmc-base/nm-polkit-listener.c new file mode 100644 index 0000000000..29c25b4e67 --- /dev/null +++ b/src/libnmc-base/nm-polkit-listener.c @@ -0,0 +1,910 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2014 Red Hat, Inc. + */ + +/** + * SECTION:nm-polkit-listener + * @short_description: A polkit agent listener + * + * #NMPolkitListener is the polkit agent listener used by nmcli and nmtui. + * http://www.freedesktop.org/software/polkit/docs/latest/index.html + * + * For an example polkit agent you can look at polkit source tree: + * http://cgit.freedesktop.org/polkit/tree/src/polkitagent/polkitagenttextlistener.c + * http://cgit.freedesktop.org/polkit/tree/src/programs/pkttyagent.c + * or LXDE polkit agent: + * http://git.lxde.org/gitweb/?p=debian/lxpolkit.git;a=blob;f=src/lxpolkit-listener.c + * https://github.com/lxde/lxqt-policykit/tree/master/src + */ + +#include "libnm-client-aux-extern/nm-default-client.h" + +#include "nm-polkit-listener.h" + +#include <gio/gio.h> +#include <pwd.h> +#include <fcntl.h> + +#include "libnm-glib-aux/nm-dbus-aux.h" +#include "libnm-glib-aux/nm-str-buf.h" +#include "libnm-glib-aux/nm-secret-utils.h" +#include "libnm-glib-aux/nm-io-utils.h" +#include "libnm-core-aux-intern/nm-auth-subject.h" +#include "c-list/src/c-list.h" + +#define LOGIND_BUS_NAME "org.freedesktop.login1" +#define POLKIT_BUS_NAME "org.freedesktop.PolicyKit1" + +#define POLKIT_AUTHORITY_OBJ_PATH "/org/freedesktop/PolicyKit1/Authority" +#define POLKIT_AUTHORITY_IFACE_NAME "org.freedesktop.PolicyKit1.Authority" + +#define POLKIT_AGENT_OBJ_PATH "/org/freedesktop/PolicyKit1/AuthenticationAgent" +#define POLKIT_AGENT_DBUS_INTERFACE "org.freedesktop.PolicyKit1.AuthenticationAgent" + +#define LOGIND_OBJ_PATH "/org/freedesktop/login1" +#define LOGIND_MANAGER_INTERFACE "org.freedesktop.login1.Manager" + +#define NM_POLKIT_LISTENER_DBUS_CONNECTION "dbus-connection" +#define NM_POLKIT_LISTENER_SESSION_AGENT "session-agent" + +#define POLKIT_DBUS_ERROR_FAILED "org.freedesktop.PolicyKit1.Error.Failed" + +/*****************************************************************************/ + +enum { REGISTERED, REQUEST_SYNC, ERROR, LAST_SIGNAL }; + +static guint signals[LAST_SIGNAL] = {0}; + +struct _NMPolkitListener { + GObject parent; + GDBusConnection *dbus_connection; + char * name_owner; + GCancellable * cancellable; + GMainContext * main_context; + CList request_lst_head; + guint pk_auth_agent_reg_id; + guint name_owner_changed_id; + bool session_agent : 1; +}; + +struct _NMPolkitListenerClass { + GObjectClass parent; +}; + +G_DEFINE_TYPE(NMPolkitListener, nm_polkit_listener, G_TYPE_OBJECT); + +/*****************************************************************************/ + +typedef struct { + CList request_lst; + + NMPolkitListener * listener; + GSource * child_stdout_watch_source; + GSource * child_stdin_watch_source; + GDBusMethodInvocation *dbus_invocation; + char * action_id; + char * message; + char * username; + char * cookie; + NMStrBuf in_buffer; + NMStrBuf out_buffer; + gsize out_buffer_offset; + + int child_stdout; + int child_stdin; + + bool request_any_response : 1; + bool request_is_completed : 1; +} AuthRequest; + +static const GDBusInterfaceInfo interface_info = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT( + POLKIT_AGENT_DBUS_INTERFACE, + .methods = NM_DEFINE_GDBUS_METHOD_INFOS( + NM_DEFINE_GDBUS_METHOD_INFO("BeginAuthentication", + .in_args = NM_DEFINE_GDBUS_ARG_INFOS( + NM_DEFINE_GDBUS_ARG_INFO("action_id", "s"), + NM_DEFINE_GDBUS_ARG_INFO("message", "s"), + NM_DEFINE_GDBUS_ARG_INFO("icon_name", "s"), + NM_DEFINE_GDBUS_ARG_INFO("details", "a{ss}"), + NM_DEFINE_GDBUS_ARG_INFO("cookie", "s"), + NM_DEFINE_GDBUS_ARG_INFO("identities", "a(sa{sv})"), ), ), + NM_DEFINE_GDBUS_METHOD_INFO("CancelAuthentication", + .in_args = NM_DEFINE_GDBUS_ARG_INFOS( + NM_DEFINE_GDBUS_ARG_INFO("cookie", "s"), ), ), ), ); + +static void +auth_request_complete(AuthRequest *request, gboolean success) +{ + c_list_unlink(&request->request_lst); + + if (success) + g_dbus_method_invocation_return_value(request->dbus_invocation, NULL); + else { + g_dbus_method_invocation_return_dbus_error(request->dbus_invocation, + POLKIT_DBUS_ERROR_FAILED, + ""); + } + + nm_clear_g_free(&request->action_id); + nm_clear_g_free(&request->message); + nm_clear_g_free(&request->username); + nm_clear_g_free(&request->cookie); + nm_clear_g_source_inst(&request->child_stdout_watch_source); + nm_clear_g_source_inst(&request->child_stdin_watch_source); + + nm_str_buf_destroy(&request->in_buffer); + nm_str_buf_destroy(&request->out_buffer); + + if (request->child_stdout != -1) { + nm_close(request->child_stdout); + request->child_stdout = -1; + } + + if (request->child_stdin != -1) { + nm_close(request->child_stdin); + request->child_stdin = -1; + } + + nm_g_slice_free(request); +} + +static gboolean +uid_to_name(uid_t uid, gboolean *out_cached, char **out_name) +{ + if (!*out_cached) { + *out_cached = TRUE; + *out_name = nm_utils_uid_to_name(uid); + } + return !!(*out_name); +} + +static char * +choose_identity(GVariant *identities) +{ + GVariantIter identity_iter; + GVariant * details_tmp; + const char * kind; + gs_free char *username_first = NULL; + gs_free char *username_root = NULL; + const char * user; + + /* Choose identity. First try current user, then root, and else + * take the first one we find. */ + + user = getenv("USER"); + + g_variant_iter_init(&identity_iter, identities); + while (g_variant_iter_next(&identity_iter, "(&s@a{sv})", &kind, &details_tmp)) { + gs_unref_variant GVariant *details = g_steal_pointer(&details_tmp); + + if (nm_streq(kind, "unix-user")) { + gs_unref_variant GVariant *v = NULL; + + v = g_variant_lookup_value(details, "uid", G_VARIANT_TYPE_UINT32); + if (v) { + guint32 uid = g_variant_get_uint32(v); + gs_free char *u = NULL; + gboolean cached = FALSE; + + if (user) { + if (!uid_to_name(uid, &cached, &u)) + continue; + if (nm_streq(u, user)) + return g_steal_pointer(&u); + } + if (!username_root && uid == 0) { + if (!uid_to_name(uid, &cached, &u)) + continue; + username_root = g_strdup(u); + if (!user) + break; + } + if (!username_root && !username_first) { + if (!uid_to_name(uid, &cached, &u)) + continue; + username_first = g_strdup(u); + } + } + } + } + + if (username_root) + return g_steal_pointer(&username_root); + + if (username_first) + return g_steal_pointer(&username_first); + + return NULL; +} + +static void +agent_register_cb(GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + NMPolkitListener *listener = NM_POLKIT_LISTENER(user_data); + GDBusConnection * dbus_connection = G_DBUS_CONNECTION(source_object); + gs_free_error GError *error = NULL; + gs_unref_variant GVariant *ret = NULL; + + ret = g_dbus_connection_call_finish(dbus_connection, res, &error); + + if (nm_utils_error_is_cancelled(error)) { + return; + } + + if (ret) { + g_signal_emit(listener, signals[REGISTERED], 0); + } else { + g_signal_emit(listener, signals[ERROR], 0, error->message); + } +} + +static void +agent_register(NMPolkitListener *self, const char *session_id) +{ + const char * locale = NULL; + gs_unref_object NMAuthSubject *subject = NULL; + GVariant * subject_variant = NULL; + + locale = g_getenv("LANG"); + if (locale == NULL) { + locale = "en_US.UTF-8"; + } + + if (self->session_agent) { + subject = nm_auth_subject_new_unix_session(session_id); + } else { + subject = nm_auth_subject_new_unix_process_self(); + } + subject_variant = nm_auth_subject_unix_to_polkit_gvariant(subject); + + g_dbus_connection_call( + self->dbus_connection, + self->name_owner, + POLKIT_AUTHORITY_OBJ_PATH, + POLKIT_AUTHORITY_IFACE_NAME, + "RegisterAuthenticationAgent", + g_variant_new("(@(sa{sv})ss)", subject_variant, locale, POLKIT_AGENT_OBJ_PATH), + NULL, + G_DBUS_CALL_FLAGS_NONE, + -1, + self->cancellable, + agent_register_cb, + self); +} + +static void +agent_unregister(NMPolkitListener *self) +{ + gs_unref_object NMAuthSubject *subject = NULL; + GVariant * subject_variant = NULL; + + subject = nm_auth_subject_new_unix_process_self(); + subject_variant = nm_auth_subject_unix_to_polkit_gvariant(subject); + + g_dbus_connection_call(self->dbus_connection, + self->name_owner, + POLKIT_AUTHORITY_OBJ_PATH, + POLKIT_AUTHORITY_IFACE_NAME, + "UnregisterAuthenticationAgent", + g_variant_new("(@(sa{sv})s)", subject_variant, POLKIT_AGENT_OBJ_PATH), + NULL, + G_DBUS_CALL_FLAGS_NONE, + -1, + NULL, + NULL, + self); +} + +static void +retrieve_session_id_cb(GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + NMPolkitListener * listener = NM_POLKIT_LISTENER(user_data); + char * session_id; + guint32 session_uid; + nm_auto_free_variant_iter GVariantIter *iter = NULL; + gs_unref_variant GVariant *ret = NULL; + gs_free_error GError *error = NULL; + gs_free char * err_str = NULL; + uid_t uid = getuid(); + + ret = g_dbus_connection_call_finish(listener->dbus_connection, res, &error); + + if (nm_utils_error_is_cancelled(error)) { + return; + } + + if (ret) { + g_variant_get_child(ret, 0, "a(susso)", &iter); + + while ( + g_variant_iter_next(iter, "(&su@s@s@o)", &session_id, &session_uid, NULL, NULL, NULL)) { + if (session_uid == uid) { + agent_register(listener, session_id); + return; + } + } + err_str = g_strdup_printf(_("Could not find any session id for uid %d"), uid); + } else { + err_str = g_strdup_printf(_("Could not retrieve session id: %s"), error->message); + } + + g_signal_emit(listener, signals[ERROR], 0, err_str); +} + +static void +retrieve_session_id(NMPolkitListener *self) +{ + g_dbus_connection_call(self->dbus_connection, + LOGIND_BUS_NAME, + LOGIND_OBJ_PATH, + LOGIND_MANAGER_INTERFACE, + "ListSessions", + NULL, + G_VARIANT_TYPE("(a(susso))"), + G_DBUS_CALL_FLAGS_NONE, + -1, + self->cancellable, + retrieve_session_id_cb, + self); +} + +static gboolean +io_watch_can_write(int fd, GIOCondition condition, gpointer user_data) +{ + AuthRequest *request = user_data; + gssize n_written; + + if (NM_FLAGS_ANY(condition, (G_IO_HUP | G_IO_ERR))) + goto done; + + n_written = + write(request->child_stdin, + &((nm_str_buf_get_str_unsafe(&request->out_buffer))[request->out_buffer_offset]), + request->out_buffer.len - request->out_buffer_offset); + + if (n_written < 0 && errno != EAGAIN) + goto done; + + if (n_written > 0) { + if ((gsize) n_written >= (request->out_buffer.len - request->out_buffer_offset)) { + nm_assert((gsize) n_written == (request->out_buffer.len - request->out_buffer_offset)); + goto done; + } + request->out_buffer_offset += (gsize) n_written; + } + + return G_SOURCE_CONTINUE; + +done: + nm_str_buf_set_size(&request->out_buffer, 0, TRUE, FALSE); + request->out_buffer_offset = 0; + nm_clear_g_source_inst(&request->child_stdin_watch_source); + + if (request->request_is_completed) + auth_request_complete(request, TRUE); + + return G_SOURCE_CONTINUE; +} + +static void +queue_string_to_helper(AuthRequest *request, const char *response) +{ + g_return_if_fail(response); + + if (!nm_str_buf_is_initalized(&request->out_buffer)) + nm_str_buf_init(&request->out_buffer, strlen(response) + 2u, TRUE); + + nm_str_buf_append(&request->out_buffer, response); + nm_str_buf_ensure_trailing_c(&request->out_buffer, '\n'); + + if (!request->child_stdin_watch_source) { + request->child_stdin_watch_source = nm_g_unix_fd_source_new(request->child_stdin, + G_IO_OUT | G_IO_ERR | G_IO_HUP, + G_PRIORITY_DEFAULT, + io_watch_can_write, + request, + NULL); + g_source_attach(request->child_stdin_watch_source, request->listener->main_context); + } +} + +static gboolean +io_watch_have_data(int fd, GIOCondition condition, gpointer user_data) +{ + AuthRequest *request = user_data; + gboolean auth_result; + gssize n_read; + + if (NM_FLAGS_ANY(condition, G_IO_HUP | G_IO_ERR)) + n_read = -EIO; + else + n_read = nm_utils_fd_read(fd, &request->in_buffer); + + if (n_read <= 0) { + if (n_read == -EAGAIN) { + /* wait longer. */ + return G_SOURCE_CONTINUE; + } + + /* Either an error or EOF happened. The data we parsed so far was not relevant. + * Regardless of what we still have unprocessed in the receive buffers, we are done. + * + * We would expect that the other side completed with SUCCESS or FAILURE. Apparently + * it didn't. If we had any good request, we assume success. */ + auth_result = request->request_any_response; + goto out; + } + + while (TRUE) { + char * line_terminator; + const char *line; + + line = nm_str_buf_get_str(&request->in_buffer); + line_terminator = (char *) strchr(line, '\n'); + if (!line_terminator) { + /* We don't have a complete line. Wait longer. */ + return G_SOURCE_CONTINUE; + } + line_terminator[0] = '\0'; + + if (NM_STR_HAS_PREFIX(line, "PAM_PROMPT_ECHO_OFF ") + || NM_STR_HAS_PREFIX(line, "PAM_PROMPT_ECHO_ON ")) { + nm_auto_free_secret char *response = NULL; + + /* FIXME(cli-async): emit signal and wait for response (blocking) */ + g_signal_emit(request->listener, + signals[REQUEST_SYNC], + 0, + request->action_id, + request->message, + request->username, + &response); + + if (response) { + queue_string_to_helper(request, response); + request->request_any_response = TRUE; + goto erase_line; + } + auth_result = FALSE; + } else if (nm_streq(line, "SUCCESS")) + auth_result = TRUE; + else if (nm_streq(line, "FAILURE")) + auth_result = FALSE; + else if (NM_STR_HAS_PREFIX(line, "PAM_ERROR_MSG ") + || NM_STR_HAS_PREFIX(line, "PAM_TEXT_INFO ")) { + /* ignore. */ + goto erase_line; + } else { + /* unknown command. Fail. */ + auth_result = FALSE; + } + + break; +erase_line: + nm_str_buf_erase(&request->in_buffer, 0, line_terminator - line + 1u, TRUE); + } + +out: + request->request_is_completed = TRUE; + nm_clear_g_source_inst(&request->child_stdout_watch_source); + if (auth_result && request->child_stdin_watch_source) { + /* we need to wait for the buffer to send the response. */ + } else + auth_request_complete(request, auth_result); + + return G_SOURCE_CONTINUE; +} + +static void +begin_authentication(AuthRequest *request) +{ + int fd_flags; + const char *helper_argv[] = { + POLKIT_AGENT_HELPER_1_PATH, + request->username, + NULL, + }; + + if (!request->username) { + auth_request_complete(request, FALSE); + return; + } + + if (!g_spawn_async_with_pipes(NULL, + (char **) helper_argv, + NULL, + G_SPAWN_STDERR_TO_DEV_NULL, + NULL, + NULL, + NULL, + &request->child_stdin, + &request->child_stdout, + NULL, + NULL)) { + /* not findind the PolicyKit setuid helper is a critical error */ + request->child_stdin = -1; + request->child_stdout = -1; + g_signal_emit(request->listener, + signals[ERROR], + 0, + "The PolicyKit setuid helper 'polkit-agent-helper-1' has not been found"); + + auth_request_complete(request, FALSE); + return; + } + + fd_flags = fcntl(request->child_stdin, F_GETFD, 0); + fcntl(request->child_stdin, F_SETFL, fd_flags | O_NONBLOCK); + + fd_flags = fcntl(request->child_stdout, F_GETFD, 0); + fcntl(request->child_stdout, F_SETFL, fd_flags | O_NONBLOCK); + + request->child_stdout_watch_source = nm_g_unix_fd_source_new(request->child_stdout, + G_IO_IN | G_IO_ERR | G_IO_HUP, + G_PRIORITY_DEFAULT, + io_watch_have_data, + request, + NULL); + g_source_attach(request->child_stdout_watch_source, request->listener->main_context); + + /* Write the cookie on stdin so it can't be seen by other processes */ + queue_string_to_helper(request, request->cookie); + + return; +} + +static AuthRequest * +get_request(NMPolkitListener *listener, const char *cookie) +{ + AuthRequest *request; + + c_list_for_each_entry (request, &listener->request_lst_head, request_lst) { + if (nm_streq0(cookie, request->cookie)) { + return request; + } + } + return NULL; +} + +static AuthRequest * +create_request(NMPolkitListener * listener, + GDBusMethodInvocation *invocation, + const char * action_id, + const char * message, + char * username_take, + const char * cookie) +{ + AuthRequest *request; + + request = g_slice_new(AuthRequest); + *request = (AuthRequest){ + .listener = listener, + .dbus_invocation = invocation, + .action_id = g_strdup(action_id), + .message = g_strdup(message), + .username = g_steal_pointer(&username_take), + .cookie = g_strdup(cookie), + .request_any_response = FALSE, + .request_is_completed = FALSE, + }; + + nm_str_buf_init(&request->in_buffer, NM_UTILS_GET_NEXT_REALLOC_SIZE_1000, FALSE); + + c_list_link_tail(&listener->request_lst_head, &request->request_lst); + return request; +} + +static void +dbus_method_call_cb(GDBusConnection * connection, + const char * sender, + const char * object_path, + const char * interface_name, + const char * method_name, + GVariant * parameters, + GDBusMethodInvocation *invocation, + gpointer user_data) +{ + NMPolkitListener *listener = NM_POLKIT_LISTENER(user_data); + const char * action_id; + const char * message; + const char * cookie; + AuthRequest * request; + gs_unref_variant GVariant *identities_gvariant = NULL; + + if (nm_streq(method_name, "BeginAuthentication")) { + g_variant_get(parameters, + "(&s&s&s@a{ss}&s@a(sa{sv}))", + &action_id, + &message, + NULL, + NULL, + &cookie, + &identities_gvariant); + + request = create_request(listener, + invocation, + action_id, + message, + choose_identity(identities_gvariant), + cookie); + begin_authentication(request); + return; + } + + if (nm_streq(method_name, "CancelAuthentication")) { + g_variant_get(parameters, "&s", &cookie); + request = get_request(listener, cookie); + + if (!request) { + gs_free char *msg = NULL; + + msg = g_strdup_printf("No pending authentication request for cookie '%s'", cookie); + g_dbus_method_invocation_return_dbus_error(invocation, POLKIT_DBUS_ERROR_FAILED, msg); + return; + } + + /* Complete a cancelled request with success. */ + auth_request_complete(request, TRUE); + return; + } + + g_dbus_method_invocation_return_error(invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_UNKNOWN_METHOD, + "Unknown method %s", + method_name); +} + +static gboolean +export_dbus_iface(NMPolkitListener *self, GError **error) +{ + GDBusInterfaceVTable interface_vtable = { + .method_call = dbus_method_call_cb, + .set_property = NULL, + .get_property = NULL, + }; + + g_return_val_if_fail(NM_IS_POLKIT_LISTENER(self), FALSE); + g_return_val_if_fail(error == NULL || *error == NULL, FALSE); + + /* Agent listener iface has been exported already */ + if (self->pk_auth_agent_reg_id) { + return TRUE; + } + + self->pk_auth_agent_reg_id = + g_dbus_connection_register_object(self->dbus_connection, + POLKIT_AGENT_OBJ_PATH, + (GDBusInterfaceInfo *) &interface_info, + &interface_vtable, + self, + NULL, + error); + if (!self->pk_auth_agent_reg_id) { + g_signal_emit(self, + signals[ERROR], + 0, + "Could not register as a PolicyKit Authentication Agent"); + } + return self->pk_auth_agent_reg_id; +} + +static void +name_owner_changed(NMPolkitListener *self, const char *name_owner) +{ + gs_free_error GError *error = NULL; + + name_owner = nm_str_not_empty(name_owner); + + if (nm_streq0(self->name_owner, name_owner)) { + return; + } + + g_free(self->name_owner); + self->name_owner = g_strdup(name_owner); + + if (!self->name_owner) { + return; + } + + if (export_dbus_iface(self, &error)) { + if (self->session_agent) { + retrieve_session_id(self); + } else { + agent_register(self, NULL); + } + } else { + g_signal_emit(self, + signals[ERROR], + 0, + "Could not export the PolicyKit Authentication Agent DBus interface"); + } +} + +static void +name_owner_changed_cb(GDBusConnection *connection, + const char * sender_name, + const char * object_path, + const char * interface_name, + const char * signal_name, + GVariant * parameters, + gpointer user_data) +{ + NMPolkitListener *self = user_data; + const char * new_owner; + + if (!g_variant_is_of_type(parameters, G_VARIANT_TYPE("(sss)"))) { + return; + } + + g_variant_get(parameters, "(&s&s&s)", NULL, NULL, &new_owner); + + name_owner_changed(self, new_owner); +} + +static void +get_name_owner_cb(const char *name_owner, GError *error, gpointer user_data) +{ + if (!name_owner && g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) { + return; + } + name_owner_changed(user_data, name_owner); +} + +/*****************************************************************************/ + +NM_GOBJECT_PROPERTIES_DEFINE(NMPolkitListener, PROP_DBUS_CONNECTION, PROP_SESSION_AGENT, ); + +static void +set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) +{ + NMPolkitListener *self = NM_POLKIT_LISTENER(object); + + switch (prop_id) { + case PROP_DBUS_CONNECTION: + self->dbus_connection = g_value_dup_object(value); + break; + case PROP_SESSION_AGENT: + self->session_agent = g_value_get_boolean(value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +static void +nm_polkit_listener_init(NMPolkitListener *self) +{ + c_list_init(&self->request_lst_head); + self->main_context = g_main_context_ref_thread_default(); +} + +static void +constructed(GObject *object) +{ + NMPolkitListener *self = NM_POLKIT_LISTENER(object); + + self->cancellable = g_cancellable_new(); + + self->name_owner_changed_id = + nm_dbus_connection_signal_subscribe_name_owner_changed(self->dbus_connection, + POLKIT_BUS_NAME, + name_owner_changed_cb, + self, + NULL); + + nm_dbus_connection_call_get_name_owner(self->dbus_connection, + POLKIT_BUS_NAME, + -1, + self->cancellable, + get_name_owner_cb, + self); + + G_OBJECT_CLASS(nm_polkit_listener_parent_class)->constructed(object); +} + +/** + * nm_polkit_listener_new: + * @dbus_connection: a open DBus connection + * @session_agent: TRUE if a session agent is wanted, FALSE for a process agent + * + * Creates a new #NMPolkitListener and registers it as a polkit agent. + * + * Returns: a new #NMPolkitListener + */ +NMPolkitListener * +nm_polkit_listener_new(GDBusConnection *dbus_connection, gboolean session_agent) +{ + return g_object_new(NM_TYPE_POLKIT_LISTENER, + NM_POLKIT_LISTENER_DBUS_CONNECTION, + dbus_connection, + NM_POLKIT_LISTENER_SESSION_AGENT, + session_agent, + NULL); +} + +static void +dispose(GObject *object) +{ + NMPolkitListener *self = NM_POLKIT_LISTENER(object); + AuthRequest * request; + + nm_clear_g_cancellable(&self->cancellable); + + while ((request = c_list_first_entry(&self->request_lst_head, AuthRequest, request_lst))) + auth_request_complete(request, FALSE); + + if (self->dbus_connection) { + nm_clear_g_dbus_connection_signal(self->dbus_connection, &self->name_owner_changed_id); + g_dbus_connection_unregister_object(self->dbus_connection, self->pk_auth_agent_reg_id); + agent_unregister(self); + nm_clear_g_free(&self->name_owner); + g_clear_object(&self->dbus_connection); + } + + nm_clear_pointer(&self->main_context, g_main_context_unref); + + G_OBJECT_CLASS(nm_polkit_listener_parent_class)->dispose(object); +} + +static void +nm_polkit_listener_class_init(NMPolkitListenerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS(klass); + + object_class->set_property = set_property; + object_class->constructed = constructed; + object_class->dispose = dispose; + + obj_properties[PROP_DBUS_CONNECTION] = + g_param_spec_object(NM_POLKIT_LISTENER_DBUS_CONNECTION, + "", + "", + G_TYPE_DBUS_CONNECTION, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_SESSION_AGENT] = + g_param_spec_boolean(NM_POLKIT_LISTENER_SESSION_AGENT, + "", + "", + FALSE, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties); + + signals[REQUEST_SYNC] = g_signal_new(NM_POLKIT_LISTENER_SIGNAL_REQUEST_SYNC, + NM_TYPE_POLKIT_LISTENER, + G_SIGNAL_RUN_LAST, + 0, + NULL, + NULL, + NULL, + G_TYPE_STRING, + 3, + G_TYPE_STRING, + G_TYPE_STRING, + G_TYPE_STRING); + + signals[REGISTERED] = g_signal_new(NM_POLKIT_LISTENER_SIGNAL_REGISTERED, + NM_TYPE_POLKIT_LISTENER, + G_SIGNAL_RUN_FIRST, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 0); + + signals[ERROR] = g_signal_new(NM_POLKIT_LISTENER_SIGNAL_ERROR, + NM_TYPE_POLKIT_LISTENER, + G_SIGNAL_RUN_FIRST, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 1, + G_TYPE_STRING); +} diff --git a/src/libnmc-base/nm-polkit-listener.h b/src/libnmc-base/nm-polkit-listener.h new file mode 100644 index 0000000000..8a4c6c38d7 --- /dev/null +++ b/src/libnmc-base/nm-polkit-listener.h @@ -0,0 +1,33 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2014 Red Hat, Inc. + */ + +#ifndef __NM_POLKIT_LISTENER_H__ +#define __NM_POLKIT_LISTENER_H__ + +#define NM_POLKIT_LISTENER_SIGNAL_REGISTERED "registered" +#define NM_POLKIT_LISTENER_SIGNAL_REQUEST_SYNC "request-sync" +#define NM_POLKIT_LISTENER_SIGNAL_AUTH_SUCCESS "auth-success" +#define NM_POLKIT_LISTENER_SIGNAL_AUTH_FAILURE "auth-failure" +#define NM_POLKIT_LISTENER_SIGNAL_ERROR "error" + +typedef struct _NMPolkitListener NMPolkitListener; +typedef struct _NMPolkitListenerClass NMPolkitListenerClass; + +#define NM_TYPE_POLKIT_LISTENER (nm_polkit_listener_get_type()) +#define NM_POLKIT_LISTENER(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_POLKIT_LISTENER, NMPolkitListener)) +#define NM_POLKIT_LISTENER_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_POLKIT_LISTENER, NMPolkitListenerClass)) +#define NM_IS_POLKIT_LISTENER(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_POLKIT_LISTENER)) +#define NM_IS_POLKIT_LISTENER_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_POLKIT_LISTENER)) +#define NM_POLKIT_LISTENER_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_POLKIT_LISTENER, NMPolkitListenerClass)) + +GType nm_polkit_listener_get_type(void); + +NMPolkitListener *nm_polkit_listener_new(GDBusConnection *dbus_connection, gboolean session_agent); + +#endif /* __NM_POLKIT_LISTENER_H__ */ diff --git a/src/libnmc-base/nm-secret-agent-simple.c b/src/libnmc-base/nm-secret-agent-simple.c new file mode 100644 index 0000000000..69617d0fee --- /dev/null +++ b/src/libnmc-base/nm-secret-agent-simple.c @@ -0,0 +1,1402 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2011 - 2015 Red Hat, Inc. + * Copyright (C) 2011 Giovanni Campagna <scampa.giovanni@gmail.com> + */ + +/** + * SECTION:nm-secret-agent-simple + * @short_description: A simple secret agent for NetworkManager + * + * #NMSecretAgentSimple is the secret agent used by nmtui-connect and nmcli. + * + * This is a stripped-down version of gnome-shell's ShellNetworkAgent, + * with bits of the corresponding JavaScript code squished down into + * it. It is intended to eventually be generic enough that it could + * replace ShellNetworkAgent. + */ + +#include "libnm-client-aux-extern/nm-default-client.h" + +#include "nm-secret-agent-simple.h" + +#include <gio/gunixoutputstream.h> +#include <gio/gunixinputstream.h> + +#include "nm-vpn-service-plugin.h" +#include "nm-vpn-helpers.h" +#include "libnm-glib-aux/nm-secret-utils.h" + +/*****************************************************************************/ + +typedef struct { + char *request_id; + + NMSecretAgentSimple *self; + + NMConnection * connection; + const char * setting_name; + char ** hints; + NMSecretAgentOldGetSecretsFunc callback; + gpointer callback_data; + GCancellable * cancellable; + NMSecretAgentGetSecretsFlags flags; +} RequestData; + +enum { + REQUEST_SECRETS, + + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL] = {0}; + +typedef struct { + GHashTable *requests; + + char * path; + gboolean enabled; +} NMSecretAgentSimplePrivate; + +struct _NMSecretAgentSimple { + NMSecretAgentOld parent; + NMSecretAgentSimplePrivate _priv; +}; + +struct _NMSecretAgentSimpleClass { + NMSecretAgentOldClass parent; +}; + +G_DEFINE_TYPE(NMSecretAgentSimple, nm_secret_agent_simple, NM_TYPE_SECRET_AGENT_OLD) + +#define NM_SECRET_AGENT_SIMPLE_GET_PRIVATE(self) \ + _NM_GET_PRIVATE(self, NMSecretAgentSimple, NM_IS_SECRET_AGENT_SIMPLE, NMSecretAgentOld) + +/*****************************************************************************/ + +static void +_request_data_free(gpointer data) +{ + RequestData *request = data; + + g_free(request->request_id); + nm_clear_g_cancellable(&request->cancellable); + g_object_unref(request->connection); + g_strfreev(request->hints); + + g_slice_free(RequestData, request); +} + +static void +_request_data_complete(RequestData * request, + GVariant * secrets, + GError * error, + GHashTableIter *iter_to_remove) +{ + NMSecretAgentSimple * self = request->self; + NMSecretAgentSimplePrivate *priv = NM_SECRET_AGENT_SIMPLE_GET_PRIVATE(self); + + nm_assert((secrets != NULL) != (error != NULL)); + + request->callback(NM_SECRET_AGENT_OLD(request->self), + request->connection, + secrets, + error, + request->callback_data); + + if (iter_to_remove) + g_hash_table_iter_remove(iter_to_remove); + else + g_hash_table_remove(priv->requests, request); +} + +/*****************************************************************************/ + +/** + * NMSecretAgentSimpleSecret: + * @name: the user-visible name of the secret. Eg, "WEP Passphrase". + * @value: the value of the secret + * @password: %TRUE if this secret represents a password, %FALSE + * if it represents non-secret data. + * + * A single "secret" being requested. + */ + +typedef struct { + NMSecretAgentSimpleSecret base; + NMSetting * setting; + char * property; +} SecretReal; + +static void +_secret_real_free(NMSecretAgentSimpleSecret *secret) +{ + SecretReal *real = (SecretReal *) secret; + + g_free((char *) secret->pretty_name); + g_free((char *) secret->entry_id); + nm_free_secret(secret->value); + g_free((char *) secret->vpn_type); + g_free(real->property); + g_clear_object(&real->setting); + + g_slice_free(SecretReal, real); +} + +static NMSecretAgentSimpleSecret * +_secret_real_new_plain(NMSecretAgentSecretType secret_type, + const char * pretty_name, + NMSetting * setting, + const char * property) +{ + SecretReal * real; + gs_free char *value = NULL; + + nm_assert(property); + nm_assert(NM_IS_SETTING(setting)); + nm_assert(NM_IN_SET(secret_type, + NM_SECRET_AGENT_SECRET_TYPE_PROPERTY, + NM_SECRET_AGENT_SECRET_TYPE_SECRET)); + nm_assert(g_object_class_find_property(G_OBJECT_GET_CLASS(setting), property)); + nm_assert((secret_type == NM_SECRET_AGENT_SECRET_TYPE_SECRET) + == nm_setting_get_secret_flags(setting, property, NULL, NULL)); + + g_object_get(setting, property, &value, NULL); + + real = g_slice_new(SecretReal); + *real = (SecretReal){ + .base.secret_type = secret_type, + .base.pretty_name = g_strdup(pretty_name), + .base.entry_id = g_strdup_printf("%s.%s", nm_setting_get_name(setting), property), + .base.value = g_steal_pointer(&value), + .base.is_secret = (secret_type != NM_SECRET_AGENT_SECRET_TYPE_PROPERTY), + .setting = g_object_ref(setting), + .property = g_strdup(property), + }; + return &real->base; +} + +static NMSecretAgentSimpleSecret * +_secret_real_new_vpn_secret(const char *pretty_name, + NMSetting * setting, + const char *property, + const char *vpn_type) +{ + SecretReal *real; + const char *value; + + nm_assert(property); + nm_assert(NM_IS_SETTING_VPN(setting)); + nm_assert(vpn_type); + + value = nm_setting_vpn_get_secret(NM_SETTING_VPN(setting), property); + + real = g_slice_new(SecretReal); + *real = (SecretReal){ + .base.secret_type = NM_SECRET_AGENT_SECRET_TYPE_VPN_SECRET, + .base.pretty_name = g_strdup(pretty_name), + .base.entry_id = + g_strdup_printf("%s%s", NM_SECRET_AGENT_ENTRY_ID_PREFX_VPN_SECRETS, property), + .base.value = g_strdup(value), + .base.is_secret = TRUE, + .base.vpn_type = g_strdup(vpn_type), + .setting = g_object_ref(setting), + .property = g_strdup(property), + }; + return &real->base; +} + +static NMSecretAgentSimpleSecret * +_secret_real_new_wireguard_peer_psk(NMSettingWireGuard *s_wg, + const char * public_key, + const char * preshared_key) +{ + SecretReal *real; + + nm_assert(NM_IS_SETTING_WIREGUARD(s_wg)); + nm_assert(public_key); + + real = g_slice_new(SecretReal); + *real = (SecretReal){ + .base.secret_type = NM_SECRET_AGENT_SECRET_TYPE_WIREGUARD_PEER_PSK, + .base.pretty_name = g_strdup_printf(_("Preshared-key for %s"), public_key), + .base.entry_id = g_strdup_printf(NM_SETTING_WIREGUARD_SETTING_NAME + "." NM_SETTING_WIREGUARD_PEERS + ".%s." NM_WIREGUARD_PEER_ATTR_PRESHARED_KEY, + public_key), + .base.value = g_strdup(preshared_key), + .base.is_secret = TRUE, + .base.no_prompt_entry_id = TRUE, + .setting = NM_SETTING(g_object_ref(s_wg)), + .property = g_strdup(public_key), + }; + return &real->base; +} + +/*****************************************************************************/ + +static gboolean +add_8021x_secrets(RequestData *request, GPtrArray *secrets) +{ + NMSetting8021x * s_8021x = nm_connection_get_setting_802_1x(request->connection); + const char * eap_method; + NMSecretAgentSimpleSecret *secret; + + /* If hints are given, then always ask for what the hints require */ + if (request->hints && request->hints[0]) { + char **iter; + + for (iter = request->hints; *iter; iter++) { + secret = _secret_real_new_plain(NM_SECRET_AGENT_SECRET_TYPE_SECRET, + _(*iter), + NM_SETTING(s_8021x), + *iter); + g_ptr_array_add(secrets, secret); + } + + return TRUE; + } + + eap_method = nm_setting_802_1x_get_eap_method(s_8021x, 0); + if (!eap_method) + return FALSE; + + if (NM_IN_STRSET(eap_method, "md5", "leap", "ttls", "peap")) { + /* TTLS and PEAP are actually much more complicated, but this complication + * is not visible here since we only care about phase2 authentication + * (and don't even care of which one) + */ + secret = _secret_real_new_plain(NM_SECRET_AGENT_SECRET_TYPE_PROPERTY, + _("Username"), + NM_SETTING(s_8021x), + NM_SETTING_802_1X_IDENTITY); + g_ptr_array_add(secrets, secret); + secret = _secret_real_new_plain(NM_SECRET_AGENT_SECRET_TYPE_SECRET, + _("Password"), + NM_SETTING(s_8021x), + NM_SETTING_802_1X_PASSWORD); + g_ptr_array_add(secrets, secret); + return TRUE; + } + + if (nm_streq(eap_method, "tls")) { + secret = _secret_real_new_plain(NM_SECRET_AGENT_SECRET_TYPE_PROPERTY, + _("Identity"), + NM_SETTING(s_8021x), + NM_SETTING_802_1X_IDENTITY); + g_ptr_array_add(secrets, secret); + secret = _secret_real_new_plain(NM_SECRET_AGENT_SECRET_TYPE_SECRET, + _("Private key password"), + NM_SETTING(s_8021x), + NM_SETTING_802_1X_PRIVATE_KEY_PASSWORD); + g_ptr_array_add(secrets, secret); + return TRUE; + } + + return FALSE; +} + +static gboolean +add_wireless_secrets(RequestData *request, GPtrArray *secrets) +{ + NMSettingWirelessSecurity *s_wsec = + nm_connection_get_setting_wireless_security(request->connection); + const char * key_mgmt = nm_setting_wireless_security_get_key_mgmt(s_wsec); + NMSecretAgentSimpleSecret *secret; + + if (!key_mgmt || nm_streq(key_mgmt, "owe")) + return FALSE; + + if (NM_IN_STRSET(key_mgmt, "wpa-psk", "sae")) { + secret = _secret_real_new_plain(NM_SECRET_AGENT_SECRET_TYPE_SECRET, + _("Password"), + NM_SETTING(s_wsec), + NM_SETTING_WIRELESS_SECURITY_PSK); + g_ptr_array_add(secrets, secret); + return TRUE; + } + + if (nm_streq(key_mgmt, "none")) { + guint32 index; + char key[100]; + + index = nm_setting_wireless_security_get_wep_tx_keyidx(s_wsec); + secret = _secret_real_new_plain(NM_SECRET_AGENT_SECRET_TYPE_SECRET, + _("Key"), + NM_SETTING(s_wsec), + nm_sprintf_buf(key, "wep-key%u", (guint) index)); + g_ptr_array_add(secrets, secret); + return TRUE; + } + + if (nm_streq(key_mgmt, "iee8021x")) { + if (nm_streq0(nm_setting_wireless_security_get_auth_alg(s_wsec), "leap")) { + secret = _secret_real_new_plain(NM_SECRET_AGENT_SECRET_TYPE_SECRET, + _("Password"), + NM_SETTING(s_wsec), + NM_SETTING_WIRELESS_SECURITY_LEAP_PASSWORD); + g_ptr_array_add(secrets, secret); + return TRUE; + } else + return add_8021x_secrets(request, secrets); + } + + if (nm_streq(key_mgmt, "wpa-eap") || nm_streq(key_mgmt, "wpa-eap-suite-b-192")) + return add_8021x_secrets(request, secrets); + + return FALSE; +} + +static gboolean +add_pppoe_secrets(RequestData *request, GPtrArray *secrets) +{ + NMSettingPppoe * s_pppoe = nm_connection_get_setting_pppoe(request->connection); + NMSecretAgentSimpleSecret *secret; + + secret = _secret_real_new_plain(NM_SECRET_AGENT_SECRET_TYPE_PROPERTY, + _("Username"), + NM_SETTING(s_pppoe), + NM_SETTING_PPPOE_USERNAME); + g_ptr_array_add(secrets, secret); + secret = _secret_real_new_plain(NM_SECRET_AGENT_SECRET_TYPE_PROPERTY, + _("Service"), + NM_SETTING(s_pppoe), + NM_SETTING_PPPOE_SERVICE); + g_ptr_array_add(secrets, secret); + secret = _secret_real_new_plain(NM_SECRET_AGENT_SECRET_TYPE_SECRET, + _("Password"), + NM_SETTING(s_pppoe), + NM_SETTING_PPPOE_PASSWORD); + g_ptr_array_add(secrets, secret); + return TRUE; +} + +static NMSettingSecretFlags +get_vpn_secret_flags(NMSettingVpn *s_vpn, const char *secret_name) +{ + NMSettingSecretFlags flags = NM_SETTING_SECRET_FLAG_NONE; + GHashTable * vpn_data; + + g_object_get(s_vpn, NM_SETTING_VPN_DATA, &vpn_data, NULL); + nm_vpn_service_plugin_get_secret_flags(vpn_data, secret_name, &flags); + g_hash_table_unref(vpn_data); + + return flags; +} + +static void +add_vpn_secret_helper(GPtrArray * secrets, + NMSettingVpn *s_vpn, + const char * name, + const char * ui_name) +{ + NMSecretAgentSimpleSecret *secret; + NMSettingSecretFlags flags; + int i; + + flags = get_vpn_secret_flags(s_vpn, name); + if (flags & NM_SETTING_SECRET_FLAG_AGENT_OWNED || flags & NM_SETTING_SECRET_FLAG_NOT_SAVED) { + secret = _secret_real_new_vpn_secret(ui_name, + NM_SETTING(s_vpn), + name, + nm_setting_vpn_get_service_type(s_vpn)); + + /* Check for duplicates */ + for (i = 0; i < secrets->len; i++) { + NMSecretAgentSimpleSecret *s = secrets->pdata[i]; + + if (s->secret_type == secret->secret_type && nm_streq0(s->vpn_type, secret->vpn_type) + && nm_streq0(s->entry_id, secret->entry_id)) { + _secret_real_free(secret); + return; + } + } + + g_ptr_array_add(secrets, secret); + } +} + +#define VPN_MSG_TAG "x-vpn-message:" + +static gboolean +add_vpn_secrets(RequestData *request, GPtrArray *secrets, char **msg) +{ + NMSettingVpn * s_vpn = nm_connection_get_setting_vpn(request->connection); + const NmcVpnPasswordName *p; + const char * vpn_msg = NULL; + char ** iter; + + /* If hints are given, then always ask for what the hints require */ + if (request->hints) { + for (iter = request->hints; *iter; iter++) { + if (!vpn_msg && g_str_has_prefix(*iter, VPN_MSG_TAG)) + vpn_msg = &(*iter)[NM_STRLEN(VPN_MSG_TAG)]; + else + add_vpn_secret_helper(secrets, s_vpn, *iter, *iter); + } + } + + NM_SET_OUT(msg, g_strdup(vpn_msg)); + + /* Now add what client thinks might be required, because hints may be empty or incomplete */ + p = nm_vpn_get_secret_names(nm_setting_vpn_get_service_type(s_vpn)); + while (p && p->name) { + add_vpn_secret_helper(secrets, s_vpn, p->name, _(p->ui_name)); + p++; + } + + return TRUE; +} + +static gboolean +add_wireguard_secrets(RequestData *request, GPtrArray *secrets, char **msg, GError **error) +{ + NMSettingWireGuard * s_wg; + NMSecretAgentSimpleSecret *secret; + guint i; + + s_wg = NM_SETTING_WIREGUARD( + nm_connection_get_setting(request->connection, NM_TYPE_SETTING_WIREGUARD)); + if (!s_wg) { + g_set_error(error, + NM_SECRET_AGENT_ERROR, + NM_SECRET_AGENT_ERROR_FAILED, + "Cannot service a WireGuard secrets request %s for a connection without " + "WireGuard settings", + request->request_id); + return FALSE; + } + + if (!request->hints || !request->hints[0] + || g_strv_contains(NM_CAST_STRV_CC(request->hints), NM_SETTING_WIREGUARD_PRIVATE_KEY)) { + secret = _secret_real_new_plain(NM_SECRET_AGENT_SECRET_TYPE_SECRET, + _("WireGuard private-key"), + NM_SETTING(s_wg), + NM_SETTING_WIREGUARD_PRIVATE_KEY); + g_ptr_array_add(secrets, secret); + } + + if (request->hints) { + for (i = 0; request->hints[i]; i++) { + NMWireGuardPeer *peer; + const char * name = request->hints[i]; + gs_free char * public_key = NULL; + + if (nm_streq(name, NM_SETTING_WIREGUARD_PRIVATE_KEY)) + continue; + + if (NM_STR_HAS_PREFIX(name, NM_SETTING_WIREGUARD_PEERS ".")) { + const char *tmp; + + tmp = &name[NM_STRLEN(NM_SETTING_WIREGUARD_PEERS ".")]; + if (NM_STR_HAS_SUFFIX(tmp, "." NM_WIREGUARD_PEER_ATTR_PRESHARED_KEY)) { + public_key = g_strndup( + tmp, + strlen(tmp) - NM_STRLEN("." NM_WIREGUARD_PEER_ATTR_PRESHARED_KEY)); + } + } + + if (!public_key) + continue; + + peer = nm_setting_wireguard_get_peer_by_public_key(s_wg, public_key, NULL); + + g_ptr_array_add(secrets, + _secret_real_new_wireguard_peer_psk( + s_wg, + (peer ? nm_wireguard_peer_get_public_key(peer) : public_key), + (peer ? nm_wireguard_peer_get_preshared_key(peer) : NULL))); + } + } + + *msg = g_strdup_printf(_("Secrets are required to connect WireGuard VPN '%s'"), + nm_connection_get_id(request->connection)); + return TRUE; +} + +typedef struct { + GPid auth_dialog_pid; + GString * auth_dialog_response; + RequestData * request; + GPtrArray * secrets; + GCancellable * cancellable; + gulong cancellable_id; + guint child_watch_id; + GInputStream * input_stream; + GOutputStream *output_stream; + char read_buf[5]; +} AuthDialogData; + +static void +_auth_dialog_data_free(AuthDialogData *data) +{ + nm_clear_g_signal_handler(data->cancellable, &data->cancellable_id); + g_clear_object(&data->cancellable); + nm_clear_g_source(&data->child_watch_id); + g_ptr_array_unref(data->secrets); + g_spawn_close_pid(data->auth_dialog_pid); + g_string_free(data->auth_dialog_response, TRUE); + g_object_unref(data->input_stream); + g_object_unref(data->output_stream); + g_slice_free(AuthDialogData, data); +} + +static void +_auth_dialog_exited(GPid pid, int status, gpointer user_data) +{ + AuthDialogData * data = user_data; + RequestData * request = data->request; + GPtrArray * secrets = data->secrets; + NMSettingVpn * s_vpn = nm_connection_get_setting_vpn(request->connection); + nm_auto_unref_keyfile GKeyFile *keyfile = NULL; + gs_strfreev char ** groups = NULL; + gs_free char * title = NULL; + gs_free char * message = NULL; + int i; + gs_free_error GError *error = NULL; + + data->child_watch_id = 0; + + nm_clear_g_cancellable_disconnect(data->cancellable, &data->cancellable_id); + + if (status != 0) { + g_set_error(&error, + NM_SECRET_AGENT_ERROR, + NM_SECRET_AGENT_ERROR_FAILED, + "Auth dialog failed with error code %d\n", + status); + goto out; + } + + keyfile = g_key_file_new(); + if (!g_key_file_load_from_data(keyfile, + data->auth_dialog_response->str, + data->auth_dialog_response->len, + G_KEY_FILE_NONE, + &error)) { + goto out; + } + + groups = g_key_file_get_groups(keyfile, NULL); + if (!nm_streq0(groups[0], "VPN Plugin UI")) { + g_set_error(&error, + NM_SECRET_AGENT_ERROR, + NM_SECRET_AGENT_ERROR_FAILED, + "Expected [VPN Plugin UI] in auth dialog response"); + goto out; + } + + title = g_key_file_get_string(keyfile, "VPN Plugin UI", "Title", &error); + if (!title) + goto out; + + message = g_key_file_get_string(keyfile, "VPN Plugin UI", "Description", &error); + if (!message) + goto out; + + for (i = 1; groups[i]; i++) { + gs_free char *pretty_name = NULL; + + if (!g_key_file_get_boolean(keyfile, groups[i], "IsSecret", NULL)) + continue; + if (!g_key_file_get_boolean(keyfile, groups[i], "ShouldAsk", NULL)) + continue; + + pretty_name = g_key_file_get_string(keyfile, groups[i], "Label", NULL); + g_ptr_array_add(secrets, + _secret_real_new_vpn_secret(pretty_name, + NM_SETTING(s_vpn), + groups[i], + nm_setting_vpn_get_service_type(s_vpn))); + } + +out: + /* Try to fall back to the hardwired VPN support if the auth dialog fails. + * We may eventually get rid of the whole hardwired secrets handling at some point, + * when the auth helpers are goode enough.. */ + if (error && add_vpn_secrets(request, secrets, &message)) { + g_clear_error(&error); + if (!message) { + message = g_strdup_printf(_("A password is required to connect to '%s'."), + nm_connection_get_id(request->connection)); + } + } + + if (error) + _request_data_complete(request, NULL, error, NULL); + else { + g_signal_emit(request->self, + signals[REQUEST_SECRETS], + 0, + request->request_id, + title, + message, + secrets); + } + + _auth_dialog_data_free(data); +} + +static void +_request_cancelled(GObject *object, gpointer user_data) +{ + _auth_dialog_data_free(user_data); +} + +static void +_auth_dialog_read_done(GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + GInputStream * auth_dialog_out = G_INPUT_STREAM(source_object); + AuthDialogData *data = user_data; + gssize read_size; + gs_free_error GError *error = NULL; + + read_size = g_input_stream_read_finish(auth_dialog_out, res, &error); + switch (read_size) { + case -1: + if (!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + _request_data_complete(data->request, NULL, error, NULL); + _auth_dialog_data_free(data); + break; + case 0: + /* Done reading. Let's wait for the auth dialog to exit so that we're able to collect the status. + * Remember we can be cancelled in between. */ + data->child_watch_id = g_child_watch_add(data->auth_dialog_pid, _auth_dialog_exited, data); + data->cancellable = g_object_ref(data->request->cancellable); + data->cancellable_id = + g_cancellable_connect(data->cancellable, G_CALLBACK(_request_cancelled), data, NULL); + break; + default: + g_string_append_len(data->auth_dialog_response, data->read_buf, read_size); + g_input_stream_read_async(auth_dialog_out, + data->read_buf, + sizeof(data->read_buf), + G_PRIORITY_DEFAULT, + NULL, + _auth_dialog_read_done, + data); + return; + } + + g_input_stream_close(auth_dialog_out, NULL, NULL); +} + +static void +_auth_dialog_write_done(GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + GOutputStream *auth_dialog_out = G_OUTPUT_STREAM(source_object); + _nm_unused gs_free char *auth_dialog_request_free = user_data; + + /* We don't care about write errors. If there are any problems, the + * reader shall notice. */ + g_output_stream_write_finish(auth_dialog_out, res, NULL); + g_output_stream_close(auth_dialog_out, NULL, NULL); +} + +static void +_add_to_string(GString *string, const char *key, const char *value) +{ + gs_strfreev char **lines = NULL; + int i; + + lines = g_strsplit(value, "\n", -1); + + g_string_append(string, key); + for (i = 0; lines[i]; i++) { + g_string_append_c(string, '='); + g_string_append(string, lines[i]); + g_string_append_c(string, '\n'); + } +} + +static void +_add_data_item_to_string(const char *key, const char *value, gpointer user_data) +{ + GString *string = user_data; + + _add_to_string(string, "DATA_KEY", key); + _add_to_string(string, "DATA_VAL", value); + g_string_append_c(string, '\n'); +} + +static void +_add_secret_to_string(const char *key, const char *value, gpointer user_data) +{ + GString *string = user_data; + + _add_to_string(string, "SECRET_KEY", key); + _add_to_string(string, "SECRET_VAL", value); + g_string_append_c(string, '\n'); +} + +static gboolean +try_spawn_vpn_auth_helper(RequestData *request, GPtrArray *secrets) +{ + NMSettingVpn * s_vpn = nm_connection_get_setting_vpn(request->connection); + gs_unref_ptrarray GPtrArray *auth_dialog_argv = NULL; + NMVpnPluginInfo * plugin_info; + const char * s; + GPid auth_dialog_pid; + int auth_dialog_in_fd; + int auth_dialog_out_fd; + GOutputStream * auth_dialog_in; + GInputStream * auth_dialog_out; + GError * error = NULL; + GString * auth_dialog_request; + char * auth_dialog_request_str; + gsize auth_dialog_request_len; + AuthDialogData * data; + int i; + + plugin_info = nm_vpn_plugin_info_list_find_by_service(nm_vpn_get_plugin_infos(), + nm_setting_vpn_get_service_type(s_vpn)); + if (!plugin_info) + return FALSE; + + s = nm_vpn_plugin_info_lookup_property(plugin_info, "GNOME", "supports-external-ui-mode"); + if (!_nm_utils_ascii_str_to_bool(s, FALSE)) + return FALSE; + + auth_dialog_argv = g_ptr_array_new(); + + s = nm_vpn_plugin_info_lookup_property(plugin_info, "GNOME", "auth-dialog"); + g_return_val_if_fail(s, FALSE); + g_ptr_array_add(auth_dialog_argv, (gpointer) s); + + g_ptr_array_add(auth_dialog_argv, "-u"); + g_ptr_array_add(auth_dialog_argv, (gpointer) nm_connection_get_uuid(request->connection)); + g_ptr_array_add(auth_dialog_argv, "-n"); + g_ptr_array_add(auth_dialog_argv, (gpointer) nm_connection_get_id(request->connection)); + g_ptr_array_add(auth_dialog_argv, "-s"); + g_ptr_array_add(auth_dialog_argv, (gpointer) nm_setting_vpn_get_service_type(s_vpn)); + g_ptr_array_add(auth_dialog_argv, "--external-ui-mode"); + g_ptr_array_add(auth_dialog_argv, "-i"); + + if (request->flags & NM_SECRET_AGENT_GET_SECRETS_FLAG_REQUEST_NEW) + g_ptr_array_add(auth_dialog_argv, "-r"); + + s = nm_vpn_plugin_info_lookup_property(plugin_info, "GNOME", "supports-hints"); + if (_nm_utils_ascii_str_to_bool(s, FALSE)) { + for (i = 0; request->hints[i]; i++) { + g_ptr_array_add(auth_dialog_argv, "-t"); + g_ptr_array_add(auth_dialog_argv, request->hints[i]); + } + } + + g_ptr_array_add(auth_dialog_argv, NULL); + if (!g_spawn_async_with_pipes(NULL, + (char **) auth_dialog_argv->pdata, + NULL, + G_SPAWN_DO_NOT_REAP_CHILD, + NULL, + NULL, + &auth_dialog_pid, + &auth_dialog_in_fd, + &auth_dialog_out_fd, + NULL, + &error)) { + g_warning("Failed to spawn the auth dialog%s\n", error->message); + return FALSE; + } + + auth_dialog_in = g_unix_output_stream_new(auth_dialog_in_fd, TRUE); + auth_dialog_out = g_unix_input_stream_new(auth_dialog_out_fd, TRUE); + + auth_dialog_request = g_string_new_len(NULL, 1024); + nm_setting_vpn_foreach_data_item(s_vpn, _add_data_item_to_string, auth_dialog_request); + nm_setting_vpn_foreach_secret(s_vpn, _add_secret_to_string, auth_dialog_request); + g_string_append(auth_dialog_request, "DONE\nQUIT\n"); + auth_dialog_request_len = auth_dialog_request->len; + auth_dialog_request_str = g_string_free(auth_dialog_request, FALSE); + + data = g_slice_new(AuthDialogData); + *data = (AuthDialogData){ + .auth_dialog_response = g_string_new_len(NULL, sizeof(data->read_buf)), + .auth_dialog_pid = auth_dialog_pid, + .request = request, + .secrets = g_ptr_array_ref(secrets), + .input_stream = auth_dialog_out, + .output_stream = auth_dialog_in, + }; + + g_output_stream_write_async(auth_dialog_in, + auth_dialog_request_str, + auth_dialog_request_len, + G_PRIORITY_DEFAULT, + request->cancellable, + _auth_dialog_write_done, + auth_dialog_request_str); + + g_input_stream_read_async(auth_dialog_out, + data->read_buf, + sizeof(data->read_buf), + G_PRIORITY_DEFAULT, + request->cancellable, + _auth_dialog_read_done, + data); + + return TRUE; +} + +static void +request_secrets_from_ui(RequestData *request) +{ + gs_unref_ptrarray GPtrArray *secrets = NULL; + gs_free_error GError * error = NULL; + NMSecretAgentSimplePrivate *priv; + NMSecretAgentSimpleSecret * secret; + const char * title; + gs_free char * msg = NULL; + + priv = NM_SECRET_AGENT_SIMPLE_GET_PRIVATE(request->self); + g_return_if_fail(priv->enabled); + + /* We only handle requests for connection with @path if set. */ + if (priv->path && !g_str_has_prefix(request->request_id, priv->path)) { + g_set_error(&error, + NM_SECRET_AGENT_ERROR, + NM_SECRET_AGENT_ERROR_FAILED, + "Request for %s secrets doesn't match path %s", + request->request_id, + priv->path); + goto out_fail_error; + } + + secrets = g_ptr_array_new_with_free_func((GDestroyNotify) _secret_real_free); + + if (nm_connection_is_type(request->connection, NM_SETTING_WIRELESS_SETTING_NAME)) { + NMSettingWireless *s_wireless; + GBytes * ssid; + char * ssid_utf8; + + s_wireless = nm_connection_get_setting_wireless(request->connection); + ssid = nm_setting_wireless_get_ssid(s_wireless); + ssid_utf8 = nm_utils_ssid_to_utf8(g_bytes_get_data(ssid, NULL), g_bytes_get_size(ssid)); + + title = _("Authentication required by wireless network"); + msg = g_strdup_printf( + _("Passwords or encryption keys are required to access the wireless network '%s'."), + ssid_utf8); + + if (!add_wireless_secrets(request, secrets)) + goto out_fail; + } else if (nm_connection_is_type(request->connection, NM_SETTING_WIRED_SETTING_NAME)) { + title = _("Wired 802.1X authentication"); + msg = g_strdup_printf(_("Secrets are required to access the wired network '%s'"), + nm_connection_get_id(request->connection)); + + if (!add_8021x_secrets(request, secrets)) + goto out_fail; + } else if (nm_connection_is_type(request->connection, NM_SETTING_PPPOE_SETTING_NAME)) { + title = _("DSL authentication"); + msg = g_strdup_printf(_("Secrets are required for the DSL connection '%s'"), + nm_connection_get_id(request->connection)); + + if (!add_pppoe_secrets(request, secrets)) + goto out_fail; + } else if (nm_connection_is_type(request->connection, NM_SETTING_GSM_SETTING_NAME)) { + NMSettingGsm *s_gsm = nm_connection_get_setting_gsm(request->connection); + + if (g_strv_contains(NM_CAST_STRV_CC(request->hints), NM_SETTING_GSM_PIN)) { + title = _("PIN code required"); + msg = g_strdup(_("PIN code is needed for the mobile broadband device")); + + secret = _secret_real_new_plain(NM_SECRET_AGENT_SECRET_TYPE_SECRET, + _("PIN"), + NM_SETTING(s_gsm), + NM_SETTING_GSM_PIN); + g_ptr_array_add(secrets, secret); + } else { + title = _("Mobile broadband network password"); + msg = g_strdup_printf(_("A password is required to connect to '%s'."), + nm_connection_get_id(request->connection)); + + secret = _secret_real_new_plain(NM_SECRET_AGENT_SECRET_TYPE_SECRET, + _("Password"), + NM_SETTING(s_gsm), + NM_SETTING_GSM_PASSWORD); + g_ptr_array_add(secrets, secret); + } + } else if (nm_connection_is_type(request->connection, NM_SETTING_MACSEC_SETTING_NAME)) { + NMSettingMacsec *s_macsec = nm_connection_get_setting_macsec(request->connection); + + msg = g_strdup_printf(_("Secrets are required to access the MACsec network '%s'"), + nm_connection_get_id(request->connection)); + + if (nm_setting_macsec_get_mode(s_macsec) == NM_SETTING_MACSEC_MODE_PSK) { + title = _("MACsec PSK authentication"); + secret = _secret_real_new_plain(NM_SECRET_AGENT_SECRET_TYPE_SECRET, + _("MKA CAK"), + NM_SETTING(s_macsec), + NM_SETTING_MACSEC_MKA_CAK); + g_ptr_array_add(secrets, secret); + } else { + title = _("MACsec EAP authentication"); + if (!add_8021x_secrets(request, secrets)) + goto out_fail; + } + } else if (nm_connection_is_type(request->connection, NM_SETTING_WIREGUARD_SETTING_NAME)) { + title = _("WireGuard VPN secret"); + if (!add_wireguard_secrets(request, secrets, &msg, &error)) + goto out_fail_error; + } else if (nm_connection_is_type(request->connection, NM_SETTING_CDMA_SETTING_NAME)) { + NMSettingCdma *s_cdma = nm_connection_get_setting_cdma(request->connection); + + title = _("Mobile broadband network password"); + msg = g_strdup_printf(_("A password is required to connect to '%s'."), + nm_connection_get_id(request->connection)); + + secret = _secret_real_new_plain(NM_SECRET_AGENT_SECRET_TYPE_SECRET, + _("Password"), + NM_SETTING(s_cdma), + NM_SETTING_CDMA_PASSWORD); + g_ptr_array_add(secrets, secret); + } else if (nm_connection_is_type(request->connection, NM_SETTING_BLUETOOTH_SETTING_NAME)) { + NMSetting *setting = NULL; + + setting = nm_connection_get_setting_by_name(request->connection, + NM_SETTING_BLUETOOTH_SETTING_NAME); + if (setting + && !nm_streq0(nm_setting_bluetooth_get_connection_type(NM_SETTING_BLUETOOTH(setting)), + NM_SETTING_BLUETOOTH_TYPE_NAP)) { + setting = + nm_connection_get_setting_by_name(request->connection, NM_SETTING_GSM_SETTING_NAME); + if (!setting) + setting = nm_connection_get_setting_by_name(request->connection, + NM_SETTING_CDMA_SETTING_NAME); + } + + if (!setting) + goto out_fail; + + title = _("Mobile broadband network password"); + msg = g_strdup_printf(_("A password is required to connect to '%s'."), + nm_connection_get_id(request->connection)); + + secret = _secret_real_new_plain(NM_SECRET_AGENT_SECRET_TYPE_SECRET, + _("Password"), + setting, + "password"); + g_ptr_array_add(secrets, secret); + } else if (nm_connection_is_type(request->connection, NM_SETTING_VPN_SETTING_NAME)) { + title = _("VPN password required"); + + if (try_spawn_vpn_auth_helper(request, secrets)) { + /* This will emit REQUEST_SECRETS when ready */ + return; + } + + if (!add_vpn_secrets(request, secrets, &msg)) + goto out_fail; + if (!msg) { + msg = g_strdup_printf(_("A password is required to connect to '%s'."), + nm_connection_get_id(request->connection)); + } + } else + goto out_fail; + + if (secrets->len == 0) + goto out_fail; + + g_signal_emit(request->self, + signals[REQUEST_SECRETS], + 0, + request->request_id, + title, + msg, + secrets); + return; + +out_fail: + g_set_error(&error, + NM_SECRET_AGENT_ERROR, + NM_SECRET_AGENT_ERROR_FAILED, + "Cannot service a secrets request %s for a %s connection", + request->request_id, + nm_connection_get_connection_type(request->connection)); +out_fail_error: + _request_data_complete(request, NULL, error, NULL); +} + +static void +get_secrets(NMSecretAgentOld * agent, + NMConnection * connection, + const char * connection_path, + const char * setting_name, + const char ** hints, + NMSecretAgentGetSecretsFlags flags, + NMSecretAgentOldGetSecretsFunc callback, + gpointer callback_data) +{ + NMSecretAgentSimple * self = NM_SECRET_AGENT_SIMPLE(agent); + NMSecretAgentSimplePrivate *priv = NM_SECRET_AGENT_SIMPLE_GET_PRIVATE(self); + RequestData * request; + gs_free_error GError *error = NULL; + gs_free char * request_id = NULL; + const char * request_id_setting_name; + + request_id = g_strdup_printf("%s/%s", connection_path, setting_name); + + if (g_hash_table_contains(priv->requests, &request_id)) { + /* We already have a request pending for this (connection, setting) */ + error = g_error_new(NM_SECRET_AGENT_ERROR, + NM_SECRET_AGENT_ERROR_FAILED, + "Request for %s secrets already pending", + request_id); + callback(agent, connection, NULL, error, callback_data); + return; + } + + if (!(flags & NM_SECRET_AGENT_GET_SECRETS_FLAG_ALLOW_INTERACTION)) { + /* We don't do stored passwords */ + error = g_error_new(NM_SECRET_AGENT_ERROR, + NM_SECRET_AGENT_ERROR_NO_SECRETS, + "Stored passwords not supported"); + callback(agent, connection, NULL, error, callback_data); + return; + } + + nm_assert(g_str_has_suffix(request_id, setting_name)); + request_id_setting_name = &request_id[strlen(request_id) - strlen(setting_name)]; + nm_assert(nm_streq(request_id_setting_name, setting_name)); + + request = g_slice_new(RequestData); + *request = (RequestData){ + .self = self, + .connection = g_object_ref(connection), + .setting_name = request_id_setting_name, + .hints = g_strdupv((char **) hints), + .callback = callback, + .callback_data = callback_data, + .request_id = g_steal_pointer(&request_id), + .flags = flags, + .cancellable = g_cancellable_new(), + }; + g_hash_table_add(priv->requests, request); + + if (priv->enabled) + request_secrets_from_ui(request); +} + +/** + * nm_secret_agent_simple_response: + * @self: the #NMSecretAgentSimple + * @request_id: the request ID being responded to + * @secrets: (allow-none): the array of secrets, or %NULL + * + * Response to a #NMSecretAgentSimple::get-secrets signal. + * + * If the user provided secrets, the caller should set the + * corresponding <literal>value</literal> fields in the + * #NMSecretAgentSimpleSecrets (freeing any initial values they had), and + * pass the array to nm_secret_agent_simple_response(). If the user + * cancelled the request, @secrets should be NULL. + */ +void +nm_secret_agent_simple_response(NMSecretAgentSimple *self, + const char * request_id, + GPtrArray * secrets) +{ + NMSecretAgentSimplePrivate *priv; + RequestData * request; + gs_unref_variant GVariant *secrets_dict = NULL; + gs_free_error GError *error = NULL; + int i; + + g_return_if_fail(NM_IS_SECRET_AGENT_SIMPLE(self)); + + priv = NM_SECRET_AGENT_SIMPLE_GET_PRIVATE(self); + request = g_hash_table_lookup(priv->requests, &request_id); + g_return_if_fail(request != NULL); + + if (secrets) { + GVariantBuilder conn_builder, *setting_builder; + GVariantBuilder vpn_secrets_builder; + GVariantBuilder wg_secrets_builder; + GVariantBuilder wg_peer_builder; + GHashTable * settings; + GHashTableIter iter; + const char * name; + gboolean has_vpn = FALSE; + gboolean has_wg = FALSE; + + settings = g_hash_table_new_full(nm_str_hash, + g_str_equal, + NULL, + (GDestroyNotify) g_variant_builder_unref); + for (i = 0; i < secrets->len; i++) { + SecretReal *secret = secrets->pdata[i]; + + setting_builder = g_hash_table_lookup(settings, nm_setting_get_name(secret->setting)); + if (!setting_builder) { + setting_builder = g_variant_builder_new(NM_VARIANT_TYPE_SETTING); + g_hash_table_insert(settings, + (char *) nm_setting_get_name(secret->setting), + setting_builder); + } + + switch (secret->base.secret_type) { + case NM_SECRET_AGENT_SECRET_TYPE_PROPERTY: + case NM_SECRET_AGENT_SECRET_TYPE_SECRET: + g_variant_builder_add(setting_builder, + "{sv}", + secret->property, + g_variant_new_string(secret->base.value)); + break; + case NM_SECRET_AGENT_SECRET_TYPE_VPN_SECRET: + if (!has_vpn) { + g_variant_builder_init(&vpn_secrets_builder, G_VARIANT_TYPE("a{ss}")); + has_vpn = TRUE; + } + g_variant_builder_add(&vpn_secrets_builder, + "{ss}", + secret->property, + secret->base.value); + break; + case NM_SECRET_AGENT_SECRET_TYPE_WIREGUARD_PEER_PSK: + if (!has_wg) { + g_variant_builder_init(&wg_secrets_builder, G_VARIANT_TYPE("aa{sv}")); + has_wg = TRUE; + } + g_variant_builder_init(&wg_peer_builder, G_VARIANT_TYPE("a{sv}")); + g_variant_builder_add(&wg_peer_builder, + "{sv}", + NM_WIREGUARD_PEER_ATTR_PUBLIC_KEY, + g_variant_new_string(secret->property)); + g_variant_builder_add(&wg_peer_builder, + "{sv}", + NM_WIREGUARD_PEER_ATTR_PRESHARED_KEY, + g_variant_new_string(secret->base.value)); + g_variant_builder_add(&wg_secrets_builder, "a{sv}", &wg_peer_builder); + break; + } + } + + if (has_vpn) { + g_variant_builder_add(setting_builder, + "{sv}", + "secrets", + g_variant_builder_end(&vpn_secrets_builder)); + } + + if (has_wg) { + g_variant_builder_add(setting_builder, + "{sv}", + NM_SETTING_WIREGUARD_PEERS, + g_variant_builder_end(&wg_secrets_builder)); + } + + g_variant_builder_init(&conn_builder, NM_VARIANT_TYPE_CONNECTION); + g_hash_table_iter_init(&iter, settings); + while (g_hash_table_iter_next(&iter, (gpointer *) &name, (gpointer *) &setting_builder)) + g_variant_builder_add(&conn_builder, "{sa{sv}}", name, setting_builder); + secrets_dict = g_variant_ref_sink(g_variant_builder_end(&conn_builder)); + g_hash_table_destroy(settings); + } else { + error = g_error_new(NM_SECRET_AGENT_ERROR, + NM_SECRET_AGENT_ERROR_USER_CANCELED, + "User cancelled"); + } + + _request_data_complete(request, secrets_dict, error, NULL); +} + +static void +cancel_get_secrets(NMSecretAgentOld *agent, const char *connection_path, const char *setting_name) +{ + NMSecretAgentSimple * self = NM_SECRET_AGENT_SIMPLE(agent); + NMSecretAgentSimplePrivate *priv = NM_SECRET_AGENT_SIMPLE_GET_PRIVATE(self); + gs_free_error GError *error = NULL; + gs_free char * request_id = NULL; + RequestData * request; + + request_id = g_strdup_printf("%s/%s", connection_path, setting_name); + request = g_hash_table_lookup(priv->requests, &request_id); + if (!request) { + /* this is really a bug of the caller (or us?). We cannot invoke a callback, + * hence the caller cannot cleanup the request. */ + g_return_if_reached(); + } + + g_set_error(&error, + NM_SECRET_AGENT_ERROR, + NM_SECRET_AGENT_ERROR_AGENT_CANCELED, + "The secret agent is going away"); + _request_data_complete(request, NULL, error, NULL); +} + +static void +save_secrets(NMSecretAgentOld * agent, + NMConnection * connection, + const char * connection_path, + NMSecretAgentOldSaveSecretsFunc callback, + gpointer callback_data) +{ + /* We don't support secret storage */ + callback(agent, connection, NULL, callback_data); +} + +static void +delete_secrets(NMSecretAgentOld * agent, + NMConnection * connection, + const char * connection_path, + NMSecretAgentOldDeleteSecretsFunc callback, + gpointer callback_data) +{ + /* We don't support secret storage, so there's nothing to delete. */ + callback(agent, connection, NULL, callback_data); +} + +/** + * nm_secret_agent_simple_enable: + * @self: the #NMSecretAgentSimple + * @path: (allow-none): the path of the connection (if any) to handle secrets + * for. If %NULL, secrets for any connection will be handled. + * + * Enables servicing the requests including the already queued ones. If @path + * is given, the agent will only handle requests for connections that match + * @path. + */ +void +nm_secret_agent_simple_enable(NMSecretAgentSimple *self, const char *path) +{ + NMSecretAgentSimplePrivate *priv = NM_SECRET_AGENT_SIMPLE_GET_PRIVATE(self); + gs_free RequestData **requests = NULL; + gsize i; + gs_free char * path_full = NULL; + + /* The path is only used to match a request_id with the current + * connection. Since the request_id is "${CONNECTION_PATH}/${SETTING}", + * add a trailing '/' to the path to match the full connection path. + */ + path_full = path ? g_strdup_printf("%s/", path) : NULL; + + if (!nm_streq0(path_full, priv->path)) { + g_free(priv->path); + priv->path = g_steal_pointer(&path_full); + } + + if (priv->enabled) + return; + priv->enabled = TRUE; + + /* Service pending secret requests. */ + requests = (RequestData **) g_hash_table_get_keys_as_array(priv->requests, NULL); + for (i = 0; requests[i]; i++) + request_secrets_from_ui(requests[i]); +} + +/*****************************************************************************/ + +static void +nm_secret_agent_simple_init(NMSecretAgentSimple *agent) +{ + NMSecretAgentSimplePrivate *priv = NM_SECRET_AGENT_SIMPLE_GET_PRIVATE(agent); + + G_STATIC_ASSERT_EXPR(G_STRUCT_OFFSET(RequestData, request_id) == 0); + priv->requests = g_hash_table_new_full(nm_pstr_hash, nm_pstr_equal, NULL, _request_data_free); +} + +/** + * nm_secret_agent_simple_new: + * @name: the identifier of secret agent + * + * Creates a new #NMSecretAgentSimple. It does not serve any requests until + * nm_secret_agent_simple_enable() is called. + * + * Returns: a new #NMSecretAgentSimple if the agent creation is successful + * or %NULL in case of a failure. + */ +NMSecretAgentSimple * +nm_secret_agent_simple_new(const char *name) +{ + return g_initable_new(NM_TYPE_SECRET_AGENT_SIMPLE, + NULL, + NULL, + NM_SECRET_AGENT_OLD_IDENTIFIER, + name, + NM_SECRET_AGENT_OLD_CAPABILITIES, + NM_SECRET_AGENT_CAPABILITY_VPN_HINTS, + NULL); +} + +static void +dispose(GObject *object) +{ + NMSecretAgentSimplePrivate *priv = NM_SECRET_AGENT_SIMPLE_GET_PRIVATE(object); + gs_free_error GError *error = NULL; + GHashTableIter iter; + RequestData * request; + + g_hash_table_iter_init(&iter, priv->requests); + while (g_hash_table_iter_next(&iter, NULL, (gpointer *) &request)) { + if (!error) + nm_utils_error_set_cancelled(&error, TRUE, "NMSecretAgentSimple"); + _request_data_complete(request, NULL, error, &iter); + } + + G_OBJECT_CLASS(nm_secret_agent_simple_parent_class)->dispose(object); +} + +static void +finalize(GObject *object) +{ + NMSecretAgentSimplePrivate *priv = NM_SECRET_AGENT_SIMPLE_GET_PRIVATE(object); + + g_hash_table_destroy(priv->requests); + + g_free(priv->path); + + G_OBJECT_CLASS(nm_secret_agent_simple_parent_class)->finalize(object); +} + +void +nm_secret_agent_simple_class_init(NMSecretAgentSimpleClass *klass) +{ + GObjectClass * object_class = G_OBJECT_CLASS(klass); + NMSecretAgentOldClass *agent_class = NM_SECRET_AGENT_OLD_CLASS(klass); + + object_class->dispose = dispose; + object_class->finalize = finalize; + + agent_class->get_secrets = get_secrets; + agent_class->cancel_get_secrets = cancel_get_secrets; + agent_class->save_secrets = save_secrets; + agent_class->delete_secrets = delete_secrets; + + /** + * NMSecretAgentSimple::request-secrets: + * @agent: the #NMSecretAgentSimple + * @request_id: request ID, to eventually pass to + * nm_secret_agent_simple_response(). + * @title: a title for the password dialog + * @prompt: a prompt message for the password dialog + * @secrets: (element-type #NMSecretAgentSimpleSecret): array of secrets + * being requested. + * + * Emitted when the agent requires secrets from the user. + * + * The application should ask user for the secrets. For example, + * nmtui should create a password dialog (#NmtPasswordDialog) + * with the given title and prompt, and an entry for each + * element of @secrets. If any of the secrets already have a + * <literal>value</literal> filled in, the corresponding entry + * should be initialized to that value. + * + * When the dialog is complete, the app must call + * nm_secret_agent_simple_response() with the results. + */ + signals[REQUEST_SECRETS] = g_signal_new(NM_SECRET_AGENT_SIMPLE_REQUEST_SECRETS, + G_TYPE_FROM_CLASS(klass), + 0, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 4, + G_TYPE_STRING, /* request_id */ + G_TYPE_STRING, /* title */ + G_TYPE_STRING, /* prompt */ + G_TYPE_PTR_ARRAY); +} diff --git a/src/libnmc-base/nm-secret-agent-simple.h b/src/libnmc-base/nm-secret-agent-simple.h new file mode 100644 index 0000000000..878f9c75c0 --- /dev/null +++ b/src/libnmc-base/nm-secret-agent-simple.h @@ -0,0 +1,61 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2013 - 2015 Red Hat, Inc. + */ + +#ifndef __NM_SECRET_AGENT_SIMPLE_H__ +#define __NM_SECRET_AGENT_SIMPLE_H__ + +#include "nm-secret-agent-old.h" + +typedef enum { + NM_SECRET_AGENT_SECRET_TYPE_PROPERTY, + NM_SECRET_AGENT_SECRET_TYPE_SECRET, + NM_SECRET_AGENT_SECRET_TYPE_VPN_SECRET, + NM_SECRET_AGENT_SECRET_TYPE_WIREGUARD_PEER_PSK, +} NMSecretAgentSecretType; + +typedef struct { + NMSecretAgentSecretType secret_type; + const char * pretty_name; + const char * entry_id; + char * value; + const char * vpn_type; + bool is_secret : 1; + bool no_prompt_entry_id : 1; +} NMSecretAgentSimpleSecret; + +#define NM_SECRET_AGENT_ENTRY_ID_PREFX_VPN_SECRETS "vpn.secrets." + +#define NM_SECRET_AGENT_VPN_TYPE_OPENCONNECT NM_DBUS_INTERFACE ".openconnect" + +/*****************************************************************************/ + +#define NM_TYPE_SECRET_AGENT_SIMPLE (nm_secret_agent_simple_get_type()) +#define NM_SECRET_AGENT_SIMPLE(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_SECRET_AGENT_SIMPLE, NMSecretAgentSimple)) +#define NM_SECRET_AGENT_SIMPLE_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_SECRET_AGENT_SIMPLE, NMSecretAgentSimpleClass)) +#define NM_IS_SECRET_AGENT_SIMPLE(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_SECRET_AGENT_SIMPLE)) +#define NM_IS_SECRET_AGENT_SIMPLE_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_SECRET_AGENT_SIMPLE)) +#define NM_SECRET_AGENT_SIMPLE_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_SECRET_AGENT_SIMPLE, NMSecretAgentSimpleClass)) + +#define NM_SECRET_AGENT_SIMPLE_REQUEST_SECRETS "request-secrets" + +typedef struct _NMSecretAgentSimple NMSecretAgentSimple; +typedef struct _NMSecretAgentSimpleClass NMSecretAgentSimpleClass; + +GType nm_secret_agent_simple_get_type(void); + +NMSecretAgentSimple *nm_secret_agent_simple_new(const char *name); + +void nm_secret_agent_simple_response(NMSecretAgentSimple *self, + const char * request_id, + GPtrArray * secrets); + +void nm_secret_agent_simple_enable(NMSecretAgentSimple *self, const char *path); + +#endif /* __NM_SECRET_AGENT_SIMPLE_H__ */ diff --git a/src/libnmc-base/nm-vpn-helpers.c b/src/libnmc-base/nm-vpn-helpers.c new file mode 100644 index 0000000000..72691e34c2 --- /dev/null +++ b/src/libnmc-base/nm-vpn-helpers.c @@ -0,0 +1,821 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2013 - 2015 Red Hat, Inc. + */ + +/** + * SECTION:nm-vpn-helpers + * @short_description: VPN-related utilities + */ + +#include "libnm-client-aux-extern/nm-default-client.h" + +#include "nm-vpn-helpers.h" + +#include <arpa/inet.h> +#include <net/if.h> + +#include "nm-client-utils.h" +#include "nm-utils.h" +#include "libnm-glib-aux/nm-io-utils.h" +#include "libnm-glib-aux/nm-secret-utils.h" + +/*****************************************************************************/ + +NMVpnEditorPlugin * +nm_vpn_get_editor_plugin(const char *service_type, GError **error) +{ + NMVpnEditorPlugin *plugin = NULL; + NMVpnPluginInfo * plugin_info; + gs_free_error GError *local = NULL; + + g_return_val_if_fail(service_type, NULL); + g_return_val_if_fail(error == NULL || *error == NULL, NULL); + + plugin_info = nm_vpn_plugin_info_list_find_by_service(nm_vpn_get_plugin_infos(), service_type); + + if (!plugin_info) { + g_set_error(error, + NM_VPN_PLUGIN_ERROR, + NM_VPN_PLUGIN_ERROR_FAILED, + _("unknown VPN plugin \"%s\""), + service_type); + return NULL; + } + plugin = nm_vpn_plugin_info_get_editor_plugin(plugin_info); + if (!plugin) + plugin = nm_vpn_plugin_info_load_editor_plugin(plugin_info, &local); + + if (!plugin) { + if (!nm_vpn_plugin_info_get_plugin(plugin_info) + && nm_vpn_plugin_info_lookup_property(plugin_info, + NM_VPN_PLUGIN_INFO_KF_GROUP_GNOME, + "properties")) { + g_set_error(error, + NM_VPN_PLUGIN_ERROR, + NM_VPN_PLUGIN_ERROR_FAILED, + _("cannot load legacy-only VPN plugin \"%s\" for \"%s\""), + nm_vpn_plugin_info_get_name(plugin_info), + nm_vpn_plugin_info_get_filename(plugin_info)); + } else if (g_error_matches(local, G_FILE_ERROR, G_FILE_ERROR_NOENT)) { + g_set_error( + error, + NM_VPN_PLUGIN_ERROR, + NM_VPN_PLUGIN_ERROR_FAILED, + _("cannot load VPN plugin \"%s\" due to missing \"%s\". Missing client plugin?"), + nm_vpn_plugin_info_get_name(plugin_info), + nm_vpn_plugin_info_get_plugin(plugin_info)); + } else { + g_set_error(error, + NM_VPN_PLUGIN_ERROR, + NM_VPN_PLUGIN_ERROR_FAILED, + _("failed to load VPN plugin \"%s\": %s"), + nm_vpn_plugin_info_get_name(plugin_info), + local->message); + } + return NULL; + } + + return plugin; +} + +GSList * +nm_vpn_get_plugin_infos(void) +{ + static bool plugins_loaded; + static GSList *plugins = NULL; + + if (G_LIKELY(plugins_loaded)) + return plugins; + plugins_loaded = TRUE; + plugins = nm_vpn_plugin_info_list_load(); + return plugins; +} + +gboolean +nm_vpn_supports_ipv6(NMConnection *connection) +{ + NMSettingVpn * s_vpn; + const char * service_type; + NMVpnEditorPlugin *plugin; + guint32 capabilities; + + s_vpn = nm_connection_get_setting_vpn(connection); + g_return_val_if_fail(s_vpn != NULL, FALSE); + + service_type = nm_setting_vpn_get_service_type(s_vpn); + if (!service_type) + return FALSE; + + plugin = nm_vpn_get_editor_plugin(service_type, NULL); + if (!plugin) + return FALSE; + + capabilities = nm_vpn_editor_plugin_get_capabilities(plugin); + return NM_FLAGS_HAS(capabilities, NM_VPN_EDITOR_PLUGIN_CAPABILITY_IPV6); +} + +const NmcVpnPasswordName * +nm_vpn_get_secret_names(const char *service_type) +{ + const char *type; + + if (!service_type) + return NULL; + + if (!NM_STR_HAS_PREFIX(service_type, NM_DBUS_INTERFACE) + || service_type[NM_STRLEN(NM_DBUS_INTERFACE)] != '.') { + /* all our well-known, hard-coded vpn-types start with NM_DBUS_INTERFACE. */ + return NULL; + } + + type = service_type + (NM_STRLEN(NM_DBUS_INTERFACE) + 1); + +#define _VPN_PASSWORD_LIST(...) \ + ({ \ + static const NmcVpnPasswordName _arr[] = { \ + __VA_ARGS__{0}, \ + }; \ + _arr; \ + }) + + if (NM_IN_STRSET(type, "pptp", "iodine", "ssh", "l2tp", "fortisslvpn")) { + return _VPN_PASSWORD_LIST({"password", N_("Password")}, ); + } + + if (NM_IN_STRSET(type, "openvpn")) { + return _VPN_PASSWORD_LIST({"password", N_("Password")}, + {"cert-pass", N_("Certificate password")}, + {"http-proxy-password", N_("HTTP proxy password")}, ); + } + + if (NM_IN_STRSET(type, "vpnc")) { + return _VPN_PASSWORD_LIST({"Xauth password", N_("Password")}, + {"IPSec secret", N_("Group password")}, ); + }; + + if (NM_IN_STRSET(type, "openswan", "libreswan", "strongswan")) { + return _VPN_PASSWORD_LIST({"xauthpassword", N_("Password")}, + {"pskvalue", N_("Group password")}, ); + }; + + if (NM_IN_STRSET(type, "openconnect")) { + return _VPN_PASSWORD_LIST({"gateway", N_("Gateway")}, + {"cookie", N_("Cookie")}, + {"gwcert", N_("Gateway certificate hash")}, ); + }; + + return NULL; +} + +static gboolean +_extract_variable_value(char *line, const char *tag, char **value) +{ + char *p1, *p2; + + if (!g_str_has_prefix(line, tag)) + return FALSE; + + p1 = line + strlen(tag); + p2 = line + strlen(line) - 1; + if ((*p1 == '\'' || *p1 == '"') && (*p1 == *p2)) { + p1++; + *p2 = '\0'; + } + NM_SET_OUT(value, g_strdup(p1)); + return TRUE; +} + +gboolean +nm_vpn_openconnect_authenticate_helper(const char *host, + char ** cookie, + char ** gateway, + char ** gwcert, + int * status, + GError ** error) +{ + gs_free char * output = NULL; + gs_free const char **output_v = NULL; + const char *const * iter; + const char * path; + const char *const DEFAULT_PATHS[] = { + "/sbin/", + "/usr/sbin/", + "/usr/local/sbin/", + "/bin/", + "/usr/bin/", + "/usr/local/bin/", + NULL, + }; + + path = nm_utils_file_search_in_paths("openconnect", + "/usr/sbin/openconnect", + DEFAULT_PATHS, + G_FILE_TEST_IS_EXECUTABLE, + NULL, + NULL, + error); + if (!path) + return FALSE; + + if (!g_spawn_sync(NULL, + (char **) NM_MAKE_STRV(path, "--authenticate", host), + NULL, + G_SPAWN_SEARCH_PATH | G_SPAWN_CHILD_INHERITS_STDIN, + NULL, + NULL, + &output, + NULL, + status, + error)) + return FALSE; + + /* Parse output and set cookie, gateway and gwcert + * output example: + * COOKIE='loremipsum' + * HOST='1.2.3.4' + * FINGERPRINT='sha1:32bac90cf09a722e10ecc1942c67fe2ac8c21e2e' + */ + output_v = nm_utils_strsplit_set_with_empty(output, "\r\n"); + for (iter = output_v; iter && *iter; iter++) { + char *s_mutable = (char *) *iter; + + _extract_variable_value(s_mutable, "COOKIE=", cookie); + _extract_variable_value(s_mutable, "HOST=", gateway); + _extract_variable_value(s_mutable, "FINGERPRINT=", gwcert); + } + + return TRUE; +} + +static gboolean +_wg_complete_peer(GPtrArray ** p_peers, + NMWireGuardPeer *peer_take, + gsize peer_start_line_nr, + const char * filename, + GError ** error) +{ + nm_auto_unref_wgpeer NMWireGuardPeer *peer = peer_take; + gs_free_error GError *local = NULL; + + if (!peer) + return TRUE; + + if (!nm_wireguard_peer_is_valid(peer, TRUE, TRUE, &local)) { + nm_utils_error_set(error, + NM_UTILS_ERROR_UNKNOWN, + _("Invalid peer starting at %s:%zu: %s"), + filename, + peer_start_line_nr, + local->message); + return FALSE; + } + + if (!*p_peers) + *p_peers = g_ptr_array_new_with_free_func((GDestroyNotify) nm_wireguard_peer_unref); + g_ptr_array_add(*p_peers, g_steal_pointer(&peer)); + return TRUE; +} + +static gboolean +_line_match(char *line, const char *key, gsize key_len, const char **out_key, char **out_value) +{ + nm_assert(line); + nm_assert(key); + nm_assert(strlen(key) == key_len); + nm_assert(!strchr(key, '=')); + nm_assert(out_key && !*out_key); + nm_assert(out_value && !*out_value); + + /* Note that `wg-quick` (linux.bash) does case-insensitive comparison (shopt -s nocasematch). + * `wg setconf` does case-insensitive comparison too (with strncasecmp, which is locale dependent). + * + * We do a case-insensitive comparison of the key, however in a locale-independent manner. */ + + if (g_ascii_strncasecmp(line, key, key_len) != 0) + return FALSE; + + if (line[key_len] != '=') + return FALSE; + + *out_key = key; + *out_value = &line[key_len + 1]; + return TRUE; +} + +#define line_match(line, key, out_key, out_value) \ + _line_match((line), "" key "", NM_STRLEN(key), (out_key), (out_value)) + +static gboolean +value_split_word(char **line_remainder, char **out_word) +{ + char *str; + + if ((*line_remainder)[0] == '\0') + return FALSE; + + *out_word = *line_remainder; + + str = strchrnul(*line_remainder, ','); + if (str[0] == ',') { + str[0] = '\0'; + *line_remainder = &str[1]; + } else + *line_remainder = str; + return TRUE; +} + +NMConnection * +nm_vpn_wireguard_import(const char *filename, GError **error) +{ + nm_auto_clear_secret_ptr NMSecretPtr file_content = NM_SECRET_PTR_INIT(); + char ifname[IFNAMSIZ]; + gs_free char * uuid = NULL; + gboolean ifname_valid = FALSE; + const char * cstr; + char * line_remainder; + gs_unref_object NMConnection *connection = NULL; + NMSettingConnection * s_con; + NMSettingIPConfig * s_ip4; + NMSettingIPConfig * s_ip6; + NMSettingWireGuard * s_wg; + gs_free_error GError *local = NULL; + enum { + LINE_CONTEXT_INIT, + LINE_CONTEXT_INTERFACE, + LINE_CONTEXT_PEER, + } line_context; + gsize line_nr; + gsize current_peer_start_line_nr = 0; + nm_auto_unref_wgpeer NMWireGuardPeer *current_peer = NULL; + gs_unref_ptrarray GPtrArray *data_dns_search = NULL; + gs_unref_ptrarray GPtrArray *data_dns_v4 = NULL; + gs_unref_ptrarray GPtrArray *data_dns_v6 = NULL; + gs_unref_ptrarray GPtrArray *data_addr_v4 = NULL; + gs_unref_ptrarray GPtrArray *data_addr_v6 = NULL; + gs_unref_ptrarray GPtrArray *data_peers = NULL; + const char * data_private_key = NULL; + gint64 data_table; + guint data_listen_port = 0; + guint data_fwmark = 0; + guint data_mtu = 0; + int is_v4; + guint i; + + g_return_val_if_fail(filename, NULL); + g_return_val_if_fail(!error || !*error, NULL); + + /* contrary to "wg-quick", we never interpret the filename as "/etc/wireguard/$INTERFACE.conf". + * If the filename has no '/', it is interpreted as relative to the current working directory. + * However, we do require a suitable filename suffix and that the name corresponds to the interface + * name. */ + cstr = strrchr(filename, '/'); + cstr = cstr ? &cstr[1] : filename; + if (NM_STR_HAS_SUFFIX(cstr, ".conf")) { + gsize len = strlen(cstr) - NM_STRLEN(".conf"); + + if (len > 0 && len < sizeof(ifname)) { + memcpy(ifname, cstr, len); + ifname[len] = '\0'; + + if (nm_utils_ifname_valid(ifname, NMU_IFACE_KERNEL, NULL)) + ifname_valid = TRUE; + } + } + if (!ifname_valid) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_UNKNOWN, + _("The name of the WireGuard config must be a valid interface " + "name followed by \".conf\"")); + return FALSE; + } + + if (!nm_utils_file_get_contents(-1, + filename, + 10 * 1024 * 1024, + NM_UTILS_FILE_GET_CONTENTS_FLAG_SECRET, + &file_content.str, + &file_content.len, + NULL, + error)) + return NULL; + + /* We interpret the file like `wg-quick up` and `wg setconf` do. + * + * Of course the WireGuard scripts do something fundamentlly different. They + * perform actions to configure the WireGuard link in kernel, add routes and + * addresses, and call resolvconf. It all happens at the time when the script + * run. + * + * This code here instead generates a NetworkManager connection profile so that + * NetworkManager will apply a similar configuration when later activating the profile. */ + +#define _TABLE_AUTO ((gint64) -1) +#define _TABLE_OFF ((gint64) -2) + + data_table = _TABLE_AUTO; + + line_remainder = file_content.str; + line_context = LINE_CONTEXT_INIT; + line_nr = 0; + while (line_remainder[0] != '\0') { + const char *matched_key = NULL; + char * value = NULL; + char * line; + char ch; + gint64 i64; + + line_nr++; + + line = line_remainder; + line_remainder = strchrnul(line, '\n'); + if (line_remainder[0] != '\0') + (line_remainder++)[0] = '\0'; + + /* Drop all spaces and truncate at first '#'. + * See wg's config_read_line(). + * + * Note that wg-quick doesn't do that. + * + * Neither `wg setconf` nor `wg-quick` does a strict parsing. + * We don't either. Just try to interpret the file (mostly) the same as + * they would. + */ + { + gsize l, n; + + n = 0; + for (l = 0; (ch = line[l]); l++) { + if (g_ascii_isspace(ch)) { + /* wg-setconf strips all whitespace before parsing the content. That means, + * *[I nterface]" will be accepted. We do that too. */ + continue; + } + if (ch == '#') + break; + line[n++] = line[l]; + } + if (n == 0) + continue; + line[n] = '\0'; + } + + if (g_ascii_strcasecmp(line, "[Interface]") == 0) { + if (!_wg_complete_peer(&data_peers, + g_steal_pointer(¤t_peer), + current_peer_start_line_nr, + filename, + error)) + return FALSE; + line_context = LINE_CONTEXT_INTERFACE; + continue; + } + + if (g_ascii_strcasecmp(line, "[Peer]") == 0) { + if (!_wg_complete_peer(&data_peers, + g_steal_pointer(¤t_peer), + current_peer_start_line_nr, + filename, + error)) + return FALSE; + current_peer_start_line_nr = line_nr; + current_peer = nm_wireguard_peer_new(); + line_context = LINE_CONTEXT_PEER; + continue; + } + + if (line_context == LINE_CONTEXT_INTERFACE) { + if (line_match(line, "Address", &matched_key, &value)) { + char *value_word; + + while (value_split_word(&value, &value_word)) { + GPtrArray **p_data_addr; + NMIPAddr addr_bin; + int addr_family; + int prefix_len; + + if (!nm_utils_parse_inaddr_prefix_bin(AF_UNSPEC, + value_word, + &addr_family, + &addr_bin, + &prefix_len)) + goto fail_invalid_value; + + p_data_addr = (addr_family == AF_INET) ? &data_addr_v4 : &data_addr_v6; + + if (!*p_data_addr) + *p_data_addr = + g_ptr_array_new_with_free_func((GDestroyNotify) nm_ip_address_unref); + + g_ptr_array_add( + *p_data_addr, + nm_ip_address_new_binary( + addr_family, + &addr_bin, + prefix_len == -1 ? ((addr_family == AF_INET) ? 32 : 128) : prefix_len, + NULL)); + } + continue; + } + + if (line_match(line, "MTU", &matched_key, &value)) { + i64 = _nm_utils_ascii_str_to_int64(value, 0, 0, G_MAXUINT32, -1); + if (i64 == -1) + goto fail_invalid_value; + + /* wg-quick accepts the "MTU" value, but it also fetches routes to + * autodetect it. NetworkManager won't do that, we can only configure + * an explicit MTU or no autodetection will be performed. */ + data_mtu = i64; + continue; + } + + if (line_match(line, "DNS", &matched_key, &value)) { + char *value_word; + + while (value_split_word(&value, &value_word)) { + GPtrArray **p_data_dns; + NMIPAddr addr_bin; + int addr_family; + + if (nm_utils_parse_inaddr_bin(AF_UNSPEC, value_word, &addr_family, &addr_bin)) { + p_data_dns = (addr_family == AF_INET) ? &data_dns_v4 : &data_dns_v6; + if (!*p_data_dns) + *p_data_dns = g_ptr_array_new_with_free_func(g_free); + + g_ptr_array_add(*p_data_dns, + nm_utils_inet_ntop_dup(addr_family, &addr_bin)); + continue; + } + + if (!data_dns_search) + data_dns_search = g_ptr_array_new_with_free_func(g_free); + g_ptr_array_add(data_dns_search, g_strdup(value_word)); + } + continue; + } + + if (line_match(line, "Table", &matched_key, &value)) { + if (nm_streq(value, "auto")) + data_table = _TABLE_AUTO; + else if (nm_streq(value, "off")) + data_table = _TABLE_OFF; + else { + /* we don't support table names from /etc/iproute2/rt_tables + * But we accept hex like `ip route add` would. */ + i64 = _nm_utils_ascii_str_to_int64(value, 0, 0, G_MAXINT32, -1); + if (i64 == -1) + goto fail_invalid_value; + data_table = i64; + } + continue; + } + + if (line_match(line, "PreUp", &matched_key, &value) + || line_match(line, "PreDown", &matched_key, &value) + || line_match(line, "PostUp", &matched_key, &value) + || line_match(line, "PostDown", &matched_key, &value)) { + /* we don't run any scripts. Silently ignore these parameters. */ + continue; + } + + if (line_match(line, "SaveConfig", &matched_key, &value)) { + /* we ignore the setting, but enforce that it's either true or false (like + * wg-quick. */ + if (!NM_IN_STRSET(value, "true", "false")) + goto fail_invalid_value; + continue; + } + + if (line_match(line, "ListenPort", &matched_key, &value)) { + /* we don't use getaddrinfo(), unlike `wg setconf`. Just interpret + * the port as plain decimal number. */ + i64 = _nm_utils_ascii_str_to_int64(value, 10, 0, 0xFFFF, -1); + if (i64 == -1) + goto fail_invalid_value; + data_listen_port = i64; + continue; + } + + if (line_match(line, "FwMark", &matched_key, &value)) { + if (nm_streq(value, "off")) + data_fwmark = 0; + else { + i64 = _nm_utils_ascii_str_to_int64(value, 0, 0, G_MAXINT32, -1); + if (i64 == -1) + goto fail_invalid_value; + data_fwmark = i64; + } + continue; + } + + if (line_match(line, "PrivateKey", &matched_key, &value)) { + if (!nm_utils_base64secret_decode(value, NM_WIREGUARD_PUBLIC_KEY_LEN, NULL)) + goto fail_invalid_secret; + data_private_key = value; + continue; + } + + goto fail_invalid_line; + } + + if (line_context == LINE_CONTEXT_PEER) { + if (line_match(line, "Endpoint", &matched_key, &value)) { + if (!nm_wireguard_peer_set_endpoint(current_peer, value, FALSE)) + goto fail_invalid_value; + continue; + } + + if (line_match(line, "PublicKey", &matched_key, &value)) { + if (!nm_wireguard_peer_set_public_key(current_peer, value, FALSE)) + goto fail_invalid_value; + continue; + } + + if (line_match(line, "AllowedIPs", &matched_key, &value)) { + char *value_word; + + while (value_split_word(&value, &value_word)) { + if (!nm_wireguard_peer_append_allowed_ip(current_peer, value_word, FALSE)) + goto fail_invalid_value; + } + continue; + } + + if (line_match(line, "PersistentKeepalive", &matched_key, &value)) { + if (nm_streq(value, "off")) + i64 = 0; + else { + i64 = _nm_utils_ascii_str_to_int64(value, 10, 0, G_MAXUINT16, -1); + if (i64 == -1) + goto fail_invalid_value; + } + nm_wireguard_peer_set_persistent_keepalive(current_peer, i64); + continue; + } + + if (line_match(line, "PresharedKey", &matched_key, &value)) { + if (!nm_wireguard_peer_set_preshared_key(current_peer, value, FALSE)) + goto fail_invalid_secret; + nm_wireguard_peer_set_preshared_key_flags(current_peer, + NM_SETTING_SECRET_FLAG_NONE); + continue; + } + + goto fail_invalid_line; + } + +fail_invalid_line: + nm_utils_error_set(error, + NM_UTILS_ERROR_INVALID_ARGUMENT, + _("unrecognized line at %s:%zu"), + filename, + line_nr); + return FALSE; +fail_invalid_value: + nm_utils_error_set(error, + NM_UTILS_ERROR_INVALID_ARGUMENT, + _("invalid value for '%s' at %s:%zu"), + matched_key, + filename, + line_nr); + return FALSE; +fail_invalid_secret: + nm_utils_error_set(error, + NM_UTILS_ERROR_INVALID_ARGUMENT, + _("invalid secret '%s' at %s:%zu"), + matched_key, + filename, + line_nr); + return FALSE; + } + + if (!_wg_complete_peer(&data_peers, + g_steal_pointer(¤t_peer), + current_peer_start_line_nr, + filename, + error)) + return FALSE; + + connection = nm_simple_connection_new(); + s_con = NM_SETTING_CONNECTION(nm_setting_connection_new()); + nm_connection_add_setting(connection, NM_SETTING(s_con)); + s_ip4 = NM_SETTING_IP_CONFIG(nm_setting_ip4_config_new()); + nm_connection_add_setting(connection, NM_SETTING(s_ip4)); + s_ip6 = NM_SETTING_IP_CONFIG(nm_setting_ip6_config_new()); + nm_connection_add_setting(connection, NM_SETTING(s_ip6)); + s_wg = NM_SETTING_WIREGUARD(nm_setting_wireguard_new()); + nm_connection_add_setting(connection, NM_SETTING(s_wg)); + + uuid = nm_utils_uuid_generate(); + + g_object_set(s_con, + NM_SETTING_CONNECTION_ID, + ifname, + NM_SETTING_CONNECTION_UUID, + uuid, + NM_SETTING_CONNECTION_TYPE, + NM_SETTING_WIREGUARD_SETTING_NAME, + NM_SETTING_CONNECTION_INTERFACE_NAME, + ifname, + NULL); + + g_object_set(s_wg, + NM_SETTING_WIREGUARD_PRIVATE_KEY, + data_private_key, + NM_SETTING_WIREGUARD_LISTEN_PORT, + data_listen_port, + NM_SETTING_WIREGUARD_FWMARK, + data_fwmark, + NM_SETTING_WIREGUARD_MTU, + data_mtu, + NULL); + + if (data_peers) { + for (i = 0; i < data_peers->len; i++) + nm_setting_wireguard_append_peer(s_wg, data_peers->pdata[i]); + } + + for (is_v4 = 0; is_v4 < 2; is_v4++) { + const char *method_disabled = + is_v4 ? NM_SETTING_IP4_CONFIG_METHOD_DISABLED : NM_SETTING_IP6_CONFIG_METHOD_DISABLED; + const char *method_manual = + is_v4 ? NM_SETTING_IP4_CONFIG_METHOD_MANUAL : NM_SETTING_IP6_CONFIG_METHOD_MANUAL; + NMSettingIPConfig *s_ip = is_v4 ? s_ip4 : s_ip6; + GPtrArray * data_dns = is_v4 ? data_dns_v4 : data_dns_v6; + GPtrArray * data_addr = is_v4 ? data_addr_v4 : data_addr_v6; + GPtrArray * data_dns_search2 = data_dns_search; + + if (data_dns && !data_addr) { + /* When specifying "DNS", we also require an "Address" for the same address + * family. That is because a NMSettingIPConfig cannot have @method_disabled + * and DNS settings at the same time. + * + * We don't have addresses. Silently ignore the DNS setting. */ + data_dns = NULL; + data_dns_search2 = NULL; + } + + g_object_set(s_ip, + NM_SETTING_IP_CONFIG_METHOD, + data_addr ? method_manual : method_disabled, + NULL); + + /* For WireGuard profiles, always set dns-priority to a negative value, + * so that DNS servers on other profiles get ignored. This is also what + * wg-quick does, by calling `resolvconf -x`. */ + g_object_set(s_ip, NM_SETTING_IP_CONFIG_DNS_PRIORITY, (int) -50, NULL); + + if (data_addr) { + for (i = 0; i < data_addr->len; i++) + nm_setting_ip_config_add_address(s_ip, data_addr->pdata[i]); + } + if (data_dns) { + for (i = 0; i < data_dns->len; i++) + nm_setting_ip_config_add_dns(s_ip, data_dns->pdata[i]); + + /* Of the wg-quick doesn't specify a search domain, assume the user + * wants to use the domain server for all searches. */ + if (!data_dns_search2) + nm_setting_ip_config_add_dns_search(s_ip, "~"); + } + if (data_dns_search2) { + for (i = 0; i < data_dns_search2->len; i++) + nm_setting_ip_config_add_dns_search(s_ip, data_dns_search2->pdata[i]); + } + + if (data_table == _TABLE_AUTO) { + /* in the "auto" setting, wg-quick adds peer-routes automatically to the main + * table. NetworkManager will do that too, but there are differences: + * + * - NetworkManager (contrary to wg-quick) does not check whether the peer-route is necessary. + * It will always add a route for each allowed-ips range, even if there is already another + * route that would ensure packets to the endpoint are routed via the WireGuard interface. + * If you don't want that, disable "wireguard.peer-routes", and add the necessary routes + * yourself to "ipv4.routes" and "ipv6.routes". + * + * - With "auto", wg-quick also configures policy routing to handle default-routes (/0) to + * avoid routing loops. + * The imported connection profile will have wireguard.ip4-auto-default-route and + * wireguard.ip6-auto-default-route set to "default". It will thus configure wg-quick's + * policy routing if the profile has any AllowedIPs ranges with /0. + */ + } else if (data_table == _TABLE_OFF) { + if (is_v4) { + g_object_set(s_wg, NM_SETTING_WIREGUARD_PEER_ROUTES, FALSE, NULL); + } + } else { + g_object_set(s_ip, NM_SETTING_IP_CONFIG_ROUTE_TABLE, (guint) data_table, NULL); + } + } + + if (!nm_connection_normalize(connection, NULL, NULL, &local)) { + nm_utils_error_set(error, + NM_UTILS_ERROR_INVALID_ARGUMENT, + _("Failed to create WireGuard connection: %s"), + local->message); + return FALSE; + } + + return g_steal_pointer(&connection); +} diff --git a/src/libnmc-base/nm-vpn-helpers.h b/src/libnmc-base/nm-vpn-helpers.h new file mode 100644 index 0000000000..1cf06743df --- /dev/null +++ b/src/libnmc-base/nm-vpn-helpers.h @@ -0,0 +1,31 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2013 - 2015 Red Hat, Inc. + */ + +#ifndef __NM_VPN_HELPERS_H__ +#define __NM_VPN_HELPERS_H__ + +typedef struct { + const char *name; + const char *ui_name; +} NmcVpnPasswordName; + +GSList *nm_vpn_get_plugin_infos(void); + +NMVpnEditorPlugin *nm_vpn_get_editor_plugin(const char *service_type, GError **error); + +gboolean nm_vpn_supports_ipv6(NMConnection *connection); + +const NmcVpnPasswordName *nm_vpn_get_secret_names(const char *service_type); + +gboolean nm_vpn_openconnect_authenticate_helper(const char *host, + char ** cookie, + char ** gateway, + char ** gwcert, + int * status, + GError ** error); + +NMConnection *nm_vpn_wireguard_import(const char *filename, GError **error); + +#endif /* __NM_VPN_HELPERS_H__ */ diff --git a/src/libnmc-base/qrcodegen.c b/src/libnmc-base/qrcodegen.c new file mode 100644 index 0000000000..2c40fcb94a --- /dev/null +++ b/src/libnmc-base/qrcodegen.c @@ -0,0 +1,1141 @@ +/* + * QR Code generator library (C) + * + * Copyright (c) Project Nayuki. (MIT License) + * https://www.nayuki.io/page/qr-code-generator-library + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * - The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * - The Software is provided "as is", without warranty of any kind, express or + * implied, including but not limited to the warranties of merchantability, + * fitness for a particular purpose and noninfringement. In no event shall the + * authors or copyright holders be liable for any claim, damages or other + * liability, whether in an action of contract, tort or otherwise, arising from, + * out of or in connection with the Software or the use or other dealings in the + * Software. + */ + +#include <assert.h> +#include <limits.h> +#include <stdlib.h> +#include <string.h> +#include "qrcodegen.h" + +#ifndef QRCODEGEN_TEST + #define testable static // Keep functions private +#else + #define testable // Expose private functions +#endif + +/*---- Forward declarations for private functions ----*/ + +// Regarding all public and private functions defined in this source file: +// - They require all pointer/array arguments to be not null unless the array length is zero. +// - They only read input scalar/array arguments, write to output pointer/array +// arguments, and return scalar values; they are "pure" functions. +// - They don't read mutable global variables or write to any global variables. +// - They don't perform I/O, read the clock, print to console, etc. +// - They allocate a small and constant amount of stack memory. +// - They don't allocate or free any memory on the heap. +// - They don't recurse or mutually recurse. All the code +// could be inlined into the top-level public functions. +// - They run in at most quadratic time with respect to input arguments. +// Most functions run in linear time, and some in constant time. +// There are no unbounded loops or non-obvious termination conditions. +// - They are completely thread-safe if the caller does not give the +// same writable buffer to concurrent calls to these functions. + +testable void appendBitsToBuffer(unsigned int val, int numBits, uint8_t buffer[], int *bitLen); + +testable void +addEccAndInterleave(uint8_t data[], int version, enum qrcodegen_Ecc ecl, uint8_t result[]); +testable int getNumDataCodewords(int version, enum qrcodegen_Ecc ecl); +testable int getNumRawDataModules(int ver); + +testable void calcReedSolomonGenerator(int degree, uint8_t result[]); +testable void calcReedSolomonRemainder(const uint8_t data[], + int dataLen, + const uint8_t generator[], + int degree, + uint8_t result[]); +testable uint8_t finiteFieldMultiply(uint8_t x, uint8_t y); + +testable void initializeFunctionModules(int version, uint8_t qrcode[]); +static void drawWhiteFunctionModules(uint8_t qrcode[], int version); +static void drawFormatBits(enum qrcodegen_Ecc ecl, enum qrcodegen_Mask mask, uint8_t qrcode[]); +testable int getAlignmentPatternPositions(int version, uint8_t result[7]); +static void fillRectangle(int left, int top, int width, int height, uint8_t qrcode[]); + +static void drawCodewords(const uint8_t data[], int dataLen, uint8_t qrcode[]); +static void applyMask(const uint8_t functionModules[], uint8_t qrcode[], enum qrcodegen_Mask mask); +static long getPenaltyScore(const uint8_t qrcode[]); +static void addRunToHistory(unsigned char run, unsigned char history[7]); +static bool hasFinderLikePattern(unsigned char runHistory[7]); + +testable bool getModule(const uint8_t qrcode[], int x, int y); +testable void setModule(uint8_t qrcode[], int x, int y, bool isBlack); +testable void setModuleBounded(uint8_t qrcode[], int x, int y, bool isBlack); +static bool getBit(int x, int i); + +testable int calcSegmentBitLength(enum qrcodegen_Mode mode, size_t numChars); +testable int getTotalBits(const struct qrcodegen_Segment segs[], size_t len, int version); +static int numCharCountBits(enum qrcodegen_Mode mode, int version); + +/*---- Private tables of constants ----*/ + +// The set of all legal characters in alphanumeric mode, where each character +// value maps to the index in the string. For checking text and encoding segments. +static const char *ALPHANUMERIC_CHARSET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:"; + +// For generating error correction codes. +testable const int8_t ECC_CODEWORDS_PER_BLOCK[4][41] = { + // Version: (note that index 0 is for padding, and is set to an illegal value) + //0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40 Error correction level + {-1, 7, 10, 15, 20, 26, 18, 20, 24, 30, 18, 20, 24, 26, 30, 22, 24, 28, 30, 28, 28, + 28, 28, 30, 30, 26, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30}, // Low + {-1, 10, 16, 26, 18, 24, 16, 18, 22, 22, 26, 30, 22, 22, 24, 24, 28, 28, 26, 26, 26, + 26, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28}, // Medium + {-1, 13, 22, 18, 26, 18, 24, 18, 22, 20, 24, 28, 26, 24, 20, 30, 24, 28, 28, 26, 30, + 28, 30, 30, 30, 30, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30}, // Quartile + {-1, 17, 28, 22, 16, 22, 28, 26, 26, 24, 28, 24, 28, 22, 24, 24, 30, 28, 28, 26, 28, + 30, 24, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30}, // High +}; + +#define qrcodegen_REED_SOLOMON_DEGREE_MAX 30 // Based on the table above + +// For generating error correction codes. +testable const int8_t NUM_ERROR_CORRECTION_BLOCKS[4][41] = { + // Version: (note that index 0 is for padding, and is set to an illegal value) + //0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40 Error correction level + {-1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 4, 4, 4, 4, 4, 6, 6, 6, 6, 7, 8, + 8, 9, 9, 10, 12, 12, 12, 13, 14, 15, 16, 17, 18, 19, 19, 20, 21, 22, 24, 25}, // Low + {-1, 1, 1, 1, 2, 2, 4, 4, 4, 5, 5, 5, 8, 9, 9, 10, 10, 11, 13, 14, 16, + 17, 17, 18, 20, 21, 23, 25, 26, 28, 29, 31, 33, 35, 37, 38, 40, 43, 45, 47, 49}, // Medium + {-1, 1, 1, 2, 2, 4, 4, 6, 6, 8, 8, 8, 10, 12, 16, 12, 17, 16, 18, 21, 20, + 23, 23, 25, 27, 29, 34, 34, 35, 38, 40, 43, 45, 48, 51, 53, 56, 59, 62, 65, 68}, // Quartile + {-1, 1, 1, 2, 4, 4, 4, 5, 6, 8, 8, 11, 11, 16, 16, 18, 16, 19, 21, 25, 25, + 25, 34, 30, 32, 35, 37, 40, 42, 45, 48, 51, 54, 57, 60, 63, 66, 70, 74, 77, 81}, // High +}; + +// For automatic mask pattern selection. +static const int PENALTY_N1 = 3; +static const int PENALTY_N2 = 3; +static const int PENALTY_N3 = 40; +static const int PENALTY_N4 = 10; + +/*---- High-level QR Code encoding functions ----*/ + +// Public function - see documentation comment in header file. +bool +qrcodegen_encodeText(const char * text, + uint8_t tempBuffer[], + uint8_t qrcode[], + enum qrcodegen_Ecc ecl, + int minVersion, + int maxVersion, + enum qrcodegen_Mask mask, + bool boostEcl) +{ + size_t textLen = strlen(text); + if (textLen == 0) + return qrcodegen_encodeSegmentsAdvanced(NULL, + 0, + ecl, + minVersion, + maxVersion, + mask, + boostEcl, + tempBuffer, + qrcode); + size_t bufLen = qrcodegen_BUFFER_LEN_FOR_VERSION(maxVersion); + + struct qrcodegen_Segment seg; + if (qrcodegen_isNumeric(text)) { + if (qrcodegen_calcSegmentBufferSize(qrcodegen_Mode_NUMERIC, textLen) > bufLen) + goto fail; + seg = qrcodegen_makeNumeric(text, tempBuffer); + } else if (qrcodegen_isAlphanumeric(text)) { + if (qrcodegen_calcSegmentBufferSize(qrcodegen_Mode_ALPHANUMERIC, textLen) > bufLen) + goto fail; + seg = qrcodegen_makeAlphanumeric(text, tempBuffer); + } else { + if (textLen > bufLen) + goto fail; + for (size_t i = 0; i < textLen; i++) + tempBuffer[i] = (uint8_t) text[i]; + seg.mode = qrcodegen_Mode_BYTE; + seg.bitLength = calcSegmentBitLength(seg.mode, textLen); + if (seg.bitLength == -1) + goto fail; + seg.numChars = (int) textLen; + seg.data = tempBuffer; + } + return qrcodegen_encodeSegmentsAdvanced(&seg, + 1, + ecl, + minVersion, + maxVersion, + mask, + boostEcl, + tempBuffer, + qrcode); + +fail: + qrcode[0] = 0; // Set size to invalid value for safety + return false; +} + +// Public function - see documentation comment in header file. +bool +qrcodegen_encodeBinary(uint8_t dataAndTemp[], + size_t dataLen, + uint8_t qrcode[], + enum qrcodegen_Ecc ecl, + int minVersion, + int maxVersion, + enum qrcodegen_Mask mask, + bool boostEcl) +{ + struct qrcodegen_Segment seg; + seg.mode = qrcodegen_Mode_BYTE; + seg.bitLength = calcSegmentBitLength(seg.mode, dataLen); + if (seg.bitLength == -1) { + qrcode[0] = 0; // Set size to invalid value for safety + return false; + } + seg.numChars = (int) dataLen; + seg.data = dataAndTemp; + return qrcodegen_encodeSegmentsAdvanced(&seg, + 1, + ecl, + minVersion, + maxVersion, + mask, + boostEcl, + dataAndTemp, + qrcode); +} + +// Appends the given number of low-order bits of the given value to the given byte-based +// bit buffer, increasing the bit length. Requires 0 <= numBits <= 16 and val < 2^numBits. +testable void +appendBitsToBuffer(unsigned int val, int numBits, uint8_t buffer[], int *bitLen) +{ + assert(0 <= numBits && numBits <= 16 && (unsigned long) val >> numBits == 0); + for (int i = numBits - 1; i >= 0; i--, (*bitLen)++) + buffer[*bitLen >> 3] |= ((val >> i) & 1) << (7 - (*bitLen & 7)); +} + +/*---- Low-level QR Code encoding functions ----*/ + +// Public function - see documentation comment in header file. +bool +qrcodegen_encodeSegments(const struct qrcodegen_Segment segs[], + size_t len, + enum qrcodegen_Ecc ecl, + uint8_t tempBuffer[], + uint8_t qrcode[]) +{ + return qrcodegen_encodeSegmentsAdvanced(segs, + len, + ecl, + qrcodegen_VERSION_MIN, + qrcodegen_VERSION_MAX, + -1, + true, + tempBuffer, + qrcode); +} + +// Public function - see documentation comment in header file. +bool +qrcodegen_encodeSegmentsAdvanced(const struct qrcodegen_Segment segs[], + size_t len, + enum qrcodegen_Ecc ecl, + int minVersion, + int maxVersion, + int mask, + bool boostEcl, + uint8_t tempBuffer[], + uint8_t qrcode[]) +{ + assert(segs != NULL || len == 0); + assert(qrcodegen_VERSION_MIN <= minVersion && minVersion <= maxVersion + && maxVersion <= qrcodegen_VERSION_MAX); + assert(0 <= (int) ecl && (int) ecl <= 3 && -1 <= (int) mask && (int) mask <= 7); + + // Find the minimal version number to use + int version, dataUsedBits; + for (version = minVersion;; version++) { + int dataCapacityBits = + getNumDataCodewords(version, ecl) * 8; // Number of data bits available + dataUsedBits = getTotalBits(segs, len, version); + if (dataUsedBits != -1 && dataUsedBits <= dataCapacityBits) + break; // This version number is found to be suitable + if (version >= maxVersion) { // All versions in the range could not fit the given data + qrcode[0] = 0; // Set size to invalid value for safety + return false; + } + } + assert(dataUsedBits != -1); + + // Increase the error correction level while the data still fits in the current version number + for (int i = (int) qrcodegen_Ecc_MEDIUM; i <= (int) qrcodegen_Ecc_HIGH; + i++) { // From low to high + if (boostEcl && dataUsedBits <= getNumDataCodewords(version, (enum qrcodegen_Ecc) i) * 8) + ecl = (enum qrcodegen_Ecc) i; + } + + // Concatenate all segments to create the data bit string + memset(qrcode, 0, qrcodegen_BUFFER_LEN_FOR_VERSION(version) * sizeof(qrcode[0])); + int bitLen = 0; + for (size_t i = 0; i < len; i++) { + const struct qrcodegen_Segment *seg = &segs[i]; + appendBitsToBuffer((int) seg->mode, 4, qrcode, &bitLen); + appendBitsToBuffer(seg->numChars, numCharCountBits(seg->mode, version), qrcode, &bitLen); + for (int j = 0; j < seg->bitLength; j++) + appendBitsToBuffer((seg->data[j >> 3] >> (7 - (j & 7))) & 1, 1, qrcode, &bitLen); + } + assert(bitLen == dataUsedBits); + + // Add terminator and pad up to a byte if applicable + int dataCapacityBits = getNumDataCodewords(version, ecl) * 8; + assert(bitLen <= dataCapacityBits); + int terminatorBits = dataCapacityBits - bitLen; + if (terminatorBits > 4) + terminatorBits = 4; + appendBitsToBuffer(0, terminatorBits, qrcode, &bitLen); + appendBitsToBuffer(0, (8 - bitLen % 8) % 8, qrcode, &bitLen); + assert(bitLen % 8 == 0); + + // Pad with alternating bytes until data capacity is reached + for (uint8_t padByte = 0xEC; bitLen < dataCapacityBits; padByte ^= 0xEC ^ 0x11) + appendBitsToBuffer(padByte, 8, qrcode, &bitLen); + + // Draw function and data codeword modules + addEccAndInterleave(qrcode, version, ecl, tempBuffer); + initializeFunctionModules(version, qrcode); + drawCodewords(tempBuffer, getNumRawDataModules(version) / 8, qrcode); + drawWhiteFunctionModules(qrcode, version); + initializeFunctionModules(version, tempBuffer); + + // Handle masking + if (mask == qrcodegen_Mask_AUTO) { // Automatically choose best mask + long minPenalty = LONG_MAX; + for (int i = 0; i < 8; i++) { + enum qrcodegen_Mask msk = (enum qrcodegen_Mask) i; + drawFormatBits(ecl, msk, qrcode); + applyMask(tempBuffer, qrcode, msk); + long penalty = getPenaltyScore(qrcode); + if (penalty < minPenalty) { + mask = msk; + minPenalty = penalty; + } + applyMask(tempBuffer, qrcode, msk); // Undoes the mask due to XOR + } + } + assert(0 <= (int) mask && (int) mask <= 7); + drawFormatBits(ecl, mask, qrcode); + applyMask(tempBuffer, qrcode, mask); + return true; +} + +/*---- Error correction code generation functions ----*/ + +// Appends error correction bytes to each block of the given data array, then interleaves +// bytes from the blocks and stores them in the result array. data[0 : dataLen] contains +// the input data. data[dataLen : rawCodewords] is used as a temporary work area and will +// be clobbered by this function. The final answer is stored in result[0 : rawCodewords]. +testable void +addEccAndInterleave(uint8_t data[], int version, enum qrcodegen_Ecc ecl, uint8_t result[]) +{ + // Calculate parameter numbers + assert(0 <= (int) ecl && (int) ecl < 4 && qrcodegen_VERSION_MIN <= version + && version <= qrcodegen_VERSION_MAX); + int numBlocks = NUM_ERROR_CORRECTION_BLOCKS[(int) ecl][version]; + int blockEccLen = ECC_CODEWORDS_PER_BLOCK[(int) ecl][version]; + int rawCodewords = getNumRawDataModules(version) / 8; + int dataLen = getNumDataCodewords(version, ecl); + int numShortBlocks = numBlocks - rawCodewords % numBlocks; + int shortBlockDataLen = rawCodewords / numBlocks - blockEccLen; + + // Split data into blocks, calculate ECC, and interleave + // (not concatenate) the bytes into a single sequence + uint8_t generator[qrcodegen_REED_SOLOMON_DEGREE_MAX]; + calcReedSolomonGenerator(blockEccLen, generator); + const uint8_t *dat = data; + for (int i = 0; i < numBlocks; i++) { + int datLen = shortBlockDataLen + (i < numShortBlocks ? 0 : 1); + uint8_t *ecc = &data[dataLen]; // Temporary storage + calcReedSolomonRemainder(dat, datLen, generator, blockEccLen, ecc); + for (int j = 0, k = i; j < datLen; j++, k += numBlocks) { // Copy data + if (j == shortBlockDataLen) + k -= numShortBlocks; + result[k] = dat[j]; + } + for (int j = 0, k = dataLen + i; j < blockEccLen; j++, k += numBlocks) // Copy ECC + result[k] = ecc[j]; + dat += datLen; + } +} + +// Returns the number of 8-bit codewords that can be used for storing data (not ECC), +// for the given version number and error correction level. The result is in the range [9, 2956]. +testable int +getNumDataCodewords(int version, enum qrcodegen_Ecc ecl) +{ + int v = version, e = (int) ecl; + assert(0 <= e && e < 4); + return getNumRawDataModules(v) / 8 + - ECC_CODEWORDS_PER_BLOCK[e][v] * NUM_ERROR_CORRECTION_BLOCKS[e][v]; +} + +// Returns the number of data bits that can be stored in a QR Code of the given version number, after +// all function modules are excluded. This includes remainder bits, so it might not be a multiple of 8. +// The result is in the range [208, 29648]. This could be implemented as a 40-entry lookup table. +testable int +getNumRawDataModules(int ver) +{ + assert(qrcodegen_VERSION_MIN <= ver && ver <= qrcodegen_VERSION_MAX); + int result = (16 * ver + 128) * ver + 64; + if (ver >= 2) { + int numAlign = ver / 7 + 2; + result -= (25 * numAlign - 10) * numAlign - 55; + if (ver >= 7) + result -= 36; + } + return result; +} + +/*---- Reed-Solomon ECC generator functions ----*/ + +// Calculates the Reed-Solomon generator polynomial of the given degree, storing in result[0 : degree]. +testable void +calcReedSolomonGenerator(int degree, uint8_t result[]) +{ + // Start with the monomial x^0 + assert(1 <= degree && degree <= qrcodegen_REED_SOLOMON_DEGREE_MAX); + memset(result, 0, degree * sizeof(result[0])); + result[degree - 1] = 1; + + // Compute the product polynomial (x - r^0) * (x - r^1) * (x - r^2) * ... * (x - r^{degree-1}), + // drop the highest term, and store the rest of the coefficients in order of descending powers. + // Note that r = 0x02, which is a generator element of this field GF(2^8/0x11D). + uint8_t root = 1; + for (int i = 0; i < degree; i++) { + // Multiply the current product by (x - r^i) + for (int j = 0; j < degree; j++) { + result[j] = finiteFieldMultiply(result[j], root); + if (j + 1 < degree) + result[j] ^= result[j + 1]; + } + root = finiteFieldMultiply(root, 0x02); + } +} + +// Calculates the remainder of the polynomial data[0 : dataLen] when divided by the generator[0 : degree], where all +// polynomials are in big endian and the generator has an implicit leading 1 term, storing the result in result[0 : degree]. +testable void +calcReedSolomonRemainder(const uint8_t data[], + int dataLen, + const uint8_t generator[], + int degree, + uint8_t result[]) +{ + // Perform polynomial division + assert(1 <= degree && degree <= qrcodegen_REED_SOLOMON_DEGREE_MAX); + memset(result, 0, degree * sizeof(result[0])); + for (int i = 0; i < dataLen; i++) { + uint8_t factor = data[i] ^ result[0]; + memmove(&result[0], &result[1], (degree - 1) * sizeof(result[0])); + result[degree - 1] = 0; + for (int j = 0; j < degree; j++) + result[j] ^= finiteFieldMultiply(generator[j], factor); + } +} + +#undef qrcodegen_REED_SOLOMON_DEGREE_MAX + +// Returns the product of the two given field elements modulo GF(2^8/0x11D). +// All inputs are valid. This could be implemented as a 256*256 lookup table. +testable uint8_t +finiteFieldMultiply(uint8_t x, uint8_t y) +{ + // Russian peasant multiplication + uint8_t z = 0; + for (int i = 7; i >= 0; i--) { + z = (z << 1) ^ ((z >> 7) * 0x11D); + z ^= ((y >> i) & 1) * x; + } + return z; +} + +/*---- Drawing function modules ----*/ + +// Clears the given QR Code grid with white modules for the given +// version's size, then marks every function module as black. +testable void +initializeFunctionModules(int version, uint8_t qrcode[]) +{ + // Initialize QR Code + int qrsize = version * 4 + 17; + memset(qrcode, 0, ((qrsize * qrsize + 7) / 8 + 1) * sizeof(qrcode[0])); + qrcode[0] = (uint8_t) qrsize; + + // Fill horizontal and vertical timing patterns + fillRectangle(6, 0, 1, qrsize, qrcode); + fillRectangle(0, 6, qrsize, 1, qrcode); + + // Fill 3 finder patterns (all corners except bottom right) and format bits + fillRectangle(0, 0, 9, 9, qrcode); + fillRectangle(qrsize - 8, 0, 8, 9, qrcode); + fillRectangle(0, qrsize - 8, 9, 8, qrcode); + + // Fill numerous alignment patterns + uint8_t alignPatPos[7]; + int numAlign = getAlignmentPatternPositions(version, alignPatPos); + for (int i = 0; i < numAlign; i++) { + for (int j = 0; j < numAlign; j++) { + // Don't draw on the three finder corners + if (!((i == 0 && j == 0) || (i == 0 && j == numAlign - 1) + || (i == numAlign - 1 && j == 0))) + fillRectangle(alignPatPos[i] - 2, alignPatPos[j] - 2, 5, 5, qrcode); + } + } + + // Fill version blocks + if (version >= 7) { + fillRectangle(qrsize - 11, 0, 3, 6, qrcode); + fillRectangle(0, qrsize - 11, 6, 3, qrcode); + } +} + +// Draws white function modules and possibly some black modules onto the given QR Code, without changing +// non-function modules. This does not draw the format bits. This requires all function modules to be previously +// marked black (namely by initializeFunctionModules()), because this may skip redrawing black function modules. +static void +drawWhiteFunctionModules(uint8_t qrcode[], int version) +{ + // Draw horizontal and vertical timing patterns + int qrsize = qrcodegen_getSize(qrcode); + for (int i = 7; i < qrsize - 7; i += 2) { + setModule(qrcode, 6, i, false); + setModule(qrcode, i, 6, false); + } + + // Draw 3 finder patterns (all corners except bottom right; overwrites some timing modules) + for (int dy = -4; dy <= 4; dy++) { + for (int dx = -4; dx <= 4; dx++) { + int dist = abs(dx); + if (abs(dy) > dist) + dist = abs(dy); + if (dist == 2 || dist == 4) { + setModuleBounded(qrcode, 3 + dx, 3 + dy, false); + setModuleBounded(qrcode, qrsize - 4 + dx, 3 + dy, false); + setModuleBounded(qrcode, 3 + dx, qrsize - 4 + dy, false); + } + } + } + + // Draw numerous alignment patterns + uint8_t alignPatPos[7]; + int numAlign = getAlignmentPatternPositions(version, alignPatPos); + for (int i = 0; i < numAlign; i++) { + for (int j = 0; j < numAlign; j++) { + if ((i == 0 && j == 0) || (i == 0 && j == numAlign - 1) + || (i == numAlign - 1 && j == 0)) + continue; // Don't draw on the three finder corners + for (int dy = -1; dy <= 1; dy++) { + for (int dx = -1; dx <= 1; dx++) + setModule(qrcode, alignPatPos[i] + dx, alignPatPos[j] + dy, dx == 0 && dy == 0); + } + } + } + + // Draw version blocks + if (version >= 7) { + // Calculate error correction code and pack bits + int rem = version; // version is uint6, in the range [7, 40] + for (int i = 0; i < 12; i++) + rem = (rem << 1) ^ ((rem >> 11) * 0x1F25); + long bits = (long) version << 12 | rem; // uint18 + assert(bits >> 18 == 0); + + // Draw two copies + for (int i = 0; i < 6; i++) { + for (int j = 0; j < 3; j++) { + int k = qrsize - 11 + j; + setModule(qrcode, k, i, (bits & 1) != 0); + setModule(qrcode, i, k, (bits & 1) != 0); + bits >>= 1; + } + } + } +} + +// Draws two copies of the format bits (with its own error correction code) based +// on the given mask and error correction level. This always draws all modules of +// the format bits, unlike drawWhiteFunctionModules() which might skip black modules. +static void +drawFormatBits(enum qrcodegen_Ecc ecl, enum qrcodegen_Mask mask, uint8_t qrcode[]) +{ + // Calculate error correction code and pack bits + assert(0 <= (int) mask && (int) mask <= 7); + static const int table[] = {1, 0, 3, 2}; + int data = table[(int) ecl] << 3 | (int) mask; // errCorrLvl is uint2, mask is uint3 + int rem = data; + for (int i = 0; i < 10; i++) + rem = (rem << 1) ^ ((rem >> 9) * 0x537); + int bits = (data << 10 | rem) ^ 0x5412; // uint15 + assert(bits >> 15 == 0); + + // Draw first copy + for (int i = 0; i <= 5; i++) + setModule(qrcode, 8, i, getBit(bits, i)); + setModule(qrcode, 8, 7, getBit(bits, 6)); + setModule(qrcode, 8, 8, getBit(bits, 7)); + setModule(qrcode, 7, 8, getBit(bits, 8)); + for (int i = 9; i < 15; i++) + setModule(qrcode, 14 - i, 8, getBit(bits, i)); + + // Draw second copy + int qrsize = qrcodegen_getSize(qrcode); + for (int i = 0; i < 8; i++) + setModule(qrcode, qrsize - 1 - i, 8, getBit(bits, i)); + for (int i = 8; i < 15; i++) + setModule(qrcode, 8, qrsize - 15 + i, getBit(bits, i)); + setModule(qrcode, 8, qrsize - 8, true); // Always black +} + +// Calculates and stores an ascending list of positions of alignment patterns +// for this version number, returning the length of the list (in the range [0,7]). +// Each position is in the range [0,177), and are used on both the x and y axes. +// This could be implemented as lookup table of 40 variable-length lists of unsigned bytes. +testable int +getAlignmentPatternPositions(int version, uint8_t result[7]) +{ + if (version == 1) + return 0; + int numAlign = version / 7 + 2; + int step = (version == 32) ? 26 : (version * 4 + numAlign * 2 + 1) / (numAlign * 2 - 2) * 2; + for (int i = numAlign - 1, pos = version * 4 + 10; i >= 1; i--, pos -= step) + result[i] = pos; + result[0] = 6; + return numAlign; +} + +// Sets every pixel in the range [left : left + width] * [top : top + height] to black. +static void +fillRectangle(int left, int top, int width, int height, uint8_t qrcode[]) +{ + for (int dy = 0; dy < height; dy++) { + for (int dx = 0; dx < width; dx++) + setModule(qrcode, left + dx, top + dy, true); + } +} + +/*---- Drawing data modules and masking ----*/ + +// Draws the raw codewords (including data and ECC) onto the given QR Code. This requires the initial state of +// the QR Code to be black at function modules and white at codeword modules (including unused remainder bits). +static void +drawCodewords(const uint8_t data[], int dataLen, uint8_t qrcode[]) +{ + int qrsize = qrcodegen_getSize(qrcode); + int i = 0; // Bit index into the data + // Do the funny zigzag scan + for (int right = qrsize - 1; right >= 1; + right -= 2) { // Index of right column in each column pair + if (right == 6) + right = 5; + for (int vert = 0; vert < qrsize; vert++) { // Vertical counter + for (int j = 0; j < 2; j++) { + int x = right - j; // Actual x coordinate + bool upward = ((right + 1) & 2) == 0; + int y = upward ? qrsize - 1 - vert : vert; // Actual y coordinate + if (!getModule(qrcode, x, y) && i < dataLen * 8) { + bool black = getBit(data[i >> 3], 7 - (i & 7)); + setModule(qrcode, x, y, black); + i++; + } + // If this QR Code has any remainder bits (0 to 7), they were assigned as + // 0/false/white by the constructor and are left unchanged by this method + } + } + } + assert(i == dataLen * 8); +} + +// XORs the codeword modules in this QR Code with the given mask pattern. +// The function modules must be marked and the codeword bits must be drawn +// before masking. Due to the arithmetic of XOR, calling applyMask() with +// the same mask value a second time will undo the mask. A final well-formed +// QR Code needs exactly one (not zero, two, etc.) mask applied. +static void +applyMask(const uint8_t functionModules[], uint8_t qrcode[], enum qrcodegen_Mask mask) +{ + assert(0 <= (int) mask && (int) mask <= 7); // Disallows qrcodegen_Mask_AUTO + int qrsize = qrcodegen_getSize(qrcode); + for (int y = 0; y < qrsize; y++) { + for (int x = 0; x < qrsize; x++) { + if (getModule(functionModules, x, y)) + continue; + bool invert; + switch ((int) mask) { + case 0: + invert = (x + y) % 2 == 0; + break; + case 1: + invert = y % 2 == 0; + break; + case 2: + invert = x % 3 == 0; + break; + case 3: + invert = (x + y) % 3 == 0; + break; + case 4: + invert = (x / 3 + y / 2) % 2 == 0; + break; + case 5: + invert = x * y % 2 + x * y % 3 == 0; + break; + case 6: + invert = (x * y % 2 + x * y % 3) % 2 == 0; + break; + case 7: + invert = ((x + y) % 2 + x * y % 3) % 2 == 0; + break; + default: + assert(false); + return; + } + bool val = getModule(qrcode, x, y); + setModule(qrcode, x, y, val ^ invert); + } + } +} + +// Calculates and returns the penalty score based on state of the given QR Code's current modules. +// This is used by the automatic mask choice algorithm to find the mask pattern that yields the lowest score. +static long +getPenaltyScore(const uint8_t qrcode[]) +{ + int qrsize = qrcodegen_getSize(qrcode); + long result = 0; + + // Adjacent modules in row having same color, and finder-like patterns + for (int y = 0; y < qrsize; y++) { + unsigned char runHistory[7] = {0}; + bool color = false; + unsigned char runX = 0; + for (int x = 0; x < qrsize; x++) { + if (getModule(qrcode, x, y) == color) { + runX++; + if (runX == 5) + result += PENALTY_N1; + else if (runX > 5) + result++; + } else { + addRunToHistory(runX, runHistory); + if (!color && hasFinderLikePattern(runHistory)) + result += PENALTY_N3; + color = getModule(qrcode, x, y); + runX = 1; + } + } + addRunToHistory(runX, runHistory); + if (color) + addRunToHistory(0, runHistory); // Dummy run of white + if (hasFinderLikePattern(runHistory)) + result += PENALTY_N3; + } + // Adjacent modules in column having same color, and finder-like patterns + for (int x = 0; x < qrsize; x++) { + unsigned char runHistory[7] = {0}; + bool color = false; + unsigned char runY = 0; + for (int y = 0; y < qrsize; y++) { + if (getModule(qrcode, x, y) == color) { + runY++; + if (runY == 5) + result += PENALTY_N1; + else if (runY > 5) + result++; + } else { + addRunToHistory(runY, runHistory); + if (!color && hasFinderLikePattern(runHistory)) + result += PENALTY_N3; + color = getModule(qrcode, x, y); + runY = 1; + } + } + addRunToHistory(runY, runHistory); + if (color) + addRunToHistory(0, runHistory); // Dummy run of white + if (hasFinderLikePattern(runHistory)) + result += PENALTY_N3; + } + + // 2*2 blocks of modules having same color + for (int y = 0; y < qrsize - 1; y++) { + for (int x = 0; x < qrsize - 1; x++) { + bool color = getModule(qrcode, x, y); + if (color == getModule(qrcode, x + 1, y) && color == getModule(qrcode, x, y + 1) + && color == getModule(qrcode, x + 1, y + 1)) + result += PENALTY_N2; + } + } + + // Balance of black and white modules + int black = 0; + for (int y = 0; y < qrsize; y++) { + for (int x = 0; x < qrsize; x++) { + if (getModule(qrcode, x, y)) + black++; + } + } + int total = qrsize * qrsize; // Note that size is odd, so black/total != 1/2 + // Compute the smallest integer k >= 0 such that (45-5k)% <= black/total <= (55+5k)% + int k = (int) ((labs(black * 20L - total * 10L) + total - 1) / total) - 1; + result += k * PENALTY_N4; + return result; +} + +// Inserts the given value to the front of the given array, which shifts over the +// existing values and deletes the last value. A helper function for getPenaltyScore(). +static void +addRunToHistory(unsigned char run, unsigned char history[7]) +{ + memmove(&history[1], &history[0], 6 * sizeof(history[0])); + history[0] = run; +} + +// Tests whether the given run history has the pattern of ratio 1:1:3:1:1 in the middle, and +// surrounded by at least 4 on either or both ends. A helper function for getPenaltyScore(). +// Must only be called immediately after a run of white modules has ended. +static bool +hasFinderLikePattern(unsigned char runHistory[7]) +{ + unsigned char n = runHistory[1]; + // The maximum QR Code size is 177, hence the run length n <= 177. + // Arithmetic is promoted to int, so n*4 will not overflow. + return n > 0 && runHistory[2] == n && runHistory[4] == n && runHistory[5] == n + && runHistory[3] == n * 3 && (runHistory[0] >= n * 4 || runHistory[6] >= n * 4); +} + +/*---- Basic QR Code information ----*/ + +// Public function - see documentation comment in header file. +int +qrcodegen_getSize(const uint8_t qrcode[]) +{ + assert(qrcode != NULL); + int result = qrcode[0]; + assert((qrcodegen_VERSION_MIN * 4 + 17) <= result + && result <= (qrcodegen_VERSION_MAX * 4 + 17)); + return result; +} + +// Public function - see documentation comment in header file. +bool +qrcodegen_getModule(const uint8_t qrcode[], int x, int y) +{ + assert(qrcode != NULL); + int qrsize = qrcode[0]; + return (0 <= x && x < qrsize && 0 <= y && y < qrsize) && getModule(qrcode, x, y); +} + +// Gets the module at the given coordinates, which must be in bounds. +testable bool +getModule(const uint8_t qrcode[], int x, int y) +{ + int qrsize = qrcode[0]; + assert(21 <= qrsize && qrsize <= 177 && 0 <= x && x < qrsize && 0 <= y && y < qrsize); + int index = y * qrsize + x; + return getBit(qrcode[(index >> 3) + 1], index & 7); +} + +// Sets the module at the given coordinates, which must be in bounds. +testable void +setModule(uint8_t qrcode[], int x, int y, bool isBlack) +{ + int qrsize = qrcode[0]; + assert(21 <= qrsize && qrsize <= 177 && 0 <= x && x < qrsize && 0 <= y && y < qrsize); + int index = y * qrsize + x; + int bitIndex = index & 7; + int byteIndex = (index >> 3) + 1; + if (isBlack) + qrcode[byteIndex] |= 1 << bitIndex; + else + qrcode[byteIndex] &= (1 << bitIndex) ^ 0xFF; +} + +// Sets the module at the given coordinates, doing nothing if out of bounds. +testable void +setModuleBounded(uint8_t qrcode[], int x, int y, bool isBlack) +{ + int qrsize = qrcode[0]; + if (0 <= x && x < qrsize && 0 <= y && y < qrsize) + setModule(qrcode, x, y, isBlack); +} + +// Returns true iff the i'th bit of x is set to 1. Requires x >= 0 and 0 <= i <= 14. +static bool +getBit(int x, int i) +{ + return ((x >> i) & 1) != 0; +} + +/*---- Segment handling ----*/ + +// Public function - see documentation comment in header file. +bool +qrcodegen_isAlphanumeric(const char *text) +{ + assert(text != NULL); + for (; *text != '\0'; text++) { + if (strchr(ALPHANUMERIC_CHARSET, *text) == NULL) + return false; + } + return true; +} + +// Public function - see documentation comment in header file. +bool +qrcodegen_isNumeric(const char *text) +{ + assert(text != NULL); + for (; *text != '\0'; text++) { + if (*text < '0' || *text > '9') + return false; + } + return true; +} + +// Public function - see documentation comment in header file. +size_t +qrcodegen_calcSegmentBufferSize(enum qrcodegen_Mode mode, size_t numChars) +{ + int temp = calcSegmentBitLength(mode, numChars); + if (temp == -1) + return SIZE_MAX; + assert(0 <= temp && temp <= INT16_MAX); + return ((size_t) temp + 7) / 8; +} + +// Returns the number of data bits needed to represent a segment +// containing the given number of characters using the given mode. Notes: +// - Returns -1 on failure, i.e. numChars > INT16_MAX or +// the number of needed bits exceeds INT16_MAX (i.e. 32767). +// - Otherwise, all valid results are in the range [0, INT16_MAX]. +// - For byte mode, numChars measures the number of bytes, not Unicode code points. +// - For ECI mode, numChars must be 0, and the worst-case number of bits is returned. +// An actual ECI segment can have shorter data. For non-ECI modes, the result is exact. +testable int +calcSegmentBitLength(enum qrcodegen_Mode mode, size_t numChars) +{ + // All calculations are designed to avoid overflow on all platforms + if (numChars > (unsigned int) INT16_MAX) + return -1; + long result = (long) numChars; + if (mode == qrcodegen_Mode_NUMERIC) + result = (result * 10 + 2) / 3; // ceil(10/3 * n) + else if (mode == qrcodegen_Mode_ALPHANUMERIC) + result = (result * 11 + 1) / 2; // ceil(11/2 * n) + else if (mode == qrcodegen_Mode_BYTE) + result *= 8; + else if (mode == qrcodegen_Mode_KANJI) + result *= 13; + else if (mode == qrcodegen_Mode_ECI && numChars == 0) + result = 3 * 8; + else { // Invalid argument + assert(false); + return -1; + } + assert(result >= 0); + if (result > (unsigned int) INT16_MAX) + return -1; + return (int) result; +} + +// Public function - see documentation comment in header file. +struct qrcodegen_Segment +qrcodegen_makeBytes(const uint8_t data[], size_t len, uint8_t buf[]) +{ + assert(data != NULL || len == 0); + struct qrcodegen_Segment result; + result.mode = qrcodegen_Mode_BYTE; + result.bitLength = calcSegmentBitLength(result.mode, len); + assert(result.bitLength != -1); + result.numChars = (int) len; + if (len > 0) + memcpy(buf, data, len * sizeof(buf[0])); + result.data = buf; + return result; +} + +// Public function - see documentation comment in header file. +struct qrcodegen_Segment +qrcodegen_makeNumeric(const char *digits, uint8_t buf[]) +{ + assert(digits != NULL); + struct qrcodegen_Segment result; + size_t len = strlen(digits); + result.mode = qrcodegen_Mode_NUMERIC; + int bitLen = calcSegmentBitLength(result.mode, len); + assert(bitLen != -1); + result.numChars = (int) len; + if (bitLen > 0) + memset(buf, 0, ((size_t) bitLen + 7) / 8 * sizeof(buf[0])); + result.bitLength = 0; + + unsigned int accumData = 0; + int accumCount = 0; + for (; *digits != '\0'; digits++) { + char c = *digits; + assert('0' <= c && c <= '9'); + accumData = accumData * 10 + (unsigned int) (c - '0'); + accumCount++; + if (accumCount == 3) { + appendBitsToBuffer(accumData, 10, buf, &result.bitLength); + accumData = 0; + accumCount = 0; + } + } + if (accumCount > 0) // 1 or 2 digits remaining + appendBitsToBuffer(accumData, accumCount * 3 + 1, buf, &result.bitLength); + assert(result.bitLength == bitLen); + result.data = buf; + return result; +} + +// Public function - see documentation comment in header file. +struct qrcodegen_Segment +qrcodegen_makeAlphanumeric(const char *text, uint8_t buf[]) +{ + assert(text != NULL); + struct qrcodegen_Segment result; + size_t len = strlen(text); + result.mode = qrcodegen_Mode_ALPHANUMERIC; + int bitLen = calcSegmentBitLength(result.mode, len); + assert(bitLen != -1); + result.numChars = (int) len; + if (bitLen > 0) + memset(buf, 0, ((size_t) bitLen + 7) / 8 * sizeof(buf[0])); + result.bitLength = 0; + + unsigned int accumData = 0; + int accumCount = 0; + for (; *text != '\0'; text++) { + const char *temp = strchr(ALPHANUMERIC_CHARSET, *text); + assert(temp != NULL); + accumData = accumData * 45 + (unsigned int) (temp - ALPHANUMERIC_CHARSET); + accumCount++; + if (accumCount == 2) { + appendBitsToBuffer(accumData, 11, buf, &result.bitLength); + accumData = 0; + accumCount = 0; + } + } + if (accumCount > 0) // 1 character remaining + appendBitsToBuffer(accumData, 6, buf, &result.bitLength); + assert(result.bitLength == bitLen); + result.data = buf; + return result; +} + +// Public function - see documentation comment in header file. +struct qrcodegen_Segment +qrcodegen_makeEci(long assignVal, uint8_t buf[]) +{ + struct qrcodegen_Segment result; + result.mode = qrcodegen_Mode_ECI; + result.numChars = 0; + result.bitLength = 0; + if (assignVal < 0) + assert(false); + else if (assignVal < (1 << 7)) { + memset(buf, 0, 1 * sizeof(buf[0])); + appendBitsToBuffer(assignVal, 8, buf, &result.bitLength); + } else if (assignVal < (1 << 14)) { + memset(buf, 0, 2 * sizeof(buf[0])); + appendBitsToBuffer(2, 2, buf, &result.bitLength); + appendBitsToBuffer(assignVal, 14, buf, &result.bitLength); + } else if (assignVal < 1000000L) { + memset(buf, 0, 3 * sizeof(buf[0])); + appendBitsToBuffer(6, 3, buf, &result.bitLength); + appendBitsToBuffer(assignVal >> 10, 11, buf, &result.bitLength); + appendBitsToBuffer(assignVal & 0x3FF, 10, buf, &result.bitLength); + } else + assert(false); + result.data = buf; + return result; +} + +// Calculates the number of bits needed to encode the given segments at the given version. +// Returns a non-negative number if successful. Otherwise, returns -1 if a segment has too +// many characters to fit its length field, or the total bits exceeds INT16_MAX. +testable int +getTotalBits(const struct qrcodegen_Segment segs[], size_t len, int version) +{ + assert(segs != NULL || len == 0); + long result = 0; + for (size_t i = 0; i < len; i++) { + int numChars = segs[i].numChars; + int bitLength = segs[i].bitLength; + assert(0 <= numChars && numChars <= INT16_MAX); + assert(0 <= bitLength && bitLength <= INT16_MAX); + int ccbits = numCharCountBits(segs[i].mode, version); + assert(0 <= ccbits && ccbits <= 16); + if (numChars >= (1L << ccbits)) + return -1; // The segment's length doesn't fit the field's bit width + result += 4L + ccbits + bitLength; + if (result > INT16_MAX) + return -1; // The sum might overflow an int type + } + assert(0 <= result && result <= INT16_MAX); + return (int) result; +} + +// Returns the bit width of the character count field for a segment in the given mode +// in a QR Code at the given version number. The result is in the range [0, 16]. +static int +numCharCountBits(enum qrcodegen_Mode mode, int version) +{ + assert(qrcodegen_VERSION_MIN <= version && version <= qrcodegen_VERSION_MAX); + int i = (version + 7) / 17; + switch (mode) { + case qrcodegen_Mode_NUMERIC: + { + static const int temp[] = {10, 12, 14}; + return temp[i]; + } + case qrcodegen_Mode_ALPHANUMERIC: + { + static const int temp[] = {9, 11, 13}; + return temp[i]; + } + case qrcodegen_Mode_BYTE: + { + static const int temp[] = {8, 16, 16}; + return temp[i]; + } + case qrcodegen_Mode_KANJI: + { + static const int temp[] = {8, 10, 12}; + return temp[i]; + } + case qrcodegen_Mode_ECI: + return 0; + default: + assert(false); + return -1; // Dummy value + } +} diff --git a/src/libnmc-base/qrcodegen.h b/src/libnmc-base/qrcodegen.h new file mode 100644 index 0000000000..b91ae49ab6 --- /dev/null +++ b/src/libnmc-base/qrcodegen.h @@ -0,0 +1,312 @@ +/* + * QR Code generator library (C) + * + * Copyright (c) Project Nayuki. (MIT License) + * https://www.nayuki.io/page/qr-code-generator-library + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * - The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * - The Software is provided "as is", without warranty of any kind, express or + * implied, including but not limited to the warranties of merchantability, + * fitness for a particular purpose and noninfringement. In no event shall the + * authors or copyright holders be liable for any claim, damages or other + * liability, whether in an action of contract, tort or otherwise, arising from, + * out of or in connection with the Software or the use or other dealings in the + * Software. + */ + +#pragma once + +#include <stdbool.h> +#include <stddef.h> +#include <stdint.h> + +#ifdef __cplusplus +extern "C" { +#endif + +/* + * This library creates QR Code symbols, which is a type of two-dimension barcode. + * Invented by Denso Wave and described in the ISO/IEC 18004 standard. + * A QR Code structure is an immutable square grid of black and white cells. + * The library provides functions to create a QR Code from text or binary data. + * The library covers the QR Code Model 2 specification, supporting all versions (sizes) + * from 1 to 40, all 4 error correction levels, and 4 character encoding modes. + * + * Ways to create a QR Code object: + * - High level: Take the payload data and call qrcodegen_encodeText() or qrcodegen_encodeBinary(). + * - Low level: Custom-make the list of segments and call + * qrcodegen_encodeSegments() or qrcodegen_encodeSegmentsAdvanced(). + * (Note that all ways require supplying the desired error correction level and various byte buffers.) + */ + +/*---- Enum and struct types----*/ + +/* + * The error correction level in a QR Code symbol. + */ +enum qrcodegen_Ecc { + // Must be declared in ascending order of error protection + // so that an internal qrcodegen function works properly + qrcodegen_Ecc_LOW = 0, // The QR Code can tolerate about 7% erroneous codewords + qrcodegen_Ecc_MEDIUM, // The QR Code can tolerate about 15% erroneous codewords + qrcodegen_Ecc_QUARTILE, // The QR Code can tolerate about 25% erroneous codewords + qrcodegen_Ecc_HIGH, // The QR Code can tolerate about 30% erroneous codewords +}; + +/* + * The mask pattern used in a QR Code symbol. + */ +enum qrcodegen_Mask { + // A special value to tell the QR Code encoder to + // automatically select an appropriate mask pattern + qrcodegen_Mask_AUTO = -1, + // The eight actual mask patterns + qrcodegen_Mask_0 = 0, + qrcodegen_Mask_1, + qrcodegen_Mask_2, + qrcodegen_Mask_3, + qrcodegen_Mask_4, + qrcodegen_Mask_5, + qrcodegen_Mask_6, + qrcodegen_Mask_7, +}; + +/* + * Describes how a segment's data bits are interpreted. + */ +enum qrcodegen_Mode { + qrcodegen_Mode_NUMERIC = 0x1, + qrcodegen_Mode_ALPHANUMERIC = 0x2, + qrcodegen_Mode_BYTE = 0x4, + qrcodegen_Mode_KANJI = 0x8, + qrcodegen_Mode_ECI = 0x7, +}; + +/* + * A segment of character/binary/control data in a QR Code symbol. + * The mid-level way to create a segment is to take the payload data + * and call a factory function such as qrcodegen_makeNumeric(). + * The low-level way to create a segment is to custom-make the bit buffer + * and initialize a qrcodegen_Segment struct with appropriate values. + * Even in the most favorable conditions, a QR Code can only hold 7089 characters of data. + * Any segment longer than this is meaningless for the purpose of generating QR Codes. + * Moreover, the maximum allowed bit length is 32767 because + * the largest QR Code (version 40) has 31329 modules. + */ +struct qrcodegen_Segment { + // The mode indicator of this segment. + enum qrcodegen_Mode mode; + + // The length of this segment's unencoded data. Measured in characters for + // numeric/alphanumeric/kanji mode, bytes for byte mode, and 0 for ECI mode. + // Always zero or positive. Not the same as the data's bit length. + int numChars; + + // The data bits of this segment, packed in bitwise big endian. + // Can be null if the bit length is zero. + uint8_t *data; + + // The number of valid data bits used in the buffer. Requires + // 0 <= bitLength <= 32767, and bitLength <= (capacity of data array) * 8. + // The character count (numChars) must agree with the mode and the bit buffer length. + int bitLength; +}; + +/*---- Macro constants and functions ----*/ + +#define qrcodegen_VERSION_MIN \ + 1 // The minimum version number supported in the QR Code Model 2 standard +#define qrcodegen_VERSION_MAX \ + 40 // The maximum version number supported in the QR Code Model 2 standard + +// Calculates the number of bytes needed to store any QR Code up to and including the given version number, +// as a compile-time constant. For example, 'uint8_t buffer[qrcodegen_BUFFER_LEN_FOR_VERSION(25)];' +// can store any single QR Code from version 1 to 25 (inclusive). The result fits in an int (or int16). +// Requires qrcodegen_VERSION_MIN <= n <= qrcodegen_VERSION_MAX. +#define qrcodegen_BUFFER_LEN_FOR_VERSION(n) ((((n) *4 + 17) * ((n) *4 + 17) + 7) / 8 + 1) + +// The worst-case number of bytes needed to store one QR Code, up to and including +// version 40. This value equals 3918, which is just under 4 kilobytes. +// Use this more convenient value to avoid calculating tighter memory bounds for buffers. +#define qrcodegen_BUFFER_LEN_MAX qrcodegen_BUFFER_LEN_FOR_VERSION(qrcodegen_VERSION_MAX) + +/*---- Functions (high level) to generate QR Codes ----*/ + +/* + * Encodes the given text string to a QR Code, returning true if encoding succeeded. + * If the data is too long to fit in any version in the given range + * at the given ECC level, then false is returned. + * - The input text must be encoded in UTF-8 and contain no NULs. + * - The variables ecl and mask must correspond to enum constant values. + * - Requires 1 <= minVersion <= maxVersion <= 40. + * - The arrays tempBuffer and qrcode must each have a length + * of at least qrcodegen_BUFFER_LEN_FOR_VERSION(maxVersion). + * - After the function returns, tempBuffer contains no useful data. + * - If successful, the resulting QR Code may use numeric, + * alphanumeric, or byte mode to encode the text. + * - In the most optimistic case, a QR Code at version 40 with low ECC + * can hold any UTF-8 string up to 2953 bytes, or any alphanumeric string + * up to 4296 characters, or any digit string up to 7089 characters. + * These numbers represent the hard upper limit of the QR Code standard. + * - Please consult the QR Code specification for information on + * data capacities per version, ECC level, and text encoding mode. + */ +bool qrcodegen_encodeText(const char * text, + uint8_t tempBuffer[], + uint8_t qrcode[], + enum qrcodegen_Ecc ecl, + int minVersion, + int maxVersion, + enum qrcodegen_Mask mask, + bool boostEcl); + +/* + * Encodes the given binary data to a QR Code, returning true if encoding succeeded. + * If the data is too long to fit in any version in the given range + * at the given ECC level, then false is returned. + * - The input array range dataAndTemp[0 : dataLen] should normally be + * valid UTF-8 text, but is not required by the QR Code standard. + * - The variables ecl and mask must correspond to enum constant values. + * - Requires 1 <= minVersion <= maxVersion <= 40. + * - The arrays dataAndTemp and qrcode must each have a length + * of at least qrcodegen_BUFFER_LEN_FOR_VERSION(maxVersion). + * - After the function returns, the contents of dataAndTemp may have changed, + * and does not represent useful data anymore. + * - If successful, the resulting QR Code will use byte mode to encode the data. + * - In the most optimistic case, a QR Code at version 40 with low ECC can hold any byte + * sequence up to length 2953. This is the hard upper limit of the QR Code standard. + * - Please consult the QR Code specification for information on + * data capacities per version, ECC level, and text encoding mode. + */ +bool qrcodegen_encodeBinary(uint8_t dataAndTemp[], + size_t dataLen, + uint8_t qrcode[], + enum qrcodegen_Ecc ecl, + int minVersion, + int maxVersion, + enum qrcodegen_Mask mask, + bool boostEcl); + +/*---- Functions (low level) to generate QR Codes ----*/ + +/* + * Renders a QR Code representing the given segments at the given error correction level. + * The smallest possible QR Code version is automatically chosen for the output. Returns true if + * QR Code creation succeeded, or false if the data is too long to fit in any version. The ECC level + * of the result may be higher than the ecl argument if it can be done without increasing the version. + * This function allows the user to create a custom sequence of segments that switches + * between modes (such as alphanumeric and byte) to encode text in less space. + * This is a low-level API; the high-level API is qrcodegen_encodeText() and qrcodegen_encodeBinary(). + * To save memory, the segments' data buffers can alias/overlap tempBuffer, and will + * result in them being clobbered, but the QR Code output will still be correct. + * But the qrcode array must not overlap tempBuffer or any segment's data buffer. + */ +bool qrcodegen_encodeSegments(const struct qrcodegen_Segment segs[], + size_t len, + enum qrcodegen_Ecc ecl, + uint8_t tempBuffer[], + uint8_t qrcode[]); + +/* + * Renders a QR Code representing the given segments with the given encoding parameters. + * Returns true if QR Code creation succeeded, or false if the data is too long to fit in the range of versions. + * The smallest possible QR Code version within the given range is automatically + * chosen for the output. Iff boostEcl is true, then the ECC level of the result + * may be higher than the ecl argument if it can be done without increasing the + * version. The mask number is either between 0 to 7 (inclusive) to force that + * mask, or -1 to automatically choose an appropriate mask (which may be slow). + * This function allows the user to create a custom sequence of segments that switches + * between modes (such as alphanumeric and byte) to encode text in less space. + * This is a low-level API; the high-level API is qrcodegen_encodeText() and qrcodegen_encodeBinary(). + * To save memory, the segments' data buffers can alias/overlap tempBuffer, and will + * result in them being clobbered, but the QR Code output will still be correct. + * But the qrcode array must not overlap tempBuffer or any segment's data buffer. + */ +bool qrcodegen_encodeSegmentsAdvanced(const struct qrcodegen_Segment segs[], + size_t len, + enum qrcodegen_Ecc ecl, + int minVersion, + int maxVersion, + int mask, + bool boostEcl, + uint8_t tempBuffer[], + uint8_t qrcode[]); + +/* + * Tests whether the given string can be encoded as a segment in alphanumeric mode. + * A string is encodable iff each character is in the following set: 0 to 9, A to Z + * (uppercase only), space, dollar, percent, asterisk, plus, hyphen, period, slash, colon. + */ +bool qrcodegen_isAlphanumeric(const char *text); + +/* + * Tests whether the given string can be encoded as a segment in numeric mode. + * A string is encodable iff each character is in the range 0 to 9. + */ +bool qrcodegen_isNumeric(const char *text); + +/* + * Returns the number of bytes (uint8_t) needed for the data buffer of a segment + * containing the given number of characters using the given mode. Notes: + * - Returns SIZE_MAX on failure, i.e. numChars > INT16_MAX or + * the number of needed bits exceeds INT16_MAX (i.e. 32767). + * - Otherwise, all valid results are in the range [0, ceil(INT16_MAX / 8)], i.e. at most 4096. + * - It is okay for the user to allocate more bytes for the buffer than needed. + * - For byte mode, numChars measures the number of bytes, not Unicode code points. + * - For ECI mode, numChars must be 0, and the worst-case number of bytes is returned. + * An actual ECI segment can have shorter data. For non-ECI modes, the result is exact. + */ +size_t qrcodegen_calcSegmentBufferSize(enum qrcodegen_Mode mode, size_t numChars); + +/* + * Returns a segment representing the given binary data encoded in + * byte mode. All input byte arrays are acceptable. Any text string + * can be converted to UTF-8 bytes and encoded as a byte mode segment. + */ +struct qrcodegen_Segment qrcodegen_makeBytes(const uint8_t data[], size_t len, uint8_t buf[]); + +/* + * Returns a segment representing the given string of decimal digits encoded in numeric mode. + */ +struct qrcodegen_Segment qrcodegen_makeNumeric(const char *digits, uint8_t buf[]); + +/* + * Returns a segment representing the given text string encoded in alphanumeric mode. + * The characters allowed are: 0 to 9, A to Z (uppercase only), space, + * dollar, percent, asterisk, plus, hyphen, period, slash, colon. + */ +struct qrcodegen_Segment qrcodegen_makeAlphanumeric(const char *text, uint8_t buf[]); + +/* + * Returns a segment representing an Extended Channel Interpretation + * (ECI) designator with the given assignment value. + */ +struct qrcodegen_Segment qrcodegen_makeEci(long assignVal, uint8_t buf[]); + +/*---- Functions to extract raw data from QR Codes ----*/ + +/* + * Returns the side length of the given QR Code, assuming that encoding succeeded. + * The result is in the range [21, 177]. Note that the length of the array buffer + * is related to the side length - every 'uint8_t qrcode[]' must have length at least + * qrcodegen_BUFFER_LEN_FOR_VERSION(version), which equals ceil(size^2 / 8 + 1). + */ +int qrcodegen_getSize(const uint8_t qrcode[]); + +/* + * Returns the color of the module (pixel) at the given coordinates, which is false + * for white or true for black. The top left corner has the coordinates (x=0, y=0). + * If the given coordinates are out of bounds, then false (white) is returned. + */ +bool qrcodegen_getModule(const uint8_t qrcode[], int x, int y); + +#ifdef __cplusplus +} +#endif |