NetworkManager/libnm-core/nm-setting-user.c

625 lines
17 KiB
C
Raw Normal View History

/* -*- 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,
2017-06-01 11:43:52 +00:00
_nm_register_setting (USER, NM_SETTING_PRIORITY_USER))
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
**/
/* ---ifcfg-rh---
* property: data
* variable: NM_USER_*
* description: each key/value pair is stored as a separate variable with
* name composed by concatenating NM_USER_ with the encoded key. The key is
* encoded by substituting lowercase letters with uppercase and prepending
* uppercase letters with an underscore. A dot is encoded as a double
* underscore. Remaining characters are encoded as underscore followed by a
* 3 digit octal representation of the character.
* example: NM_USER_FOO__BAR=something
* ---end---
*/
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);
}