Code refactor, support multiple gateways and non-SAML authentication (#9)

* Code refactor

* Update README.md
This commit is contained in:
Kevin Yue 2020-05-23 15:51:10 +08:00 committed by GitHub
parent 76a4977e92
commit 7f5bf0ce52
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 1665 additions and 192 deletions

4
.gitmodules vendored
View File

@ -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

View File

@ -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

View File

@ -1,5 +1,6 @@
#include "cdpcommandmanager.h"
#include <QVariantMap>
#include <plog/Log.h>
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;
}

View File

@ -0,0 +1,160 @@
#include "gatewayauthenticator.h"
#include "gphelper.h"
#include "loginparams.h"
#include "preloginresponse.h"
#include <QNetworkReply>
#include <plog/Log.h>
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 &params)
{
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<QNetworkReply*>(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<QNetworkReply*>(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<QString, QString> &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);
}

View File

@ -0,0 +1,44 @@
#ifndef GATEWAYAUTHENTICATOR_H
#define GATEWAYAUTHENTICATOR_H
#include "portalconfigresponse.h"
#include "normalloginwindow.h"
#include "loginparams.h"
#include <QObject>
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<QString, QString> &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

View File

@ -1,224 +1,262 @@
#include "gpclient.h"
#include "gphelper.h"
#include "ui_gpclient.h"
#include "samlloginwindow.h"
#include "portalauthenticator.h"
#include "gatewayauthenticator.h"
#include <QDesktopWidget>
#include <QGraphicsScene>
#include <QGraphicsView>
#include <QGraphicsPixmapItem>
#include <QImage>
#include <QStyle>
#include <QMessageBox>
#include <plog/Log.h>
#include <QIcon>
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;
doConnect();
}
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 (<saml-auth-method> or <saml-request> tags missing)");
emit connectFailed();
return;
}
if (samlMethod == "POST") {
samlLogin(reply->url().toString(), samlRequest);
} else if (samlMethod == "REDIRECT") {
samlLogin(samlRequest);
}
}
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"};
for (int i = 0; i < cookies->length(); i++) {
cookieValue = loginResult.value(cookies[i]).toString();
if (cookieValue != nullptr) {
cookieName = cookies[i];
break;
}
}
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");
}
void GPClient::updateConnectionStatus(QString status)
{
if (status == "not_connected") {
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);
} else if (status == "pending") {
ui->portalInput->setReadOnly(false);
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->setText("Cancel");
ui->connectButton->setDisabled(false);
} else if (status == "connected") {
ui->connectButton->setDisabled(true);
ui->portalInput->setReadOnly(true);
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);
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;
screenWidth = desktop->width();
screenHeight = desktop->height();
windowSize = size();
width = windowSize.width();
height = windowSize.height();
x = (screenWidth - width) / 2;
y = (screenHeight - height) / 2;
y -= 50;
move(x, y);
if (portal.isEmpty()) {
activiate();
return;
}
void GPClient::doAuth(const QString portal)
if (btnText.endsWith("Connect")) {
settings::save("portal", portal);
ui->statusLabel->setText("Authenticating...");
updateConnectionStatus(VpnStatus::pending);
// Perform the portal login
portalLogin(portal);
} else {
ui->statusLabel->setText("Disconnecting...");
updateConnectionStatus(VpnStatus::pending);
vpn->disconnect();
}
}
// 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);
}

View File

@ -2,9 +2,11 @@
#define GPCLIENT_H
#include "gpservice_interface.h"
#include "portalconfigresponse.h"
#include <QMainWindow>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QSystemTrayIcon>
#include <QMenu>
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

View File

@ -11,7 +11,7 @@
</rect>
</property>
<property name="windowTitle">
<string>GP VPN Client</string>
<string>GlobalProtect OpenConnect</string>
</property>
<property name="windowIcon">
<iconset resource="resources.qrc">
@ -113,6 +113,12 @@
<property name="text">
<string>Connect</string>
</property>
<property name="autoDefault">
<bool>true</bool>
</property>
<property name="default">
<bool>false</bool>
</property>
</widget>
</item>
</layout>

38
GPClient/gpgateway.cpp Normal file
View File

@ -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<QString, int> &priorityRules)
{
_priorityRules = priorityRules;
}
int GPGateway::priorityOf(QString ruleName)
{
if (_priorityRules.contains(ruleName)) {
return _priorityRules.value(ruleName);
}
return 0;
}

26
GPClient/gpgateway.h Normal file
View File

@ -0,0 +1,26 @@
#ifndef GPGATEWAY_H
#define GPGATEWAY_H
#include <QString>
#include <QMap>
class GPGateway
{
public:
GPGateway();
QString name() const;
QString address() const;
void setName(const QString &name);
void setAddress(const QString &address);
void setPriorityRules(const QMap<QString, int> &priorityRules);
int priorityOf(QString ruleName);
private:
QString _name;
QString _address;
QMap<QString, int> _priorityRules;
};
#endif // GPGATEWAY_H

118
GPClient/gphelper.cpp Normal file
View File

@ -0,0 +1,118 @@
#include "gphelper.h"
#include <QNetworkRequest>
#include <QXmlStreamReader>
#include <QMessageBox>
#include <QDesktopWidget>
#include <QApplication>
#include <QWidget>
#include <plog/Log.h>
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<GPGateway> &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<QString> 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);
}

43
GPClient/gphelper.h Normal file
View File

@ -0,0 +1,43 @@
#ifndef GPHELPER_H
#define GPHELPER_H
#include "samlloginwindow.h"
#include "gpgateway.h"
#include <QObject>
#include <QNetworkAccessManager>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QUrlQuery>
#include <QSettings>
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<GPGateway> &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

54
GPClient/loginparams.cpp Normal file
View File

@ -0,0 +1,54 @@
#include "loginparams.h"
#include <QUrlQuery>
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));
}

44
GPClient/loginparams.h Normal file
View File

@ -0,0 +1,44 @@
#ifndef LOGINPARAMS_H
#define LOGINPARAMS_H
#include <QUrlQuery>
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

View File

@ -2,17 +2,32 @@
#include "gpclient.h"
#include "enhancedwebview.h"
#include <QStandardPaths>
#include <plog/Log.h>
#include <plog/Appenders/ColorConsoleAppender.h>
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<plog::TxtFormatter> 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();
}

View File

@ -0,0 +1,62 @@
#include "normalloginwindow.h"
#include "ui_normalloginwindow.h"
#include <QCloseEvent>
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();
}

View File

@ -0,0 +1,37 @@
#ifndef PORTALAUTHWINDOW_H
#define PORTALAUTHWINDOW_H
#include <QDialog>
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

View File

@ -0,0 +1,148 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>NormalLoginWindow</class>
<widget class="QDialog" name="NormalLoginWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>255</width>
<height>269</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="cursor">
<cursorShape>ArrowCursor</cursorShape>
</property>
<property name="windowTitle">
<string>Login</string>
</property>
<property name="modal">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout_5">
<item>
<layout class="QVBoxLayout" name="verticalLayout_4" stretch="1,0,0">
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label">
<property name="font">
<font>
<pointsize>20</pointsize>
</font>
</property>
<property name="text">
<string>Login</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="authMessage">
<property name="enabled">
<bool>true</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>2</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Please enter the login credentials</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>6</number>
</property>
<item>
<widget class="QLabel" name="portalLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Portal:</string>
</property>
<property name="margin">
<number>0</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="portalAddress">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>vpn.example.com</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QLineEdit" name="username">
<property name="placeholderText">
<string>Username</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="password">
<property name="text">
<string/>
</property>
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
<property name="placeholderText">
<string>Password</string>
</property>
<property name="clearButtonEnabled">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="loginButton">
<property name="text">
<string>Login</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -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 <plog/Log.h>
#include <QNetworkReply>
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<QNetworkReply*>(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<QString, QString> &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<QNetworkReply*>(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);
}

View File

@ -0,0 +1,52 @@
#ifndef PORTALAUTHENTICATOR_H
#define PORTALAUTHENTICATOR_H
#include "portalconfigresponse.h"
#include "normalloginwindow.h"
#include "samlloginwindow.h"
#include "preloginresponse.h"
#include <QObject>
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<QString, QString> &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

View File

@ -0,0 +1,145 @@
#include "portalconfigresponse.h"
#include <QXmlStreamReader>
#include <plog/Log.h>
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<GPGateway> PortalConfigResponse::parseGateways(QXmlStreamReader &xmlReader)
{
QList<GPGateway> 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<QString, int> PortalConfigResponse::parsePriorityRules(QXmlStreamReader &xmlReader)
{
QMap<QString, int> 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: <description> tag not found";
return "";
}
QString PortalConfigResponse::userAuthCookie() const
{
return _userAuthCookie;
}
QString PortalConfigResponse::prelogonUserAuthCookie() const
{
return _prelogonAuthCookie;
}
QList<GPGateway>& 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<GPGateway> &gateways)
{
_gateways = gateways;
}

View File

@ -0,0 +1,50 @@
#ifndef PORTALCONFIGRESPONSE_H
#define PORTALCONFIGRESPONSE_H
#include "gpgateway.h"
#include <QString>
#include <QList>
#include <QXmlStreamReader>
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<GPGateway>& 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<GPGateway> _gateways;
void setRawResponse(const QByteArray& response);
void setUserAuthCookie(const QString& cookie);
void setPrelogonUserAuthCookie(const QString& cookie);
void setGateways(const QList<GPGateway>& gateways);
static QList<GPGateway> parseGateways(QXmlStreamReader &xmlReader);
static QMap<QString, int> parsePriorityRules(QXmlStreamReader &xmlReader);
static QString parseGatewayName(QXmlStreamReader &xmlReader);
};
#endif // PORTALCONFIGRESPONSE_H

View File

@ -0,0 +1,97 @@
#include "preloginresponse.h"
#include <QXmlStreamReader>
#include <QMap>
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);
}

View File

@ -0,0 +1,41 @@
#ifndef PRELOGINRESPONSE_H
#define PRELOGINRESPONSE_H
#include <QString>
#include <QMap>
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<QString, QString> 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

View File

@ -1,19 +1,24 @@
#include "samlloginwindow.h"
#include <QVBoxLayout>
#include <plog/Log.h>
#include <QWebEngineProfile>
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();
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.contains("saml-username")
&& (samlResult.contains("prelogin-cookie") || samlResult.contains("portal-userauthcookie"))) {
samlResult.insert("server", QUrl(response.value("url").toString()).authority());
if (!samlResult.value("username").isEmpty() && !samlResult.value("preloginCookie").isEmpty()) {
emit success(samlResult);
accept();
} else {
open();
}
}

View File

@ -4,7 +4,7 @@
#include "enhancedwebview.h"
#include <QDialog>
#include <QJsonObject>
#include <QMap>
#include <QCloseEvent>
class SAMLLoginWindow : public QDialog
@ -18,14 +18,15 @@ public:
void login(QString url, QString html = "");
signals:
void success(QJsonObject samlResult);
void success(QMap<QString, QString> samlResult);
private slots:
void onResponseReceived(QJsonObject params);
void onLoadFinished();
private:
EnhancedWebView *webView;
QJsonObject samlResult;
QMap<QString, QString> samlResult;
void closeEvent(QCloseEvent *event);
};

View File

@ -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);
}

View File

@ -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

View File

@ -5,6 +5,12 @@ A GlobalProtect VPN client (GUI) for Linux based on Openconnect and built with Q
<img src="screenshot.png">
</p>
## 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

1
plog Submodule

@ -0,0 +1 @@
Subproject commit fda4a26c26b2d1b2beb68d7b92b56950ec2b8ad2