diff --git a/.gitmodules b/.gitmodules index fe83277..a95cc4c 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,7 @@ [submodule "singleapplication"] path = singleapplication url = https://github.com/itay-grudev/SingleApplication.git + +[submodule "plog"] + path = plog + url = https://github.com/SergiusTheBest/plog.git diff --git a/GPClient/GPClient.pro b/GPClient/GPClient.pro index 11d59a9..7d38d2c 100644 --- a/GPClient/GPClient.pro +++ b/GPClient/GPClient.pro @@ -15,6 +15,8 @@ DEFINES += QAPPLICATION_CLASS=QApplication # deprecated API in order to know how to port your code away from it. DEFINES += QT_DEPRECATED_WARNINGS +INCLUDEPATH += ../plog/include + # You can also make your code fail to compile if it uses deprecated APIs. # In order to do so, uncomment the following line. # You can also select to disable deprecated APIs only up to a certain version of Qt. @@ -23,7 +25,15 @@ SOURCES += \ cdpcommand.cpp \ cdpcommandmanager.cpp \ enhancedwebview.cpp \ + gatewayauthenticator.cpp \ + gpgateway.cpp \ + gphelper.cpp \ + loginparams.cpp \ main.cpp \ + normalloginwindow.cpp \ + portalauthenticator.cpp \ + portalconfigresponse.cpp \ + preloginresponse.cpp \ samlloginwindow.cpp \ gpclient.cpp @@ -31,11 +41,20 @@ HEADERS += \ cdpcommand.h \ cdpcommandmanager.h \ enhancedwebview.h \ + gatewayauthenticator.h \ + gpgateway.h \ + gphelper.h \ + loginparams.h \ + normalloginwindow.h \ + portalauthenticator.h \ + portalconfigresponse.h \ + preloginresponse.h \ samlloginwindow.h \ gpclient.h FORMS += \ - gpclient.ui + gpclient.ui \ + normalloginwindow.ui DBUS_INTERFACES += ../GPService/gpservice.xml diff --git a/GPClient/cdpcommandmanager.cpp b/GPClient/cdpcommandmanager.cpp index 6b0f998..9969dbd 100644 --- a/GPClient/cdpcommandmanager.cpp +++ b/GPClient/cdpcommandmanager.cpp @@ -1,5 +1,6 @@ #include "cdpcommandmanager.h" #include +#include CDPCommandManager::CDPCommandManager(QObject *parent) : QObject(parent) @@ -27,7 +28,7 @@ void CDPCommandManager::initialize(QString endpoint) reply, &QNetworkReply::finished, [reply, this]() { if (reply->error()) { - qDebug() << "CDP request error"; + PLOGE << "CDP request error"; return; } @@ -76,10 +77,10 @@ void CDPCommandManager::onTextMessageReceived(QString message) void CDPCommandManager::onSocketDisconnected() { - qDebug() << "WebSocket disconnected"; + PLOGI << "WebSocket disconnected"; } void CDPCommandManager::onSocketError(QAbstractSocket::SocketError error) { - qDebug() << "WebSocket error" << error; + PLOGE << "WebSocket error" << error; } diff --git a/GPClient/gatewayauthenticator.cpp b/GPClient/gatewayauthenticator.cpp new file mode 100644 index 0000000..eb55b6f --- /dev/null +++ b/GPClient/gatewayauthenticator.cpp @@ -0,0 +1,160 @@ +#include "gatewayauthenticator.h" +#include "gphelper.h" +#include "loginparams.h" +#include "preloginresponse.h" + +#include +#include + +using namespace gpclient::helper; + +GatewayAuthenticator::GatewayAuthenticator(const QString& gateway, const PortalConfigResponse& portalConfig) + : QObject() + , preloginUrl("https://" + gateway + "/ssl-vpn/prelogin.esp") + , loginUrl("https://" + gateway + "/ssl-vpn/login.esp") + , portalConfig(portalConfig) +{ +} + +GatewayAuthenticator::~GatewayAuthenticator() +{ + delete normalLoginWindow; +} + +void GatewayAuthenticator::authenticate() +{ + LoginParams params; + params.setUser(portalConfig.username()); + params.setPassword(portalConfig.password()); + params.setUserAuthCookie(portalConfig.userAuthCookie()); + + login(params); +} + +void GatewayAuthenticator::login(const LoginParams ¶ms) +{ + PLOGI << "Trying to login the gateway at " << loginUrl << " with " << params.toUtf8(); + + QNetworkReply *reply = createRequest(loginUrl, params.toUtf8()); + connect(reply, &QNetworkReply::finished, this, &GatewayAuthenticator::onLoginFinished); +} + +void GatewayAuthenticator::onLoginFinished() +{ + QNetworkReply *reply = qobject_cast(sender()); + + if (reply->error()) { + PLOGE << QString("Failed to login the gateway at %1, %2").arg(loginUrl).arg(reply->errorString()); + + if (normalLoginWindow) { + normalLoginWindow->setProcessing(false); + openMessageBox("Gateway login failed.", "Please check your credentials and try again."); + } else { + doAuth(); + } + return; + } + + if (normalLoginWindow) { + normalLoginWindow->close(); + } + + const QUrlQuery params = gpclient::helper::parseGatewayResponse(reply->readAll()); + emit success(params.toString()); +} + +void GatewayAuthenticator::doAuth() +{ + PLOGI << "Perform the gateway prelogin at " << preloginUrl; + + QNetworkReply *reply = createRequest(preloginUrl); + connect(reply, &QNetworkReply::finished, this, &GatewayAuthenticator::onPreloginFinished); +} + +void GatewayAuthenticator::onPreloginFinished() +{ + QNetworkReply *reply = qobject_cast(sender()); + + if (reply->error()) { + PLOGE << QString("Failed to prelogin the gateway at %1, %2").arg(preloginUrl).arg(reply->errorString()); + + emit fail("Error occurred on the gateway prelogin interface."); + return; + } + + PLOGI << "Gateway prelogin succeeded."; + + PreloginResponse response = PreloginResponse::parse(reply->readAll()); + + if (response.hasSamlAuthFields()) { + samlAuth(response.samlMethod(), response.samlRequest(), reply->url().toString()); + } else if (response.hasNormalAuthFields()) { + normalAuth(response.labelUsername(), response.labelPassword(), response.authMessage()); + } else { + PLOGE << QString("Unknown prelogin response for %1, got %2").arg(preloginUrl).arg(QString::fromUtf8(response.rawResponse())); + emit fail("Unknown response for gateway prelogin interface."); + } + + delete reply; +} + +void GatewayAuthenticator::normalAuth(QString labelUsername, QString labelPassword, QString authMessage) +{ + PLOGI << QString("Trying to perform the normal login with %1 / %2 credentials").arg(labelUsername).arg(labelPassword); + + normalLoginWindow = new NormalLoginWindow; + normalLoginWindow->setPortalAddress(gateway); + normalLoginWindow->setAuthMessage(authMessage); + normalLoginWindow->setUsernameLabel(labelUsername); + normalLoginWindow->setPasswordLabel(labelPassword); + + // Do login + connect(normalLoginWindow, &NormalLoginWindow::performLogin, this, &GatewayAuthenticator::onPerformNormalLogin); + connect(normalLoginWindow, &NormalLoginWindow::rejected, this, &GatewayAuthenticator::onLoginWindowRejected); + + normalLoginWindow->exec(); + delete normalLoginWindow; + normalLoginWindow = nullptr; +} + +void GatewayAuthenticator::onPerformNormalLogin(const QString &username, const QString &password) +{ + normalLoginWindow->setProcessing(true); + LoginParams params; + params.setUser(username); + params.setPassword(password); + login(params); +} + +void GatewayAuthenticator::onLoginWindowRejected() +{ + emit fail(); +} + +void GatewayAuthenticator::samlAuth(QString samlMethod, QString samlRequest, QString preloginUrl) +{ + PLOGI << "Trying to perform SAML login with saml-method " << samlMethod; + + SAMLLoginWindow *loginWindow = samlLogin(samlMethod, samlRequest, preloginUrl); + + if (!loginWindow) { + openMessageBox("SAML Login failed for gateway"); + return; + } + + connect(loginWindow, &SAMLLoginWindow::success, this, &GatewayAuthenticator::onSAMLLoginFinished); + connect(loginWindow, &SAMLLoginWindow::rejected, this, &GatewayAuthenticator::onLoginWindowRejected); +// loginWindow->exec(); +// delete loginWindow; +} + +void GatewayAuthenticator::onSAMLLoginFinished(const QMap &samlResult) +{ + PLOGI << "SAML login succeeded, got the prelogin cookie " << samlResult.value("preloginCookie"); + + LoginParams params; + params.setUser(samlResult.value("username")); + params.setPreloginCookie(samlResult.value("preloginCookie")); + + login(params); +} diff --git a/GPClient/gatewayauthenticator.h b/GPClient/gatewayauthenticator.h new file mode 100644 index 0000000..89b6499 --- /dev/null +++ b/GPClient/gatewayauthenticator.h @@ -0,0 +1,44 @@ +#ifndef GATEWAYAUTHENTICATOR_H +#define GATEWAYAUTHENTICATOR_H + +#include "portalconfigresponse.h" +#include "normalloginwindow.h" +#include "loginparams.h" +#include + +class GatewayAuthenticator : public QObject +{ + Q_OBJECT +public: + explicit GatewayAuthenticator(const QString& gateway, const PortalConfigResponse& portalConfig); + ~GatewayAuthenticator(); + + void authenticate(); + +signals: + void success(const QString& authCookie); + void fail(const QString& msg = ""); + +private slots: + void onLoginFinished(); + void onPreloginFinished(); + void onPerformNormalLogin(const QString &username, const QString &password); + void onSAMLLoginFinished(const QMap &samlResult); + void onLoginWindowRejected(); + +private: + QString gateway; + QString preloginUrl; + QString loginUrl; + + const PortalConfigResponse& portalConfig; + + NormalLoginWindow *normalLoginWindow{nullptr}; + + void login(const LoginParams& params); + void doAuth(); + void normalAuth(QString labelUsername, QString labelPassword, QString authMessage); + void samlAuth(QString samlMethod, QString samlRequest, QString preloginUrl = ""); +}; + +#endif // GATEWAYAUTHENTICATOR_H diff --git a/GPClient/gpclient.cpp b/GPClient/gpclient.cpp index af4e0e5..ff20468 100644 --- a/GPClient/gpclient.cpp +++ b/GPClient/gpclient.cpp @@ -1,224 +1,262 @@ #include "gpclient.h" +#include "gphelper.h" #include "ui_gpclient.h" -#include "samlloginwindow.h" +#include "portalauthenticator.h" +#include "gatewayauthenticator.h" -#include -#include -#include -#include -#include -#include -#include +#include +#include + +using namespace gpclient::helper; GPClient::GPClient(QWidget *parent) : QMainWindow(parent) , ui(new Ui::GPClient) + , systemTrayIcon(new QSystemTrayIcon(parent)) + , contextMenu(new QMenu("GlobalProtect", parent)) { ui->setupUi(this); + setWindowTitle("GlobalProtect"); setFixedSize(width(), height()); - moveCenter(); + gpclient::helper::moveCenter(this); // Restore portal from the previous settings - settings = new QSettings("com.yuezk.qt", "GPClient"); - ui->portalInput->setText(settings->value("portal", "").toString()); - - QObject::connect(this, &GPClient::connectFailed, [this]() { - updateConnectionStatus("not_connected"); - }); - - // QNetworkAccessManager setup - networkManager = new QNetworkAccessManager(this); + ui->portalInput->setText(settings::get("portal", "").toString()); // DBus service setup vpn = new com::yuezk::qt::GPService("com.yuezk.qt.GPService", "/", QDBusConnection::systemBus(), this); - QObject::connect(vpn, &com::yuezk::qt::GPService::connected, this, &GPClient::onVPNConnected); - QObject::connect(vpn, &com::yuezk::qt::GPService::disconnected, this, &GPClient::onVPNDisconnected); - QObject::connect(vpn, &com::yuezk::qt::GPService::logAvailable, this, &GPClient::onVPNLogAvailable); + connect(vpn, &com::yuezk::qt::GPService::connected, this, &GPClient::onVPNConnected); + connect(vpn, &com::yuezk::qt::GPService::disconnected, this, &GPClient::onVPNDisconnected); + connect(vpn, &com::yuezk::qt::GPService::logAvailable, this, &GPClient::onVPNLogAvailable); + + connect(systemTrayIcon, &QSystemTrayIcon::activated, this, &GPClient::onSystemTrayActivated); + + // Initiallize the context menu of system tray. + openAction = contextMenu->addAction(QIcon::fromTheme("system-run"), "Open", this, &GPClient::activiate); + connectAction = contextMenu->addAction(QIcon::fromTheme("preferences-system-network"), "Connect", this, &GPClient::doConnect); + contextMenu->addSeparator(); + quitAction = contextMenu->addAction(QIcon::fromTheme("application-exit"), "Quit", this, &GPClient::quit); + systemTrayIcon->setContextMenu(contextMenu); + systemTrayIcon->setToolTip("GlobalProtect"); initVpnStatus(); + systemTrayIcon->show(); } GPClient::~GPClient() { delete ui; - delete networkManager; - delete reply; delete vpn; - delete settings; + delete systemTrayIcon; + delete openAction; + delete connectAction; + delete quitAction; + delete contextMenu; } void GPClient::on_connectButton_clicked() { - QString btnText = ui->connectButton->text(); - - if (btnText.endsWith("Connect")) { - QString portal = ui->portalInput->text(); - settings->setValue("portal", portal); - ui->statusLabel->setText("Authenticating..."); - updateConnectionStatus("pending"); - doAuth(portal); - } else if (btnText.endsWith("Cancel")) { - ui->statusLabel->setText("Canceling..."); - updateConnectionStatus("pending"); - - if (reply->isRunning()) { - reply->abort(); - } - vpn->disconnect(); - } else { - ui->statusLabel->setText("Disconnecting..."); - updateConnectionStatus("pending"); - vpn->disconnect(); - } + doConnect(); } -void GPClient::preloginResultFinished() +void GPClient::on_portalInput_returnPressed() { - QNetworkReply::NetworkError err = reply->error(); - if (err) { - qWarning() << "Prelogin request error: " << err; - emit connectFailed(); - return; - } - - QByteArray xmlBytes = reply->readAll(); - const QString tagMethod = "saml-auth-method"; - const QString tagRequest = "saml-request"; - QString samlMethod; - QString samlRequest; - - QXmlStreamReader xml(xmlBytes); - while (!xml.atEnd()) { - xml.readNext(); - if (xml.tokenType() == xml.StartElement) { - if (xml.name() == tagMethod) { - samlMethod = xml.readElementText(); - } else if (xml.name() == tagRequest) { - samlRequest = QByteArray::fromBase64(QByteArray::fromStdString(xml.readElementText().toStdString())); - } - } - } - - if (samlMethod == nullptr || samlRequest == nullptr) { - qWarning("This does not appear to be a SAML prelogin response ( or tags missing)"); - emit connectFailed(); - return; - } - - if (samlMethod == "POST") { - samlLogin(reply->url().toString(), samlRequest); - } else if (samlMethod == "REDIRECT") { - samlLogin(samlRequest); - } + doConnect(); } -void GPClient::onLoginSuccess(QJsonObject loginResult) +void GPClient::updateConnectionStatus(const GPClient::VpnStatus &status) { - QString fullpath = "/ssl-vpn/login.esp"; - QString shortpath = "gateway"; - QString user = loginResult.value("saml-username").toString(); - QString cookieName; - QString cookieValue; - QString cookies[]{"prelogin-cookie", "portal-userauthcookie"}; + switch (status) { + case VpnStatus::disconnected: + ui->statusLabel->setText("Not Connected"); + ui->statusImage->setStyleSheet("image: url(:/images/not_connected.png); padding: 15;"); + ui->connectButton->setText("Connect"); + ui->connectButton->setDisabled(false); + ui->portalInput->setReadOnly(false); - for (int i = 0; i < cookies->length(); i++) { - cookieValue = loginResult.value(cookies[i]).toString(); - if (cookieValue != nullptr) { - cookieName = cookies[i]; + systemTrayIcon->setIcon(QIcon{ ":/images/not_connected.png" }); + connectAction->setEnabled(true); + connectAction->setText("Connect"); break; - } - } + case VpnStatus::pending: + ui->statusImage->setStyleSheet("image: url(:/images/pending.png); padding: 15;"); + ui->connectButton->setDisabled(true); + ui->portalInput->setReadOnly(true); - QString host = QString("https://%1/%2:%3").arg(loginResult.value("server").toString(), shortpath, cookieName); - vpn->connect(host, user, cookieValue); - ui->statusLabel->setText("Connecting..."); - updateConnectionStatus("pending"); -} + systemTrayIcon->setIcon(QIcon{ ":/images/pending.png" }); + connectAction->setEnabled(false); + break; + case VpnStatus::connected: + ui->statusLabel->setText("Connected"); + ui->statusImage->setStyleSheet("image: url(:/images/connected.png); padding: 15;"); + ui->connectButton->setText("Disconnect"); + ui->connectButton->setDisabled(false); + ui->portalInput->setReadOnly(true); -void GPClient::updateConnectionStatus(QString status) -{ - if (status == "not_connected") { - ui->statusLabel->setText("Not Connected"); - ui->statusImage->setStyleSheet("image: url(:/images/not_connected.png); padding: 15;"); - ui->connectButton->setText("Connect"); - ui->connectButton->setDisabled(false); - } else if (status == "pending") { - ui->statusImage->setStyleSheet("image: url(:/images/pending.png); padding: 15;"); - ui->connectButton->setText("Cancel"); - ui->connectButton->setDisabled(false); - } else if (status == "connected") { - ui->statusLabel->setText("Connected"); - ui->statusImage->setStyleSheet("image: url(:/images/connected.png); padding: 15;"); - ui->connectButton->setText("Disconnect"); - ui->connectButton->setDisabled(false); + systemTrayIcon->setIcon(QIcon{ ":/images/connected.png" }); + connectAction->setEnabled(true); + connectAction->setText("Disconnect"); + break; + default: + break; } } void GPClient::onVPNConnected() { - updateConnectionStatus("connected"); + updateConnectionStatus(VpnStatus::connected); } void GPClient::onVPNDisconnected() { - updateConnectionStatus("not_connected"); + updateConnectionStatus(VpnStatus::disconnected); } void GPClient::onVPNLogAvailable(QString log) { - qInfo() << log; + PLOGI << log; +} + +void GPClient::onSystemTrayActivated(QSystemTrayIcon::ActivationReason reason) +{ + switch (reason) { + case QSystemTrayIcon::Trigger: + case QSystemTrayIcon::DoubleClick: + this->activiate(); + break; + default: + break; + } +} + +void GPClient::activiate() +{ + activateWindow(); + showNormal(); +} + +QString GPClient::portal() const +{ + const QString input = ui->portalInput->text().trimmed(); + + if (input.startsWith("http")) { + return QUrl(input).authority(); + } + return input; } void GPClient::initVpnStatus() { int status = vpn->status(); + if (status == 1) { ui->statusLabel->setText("Connecting..."); - updateConnectionStatus("pending"); + updateConnectionStatus(VpnStatus::pending); } else if (status == 2) { - updateConnectionStatus("connected"); + updateConnectionStatus(VpnStatus::connected); } else if (status == 3) { ui->statusLabel->setText("Disconnecting..."); - updateConnectionStatus("pending"); + updateConnectionStatus(VpnStatus::pending); + } else { + updateConnectionStatus(VpnStatus::disconnected); } } -void GPClient::moveCenter() +void GPClient::doConnect() { - QDesktopWidget *desktop = QApplication::desktop(); + const QString btnText = ui->connectButton->text(); + const QString portal = this->portal(); - int screenWidth, width; - int screenHeight, height; - int x, y; - QSize windowSize; + if (portal.isEmpty()) { + activiate(); + return; + } - screenWidth = desktop->width(); - screenHeight = desktop->height(); + if (btnText.endsWith("Connect")) { + settings::save("portal", portal); + ui->statusLabel->setText("Authenticating..."); + updateConnectionStatus(VpnStatus::pending); - windowSize = size(); - width = windowSize.width(); - height = windowSize.height(); + // Perform the portal login + portalLogin(portal); + } else { + ui->statusLabel->setText("Disconnecting..."); + updateConnectionStatus(VpnStatus::pending); - x = (screenWidth - width) / 2; - y = (screenHeight - height) / 2; - y -= 50; - move(x, y); + vpn->disconnect(); + } } -void GPClient::doAuth(const QString portal) +// Login to the portal interface to get the portal config and preferred gateway +void GPClient::portalLogin(const QString& portal) { - const QString preloginUrl = "https://" + portal + "/ssl-vpn/prelogin.esp"; - reply = networkManager->post(QNetworkRequest(preloginUrl), (QByteArray) nullptr); - connect(reply, &QNetworkReply::finished, this, &GPClient::preloginResultFinished); + PortalAuthenticator *portalAuth = new PortalAuthenticator(portal); + + connect(portalAuth, &PortalAuthenticator::success, this, &GPClient::onPortalSuccess); + // Prelogin failed on the portal interface, try to treat the portal as a gateway interface + connect(portalAuth, &PortalAuthenticator::preloginFailed, this, &GPClient::onPortalPreloginFail); + // Portal login failed + connect(portalAuth, &PortalAuthenticator::fail, this, &GPClient::onPortalFail); + + portalAuth->authenticate(); } -void GPClient::samlLogin(const QString loginUrl, const QString html) +void GPClient::onPortalSuccess(const PortalConfigResponse &portalConfig, const GPGateway &gateway) { - SAMLLoginWindow *loginWindow = new SAMLLoginWindow(this); + this->portalConfig = portalConfig; + this->gateway = gateway; - QObject::connect(loginWindow, &SAMLLoginWindow::success, this, &GPClient::onLoginSuccess); - QObject::connect(loginWindow, &SAMLLoginWindow::rejected, this, &GPClient::connectFailed); - - loginWindow->login(loginUrl, html); - loginWindow->exec(); - delete loginWindow; + gatewayLogin(); +} + +void GPClient::onPortalPreloginFail() +{ + PLOGI << "Portal prelogin failed, try to preform login on the the gateway interface..."; + + // Set the gateway address to portal input + gateway.setAddress(portal()); + gatewayLogin(); +} + +void GPClient::onPortalFail(const QString &msg) +{ + if (!msg.isEmpty()) { + openMessageBox("Portal authentication failed.", msg); + } + + updateConnectionStatus(VpnStatus::disconnected); +} + +// Login to the gateway +void GPClient::gatewayLogin() const +{ + GatewayAuthenticator *gatewayAuth = new GatewayAuthenticator(gateway.address(), portalConfig); + + connect(gatewayAuth, &GatewayAuthenticator::success, this, &GPClient::onGatewaySuccess); + connect(gatewayAuth, &GatewayAuthenticator::fail, this, &GPClient::onGatewayFail); + + gatewayAuth->authenticate(); +} + +void GPClient::quit() +{ + vpn->disconnect(); + QApplication::quit(); +} + +void GPClient::onGatewaySuccess(const QString &authCookie) +{ + PLOGI << "Gateway login succeeded, got the cookie " << authCookie; + + vpn->connect(gateway.address(), portalConfig.username(), authCookie); + ui->statusLabel->setText("Connecting..."); + updateConnectionStatus(VpnStatus::pending); +} + +void GPClient::onGatewayFail(const QString &msg) +{ + if (!msg.isEmpty()) { + openMessageBox("Portal authentication failed.", msg); + } + + updateConnectionStatus(VpnStatus::disconnected); } diff --git a/GPClient/gpclient.h b/GPClient/gpclient.h index 3bc771c..f03a50f 100644 --- a/GPClient/gpclient.h +++ b/GPClient/gpclient.h @@ -2,9 +2,11 @@ #define GPCLIENT_H #include "gpservice_interface.h" +#include "portalconfigresponse.h" + #include -#include -#include +#include +#include QT_BEGIN_NAMESPACE namespace Ui { class GPClient; } @@ -17,31 +19,53 @@ class GPClient : public QMainWindow public: GPClient(QWidget *parent = nullptr); ~GPClient(); - -signals: - void connectFailed(); + void activiate(); private slots: void on_connectButton_clicked(); - void preloginResultFinished(); + void on_portalInput_returnPressed(); - void onLoginSuccess(QJsonObject loginResult); + void onPortalSuccess(const PortalConfigResponse &portalConfig, const GPGateway &gateway); + void onPortalPreloginFail(); + void onPortalFail(const QString &msg); + void onGatewaySuccess(const QString &authCookie); + void onGatewayFail(const QString &msg); void onVPNConnected(); void onVPNDisconnected(); void onVPNLogAvailable(QString log); + void onSystemTrayActivated(QSystemTrayIcon::ActivationReason reason); + private: + enum class VpnStatus + { + disconnected, + pending, + connected + }; + Ui::GPClient *ui; - QNetworkAccessManager *networkManager; - QNetworkReply *reply; com::yuezk::qt::GPService *vpn; - QSettings *settings; + + QSystemTrayIcon *systemTrayIcon; + QMenu *contextMenu; + QAction *openAction; + QAction *connectAction; + QAction *quitAction; + + GPGateway gateway; + PortalConfigResponse portalConfig; + + QString portal() const; void initVpnStatus(); - void moveCenter(); - void updateConnectionStatus(QString status); - void doAuth(const QString portal); - void samlLogin(const QString loginUrl, const QString html = ""); + void doConnect(); + void updateConnectionStatus(const VpnStatus &status); + + void portalLogin(const QString& portal); + void gatewayLogin() const; + + void quit(); }; #endif // GPCLIENT_H diff --git a/GPClient/gpclient.ui b/GPClient/gpclient.ui index ad50a25..a263af1 100644 --- a/GPClient/gpclient.ui +++ b/GPClient/gpclient.ui @@ -11,7 +11,7 @@ - GP VPN Client + GlobalProtect OpenConnect @@ -113,6 +113,12 @@ Connect + + true + + + false + diff --git a/GPClient/gpgateway.cpp b/GPClient/gpgateway.cpp new file mode 100644 index 0000000..d3e539b --- /dev/null +++ b/GPClient/gpgateway.cpp @@ -0,0 +1,38 @@ +#include "gpgateway.h" + +GPGateway::GPGateway() +{ +} + +QString GPGateway::name() const +{ + return _name; +} + +QString GPGateway::address() const +{ + return _address; +} + +void GPGateway::setName(const QString &name) +{ + _name = name; +} + +void GPGateway::setAddress(const QString &address) +{ + _address = address; +} + +void GPGateway::setPriorityRules(const QMap &priorityRules) +{ + _priorityRules = priorityRules; +} + +int GPGateway::priorityOf(QString ruleName) +{ + if (_priorityRules.contains(ruleName)) { + return _priorityRules.value(ruleName); + } + return 0; +} diff --git a/GPClient/gpgateway.h b/GPClient/gpgateway.h new file mode 100644 index 0000000..129fe41 --- /dev/null +++ b/GPClient/gpgateway.h @@ -0,0 +1,26 @@ +#ifndef GPGATEWAY_H +#define GPGATEWAY_H + +#include +#include + +class GPGateway +{ +public: + GPGateway(); + + QString name() const; + QString address() const; + + void setName(const QString &name); + void setAddress(const QString &address); + void setPriorityRules(const QMap &priorityRules); + int priorityOf(QString ruleName); + +private: + QString _name; + QString _address; + QMap _priorityRules; +}; + +#endif // GPGATEWAY_H diff --git a/GPClient/gphelper.cpp b/GPClient/gphelper.cpp new file mode 100644 index 0000000..ca2b8b3 --- /dev/null +++ b/GPClient/gphelper.cpp @@ -0,0 +1,118 @@ +#include "gphelper.h" +#include +#include +#include +#include +#include +#include +#include + +QNetworkAccessManager* gpclient::helper::networkManager = new QNetworkAccessManager; + +QNetworkReply* gpclient::helper::createRequest(QString url, QByteArray params) +{ + QNetworkRequest request(url); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); + request.setHeader(QNetworkRequest::UserAgentHeader, UA); + + if (params == nullptr) { + return networkManager->post(request, QByteArray(nullptr)); + } + return networkManager->post(request, params); +} + +SAMLLoginWindow* gpclient::helper::samlLogin(QString samlMethod, QString samlRequest, QString preloginUrl) +{ + SAMLLoginWindow *loginWindow = new SAMLLoginWindow; + + if (samlMethod == "POST") { + loginWindow->login(preloginUrl, samlRequest); + } else if (samlMethod == "REDIRECT") { + loginWindow->login(samlRequest); + } else { + PLOGE << "Unknown saml-auth-method expected POST or REDIRECT, got " << samlMethod; + return nullptr; + } + return loginWindow; +} + +GPGateway &gpclient::helper::filterPreferredGateway(QList &gateways, QString ruleName) +{ + GPGateway& gateway = gateways.first(); + + for (GPGateway& g : gateways) { + if (g.priorityOf(ruleName) > gateway.priorityOf(ruleName)) { + gateway = g; + } + } + + return gateway; +} + +QUrlQuery gpclient::helper::parseGatewayResponse(const QByteArray &xml) +{ + QXmlStreamReader xmlReader{xml}; + QList args; + + while (!xmlReader.atEnd()) { + xmlReader.readNextStartElement(); + if (xmlReader.name() == "argument") { + args.append(QUrl::toPercentEncoding(xmlReader.readElementText())); + } + } + + QUrlQuery params{}; + params.addQueryItem("authcookie", args.at(1)); + params.addQueryItem("portal", args.at(3)); + params.addQueryItem("user", args.at(4)); + params.addQueryItem("domain", args.at(7)); + params.addQueryItem("preferred-ip", args.at(15)); + params.addQueryItem("computer", QUrl::toPercentEncoding(QSysInfo::machineHostName())); + + return params; +} + +void gpclient::helper::openMessageBox(const QString &message, const QString& informativeText) +{ + QMessageBox msgBox; + msgBox.setWindowTitle("GlobalProtect"); + msgBox.setText(message); + msgBox.setFixedWidth(500); + msgBox.setStyleSheet("QLabel{min-width: 250px}"); + msgBox.setInformativeText(informativeText); + msgBox.exec(); +} + +void gpclient::helper::moveCenter(QWidget *widget) +{ + QDesktopWidget *desktop = QApplication::desktop(); + + int screenWidth, width; + int screenHeight, height; + int x, y; + QSize windowSize; + + screenWidth = desktop->width(); + screenHeight = desktop->height(); + + windowSize = widget->size(); + width = windowSize.width(); + height = windowSize.height(); + + x = (screenWidth - width) / 2; + y = (screenHeight - height) / 2; + y -= 50; + widget->move(x, y); +} + +QSettings *gpclient::helper::settings::_settings = new QSettings("com.yuezk.qt", "GPClient"); + +QVariant gpclient::helper::settings::get(const QString &key, const QVariant &defaultValue) +{ + return _settings->value(key, defaultValue); +} + +void gpclient::helper::settings::save(const QString &key, const QVariant &value) +{ + _settings->setValue(key, value); +} diff --git a/GPClient/gphelper.h b/GPClient/gphelper.h new file mode 100644 index 0000000..5dce246 --- /dev/null +++ b/GPClient/gphelper.h @@ -0,0 +1,43 @@ +#ifndef GPHELPER_H +#define GPHELPER_H + +#include "samlloginwindow.h" +#include "gpgateway.h" + +#include +#include +#include +#include +#include +#include + + +const QString UA = "PAN GlobalProtect"; + +namespace gpclient { + namespace helper { + extern QNetworkAccessManager *networkManager; + + QNetworkReply* createRequest(QString url, QByteArray params = nullptr); + + SAMLLoginWindow *samlLogin(QString samlMethod, QString samlRequest, QString preloginUrl); + + GPGateway& filterPreferredGateway(QList &gateways, QString ruleName); + + QUrlQuery parseGatewayResponse(const QByteArray& xml); + + void openMessageBox(const QString& message, const QString& informativeText = ""); + + void moveCenter(QWidget *widget); + + namespace settings { + + extern QSettings *_settings; + + QVariant get(const QString &key, const QVariant &defaultValue = QVariant()); + void save(const QString &key, const QVariant &value); + } + } +} + +#endif // GPHELPER_H diff --git a/GPClient/loginparams.cpp b/GPClient/loginparams.cpp new file mode 100644 index 0000000..0a0ea61 --- /dev/null +++ b/GPClient/loginparams.cpp @@ -0,0 +1,54 @@ +#include "loginparams.h" + +#include + +LoginParams::LoginParams() +{ +} + +LoginParams::~LoginParams() +{ +} + +void LoginParams::setUser(const QString &user) +{ + updateQueryItem("user", user); +} + +void LoginParams::setServer(const QString &server) +{ + updateQueryItem("server", server); +} + +void LoginParams::setPassword(const QString &password) +{ + updateQueryItem("passwd", password); +} + +void LoginParams::setUserAuthCookie(const QString &cookie) +{ + updateQueryItem("portal-userauthcookie", cookie); +} + +void LoginParams::setPrelogonAuthCookie(const QString &cookie) +{ + updateQueryItem("portal-prelogonuserauthcookie", cookie); +} + +void LoginParams::setPreloginCookie(const QString &cookie) +{ + updateQueryItem("prelogin-cookie", cookie); +} + +QByteArray LoginParams::toUtf8() const +{ + return params.toString().toUtf8(); +} + +void LoginParams::updateQueryItem(const QString &key, const QString &value) +{ + if (params.hasQueryItem(key)) { + params.removeQueryItem(key); + } + params.addQueryItem(key, QUrl::toPercentEncoding(value)); +} diff --git a/GPClient/loginparams.h b/GPClient/loginparams.h new file mode 100644 index 0000000..c14bc6d --- /dev/null +++ b/GPClient/loginparams.h @@ -0,0 +1,44 @@ +#ifndef LOGINPARAMS_H +#define LOGINPARAMS_H + +#include + +class LoginParams +{ +public: + LoginParams(); + ~LoginParams(); + + void setUser(const QString &user); + void setServer(const QString &server); + void setPassword(const QString &password); + void setUserAuthCookie(const QString &cookie); + void setPrelogonAuthCookie(const QString &cookie); + void setPreloginCookie(const QString &cookie); + + QByteArray toUtf8() const; + +private: + QUrlQuery params { + {"prot", QUrl::toPercentEncoding("https:")}, + {"server", ""}, + {"inputSrc", ""}, + {"jnlpReady", "jnlpReady"}, + {"user", ""}, + {"passwd", ""}, + {"computer", QUrl::toPercentEncoding(QSysInfo::machineHostName())}, + {"ok", "Login"}, + {"direct", "yes"}, + {"clientVer", "4100"}, + {"os-version", QUrl::toPercentEncoding(QSysInfo::prettyProductName())}, + {"clientos", "Linux"}, + {"portal-userauthcookie", ""}, + {"portal-prelogonuserauthcookie", ""}, + {"prelogin-cookie", ""}, + {"ipv6-support", "yes"} + }; + + void updateQueryItem(const QString &key, const QString &value); +}; + +#endif // LOGINPARAMS_H diff --git a/GPClient/main.cpp b/GPClient/main.cpp index a30aee4..14b7937 100644 --- a/GPClient/main.cpp +++ b/GPClient/main.cpp @@ -2,17 +2,32 @@ #include "gpclient.h" #include "enhancedwebview.h" +#include +#include +#include + int main(int argc, char *argv[]) { + const QDir path = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + "/GlobalProtect-openconnect"; + const QString logFile = path.path() + "/gpclient.log"; + if (!path.exists()) { + path.mkpath("."); + } + + static plog::ColorConsoleAppender consoleAppender; + plog::init(plog::debug, logFile.toUtf8()).addAppender(&consoleAppender); + QString port = QString::fromLocal8Bit(qgetenv(ENV_CDP_PORT)); + if (port == "") { qputenv(ENV_CDP_PORT, "12315"); } + SingleApplication app(argc, argv); GPClient w; w.show(); - QObject::connect(&app, &SingleApplication::instanceStarted, &w, &GPClient::raise); + QObject::connect(&app, &SingleApplication::instanceStarted, &w, &GPClient::activiate); return app.exec(); } diff --git a/GPClient/normalloginwindow.cpp b/GPClient/normalloginwindow.cpp new file mode 100644 index 0000000..44dde14 --- /dev/null +++ b/GPClient/normalloginwindow.cpp @@ -0,0 +1,62 @@ +#include "normalloginwindow.h" +#include "ui_normalloginwindow.h" + +#include + +NormalLoginWindow::NormalLoginWindow(QWidget *parent) : + QDialog(parent), + ui(new Ui::NormalLoginWindow) +{ + ui->setupUi(this); + setFixedSize(width(), height()); +} + +NormalLoginWindow::~NormalLoginWindow() +{ + delete ui; +} + +void NormalLoginWindow::setAuthMessage(QString message) +{ + ui->authMessage->setText(message); +} + +void NormalLoginWindow::setUsernameLabel(QString label) +{ + ui->username->setPlaceholderText(label); +} + +void NormalLoginWindow::setPasswordLabel(QString label) +{ + ui->password->setPlaceholderText(label); +} + +void NormalLoginWindow::setPortalAddress(QString portal) +{ + ui->portalAddress->setText(portal); +} + +void NormalLoginWindow::setProcessing(bool isProcessing) +{ + ui->username->setReadOnly(isProcessing); + ui->password->setReadOnly(isProcessing); + ui->loginButton->setDisabled(isProcessing); +} + +void NormalLoginWindow::on_loginButton_clicked() +{ + const QString username = ui->username->text().trimmed(); + const QString password = ui->password->text().trimmed(); + + if (username.isEmpty() || password.isEmpty()) { + return; + } + + emit performLogin(username, password); +} + +void NormalLoginWindow::closeEvent(QCloseEvent *event) +{ + event->accept(); + reject(); +} diff --git a/GPClient/normalloginwindow.h b/GPClient/normalloginwindow.h new file mode 100644 index 0000000..d697aba --- /dev/null +++ b/GPClient/normalloginwindow.h @@ -0,0 +1,37 @@ +#ifndef PORTALAUTHWINDOW_H +#define PORTALAUTHWINDOW_H + +#include + +namespace Ui { +class NormalLoginWindow; +} + +class NormalLoginWindow : public QDialog +{ + Q_OBJECT + +public: + explicit NormalLoginWindow(QWidget *parent = nullptr); + ~NormalLoginWindow(); + + void setAuthMessage(QString); + void setUsernameLabel(QString); + void setPasswordLabel(QString); + void setPortalAddress(QString); + + void setProcessing(bool isProcessing); + +private slots: + void on_loginButton_clicked(); + +signals: + void performLogin(QString username, QString password); + +private: + Ui::NormalLoginWindow *ui; + + void closeEvent(QCloseEvent *event); +}; + +#endif // PORTALAUTHWINDOW_H diff --git a/GPClient/normalloginwindow.ui b/GPClient/normalloginwindow.ui new file mode 100644 index 0000000..4183116 --- /dev/null +++ b/GPClient/normalloginwindow.ui @@ -0,0 +1,148 @@ + + + NormalLoginWindow + + + + 0 + 0 + 255 + 269 + + + + + 0 + 0 + + + + ArrowCursor + + + Login + + + true + + + + + + + + + + + 20 + + + + Login + + + Qt::AlignCenter + + + + + + + true + + + + 0 + 2 + + + + Please enter the login credentials + + + Qt::AlignCenter + + + + + + + + + 0 + + + 6 + + + + + + 0 + 0 + + + + Portal: + + + 0 + + + + + + + + 0 + 0 + + + + vpn.example.com + + + + + + + + + + + Username + + + + + + + + + + QLineEdit::Password + + + Password + + + false + + + + + + + Login + + + + + + + + + + + + diff --git a/GPClient/portalauthenticator.cpp b/GPClient/portalauthenticator.cpp new file mode 100644 index 0000000..786490e --- /dev/null +++ b/GPClient/portalauthenticator.cpp @@ -0,0 +1,190 @@ +#include "portalauthenticator.h" +#include "gphelper.h" +#include "normalloginwindow.h" +#include "samlloginwindow.h" +#include "loginparams.h" +#include "preloginresponse.h" +#include "portalconfigresponse.h" +#include "gpgateway.h" + +#include +#include + +using namespace gpclient::helper; + +PortalAuthenticator::PortalAuthenticator(const QString& portal) : QObject() + , portal(portal) + , preloginUrl("https://" + portal + "/global-protect/prelogin.esp") + , configUrl("https://" + portal + "/global-protect/getconfig.esp") +{ +} + +PortalAuthenticator::~PortalAuthenticator() +{ + delete normalLoginWindow; +} + + +void PortalAuthenticator::authenticate() +{ + PLOGI << "Preform portal prelogin at " << preloginUrl; + + QNetworkReply *reply = createRequest(preloginUrl); + connect(reply, &QNetworkReply::finished, this, &PortalAuthenticator::onPreloginFinished); +} + +void PortalAuthenticator::onPreloginFinished() +{ + QNetworkReply *reply = qobject_cast(sender()); + + if (reply->error()) { + PLOGE << QString("Error occurred while accessing %1, %2").arg(preloginUrl).arg(reply->errorString()); + emit preloginFailed("Error occurred on the portal prelogin interface."); + delete reply; + return; + } + + PLOGI << "Portal prelogin succeeded."; + + preloginResponse = PreloginResponse::parse(reply->readAll()); + if (preloginResponse.hasSamlAuthFields()) { + // Do SAML authentication + samlAuth(); + } else if (preloginResponse.hasNormalAuthFields()) { + // Do normal username/password authentication + tryAutoLogin(); + } else { + PLOGE << QString("Unknown prelogin response for %1 got %2").arg(preloginUrl).arg(QString::fromUtf8(preloginResponse.rawResponse())); + emitFail("Unknown response for portal prelogin interface."); + } + + delete reply; +} + +void PortalAuthenticator::tryAutoLogin() +{ + const QString username = settings::get("username").toString(); + const QString password = settings::get("password").toString(); + + if (!username.isEmpty() && !password.isEmpty()) { + PLOGI << "Trying auto login using the saved credentials"; + isAutoLogin = true; + fetchConfig(settings::get("username").toString(), settings::get("password").toString()); + } else { + normalAuth(); + } +} + +void PortalAuthenticator::normalAuth() +{ + PLOGI << "Trying to launch the normal login window..."; + + normalLoginWindow = new NormalLoginWindow; + normalLoginWindow->setPortalAddress(portal); + normalLoginWindow->setAuthMessage(preloginResponse.authMessage()); + normalLoginWindow->setUsernameLabel(preloginResponse.labelUsername()); + normalLoginWindow->setPasswordLabel(preloginResponse.labelPassword()); + + // Do login + connect(normalLoginWindow, &NormalLoginWindow::performLogin, this, &PortalAuthenticator::onPerformNormalLogin); + connect(normalLoginWindow, &NormalLoginWindow::rejected, this, &PortalAuthenticator::onLoginWindowRejected); + + normalLoginWindow->exec(); + delete normalLoginWindow; + normalLoginWindow = nullptr; +} + +void PortalAuthenticator::onPerformNormalLogin(const QString &username, const QString &password) +{ + normalLoginWindow->setProcessing(true); + fetchConfig(username, password); +} + +void PortalAuthenticator::onLoginWindowRejected() +{ + emitFail(); +} + +void PortalAuthenticator::samlAuth() +{ + PLOGI << "Trying to perform SAML login with saml-method " << preloginResponse.samlMethod(); + + SAMLLoginWindow *loginWindow = samlLogin(preloginResponse.samlMethod(), preloginResponse.samlRequest(), preloginUrl); + + if (!loginWindow) { + openMessageBox("SAML Login failed for portal"); + return; + } + + connect(loginWindow, &SAMLLoginWindow::success, this, &PortalAuthenticator::onSAMLLoginSuccess); + connect(loginWindow, &SAMLLoginWindow::rejected, this, &PortalAuthenticator::onLoginWindowRejected); +} + +void PortalAuthenticator::onSAMLLoginSuccess(const QMap &samlResult) +{ + PLOGI << "SAML login succeeded, got the prelogin cookie " << samlResult.value("preloginCookie"); + + fetchConfig(samlResult.value("username"), "", samlResult.value("preloginCookie")); +} + +void PortalAuthenticator::fetchConfig(QString username, QString password, QString preloginCookie) +{ + LoginParams params; + params.setServer(portal); + params.setUser(username); + params.setPassword(password); + params.setPreloginCookie(preloginCookie); + + // Save the username and password for future use. + this->username = username; + this->password = password; + + PLOGI << "Fetching the portal config from " << configUrl << " for user: " << username; + + QNetworkReply *reply = createRequest(configUrl, params.toUtf8()); + + connect(reply, &QNetworkReply::finished, this, &PortalAuthenticator::onFetchConfigFinished); +} + +void PortalAuthenticator::onFetchConfigFinished() +{ + QNetworkReply *reply = qobject_cast(sender()); + + if (reply->error()) { + PLOGE << QString("Failed to fetch the portal config from %1, %2").arg(configUrl).arg(reply->errorString()); + + // Login failed, enable the fields of the normal login window + if (normalLoginWindow) { + normalLoginWindow->setProcessing(false); + openMessageBox("Portal login failed.", "Please check your credentials and try again."); + } else if (isAutoLogin) { + isAutoLogin = false; + normalAuth(); + } else { + emitFail("Failed to fetch the portal config."); + } + return; + } + + PLOGI << "Fetch the portal config succeeded."; + + PortalConfigResponse response = PortalConfigResponse::parse(reply->readAll()); + // Add the username & password to the response object + response.setUsername(username); + response.setPassword(password); + + // Close the login window + if (normalLoginWindow) { + // Save the credentials for reuse + settings::save("username", username); + settings::save("password", password); + normalLoginWindow->close(); + } + + emit success(response, filterPreferredGateway(response.allGateways(), preloginResponse.region())); +} + +void PortalAuthenticator::emitFail(const QString& msg) +{ + emit fail(msg); +} diff --git a/GPClient/portalauthenticator.h b/GPClient/portalauthenticator.h new file mode 100644 index 0000000..aaf8b39 --- /dev/null +++ b/GPClient/portalauthenticator.h @@ -0,0 +1,52 @@ +#ifndef PORTALAUTHENTICATOR_H +#define PORTALAUTHENTICATOR_H + +#include "portalconfigresponse.h" +#include "normalloginwindow.h" +#include "samlloginwindow.h" +#include "preloginresponse.h" + +#include + +class PortalAuthenticator : public QObject +{ + Q_OBJECT +public: + explicit PortalAuthenticator(const QString& portal); + ~PortalAuthenticator(); + + void authenticate(); + +signals: + void success(const PortalConfigResponse&, const GPGateway&); + void fail(const QString& msg); + void preloginFailed(const QString& msg); + +private slots: + void onPreloginFinished(); + void onPerformNormalLogin(const QString &username, const QString &password); + void onLoginWindowRejected(); + void onSAMLLoginSuccess(const QMap &samlResult); + void onFetchConfigFinished(); + +private: + QString portal; + QString preloginUrl; + QString configUrl; + QString username; + QString password; + + PreloginResponse preloginResponse; + + bool isAutoLogin { false }; + + NormalLoginWindow *normalLoginWindow{ nullptr }; + + void tryAutoLogin(); + void normalAuth(); + void samlAuth(); + void fetchConfig(QString username, QString password, QString preloginCookie = ""); + void emitFail(const QString& msg = ""); +}; + +#endif // PORTALAUTHENTICATOR_H diff --git a/GPClient/portalconfigresponse.cpp b/GPClient/portalconfigresponse.cpp new file mode 100644 index 0000000..6b57f1c --- /dev/null +++ b/GPClient/portalconfigresponse.cpp @@ -0,0 +1,145 @@ +#include "portalconfigresponse.h" + +#include +#include + +QString PortalConfigResponse::xmlUserAuthCookie = "portal-userauthcookie"; +QString PortalConfigResponse::xmlPrelogonUserAuthCookie = "portal-prelogonuserauthcookie"; +QString PortalConfigResponse::xmlGateways = "gateways"; + +PortalConfigResponse::PortalConfigResponse() +{ +} + +PortalConfigResponse PortalConfigResponse::parse(const QByteArray& xml) +{ + QXmlStreamReader xmlReader(xml); + PortalConfigResponse response; + response.setRawResponse(xml); + + while (!xmlReader.atEnd()) { + xmlReader.readNextStartElement(); + + QString name = xmlReader.name().toString(); + + if (name == xmlUserAuthCookie) { + response.setUserAuthCookie(xmlReader.readElementText()); + } else if (name == xmlPrelogonUserAuthCookie) { + response.setPrelogonUserAuthCookie(xmlReader.readElementText()); + } else if (name == xmlGateways) { + response.setGateways(parseGateways(xmlReader)); + } + } + + return response; +} + +const QByteArray& PortalConfigResponse::rawResponse() const +{ + return _rawResponse; +} + +QString PortalConfigResponse::username() const +{ + return _username; +} + +QString PortalConfigResponse::password() const +{ + return _password; +} + +QList PortalConfigResponse::parseGateways(QXmlStreamReader &xmlReader) +{ + QList gateways; + + while (xmlReader.name() != xmlGateways || !xmlReader.isEndElement()) { + xmlReader.readNext(); + // Parse the gateways -> external -> list -> entry + if (xmlReader.name() == "entry" && xmlReader.isStartElement()) { + GPGateway gateway; + QString address = xmlReader.attributes().value("name").toString(); + gateway.setAddress(address); + gateway.setPriorityRules(parsePriorityRules(xmlReader)); + gateway.setName(parseGatewayName(xmlReader)); + gateways.append(gateway); + } + } + return gateways; +} + +QMap PortalConfigResponse::parsePriorityRules(QXmlStreamReader &xmlReader) +{ + QMap priorityRules; + + while (xmlReader.name() != "priority-rule" || !xmlReader.isEndElement()) { + xmlReader.readNext(); + + if (xmlReader.name() == "entry" && xmlReader.isStartElement()) { + QString ruleName = xmlReader.attributes().value("name").toString(); + // Read the priority tag + xmlReader.readNextStartElement(); + int ruleValue = xmlReader.readElementText().toUInt(); + priorityRules.insert(ruleName, ruleValue); + } + } + return priorityRules; +} + +QString PortalConfigResponse::parseGatewayName(QXmlStreamReader &xmlReader) +{ + while (xmlReader.name() != "description" || !xmlReader.isEndElement()) { + xmlReader.readNext(); + if (xmlReader.name() == "description" && xmlReader.tokenType() == xmlReader.StartElement) { + return xmlReader.readElementText(); + } + } + + PLOGE << "Error: tag not found"; + return ""; +} + +QString PortalConfigResponse::userAuthCookie() const +{ + return _userAuthCookie; +} + +QString PortalConfigResponse::prelogonUserAuthCookie() const +{ + return _prelogonAuthCookie; +} + +QList& PortalConfigResponse::allGateways() +{ + return _gateways; +} + +void PortalConfigResponse::setRawResponse(const QByteArray &response) +{ + _rawResponse = response; +} + +void PortalConfigResponse::setUsername(const QString& username) +{ + _username = username; +} + +void PortalConfigResponse::setPassword(const QString& password) +{ + _password = password; +} + +void PortalConfigResponse::setUserAuthCookie(const QString &cookie) +{ + _userAuthCookie = cookie; +} + +void PortalConfigResponse::setPrelogonUserAuthCookie(const QString &cookie) +{ + _prelogonAuthCookie = cookie; +} + +void PortalConfigResponse::setGateways(const QList &gateways) +{ + _gateways = gateways; +} diff --git a/GPClient/portalconfigresponse.h b/GPClient/portalconfigresponse.h new file mode 100644 index 0000000..aa5b73b --- /dev/null +++ b/GPClient/portalconfigresponse.h @@ -0,0 +1,50 @@ +#ifndef PORTALCONFIGRESPONSE_H +#define PORTALCONFIGRESPONSE_H + +#include "gpgateway.h" + +#include +#include +#include + +class PortalConfigResponse +{ +public: + PortalConfigResponse(); + + static PortalConfigResponse parse(const QByteArray& xml); + + const QByteArray& rawResponse() const; + QString username() const; + QString password() const; + QString userAuthCookie() const; + QString prelogonUserAuthCookie() const; + QList& allGateways(); + + void setUsername(const QString& username); + void setPassword(const QString& password); + +private: + static QString xmlUserAuthCookie; + static QString xmlPrelogonUserAuthCookie; + static QString xmlGateways; + + QByteArray _rawResponse; + QString _username; + QString _password; + QString _userAuthCookie; + QString _prelogonAuthCookie; + + QList _gateways; + + void setRawResponse(const QByteArray& response); + void setUserAuthCookie(const QString& cookie); + void setPrelogonUserAuthCookie(const QString& cookie); + void setGateways(const QList& gateways); + + static QList parseGateways(QXmlStreamReader &xmlReader); + static QMap parsePriorityRules(QXmlStreamReader &xmlReader); + static QString parseGatewayName(QXmlStreamReader &xmlReader); +}; + +#endif // PORTALCONFIGRESPONSE_H diff --git a/GPClient/preloginresponse.cpp b/GPClient/preloginresponse.cpp new file mode 100644 index 0000000..e15cd8c --- /dev/null +++ b/GPClient/preloginresponse.cpp @@ -0,0 +1,97 @@ +#include "preloginresponse.h" + +#include +#include + +QString PreloginResponse::xmlAuthMessage = "authentication-message"; +QString PreloginResponse::xmlLabelUsername = "username-label"; +QString PreloginResponse::xmlLabelPassword = "password-label"; +QString PreloginResponse::xmlSamlMethod = "saml-auth-method"; +QString PreloginResponse::xmlSamlRequest = "saml-request"; +QString PreloginResponse::xmlRegion = "region"; + +PreloginResponse::PreloginResponse() +{ + add(xmlAuthMessage, ""); + add(xmlLabelUsername, ""); + add(xmlLabelPassword, ""); + add(xmlSamlMethod, ""); + add(xmlSamlRequest, ""); + add(xmlRegion, ""); +} + +PreloginResponse PreloginResponse::parse(const QByteArray& xml) +{ + QXmlStreamReader xmlReader(xml); + PreloginResponse response; + response.setRawResponse(xml); + + while (!xmlReader.atEnd()) { + xmlReader.readNextStartElement(); + QString name = xmlReader.name().toString(); + if (response.has(name)) { + response.add(name, xmlReader.readElementText()); + } + } + return response; +} + +const QByteArray& PreloginResponse::rawResponse() const +{ + return _rawResponse; +} + +QString PreloginResponse::authMessage() const +{ + return resultMap.value(xmlAuthMessage); +} + +QString PreloginResponse::labelUsername() const +{ + return resultMap.value(xmlLabelUsername); +} + +QString PreloginResponse::labelPassword() const +{ + return resultMap.value(xmlLabelPassword); +} + +QString PreloginResponse::samlMethod() const +{ + return resultMap.value(xmlSamlMethod); +} + +QString PreloginResponse::samlRequest() const +{ + return QByteArray::fromBase64(resultMap.value(xmlSamlRequest).toUtf8()); +} + +QString PreloginResponse::region() const +{ + return resultMap.value(xmlRegion); +} + +bool PreloginResponse::hasSamlAuthFields() const +{ + return !samlMethod().isEmpty() && !samlRequest().isEmpty(); +} + +bool PreloginResponse::hasNormalAuthFields() const +{ + return !labelUsername().isEmpty() && !labelPassword().isEmpty(); +} + +void PreloginResponse::setRawResponse(const QByteArray &response) +{ + _rawResponse = response; +} + +bool PreloginResponse::has(const QString &name) const +{ + return resultMap.contains(name); +} + +void PreloginResponse::add(const QString &name, const QString &value) +{ + resultMap.insert(name, value); +} diff --git a/GPClient/preloginresponse.h b/GPClient/preloginresponse.h new file mode 100644 index 0000000..784dfc7 --- /dev/null +++ b/GPClient/preloginresponse.h @@ -0,0 +1,41 @@ +#ifndef PRELOGINRESPONSE_H +#define PRELOGINRESPONSE_H + +#include +#include + +class PreloginResponse +{ +public: + PreloginResponse(); + + static PreloginResponse parse(const QByteArray& xml); + + const QByteArray& rawResponse() const; + QString authMessage() const; + QString labelUsername() const; + QString labelPassword() const; + QString samlMethod() const; + QString samlRequest() const; + QString region() const; + + bool hasSamlAuthFields() const; + bool hasNormalAuthFields() const; + +private: + static QString xmlAuthMessage; + static QString xmlLabelUsername; + static QString xmlLabelPassword; + static QString xmlSamlMethod; + static QString xmlSamlRequest; + static QString xmlRegion; + + QMap resultMap; + QByteArray _rawResponse; + + void setRawResponse(const QByteArray &response); + void add(const QString &name, const QString &value); + bool has(const QString &name) const; +}; + +#endif // PRELOGINRESPONSE_H diff --git a/GPClient/samlloginwindow.cpp b/GPClient/samlloginwindow.cpp index 60b299a..7a16c9d 100644 --- a/GPClient/samlloginwindow.cpp +++ b/GPClient/samlloginwindow.cpp @@ -1,19 +1,24 @@ #include "samlloginwindow.h" #include +#include +#include SAMLLoginWindow::SAMLLoginWindow(QWidget *parent) : QDialog(parent) { - setWindowTitle("SAML Login"); - resize(610, 406); + setWindowTitle("GlobalProtect SAML Login"); + resize(700, 550); + QVBoxLayout *verticalLayout = new QVBoxLayout(this); webView = new EnhancedWebView(this); webView->setUrl(QUrl("about:blank")); + // webView->page()->profile()->setPersistentCookiesPolicy(QWebEngineProfile::NoPersistentCookies); verticalLayout->addWidget(webView); webView->initialize(); - QObject::connect(webView, &EnhancedWebView::responseReceived, this, &SAMLLoginWindow::onResponseReceived); + connect(webView, &EnhancedWebView::responseReceived, this, &SAMLLoginWindow::onResponseReceived); + connect(webView, &EnhancedWebView::loadFinished, this, &SAMLLoginWindow::onLoadFinished); } SAMLLoginWindow::~SAMLLoginWindow() @@ -29,7 +34,7 @@ void SAMLLoginWindow::closeEvent(QCloseEvent *event) void SAMLLoginWindow::login(QString url, QString html) { - if (html == "") { + if (html.isEmpty()) { webView->load(QUrl(url)); } else { webView->setHtml(html, url); @@ -47,17 +52,24 @@ void SAMLLoginWindow::onResponseReceived(QJsonObject params) QJsonObject response = params.value("response").toObject(); QJsonObject headers = response.value("headers").toObject(); - foreach (const QString& key, headers.keys()) { - if (key.startsWith("saml-") || key == "prelogin-cookie" || key == "portal-userauthcookie") { - samlResult.insert(key, headers.value(key)); - } - } + const QString username = headers.value("saml-username").toString(); + const QString preloginCookie = headers.value("prelogin-cookie").toString(); - // Check the SAML result - if (samlResult.contains("saml-username") - && (samlResult.contains("prelogin-cookie") || samlResult.contains("portal-userauthcookie"))) { - samlResult.insert("server", QUrl(response.value("url").toString()).authority()); - emit success(samlResult); - accept(); + if (!username.isEmpty() && !preloginCookie.isEmpty()) { + samlResult.insert("username", username); + samlResult.insert("preloginCookie", preloginCookie); + } +} + +void SAMLLoginWindow::onLoadFinished() +{ + LOGI << "Load finished " << this->webView->page()->url().toString(); + + // Check the SAML result + if (!samlResult.value("username").isEmpty() && !samlResult.value("preloginCookie").isEmpty()) { + emit success(samlResult); + accept(); + } else { + open(); } } diff --git a/GPClient/samlloginwindow.h b/GPClient/samlloginwindow.h index 6279e6f..ec36b12 100644 --- a/GPClient/samlloginwindow.h +++ b/GPClient/samlloginwindow.h @@ -4,7 +4,7 @@ #include "enhancedwebview.h" #include -#include +#include #include class SAMLLoginWindow : public QDialog @@ -18,14 +18,15 @@ public: void login(QString url, QString html = ""); signals: - void success(QJsonObject samlResult); + void success(QMap samlResult); private slots: void onResponseReceived(QJsonObject params); + void onLoadFinished(); private: EnhancedWebView *webView; - QJsonObject samlResult; + QMap samlResult; void closeEvent(QCloseEvent *event); }; diff --git a/GPService/gpservice.cpp b/GPService/gpservice.cpp index 8a38fe8..5426d59 100644 --- a/GPService/gpservice.cpp +++ b/GPService/gpservice.cpp @@ -66,13 +66,10 @@ void GPService::connect(QString server, QString username, QString passwd) args << QCoreApplication::arguments().mid(1) << "--protocol=gp" << "-u" << username - << "--passwd-on-stdin" - << "--timestamp" + << "-C" << passwd << server; openconnect->start(bin, args); - openconnect->write(passwd.toUtf8()); - openconnect->closeWriteChannel(); } void GPService::disconnect() @@ -130,6 +127,5 @@ void GPService::onProcessFinished(int exitCode, QProcess::ExitStatus exitStatus) void GPService::log(QString msg) { - qInfo() << msg; emit logAvailable(msg); } diff --git a/GPService/systemd/gpservice.service b/GPService/systemd/gpservice.service index bb50baf..762d008 100644 --- a/GPService/systemd/gpservice.service +++ b/GPService/systemd/gpservice.service @@ -2,6 +2,7 @@ Description=GlobalProtect openconnect DBus service [Service] +Environment=LC_ALL=en_US Type=dbus BusName=com.yuezk.qt.GPService ExecStart=/usr/bin/gpservice diff --git a/README.md b/README.md index fb6be1a..3d6a6dc 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,12 @@ A GlobalProtect VPN client (GUI) for Linux based on Openconnect and built with Q

+## Features + +- Supports both SAML and non-SAML authentication modes. +- Supports automatically select the preferred gateway from the multiple gateways. +- Similar user experience as the offical client in macOS. + ## Prerequisites - Openconnect v8.x diff --git a/plog b/plog new file mode 160000 index 0000000..fda4a26 --- /dev/null +++ b/plog @@ -0,0 +1 @@ +Subproject commit fda4a26c26b2d1b2beb68d7b92b56950ec2b8ad2