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"