File indexing completed on 2024-05-12 09:39:07

0001 /*
0002     SPDX-FileCopyrightText: 2011 Ilia Kats <ilia-kats@gmx.net>
0003     SPDX-FileCopyrightText: 2013 Lukáš Tinkl <ltinkl@redhat.com>
0004 
0005     SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
0006 */
0007 
0008 #include "openconnectauth.h"
0009 #include "openconnectauthworkerthread.h"
0010 #include "ui_openconnectauth.h"
0011 
0012 #include "passwordfield.h"
0013 #include "plasma_nm_openconnect.h"
0014 
0015 #include <QAtomicPointer>
0016 #include <QByteArray>
0017 #include <QComboBox>
0018 #include <QCryptographicHash>
0019 #include <QDialog>
0020 #include <QDialogButtonBox>
0021 #include <QDomDocument>
0022 #include <QEventLoop>
0023 #include <QFile>
0024 #include <QFormLayout>
0025 #include <QIcon>
0026 #include <QLabel>
0027 #include <QMutex>
0028 #include <QPointer>
0029 #include <QPushButton>
0030 #include <QTimer>
0031 #include <QWaitCondition>
0032 #include <QWebEngineCookieStore>
0033 #include <QWebEnginePage>
0034 #include <QWebEngineProfile>
0035 #include <QWebEngineView>
0036 
0037 #include <KLocalizedString>
0038 
0039 #include "nm-openconnect-service.h"
0040 
0041 #include <cstdarg>
0042 
0043 extern "C" {
0044 #include <cstring>
0045 #include <fcntl.h>
0046 #include <unistd.h>
0047 }
0048 
0049 #if !OPENCONNECT_CHECK_VER(2, 1)
0050 #define __openconnect_set_token_mode(...) -EOPNOTSUPP
0051 #elif !OPENCONNECT_CHECK_VER(2, 2)
0052 #define __openconnect_set_token_mode(vpninfo, mode, secret) openconnect_set_stoken_mode(vpninfo, 1, secret)
0053 #else
0054 #define __openconnect_set_token_mode openconnect_set_token_mode
0055 #endif
0056 
0057 #if OPENCONNECT_CHECK_VER(3, 4)
0058 static int updateToken(void *, const char *);
0059 #endif
0060 
0061 // name/address: IP/domain name of the host (OpenConnect accepts both, so no difference here)
0062 // group: user group on the server
0063 using VPNHost = struct {
0064     QString name;
0065     QString group;
0066     QString address;
0067 };
0068 
0069 using Token = struct {
0070     oc_token_mode_t tokenMode;
0071     QByteArray tokenSecret;
0072 };
0073 
0074 class OpenconnectAuthWidgetPrivate
0075 {
0076 public:
0077     Ui_OpenconnectAuth ui;
0078     NetworkManager::VpnSetting::Ptr setting;
0079     struct openconnect_info *vpninfo;
0080     NMStringMap secrets;
0081     NMStringMap tmpSecrets;
0082     QMutex mutex;
0083     QWaitCondition workerWaiting;
0084     OpenconnectAuthWorkerThread *worker;
0085     QList<VPNHost> hosts;
0086     bool userQuit;
0087     bool formGroupChanged;
0088     int cancelPipes[2];
0089     QList<QPair<QString, int>> serverLog;
0090     int passwordFormIndex;
0091     QByteArray tokenMode;
0092     Token token;
0093     QAtomicPointer<QSemaphore> waitForWebEngineFinish;
0094 
0095     enum LogLevels { Error = 0, Info, Debug, Trace };
0096 };
0097 
0098 OpenconnectAuthWidget::OpenconnectAuthWidget(const NetworkManager::VpnSetting::Ptr &setting, const QStringList &hints, QWidget *parent)
0099     : SettingWidget(setting, hints, parent)
0100     , d_ptr(new OpenconnectAuthWidgetPrivate)
0101 {
0102     Q_D(OpenconnectAuthWidget);
0103     d->setting = setting;
0104     d->ui.setupUi(this);
0105     d->userQuit = false;
0106     d->formGroupChanged = false;
0107 
0108     if (pipe2(d->cancelPipes, O_NONBLOCK | O_CLOEXEC)) {
0109         // Should never happen. Just don't do real cancellation if it does
0110         d->cancelPipes[0] = -1;
0111         d->cancelPipes[1] = -1;
0112     }
0113 
0114     connect(d->ui.cmbLogLevel, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &OpenconnectAuthWidget::logLevelChanged);
0115     connect(d->ui.viewServerLog, &QCheckBox::toggled, this, &OpenconnectAuthWidget::viewServerLogToggled);
0116     connect(d->ui.btnConnect, &QPushButton::clicked, this, &OpenconnectAuthWidget::connectHost);
0117 
0118     d->ui.cmbLogLevel->setCurrentIndex(OpenconnectAuthWidgetPrivate::Debug);
0119     d->ui.btnConnect->setIcon(QIcon::fromTheme("network-connect"));
0120     d->ui.viewServerLog->setChecked(false);
0121 
0122     d->worker = new OpenconnectAuthWorkerThread(&d->mutex, &d->workerWaiting, &d->userQuit, &d->formGroupChanged, d->cancelPipes[0]);
0123 
0124     // gets the pointer to struct openconnect_info (defined in openconnect.h), which contains data that OpenConnect needs,
0125     // and which needs to be populated with settings we get from NM, like host, certificate or private key
0126     d->vpninfo = d->worker->getOpenconnectInfo();
0127 
0128     connect(d->worker,
0129             QOverload<const QString &, const QString &, const QString &, bool *>::of(&OpenconnectAuthWorkerThread::validatePeerCert),
0130             this,
0131             &OpenconnectAuthWidget::validatePeerCert);
0132     connect(d->worker, &OpenconnectAuthWorkerThread::openWebEngine, this, &OpenconnectAuthWidget::openWebEngine);
0133     connect(d->worker, &OpenconnectAuthWorkerThread::processAuthForm, this, &OpenconnectAuthWidget::processAuthForm);
0134     connect(d->worker, &OpenconnectAuthWorkerThread::updateLog, this, &OpenconnectAuthWidget::updateLog);
0135     connect(d->worker, QOverload<const QString &>::of(&OpenconnectAuthWorkerThread::writeNewConfig), this, &OpenconnectAuthWidget::writeNewConfig);
0136     connect(d->worker, &OpenconnectAuthWorkerThread::cookieObtained, this, &OpenconnectAuthWidget::workerFinished);
0137     connect(d->worker, &OpenconnectAuthWorkerThread::initTokens, this, &OpenconnectAuthWidget::initTokens);
0138 
0139     readConfig();
0140     readSecrets();
0141 
0142 #if OPENCONNECT_CHECK_VER(3, 4)
0143     openconnect_set_token_callbacks(d->vpninfo, &d->secrets, NULL, &updateToken);
0144 #endif
0145 
0146     // This might be set by readSecrets() so don't connect it until now
0147     connect(d->ui.cmbHosts, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &OpenconnectAuthWidget::connectHost);
0148 
0149     KAcceleratorManager::manage(this);
0150 }
0151 
0152 OpenconnectAuthWidget::~OpenconnectAuthWidget()
0153 {
0154     Q_D(OpenconnectAuthWidget);
0155     QSemaphore *webEngineCancel = d->waitForWebEngineFinish.fetchAndStoreRelaxed(nullptr);
0156     if (webEngineCancel) {
0157         webEngineCancel->release();
0158     }
0159     d->userQuit = true;
0160     if (write(d->cancelPipes[1], "x", 1)) {
0161         // not a lot we can do
0162     }
0163     d->workerWaiting.wakeAll();
0164     d->worker->wait();
0165     ::close(d->cancelPipes[0]);
0166     ::close(d->cancelPipes[1]);
0167     deleteAllFromLayout(d->ui.loginBoxLayout);
0168     delete d->worker;
0169     delete d;
0170 }
0171 
0172 void OpenconnectAuthWidget::readConfig()
0173 {
0174     Q_D(OpenconnectAuthWidget);
0175 
0176     const NMStringMap dataMap = d->setting->data();
0177 
0178     if (!dataMap[NM_OPENCONNECT_KEY_GATEWAY].isEmpty()) {
0179         const QString gw = dataMap[NM_OPENCONNECT_KEY_GATEWAY];
0180         VPNHost host;
0181         const int index = gw.indexOf(QLatin1Char('/'));
0182         if (index > -1) {
0183             host.name = host.address = gw.left(index);
0184             host.group = gw.right(gw.length() - index - 1);
0185         } else {
0186             host.name = host.address = gw;
0187         }
0188         d->hosts.append(host);
0189     }
0190     if (!dataMap[NM_OPENCONNECT_KEY_CACERT].isEmpty()) {
0191         const QByteArray crt = QFile::encodeName(dataMap[NM_OPENCONNECT_KEY_CACERT]);
0192         openconnect_set_cafile(d->vpninfo, OC3DUP(crt.data()));
0193     }
0194     if (dataMap[NM_OPENCONNECT_KEY_CSD_ENABLE] == "yes") {
0195         char *wrapper;
0196         wrapper = nullptr;
0197         if (!dataMap[NM_OPENCONNECT_KEY_CSD_WRAPPER].isEmpty()) {
0198             const QByteArray wrapperScript = QFile::encodeName(dataMap[NM_OPENCONNECT_KEY_CSD_WRAPPER]);
0199             wrapper = strdup(wrapperScript.data());
0200         }
0201         openconnect_setup_csd(d->vpninfo, getuid(), 1, wrapper);
0202     }
0203     if (!dataMap[NM_OPENCONNECT_KEY_PROXY].isEmpty()) {
0204         const QByteArray proxy = dataMap[NM_OPENCONNECT_KEY_PROXY].toUtf8();
0205         openconnect_set_http_proxy(d->vpninfo, OC3DUP(proxy.data()));
0206     }
0207 #if OPENCONNECT_CHECK_VER(5, 8)
0208     if (!dataMap[NM_OPENCONNECT_KEY_USERAGENT].isEmpty()) {
0209         const QByteArray useragent = dataMap[NM_OPENCONNECT_KEY_USERAGENT].toUtf8();
0210         openconnect_set_useragent(d->vpninfo, OC3DUP(useragent.data()));
0211     }
0212 #endif
0213 #if OPENCONNECT_CHECK_VER(5, 5)
0214     if (!dataMap[NM_OPENCONNECT_KEY_VERSION_STRING].isEmpty()) {
0215         const QByteArray versionstring = dataMap[NM_OPENCONNECT_KEY_VERSION_STRING].toUtf8();
0216         openconnect_set_version_string(d->vpninfo, OC3DUP(versionstring.data()));
0217     }
0218 #endif
0219     if (!dataMap[NM_OPENCONNECT_KEY_USERCERT].isEmpty()) {
0220         const QByteArray crt = QFile::encodeName(dataMap[NM_OPENCONNECT_KEY_USERCERT]);
0221         const QByteArray key = QFile::encodeName(dataMap[NM_OPENCONNECT_KEY_PRIVKEY]);
0222         openconnect_set_client_cert(d->vpninfo, OC3DUP(crt.data()), OC3DUP(key.isEmpty() ? nullptr : key.data()));
0223 
0224         if (!crt.isEmpty() && dataMap[NM_OPENCONNECT_KEY_PEM_PASSPHRASE_FSID] == "yes") {
0225             openconnect_passphrase_from_fsid(d->vpninfo);
0226         }
0227     }
0228     if (!dataMap[NM_OPENCONNECT_KEY_PROTOCOL].isEmpty()) {
0229         const QString protocol = dataMap[NM_OPENCONNECT_KEY_PROTOCOL];
0230         openconnect_set_protocol(d->vpninfo, OC3DUP(protocol == "juniper" ? "nc" : protocol.toUtf8().data()));
0231     }
0232     if (!dataMap[NM_OPENCONNECT_KEY_REPORTED_OS].isEmpty()) {
0233         const QString reportedOs = dataMap[NM_OPENCONNECT_KEY_REPORTED_OS];
0234         openconnect_set_reported_os(d->vpninfo, reportedOs.toUtf8().data());
0235     }
0236 
0237     d->tokenMode = dataMap[NM_OPENCONNECT_KEY_TOKEN_MODE].toUtf8();
0238 }
0239 
0240 void OpenconnectAuthWidget::readSecrets()
0241 {
0242     Q_D(OpenconnectAuthWidget);
0243 
0244     d->secrets = d->setting->secrets();
0245 
0246     if (!d->secrets["xmlconfig"].isEmpty()) {
0247         const QByteArray config = QByteArray::fromBase64(d->secrets["xmlconfig"].toLatin1());
0248 
0249         QCryptographicHash hash(QCryptographicHash::Sha1);
0250         hash.addData(config.data(), config.size());
0251         const char *sha1_text = hash.result().toHex();
0252         openconnect_set_xmlsha1(d->vpninfo, (char *)sha1_text, strlen(sha1_text) + 1);
0253 
0254         QDomDocument xmlconfig;
0255         xmlconfig.setContent(config);
0256         const QDomNode anyConnectProfile = xmlconfig.elementsByTagName(QLatin1String("AnyConnectProfile")).at(0);
0257         bool matchedGw = false;
0258         const QDomNode serverList = anyConnectProfile.firstChildElement(QLatin1String("ServerList"));
0259         for (QDomElement entry = serverList.firstChildElement(QLatin1String("HostEntry")); !entry.isNull();
0260              entry = entry.nextSiblingElement(QLatin1String("HostEntry"))) {
0261             VPNHost host;
0262             host.name = entry.firstChildElement(QLatin1String("HostName")).text();
0263             host.group = entry.firstChildElement(QLatin1String("UserGroup")).text();
0264             host.address = entry.firstChildElement(QLatin1String("HostAddress")).text();
0265             // We added the originally configured host in readConfig(). But if
0266             // it matches one of the ones in the XML config (as presumably it
0267             // should), remove the original and use the one with the pretty name.
0268             if (!matchedGw && host.address == d->hosts.at(0).address) {
0269                 d->hosts.removeFirst();
0270                 matchedGw = true;
0271             }
0272             d->hosts.append(host);
0273         }
0274     }
0275 
0276     for (int i = 0; i < d->hosts.size(); i++) {
0277         d->ui.cmbHosts->addItem(d->hosts.at(i).name, i);
0278         if (d->secrets["lasthost"] == d->hosts.at(i).name || d->secrets["lasthost"] == d->hosts.at(i).address) {
0279             d->ui.cmbHosts->setCurrentIndex(i);
0280         }
0281     }
0282 
0283     if (d->secrets["autoconnect"] == "yes") {
0284         d->ui.chkAutoconnect->setChecked(true);
0285         QTimer::singleShot(0, this, &OpenconnectAuthWidget::connectHost);
0286     }
0287 
0288     if (d->secrets["save_passwords"] == "yes") {
0289         d->ui.chkStorePasswords->setChecked(true);
0290     }
0291 
0292     d->token.tokenMode = OC_TOKEN_MODE_NONE;
0293     d->token.tokenSecret = nullptr;
0294 
0295     if (!d->tokenMode.isEmpty()) {
0296         int ret = 0;
0297         QByteArray tokenSecret = d->secrets[NM_OPENCONNECT_KEY_TOKEN_SECRET].toUtf8();
0298 
0299         if (d->tokenMode == QStringLiteral("manual") && !tokenSecret.isEmpty()) {
0300             ret = __openconnect_set_token_mode(d->vpninfo, OC_TOKEN_MODE_STOKEN, tokenSecret);
0301         } else if (d->tokenMode == QStringLiteral("stokenrc")) {
0302             ret = __openconnect_set_token_mode(d->vpninfo, OC_TOKEN_MODE_STOKEN, NULL);
0303         } else if (d->tokenMode == QStringLiteral("totp") && !tokenSecret.isEmpty()) {
0304             ret = __openconnect_set_token_mode(d->vpninfo, OC_TOKEN_MODE_TOTP, tokenSecret);
0305         }
0306 #if OPENCONNECT_CHECK_VER(3, 4)
0307         else if (d->tokenMode == QStringLiteral("hotp") && !tokenSecret.isEmpty()) {
0308             ret = __openconnect_set_token_mode(d->vpninfo, OC_TOKEN_MODE_HOTP, tokenSecret);
0309         }
0310 #endif
0311 #if OPENCONNECT_CHECK_VER(5, 0)
0312         else if (d->tokenMode == "yubioath") {
0313             /* This needs to be done from a thread because it can call back to
0314                 ask for the PIN */
0315             d->token.tokenMode = OC_TOKEN_MODE_YUBIOATH;
0316             if (!tokenSecret.isEmpty()) {
0317                 d->token.tokenSecret = tokenSecret;
0318             }
0319         }
0320 #endif
0321         if (ret) {
0322             addFormInfo(QLatin1String("dialog-error"), i18n("Failed to initialize software token: %1", ret));
0323         }
0324     }
0325 }
0326 
0327 void OpenconnectAuthWidget::acceptDialog()
0328 {
0329     // Find top-level widget as this should be the QDialog itself
0330     QWidget *widget = parentWidget();
0331     while (widget->parentWidget() != nullptr) {
0332         widget = widget->parentWidget();
0333     }
0334 
0335     auto dialog = qobject_cast<QDialog *>(widget);
0336     if (dialog) {
0337         dialog->accept();
0338     }
0339 }
0340 
0341 // This starts the worker thread, which connects to the selected AnyConnect host
0342 // and retrieves the login form
0343 void OpenconnectAuthWidget::connectHost()
0344 {
0345     Q_D(OpenconnectAuthWidget);
0346 
0347     d->userQuit = true;
0348     if (write(d->cancelPipes[1], "x", 1)) {
0349         // not a lot we can do
0350     }
0351     d->workerWaiting.wakeAll();
0352     d->worker->wait();
0353     d->userQuit = false;
0354 
0355     /* Suck out the cancel byte(s) */
0356     char buf;
0357     while (read(d->cancelPipes[0], &buf, 1) == 1) {
0358         ;
0359     }
0360     deleteAllFromLayout(d->ui.loginBoxLayout);
0361     int i = d->ui.cmbHosts->currentIndex();
0362     if (i == -1) {
0363         return;
0364     }
0365     i = d->ui.cmbHosts->itemData(i).toInt();
0366     const VPNHost &host = d->hosts.at(i);
0367     if (openconnect_parse_url(d->vpninfo, host.address.toLatin1().data())) {
0368         qCWarning(PLASMA_NM_OPENCONNECT_LOG) << "Failed to parse server URL" << host.address;
0369         openconnect_set_hostname(d->vpninfo, OC3DUP(host.address.toLatin1().data()));
0370     }
0371     if (!openconnect_get_urlpath(d->vpninfo) && !host.group.isEmpty()) {
0372         openconnect_set_urlpath(d->vpninfo, OC3DUP(host.group.toLatin1().data()));
0373     }
0374     d->secrets["lasthost"] = host.name;
0375     addFormInfo(QLatin1String("dialog-information"), i18n("Contacting host, please wait…"));
0376     d->worker->start();
0377 }
0378 
0379 void OpenconnectAuthWidget::initTokens()
0380 {
0381     Q_D(OpenconnectAuthWidget);
0382 
0383     if (d->token.tokenMode != OC_TOKEN_MODE_NONE) {
0384         __openconnect_set_token_mode(d->vpninfo, d->token.tokenMode, d->token.tokenSecret);
0385     }
0386 }
0387 
0388 QVariantMap OpenconnectAuthWidget::setting() const
0389 {
0390     Q_D(const OpenconnectAuthWidget);
0391 
0392     NMStringMap secrets;
0393     QVariantMap secretData;
0394 
0395     secrets.insert(d->secrets);
0396     QString host(openconnect_get_hostname(d->vpninfo));
0397     const QString port = QString::number(openconnect_get_port(d->vpninfo));
0398     QString gateway = host + ':' + port;
0399     const char *urlpath = openconnect_get_urlpath(d->vpninfo);
0400     if (urlpath) {
0401         gateway += '/';
0402         gateway += urlpath;
0403     }
0404     secrets.insert(QLatin1String(NM_OPENCONNECT_KEY_GATEWAY), gateway);
0405 
0406     secrets.insert(QLatin1String(NM_OPENCONNECT_KEY_COOKIE), QLatin1String(openconnect_get_cookie(d->vpninfo)));
0407     openconnect_clear_cookie(d->vpninfo);
0408 
0409 #if OPENCONNECT_CHECK_VER(5, 0)
0410     const char *fingerprint = openconnect_get_peer_cert_hash(d->vpninfo);
0411 #else
0412     OPENCONNECT_X509 *cert = openconnect_get_peer_cert(d->vpninfo);
0413     char fingerprint[41];
0414     openconnect_get_cert_sha1(d->vpninfo, cert, fingerprint);
0415 #endif
0416     secrets.insert(QLatin1String(NM_OPENCONNECT_KEY_GWCERT), QLatin1String(fingerprint));
0417     secrets.insert(QLatin1String("autoconnect"), d->ui.chkAutoconnect->isChecked() ? "yes" : "no");
0418     secrets.insert(QLatin1String("save_passwords"), d->ui.chkStorePasswords->isChecked() ? "yes" : "no");
0419 
0420     NMStringMap::iterator i = secrets.begin();
0421     while (i != secrets.end()) {
0422         if (i.value().isEmpty()) {
0423             i = secrets.erase(i);
0424         } else {
0425             ++i;
0426         }
0427     }
0428 
0429     secretData.insert("secrets", QVariant::fromValue<NMStringMap>(secrets));
0430 
0431     // These secrets are not officially part of the secrets which would be returned back to NetworkManager. We just
0432     // need to somehow get them to our secret agent which will handle them separately and store them.
0433     if (!d->tmpSecrets.isEmpty()) {
0434         secretData.insert("tmp-secrets", QVariant::fromValue<NMStringMap>(d->tmpSecrets));
0435     }
0436     return secretData;
0437 }
0438 
0439 #if OPENCONNECT_CHECK_VER(3, 4)
0440 static int updateToken(void *cbdata, const char *tok)
0441 {
0442     auto secrets = static_cast<NMStringMap *>(cbdata);
0443     secrets->insert(QLatin1String(NM_OPENCONNECT_KEY_TOKEN_SECRET), QLatin1String(tok));
0444     return 0;
0445 }
0446 #endif
0447 
0448 void OpenconnectAuthWidget::writeNewConfig(const QString &buf)
0449 {
0450     Q_D(OpenconnectAuthWidget);
0451     d->secrets["xmlconfig"] = buf;
0452 }
0453 
0454 void OpenconnectAuthWidget::updateLog(const QString &message, const int &level)
0455 {
0456     Q_D(OpenconnectAuthWidget);
0457 
0458     QPair<QString, int> pair;
0459     pair.first = message;
0460     if (pair.first.endsWith(QLatin1String("\n"))) {
0461         pair.first.chop(1);
0462     }
0463     switch (level) {
0464     case PRG_ERR:
0465         pair.second = OpenconnectAuthWidgetPrivate::Error;
0466         break;
0467     case PRG_INFO:
0468         pair.second = OpenconnectAuthWidgetPrivate::Info;
0469         break;
0470     case PRG_DEBUG:
0471         pair.second = OpenconnectAuthWidgetPrivate::Debug;
0472         break;
0473     case PRG_TRACE:
0474         pair.second = OpenconnectAuthWidgetPrivate::Trace;
0475         break;
0476     }
0477     if (pair.second <= d->ui.cmbLogLevel->currentIndex()) {
0478         d->ui.serverLog->append(pair.first);
0479     }
0480 
0481     d->serverLog.append(pair);
0482     if (d->serverLog.size() > 100) {
0483         d->serverLog.removeFirst();
0484     }
0485 }
0486 
0487 void OpenconnectAuthWidget::logLevelChanged(int newLevel)
0488 {
0489     Q_D(OpenconnectAuthWidget);
0490     d->ui.serverLog->clear();
0491     QList<QPair<QString, int>>::const_iterator i;
0492 
0493     for (i = d->serverLog.constBegin(); i != d->serverLog.constEnd(); ++i) {
0494         QPair<QString, int> pair = *i;
0495         if (pair.second <= newLevel) {
0496             d->ui.serverLog->append(pair.first);
0497         }
0498     }
0499 }
0500 
0501 void OpenconnectAuthWidget::handleWebEngineCookie(const QNetworkCookie &cookie)
0502 {
0503     Q_D(OpenconnectAuthWidget);
0504     const char *cookiesArr[3] = {nullptr};
0505 
0506     cookiesArr[0] = cookie.name().constData();
0507     cookiesArr[1] = cookie.value().constData();
0508 
0509 #if OPENCONNECT_CHECK_VER(5, 7)
0510     struct oc_webview_result res;
0511     res.headers = nullptr;
0512     res.cookies = cookiesArr;
0513     // Hack due to lack of NULL pointer check in AnyConnect sso_detect_done
0514     // logic in libopenconnect.
0515     res.uri = "";
0516     if (!openconnect_webview_load_changed(d->vpninfo, &res)) {
0517         QSemaphore *waitForWebEngineFinish = d->waitForWebEngineFinish.fetchAndStoreRelaxed(nullptr);
0518         if (waitForWebEngineFinish) {
0519             waitForWebEngineFinish->release();
0520         }
0521     }
0522 #endif
0523 }
0524 
0525 void OpenconnectAuthWidget::handleWebEngineLoad(const QWebEngineLoadingInfo &loadingInfo)
0526 {
0527     Q_D(OpenconnectAuthWidget);
0528     const char *cookiesArr[1] = {nullptr};
0529     QList<const char *> responseHeaderList;
0530 
0531     switch (loadingInfo.status()) {
0532     case QWebEngineLoadingInfo::LoadSucceededStatus:
0533     case QWebEngineLoadingInfo::LoadFailedStatus:
0534         break;
0535     default:
0536         return;
0537     }
0538 
0539     const QMultiMap<QByteArray, QByteArray> responseHeaders = loadingInfo.responseHeaders();
0540     QMultiMapIterator<QByteArray, QByteArray> headerIter(responseHeaders);
0541     responseHeaderList.reserve((responseHeaders.size() * 2) + 1);
0542     while (headerIter.hasNext()) {
0543         headerIter.next();
0544         responseHeaderList.push_back(headerIter.key().constData());
0545         responseHeaderList.push_back(headerIter.value().constData());
0546     }
0547     responseHeaderList.push_back(nullptr);
0548 
0549 #if OPENCONNECT_CHECK_VER(5, 7)
0550     struct oc_webview_result res;
0551     res.headers = responseHeaderList.data();
0552     // Hack due to lack of NULL pointer check in AnyConnect sso_detect_done
0553     // logic in libopenconnect.
0554     res.cookies = cookiesArr;
0555     res.uri = "";
0556     if (!openconnect_webview_load_changed(d->vpninfo, &res)) {
0557         QSemaphore *waitForWebEngineFinish = d->waitForWebEngineFinish.fetchAndStoreRelaxed(nullptr);
0558         if (waitForWebEngineFinish) {
0559             waitForWebEngineFinish->release();
0560         }
0561     }
0562 #endif
0563 }
0564 
0565 void OpenconnectAuthWidget::handleWebEngineUrl(const QUrl &url)
0566 {
0567     Q_D(OpenconnectAuthWidget);
0568     // Hack due to lack of NULL pointer check in AnyConnect sso_detect_done
0569     // logic in libopenconnect.
0570     const char *cookiesArr[1] = {nullptr};
0571     QByteArray urlByteArray = url.toString().toLocal8Bit();
0572 
0573 #if OPENCONNECT_CHECK_VER(5, 7)
0574     struct oc_webview_result res;
0575     res.headers = nullptr;
0576     res.cookies = cookiesArr;
0577     res.uri = urlByteArray.constData();
0578     if (!openconnect_webview_load_changed(d->vpninfo, &res)) {
0579         QSemaphore *waitForWebEngineFinish = d->waitForWebEngineFinish.fetchAndStoreRelaxed(nullptr);
0580         if (waitForWebEngineFinish) {
0581             waitForWebEngineFinish->release();
0582         }
0583     }
0584 #endif
0585 }
0586 
0587 void OpenconnectAuthWidget::openWebEngine(const char *loginUri, QSemaphore *waitForWebEngineFinish)
0588 {
0589     Q_D(OpenconnectAuthWidget);
0590     d->waitForWebEngineFinish.storeRelease(waitForWebEngineFinish);
0591     auto webEngineView = new QWebEngineView(this);
0592     QWebEnginePage *page = webEngineView->page();
0593     QWebEngineCookieStore *cookieStore = page->profile()->cookieStore();
0594 
0595     connect(webEngineView, &QWebEngineView::urlChanged, this, &OpenconnectAuthWidget::handleWebEngineUrl);
0596     connect(page, &QWebEnginePage::loadingChanged, this, &OpenconnectAuthWidget::handleWebEngineLoad);
0597     connect(cookieStore, &QWebEngineCookieStore::cookieAdded, this, &OpenconnectAuthWidget::handleWebEngineCookie);
0598     cookieStore->loadAllCookies();
0599 
0600     webEngineView->load(QUrl(loginUri, QUrl::TolerantMode));
0601     // QWebEngineView sizeHint fails to size window correctly based on contents
0602     // when QLayout::setSizeConstraint(QLayout::SetFixedSize) is set. Using same
0603     // size as webkitgtk is set to in GNOME/NetworkManager-openconnect.
0604     webEngineView->setFixedSize(640, 480);
0605 
0606     d->ui.loginBoxLayout->addWidget(webEngineView);
0607 }
0608 
0609 void OpenconnectAuthWidget::addFormInfo(const QString &iconName, const QString &message)
0610 {
0611     Q_D(OpenconnectAuthWidget);
0612 
0613     auto layout = new QHBoxLayout();
0614     auto icon = new QLabel(this);
0615     QSizePolicy sizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
0616     sizePolicy.setHorizontalStretch(0);
0617     sizePolicy.setVerticalStretch(0);
0618     sizePolicy.setHeightForWidth(icon->sizePolicy().hasHeightForWidth());
0619     icon->setSizePolicy(sizePolicy);
0620     icon->setMinimumSize(QSize(16, 16));
0621     icon->setMaximumSize(QSize(16, 16));
0622     layout->addWidget(icon);
0623 
0624     auto text = new QLabel(this);
0625     text->setAlignment(Qt::AlignLeading | Qt::AlignLeft | Qt::AlignVCenter);
0626     text->setWordWrap(true);
0627     layout->addWidget(text);
0628 
0629     const int iconSize = icon->style()->pixelMetric(QStyle::PixelMetric::PM_SmallIconSize);
0630     icon->setPixmap(QIcon::fromTheme(iconName).pixmap(iconSize));
0631     text->setText(message);
0632 
0633     d->ui.loginBoxLayout->addLayout(layout);
0634 }
0635 
0636 void OpenconnectAuthWidget::processAuthForm(struct oc_auth_form *form)
0637 {
0638     Q_D(OpenconnectAuthWidget);
0639 
0640     deleteAllFromLayout(d->ui.loginBoxLayout);
0641 
0642     struct oc_form_opt *opt;
0643     auto layout = new QFormLayout();
0644     QSizePolicy policy(QSizePolicy::Expanding, QSizePolicy::Fixed);
0645     bool focusSet = false;
0646     for (opt = form->opts; opt; opt = opt->next) {
0647         if (opt->type == OC_FORM_OPT_HIDDEN || IGNORE_OPT(opt)) {
0648             continue;
0649         }
0650         auto text = new QLabel(this);
0651         text->setAlignment(Qt::AlignLeading | Qt::AlignLeft | Qt::AlignVCenter);
0652         text->setText(QString(opt->label));
0653         QWidget *widget = nullptr;
0654         const QString key = QString("form:%1:%2").arg(QLatin1String(form->auth_id)).arg(QLatin1String(opt->name));
0655         const QString value = d->secrets.value(key);
0656         if (opt->type == OC_FORM_OPT_PASSWORD || opt->type == OC_FORM_OPT_TEXT) {
0657             auto le = new PasswordField(this);
0658             le->setText(value);
0659             if (opt->type == OC_FORM_OPT_PASSWORD) {
0660                 le->setPasswordModeEnabled(true);
0661             }
0662             if (!focusSet && le->text().isEmpty()) {
0663                 le->setFocus(Qt::OtherFocusReason);
0664                 focusSet = true;
0665             }
0666             widget = qobject_cast<QWidget *>(le);
0667         } else if (opt->type == OC_FORM_OPT_SELECT) {
0668             auto cmb = new QComboBox(this);
0669             auto sopt = reinterpret_cast<oc_form_opt_select *>(opt);
0670 #if !OPENCONNECT_CHECK_VER(8, 0)
0671             const QString protocol = d->setting->data()[NM_OPENCONNECT_KEY_PROTOCOL];
0672 #endif
0673             for (int i = 0; i < sopt->nr_choices; i++) {
0674                 cmb->addItem(QString::fromUtf8(FORMCHOICE(sopt, i)->label), QString::fromUtf8(FORMCHOICE(sopt, i)->name));
0675                 if (value == QString::fromUtf8(FORMCHOICE(sopt, i)->name)) {
0676                     cmb->setCurrentIndex(i);
0677 #if !OPENCONNECT_CHECK_VER(8, 0)
0678                     if (protocol != QLatin1String("nc") && sopt == AUTHGROUP_OPT(form) && i != AUTHGROUP_SELECTION(form)) {
0679 #else
0680                     if (sopt == AUTHGROUP_OPT(form) && i != AUTHGROUP_SELECTION(form)) {
0681 #endif
0682                         QTimer::singleShot(0, this, &OpenconnectAuthWidget::formGroupChanged);
0683                     }
0684                 }
0685             }
0686 #if !OPENCONNECT_CHECK_VER(8, 0)
0687             if (protocol != QLatin1String("nc") && sopt == AUTHGROUP_OPT(form)) {
0688 #else
0689             if (sopt == AUTHGROUP_OPT(form)) {
0690 #endif
0691                 connect(cmb, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &OpenconnectAuthWidget::formGroupChanged);
0692             }
0693             widget = qobject_cast<QWidget *>(cmb);
0694         }
0695         if (widget) {
0696             widget->setProperty("openconnect_opt", (quintptr)opt);
0697             widget->setSizePolicy(policy);
0698             layout->addRow(text, widget);
0699         }
0700     }
0701     if (!layout->rowCount()) {
0702         delete layout;
0703         d->workerWaiting.wakeAll();
0704         return;
0705     }
0706 
0707     if (form->banner) {
0708         addFormInfo(QLatin1String("dialog-information"), form->banner);
0709     }
0710     if (form->message) {
0711         addFormInfo(QLatin1String("dialog-information"), form->message);
0712     }
0713     if (form->error) {
0714         addFormInfo(QLatin1String("dialog-error"), form->error);
0715     }
0716 
0717     d->ui.loginBoxLayout->addLayout(layout);
0718     d->passwordFormIndex = d->ui.loginBoxLayout->count() - 1;
0719 
0720     auto box = new QDialogButtonBox(this);
0721     QPushButton *btn = box->addButton(QDialogButtonBox::Ok);
0722     btn->setText(i18nc("Verb, to proceed with login", "Login"));
0723     btn->setDefault(true);
0724     d->ui.loginBoxLayout->addWidget(box);
0725     box->setProperty("openconnect_form", (quintptr)form);
0726 
0727     connect(box, &QDialogButtonBox::accepted, this, &OpenconnectAuthWidget::formLoginClicked);
0728 }
0729 
0730 void OpenconnectAuthWidget::validatePeerCert(const QString &fingerprint, const QString &peerCert, const QString &reason, bool *accepted)
0731 {
0732     Q_D(OpenconnectAuthWidget);
0733 
0734     const QString host = QLatin1String(openconnect_get_hostname(d->vpninfo));
0735     const QString port = QString::number(openconnect_get_port(d->vpninfo));
0736     const QString key = QString("certificate:%1:%2").arg(host, port);
0737     const QString value = d->secrets.value(key);
0738 
0739 #if !OPENCONNECT_CHECK_VER(5, 0)
0740 #define openconnect_check_peer_cert_hash(v, d) strcmp(d, fingerprint.toUtf8().data())
0741 #endif
0742 
0743     if (openconnect_check_peer_cert_hash(d->vpninfo, value.toUtf8().data())) {
0744         QPointer<QDialog> dialog = new QDialog(this);
0745         dialog->setAttribute(Qt::WA_DeleteOnClose);
0746         dialog.data()->setWindowModality(Qt::WindowModal);
0747 
0748         auto widget = new QWidget(dialog.data());
0749         QVBoxLayout *verticalLayout;
0750         QHBoxLayout *horizontalLayout;
0751         QLabel *icon;
0752         QLabel *infoText;
0753         QTextBrowser *certificate;
0754 
0755         verticalLayout = new QVBoxLayout(widget);
0756         horizontalLayout = new QHBoxLayout(widget);
0757         icon = new QLabel(widget);
0758         QSizePolicy sizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
0759         sizePolicy.setHorizontalStretch(0);
0760         sizePolicy.setVerticalStretch(0);
0761         sizePolicy.setHeightForWidth(icon->sizePolicy().hasHeightForWidth());
0762         icon->setSizePolicy(sizePolicy);
0763         icon->setMinimumSize(QSize(48, 48));
0764         icon->setMaximumSize(QSize(48, 48));
0765 
0766         horizontalLayout->addWidget(icon);
0767 
0768         infoText = new QLabel(widget);
0769         infoText->setAlignment(Qt::AlignLeading | Qt::AlignLeft | Qt::AlignVCenter);
0770 
0771         horizontalLayout->addWidget(infoText);
0772 
0773         verticalLayout->addLayout(horizontalLayout);
0774 
0775         certificate = new QTextBrowser(widget);
0776         certificate->setTextInteractionFlags(Qt::TextSelectableByMouse);
0777         certificate->setOpenLinks(false);
0778 
0779         verticalLayout->addWidget(certificate);
0780 
0781         const int iconSize = icon->style()->pixelMetric(QStyle::PixelMetric::PM_LargeIconSize);
0782         icon->setPixmap(QIcon::fromTheme("dialog-information").pixmap(iconSize));
0783         infoText->setText(
0784             i18n("Check failed for certificate from VPN server \"%1\".\n"
0785                  "Reason: %2\nAccept it anyway?",
0786                  openconnect_get_hostname(d->vpninfo),
0787                  reason));
0788         infoText->setWordWrap(true);
0789         certificate->setText(peerCert);
0790 
0791         dialog->setLayout(new QVBoxLayout);
0792         auto buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, dialog);
0793         connect(buttons, &QDialogButtonBox::accepted, dialog.data(), &QDialog::accept);
0794         connect(buttons, &QDialogButtonBox::rejected, dialog.data(), &QDialog::reject);
0795         dialog->layout()->addWidget(widget);
0796         dialog->layout()->addWidget(buttons);
0797 
0798         const NMStringMap dataMap = d->setting->data();
0799         buttons->button(QDialogButtonBox::Ok)->setEnabled(dataMap[NM_OPENCONNECT_KEY_PREVENT_INVALID_CERT] != "yes");
0800 
0801         if (dialog.data()->exec() == QDialog::Accepted) {
0802             *accepted = true;
0803         } else {
0804             *accepted = false;
0805         }
0806     } else {
0807         *accepted = true;
0808     }
0809     if (*accepted) {
0810         d->secrets.insert(key, QString(fingerprint));
0811     }
0812     d->mutex.lock();
0813     d->workerWaiting.wakeAll();
0814     d->mutex.unlock();
0815 }
0816 
0817 void OpenconnectAuthWidget::formGroupChanged()
0818 {
0819     Q_D(OpenconnectAuthWidget);
0820 
0821     d->formGroupChanged = true;
0822     formLoginClicked();
0823 }
0824 
0825 // Writes the user input from the form into the oc_auth_form structs we got from
0826 // libopenconnect, and wakes the worker thread up to try to log in and obtain a
0827 // cookie with this data
0828 void OpenconnectAuthWidget::formLoginClicked()
0829 {
0830     Q_D(OpenconnectAuthWidget);
0831 
0832     const int lastIndex = d->ui.loginBoxLayout->count() - 1;
0833     QLayout *layout = d->ui.loginBoxLayout->itemAt(d->passwordFormIndex)->layout();
0834     struct oc_auth_form *form = (struct oc_auth_form *)d->ui.loginBoxLayout->itemAt(lastIndex)->widget()->property("openconnect_form").value<quintptr>();
0835 
0836     for (int i = 0; i < layout->count(); i++) {
0837         QLayoutItem *item = layout->itemAt(i);
0838         QWidget *widget = item->widget();
0839         if (widget && widget->property("openconnect_opt").isValid()) {
0840             struct oc_form_opt *opt = (struct oc_form_opt *)widget->property("openconnect_opt").value<quintptr>();
0841             const QString key = QString("form:%1:%2").arg(QLatin1String(form->auth_id)).arg(QLatin1String(opt->name));
0842             if (opt->type == OC_FORM_OPT_PASSWORD || opt->type == OC_FORM_OPT_TEXT) {
0843                 auto le = qobject_cast<PasswordField *>(widget);
0844                 QByteArray text = le->text().toUtf8();
0845                 openconnect_set_option_value(opt, text.data());
0846                 if (opt->type == OC_FORM_OPT_TEXT) {
0847                     d->secrets.insert(key, le->text());
0848                 } else {
0849                     d->tmpSecrets.insert(key, le->text());
0850                 }
0851             } else if (opt->type == OC_FORM_OPT_SELECT) {
0852                 auto cbo = qobject_cast<QComboBox *>(widget);
0853                 QByteArray text = cbo->itemData(cbo->currentIndex()).toString().toLatin1();
0854                 openconnect_set_option_value(opt, text.data());
0855                 d->secrets.insert(key, cbo->itemData(cbo->currentIndex()).toString());
0856             }
0857         }
0858     }
0859 
0860     deleteAllFromLayout(d->ui.loginBoxLayout);
0861     d->workerWaiting.wakeAll();
0862 }
0863 
0864 void OpenconnectAuthWidget::workerFinished(const int &ret)
0865 {
0866     Q_D(OpenconnectAuthWidget);
0867 
0868     if (ret < 0) {
0869         QString message;
0870         QList<QPair<QString, int>>::const_iterator i;
0871         for (i = d->serverLog.constEnd() - 1; i >= d->serverLog.constBegin(); --i) {
0872             QPair<QString, int> pair = *i;
0873             if (pair.second <= OpenconnectAuthWidgetPrivate::Error) {
0874                 message = pair.first;
0875                 break;
0876             }
0877         }
0878         if (message.isEmpty()) {
0879             message = i18n("Connection attempt was unsuccessful.");
0880         }
0881         deleteAllFromLayout(d->ui.loginBoxLayout);
0882         addFormInfo(QLatin1String("dialog-error"), message);
0883     } else {
0884         deleteAllFromLayout(d->ui.loginBoxLayout);
0885         acceptDialog();
0886     }
0887 }
0888 
0889 void OpenconnectAuthWidget::deleteAllFromLayout(QLayout *layout)
0890 {
0891     while (QLayoutItem *item = layout->takeAt(0)) {
0892         if (QLayout *itemLayout = item->layout()) {
0893             deleteAllFromLayout(itemLayout);
0894             itemLayout->deleteLater();
0895         } else {
0896             item->widget()->deleteLater();
0897         }
0898         delete item;
0899     }
0900     layout->invalidate();
0901 }
0902 
0903 void OpenconnectAuthWidget::viewServerLogToggled(bool toggled)
0904 {
0905     Q_D(OpenconnectAuthWidget);
0906     d->ui.lblLogLevel->setVisible(toggled);
0907     d->ui.cmbLogLevel->setVisible(toggled);
0908     if (toggled) {
0909         delete d->ui.verticalLayout->takeAt(5);
0910         QSizePolicy policy = d->ui.serverLogBox->sizePolicy();
0911         policy.setVerticalPolicy(QSizePolicy::Expanding);
0912         d->ui.serverLogBox->setSizePolicy(policy);
0913         d->ui.serverLog->setVisible(true);
0914     } else {
0915         auto verticalSpacer = new QSpacerItem(0, 0, QSizePolicy::Minimum, QSizePolicy::Expanding);
0916         d->ui.verticalLayout->addItem(verticalSpacer);
0917         d->ui.serverLog->setVisible(false);
0918         QSizePolicy policy = d->ui.serverLogBox->sizePolicy();
0919         policy.setVerticalPolicy(QSizePolicy::Fixed);
0920         d->ui.serverLogBox->setSizePolicy(policy);
0921     }
0922 }
0923 
0924 #include "moc_openconnectauth.cpp"