/* SPDX-License-Identifier: LGPL-2.1-or-later */
/*
* Copyright (C) 2015 Red Hat, Inc.
*/
#include "nm-default.h"
#include "nm-vpn-plugin-info.h"
#include <sys/stat.h>
#include "nm-errors.h"
#include "nm-core-internal.h"
#define DEFAULT_DIR_ETC NMCONFDIR "/VPN"
#define DEFAULT_DIR_LIB NMLIBDIR "/VPN"
enum {
PROP_0,
PROP_NAME,
PROP_FILENAME,
PROP_KEYFILE,
LAST_PROP,
};
typedef struct {
char * filename;
char * name;
char * service;
char * auth_dialog;
char ** aliases;
GKeyFile *keyfile;
/* It is convenient for nm_vpn_plugin_info_lookup_property() to return a const char *,
* contrary to what g_key_file_get_string() does. Hence we must cache the returned
* value somewhere... let's put it in an internal hash table.
* This contains a clone of all the strings in keyfile. */
GHashTable *keys;
gboolean editor_plugin_loaded;
NMVpnEditorPlugin *editor_plugin;
} NMVpnPluginInfoPrivate;
/**
* NMVpnPluginInfo:
*/
struct _NMVpnPluginInfo {
GObject parent;
NMVpnPluginInfoPrivate _priv;
};
struct _NMVpnPluginInfoClass {
GObjectClass parent;
};
#define NM_VPN_PLUGIN_INFO_GET_PRIVATE(self) \
_NM_GET_PRIVATE(self, NMVpnPluginInfo, NM_IS_VPN_PLUGIN_INFO)
static void nm_vpn_plugin_info_initable_iface_init(GInitableIface *iface);
G_DEFINE_TYPE_WITH_CODE(NMVpnPluginInfo,
nm_vpn_plugin_info,
G_TYPE_OBJECT,
G_IMPLEMENT_INTERFACE(G_TYPE_INITABLE,
nm_vpn_plugin_info_initable_iface_init);)
/*****************************************************************************/
static NMVpnPluginInfo *_list_find_by_service(GSList *list, const char *name, const char *service);
/*****************************************************************************/
/**
* nm_vpn_plugin_info_validate_filename:
* @filename: the filename to check
*
* Regular name files have a certain pattern. That basically means
* they have the file extension "name". Check if @filename
* is valid according to that pattern.
*
* Since: 1.2
*/
gboolean
nm_vpn_plugin_info_validate_filename(const char *filename)
{
if (!filename || !g_str_has_suffix(filename, ".name"))
return FALSE;
/* originally, we didn't do further checks... but here we go. */
if (filename[0] == '.') {
/* this also rejects name ".name" alone. */
return FALSE;
}
return TRUE;
}
static gboolean
nm_vpn_plugin_info_check_file_full(const char * filename,
gboolean check_absolute,
gboolean do_validate_filename,
gint64 check_owner,
NMUtilsCheckFilePredicate check_file,
gpointer user_data,
struct stat * out_st,
GError ** error)
{
if (!filename || !*filename) {
g_set_error(error, NM_VPN_PLUGIN_ERROR, NM_VPN_PLUGIN_ERROR_FAILED, _("missing filename"));
return FALSE;
}
if (check_absolute && !g_path_is_absolute(filename)) {
g_set_error(error,
NM_VPN_PLUGIN_ERROR,
NM_VPN_PLUGIN_ERROR_FAILED,
_("filename must be an absolute path (%s)"),
filename);
return FALSE;
}
if (do_validate_filename && !nm_vpn_plugin_info_validate_filename(filename)) {
g_set_error(error,
NM_VPN_PLUGIN_ERROR,
NM_VPN_PLUGIN_ERROR_FAILED,
_("filename has invalid format (%s)"),
filename);
return FALSE;
}
return _nm_utils_check_file(filename, check_owner, check_file, user_data, out_st, error);
}
/**
* _nm_vpn_plugin_info_check_file:
* @filename: the file to check
* @check_absolute: if %TRUE, only allow absolute path names.
* @do_validate_filename: if %TRUE, only accept the filename if
* nm_vpn_plugin_info_validate_filename() succeeds.
* @check_owner: if non-negative, only accept the file if the
* owner UID is equal to @check_owner or if the owner is 0.
* In this case, also check that the file is not writable by
* other users.
* @check_file: pass a callback to do your own validation.
* @user_data: user data for @check_file.
* @error: (allow-none) (out): the error reason if the check fails.
*
* Check whether the file exists and is a valid name file (in keyfile format).
* Additionally, also check for file permissions.
*
* Returns: %TRUE if a file @filename exists and has valid permissions.
*
* Since: 1.2
*/
gboolean
_nm_vpn_plugin_info_check_file(const char * filename,
gboolean check_absolute,
gboolean do_validate_filename,
gint64 check_owner,
NMUtilsCheckFilePredicate check_file,
gpointer user_data,
GError ** error)
{
return nm_vpn_plugin_info_check_file_full(filename,
check_absolute,
do_validate_filename,
check_owner,
check_file,
user_data,
NULL,
error);
}
typedef struct {
NMVpnPluginInfo *plugin_info;
struct stat stat;
} LoadDirInfo;
static int
_sort_files(LoadDirInfo *a, LoadDirInfo *b)
{
time_t ta, tb;
ta = MAX(a->stat.st_mtime, a->stat.st_ctime);
tb = MAX(b->stat.st_mtime, b->stat.st_ctime);
if (ta < tb)
return 1;
if (ta > tb)
return -1;
return g_strcmp0(nm_vpn_plugin_info_get_filename(a->plugin_info),
nm_vpn_plugin_info_get_filename(b->plugin_info));
}
/**
* _nm_vpn_plugin_info_get_default_dir_etc:
*
* Returns: (transfer none): compile time constant of the default
* VPN plugin directory.
*/
const char *
_nm_vpn_plugin_info_get_default_dir_etc()
{
return DEFAULT_DIR_ETC;
}
/**
* _nm_vpn_plugin_info_get_default_dir_lib:
*
* Returns: (transfer none): compile time constant of the default
* VPN plugin directory.
*/
const char *
_nm_vpn_plugin_info_get_default_dir_lib()
{
return DEFAULT_DIR_LIB;
}
/**
* _nm_vpn_plugin_info_get_default_dir_user:
*
* Returns: The user can specify a different directory for VPN plugins
* by setting NM_VPN_PLUGIN_DIR environment variable. Return
* that directory.
*/
const char *
_nm_vpn_plugin_info_get_default_dir_user()
{
return nm_str_not_empty(g_getenv("NM_VPN_PLUGIN_DIR"));
}
/**
* _nm_vpn_plugin_info_list_load_dir:
* @dirname: the name of the directory to load.
* @do_validate_filename: only consider filenames that have a certain
* pattern (i.e. end with ".name").
* @check_owner: if set to a non-negative number, check that the file
* owner is either the same uid or 0. In that case, also check
* that the file is not writable by group or other.
* @check_file: (allow-none): callback to check whether the file is valid.
* @user_data: data for @check_file
*
* Iterate over the content of @dirname and load name files.
*
* Returns: (transfer full) (element-type NMVpnPluginInfo): list of loaded plugin infos.
*/
GSList *
_nm_vpn_plugin_info_list_load_dir(const char * dirname,
gboolean do_validate_filename,
gint64 check_owner,
NMUtilsCheckFilePredicate check_file,
gpointer user_data)
{
GDir * dir;
const char *fn;
GArray * array;
GSList * res = NULL;
guint i;
g_return_val_if_fail(dirname, NULL);
if (!dirname[0])
return NULL;
dir = g_dir_open(dirname, 0, NULL);
if (!dir)
return NULL;
array = g_array_new(FALSE, FALSE, sizeof(LoadDirInfo));
while ((fn = g_dir_read_name(dir))) {
gs_free char *filename = NULL;
LoadDirInfo info = {0};
filename = g_build_filename(dirname, fn, NULL);
if (nm_vpn_plugin_info_check_file_full(filename,
FALSE,
do_validate_filename,
check_owner,
check_file,
user_data,
&info.stat,
NULL)) {
info.plugin_info = nm_vpn_plugin_info_new_from_file(filename, NULL);
if (info.plugin_info) {
g_array_append_val(array, info);
continue;
}
}
}
g_dir_close(dir);
/* sort the files so that we have a stable behavior. The directory might contain
* duplicate VPNs, so while nm_vpn_plugin_info_list_load() would load them all, the
* caller probably wants to reject duplicates. Having a stable order means we always
* reject the same files in face of duplicates. */
g_array_sort(array, (GCompareFunc) _sort_files);
for (i = 0; i < array->len; i++)
res = g_slist_prepend(res, g_array_index(array, LoadDirInfo, i).plugin_info);
g_array_unref(array);
return g_slist_reverse(res);
}
/**
* nm_vpn_plugin_info_list_load:
*
* Returns: (element-type NMVpnPluginInfo) (transfer full): list of plugins
* loaded from the default directories rejecting duplicates.
*
* Since: 1.2
*/
GSList *
nm_vpn_plugin_info_list_load()
{
int i;
gint64 uid;
GSList * list = NULL;
GSList * infos, *info;
const char *const dir[] = {
/* We load plugins from NM_VPN_PLUGIN_DIR *and* DEFAULT_DIR*, with
* preference to the former.
*
* load user directory with highest priority. */
_nm_vpn_plugin_info_get_default_dir_user(),
/* lib directory has higher priority then etc. The reason is that
* etc is deprecated and used by old plugins. We expect newer plugins
* to install their file in lib, where they have higher priority.
*
* Optimally, there are no duplicates anyway, so it doesn't really matter. */
_nm_vpn_plugin_info_get_default_dir_lib(),
_nm_vpn_plugin_info_get_default_dir_etc(),
};
uid = getuid();
for (i = 0; i < G_N_ELEMENTS(dir); i++) {
if (!dir[i] || nm_utils_strv_find_first((char **) dir, i, dir[i]) >= 0)
continue;
infos = _nm_vpn_plugin_info_list_load_dir(dir[i], TRUE, uid, NULL, NULL);
for (info = infos; info; info = info->next)
nm_vpn_plugin_info_list_add(&list, info->data, NULL);
g_slist_free_full(infos, g_object_unref);
}
return list;
}
/**
* nm_vpn_plugin_info_new_search_file:
* @name: (allow-none): the name to search for. Either @name or @service
* must be present.
* @service: (allow-none): the service to search for. Either @name or
* @service must be present.
*
* This has the same effect as doing a full nm_vpn_plugin_info_list_load()
* followed by a search for the first matching VPN plugin info that has the
* given @name and/or @service.
*
* Returns: (transfer full): a newly created instance of plugin info
* or %NULL if no matching value was found.
*
* Since: 1.4
*/
NMVpnPluginInfo *
nm_vpn_plugin_info_new_search_file(const char *name, const char *service)
{
NMVpnPluginInfo *info;
GSList * infos;
if (!name && !service)
g_return_val_if_reached(NULL);
infos = nm_vpn_plugin_info_list_load();
info = nm_g_object_ref(_list_find_by_service(infos, name, service));
g_slist_free_full(infos, g_object_unref);
return info;
}
/*****************************************************************************/
static gboolean
_check_no_conflict(NMVpnPluginInfo *i1, NMVpnPluginInfo *i2, GError **error)
{
NMVpnPluginInfoPrivate *priv1, *priv2;
uint i;
struct {
const char *group;
const char *key;
} check_list[] = {
{NM_VPN_PLUGIN_INFO_KF_GROUP_CONNECTION, "service"},
{NM_VPN_PLUGIN_INFO_KF_GROUP_LIBNM, "plugin"},
{NM_VPN_PLUGIN_INFO_KF_GROUP_GNOME, "properties"},
};
priv1 = NM_VPN_PLUGIN_INFO_GET_PRIVATE(i1);
priv2 = NM_VPN_PLUGIN_INFO_GET_PRIVATE(i2);
for (i = 0; i < G_N_ELEMENTS(check_list); i++) {
gs_free NMUtilsStrStrDictKey *k = NULL;
const char * s1, *s2;
k = _nm_utils_strstrdictkey_create(check_list[i].group, check_list[i].key);
s1 = g_hash_table_lookup(priv1->keys, k);
if (!s1)
continue;
s2 = g_hash_table_lookup(priv2->keys, k);
if (!s2)
continue;
if (strcmp(s1, s2) == 0) {
g_set_error(error,
NM_VPN_PLUGIN_ERROR,
NM_VPN_PLUGIN_ERROR_FAILED,
_("there exists a conflicting plugin (%s) that has the same %s.%s value"),
priv2->name,
check_list[i].group,
check_list[i].key);
return FALSE;
}
}
return TRUE;
}
/**
* nm_vpn_plugin_info_list_add:
* @list: (element-type NMVpnPluginInfo): list of plugins
* @plugin_info: instance to add
* @error: failure reason
*
* Returns: %TRUE if the plugin was added to @list. This will fail
* to add duplicate plugins.
*
* Since: 1.2
*/
gboolean
nm_vpn_plugin_info_list_add(GSList **list, NMVpnPluginInfo *plugin_info, GError **error)
{
GSList * iter;
const char *name;
g_return_val_if_fail(list, FALSE);
g_return_val_if_fail(NM_IS_VPN_PLUGIN_INFO(plugin_info), FALSE);
name = nm_vpn_plugin_info_get_name(plugin_info);
for (iter = *list; iter; iter = iter->next) {
if (iter->data == plugin_info)
return TRUE;
if (strcmp(nm_vpn_plugin_info_get_name(iter->data), name) == 0) {
g_set_error(error,
NM_VPN_PLUGIN_ERROR,
NM_VPN_PLUGIN_ERROR_FAILED,
_("there exists a conflicting plugin with the same name (%s)"),
name);
return FALSE;
}
/* the plugin must have unique values for certain properties. E.g. two different
* plugins cannot share the same service type. */
if (!_check_no_conflict(plugin_info, iter->data, error))
return FALSE;
}
*list = g_slist_append(*list, g_object_ref(plugin_info));
return TRUE;
}
/**
* nm_vpn_plugin_info_list_remove:
* @list: (element-type NMVpnPluginInfo): list of plugins
* @plugin_info: instance
*
* Remove @plugin_info from @list.
*
* Returns: %TRUE if @plugin_info was in @list and successfully removed.
*
* Since: 1.2
*/
gboolean
nm_vpn_plugin_info_list_remove(GSList **list, NMVpnPluginInfo *plugin_info)
{
g_return_val_if_fail(list, FALSE);
g_return_val_if_fail(NM_IS_VPN_PLUGIN_INFO(plugin_info), FALSE);
if (!g_slist_find(*list, plugin_info))
return FALSE;
*list = g_slist_remove(*list, plugin_info);
g_object_unref(plugin_info);
return TRUE;
}
/**
* nm_vpn_plugin_info_list_find_by_name:
* @list: (element-type NMVpnPluginInfo): list of plugins
* @name: name to search
*
* Returns: (transfer none): the first plugin with a matching @name (or %NULL).
*
* Since: 1.2
*/
NMVpnPluginInfo *
nm_vpn_plugin_info_list_find_by_name(GSList *list, const char *name)
{
GSList *iter;
if (!name)
g_return_val_if_reached(NULL);
for (iter = list; iter; iter = iter->next) {
if (strcmp(nm_vpn_plugin_info_get_name(iter->data), name) == 0)
return iter->data;
}
return NULL;
}
/**
* nm_vpn_plugin_info_list_find_by_filename:
* @list: (element-type NMVpnPluginInfo): list of plugins
* @filename: filename to search
*
* Returns: (transfer none): the first plugin with a matching @filename (or %NULL).
*
* Since: 1.2
*/
NMVpnPluginInfo *
nm_vpn_plugin_info_list_find_by_filename(GSList *list, const char *filename)
{
GSList *iter;
if (!filename)
g_return_val_if_reached(NULL);
for (iter = list; iter; iter = iter->next) {
if (g_strcmp0(nm_vpn_plugin_info_get_filename(iter->data), filename) == 0)
return iter->data;
}
return NULL;
}
static NMVpnPluginInfo *
_list_find_by_service(GSList *list, const char *name, const char *service)
{
for (; list; list = list->next) {
NMVpnPluginInfoPrivate *priv = NM_VPN_PLUGIN_INFO_GET_PRIVATE(list->data);
if (name && !nm_streq(name, priv->name))
continue;
if (service && !nm_streq(priv->service, service)
&& (nm_utils_strv_find_first(priv->aliases, -1, service) < 0))
continue;
return list->data;
}
return NULL;
}
/**
* nm_vpn_plugin_info_list_find_by_service:
* @list: (element-type NMVpnPluginInfo): list of plugins
* @service: service to search. This can be the main service-type
* or one of the provided aliases.
*
* Returns: (transfer none): the first plugin with a matching @service (or %NULL).
*
* Since: 1.2
*/
NMVpnPluginInfo *
nm_vpn_plugin_info_list_find_by_service(GSList *list, const char *service)
{
if (!service)
g_return_val_if_reached(NULL);
return _list_find_by_service(list, NULL, service);
}
/* known_names are well known short names for the service-type. They all implicitly
* have a prefix "org.freedesktop.NetworkManager." + known_name. */
static const char *known_names[] = {
"openvpn",
"vpnc",
"pptp",
"openconnect",
"openswan",
"libreswan",
"strongswan",
"ssh",
"l2tp",
"iodine",
"fortisslvpn",
};
/**
* nm_vpn_plugin_info_list_find_service_type:
* @list: (element-type NMVpnPluginInfo): a possibly empty #GSList of #NMVpnPluginInfo instances
* @name: a name to lookup the service-type.
*
* A VPN plugin provides one or several service-types, like org.freedesktop.NetworkManager.libreswan
* Certain plugins provide more then one service type, via aliases (org.freedesktop.NetworkManager.openswan).
* This function looks up a service-type (or an alias) based on a name.
*
* Preferably, the name can be a full service-type/alias of an installed
* plugin. Otherwise, it can be the name of a VPN plugin (in which case, the
* primary, non-aliased service-type is returned). Otherwise, it can be
* one of several well known short-names (which is a hard-coded list of
* types in libnm). On success, this returns a full qualified service-type
* (or an alias). It doesn't say, that such an plugin is actually available,
* but it could be retrieved via nm_vpn_plugin_info_list_find_by_service().
*
* Returns: (transfer full): the resolved service-type or %NULL on failure.
*
* Since: 1.4
*/
char *
nm_vpn_plugin_info_list_find_service_type(GSList *list, const char *name)
{
NMVpnPluginInfo *info;
char * n;
if (!name)
g_return_val_if_reached(NULL);
if (!*name)
return NULL;
/* First, try to interpret @name as a full service-type (or alias). */
info = _list_find_by_service(list, NULL, name);
if (info)
return g_strdup(name);
/* try to interpret @name as plugin name, in which case we return
* the main service-type (not an alias). */
info = _list_find_by_service(list, name, NULL);
if (info)
return g_strdup(NM_VPN_PLUGIN_INFO_GET_PRIVATE(info)->service);
/* check the hard-coded list of short-names. They all have the same
* well-known prefix org.freedesktop.NetworkManager and the name. */
if (nm_utils_strv_find_first((char **) known_names, G_N_ELEMENTS(known_names), name) >= 0)
return g_strdup_printf("%s.%s", NM_DBUS_INTERFACE, name);
/* try, if there exists a plugin with @name under org.freedesktop.NetworkManager.
* Allow this to be a valid abbreviation. */
n = g_strdup_printf("%s.%s", NM_DBUS_INTERFACE, name);
if (_list_find_by_service(list, NULL, n))
return n;
g_free(n);
/* currently, VPN plugins have no way to define a short-name for their
* alias name, unless the alias name is prefixed by org.freedesktop.NetworkManager. */
return NULL;
}
static const char *
_service_type_get_default_abbreviation(const char *service_type)
{
if (!g_str_has_prefix(service_type, NM_DBUS_INTERFACE))
return NULL;
service_type += NM_STRLEN(NM_DBUS_INTERFACE);
if (service_type[0] != '.')
return NULL;
service_type++;
if (!service_type[0])
return NULL;
return service_type;
}
/**
* nm_vpn_plugin_info_list_get_service_types:
* @list: (element-type NMVpnPluginInfo): a possibly empty #GSList of #NMVpnPluginInfo
* @only_existing: only include results that are actually in @list.
* Otherwise, the result is extended with a hard-code list or
* well-known plugins
* @with_abbreviations: if %FALSE, only full service types are returned.
* Otherwise, this also includes abbreviated names that can be used
* with nm_vpn_plugin_info_list_find_service_type().
*
* Returns: (transfer full): a %NULL terminated strv list of strings.
* The list itself and the values must be freed with g_strfreev().
*
* Since: 1.4
*/
char **
nm_vpn_plugin_info_list_get_service_types(GSList * list,
gboolean only_existing,
gboolean with_abbreviations)
{
GSList * iter;
GPtrArray * l;
guint i, j;
const char *n;
l = g_ptr_array_sized_new(20);
for (iter = list; iter; iter = iter->next) {
NMVpnPluginInfoPrivate *priv = NM_VPN_PLUGIN_INFO_GET_PRIVATE(iter->data);
g_ptr_array_add(l, g_strdup(priv->service));
if (priv->aliases) {
for (i = 0; priv->aliases[i]; i++)
g_ptr_array_add(l, g_strdup(priv->aliases[i]));
}
if (with_abbreviations) {
g_ptr_array_add(l, g_strdup(priv->name));
n = _service_type_get_default_abbreviation(priv->service);
if (n)
g_ptr_array_add(l, g_strdup(n));
for (i = 0; priv->aliases && priv->aliases[i]; i++) {
n = _service_type_get_default_abbreviation(priv->aliases[i]);
if (n)
g_ptr_array_add(l, g_strdup(n));
}
}
}
if (!only_existing) {
for (i = 0; i < G_N_ELEMENTS(known_names); i++) {
g_ptr_array_add(l, g_strdup_printf("%s.%s", NM_DBUS_INTERFACE, known_names[i]));
if (with_abbreviations)
g_ptr_array_add(l, g_strdup(known_names[i]));
}
}
if (l->len <= 0) {
g_ptr_array_free(l, TRUE);
return g_new0(char *, 1);
}
/* sort the result and remove duplicates. */
g_ptr_array_sort(l, nm_strcmp_p);
for (i = 1, j = 1; i < l->len; i++) {
if (nm_streq(l->pdata[j - 1], l->pdata[i]))
g_free(l->pdata[i]);
else
l->pdata[j++] = l->pdata[i];
}
if (j == l->len)
g_ptr_array_add(l, NULL);
else
l->pdata[j] = NULL;
return (char **) g_ptr_array_free(l, FALSE);
}
/*****************************************************************************/
/**
* nm_vpn_plugin_info_get_filename:
* @self: plugin info instance
*
* Returns: (transfer none): the filename. Can be %NULL.
*
* Since: 1.2
*/
const char *
nm_vpn_plugin_info_get_filename(NMVpnPluginInfo *self)
{
g_return_val_if_fail(NM_IS_VPN_PLUGIN_INFO(self), NULL);
return NM_VPN_PLUGIN_INFO_GET_PRIVATE(self)->filename;
}
/**
* nm_vpn_plugin_info_get_name:
* @self: plugin info instance
*
* Returns: (transfer none): the name. Cannot be %NULL.
*
* Since: 1.2
*/
const char *
nm_vpn_plugin_info_get_name(NMVpnPluginInfo *self)
{
g_return_val_if_fail(NM_IS_VPN_PLUGIN_INFO(self), NULL);
return NM_VPN_PLUGIN_INFO_GET_PRIVATE(self)->name;
}
/**
* nm_vpn_plugin_info_get_service:
* @self: plugin info instance
*
* Returns: (transfer none): the service. Cannot be %NULL.
*
* Since: 1.4
*/
const char *
nm_vpn_plugin_info_get_service(NMVpnPluginInfo *self)
{
g_return_val_if_fail(NM_IS_VPN_PLUGIN_INFO(self), NULL);
return NM_VPN_PLUGIN_INFO_GET_PRIVATE(self)->service;
}
/**
* nm_vpn_plugin_info_get_auth_dialog:
* @self: plugin info instance
*
* Returns: the absolute path to the auth-dialog helper or %NULL.
*
* Since: 1.4
**/
const char *
nm_vpn_plugin_info_get_auth_dialog(NMVpnPluginInfo *self)
{
NMVpnPluginInfoPrivate *priv;
g_return_val_if_fail(NM_IS_VPN_PLUGIN_INFO(self), NULL);
priv = NM_VPN_PLUGIN_INFO_GET_PRIVATE(self);
if (G_UNLIKELY(priv->auth_dialog == NULL)) {
const char *s;
s = g_hash_table_lookup(
priv->keys,
_nm_utils_strstrdictkey_static(NM_VPN_PLUGIN_INFO_KF_GROUP_GNOME, "auth-dialog"));
if (!s || !s[0])
priv->auth_dialog = g_strdup("");
else if (g_path_is_absolute(s))
priv->auth_dialog = g_strdup(s);
else {
/* for relative paths, we take the basename and assume it's in LIBEXECDIR. */
gs_free char *prog_basename = g_path_get_basename(s);
priv->auth_dialog = g_build_filename(LIBEXECDIR, prog_basename, NULL);
}
}
return priv->auth_dialog[0] ? priv->auth_dialog : NULL;
}
/**
* nm_vpn_plugin_info_supports_hints:
* @self: plugin info instance
*
* Returns: %TRUE if the supports hints for secret requests, otherwise %FALSE
*
* Since: 1.4
*/
gboolean
nm_vpn_plugin_info_supports_hints(NMVpnPluginInfo *self)
{
const char *s;
g_return_val_if_fail(NM_IS_VPN_PLUGIN_INFO(self), FALSE);
s = nm_vpn_plugin_info_lookup_property(self,
NM_VPN_PLUGIN_INFO_KF_GROUP_GNOME,
"supports-hints");
return _nm_utils_ascii_str_to_bool(s, FALSE);
}
/**
* nm_vpn_plugin_info_get_plugin:
* @self: plugin info instance
*
* Returns: (transfer none): the plugin. Can be %NULL.
*
* Since: 1.2
*/
const char *
nm_vpn_plugin_info_get_plugin(NMVpnPluginInfo *self)
{
g_return_val_if_fail(NM_IS_VPN_PLUGIN_INFO(self), NULL);
return g_hash_table_lookup(
NM_VPN_PLUGIN_INFO_GET_PRIVATE(self)->keys,
_nm_utils_strstrdictkey_static(NM_VPN_PLUGIN_INFO_KF_GROUP_LIBNM, "plugin"));
}
/**
* nm_vpn_plugin_info_get_program:
* @self: plugin info instance
*
* Returns: (transfer none): the program. Can be %NULL.
*
* Since: 1.2
*/
const char *
nm_vpn_plugin_info_get_program(NMVpnPluginInfo *self)
{
g_return_val_if_fail(NM_IS_VPN_PLUGIN_INFO(self), NULL);
return g_hash_table_lookup(
NM_VPN_PLUGIN_INFO_GET_PRIVATE(self)->keys,
_nm_utils_strstrdictkey_static(NM_VPN_PLUGIN_INFO_KF_GROUP_CONNECTION, "program"));
}
/**
* nm_vpn_plugin_info_supports_multiple:
* @self: plugin info instance
*
* Returns: %TRUE if the service supports multiple instances with different bus names, otherwise %FALSE
*
* Since: 1.2
*/
gboolean
nm_vpn_plugin_info_supports_multiple(NMVpnPluginInfo *self)
{
const char *s;
g_return_val_if_fail(NM_IS_VPN_PLUGIN_INFO(self), FALSE);
s = nm_vpn_plugin_info_lookup_property(self,
NM_VPN_PLUGIN_INFO_KF_GROUP_CONNECTION,
"supports-multiple-connections");
return _nm_utils_ascii_str_to_bool(s, FALSE);
}
/**
* nm_vpn_plugin_info_get_aliases:
* @self: plugin info instance
*
* Returns: (array zero-terminated=1) (element-type utf8) (transfer none):
* the aliases from the name-file.
*
* Since: 1.4
*/
const char *const *
nm_vpn_plugin_info_get_aliases(NMVpnPluginInfo *self)
{
NMVpnPluginInfoPrivate *priv;
g_return_val_if_fail(NM_IS_VPN_PLUGIN_INFO(self), NULL);
priv = NM_VPN_PLUGIN_INFO_GET_PRIVATE(self);
if (priv->aliases)
return (const char *const *) priv->aliases;
/* For convenience, we always want to return non-NULL, even for empty
* aliases. Hack around that, by making a NULL terminated array using
* the NULL of priv->aliases. */
return (const char *const *) &priv->aliases;
}
/**
* nm_vpn_plugin_info_lookup_property:
* @self: plugin info instance
* @group: group name
* @key: name of the property
*
* Returns: (transfer none): #NMVpnPluginInfo is internally a #GKeyFile. Returns the matching
* property.
*
* Since: 1.2
*/
const char *
nm_vpn_plugin_info_lookup_property(NMVpnPluginInfo *self, const char *group, const char *key)
{
NMVpnPluginInfoPrivate *priv;
gs_free NMUtilsStrStrDictKey *k = NULL;
g_return_val_if_fail(NM_IS_VPN_PLUGIN_INFO(self), NULL);
g_return_val_if_fail(group, NULL);
g_return_val_if_fail(key, NULL);
priv = NM_VPN_PLUGIN_INFO_GET_PRIVATE(self);
k = _nm_utils_strstrdictkey_create(group, key);
return g_hash_table_lookup(priv->keys, k);
}
/*****************************************************************************/
/**
* nm_vpn_plugin_info_get_editor_plugin:
* @self: plugin info instance
*
* Returns: (transfer none): the cached #NMVpnEditorPlugin instance.
*
* Since: 1.2
*/
NMVpnEditorPlugin *
nm_vpn_plugin_info_get_editor_plugin(NMVpnPluginInfo *self)
{
g_return_val_if_fail(NM_IS_VPN_PLUGIN_INFO(self), NULL);
return NM_VPN_PLUGIN_INFO_GET_PRIVATE(self)->editor_plugin;
}
/**
* nm_vpn_plugin_info_set_editor_plugin:
* @self: plugin info instance
* @plugin: (allow-none): plugin instance
*
* Set the internal plugin instance. If %NULL, only clear the previous instance.
*
* Since: 1.2
*/
void
nm_vpn_plugin_info_set_editor_plugin(NMVpnPluginInfo *self, NMVpnEditorPlugin *plugin)
{
NMVpnPluginInfoPrivate *priv;
NMVpnEditorPlugin * old;
g_return_if_fail(NM_IS_VPN_PLUGIN_INFO(self));
g_return_if_fail(!plugin || G_IS_OBJECT(plugin));
priv = NM_VPN_PLUGIN_INFO_GET_PRIVATE(self);
if (!plugin) {
priv->editor_plugin_loaded = FALSE;
g_clear_object(&priv->editor_plugin);
} else {
old = priv->editor_plugin;
priv->editor_plugin = g_object_ref(plugin);
priv->editor_plugin_loaded = TRUE;
if (old)
g_object_unref(old);
}
}
/**
* nm_vpn_plugin_info_load_editor_plugin:
* @self: plugin info instance
* @error: error reason on failure
*
* Returns: (transfer none): loads the plugin and returns the newly created
* instance. The plugin is owned by @self and can be later retrieved again
* via nm_vpn_plugin_info_get_editor_plugin(). You can load the
* plugin only once, unless you reset the state via
* nm_vpn_plugin_info_set_editor_plugin().
*
* Since: 1.2
*/
NMVpnEditorPlugin *
nm_vpn_plugin_info_load_editor_plugin(NMVpnPluginInfo *self, GError **error)
{
NMVpnPluginInfoPrivate *priv;
const char * plugin_filename;
g_return_val_if_fail(NM_IS_VPN_PLUGIN_INFO(self), NULL);
priv = NM_VPN_PLUGIN_INFO_GET_PRIVATE(self);
if (priv->editor_plugin)
return priv->editor_plugin;
plugin_filename = nm_vpn_plugin_info_get_plugin(self);
if (!plugin_filename || !*plugin_filename) {
g_set_error(error,
NM_VPN_PLUGIN_ERROR,
NM_VPN_PLUGIN_ERROR_FAILED,
_("missing \"plugin\" setting"));
return NULL;
}
/* We only try once to load the plugin. If we previously tried and it was
* unsuccessful, error out immediately. */
if (priv->editor_plugin_loaded) {
g_set_error(error,
NM_VPN_PLUGIN_ERROR,
NM_VPN_PLUGIN_ERROR_FAILED,
_("%s: don't retry loading plugin which already failed previously"),
priv->name);
return NULL;
}
priv->editor_plugin_loaded = TRUE;
priv->editor_plugin = nm_vpn_editor_plugin_load_from_file(plugin_filename,
nm_vpn_plugin_info_get_service(self),
getuid(),
NULL,
NULL,
error);
if (priv->editor_plugin)
nm_vpn_editor_plugin_set_plugin_info(priv->editor_plugin, self);
return priv->editor_plugin;
}
/*****************************************************************************/
static void
get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
{
NMVpnPluginInfoPrivate *priv = NM_VPN_PLUGIN_INFO_GET_PRIVATE(object);
switch (prop_id) {
case PROP_NAME:
g_value_set_string(value, priv->name);
break;
case PROP_FILENAME:
g_value_set_string(value, priv->filename);
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)
{
NMVpnPluginInfoPrivate *priv = NM_VPN_PLUGIN_INFO_GET_PRIVATE(object);
switch (prop_id) {
case PROP_FILENAME:
priv->filename = g_value_dup_string(value);
break;
case PROP_KEYFILE:
priv->keyfile = g_value_dup_boxed(value);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
break;
}
}
/*****************************************************************************/
static void
nm_vpn_plugin_info_init(NMVpnPluginInfo *plugin)
{}
static gboolean
init_sync(GInitable *initable, GCancellable *cancellable, GError **error)
{
NMVpnPluginInfo * self = NM_VPN_PLUGIN_INFO(initable);
NMVpnPluginInfoPrivate *priv = NM_VPN_PLUGIN_INFO_GET_PRIVATE(self);
gs_strfreev char ** groups = NULL;
guint i, j;
if (!priv->keyfile) {
if (!priv->filename) {
g_set_error_literal(error,
NM_VPN_PLUGIN_ERROR,
NM_VPN_PLUGIN_ERROR_BAD_ARGUMENTS,
_("missing filename to load VPN plugin info"));
return FALSE;
}
priv->keyfile = g_key_file_new();
if (!g_key_file_load_from_file(priv->keyfile, priv->filename, G_KEY_FILE_NONE, error))
return FALSE;
}
/* we reqire at least a "name" */
priv->name =
g_key_file_get_string(priv->keyfile, NM_VPN_PLUGIN_INFO_KF_GROUP_CONNECTION, "name", NULL);
if (!priv->name || !priv->name[0]) {
g_set_error_literal(error,
NM_VPN_PLUGIN_ERROR,
NM_VPN_PLUGIN_ERROR_BAD_ARGUMENTS,
_("missing name for VPN plugin info"));
return FALSE;
}
/* we also require "service", because that how we associate NMSettingVpn:service-type with the
* NMVpnPluginInfo. */
priv->service = g_key_file_get_string(priv->keyfile,
NM_VPN_PLUGIN_INFO_KF_GROUP_CONNECTION,
"service",
NULL);
if (!priv->service || !*priv->service) {
g_set_error_literal(error,
NM_VPN_PLUGIN_ERROR,
NM_VPN_PLUGIN_ERROR_BAD_ARGUMENTS,
_("missing service for VPN plugin info"));
return FALSE;
}
priv->aliases = g_key_file_get_string_list(priv->keyfile,
NM_VPN_PLUGIN_INFO_KF_GROUP_CONNECTION,
"aliases",
NULL,
NULL);
if (priv->aliases && !priv->aliases[0])
nm_clear_g_free(&priv->aliases);
priv->keys = g_hash_table_new_full(_nm_utils_strstrdictkey_hash,
_nm_utils_strstrdictkey_equal,
g_free,
g_free);
groups = g_key_file_get_groups(priv->keyfile, NULL);
for (i = 0; groups && groups[i]; i++) {
gs_strfreev char **keys = NULL;
keys = g_key_file_get_keys(priv->keyfile, groups[i], NULL, NULL);
for (j = 0; keys && keys[j]; j++) {
char *s;
/* Lookup the value via get_string(). We want that behavior for all our
* values. */
s = g_key_file_get_string(priv->keyfile, groups[i], keys[j], NULL);
if (s)
g_hash_table_insert(priv->keys,
_nm_utils_strstrdictkey_create(groups[i], keys[j]),
s);
}
}
nm_clear_pointer(&priv->keyfile, g_key_file_unref);
return TRUE;
}
/**
* nm_vpn_plugin_info_new_from_file:
* @filename: filename to read.
* @error: on failure, the error reason.
*
* Read the plugin info from file @filename. Does not do
* any further verification on the file. You might want to check
* file permissions and ownership of the file.
*
* Returns: %NULL if there is any error or a newly created
* #NMVpnPluginInfo instance.
*
* Since: 1.2
*/
NMVpnPluginInfo *
nm_vpn_plugin_info_new_from_file(const char *filename, GError **error)
{
g_return_val_if_fail(filename, NULL);
return NM_VPN_PLUGIN_INFO(g_initable_new(NM_TYPE_VPN_PLUGIN_INFO,
NULL,
error,
NM_VPN_PLUGIN_INFO_FILENAME,
filename,
NULL));
}
/**
* nm_vpn_plugin_info_new_with_data:
* @filename: optional filename.
* @keyfile: inject data for the plugin info instance.
* @error: construction may fail if the keyfile lacks mandatory fields.
* In this case, return the error reason.
*
* This constructor does not read any data from file but
* takes instead a @keyfile argument.
*
* Returns: new plugin info instance.
*
* Since: 1.2
*/
NMVpnPluginInfo *
nm_vpn_plugin_info_new_with_data(const char *filename, GKeyFile *keyfile, GError **error)
{
g_return_val_if_fail(keyfile, NULL);
return NM_VPN_PLUGIN_INFO(g_initable_new(NM_TYPE_VPN_PLUGIN_INFO,
NULL,
error,
NM_VPN_PLUGIN_INFO_FILENAME,
filename,
NM_VPN_PLUGIN_INFO_KEYFILE,
keyfile,
NULL));
}
static void
dispose(GObject *object)
{
NMVpnPluginInfo * self = NM_VPN_PLUGIN_INFO(object);
NMVpnPluginInfoPrivate *priv = NM_VPN_PLUGIN_INFO_GET_PRIVATE(self);
g_clear_object(&priv->editor_plugin);
G_OBJECT_CLASS(nm_vpn_plugin_info_parent_class)->dispose(object);
}
static void
finalize(GObject *object)
{
NMVpnPluginInfo * self = NM_VPN_PLUGIN_INFO(object);
NMVpnPluginInfoPrivate *priv = NM_VPN_PLUGIN_INFO_GET_PRIVATE(self);
g_free(priv->name);
g_free(priv->service);
g_free(priv->auth_dialog);
g_strfreev(priv->aliases);
g_free(priv->filename);
g_hash_table_unref(priv->keys);
nm_clear_pointer(&priv->keyfile, g_key_file_unref);
G_OBJECT_CLASS(nm_vpn_plugin_info_parent_class)->finalize(object);
}
static void
nm_vpn_plugin_info_class_init(NMVpnPluginInfoClass *plugin_class)
{
GObjectClass *object_class = G_OBJECT_CLASS(plugin_class);
object_class->set_property = set_property;
object_class->get_property = get_property;
object_class->dispose = dispose;
object_class->finalize = finalize;
/**
* NMVpnPluginInfo:name:
*
* The name of the VPN plugin.
*
* Since: 1.2
*/
g_object_class_install_property(object_class,
PROP_NAME,
g_param_spec_string(NM_VPN_PLUGIN_INFO_NAME,
"",
"",
NULL,
G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
/**
* NMVpnPluginInfo:filename:
*
* The filename from which the info was loaded.
* Can be %NULL if the instance was not loaded from
* a file (i.e. the keyfile instance was passed to the
* constructor).
*
* Since: 1.2
*/
g_object_class_install_property(
object_class,
PROP_FILENAME,
g_param_spec_string(NM_VPN_PLUGIN_INFO_FILENAME,
"",
"",
NULL,
G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
/**
* NMVpnPluginInfo:keyfile:
*
* Initialize the instance with a different keyfile instance.
* When passing a keyfile instance, the constructor will not
* try to read from filename.
*
* Since: 1.2
*/
g_object_class_install_property(
object_class,
PROP_KEYFILE,
g_param_spec_boxed(NM_VPN_PLUGIN_INFO_KEYFILE,
"",
"",
G_TYPE_KEY_FILE,
G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
}
static void
nm_vpn_plugin_info_initable_iface_init(GInitableIface *iface)
{
iface->init = init_sync;
}