diff options
Diffstat (limited to 'libnm-core/nm-setting-user.c')
| -rw-r--r-- | libnm-core/nm-setting-user.c | 612 |
1 files changed, 612 insertions, 0 deletions
diff --git a/libnm-core/nm-setting-user.c b/libnm-core/nm-setting-user.c new file mode 100644 index 00000000..71d73891 --- /dev/null +++ b/libnm-core/nm-setting-user.c @@ -0,0 +1,612 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */ + +/* + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + * Copyright 2017 Red Hat, Inc. + */ + +#include "nm-default.h" + +#include "nm-setting-user.h" + +#include "nm-setting.h" +#include "nm-setting-private.h" +#include "nm-utils-private.h" + +/** + * SECTION:nm-setting-user + * @short_description: Describes user properties + * + * The #NMSettingUser object is a #NMSetting subclass that allow to attach + * arbitrary user data to #NMConnection objects. + **/ + +#define MAX_NUM_KEYS 256 + +/*****************************************************************************/ + +NM_GOBJECT_PROPERTIES_DEFINE (NMSettingUser, + PROP_DATA, +); + +typedef struct { + GHashTable *data; + GHashTable *data_invalid; + const char **keys; +} NMSettingUserPrivate; + +/** + * NMSettingUser: + * + * General User Profile Settings + */ +struct _NMSettingUser { + NMSetting parent; + NMSettingUserPrivate _priv; +}; + +struct _NMSettingUserClass { + NMSettingClass parent; +}; + +G_DEFINE_TYPE_WITH_CODE (NMSettingUser, nm_setting_user, NM_TYPE_SETTING, + _nm_register_setting (USER, 10)) +NM_SETTING_REGISTER_TYPE (NM_TYPE_SETTING_USER) + +#define NM_SETTING_USER_GET_PRIVATE(self) _NM_GET_PRIVATE(self, NMSettingUser, NM_IS_SETTING_USER) + +/*****************************************************************************/ + +static gboolean +_key_char_is_regular (char ch) +{ + /* allow words of printable characters, plus some + * special characters, for example to support base64 encoding. */ + return (ch >= 'a' && ch <= 'z') + || (ch >= 'A' && ch <= 'Z') + || (ch >= '0' && ch <= '9') + || NM_IN_SET (ch, '-', '_', '+', '/', '='); +} + +/** + * nm_setting_user_check_key: + * @key: the key to check + * @error: a #GError, %NULL to ignore. + * + * Checks whether @key is a valid user data key. This means, + * key is not %NULL, not too large and valid ASCII. Also, + * only digits and numbers are allowed with a few special + * characters. The key must contain at least one '.' and + * look like a fully qualified DNS name. + * + * Since: 1.8 + * + * Returns: %TRUE if @key is a valid user data key. + */ +gboolean +nm_setting_user_check_key (const char *key, GError **error) +{ + gsize len; + gboolean has_dot; + char ch; + + g_return_val_if_fail (!error || !*error, FALSE); + + if (!key || !key[0]) { + g_set_error_literal (error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_INVALID_PROPERTY, + _("missing key")); + return FALSE; + } + len = strlen (key); + if (len > 255) { + g_set_error_literal (error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_INVALID_PROPERTY, + _("key is too long")); + return FALSE; + } + if (!g_utf8_validate (key, len, NULL)) { + g_set_error_literal (error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_INVALID_PROPERTY, + _("key must be UTF8")); + return FALSE; + } + + has_dot = FALSE; + while (TRUE) { + ch = (key++)[0]; + + /* Allow something that looks like a FQN, separating namespaces by a single '.' + * We want to print the keys nicely in nmcli requiring escaping. + * + * If a user really has to encode special values in the name, he may base64 encode it. */ + + if (!_key_char_is_regular (ch)) + break; + + while (_key_char_is_regular (key[0])) + key++; + + ch = key[0]; + if (ch == '\0') { + if (!has_dot) { + g_set_error_literal (error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_INVALID_PROPERTY, + _("key requires a '.' for a namespace")); + return FALSE; + } + return TRUE; + } + + if (ch != '.') + break; + + has_dot = TRUE; + ch = (++key)[0]; + if (ch == '.') { + g_set_error_literal (error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_INVALID_PROPERTY, + _("key cannot contain \"..\"")); + return FALSE; + } + } + g_set_error_literal (error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_INVALID_PROPERTY, + _("key contains invalid characters")); + return FALSE; +} + +/** + * nm_setting_user_check_val: + * @val: the value to check + * @error: a #GError, %NULL to ignore. + * + * Checks whether @val is a valid user data value. This means, + * value is not %NULL, not too large and valid UTF-8. + * + * Since: 1.8 + * + * Returns: %TRUE if @val is a valid user data value. + */ +gboolean +nm_setting_user_check_val (const char *val, GError **error) +{ + gsize len; + + g_return_val_if_fail (!error || !*error, FALSE); + + if (!val) { + g_set_error_literal (error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_INVALID_PROPERTY, + _("value is missing")); + return FALSE; + } + + len = strlen (val); + if (len > 8*1024) { + g_set_error_literal (error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_INVALID_PROPERTY, + _("value is too large")); + return FALSE; + } + + if (!g_utf8_validate (val, len, NULL)) { + g_set_error_literal (error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_INVALID_PROPERTY, + _("value is not valid UTF8")); + return FALSE; + } + + return TRUE; +} + +/*****************************************************************************/ + +static GHashTable * +_create_data_hash (void) +{ + return g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); +} + +/** + * nm_setting_user_get_keys: + * @setting: the #NMSettingUser + * @out_len: (out): the length of the returned array + * + * Returns: (array length=out_len) (transfer none): a + * %NULL-terminated array containing each key from the table. + **/ +const char *const* +nm_setting_user_get_keys (NMSettingUser *setting, guint *out_len) +{ + NMSettingUser *self = setting; + NMSettingUserPrivate *priv; + guint len; + + g_return_val_if_fail (NM_IS_SETTING_USER (self), NULL); + + priv = NM_SETTING_USER_GET_PRIVATE (self); + + if (priv->keys) { + NM_SET_OUT (out_len, g_hash_table_size (priv->data)); + return priv->keys; + } + + if (!priv->data || !g_hash_table_size (priv->data)) { + NM_SET_OUT (out_len, 0); + return (const char **) &priv->keys; + } + + priv->keys = (const char **) g_hash_table_get_keys_as_array (priv->data, &len); + g_qsort_with_data (priv->keys, + len, + sizeof (const char *), + nm_strcmp_p_with_data, + NULL); + NM_SET_OUT (out_len, len); + return priv->keys; +} + +/*****************************************************************************/ + +/** + * nm_setting_user_get_data: + * @setting: the #NMSettingUser instance + * @key: the key to lookup + * + * Since: 1.8 + * + * Returns: (transfer none): the value associated with @key or %NULL if no such + * value exists. + */ +const char * +nm_setting_user_get_data (NMSettingUser *setting, const char *key) +{ + NMSettingUser *self = setting; + NMSettingUserPrivate *priv; + + g_return_val_if_fail (NM_IS_SETTING_USER (self), NULL); + g_return_val_if_fail (key, NULL); + + priv = NM_SETTING_USER_GET_PRIVATE (self); + + if (!priv->data) + return NULL; + + return g_hash_table_lookup (priv->data, key); +} + +/** + * nm_setting_user_set_data: + * @setting: the #NMSettingUser instance + * @key: the key to set + * @val: (allow-none): the value to set or %NULL to clear a key. + * @error: (allow-none): optional error argument + * + * Since: 1.8 + * + * Returns: %TRUE if the operation was successful. The operation + * can fail if @key or @val are not valid strings according + * to nm_setting_user_check_key() and nm_setting_user_check_val(). + */ +gboolean +nm_setting_user_set_data (NMSettingUser *setting, + const char *key, + const char *val, + GError **error) +{ + NMSettingUser *self = setting; + NMSettingUserPrivate *priv; + gboolean changed = FALSE; + + g_return_val_if_fail (NM_IS_SETTING (self), FALSE); + g_return_val_if_fail (!error || !*error, FALSE); + + if (!nm_setting_user_check_key (key, error)) + return FALSE; + + if ( val + && !nm_setting_user_check_val (val, error)) + return FALSE; + + priv = NM_SETTING_USER_GET_PRIVATE (self); + + if (!val) { + if ( priv->data + && g_hash_table_remove (priv->data, key)) { + nm_clear_g_free (&priv->keys); + changed = TRUE; + } + goto out; + } + + if (priv->data) { + const char *key2, *val2; + + if (g_hash_table_lookup_extended (priv->data, key, (gpointer *) &key2, (gpointer *) &val2)) { + if (nm_streq (val, val2)) + goto out; + } else { + if (g_hash_table_size (priv->data) >= MAX_NUM_KEYS) { + /* limit the number of valid keys */ + g_set_error_literal (error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_INVALID_PROPERTY, + _("maximum number of user data entries reached")); + return FALSE; + } + + nm_clear_g_free (&priv->keys); + } + } else + priv->data = _create_data_hash (); + + g_hash_table_insert (priv->data, g_strdup (key), g_strdup (val)); + changed = TRUE; + +out: + if (priv->data_invalid) { + /* setting a value purges all invalid values that were set + * via GObject property. */ + changed = TRUE; + g_clear_pointer (&priv->data_invalid, g_hash_table_unref); + } + if (changed) + _notify (self, PROP_DATA); + return TRUE; +} + +/*****************************************************************************/ + +static gboolean +verify (NMSetting *setting, NMConnection *connection, GError **error) +{ + NMSettingUser *self = NM_SETTING_USER (setting); + NMSettingUserPrivate *priv = NM_SETTING_USER_GET_PRIVATE (self); + + if (priv->data_invalid) { + const char *key, *val; + GHashTableIter iter; + gs_free_error GError *local = NULL; + + g_hash_table_iter_init (&iter, priv->data_invalid); + while (g_hash_table_iter_next (&iter, (gpointer *) &key, (gpointer *) &val)) { + if (!nm_setting_user_check_key (key, &local)) { + g_set_error (error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_FAILED, + _("invalid key \"%s\": %s"), + key, local->message); + } else if (!nm_setting_user_check_val (val, &local)) { + g_set_error (error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_FAILED, + _("invalid value for \"%s\": %s"), + key, local->message); + } else { + nm_assert_not_reached (); + continue; + } + g_prefix_error (error, "%s.%s: ", NM_SETTING_USER_SETTING_NAME, NM_SETTING_USER_DATA); + return FALSE; + } + nm_assert_not_reached (); + } + + if ( priv->data + && g_hash_table_size (priv->data) > MAX_NUM_KEYS) { + g_set_error (error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_INVALID_PROPERTY, + _("maximum number of user data entries reached (%u instead of %u)"), + g_hash_table_size (priv->data), (unsigned) MAX_NUM_KEYS); + g_prefix_error (error, "%s.%s: ", NM_SETTING_USER_SETTING_NAME, NM_SETTING_USER_DATA); + return FALSE; + } + + return TRUE; +} + +static gboolean +hash_table_equal (GHashTable *a, GHashTable *b) +{ + guint n; + GHashTableIter iter; + const char *key, *value, *valu2; + + n = a ? g_hash_table_size (a) : 0; + if (n != (b ? g_hash_table_size (b) : 0)) + return FALSE; + if (n > 0) { + g_hash_table_iter_init (&iter, a); + while (g_hash_table_iter_next (&iter, (gpointer *) &key, (gpointer *) &value)) { + if (!g_hash_table_lookup_extended (b, key, NULL, (gpointer *) &valu2)) + return FALSE; + if (!nm_streq (value, valu2)) + return FALSE; + } + } + return TRUE; + +} + +static gboolean +compare_property (NMSetting *setting, + NMSetting *other, + const GParamSpec *prop_spec, + NMSettingCompareFlags flags) +{ + NMSettingUserPrivate *priv, *pri2; + + g_return_val_if_fail (NM_IS_SETTING_USER (setting), FALSE); + g_return_val_if_fail (NM_IS_SETTING_USER (other), FALSE); + + if (!nm_streq0 (prop_spec->name, NM_SETTING_USER_DATA)) + goto call_parent; + + priv = NM_SETTING_USER_GET_PRIVATE (NM_SETTING_USER (setting)); + pri2 = NM_SETTING_USER_GET_PRIVATE (NM_SETTING_USER (other)); + + if (!hash_table_equal (priv->data, pri2->data)) + return FALSE; + + if (!hash_table_equal (priv->data_invalid, pri2->data_invalid)) + return FALSE; + + return TRUE; + +call_parent: + return NM_SETTING_CLASS (nm_setting_user_parent_class)->compare_property (setting, other, prop_spec, flags); +} + +/*****************************************************************************/ + +static void +get_property (GObject *object, guint prop_id, + GValue *value, GParamSpec *pspec) +{ + NMSettingUser *self = NM_SETTING_USER (object); + NMSettingUserPrivate *priv = NM_SETTING_USER_GET_PRIVATE (self); + GHashTableIter iter; + GHashTable *data; + const char *key, *val; + + switch (prop_id) { + case PROP_DATA: + data = _create_data_hash (); + if (priv->data) { + g_hash_table_iter_init (&iter, priv->data); + while (g_hash_table_iter_next (&iter, (gpointer *) &key, (gpointer *) &val)) + g_hash_table_insert (data, g_strdup (key), g_strdup (val)); + } + if (priv->data_invalid) { + g_hash_table_iter_init (&iter, priv->data_invalid); + while (g_hash_table_iter_next (&iter, (gpointer *) &key, (gpointer *) &val)) + g_hash_table_insert (data, g_strdup (key), g_strdup (val)); + } + g_value_take_boxed (value, data); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +set_property (GObject *object, guint prop_id, + const GValue *value, GParamSpec *pspec) +{ + NMSettingUser *self = NM_SETTING_USER (object); + NMSettingUserPrivate *priv = NM_SETTING_USER_GET_PRIVATE (self); + GHashTableIter iter; + GHashTable *data; + const char *key, *val; + + switch (prop_id) { + case PROP_DATA: + nm_clear_g_free (&priv->keys); + + data = g_value_get_boxed (value); + if (!data || !g_hash_table_size (data)) { + g_clear_pointer (&priv->data, g_hash_table_unref); + g_clear_pointer (&priv->data_invalid, g_hash_table_unref); + return; + } + + if (priv->data) + g_hash_table_remove_all (priv->data); + else + priv->data = _create_data_hash (); + + if (priv->data_invalid) + g_hash_table_remove_all (priv->data_invalid); + + g_hash_table_iter_init (&iter, data); + while (g_hash_table_iter_next (&iter, (gpointer *) &key, (gpointer *) &val)) { + if ( nm_setting_user_check_key (key, NULL) + && nm_setting_user_check_val (val, NULL)) + g_hash_table_insert (priv->data, g_strdup (key), g_strdup (val)); + else { + if (!priv->data_invalid) + priv->data_invalid = _create_data_hash (); + g_hash_table_insert (priv->data_invalid, g_strdup (key), g_strdup (val)); + } + } + if ( priv->data_invalid + && !g_hash_table_size (priv->data_invalid)) + g_clear_pointer (&priv->data_invalid, g_hash_table_unref); + + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +/*****************************************************************************/ + +static void +nm_setting_user_init (NMSettingUser *self) +{ +} + +/** + * nm_setting_user_new: + * + * Creates a new #NMSettingUser object with default values. + * + * Returns: the new empty #NMSettingUser object + **/ +NMSetting *nm_setting_user_new (void) +{ + return (NMSetting *) g_object_new (NM_TYPE_SETTING_USER, NULL); +} + +static void +finalize (GObject *object) +{ + NMSettingUser *self = NM_SETTING_USER (object); + NMSettingUserPrivate *priv = NM_SETTING_USER_GET_PRIVATE (self); + + g_free (priv->keys); + if (priv->data) + g_hash_table_unref (priv->data); + if (priv->data_invalid) + g_hash_table_unref (priv->data_invalid); + + G_OBJECT_CLASS (nm_setting_user_parent_class)->finalize (object); +} + +static void +nm_setting_user_class_init (NMSettingUserClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + NMSettingClass *setting_class = NM_SETTING_CLASS (klass); + + object_class->set_property = set_property; + object_class->get_property = get_property; + object_class->finalize = finalize; + setting_class->compare_property = compare_property; + setting_class->verify = verify; + + /** + * NMSettingUser:data: + * + * A dictionary of key/value pairs with user data. This data is ignored by NetworkManager + * and can be used at the users discretion. The keys only support a strict ascii format, + * but the values can be arbitrary UTF8 strings up to a certain length. + * + * Type: GHashTable(utf8,utf8) + * + * Since: 1.8 + **/ + obj_properties[PROP_DATA] = + g_param_spec_boxed (NM_SETTING_USER_DATA, "", "", + G_TYPE_HASH_TABLE, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, _PROPERTY_ENUMS_LAST, obj_properties); + + _nm_setting_class_transform_property (setting_class, NM_SETTING_USER_DATA, + G_VARIANT_TYPE ("a{ss}"), + _nm_utils_strdict_to_dbus, + _nm_utils_strdict_from_dbus); +} |