// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- /* * Copyright 2011 Giovanni Campagna * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2, or (at your option) * any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA * 02111-1307, USA. * */ 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; 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, contentOverride) { this.parent({ styleClass: 'prompt-dialog' }); this._agent = agent; this._requestId = requestId; this._connection = connection; this._settingName = settingName; this._hints = hints; if (contentOverride) this._content = contentOverride; else this._content = this._getContent(); let mainContentBox = new St.BoxLayout({ style_class: 'prompt-dialog-main-layout', vertical: false }); this.contentLayout.add(mainContentBox, { x_fill: true, y_fill: true }); let icon = new St.Icon({ icon_name: 'dialog-password-symbolic' }); mainContentBox.add(icon, { x_fill: true, y_fill: false, x_align: St.Align.END, y_align: St.Align.START }); let messageBox = new St.BoxLayout({ style_class: 'prompt-dialog-message-layout', vertical: true }); mainContentBox.add(messageBox, { y_align: St.Align.START }); let subjectLabel = new St.Label({ style_class: 'prompt-dialog-headline', text: this._content.title }); messageBox.add(subjectLabel, { y_fill: false, y_align: St.Align.START }); if (this._content.message != null) { let descriptionLabel = new St.Label({ style_class: 'prompt-dialog-description', text: this._content.message, // HACK: for reasons unknown to me, the label // is not asked the correct height for width, // and thus is underallocated // place a fixed height to avoid overflowing style: 'height: 3em' }); descriptionLabel.clutter_text.line_wrap = true; messageBox.add(descriptionLabel, { y_fill: true, y_align: St.Align.START, expand: true }); } let secretTable = new St.Table({ style_class: 'network-dialog-secret-table' }); let initialFocusSet = false; let pos = 0; for (let i = 0; i < this._content.secrets.length; i++) { let secret = this._content.secrets[i]; let label = new St.Label({ style_class: 'prompt-dialog-password-label', text: secret.label }); let reactive = secret.key != null; secret.entry = new St.Entry({ style_class: 'prompt-dialog-password-entry', text: secret.value, can_focus: reactive, reactive: reactive }); ShellEntry.addContextMenu(secret.entry, { isPassword: secret.password }); if (secret.validate) secret.valid = secret.validate(secret); else // no special validation, just ensure it's not empty secret.valid = secret.value.length > 0; if (reactive) { if (!initialFocusSet) { this.setInitialKeyFocus(secret.entry); initialFocusSet = true; } secret.entry.clutter_text.connect('activate', Lang.bind(this, this._onOk)); secret.entry.clutter_text.connect('text-changed', Lang.bind(this, function() { secret.value = secret.entry.get_text(); if (secret.validate) secret.valid = secret.validate(secret); else secret.valid = secret.value.length > 0; this._updateOkButton(); })); } else secret.valid = true; secretTable.add(label, { row: pos, col: 0, x_expand: false, x_fill: true, x_align: St.Align.START, y_align: St.Align.START }); secretTable.add(secret.entry, { row: pos, col: 1, x_expand: true, x_fill: true, y_align: St.Align.END }); pos++; if (secret.password) secret.entry.clutter_text.set_password_char('\u25cf'); } messageBox.add(secretTable); this._okButton = { label: _("Connect"), action: Lang.bind(this, this._onOk), key: Clutter.KEY_Return, }; this.setButtons([{ label: _("Cancel"), action: Lang.bind(this, this.cancel), key: Clutter.KEY_Escape, }, this._okButton]); }, _updateOkButton: function() { let valid = true; for (let i = 0; i < this._content.secrets.length; i++) { let secret = this._content.secrets[i]; valid = valid && secret.valid; } this._okButton.button.reactive = valid; this._okButton.button.can_focus = valid; if (valid) this._okButton.button.remove_style_pseudo_class('disabled'); else this._okButton.button.add_style_pseudo_class('disabled'); }, _onOk: function() { let valid = true; for (let i = 0; i < this._content.secrets.length; i++) { let secret = this._content.secrets[i]; valid = valid && secret.valid; if (secret.key != null) this._agent.set_password(this._requestId, secret.key, secret.value); } if (valid) { 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, Shell.NetworkAgentResponse.USER_CANCELED); this.close(global.get_current_time()); }, _validateWpaPsk: function(secret) { let value = secret.value; if (value.length == 64) { // must be composed of hexadecimal digits only for (let i = 0; i < 64; i++) { if (!((value[i] >= 'a' && value[i] <= 'f') || (value[i] >= 'A' && value[i] <= 'F') || (value[i] >= '0' && value[i] <= '9'))) return false; } return true; } return (value.length >= 8 && value.length <= 63); }, _validateStaticWep: function(secret) { let value = secret.value; if (secret.wep_key_type == NetworkManager.WepKeyType.KEY) { if (value.length == 10 || value.length == 26) { for (let i = 0; i < value.length; i++) { if (!((value[i] >= 'a' && value[i] <= 'f') || (value[i] >= 'A' && value[i] <= 'F') || (value[i] >= '0' && value[i] <= '9'))) return false; } } else if (value.length == 5 || value.length == 13) { for (let i = 0; i < value.length; i++) { if (!((value[i] >= 'a' && value[i] <= 'z') || (value[i] >= 'A' && value[i] <= 'Z'))) return false; } } else return false; } else if (secret.wep_key_type == NetworkManager.WepKeyType.PASSPHRASE) { if (value.length < 0 || value.length > 64) return false; } return true; }, _getWirelessSecrets: function(secrets, wirelessSetting) { let wirelessSecuritySetting = this._connection.get_setting_wireless_security(); switch (wirelessSecuritySetting.key_mgmt) { // First the easy ones case 'wpa-none': case 'wpa-psk': secrets.push({ label: _("Password: "), key: 'psk', value: wirelessSecuritySetting.psk || '', validate: this._validateWpaPsk, password: true }); break; case 'none': // static WEP secrets.push({ label: _("Key: "), key: 'wep-key' + wirelessSecuritySetting.wep_tx_keyidx, value: wirelessSecuritySetting.get_wep_key(wirelessSecuritySetting.wep_tx_keyidx) || '', wep_key_type: wirelessSecuritySetting.wep_key_type, validate: this._validateStaticWep, password: true }); break; case 'ieee8021x': if (wirelessSecuritySetting.auth_alg == 'leap') // Cisco LEAP secrets.push({ label: _("Password: "), key: 'leap-password', value: wirelessSecuritySetting.leap_password || '', password: true }); else // Dynamic (IEEE 802.1x) WEP this._get8021xSecrets(secrets); break; case 'wpa-eap': this._get8021xSecrets(secrets); break; default: log('Invalid wireless key management: ' + wirelessSecuritySetting.key_mgmt); } }, _get8021xSecrets: function(secrets) { let ieee8021xSetting = this._connection.get_setting_802_1x(); let phase2method; switch (ieee8021xSetting.get_eap_method(0)) { case 'md5': case 'leap': case 'ttls': case '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) secrets.push({ label: _("Username: "), key: null, value: ieee8021xSetting.identity || '', password: false }); secrets.push({ label: _("Password: "), key: 'password', value: ieee8021xSetting.password || '', password: true }); break; case 'tls': secrets.push({ label: _("Identity: "), key: null, value: ieee8021xSetting.identity || '', password: false }); secrets.push({ label: _("Private key password: "), key: 'private-key-password', value: ieee8021xSetting.private_key_password || '', password: true }); break; default: log('Invalid EAP/IEEE802.1x method: ' + ieee8021xSetting.get_eap_method(0)); } }, _getPPPoESecrets: function(secrets) { let pppoeSetting = this._connection.get_setting_pppoe(); secrets.push({ label: _("Username: "), key: 'username', value: pppoeSetting.username || '', password: false }); secrets.push({ label: _("Service: "), key: 'service', value: pppoeSetting.service || '', password: false }); secrets.push({ label: _("Password: "), key: 'password', value: pppoeSetting.password || '', password: true }); }, _getMobileSecrets: function(secrets, connectionType) { let setting; if (connectionType == 'bluetooth') setting = this._connection.get_setting_cdma() || this._connection.get_setting_gsm(); else setting = this._connection.get_setting_by_name(connectionType); secrets.push({ label: _("Password: "), key: 'password', value: setting.value || '', password: true }); }, _getContent: function() { let connectionSetting = this._connection.get_setting_connection(); let connectionType = connectionSetting.get_connection_type(); let wirelessSetting; let ssid; let content = { }; content.secrets = [ ]; switch (connectionType) { case '802-11-wireless': wirelessSetting = this._connection.get_setting_wireless(); ssid = NetworkManager.utils_ssid_to_utf8(wirelessSetting.get_ssid()); content.title = _("Authentication required by wireless network"); content.message = _("Passwords or encryption keys are required to access the wireless network '%s'.").format(ssid); this._getWirelessSecrets(content.secrets, wirelessSetting); break; case '802-3-ethernet': content.title = _("Wired 802.1X authentication"); content.message = null; content.secrets.push({ label: _("Network name: "), key: null, value: connectionSetting.get_id(), password: false }); this._get8021xSecrets(content.secrets); break; case 'pppoe': content.title = _("DSL authentication"); content.message = null; this._getPPPoESecrets(content.secrets); break; case 'gsm': if (this._hints.indexOf('pin') != -1) { let gsmSetting = this._connection.get_setting_gsm(); content.title = _("PIN code required"); content.message = _("PIN code is needed for the mobile broadband device"); content.secrets.push({ label: _("PIN: "), key: 'pin', value: gsmSetting.pin || '', password: true }); } // fall through case 'cdma': case 'bluetooth': content.title = _("Mobile broadband network password"); content.message = _("A password is required to connect to '%s'.").format(connectionSetting.get_id()); this._getMobileSecrets(content.secrets, connectionType); break; default: log('Invalid connection type: ' + connectionType); }; return content; } }); 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(), -1, 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', _init: function() { this._native = new Shell.NetworkAgent({ auto_register: true, 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, 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]; })); this._dialogs[requestId] = dialog; dialog.open(global.get_current_time()); }, _cancelRequest: function(agent, requestId) { 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'); } } });