71515a8a11
After the networking code has been ported to libnm, we can use its API for loading VPN plugins instead of rolling our own ... https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/39
794 lines
30 KiB
JavaScript
794 lines
30 KiB
JavaScript
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
|
|
|
|
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 NM = imports.gi.NM;
|
|
const Pango = imports.gi.Pango;
|
|
const Shell = imports.gi.Shell;
|
|
const Signals = imports.signals;
|
|
const St = imports.gi.St;
|
|
|
|
const Config = imports.misc.config;
|
|
const Dialog = imports.ui.dialog;
|
|
const Main = imports.ui.main;
|
|
const MessageTray = imports.ui.messageTray;
|
|
const ModalDialog = imports.ui.modalDialog;
|
|
const PopupMenu = imports.ui.popupMenu;
|
|
const ShellEntry = imports.ui.shellEntry;
|
|
|
|
const VPN_UI_GROUP = 'VPN Plugin UI';
|
|
|
|
var NetworkSecretDialog = new Lang.Class({
|
|
Name: 'NetworkSecretDialog',
|
|
Extends: ModalDialog.ModalDialog,
|
|
|
|
_init(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 icon = new Gio.ThemedIcon({ name: 'dialog-password-symbolic' });
|
|
let contentParams = { icon,
|
|
title: this._content.title,
|
|
body: this._content.message };
|
|
let contentBox = new Dialog.MessageDialogContent(contentParams);
|
|
this.contentLayout.add_actor(contentBox);
|
|
|
|
let layout = new Clutter.GridLayout({ orientation: Clutter.Orientation.VERTICAL });
|
|
let secretTable = new St.Widget({ style_class: 'network-dialog-secret-table',
|
|
layout_manager: layout });
|
|
layout.hookup_style(secretTable);
|
|
|
|
let rtl = secretTable.get_text_direction() == Clutter.TextDirection.RTL;
|
|
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,
|
|
x_align: Clutter.ActorAlign.START,
|
|
y_align: Clutter.ActorAlign.CENTER });
|
|
label.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
|
|
|
|
let reactive = secret.key != null;
|
|
|
|
secret.entry = new St.Entry({ style_class: 'prompt-dialog-password-entry',
|
|
text: secret.value, can_focus: reactive,
|
|
reactive: reactive,
|
|
x_expand: true });
|
|
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', this._onOk.bind(this));
|
|
secret.entry.clutter_text.connect('text-changed', () => {
|
|
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;
|
|
|
|
if (rtl) {
|
|
layout.attach(secret.entry, 0, pos, 1, 1);
|
|
layout.attach(label, 1, pos, 1, 1);
|
|
} else {
|
|
layout.attach(label, 0, pos, 1, 1);
|
|
layout.attach(secret.entry, 1, pos, 1, 1);
|
|
}
|
|
pos++;
|
|
|
|
if (secret.password)
|
|
secret.entry.clutter_text.set_password_char('\u25cf');
|
|
}
|
|
|
|
contentBox.messageBox.add(secretTable);
|
|
|
|
this._okButton = { label: _("Connect"),
|
|
action: this._onOk.bind(this),
|
|
default: true
|
|
};
|
|
|
|
this.setButtons([{ label: _("Cancel"),
|
|
action: this.cancel.bind(this),
|
|
key: Clutter.KEY_Escape,
|
|
},
|
|
this._okButton]);
|
|
|
|
this._updateOkButton();
|
|
},
|
|
|
|
_updateOkButton() {
|
|
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;
|
|
},
|
|
|
|
_onOk() {
|
|
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() {
|
|
this._agent.respond(this._requestId, Shell.NetworkAgentResponse.USER_CANCELED);
|
|
this.close(global.get_current_time());
|
|
},
|
|
|
|
_validateWpaPsk(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(secret) {
|
|
let value = secret.value;
|
|
if (secret.wep_key_type == NM.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 == NM.WepKeyType.PASSPHRASE) {
|
|
if (value.length < 0 || value.length > 64)
|
|
return false;
|
|
}
|
|
return true;
|
|
},
|
|
|
|
_getWirelessSecrets(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(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':
|
|
case 'fast':
|
|
// 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(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(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() {
|
|
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 = NM.utils_ssid_to_utf8(wirelessSetting.get_ssid().get_data());
|
|
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 });
|
|
break;
|
|
}
|
|
// 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;
|
|
}
|
|
});
|
|
|
|
var VPNRequestHandler = new Lang.Class({
|
|
Name: 'VPNRequestHandler',
|
|
|
|
_init(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 & NM.SecretAgentGetSecretsFlags.ALLOW_INTERACTION)
|
|
argv.push('-i');
|
|
if (flags & NM.SecretAgentGetSecretsFlags.REQUEST_NEW)
|
|
argv.push('-r');
|
|
if (authHelper.supportsHints) {
|
|
for (let i = 0; i < hints.length; i++) {
|
|
argv.push('-t');
|
|
argv.push(hints[i]);
|
|
}
|
|
}
|
|
|
|
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 });
|
|
GLib.close(stderr);
|
|
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,
|
|
this._vpnChildFinished.bind(this));
|
|
|
|
this._writeConnection();
|
|
} catch(e) {
|
|
logError(e, 'error while spawning VPN auth helper');
|
|
|
|
this._agent.respond(requestId, Shell.NetworkAgentResponse.INTERNAL_ERROR);
|
|
}
|
|
},
|
|
|
|
cancel(respond) {
|
|
if (respond)
|
|
this._agent.respond(this._requestId, Shell.NetworkAgentResponse.USER_CANCELED);
|
|
|
|
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() {
|
|
if (this._destroyed)
|
|
return;
|
|
|
|
this.emit('destroy');
|
|
if (this._childWatch)
|
|
GLib.source_remove(this._childWatch);
|
|
|
|
this._stdin.close(null);
|
|
// Stdout is closed when we finish reading from it
|
|
|
|
this._destroyed = true;
|
|
},
|
|
|
|
_vpnChildFinished(pid, status, requestObj) {
|
|
this._childWatch = 0;
|
|
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(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() {
|
|
this._dataStdout.read_line_async(GLib.PRIORITY_DEFAULT, null, (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() {
|
|
this._dataStdout.fill_async(-1, GLib.PRIORITY_DEFAULT, null, (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() {
|
|
let keyfile = new GLib.KeyFile();
|
|
let data;
|
|
let contentOverride;
|
|
|
|
try {
|
|
data = this._dataStdout.peek_buffer();
|
|
|
|
keyfile.load_from_data(data.toString(), data.length,
|
|
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) {
|
|
// No output is a valid case it means "both secrets are stored"
|
|
if (data.length > 0) {
|
|
logError(e, 'error while reading VPN plugin output keyfile');
|
|
|
|
this._agent.respond(this._requestId, Shell.NetworkAgentResponse.INTERNAL_ERROR);
|
|
this.destroy();
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (contentOverride && 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);
|
|
this.destroy();
|
|
}
|
|
},
|
|
|
|
_writeConnection() {
|
|
let vpnSetting = this._connection.get_setting_vpn();
|
|
|
|
try {
|
|
vpnSetting.foreach_data_item((key, value) => {
|
|
this._stdin.write('DATA_KEY=' + key + '\n', null);
|
|
this._stdin.write('DATA_VAL=' + (value || '') + '\n\n', null);
|
|
});
|
|
vpnSetting.foreach_secret((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);
|
|
this.destroy();
|
|
}
|
|
},
|
|
});
|
|
Signals.addSignalMethods(VPNRequestHandler.prototype);
|
|
|
|
var NetworkAgent = new Lang.Class({
|
|
Name: 'NetworkAgent',
|
|
|
|
_init() {
|
|
this._native = new Shell.NetworkAgent({ identifier: 'org.gnome.Shell.NetworkAgent',
|
|
capabilities: NM.SecretAgentCapabilities.VPN_HINTS,
|
|
auto_register: false
|
|
});
|
|
|
|
this._dialogs = { };
|
|
this._vpnRequests = { };
|
|
this._notifications = { };
|
|
|
|
this._pluginDir = Gio.file_new_for_path(Config.VPNDIR);
|
|
try {
|
|
let monitor = this._pluginDir.monitor(Gio.FileMonitorFlags.NONE, null);
|
|
monitor.connect('changed', () => { this._vpnCacheBuilt = false; });
|
|
} catch(e) {
|
|
log('Failed to create monitor for VPN plugin dir: ' + e.message);
|
|
}
|
|
|
|
this._native.connect('new-request', this._newRequest.bind(this));
|
|
this._native.connect('cancel-request', this._cancelRequest.bind(this));
|
|
try {
|
|
this._native.init(null);
|
|
} catch(e) {
|
|
this._native = null;
|
|
logError(e, 'error initializing the NetworkManager Agent');
|
|
}
|
|
},
|
|
|
|
enable() {
|
|
if (!this._native)
|
|
return;
|
|
|
|
this._native.auto_register = true;
|
|
if (!this._native.registered)
|
|
this._native.register_async(null, null);
|
|
},
|
|
|
|
disable() {
|
|
let requestId;
|
|
|
|
for (requestId in this._dialogs)
|
|
this._dialogs[requestId].cancel();
|
|
this._dialogs = { };
|
|
|
|
for (requestId in this._vpnRequests)
|
|
this._vpnRequests[requestId].cancel(true);
|
|
this._vpnRequests = { };
|
|
|
|
for (requestId in this._notifications)
|
|
this._notifications[requestId].destroy();
|
|
this._notifications = { };
|
|
|
|
if (!this._native)
|
|
return;
|
|
|
|
this._native.auto_register = false;
|
|
if (this._native.registered)
|
|
this._native.unregister_async(null, null);
|
|
},
|
|
|
|
_showNotification(requestId, connection, settingName, hints, flags) {
|
|
let source = new MessageTray.Source(_("Network Manager"), 'network-transmit-receive');
|
|
source.policy = new MessageTray.NotificationApplicationPolicy('gnome-network-panel');
|
|
|
|
let title, body;
|
|
|
|
let connectionSetting = connection.get_setting_connection();
|
|
let connectionType = connectionSetting.get_connection_type();
|
|
switch (connectionType) {
|
|
case '802-11-wireless':
|
|
let wirelessSetting = connection.get_setting_wireless();
|
|
let ssid = NM.utils_ssid_to_utf8(wirelessSetting.get_ssid());
|
|
title = _("Authentication required by wireless network");
|
|
body = _("Passwords or encryption keys are required to access the wireless network “%s”.").format(ssid);
|
|
break;
|
|
case '802-3-ethernet':
|
|
title = _("Wired 802.1X authentication");
|
|
body = _("A password is required to connect to “%s”.".format(connection.get_id()));
|
|
break;
|
|
case 'pppoe':
|
|
title = _("DSL authentication");
|
|
body = _("A password is required to connect to “%s”.".format(connection.get_id()));
|
|
break;
|
|
case 'gsm':
|
|
if (hints.indexOf('pin') != -1) {
|
|
let gsmSetting = connection.get_setting_gsm();
|
|
title = _("PIN code required");
|
|
message = _("PIN code is needed for the mobile broadband device");
|
|
break;
|
|
}
|
|
// fall through
|
|
case 'cdma':
|
|
case 'bluetooth':
|
|
title = _("Mobile broadband network password");
|
|
message = _("A password is required to connect to “%s”.").format(connectionSetting.get_id());
|
|
break;
|
|
default:
|
|
log('Invalid connection type: ' + connectionType);
|
|
this._native.respond(requestId, Shell.NetworkAgentResponse.INTERNAL_ERROR);
|
|
return;
|
|
}
|
|
|
|
let notification = new MessageTray.Notification(source, title, body);
|
|
|
|
notification.connect('activated', () => {
|
|
notification.answered = true;
|
|
this._handleRequest(requestId, connection, settingName, hints, flags);
|
|
});
|
|
|
|
this._notifications[requestId] = notification;
|
|
notification.connect('destroy', () => {
|
|
if (!notification.answered)
|
|
this._native.respond(requestId, Shell.NetworkAgentResponse.USER_CANCELED);
|
|
delete this._notifications[requestId];
|
|
});
|
|
|
|
Main.messageTray.add(source);
|
|
source.notify(notification);
|
|
},
|
|
|
|
_newRequest(agent, requestId, connection, settingName, hints, flags) {
|
|
if (!(flags & NM.SecretAgentGetSecretsFlags.USER_REQUESTED))
|
|
this._showNotification(requestId, connection, settingName, hints, flags);
|
|
else
|
|
this._handleRequest(requestId, connection, settingName, hints, flags);
|
|
},
|
|
|
|
_handleRequest(requestId, connection, settingName, hints, flags) {
|
|
if (settingName == 'vpn') {
|
|
this._vpnRequest(requestId, connection, hints, flags);
|
|
return;
|
|
}
|
|
|
|
let dialog = new NetworkSecretDialog(this._native, requestId, connection, settingName, hints);
|
|
dialog.connect('destroy', () => {
|
|
delete this._dialogs[requestId];
|
|
});
|
|
this._dialogs[requestId] = dialog;
|
|
dialog.open(global.get_current_time());
|
|
},
|
|
|
|
_cancelRequest(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(false);
|
|
delete this._vpnRequests[requestId];
|
|
}
|
|
},
|
|
|
|
_vpnRequest(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;
|
|
}
|
|
|
|
let vpnRequest = new VPNRequestHandler(this._native, requestId, binary, serviceType, connection, hints, flags);
|
|
vpnRequest.connect('destroy', () => {
|
|
delete this._vpnRequests[requestId];
|
|
});
|
|
this._vpnRequests[requestId] = vpnRequest;
|
|
},
|
|
|
|
_buildVPNServiceCache() {
|
|
if (this._vpnCacheBuilt)
|
|
return;
|
|
|
|
this._vpnCacheBuilt = true;
|
|
this._vpnBinaries = { };
|
|
|
|
NM.VpnPluginInfo.list_load().forEach(plugin => {
|
|
let service = plugin.get_service();
|
|
let fileName = plugin.get_auth_dialog();
|
|
let supportsHints = plugin.supports_hints();
|
|
let externalUIMode = false;
|
|
|
|
let prop = plugin.lookup_property('GNOME', 'supports-external-ui-mode');
|
|
if (prop) {
|
|
prop = prop.trim().toLowerCase();
|
|
externalUIMode = ['true', 'yes', 'on', '1'].includes(prop);
|
|
}
|
|
|
|
if (GLib.file_test(fileName, GLib.FileTest.IS_EXECUTABLE)) {
|
|
let binary = { fileName, externalUIMode, supportsHints };
|
|
this._vpnBinaries[service] = binary;
|
|
|
|
plugin.get_aliases().forEach(alias => {
|
|
this._vpnBinaries[alias] = binary;
|
|
});
|
|
} else {
|
|
log('VPN plugin at %s is not executable'.format(fileName));
|
|
}
|
|
});
|
|
}
|
|
});
|
|
var Component = NetworkAgent;
|