Compare commits
	
		
			256 Commits
		
	
	
		
			v0.0.3
			...
			c4fa91f6ea
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | c4fa91f6ea | ||
|  | 69502b22a9 | ||
|  | 356946e635 | ||
|  | 3674a28dee | ||
|  | 3098d1170f | ||
|  | acf7ef28ab | ||
|  | 11a374765c | ||
|  | f6ceb5ac0a | ||
|  | 94a2cd2886 | ||
|  | 867ea71144 | ||
|  | c9484248a5 | ||
|  | a10539e9c3 | ||
|  | 601f422863 | ||
|  | bf96a88e21 | ||
|  | 963b7d5407 | ||
|  | f91f0bcd17 | ||
|  | 5603157679 | ||
|  | 4191e9a19f | ||
|  | 5bc3da99ac | ||
|  | e8db014336 | ||
|  | e5fd2ba280 | ||
|  | 15e798c1e7 | ||
|  | 1af21432d4 | ||
|  | c07e232ec2 | ||
|  | d975f981cc | ||
|  | c74ce52c2d | ||
|  | da3dc10569 | ||
|  | 75db9e162f | ||
|  | 89eb42ceac | ||
|  | 54d3bb8a92 | ||
|  | a1b49fde47 | ||
|  | f42f0d248e | ||
|  | ec0bff1e36 | ||
|  | 77fda0f527 | ||
|  | 8de183a53d | ||
|  | 462428f99a | ||
|  | cbd8b0f144 | ||
|  | 5a84c99cd9 | ||
|  | d5af0e58c2 | ||
|  | 16696e3840 | ||
|  | 6df9877895 | ||
|  | 19b9b757f4 | ||
|  | 7bef2ccc68 | ||
|  | bffc5d733b | ||
|  | 8ca2610550 | ||
|  | acf184134a | ||
|  | 4a3f74f1c3 | ||
|  | b39983a0f8 | ||
|  | d6fa32d95d | ||
|  | 7c299f6e68 | ||
|  | 25e8ccd07e | ||
|  | 092123b075 | ||
|  | feb2956cc1 | ||
|  | d356839859 | ||
|  | 2ff39fd14e | ||
|  | c3d300c807 | ||
|  | ef43d10a70 | ||
|  | bd73466e48 | ||
|  | cc2c0ae34e | ||
|  | 9207f7a798 | ||
|  | 2069b7fd8e | ||
|  | f552ef6204 | ||
|  | 2761f7521a | ||
|  | c3939a774b | ||
|  | 49e5242bf2 | ||
|  | 3181d37b20 | ||
|  | 6d788a5e91 | ||
|  | 74c7549444 | ||
|  | c52ccb87f1 | ||
|  | fab25848e1 | ||
|  | 75a24c89cd | ||
|  | 15a73b7dba | ||
|  | 0adeaf9c28 | ||
|  | fe64b2cd19 | ||
|  | 5788474d7e | ||
|  | 3559834762 | ||
|  | f9926b4026 | ||
|  | cb457c4b09 | ||
|  | 5ebfe9b0f4 | ||
|  | 35266dd8bf | ||
|  | bf03d375e0 | ||
|  | 6cf909e34f | ||
|  | 343a6d03c1 | ||
|  | fab8e7591e | ||
|  | 5a485197b7 | ||
|  | 7bc02a4208 | ||
|  | 3067e6e911 | ||
|  | 5db77e8404 | ||
|  | 5714063457 | ||
|  | 41f88ed2e0 | ||
|  | 4fada9bd14 | ||
|  | b57fb993ca | ||
|  | f6d06ed978 | ||
|  | cc67de3a2b | ||
|  | e2d28c83b2 | ||
|  | a489c5881b | ||
|  | 44fd2f1d3f | ||
|  | 9c9b42b87f | ||
|  | fb2b148b72 | ||
|  | 64bec9660a | ||
|  | 0619e91bf5 | ||
|  | 048aa4799f | ||
|  | db0e8b801d | ||
|  | d03bbc339e | ||
|  | 1312d54d08 | ||
|  | 39f99d9143 | ||
|  | 7a4eb0def3 | ||
|  | d9b2094edd | ||
|  | e6118af9f3 | ||
|  | 108b4be3ec | ||
|  | 65c59e47ec | ||
|  | 177da7f3a2 | ||
|  | d5cd90373b | ||
|  | ffa99d3783 | ||
|  | 4940830885 | ||
|  | ad178fe56c | ||
|  | 829298bb84 | ||
|  | 8fe717d844 | ||
|  | dffbc64ef5 | ||
|  | b99c5a8391 | ||
|  | c2f7576d10 | ||
|  | 4327235093 | ||
|  | 0699878b92 | ||
|  | e3aba11506 | ||
|  | ff58258d5c | ||
|  | 991cf25a7b | ||
|  | 02c70150ba | ||
|  | 28d8321958 | ||
|  | e1c9180cae | ||
|  | 57df34fd1e | ||
|  | 04d180e11a | ||
|  | 6d3b127569 | ||
|  | e72b25e415 | ||
|  | 37a511c24d | ||
|  | ad7db36c92 | ||
|  | 11dc5920ef | ||
|  | e6383916c7 | ||
|  | 1d9d928b26 | ||
|  | c02ad5d46d | ||
|  | 2319c7c49c | ||
|  | e0c2c14dc3 | ||
|  | 8f27c92e7b | ||
|  | 9d6ec84c14 | ||
|  | dd81ed9519 | ||
|  | 32bd713965 | ||
|  | ba92517141 | ||
|  | 0e4e082594 | ||
|  | 3e590cab7b | ||
|  | 3e0e4cff12 | ||
|  | 692df2f2c5 | ||
|  | f2b9ffddde | ||
|  | ca38925066 | ||
|  | 8591dd7e81 | ||
|  | b07880930e | ||
|  | fceb80e10e | ||
|  | d802c56d8f | ||
|  | 386f08d0e8 | ||
|  | 9e7fb17bd3 | ||
|  | 36d9753008 | ||
|  | e5b3df9cda | ||
|  | 0dd705d0c0 | ||
|  | ce2360be61 | ||
|  | b5b7033eee | ||
|  | 9e7db4eb86 | ||
|  | bc07e3d496 | ||
|  | 452fe2f189 | ||
|  | 8a65099ca7 | ||
|  | 5c97b2df7a | ||
|  | 0d4485d754 | ||
|  | 98e641e99d | ||
|  | 6fa77cdbd2 | ||
|  | 64e6487e7e | ||
|  | e8b2c1606f | ||
|  | 84f1480653 | ||
|  | 3175855122 | ||
|  | fa8b5c1528 | ||
|  | 7b9942c7e6 | ||
|  | 011a1a0dec | ||
|  | 4a53033023 | ||
|  | 9c6ea1c4b5 | ||
|  | 3369ad4c1d | ||
|  | 25c9f2291a | ||
|  | bba3bc7e4f | ||
|  | b12b692090 | ||
|  | 1300a0cc43 | ||
|  | 165080b476 | ||
|  | d6af8a1598 | ||
|  | eef92b1d31 | ||
|  | 946ead24a4 | ||
|  | 39e57c8598 | ||
|  | 4e2e423c27 | ||
|  | 732a62f1ee | ||
|  | 9f9444a72b | ||
|  | 6352e1fb2b | ||
|  | 42cae3ff26 | ||
|  | 53c8572cf6 | ||
|  | 3f6467321f | ||
|  | 563ec48c8c | ||
|  | 3787ae164c | ||
|  | 04a24c34e8 | ||
|  | fe68248b1f | ||
|  | 47013033ec | ||
|  | 05fb9a26bd | ||
|  | 96962f957c | ||
|  | b4f9cfae67 | ||
|  | c8942984a8 | ||
|  | 3907827d0e | ||
|  | f089996cdc | ||
|  | 260b557238 | ||
|  | 3495dbfe18 | ||
|  | cdf193024c | ||
|  | 76de070d78 | ||
|  | 420ae27888 | ||
|  | 6a347746cc | ||
|  | 624babb380 | ||
|  | 511b20fdcd | ||
|  | abe33c7407 | ||
|  | 99a82c8641 | ||
|  | e5d0acad3c | ||
|  | 38a1eded19 | ||
|  | 3e23e7eaae | ||
|  | cf46848e63 | ||
|  | 2e826201d2 | ||
|  | adba408dc3 | ||
|  | 5d613369ee | ||
|  | ebd3de6f63 | ||
|  | 266ab65892 | ||
|  | ccaf93ec31 | ||
|  | e08d7d7c4d | ||
|  | c14a6ad1d2 | ||
|  | d91fad089f | ||
|  | 2c1036ff10 | ||
|  | d5f9283b93 | ||
|  | fe7b96ce9b | ||
|  | 790865c060 | ||
|  | 7f056c98ce | ||
|  | 70816a9600 | ||
|  | 337a94efcd | ||
|  | cf34f9f70f | ||
|  | 3a790cdc63 | ||
|  | 73925fd1e2 | ||
|  | e12613d9a4 | ||
|  | 86ad51b0ad | ||
|  | 1e2322b938 | ||
|  | 4313b9d0e7 | ||
|  | 4fa08c7153 | ||
|  | 599ff3668f | ||
|  | e22bb8e1b7 | ||
|  | 7f5bf0ce52 | ||
|  | 76a4977e92 | ||
|  | 246ef6d9ed | ||
|  | 0ccb1371ab | ||
|  | 81d4f9836f | ||
|  | cf32e44366 | ||
|  | bdad3ffe4d | ||
|  | cc59f031b0 | 
							
								
								
									
										21
									
								
								.editorconfig
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,21 @@ | ||||
| root = true | ||||
|  | ||||
| [*] | ||||
| charset = utf-8 | ||||
| indent_style = space | ||||
| indent_size = 2 | ||||
| end_of_line = lf | ||||
| insert_final_newline = true | ||||
| trim_trailing_whitespace = true | ||||
|  | ||||
| [*.{rs,toml}] | ||||
| indent_size = 4 | ||||
|  | ||||
| [*.{c,h}] | ||||
| indent_size = 4 | ||||
|  | ||||
| [*.{js,jsx,ts,tsx}] | ||||
| indent_size = 2 | ||||
|  | ||||
| [*.md] | ||||
| trim_trailing_whitespace = false | ||||
							
								
								
									
										75
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,56 +1,35 @@ | ||||
| # Binaries | ||||
| gpclient | ||||
| gpservice | ||||
| # Created by https://www.toptal.com/developers/gitignore/api/rust,visualstudiocode | ||||
| # Edit at https://www.toptal.com/developers/gitignore?templates=rust,visualstudiocode | ||||
|  | ||||
| # C++ objects and libs | ||||
| *.slo | ||||
| *.lo | ||||
| *.o | ||||
| *.a | ||||
| *.la | ||||
| *.lai | ||||
| *.so | ||||
| *.so.* | ||||
| *.dll | ||||
| *.dylib | ||||
| ### Rust ### | ||||
| # Generated by Cargo | ||||
| # will have compiled files and executables | ||||
| debug/ | ||||
| target/ | ||||
|  | ||||
| # Qt-es | ||||
| object_script.*.Release | ||||
| object_script.*.Debug | ||||
| *_plugin_import.cpp | ||||
| /.qmake.cache | ||||
| /.qmake.stash | ||||
| *.pro.user | ||||
| *.pro.user.* | ||||
| *.qbs.user | ||||
| *.qbs.user.* | ||||
| *.moc | ||||
| moc_*.cpp | ||||
| moc_*.h | ||||
| qrc_*.cpp | ||||
| ui_*.h | ||||
| *.qmlc | ||||
| *.jsc | ||||
| Makefile* | ||||
| *build-* | ||||
| *.qm | ||||
| *.prl | ||||
| # These are backup files generated by rustfmt | ||||
| **/*.rs.bk | ||||
|  | ||||
| # Qt unit tests | ||||
| target_wrapper.* | ||||
| # MSVC Windows builds of rustc generate these, which store debugging information | ||||
| *.pdb | ||||
|  | ||||
| # QtCreator | ||||
| *.autosave | ||||
| ### VisualStudioCode ### | ||||
| .vscode/* | ||||
| !.vscode/settings.json | ||||
| !.vscode/tasks.json | ||||
| !.vscode/launch.json | ||||
| !.vscode/extensions.json | ||||
| !.vscode/*.code-snippets | ||||
|  | ||||
| # QtCreator Qml | ||||
| *.qmlproject.user | ||||
| *.qmlproject.user.* | ||||
| # Local History for Visual Studio Code | ||||
| .history/ | ||||
|  | ||||
| # QtCreator CMake | ||||
| CMakeLists.txt.user* | ||||
| # Built Visual Studio Code Extensions | ||||
| *.vsix | ||||
|  | ||||
| # QtCreator 4.8< compilation database  | ||||
| compile_commands.json | ||||
| ### VisualStudioCode Patch ### | ||||
| # Ignore all local history of files | ||||
| .history | ||||
| .ionide | ||||
|  | ||||
| # QtCreator local machine specific files for imported projects | ||||
| *creator.user* | ||||
| # End of https://www.toptal.com/developers/gitignore/api/rust,visualstudiocode | ||||
|   | ||||
							
								
								
									
										3
									
								
								.gitmodules
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,3 +0,0 @@ | ||||
| [submodule "singleapplication"] | ||||
| 	path = singleapplication | ||||
| 	url = https://github.com/itay-grudev/SingleApplication.git | ||||
							
								
								
									
										52
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,52 @@ | ||||
| { | ||||
|   "cSpell.words": [ | ||||
|     "authcookie", | ||||
|     "bindgen", | ||||
|     "clickaway", | ||||
|     "clientgpversion", | ||||
|     "clientos", | ||||
|     "configparser", | ||||
|     "consts", | ||||
|     "devicename", | ||||
|     "distro", | ||||
|     "gpcommon", | ||||
|     "gpconf", | ||||
|     "gpgui", | ||||
|     "gpservice", | ||||
|     "humantime", | ||||
|     "Immer", | ||||
|     "jnlp", | ||||
|     "lexopt", | ||||
|     "notistack", | ||||
|     "oneshot", | ||||
|     "openconnect", | ||||
|     "pkexec", | ||||
|     "prelogin", | ||||
|     "prelogon", | ||||
|     "prelogonuserauthcookie", | ||||
|     "repr", | ||||
|     "rustc", | ||||
|     "servercert", | ||||
|     "shlex", | ||||
|     "tauri", | ||||
|     "tempdir", | ||||
|     "tempfile", | ||||
|     "thiserror", | ||||
|     "unlisten", | ||||
|     "userauthcookie", | ||||
|     "vpnc", | ||||
|     "vpninfo" | ||||
|   ], | ||||
|   "files.associations": { | ||||
|     "*.css": "css", | ||||
|     "errno.h": "c", | ||||
|     "stdarg.h": "c", | ||||
|     "wrapper.h": "c", | ||||
|     "cstdlib": "c", | ||||
|     "stdio.h": "c", | ||||
|     "openconnect.h": "c", | ||||
|     "compare": "c", | ||||
|     "stdlib.h": "c", | ||||
|     "vpn.h": "c" | ||||
|   } | ||||
| } | ||||
							
								
								
									
										4739
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										12
									
								
								Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,12 @@ | ||||
| [workspace] | ||||
|  | ||||
| members = [ | ||||
|     "gpcommon", | ||||
|     "gpclient", | ||||
|     "gpservice", | ||||
|     "gpgui/src-tauri" | ||||
| ] | ||||
|  | ||||
| [profile.release] | ||||
| strip = true | ||||
| opt-level = "z" | ||||
| @@ -1,59 +0,0 @@ | ||||
| TARGET = gpclient | ||||
|  | ||||
| QT       += core gui network websockets dbus webenginewidgets | ||||
|  | ||||
| greaterThan(QT_MAJOR_VERSION, 4): QT += widgets | ||||
|  | ||||
| CONFIG += c++11 | ||||
|  | ||||
| include(../singleapplication/singleapplication.pri) | ||||
| DEFINES += QAPPLICATION_CLASS=QApplication | ||||
|  | ||||
| # The following define makes your compiler emit warnings if you use | ||||
| # any Qt feature that has been marked deprecated (the exact warnings | ||||
| # depend on your compiler). Please consult the documentation of the | ||||
| # deprecated API in order to know how to port your code away from it. | ||||
| DEFINES += QT_DEPRECATED_WARNINGS | ||||
|  | ||||
| # 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. | ||||
| #DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000    # disables all the APIs deprecated before Qt 6.0.0 | ||||
| SOURCES += \ | ||||
|     cdpcommand.cpp \ | ||||
|     cdpcommandmanager.cpp \ | ||||
|     enhancedwebview.cpp \ | ||||
|     main.cpp \ | ||||
|     samlloginwindow.cpp \ | ||||
|     gpclient.cpp | ||||
|  | ||||
| HEADERS += \ | ||||
|     cdpcommand.h \ | ||||
|     cdpcommandmanager.h \ | ||||
|     enhancedwebview.h \ | ||||
|     samlloginwindow.h \ | ||||
|     gpclient.h | ||||
|  | ||||
| FORMS += \ | ||||
|     gpclient.ui | ||||
|  | ||||
| DBUS_INTERFACES += ../GPService/gpservice.xml | ||||
|  | ||||
| # Default rules for deployment. | ||||
| target.path = /usr/bin | ||||
| INSTALLS += target | ||||
|  | ||||
| DISTFILES += \ | ||||
|     com.yuezk.qt.GPClient.svg \ | ||||
|     com.yuezk.qt.gpclient.desktop | ||||
|  | ||||
| desktop_entry.path = /usr/share/applications/ | ||||
| desktop_entry.files = com.yuezk.qt.gpclient.desktop | ||||
|  | ||||
| desktop_icon.path = /usr/share/pixmaps/ | ||||
| desktop_icon.files = com.yuezk.qt.GPClient.svg | ||||
|  | ||||
| INSTALLS += desktop_entry desktop_icon | ||||
|  | ||||
| RESOURCES += \ | ||||
|     resources.qrc | ||||
| @@ -1,30 +0,0 @@ | ||||
| #include "cdpcommand.h" | ||||
|  | ||||
| #include <QVariantMap> | ||||
| #include <QJsonDocument> | ||||
| #include <QJsonObject> | ||||
|  | ||||
| CDPCommand::CDPCommand(QObject *parent) : QObject(parent) | ||||
| { | ||||
| } | ||||
|  | ||||
| CDPCommand::CDPCommand(int id, QString cmd, QVariantMap& params) : | ||||
|     QObject(nullptr), | ||||
|     id(id), | ||||
|     cmd(cmd), | ||||
|     params(¶ms) | ||||
| { | ||||
| } | ||||
|  | ||||
| QByteArray CDPCommand::toJson() | ||||
| { | ||||
|     QVariantMap payloadMap; | ||||
|     payloadMap["id"] = id; | ||||
|     payloadMap["method"] = cmd; | ||||
|     payloadMap["params"] = *params; | ||||
|  | ||||
|     QJsonObject payloadJsonObject = QJsonObject::fromVariantMap(payloadMap); | ||||
|     QJsonDocument payloadJson(payloadJsonObject); | ||||
|  | ||||
|     return payloadJson.toJson(); | ||||
| } | ||||
| @@ -1,24 +0,0 @@ | ||||
| #ifndef CDPCOMMAND_H | ||||
| #define CDPCOMMAND_H | ||||
|  | ||||
| #include <QObject> | ||||
|  | ||||
| class CDPCommand : public QObject | ||||
| { | ||||
|     Q_OBJECT | ||||
| public: | ||||
|     explicit CDPCommand(QObject *parent = nullptr); | ||||
|     CDPCommand(int id, QString cmd, QVariantMap& params); | ||||
|  | ||||
|     QByteArray toJson(); | ||||
|  | ||||
| signals: | ||||
|     void finished(); | ||||
|  | ||||
| private: | ||||
|     int id; | ||||
|     QString cmd; | ||||
|     QVariantMap *params; | ||||
| }; | ||||
|  | ||||
| #endif // CDPCOMMAND_H | ||||
| @@ -1,85 +0,0 @@ | ||||
| #include "cdpcommandmanager.h" | ||||
| #include <QVariantMap> | ||||
|  | ||||
| CDPCommandManager::CDPCommandManager(QObject *parent) | ||||
|     : QObject(parent) | ||||
|     , networkManager(new QNetworkAccessManager) | ||||
|     , socket(new QWebSocket) | ||||
| { | ||||
|     // WebSocket setup | ||||
|     QObject::connect(socket, &QWebSocket::connected, this, &CDPCommandManager::ready); | ||||
|     QObject::connect(socket, &QWebSocket::textMessageReceived, this, &CDPCommandManager::onTextMessageReceived); | ||||
|     QObject::connect(socket, &QWebSocket::disconnected, this, &CDPCommandManager::onSocketDisconnected); | ||||
|     QObject::connect(socket, QOverload<QAbstractSocket::SocketError>::of(&QWebSocket::error), this, &CDPCommandManager::onSocketError); | ||||
| } | ||||
|  | ||||
| CDPCommandManager::~CDPCommandManager() | ||||
| { | ||||
|     delete networkManager; | ||||
|     delete socket; | ||||
| } | ||||
|  | ||||
| void CDPCommandManager::initialize(QString endpoint) | ||||
| { | ||||
|     QNetworkReply *reply = networkManager->get(QNetworkRequest(endpoint)); | ||||
|  | ||||
|     QObject::connect( | ||||
|         reply, &QNetworkReply::finished, | ||||
|         [reply, this]() { | ||||
|             if (reply->error()) { | ||||
|                 qDebug() << "CDP request error"; | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             QJsonDocument doc = QJsonDocument::fromJson(reply->readAll()); | ||||
|             QJsonArray pages = doc.array(); | ||||
|             QJsonObject page = pages.first().toObject(); | ||||
|             QString wsUrl = page.value("webSocketDebuggerUrl").toString(); | ||||
|  | ||||
|             socket->open(wsUrl); | ||||
|         } | ||||
|     ); | ||||
| } | ||||
|  | ||||
| CDPCommand *CDPCommandManager::sendCommand(QString cmd) | ||||
| { | ||||
|     QVariantMap emptyParams; | ||||
|     return sendCommend(cmd, emptyParams); | ||||
| } | ||||
|  | ||||
| CDPCommand *CDPCommandManager::sendCommend(QString cmd, QVariantMap ¶ms) | ||||
| { | ||||
|     int id = ++commandId; | ||||
|     CDPCommand *command = new CDPCommand(id, cmd, params); | ||||
|     socket->sendTextMessage(command->toJson()); | ||||
|     commandPool.insert(id, command); | ||||
|  | ||||
|     return command; | ||||
| } | ||||
|  | ||||
| void CDPCommandManager::onTextMessageReceived(QString message) | ||||
| { | ||||
|     QJsonDocument responseDoc = QJsonDocument::fromJson(message.toUtf8()); | ||||
|     QJsonObject response = responseDoc.object(); | ||||
|  | ||||
|     // Response for method | ||||
|     if (response.contains("id")) { | ||||
|         int id = response.value("id").toInt(); | ||||
|         if (commandPool.contains(id)) { | ||||
|             CDPCommand *command = commandPool.take(id); | ||||
|             command->finished(); | ||||
|         } | ||||
|     } else { // Response for event | ||||
|         emit eventReceived(response.value("method").toString(), response.value("params").toObject()); | ||||
|     } | ||||
| } | ||||
|  | ||||
| void CDPCommandManager::onSocketDisconnected() | ||||
| { | ||||
|     qDebug() << "WebSocket disconnected"; | ||||
| } | ||||
|  | ||||
| void CDPCommandManager::onSocketError(QAbstractSocket::SocketError error) | ||||
| { | ||||
|     qDebug() << "WebSocket error" << error; | ||||
| } | ||||
| @@ -1,39 +0,0 @@ | ||||
| #ifndef CDPCOMMANDMANAGER_H | ||||
| #define CDPCOMMANDMANAGER_H | ||||
|  | ||||
| #include "cdpcommand.h" | ||||
| #include <QObject> | ||||
| #include <QHash> | ||||
| #include <QtWebSockets> | ||||
| #include <QNetworkAccessManager> | ||||
|  | ||||
| class CDPCommandManager : public QObject | ||||
| { | ||||
|     Q_OBJECT | ||||
| public: | ||||
|     explicit CDPCommandManager(QObject *parent = nullptr); | ||||
|     ~CDPCommandManager(); | ||||
|  | ||||
|     void initialize(QString endpoint); | ||||
|  | ||||
|     CDPCommand *sendCommand(QString cmd); | ||||
|     CDPCommand *sendCommend(QString cmd, QVariantMap& params); | ||||
|  | ||||
| signals: | ||||
|     void ready(); | ||||
|     void eventReceived(QString eventName, QJsonObject params); | ||||
|  | ||||
| private: | ||||
|     QNetworkAccessManager *networkManager; | ||||
|     QWebSocket *socket; | ||||
|  | ||||
|     int commandId = 0; | ||||
|     QHash<int, CDPCommand*> commandPool; | ||||
|  | ||||
| private slots: | ||||
|     void onTextMessageReceived(QString message); | ||||
|     void onSocketDisconnected(); | ||||
|     void onSocketError(QAbstractSocket::SocketError error); | ||||
| }; | ||||
|  | ||||
| #endif // CDPCOMMANDMANAGER_H | ||||
| @@ -1,99 +0,0 @@ | ||||
| <?xml version="1.0" encoding="UTF-8" standalone="no"?> | ||||
| <!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  --> | ||||
|  | ||||
| <svg | ||||
|    xmlns:dc="http://purl.org/dc/elements/1.1/" | ||||
|    xmlns:cc="http://creativecommons.org/ns#" | ||||
|    xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" | ||||
|    xmlns:svg="http://www.w3.org/2000/svg" | ||||
|    xmlns="http://www.w3.org/2000/svg" | ||||
|    xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" | ||||
|    xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" | ||||
|    version="1.1" | ||||
|    id="Layer_1" | ||||
|    x="0px" | ||||
|    y="0px" | ||||
|    viewBox="0 0 96 96" | ||||
|    style="enable-background:new 0 0 96 96;" | ||||
|    xml:space="preserve" | ||||
|    sodipodi:docname="com.yuezk.qt.GPClient.svg" | ||||
|    inkscape:version="0.92.4 5da689c313, 2019-01-14"><metadata | ||||
|    id="metadata14"><rdf:RDF><cc:Work | ||||
|        rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type | ||||
|          rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><defs | ||||
|    id="defs12" /><sodipodi:namedview | ||||
|    pagecolor="#ffffff" | ||||
|    bordercolor="#666666" | ||||
|    borderopacity="1" | ||||
|    objecttolerance="10" | ||||
|    gridtolerance="10" | ||||
|    guidetolerance="10" | ||||
|    inkscape:pageopacity="0" | ||||
|    inkscape:pageshadow="2" | ||||
|    inkscape:window-width="1920" | ||||
|    inkscape:window-height="1006" | ||||
|    id="namedview10" | ||||
|    showgrid="false" | ||||
|    inkscape:zoom="6.9532168" | ||||
|    inkscape:cx="7.9545315" | ||||
|    inkscape:cy="59.062386" | ||||
|    inkscape:window-x="0" | ||||
|    inkscape:window-y="0" | ||||
|    inkscape:window-maximized="1" | ||||
|    inkscape:current-layer="g8499" /> | ||||
| <style | ||||
|    type="text/css" | ||||
|    id="style2"> | ||||
| 	.st0{fill:#2980B9;} | ||||
| 	.st1{fill:#3498DB;} | ||||
| 	.st2{fill:#2ECC71;} | ||||
| 	.st3{fill:#27AE60;} | ||||
| </style> | ||||
|  | ||||
| <g | ||||
|    id="g8499" | ||||
|    transform="matrix(1.3407388,0,0,1.3407388,-16.409202,-16.355463)"><g | ||||
|      id="XMLID_1_"> | ||||
| 	<circle | ||||
|    r="32.5" | ||||
|    cy="48" | ||||
|    cx="48" | ||||
|    class="st0" | ||||
|    id="XMLID_3_" | ||||
|    style="fill:#2980b9" /> | ||||
| 	<path | ||||
|    d="m 48,15.5 v 65 C 65.9,80.5 80.5,65.7 80.5,48 80.5,30 65.9,15.5 48,15.5 Z" | ||||
|    class="st1" | ||||
|    id="XMLID_4_" | ||||
|    inkscape:connector-curvature="0" | ||||
|    style="fill:#3498db" /> | ||||
| 	<path | ||||
|    d="m 48,15.5 v 0.6 l 1.2,-0.3 c 0.3,-0.3 0.4,-0.3 0.6,-0.3 h -1.1 z m 7.3,0.9 c -0.1,0 0.4,0.9 1.1,1.8 0.8,1.5 1.1,2.1 1.3,2.1 0.3,-0.3 1.9,-1.2 3,-2.1 -1.7,-0.9 -3.5,-1.5 -5.4,-1.8 z m 10.3,6.2 c -0.1,0 -0.4,0 -0.9,0.6 l -0.8,0.9 0.6,0.6 c 0.3,0.6 0.8,0.9 1,1.2 0.5,0.6 0.6,0.6 0.1,1.5 -0.2,0.6 -0.3,0.9 -0.3,0.9 0.1,0.3 0.3,0.3 1.4,0.3 h 1.6 c 0.1,0 0.3,-0.6 0.4,-1.2 l 0.1,-0.9 -1.1,-0.9 c -1,-0.9 -1,-0.9 -1.4,-1.8 -0.3,-0.6 -0.6,-1.2 -0.7,-1.2 z m -3,2.4 c -0.2,0 -1.3,2.1 -1.3,2.4 0,0 0.3,0.6 0.7,0.9 0.4,0.3 0.7,0.6 0.7,0.6 0.1,0 1.2,-1.2 1.4,-1.5 C 64.2,27.1 64,26.8 63.5,26.2 63.1,25.5 62.7,25 62.6,25 Z m 9.5,1.1 0.2,0.3 c 0,0.3 -0.7,0.9 -1.4,1.5 -1.2,0.9 -1.4,1.2 -2,1.2 -0.6,0 -0.9,0.3 -1.8,0.9 -0.6,0.6 -1.2,0.9 -1.2,1.2 0,0 0.2,0.3 0.6,0.9 0.7,0.6 0.7,0.9 0.2,1.8 l -0.4,0.3 h -1.1 c -0.6,0 -1.5,0 -1.8,-0.3 -0.9,0 -0.8,0 -0.1,2.1 1,3 1.1,3.2 1.3,3.2 0.1,0 1.3,-1.2 2.8,-2.4 1.5,-1.2 2.7,-2.4 2.8,-2.4 l 0.6,0.3 c 0.4,0.3 0.5,0 1.3,-0.6 l 0.8,-0.6 0.8,0.6 c 1.9,1.2 2.2,1.5 2.3,2.4 0.2,1.5 0.3,1.8 0.5,1.8 0.1,0 1.3,-1.5 1.6,-1.8 0.1,-0.3 -0.1,-0.6 -1.1,-2.1 -0.7,-0.9 -1.1,-1.8 -1.1,-2.1 0,0 0.1,0 0.3,-0.3 0.2,0 0.4,0.3 1,0.9 -1.6,-2.3 -3.2,-4.7 -5.1,-6.8 z m 2.8,10.7 c -0.2,0 -0.9,0.9 -0.8,1.2 l 0.5,0.3 H 75 c 0.2,0 0.3,0 0.2,-0.3 C 75.1,37.4 75,36.8 74.9,36.8 Z M 72.3,38 h -2.4 l -2.4,0.3 -4.5,3.5 -4.4,3.8 v 3.5 c 0,2.1 0,3.8 0.1,3.8 0.1,0 0.7,0.9 1.5,1.5 0.8,0.9 1.5,1.5 1.8,1.8 0.4,0.3 0.5,0.3 4,0.6 l 3.4,0.3 1.6,0.9 c 0.8,0.6 1.5,1.2 1.6,1.2 0.1,0 -0.3,0.3 -0.6,0.6 l -0.6,0.6 1,1.2 c 0.5,0.6 1.3,1.5 1.7,1.8 l 0.6,0.9 v 1.7 0.9 c 3.7,-5 5.9,-11.5 6.1,-18.3 0.1,-2.7 -0.3,-5.3 -0.8,-8 l -0.6,-0.3 c -0.1,0 -0.5,0.3 -1,0.6 -0.5,0.3 -1,0.9 -1.1,0.9 -0.1,0 -0.8,-0.3 -1.8,-0.6 l -1.8,-0.6 v -0.9 c 0,-0.6 0,-0.9 -0.6,-1.5 z M 48,63.7 V 64 h 0.2 z" | ||||
|    class="st2" | ||||
|    id="XMLID_13_" | ||||
|    inkscape:connector-curvature="0" | ||||
|    style="fill:#2ecc71" /> | ||||
| 	<path | ||||
|    d="m 48,15.5 c -3.1,0 -6.2,0.5 -9,1.3 0.3,0.4 0.3,0.4 0.6,0.9 1.5,2.5 1.7,2.8 2.1,2.9 0.3,0 0.9,0.1 1.6,0.1 h 1.2 l 0.9,-2 0.8,-1.9 1.8,-0.6 z m -16.9,4.7 c -2.8,1.7 -5.4,3.9 -7.6,6.4 -3.8,4.3 -6.3,9.6 -7.4,15.4 0.5,0 0.9,-0.1 1.8,-0.1 2.8,0.1 2.5,0 3.4,1.4 0.5,0.8 0.6,0.8 1.4,0.8 1,0.1 0.9,0 0.5,-1.6 -0.2,-0.6 -0.3,-1.2 -0.3,-1.4 0,-0.2 0.5,-0.7 1.7,-1.6 1.9,-1.5 1.8,-1.3 1.5,-2.9 -0.1,-0.3 0.1,-0.6 0.6,-1.2 0.7,-0.7 0.7,-0.6 1.4,-0.6 h 0.7 l 0.1,-1.2 c 0.1,-0.7 0.1,-1.3 0.2,-1.3 0,0 1.9,-1.1 4.1,-2.3 2.2,-1.2 4.1,-2.2 4.2,-2.3 0.2,-0.2 -0.3,-0.8 -2.7,-3.8 -1.5,-1.9 -2.8,-3.6 -2.9,-3.7 z m -5.8,23 c -0.1,0 -0.1,0.3 -0.1,0.6 0,0.6 0,0.7 0.6,1 0.8,0.4 0.9,0.5 0.8,0.2 -0.1,-0.4 -1.2,-1.9 -1.3,-1.8 z m -3.4,2.1 -0.5,1.8 c 0.1,0.1 0.9,0.3 1.8,0.5 1,0.2 1.6,0.4 1.8,0.3 l 0.5,-1.3 z m -3.8,1 -1.1,0.6 c -0.6,0.3 -1.2,0.6 -1.4,0.6 h -0.1 c 0,1.4 0.1,2.8 0.3,4.2 l 0.6,0.4 1,-0.1 h 1 l 0.6,1.4 c 0.3,0.7 0.7,1.4 0.8,1.5 0.1,0.1 1,0.1 1.8,0.1 h 1.5 L 23,56.2 c 0,1.2 0,1.3 -0.6,2.2 -0.4,0.5 -0.6,1.2 -0.6,1.4 0,0.2 0.7,2.1 1.6,4.3 l 1.5,4 1.6,0.8 c 1.2,0.6 1.5,0.8 1.5,1 0,0.1 -0.4,2.1 -0.6,3.1 3,2.5 6.4,4.5 10.2,5.8 3.5,-3.6 6.8,-7.1 7.3,-7.6 l 0.7,-0.7 0.2,-1.9 c 0.2,-1.1 0.4,-2.1 0.4,-2.2 0,-0.1 0.5,-0.6 1,-1.2 0.5,-0.5 0.8,-1 0.8,-1.1 v -0.2 c -0.1,-0.1 -1.4,-1.1 -3,-2.2 l -3.1,-2.1 -1.1,-0.1 c -0.8,0 -1.2,0 -1.3,-0.2 C 39.4,59.2 39.2,58.5 39.1,57.7 39,56.9 38.9,56.2 38.8,56.1 38.8,56 38,56 37.1,56 36.2,56 35.4,55.9 35.3,55.8 35.2,55.7 35.2,55.1 35.1,54.3 35,53.6 34.9,53 34.8,52.9 34.7,52.8 33.7,52.7 32.5,52.6 30.5,52.5 30.1,52.5 29.1,52 l -1.2,-0.6 -1.6,0.7 -1.7,0.9 -1.8,-0.1 c -2,0 -1.9,0.2 -2.1,-1.6 C 20.6,50.7 20.6,50.1 20.5,50.1 20.4,50 20,50 19.6,49.9 L 18.9,49.7 19,49.2 c 0,-0.3 0,-1 0.1,-1.4 L 19.2,47 18.7,46.5 Z m 9.1,1.1 C 27.1,47.5 27.1,47.8 27,48 l -0.1,0.5 2.9,1.2 c 2.9,1.1 3.4,1.2 3.9,0.7 0.2,-0.2 0.1,-0.2 -0.3,-0.4 -0.3,-0.1 -1.7,-0.9 -3.2,-1.6 -1.7,-0.7 -2.9,-1.1 -3,-1 z" | ||||
|    class="st3" | ||||
|    id="XMLID_20_" | ||||
|    inkscape:connector-curvature="0" | ||||
|    style="fill:#27ae60" /> | ||||
| </g><g | ||||
|      transform="matrix(1.458069,0,0,1.458069,-22.631538,-19.615144)" | ||||
|      id="g7664"><path | ||||
|        inkscape:connector-curvature="0" | ||||
|        id="XMLID_6_" | ||||
|        class="st3" | ||||
|        d="m 38.8,56.1 c 0,1.2 1,2.2 2.2,2.2 h 15.2 c 1.2,0 2.2,-1 2.2,-2.2 V 45.3 c 0,-1.2 -1,-2.2 -2.2,-2.2 H 40.9 c -1.2,0 -2.2,1 -2.2,2.2 v 10.8 z" | ||||
|        style="fill:#f1aa27;fill-opacity:1" /><path | ||||
|        style="fill:#e6e6e6" | ||||
|        inkscape:connector-curvature="0" | ||||
|        id="XMLID_7_" | ||||
|        class="st4" | ||||
|        d="m 55.5,43.1 h -3.3 v -3.7 c 0,-2.1 -1.7,-3.8 -3.8,-3.8 -2.1,0 -3.8,1.7 -3.8,3.8 v 3.8 h -3.1 v -3.8 c 0,-3.9 3.2,-7 7,-7 3.9,0 7,3.2 7,7 z" /><path | ||||
|        style="fill:#e6e6e6;fill-opacity:1" | ||||
|        inkscape:connector-curvature="0" | ||||
|        id="XMLID_8_" | ||||
|        class="st5" | ||||
|        d="m 50.35,48.2 c 0,-1 -0.8,-1.8 -1.8,-1.8 -1,0 -1.8,0.8 -1.8,1.8 0,0.7 0.4,1.3 1,1.6 l -1,5.2 h 3.6 l -1,-5.2 c 0.6,-0.3 1,-0.9 1,-1.6 z" /></g></g></svg> | ||||
| Before Width: | Height: | Size: 6.7 KiB | 
| @@ -1,10 +0,0 @@ | ||||
| [Desktop Entry] | ||||
|  | ||||
| Type=Application | ||||
| Version=1.0.0 | ||||
| Name=GlobalProtect VPN | ||||
| Comment=GlobalProtect VPN client, supports SAML auth mode | ||||
| Exec=/usr/bin/gpclient | ||||
| Icon=com.yuezk.qt.GPClient | ||||
| Categories=Network;VPN;Utility;Qt; | ||||
| Keywords=GlobalProtect;Openconnect;SAML;connection;VPN; | ||||
| Before Width: | Height: | Size: 18 KiB | 
| @@ -1,36 +0,0 @@ | ||||
| #include "enhancedwebview.h" | ||||
| #include "cdpcommandmanager.h" | ||||
|  | ||||
| #include <QtWebEngineWidgets/QWebEngineView> | ||||
| #include <QProcessEnvironment> | ||||
|  | ||||
| EnhancedWebView::EnhancedWebView(QWidget *parent) | ||||
|     : QWebEngineView(parent) | ||||
|     , cdp(new CDPCommandManager) | ||||
| { | ||||
|     QObject::connect(cdp, &CDPCommandManager::ready, this, &EnhancedWebView::onCDPReady); | ||||
|     QObject::connect(cdp, &CDPCommandManager::eventReceived, this, &EnhancedWebView::onEventReceived); | ||||
| } | ||||
|  | ||||
| EnhancedWebView::~EnhancedWebView() | ||||
| { | ||||
|     delete cdp; | ||||
| } | ||||
|  | ||||
| void EnhancedWebView::initialize() | ||||
| { | ||||
|     QString port = QProcessEnvironment::systemEnvironment().value(ENV_CDP_PORT); | ||||
|     cdp->initialize("http://127.0.0.1:" + port + "/json"); | ||||
| } | ||||
|  | ||||
| void EnhancedWebView::onCDPReady() | ||||
| { | ||||
|     cdp->sendCommand("Network.enable"); | ||||
| } | ||||
|  | ||||
| void EnhancedWebView::onEventReceived(QString eventName, QJsonObject params) | ||||
| { | ||||
|     if (eventName == "Network.responseReceived") { | ||||
|         emit responseReceived(params); | ||||
|     } | ||||
| } | ||||
| @@ -1,29 +0,0 @@ | ||||
| #ifndef ENHANCEDWEBVIEW_H | ||||
| #define ENHANCEDWEBVIEW_H | ||||
|  | ||||
| #include "cdpcommandmanager.h" | ||||
| #include <QtWebEngineWidgets/QWebEngineView> | ||||
|  | ||||
| #define ENV_CDP_PORT "QTWEBENGINE_REMOTE_DEBUGGING" | ||||
|  | ||||
| class EnhancedWebView : public QWebEngineView | ||||
| { | ||||
|     Q_OBJECT | ||||
| public: | ||||
|     explicit EnhancedWebView(QWidget *parent = nullptr); | ||||
|     ~EnhancedWebView(); | ||||
|  | ||||
|     void initialize(); | ||||
|  | ||||
| signals: | ||||
|     void responseReceived(QJsonObject params); | ||||
|  | ||||
| private slots: | ||||
|     void onCDPReady(); | ||||
|     void onEventReceived(QString eventName, QJsonObject params); | ||||
|  | ||||
| private: | ||||
|     CDPCommandManager *cdp; | ||||
| }; | ||||
|  | ||||
| #endif // ENHANCEDWEBVIEW_H | ||||
| @@ -1,227 +0,0 @@ | ||||
| #include "gpclient.h" | ||||
| #include "ui_gpclient.h" | ||||
| #include "samlloginwindow.h" | ||||
|  | ||||
| #include <QDesktopWidget> | ||||
| #include <QGraphicsScene> | ||||
| #include <QGraphicsView> | ||||
| #include <QGraphicsPixmapItem> | ||||
| #include <QImage> | ||||
| #include <QStyle> | ||||
| #include <QMessageBox> | ||||
|  | ||||
| GPClient::GPClient(QWidget *parent) | ||||
|     : QMainWindow(parent) | ||||
|     , ui(new Ui::GPClient) | ||||
| { | ||||
|     ui->setupUi(this); | ||||
|     setFixedSize(width(), height()); | ||||
|     moveCenter(); | ||||
|  | ||||
|     // 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); | ||||
|  | ||||
|     // 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); | ||||
|  | ||||
|     initVpnStatus(); | ||||
| } | ||||
|  | ||||
| GPClient::~GPClient() | ||||
| { | ||||
|     delete ui; | ||||
|     delete networkManager; | ||||
|     delete reply; | ||||
|     delete vpn; | ||||
|     delete settings; | ||||
| } | ||||
|  | ||||
| void GPClient::on_connectButton_clicked() | ||||
| { | ||||
|     QString btnText = ui->connectButton->text(); | ||||
|  | ||||
|     if (btnText == "Connect") { | ||||
|         QString portal = ui->portalInput->text(); | ||||
|         settings->setValue("portal", portal); | ||||
|         ui->statusLabel->setText("Authenticating..."); | ||||
|         updateConnectionStatus("pending"); | ||||
|         doAuth(portal); | ||||
|     } else if (btnText == "Cancel") { | ||||
|         ui->statusLabel->setText("Canceling..."); | ||||
|         updateConnectionStatus("pending"); | ||||
|  | ||||
|         if (reply->isRunning()) { | ||||
|             reply->abort(); | ||||
|         } | ||||
|         vpn->disconnect(); | ||||
|     } else { | ||||
|         ui->statusLabel->setText("Disconnecting..."); | ||||
|         updateConnectionStatus("pending"); | ||||
|         vpn->disconnect(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| void GPClient::preloginResultFinished() | ||||
| { | ||||
|     if (reply->error()) { | ||||
|         qWarning() << "Prelogin request error"; | ||||
|         emit connectFailed(); | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     QByteArray bytes = reply->readAll(); | ||||
|     const QString tagMethod = "saml-auth-method"; | ||||
|     const QString tagRequest = "saml-request"; | ||||
|     QString samlMethod; | ||||
|     QString samlRequest; | ||||
|  | ||||
|     QXmlStreamReader xml(bytes); | ||||
|     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") { | ||||
|         // TODO | ||||
|         emit connectFailed(); | ||||
|         QMessageBox msgBox; | ||||
|         msgBox.setText("TODO: SAML method is POST"); | ||||
|         msgBox.exec(); | ||||
|     } else if (samlMethod == "REDIRECT") { | ||||
|         samlLogin(samlRequest); | ||||
|     } | ||||
| } | ||||
|  | ||||
| void GPClient::onLoginSuccess(QJsonObject loginResult) | ||||
| { | ||||
|     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") { | ||||
|         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); | ||||
|     } | ||||
| } | ||||
|  | ||||
| void GPClient::onVPNConnected() | ||||
| { | ||||
|     updateConnectionStatus("connected"); | ||||
| } | ||||
|  | ||||
| void GPClient::onVPNDisconnected() | ||||
| { | ||||
|     updateConnectionStatus("not_connected"); | ||||
| } | ||||
|  | ||||
| void GPClient::onVPNLogAvailable(QString log) | ||||
| { | ||||
|     qInfo() << log; | ||||
| } | ||||
|  | ||||
| void GPClient::initVpnStatus() { | ||||
|     int status = vpn->status(); | ||||
|     if (status == 1) { | ||||
|         ui->statusLabel->setText("Connecting..."); | ||||
|         updateConnectionStatus("pending"); | ||||
|     } else if (status == 2) { | ||||
|         updateConnectionStatus("connected"); | ||||
|     } else if (status == 3) { | ||||
|         ui->statusLabel->setText("Disconnecting..."); | ||||
|         updateConnectionStatus("pending"); | ||||
|     } | ||||
| } | ||||
|  | ||||
| void GPClient::moveCenter() | ||||
| { | ||||
|     QDesktopWidget *desktop = QApplication::desktop(); | ||||
|  | ||||
|     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); | ||||
| } | ||||
|  | ||||
| void GPClient::doAuth(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); | ||||
| } | ||||
|  | ||||
| void GPClient::samlLogin(const QString loginUrl) | ||||
| { | ||||
|     SAMLLoginWindow *loginWindow = new SAMLLoginWindow(this); | ||||
|  | ||||
|     QObject::connect(loginWindow, &SAMLLoginWindow::success, this, &GPClient::onLoginSuccess); | ||||
|     QObject::connect(loginWindow, &SAMLLoginWindow::rejected, this, &GPClient::connectFailed); | ||||
|  | ||||
|     loginWindow->login(loginUrl); | ||||
|     loginWindow->exec(); | ||||
|     delete loginWindow; | ||||
| } | ||||
| @@ -1,47 +0,0 @@ | ||||
| #ifndef GPCLIENT_H | ||||
| #define GPCLIENT_H | ||||
|  | ||||
| #include "gpservice_interface.h" | ||||
| #include <QMainWindow> | ||||
| #include <QNetworkAccessManager> | ||||
| #include <QNetworkReply> | ||||
|  | ||||
| QT_BEGIN_NAMESPACE | ||||
| namespace Ui { class GPClient; } | ||||
| QT_END_NAMESPACE | ||||
|  | ||||
| class GPClient : public QMainWindow | ||||
| { | ||||
|     Q_OBJECT | ||||
|  | ||||
| public: | ||||
|     GPClient(QWidget *parent = nullptr); | ||||
|     ~GPClient(); | ||||
|  | ||||
| signals: | ||||
|     void connectFailed(); | ||||
|  | ||||
| private slots: | ||||
|     void on_connectButton_clicked(); | ||||
|     void preloginResultFinished(); | ||||
|  | ||||
|     void onLoginSuccess(QJsonObject loginResult); | ||||
|  | ||||
|     void onVPNConnected(); | ||||
|     void onVPNDisconnected(); | ||||
|     void onVPNLogAvailable(QString log); | ||||
|  | ||||
| private: | ||||
|     Ui::GPClient *ui; | ||||
|     QNetworkAccessManager *networkManager; | ||||
|     QNetworkReply *reply; | ||||
|     com::yuezk::qt::GPService *vpn; | ||||
|     QSettings *settings; | ||||
|  | ||||
|     void initVpnStatus(); | ||||
|     void moveCenter(); | ||||
|     void updateConnectionStatus(QString status); | ||||
|     void doAuth(const QString portal); | ||||
|     void samlLogin(const QString loginUrl); | ||||
| }; | ||||
| #endif // GPCLIENT_H | ||||
| @@ -1,127 +0,0 @@ | ||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <ui version="4.0"> | ||||
|  <class>GPClient</class> | ||||
|  <widget class="QMainWindow" name="GPClient"> | ||||
|   <property name="geometry"> | ||||
|    <rect> | ||||
|     <x>0</x> | ||||
|     <y>0</y> | ||||
|     <width>260</width> | ||||
|     <height>338</height> | ||||
|    </rect> | ||||
|   </property> | ||||
|   <property name="windowTitle"> | ||||
|    <string>GP VPN Client</string> | ||||
|   </property> | ||||
|   <property name="windowIcon"> | ||||
|    <iconset resource="resources.qrc"> | ||||
|     <normaloff>:/images/logo.svg</normaloff>:/images/logo.svg</iconset> | ||||
|   </property> | ||||
|   <property name="styleSheet"> | ||||
|    <string notr="true"/> | ||||
|   </property> | ||||
|   <property name="iconSize"> | ||||
|    <size> | ||||
|     <width>22</width> | ||||
|     <height>22</height> | ||||
|    </size> | ||||
|   </property> | ||||
|   <widget class="QWidget" name="centralwidget"> | ||||
|    <property name="sizePolicy"> | ||||
|     <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> | ||||
|      <horstretch>0</horstretch> | ||||
|      <verstretch>0</verstretch> | ||||
|     </sizepolicy> | ||||
|    </property> | ||||
|    <property name="layoutDirection"> | ||||
|     <enum>Qt::LeftToRight</enum> | ||||
|    </property> | ||||
|    <layout class="QVBoxLayout" name="verticalLayout_3" stretch="1,0"> | ||||
|     <property name="leftMargin"> | ||||
|      <number>15</number> | ||||
|     </property> | ||||
|     <property name="topMargin"> | ||||
|      <number>15</number> | ||||
|     </property> | ||||
|     <property name="rightMargin"> | ||||
|      <number>15</number> | ||||
|     </property> | ||||
|     <property name="bottomMargin"> | ||||
|      <number>15</number> | ||||
|     </property> | ||||
|     <item> | ||||
|      <layout class="QVBoxLayout" name="verticalLayout" stretch="1,0"> | ||||
|       <property name="bottomMargin"> | ||||
|        <number>15</number> | ||||
|       </property> | ||||
|       <item> | ||||
|        <widget class="QLabel" name="statusImage"> | ||||
|         <property name="styleSheet"> | ||||
|          <string notr="true">#statusImage { | ||||
| 	image: url(:/images/not_connected.png); | ||||
| 	padding: 15 | ||||
| }</string> | ||||
|         </property> | ||||
|         <property name="text"> | ||||
|          <string/> | ||||
|         </property> | ||||
|        </widget> | ||||
|       </item> | ||||
|       <item> | ||||
|        <widget class="QLabel" name="statusLabel"> | ||||
|         <property name="font"> | ||||
|          <font> | ||||
|           <pointsize>14</pointsize> | ||||
|           <weight>50</weight> | ||||
|           <bold>false</bold> | ||||
|           <underline>false</underline> | ||||
|          </font> | ||||
|         </property> | ||||
|         <property name="text"> | ||||
|          <string>Not Connected</string> | ||||
|         </property> | ||||
|         <property name="alignment"> | ||||
|          <set>Qt::AlignCenter</set> | ||||
|         </property> | ||||
|        </widget> | ||||
|       </item> | ||||
|      </layout> | ||||
|     </item> | ||||
|     <item> | ||||
|      <layout class="QVBoxLayout" name="verticalLayout_2"> | ||||
|       <property name="bottomMargin"> | ||||
|        <number>0</number> | ||||
|       </property> | ||||
|       <item> | ||||
|        <widget class="QLineEdit" name="portalInput"> | ||||
|         <property name="text"> | ||||
|          <string/> | ||||
|         </property> | ||||
|         <property name="placeholderText"> | ||||
|          <string>Please enter your portal address</string> | ||||
|         </property> | ||||
|        </widget> | ||||
|       </item> | ||||
|       <item> | ||||
|        <widget class="QPushButton" name="connectButton"> | ||||
|         <property name="sizePolicy"> | ||||
|          <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> | ||||
|           <horstretch>0</horstretch> | ||||
|           <verstretch>0</verstretch> | ||||
|          </sizepolicy> | ||||
|         </property> | ||||
|         <property name="text"> | ||||
|          <string>Connect</string> | ||||
|         </property> | ||||
|        </widget> | ||||
|       </item> | ||||
|      </layout> | ||||
|     </item> | ||||
|    </layout> | ||||
|   </widget> | ||||
|  </widget> | ||||
|  <resources> | ||||
|   <include location="resources.qrc"/> | ||||
|  </resources> | ||||
|  <connections/> | ||||
| </ui> | ||||
| @@ -1,25 +0,0 @@ | ||||
| /* | ||||
|  * This file was generated by qdbusxml2cpp version 0.8 | ||||
|  * Command line was: qdbusxml2cpp -i gpservice_interface.h -p :gpservice_interface.cpp ../GPService/gpservice.xml | ||||
|  * | ||||
|  * qdbusxml2cpp is Copyright (C) 2020 The Qt Company Ltd. | ||||
|  * | ||||
|  * This is an auto-generated file. | ||||
|  * This file may have been hand-edited. Look for HAND-EDIT comments | ||||
|  * before re-generating it. | ||||
|  */ | ||||
|  | ||||
| #include "gpservice_interface.h" | ||||
| /* | ||||
|  * Implementation of interface class ComYuezkQtGPServiceInterface | ||||
|  */ | ||||
|  | ||||
| ComYuezkQtGPServiceInterface::ComYuezkQtGPServiceInterface(const QString &service, const QString &path, const QDBusConnection &connection, QObject *parent) | ||||
|     : QDBusAbstractInterface(service, path, staticInterfaceName(), connection, parent) | ||||
| { | ||||
| } | ||||
|  | ||||
| ComYuezkQtGPServiceInterface::~ComYuezkQtGPServiceInterface() | ||||
| { | ||||
| } | ||||
|  | ||||
| @@ -1,71 +0,0 @@ | ||||
| /* | ||||
|  * This file was generated by qdbusxml2cpp version 0.8 | ||||
|  * Command line was: qdbusxml2cpp -p gpservice_interface.h: ../GPService/gpservice.xml | ||||
|  * | ||||
|  * qdbusxml2cpp is Copyright (C) 2020 The Qt Company Ltd. | ||||
|  * | ||||
|  * This is an auto-generated file. | ||||
|  * Do not edit! All changes made to it will be lost. | ||||
|  */ | ||||
|  | ||||
| #ifndef GPSERVICE_INTERFACE_H | ||||
| #define GPSERVICE_INTERFACE_H | ||||
|  | ||||
| #include <QtCore/QObject> | ||||
| #include <QtCore/QByteArray> | ||||
| #include <QtCore/QList> | ||||
| #include <QtCore/QMap> | ||||
| #include <QtCore/QString> | ||||
| #include <QtCore/QStringList> | ||||
| #include <QtCore/QVariant> | ||||
| #include <QtDBus/QtDBus> | ||||
|  | ||||
| /* | ||||
|  * Proxy class for interface com.yuezk.qt.GPService | ||||
|  */ | ||||
| class ComYuezkQtGPServiceInterface: public QDBusAbstractInterface | ||||
| { | ||||
|     Q_OBJECT | ||||
| public: | ||||
|     static inline const char *staticInterfaceName() | ||||
|     { return "com.yuezk.qt.GPService"; } | ||||
|  | ||||
| public: | ||||
|     ComYuezkQtGPServiceInterface(const QString &service, const QString &path, const QDBusConnection &connection, QObject *parent = nullptr); | ||||
|  | ||||
|     ~ComYuezkQtGPServiceInterface(); | ||||
|  | ||||
| public Q_SLOTS: // METHODS | ||||
|     inline QDBusPendingReply<> connect(const QString &server, const QString &username, const QString &passwd) | ||||
|     { | ||||
|         QList<QVariant> argumentList; | ||||
|         argumentList << QVariant::fromValue(server) << QVariant::fromValue(username) << QVariant::fromValue(passwd); | ||||
|         return asyncCallWithArgumentList(QStringLiteral("connect"), argumentList); | ||||
|     } | ||||
|  | ||||
|     inline QDBusPendingReply<> disconnect() | ||||
|     { | ||||
|         QList<QVariant> argumentList; | ||||
|         return asyncCallWithArgumentList(QStringLiteral("disconnect"), argumentList); | ||||
|     } | ||||
|  | ||||
|     inline QDBusPendingReply<int> status() | ||||
|     { | ||||
|         QList<QVariant> argumentList; | ||||
|         return asyncCallWithArgumentList(QStringLiteral("status"), argumentList); | ||||
|     } | ||||
|  | ||||
| Q_SIGNALS: // SIGNALS | ||||
|     void connected(); | ||||
|     void disconnected(); | ||||
|     void logAvailable(const QString &log); | ||||
| }; | ||||
|  | ||||
| namespace com { | ||||
|   namespace yuezk { | ||||
|     namespace qt { | ||||
|       typedef ::ComYuezkQtGPServiceInterface GPService; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| #endif | ||||
| @@ -1,18 +0,0 @@ | ||||
| #include "singleapplication.h" | ||||
| #include "gpclient.h" | ||||
| #include "enhancedwebview.h" | ||||
|  | ||||
| int main(int argc, char *argv[]) | ||||
| { | ||||
|     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); | ||||
|  | ||||
|     return app.exec(); | ||||
| } | ||||
| Before Width: | Height: | Size: 16 KiB | 
| Before Width: | Height: | Size: 16 KiB | 
| @@ -1,8 +0,0 @@ | ||||
| <RCC> | ||||
|     <qresource prefix="/images"> | ||||
|         <file alias="logo.svg">com.yuezk.qt.GPClient.svg</file> | ||||
|         <file>connected.png</file> | ||||
|         <file>pending.png</file> | ||||
|         <file>not_connected.png</file> | ||||
|     </qresource> | ||||
| </RCC> | ||||
| @@ -1,59 +0,0 @@ | ||||
| #include "samlloginwindow.h" | ||||
|  | ||||
| #include <QVBoxLayout> | ||||
|  | ||||
| SAMLLoginWindow::SAMLLoginWindow(QWidget *parent) | ||||
|     : QDialog(parent) | ||||
| { | ||||
|     setWindowTitle("SAML Login"); | ||||
|     resize(610, 406); | ||||
|     QVBoxLayout *verticalLayout = new QVBoxLayout(this); | ||||
|     webView = new EnhancedWebView(this); | ||||
|     webView->setUrl(QUrl("about:blank")); | ||||
|     verticalLayout->addWidget(webView); | ||||
|  | ||||
|     webView->initialize(); | ||||
|     QObject::connect(webView, &EnhancedWebView::responseReceived, this, &SAMLLoginWindow::onResponseReceived); | ||||
| } | ||||
|  | ||||
| SAMLLoginWindow::~SAMLLoginWindow() | ||||
| { | ||||
|     delete webView; | ||||
| } | ||||
|  | ||||
| void SAMLLoginWindow::closeEvent(QCloseEvent *event) | ||||
| { | ||||
|     event->accept(); | ||||
|     reject(); | ||||
| } | ||||
|  | ||||
| void SAMLLoginWindow::login(QString url) | ||||
| { | ||||
|     webView->load(QUrl(url)); | ||||
| } | ||||
|  | ||||
| void SAMLLoginWindow::onResponseReceived(QJsonObject params) | ||||
| { | ||||
|     QString type = params.value("type").toString(); | ||||
|     // Skip non-document response | ||||
|     if (type != "Document") { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     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)); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // 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(); | ||||
|     } | ||||
| } | ||||
| @@ -1,33 +0,0 @@ | ||||
| #ifndef SAMLLOGINWINDOW_H | ||||
| #define SAMLLOGINWINDOW_H | ||||
|  | ||||
| #include "enhancedwebview.h" | ||||
|  | ||||
| #include <QDialog> | ||||
| #include <QJsonObject> | ||||
| #include <QCloseEvent> | ||||
|  | ||||
| class SAMLLoginWindow : public QDialog | ||||
| { | ||||
|     Q_OBJECT | ||||
|  | ||||
| public: | ||||
|     explicit SAMLLoginWindow(QWidget *parent = nullptr); | ||||
|     ~SAMLLoginWindow(); | ||||
|  | ||||
|     void login(QString url); | ||||
|  | ||||
| signals: | ||||
|     void success(QJsonObject samlResult); | ||||
|  | ||||
| private slots: | ||||
|     void onResponseReceived(QJsonObject params); | ||||
|  | ||||
| private: | ||||
|     EnhancedWebView *webView; | ||||
|     QJsonObject samlResult; | ||||
|  | ||||
|     void closeEvent(QCloseEvent *event); | ||||
| }; | ||||
|  | ||||
| #endif // SAMLLOGINWINDOW_H | ||||
| @@ -1,52 +0,0 @@ | ||||
| TARGET = gpservice | ||||
|  | ||||
| QT += dbus | ||||
| QT -= gui | ||||
|  | ||||
| CONFIG += c++11 console | ||||
| CONFIG -= app_bundle | ||||
|  | ||||
| include(../singleapplication/singleapplication.pri) | ||||
| DEFINES += QAPPLICATION_CLASS=QCoreApplication | ||||
|  | ||||
| # The following define makes your compiler emit warnings if you use | ||||
| # any Qt feature that has been marked deprecated (the exact warnings | ||||
| # depend on your compiler). Please consult the documentation of the | ||||
| # deprecated API in order to know how to port your code away from it. | ||||
| DEFINES += QT_DEPRECATED_WARNINGS | ||||
|  | ||||
| # 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. | ||||
| #DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000    # disables all the APIs deprecated before Qt 6.0.0 | ||||
|  | ||||
| HEADERS += \ | ||||
|     gpservice.h \ | ||||
|     sigwatch.h | ||||
|  | ||||
| SOURCES += \ | ||||
|         gpservice.cpp \ | ||||
|         main.cpp \ | ||||
|         sigwatch.cpp | ||||
|  | ||||
| DBUS_ADAPTORS += gpservice.xml | ||||
|  | ||||
| # Default rules for deployment. | ||||
| target.path = /usr/bin | ||||
| INSTALLS += target | ||||
|  | ||||
| DISTFILES += \ | ||||
|     dbus/com.yuezk.qt.GPService.conf \ | ||||
|     dbus/com.yuezk.qt.GPService.service \ | ||||
|     systemd/gpservice.service | ||||
|  | ||||
| dbus_config.path = /usr/share/dbus-1/system.d/ | ||||
| dbus_config.files = dbus/com.yuezk.qt.GPService.conf | ||||
|  | ||||
| dbus_service.path = /usr/share/dbus-1/system-services/ | ||||
| dbus_service.files = dbus/com.yuezk.qt.GPService.service | ||||
|  | ||||
| systemd_service.path = /etc/systemd/system/ | ||||
| systemd_service.files = systemd/gpservice.service | ||||
|  | ||||
| INSTALLS += dbus_config dbus_service systemd_service | ||||
| @@ -1,18 +0,0 @@ | ||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <!DOCTYPE busconfig PUBLIC | ||||
| "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN" | ||||
| "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd"> | ||||
| <busconfig> | ||||
|         <policy user="root"> | ||||
|                 <allow own="com.yuezk.qt.GPService"/> | ||||
|         </policy> | ||||
|  | ||||
|         <policy context="default"> | ||||
|                 <allow send_destination="com.yuezk.qt.GPService" | ||||
|                         send_interface="com.yuezk.qt.GPService" | ||||
|                         /> | ||||
|                 <allow send_destination="com.yuezk.qt.GPService" | ||||
|                         send_interface="org.freedesktop.DBus.Introspectable" | ||||
|                         /> | ||||
|         </policy> | ||||
| </busconfig> | ||||
| @@ -1,5 +0,0 @@ | ||||
| [D-BUS Service] | ||||
| Name=com.yuezk.qt.GPService | ||||
| Exec=/usr/bin/gpservice | ||||
| User=root | ||||
| SystemdService=gpservice.service | ||||
| @@ -1,135 +0,0 @@ | ||||
| #include "gpservice.h" | ||||
| #include "gpservice_adaptor.h" | ||||
|  | ||||
| #include <QFileInfo> | ||||
| #include <QtDBus> | ||||
| #include <QDateTime> | ||||
| #include <QVariant> | ||||
|  | ||||
| GPService::GPService(QObject *parent) | ||||
|     : QObject(parent) | ||||
|     , openconnect(new QProcess) | ||||
| { | ||||
|     // Register the DBus service | ||||
|     new GPServiceAdaptor(this); | ||||
|     QDBusConnection dbus = QDBusConnection::systemBus(); | ||||
|     dbus.registerObject("/", this); | ||||
|     dbus.registerService("com.yuezk.qt.GPService"); | ||||
|  | ||||
|     // Setup the openconnect process | ||||
|     QObject::connect(openconnect, &QProcess::started, this, &GPService::onProcessStarted); | ||||
|     QObject::connect(openconnect, &QProcess::errorOccurred, this, &GPService::onProcessError); | ||||
|     QObject::connect(openconnect, &QProcess::readyReadStandardOutput, this, &GPService::onProcessStdout); | ||||
|     QObject::connect(openconnect, &QProcess::readyReadStandardError, this, &GPService::onProcessStderr); | ||||
|     QObject::connect(openconnect, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), this, &GPService::onProcessFinished); | ||||
| } | ||||
|  | ||||
| GPService::~GPService() | ||||
| { | ||||
|     delete openconnect; | ||||
| } | ||||
|  | ||||
| QString GPService::findBinary() | ||||
| { | ||||
|     for (int i = 0; i < binaryPaths->length(); i++) { | ||||
|         if (QFileInfo::exists(binaryPaths[i])) { | ||||
|             return binaryPaths[i]; | ||||
|         } | ||||
|     } | ||||
|     return nullptr; | ||||
| } | ||||
|  | ||||
| void GPService::quit() | ||||
| { | ||||
|     if (openconnect->state() == QProcess::NotRunning) { | ||||
|         exit(0); | ||||
|     } else { | ||||
|         aboutToQuit = true; | ||||
|         openconnect->terminate(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| void GPService::connect(QString server, QString username, QString passwd) | ||||
| { | ||||
|     if (vpnStatus != GPService::VpnNotConnected) { | ||||
|         log("VPN status is: " + QVariant::fromValue(vpnStatus).toString()); | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     QString bin = findBinary(); | ||||
|     if (bin == nullptr) { | ||||
|         log("Could not found openconnect binary, make sure openconnect is installed, exiting."); | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     QStringList args; | ||||
|     args << QCoreApplication::arguments().mid(1) | ||||
|      << "--protocol=gp" | ||||
|      << "-u" << username | ||||
|      << "--passwd-on-stdin" | ||||
|      << "--timestamp" | ||||
|      << server; | ||||
|  | ||||
|     openconnect->start(bin, args); | ||||
|     openconnect->write(passwd.toUtf8()); | ||||
|     openconnect->closeWriteChannel(); | ||||
| } | ||||
|  | ||||
| void GPService::disconnect() | ||||
| { | ||||
|     if (openconnect->state() != QProcess::NotRunning) { | ||||
|         vpnStatus = GPService::VpnDisconnecting; | ||||
|         openconnect->terminate(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| int GPService::status() | ||||
| { | ||||
|     return vpnStatus; | ||||
| } | ||||
|  | ||||
| void GPService::onProcessStarted() | ||||
| { | ||||
|     log("Openconnect started successfully, PID=" + QString::number(openconnect->processId())); | ||||
|     vpnStatus = GPService::VpnConnecting; | ||||
| } | ||||
|  | ||||
| void GPService::onProcessError(QProcess::ProcessError error) | ||||
| { | ||||
|     log("Error occurred: " + QVariant::fromValue(error).toString()); | ||||
|     vpnStatus = GPService::VpnNotConnected; | ||||
|     emit disconnected(); | ||||
| } | ||||
|  | ||||
| void GPService::onProcessStdout() | ||||
| { | ||||
|     QString output = openconnect->readAllStandardOutput(); | ||||
|  | ||||
|     log(output); | ||||
|     if (output.indexOf("Connected as") >= 0) { | ||||
|         vpnStatus = GPService::VpnConnected; | ||||
|         emit connected(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| void GPService::onProcessStderr() | ||||
| { | ||||
|     log(openconnect->readAllStandardError()); | ||||
| } | ||||
|  | ||||
| void GPService::onProcessFinished(int exitCode, QProcess::ExitStatus exitStatus) | ||||
| { | ||||
|     log("Openconnect process exited with code " + QString::number(exitCode) + " and exit status " + QVariant::fromValue(exitStatus).toString()); | ||||
|     vpnStatus = GPService::VpnNotConnected; | ||||
|     emit disconnected(); | ||||
|  | ||||
|     if (aboutToQuit) { | ||||
|         exit(0); | ||||
|     }; | ||||
| } | ||||
|  | ||||
| void GPService::log(QString msg) | ||||
| { | ||||
|     qInfo() << msg; | ||||
|     emit logAvailable(msg); | ||||
| } | ||||
| @@ -1,58 +0,0 @@ | ||||
| #ifndef GLOBALPROTECTSERVICE_H | ||||
| #define GLOBALPROTECTSERVICE_H | ||||
|  | ||||
| #include <QObject> | ||||
| #include <QProcess> | ||||
|  | ||||
| static const QString binaryPaths[] { | ||||
|     "/usr/local/bin/openconnect", | ||||
|     "/usr/local/sbin/openconnect", | ||||
|     "/usr/bin/openconnect", | ||||
|     "/usr/sbin/openconnect", | ||||
|     "/opt/bin/openconnect", | ||||
|     "/opt/sbin/openconnect" | ||||
| }; | ||||
|  | ||||
| class GPService : public QObject | ||||
| { | ||||
|     Q_OBJECT | ||||
|     Q_CLASSINFO("D-Bus Interface", "com.yuezk.qt.GPService") | ||||
| public: | ||||
|     explicit GPService(QObject *parent = nullptr); | ||||
|     ~GPService(); | ||||
|  | ||||
|     enum VpnStatus { | ||||
|         VpnNotConnected, | ||||
|         VpnConnecting, | ||||
|         VpnConnected, | ||||
|         VpnDisconnecting, | ||||
|     }; | ||||
|  | ||||
| signals: | ||||
|     void connected(); | ||||
|     void disconnected(); | ||||
|     void logAvailable(QString log); | ||||
|  | ||||
| public slots: | ||||
|     void connect(QString server, QString username, QString passwd); | ||||
|     void disconnect(); | ||||
|     int status(); | ||||
|     void quit(); | ||||
|  | ||||
| private slots: | ||||
|     void onProcessStarted(); | ||||
|     void onProcessError(QProcess::ProcessError error); | ||||
|     void onProcessStdout(); | ||||
|     void onProcessStderr(); | ||||
|     void onProcessFinished(int exitCode, QProcess::ExitStatus exitStatus); | ||||
|  | ||||
| private: | ||||
|     QProcess *openconnect; | ||||
|     bool aboutToQuit = false; | ||||
|     int vpnStatus = GPService::VpnNotConnected; | ||||
|  | ||||
|     void log(QString msg); | ||||
|     static QString findBinary(); | ||||
| }; | ||||
|  | ||||
| #endif // GLOBALPROTECTSERVICE_H | ||||
| @@ -1,22 +0,0 @@ | ||||
| <!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN" "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd"> | ||||
| <node> | ||||
|   <interface name="com.yuezk.qt.GPService"> | ||||
|     <signal name="connected"> | ||||
|     </signal> | ||||
|     <signal name="disconnected"> | ||||
|     </signal> | ||||
|     <signal name="logAvailable"> | ||||
|       <arg name="log" type="s" /> | ||||
|     </signal> | ||||
|     <method name="connect"> | ||||
|       <arg name="server" type="s" direction="in"/> | ||||
|       <arg name="username" type="s" direction="in"/> | ||||
|       <arg name="passwd" type="s" direction="in"/> | ||||
|     </method> | ||||
|     <method name="disconnect"> | ||||
|     </method> | ||||
|     <method name="status"> | ||||
|       <arg type="i" direction="out"/> | ||||
|     </method> | ||||
|   </interface> | ||||
| </node> | ||||
| @@ -1,55 +0,0 @@ | ||||
| /* | ||||
|  * This file was generated by qdbusxml2cpp version 0.8 | ||||
|  * Command line was: qdbusxml2cpp -i gpservice_adaptor.h -a :gpservice_adaptor.cpp gpservice.xml | ||||
|  * | ||||
|  * qdbusxml2cpp is Copyright (C) 2020 The Qt Company Ltd. | ||||
|  * | ||||
|  * This is an auto-generated file. | ||||
|  * Do not edit! All changes made to it will be lost. | ||||
|  */ | ||||
|  | ||||
| #include "gpservice_adaptor.h" | ||||
| #include <QtCore/QMetaObject> | ||||
| #include <QtCore/QByteArray> | ||||
| #include <QtCore/QList> | ||||
| #include <QtCore/QMap> | ||||
| #include <QtCore/QString> | ||||
| #include <QtCore/QStringList> | ||||
| #include <QtCore/QVariant> | ||||
|  | ||||
| /* | ||||
|  * Implementation of adaptor class GPServiceAdaptor | ||||
|  */ | ||||
|  | ||||
| GPServiceAdaptor::GPServiceAdaptor(QObject *parent) | ||||
|     : QDBusAbstractAdaptor(parent) | ||||
| { | ||||
|     // constructor | ||||
|     setAutoRelaySignals(true); | ||||
| } | ||||
|  | ||||
| GPServiceAdaptor::~GPServiceAdaptor() | ||||
| { | ||||
|     // destructor | ||||
| } | ||||
|  | ||||
| void GPServiceAdaptor::connect(const QString &server, const QString &username, const QString &passwd) | ||||
| { | ||||
|     // handle method call com.yuezk.qt.GPService.connect | ||||
|     QMetaObject::invokeMethod(parent(), "connect", Q_ARG(QString, server), Q_ARG(QString, username), Q_ARG(QString, passwd)); | ||||
| } | ||||
|  | ||||
| void GPServiceAdaptor::disconnect() | ||||
| { | ||||
|     // handle method call com.yuezk.qt.GPService.disconnect | ||||
|     QMetaObject::invokeMethod(parent(), "disconnect"); | ||||
| } | ||||
|  | ||||
| int GPServiceAdaptor::status() | ||||
| { | ||||
|     // handle method call com.yuezk.qt.GPService.status | ||||
|     int out0; | ||||
|     QMetaObject::invokeMethod(parent(), "status", Q_RETURN_ARG(int, out0)); | ||||
|     return out0; | ||||
| } | ||||
|  | ||||
| @@ -1,66 +0,0 @@ | ||||
| /* | ||||
|  * This file was generated by qdbusxml2cpp version 0.8 | ||||
|  * Command line was: qdbusxml2cpp -a gpservice_adaptor.h: gpservice.xml | ||||
|  * | ||||
|  * qdbusxml2cpp is Copyright (C) 2020 The Qt Company Ltd. | ||||
|  * | ||||
|  * This is an auto-generated file. | ||||
|  * This file may have been hand-edited. Look for HAND-EDIT comments | ||||
|  * before re-generating it. | ||||
|  */ | ||||
|  | ||||
| #ifndef GPSERVICE_ADAPTOR_H | ||||
| #define GPSERVICE_ADAPTOR_H | ||||
|  | ||||
| #include <QtCore/QObject> | ||||
| #include <QtDBus/QtDBus> | ||||
| QT_BEGIN_NAMESPACE | ||||
| class QByteArray; | ||||
| template<class T> class QList; | ||||
| template<class Key, class Value> class QMap; | ||||
| class QString; | ||||
| class QStringList; | ||||
| class QVariant; | ||||
| QT_END_NAMESPACE | ||||
|  | ||||
| /* | ||||
|  * Adaptor class for interface com.yuezk.qt.GPService | ||||
|  */ | ||||
| class GPServiceAdaptor: public QDBusAbstractAdaptor | ||||
| { | ||||
|     Q_OBJECT | ||||
|     Q_CLASSINFO("D-Bus Interface", "com.yuezk.qt.GPService") | ||||
|     Q_CLASSINFO("D-Bus Introspection", "" | ||||
| "  <interface name=\"com.yuezk.qt.GPService\">\n" | ||||
| "    <signal name=\"connected\"/>\n" | ||||
| "    <signal name=\"disconnected\"/>\n" | ||||
| "    <signal name=\"logAvailable\">\n" | ||||
| "      <arg type=\"s\" name=\"log\"/>\n" | ||||
| "    </signal>\n" | ||||
| "    <method name=\"connect\">\n" | ||||
| "      <arg direction=\"in\" type=\"s\" name=\"server\"/>\n" | ||||
| "      <arg direction=\"in\" type=\"s\" name=\"username\"/>\n" | ||||
| "      <arg direction=\"in\" type=\"s\" name=\"passwd\"/>\n" | ||||
| "    </method>\n" | ||||
| "    <method name=\"disconnect\"/>\n" | ||||
| "    <method name=\"status\">\n" | ||||
| "      <arg direction=\"out\" type=\"i\"/>\n" | ||||
| "    </method>\n" | ||||
| "  </interface>\n" | ||||
|         "") | ||||
| public: | ||||
|     GPServiceAdaptor(QObject *parent); | ||||
|     virtual ~GPServiceAdaptor(); | ||||
|  | ||||
| public: // PROPERTIES | ||||
| public Q_SLOTS: // METHODS | ||||
|     void connect(const QString &server, const QString &username, const QString &passwd); | ||||
|     void disconnect(); | ||||
|     int status(); | ||||
| Q_SIGNALS: // SIGNALS | ||||
|     void connected(); | ||||
|     void disconnected(); | ||||
|     void logAvailable(const QString &log); | ||||
| }; | ||||
|  | ||||
| #endif | ||||
| @@ -1,26 +0,0 @@ | ||||
| #include <QtDBus> | ||||
| #include "gpservice.h" | ||||
| #include "singleapplication.h" | ||||
| #include "sigwatch.h" | ||||
|  | ||||
| int main(int argc, char *argv[]) | ||||
| { | ||||
|     SingleApplication app(argc, argv); | ||||
|  | ||||
|     if (!QDBusConnection::systemBus().isConnected()) { | ||||
|         qWarning("Cannot connect to the D-Bus session bus.\n" | ||||
|                  "Please check your system settings and try again.\n"); | ||||
|         return 1; | ||||
|     } | ||||
|  | ||||
|     GPService service; | ||||
|  | ||||
|     UnixSignalWatcher sigwatch; | ||||
|     sigwatch.watchForSignal(SIGINT); | ||||
|     sigwatch.watchForSignal(SIGTERM); | ||||
|     sigwatch.watchForSignal(SIGQUIT); | ||||
|     sigwatch.watchForSignal(SIGHUP); | ||||
|     QObject::connect(&sigwatch, &UnixSignalWatcher::unixSignal, &service, &GPService::quit); | ||||
|  | ||||
|     return app.exec(); | ||||
| } | ||||
| @@ -1,176 +0,0 @@ | ||||
| /* | ||||
|  * Unix signal watcher for Qt. | ||||
|  * | ||||
|  * Copyright (C) 2014 Simon Knopp | ||||
|  * | ||||
|  * Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
|  * of this software and associated documentation files (the "Software"), to deal | ||||
|  * in the Software without restriction, including without limitation the rights | ||||
|  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||||
|  * copies of the Software, and to permit persons to whom the Software is | ||||
|  * furnished to do so, subject to the following conditions: | ||||
|  * | ||||
|  * The above copyright notice and this permission notice shall be included in | ||||
|  * all copies or substantial portions of the Software. | ||||
|  * | ||||
|  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||||
|  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||||
|  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||||
|  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||||
|  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||||
|  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||||
|  * SOFTWARE. | ||||
|  */ | ||||
|  | ||||
| #include <sys/socket.h> | ||||
| #include <unistd.h> | ||||
| #include <errno.h> | ||||
| #include <QMap> | ||||
| #include <QSocketNotifier> | ||||
| #include <QDebug> | ||||
| #include "sigwatch.h" | ||||
|  | ||||
|  | ||||
| /*! | ||||
|  * \brief The UnixSignalWatcherPrivate class implements the back-end signal | ||||
|  * handling for the UnixSignalWatcher. | ||||
|  * | ||||
|  * \see http://qt-project.org/doc/qt-5.0/qtdoc/unix-signals.html | ||||
|  */ | ||||
| class UnixSignalWatcherPrivate : public QObject | ||||
| { | ||||
|     UnixSignalWatcher * const q_ptr; | ||||
|     Q_DECLARE_PUBLIC(UnixSignalWatcher) | ||||
|  | ||||
| public: | ||||
|     UnixSignalWatcherPrivate(UnixSignalWatcher *q); | ||||
|     ~UnixSignalWatcherPrivate(); | ||||
|  | ||||
|     void watchForSignal(int signal); | ||||
|     static void signalHandler(int signal); | ||||
|  | ||||
|     void _q_onNotify(int sockfd); | ||||
|  | ||||
| private: | ||||
|     static int sockpair[2]; | ||||
|     QSocketNotifier *notifier; | ||||
|     QList<int> watchedSignals; | ||||
| }; | ||||
|  | ||||
|  | ||||
| int UnixSignalWatcherPrivate::sockpair[2]; | ||||
|  | ||||
| UnixSignalWatcherPrivate::UnixSignalWatcherPrivate(UnixSignalWatcher *q) : | ||||
|     q_ptr(q) | ||||
| { | ||||
|     // Create socket pair | ||||
|     if (::socketpair(AF_UNIX, SOCK_STREAM, 0, sockpair)) { | ||||
|         qDebug() << "UnixSignalWatcher: socketpair: " << ::strerror(errno); | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     // Create a notifier for the read end of the pair | ||||
|     notifier = new QSocketNotifier(sockpair[1], QSocketNotifier::Read); | ||||
|     QObject::connect(notifier, SIGNAL(activated(int)), q, SLOT(_q_onNotify(int))); | ||||
|     notifier->setEnabled(true); | ||||
| } | ||||
|  | ||||
| UnixSignalWatcherPrivate::~UnixSignalWatcherPrivate() | ||||
| { | ||||
|     delete notifier; | ||||
| } | ||||
|  | ||||
| /*! | ||||
|  * Registers a handler for the given Unix \a signal. The handler will write to | ||||
|  * a socket pair, the other end of which is connected to a QSocketNotifier. | ||||
|  * This provides a way to break out of the asynchronous context from which the | ||||
|  * signal handler is called and back into the Qt event loop. | ||||
|  */ | ||||
| void UnixSignalWatcherPrivate::watchForSignal(int signal) | ||||
| { | ||||
|     if (watchedSignals.contains(signal)) { | ||||
|         qDebug() << "Already watching for signal" << signal; | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     // Register a sigaction which will write to the socket pair | ||||
|     struct sigaction sigact; | ||||
|     sigact.sa_handler = UnixSignalWatcherPrivate::signalHandler; | ||||
|     sigact.sa_flags = 0; | ||||
|     ::sigemptyset(&sigact.sa_mask); | ||||
|     sigact.sa_flags |= SA_RESTART; | ||||
|     if (::sigaction(signal, &sigact, NULL)) { | ||||
|         qDebug() << "UnixSignalWatcher: sigaction: " << ::strerror(errno); | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     watchedSignals.append(signal); | ||||
| } | ||||
|  | ||||
| /*! | ||||
|  * Called when a Unix \a signal is received. Write to the socket to wake up the | ||||
|  * QSocketNotifier. | ||||
|  */ | ||||
| void UnixSignalWatcherPrivate::signalHandler(int signal) | ||||
| { | ||||
|     ssize_t nBytes = ::write(sockpair[0], &signal, sizeof(signal)); | ||||
|     Q_UNUSED(nBytes); | ||||
| } | ||||
|  | ||||
| /*! | ||||
|  * Called when the signal handler has written to the socket pair. Emits the Unix | ||||
|  * signal as a Qt signal. | ||||
|  */ | ||||
| void UnixSignalWatcherPrivate::_q_onNotify(int sockfd) | ||||
| { | ||||
|     Q_Q(UnixSignalWatcher); | ||||
|  | ||||
|     int signal; | ||||
|     ssize_t nBytes = ::read(sockfd, &signal, sizeof(signal)); | ||||
|     Q_UNUSED(nBytes); | ||||
|     qDebug() << "Caught signal:" << ::strsignal(signal); | ||||
|     emit q->unixSignal(signal); | ||||
| } | ||||
|  | ||||
|  | ||||
| /*! | ||||
|  * Create a new UnixSignalWatcher as a child of the given \a parent. | ||||
|  */ | ||||
| UnixSignalWatcher::UnixSignalWatcher(QObject *parent) : | ||||
|     QObject(parent), | ||||
|     d_ptr(new UnixSignalWatcherPrivate(this)) | ||||
| { | ||||
| } | ||||
|  | ||||
| /*! | ||||
|  * Destroy this UnixSignalWatcher. | ||||
|  */ | ||||
| UnixSignalWatcher::~UnixSignalWatcher() | ||||
| { | ||||
|     delete d_ptr; | ||||
| } | ||||
|  | ||||
| /*! | ||||
|  * Register a signal handler for the given \a signal. | ||||
|  * | ||||
|  * After calling this method you can \c connect() to the unixSignal() Qt signal | ||||
|  * to be notified when the Unix signal is received. | ||||
|  */ | ||||
| void UnixSignalWatcher::watchForSignal(int signal) | ||||
| { | ||||
|     Q_D(UnixSignalWatcher); | ||||
|     d->watchForSignal(signal); | ||||
| } | ||||
|  | ||||
| /*! | ||||
|  * \fn void UnixSignalWatcher::unixSignal(int signal) | ||||
|  * Emitted when the given Unix \a signal is received. | ||||
|  * | ||||
|  * watchForSignal() must be called for each Unix signal that you want to receive | ||||
|  * via the unixSignal() Qt signal. If a watcher is watching multiple signals, | ||||
|  * unixSignal() will be emitted whenever *any* of the watched Unix signals are | ||||
|  * received, and the \a signal argument can be inspected to find out which one | ||||
|  * was actually received. | ||||
|  */ | ||||
|  | ||||
| #include "moc_sigwatch.cpp" | ||||
| @@ -1,59 +0,0 @@ | ||||
| /* | ||||
|  * Unix signal watcher for Qt. | ||||
|  * | ||||
|  * Copyright (C) 2014 Simon Knopp | ||||
|  * | ||||
|  * Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
|  * of this software and associated documentation files (the "Software"), to deal | ||||
|  * in the Software without restriction, including without limitation the rights | ||||
|  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||||
|  * copies of the Software, and to permit persons to whom the Software is | ||||
|  * furnished to do so, subject to the following conditions: | ||||
|  * | ||||
|  * The above copyright notice and this permission notice shall be included in | ||||
|  * all copies or substantial portions of the Software. | ||||
|  * | ||||
|  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||||
|  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||||
|  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||||
|  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||||
|  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||||
|  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||||
|  * SOFTWARE. | ||||
|  */ | ||||
|  | ||||
| #ifndef SIGWATCH_H | ||||
| #define SIGWATCH_H | ||||
|  | ||||
| #include <QObject> | ||||
| #include <signal.h> | ||||
|  | ||||
| class UnixSignalWatcherPrivate; | ||||
|  | ||||
|  | ||||
| /*! | ||||
|  * \brief The UnixSignalWatcher class converts Unix signals to Qt signals. | ||||
|  * | ||||
|  * To watch for a given signal, e.g. \c SIGINT, call \c watchForSignal(SIGINT) | ||||
|  * and \c connect() your handler to unixSignal(). | ||||
|  */ | ||||
|  | ||||
| class UnixSignalWatcher : public QObject | ||||
| { | ||||
|     Q_OBJECT | ||||
| public: | ||||
|     explicit UnixSignalWatcher(QObject *parent = 0); | ||||
|     ~UnixSignalWatcher(); | ||||
|  | ||||
|     void watchForSignal(int signal); | ||||
|  | ||||
| signals: | ||||
|     void unixSignal(int signal); | ||||
|  | ||||
| private: | ||||
|     UnixSignalWatcherPrivate * const d_ptr; | ||||
|     Q_DECLARE_PRIVATE(UnixSignalWatcher) | ||||
|     Q_PRIVATE_SLOT(d_func(), void _q_onNotify(int)) | ||||
| }; | ||||
|  | ||||
| #endif // SIGWATCH_H | ||||
| @@ -1,10 +0,0 @@ | ||||
| [Unit] | ||||
| Description=GlobalProtect openconnect DBus service | ||||
|  | ||||
| [Service] | ||||
| Type=dbus | ||||
| BusName=com.yuezk.qt.GPService | ||||
| ExecStart=/usr/bin/gpservice | ||||
|  | ||||
| [Install] | ||||
| WantedBy=multi-user.target | ||||
| @@ -1,5 +0,0 @@ | ||||
| TEMPLATE = subdirs | ||||
|  | ||||
| SUBDIRS += \ | ||||
|     GPClient \ | ||||
|     GPService | ||||
							
								
								
									
										674
									
								
								LICENSE
									
									
									
									
									
								
							
							
						
						| @@ -1,674 +0,0 @@ | ||||
|                     GNU GENERAL PUBLIC LICENSE | ||||
|                        Version 3, 29 June 2007 | ||||
|  | ||||
|  Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> | ||||
|  Everyone is permitted to copy and distribute verbatim copies | ||||
|  of this license document, but changing it is not allowed. | ||||
|  | ||||
|                             Preamble | ||||
|  | ||||
|   The GNU General Public License is a free, copyleft license for | ||||
| software and other kinds of works. | ||||
|  | ||||
|   The licenses for most software and other practical works are designed | ||||
| to take away your freedom to share and change the works.  By contrast, | ||||
| the GNU General Public License is intended to guarantee your freedom to | ||||
| share and change all versions of a program--to make sure it remains free | ||||
| software for all its users.  We, the Free Software Foundation, use the | ||||
| GNU General Public License for most of our software; it applies also to | ||||
| any other work released this way by its authors.  You can apply it to | ||||
| your programs, too. | ||||
|  | ||||
|   When we speak of free software, we are referring to freedom, not | ||||
| price.  Our General Public Licenses are designed to make sure that you | ||||
| have the freedom to distribute copies of free software (and charge for | ||||
| them if you wish), that you receive source code or can get it if you | ||||
| want it, that you can change the software or use pieces of it in new | ||||
| free programs, and that you know you can do these things. | ||||
|  | ||||
|   To protect your rights, we need to prevent others from denying you | ||||
| these rights or asking you to surrender the rights.  Therefore, you have | ||||
| certain responsibilities if you distribute copies of the software, or if | ||||
| you modify it: responsibilities to respect the freedom of others. | ||||
|  | ||||
|   For example, if you distribute copies of such a program, whether | ||||
| gratis or for a fee, you must pass on to the recipients the same | ||||
| freedoms that you received.  You must make sure that they, too, receive | ||||
| or can get the source code.  And you must show them these terms so they | ||||
| know their rights. | ||||
|  | ||||
|   Developers that use the GNU GPL protect your rights with two steps: | ||||
| (1) assert copyright on the software, and (2) offer you this License | ||||
| giving you legal permission to copy, distribute and/or modify it. | ||||
|  | ||||
|   For the developers' and authors' protection, the GPL clearly explains | ||||
| that there is no warranty for this free software.  For both users' and | ||||
| authors' sake, the GPL requires that modified versions be marked as | ||||
| changed, so that their problems will not be attributed erroneously to | ||||
| authors of previous versions. | ||||
|  | ||||
|   Some devices are designed to deny users access to install or run | ||||
| modified versions of the software inside them, although the manufacturer | ||||
| can do so.  This is fundamentally incompatible with the aim of | ||||
| protecting users' freedom to change the software.  The systematic | ||||
| pattern of such abuse occurs in the area of products for individuals to | ||||
| use, which is precisely where it is most unacceptable.  Therefore, we | ||||
| have designed this version of the GPL to prohibit the practice for those | ||||
| products.  If such problems arise substantially in other domains, we | ||||
| stand ready to extend this provision to those domains in future versions | ||||
| of the GPL, as needed to protect the freedom of users. | ||||
|  | ||||
|   Finally, every program is threatened constantly by software patents. | ||||
| States should not allow patents to restrict development and use of | ||||
| software on general-purpose computers, but in those that do, we wish to | ||||
| avoid the special danger that patents applied to a free program could | ||||
| make it effectively proprietary.  To prevent this, the GPL assures that | ||||
| patents cannot be used to render the program non-free. | ||||
|  | ||||
|   The precise terms and conditions for copying, distribution and | ||||
| modification follow. | ||||
|  | ||||
|                        TERMS AND CONDITIONS | ||||
|  | ||||
|   0. Definitions. | ||||
|  | ||||
|   "This License" refers to version 3 of the GNU General Public License. | ||||
|  | ||||
|   "Copyright" also means copyright-like laws that apply to other kinds of | ||||
| works, such as semiconductor masks. | ||||
|  | ||||
|   "The Program" refers to any copyrightable work licensed under this | ||||
| License.  Each licensee is addressed as "you".  "Licensees" and | ||||
| "recipients" may be individuals or organizations. | ||||
|  | ||||
|   To "modify" a work means to copy from or adapt all or part of the work | ||||
| in a fashion requiring copyright permission, other than the making of an | ||||
| exact copy.  The resulting work is called a "modified version" of the | ||||
| earlier work or a work "based on" the earlier work. | ||||
|  | ||||
|   A "covered work" means either the unmodified Program or a work based | ||||
| on the Program. | ||||
|  | ||||
|   To "propagate" a work means to do anything with it that, without | ||||
| permission, would make you directly or secondarily liable for | ||||
| infringement under applicable copyright law, except executing it on a | ||||
| computer or modifying a private copy.  Propagation includes copying, | ||||
| distribution (with or without modification), making available to the | ||||
| public, and in some countries other activities as well. | ||||
|  | ||||
|   To "convey" a work means any kind of propagation that enables other | ||||
| parties to make or receive copies.  Mere interaction with a user through | ||||
| a computer network, with no transfer of a copy, is not conveying. | ||||
|  | ||||
|   An interactive user interface displays "Appropriate Legal Notices" | ||||
| to the extent that it includes a convenient and prominently visible | ||||
| feature that (1) displays an appropriate copyright notice, and (2) | ||||
| tells the user that there is no warranty for the work (except to the | ||||
| extent that warranties are provided), that licensees may convey the | ||||
| work under this License, and how to view a copy of this License.  If | ||||
| the interface presents a list of user commands or options, such as a | ||||
| menu, a prominent item in the list meets this criterion. | ||||
|  | ||||
|   1. Source Code. | ||||
|  | ||||
|   The "source code" for a work means the preferred form of the work | ||||
| for making modifications to it.  "Object code" means any non-source | ||||
| form of a work. | ||||
|  | ||||
|   A "Standard Interface" means an interface that either is an official | ||||
| standard defined by a recognized standards body, or, in the case of | ||||
| interfaces specified for a particular programming language, one that | ||||
| is widely used among developers working in that language. | ||||
|  | ||||
|   The "System Libraries" of an executable work include anything, other | ||||
| than the work as a whole, that (a) is included in the normal form of | ||||
| packaging a Major Component, but which is not part of that Major | ||||
| Component, and (b) serves only to enable use of the work with that | ||||
| Major Component, or to implement a Standard Interface for which an | ||||
| implementation is available to the public in source code form.  A | ||||
| "Major Component", in this context, means a major essential component | ||||
| (kernel, window system, and so on) of the specific operating system | ||||
| (if any) on which the executable work runs, or a compiler used to | ||||
| produce the work, or an object code interpreter used to run it. | ||||
|  | ||||
|   The "Corresponding Source" for a work in object code form means all | ||||
| the source code needed to generate, install, and (for an executable | ||||
| work) run the object code and to modify the work, including scripts to | ||||
| control those activities.  However, it does not include the work's | ||||
| System Libraries, or general-purpose tools or generally available free | ||||
| programs which are used unmodified in performing those activities but | ||||
| which are not part of the work.  For example, Corresponding Source | ||||
| includes interface definition files associated with source files for | ||||
| the work, and the source code for shared libraries and dynamically | ||||
| linked subprograms that the work is specifically designed to require, | ||||
| such as by intimate data communication or control flow between those | ||||
| subprograms and other parts of the work. | ||||
|  | ||||
|   The Corresponding Source need not include anything that users | ||||
| can regenerate automatically from other parts of the Corresponding | ||||
| Source. | ||||
|  | ||||
|   The Corresponding Source for a work in source code form is that | ||||
| same work. | ||||
|  | ||||
|   2. Basic Permissions. | ||||
|  | ||||
|   All rights granted under this License are granted for the term of | ||||
| copyright on the Program, and are irrevocable provided the stated | ||||
| conditions are met.  This License explicitly affirms your unlimited | ||||
| permission to run the unmodified Program.  The output from running a | ||||
| covered work is covered by this License only if the output, given its | ||||
| content, constitutes a covered work.  This License acknowledges your | ||||
| rights of fair use or other equivalent, as provided by copyright law. | ||||
|  | ||||
|   You may make, run and propagate covered works that you do not | ||||
| convey, without conditions so long as your license otherwise remains | ||||
| in force.  You may convey covered works to others for the sole purpose | ||||
| of having them make modifications exclusively for you, or provide you | ||||
| with facilities for running those works, provided that you comply with | ||||
| the terms of this License in conveying all material for which you do | ||||
| not control copyright.  Those thus making or running the covered works | ||||
| for you must do so exclusively on your behalf, under your direction | ||||
| and control, on terms that prohibit them from making any copies of | ||||
| your copyrighted material outside their relationship with you. | ||||
|  | ||||
|   Conveying under any other circumstances is permitted solely under | ||||
| the conditions stated below.  Sublicensing is not allowed; section 10 | ||||
| makes it unnecessary. | ||||
|  | ||||
|   3. Protecting Users' Legal Rights From Anti-Circumvention Law. | ||||
|  | ||||
|   No covered work shall be deemed part of an effective technological | ||||
| measure under any applicable law fulfilling obligations under article | ||||
| 11 of the WIPO copyright treaty adopted on 20 December 1996, or | ||||
| similar laws prohibiting or restricting circumvention of such | ||||
| measures. | ||||
|  | ||||
|   When you convey a covered work, you waive any legal power to forbid | ||||
| circumvention of technological measures to the extent such circumvention | ||||
| is effected by exercising rights under this License with respect to | ||||
| the covered work, and you disclaim any intention to limit operation or | ||||
| modification of the work as a means of enforcing, against the work's | ||||
| users, your or third parties' legal rights to forbid circumvention of | ||||
| technological measures. | ||||
|  | ||||
|   4. Conveying Verbatim Copies. | ||||
|  | ||||
|   You may convey verbatim copies of the Program's source code as you | ||||
| receive it, in any medium, provided that you conspicuously and | ||||
| appropriately publish on each copy an appropriate copyright notice; | ||||
| keep intact all notices stating that this License and any | ||||
| non-permissive terms added in accord with section 7 apply to the code; | ||||
| keep intact all notices of the absence of any warranty; and give all | ||||
| recipients a copy of this License along with the Program. | ||||
|  | ||||
|   You may charge any price or no price for each copy that you convey, | ||||
| and you may offer support or warranty protection for a fee. | ||||
|  | ||||
|   5. Conveying Modified Source Versions. | ||||
|  | ||||
|   You may convey a work based on the Program, or the modifications to | ||||
| produce it from the Program, in the form of source code under the | ||||
| terms of section 4, provided that you also meet all of these conditions: | ||||
|  | ||||
|     a) The work must carry prominent notices stating that you modified | ||||
|     it, and giving a relevant date. | ||||
|  | ||||
|     b) The work must carry prominent notices stating that it is | ||||
|     released under this License and any conditions added under section | ||||
|     7.  This requirement modifies the requirement in section 4 to | ||||
|     "keep intact all notices". | ||||
|  | ||||
|     c) You must license the entire work, as a whole, under this | ||||
|     License to anyone who comes into possession of a copy.  This | ||||
|     License will therefore apply, along with any applicable section 7 | ||||
|     additional terms, to the whole of the work, and all its parts, | ||||
|     regardless of how they are packaged.  This License gives no | ||||
|     permission to license the work in any other way, but it does not | ||||
|     invalidate such permission if you have separately received it. | ||||
|  | ||||
|     d) If the work has interactive user interfaces, each must display | ||||
|     Appropriate Legal Notices; however, if the Program has interactive | ||||
|     interfaces that do not display Appropriate Legal Notices, your | ||||
|     work need not make them do so. | ||||
|  | ||||
|   A compilation of a covered work with other separate and independent | ||||
| works, which are not by their nature extensions of the covered work, | ||||
| and which are not combined with it such as to form a larger program, | ||||
| in or on a volume of a storage or distribution medium, is called an | ||||
| "aggregate" if the compilation and its resulting copyright are not | ||||
| used to limit the access or legal rights of the compilation's users | ||||
| beyond what the individual works permit.  Inclusion of a covered work | ||||
| in an aggregate does not cause this License to apply to the other | ||||
| parts of the aggregate. | ||||
|  | ||||
|   6. Conveying Non-Source Forms. | ||||
|  | ||||
|   You may convey a covered work in object code form under the terms | ||||
| of sections 4 and 5, provided that you also convey the | ||||
| machine-readable Corresponding Source under the terms of this License, | ||||
| in one of these ways: | ||||
|  | ||||
|     a) Convey the object code in, or embodied in, a physical product | ||||
|     (including a physical distribution medium), accompanied by the | ||||
|     Corresponding Source fixed on a durable physical medium | ||||
|     customarily used for software interchange. | ||||
|  | ||||
|     b) Convey the object code in, or embodied in, a physical product | ||||
|     (including a physical distribution medium), accompanied by a | ||||
|     written offer, valid for at least three years and valid for as | ||||
|     long as you offer spare parts or customer support for that product | ||||
|     model, to give anyone who possesses the object code either (1) a | ||||
|     copy of the Corresponding Source for all the software in the | ||||
|     product that is covered by this License, on a durable physical | ||||
|     medium customarily used for software interchange, for a price no | ||||
|     more than your reasonable cost of physically performing this | ||||
|     conveying of source, or (2) access to copy the | ||||
|     Corresponding Source from a network server at no charge. | ||||
|  | ||||
|     c) Convey individual copies of the object code with a copy of the | ||||
|     written offer to provide the Corresponding Source.  This | ||||
|     alternative is allowed only occasionally and noncommercially, and | ||||
|     only if you received the object code with such an offer, in accord | ||||
|     with subsection 6b. | ||||
|  | ||||
|     d) Convey the object code by offering access from a designated | ||||
|     place (gratis or for a charge), and offer equivalent access to the | ||||
|     Corresponding Source in the same way through the same place at no | ||||
|     further charge.  You need not require recipients to copy the | ||||
|     Corresponding Source along with the object code.  If the place to | ||||
|     copy the object code is a network server, the Corresponding Source | ||||
|     may be on a different server (operated by you or a third party) | ||||
|     that supports equivalent copying facilities, provided you maintain | ||||
|     clear directions next to the object code saying where to find the | ||||
|     Corresponding Source.  Regardless of what server hosts the | ||||
|     Corresponding Source, you remain obligated to ensure that it is | ||||
|     available for as long as needed to satisfy these requirements. | ||||
|  | ||||
|     e) Convey the object code using peer-to-peer transmission, provided | ||||
|     you inform other peers where the object code and Corresponding | ||||
|     Source of the work are being offered to the general public at no | ||||
|     charge under subsection 6d. | ||||
|  | ||||
|   A separable portion of the object code, whose source code is excluded | ||||
| from the Corresponding Source as a System Library, need not be | ||||
| included in conveying the object code work. | ||||
|  | ||||
|   A "User Product" is either (1) a "consumer product", which means any | ||||
| tangible personal property which is normally used for personal, family, | ||||
| or household purposes, or (2) anything designed or sold for incorporation | ||||
| into a dwelling.  In determining whether a product is a consumer product, | ||||
| doubtful cases shall be resolved in favor of coverage.  For a particular | ||||
| product received by a particular user, "normally used" refers to a | ||||
| typical or common use of that class of product, regardless of the status | ||||
| of the particular user or of the way in which the particular user | ||||
| actually uses, or expects or is expected to use, the product.  A product | ||||
| is a consumer product regardless of whether the product has substantial | ||||
| commercial, industrial or non-consumer uses, unless such uses represent | ||||
| the only significant mode of use of the product. | ||||
|  | ||||
|   "Installation Information" for a User Product means any methods, | ||||
| procedures, authorization keys, or other information required to install | ||||
| and execute modified versions of a covered work in that User Product from | ||||
| a modified version of its Corresponding Source.  The information must | ||||
| suffice to ensure that the continued functioning of the modified object | ||||
| code is in no case prevented or interfered with solely because | ||||
| modification has been made. | ||||
|  | ||||
|   If you convey an object code work under this section in, or with, or | ||||
| specifically for use in, a User Product, and the conveying occurs as | ||||
| part of a transaction in which the right of possession and use of the | ||||
| User Product is transferred to the recipient in perpetuity or for a | ||||
| fixed term (regardless of how the transaction is characterized), the | ||||
| Corresponding Source conveyed under this section must be accompanied | ||||
| by the Installation Information.  But this requirement does not apply | ||||
| if neither you nor any third party retains the ability to install | ||||
| modified object code on the User Product (for example, the work has | ||||
| been installed in ROM). | ||||
|  | ||||
|   The requirement to provide Installation Information does not include a | ||||
| requirement to continue to provide support service, warranty, or updates | ||||
| for a work that has been modified or installed by the recipient, or for | ||||
| the User Product in which it has been modified or installed.  Access to a | ||||
| network may be denied when the modification itself materially and | ||||
| adversely affects the operation of the network or violates the rules and | ||||
| protocols for communication across the network. | ||||
|  | ||||
|   Corresponding Source conveyed, and Installation Information provided, | ||||
| in accord with this section must be in a format that is publicly | ||||
| documented (and with an implementation available to the public in | ||||
| source code form), and must require no special password or key for | ||||
| unpacking, reading or copying. | ||||
|  | ||||
|   7. Additional Terms. | ||||
|  | ||||
|   "Additional permissions" are terms that supplement the terms of this | ||||
| License by making exceptions from one or more of its conditions. | ||||
| Additional permissions that are applicable to the entire Program shall | ||||
| be treated as though they were included in this License, to the extent | ||||
| that they are valid under applicable law.  If additional permissions | ||||
| apply only to part of the Program, that part may be used separately | ||||
| under those permissions, but the entire Program remains governed by | ||||
| this License without regard to the additional permissions. | ||||
|  | ||||
|   When you convey a copy of a covered work, you may at your option | ||||
| remove any additional permissions from that copy, or from any part of | ||||
| it.  (Additional permissions may be written to require their own | ||||
| removal in certain cases when you modify the work.)  You may place | ||||
| additional permissions on material, added by you to a covered work, | ||||
| for which you have or can give appropriate copyright permission. | ||||
|  | ||||
|   Notwithstanding any other provision of this License, for material you | ||||
| add to a covered work, you may (if authorized by the copyright holders of | ||||
| that material) supplement the terms of this License with terms: | ||||
|  | ||||
|     a) Disclaiming warranty or limiting liability differently from the | ||||
|     terms of sections 15 and 16 of this License; or | ||||
|  | ||||
|     b) Requiring preservation of specified reasonable legal notices or | ||||
|     author attributions in that material or in the Appropriate Legal | ||||
|     Notices displayed by works containing it; or | ||||
|  | ||||
|     c) Prohibiting misrepresentation of the origin of that material, or | ||||
|     requiring that modified versions of such material be marked in | ||||
|     reasonable ways as different from the original version; or | ||||
|  | ||||
|     d) Limiting the use for publicity purposes of names of licensors or | ||||
|     authors of the material; or | ||||
|  | ||||
|     e) Declining to grant rights under trademark law for use of some | ||||
|     trade names, trademarks, or service marks; or | ||||
|  | ||||
|     f) Requiring indemnification of licensors and authors of that | ||||
|     material by anyone who conveys the material (or modified versions of | ||||
|     it) with contractual assumptions of liability to the recipient, for | ||||
|     any liability that these contractual assumptions directly impose on | ||||
|     those licensors and authors. | ||||
|  | ||||
|   All other non-permissive additional terms are considered "further | ||||
| restrictions" within the meaning of section 10.  If the Program as you | ||||
| received it, or any part of it, contains a notice stating that it is | ||||
| governed by this License along with a term that is a further | ||||
| restriction, you may remove that term.  If a license document contains | ||||
| a further restriction but permits relicensing or conveying under this | ||||
| License, you may add to a covered work material governed by the terms | ||||
| of that license document, provided that the further restriction does | ||||
| not survive such relicensing or conveying. | ||||
|  | ||||
|   If you add terms to a covered work in accord with this section, you | ||||
| must place, in the relevant source files, a statement of the | ||||
| additional terms that apply to those files, or a notice indicating | ||||
| where to find the applicable terms. | ||||
|  | ||||
|   Additional terms, permissive or non-permissive, may be stated in the | ||||
| form of a separately written license, or stated as exceptions; | ||||
| the above requirements apply either way. | ||||
|  | ||||
|   8. Termination. | ||||
|  | ||||
|   You may not propagate or modify a covered work except as expressly | ||||
| provided under this License.  Any attempt otherwise to propagate or | ||||
| modify it is void, and will automatically terminate your rights under | ||||
| this License (including any patent licenses granted under the third | ||||
| paragraph of section 11). | ||||
|  | ||||
|   However, if you cease all violation of this License, then your | ||||
| license from a particular copyright holder is reinstated (a) | ||||
| provisionally, unless and until the copyright holder explicitly and | ||||
| finally terminates your license, and (b) permanently, if the copyright | ||||
| holder fails to notify you of the violation by some reasonable means | ||||
| prior to 60 days after the cessation. | ||||
|  | ||||
|   Moreover, your license from a particular copyright holder is | ||||
| reinstated permanently if the copyright holder notifies you of the | ||||
| violation by some reasonable means, this is the first time you have | ||||
| received notice of violation of this License (for any work) from that | ||||
| copyright holder, and you cure the violation prior to 30 days after | ||||
| your receipt of the notice. | ||||
|  | ||||
|   Termination of your rights under this section does not terminate the | ||||
| licenses of parties who have received copies or rights from you under | ||||
| this License.  If your rights have been terminated and not permanently | ||||
| reinstated, you do not qualify to receive new licenses for the same | ||||
| material under section 10. | ||||
|  | ||||
|   9. Acceptance Not Required for Having Copies. | ||||
|  | ||||
|   You are not required to accept this License in order to receive or | ||||
| run a copy of the Program.  Ancillary propagation of a covered work | ||||
| occurring solely as a consequence of using peer-to-peer transmission | ||||
| to receive a copy likewise does not require acceptance.  However, | ||||
| nothing other than this License grants you permission to propagate or | ||||
| modify any covered work.  These actions infringe copyright if you do | ||||
| not accept this License.  Therefore, by modifying or propagating a | ||||
| covered work, you indicate your acceptance of this License to do so. | ||||
|  | ||||
|   10. Automatic Licensing of Downstream Recipients. | ||||
|  | ||||
|   Each time you convey a covered work, the recipient automatically | ||||
| receives a license from the original licensors, to run, modify and | ||||
| propagate that work, subject to this License.  You are not responsible | ||||
| for enforcing compliance by third parties with this License. | ||||
|  | ||||
|   An "entity transaction" is a transaction transferring control of an | ||||
| organization, or substantially all assets of one, or subdividing an | ||||
| organization, or merging organizations.  If propagation of a covered | ||||
| work results from an entity transaction, each party to that | ||||
| transaction who receives a copy of the work also receives whatever | ||||
| licenses to the work the party's predecessor in interest had or could | ||||
| give under the previous paragraph, plus a right to possession of the | ||||
| Corresponding Source of the work from the predecessor in interest, if | ||||
| the predecessor has it or can get it with reasonable efforts. | ||||
|  | ||||
|   You may not impose any further restrictions on the exercise of the | ||||
| rights granted or affirmed under this License.  For example, you may | ||||
| not impose a license fee, royalty, or other charge for exercise of | ||||
| rights granted under this License, and you may not initiate litigation | ||||
| (including a cross-claim or counterclaim in a lawsuit) alleging that | ||||
| any patent claim is infringed by making, using, selling, offering for | ||||
| sale, or importing the Program or any portion of it. | ||||
|  | ||||
|   11. Patents. | ||||
|  | ||||
|   A "contributor" is a copyright holder who authorizes use under this | ||||
| License of the Program or a work on which the Program is based.  The | ||||
| work thus licensed is called the contributor's "contributor version". | ||||
|  | ||||
|   A contributor's "essential patent claims" are all patent claims | ||||
| owned or controlled by the contributor, whether already acquired or | ||||
| hereafter acquired, that would be infringed by some manner, permitted | ||||
| by this License, of making, using, or selling its contributor version, | ||||
| but do not include claims that would be infringed only as a | ||||
| consequence of further modification of the contributor version.  For | ||||
| purposes of this definition, "control" includes the right to grant | ||||
| patent sublicenses in a manner consistent with the requirements of | ||||
| this License. | ||||
|  | ||||
|   Each contributor grants you a non-exclusive, worldwide, royalty-free | ||||
| patent license under the contributor's essential patent claims, to | ||||
| make, use, sell, offer for sale, import and otherwise run, modify and | ||||
| propagate the contents of its contributor version. | ||||
|  | ||||
|   In the following three paragraphs, a "patent license" is any express | ||||
| agreement or commitment, however denominated, not to enforce a patent | ||||
| (such as an express permission to practice a patent or covenant not to | ||||
| sue for patent infringement).  To "grant" such a patent license to a | ||||
| party means to make such an agreement or commitment not to enforce a | ||||
| patent against the party. | ||||
|  | ||||
|   If you convey a covered work, knowingly relying on a patent license, | ||||
| and the Corresponding Source of the work is not available for anyone | ||||
| to copy, free of charge and under the terms of this License, through a | ||||
| publicly available network server or other readily accessible means, | ||||
| then you must either (1) cause the Corresponding Source to be so | ||||
| available, or (2) arrange to deprive yourself of the benefit of the | ||||
| patent license for this particular work, or (3) arrange, in a manner | ||||
| consistent with the requirements of this License, to extend the patent | ||||
| license to downstream recipients.  "Knowingly relying" means you have | ||||
| actual knowledge that, but for the patent license, your conveying the | ||||
| covered work in a country, or your recipient's use of the covered work | ||||
| in a country, would infringe one or more identifiable patents in that | ||||
| country that you have reason to believe are valid. | ||||
|  | ||||
|   If, pursuant to or in connection with a single transaction or | ||||
| arrangement, you convey, or propagate by procuring conveyance of, a | ||||
| covered work, and grant a patent license to some of the parties | ||||
| receiving the covered work authorizing them to use, propagate, modify | ||||
| or convey a specific copy of the covered work, then the patent license | ||||
| you grant is automatically extended to all recipients of the covered | ||||
| work and works based on it. | ||||
|  | ||||
|   A patent license is "discriminatory" if it does not include within | ||||
| the scope of its coverage, prohibits the exercise of, or is | ||||
| conditioned on the non-exercise of one or more of the rights that are | ||||
| specifically granted under this License.  You may not convey a covered | ||||
| work if you are a party to an arrangement with a third party that is | ||||
| in the business of distributing software, under which you make payment | ||||
| to the third party based on the extent of your activity of conveying | ||||
| the work, and under which the third party grants, to any of the | ||||
| parties who would receive the covered work from you, a discriminatory | ||||
| patent license (a) in connection with copies of the covered work | ||||
| conveyed by you (or copies made from those copies), or (b) primarily | ||||
| for and in connection with specific products or compilations that | ||||
| contain the covered work, unless you entered into that arrangement, | ||||
| or that patent license was granted, prior to 28 March 2007. | ||||
|  | ||||
|   Nothing in this License shall be construed as excluding or limiting | ||||
| any implied license or other defenses to infringement that may | ||||
| otherwise be available to you under applicable patent law. | ||||
|  | ||||
|   12. No Surrender of Others' Freedom. | ||||
|  | ||||
|   If conditions are imposed on you (whether by court order, agreement or | ||||
| otherwise) that contradict the conditions of this License, they do not | ||||
| excuse you from the conditions of this License.  If you cannot convey a | ||||
| covered work so as to satisfy simultaneously your obligations under this | ||||
| License and any other pertinent obligations, then as a consequence you may | ||||
| not convey it at all.  For example, if you agree to terms that obligate you | ||||
| to collect a royalty for further conveying from those to whom you convey | ||||
| the Program, the only way you could satisfy both those terms and this | ||||
| License would be to refrain entirely from conveying the Program. | ||||
|  | ||||
|   13. Use with the GNU Affero General Public License. | ||||
|  | ||||
|   Notwithstanding any other provision of this License, you have | ||||
| permission to link or combine any covered work with a work licensed | ||||
| under version 3 of the GNU Affero General Public License into a single | ||||
| combined work, and to convey the resulting work.  The terms of this | ||||
| License will continue to apply to the part which is the covered work, | ||||
| but the special requirements of the GNU Affero General Public License, | ||||
| section 13, concerning interaction through a network will apply to the | ||||
| combination as such. | ||||
|  | ||||
|   14. Revised Versions of this License. | ||||
|  | ||||
|   The Free Software Foundation may publish revised and/or new versions of | ||||
| the GNU General Public License from time to time.  Such new versions will | ||||
| be similar in spirit to the present version, but may differ in detail to | ||||
| address new problems or concerns. | ||||
|  | ||||
|   Each version is given a distinguishing version number.  If the | ||||
| Program specifies that a certain numbered version of the GNU General | ||||
| Public License "or any later version" applies to it, you have the | ||||
| option of following the terms and conditions either of that numbered | ||||
| version or of any later version published by the Free Software | ||||
| Foundation.  If the Program does not specify a version number of the | ||||
| GNU General Public License, you may choose any version ever published | ||||
| by the Free Software Foundation. | ||||
|  | ||||
|   If the Program specifies that a proxy can decide which future | ||||
| versions of the GNU General Public License can be used, that proxy's | ||||
| public statement of acceptance of a version permanently authorizes you | ||||
| to choose that version for the Program. | ||||
|  | ||||
|   Later license versions may give you additional or different | ||||
| permissions.  However, no additional obligations are imposed on any | ||||
| author or copyright holder as a result of your choosing to follow a | ||||
| later version. | ||||
|  | ||||
|   15. Disclaimer of Warranty. | ||||
|  | ||||
|   THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY | ||||
| APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT | ||||
| HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY | ||||
| OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, | ||||
| THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR | ||||
| PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM | ||||
| IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF | ||||
| ALL NECESSARY SERVICING, REPAIR OR CORRECTION. | ||||
|  | ||||
|   16. Limitation of Liability. | ||||
|  | ||||
|   IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING | ||||
| WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS | ||||
| THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY | ||||
| GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE | ||||
| USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF | ||||
| DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD | ||||
| PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), | ||||
| EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF | ||||
| SUCH DAMAGES. | ||||
|  | ||||
|   17. Interpretation of Sections 15 and 16. | ||||
|  | ||||
|   If the disclaimer of warranty and limitation of liability provided | ||||
| above cannot be given local legal effect according to their terms, | ||||
| reviewing courts shall apply local law that most closely approximates | ||||
| an absolute waiver of all civil liability in connection with the | ||||
| Program, unless a warranty or assumption of liability accompanies a | ||||
| copy of the Program in return for a fee. | ||||
|  | ||||
|                      END OF TERMS AND CONDITIONS | ||||
|  | ||||
|             How to Apply These Terms to Your New Programs | ||||
|  | ||||
|   If you develop a new program, and you want it to be of the greatest | ||||
| possible use to the public, the best way to achieve this is to make it | ||||
| free software which everyone can redistribute and change under these terms. | ||||
|  | ||||
|   To do so, attach the following notices to the program.  It is safest | ||||
| to attach them to the start of each source file to most effectively | ||||
| state the exclusion of warranty; and each file should have at least | ||||
| the "copyright" line and a pointer to where the full notice is found. | ||||
|  | ||||
|     <one line to give the program's name and a brief idea of what it does.> | ||||
|     Copyright (C) <year>  <name of author> | ||||
|  | ||||
|     This program is free software: you can redistribute it and/or modify | ||||
|     it under the terms of the GNU General Public License as published by | ||||
|     the Free Software Foundation, either version 3 of the License, or | ||||
|     (at your option) any later version. | ||||
|  | ||||
|     This program is distributed in the hope that it will be useful, | ||||
|     but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
|     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
|     GNU General Public License for more details. | ||||
|  | ||||
|     You should have received a copy of the GNU General Public License | ||||
|     along with this program.  If not, see <https://www.gnu.org/licenses/>. | ||||
|  | ||||
| Also add information on how to contact you by electronic and paper mail. | ||||
|  | ||||
|   If the program does terminal interaction, make it output a short | ||||
| notice like this when it starts in an interactive mode: | ||||
|  | ||||
|     <program>  Copyright (C) <year>  <name of author> | ||||
|     This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. | ||||
|     This is free software, and you are welcome to redistribute it | ||||
|     under certain conditions; type `show c' for details. | ||||
|  | ||||
| The hypothetical commands `show w' and `show c' should show the appropriate | ||||
| parts of the General Public License.  Of course, your program's commands | ||||
| might be different; for a GUI interface, you would use an "about box". | ||||
|  | ||||
|   You should also get your employer (if you work as a programmer) or school, | ||||
| if any, to sign a "copyright disclaimer" for the program, if necessary. | ||||
| For more information on this, and how to apply and follow the GNU GPL, see | ||||
| <https://www.gnu.org/licenses/>. | ||||
|  | ||||
|   The GNU General Public License does not permit incorporating your program | ||||
| into proprietary programs.  If your program is a subroutine library, you | ||||
| may consider it more useful to permit linking proprietary applications with | ||||
| the library.  If this is what you want to do, use the GNU Lesser General | ||||
| Public License instead of this License.  But first, please read | ||||
| <https://www.gnu.org/licenses/why-not-lgpl.html>. | ||||
							
								
								
									
										59
									
								
								README.md
									
									
									
									
									
								
							
							
						
						| @@ -1,33 +1,42 @@ | ||||
| # GlobalProtect-openconnect | ||||
| A GlobalProtect VPN client (GUI) for Linux based on Openconnect and built with Qt5, supports SAML auth mode, inspired by [gp-saml-gui](https://github.com/dlenski/gp-saml-gui). | ||||
|  | ||||
| <p align="center"> | ||||
|   <img src="screenshot.png"> | ||||
|   <img width="300" src="https://github.com/yuezk/GlobalProtect-openconnect/assets/3297602/9242df9c-217d-42ab-8c21-8f9f69cd4eb5"> | ||||
| </p> | ||||
|  | ||||
| ## Prerequisites | ||||
| ## Development | ||||
|  | ||||
| - Openconnect v8.x | ||||
| - Qt5, qt5-webengine, qt5-websockets | ||||
| ### Dependencies | ||||
|  | ||||
| ### Ubuntu | ||||
| 1. Install openconnect v8.x | ||||
|     Update openconnect to 8.x, for ubuntu 18.04 you might need to [build the latest openconnect from source code](https://gist.github.com/yuezk/ab9a4b87a9fa0182bdb2df41fab5f613). | ||||
| 2. Install the Qt dependencies | ||||
|     ```sh | ||||
|     sudo apt install qt5-default libqt5websockets5-dev qtwebengine5-dev | ||||
|     ``` | ||||
| ## Install | ||||
| The following packages will be required to build depending on your environment: | ||||
|  | ||||
| - [Cargo](https://doc.rust-lang.org/cargo/getting-started/installation.html) | ||||
| - [pnpm](https://pnpm.io/installation) | ||||
| - openconnect-devel (containing `openconnect.h`): `sudo apt install libopenconnect-dev` or `sudo yum install openconnect-devel` | ||||
| - GDK dependencies `sudo apt install libgdk3.0-cil-dev libcairo2-dev libsoup2.4-dev libgdk-pixbuf-2.0-dev libjavascriptcoregtk-4.0-dev libatk1.0-dev libpango1.0-dev libwebkit2gtk-4.0-dev` | ||||
|  | ||||
| ### Build the service | ||||
|  | ||||
| ```sh | ||||
| git clone https://github.com/yuezk/GlobalProtect-openconnect.git | ||||
| cd GlobalProtect-openconnect | ||||
| git submodule init && git submodule update | ||||
| qmake CONFIG+=release | ||||
| make | ||||
| sudo make install | ||||
| ``` | ||||
| Open `GlobalProtect VPN` in the application dashboard. | ||||
| # Build the client first | ||||
| cargo build -p gpclient | ||||
|  | ||||
| ## [License](./LICENSE) | ||||
| GPLv3 | ||||
| # Build the service | ||||
| cargo build -p gpservice | ||||
| ``` | ||||
|  | ||||
| ### Start the service | ||||
|  | ||||
| ```sh | ||||
| sudo ./target/debug/gpservice | ||||
| ``` | ||||
|  | ||||
| ### Start the GUI | ||||
|  | ||||
| ```sh | ||||
| cd gpgui | ||||
| pnpm install | ||||
| pnpm tauri dev | ||||
| ``` | ||||
|  | ||||
| ### Open the DevTools | ||||
|  | ||||
| Right-click on the GUI window and select "Inspect Element". | ||||
|   | ||||
							
								
								
									
										19
									
								
								com.yuezk.gp.policy
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,19 @@ | ||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <!DOCTYPE policyconfig PUBLIC "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN" "http://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd"> | ||||
| <policyconfig> | ||||
|   <vendor>The GlobalProtect-openconnect Project</vendor> | ||||
|   <vendor_url>https://github.com/yuezk/GlobalProtect-openconnect</vendor_url> | ||||
|   <icon_name>gpgui</icon_name> | ||||
|   <action id="com.yuezk.gp.update-gpconf"> | ||||
|     <description>Update the /etc/gpservice/gp.conf</description> | ||||
|     <message>Authentication is required to update the GlobalProtect service configuration</message> | ||||
|     <defaults> | ||||
|       <allow_any>auth_admin</allow_any> | ||||
|       <allow_inactive>auth_admin</allow_inactive> | ||||
|       <allow_active>auth_admin</allow_active> | ||||
|     </defaults> | ||||
|     <annotate key="org.freedesktop.policykit.exec.path">/usr/bin/tee</annotate> | ||||
|     <annotate key="org.freedesktop.policykit.exec.argv1">/etc/gpservice/gp.conf</annotate> | ||||
|     <annotate key="org.freedesktop.policykit.exec.allow_gui">true</annotate> | ||||
|   </action> | ||||
| </policyconfig> | ||||
							
								
								
									
										1
									
								
								gpclient/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | ||||
| /target | ||||
							
								
								
									
										10
									
								
								gpclient/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,10 @@ | ||||
| [package] | ||||
| name = "gpclient" | ||||
| version = "0.1.0" | ||||
| edition = "2021" | ||||
|  | ||||
| # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | ||||
|  | ||||
| [dependencies] | ||||
| gpcommon = { path = "../gpcommon" } | ||||
| tokio = { version = "1.0", features = ["full"] } | ||||
							
								
								
									
										25
									
								
								gpclient/src/main.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,25 @@ | ||||
| use gpcommon::{Client, SOCKET_PATH}; | ||||
| use tokio::{io::AsyncReadExt, net::UnixStream, sync::mpsc}; | ||||
|  | ||||
| #[tokio::main] | ||||
| async fn main() { | ||||
|     // let mut stream = UnixStream::connect(SOCKET_PATH).await.unwrap(); | ||||
|  | ||||
|     // let mut buf = [0u8; 34]; | ||||
|     // let _ = stream.read(&mut buf).await.unwrap(); | ||||
|  | ||||
|     // // The first two bytes are the port number, the rest is the AES key | ||||
|     // let http_port = u16::from_be_bytes([buf[0], buf[1]]); | ||||
|     // let aes_key = &buf[2..]; | ||||
|  | ||||
|     // println!("http_port: {http_port}"); | ||||
|     // println!("aes_key: {aes_key:?}"); | ||||
|     let (output_tx, mut output_rx) = mpsc::channel::<String>(32); | ||||
|     let client = Client::default(); | ||||
|  | ||||
|     tokio::select! { | ||||
|         _ = client.run() => { | ||||
|             println!("Client finished"); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										2
									
								
								gpcommon/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,2 @@ | ||||
| /target | ||||
| /Cargo.lock | ||||
							
								
								
									
										27
									
								
								gpcommon/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,27 @@ | ||||
| [package] | ||||
| name = "gpcommon" | ||||
| version = "0.1.0" | ||||
| edition = "2021" | ||||
|  | ||||
| # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | ||||
|  | ||||
| [dependencies] | ||||
| tokio = { version = "1.14", features = ["full"] } | ||||
| tokio-util = "0.7" | ||||
| thiserror = "1.0" | ||||
| serde = { version = "1.0", features = ["derive"] } | ||||
| bytes = "1.0" | ||||
| serde_json = "1.0" | ||||
| async-trait = "0.1" | ||||
| ring = "0.16" | ||||
| data-encoding = "2.3" | ||||
| log = "0.4" | ||||
| is_executable = "1.0" | ||||
| configparser = "3.0" | ||||
| shlex = "1.0" | ||||
| anyhow = "1.0" | ||||
| tempfile = "3.8" | ||||
| lexopt = "0.3.0" | ||||
|  | ||||
| [build-dependencies] | ||||
| cc = "1.0" | ||||
							
								
								
									
										12
									
								
								gpcommon/build.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,12 @@ | ||||
| fn main() { | ||||
|     // Link to the native openconnect library | ||||
|     println!("cargo:rustc-link-lib=openconnect"); | ||||
|     println!("cargo:rerun-if-changed=src/vpn/vpn.c"); | ||||
|     println!("cargo:rerun-if-changed=src/vpn/vpn.h"); | ||||
|  | ||||
|     // Compile the vpn.c file | ||||
|     cc::Build::new() | ||||
|         .file("src/vpn/vpn.c") | ||||
|         .include("src/vpn") | ||||
|         .compile("vpn"); | ||||
| } | ||||
							
								
								
									
										286
									
								
								gpcommon/src/client.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,286 @@ | ||||
| use crate::cmd::{Connect, Disconnect, GetStatus}; | ||||
| use crate::reader::Reader; | ||||
| use crate::request::CommandPayload; | ||||
| use crate::response::ResponseData; | ||||
| use crate::writer::Writer; | ||||
| use crate::RequestPool; | ||||
| use crate::Response; | ||||
| use crate::SOCKET_PATH; | ||||
| use crate::{Request, VpnStatus}; | ||||
| use log::{debug, info, warn}; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use std::fmt::Display; | ||||
| use std::sync::Arc; | ||||
| use tokio::io::{self, ReadHalf, WriteHalf}; | ||||
| use tokio::net::UnixStream; | ||||
| use tokio::sync::{mpsc, Mutex, RwLock}; | ||||
| use tokio_util::sync::CancellationToken; | ||||
|  | ||||
| #[derive(Debug)] | ||||
| enum ServiceEvent { | ||||
|     Online, | ||||
|     Response(Response), | ||||
|     Offline, | ||||
| } | ||||
|  | ||||
| impl From<Response> for ServiceEvent { | ||||
|     fn from(response: Response) -> Self { | ||||
|         Self::Response(response) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug)] | ||||
| pub enum ClientStatus { | ||||
|     Vpn(VpnStatus), | ||||
|     Service(bool), | ||||
| } | ||||
|  | ||||
| #[derive(Debug)] | ||||
| pub struct Client { | ||||
|     // pool of requests that are waiting for responses | ||||
|     request_pool: Arc<RequestPool>, | ||||
|     // tx for sending requests to the channel | ||||
|     request_tx: mpsc::Sender<Request>, | ||||
|     // rx for receiving requests from the channel | ||||
|     request_rx: Arc<Mutex<mpsc::Receiver<Request>>>, | ||||
|     // tx for sending responses to the channel | ||||
|     service_event_tx: mpsc::Sender<ServiceEvent>, | ||||
|     // rx for receiving responses from the channel | ||||
|     service_event_rx: Arc<Mutex<mpsc::Receiver<ServiceEvent>>>, | ||||
|     is_online: Arc<RwLock<bool>>, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Serialize, Deserialize)] | ||||
| pub struct ServerApiError { | ||||
|     pub message: String, | ||||
| } | ||||
|  | ||||
| impl Display for ServerApiError { | ||||
|     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||||
|         write!(f, "{message}", message = self.message) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl From<String> for ServerApiError { | ||||
|     fn from(message: String) -> Self { | ||||
|         Self { message } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl From<&str> for ServerApiError { | ||||
|     fn from(message: &str) -> Self { | ||||
|         Self { | ||||
|             message: message.to_string(), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Default for Client { | ||||
|     fn default() -> Self { | ||||
|         let (request_tx, request_rx) = mpsc::channel::<Request>(32); | ||||
|         let (service_event_tx, server_event_rx) = mpsc::channel::<ServiceEvent>(32); | ||||
|  | ||||
|         Self { | ||||
|             request_pool: Default::default(), | ||||
|             request_tx, | ||||
|             request_rx: Arc::new(Mutex::new(request_rx)), | ||||
|             service_event_tx, | ||||
|             service_event_rx: Arc::new(Mutex::new(server_event_rx)), | ||||
|             is_online: Default::default(), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Client { | ||||
|     pub async fn is_online(&self) -> bool { | ||||
|         *self.is_online.read().await | ||||
|     } | ||||
|  | ||||
|     pub fn subscribe_status(&self, callback: impl Fn(ClientStatus) + Send + Sync + 'static) { | ||||
|         let service_event_rx = self.service_event_rx.clone(); | ||||
|  | ||||
|         tokio::spawn(async move { | ||||
|             loop { | ||||
|                 let mut server_event_rx = service_event_rx.lock().await; | ||||
|                 if let Some(server_event) = server_event_rx.recv().await { | ||||
|                     match server_event { | ||||
|                         ServiceEvent::Online => { | ||||
|                             callback(ClientStatus::Service(true)); | ||||
|                         } | ||||
|                         ServiceEvent::Offline => { | ||||
|                             callback(ClientStatus::Service(false)); | ||||
|                         } | ||||
|                         ServiceEvent::Response(response) => { | ||||
|                             if let ResponseData::Status(vpn_status) = response.data() { | ||||
|                                 callback(ClientStatus::Vpn(vpn_status)); | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     pub async fn run(&self) { | ||||
|         info!("Connecting to the background service..."); | ||||
|  | ||||
|         // TODO exit the loop properly | ||||
|         loop { | ||||
|             match self.connect_to_server().await { | ||||
|                 Ok(_) => { | ||||
|                     debug!("Disconnected from server, reconnecting..."); | ||||
|                 } | ||||
|                 Err(err) => { | ||||
|                     debug!("Error connecting to server, retrying, error: {:?}", err) | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             // wait for a second before trying to reconnect | ||||
|             tokio::time::sleep(std::time::Duration::from_secs(1)).await; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     async fn connect_to_server(&self) -> Result<(), Box<dyn std::error::Error>> { | ||||
|         let stream = UnixStream::connect(SOCKET_PATH).await?; | ||||
|         let (read_stream, write_stream) = io::split(stream); | ||||
|         let cancel_token = CancellationToken::new(); | ||||
|  | ||||
|         let read_handle = tokio::spawn(handle_read( | ||||
|             read_stream, | ||||
|             self.request_pool.clone(), | ||||
|             self.service_event_tx.clone(), | ||||
|             cancel_token.clone(), | ||||
|         )); | ||||
|  | ||||
|         let write_handle = tokio::spawn(handle_write( | ||||
|             write_stream, | ||||
|             self.request_rx.clone(), | ||||
|             cancel_token, | ||||
|         )); | ||||
|  | ||||
|         *self.is_online.write().await = true; | ||||
|         info!("Connected to the background service"); | ||||
|         if let Err(err) = self.service_event_tx.send(ServiceEvent::Online).await { | ||||
|             warn!("Error sending online event to the channel: {}", err); | ||||
|         } | ||||
|  | ||||
|         let _ = tokio::join!(read_handle, write_handle); | ||||
|         *self.is_online.write().await = false; | ||||
|  | ||||
|         // TODO connection was lost, cleanup the request pool | ||||
|  | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     async fn send_command<T: TryFrom<ResponseData>>( | ||||
|         &self, | ||||
|         payload: CommandPayload, | ||||
|     ) -> Result<T, ServerApiError> { | ||||
|         if !*self.is_online.read().await { | ||||
|             return Err("Background service is not running".into()); | ||||
|         } | ||||
|  | ||||
|         let (request, response_rx) = self.request_pool.create_request(payload).await; | ||||
|  | ||||
|         if let Err(err) = self.request_tx.send(request).await { | ||||
|             return Err(format!("Error sending request to the channel: {}", err).into()); | ||||
|         } | ||||
|  | ||||
|         response_rx | ||||
|             .await | ||||
|             .map_err(|_| "Error receiving response from the channel".into()) | ||||
|             .and_then(|response| { | ||||
|                 if response.success() { | ||||
|                     response | ||||
|                         .data() | ||||
|                         .try_into() | ||||
|                         .map_err(|_| "Error parsing response data".into()) | ||||
|                 } else { | ||||
|                     Err(response.message().into()) | ||||
|                 } | ||||
|             }) | ||||
|     } | ||||
|  | ||||
|     pub async fn connect( | ||||
|         &self, | ||||
|         server: String, | ||||
|         cookie: String, | ||||
|         user_agent: String, | ||||
|     ) -> Result<(), ServerApiError> { | ||||
|         self.send_command(Connect::new(server, cookie, user_agent).into()) | ||||
|             .await | ||||
|     } | ||||
|  | ||||
|     pub async fn disconnect(&self) -> Result<(), ServerApiError> { | ||||
|         self.send_command(Disconnect.into()).await | ||||
|     } | ||||
|  | ||||
|     pub async fn status(&self) -> Result<VpnStatus, ServerApiError> { | ||||
|         self.send_command(GetStatus.into()).await | ||||
|     } | ||||
| } | ||||
|  | ||||
| async fn handle_read( | ||||
|     read_stream: ReadHalf<UnixStream>, | ||||
|     request_pool: Arc<RequestPool>, | ||||
|     service_event_tx: mpsc::Sender<ServiceEvent>, | ||||
|     cancel_token: CancellationToken, | ||||
| ) { | ||||
|     let mut reader: Reader = read_stream.into(); | ||||
|  | ||||
|     loop { | ||||
|         match reader.read_multiple::<Response>().await { | ||||
|             Ok(responses) => { | ||||
|                 for response in responses { | ||||
|                     match response.request_id() { | ||||
|                         Some(id) => request_pool.complete_request(id, response).await, | ||||
|                         None => { | ||||
|                             if let Err(err) = service_event_tx.send(response.into()).await { | ||||
|                                 warn!("Error sending response to output channel: {}", err); | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             Err(err) if err.kind() == io::ErrorKind::ConnectionAborted => { | ||||
|                 warn!("Disconnected from the background service"); | ||||
|                 if let Err(err) = service_event_tx.send(ServiceEvent::Offline).await { | ||||
|                     warn!( | ||||
|                         "Error sending server disconnected event to channel: {}", | ||||
|                         err | ||||
|                     ); | ||||
|                 } | ||||
|                 cancel_token.cancel(); | ||||
|                 break; | ||||
|             } | ||||
|             Err(err) => { | ||||
|                 warn!("Error reading from server: {}", err); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| async fn handle_write( | ||||
|     write_stream: WriteHalf<UnixStream>, | ||||
|     request_rx: Arc<Mutex<mpsc::Receiver<Request>>>, | ||||
|     cancel_token: CancellationToken, | ||||
| ) { | ||||
|     let mut writer: Writer = write_stream.into(); | ||||
|     loop { | ||||
|         let mut request_rx = request_rx.lock().await; | ||||
|         tokio::select! { | ||||
|             Some(request) = request_rx.recv() => { | ||||
|                 if let Err(err) = writer.write(&request).await { | ||||
|                     warn!("Error writing to server: {}", err); | ||||
|                 } | ||||
|             } | ||||
|             _ = cancel_token.cancelled() => { | ||||
|                 info!("The read loop has been cancelled, exiting the write loop"); | ||||
|                 break; | ||||
|             } | ||||
|             else => { | ||||
|                 warn!("Error reading command from channel"); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										39
									
								
								gpcommon/src/cmd/connect.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,39 @@ | ||||
| use super::{Command, CommandContext, CommandError}; | ||||
| use crate::{ResponseData, VpnStatus}; | ||||
| use async_trait::async_trait; | ||||
| use serde::{Deserialize, Serialize}; | ||||
|  | ||||
| #[derive(Debug, Serialize, Deserialize, Clone)] | ||||
| pub struct Connect { | ||||
|     server: String, | ||||
|     cookie: String, | ||||
|     user_agent: String, | ||||
| } | ||||
|  | ||||
| impl Connect { | ||||
|     pub fn new(server: String, cookie: String, user_agent: String) -> Self { | ||||
|         Self { | ||||
|             server, | ||||
|             cookie, | ||||
|             user_agent, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[async_trait] | ||||
| impl Command for Connect { | ||||
|     async fn handle(&self, context: CommandContext) -> Result<ResponseData, CommandError> { | ||||
|         let vpn = context.server_context.vpn(); | ||||
|         let status = vpn.status().await; | ||||
|  | ||||
|         if status != VpnStatus::Disconnected { | ||||
|             return Err(format!("VPN is already in state: {:?}", status).into()); | ||||
|         } | ||||
|  | ||||
|         if let Err(err) = vpn.connect(&self.server, &self.cookie, &self.user_agent).await { | ||||
|             return Err(err.to_string().into()); | ||||
|         } | ||||
|  | ||||
|         Ok(ResponseData::Empty) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										15
									
								
								gpcommon/src/cmd/disconnect.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,15 @@ | ||||
| use super::{Command, CommandContext, CommandError}; | ||||
| use crate::ResponseData; | ||||
| use async_trait::async_trait; | ||||
| use serde::{Deserialize, Serialize}; | ||||
|  | ||||
| #[derive(Debug, Serialize, Deserialize, Clone)] | ||||
| pub struct Disconnect; | ||||
|  | ||||
| #[async_trait] | ||||
| impl Command for Disconnect { | ||||
|     async fn handle(&self, context: CommandContext) -> Result<ResponseData, CommandError> { | ||||
|         context.server_context.vpn().disconnect().await; | ||||
|         Ok(ResponseData::Empty) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										54
									
								
								gpcommon/src/cmd/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,54 @@ | ||||
| use crate::{response::ResponseData, server::ServerContext}; | ||||
| use async_trait::async_trait; | ||||
| use core::fmt::Debug; | ||||
| use std::{ | ||||
|     fmt::{self, Display}, | ||||
|     sync::Arc, | ||||
| }; | ||||
|  | ||||
| mod connect; | ||||
| mod disconnect; | ||||
| mod status; | ||||
|  | ||||
| pub use connect::Connect; | ||||
| pub use disconnect::Disconnect; | ||||
| pub use status::GetStatus; | ||||
|  | ||||
| #[derive(Debug)] | ||||
| pub(crate) struct CommandContext { | ||||
|     server_context: Arc<ServerContext>, | ||||
| } | ||||
|  | ||||
| impl From<Arc<ServerContext>> for CommandContext { | ||||
|     fn from(server_context: Arc<ServerContext>) -> Self { | ||||
|         Self { server_context } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug)] | ||||
| pub(crate) struct CommandError { | ||||
|     message: String, | ||||
| } | ||||
|  | ||||
| impl From<String> for CommandError { | ||||
|     fn from(message: String) -> Self { | ||||
|         Self { message } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Display for CommandError { | ||||
|     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||||
|         write!(f, "CommandError {:#?}", self.message) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[async_trait] | ||||
| pub(crate) trait Command: Send + Sync { | ||||
|     async fn handle(&self, context: CommandContext) -> Result<ResponseData, CommandError>; | ||||
| } | ||||
|  | ||||
| impl Debug for dyn Command { | ||||
|     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||||
|         write!(f, "Command") | ||||
|     } | ||||
| } | ||||
							
								
								
									
										16
									
								
								gpcommon/src/cmd/status.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,16 @@ | ||||
| use super::{Command, CommandContext, CommandError}; | ||||
| use crate::ResponseData; | ||||
| use async_trait::async_trait; | ||||
| use serde::{Deserialize, Serialize}; | ||||
|  | ||||
| #[derive(Debug, Serialize, Deserialize, Clone)] | ||||
| pub struct GetStatus; | ||||
|  | ||||
| #[async_trait] | ||||
| impl Command for GetStatus { | ||||
|     async fn handle(&self, context: CommandContext) -> Result<ResponseData, CommandError> { | ||||
|         let status = context.server_context.vpn().status().await; | ||||
|  | ||||
|         Ok(ResponseData::Status(status)) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										179
									
								
								gpcommon/src/connection.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,179 @@ | ||||
| use crate::request::Request; | ||||
| use crate::server::ServerContext; | ||||
| use crate::Reader; | ||||
| use crate::Response; | ||||
| use crate::ResponseData; | ||||
| use crate::VpnStatus; | ||||
| use crate::Writer; | ||||
| use log::{debug, info, warn}; | ||||
| use std::sync::Arc; | ||||
| use tokio::io::{self, ReadHalf, WriteHalf}; | ||||
| use tokio::net::UnixStream; | ||||
| use tokio::sync::{mpsc, watch}; | ||||
| use tokio_util::sync::CancellationToken; | ||||
|  | ||||
| async fn handle_read( | ||||
|     read_stream: ReadHalf<UnixStream>, | ||||
|     server_context: Arc<ServerContext>, | ||||
|     response_tx: mpsc::Sender<Response>, | ||||
|     peer_pid: Option<i32>, | ||||
|     cancel_token: CancellationToken, | ||||
| ) { | ||||
|     let mut reader: Reader = read_stream.into(); | ||||
|     let mut authenticated: Option<bool> = None; | ||||
|  | ||||
|     loop { | ||||
|         match reader.read_multiple::<Request>().await { | ||||
|             Ok(requests) => { | ||||
|                 if authenticated.is_none() { | ||||
|                     authenticated = Some(authenticate(peer_pid)); | ||||
|                 } | ||||
|                 if !authenticated.unwrap_or(false) { | ||||
|                     warn!("Client not authenticated, closing connection"); | ||||
|                     cancel_token.cancel(); | ||||
|                     break; | ||||
|                 } | ||||
|  | ||||
|                 for request in requests { | ||||
|                     debug!("Received client request: {:?}", request); | ||||
|  | ||||
|                     let command = request.command(); | ||||
|                     let context = server_context.clone().into(); | ||||
|  | ||||
|                     let mut response = match command.handle(context).await { | ||||
|                         Ok(data) => Response::from(data), | ||||
|                         Err(err) => Response::from(err.to_string()), | ||||
|                     }; | ||||
|                     response.set_request_id(request.id()); | ||||
|  | ||||
|                     let _ = response_tx.send(response).await; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             Err(err) if err.kind() == io::ErrorKind::ConnectionAborted => { | ||||
|                 info!("Client disconnected"); | ||||
|                 cancel_token.cancel(); | ||||
|                 break; | ||||
|             } | ||||
|  | ||||
|             Err(err) => { | ||||
|                 warn!("Error receiving request: {:?}", err); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| async fn handle_write( | ||||
|     write_stream: WriteHalf<UnixStream>, | ||||
|     mut response_rx: mpsc::Receiver<Response>, | ||||
|     cancel_token: CancellationToken, | ||||
| ) { | ||||
|     let mut writer: Writer = write_stream.into(); | ||||
|  | ||||
|     loop { | ||||
|         tokio::select! { | ||||
|             Some(response) = response_rx.recv() => { | ||||
|                 debug!("Sending response: {:?}", response); | ||||
|                 if let Err(err) = writer.write(&response).await { | ||||
|                     warn!("Error sending response: {:?}", err); | ||||
|                 } | ||||
|             } | ||||
|             _ = cancel_token.cancelled() => { | ||||
|                 info!("Exiting the write loop"); | ||||
|                 break; | ||||
|             } | ||||
|             else => { | ||||
|                 warn!("Error receiving response from channel"); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| async fn handle_status_change( | ||||
|     mut status_rx: watch::Receiver<VpnStatus>, | ||||
|     response_tx: mpsc::Sender<Response>, | ||||
|     cancel_token: CancellationToken, | ||||
| ) { | ||||
|     // Send the initial status | ||||
|     send_status(&status_rx, &response_tx).await; | ||||
|     debug!("Waiting for status change"); | ||||
|     let start_time = std::time::Instant::now(); | ||||
|  | ||||
|     loop { | ||||
|         tokio::select! { | ||||
|             _ = status_rx.changed() => { | ||||
|                 debug!("Status changed: {:?}", start_time.elapsed()); | ||||
|                 send_status(&status_rx, &response_tx).await; | ||||
|             } | ||||
|             _ = cancel_token.cancelled() => { | ||||
|                 info!("Exiting the status loop"); | ||||
|                 break; | ||||
|             } | ||||
|             else => { | ||||
|                 warn!("Error receiving status from channel"); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| async fn send_status(status_rx: &watch::Receiver<VpnStatus>, response_tx: &mpsc::Sender<Response>) { | ||||
|     let status = *status_rx.borrow(); | ||||
|     if let Err(err) = response_tx | ||||
|         .send(Response::from(ResponseData::Status(status))) | ||||
|         .await | ||||
|     { | ||||
|         warn!("Error sending status: {:?}", err); | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub(crate) async fn handle_connection(socket: UnixStream, context: Arc<ServerContext>) { | ||||
|     let peer_pid = peer_pid(&socket); | ||||
|     let (read_stream, write_stream) = io::split(socket); | ||||
|     let (response_tx, response_rx) = mpsc::channel::<Response>(32); | ||||
|     let cancel_token = CancellationToken::new(); | ||||
|     let status_rx = context.vpn().status_rx().await; | ||||
|  | ||||
|     // Read requests from the client | ||||
|     let read_handle = tokio::spawn(handle_read( | ||||
|         read_stream, | ||||
|         context.clone(), | ||||
|         response_tx.clone(), | ||||
|         peer_pid, | ||||
|         cancel_token.clone(), | ||||
|     )); | ||||
|  | ||||
|     // Write responses to the client | ||||
|     let write_handle = tokio::spawn(handle_write( | ||||
|         write_stream, | ||||
|         response_rx, | ||||
|         cancel_token.clone(), | ||||
|     )); | ||||
|  | ||||
|     // Watch for status changes | ||||
|     let status_handle = tokio::spawn(handle_status_change( | ||||
|         status_rx, | ||||
|         response_tx.clone(), | ||||
|         cancel_token, | ||||
|     )); | ||||
|  | ||||
|     let _ = tokio::join!(read_handle, write_handle, status_handle); | ||||
|  | ||||
|     debug!("Client connection closed"); | ||||
| } | ||||
|  | ||||
| fn peer_pid(socket: &UnixStream) -> Option<i32> { | ||||
|     match socket.peer_cred() { | ||||
|         Ok(ucred) => ucred.pid(), | ||||
|         Err(_) => None, | ||||
|     } | ||||
| } | ||||
|  | ||||
| // TODO - Implement authentication | ||||
| fn authenticate(peer_pid: Option<i32>) -> bool { | ||||
|     if let Some(pid) = peer_pid { | ||||
|         info!("Peer PID: {}", pid); | ||||
|         true | ||||
|     } else { | ||||
|         false | ||||
|     } | ||||
| } | ||||
							
								
								
									
										51
									
								
								gpcommon/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,51 @@ | ||||
| use data_encoding::HEXUPPER; | ||||
| use ring::digest::{Context, SHA256}; | ||||
| use std::{ | ||||
|     fs::File, | ||||
|     io::{BufReader, Read}, | ||||
|     path::Path, | ||||
| }; | ||||
|  | ||||
| pub const SOCKET_PATH: &str = "/tmp/gpservice.sock"; | ||||
|  | ||||
| mod client; | ||||
| mod cmd; | ||||
| mod connection; | ||||
| mod reader; | ||||
| mod request; | ||||
| mod response; | ||||
| pub mod server; | ||||
| mod vpn; | ||||
| mod writer; | ||||
|  | ||||
| pub(crate) use request::Request; | ||||
| pub(crate) use request::RequestPool; | ||||
|  | ||||
| pub use response::Response; | ||||
| pub use response::ResponseData; | ||||
| pub use response::TryFromResponseDataError; | ||||
|  | ||||
| pub(crate) use reader::Reader; | ||||
| pub(crate) use writer::Writer; | ||||
|  | ||||
| pub use client::Client; | ||||
| pub use client::ServerApiError; | ||||
| pub use client::ClientStatus; | ||||
| pub use vpn::VpnStatus; | ||||
|  | ||||
| pub fn sha256_digest<P: AsRef<Path>>(file_path: P) -> Result<String, std::io::Error> { | ||||
|     let input = File::open(file_path)?; | ||||
|     let mut reader = BufReader::new(input); | ||||
|  | ||||
|     let mut context = Context::new(&SHA256); | ||||
|     let mut buffer = [0; 1024]; | ||||
|  | ||||
|     loop { | ||||
|         let count = reader.read(&mut buffer)?; | ||||
|         if count == 0 { | ||||
|             break; | ||||
|         } | ||||
|         context.update(&buffer[..count]); | ||||
|     } | ||||
|     Ok(HEXUPPER.encode(context.finish().as_ref())) | ||||
| } | ||||
							
								
								
									
										43
									
								
								gpcommon/src/reader.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,43 @@ | ||||
| use serde::Deserialize; | ||||
| use tokio::io::{self, AsyncReadExt, ReadHalf}; | ||||
| use tokio::net::UnixStream; | ||||
|  | ||||
| pub(crate) struct Reader { | ||||
|     stream: ReadHalf<UnixStream>, | ||||
| } | ||||
|  | ||||
| impl From<ReadHalf<UnixStream>> for Reader { | ||||
|     fn from(stream: ReadHalf<UnixStream>) -> Self { | ||||
|         Self { stream } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Reader { | ||||
|     pub async fn read_multiple<T: for<'a> Deserialize<'a>>(&mut self) -> Result<Vec<T>, io::Error> { | ||||
|         let mut buffer = [0; 2048]; | ||||
|  | ||||
|         match self.stream.read(&mut buffer).await { | ||||
|             Ok(0) => Err(io::Error::new( | ||||
|                 io::ErrorKind::ConnectionAborted, | ||||
|                 "Peer disconnected", | ||||
|             )), | ||||
|             Ok(bytes_read) => { | ||||
|                 let response_str = String::from_utf8_lossy(&buffer[..bytes_read]); | ||||
|                 let responses: Vec<&str> = response_str.split("\n\n").collect(); | ||||
|                 let responses = responses | ||||
|                     .iter() | ||||
|                     .filter_map(|r| { | ||||
|                         if !r.is_empty() { | ||||
|                             serde_json::from_str(r).ok() | ||||
|                         } else { | ||||
|                             None | ||||
|                         } | ||||
|                     }) | ||||
|                     .collect::<Vec<T>>(); | ||||
|  | ||||
|                 Ok(responses) | ||||
|             } | ||||
|             Err(err) => Err(err), | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										105
									
								
								gpcommon/src/request.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,105 @@ | ||||
| use crate::cmd::{Command, Connect, Disconnect, GetStatus}; | ||||
| use crate::Response; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use std::sync::Arc; | ||||
| use tokio::sync::{oneshot, RwLock}; | ||||
|  | ||||
| #[derive(Debug, Serialize, Deserialize)] | ||||
| pub(crate) struct Request { | ||||
|     id: u64, | ||||
|     payload: CommandPayload, | ||||
| } | ||||
|  | ||||
| impl Request { | ||||
|     fn new(id: u64, payload: CommandPayload) -> Self { | ||||
|         Self { id, payload } | ||||
|     } | ||||
|  | ||||
|     pub fn id(&self) -> u64 { | ||||
|         self.id | ||||
|     } | ||||
|  | ||||
|     pub fn command(&self) -> Box<dyn Command> { | ||||
|         match &self.payload { | ||||
|             CommandPayload::GetStatus(status) => Box::new(status.clone()), | ||||
|             CommandPayload::Connect(connect) => Box::new(connect.clone()), | ||||
|             CommandPayload::Disconnect(disconnect) => Box::new(disconnect.clone()), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Serialize, Deserialize)] | ||||
| pub(crate) enum CommandPayload { | ||||
|     GetStatus(GetStatus), | ||||
|     Connect(Connect), | ||||
|     Disconnect(Disconnect), | ||||
| } | ||||
|  | ||||
| impl From<GetStatus> for CommandPayload { | ||||
|     fn from(status: GetStatus) -> Self { | ||||
|         Self::GetStatus(status) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl From<Connect> for CommandPayload { | ||||
|     fn from(connect: Connect) -> Self { | ||||
|         Self::Connect(connect) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl From<Disconnect> for CommandPayload { | ||||
|     fn from(disconnect: Disconnect) -> Self { | ||||
|         Self::Disconnect(disconnect) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug)] | ||||
| struct RequestHandle { | ||||
|     id: u64, | ||||
|     response_tx: oneshot::Sender<Response>, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Default)] | ||||
| struct IdGenerator { | ||||
|     current_id: u64, | ||||
| } | ||||
|  | ||||
| impl IdGenerator { | ||||
|     fn next(&mut self) -> u64 { | ||||
|         let current_id = self.current_id; | ||||
|         self.current_id = self.current_id.wrapping_add(1); | ||||
|         current_id | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Default)] | ||||
| pub(crate) struct RequestPool { | ||||
|     id_generator: Arc<RwLock<IdGenerator>>, | ||||
|     request_handles: Arc<RwLock<Vec<RequestHandle>>>, | ||||
| } | ||||
|  | ||||
| impl RequestPool { | ||||
|     pub async fn create_request( | ||||
|         &self, | ||||
|         payload: CommandPayload, | ||||
|     ) -> (Request, oneshot::Receiver<Response>) { | ||||
|         let id = self.id_generator.write().await.next(); | ||||
|         let (response_tx, response_rx) = oneshot::channel(); | ||||
|         let request_handle = RequestHandle { id, response_tx }; | ||||
|  | ||||
|         self.request_handles.write().await.push(request_handle); | ||||
|         (Request::new(id, payload), response_rx) | ||||
|     } | ||||
|  | ||||
|     pub async fn complete_request(&self, id: u64, response: Response) { | ||||
|         let mut request_handles = self.request_handles.write().await; | ||||
|         let request_handle = request_handles | ||||
|             .iter() | ||||
|             .position(|handle| handle.id == id) | ||||
|             .map(|index| request_handles.remove(index)); | ||||
|  | ||||
|         if let Some(request_handle) = request_handle { | ||||
|             let _ = request_handle.response_tx.send(response); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										113
									
								
								gpcommon/src/response.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,113 @@ | ||||
| use crate::vpn::VpnStatus; | ||||
| use serde::{Deserialize, Serialize}; | ||||
|  | ||||
| #[derive(Debug, Serialize, Deserialize, Clone)] | ||||
| pub struct Response { | ||||
|     request_id: Option<u64>, | ||||
|     success: bool, | ||||
|     message: String, | ||||
|     data: ResponseData, | ||||
| } | ||||
|  | ||||
| impl From<ResponseData> for Response { | ||||
|     fn from(data: ResponseData) -> Self { | ||||
|         Self { | ||||
|             request_id: None, | ||||
|             success: true, | ||||
|             message: String::from("Success"), | ||||
|             data, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl From<String> for Response { | ||||
|     fn from(message: String) -> Self { | ||||
|         Self { | ||||
|             request_id: None, | ||||
|             success: false, | ||||
|             message, | ||||
|             data: ResponseData::Empty, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Response { | ||||
|     pub fn success(&self) -> bool { | ||||
|         self.success | ||||
|     } | ||||
|  | ||||
|     pub fn message(&self) -> &str { | ||||
|         &self.message | ||||
|     } | ||||
|  | ||||
|     pub fn set_request_id(&mut self, command_id: u64) { | ||||
|         self.request_id = Some(command_id); | ||||
|     } | ||||
|  | ||||
|     pub fn request_id(&self) -> Option<u64> { | ||||
|         self.request_id | ||||
|     } | ||||
|  | ||||
|     pub fn data(&self) -> ResponseData { | ||||
|         self.data | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Serialize, Deserialize, Clone, Copy)] | ||||
| pub enum ResponseData { | ||||
|     Status(VpnStatus), | ||||
|     Empty, | ||||
| } | ||||
|  | ||||
| impl From<VpnStatus> for ResponseData { | ||||
|     fn from(status: VpnStatus) -> Self { | ||||
|         Self::Status(status) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl From<()> for ResponseData { | ||||
|     fn from(_: ()) -> Self { | ||||
|         Self::Empty | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug)] | ||||
| pub struct TryFromResponseDataError { | ||||
|     message: String, | ||||
| } | ||||
|  | ||||
| impl std::fmt::Display for TryFromResponseDataError { | ||||
|     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||||
|         write!(f, "Invalid ResponseData: {}", self.message) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl From<&str> for TryFromResponseDataError { | ||||
|     fn from(message: &str) -> Self { | ||||
|         Self { | ||||
|             message: message.into(), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl TryFrom<ResponseData> for VpnStatus { | ||||
|     type Error = TryFromResponseDataError; | ||||
|  | ||||
|     fn try_from(value: ResponseData) -> Result<Self, Self::Error> { | ||||
|         match value { | ||||
|             ResponseData::Status(status) => Ok(status), | ||||
|             _ => Err("ResponseData is not a VpnStatus".into()), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl TryFrom<ResponseData> for () { | ||||
|     type Error = TryFromResponseDataError; | ||||
|  | ||||
|     fn try_from(value: ResponseData) -> Result<Self, Self::Error> { | ||||
|         match value { | ||||
|             ResponseData::Empty => Ok(()), | ||||
|             _ => Err("ResponseData is not empty".into()), | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										91
									
								
								gpcommon/src/server.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,91 @@ | ||||
| use crate::{connection::handle_connection, vpn::Vpn}; | ||||
| use log::{warn, info}; | ||||
| use std::{future::Future, os::unix::prelude::PermissionsExt, path::Path, sync::Arc}; | ||||
| use tokio::fs; | ||||
| use tokio::net::{UnixListener, UnixStream}; | ||||
|  | ||||
| #[derive(Debug, Default)] | ||||
| pub(crate) struct ServerContext { | ||||
|     vpn: Arc<Vpn>, | ||||
| } | ||||
|  | ||||
| struct Server { | ||||
|     socket_path: String, | ||||
|     context: Arc<ServerContext>, | ||||
| } | ||||
|  | ||||
| impl ServerContext { | ||||
|     pub fn vpn(&self) -> Arc<Vpn> { | ||||
|         self.vpn.clone() | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Server { | ||||
|     fn new(socket_path: String) -> Self { | ||||
|         Self { | ||||
|             socket_path, | ||||
|             context: Default::default(), | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // Check if an instance of the server is already running. | ||||
|     // by trying to connect to the socket. | ||||
|     async fn is_running(&self) -> bool { | ||||
|         UnixStream::connect(&self.socket_path).await.is_ok() | ||||
|     } | ||||
|  | ||||
|     async fn start(&self) -> Result<(), Box<dyn std::error::Error>> { | ||||
|         if Path::new(&self.socket_path).exists() { | ||||
|             fs::remove_file(&self.socket_path).await?; | ||||
|         } | ||||
|  | ||||
|         let listener = UnixListener::bind(&self.socket_path)?; | ||||
|         info!("Listening on socket: {:?}", listener.local_addr()?); | ||||
|  | ||||
|         let metadata = fs::metadata(&self.socket_path).await?; | ||||
|         let mut permissions = metadata.permissions(); | ||||
|         permissions.set_mode(0o666); | ||||
|         fs::set_permissions(&self.socket_path, permissions).await?; | ||||
|  | ||||
|         loop { | ||||
|             match listener.accept().await { | ||||
|                 Ok((socket, _)) => { | ||||
|                     info!("Accepted connection: {:?}", socket.peer_addr()?); | ||||
|                     tokio::spawn(handle_connection(socket, self.context.clone())); | ||||
|                 } | ||||
|                 Err(err) => { | ||||
|                     warn!("Error accepting connection: {:?}", err); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     async fn stop(&self) -> Result<(), Box<dyn std::error::Error>> { | ||||
|         self.context.vpn().disconnect().await; | ||||
|         fs::remove_file(&self.socket_path).await?; | ||||
|         Ok(()) | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub async fn run( | ||||
|     socket_path: &str, | ||||
|     shutdown: impl Future, | ||||
| ) -> Result<(), Box<dyn std::error::Error>> { | ||||
|     let server = Server::new(socket_path.to_string()); | ||||
|  | ||||
|     if server.is_running().await { | ||||
|         return Err("Another instance of the server is already running".into()); | ||||
|     } | ||||
|  | ||||
|     tokio::select! { | ||||
|         res = server.start() => { | ||||
|             res? | ||||
|         }, | ||||
|         _ = shutdown => { | ||||
|             info!("Shutting down the server..."); | ||||
|             server.stop().await?; | ||||
|         }, | ||||
|     } | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
							
								
								
									
										60
									
								
								gpcommon/src/vpn/ffi.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,60 @@ | ||||
| use log::{debug, info, trace, warn}; | ||||
| use std::ffi::{c_char, c_int, c_void}; | ||||
| use tokio::sync::mpsc; | ||||
|  | ||||
| #[repr(C)] | ||||
| #[derive(Debug, Copy, Clone)] | ||||
| pub(crate) struct Options { | ||||
|     pub server: *const c_char, | ||||
|     pub cookie: *const c_char, | ||||
|     pub user_agent: *const c_char, | ||||
|     pub user_data: *mut c_void, | ||||
|  | ||||
|     pub script: *const c_char, | ||||
|     pub certificate: *const c_char, | ||||
|     pub servercert: *const c_char, | ||||
| } | ||||
|  | ||||
| #[link(name = "vpn")] | ||||
| extern "C" { | ||||
|     #[link_name = "vpn_connect"] | ||||
|     pub(crate) fn connect(options: *const Options) -> c_int; | ||||
|  | ||||
|     #[link_name = "vpn_disconnect"] | ||||
|     pub(crate) fn disconnect(); | ||||
| } | ||||
|  | ||||
| #[no_mangle] | ||||
| extern "C" fn on_vpn_connected(value: i32, sender: *mut c_void) { | ||||
|     let sender = unsafe { &*(sender as *const mpsc::Sender<i32>) }; | ||||
|     sender | ||||
|         .blocking_send(value) | ||||
|         .expect("Failed to send VPN connection code"); | ||||
| } | ||||
|  | ||||
| // Logger used in the C code. | ||||
| // level: 0 = error, 1 = info, 2 = debug, 3 = trace | ||||
| // map the error level log in openconnect to the warning level | ||||
| #[no_mangle] | ||||
| extern "C" fn vpn_log(level: i32, message: *const c_char) { | ||||
|     let message = unsafe { std::ffi::CStr::from_ptr(message) }; | ||||
|     let message = message.to_str().unwrap_or("Invalid log message"); | ||||
|     // Strip the trailing newline | ||||
|     let message = message.trim_end_matches('\n'); | ||||
|  | ||||
|     if level == 0 { | ||||
|         warn!("{}", message); | ||||
|     } else if level == 1 { | ||||
|         info!("{}", message); | ||||
|     } else if level == 2 { | ||||
|         debug!("{}", message); | ||||
|     } else if level == 3 { | ||||
|         trace!("{}", message); | ||||
|     } else { | ||||
|         warn!( | ||||
|             "Unknown log level: {}, enable DEBUG log level to see more details", | ||||
|             level | ||||
|         ); | ||||
|         debug!("{}", message); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										164
									
								
								gpcommon/src/vpn/gpconf.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,164 @@ | ||||
| use anyhow::{Error, Ok, Result}; | ||||
| use configparser::ini::Ini; | ||||
| use lexopt::Parser; | ||||
| use log::warn; | ||||
|  | ||||
| const GPCONF_PATH: &str = "/etc/gpservice/gp.conf"; | ||||
| const DEFAULT_SECTION: &str = "*"; | ||||
| const PROP_OPENCONNECT_ARGS: &str = "openconnect-args"; | ||||
|  | ||||
| /// A struct representing the CLI arguments for the `openconnect` command. | ||||
| /// Supports most of the options from the `openconnect` command line. | ||||
| #[derive(Debug, Default)] | ||||
| pub(crate) struct OpenconnectArgs { | ||||
|     script: Option<String>, | ||||
|     certificate: Option<String>, | ||||
|     servercert: Option<String>, | ||||
| } | ||||
|  | ||||
| impl OpenconnectArgs { | ||||
|     pub fn script(&self) -> Option<String> { | ||||
|         self.script.to_owned() | ||||
|     } | ||||
|  | ||||
|     pub fn certificate(&self) -> Option<String> { | ||||
|         self.certificate.to_owned() | ||||
|     } | ||||
|  | ||||
|     pub fn servercert(&self) -> Option<String> { | ||||
|         self.servercert.to_owned() | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Read the `gp.conf` file and return the `openconnect` arguments for the given server. | ||||
| /// If the server is not found, the default section is used. | ||||
| pub(crate) fn read_conf(server: &str) -> Result<OpenconnectArgs> { | ||||
|     read_conf_from(GPCONF_PATH, server) | ||||
| } | ||||
|  | ||||
| /// Private function to read the `openconnect` arguments for the given server from a given path. | ||||
| /// Make it easy to write tests. | ||||
| fn read_conf_from(path: &str, server: &str) -> Result<OpenconnectArgs> { | ||||
|     let mut config = Ini::new(); | ||||
|  | ||||
|     config.set_default_section(DEFAULT_SECTION); | ||||
|     config.set_multiline(true); | ||||
|  | ||||
|     config.load(path).map_err(Error::msg)?; | ||||
|  | ||||
|     let default_openconnect_config = config | ||||
|         .get(DEFAULT_SECTION, PROP_OPENCONNECT_ARGS) | ||||
|         .unwrap_or_default(); | ||||
|     let server_openconnect_config = config | ||||
|         .get(server, PROP_OPENCONNECT_ARGS) | ||||
|         .unwrap_or(default_openconnect_config); | ||||
|  | ||||
|     let args = shlex::split(&server_openconnect_config).unwrap_or_default(); | ||||
|     parse_args(&args) | ||||
| } | ||||
|  | ||||
| fn parse_args(args: &Vec<String>) -> Result<OpenconnectArgs> { | ||||
|     use lexopt::prelude::*; | ||||
|  | ||||
|     let mut parser = Parser::from_args(args); | ||||
|  | ||||
|     let mut script: Option<String> = None; | ||||
|     let mut certificate: Option<String> = None; | ||||
|     let mut servercert: Option<String> = None; | ||||
|     while let Some(arg) = parser.next()? { | ||||
|         match arg { | ||||
|             Long("script") | Short('s') => { | ||||
|                 script = Some(parser.value()?.parse()?); | ||||
|             } | ||||
|             Long("certificate") | Short('c') => { | ||||
|                 certificate = Some(parser.value()?.parse()?); | ||||
|             } | ||||
|             Long("servercert") => { | ||||
|                 servercert = Some(parser.value()?.parse()?); | ||||
|             } | ||||
|             _ => { | ||||
|                 warn!("Ignoring unknown argument: {}", arg.unexpected()); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     Ok(OpenconnectArgs { | ||||
|         script, | ||||
|         certificate, | ||||
|         servercert, | ||||
|     }) | ||||
| } | ||||
|  | ||||
| #[cfg(test)] | ||||
| mod tests { | ||||
|     use std::io::Write; | ||||
|     use tempfile::NamedTempFile; | ||||
|  | ||||
|     use super::*; | ||||
|  | ||||
|     // Macro to create a temporary file with the given content. | ||||
|     macro_rules! tempfile { | ||||
|         ($content:expr) => {{ | ||||
|             let mut file = NamedTempFile::new().unwrap(); | ||||
|             write!(file, $content).unwrap(); | ||||
|             file | ||||
|         }}; | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn test_config_not_found() { | ||||
|         let args = read_conf_from("non-existent-file", "server"); | ||||
|         assert!(args.is_err()); | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn test_default_config() { | ||||
|         let file = tempfile!( | ||||
|             r#" | ||||
| [*] | ||||
| openconnect-args=--script=/script.sh | ||||
| "# | ||||
|         ); | ||||
|         let path = file.path().to_str().unwrap(); | ||||
|         let args = read_conf_from(path, "any server").unwrap(); | ||||
|  | ||||
|         assert_eq!(args.script, Some("/script.sh".to_string())); | ||||
|         assert_eq!(args.certificate, None); | ||||
|         assert_eq!(args.servercert, None); | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn test_server_config() { | ||||
|         let file = tempfile!( | ||||
|             r#" | ||||
| [*] | ||||
| openconnect-args=--script=/script.sh | ||||
|  | ||||
| [server] | ||||
| openconnect-args=--certificate=/cert.pem | ||||
| "# | ||||
|         ); | ||||
|         let path = file.path().to_str().unwrap(); | ||||
|         let args = read_conf_from(path, "server").unwrap(); | ||||
|  | ||||
|         assert_eq!(args.script, None); | ||||
|         assert_eq!(args.certificate, Some("/cert.pem".to_string())); | ||||
|         assert_eq!(args.servercert, None); | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn test_ignore_unknown_args() { | ||||
|         let file = tempfile!( | ||||
|             r#" | ||||
| [*] | ||||
| openconnect-args=--script=/script.sh --unknown-arg -c /cert.pem | ||||
| "# | ||||
|         ); | ||||
|         let path = file.path().to_str().unwrap(); | ||||
|         let args = read_conf_from(path, "server").unwrap(); | ||||
|  | ||||
|         assert_eq!(args.script, Some("/script.sh".to_string())); | ||||
|         assert_eq!(args.certificate, Some("/cert.pem".to_string())); | ||||
|         assert_eq!(args.servercert, None); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										145
									
								
								gpcommon/src/vpn/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,145 @@ | ||||
| use self::vpn_options::VpnOptions; | ||||
| use log::{debug, info, warn}; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use std::ffi::c_void; | ||||
| use std::sync::Arc; | ||||
| use std::thread; | ||||
| use tokio::sync::watch; | ||||
| use tokio::sync::{mpsc, Mutex}; | ||||
|  | ||||
| mod ffi; | ||||
| mod gpconf; | ||||
| mod vpn_options; | ||||
| mod vpnc_script; | ||||
|  | ||||
| #[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize)] | ||||
| #[serde(rename_all = "lowercase")] | ||||
| pub enum VpnStatus { | ||||
|     Disconnected, | ||||
|     Connecting, | ||||
|     Connected, | ||||
|     Disconnecting, | ||||
| } | ||||
|  | ||||
| #[derive(Debug)] | ||||
| struct StatusHolder { | ||||
|     status: VpnStatus, | ||||
|     status_tx: watch::Sender<VpnStatus>, | ||||
|     status_rx: watch::Receiver<VpnStatus>, | ||||
| } | ||||
|  | ||||
| impl Default for StatusHolder { | ||||
|     fn default() -> Self { | ||||
|         let (status_tx, status_rx) = watch::channel(VpnStatus::Disconnected); | ||||
|  | ||||
|         Self { | ||||
|             status: VpnStatus::Disconnected, | ||||
|             status_tx, | ||||
|             status_rx, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl StatusHolder { | ||||
|     fn set(&mut self, status: VpnStatus) { | ||||
|         self.status = status; | ||||
|         if let Err(err) = self.status_tx.send(status) { | ||||
|             warn!("Failed to send VPN status: {}", err); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fn status_rx(&self) -> watch::Receiver<VpnStatus> { | ||||
|         self.status_rx.clone() | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Default)] | ||||
| pub(crate) struct Vpn { | ||||
|     status_holder: Arc<Mutex<StatusHolder>>, | ||||
|     vpn_options: Arc<Mutex<Option<VpnOptions>>>, | ||||
| } | ||||
|  | ||||
| impl Vpn { | ||||
|     pub async fn status_rx(&self) -> watch::Receiver<VpnStatus> { | ||||
|         self.status_holder.lock().await.status_rx() | ||||
|     } | ||||
|  | ||||
|     pub async fn connect( | ||||
|         &self, | ||||
|         server: &str, | ||||
|         cookie: &str, | ||||
|         user_agent: &str, | ||||
|     ) -> Result<(), Box<dyn std::error::Error>> { | ||||
|         let mut options_builder = VpnOptions::builder(); | ||||
|         let mut options_builder = options_builder | ||||
|             .server(server) | ||||
|             .cookie(cookie) | ||||
|             .user_agent(user_agent); | ||||
|  | ||||
|         if let Ok(openconnect_args) = gpconf::read_conf(server) { | ||||
|             info!("Found openconnect args in /etc/gpservice/gp.conf"); | ||||
|             options_builder = options_builder.with_openconnect_args(openconnect_args); | ||||
|         } | ||||
|         // Save the VPN options so we can use them later, e.g. reconnect | ||||
|         *self.vpn_options.lock().await = Some(options_builder.build()); | ||||
|  | ||||
|         let vpn_options = self.vpn_options.clone(); | ||||
|         let status_holder = self.status_holder.clone(); | ||||
|         let (vpn_tx, mut vpn_rx) = mpsc::channel::<i32>(1); | ||||
|  | ||||
|         thread::spawn(move || { | ||||
|             let vpn_tx = &vpn_tx as *const _ as *mut c_void; | ||||
|             let oc_options = vpn_options | ||||
|                 .blocking_lock() | ||||
|                 .as_ref() | ||||
|                 .expect("Failed to unwrap vpn_options") | ||||
|                 .as_oc_options(vpn_tx); | ||||
|  | ||||
|             // Start the VPN connection, this will block until the connection is closed | ||||
|             status_holder.blocking_lock().set(VpnStatus::Connecting); | ||||
|             let ret = unsafe { ffi::connect(&oc_options) }; | ||||
|  | ||||
|             info!("VPN connection closed with code: {}", ret); | ||||
|             status_holder.blocking_lock().set(VpnStatus::Disconnected); | ||||
|         }); | ||||
|  | ||||
|         info!("Waiting for the VPN connection..."); | ||||
|  | ||||
|         if let Some(cmd_pipe_fd) = vpn_rx.recv().await { | ||||
|             info!("VPN connection started, cmd_pipe_fd: {}", cmd_pipe_fd); | ||||
|             self.status_holder.lock().await.set(VpnStatus::Connected); | ||||
|         } else { | ||||
|             warn!("VPN connection failed to start"); | ||||
|         } | ||||
|  | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     pub async fn disconnect(&self) { | ||||
|         if self.status().await == VpnStatus::Disconnected { | ||||
|             info!("VPN is not connected, nothing to do"); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         info!("Disconnecting VPN..."); | ||||
|         self.status_holder | ||||
|             .lock() | ||||
|             .await | ||||
|             .set(VpnStatus::Disconnecting); | ||||
|         unsafe { ffi::disconnect() }; | ||||
|  | ||||
|         let mut status_rx = self.status_rx().await; | ||||
|         debug!("Waiting for the VPN to disconnect..."); | ||||
|  | ||||
|         while status_rx.changed().await.is_ok() { | ||||
|             if *status_rx.borrow() == VpnStatus::Disconnected { | ||||
|                 info!("VPN disconnected"); | ||||
|                 break; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub async fn status(&self) -> VpnStatus { | ||||
|         self.status_holder.lock().await.status | ||||
|     } | ||||
| } | ||||
							
								
								
									
										131
									
								
								gpcommon/src/vpn/vpn.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,131 @@ | ||||
| #include <stdio.h> | ||||
| #include <stdlib.h> | ||||
| #include <stdarg.h> | ||||
| #include <unistd.h> | ||||
| #include <sys/utsname.h> | ||||
| #include <openconnect.h> | ||||
|  | ||||
| #include "vpn.h" | ||||
|  | ||||
| void *g_user_data; | ||||
|  | ||||
| static int g_cmd_pipe_fd; | ||||
| const char *g_vpnc_script; | ||||
|  | ||||
| /* Validate the peer certificate */ | ||||
| static int validate_peer_cert(__attribute__((unused)) void *_vpninfo, const char *reason) | ||||
| { | ||||
|     INFO("Validating peer cert: %s", reason); | ||||
|     return 0; | ||||
| } | ||||
|  | ||||
| /* Print progress messages */ | ||||
| static void print_progress(__attribute__((unused)) void *_vpninfo, int level, const char *format, ...) | ||||
| { | ||||
|     va_list args; | ||||
|     va_start(args, format); | ||||
|     char *message = format_message(format, args); | ||||
|     va_end(args); | ||||
|  | ||||
|     if (message == NULL) | ||||
|     { | ||||
|         ERROR("Failed to format log message"); | ||||
|     } | ||||
|     else | ||||
|     { | ||||
|         LOG(level, message); | ||||
|         free(message); | ||||
|     } | ||||
| } | ||||
|  | ||||
| static void setup_tun_handler(void *_vpninfo) | ||||
| { | ||||
|     openconnect_setup_tun_device(_vpninfo, g_vpnc_script, NULL); | ||||
|     on_vpn_connected(g_cmd_pipe_fd, g_user_data); | ||||
| } | ||||
|  | ||||
| /* Initialize VPN connection */ | ||||
| int vpn_connect(const vpn_options *options) | ||||
| { | ||||
|     struct openconnect_info *vpninfo; | ||||
|     struct utsname utsbuf; | ||||
|  | ||||
|     g_user_data = options->user_data; | ||||
|     g_vpnc_script = options->script; | ||||
|  | ||||
|     vpninfo = openconnect_vpninfo_new(options->user_agent, validate_peer_cert, NULL, NULL, print_progress, NULL); | ||||
|  | ||||
|     if (!vpninfo) | ||||
|     { | ||||
|         ERROR("openconnect_vpninfo_new failed"); | ||||
|         return 1; | ||||
|     } | ||||
|  | ||||
|     openconnect_set_loglevel(vpninfo, 1); | ||||
|     openconnect_init_ssl(); | ||||
|     openconnect_set_protocol(vpninfo, "gp"); | ||||
|     openconnect_set_hostname(vpninfo, options->server); | ||||
|     openconnect_set_cookie(vpninfo, options->cookie); | ||||
|  | ||||
|     if (options->certificate) | ||||
|     { | ||||
|         INFO("Setting client certificate: %s", options->certificate); | ||||
|         openconnect_set_client_cert(vpninfo, options->certificate, NULL); | ||||
|     } | ||||
|  | ||||
|     if (options->servercert) { | ||||
|         INFO("Setting server certificate: %s", options->servercert); | ||||
|         openconnect_set_system_trust(vpninfo, 0); | ||||
|     } | ||||
|  | ||||
|     g_cmd_pipe_fd = openconnect_setup_cmd_pipe(vpninfo); | ||||
|     if (g_cmd_pipe_fd < 0) | ||||
|     { | ||||
|         ERROR("openconnect_setup_cmd_pipe failed"); | ||||
|         return 1; | ||||
|     } | ||||
|  | ||||
|     if (!uname(&utsbuf)) | ||||
|     { | ||||
|         openconnect_set_localname(vpninfo, utsbuf.nodename); | ||||
|     } | ||||
|  | ||||
|     // Essential step | ||||
|     if (openconnect_make_cstp_connection(vpninfo) != 0) | ||||
|     { | ||||
|         ERROR("openconnect_make_cstp_connection failed"); | ||||
|         return 1; | ||||
|     } | ||||
|  | ||||
|     if (openconnect_setup_dtls(vpninfo, 60) != 0) | ||||
|     { | ||||
|         openconnect_disable_dtls(vpninfo); | ||||
|     } | ||||
|  | ||||
|     // Essential step | ||||
|     openconnect_set_setup_tun_handler(vpninfo, setup_tun_handler); | ||||
|  | ||||
|     while (1) | ||||
|     { | ||||
|         int ret = openconnect_mainloop(vpninfo, 300, 10); | ||||
|  | ||||
|         if (ret) | ||||
|         { | ||||
|             INFO("openconnect_mainloop returned %d, exiting", ret); | ||||
|             openconnect_vpninfo_free(vpninfo); | ||||
|             return ret; | ||||
|         } | ||||
|  | ||||
|         INFO("openconnect_mainloop returned 0, reconnecting"); | ||||
|     } | ||||
| } | ||||
|  | ||||
| /* Stop the VPN connection */ | ||||
| void vpn_disconnect() | ||||
| { | ||||
|     char cmd = OC_CMD_CANCEL; | ||||
|     if (write(g_cmd_pipe_fd, &cmd, 1) < 0) | ||||
|     { | ||||
|         ERROR("Failed to write to command pipe, VPN connection may not be stopped"); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										66
									
								
								gpcommon/src/vpn/vpn.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,66 @@ | ||||
| #include <stdio.h> | ||||
| #include <stdlib.h> | ||||
| #include <stdarg.h> | ||||
| #include <openconnect.h> | ||||
|  | ||||
| typedef struct vpn_options | ||||
| { | ||||
|     const char *server; | ||||
|     const char *cookie; | ||||
|     const char *user_agent; | ||||
|     void *user_data; | ||||
|  | ||||
|     const char *script; | ||||
|     const char *certificate; | ||||
|     const char *servercert; | ||||
| } vpn_options; | ||||
|  | ||||
| int vpn_connect(const vpn_options *options); | ||||
| void vpn_disconnect(); | ||||
|  | ||||
| extern void on_vpn_connected(int cmd_pipe_fd, void *user_data); | ||||
| extern void vpn_log(int level, const char *msg); | ||||
|  | ||||
| static char *format_message(const char *format, va_list args) | ||||
| { | ||||
|     va_list args_copy; | ||||
|     va_copy(args_copy, args); | ||||
|     int len = vsnprintf(NULL, 0, format, args_copy); | ||||
|     va_end(args_copy); | ||||
|  | ||||
|     char *buffer = malloc(len + 1); | ||||
|     if (buffer == NULL) | ||||
|     { | ||||
|         return NULL; | ||||
|     } | ||||
|  | ||||
|     vsnprintf(buffer, len + 1, format, args); | ||||
|     return buffer; | ||||
| } | ||||
|  | ||||
| static void _log(int level, ...) | ||||
| { | ||||
|     va_list args; | ||||
|     va_start(args, level); | ||||
|  | ||||
|     char *format = va_arg(args, char *); | ||||
|     char *message = format_message(format, args); | ||||
|  | ||||
|     va_end(args); | ||||
|  | ||||
|     if (message == NULL) | ||||
|     { | ||||
|         vpn_log(PRG_ERR, "Failed to format log message"); | ||||
|     } | ||||
|     else | ||||
|     { | ||||
|         vpn_log(level, message); | ||||
|         free(message); | ||||
|     } | ||||
| } | ||||
|  | ||||
| #define LOG(level, ...) _log(level, __VA_ARGS__) | ||||
| #define ERROR(...) LOG(PRG_ERR, __VA_ARGS__) | ||||
| #define INFO(...) LOG(PRG_INFO, __VA_ARGS__) | ||||
| #define DEBUG(...) LOG(PRG_DEBUG, __VA_ARGS__) | ||||
| #define TRACE(...) LOG(PRG_TRACE, __VA_ARGS__) | ||||
							
								
								
									
										93
									
								
								gpcommon/src/vpn/vpn_options.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,93 @@ | ||||
| use super::{ffi, gpconf::OpenconnectArgs, vpnc_script::find_default_vpnc_script}; | ||||
| use std::ffi::{c_char, c_void, CString}; | ||||
|  | ||||
| #[derive(Debug)] | ||||
| pub(crate) struct VpnOptions { | ||||
|     server: CString, | ||||
|     cookie: CString, | ||||
|     user_agent: CString, | ||||
|  | ||||
|     script: CString, | ||||
|     certificate: Option<CString>, | ||||
|     servercert: Option<CString>, | ||||
| } | ||||
|  | ||||
| impl VpnOptions { | ||||
|     pub fn builder() -> VpnOptionsBuilder { | ||||
|         VpnOptionsBuilder::default() | ||||
|     } | ||||
|  | ||||
|     pub fn as_oc_options(&self, user_data: *mut c_void) -> ffi::Options { | ||||
|         ffi::Options { | ||||
|             server: self.server.as_ptr(), | ||||
|             cookie: self.cookie.as_ptr(), | ||||
|             user_agent: self.user_agent.as_ptr(), | ||||
|             user_data, | ||||
|  | ||||
|             script: self.script.as_ptr(), | ||||
|             certificate: Self::option_as_ptr(&self.certificate), | ||||
|             servercert: Self::option_as_ptr(&self.servercert), | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fn option_as_ptr(value: &Option<CString>) -> *const c_char { | ||||
|         match value { | ||||
|             Some(value) => value.as_ptr(), | ||||
|             None => std::ptr::null(), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Default)] | ||||
| pub(crate) struct VpnOptionsBuilder { | ||||
|     server: String, | ||||
|     cookie: String, | ||||
|     user_agent: String, | ||||
|     openconnect_args: OpenconnectArgs, | ||||
| } | ||||
|  | ||||
| impl VpnOptionsBuilder { | ||||
|     pub fn server(&mut self, server: &str) -> &mut Self { | ||||
|         self.server = server.to_string(); | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     pub fn cookie(&mut self, cookie: &str) -> &mut Self { | ||||
|         self.cookie = cookie.to_string(); | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     pub fn user_agent(&mut self, user_agent: &str) -> &mut Self { | ||||
|         self.user_agent = user_agent.to_string(); | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     pub fn with_openconnect_args(&mut self, openconnect_args: OpenconnectArgs) -> &mut Self { | ||||
|         self.openconnect_args = openconnect_args; | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     fn to_cstr(value: &str) -> CString { | ||||
|         CString::new(value.to_string()).expect("Failed to convert to CString") | ||||
|     } | ||||
|  | ||||
|     pub fn build(&self) -> VpnOptions { | ||||
|         let openconnect_args = &self.openconnect_args; | ||||
|         let script = openconnect_args | ||||
|             .script() | ||||
|             .or_else(|| find_default_vpnc_script().map(|s| s.to_string())) | ||||
|             .map(|s| Self::to_cstr(&s)) | ||||
|             .unwrap_or_default(); | ||||
|         let certificate = openconnect_args.certificate().map(|s| Self::to_cstr(&s)); | ||||
|         let servercert = openconnect_args.servercert().map(|s| Self::to_cstr(&s)); | ||||
|  | ||||
|         VpnOptions { | ||||
|             server: Self::to_cstr(&self.server), | ||||
|             cookie: Self::to_cstr(&self.cookie), | ||||
|             user_agent: Self::to_cstr(&self.user_agent), | ||||
|             script, | ||||
|             certificate, | ||||
|             servercert, | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										21
									
								
								gpcommon/src/vpn/vpnc_script.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,21 @@ | ||||
| use std::path::Path; | ||||
|  | ||||
| use is_executable::IsExecutable; | ||||
|  | ||||
| const VPNC_SCRIPT_LOCATIONS: [&str; 4] = [ | ||||
|     "/usr/local/share/vpnc-scripts/vpnc-script", | ||||
|     "/usr/local/sbin/vpnc-script", | ||||
|     "/usr/share/vpnc-scripts/vpnc-script", | ||||
|     "/usr/sbin/vpnc-script /etc/vpnc/vpnc-script", | ||||
| ]; | ||||
|  | ||||
| pub(crate) fn find_default_vpnc_script() -> Option<&'static str> { | ||||
|     for location in VPNC_SCRIPT_LOCATIONS.iter() { | ||||
|         let path = Path::new(location); | ||||
|         if path.is_executable() { | ||||
|             return Some(location); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     None | ||||
| } | ||||
							
								
								
									
										24
									
								
								gpcommon/src/writer.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,24 @@ | ||||
| use serde::Serialize; | ||||
| use tokio::io::{self, AsyncWriteExt, WriteHalf}; | ||||
| use tokio::net::UnixStream; | ||||
|  | ||||
| pub(crate) struct Writer { | ||||
|     stream: WriteHalf<UnixStream>, | ||||
| } | ||||
|  | ||||
| impl From<WriteHalf<UnixStream>> for Writer { | ||||
|     fn from(stream: WriteHalf<UnixStream>) -> Self { | ||||
|         Self { stream } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Writer { | ||||
|     pub async fn write<T: Serialize>(&mut self, data: &T) -> Result<(), io::Error> { | ||||
|         let data = serde_json::to_string(data)?; | ||||
|         let data = format!("{}\n\n", data); | ||||
|  | ||||
|         self.stream.write_all(data.as_bytes()).await?; | ||||
|         self.stream.flush().await?; | ||||
|         Ok(()) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										24
									
								
								gpgui/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,24 @@ | ||||
| # Logs | ||||
| logs | ||||
| *.log | ||||
| npm-debug.log* | ||||
| yarn-debug.log* | ||||
| yarn-error.log* | ||||
| pnpm-debug.log* | ||||
| lerna-debug.log* | ||||
|  | ||||
| node_modules | ||||
| dist | ||||
| dist-ssr | ||||
| *.local | ||||
|  | ||||
| # Editor directories and files | ||||
| .vscode/* | ||||
| !.vscode/extensions.json | ||||
| .idea | ||||
| .DS_Store | ||||
| *.suo | ||||
| *.ntvs* | ||||
| *.njsproj | ||||
| *.sln | ||||
| *.sw? | ||||
							
								
								
									
										27
									
								
								gpgui/docs/gateway-login-response.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,27 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <jnlp> | ||||
|     <application-desc> | ||||
|         <argument>(null)</argument> | ||||
|         <argument>44d9988f3a3b5a247d359b1d39229add</argument> | ||||
|         <argument>1cb389d44ec35e98665211761b65a049ef7ba77e</argument> | ||||
|         <argument>GP-Gateway-N</argument> | ||||
|         <argument>user</argument> | ||||
|         <argument>AD_Authentication</argument> | ||||
|         <argument>vsys1</argument> | ||||
|         <argument>vpn.example.com</argument> | ||||
|         <argument>(null)</argument> | ||||
|         <argument></argument> | ||||
|         <argument></argument> | ||||
|         <argument></argument> | ||||
|         <argument>tunnel</argument> | ||||
|         <argument>-1</argument> | ||||
|         <argument>4100</argument> | ||||
|         <argument></argument> | ||||
|         <argument>xxxxxxxxxxxxxxxx</argument> | ||||
|         <argument>xxxxxxxxxxxxxxxx</argument> | ||||
|         <argument></argument> | ||||
|         <argument>4</argument> | ||||
|         <argument>unknown</argument> | ||||
|         <argument></argument> | ||||
|     </application-desc> | ||||
| </jnlp> | ||||
							
								
								
									
										212
									
								
								gpgui/docs/portal-config-response.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,212 @@ | ||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <policy> | ||||
|     <portal-name>vpn.example.com</portal-name> | ||||
|     <portal-config-version>4100</portal-config-version> | ||||
|     <version>6.0.1-19 </version> | ||||
|     <client-role>global-protect-full</client-role> | ||||
|     <agent-user-override-key>****</agent-user-override-key> | ||||
|     <root-ca> | ||||
|         <entry name="DigiCert Global Root CA"> | ||||
|             <cert> | ||||
|                 -----BEGIN CERTIFICATE----- | ||||
|                 -----END CERTIFICATE----- | ||||
|             </cert> | ||||
|             <install-in-cert-store>yes</install-in-cert-store> | ||||
|         </entry> | ||||
|         <entry name="Thawte RSA CA 2018"> | ||||
|             <cert> | ||||
|                 -----BEGIN CERTIFICATE----- | ||||
|                 -----END CERTIFICATE----- | ||||
|             </cert> | ||||
|             <install-in-cert-store>yes</install-in-cert-store> | ||||
|         </entry> | ||||
|         <entry name="Temp_VPN_Root_Certificate"> | ||||
|             <cert> | ||||
|                 -----BEGIN CERTIFICATE----- | ||||
|                 -----END CERTIFICATE----- | ||||
|             </cert> | ||||
|             <install-in-cert-store>no</install-in-cert-store> | ||||
|         </entry> | ||||
|     </root-ca> | ||||
|     <connect-method>on-demand</connect-method> | ||||
|     <pre-logon-then-on-demand>yes</pre-logon-then-on-demand> | ||||
|     <refresh-config>yes</refresh-config> | ||||
|     <refresh-config-interval>24</refresh-config-interval> | ||||
|     <authentication-modifier> | ||||
|         <none /> | ||||
|     </authentication-modifier> | ||||
|     <authentication-override> | ||||
|         <accept-cookie>yes</accept-cookie> | ||||
|         <generate-cookie>yes</generate-cookie> | ||||
|         <cookie-lifetime> | ||||
|             <lifetime-in-days>365</lifetime-in-days> | ||||
|         </cookie-lifetime> | ||||
|         <cookie-encrypt-decrypt-cert>vpn.example.com</cookie-encrypt-decrypt-cert> | ||||
|     </authentication-override> | ||||
|     <use-sso>yes</use-sso> | ||||
|     <ip-address></ip-address> | ||||
|     <host></host> | ||||
|     <gateways> | ||||
|         <cutoff-time>5</cutoff-time> | ||||
|         <external> | ||||
|             <list> | ||||
|                 <entry name="xxx.xxx.xxx.xxx"> | ||||
|                     <priority-rule> | ||||
|                         <entry name="Any"> | ||||
|                             <priority>1</priority> | ||||
|                         </entry> | ||||
|                     </priority-rule> | ||||
|                     <priority>1</priority> | ||||
|                     <description>vpn_gateway</description> | ||||
|                 </entry> | ||||
|             </list> | ||||
|         </external> | ||||
|     </gateways> | ||||
|     <gateways-v6> | ||||
|         <cutoff-time>5</cutoff-time> | ||||
|         <external> | ||||
|             <list> | ||||
|                 <entry name="vpn_gateway"> | ||||
|                     <ipv4>xxx.xxx.xxx.xxx</ipv4> | ||||
|                     <priority-rule> | ||||
|                         <entry name="Any"> | ||||
|                             <priority>1</priority> | ||||
|                         </entry> | ||||
|                     </priority-rule> | ||||
|                     <priority>1</priority> | ||||
|                 </entry> | ||||
|             </list> | ||||
|         </external> | ||||
|     </gateways-v6> | ||||
|     <agent-ui> | ||||
|         <can-save-password>yes</can-save-password> | ||||
|         <passcode></passcode> | ||||
|         <uninstall-passwd></uninstall-passwd> | ||||
|         <agent-user-override-timeout>0</agent-user-override-timeout> | ||||
|         <max-agent-user-overrides>0</max-agent-user-overrides> | ||||
|         <help-page></help-page> | ||||
|         <help-page-2></help-page-2> | ||||
|         <welcome-page> | ||||
|             <display>no</display> | ||||
|             <page></page> | ||||
|         </welcome-page> | ||||
|         <agent-user-override>allowed</agent-user-override> | ||||
|         <enable-advanced-view>yes</enable-advanced-view> | ||||
|         <enable-do-not-display-this-welcome-page-again>yes</enable-do-not-display-this-welcome-page-again> | ||||
|         <can-change-portal>yes</can-change-portal> | ||||
|         <show-agent-icon>yes</show-agent-icon> | ||||
|         <password-expiry-message></password-expiry-message> | ||||
|         <init-panel>no</init-panel> | ||||
|         <user-input-on-top>no</user-input-on-top> | ||||
|     </agent-ui> | ||||
|     <hip-collection> | ||||
|         <hip-report-interval>3600</hip-report-interval> | ||||
|         <max-wait-time>20</max-wait-time> | ||||
|         <collect-hip-data>yes</collect-hip-data> | ||||
|         <default> | ||||
|             <category> | ||||
|                 <member>antivirus</member> | ||||
|                 <member>anti-spyware</member> | ||||
|                 <member>host-info</member> | ||||
|                 <member>data-loss-prevention</member> | ||||
|                 <member>patch-management</member> | ||||
|                 <member>firewall</member> | ||||
|                 <member>anti-malware</member> | ||||
|                 <member>disk-backup</member> | ||||
|                 <member>disk-encryption</member> | ||||
|             </category> | ||||
|         </default> | ||||
|     </hip-collection> | ||||
|     <agent-config> | ||||
|         <save-user-credentials>1</save-user-credentials> | ||||
|         <portal-2fa>no</portal-2fa> | ||||
|         <internal-gateway-2fa>no</internal-gateway-2fa> | ||||
|         <auto-discovery-external-gateway-2fa>no</auto-discovery-external-gateway-2fa> | ||||
|         <manual-only-gateway-2fa>no</manual-only-gateway-2fa> | ||||
|         <disconnect-reasons></disconnect-reasons> | ||||
|         <uninstall>allowed</uninstall> | ||||
|         <client-upgrade>prompt</client-upgrade> | ||||
|         <enable-signout>yes</enable-signout> | ||||
|         <use-sso-pin>no</use-sso-pin> | ||||
|         <use-sso-macos>no</use-sso-macos> | ||||
|         <logout-remove-sso>yes</logout-remove-sso> | ||||
|         <krb-auth-fail-fallback>yes</krb-auth-fail-fallback> | ||||
|         <default-browser>no</default-browser> | ||||
|         <retry-tunnel>30</retry-tunnel> | ||||
|         <retry-timeout>5</retry-timeout> | ||||
|         <traffic-enforcement>no</traffic-enforcement> | ||||
|         <enforce-globalprotect>no</enforce-globalprotect> | ||||
|         <enforcer-exception-list /> | ||||
|         <enforcer-exception-list-domain /> | ||||
|         <captive-portal-exception-timeout>0</captive-portal-exception-timeout> | ||||
|         <captive-portal-login-url></captive-portal-login-url> | ||||
|         <traffic-blocking-notification-delay>15</traffic-blocking-notification-delay> | ||||
|         <display-traffic-blocking-notification-msg>yes</display-traffic-blocking-notification-msg> | ||||
|         <traffic-blocking-notification-msg><div style="font-family:'Helvetica | ||||
|             Neue';"><h1 style="color:red;text-align:center; margin: 0; font-size: | ||||
|             30px;">Notice</h1><p style="margin: 0;font-size: 15px; | ||||
|             line-height: 1.2em;">To access the network, you must first connect to | ||||
|             GlobalProtect.</p></div></traffic-blocking-notification-msg> | ||||
|         <allow-traffic-blocking-notification-dismissal>yes</allow-traffic-blocking-notification-dismissal> | ||||
|         <display-captive-portal-detection-msg>no</display-captive-portal-detection-msg> | ||||
|         <captive-portal-detection-msg><div style="font-family:'Helvetica | ||||
|             Neue';"><h1 style="color:red;text-align:center; margin: 0; font-size: | ||||
|             30px;">Captive Portal Detected</h1><p style="margin: 0; font-size: | ||||
|             15px; line-height: 1.2em;">GlobalProtect has temporarily permitted network | ||||
|             access for you to connect to the Internet. Follow instructions from your internet | ||||
|             provider.</p><p style="margin: 0; font-size: 15px; line-height: | ||||
|             1.2em;">If you let the connection time out, open GlobalProtect and click Connect | ||||
|             to try again.</p></div></captive-portal-detection-msg> | ||||
|         <captive-portal-notification-delay>5</captive-portal-notification-delay> | ||||
|         <certificate-store-lookup>user-and-machine</certificate-store-lookup> | ||||
|         <scep-certificate-renewal-period>7</scep-certificate-renewal-period> | ||||
|         <ext-key-usage-oid-for-client-cert></ext-key-usage-oid-for-client-cert> | ||||
|         <retain-connection-smartcard-removal>yes</retain-connection-smartcard-removal> | ||||
|         <user-accept-terms-before-creating-tunnel>no</user-accept-terms-before-creating-tunnel> | ||||
|         <rediscover-network>yes</rediscover-network> | ||||
|         <resubmit-host-info>yes</resubmit-host-info> | ||||
|         <can-continue-if-portal-cert-invalid>yes</can-continue-if-portal-cert-invalid> | ||||
|         <user-switch-tunnel-rename-timeout>0</user-switch-tunnel-rename-timeout> | ||||
|         <pre-logon-tunnel-rename-timeout>0</pre-logon-tunnel-rename-timeout> | ||||
|         <preserve-tunnel-upon-user-logoff-timeout>0</preserve-tunnel-upon-user-logoff-timeout> | ||||
|         <ipsec-failover-ssl>0</ipsec-failover-ssl> | ||||
|         <display-tunnel-fallback-notification>yes</display-tunnel-fallback-notification> | ||||
|         <ssl-only-selection>0</ssl-only-selection> | ||||
|         <tunnel-mtu>1400</tunnel-mtu> | ||||
|         <max-internal-gateway-connection-attempts>0</max-internal-gateway-connection-attempts> | ||||
|         <adv-internal-host-detection>no</adv-internal-host-detection> | ||||
|         <portal-timeout>30</portal-timeout> | ||||
|         <connect-timeout>60</connect-timeout> | ||||
|         <receive-timeout>30</receive-timeout> | ||||
|         <split-tunnel-option>network-traffic</split-tunnel-option> | ||||
|         <enforce-dns>yes</enforce-dns> | ||||
|         <append-local-search-domain>no</append-local-search-domain> | ||||
|         <flush-dns>no</flush-dns> | ||||
|         <auto-proxy-pac></auto-proxy-pac> | ||||
|         <proxy-multiple-autodetect>no</proxy-multiple-autodetect> | ||||
|         <use-proxy>yes</use-proxy> | ||||
|         <wsc-autodetect>yes</wsc-autodetect> | ||||
|         <mfa-enabled>no</mfa-enabled> | ||||
|         <mfa-listening-port>4501</mfa-listening-port> | ||||
|         <mfa-trusted-host-list /> | ||||
|         <mfa-notification-msg>You have attempted to access a protected resource that requires | ||||
|             additional authentication. Proceed to authenticate at</mfa-notification-msg> | ||||
|         <mfa-prompt-suppress-time>0</mfa-prompt-suppress-time> | ||||
|         <ipv6-preferred>yes</ipv6-preferred> | ||||
|         <change-password-message></change-password-message> | ||||
|         <log-gateway>no</log-gateway> | ||||
|         <cdl-log>no</cdl-log> | ||||
|         <dem-notification>yes</dem-notification> | ||||
|         <diagnostic-servers /> | ||||
|         <dem-agent>not-install</dem-agent> | ||||
|         <quarantine-add-message>Access to the network from this device has been restricted as per | ||||
|             your organization's security policy. Please contact your IT Administrator.</quarantine-add-message> | ||||
|         <quarantine-remove-message>Access to the network from this device has been restored as per | ||||
|             your organization's security policy.</quarantine-remove-message> | ||||
|  | ||||
|     </agent-config> | ||||
|     <user-email>user@example.com</user-email> | ||||
|     <portal-userauthcookie>xxxxxx</portal-userauthcookie> | ||||
|     <portal-prelogonuserauthcookie>xxxxxx</portal-prelogonuserauthcookie> | ||||
|     <config-digest>2d8e997765a2f59cbf80284b2f2fbd38</config-digest> | ||||
| </policy> | ||||
							
								
								
									
										19
									
								
								gpgui/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,19 @@ | ||||
| <!DOCTYPE html> | ||||
| <html lang="en"> | ||||
|   <head> | ||||
|     <meta charset="UTF-8" /> | ||||
|     <link rel="icon" type="image/svg+xml" href="/vite.svg" /> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1" /> | ||||
|     <title>GlobalProtect</title> | ||||
|   </head> | ||||
|   <body> | ||||
|     <script> | ||||
|       /* workaround to webview font size auto scaling */ | ||||
|       var htmlFontSize = getComputedStyle(document.documentElement).fontSize; | ||||
|       var ratio = parseInt(htmlFontSize, 10) / 16; | ||||
|       document.documentElement.style.fontSize = 16 / ratio + "px"; | ||||
|     </script> | ||||
|     <div id="root" data-tauri-drag-region></div> | ||||
|     <script type="module" src="/src/pages/main.tsx"></script> | ||||
|   </body> | ||||
| </html> | ||||
							
								
								
									
										38
									
								
								gpgui/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,38 @@ | ||||
| { | ||||
|   "name": "gpgui", | ||||
|   "private": true, | ||||
|   "version": "0.0.0", | ||||
|   "type": "module", | ||||
|   "scripts": { | ||||
|     "dev": "vite", | ||||
|     "build": "tsc && vite build", | ||||
|     "preview": "vite preview" | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "@emotion/react": "^11.10.6", | ||||
|     "@emotion/styled": "^11.10.6", | ||||
|     "@mui/icons-material": "^5.11.11", | ||||
|     "@mui/lab": "5.0.0-alpha.125", | ||||
|     "@mui/material": "^5.11.11", | ||||
|     "@tauri-apps/api": "^1.3.0", | ||||
|     "immer": "^10.0.2", | ||||
|     "jotai": "^2.2.1", | ||||
|     "jotai-immer": "^0.2.0", | ||||
|     "jotai-optics": "^0.3.0", | ||||
|     "notistack": "^3.0.1", | ||||
|     "optics-ts": "^2.4.0", | ||||
|     "react": "^18.2.0", | ||||
|     "react-dom": "^18.2.0", | ||||
|     "react-spinners": "^0.13.8", | ||||
|     "tauri-plugin-log-api": "github:tauri-apps/tauri-plugin-log#v1" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@tauri-apps/cli": "^1.3.1", | ||||
|     "@types/node": "^20.3.3", | ||||
|     "@types/react": "^18.0.27", | ||||
|     "@types/react-dom": "^18.0.10", | ||||
|     "@vitejs/plugin-react-swc": "^3.0.0", | ||||
|     "typescript": "^4.9.3", | ||||
|     "vite": "^4.1.0" | ||||
|   } | ||||
| } | ||||
							
								
								
									
										12
									
								
								gpgui/pages/settings/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,12 @@ | ||||
| <!DOCTYPE html> | ||||
| <html lang="en"> | ||||
|   <head> | ||||
|     <meta charset="UTF-8" /> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||||
|     <title>GlobalProtect Settings</title> | ||||
|   </head> | ||||
|   <body> | ||||
|     <div id="root"></div> | ||||
|     <script type="module" src="/src/pages/settings.tsx"></script> | ||||
|   </body> | ||||
| </html> | ||||
							
								
								
									
										1502
									
								
								gpgui/pnpm-lock.yaml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										12
									
								
								gpgui/public/auth.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,12 @@ | ||||
| <!DOCTYPE html> | ||||
| <html lang="en"> | ||||
| <head> | ||||
|     <meta charset="UTF-8"> | ||||
|     <meta http-equiv="X-UA-Compatible" content="IE=edge"> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||||
|     <title>GlobalProtect Login</title> | ||||
| </head> | ||||
| <body> | ||||
|     <p>Redirecting...</p> | ||||
| </body> | ||||
| </html> | ||||
							
								
								
									
										1
									
								
								gpgui/public/vite.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | ||||
| <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg> | ||||
| After Width: | Height: | Size: 1.5 KiB | 
							
								
								
									
										3
									
								
								gpgui/src-tauri/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,3 @@ | ||||
| # Generated by Cargo | ||||
| # will have compiled files and executables | ||||
| /target/ | ||||
							
								
								
									
										45
									
								
								gpgui/src-tauri/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,45 @@ | ||||
| [package] | ||||
| name = "app" | ||||
| version = "0.1.0" | ||||
| description = "A Tauri App" | ||||
| authors = ["you"] | ||||
| license = "" | ||||
| repository = "" | ||||
| default-run = "app" | ||||
| edition = "2021" | ||||
| rust-version = "1.59" | ||||
|  | ||||
| # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | ||||
|  | ||||
| [build-dependencies] | ||||
| tauri-build = { version = "1.3", features = [] } | ||||
|  | ||||
| [dependencies] | ||||
| gpcommon = { path = "../../gpcommon" } | ||||
| tauri = { version = "1.3", features = ["fs-write-file", "http-all", "os-all", "process-exit", "shell-open", "window-all", "window-data-url"] } | ||||
| tauri-plugin-log = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1", features = [ | ||||
|     "colored", | ||||
| ] } | ||||
| serde_json = "1.0" | ||||
| serde = { version = "1.0", features = ["derive"] } | ||||
| log = "0.4" | ||||
| webkit2gtk = "0.18.2" | ||||
| regex = "1" | ||||
| url = "2.3" | ||||
| tokio = { version = "1.14", features = ["full"] } | ||||
| veil = "0.1.6" | ||||
| whoami = "1.4.1" | ||||
| tauri-plugin-store = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" } | ||||
| openssl = "0.10" | ||||
| keyring = "2" | ||||
| aes-gcm = { version = "0.10", features = ["std"] } | ||||
| hex = "0.4" | ||||
| anyhow = "1.0" | ||||
|  | ||||
| [features] | ||||
| # by default Tauri runs in production mode | ||||
| # when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL | ||||
| default = ["custom-protocol"] | ||||
| # this feature is used for production builds where `devPath` points to the filesystem | ||||
| # DO NOT remove this | ||||
| custom-protocol = ["tauri/custom-protocol"] | ||||
							
								
								
									
										3
									
								
								gpgui/src-tauri/build.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,3 @@ | ||||
| fn main() { | ||||
|   tauri_build::build() | ||||
| } | ||||
							
								
								
									
										
											BIN
										
									
								
								gpgui/src-tauri/icons/128x128.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 11 KiB | 
							
								
								
									
										
											BIN
										
									
								
								gpgui/src-tauri/icons/128x128@2x.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 23 KiB | 
							
								
								
									
										
											BIN
										
									
								
								gpgui/src-tauri/icons/32x32.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 2.2 KiB | 
							
								
								
									
										
											BIN
										
									
								
								gpgui/src-tauri/icons/Square107x107Logo.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 9.0 KiB | 
							
								
								
									
										
											BIN
										
									
								
								gpgui/src-tauri/icons/Square142x142Logo.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 12 KiB | 
							
								
								
									
										
											BIN
										
									
								
								gpgui/src-tauri/icons/Square150x150Logo.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 13 KiB | 
							
								
								
									
										
											BIN
										
									
								
								gpgui/src-tauri/icons/Square284x284Logo.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 25 KiB | 
							
								
								
									
										
											BIN
										
									
								
								gpgui/src-tauri/icons/Square30x30Logo.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 2.0 KiB | 
							
								
								
									
										
											BIN
										
									
								
								gpgui/src-tauri/icons/Square310x310Logo.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 28 KiB | 
							
								
								
									
										
											BIN
										
									
								
								gpgui/src-tauri/icons/Square44x44Logo.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 3.3 KiB | 
							
								
								
									
										
											BIN
										
									
								
								gpgui/src-tauri/icons/Square71x71Logo.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 5.9 KiB | 
							
								
								
									
										
											BIN
										
									
								
								gpgui/src-tauri/icons/Square89x89Logo.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 7.4 KiB | 
							
								
								
									
										
											BIN
										
									
								
								gpgui/src-tauri/icons/StoreLogo.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 3.9 KiB | 
							
								
								
									
										
											BIN
										
									
								
								gpgui/src-tauri/icons/icon.icns
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								gpgui/src-tauri/icons/icon.ico
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 37 KiB | 
							
								
								
									
										
											BIN
										
									
								
								gpgui/src-tauri/icons/icon.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 49 KiB | 
							
								
								
									
										486
									
								
								gpgui/src-tauri/src/auth.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,486 @@ | ||||
| use crate::utils::{clear_webview_cookies, redact_url}; | ||||
| use log::{debug, info, warn}; | ||||
| use regex::Regex; | ||||
| use serde::de::Error; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use std::{sync::Arc, time::Duration}; | ||||
| use tauri::{AppHandle, Manager, Window, WindowUrl}; | ||||
| use tauri::{EventHandler, WindowEvent}; | ||||
| use tokio::sync::{mpsc, oneshot, Mutex}; | ||||
| use tokio::time::timeout; | ||||
| use veil::Redact; | ||||
| use webkit2gtk::gio::Cancellable; | ||||
| use webkit2gtk::glib::GString; | ||||
| use webkit2gtk::traits::{URIResponseExt, WebViewExt}; | ||||
| use webkit2gtk::{LoadEvent, WebResource, WebResourceExt}; | ||||
|  | ||||
| const AUTH_WINDOW_LABEL: &str = "auth_window"; | ||||
| const AUTH_ERROR_EVENT: &str = "auth-error"; | ||||
| const AUTH_REQUEST_EVENT: &str = "auth-request"; | ||||
| // Timeout to show the window if the token is not found in the response | ||||
| // It will be cancelled if the token is found in the response | ||||
| const SHOW_WINDOW_TIMEOUT: u64 = 3; | ||||
| // A fallback timeout to show the window in case the authentication process takes longer than expected | ||||
| const FALLBACK_SHOW_WINDOW_TIMEOUT: u64 = 15; | ||||
|  | ||||
| #[derive(Debug, Clone, Deserialize)] | ||||
| pub(crate) enum SamlBinding { | ||||
|     #[serde(rename = "REDIRECT")] | ||||
|     Redirect, | ||||
|     #[serde(rename = "POST")] | ||||
|     Post, | ||||
| } | ||||
|  | ||||
| #[derive(Redact, Clone, Deserialize)] | ||||
| pub(crate) struct AuthRequest { | ||||
|     #[serde(alias = "samlBinding")] | ||||
|     saml_binding: SamlBinding, | ||||
|     #[redact(fixed = 10)] | ||||
|     #[serde(alias = "samlRequest")] | ||||
|     saml_request: String, | ||||
| } | ||||
|  | ||||
| impl AuthRequest { | ||||
|     pub fn new(saml_binding: SamlBinding, saml_request: String) -> Self { | ||||
|         Self { | ||||
|             saml_binding, | ||||
|             saml_request, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl TryFrom<Option<&str>> for AuthRequest { | ||||
|     type Error = serde_json::Error; | ||||
|  | ||||
|     fn try_from(value: Option<&str>) -> Result<Self, Self::Error> { | ||||
|         match value { | ||||
|             Some(value) => serde_json::from_str(value), | ||||
|             None => Err(Error::custom("No auth request provided")), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Redact, Clone, Serialize)] | ||||
| pub(crate) struct AuthData { | ||||
|     #[redact] | ||||
|     username: Option<String>, | ||||
|     #[redact] | ||||
|     prelogin_cookie: Option<String>, | ||||
|     #[redact] | ||||
|     portal_userauthcookie: Option<String>, | ||||
| } | ||||
|  | ||||
| impl AuthData { | ||||
|     fn new( | ||||
|         username: Option<String>, | ||||
|         prelogin_cookie: Option<String>, | ||||
|         portal_userauthcookie: Option<String>, | ||||
|     ) -> Self { | ||||
|         Self { | ||||
|             username, | ||||
|             prelogin_cookie, | ||||
|             portal_userauthcookie, | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fn check(&self) -> bool { | ||||
|         let username_valid = self.username.clone().is_some_and(|username| !username.is_empty()); | ||||
|         let prelogin_cookie_valid = self.prelogin_cookie.clone().is_some_and(|val| val.len() > 5); | ||||
|         let portal_userauthcookie_valid = self.portal_userauthcookie.clone().is_some_and(|val| val.len() > 5); | ||||
|  | ||||
|         username_valid && (prelogin_cookie_valid || portal_userauthcookie_valid) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug)] | ||||
| enum AuthError { | ||||
|     TokenNotFound, | ||||
|     TokenInvalid, | ||||
| } | ||||
|  | ||||
| #[derive(Debug)] | ||||
| enum AuthEvent { | ||||
|     Request(AuthRequest), | ||||
|     Success(AuthData), | ||||
|     Error(AuthError), | ||||
| } | ||||
|  | ||||
| pub(crate) struct SamlLoginParams { | ||||
|     pub auth_request: AuthRequest, | ||||
|     pub user_agent: String, | ||||
|     pub clear_cookies: bool, | ||||
|     pub app_handle: AppHandle, | ||||
| } | ||||
|  | ||||
| pub(crate) async fn saml_login(params: SamlLoginParams) -> tauri::Result<Option<AuthData>> { | ||||
|     info!("Starting SAML login"); | ||||
|  | ||||
|     let (auth_event_tx, auth_event_rx) = mpsc::channel::<AuthEvent>(1); | ||||
|     let window = build_window(¶ms.app_handle, ¶ms.user_agent)?; | ||||
|     setup_webview(&window, auth_event_tx.clone())?; | ||||
|     let handler = setup_window(&window, auth_event_tx); | ||||
|  | ||||
|     if params.clear_cookies { | ||||
|         if let Err(err) = clear_webview_cookies(&window).await { | ||||
|             warn!("Failed to clear webview cookies: {}", err); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     let result = process(&window, params.auth_request, auth_event_rx).await; | ||||
|     window.unlisten(handler); | ||||
|     result | ||||
| } | ||||
|  | ||||
| fn build_window(app_handle: &AppHandle, ua: &str) -> tauri::Result<Window> { | ||||
|     let url = WindowUrl::App("auth.html".into()); | ||||
|     Window::builder(app_handle, AUTH_WINDOW_LABEL, url) | ||||
|         .visible(false) | ||||
|         .title("GlobalProtect Login") | ||||
|         .inner_size(600.0, 500.0) | ||||
|         .min_inner_size(370.0, 600.0) | ||||
|         .user_agent(ua) | ||||
|         .always_on_top(true) | ||||
|         .focused(true) | ||||
|         .center() | ||||
|         .build() | ||||
| } | ||||
|  | ||||
| // Setup webview events | ||||
| fn setup_webview(window: &Window, auth_event_tx: mpsc::Sender<AuthEvent>) -> tauri::Result<()> { | ||||
|     window.with_webview(move |wv| { | ||||
|         let wv = wv.inner(); | ||||
|         let auth_event_tx_clone = auth_event_tx.clone(); | ||||
|  | ||||
|         wv.connect_load_changed(move |wv, event| { | ||||
|             if LoadEvent::Finished != event { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             let uri = wv.uri().unwrap_or("".into()); | ||||
|             // Empty URI indicates that an error occurred | ||||
|             if uri.is_empty() { | ||||
|                 warn!("Empty URI loaded, retrying"); | ||||
|                 send_auth_error(auth_event_tx_clone.clone(), AuthError::TokenInvalid); | ||||
|                 return; | ||||
|             } | ||||
|             info!("Loaded URI: {}", redact_url(&uri)); | ||||
|  | ||||
|             if let Some(main_res) = wv.main_resource() { | ||||
|                 parse_auth_data(&main_res, auth_event_tx_clone.clone()); | ||||
|             } else { | ||||
|                 warn!("No main_resource"); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         wv.connect_load_failed(move |_wv, event, _uri, err| { | ||||
|             warn!("Load failed: {:?}, {:?}", event, err); | ||||
|             send_auth_error(auth_event_tx.clone(), AuthError::TokenInvalid); | ||||
|             false | ||||
|         }); | ||||
|     }) | ||||
| } | ||||
|  | ||||
| fn setup_window(window: &Window, event_tx: mpsc::Sender<AuthEvent>) -> EventHandler { | ||||
|     window.listen_global(AUTH_REQUEST_EVENT, move |event| { | ||||
|         if let Ok(payload) = TryInto::<AuthRequest>::try_into(event.payload()) { | ||||
|             let event_tx = event_tx.clone(); | ||||
|             send_auth_event(event_tx, AuthEvent::Request(payload)); | ||||
|         } else { | ||||
|             warn!("Invalid auth request payload"); | ||||
|         } | ||||
|     }) | ||||
| } | ||||
|  | ||||
| async fn process( | ||||
|     window: &Window, | ||||
|     auth_request: AuthRequest, | ||||
|     event_rx: mpsc::Receiver<AuthEvent>, | ||||
| ) -> tauri::Result<Option<AuthData>> { | ||||
|     info!("Processing auth request: {:?}", auth_request); | ||||
|  | ||||
|     process_request(window, auth_request)?; | ||||
|  | ||||
|     let handle = tokio::spawn(show_window_after_timeout(window.clone())); | ||||
|     let auth_data = monitor_events(window, event_rx).await; | ||||
|  | ||||
|     if !handle.is_finished() { | ||||
|         handle.abort(); | ||||
|     } | ||||
|     Ok(auth_data) | ||||
| } | ||||
|  | ||||
| fn process_request(window: &Window, auth_request: AuthRequest) -> tauri::Result<()> { | ||||
|     let saml_request = auth_request.saml_request; | ||||
|     let is_post = matches!(auth_request.saml_binding, SamlBinding::Post); | ||||
|  | ||||
|     window.with_webview(move |wv| { | ||||
|         let wv = wv.inner(); | ||||
|         if is_post { | ||||
|             // Load SAML request as HTML if POST binding is used | ||||
|             info!("Loading SAML request as HTML"); | ||||
|             wv.load_html(&saml_request, None); | ||||
|         } else { | ||||
|             // Redirect to SAML request URL if REDIRECT binding is used | ||||
|             info!("Redirecting to SAML request URL"); | ||||
|             wv.load_uri(&saml_request); | ||||
|         } | ||||
|     }) | ||||
| } | ||||
|  | ||||
| async fn show_window_after_timeout(window: Window) { | ||||
|     tokio::time::sleep(Duration::from_secs(FALLBACK_SHOW_WINDOW_TIMEOUT)).await; | ||||
|     info!( | ||||
|         "Showing window after timeout ({:?} seconds)", | ||||
|         FALLBACK_SHOW_WINDOW_TIMEOUT | ||||
|     ); | ||||
|     show_window(&window); | ||||
| } | ||||
|  | ||||
| async fn monitor_events(window: &Window, event_rx: mpsc::Receiver<AuthEvent>) -> Option<AuthData> { | ||||
|     tokio::select! { | ||||
|         auth_data = monitor_auth_event(window, event_rx) => Some(auth_data), | ||||
|         _ = monitor_window_close_event(window) => { | ||||
|             warn!("Auth window closed without auth data"); | ||||
|             None | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| async fn monitor_auth_event(window: &Window, mut event_rx: mpsc::Receiver<AuthEvent>) -> AuthData { | ||||
|     info!("Monitoring auth events"); | ||||
|  | ||||
|     let (cancel_timeout_tx, cancel_timeout_rx) = mpsc::channel::<()>(1); | ||||
|     let cancel_timeout_rx = Arc::new(Mutex::new(cancel_timeout_rx)); | ||||
|     let mut attempt_times = 1; | ||||
|  | ||||
|     loop { | ||||
|         if let Some(auth_event) = event_rx.recv().await { | ||||
|             match auth_event { | ||||
|                 AuthEvent::Request(auth_request) => { | ||||
|                     attempt_times += 1; | ||||
|                     info!( | ||||
|                         "Got auth request from auth-request event, attempt #{}", | ||||
|                         attempt_times | ||||
|                     ); | ||||
|                     if let Err(err) = process_request(window, auth_request) { | ||||
|                         warn!("Error processing auth request: {}", err); | ||||
|                     } | ||||
|                 } | ||||
|                 AuthEvent::Success(auth_data) => { | ||||
|                     info!("Got auth data successfully, closing window"); | ||||
|                     close_window(window); | ||||
|                     return auth_data; | ||||
|                 } | ||||
|                 AuthEvent::Error(AuthError::TokenInvalid) => { | ||||
|                     // Found the invalid token, means that user is authenticated, keep retrying and no need to show the window | ||||
|                     warn!( | ||||
|                         "Attempt #{} failed, found invalid token, retrying", | ||||
|                         attempt_times | ||||
|                     ); | ||||
|  | ||||
|                     // If the cancel timeout is locked, it means that the window is about to show, so we need to cancel it | ||||
|                     if cancel_timeout_rx.try_lock().is_err() { | ||||
|                         if let Err(err) = cancel_timeout_tx.try_send(()) { | ||||
|                             warn!("Error sending cancel timeout: {}", err); | ||||
|                         } | ||||
|                     } else { | ||||
|                         info!("Window is not about to show, skipping cancel timeout"); | ||||
|                     } | ||||
|  | ||||
|                     // Send the error event to the outside, so that we can retry it when receiving the auth-request event | ||||
|                     if let Err(err) = window.emit_all(AUTH_ERROR_EVENT, attempt_times) { | ||||
|                         warn!("Error emitting auth-error event: {:?}", err); | ||||
|                     } | ||||
|                 } | ||||
|                 AuthEvent::Error(AuthError::TokenNotFound) => { | ||||
|                     let window_visible = window.is_visible().unwrap_or(false); | ||||
|                     if window_visible { | ||||
|                         continue; | ||||
|                     } | ||||
|  | ||||
|                     info!( | ||||
|                         "Token not found, showing window in {} seconds", | ||||
|                         SHOW_WINDOW_TIMEOUT | ||||
|                     ); | ||||
|  | ||||
|                     let cancel_timeout_rx = cancel_timeout_rx.clone(); | ||||
|                     tokio::spawn(handle_token_not_found(window.clone(), cancel_timeout_rx)); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| async fn monitor_window_close_event(window: &Window) { | ||||
|     let (close_tx, close_rx) = oneshot::channel(); | ||||
|     let close_tx = Arc::new(Mutex::new(Some(close_tx))); | ||||
|  | ||||
|     window.on_window_event(move |event| { | ||||
|         if matches!(event, WindowEvent::CloseRequested { .. }) { | ||||
|             if let Ok(mut close_tx_locked) = close_tx.try_lock() { | ||||
|                 if let Some(close_tx) = close_tx_locked.take() { | ||||
|                     if close_tx.send(()).is_err() { | ||||
|                         println!("Error sending close event"); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     }); | ||||
|  | ||||
|     if let Err(err) = close_rx.await { | ||||
|         warn!("Error receiving close event: {}", err); | ||||
|     } | ||||
| } | ||||
| /// Tokens not found means that the page might need the user interaction to login, | ||||
| /// we should show the window after a short timeout, it will be cancelled if the | ||||
| /// token is found in the response, no matter it's valid or not. | ||||
| async fn handle_token_not_found(window: Window, cancel_timeout_rx: Arc<Mutex<mpsc::Receiver<()>>>) { | ||||
|     if let Ok(mut cancel_timeout_rx) = cancel_timeout_rx.try_lock() { | ||||
|         let duration = Duration::from_secs(SHOW_WINDOW_TIMEOUT); | ||||
|         if timeout(duration, cancel_timeout_rx.recv()).await.is_err() { | ||||
|             info!( | ||||
|                 "Timeout expired after {} seconds, showing window", | ||||
|                 SHOW_WINDOW_TIMEOUT | ||||
|             ); | ||||
|             show_window(&window); | ||||
|         } else { | ||||
|             info!("The scheduled show window task is cancelled"); | ||||
|         } | ||||
|     } else { | ||||
|         info!("The show window task has been already been scheduled, skipping"); | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Parse the authentication data from the response headers or HTML content | ||||
| /// and send it to the event channel | ||||
| fn parse_auth_data(main_res: &WebResource, auth_event_tx: mpsc::Sender<AuthEvent>) { | ||||
|     if let Some(response) = main_res.response() { | ||||
|         match read_auth_data_from_response(&response) { | ||||
|             Ok(auth_data) => { | ||||
|                 debug!("Got auth data from HTTP headers: {:?}", auth_data); | ||||
|                 send_auth_data(auth_event_tx, auth_data); | ||||
|                 return; | ||||
|             } | ||||
|             Err(AuthError::TokenInvalid) => { | ||||
|                 debug!("Received invalid token from HTTP headers"); | ||||
|                 send_auth_error(auth_event_tx, AuthError::TokenInvalid); | ||||
|                 return; | ||||
|             } | ||||
|             Err(AuthError::TokenNotFound) => { | ||||
|                 debug!("Token not found in HTTP headers, trying to read from HTML"); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     main_res.data(Cancellable::NONE, move |data| { | ||||
|         if let Ok(data) = data { | ||||
|             let html = String::from_utf8_lossy(&data); | ||||
|             match read_auth_data_from_html(&html) { | ||||
|                 Ok(auth_data) => { | ||||
|                     debug!("Got auth data from HTML: {:?}", auth_data); | ||||
|                     send_auth_data(auth_event_tx, auth_data); | ||||
|                 } | ||||
|                 Err(err) => { | ||||
|                     debug!("Error reading auth data from HTML: {:?}", err); | ||||
|                     send_auth_error(auth_event_tx, err); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     }); | ||||
| } | ||||
|  | ||||
| /// Read the authentication data from the response headers | ||||
| fn read_auth_data_from_response(response: &webkit2gtk::URIResponse) -> Result<AuthData, AuthError> { | ||||
|     response | ||||
|         .http_headers() | ||||
|         .map_or(Err(AuthError::TokenNotFound), |mut headers| { | ||||
|             let saml_status: Option<String> = headers.get("saml-auth-status").map(GString::into); | ||||
|             if saml_status == Some("-1".to_string()) { | ||||
|                 return Err(AuthError::TokenInvalid); | ||||
|             } | ||||
|  | ||||
|             let auth_data = AuthData::new( | ||||
|                 headers.get("saml-username").map(GString::into), | ||||
|                 headers.get("prelogin-cookie").map(GString::into), | ||||
|                 headers.get("portal-userauthcookie").map(GString::into), | ||||
|             ); | ||||
|  | ||||
|             if auth_data.check() { | ||||
|                 Ok(auth_data) | ||||
|             } else { | ||||
|                 Err(AuthError::TokenNotFound) | ||||
|             } | ||||
|         }) | ||||
| } | ||||
|  | ||||
| /// Read the authentication data from the HTML content | ||||
| fn read_auth_data_from_html(html: &str) -> Result<AuthData, AuthError> { | ||||
|     if html.contains("Temporarily Unavailable") { | ||||
|         info!("SAML result page temporarily unavailable, retrying"); | ||||
|         return Err(AuthError::TokenInvalid); | ||||
|     } | ||||
|  | ||||
|     let saml_auth_status = parse_xml_tag(html, "saml-auth-status"); | ||||
|  | ||||
|     match saml_auth_status { | ||||
|         Some(status) if status == "1" => extract_auth_data(html).ok_or(AuthError::TokenInvalid), | ||||
|         Some(status) if status == "-1" => Err(AuthError::TokenInvalid), | ||||
|         _ => Err(AuthError::TokenNotFound), | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Extract the authentication data from the HTML content | ||||
| fn extract_auth_data(html: &str) -> Option<AuthData> { | ||||
|     let auth_data = AuthData::new( | ||||
|         parse_xml_tag(html, "saml-username"), | ||||
|         parse_xml_tag(html, "prelogin-cookie"), | ||||
|         parse_xml_tag(html, "portal-userauthcookie"), | ||||
|     ); | ||||
|  | ||||
|     if auth_data.check() { | ||||
|         Some(auth_data) | ||||
|     } else { | ||||
|         None | ||||
|     } | ||||
| } | ||||
|  | ||||
| fn parse_xml_tag(html: &str, tag: &str) -> Option<String> { | ||||
|     let re = Regex::new(&format!("<{}>(.*)</{}>", tag, tag)).unwrap(); | ||||
|     re.captures(html) | ||||
|         .and_then(|captures| captures.get(1)) | ||||
|         .map(|m| m.as_str().to_string()) | ||||
| } | ||||
|  | ||||
| fn send_auth_data(auth_event_tx: mpsc::Sender<AuthEvent>, auth_data: AuthData) { | ||||
|     send_auth_event(auth_event_tx, AuthEvent::Success(auth_data)); | ||||
| } | ||||
|  | ||||
| fn send_auth_error(auth_event_tx: mpsc::Sender<AuthEvent>, err: AuthError) { | ||||
|     send_auth_event(auth_event_tx, AuthEvent::Error(err)); | ||||
| } | ||||
|  | ||||
| fn send_auth_event(auth_event_tx: mpsc::Sender<AuthEvent>, auth_event: AuthEvent) { | ||||
|     tauri::async_runtime::spawn(async move { | ||||
|         if let Err(err) = auth_event_tx.send(auth_event).await { | ||||
|             warn!("Error sending event: {}", err); | ||||
|         } | ||||
|     }); | ||||
| } | ||||
|  | ||||
| fn show_window(window: &Window) { | ||||
|     let visible = window.is_visible().unwrap_or(false); | ||||
|     if visible { | ||||
|         debug!("Window is already visible, skipping"); | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     if let Err(err) = window.show() { | ||||
|         warn!("Error showing window: {}", err); | ||||
|     } | ||||
| } | ||||
|  | ||||
| fn close_window(window: &Window) { | ||||
|     if let Err(err) = window.close() { | ||||
|         warn!("Error closing window: {}", err); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										136
									
								
								gpgui/src-tauri/src/commands.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,136 @@ | ||||
| use crate::{ | ||||
|     auth::{self, AuthData, AuthRequest, SamlBinding, SamlLoginParams}, | ||||
|     storage::{AppStorage, KeyHint}, | ||||
|     utils::get_openssl_conf, | ||||
|     utils::get_openssl_conf_path, | ||||
| }; | ||||
| use gpcommon::{Client, ServerApiError, VpnStatus}; | ||||
| use serde_json::Value; | ||||
| use std::{process::Stdio, sync::Arc}; | ||||
| use tauri::{AppHandle, State}; | ||||
| use tokio::{fs, io::AsyncWriteExt, process::Command}; | ||||
|  | ||||
| #[tauri::command] | ||||
| pub(crate) async fn service_online<'a>(client: State<'a, Arc<Client>>) -> Result<bool, ()> { | ||||
|     Ok(client.is_online().await) | ||||
| } | ||||
|  | ||||
| #[tauri::command] | ||||
| pub(crate) async fn vpn_status<'a>( | ||||
|     client: State<'a, Arc<Client>>, | ||||
| ) -> Result<VpnStatus, ServerApiError> { | ||||
|     client.status().await | ||||
| } | ||||
|  | ||||
| #[tauri::command] | ||||
| pub(crate) async fn vpn_connect<'a>( | ||||
|     server: String, | ||||
|     cookie: String, | ||||
|     user_agent: String, | ||||
|     client: State<'a, Arc<Client>>, | ||||
| ) -> Result<(), ServerApiError> { | ||||
|     client.connect(server, cookie, user_agent).await | ||||
| } | ||||
|  | ||||
| #[tauri::command] | ||||
| pub(crate) async fn vpn_disconnect<'a>( | ||||
|     client: State<'a, Arc<Client>>, | ||||
| ) -> Result<(), ServerApiError> { | ||||
|     client.disconnect().await | ||||
| } | ||||
|  | ||||
| #[tauri::command] | ||||
| pub(crate) async fn saml_login( | ||||
|     binding: SamlBinding, | ||||
|     request: String, | ||||
|     user_agent: String, | ||||
|     clear_cookies: bool, | ||||
|     app_handle: AppHandle, | ||||
| ) -> tauri::Result<Option<AuthData>> { | ||||
|     let params = SamlLoginParams { | ||||
|         auth_request: AuthRequest::new(binding, request), | ||||
|         user_agent, | ||||
|         clear_cookies, | ||||
|         app_handle, | ||||
|     }; | ||||
|     auth::saml_login(params).await | ||||
| } | ||||
|  | ||||
| #[tauri::command] | ||||
| pub(crate) fn os_version() -> String { | ||||
|     whoami::distro() | ||||
| } | ||||
|  | ||||
| #[tauri::command] | ||||
| pub(crate) async fn openssl_config() -> Result<String, ()> { | ||||
|     Ok(get_openssl_conf()) | ||||
| } | ||||
|  | ||||
| #[tauri::command] | ||||
| pub(crate) async fn update_openssl_config(app_handle: AppHandle) -> tauri::Result<()> { | ||||
|     let openssl_conf = get_openssl_conf(); | ||||
|     let openssl_conf_path = get_openssl_conf_path(&app_handle); | ||||
|  | ||||
|     fs::write(openssl_conf_path, openssl_conf).await?; | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| #[tauri::command] | ||||
| pub(crate) async fn openconnect_config() -> tauri::Result<String> { | ||||
|     let file = "/etc/gpservice/gp.conf"; | ||||
|     let content = fs::read_to_string(file).await?; | ||||
|     Ok(content) | ||||
| } | ||||
|  | ||||
| #[tauri::command] | ||||
| pub(crate) async fn update_openconnect_config(content: String) -> tauri::Result<i32> { | ||||
|     let file = "/etc/gpservice/gp.conf"; | ||||
|     let mut child = Command::new("pkexec") | ||||
|         .arg("tee") | ||||
|         .arg(file) | ||||
|         .stdin(Stdio::piped()) | ||||
|         .stdout(Stdio::null()) | ||||
|         .spawn()?; | ||||
|  | ||||
|     let mut stdin = child.stdin.take().unwrap(); | ||||
|  | ||||
|     tokio::spawn(async move { | ||||
|         stdin.write_all(content.as_bytes()).await.unwrap(); | ||||
|         drop(stdin); | ||||
|     }); | ||||
|  | ||||
|     let exit_status = child.wait().await?; | ||||
|  | ||||
|     exit_status.code().ok_or_else(|| { | ||||
|         tauri::Error::Io(std::io::Error::new( | ||||
|             std::io::ErrorKind::Other, | ||||
|             "Process exited without a code", | ||||
|         )) | ||||
|     }) | ||||
| } | ||||
|  | ||||
| #[tauri::command] | ||||
| pub(crate) async fn store_get<'a>( | ||||
|     hint: KeyHint<'_>, | ||||
|     app_storage: State<'_, AppStorage<'_>>, | ||||
| ) -> Result<Option<Value>, ()> { | ||||
|     Ok(app_storage.get(hint)) | ||||
| } | ||||
|  | ||||
| #[tauri::command] | ||||
| pub(crate) fn store_set( | ||||
|     hint: KeyHint, | ||||
|     value: Value, | ||||
|     app_storage: State<'_, AppStorage>, | ||||
| ) -> Result<(), tauri_plugin_store::Error> { | ||||
|     app_storage.set(hint, &value)?; | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| #[tauri::command] | ||||
| pub(crate) fn store_save( | ||||
|     app_storage: State<'_, AppStorage>, | ||||
| ) -> Result<(), tauri_plugin_store::Error> { | ||||
|     app_storage.save()?; | ||||
|     Ok(()) | ||||
| } | ||||
							
								
								
									
										46
									
								
								gpgui/src-tauri/src/crypto.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,46 @@ | ||||
| use aes_gcm::{ | ||||
|     aead::{consts::U12, Aead, OsRng}, | ||||
|     AeadCore, Aes256Gcm, Key, KeyInit, Nonce, | ||||
| }; | ||||
| use keyring::Entry; | ||||
|  | ||||
| const SERVICE_NAME: &str = "GlobalProtect-openconnect"; | ||||
| const ENTRY_KEY: &str = "master-key"; | ||||
|  | ||||
| fn get_master_key() -> Result<Key<Aes256Gcm>, anyhow::Error> { | ||||
|     let key_entry = Entry::new(SERVICE_NAME, ENTRY_KEY)?; | ||||
|  | ||||
|     if let Ok(key) = key_entry.get_password() { | ||||
|         let key = hex::decode(key)?; | ||||
|         return Ok(Key::<Aes256Gcm>::clone_from_slice(&key)); | ||||
|     } | ||||
|  | ||||
|     let key = Aes256Gcm::generate_key(OsRng); | ||||
|     let encoded_key = hex::encode(key); | ||||
|  | ||||
|     key_entry.set_password(&encoded_key)?; | ||||
|  | ||||
|     Ok(key) | ||||
| } | ||||
|  | ||||
| pub(crate) fn encrypt(data: &str) -> Result<String, anyhow::Error> { | ||||
|     let master_key = get_master_key()?; | ||||
|     let cipher = Aes256Gcm::new(&master_key); | ||||
|     let nonce = Aes256Gcm::generate_nonce(&mut OsRng); | ||||
|     let cipher_text = cipher.encrypt(&nonce, data.as_bytes())?; | ||||
|  | ||||
|     let mut encrypted = nonce.to_vec(); | ||||
|     encrypted.extend_from_slice(&cipher_text); | ||||
|     Ok(hex::encode(encrypted)) | ||||
| } | ||||
|  | ||||
| pub(crate) fn decrypt(encrypted: &str) -> Result<String, anyhow::Error> { | ||||
|     let master_key = get_master_key()?; | ||||
|     let encrypted = hex::decode(encrypted)?; | ||||
|     let nonce = Nonce::<U12>::from_slice(&encrypted[..12]); | ||||
|     let cipher_text = &encrypted[12..]; | ||||
|     let cipher = Aes256Gcm::new(&master_key); | ||||
|     let plain_text = cipher.decrypt(nonce, cipher_text)?; | ||||
|  | ||||
|     String::from_utf8(plain_text).map_err(|err| err.into()) | ||||
| } | ||||
							
								
								
									
										42
									
								
								gpgui/src-tauri/src/main.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,42 @@ | ||||
| #![cfg_attr( | ||||
|     all(not(debug_assertions), target_os = "windows"), | ||||
|     windows_subsystem = "windows" | ||||
| )] | ||||
| use tauri_plugin_log::LogTarget; | ||||
|  | ||||
| mod auth; | ||||
| mod commands; | ||||
| mod crypto; | ||||
| mod settings; | ||||
| mod setup; | ||||
| mod storage; | ||||
| mod utils; | ||||
|  | ||||
| fn main() { | ||||
|     tauri::Builder::default() | ||||
|         .plugin( | ||||
|             tauri_plugin_log::Builder::default() | ||||
|                 .targets([LogTarget::LogDir, LogTarget::Stdout]) | ||||
|                 .level(log::LevelFilter::Info) | ||||
|                 .build(), | ||||
|         ) | ||||
|         .plugin(tauri_plugin_store::Builder::default().build()) | ||||
|         .setup(setup::setup) | ||||
|         .invoke_handler(tauri::generate_handler![ | ||||
|             commands::service_online, | ||||
|             commands::vpn_status, | ||||
|             commands::vpn_connect, | ||||
|             commands::vpn_disconnect, | ||||
|             commands::saml_login, | ||||
|             commands::os_version, | ||||
|             commands::openssl_config, | ||||
|             commands::update_openssl_config, | ||||
|             commands::openconnect_config, | ||||
|             commands::update_openconnect_config, | ||||
|             commands::store_get, | ||||
|             commands::store_set, | ||||
|             commands::store_save, | ||||
|         ]) | ||||
|         .run(tauri::generate_context!()) | ||||
|         .expect("error while running tauri application"); | ||||
| } | ||||