diff --git a/js/ui/networkAgent.js b/js/ui/networkAgent.js index 2f8848098..10b090b6c 100644 --- a/js/ui/networkAgent.js +++ b/js/ui/networkAgent.js @@ -21,6 +21,8 @@ const Clutter = imports.gi.Clutter; const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; const Lang = imports.lang; const NetworkManager = imports.gi.NetworkManager; const NMClient = imports.gi.NMClient; @@ -28,15 +30,18 @@ const Pango = imports.gi.Pango; const Shell = imports.gi.Shell; const St = imports.gi.St; +const Config = imports.misc.config; const ModalDialog = imports.ui.modalDialog; const PopupMenu = imports.ui.popupMenu; const ShellEntry = imports.ui.shellEntry; +const VPN_UI_GROUP = 'VPN Plugin UI'; + const NetworkSecretDialog = new Lang.Class({ Name: 'NetworkSecretDialog', Extends: ModalDialog.ModalDialog, - _init: function(agent, requestId, connection, settingName, hints) { + _init: function(agent, requestId, connection, settingName, hints, contentOverride) { this.parent({ styleClass: 'prompt-dialog' }); this._agent = agent; @@ -45,7 +50,10 @@ const NetworkSecretDialog = new Lang.Class({ this._settingName = settingName; this._hints = hints; - this._content = this._getContent(); + if (contentOverride) + this._content = contentOverride; + else + this._content = this._getContent(); let mainContentBox = new St.BoxLayout({ style_class: 'prompt-dialog-main-layout', vertical: false }); @@ -174,14 +182,14 @@ const NetworkSecretDialog = new Lang.Class({ } if (valid) { - this._agent.respond(this._requestId, false); + this._agent.respond(this._requestId, Shell.NetworkAgentResponse.CONFIRMED); this.close(global.get_current_time()); } // do nothing if not valid }, cancel: function() { - this._agent.respond(this._requestId, true); + this._agent.respond(this._requestId, Shell.NetworkAgentResponse.USER_CANCELED); this.close(global.get_current_time()); }, @@ -357,6 +365,240 @@ const NetworkSecretDialog = new Lang.Class({ } }); +const VPNRequestHandler = new Lang.Class({ + Name: 'VPNRequestHandler', + + _init: function(agent, requestId, authHelper, serviceType, connection, hints, flags) { + this._agent = agent; + this._requestId = requestId; + this._connection = connection; + this._pluginOutBuffer = []; + this._title = null; + this._description = null; + this._content = [ ]; + this._shellDialog = null; + + let connectionSetting = connection.get_setting_connection(); + + let argv = [ authHelper.fileName, + '-u', connectionSetting.uuid, + '-n', connectionSetting.id, + '-s', serviceType + ]; + if (authHelper.externalUIMode) + argv.push('--external-ui-mode'); + if (flags & NMClient.SecretAgentGetSecretsFlags.ALLOW_INTERACTION) + argv.push('-i'); + if (flags & NMClient.SecretAgentGetSecretsFlags.REQUEST_NEW) + argv.push('-r'); + + this._newStylePlugin = authHelper.externalUIMode; + + try { + let [success, pid, stdin, stdout, stderr] = + GLib.spawn_async_with_pipes(null, /* pwd */ + argv, + null, /* envp */ + GLib.SpawnFlags.DO_NOT_REAP_CHILD, + null /* child_setup */); + + this._childPid = pid; + this._stdin = new Gio.UnixOutputStream({ fd: stdin, close_fd: true }); + this._stdout = new Gio.UnixInputStream({ fd: stdout, close_fd: true }); + // We need this one too, even if don't actually care of what the process + // has to say on stderr, because otherwise the fd opened by g_spawn_async_with_pipes + // is kept open indefinitely + let stderrStream = new Gio.UnixInputStream({ fd: stderr, close_fd: true }); + stderrStream.close(null); + this._dataStdout = new Gio.DataInputStream({ base_stream: this._stdout }); + + if (this._newStylePlugin) + this._readStdoutNewStyle(); + else + this._readStdoutOldStyle(); + + this._childWatch = GLib.child_watch_add(GLib.PRIORITY_DEFAULT, pid, + Lang.bind(this, this._vpnChildFinished)); + + this._writeConnection(); + } catch(e) { + logError(e, 'error while spawning VPN auth helper'); + + this._agent.respond(requestId, Shell.NetworkAgentResponse.INTERNAL_ERROR); + } + }, + + cancel: function() { + if (this._newStylePlugin && this._shellDialog) { + this._shellDialog.close(global.get_current_time()); + this._shellDialog.destroy(); + } else { + try { + this._stdin.write('QUIT\n\n', null); + } catch(e) { /* ignore broken pipe errors */ } + } + + this.destroy(); + }, + + destroy: function() { + if (this._destroyed) + return; + + GLib.source_remove(this._childWatch); + + this._stdin.close(null); + // Stdout is closed when we finish reading from it + + this._destroyed = true; + }, + + _vpnChildFinished: function(pid, status, requestObj) { + if (this._newStylePlugin) { + // For new style plugin, all work is done in the async reading functions + // Just reap the process here + return; + } + + let [exited, exitStatus] = Shell.util_wifexited(status); + + if (exited) { + if (exitStatus != 0) + this._agent.respond(this._requestId, Shell.NetworkAgentResponse.USER_CANCELED); + else + this._agent.respond(this._requestId, Shell.NetworkAgentResponse.CONFIRMED); + } else + this._agent.respond(this._requestId, Shell.NetworkAgentResponse.INTERNAL_ERROR); + + this.destroy(); + }, + + _vpnChildProcessLineOldStyle: function(line) { + if (this._previousLine != undefined) { + // Two consecutive newlines mean that the child should be closed + // (the actual newlines are eaten by Gio.DataInputStream) + // Send a termination message + if (line == '' && this._previousLine == '') { + try { + this._stdin.write('QUIT\n\n', null); + } catch(e) { /* ignore broken pipe errors */ } + } else { + this._agent.set_password(this._requestId, this._previousLine, line); + this._previousLine = undefined; + } + } else { + this._previousLine = line; + } + }, + + _readStdoutOldStyle: function() { + this._dataStdout.read_line_async(GLib.PRIORITY_DEFAULT, null, Lang.bind(this, function(stream, result) { + let [line, len] = this._dataStdout.read_line_finish_utf8(result); + + if (line == null) { + // end of file + this._stdout.close(null); + return; + } + + this._vpnChildProcessLineOldStyle(line); + + // try to read more! + this._readStdoutOldStyle(); + })); + }, + + _readStdoutNewStyle: function() { + this._dataStdout.fill_async(-1, GLib.PRIORITY_DEFAULT, null, Lang.bind(this, function(stream, result) { + let cnt = this._dataStdout.fill_finish(result); + + if (cnt == 0) { + // end of file + this._showNewStyleDialog(); + + this._stdout.close(null); + return; + } + + // Try to read more + this._dataStdout.set_buffer_size(2 * this._dataStdout.get_buffer_size()); + this._readStdoutNewStyle(); + })); + }, + + _showNewStyleDialog: function() { + let keyfile = new GLib.KeyFile(); + let contentOverride; + + try { + keyfile.load_from_data(this._dataStdout.peek_buffer(), + GLib.KeyFileFlags.NONE); + + if (keyfile.get_integer(VPN_UI_GROUP, 'Version') != 2) + throw new Error('Invalid plugin keyfile version, is %d'); + + contentOverride = { title: keyfile.get_string(VPN_UI_GROUP, 'Title'), + message: keyfile.get_string(VPN_UI_GROUP, 'Description'), + secrets: [] }; + + let [groups, len] = keyfile.get_groups(); + for (let i = 0; i < groups.length; i++) { + if (groups[i] == VPN_UI_GROUP) + continue; + + let value = keyfile.get_string(groups[i], 'Value'); + let shouldAsk = keyfile.get_boolean(groups[i], 'ShouldAsk'); + + if (shouldAsk) { + contentOverride.secrets.push({ label: keyfile.get_string(groups[i], 'Label'), + key: groups[i], + value: value, + password: keyfile.get_boolean(groups[i], 'IsSecret') + }); + } else { + if (!value.length) // Ignore empty secrets + continue; + + this._agent.set_password(this._requestId, groups[i], value); + } + } + } catch(e) { + logError(e, 'error while reading VPN plugin output keyfile'); + + this._agent.respond(this._requestId, Shell.NetworkAgentResponse.INTERNAL_ERROR); + return; + } + + if (contentOverride.secrets.length) { + // Only show the dialog if we actually have something to ask + this._shellDialog = new NetworkSecretDialog(this._agent, this._requestId, this._connection, 'vpn', [], contentOverride); + this._shellDialog.open(global.get_current_time()); + } else { + this._agent.respond(this._requestId, Shell.NetworkAgentResponse.CONFIRMED); + } + }, + + _writeConnection: function() { + let vpnSetting = this._connection.get_setting_vpn(); + + try { + vpnSetting.foreach_data_item(Lang.bind(this, function(key, value) { + this._stdin.write('DATA_KEY=' + key + '\n', null); + this._stdin.write('DATA_VAL=' + (value || '') + '\n\n', null); + })); + vpnSetting.foreach_secret(Lang.bind(this, function(key, value) { + this._stdin.write('SECRET_KEY=' + key + '\n', null); + this._stdin.write('SECRET_VAL=' + (value || '') + '\n\n', null); + })); + this._stdin.write('DONE\n\n', null); + } catch(e) { + logError(e, 'internal error while writing connection to helper'); + + this._agent.respond(this._requestId, Shell.NetworkAgentResponse.INTERNAL_ERROR); + } + }, +}); + const NetworkAgent = new Lang.Class({ Name: 'NetworkAgent', @@ -365,11 +607,18 @@ const NetworkAgent = new Lang.Class({ identifier: 'org.gnome.Shell.NetworkAgent' }); this._dialogs = { }; + this._vpnRequests = { }; + this._native.connect('new-request', Lang.bind(this, this._newRequest)); this._native.connect('cancel-request', Lang.bind(this, this._cancelRequest)); }, - _newRequest: function(agent, requestId, connection, settingName, hints) { + _newRequest: function(agent, requestId, connection, settingName, hints, flags) { + if (settingName == 'vpn') { + this._vpnRequest(requestId, connection, hints, flags); + return; + } + let dialog = new NetworkSecretDialog(agent, requestId, connection, settingName, hints); dialog.connect('destroy', Lang.bind(this, function() { delete this._dialogs[requestId]; @@ -379,7 +628,74 @@ const NetworkAgent = new Lang.Class({ }, _cancelRequest: function(agent, requestId) { - this._dialogs[requestId].close(global.get_current_time()); - this._dialogs[requestId].destroy(); + if (this._dialogs[requestId]) { + this._dialogs[requestId].close(global.get_current_time()); + this._dialogs[requestId].destroy(); + delete this._dialogs[requestId]; + } else if (this._vpnRequests[requestId]) { + this._vpnRequests[requestId].cancel(); + delete this._vpnRequests[requestId]; + } + }, + + _vpnRequest: function(requestId, connection, hints, flags) { + let vpnSetting = connection.get_setting_vpn(); + let serviceType = vpnSetting.service_type; + + this._buildVPNServiceCache(); + + let binary = this._vpnBinaries[serviceType]; + if (!binary) { + log('Invalid VPN service type (cannot find authentication binary)'); + + /* cancel the auth process */ + this._native.respond(requestId, Shell.NetworkAgentResponse.INTERNAL_ERROR); + return; + } + + this._vpnRequests[requestId] = new VPNRequestHandler(this._native, requestId, binary, serviceType, connection, hints, flags); + }, + + _buildVPNServiceCache: function() { + if (this._vpnCacheBuilt) + return; + + this._vpnCacheBuilt = true; + this._vpnBinaries = { }; + + let dir = Gio.file_new_for_path(GLib.build_filenamev([Config.SYSCONFDIR, 'NetworkManager/VPN'])); + try { + let fileEnum = dir.enumerate_children('standard::name', Gio.FileQueryInfoFlags.NONE, null); + let info; + + while ((info = fileEnum.next_file(null))) { + let name = info.get_name(); + if (name.substr(-5) != '.name') + continue; + + try { + let keyfile = new GLib.KeyFile(); + keyfile.load_from_file(dir.get_child(name).get_path(), GLib.KeyFileFlags.NONE); + let service = keyfile.get_string('VPN Connection', 'service'); + let binary = keyfile.get_string('GNOME', 'auth-dialog'); + let externalUIMode = false; + try { + externalUIMode = keyfile.get_boolean('GNOME', 'external-ui-mode'); + } catch(e) { } // ignore errors if key does not exist + let path = GLib.build_filenamev([Config.LIBEXECDIR, binary]); + + if (GLib.file_test(path, GLib.FileTest.IS_EXECUTABLE)) + this._vpnBinaries[service] = { fileName: path, externalUIMode: externalUIMode }; + else + throw new Error('VPN plugin at %s is not executable'.format(path)); + } catch(e) { + log('Error \'%s\' while processing VPN keyfile \'%s\''. + format(e.message, dir.get_child(name).get_path())); + continue; + } + } + } catch(e) { + logError(e, 'error while enumerating VPN auth helpers'); + } } }); diff --git a/src/shell-network-agent.c b/src/shell-network-agent.c index 0e547aaa8..b24999b75 100644 --- a/src/shell-network-agent.c +++ b/src/shell-network-agent.c @@ -22,6 +22,7 @@ #include "config.h" #include #include +#include #include "shell-network-agent.h" @@ -47,6 +48,8 @@ typedef struct { /* */ GHashTable *entries; + GHashTable *vpn_entries; + gboolean is_vpn; } ShellAgentRequest; struct _ShellNetworkAgentPrivate { @@ -68,7 +71,6 @@ shell_agent_request_free (gpointer data) g_object_unref (request->connection); g_free (request->setting_name); g_strfreev (request->hints); - g_hash_table_destroy (request->entries); g_slice_free (ShellAgentRequest, request); @@ -122,7 +124,8 @@ request_secrets_from_ui (ShellAgentRequest *request) request->request_id, request->connection, request->setting_name, - request->hints); + request->hints, + (int)request->flags); } static void @@ -274,11 +277,17 @@ get_secrets_keyring_cb (GnomeKeyringResult result, && (attr->type == GNOME_KEYRING_ATTRIBUTE_TYPE_STRING)) { gchar *secret_name = g_strdup (attr->value.string); - GValue *secret_value = g_slice_new0 (GValue); - g_value_init (secret_value, G_TYPE_STRING); - g_value_set_string (secret_value, item->secret); - g_hash_table_insert (closure->entries, secret_name, secret_value); + if (!closure->is_vpn) + { + GValue *secret_value = g_slice_new0 (GValue); + g_value_init (secret_value, G_TYPE_STRING); + g_value_set_string (secret_value, item->secret); + + g_hash_table_insert (closure->entries, secret_name, secret_value); + } + else + g_hash_table_insert (closure->vpn_entries, secret_name, g_strdup (item->secret)); if (closure->hints) n_found += strv_has (closure->hints, secret_name); @@ -293,7 +302,6 @@ get_secrets_keyring_cb (GnomeKeyringResult result, if (n_found == 0 && (closure->flags & NM_SECRET_AGENT_GET_SECRETS_FLAG_ALLOW_INTERACTION)) { - /* Even if n_found == 0, secrets is not necessarily empty */ nm_connection_update_secrets (closure->connection, closure->setting_name, closure->entries, NULL); request_secrets_from_ui (closure); @@ -327,17 +335,8 @@ shell_network_agent_get_secrets (NMSecretAgent *agent, NMSettingConnection *setting_connection; const char *connection_type; - /* VPN secrets are currently unimplemented - bail out early */ setting_connection = nm_connection_get_setting_connection (connection); connection_type = nm_setting_connection_get_connection_type (setting_connection); - if (strcmp (connection_type, "vpn") == 0) - { - GError *error = g_error_new (NM_SECRET_AGENT_ERROR, - NM_SECRET_AGENT_ERROR_AGENT_CANCELED, - "VPN secrets are currently unhandled."); - callback (NM_SECRET_AGENT (self), connection, NULL, error, callback_data); - return; - } request = g_slice_new (ShellAgentRequest); request->self = g_object_ref (self); @@ -347,8 +346,24 @@ shell_network_agent_get_secrets (NMSecretAgent *agent, request->flags = flags; request->callback = callback; request->callback_data = callback_data; + request->is_vpn = !strcmp(connection_type, NM_SETTING_VPN_SETTING_NAME); + request->keyring_op = NULL; request->entries = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, gvalue_destroy_notify); + if (request->is_vpn) + { + GValue *secret_value; + + request->vpn_entries = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); + + secret_value = g_slice_new0 (GValue); + g_value_init (secret_value, dbus_g_type_get_map ("GHashTable", G_TYPE_STRING, G_TYPE_STRING)); + g_value_take_boxed (secret_value, request->vpn_entries); + g_hash_table_insert (request->entries, g_strdup(NM_SETTING_VPN_SECRETS), secret_value); + } + else + request->vpn_entries = NULL; + request->request_id = g_strdup_printf ("%s/%s", connection_path, setting_name); g_hash_table_replace (self->priv->requests, request->request_id, request); @@ -388,17 +403,24 @@ shell_network_agent_set_password (ShellNetworkAgent *self, priv = self->priv; request = g_hash_table_lookup (priv->requests, request_id); - value = g_slice_new0 (GValue); - g_value_init (value, G_TYPE_STRING); - g_value_set_string (value, setting_value); + if (!request->is_vpn) + { + value = g_slice_new0 (GValue); + g_value_init (value, G_TYPE_STRING); + g_value_set_string (value, setting_value); - g_hash_table_replace (request->entries, g_strdup (setting_key), value); + g_hash_table_replace (request->entries, g_strdup (setting_key), value); + } + else + { + g_hash_table_replace (request->vpn_entries, g_strdup (setting_key), g_strdup (setting_value)); + } } void -shell_network_agent_respond (ShellNetworkAgent *self, - gchar *request_id, - gboolean canceled) +shell_network_agent_respond (ShellNetworkAgent *self, + gchar *request_id, + ShellNetworkAgentResponse response) { ShellNetworkAgentPrivate *priv; ShellAgentRequest *request; @@ -410,7 +432,7 @@ shell_network_agent_respond (ShellNetworkAgent *self, priv = self->priv; request = g_hash_table_lookup (priv->requests, request_id); - if (canceled) + if (response == SHELL_NETWORK_AGENT_USER_CANCELED) { GError *error = g_error_new (NM_SECRET_AGENT_ERROR, NM_SECRET_AGENT_ERROR_USER_CANCELED, @@ -422,10 +444,24 @@ shell_network_agent_respond (ShellNetworkAgent *self, return; } + if (response == SHELL_NETWORK_AGENT_INTERNAL_ERROR) + { + GError *error = g_error_new (NM_SECRET_AGENT_ERROR, + NM_SECRET_AGENT_ERROR_INTERNAL_ERROR, + "An internal error occurred while processing the request."); + + request->callback (NM_SECRET_AGENT (self), request->connection, NULL, error, request->callback_data); + g_error_free (error); + g_hash_table_remove (priv->requests, request_id); + return; + } + + /* response == SHELL_NETWORK_AGENT_CONFIRMED */ + /* Save updated secrets */ dup = nm_connection_duplicate (request->connection); - nm_connection_update_secrets (dup, request->setting_name, request->entries, NULL); + nm_connection_update_secrets (dup, request->setting_name, request->entries, NULL); nm_secret_agent_save_secrets (NM_SECRET_AGENT (self), dup, NULL, NULL); outer = g_hash_table_new (g_str_hash, g_str_equal); @@ -776,11 +812,12 @@ shell_network_agent_class_init (ShellNetworkAgentClass *klass) NULL, /* accu_data */ NULL, /* marshaller */ G_TYPE_NONE, /* return */ - 3, /* n_params */ + 5, /* n_params */ G_TYPE_STRING, NM_TYPE_CONNECTION, G_TYPE_STRING, - G_TYPE_STRV); + G_TYPE_STRV, + G_TYPE_INT); signals[SIGNAL_CANCEL_REQUEST] = g_signal_new ("cancel-request", G_TYPE_FROM_CLASS (klass), diff --git a/src/shell-network-agent.h b/src/shell-network-agent.h index 179939a48..e27b02eee 100644 --- a/src/shell-network-agent.h +++ b/src/shell-network-agent.h @@ -9,6 +9,12 @@ G_BEGIN_DECLS +typedef enum { + SHELL_NETWORK_AGENT_CONFIRMED, + SHELL_NETWORK_AGENT_USER_CANCELED, + SHELL_NETWORK_AGENT_INTERNAL_ERROR +} ShellNetworkAgentResponse; + typedef struct _ShellNetworkAgent ShellNetworkAgent; typedef struct _ShellNetworkAgentClass ShellNetworkAgentClass; typedef struct _ShellNetworkAgentPrivate ShellNetworkAgentPrivate; @@ -45,7 +51,7 @@ void shell_network_agent_set_password (ShellNetworkAgent *self, gchar *setting_value); void shell_network_agent_respond (ShellNetworkAgent *self, gchar *request_id, - gboolean canceled); + ShellNetworkAgentResponse response); /* If these are kept in sync with nm-applet, secrets will be shared */ #define SHELL_KEYRING_UUID_TAG "connection-uuid" diff --git a/src/shell-util.c b/src/shell-util.c index c389425ab..6647baf3d 100644 --- a/src/shell-util.c +++ b/src/shell-util.c @@ -2,6 +2,9 @@ #include "config.h" +#include +#include + #include "shell-util.h" #include #include @@ -861,3 +864,26 @@ shell_session_is_active_for_systemd (void) return TRUE; #endif } + +/** + * shell_util_wifexited: + * @status: the status returned by wait() or waitpid() + * @exit: (out): the actual exit status of the process + * + * Implements libc standard WIFEXITED, that cannot be used JS + * code. + * Returns: TRUE if the process exited normally, FALSE otherwise + */ +gboolean +shell_util_wifexited (int status, + int *exit) +{ + gboolean ret; + + ret = WIFEXITED(status); + + if (ret) + *exit = WEXITSTATUS(status); + + return ret; +} diff --git a/src/shell-util.h b/src/shell-util.h index 5de947342..5b93bd4b6 100644 --- a/src/shell-util.h +++ b/src/shell-util.h @@ -52,6 +52,9 @@ void shell_shader_effect_set_double_uniform (ClutterShaderEffect *effect, gboolean shell_session_is_active_for_systemd (void); +gboolean shell_util_wifexited (int status, + int *exit); + G_END_DECLS #endif /* __SHELL_UTIL_H__ */