// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Clutter from 'gi://Clutter';
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import NM from 'gi://NM';
import Pango from 'gi://Pango';
import Shell from 'gi://Shell';
import St from 'gi://St';
import * as Signals from '../../misc/signals.js';

import * as Dialog from '../dialog.js';
import * as Main from '../main.js';
import * as MessageTray from '../messageTray.js';
import * as ModalDialog from '../modalDialog.js';
import * as ShellEntry from '../shellEntry.js';

Gio._promisify(Shell.NetworkAgent.prototype, 'init_async');
Gio._promisify(Shell.NetworkAgent.prototype, 'search_vpn_plugin');

const VPN_UI_GROUP = 'VPN Plugin UI';

const NetworkSecretDialog = GObject.registerClass(
class NetworkSecretDialog extends ModalDialog.ModalDialog {
    _init(agent, requestId, connection, settingName, hints, flags, contentOverride) {
        super._init({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 contentBox = new Dialog.MessageDialogContent({
            title: this._content.title,
            description: this._content.message,
        });

        let initialFocusSet = false;
        for (let i = 0; i < this._content.secrets.length; i++) {
            let secret = this._content.secrets[i];
            let reactive = secret.key != null;

            let entryParams = {
                style_class: 'prompt-dialog-password-entry',
                hint_text: secret.label,
                text: secret.value,
                can_focus: reactive,
                reactive,
                x_align: Clutter.ActorAlign.CENTER,
            };
            if (secret.password)
                secret.entry = new St.PasswordEntry(entryParams);
            else
                secret.entry = new St.Entry(entryParams);
            ShellEntry.addContextMenu(secret.entry);
            contentBox.add_child(secret.entry);

            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 (this._content.secrets.some(s => s.password)) {
            let capsLockWarning = new ShellEntry.CapsLockWarning();
            contentBox.add_child(capsLockWarning);
        }

        if (flags & NM.SecretAgentGetSecretsFlags.WPS_PBC_ACTIVE) {
            let descriptionLabel = new St.Label({
                text: _('Alternatively you can connect by pushing the “WPS” button on your router.'),
                style_class: 'message-dialog-description',
            });
            descriptionLabel.clutter_text.line_wrap = true;
            descriptionLabel.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;

            contentBox.add_child(descriptionLabel);
        }

        this.contentLayout.add_child(contentBox);

        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 &&= 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 &&= secret.valid;
            if (secret.key !== null) {
                if (this._settingName === 'vpn')
                    this._agent.add_vpn_secret(this._requestId, secret.key, secret.value);
                else
                    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();

        if (this._settingName === '802-1x') {
            this._get8021xSecrets(secrets);
            return;
        }

        switch (wirelessSecuritySetting.key_mgmt) {
        // First the easy ones
        case 'wpa-none':
        case 'wpa-psk':
        case 'sae':
            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();

        /* If hints were given we know exactly what we need to ask */
        if (this._settingName === '802-1x' && this._hints.length) {
            if (this._hints.includes('identity')) {
                secrets.push({
                    label: _('Username'),
                    key: 'identity',
                    value: ieee8021xSetting.identity || '',
                    password: false,
                });
            }
            if (this._hints.includes('password')) {
                secrets.push({
                    label: _('Password'),
                    key: 'password',
                    value: ieee8021xSetting.password || '',
                    password: true,
                });
            }
            if (this._hints.includes('private-key-password')) {
                secrets.push({
                    label: _('Private key password'),
                    key: 'private-key-password',
                    value: ieee8021xSetting.private_key_password || '',
                    password: true,
                });
            }
            return;
        }

        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');
            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.includes('pin')) {
                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 = _('Authentication required');
            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;
    }
});

class VPNRequestHandler extends Signals.EventEmitter {
    constructor(agent, requestId, authHelper, serviceType, connection, hints, flags) {
        super();

        this._agent = agent;
        this._requestId = requestId;
        this._connection = connection;
        this._flags = flags;
        this._pluginOutBuffer = [];
        this._title = null;
        this._description = null;
        this._content = [];
        this._shellDialog = null;

        let connectionSetting = connection.get_setting_connection();

        const 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 {
            const launchContext = global.create_app_launch_context(0, -1);
            let [pid, stdin, stdout, stderr] =
                Shell.util_spawn_async_with_pipes(
                    null, /* pwd */
                    argv,
                    launchContext.get_environment(),
                    GLib.SpawnFlags.DO_NOT_REAP_CHILD);

            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.add_vpn_secret(this._requestId, this._previousLine, line);
                this._previousLine = undefined;
            }
        } else {
            this._previousLine = line;
        }
    }

    async _readStdoutOldStyle() {
        const [line, len_] =
            await this._dataStdout.read_line_async(GLib.PRIORITY_DEFAULT, null);

        if (line === null) {
            // end of file
            this._stdout.close(null);
            return;
        }

        const decoder = new TextDecoder();
        this._vpnChildProcessLineOldStyle(decoder.decode(line));

        // try to read more!
        this._readStdoutOldStyle();
    }

    async _readStdoutNewStyle() {
        const cnt =
            await this._dataStdout.fill_async(-1, GLib.PRIORITY_DEFAULT, null);

        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 = new GLib.Bytes(this._dataStdout.peek_buffer());
            keyfile.load_from_bytes(data, 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,
                        password: keyfile.get_boolean(groups[i], 'IsSecret'),
                    });
                } else {
                    if (!value.length) // Ignore empty secrets
                        continue;

                    this._agent.add_vpn_secret(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', [], this._flags, 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();
        }
    }
}

class NetworkAgent {
    constructor() {
        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._native.connect('new-request', this._newRequest.bind(this));
        this._native.connect('cancel-request', this._cancelRequest.bind(this));

        this._initialized = false;
        this._initNative();
    }

    async _initNative() {
        try {
            await this._native.init_async(GLib.PRIORITY_DEFAULT, null);
            this._initialized = true;
        } catch (e) {
            this._native = null;
            logError(e, 'error initializing the NetworkManager Agent');
        }
    }

    enable() {
        if (!this._native)
            return;

        this._native.auto_register = true;
        if (this._initialized && !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._initialized && 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().get_data());
            title = _('Authentication required');
            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.includes('pin')) {
                title = _('PIN code required');
                body = _('PIN code is needed for the mobile broadband device');
                break;
            }
            // fall through
        case 'cdma':
        case 'bluetooth':
            title = _('Authentication required');
            body = _('A password is required to connect to “%s”.').format(connectionSetting.get_id());
            break;
        case 'vpn':
            title = _('VPN password');
            body = _('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.showNotification(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, flags);
        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];
        }
    }

    async _vpnRequest(requestId, connection, hints, flags) {
        let vpnSetting = connection.get_setting_vpn();
        let serviceType = vpnSetting.service_type;

        let binary = await this._findAuthBinary(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;
    }

    async _findAuthBinary(serviceType) {
        let plugin;

        try {
            plugin = await this._native.search_vpn_plugin(serviceType);
        } catch (e) {
            logError(e);
            return null;
        }

        const fileName = plugin.get_auth_dialog();
        if (!GLib.file_test(fileName, GLib.FileTest.IS_EXECUTABLE)) {
            log(`VPN plugin at ${fileName} is not executable`);
            return null;
        }

        const prop = plugin.lookup_property('GNOME', 'supports-external-ui-mode');
        const trimmedProp = prop?.trim().toLowerCase() ?? '';

        return {
            fileName,
            supportsHints: plugin.supports_hints(),
            externalUIMode: ['true', 'yes', 'on', '1'].includes(trimmedProp),
        };
    }
}

export {NetworkAgent as Component};
