diff --git a/vpn/openconnect/CMakeLists.txt b/vpn/openconnect/CMakeLists.txt index c3f07088..368e29ab 100644 --- a/vpn/openconnect/CMakeLists.txt +++ b/vpn/openconnect/CMakeLists.txt @@ -1,71 +1,71 @@ add_definitions(-DTRANSLATION_DOMAIN=\"plasmanetworkmanagement_openconnectui\") set (MINIMUM_OPENCONNECT_VERSION_REQUIRED "3.99") set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_MODULE_PATH}) find_package(OpenConnect ${MINIMUM_OPENCONNECT_VERSION_REQUIRED} MODULE) if (OPENCONNECT_FOUND) find_package(Qt5Xml ${QT_MIN_VERSION} CONFIG REQUIRED) if (${OPENCONNECT_VERSION} VERSION_LESS "3.99") # macro_optional_find_package(OpenSSL) # macro_log_feature(OpenSSL_FOUND "OpenSSL headers" "Encryption suite" "http://www.openssl.org" FALSE "" "Needed for OpenConnect support in Network Management") find_package(OpenSSL) set_package_properties(OpenSSL PROPERTIES DESCRIPTION "Encryption suite" URL "http://www.openssl.org" TYPE OPTIONAL ) if (OpenSSL_FOUND) set (MINIMUM_OPENCONNECT_VERSION_REQUIRED "3.03") set (maybe_OPENSSL_LIBRARIES ${OpenSSL_LIBRARIES}) endif() endif() if (${OPENCONNECT_VERSION} VERSION_GREATER ${MINIMUM_OPENCONNECT_VERSION_REQUIRED} OR ${OPENCONNECT_VERSION} VERSION_EQUAL ${MINIMUM_OPENCONNECT_VERSION_REQUIRED}) include_directories(${OPENCONNECT_INCLUDE_DIRS}) set(openconnect_SRCS ../../libs/debug.cpp openconnectui.cpp openconnectwidget.cpp openconnectauth.cpp openconnectauthworkerthread.cpp ) - ki18n_wrap_ui(openconnect_SRCS openconnectprop.ui openconnectauth.ui) + ki18n_wrap_ui(openconnect_SRCS openconnectprop.ui openconnectauth.ui openconnecttoken.ui) add_library(plasmanetworkmanagement_openconnectui ${openconnect_SRCS}) target_link_libraries(plasmanetworkmanagement_openconnectui plasmanm_internal plasmanm_editor ${NETWORKMANAGERQT5_LDFLAGS} KF5::I18n KF5::WidgetsAddons KF5::IconThemes KF5::KIOWidgets KF5::CoreAddons Qt5::Xml ${OPENCONNECT_LIBRARIES} ${maybe_OPENSSL_LIBRARIES} ) install(TARGETS plasmanetworkmanagement_openconnectui DESTINATION ${KDE_INSTALL_PLUGINDIR}) install(FILES plasmanetworkmanagement_openconnectui.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}) install(FILES plasmanetworkmanagement_openconnect_juniperui.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}) else() message("ERROR: OpenConnection version '${OPENCONNECT_VERSION}' does not match minimum required (${MINIMUM_OPENCONNECT_VERSION_REQUIRED})") message("OpenConnect plugin will not be built") set(OPENCONNECT_FOUND False) endif() else (OPENCONNECT_FOUND) message("OpenConnect plugin will not be built") endif (OPENCONNECT_FOUND) #macro_log_feature(OPENCONNECT_FOUND "OpenConnect headers and library" "Cisco AnyConnect compatible VPN client" "http://www.infradead.org/openconnect.html" FALSE ${MINIMUM_OPENCONNECT_VERSION_REQUIRED} "Needed for OpenConnect support in Plasma NM") diff --git a/vpn/openconnect/nm-openconnect-service.h b/vpn/openconnect/nm-openconnect-service.h index f4bd98a1..6cd4af78 100644 --- a/vpn/openconnect/nm-openconnect-service.h +++ b/vpn/openconnect/nm-openconnect-service.h @@ -1,48 +1,51 @@ /* -*- Mode: C; tab-width: 5; indent-tabs-mode: t; c-basic-offset: 5 -*- */ /* NetworkManager -- Network link manager * * 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 of the License, 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., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. * * Copyright © 2008 - 2009 Intel Corporation. //krazy:exclude=copyright * * Based on nm-openconnect-vpnc.h: * Copyright © 2005 - 2008 Red Hat, Inc. //krazy:exclude=copyright * Copyright © 2007 - 2008 Novell, Inc. //krazy:exclude=copyright */ #ifndef NM_OPENCONNECT_SERVICE_H #define NM_OPENCONNECT_SERVICE_H #define NM_DBUS_SERVICE_OPENCONNECT "org.freedesktop.NetworkManager.openconnect" #define NM_DBUS_INTERFACE_OPENCONNECT "org.freedesktop.NetworkManager.openconnect" #define NM_DBUS_PATH_OPENCONNECT "/org/freedesktop/NetworkManager/openconnect" #define NM_OPENCONNECT_KEY_GATEWAY "gateway" #define NM_OPENCONNECT_KEY_COOKIE "cookie" #define NM_OPENCONNECT_KEY_GWCERT "gwcert" #define NM_OPENCONNECT_KEY_AUTHTYPE "authtype" #define NM_OPENCONNECT_KEY_USERCERT "usercert" #define NM_OPENCONNECT_KEY_CACERT "cacert" #define NM_OPENCONNECT_KEY_PRIVKEY "userkey" #define NM_OPENCONNECT_KEY_MTU "mtu" #define NM_OPENCONNECT_KEY_PEM_PASSPHRASE_FSID "pem_passphrase_fsid" +#define NM_OPENCONNECT_KEY_PREVENT_INVALID_CERT "prevent_invalid_cert" #define NM_OPENCONNECT_KEY_PROTOCOL "protocol" #define NM_OPENCONNECT_KEY_PROXY "proxy" #define NM_OPENCONNECT_KEY_CSD_ENABLE "enable_csd_trojan" #define NM_OPENCONNECT_KEY_CSD_WRAPPER "csd_wrapper" +#define NM_OPENCONNECT_KEY_TOKEN_MODE "stoken_source" +#define NM_OPENCONNECT_KEY_TOKEN_SECRET "stoken_string" #define NM_OPENCONNECT_USER "nm-openconnect" #endif /* NM_OPENCONNECT_PLUGIN_H */ diff --git a/vpn/openconnect/openconnectauth.cpp b/vpn/openconnect/openconnectauth.cpp index fb71c015..0bdb4e1f 100644 --- a/vpn/openconnect/openconnectauth.cpp +++ b/vpn/openconnect/openconnectauth.cpp @@ -1,698 +1,785 @@ /* Copyright 2011 Ilia Kats Copyright 2013 Lukáš Tinkl This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) version 3, or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 6 of version 3 of the license. This library 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library. If not, see . */ #include "openconnectauth.h" #include "openconnectauthworkerthread.h" #include "ui_openconnectauth.h" #include "debug.h" #include "passwordfield.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "nm-openconnect-service.h" #include extern "C" { #include #include #include } +#if !OPENCONNECT_CHECK_VER(2,1) +#define __openconnect_set_token_mode(...) -EOPNOTSUPP +#elif !OPENCONNECT_CHECK_VER(2,2) +#define __openconnect_set_token_mode(vpninfo, mode, secret) openconnect_set_stoken_mode(vpninfo, 1, secret) +#else +#define __openconnect_set_token_mode openconnect_set_token_mode +#endif + +#if OPENCONNECT_CHECK_VER(3,4) + static int updateToken(void*, const char*); +#endif + // name/address: IP/domain name of the host (OpenConnect accepts both, so no difference here) // group: user group on the server typedef struct { QString name; QString group; QString address; } VPNHost; +typedef struct { + oc_token_mode_t tokenMode; + QByteArray tokenSecret; +} Token; + class OpenconnectAuthWidgetPrivate { public: Ui_OpenconnectAuth ui; NetworkManager::VpnSetting::Ptr setting; struct openconnect_info *vpninfo; NMStringMap secrets; NMStringMap tmpSecrets; QMutex mutex; QWaitCondition workerWaiting; OpenconnectAuthWorkerThread *worker; QList hosts; bool userQuit; bool formGroupChanged; int cancelPipes[2]; QList > serverLog; int passwordFormIndex; + QByteArray tokenMode; + Token token; enum LogLevels {Error = 0, Info, Debug, Trace}; }; OpenconnectAuthWidget::OpenconnectAuthWidget(const NetworkManager::VpnSetting::Ptr &setting, QWidget * parent) : SettingWidget(setting, parent) , d_ptr(new OpenconnectAuthWidgetPrivate) { Q_D(OpenconnectAuthWidget); d->setting = setting; d->ui.setupUi(this); d->userQuit = false; d->formGroupChanged = false; if (pipe2(d->cancelPipes, O_NONBLOCK|O_CLOEXEC)) { // Should never happen. Just don't do real cancellation if it does d->cancelPipes[0] = -1; d->cancelPipes[1] = -1; } connect(d->ui.cmbLogLevel, QOverload::of(&QComboBox::currentIndexChanged), this, &OpenconnectAuthWidget::logLevelChanged); connect(d->ui.viewServerLog, &QCheckBox::toggled, this, &OpenconnectAuthWidget::viewServerLogToggled); connect(d->ui.btnConnect, &QPushButton::clicked, this, &OpenconnectAuthWidget::connectHost); d->ui.cmbLogLevel->setCurrentIndex(OpenconnectAuthWidgetPrivate::Debug); d->ui.btnConnect->setIcon(QIcon::fromTheme("network-connect")); d->ui.viewServerLog->setChecked(false); d->worker = new OpenconnectAuthWorkerThread(&d->mutex, &d->workerWaiting, &d->userQuit, &d->formGroupChanged, d->cancelPipes[0]); // gets the pointer to struct openconnect_info (defined in openconnect.h), which contains data that OpenConnect needs, // and which needs to be populated with settings we get from NM, like host, certificate or private key d->vpninfo = d->worker->getOpenconnectInfo(); connect(d->worker, QOverload::of(&OpenconnectAuthWorkerThread::validatePeerCert), this, &OpenconnectAuthWidget::validatePeerCert); connect(d->worker, &OpenconnectAuthWorkerThread::processAuthForm, this, &OpenconnectAuthWidget::processAuthForm); connect(d->worker, &OpenconnectAuthWorkerThread::updateLog, this, &OpenconnectAuthWidget::updateLog); connect(d->worker, QOverload::of(&OpenconnectAuthWorkerThread::writeNewConfig), this, &OpenconnectAuthWidget::writeNewConfig); connect(d->worker, &OpenconnectAuthWorkerThread::cookieObtained, this, &OpenconnectAuthWidget::workerFinished); + connect(d->worker, &OpenconnectAuthWorkerThread::initTokens, this, &OpenconnectAuthWidget::initTokens); readConfig(); readSecrets(); +#if OPENCONNECT_CHECK_VER(3,4) + openconnect_set_token_callbacks(d->vpninfo, &d->secrets, NULL, &updateToken); +#endif + // This might be set by readSecrets() so don't connect it until now connect(d->ui.cmbHosts, QOverload::of(&QComboBox::currentIndexChanged), this, &OpenconnectAuthWidget::connectHost); KAcceleratorManager::manage(this); } OpenconnectAuthWidget::~OpenconnectAuthWidget() { Q_D(OpenconnectAuthWidget); d->userQuit = true; if (write(d->cancelPipes[1], "x", 1)) { // not a lot we can do } d->workerWaiting.wakeAll(); d->worker->wait(); ::close(d->cancelPipes[0]); ::close(d->cancelPipes[1]); deleteAllFromLayout(d->ui.loginBoxLayout); delete d->worker; delete d; } void OpenconnectAuthWidget::readConfig() { Q_D(OpenconnectAuthWidget); const NMStringMap dataMap = d->setting->data(); if (!dataMap[NM_OPENCONNECT_KEY_GATEWAY].isEmpty()) { const QString gw = dataMap[NM_OPENCONNECT_KEY_GATEWAY]; VPNHost host; const int index = gw.indexOf(QLatin1Char('/')); if (index > -1) { host.name = host.address = gw.left(index); host.group = gw.right(gw.length() - index - 1); } else { host.name = host.address = gw; } d->hosts.append(host); } if (!dataMap[NM_OPENCONNECT_KEY_CACERT].isEmpty()) { const QByteArray crt = QFile::encodeName(dataMap[NM_OPENCONNECT_KEY_CACERT]); openconnect_set_cafile(d->vpninfo, OC3DUP(crt.data())); } if (dataMap[NM_OPENCONNECT_KEY_CSD_ENABLE] == "yes") { char *wrapper; wrapper = nullptr; if (!dataMap[NM_OPENCONNECT_KEY_CSD_WRAPPER].isEmpty()) { const QByteArray wrapperScript = QFile::encodeName(dataMap[NM_OPENCONNECT_KEY_CSD_WRAPPER]); wrapper = strdup(wrapperScript.data()); } openconnect_setup_csd(d->vpninfo, getuid(), 1, wrapper); } if (!dataMap[NM_OPENCONNECT_KEY_PROXY].isEmpty()) { const QByteArray proxy = QFile::encodeName(dataMap[NM_OPENCONNECT_KEY_PROXY]); openconnect_set_http_proxy(d->vpninfo, OC3DUP(proxy.data())); } if (!dataMap[NM_OPENCONNECT_KEY_USERCERT].isEmpty()) { const QByteArray crt = QFile::encodeName(dataMap[NM_OPENCONNECT_KEY_USERCERT]); const QByteArray key = QFile::encodeName(dataMap[NM_OPENCONNECT_KEY_PRIVKEY]); openconnect_set_client_cert (d->vpninfo, OC3DUP(crt.data()), OC3DUP(key.data())); if (!crt.isEmpty() && dataMap[NM_OPENCONNECT_KEY_PEM_PASSPHRASE_FSID] == "yes") { openconnect_passphrase_from_fsid(d->vpninfo); } } if (!dataMap[NM_OPENCONNECT_KEY_PROTOCOL].isEmpty()) { const QString protocol = dataMap[NM_OPENCONNECT_KEY_PROTOCOL]; openconnect_set_protocol(d->vpninfo, OC3DUP(protocol == "juniper" ? "nc" : protocol.toUtf8().data())); } + + d->tokenMode = dataMap[NM_OPENCONNECT_KEY_TOKEN_MODE].toUtf8(); } void OpenconnectAuthWidget::readSecrets() { Q_D(OpenconnectAuthWidget); d->secrets = d->setting->secrets(); if (!d->secrets["xmlconfig"].isEmpty()) { const QByteArray config = QByteArray::fromBase64(d->secrets["xmlconfig"].toAscii()); QCryptographicHash hash(QCryptographicHash::Sha1); hash.addData(config.data(), config.size()); const char *sha1_text = hash.result().toHex(); openconnect_set_xmlsha1 (d->vpninfo, (char *)sha1_text, strlen(sha1_text)+1); QDomDocument xmlconfig; xmlconfig.setContent(config); const QDomNode anyConnectProfile = xmlconfig.elementsByTagName(QLatin1String("AnyConnectProfile")).at(0); bool matchedGw = false; const QDomNode serverList = anyConnectProfile.firstChildElement(QLatin1String("ServerList")); for (QDomElement entry = serverList.firstChildElement(QLatin1String("HostEntry")); !entry.isNull(); entry = entry.nextSiblingElement(QLatin1String("HostEntry"))) { VPNHost host; host.name = entry.firstChildElement(QLatin1String("HostName")).text(); host.group = entry.firstChildElement(QLatin1String("UserGroup")).text(); host.address = entry.firstChildElement(QLatin1String("HostAddress")).text(); // We added the originally configured host in readConfig(). But if // it matches one of the ones in the XML config (as presumably it // should), remove the original and use the one with the pretty name. if (!matchedGw && host.address == d->hosts.at(0).address) { d->hosts.removeFirst(); matchedGw = true; } d->hosts.append(host); } } for (int i = 0; i < d->hosts.size(); i++) { d->ui.cmbHosts->addItem(d->hosts.at(i).name, i); if (d->secrets["lasthost"] == d->hosts.at(i).name || d->secrets["lasthost"] == d->hosts.at(i).address) { d->ui.cmbHosts->setCurrentIndex(i); } } if (d->secrets["autoconnect"] == "yes") { d->ui.chkAutoconnect->setChecked(true); QTimer::singleShot(0, this, &OpenconnectAuthWidget::connectHost); } if (d->secrets["save_passwords"] == "yes") { d->ui.chkStorePasswords->setChecked(true); } + + d->token.tokenMode = OC_TOKEN_MODE_NONE; + d->token.tokenSecret = nullptr; + + if (!d->tokenMode.isEmpty()) { + int ret = 0; + QByteArray tokenSecret = d->secrets[NM_OPENCONNECT_KEY_TOKEN_SECRET].toUtf8(); + + if (d->tokenMode == QStringLiteral("manual") && !tokenSecret.isEmpty()) { + ret = __openconnect_set_token_mode(d->vpninfo, OC_TOKEN_MODE_STOKEN, tokenSecret); + } else if (d->tokenMode ==QStringLiteral("stokenrc")) { + ret = __openconnect_set_token_mode(d->vpninfo, OC_TOKEN_MODE_STOKEN, NULL); + } else if (d->tokenMode == QStringLiteral("totp") && !tokenSecret.isEmpty()) { + ret = __openconnect_set_token_mode(d->vpninfo, OC_TOKEN_MODE_TOTP, tokenSecret); + } +#if OPENCONNECT_CHECK_VER(3,4) + else if (d->tokenMode == QStringLiteral("hotp") && !tokenSecret.isEmpty()) { + ret = __openconnect_set_token_mode(d->vpninfo, OC_TOKEN_MODE_HOTP, tokenSecret); + } +#endif +#if OPENCONNECT_CHECK_VER(5,0) + else if (d->tokenMode == "yubioath") { + /* This needs to be done from a thread because it can call back to + ask for the PIN */ + d->token.tokenMode = OC_TOKEN_MODE_YUBIOATH; + if (!tokenSecret.isEmpty()) { + d->token.tokenSecret = tokenSecret; + } + } +#endif + if (ret) { + addFormInfo(QLatin1String("dialog-error"), i18n("Failed to initialize software token: %1", ret)); + } + } } void OpenconnectAuthWidget::acceptDialog() { // Find top-level widget as this should be the QDialog itself QWidget *widget = parentWidget(); while (widget->parentWidget() != nullptr) { widget = widget->parentWidget(); } QDialog *dialog = qobject_cast(widget); if (dialog) { dialog->accept(); } } // This starts the worker thread, which connects to the selected AnyConnect host // and retrieves the login form void OpenconnectAuthWidget::connectHost() { Q_D(OpenconnectAuthWidget); + d->userQuit = true; if (write(d->cancelPipes[1], "x", 1)) { // not a lot we can do } d->workerWaiting.wakeAll(); d->worker->wait(); d->userQuit = false; /* Suck out the cancel byte(s) */ char buf; while (read(d->cancelPipes[0], &buf, 1) == 1) { ; } deleteAllFromLayout(d->ui.loginBoxLayout); int i = d->ui.cmbHosts->currentIndex(); if (i == -1) { return; } i = d->ui.cmbHosts->itemData(i).toInt(); const VPNHost &host = d->hosts.at(i); if (openconnect_parse_url(d->vpninfo, host.address.toAscii().data())) { qCWarning(PLASMA_NM) << "Failed to parse server URL" << host.address; openconnect_set_hostname(d->vpninfo, OC3DUP(host.address.toAscii().data())); } if (!openconnect_get_urlpath(d->vpninfo) && !host.group.isEmpty()) { openconnect_set_urlpath(d->vpninfo, OC3DUP(host.group.toAscii().data())); } d->secrets["lasthost"] = host.name; addFormInfo(QLatin1String("dialog-information"), i18n("Contacting host, please wait...")); d->worker->start(); } +void OpenconnectAuthWidget::initTokens() +{ + Q_D(OpenconnectAuthWidget); + + if (d->token.tokenMode != OC_TOKEN_MODE_NONE) { + __openconnect_set_token_mode(d->vpninfo, d->token.tokenMode, d->token.tokenSecret); + } +} + QVariantMap OpenconnectAuthWidget::setting() const { Q_D(const OpenconnectAuthWidget); NMStringMap secrets; QVariantMap secretData; secrets.unite(d->secrets); QString host(openconnect_get_hostname(d->vpninfo)); const QString port = QString::number(openconnect_get_port(d->vpninfo)); secrets.insert(QLatin1String(NM_OPENCONNECT_KEY_GATEWAY), host + ':' + port); secrets.insert(QLatin1String(NM_OPENCONNECT_KEY_COOKIE), QLatin1String(openconnect_get_cookie(d->vpninfo))); openconnect_clear_cookie(d->vpninfo); #if OPENCONNECT_CHECK_VER(5,0) const char *fingerprint = openconnect_get_peer_cert_hash(d->vpninfo); #else OPENCONNECT_X509 *cert = openconnect_get_peer_cert(d->vpninfo); char fingerprint[41]; openconnect_get_cert_sha1(d->vpninfo, cert, fingerprint); #endif secrets.insert(QLatin1String(NM_OPENCONNECT_KEY_GWCERT), QLatin1String(fingerprint)); secrets.insert(QLatin1String("autoconnect"), d->ui.chkAutoconnect->isChecked() ? "yes" : "no"); secrets.insert(QLatin1String("save_passwords"), d->ui.chkStorePasswords->isChecked() ? "yes" : "no"); NMStringMap::iterator i = secrets.begin(); while (i != secrets.end()) { if (i.value().isEmpty()) { i = secrets.erase(i); } else { i++; } } secretData.insert("secrets", QVariant::fromValue(secrets)); // These secrets are not officially part of the secrets which would be returned back to NetworkManager. We just // need to somehow get them to our secret agent which will handle them separately and store them. if (!d->tmpSecrets.isEmpty()) { secretData.insert("tmp-secrets", QVariant::fromValue(d->tmpSecrets)); } return secretData; } +#if OPENCONNECT_CHECK_VER(3,4) +static int updateToken(void *cbdata, const char *tok) +{ + NMStringMap *secrets = static_cast(cbdata); + secrets->insert(QLatin1String(NM_OPENCONNECT_KEY_TOKEN_SECRET), QLatin1String(tok)); + return 0; +} +#endif + void OpenconnectAuthWidget::writeNewConfig(const QString & buf) { Q_D(OpenconnectAuthWidget); d->secrets["xmlconfig"] = buf; } void OpenconnectAuthWidget::updateLog(const QString &message, const int &level) { Q_D(OpenconnectAuthWidget); + QPair pair; pair.first = message; if (pair.first.endsWith(QLatin1String("\n"))) { pair.first.chop(1); } switch (level) { case PRG_ERR: pair.second = OpenconnectAuthWidgetPrivate::Error; break; case PRG_INFO: pair.second = OpenconnectAuthWidgetPrivate::Info; break; case PRG_DEBUG: pair.second = OpenconnectAuthWidgetPrivate::Debug; break; case PRG_TRACE: pair.second = OpenconnectAuthWidgetPrivate::Trace; break; } if (pair.second <= d->ui.cmbLogLevel->currentIndex()) { d->ui.serverLog->append(pair.first); } d->serverLog.append(pair); if (d->serverLog.size() > 100) { d->serverLog.removeFirst(); } } void OpenconnectAuthWidget::logLevelChanged(int newLevel) { Q_D(OpenconnectAuthWidget); d->ui.serverLog->clear(); QList >::const_iterator i; for (i = d->serverLog.constBegin(); i != d->serverLog.constEnd(); ++i) { QPair pair = *i; if(pair.second <= newLevel) { d->ui.serverLog->append(pair.first); } } } void OpenconnectAuthWidget::addFormInfo(const QString &iconName, const QString &message) { Q_D(OpenconnectAuthWidget); + QHBoxLayout *layout = new QHBoxLayout(); QLabel *icon = new QLabel(this); QSizePolicy sizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); sizePolicy.setHorizontalStretch(0); sizePolicy.setVerticalStretch(0); sizePolicy.setHeightForWidth(icon->sizePolicy().hasHeightForWidth()); icon->setSizePolicy(sizePolicy); icon->setMinimumSize(QSize(16, 16)); icon->setMaximumSize(QSize(16, 16)); layout->addWidget(icon); QLabel *text = new QLabel(this); text->setAlignment(Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter); text->setWordWrap(true); layout->addWidget(text); icon->setPixmap(QIcon::fromTheme(iconName).pixmap(KIconLoader::SizeSmall)); text->setText(message); d->ui.loginBoxLayout->addLayout(layout); } void OpenconnectAuthWidget::processAuthForm(struct oc_auth_form *form) { Q_D(OpenconnectAuthWidget); + deleteAllFromLayout(d->ui.loginBoxLayout); if (form->banner) { addFormInfo(QLatin1String("dialog-information"), form->banner); } if (form->message) { addFormInfo(QLatin1String("dialog-information"), form->message); } if (form->error) { addFormInfo(QLatin1String("dialog-error"), form->error); } struct oc_form_opt *opt; QFormLayout *layout = new QFormLayout(); QSizePolicy policy(QSizePolicy::Expanding, QSizePolicy::Fixed); bool focusSet = false; for (opt = form->opts; opt; opt = opt->next) { if (opt->type == OC_FORM_OPT_HIDDEN || IGNORE_OPT(opt)) { continue; } QLabel *text = new QLabel(this); text->setAlignment(Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter); text->setText(QString(opt->label)); QWidget *widget = nullptr; const QString key = QString("form:%1:%2").arg(QLatin1String(form->auth_id)).arg(QLatin1String(opt->name)); const QString value = d->secrets.value(key); if (opt->type == OC_FORM_OPT_PASSWORD || opt->type == OC_FORM_OPT_TEXT) { PasswordField *le = new PasswordField(this); le->setText(value); if (opt->type == OC_FORM_OPT_PASSWORD) { le->setPasswordModeEnabled(true); } if (!focusSet && le->text().isEmpty()) { le->setFocus(Qt::OtherFocusReason); focusSet = true; } widget = qobject_cast(le); } else if (opt->type == OC_FORM_OPT_SELECT) { QComboBox *cmb = new QComboBox(this); struct oc_form_opt_select *sopt = reinterpret_cast(opt); #if !OPENCONNECT_CHECK_VER(8,0) const QString protocol = d->setting->data()[NM_OPENCONNECT_KEY_PROTOCOL]; #endif for (int i = 0; i < sopt->nr_choices; i++) { cmb->addItem(QString::fromUtf8(FORMCHOICE(sopt, i)->label), QString::fromUtf8(FORMCHOICE(sopt, i)->name)); if (value == QString::fromUtf8(FORMCHOICE(sopt, i)->name)) { cmb->setCurrentIndex(i); #if !OPENCONNECT_CHECK_VER(8,0) if (protocol != QLatin1String("nc") && sopt == AUTHGROUP_OPT(form) && i != AUTHGROUP_SELECTION(form)) { #else if (sopt == AUTHGROUP_OPT(form) && i != AUTHGROUP_SELECTION(form)) { #endif QTimer::singleShot(0, this, &OpenconnectAuthWidget::formGroupChanged); } } } #if !OPENCONNECT_CHECK_VER(8,0) if (protocol != QLatin1String("nc") && sopt == AUTHGROUP_OPT(form)) { #else if (sopt == AUTHGROUP_OPT(form)) { #endif connect(cmb, QOverload::of(&QComboBox::currentIndexChanged), this, &OpenconnectAuthWidget::formGroupChanged); } widget = qobject_cast(cmb); } if (widget) { widget->setProperty("openconnect_opt", (quintptr)opt); widget->setSizePolicy(policy); layout->addRow(text, widget); } } d->ui.loginBoxLayout->addLayout(layout); d->passwordFormIndex = d->ui.loginBoxLayout->count() - 1; QDialogButtonBox *box = new QDialogButtonBox(this); QPushButton *btn = box->addButton(QDialogButtonBox::Ok); btn->setText(i18n("Login")); btn->setDefault(true); d->ui.loginBoxLayout->addWidget(box); box->setProperty("openconnect_form", (quintptr)form); connect(box, &QDialogButtonBox::accepted, this, &OpenconnectAuthWidget::formLoginClicked); } void OpenconnectAuthWidget::validatePeerCert(const QString &fingerprint, const QString &peerCert, const QString &reason, bool *accepted) { Q_D(OpenconnectAuthWidget); const QString host = QLatin1String(openconnect_get_hostname(d->vpninfo)); const QString port = QString::number(openconnect_get_port(d->vpninfo)); const QString key = QString("certificate:%1:%2").arg(host, port); const QString value = d->secrets.value(key); #if !OPENCONNECT_CHECK_VER(5,0) #define openconnect_check_peer_cert_hash(v,d) strcmp(d, fingerprint.toUtf8().data()) #endif if (openconnect_check_peer_cert_hash(d->vpninfo, value.toUtf8().data())) { QWidget *widget = new QWidget(); QVBoxLayout *verticalLayout; QHBoxLayout *horizontalLayout; QLabel *icon; QLabel *infoText; QTextBrowser *certificate; verticalLayout = new QVBoxLayout(widget); horizontalLayout = new QHBoxLayout(widget); icon = new QLabel(widget); QSizePolicy sizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); sizePolicy.setHorizontalStretch(0); sizePolicy.setVerticalStretch(0); sizePolicy.setHeightForWidth(icon->sizePolicy().hasHeightForWidth()); icon->setSizePolicy(sizePolicy); icon->setMinimumSize(QSize(48, 48)); icon->setMaximumSize(QSize(48, 48)); horizontalLayout->addWidget(icon); infoText = new QLabel(widget); infoText->setAlignment(Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter); horizontalLayout->addWidget(infoText); verticalLayout->addLayout(horizontalLayout); certificate = new QTextBrowser(widget); certificate->setTextInteractionFlags(Qt::TextSelectableByMouse); certificate->setOpenLinks(false); verticalLayout->addWidget(certificate); icon->setPixmap(QIcon::fromTheme("dialog-information").pixmap(KIconLoader::SizeLarge)); infoText->setText(i18n("Check failed for certificate from VPN server \"%1\".\n" "Reason: %2\nAccept it anyway?", openconnect_get_hostname(d->vpninfo),reason)); infoText->setWordWrap(true); certificate->setText(peerCert); QPointer dialog = new QDialog(this); dialog.data()->setWindowModality(Qt::WindowModal); dialog->setLayout(new QVBoxLayout); QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Ok|QDialogButtonBox::Cancel, dialog); connect(buttons, &QDialogButtonBox::accepted, dialog.data(), &QDialog::accept); connect(buttons, &QDialogButtonBox::rejected, dialog.data(), &QDialog::reject); dialog->layout()->addWidget(widget); dialog->layout()->addWidget(buttons); + + const NMStringMap dataMap = d->setting->data(); + buttons->button(QDialogButtonBox::Ok)->setEnabled(dataMap[NM_OPENCONNECT_KEY_PREVENT_INVALID_CERT] != "yes"); + if(dialog.data()->exec() == QDialog::Accepted) { *accepted = true; } else { *accepted = false; } if (dialog) { dialog.data()->deleteLater(); } widget->deleteLater(); } else { *accepted = true; } if (*accepted) { d->secrets.insert(key, QString(fingerprint)); } d->mutex.lock(); d->workerWaiting.wakeAll(); d->mutex.unlock(); } void OpenconnectAuthWidget::formGroupChanged() { Q_D(OpenconnectAuthWidget); d->formGroupChanged = true; formLoginClicked(); } // Writes the user input from the form into the oc_auth_form structs we got from // libopenconnect, and wakes the worker thread up to try to log in and obtain a // cookie with this data void OpenconnectAuthWidget::formLoginClicked() { Q_D(OpenconnectAuthWidget); const int lastIndex = d->ui.loginBoxLayout->count() - 1; QLayout *layout = d->ui.loginBoxLayout->itemAt(d->passwordFormIndex)->layout(); struct oc_auth_form *form = (struct oc_auth_form *) d->ui.loginBoxLayout->itemAt(lastIndex)->widget()->property("openconnect_form").value(); for (int i = 0; i < layout->count(); i++) { QLayoutItem *item = layout->itemAt(i); QWidget *widget = item->widget(); if (widget && widget->property("openconnect_opt").isValid()) { struct oc_form_opt *opt = (struct oc_form_opt *) widget->property("openconnect_opt").value(); const QString key = QString("form:%1:%2").arg(QLatin1String(form->auth_id)).arg(QLatin1String(opt->name)); if (opt->type == OC_FORM_OPT_PASSWORD || opt->type == OC_FORM_OPT_TEXT) { PasswordField *le = qobject_cast(widget); QByteArray text = le->text().toUtf8(); openconnect_set_option_value(opt, text.data()); if (opt->type == OC_FORM_OPT_TEXT) { d->secrets.insert(key, le->text()); } else { d->tmpSecrets.insert(key, le->text()); } } else if (opt->type == OC_FORM_OPT_SELECT) { QComboBox *cbo = qobject_cast(widget); QByteArray text = cbo->itemData(cbo->currentIndex()).toString().toAscii(); openconnect_set_option_value(opt, text.data()); d->secrets.insert(key,cbo->itemData(cbo->currentIndex()).toString()); } } } deleteAllFromLayout(d->ui.loginBoxLayout); d->workerWaiting.wakeAll(); } void OpenconnectAuthWidget::workerFinished(const int &ret) { Q_D(OpenconnectAuthWidget); + if (ret < 0) { QString message; QList >::const_iterator i; for (i = d->serverLog.constEnd()-1; i >= d->serverLog.constBegin(); --i) { QPair pair = *i; if(pair.second <= OpenconnectAuthWidgetPrivate::Error) { message = pair.first; break; } } if (message.isEmpty()) { message = i18n("Connection attempt was unsuccessful."); } deleteAllFromLayout(d->ui.loginBoxLayout); addFormInfo(QLatin1String("dialog-error"), message); } else { deleteAllFromLayout(d->ui.loginBoxLayout); acceptDialog(); } } void OpenconnectAuthWidget::deleteAllFromLayout(QLayout *layout) { while (QLayoutItem *item = layout->takeAt(0)) { if (QLayout *itemLayout = item->layout()) { deleteAllFromLayout(itemLayout); itemLayout->deleteLater(); } else { item->widget()->deleteLater(); } delete item; } layout->invalidate(); } void OpenconnectAuthWidget::viewServerLogToggled(bool toggled) { Q_D(OpenconnectAuthWidget); d->ui.lblLogLevel->setVisible(toggled); d->ui.cmbLogLevel->setVisible(toggled); if (toggled) { delete d->ui.verticalLayout->takeAt(5); QSizePolicy policy = d->ui.serverLogBox->sizePolicy(); policy.setVerticalPolicy(QSizePolicy::Expanding); d->ui.serverLogBox->setSizePolicy(policy); d->ui.serverLog->setVisible(true); } else { QSpacerItem *verticalSpacer = new QSpacerItem(0, 0, QSizePolicy::Minimum, QSizePolicy::Expanding); d->ui.verticalLayout->addItem(verticalSpacer); d->ui.serverLog->setVisible(false); QSizePolicy policy = d->ui.serverLogBox->sizePolicy(); policy.setVerticalPolicy(QSizePolicy::Fixed); d->ui.serverLogBox->setSizePolicy(policy); } } diff --git a/vpn/openconnect/openconnectauth.h b/vpn/openconnect/openconnectauth.h index e5f5cfd8..a3c72100 100644 --- a/vpn/openconnect/openconnectauth.h +++ b/vpn/openconnect/openconnectauth.h @@ -1,67 +1,68 @@ /* Copyright 2011 Ilia Kats Copyright 2013 Lukáš Tinkl This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) version 3, or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 6 of version 3 of the license. This library 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library. If not, see . */ #ifndef OPENCONNECTAUTH_H #define OPENCONNECTAUTH_H #include "settingwidget.h" #include #include class QLayout; struct openconnect_info; struct oc_auth_form; class OpenconnectAuthWidgetPrivate; class OpenconnectAuthWidget : public SettingWidget { Q_OBJECT Q_DECLARE_PRIVATE(OpenconnectAuthWidget) public: explicit OpenconnectAuthWidget(const NetworkManager::VpnSetting::Ptr &setting, QWidget *parent = nullptr); ~OpenconnectAuthWidget() override; virtual void readSecrets(); void readConfig(); QVariantMap setting() const override; private: OpenconnectAuthWidgetPrivate * const d_ptr; void acceptDialog(); void addFormInfo(const QString &, const QString &); void deleteAllFromLayout(QLayout *); private Q_SLOTS: void writeNewConfig(const QString &); void validatePeerCert(const QString &, const QString &, const QString &, bool*); void processAuthForm(struct oc_auth_form *); void updateLog(const QString &, const int &); void logLevelChanged(int); void formLoginClicked(); void formGroupChanged(); void workerFinished(const int&); void viewServerLogToggled(bool); void connectHost(); + void initTokens(); }; #endif // OPENCONNECTAUTH_H diff --git a/vpn/openconnect/openconnectauthworkerthread.cpp b/vpn/openconnect/openconnectauthworkerthread.cpp index 41041623..547c19da 100644 --- a/vpn/openconnect/openconnectauthworkerthread.cpp +++ b/vpn/openconnect/openconnectauthworkerthread.cpp @@ -1,234 +1,235 @@ /* Copyright 2011 Ilia Kats This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) version 3, or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 6 of version 3 of the license. This library 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library. If not, see . */ #include "openconnectauthworkerthread.h" #include #include #include #include #include extern "C" { #include #include #if !OPENCONNECT_CHECK_VER(1,5) #include #include #include #endif #include } #include class OpenconnectAuthStaticWrapper { public: #if OPENCONNECT_CHECK_VER(5,0) static int writeNewConfig(void *obj, const char *str, int num) { if (obj) return static_cast(obj)->writeNewConfig(str, num); return -1; } static int validatePeerCert(void *obj, const char *str) { if (obj) return static_cast(obj)->validatePeerCert(nullptr, str); return -1; } #else static int writeNewConfig(void *obj, char *str, int num) { if (obj) return static_cast(obj)->writeNewConfig(str, num); return -1; } static int validatePeerCert(void *obj, OPENCONNECT_X509 *cert, const char *str) { if (obj) return static_cast(obj)->validatePeerCert(cert, str); return -1; } #endif static int processAuthForm(void *obj, struct oc_auth_form *form) { if (obj) return static_cast(obj)->processAuthFormP(form); return OC_FORM_RESULT_ERR; } static void writeProgress(void *obj, int level, const char *str, ...) { if (obj) { va_list argPtr; va_start(argPtr, str); static_cast(obj)->writeProgress(level, str, argPtr); va_end(argPtr); } } }; OpenconnectAuthWorkerThread::OpenconnectAuthWorkerThread(QMutex *mutex, QWaitCondition *waitForUserInput, bool *userDecidedToQuit, bool *formGroupChanged, int cancelFd) : QThread() , m_mutex(mutex) , m_waitForUserInput(waitForUserInput) , m_userDecidedToQuit(userDecidedToQuit) , m_formGroupChanged(formGroupChanged) { m_openconnectInfo = openconnect_vpninfo_new((char*)"OpenConnect VPN Agent (PlasmaNM - running on KDE)", OpenconnectAuthStaticWrapper::validatePeerCert, OpenconnectAuthStaticWrapper::writeNewConfig, OpenconnectAuthStaticWrapper::processAuthForm, OpenconnectAuthStaticWrapper::writeProgress, this); #if OPENCONNECT_CHECK_VER(1,4) openconnect_set_cancel_fd(m_openconnectInfo, cancelFd); #else // Silence warning about unused parameter Q_UNUSED(cancelFd); #endif } OpenconnectAuthWorkerThread::~OpenconnectAuthWorkerThread() { openconnect_vpninfo_free(m_openconnectInfo); } void OpenconnectAuthWorkerThread::run() { openconnect_init_ssl(); + Q_EMIT initTokens(); int ret = openconnect_obtain_cookie(m_openconnectInfo); if (*m_userDecidedToQuit) { return; } Q_EMIT cookieObtained(ret); } struct openconnect_info* OpenconnectAuthWorkerThread::getOpenconnectInfo() { return m_openconnectInfo; } int OpenconnectAuthWorkerThread::writeNewConfig(const char *buf, int buflen) { Q_UNUSED(buflen) if (*m_userDecidedToQuit) { return -EINVAL; } Q_EMIT writeNewConfig(QString(QByteArray(buf).toBase64())); return 0; } #if !OPENCONNECT_CHECK_VER(1,5) static char *openconnect_get_cert_details(struct openconnect_info *vpninfo, OPENCONNECT_X509 *cert) { Q_UNUSED(vpninfo) BIO *bp = BIO_new(BIO_s_mem()); BUF_MEM *certinfo; char zero = 0; char *ret; X509_print_ex(bp, cert, 0, 0); BIO_write(bp, &zero, 1); BIO_get_mem_ptr(bp, &certinfo); ret = strdup(certinfo->data); BIO_free(bp); return ret; } #endif int OpenconnectAuthWorkerThread::validatePeerCert(void *cert, const char *reason) { if (*m_userDecidedToQuit) { return -EINVAL; } #if OPENCONNECT_CHECK_VER(5,0) (void)cert; const char *fingerprint = openconnect_get_peer_cert_hash(m_openconnectInfo); char *details = openconnect_get_peer_cert_details(m_openconnectInfo); #else char fingerprint[41]; int ret = 0; ret = openconnect_get_cert_sha1(m_openconnectInfo, cert, fingerprint); if (ret) { return ret; } char *details = openconnect_get_cert_details(m_openconnectInfo, cert); #endif bool accepted = false; m_mutex->lock(); QString qFingerprint(fingerprint); QString qCertinfo(details); QString qReason(reason); Q_EMIT validatePeerCert(qFingerprint, qCertinfo, qReason, &accepted); m_waitForUserInput->wait(m_mutex); m_mutex->unlock(); openconnect_free_cert_info(m_openconnectInfo, details); if (*m_userDecidedToQuit) { return -EINVAL; } if (accepted) { return 0; } else { return -EINVAL; } } int OpenconnectAuthWorkerThread::processAuthFormP(struct oc_auth_form *form) { if (*m_userDecidedToQuit) { return -1; } m_mutex->lock(); *m_formGroupChanged = false; Q_EMIT processAuthForm(form); m_waitForUserInput->wait(m_mutex); m_mutex->unlock(); if (*m_userDecidedToQuit) { return OC_FORM_RESULT_CANCELLED; } if (*m_formGroupChanged) { return OC_FORM_RESULT_NEWGROUP; } return OC_FORM_RESULT_OK; } void OpenconnectAuthWorkerThread::writeProgress(int level, const char *fmt, va_list argPtr) { if (*m_userDecidedToQuit) { return; } QString msg; msg.vsprintf(fmt, argPtr); Q_EMIT updateLog(msg, level); } diff --git a/vpn/openconnect/openconnectauthworkerthread.h b/vpn/openconnect/openconnectauthworkerthread.h index 1f539512..535a347b 100644 --- a/vpn/openconnect/openconnectauthworkerthread.h +++ b/vpn/openconnect/openconnectauthworkerthread.h @@ -1,111 +1,111 @@ /* Copyright 2011 Ilia Kats This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) version 3, or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 6 of version 3 of the license. This library 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library. If not, see . */ #ifndef OPENCONNECTAUTHWORKERTHREAD_H #define OPENCONNECTAUTHWORKERTHREAD_H extern "C" { #include } #if OPENCONNECT_API_VERSION_MAJOR == 1 #define openconnect_vpninfo_new openconnect_vpninfo_new_with_cbdata #define openconnect_init_ssl openconnect_init_openssl #endif #ifndef OPENCONNECT_CHECK_VER #define OPENCONNECT_CHECK_VER(x,y) 0 #endif #if !OPENCONNECT_CHECK_VER(1,5) struct x509_st; #define OPENCONNECT_X509 struct x509_st #define OPENCONNECT_OPENSSL #endif #if OPENCONNECT_CHECK_VER(3,0) #define NEWGROUP_SUPPORTED 1 #define AUTHGROUP_OPT(form) (void *)(form)->authgroup_opt #define AUTHGROUP_SELECTION(form) (form)->authgroup_selection #define FORMCHOICE(sopt, i) ((sopt)->choices[i]) #define IGNORE_OPT(opt) ((opt)->flags & OC_FORM_OPT_IGNORE) #else #define NEWGROUP_SUPPORTED 0 #define AUTHGROUP_OPT(form) nullptr #define AUTHGROUP_SELECTION(form) 0 #define FORMCHOICE(sopt, i) (&(sopt)->choices[i]) #define IGNORE_OPT(opt) 0 #define OC_FORM_RESULT_ERR -1 #define OC_FORM_RESULT_OK 0 #define OC_FORM_RESULT_CANCELLED 1 #define OC_FORM_RESULT_NEWGROUP 2 #endif #if OPENCONNECT_CHECK_VER(4,0) #define OC3DUP(x) (x) #else #define openconnect_set_option_value(opt, val) do { \ struct oc_form_opt *_o = (opt); \ free(_o->value); _o->value = strdup(val); \ } while (0) #define openconnect_free_cert_info(v, x) ::free(x) #define OC3DUP(x) strdup(x) #endif #include class QMutex; class QWaitCondition; struct openconnect_info; class OpenconnectAuthWorkerThread : public QThread { Q_OBJECT friend class OpenconnectAuthStaticWrapper; public: OpenconnectAuthWorkerThread(QMutex *, QWaitCondition *, bool *, bool *, int); ~OpenconnectAuthWorkerThread() override; struct openconnect_info* getOpenconnectInfo(); Q_SIGNALS: void validatePeerCert(const QString &, const QString &, const QString &, bool*); void processAuthForm(struct oc_auth_form *); void updateLog(const QString &, const int&); void writeNewConfig(const QString &); void cookieObtained(const int&); - + void initTokens(void); protected: void run() override; private: int writeNewConfig(const char *, int); int validatePeerCert(void *, const char *); int processAuthFormP(struct oc_auth_form *); void writeProgress(int level, const char *, va_list); QMutex *m_mutex; QWaitCondition *m_waitForUserInput; bool *m_userDecidedToQuit; bool *m_formGroupChanged; struct openconnect_info *m_openconnectInfo; }; #endif diff --git a/vpn/openconnect/openconnectprop.ui b/vpn/openconnect/openconnectprop.ui index 6af9e4a6..0f37e151 100644 --- a/vpn/openconnect/openconnectprop.ui +++ b/vpn/openconnect/openconnectprop.ui @@ -1,199 +1,241 @@ OpenconnectProp 0 0 - 339 - 364 + 418 + 488 + + + 0 + 0 + + OpenConnect Settings - + General QFormLayout::ExpandingFieldsGrow 6 Gateway: leGateway CA Certificate: leCaCertificate *.pem *.crt *.key Proxy: leProxy CSD Wrapper Script: leCsdWrapperScript Allow Cisco Secure Desktop trojan VPN Protocol: + + cmbProtocol + 0 0 Cisco AnyConnect Juniper/Pulse Network Connect Certificate Authentication - - 6 - User Certificate: leUserCert + + + + *.pem *.crt *.key + + + Private Key: leUserPrivateKey - - - - *.pem *.crt *.key - - - *.pem *.crt *.key Use FSID for key passphrase + + + + Prevent user from manually accepting invalid certificates + + + + + + + + + + + + + Qt::Horizontal + + + + 278 + 20 + + + + + + + + Token Authentication + + + Qt::Vertical 20 0 KUrlRequester - QWidget + QFrame
kurlrequester.h
+ + enableTokenSecret(int) +
diff --git a/vpn/openconnect/openconnecttoken.ui b/vpn/openconnect/openconnecttoken.ui new file mode 100644 index 00000000..2669d7ad --- /dev/null +++ b/vpn/openconnect/openconnecttoken.ui @@ -0,0 +1,71 @@ + + + OpenConnectToken + + + + 0 + 0 + 500 + 191 + + + + OpenConnect OTP Tokens + + + + + + Software Token Authentication + + + + + + Token Mode: + + + cmbTokenMode + + + + + + + QComboBox::AdjustToContents + + + + + + + Token Secret: + + + + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + + PasswordField + QLineEdit +
passwordfield.h
+
+
+ + +
diff --git a/vpn/openconnect/openconnectwidget.cpp b/vpn/openconnect/openconnectwidget.cpp index d0631b01..cfd92bed 100644 --- a/vpn/openconnect/openconnectwidget.cpp +++ b/vpn/openconnect/openconnectwidget.cpp @@ -1,134 +1,320 @@ /* Copyright 2011 Ilia Kats Copyright 2013 Lukas Tinkl This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) version 3, or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 6 of version 3 of the license. This library 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library. If not, see . */ #include "openconnectwidget.h" #include #include +#include #include "ui_openconnectprop.h" +#include "ui_openconnecttoken.h" #include +#include #include "nm-openconnect-service.h" +#include +#ifndef OPENCONNECT_CHECK_VER +#define OPENCONNECT_CHECK_VER(x,y) 0 +#endif + +#if !OPENCONNECT_CHECK_VER(2,1) +#define openconnect_has_stoken_support() 0 +#endif +#if !OPENCONNECT_CHECK_VER(2,2) +#define openconnect_has_oath_support() 0 +#endif +#if !OPENCONNECT_CHECK_VER(5,0) +#define openconnect_has_yubioath_support() 0 +#endif + +typedef struct { + int tokenIndex; + QString tokenSecret; +} Token; + class OpenconnectSettingWidgetPrivate { public: Ui_OpenconnectProp ui; + Ui::OpenConnectToken tokenUi; NetworkManager::VpnSetting::Ptr setting; + QDialog *tokenDlg; + Token token; }; OpenconnectSettingWidget::OpenconnectSettingWidget(const NetworkManager::VpnSetting::Ptr &setting, QWidget * parent) : SettingWidget(setting, parent) , d_ptr(new OpenconnectSettingWidgetPrivate) { Q_D(OpenconnectSettingWidget); + d->ui.setupUi(this); d->setting = setting; + // Connect for validity check + connect(d->ui.leGateway, &QLineEdit::textChanged, this, &OpenconnectSettingWidget::slotWidgetChanged); + + connect(d->ui.buTokens, &QPushButton::clicked, this, &OpenconnectSettingWidget::showTokens); + + d->tokenDlg = new QDialog(this); + d->tokenUi.setupUi(d->tokenDlg); + d->tokenUi.leTokenSecret->setPasswordModeEnabled(true); + d->tokenUi.leTokenSecret->setPasswordOptionsEnabled(true); + QVBoxLayout * layout = new QVBoxLayout(d->tokenDlg); + layout->addWidget(d->tokenDlg); + d->tokenDlg->setLayout(layout); + connect(d->tokenUi.buttonBox, &QDialogButtonBox::accepted, d->tokenDlg, &QDialog::accept); + connect(d->tokenUi.buttonBox, &QDialogButtonBox::rejected, d->tokenDlg, &QDialog::reject); + connect(d->tokenDlg, &QDialog::rejected, this, &OpenconnectSettingWidget::restoreTokens); + connect(d->tokenDlg, &QDialog::accepted, this, &OpenconnectSettingWidget::saveTokens); + + connect(d->tokenUi.cmbTokenMode, QOverload::of(&QComboBox::currentIndexChanged), this, QOverload::of((&OpenconnectSettingWidget::handleTokenSecret))); + // Connect for setting check watchChangedSetting(); - // Connect for validity check - connect(d->ui.leGateway, &QLineEdit::textChanged, this, &OpenconnectSettingWidget::slotWidgetChanged); + // Remove these from setting check: + // Just popping up the tokenDlg changes nothing + disconnect(d->ui.buTokens, &QPushButton::clicked, this, &SettingWidget::settingChanged); + // User cancels means nothing should change here + disconnect(d->tokenUi.buttonBox->button(QDialogButtonBox::Cancel), &QPushButton::clicked, this, &SettingWidget::settingChanged); + + d->tokenUi.gbToken->setVisible(initTokenGroup()); KAcceleratorManager::manage(this); if (d->setting) { loadConfig(d->setting); } } OpenconnectSettingWidget::~OpenconnectSettingWidget() { delete d_ptr; } +void OpenconnectSettingWidget::handleTokenSecret(int index) +{ + Q_D(const OpenconnectSettingWidget); + + QVariant mode = d->tokenUi.cmbTokenMode->itemData(index); + if (mode == QStringLiteral("disabled")) { + d->tokenUi.leTokenSecret->setEnabled(false); + d->tokenUi.leTokenSecret->setToolTip("No secrets needed."); + } else if (mode == QStringLiteral("stokenrc")) { + d->tokenUi.leTokenSecret->setEnabled(false); + d->tokenUi.leTokenSecret->setToolTip("No secrets needed; will read them from ~/.stokenrc."); + } else if (mode == QStringLiteral("manual")) { + d->tokenUi.leTokenSecret->setToolTip("Insert the secret here. See the openconnect documentation for syntax."); + d->tokenUi.leTokenSecret->setEnabled(true); + } else if (mode == QStringLiteral("totp")) { + d->tokenUi.leTokenSecret->setEnabled(true); + d->tokenUi.leTokenSecret->setToolTip("Insert the secret here, with a sha specification and a leading '0x' or 'base32:'. See the openconnect documentation for syntax."); + } else if (mode ==QStringLiteral("hotp")) { + d->tokenUi.leTokenSecret->setEnabled(true); + d->tokenUi.leTokenSecret->setToolTip("Insert the secret here, with a leading '0x' or 'base32:' and a trailing counter after a comma (','), See the openconnect documentation for syntax."); + } else if (mode == QStringLiteral("yubioath")) { + d->tokenUi.leTokenSecret->setEnabled(true); + d->tokenUi.leTokenSecret->setToolTip("Insert the token Id here, in the form company:username. Make sure to set your Yubikey in CCID mode"); + } else { // Not really needed now, but who knows? + d->tokenUi.leTokenSecret->setEnabled(false); + d->tokenUi.leTokenSecret->setToolTip(""); + } +} + +bool OpenconnectSettingWidget::initTokenGroup() +{ + Q_D(const OpenconnectSettingWidget); + + int validRows = 0; + QStringList tokenLabelList = QStringList() << "Disabled" << "RSA SecurID — read from ~/.stokenrc" << "RSA SecurID — manually entered" << "TOTP — manually entered" << "HOTP — manually entered" << "Yubikey"; + QStringList tokenModeList = QStringList() << "disabled" << "stokenrc" << "manual" << "totp" << "hotp" << "yubioath"; + QComboBox *combo = d->tokenUi.cmbTokenMode; + + combo->addItem(tokenLabelList[validRows]); + combo->setItemData(validRows, tokenModeList[validRows], Qt::UserRole); + validRows++; + if (openconnect_has_stoken_support ()) { + for ( ; validRows < 3; validRows++) { + combo->addItem(tokenLabelList[validRows]); + combo->setItemData(validRows, tokenModeList[validRows], Qt::UserRole); + } + } + if (openconnect_has_oath_support ()) { + combo->addItem(tokenLabelList[validRows]); + combo->setItemData(validRows, tokenModeList[validRows], Qt::UserRole); + validRows++; + if (OPENCONNECT_CHECK_VER(3,4)) { + combo->addItem(tokenLabelList[validRows]); + combo->setItemData(validRows, tokenModeList[validRows], Qt::UserRole); + validRows++; + } + } + if (openconnect_has_yubioath_support ()) { + combo->addItem(tokenLabelList[validRows]); + combo->setItemData(validRows, tokenModeList[validRows], Qt::UserRole); + } + return validRows > 0; +} + void OpenconnectSettingWidget::loadConfig(const NetworkManager::Setting::Ptr &setting) { Q_D(OpenconnectSettingWidget); - Q_UNUSED(setting) // General settings - const NMStringMap dataMap = d->setting->data(); + const NMStringMap dataMap = setting.staticCast()->data(); d->ui.cmbProtocol->setCurrentIndex(dataMap[NM_OPENCONNECT_KEY_PROTOCOL] != QLatin1String("anyconnect")); d->ui.leGateway->setText(dataMap[NM_OPENCONNECT_KEY_GATEWAY]); d->ui.leCaCertificate->setUrl(QUrl::fromLocalFile(dataMap[NM_OPENCONNECT_KEY_CACERT])); d->ui.leProxy->setText(dataMap[NM_OPENCONNECT_KEY_PROXY]); d->ui.chkAllowTrojan->setChecked(dataMap[NM_OPENCONNECT_KEY_CSD_ENABLE] == "yes"); d->ui.leCsdWrapperScript->setUrl(QUrl::fromLocalFile(dataMap[NM_OPENCONNECT_KEY_CSD_WRAPPER])); d->ui.leUserCert->setUrl(QUrl::fromLocalFile(dataMap[NM_OPENCONNECT_KEY_USERCERT])); d->ui.leUserPrivateKey->setUrl(QUrl::fromLocalFile(dataMap[NM_OPENCONNECT_KEY_PRIVKEY])); d->ui.chkUseFsid->setChecked(dataMap[NM_OPENCONNECT_KEY_PEM_PASSPHRASE_FSID] == "yes"); + d->ui.preventInvalidCert->setChecked(dataMap[NM_OPENCONNECT_KEY_PREVENT_INVALID_CERT] == "yes"); + + // Token settings + const NetworkManager::Setting::SecretFlags tokenSecretFlag = static_cast(dataMap.value(NM_OPENCONNECT_KEY_TOKEN_SECRET"-flags").toInt()); + if (tokenSecretFlag == NetworkManager::Setting::None) { + d->tokenUi.leTokenSecret->setPasswordOption(PasswordField::StoreForAllUsers); + } else if (tokenSecretFlag == NetworkManager::Setting::AgentOwned) { + d->tokenUi.leTokenSecret->setPasswordOption(PasswordField::StoreForUser); + } else { + d->tokenUi.leTokenSecret->setPasswordOption(PasswordField::AlwaysAsk); + } + for (int index = 0; index < d->tokenUi.cmbTokenMode->count(); index++) { + if (d->tokenUi.cmbTokenMode->itemData(index, Qt::UserRole) == dataMap[NM_OPENCONNECT_KEY_TOKEN_MODE]) { + d->tokenUi.cmbTokenMode->setCurrentIndex(index); + d->token.tokenIndex = index; + if (index > 1) { + loadSecrets(d->setting); + } + break; + } + } +} + +void OpenconnectSettingWidget::loadSecrets(const NetworkManager::Setting::Ptr &setting) +{ + Q_D(OpenconnectSettingWidget); + + NetworkManager::VpnSetting::Ptr vpnSetting = setting.staticCast(); + + if (vpnSetting) { + const NMStringMap secrets = vpnSetting->secrets(); + d->tokenUi.leTokenSecret->setText(secrets.value(NM_OPENCONNECT_KEY_TOKEN_SECRET)); + d->token.tokenSecret = secrets.value(NM_OPENCONNECT_KEY_TOKEN_SECRET); + } } QVariantMap OpenconnectSettingWidget::setting() const { Q_D(const OpenconnectSettingWidget); NetworkManager::VpnSetting setting; setting.setServiceType(QLatin1String(NM_DBUS_SERVICE_OPENCONNECT)); NMStringMap data; + NMStringMap secrets; data.insert(NM_OPENCONNECT_KEY_PROTOCOL, d->ui.cmbProtocol->currentIndex() ? QLatin1String("nc") : QLatin1String("anyconnect")); data.insert(QLatin1String(NM_OPENCONNECT_KEY_GATEWAY), d->ui.leGateway->text()); if (d->ui.leCaCertificate->url().isValid()) { data.insert(QLatin1String(NM_OPENCONNECT_KEY_CACERT), d->ui.leCaCertificate->url().toLocalFile()); } if (!d->ui.leProxy->text().isEmpty()) { data.insert(QLatin1String(NM_OPENCONNECT_KEY_PROXY), d->ui.leProxy->text()); } data.insert(QLatin1String(NM_OPENCONNECT_KEY_CSD_ENABLE), d->ui.chkAllowTrojan->isChecked() ? "yes" : "no"); if (d->ui.leCsdWrapperScript->url().isValid()) { data.insert(QLatin1String(NM_OPENCONNECT_KEY_CSD_WRAPPER), d->ui.leCsdWrapperScript->url().toLocalFile()); } if (d->ui.leUserCert->url().isValid()) { data.insert(QLatin1String(NM_OPENCONNECT_KEY_USERCERT), d->ui.leUserCert->url().toLocalFile()); } if (d->ui.leUserPrivateKey->url().isValid()) { data.insert(QLatin1String(NM_OPENCONNECT_KEY_PRIVKEY), d->ui.leUserPrivateKey->url().toLocalFile()); } data.insert(QLatin1String(NM_OPENCONNECT_KEY_PEM_PASSPHRASE_FSID), d->ui.chkUseFsid->isChecked() ? "yes" : "no"); + data.insert(QLatin1String(NM_OPENCONNECT_KEY_PREVENT_INVALID_CERT), d->ui.preventInvalidCert->isChecked() ? "yes" : "no"); + + int index = d->tokenUi.cmbTokenMode->currentIndex(); + data.insert(QLatin1String(NM_OPENCONNECT_KEY_TOKEN_MODE), d->tokenUi.cmbTokenMode->itemData(index, Qt::UserRole).toString()); + secrets.insert(QLatin1String(NM_OPENCONNECT_KEY_TOKEN_SECRET), d->tokenUi.leTokenSecret->text()); // Restore previous flags, this is necessary for keeping secrets stored in KWallet for (const QString &key : d->setting->data().keys()) { if (key.contains(QLatin1String("-flags"))) { data.insert(key, d->setting->data().value(key)); } } + if (d->tokenUi.leTokenSecret->passwordOption() == PasswordField::StoreForAllUsers) { + data.insert(NM_OPENCONNECT_KEY_TOKEN_SECRET"-flags", QString::number(NetworkManager::Setting::None)); + } else if (d->tokenUi.leTokenSecret->passwordOption() == PasswordField::StoreForUser) { + data.insert(NM_OPENCONNECT_KEY_TOKEN_SECRET"-flags", QString::number(NetworkManager::Setting::AgentOwned)); + } else { + data.insert(NM_OPENCONNECT_KEY_TOKEN_SECRET"-flags", QString::number(NetworkManager::Setting::NotSaved)); + } + /* These are different for every login session, and should not be stored */ data.insert(QLatin1String(NM_OPENCONNECT_KEY_COOKIE"-flags"), QString::number(NetworkManager::Setting::NotSaved)); data.insert(QLatin1String(NM_OPENCONNECT_KEY_GWCERT"-flags"), QString::number(NetworkManager::Setting::NotSaved)); data.insert(QLatin1String(NM_OPENCONNECT_KEY_GATEWAY"-flags"), QString::number(NetworkManager::Setting::NotSaved)); setting.setData(data); - setting.setSecrets(d->setting->secrets()); + setting.setSecrets(secrets); return setting.toMap(); } +void OpenconnectSettingWidget::restoreTokens() +{ + Q_D(const OpenconnectSettingWidget); + + d->tokenUi.cmbTokenMode->setCurrentIndex(d->token.tokenIndex); + d->tokenUi.leTokenSecret->setText(d->token.tokenSecret); +} + +void OpenconnectSettingWidget::saveTokens() +{ + Q_D(OpenconnectSettingWidget); + + d->token.tokenIndex = d->tokenUi.cmbTokenMode->currentIndex(); + d->token.tokenSecret = d->tokenUi.leTokenSecret->text(); +} + +void OpenconnectSettingWidget::showTokens() +{ + Q_D(OpenconnectSettingWidget); + + d->tokenDlg->show(); +} + bool OpenconnectSettingWidget::isValid() const { Q_D(const OpenconnectSettingWidget); + return !d->ui.leGateway->text().isEmpty(); } diff --git a/vpn/openconnect/openconnectwidget.h b/vpn/openconnect/openconnectwidget.h index f5e06fe2..64f03c81 100644 --- a/vpn/openconnect/openconnectwidget.h +++ b/vpn/openconnect/openconnectwidget.h @@ -1,46 +1,55 @@ /* Copyright 2011 Ilia Kats Copyright 2013 Lukas Tinkl This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) version 3, or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 6 of version 3 of the license. This library 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library. If not, see . */ #ifndef OPENCONNECTWIDGET_H #define OPENCONNECTWIDGET_H #include "settingwidget.h" #include class OpenconnectSettingWidgetPrivate; class OpenconnectSettingWidget : public SettingWidget { Q_OBJECT + Q_DECLARE_PRIVATE(OpenconnectSettingWidget) public: explicit OpenconnectSettingWidget(const NetworkManager::VpnSetting::Ptr &setting, QWidget *parent = nullptr); ~OpenconnectSettingWidget() override; void loadConfig(const NetworkManager::Setting::Ptr &setting) override; + void loadSecrets(const NetworkManager::Setting::Ptr &setting) override; QVariantMap setting() const override; bool isValid() const override; + bool initTokenGroup(); + +private Q_SLOTS: + void showTokens(); + void handleTokenSecret(int index); + void saveTokens(); + void restoreTokens(); private: OpenconnectSettingWidgetPrivate *const d_ptr; }; #endif // OPENCONNECTWIDGET_H