mirror of
				https://github.com/yuezk/GlobalProtect-openconnect.git
				synced 2025-05-20 07:26:58 -04:00 
			
		
		
		
	Compare commits
	
		
			8 Commits
		
	
	
		
			v2.0.0-bet
			...
			v2.0.0-bet
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | c3bd7aeb93 | ||
|  | 0b55a80317 | ||
|  | c6315bf384 | ||
|  | 87b965f80c | ||
|  | b09b21ae0f | ||
|  | 7e372cd113 | ||
|  | 1e211e8912 | ||
|  | 8bc4049a0f | 
							
								
								
									
										138
									
								
								.github/workflows/build.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										138
									
								
								.github/workflows/build.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -6,8 +6,8 @@ on: | |||||||
|       - "*.md" |       - "*.md" | ||||||
|       - .vscode |       - .vscode | ||||||
|       - .devcontainer |       - .devcontainer | ||||||
|     # branches: |     branches: | ||||||
|       # - main |       - main | ||||||
|     # tags: |     # tags: | ||||||
|     #   - v*.*.* |     #   - v*.*.* | ||||||
| jobs: | jobs: | ||||||
| @@ -114,137 +114,3 @@ jobs: | |||||||
|           name: artifact-${{ matrix.arch }}-tauri |           name: artifact-${{ matrix.arch }}-tauri | ||||||
|           path: | |           path: | | ||||||
|             gpgui/.tmp/artifact |             gpgui/.tmp/artifact | ||||||
|  |  | ||||||
|   package-rpm: |  | ||||||
|     needs: [setup-matrix, build-tauri] |  | ||||||
|     runs-on: ubuntu-latest |  | ||||||
|     strategy: |  | ||||||
|       matrix: |  | ||||||
|         arch: ${{ fromJson(needs.setup-matrix.outputs.matrix) }} |  | ||||||
|     steps: |  | ||||||
|       - name: Checkout gpgui repo |  | ||||||
|         uses: actions/checkout@v4 |  | ||||||
|         with: |  | ||||||
|           token: ${{ secrets.GH_PAT }} |  | ||||||
|           repository: yuezk/gpgui |  | ||||||
|           path: gpgui |  | ||||||
|  |  | ||||||
|       - name: Download artifact-${{ matrix.arch }} |  | ||||||
|         uses: actions/download-artifact@v4 |  | ||||||
|         with: |  | ||||||
|           name: artifact-${{ matrix.arch }}-tauri |  | ||||||
|           path: gpgui/.tmp/artifact |  | ||||||
|  |  | ||||||
|       - name: Set up QEMU |  | ||||||
|         uses: docker/setup-qemu-action@v3 |  | ||||||
|         with: |  | ||||||
|           platforms: ${{ matrix.arch }} |  | ||||||
|  |  | ||||||
|       - name: Login to Docker Hub |  | ||||||
|         uses: docker/login-action@v3 |  | ||||||
|         with: |  | ||||||
|           username: ${{ secrets.DOCKER_HUB_USERNAME }} |  | ||||||
|           password: ${{ secrets.DOCKER_HUB_TOKEN }} |  | ||||||
|  |  | ||||||
|       - name: Create RPM package |  | ||||||
|         run: | |  | ||||||
|           docker run \ |  | ||||||
|             --rm \ |  | ||||||
|             -v $(pwd):/${{ github.workspace }} \ |  | ||||||
|             -w ${{ github.workspace }} \ |  | ||||||
|             --platform linux/${{ matrix.arch }} \ |  | ||||||
|             yuezk/gpdev:rpm-builder \ |  | ||||||
|             "./gpgui/scripts/build-rpm.sh" |  | ||||||
|  |  | ||||||
|       - name: Upload rpm artifacts |  | ||||||
|         uses: actions/upload-artifact@v4 |  | ||||||
|         with: |  | ||||||
|           name: artifact-${{ matrix.arch }}-rpm |  | ||||||
|           path: | |  | ||||||
|             gpgui/.tmp/artifact/*.rpm |  | ||||||
|  |  | ||||||
|   package-pkgbuild: |  | ||||||
|     needs: [setup-matrix, build-tauri] |  | ||||||
|     runs-on: ubuntu-latest |  | ||||||
|     strategy: |  | ||||||
|       matrix: |  | ||||||
|         arch: ${{ fromJson(needs.setup-matrix.outputs.matrix) }} |  | ||||||
|     steps: |  | ||||||
|       - name: Checkout gpgui repo |  | ||||||
|         uses: actions/checkout@v4 |  | ||||||
|         with: |  | ||||||
|           token: ${{ secrets.GH_PAT }} |  | ||||||
|           repository: yuezk/gpgui |  | ||||||
|           path: gpgui |  | ||||||
|  |  | ||||||
|       - name: Download artifact-${{ matrix.arch }} |  | ||||||
|         uses: actions/download-artifact@v4 |  | ||||||
|         with: |  | ||||||
|           name: artifact-${{ matrix.arch }}-tauri |  | ||||||
|           path: gpgui/.tmp/artifact |  | ||||||
|  |  | ||||||
|       - name: Set up QEMU |  | ||||||
|         uses: docker/setup-qemu-action@v3 |  | ||||||
|         with: |  | ||||||
|           platforms: ${{ matrix.arch }} |  | ||||||
|  |  | ||||||
|       - name: Login to Docker Hub |  | ||||||
|         uses: docker/login-action@v3 |  | ||||||
|         with: |  | ||||||
|           username: ${{ secrets.DOCKER_HUB_USERNAME }} |  | ||||||
|           password: ${{ secrets.DOCKER_HUB_TOKEN }} |  | ||||||
|  |  | ||||||
|       - name: Generate PKGBUILD |  | ||||||
|         run: | |  | ||||||
|           ./gpgui/scripts/generate-pkgbuild.sh |  | ||||||
|  |  | ||||||
|       - name: Build PKGBUILD package |  | ||||||
|         run: | |  | ||||||
|           # Generate PKGBUILD to .tmp/pkgbuild |  | ||||||
|           ./gpgui/scripts/generate-pkgbuild.sh |  | ||||||
|  |  | ||||||
|           # Build package |  | ||||||
|           docker run \ |  | ||||||
|             --rm \ |  | ||||||
|             -v $(pwd)/gpgui/.tmp/pkgbuild:/pkgbuild \ |  | ||||||
|             --platform linux/${{ matrix.arch }} \ |  | ||||||
|             yuezk/gpdev:pkgbuild |  | ||||||
|  |  | ||||||
|       - name: Upload pkgbuild artifacts |  | ||||||
|         uses: actions/upload-artifact@v4 |  | ||||||
|         with: |  | ||||||
|           name: artifact-${{ matrix.arch }}-pkgbuild |  | ||||||
|           path: | |  | ||||||
|             gpgui/.tmp/pkgbuild/*.pkg.tar.zst |  | ||||||
|  |  | ||||||
|   gh-release: |  | ||||||
|     if: startsWith(github.ref, 'refs/tags/') |  | ||||||
|     runs-on: ubuntu-latest |  | ||||||
|     needs: |  | ||||||
|       - package-rpm |  | ||||||
|       - package-pkgbuild |  | ||||||
|  |  | ||||||
|     steps: |  | ||||||
|       - name: Download artifact |  | ||||||
|         uses: actions/download-artifact@v4 |  | ||||||
|         with: |  | ||||||
|           path: artifact |  | ||||||
|           pattern: artifact-* |  | ||||||
|           merge-multiple: true |  | ||||||
|  |  | ||||||
|       - name: Generate checksum |  | ||||||
|         uses: jmgilman/actions-generate-checksum@v1 |  | ||||||
|         with: |  | ||||||
|           output: checksums.txt |  | ||||||
|           patterns: | |  | ||||||
|             artifact/* |  | ||||||
|  |  | ||||||
|       - name: Create GH release |  | ||||||
|         uses: softprops/action-gh-release@v1 |  | ||||||
|         with: |  | ||||||
|           token: ${{ secrets.GH_PAT }} |  | ||||||
|           prerelease: contains(github.ref, 'latest') |  | ||||||
|           fail_on_unmatched_files: true |  | ||||||
|           files: | |  | ||||||
|             checksums.txt |  | ||||||
|             artifact/* |  | ||||||
|   | |||||||
							
								
								
									
										8
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							| @@ -11,8 +11,10 @@ | |||||||
|         "dotenvy", |         "dotenvy", | ||||||
|         "getconfig", |         "getconfig", | ||||||
|         "globalprotect", |         "globalprotect", | ||||||
|  |         "globalprotectcallback", | ||||||
|         "gpapi", |         "gpapi", | ||||||
|         "gpauth", |         "gpauth", | ||||||
|  |         "gpcallback", | ||||||
|         "gpclient", |         "gpclient", | ||||||
|         "gpcommon", |         "gpcommon", | ||||||
|         "gpgui", |         "gpgui", | ||||||
| @@ -48,6 +50,8 @@ | |||||||
|         "vpnc", |         "vpnc", | ||||||
|         "vpninfo", |         "vpninfo", | ||||||
|         "wmctrl", |         "wmctrl", | ||||||
|         "XAUTHORITY" |         "XAUTHORITY", | ||||||
|     ] |         "yuezk" | ||||||
|  |     ], | ||||||
|  |     "rust-analyzer.cargo.features": "all", | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										62
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										62
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							| @@ -1423,13 +1423,15 @@ dependencies = [ | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "gpapi" | name = "gpapi" | ||||||
| version = "2.0.0-beta2" | version = "2.0.0-beta5" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "anyhow", |  "anyhow", | ||||||
|  "base64 0.21.5", |  "base64 0.21.5", | ||||||
|  "chacha20poly1305", |  "chacha20poly1305", | ||||||
|  |  "clap", | ||||||
|  "dotenvy_macro", |  "dotenvy_macro", | ||||||
|  "log", |  "log", | ||||||
|  |  "open", | ||||||
|  "redact-engine", |  "redact-engine", | ||||||
|  "regex", |  "regex", | ||||||
|  "reqwest", |  "reqwest", | ||||||
| @@ -1450,7 +1452,7 @@ dependencies = [ | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "gpauth" | name = "gpauth" | ||||||
| version = "2.0.0-beta2" | version = "2.0.0-beta5" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "anyhow", |  "anyhow", | ||||||
|  "clap", |  "clap", | ||||||
| @@ -1470,7 +1472,7 @@ dependencies = [ | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "gpclient" | name = "gpclient" | ||||||
| version = "2.0.0-beta2" | version = "2.0.0-beta5" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "anyhow", |  "anyhow", | ||||||
|  "clap", |  "clap", | ||||||
| @@ -1491,7 +1493,7 @@ dependencies = [ | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "gpservice" | name = "gpservice" | ||||||
| version = "2.0.0-beta2" | version = "2.0.0-beta5" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "anyhow", |  "anyhow", | ||||||
|  "axum", |  "axum", | ||||||
| @@ -1564,9 +1566,9 @@ dependencies = [ | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "h2" | name = "h2" | ||||||
| version = "0.3.22" | version = "0.3.24" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "4d6250322ef6e60f93f9a2162799302cd6f68f79f6e5d85c8c16f14d1d958178" | checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "bytes", |  "bytes", | ||||||
|  "fnv", |  "fnv", | ||||||
| @@ -1583,9 +1585,9 @@ dependencies = [ | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "h2" | name = "h2" | ||||||
| version = "0.4.0" | version = "0.4.2" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "e1d308f63daf4181410c242d34c11f928dcb3aa105852019e043c9d1f4e4368a" | checksum = "31d030e59af851932b72ceebadf4a2b5986dba4c3b99dd2493f8273a0f151943" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "bytes", |  "bytes", | ||||||
|  "fnv", |  "fnv", | ||||||
| @@ -1743,7 +1745,7 @@ dependencies = [ | |||||||
|  "futures-channel", |  "futures-channel", | ||||||
|  "futures-core", |  "futures-core", | ||||||
|  "futures-util", |  "futures-util", | ||||||
|  "h2 0.3.22", |  "h2 0.3.24", | ||||||
|  "http 0.2.11", |  "http 0.2.11", | ||||||
|  "http-body 0.4.6", |  "http-body 0.4.6", | ||||||
|  "httparse", |  "httparse", | ||||||
| @@ -1766,7 +1768,7 @@ dependencies = [ | |||||||
|  "bytes", |  "bytes", | ||||||
|  "futures-channel", |  "futures-channel", | ||||||
|  "futures-util", |  "futures-util", | ||||||
|  "h2 0.4.0", |  "h2 0.4.2", | ||||||
|  "http 1.0.0", |  "http 1.0.0", | ||||||
|  "http-body 1.0.0", |  "http-body 1.0.0", | ||||||
|  "httparse", |  "httparse", | ||||||
| @@ -1962,6 +1964,15 @@ version = "2.9.0" | |||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" | checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "is-docker" | ||||||
|  | version = "0.2.0" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" | ||||||
|  | dependencies = [ | ||||||
|  |  "once_cell", | ||||||
|  | ] | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "is-terminal" | name = "is-terminal" | ||||||
| version = "0.4.10" | version = "0.4.10" | ||||||
| @@ -1973,6 +1984,16 @@ dependencies = [ | |||||||
|  "windows-sys 0.52.0", |  "windows-sys 0.52.0", | ||||||
| ] | ] | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "is-wsl" | ||||||
|  | version = "0.4.0" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" | ||||||
|  | dependencies = [ | ||||||
|  |  "is-docker", | ||||||
|  |  "once_cell", | ||||||
|  | ] | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "is_executable" | name = "is_executable" | ||||||
| version = "1.0.1" | version = "1.0.1" | ||||||
| @@ -2444,9 +2465,20 @@ version = "0.3.0" | |||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" | checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "open" | ||||||
|  | version = "5.0.1" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "90878fb664448b54c4e592455ad02831e23a3f7e157374a8b95654731aac7349" | ||||||
|  | dependencies = [ | ||||||
|  |  "is-wsl", | ||||||
|  |  "libc", | ||||||
|  |  "pathdiff", | ||||||
|  | ] | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "openconnect" | name = "openconnect" | ||||||
| version = "2.0.0-beta2" | version = "2.0.0-beta5" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "cc", |  "cc", | ||||||
|  "is_executable", |  "is_executable", | ||||||
| @@ -2573,6 +2605,12 @@ version = "1.0.14" | |||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" | checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "pathdiff" | ||||||
|  | version = "0.2.1" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "percent-encoding" | name = "percent-encoding" | ||||||
| version = "2.3.1" | version = "2.3.1" | ||||||
| @@ -3070,7 +3108,7 @@ dependencies = [ | |||||||
|  "encoding_rs", |  "encoding_rs", | ||||||
|  "futures-core", |  "futures-core", | ||||||
|  "futures-util", |  "futures-util", | ||||||
|  "h2 0.3.22", |  "h2 0.3.24", | ||||||
|  "http 0.2.11", |  "http 0.2.11", | ||||||
|  "http-body 0.4.6", |  "http-body 0.4.6", | ||||||
|  "hyper 0.14.28", |  "hyper 0.14.28", | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ resolver = "2" | |||||||
| members = ["crates/*", "apps/gpclient", "apps/gpservice", "apps/gpauth"] | members = ["crates/*", "apps/gpclient", "apps/gpservice", "apps/gpauth"] | ||||||
|  |  | ||||||
| [workspace.package] | [workspace.package] | ||||||
| version = "2.0.0-beta2" | version = "2.0.0-beta5" | ||||||
| authors = ["Kevin Yue <k3vinyue@gmail.com>"] | authors = ["Kevin Yue <k3vinyue@gmail.com>"] | ||||||
| homepage = "https://github.com/yuezk/GlobalProtect-openconnect" | homepage = "https://github.com/yuezk/GlobalProtect-openconnect" | ||||||
| edition = "2021" | edition = "2021" | ||||||
|   | |||||||
							
								
								
									
										29
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										29
									
								
								README.md
									
									
									
									
									
								
							| @@ -11,6 +11,7 @@ A GUI for GlobalProtect VPN, based on OpenConnect, supports the SSO authenticati | |||||||
| - [x] Better Linux support | - [x] Better Linux support | ||||||
| - [x] Support both CLI and GUI | - [x] Support both CLI and GUI | ||||||
| - [x] Support both SSO and non-SSO authentication | - [x] Support both SSO and non-SSO authentication | ||||||
|  | - [x] Support authentication using default browser | ||||||
| - [x] Support multiple portals | - [x] Support multiple portals | ||||||
| - [x] Support gateway selection | - [x] Support gateway selection | ||||||
| - [x] Support auto-connect on startup | - [x] Support auto-connect on startup | ||||||
| @@ -33,11 +34,12 @@ Commands: | |||||||
|  |  | ||||||
| Options: | Options: | ||||||
|       --fix-openssl        Get around the OpenSSL `unsafe legacy renegotiation` error |       --fix-openssl        Get around the OpenSSL `unsafe legacy renegotiation` error | ||||||
|  |       --ignore-tls-errors  Ignore the TLS errors | ||||||
|   -h, --help               Print help |   -h, --help               Print help | ||||||
|   -V, --version            Print version |   -V, --version            Print version | ||||||
| ``` |  | ||||||
|  |  | ||||||
| See `gpclient -h` for help. | See 'gpclient help <command>' for more information on a specific command. | ||||||
|  | ``` | ||||||
|  |  | ||||||
| ### GUI | ### GUI | ||||||
|  |  | ||||||
| @@ -49,6 +51,15 @@ The GUI version is also available after you installed it. You can launch it from | |||||||
|  |  | ||||||
| ## Installation | ## Installation | ||||||
|  |  | ||||||
|  | > [!Note] | ||||||
|  | > | ||||||
|  | > This instruction is for the 2.x version. The 1.x version is still available on the [1.x](https://github.com/yuezk/GlobalProtect-openconnect/tree/1.x) branch, you can build it from the source code by following the instructions in the `README.md` file. | ||||||
|  |  | ||||||
|  | > [!Warning] | ||||||
|  | > | ||||||
|  | > The client requires `openconnect >= 8.20`, please make sure you have it installed, you can check it with `openconnect --version`. | ||||||
|  | > Installing the client from PPA will automatically install the required version of `openconnect`. | ||||||
|  |  | ||||||
| ### Debian/Ubuntu based distributions | ### Debian/Ubuntu based distributions | ||||||
|  |  | ||||||
| #### Install from PPA | #### Install from PPA | ||||||
| @@ -59,6 +70,10 @@ sudo apt-get update | |||||||
| sudo apt-get install globalprotect-openconnect | sudo apt-get install globalprotect-openconnect | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
|  | > [!Note] | ||||||
|  | > | ||||||
|  | > For Linux Mint, you might need to import the GPG key with: `sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 7937C393082992E5D6E4A60453FC26B43838D761` if you encountered an error `gpg: keyserver receive failed: General error`. | ||||||
|  |  | ||||||
| #### Install from deb package | #### Install from deb package | ||||||
|  |  | ||||||
| Download the latest deb package from [releases](https://github.com/yuezk/GlobalProtect-openconnect/releases) page. Then install it with `dpkg`: | Download the latest deb package from [releases](https://github.com/yuezk/GlobalProtect-openconnect/releases) page. Then install it with `dpkg`: | ||||||
| @@ -73,6 +88,10 @@ sudo dpkg -i globalprotect-openconnect_*.deb | |||||||
|  |  | ||||||
| Install from AUR: [globalprotect-openconnect-git](https://aur.archlinux.org/packages/globalprotect-openconnect-git/) | Install from AUR: [globalprotect-openconnect-git](https://aur.archlinux.org/packages/globalprotect-openconnect-git/) | ||||||
|  |  | ||||||
|  | ``` | ||||||
|  | yay -S globalprotect-openconnect-git | ||||||
|  | ``` | ||||||
|  |  | ||||||
| #### Install from package | #### Install from package | ||||||
|  |  | ||||||
| Download the latest package from [releases](https://github.com/yuezk/GlobalProtect-openconnect/releases) page. Then install it with `pacman`: | Download the latest package from [releases](https://github.com/yuezk/GlobalProtect-openconnect/releases) page. Then install it with `pacman`: | ||||||
| @@ -102,11 +121,7 @@ Download the latest RPM package from [releases](https://github.com/yuezk/GlobalP | |||||||
|  |  | ||||||
| ### Other distributions | ### Other distributions | ||||||
|  |  | ||||||
| The project depends on `openconnect`, `webkit2gtk`, `libsecret`, `libayatana-appindicator` or `libappindicator-gtk3`. You can install them first and then download the latest binary release (i.e., `*.bin.tar.gz`) from [releases](https://github.com/yuezk/GlobalProtect-openconnect/releases) page. | The project depends on `openconnect >= 8.20`, `webkit2gtk`, `libsecret`, `libayatana-appindicator` or `libappindicator-gtk3`. You can install them first and then download the latest binary release (i.e., `*.bin.tar.gz`) from [releases](https://github.com/yuezk/GlobalProtect-openconnect/releases) page. | ||||||
|  |  | ||||||
| ### Install the Old Version (v1.4.9) |  | ||||||
|  |  | ||||||
| The 1.x version is still available on the [1.x](https://github.com/yuezk/GlobalProtect-openconnect/tree/1.x) branch, you can build it from the source code by following the instructions in the `README.md` file. |  | ||||||
|  |  | ||||||
| ## [License](./LICENSE) | ## [License](./LICENSE) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -8,7 +8,7 @@ license.workspace = true | |||||||
| tauri-build = { version = "1.5", features = [] } | tauri-build = { version = "1.5", features = [] } | ||||||
|  |  | ||||||
| [dependencies] | [dependencies] | ||||||
| gpapi = { path = "../../crates/gpapi", features = ["tauri"] } | gpapi = { path = "../../crates/gpapi", features = ["tauri", "clap"] } | ||||||
| anyhow.workspace = true | anyhow.workspace = true | ||||||
| clap.workspace = true | clap.workspace = true | ||||||
| env_logger.workspace = true | env_logger.workspace = true | ||||||
|   | |||||||
| @@ -7,6 +7,7 @@ use std::{ | |||||||
| use anyhow::bail; | use anyhow::bail; | ||||||
| use gpapi::{ | use gpapi::{ | ||||||
|   auth::SamlAuthData, |   auth::SamlAuthData, | ||||||
|  |   gp_params::GpParams, | ||||||
|   portal::{prelogin, Prelogin}, |   portal::{prelogin, Prelogin}, | ||||||
|   utils::{redact::redact_uri, window::WindowExt}, |   utils::{redact::redact_uri, window::WindowExt}, | ||||||
| }; | }; | ||||||
| @@ -18,11 +19,13 @@ use tokio_util::sync::CancellationToken; | |||||||
| use webkit2gtk::{ | use webkit2gtk::{ | ||||||
|   gio::Cancellable, |   gio::Cancellable, | ||||||
|   glib::{GString, TimeSpan}, |   glib::{GString, TimeSpan}, | ||||||
|   LoadEvent, SettingsExt, URIResponse, URIResponseExt, WebContextExt, WebResource, WebResourceExt, |   LoadEvent, SettingsExt, TLSErrorsPolicy, URIResponse, URIResponseExt, WebContextExt, WebResource, | ||||||
|   WebView, WebViewExt, WebsiteDataManagerExtManual, WebsiteDataTypes, |   WebResourceExt, WebView, WebViewExt, WebsiteDataManagerExtManual, WebsiteDataTypes, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| enum AuthDataError { | enum AuthDataError { | ||||||
|  |   /// Failed to load page due to TLS error | ||||||
|  |   TlsError, | ||||||
|   /// 1. Found auth data in headers/body but it's invalid |   /// 1. Found auth data in headers/body but it's invalid | ||||||
|   /// 2. Loaded an empty page, failed to load page. etc. |   /// 2. Loaded an empty page, failed to load page. etc. | ||||||
|   Invalid, |   Invalid, | ||||||
| @@ -37,6 +40,7 @@ pub(crate) struct AuthWindow<'a> { | |||||||
|   server: &'a str, |   server: &'a str, | ||||||
|   saml_request: &'a str, |   saml_request: &'a str, | ||||||
|   user_agent: &'a str, |   user_agent: &'a str, | ||||||
|  |   gp_params: Option<GpParams>, | ||||||
|   clean: bool, |   clean: bool, | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -47,6 +51,7 @@ impl<'a> AuthWindow<'a> { | |||||||
|       server: "", |       server: "", | ||||||
|       saml_request: "", |       saml_request: "", | ||||||
|       user_agent: "", |       user_agent: "", | ||||||
|  |       gp_params: None, | ||||||
|       clean: false, |       clean: false, | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| @@ -66,6 +71,11 @@ impl<'a> AuthWindow<'a> { | |||||||
|     self |     self | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   pub fn gp_params(mut self, gp_params: GpParams) -> Self { | ||||||
|  |     self.gp_params.replace(gp_params); | ||||||
|  |     self | ||||||
|  |   } | ||||||
|  |  | ||||||
|   pub fn clean(mut self, clean: bool) -> Self { |   pub fn clean(mut self, clean: bool) -> Self { | ||||||
|     self.clean = clean; |     self.clean = clean; | ||||||
|     self |     self | ||||||
| @@ -119,6 +129,12 @@ impl<'a> AuthWindow<'a> { | |||||||
|     let saml_request = self.saml_request.to_string(); |     let saml_request = self.saml_request.to_string(); | ||||||
|     let (auth_result_tx, mut auth_result_rx) = mpsc::unbounded_channel::<AuthResult>(); |     let (auth_result_tx, mut auth_result_rx) = mpsc::unbounded_channel::<AuthResult>(); | ||||||
|     let raise_window_cancel_token: Arc<RwLock<Option<CancellationToken>>> = Default::default(); |     let raise_window_cancel_token: Arc<RwLock<Option<CancellationToken>>> = Default::default(); | ||||||
|  |     let gp_params = self.gp_params.as_ref().unwrap(); | ||||||
|  |     let tls_err_policy = if gp_params.ignore_tls_errors() { | ||||||
|  |       TLSErrorsPolicy::Ignore | ||||||
|  |     } else { | ||||||
|  |       TLSErrorsPolicy::Fail | ||||||
|  |     }; | ||||||
|  |  | ||||||
|     if self.clean { |     if self.clean { | ||||||
|       clear_webview_cookies(window).await?; |       clear_webview_cookies(window).await?; | ||||||
| @@ -128,6 +144,10 @@ impl<'a> AuthWindow<'a> { | |||||||
|     window.with_webview(move |wv| { |     window.with_webview(move |wv| { | ||||||
|       let wv = wv.inner(); |       let wv = wv.inner(); | ||||||
|  |  | ||||||
|  |       if let Some(context) = wv.context() { | ||||||
|  |         context.set_tls_errors_policy(tls_err_policy); | ||||||
|  |       } | ||||||
|  |  | ||||||
|       if let Some(settings) = wv.settings() { |       if let Some(settings) = wv.settings() { | ||||||
|         let ua = settings.user_agent().unwrap_or("".into()); |         let ua = settings.user_agent().unwrap_or("".into()); | ||||||
|         info!("Auth window user agent: {}", ua); |         info!("Auth window user agent: {}", ua); | ||||||
| @@ -168,31 +188,37 @@ impl<'a> AuthWindow<'a> { | |||||||
|         } |         } | ||||||
|       }); |       }); | ||||||
|  |  | ||||||
|       wv.connect_load_failed_with_tls_errors(|_wv, uri, cert, err| { |       let auth_result_tx_clone = auth_result_tx.clone(); | ||||||
|  |       wv.connect_load_failed_with_tls_errors(move |_wv, uri, cert, err| { | ||||||
|         let redacted_uri = redact_uri(uri); |         let redacted_uri = redact_uri(uri); | ||||||
|         warn!( |         warn!( | ||||||
|           "Failed to load uri: {} with error: {}, cert: {}", |           "Failed to load uri: {} with error: {}, cert: {}", | ||||||
|           redacted_uri, err, cert |           redacted_uri, err, cert | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|  |         send_auth_result(&auth_result_tx_clone, Err(AuthDataError::TlsError)); | ||||||
|         true |         true | ||||||
|       }); |       }); | ||||||
|  |  | ||||||
|       wv.connect_load_failed(move |_wv, _event, uri, err| { |       wv.connect_load_failed(move |_wv, _event, uri, err| { | ||||||
|         let redacted_uri = redact_uri(uri); |         let redacted_uri = redact_uri(uri); | ||||||
|         warn!("Failed to load uri: {} with error: {}", redacted_uri, err); |         warn!("Failed to load uri: {} with error: {}", redacted_uri, err); | ||||||
|         send_auth_result(&auth_result_tx, Err(AuthDataError::Invalid)); |         // NOTE: Don't send error here, since load_changed event will be triggered after this | ||||||
|  |         // send_auth_result(&auth_result_tx, Err(AuthDataError::Invalid)); | ||||||
|         // true to stop other handlers from being invoked for the event. false to propagate the event further. |         // true to stop other handlers from being invoked for the event. false to propagate the event further. | ||||||
|         true |         true | ||||||
|       }); |       }); | ||||||
|     })?; |     })?; | ||||||
|  |  | ||||||
|     let portal = self.server.to_string(); |     let portal = self.server.to_string(); | ||||||
|     let user_agent = self.user_agent.to_string(); |  | ||||||
|  |  | ||||||
|     loop { |     loop { | ||||||
|       if let Some(auth_result) = auth_result_rx.recv().await { |       if let Some(auth_result) = auth_result_rx.recv().await { | ||||||
|         match auth_result { |         match auth_result { | ||||||
|           Ok(auth_data) => return Ok(auth_data), |           Ok(auth_data) => return Ok(auth_data), | ||||||
|  |           Err(AuthDataError::TlsError) => { | ||||||
|  |             return Err(anyhow::anyhow!("TLS error: certificate verify failed")) | ||||||
|  |           } | ||||||
|           Err(AuthDataError::NotFound) => { |           Err(AuthDataError::NotFound) => { | ||||||
|             info!("No auth data found, it may not be the /SAML20/SP/ACS endpoint"); |             info!("No auth data found, it may not be the /SAML20/SP/ACS endpoint"); | ||||||
|  |  | ||||||
| @@ -237,7 +263,7 @@ impl<'a> AuthWindow<'a> { | |||||||
|               ); |               ); | ||||||
|             })?; |             })?; | ||||||
|  |  | ||||||
|             let saml_request = portal_prelogin(&portal, &user_agent).await?; |             let saml_request = portal_prelogin(&portal, gp_params).await?; | ||||||
|             window.with_webview(move |wv| { |             window.with_webview(move |wv| { | ||||||
|               let wv = wv.inner(); |               let wv = wv.inner(); | ||||||
|               load_saml_request(&wv, &saml_request); |               load_saml_request(&wv, &saml_request); | ||||||
| @@ -258,9 +284,10 @@ fn raise_window(window: &Arc<Window>) { | |||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| pub(crate) async fn portal_prelogin(portal: &str, user_agent: &str) -> anyhow::Result<String> { | pub(crate) async fn portal_prelogin(portal: &str, gp_params: &GpParams) -> anyhow::Result<String> { | ||||||
|   info!("Portal prelogin..."); |   info!("Portal prelogin..."); | ||||||
|   match prelogin(portal, user_agent).await? { |  | ||||||
|  |   match prelogin(portal, gp_params).await? { | ||||||
|     Prelogin::Saml(prelogin) => Ok(prelogin.saml_request().to_string()), |     Prelogin::Saml(prelogin) => Ok(prelogin.saml_request().to_string()), | ||||||
|     Prelogin::Standard(_) => Err(anyhow::anyhow!("Received non-SAML prelogin response")), |     Prelogin::Standard(_) => Err(anyhow::anyhow!("Received non-SAML prelogin response")), | ||||||
|   } |   } | ||||||
| @@ -397,6 +424,11 @@ fn read_auth_data(main_resource: &WebResource, auth_result_tx: mpsc::UnboundedSe | |||||||
|         send_auth_result(&auth_result_tx, auth_result) |         send_auth_result(&auth_result_tx, auth_result) | ||||||
|       }); |       }); | ||||||
|     } |     } | ||||||
|  |     Err(AuthDataError::TlsError) => { | ||||||
|  |       // NOTE: This is unreachable | ||||||
|  |       info!("TLS error found in headers, trying to read from body..."); | ||||||
|  |       send_auth_result(&auth_result_tx, Err(AuthDataError::TlsError)); | ||||||
|  |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,6 +1,8 @@ | |||||||
| use clap::Parser; | use clap::Parser; | ||||||
| use gpapi::{ | use gpapi::{ | ||||||
|   auth::{SamlAuthData, SamlAuthResult}, |   auth::{SamlAuthData, SamlAuthResult}, | ||||||
|  |   clap::args::Os, | ||||||
|  |   gp_params::{ClientOs, GpParams}, | ||||||
|   utils::{normalize_server, openssl}, |   utils::{normalize_server, openssl}, | ||||||
|   GP_USER_AGENT, |   GP_USER_AGENT, | ||||||
| }; | }; | ||||||
| @@ -26,23 +28,35 @@ struct Cli { | |||||||
|   saml_request: Option<String>, |   saml_request: Option<String>, | ||||||
|   #[arg(long, default_value = GP_USER_AGENT)] |   #[arg(long, default_value = GP_USER_AGENT)] | ||||||
|   user_agent: String, |   user_agent: String, | ||||||
|  |   #[arg(long, default_value = "Linux")] | ||||||
|  |   os: Os, | ||||||
|  |   #[arg(long)] | ||||||
|  |   os_version: Option<String>, | ||||||
|   #[arg(long)] |   #[arg(long)] | ||||||
|   hidpi: bool, |   hidpi: bool, | ||||||
|   #[arg(long)] |   #[arg(long)] | ||||||
|   fix_openssl: bool, |   fix_openssl: bool, | ||||||
|   #[arg(long)] |   #[arg(long)] | ||||||
|  |   ignore_tls_errors: bool, | ||||||
|  |   #[arg(long)] | ||||||
|   clean: bool, |   clean: bool, | ||||||
| } | } | ||||||
|  |  | ||||||
| impl Cli { | impl Cli { | ||||||
|   async fn run(&mut self) -> anyhow::Result<()> { |   async fn run(&mut self) -> anyhow::Result<()> { | ||||||
|  |     if self.ignore_tls_errors { | ||||||
|  |       info!("TLS errors will be ignored"); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     let mut openssl_conf = self.prepare_env()?; |     let mut openssl_conf = self.prepare_env()?; | ||||||
|  |  | ||||||
|     self.server = normalize_server(&self.server)?; |     self.server = normalize_server(&self.server)?; | ||||||
|  |     let gp_params = self.build_gp_params(); | ||||||
|  |  | ||||||
|     // Get the initial SAML request |     // Get the initial SAML request | ||||||
|     let saml_request = match self.saml_request { |     let saml_request = match self.saml_request { | ||||||
|       Some(ref saml_request) => saml_request.clone(), |       Some(ref saml_request) => saml_request.clone(), | ||||||
|       None => portal_prelogin(&self.server, &self.user_agent).await?, |       None => portal_prelogin(&self.server, &gp_params).await?, | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     self.saml_request.replace(saml_request); |     self.saml_request.replace(saml_request); | ||||||
| @@ -82,10 +96,22 @@ impl Cli { | |||||||
|     Ok(None) |     Ok(None) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   fn build_gp_params(&self) -> GpParams { | ||||||
|  |     let gp_params = GpParams::builder() | ||||||
|  |       .user_agent(&self.user_agent) | ||||||
|  |       .client_os(ClientOs::from(&self.os)) | ||||||
|  |       .os_version(self.os_version.clone()) | ||||||
|  |       .ignore_tls_errors(self.ignore_tls_errors) | ||||||
|  |       .build(); | ||||||
|  |  | ||||||
|  |     gp_params | ||||||
|  |   } | ||||||
|  |  | ||||||
|   async fn saml_auth(&self, app_handle: AppHandle) -> anyhow::Result<SamlAuthData> { |   async fn saml_auth(&self, app_handle: AppHandle) -> anyhow::Result<SamlAuthData> { | ||||||
|     let auth_window = AuthWindow::new(app_handle) |     let auth_window = AuthWindow::new(app_handle) | ||||||
|       .server(&self.server) |       .server(&self.server) | ||||||
|       .user_agent(&self.user_agent) |       .user_agent(&self.user_agent) | ||||||
|  |       .gp_params(self.build_gp_params()) | ||||||
|       .saml_request(self.saml_request.as_ref().unwrap()) |       .saml_request(self.saml_request.as_ref().unwrap()) | ||||||
|       .clean(self.clean); |       .clean(self.clean); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -6,7 +6,7 @@ edition.workspace = true | |||||||
| license.workspace = true | license.workspace = true | ||||||
|  |  | ||||||
| [dependencies] | [dependencies] | ||||||
| gpapi = { path = "../../crates/gpapi" } | gpapi = { path = "../../crates/gpapi", features = ["clap"] } | ||||||
| openconnect = { path = "../../crates/openconnect" } | openconnect = { path = "../../crates/openconnect" } | ||||||
| anyhow.workspace = true | anyhow.workspace = true | ||||||
| clap.workspace = true | clap.workspace = true | ||||||
|   | |||||||
| @@ -16,6 +16,11 @@ const VERSION: &str = concat!( | |||||||
|   ")" |   ")" | ||||||
| ); | ); | ||||||
|  |  | ||||||
|  | pub(crate) struct SharedArgs { | ||||||
|  |   pub(crate) fix_openssl: bool, | ||||||
|  |   pub(crate) ignore_tls_errors: bool, | ||||||
|  | } | ||||||
|  |  | ||||||
| #[derive(Subcommand)] | #[derive(Subcommand)] | ||||||
| enum CliCommand { | enum CliCommand { | ||||||
|   #[command(about = "Connect to a portal server")] |   #[command(about = "Connect to a portal server")] | ||||||
| @@ -40,6 +45,8 @@ enum CliCommand { | |||||||
| {usage-heading} {usage} | {usage-heading} {usage} | ||||||
|  |  | ||||||
| {all-args}{after-help} | {all-args}{after-help} | ||||||
|  |  | ||||||
|  | See 'gpclient help <command>' for more information on a specific command. | ||||||
| " | " | ||||||
| )] | )] | ||||||
| struct Cli { | struct Cli { | ||||||
| @@ -51,6 +58,8 @@ struct Cli { | |||||||
|     help = "Get around the OpenSSL `unsafe legacy renegotiation` error" |     help = "Get around the OpenSSL `unsafe legacy renegotiation` error" | ||||||
|   )] |   )] | ||||||
|   fix_openssl: bool, |   fix_openssl: bool, | ||||||
|  |   #[arg(long, help = "Ignore the TLS errors")] | ||||||
|  |   ignore_tls_errors: bool, | ||||||
| } | } | ||||||
|  |  | ||||||
| impl Cli { | impl Cli { | ||||||
| @@ -67,9 +76,17 @@ impl Cli { | |||||||
|     // The temp file will be dropped automatically when the file handle is dropped |     // The temp file will be dropped automatically when the file handle is dropped | ||||||
|     // So, declare it here to ensure it's not dropped |     // So, declare it here to ensure it's not dropped | ||||||
|     let _file = self.fix_openssl()?; |     let _file = self.fix_openssl()?; | ||||||
|  |     let shared_args = SharedArgs { | ||||||
|  |       fix_openssl: self.fix_openssl, | ||||||
|  |       ignore_tls_errors: self.ignore_tls_errors, | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     if self.ignore_tls_errors { | ||||||
|  |       info!("TLS errors will be ignored"); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     match &self.command { |     match &self.command { | ||||||
|       CliCommand::Connect(args) => ConnectHandler::new(args, self.fix_openssl).handle().await, |       CliCommand::Connect(args) => ConnectHandler::new(args, &shared_args).handle().await, | ||||||
|       CliCommand::Disconnect => DisconnectHandler::new().handle(), |       CliCommand::Disconnect => DisconnectHandler::new().handle(), | ||||||
|       CliCommand::LaunchGui(args) => LaunchGuiHandler::new(args).handle().await, |       CliCommand::LaunchGui(args) => LaunchGuiHandler::new(args).handle().await, | ||||||
|     } |     } | ||||||
| @@ -89,13 +106,24 @@ pub(crate) async fn run() { | |||||||
|   if let Err(err) = cli.run().await { |   if let Err(err) = cli.run().await { | ||||||
|     eprintln!("\nError: {}", err); |     eprintln!("\nError: {}", err); | ||||||
|  |  | ||||||
|     if err.to_string().contains("unsafe legacy renegotiation") && !cli.fix_openssl { |     let err = err.to_string(); | ||||||
|  |  | ||||||
|  |     if err.contains("unsafe legacy renegotiation") && !cli.fix_openssl { | ||||||
|       eprintln!("\nRe-run it with the `--fix-openssl` option to work around this issue, e.g.:\n"); |       eprintln!("\nRe-run it with the `--fix-openssl` option to work around this issue, e.g.:\n"); | ||||||
|       // Print the command |       // Print the command | ||||||
|       let args = std::env::args().collect::<Vec<_>>(); |       let args = std::env::args().collect::<Vec<_>>(); | ||||||
|       eprintln!("{} --fix-openssl {}\n", args[0], args[1..].join(" ")); |       eprintln!("{} --fix-openssl {}\n", args[0], args[1..].join(" ")); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     if err.contains("certificate verify failed") { | ||||||
|  |       eprintln!( | ||||||
|  |         "\nRe-run it with the `--ignore-tls-errors` option to ignore the certificate error, e.g.:\n" | ||||||
|  |       ); | ||||||
|  |       // Print the command | ||||||
|  |       let args = std::env::args().collect::<Vec<_>>(); | ||||||
|  |       eprintln!("{} --ignore-tls-errors {}\n", args[0], args[1..].join(" ")); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     std::process::exit(1); |     std::process::exit(1); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -2,9 +2,10 @@ use std::{fs, sync::Arc}; | |||||||
|  |  | ||||||
| use clap::Args; | use clap::Args; | ||||||
| use gpapi::{ | use gpapi::{ | ||||||
|  |   clap::args::Os, | ||||||
|   credential::{Credential, PasswordCredential}, |   credential::{Credential, PasswordCredential}, | ||||||
|   gateway::gateway_login, |   gateway::gateway_login, | ||||||
|   gp_params::GpParams, |   gp_params::{ClientOs, GpParams}, | ||||||
|   portal::{prelogin, retrieve_config, Prelogin}, |   portal::{prelogin, retrieve_config, Prelogin}, | ||||||
|   process::auth_launcher::SamlAuthLauncher, |   process::auth_launcher::SamlAuthLauncher, | ||||||
|   utils::{self, shutdown_signal}, |   utils::{self, shutdown_signal}, | ||||||
| @@ -14,7 +15,7 @@ use inquire::{Password, PasswordDisplayMode, Select, Text}; | |||||||
| use log::info; | use log::info; | ||||||
| use openconnect::Vpn; | use openconnect::Vpn; | ||||||
|  |  | ||||||
| use crate::GP_CLIENT_LOCK_FILE; | use crate::{cli::SharedArgs, GP_CLIENT_LOCK_FILE}; | ||||||
|  |  | ||||||
| #[derive(Args)] | #[derive(Args)] | ||||||
| pub(crate) struct ConnectArgs { | pub(crate) struct ConnectArgs { | ||||||
| @@ -36,20 +37,38 @@ pub(crate) struct ConnectArgs { | |||||||
|   script: Option<String>, |   script: Option<String>, | ||||||
|   #[arg(long, default_value = GP_USER_AGENT, help = "The user agent to use")] |   #[arg(long, default_value = GP_USER_AGENT, help = "The user agent to use")] | ||||||
|   user_agent: String, |   user_agent: String, | ||||||
|  |   #[arg(long, default_value = "Linux")] | ||||||
|  |   os: Os, | ||||||
|  |   #[arg(long)] | ||||||
|  |   os_version: Option<String>, | ||||||
|   #[arg(long, help = "The HiDPI mode, useful for high resolution screens")] |   #[arg(long, help = "The HiDPI mode, useful for high resolution screens")] | ||||||
|   hidpi: bool, |   hidpi: bool, | ||||||
|   #[arg(long, help = "Do not reuse the remembered authentication cookie")] |   #[arg(long, help = "Do not reuse the remembered authentication cookie")] | ||||||
|   clean: bool, |   clean: bool, | ||||||
| } | } | ||||||
|  |  | ||||||
|  | impl ConnectArgs { | ||||||
|  |   fn os_version(&self) -> String { | ||||||
|  |     if let Some(os_version) = &self.os_version { | ||||||
|  |       return os_version.to_owned(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     match self.os { | ||||||
|  |       Os::Linux => format!("Linux {}", whoami::distro()), | ||||||
|  |       Os::Windows => String::from("Microsoft Windows 11 Pro , 64-bit"), | ||||||
|  |       Os::Mac => String::from("Apple Mac OS X 13.4.0"), | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
| pub(crate) struct ConnectHandler<'a> { | pub(crate) struct ConnectHandler<'a> { | ||||||
|   args: &'a ConnectArgs, |   args: &'a ConnectArgs, | ||||||
|   fix_openssl: bool, |   shared_args: &'a SharedArgs, | ||||||
| } | } | ||||||
|  |  | ||||||
| impl<'a> ConnectHandler<'a> { | impl<'a> ConnectHandler<'a> { | ||||||
|   pub(crate) fn new(args: &'a ConnectArgs, fix_openssl: bool) -> Self { |   pub(crate) fn new(args: &'a ConnectArgs, shared_args: &'a SharedArgs) -> Self { | ||||||
|     Self { args, fix_openssl } |     Self { args, shared_args } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   pub(crate) async fn handle(&self) -> anyhow::Result<()> { |   pub(crate) async fn handle(&self) -> anyhow::Result<()> { | ||||||
| @@ -57,9 +76,12 @@ impl<'a> ConnectHandler<'a> { | |||||||
|  |  | ||||||
|     let gp_params = GpParams::builder() |     let gp_params = GpParams::builder() | ||||||
|       .user_agent(&self.args.user_agent) |       .user_agent(&self.args.user_agent) | ||||||
|  |       .client_os(ClientOs::from(&self.args.os)) | ||||||
|  |       .os_version(self.args.os_version()) | ||||||
|  |       .ignore_tls_errors(self.shared_args.ignore_tls_errors) | ||||||
|       .build(); |       .build(); | ||||||
|  |  | ||||||
|     let prelogin = prelogin(&portal, &self.args.user_agent).await?; |     let prelogin = prelogin(&portal, &gp_params).await?; | ||||||
|     let portal_credential = self.obtain_portal_credential(&prelogin).await?; |     let portal_credential = self.obtain_portal_credential(&prelogin).await?; | ||||||
|     let mut portal_config = retrieve_config(&portal, &portal_credential, &gp_params).await?; |     let mut portal_config = retrieve_config(&portal, &portal_credential, &gp_params).await?; | ||||||
|  |  | ||||||
| @@ -114,10 +136,13 @@ impl<'a> ConnectHandler<'a> { | |||||||
|     match prelogin { |     match prelogin { | ||||||
|       Prelogin::Saml(prelogin) => { |       Prelogin::Saml(prelogin) => { | ||||||
|         SamlAuthLauncher::new(&self.args.server) |         SamlAuthLauncher::new(&self.args.server) | ||||||
|           .user_agent(&self.args.user_agent) |  | ||||||
|           .saml_request(prelogin.saml_request()) |           .saml_request(prelogin.saml_request()) | ||||||
|  |           .user_agent(&self.args.user_agent) | ||||||
|  |           .os(self.args.os.as_str()) | ||||||
|  |           .os_version(Some(&self.args.os_version())) | ||||||
|           .hidpi(self.args.hidpi) |           .hidpi(self.args.hidpi) | ||||||
|           .fix_openssl(self.fix_openssl) |           .fix_openssl(self.shared_args.fix_openssl) | ||||||
|  |           .ignore_tls_errors(self.shared_args.ignore_tls_errors) | ||||||
|           .clean(self.args.clean) |           .clean(self.args.clean) | ||||||
|           .launch() |           .launch() | ||||||
|           .await |           .await | ||||||
|   | |||||||
| @@ -10,7 +10,12 @@ use log::info; | |||||||
|  |  | ||||||
| #[derive(Args)] | #[derive(Args)] | ||||||
| pub(crate) struct LaunchGuiArgs { | pub(crate) struct LaunchGuiArgs { | ||||||
|   #[clap(long, help = "Launch the GUI minimized")] |   #[arg( | ||||||
|  |     required = false, | ||||||
|  |     help = "The authentication data, used for the default browser authentication" | ||||||
|  |   )] | ||||||
|  |   auth_data: Option<String>, | ||||||
|  |   #[arg(long, help = "Launch the GUI minimized")] | ||||||
|   minimized: bool, |   minimized: bool, | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -30,6 +35,12 @@ impl<'a> LaunchGuiHandler<'a> { | |||||||
|       anyhow::bail!("`launch-gui` cannot be run as root"); |       anyhow::bail!("`launch-gui` cannot be run as root"); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     let auth_data = self.args.auth_data.as_deref().unwrap_or_default(); | ||||||
|  |     if !auth_data.is_empty() { | ||||||
|  |       // Process the authentication data, its format is `globalprotectcallback:<data>` | ||||||
|  |       return feed_auth_data(auth_data).await; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     if try_active_gui().await.is_ok() { |     if try_active_gui().await.is_ok() { | ||||||
|       info!("The GUI is already running"); |       info!("The GUI is already running"); | ||||||
|       return Ok(()); |       return Ok(()); | ||||||
| @@ -66,6 +77,19 @@ impl<'a> LaunchGuiHandler<'a> { | |||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | async fn feed_auth_data(auth_data: &str) -> anyhow::Result<()> { | ||||||
|  |   let service_endpoint = http_endpoint().await?; | ||||||
|  |  | ||||||
|  |   reqwest::Client::default() | ||||||
|  |     .post(format!("{}/auth-data", service_endpoint)) | ||||||
|  |     .json(&auth_data) | ||||||
|  |     .send() | ||||||
|  |     .await? | ||||||
|  |     .error_for_status()?; | ||||||
|  |  | ||||||
|  |   Ok(()) | ||||||
|  | } | ||||||
|  |  | ||||||
| async fn try_active_gui() -> anyhow::Result<()> { | async fn try_active_gui() -> anyhow::Result<()> { | ||||||
|   let service_endpoint = http_endpoint().await?; |   let service_endpoint = http_endpoint().await?; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,19 +0,0 @@ | |||||||
| <?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>GlobalProtect-openconnect</vendor> |  | ||||||
|   <vendor_url>https://github.com/yuezk/GlobalProtect-openconnect</vendor_url> |  | ||||||
|   <icon_name>gpgui</icon_name> |  | ||||||
|   <action id="com.yuezk.gpservice"> |  | ||||||
|     <description>Run GPService as root</description> |  | ||||||
|     <message>Authentication is required to run the GPService as root</message> |  | ||||||
|     <defaults> |  | ||||||
|       <allow_any>yes</allow_any> |  | ||||||
|       <allow_inactive>yes</allow_inactive> |  | ||||||
|       <allow_active>yes</allow_active> |  | ||||||
|     </defaults> |  | ||||||
|     <annotate key="org.freedesktop.policykit.exec.path">/home/kevin/Documents/repos/gp/target/debug/gpservice</annotate> |  | ||||||
|     <annotate key="org.freedesktop.policykit.exec.argv1">--with-gui</annotate> |  | ||||||
|     <annotate key="org.freedesktop.policykit.exec.allow_gui">true</annotate> |  | ||||||
|   </action> |  | ||||||
| </policyconfig> |  | ||||||
| @@ -21,6 +21,13 @@ pub(crate) async fn active_gui(State(ctx): State<Arc<WsServerContext>>) -> impl | |||||||
|   ctx.send_event(WsEvent::ActiveGui).await; |   ctx.send_event(WsEvent::ActiveGui).await; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | pub(crate) async fn auth_data( | ||||||
|  |   State(ctx): State<Arc<WsServerContext>>, | ||||||
|  |   body: String, | ||||||
|  | ) -> impl IntoResponse { | ||||||
|  |   ctx.send_event(WsEvent::AuthData(body)).await; | ||||||
|  | } | ||||||
|  |  | ||||||
| pub(crate) async fn ws_handler( | pub(crate) async fn ws_handler( | ||||||
|   ws: WebSocketUpgrade, |   ws: WebSocketUpgrade, | ||||||
|   State(ctx): State<Arc<WsServerContext>>, |   State(ctx): State<Arc<WsServerContext>>, | ||||||
|   | |||||||
| @@ -8,6 +8,7 @@ pub(crate) fn routes(ctx: Arc<WsServerContext>) -> Router { | |||||||
|   Router::new() |   Router::new() | ||||||
|     .route("/health", get(handlers::health)) |     .route("/health", get(handlers::health)) | ||||||
|     .route("/active-gui", post(handlers::active_gui)) |     .route("/active-gui", post(handlers::active_gui)) | ||||||
|  |     .route("/auth-data", post(handlers::auth_data)) | ||||||
|     .route("/ws", get(handlers::ws_handler)) |     .route("/ws", get(handlers::ws_handler)) | ||||||
|     .with_state(ctx) |     .with_state(ctx) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -27,6 +27,10 @@ dotenvy_macro.workspace = true | |||||||
| uzers.workspace = true | uzers.workspace = true | ||||||
|  |  | ||||||
| tauri = { workspace = true, optional = true } | tauri = { workspace = true, optional = true } | ||||||
|  | clap = { workspace = true, optional = true } | ||||||
|  | open = { version = "5", optional = true } | ||||||
|  |  | ||||||
| [features] | [features] | ||||||
| tauri = ["dep:tauri"] | tauri = ["dep:tauri"] | ||||||
|  | clap = ["dep:clap"] | ||||||
|  | browser-auth = ["dep:open"] | ||||||
|   | |||||||
| @@ -1,3 +1,5 @@ | |||||||
|  | use anyhow::bail; | ||||||
|  | use regex::Regex; | ||||||
| use serde::{Deserialize, Serialize}; | use serde::{Deserialize, Serialize}; | ||||||
|  |  | ||||||
| #[derive(Debug, Serialize, Deserialize)] | #[derive(Debug, Serialize, Deserialize)] | ||||||
| @@ -37,6 +39,32 @@ impl SamlAuthData { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   pub fn parse_html(html: &str) -> anyhow::Result<SamlAuthData> { | ||||||
|  |     match parse_xml_tag(html, "saml-auth-status") { | ||||||
|  |       Some(saml_status) if saml_status == "1" => { | ||||||
|  |         let username = parse_xml_tag(html, "saml-username"); | ||||||
|  |         let prelogin_cookie = parse_xml_tag(html, "prelogin-cookie"); | ||||||
|  |         let portal_userauthcookie = parse_xml_tag(html, "portal-userauthcookie"); | ||||||
|  |  | ||||||
|  |         if SamlAuthData::check(&username, &prelogin_cookie, &portal_userauthcookie) { | ||||||
|  |           return Ok(SamlAuthData::new( | ||||||
|  |             username.unwrap(), | ||||||
|  |             prelogin_cookie, | ||||||
|  |             portal_userauthcookie, | ||||||
|  |           )); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         bail!("Found invalid auth data in HTML"); | ||||||
|  |       } | ||||||
|  |       Some(status) => { | ||||||
|  |         bail!("Found invalid SAML status {} in HTML", status); | ||||||
|  |       } | ||||||
|  |       None => { | ||||||
|  |         bail!("No auth data found in HTML"); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|   pub fn username(&self) -> &str { |   pub fn username(&self) -> &str { | ||||||
|     &self.username |     &self.username | ||||||
|   } |   } | ||||||
| @@ -61,3 +89,10 @@ impl SamlAuthData { | |||||||
|     username_valid && (prelogin_cookie_valid || portal_userauthcookie_valid) |     username_valid && (prelogin_cookie_valid || portal_userauthcookie_valid) | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | pub 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()) | ||||||
|  | } | ||||||
|   | |||||||
							
								
								
									
										64
									
								
								crates/gpapi/src/clap/args.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								crates/gpapi/src/clap/args.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,64 @@ | |||||||
|  | use clap::{builder::PossibleValue, ValueEnum}; | ||||||
|  |  | ||||||
|  | use crate::gp_params::ClientOs; | ||||||
|  |  | ||||||
|  | #[derive(Debug, Clone)] | ||||||
|  | pub enum Os { | ||||||
|  |   Linux, | ||||||
|  |   Windows, | ||||||
|  |   Mac, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl Os { | ||||||
|  |   pub fn as_str(&self) -> &'static str { | ||||||
|  |     match self { | ||||||
|  |       Os::Linux => "Linux", | ||||||
|  |       Os::Windows => "Windows", | ||||||
|  |       Os::Mac => "Mac", | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl From<&str> for Os { | ||||||
|  |   fn from(os: &str) -> Self { | ||||||
|  |     match os.to_lowercase().as_str() { | ||||||
|  |       "linux" => Os::Linux, | ||||||
|  |       "windows" => Os::Windows, | ||||||
|  |       "mac" => Os::Mac, | ||||||
|  |       _ => Os::Linux, | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl From<&Os> for ClientOs { | ||||||
|  |   fn from(value: &Os) -> Self { | ||||||
|  |     match value { | ||||||
|  |       Os::Linux => ClientOs::Linux, | ||||||
|  |       Os::Windows => ClientOs::Windows, | ||||||
|  |       Os::Mac => ClientOs::Mac, | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl ValueEnum for Os { | ||||||
|  |   fn value_variants<'a>() -> &'a [Self] { | ||||||
|  |     &[Os::Linux, Os::Windows, Os::Mac] | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> { | ||||||
|  |     match self { | ||||||
|  |       Os::Linux => Some(PossibleValue::new("Linux")), | ||||||
|  |       Os::Windows => Some(PossibleValue::new("Windows")), | ||||||
|  |       Os::Mac => Some(PossibleValue::new("Mac")), | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   fn from_str(input: &str, _: bool) -> Result<Self, String> { | ||||||
|  |     match input.to_lowercase().as_str() { | ||||||
|  |       "linux" => Ok(Os::Linux), | ||||||
|  |       "windows" => Ok(Os::Windows), | ||||||
|  |       "mac" => Ok(Os::Mac), | ||||||
|  |       _ => Err(format!("Invalid OS: {}", input)), | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										1
									
								
								crates/gpapi/src/clap/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								crates/gpapi/src/clap/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | pub mod args; | ||||||
| @@ -3,7 +3,7 @@ use std::collections::HashMap; | |||||||
| use serde::{Deserialize, Serialize}; | use serde::{Deserialize, Serialize}; | ||||||
| use specta::Type; | use specta::Type; | ||||||
|  |  | ||||||
| use crate::auth::SamlAuthData; | use crate::{auth::SamlAuthData, utils::base64::decode_to_string}; | ||||||
|  |  | ||||||
| #[derive(Debug, Serialize, Deserialize, Type, Clone)] | #[derive(Debug, Serialize, Deserialize, Type, Clone)] | ||||||
| #[serde(rename_all = "camelCase")] | #[serde(rename_all = "camelCase")] | ||||||
| @@ -151,6 +151,17 @@ pub enum Credential { | |||||||
| } | } | ||||||
|  |  | ||||||
| impl Credential { | impl Credential { | ||||||
|  |   /// Create a credential from a globalprotectcallback:<base64 encoded string> | ||||||
|  |   pub fn parse_gpcallback(auth_data: &str) -> anyhow::Result<Self> { | ||||||
|  |     // Remove the surrounding quotes | ||||||
|  |     let auth_data = auth_data.trim_matches('"'); | ||||||
|  |     let auth_data = auth_data.trim_start_matches("globalprotectcallback:"); | ||||||
|  |     let auth_data = decode_to_string(auth_data)?; | ||||||
|  |     let auth_data = SamlAuthData::parse_html(&auth_data)?; | ||||||
|  |  | ||||||
|  |     Self::try_from(auth_data) | ||||||
|  |   } | ||||||
|  |  | ||||||
|   pub fn username(&self) -> &str { |   pub fn username(&self) -> &str { | ||||||
|     match self { |     match self { | ||||||
|       Credential::Password(cred) => cred.username(), |       Credential::Password(cred) => cred.username(), | ||||||
| @@ -164,31 +175,34 @@ impl Credential { | |||||||
|     let mut params = HashMap::new(); |     let mut params = HashMap::new(); | ||||||
|     params.insert("user", self.username()); |     params.insert("user", self.username()); | ||||||
|  |  | ||||||
|     match self { |     let (passwd, prelogin_cookie, portal_userauthcookie, portal_prelogonuserauthcookie) = match self | ||||||
|       Credential::Password(cred) => { |     { | ||||||
|         params.insert("passwd", cred.password()); |       Credential::Password(cred) => (Some(cred.password()), None, None, None), | ||||||
|       } |       Credential::PreloginCookie(cred) => (None, Some(cred.prelogin_cookie()), None, None), | ||||||
|       Credential::PreloginCookie(cred) => { |       Credential::AuthCookie(cred) => ( | ||||||
|         params.insert("prelogin-cookie", cred.prelogin_cookie()); |         None, | ||||||
|       } |         None, | ||||||
|       Credential::AuthCookie(cred) => { |         Some(cred.user_auth_cookie()), | ||||||
|         params.insert("portal-userauthcookie", cred.user_auth_cookie()); |         Some(cred.prelogon_user_auth_cookie()), | ||||||
|  |       ), | ||||||
|  |       Credential::CachedCredential(cred) => ( | ||||||
|  |         cred.password(), | ||||||
|  |         None, | ||||||
|  |         Some(cred.auth_cookie.user_auth_cookie()), | ||||||
|  |         Some(cred.auth_cookie.prelogon_user_auth_cookie()), | ||||||
|  |       ), | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     params.insert("passwd", passwd.unwrap_or_default()); | ||||||
|  |     params.insert("prelogin-cookie", prelogin_cookie.unwrap_or_default()); | ||||||
|  |     params.insert( | ||||||
|  |       "portal-userauthcookie", | ||||||
|  |       portal_userauthcookie.unwrap_or_default(), | ||||||
|  |     ); | ||||||
|     params.insert( |     params.insert( | ||||||
|       "portal-prelogonuserauthcookie", |       "portal-prelogonuserauthcookie", | ||||||
|           cred.prelogon_user_auth_cookie(), |       portal_prelogonuserauthcookie.unwrap_or_default(), | ||||||
|     ); |     ); | ||||||
|       } |  | ||||||
|       Credential::CachedCredential(cred) => { |  | ||||||
|         if let Some(password) = cred.password() { |  | ||||||
|           params.insert("passwd", password); |  | ||||||
|         } |  | ||||||
|         params.insert("portal-userauthcookie", cred.auth_cookie.user_auth_cookie()); |  | ||||||
|         params.insert( |  | ||||||
|           "portal-prelogonuserauthcookie", |  | ||||||
|           cred.auth_cookie.prelogon_user_auth_cookie(), |  | ||||||
|         ); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     params |     params | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -23,14 +23,8 @@ pub async fn gateway_login( | |||||||
|  |  | ||||||
|   info!("Gateway login, user_agent: {}", gp_params.user_agent()); |   info!("Gateway login, user_agent: {}", gp_params.user_agent()); | ||||||
|  |  | ||||||
|   let res_xml = client |   let res = client.post(&login_url).form(¶ms).send().await?; | ||||||
|     .post(&login_url) |   let res_xml = res.error_for_status()?.text().await?; | ||||||
|     .form(¶ms) |  | ||||||
|     .send() |  | ||||||
|     .await? |  | ||||||
|     .error_for_status()? |  | ||||||
|     .text() |  | ||||||
|     .await?; |  | ||||||
|  |  | ||||||
|   let doc = Document::parse(&res_xml)?; |   let doc = Document::parse(&res_xml)?; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -7,23 +7,32 @@ use crate::GP_USER_AGENT; | |||||||
|  |  | ||||||
| #[derive(Debug, Serialize, Deserialize, Clone, Type, Default)] | #[derive(Debug, Serialize, Deserialize, Clone, Type, Default)] | ||||||
| pub enum ClientOs { | pub enum ClientOs { | ||||||
|   Linux, |  | ||||||
|   #[default] |   #[default] | ||||||
|  |   Linux, | ||||||
|   Windows, |   Windows, | ||||||
|   Mac, |   Mac, | ||||||
| } | } | ||||||
|  |  | ||||||
| impl From<&ClientOs> for &str { | impl From<&str> for ClientOs { | ||||||
|   fn from(os: &ClientOs) -> Self { |   fn from(os: &str) -> Self { | ||||||
|     match os { |     match os { | ||||||
|       ClientOs::Linux => "Linux", |       "Linux" => ClientOs::Linux, | ||||||
|       ClientOs::Windows => "Windows", |       "Windows" => ClientOs::Windows, | ||||||
|       ClientOs::Mac => "Mac", |       "Mac" => ClientOs::Mac, | ||||||
|  |       _ => ClientOs::Linux, | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| impl ClientOs { | impl ClientOs { | ||||||
|  |   pub fn as_str(&self) -> &str { | ||||||
|  |     match self { | ||||||
|  |       ClientOs::Linux => "Linux", | ||||||
|  |       ClientOs::Windows => "Windows", | ||||||
|  |       ClientOs::Mac => "Mac", | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|   pub fn to_openconnect_os(&self) -> &str { |   pub fn to_openconnect_os(&self) -> &str { | ||||||
|     match self { |     match self { | ||||||
|       ClientOs::Linux => "linux", |       ClientOs::Linux => "linux", | ||||||
| @@ -39,7 +48,9 @@ pub struct GpParams { | |||||||
|   client_os: ClientOs, |   client_os: ClientOs, | ||||||
|   os_version: Option<String>, |   os_version: Option<String>, | ||||||
|   client_version: Option<String>, |   client_version: Option<String>, | ||||||
|   computer: Option<String>, |   computer: String, | ||||||
|  |   ignore_tls_errors: bool, | ||||||
|  |   prefer_default_browser: bool, | ||||||
| } | } | ||||||
|  |  | ||||||
| impl GpParams { | impl GpParams { | ||||||
| @@ -52,15 +63,20 @@ impl GpParams { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   pub(crate) fn computer(&self) -> &str { |   pub(crate) fn computer(&self) -> &str { | ||||||
|     match self.computer { |     &self.computer | ||||||
|       Some(ref computer) => computer, |  | ||||||
|       None => (&self.client_os).into() |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   pub fn ignore_tls_errors(&self) -> bool { | ||||||
|  |     self.ignore_tls_errors | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   pub fn prefer_default_browser(&self) -> bool { | ||||||
|  |     self.prefer_default_browser | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   pub(crate) fn to_params(&self) -> HashMap<&str, &str> { |   pub(crate) fn to_params(&self) -> HashMap<&str, &str> { | ||||||
|     let mut params: HashMap<&str, &str> = HashMap::new(); |     let mut params: HashMap<&str, &str> = HashMap::new(); | ||||||
|     let client_os: &str = (&self.client_os).into(); |     let client_os = self.client_os.as_str(); | ||||||
|  |  | ||||||
|     // Common params |     // Common params | ||||||
|     params.insert("prot", "https:"); |     params.insert("prot", "https:"); | ||||||
| @@ -70,22 +86,17 @@ impl GpParams { | |||||||
|     params.insert("ipv6-support", "yes"); |     params.insert("ipv6-support", "yes"); | ||||||
|     params.insert("inputStr", ""); |     params.insert("inputStr", ""); | ||||||
|     params.insert("clientVer", "4100"); |     params.insert("clientVer", "4100"); | ||||||
|  |  | ||||||
|     params.insert("clientos", client_os); |     params.insert("clientos", client_os); | ||||||
|  |     params.insert("computer", &self.computer); | ||||||
|     if let Some(computer) = &self.computer { |  | ||||||
|       params.insert("computer", computer); |  | ||||||
|     } else { |  | ||||||
|       params.insert("computer", client_os); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if let Some(os_version) = &self.os_version { |     if let Some(os_version) = &self.os_version { | ||||||
|       params.insert("os-version", os_version); |       params.insert("os-version", os_version); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     if let Some(client_version) = &self.client_version { |     // NOTE: Do not include clientgpversion for now | ||||||
|       params.insert("clientgpversion", client_version); |     // if let Some(client_version) = &self.client_version { | ||||||
|     } |     //   params.insert("clientgpversion", client_version); | ||||||
|  |     // } | ||||||
|  |  | ||||||
|     params |     params | ||||||
|   } |   } | ||||||
| @@ -96,7 +107,9 @@ pub struct GpParamsBuilder { | |||||||
|   client_os: ClientOs, |   client_os: ClientOs, | ||||||
|   os_version: Option<String>, |   os_version: Option<String>, | ||||||
|   client_version: Option<String>, |   client_version: Option<String>, | ||||||
|   computer: Option<String>, |   computer: String, | ||||||
|  |   ignore_tls_errors: bool, | ||||||
|  |   prefer_default_browser: bool, | ||||||
| } | } | ||||||
|  |  | ||||||
| impl GpParamsBuilder { | impl GpParamsBuilder { | ||||||
| @@ -106,7 +119,9 @@ impl GpParamsBuilder { | |||||||
|       client_os: ClientOs::Linux, |       client_os: ClientOs::Linux, | ||||||
|       os_version: Default::default(), |       os_version: Default::default(), | ||||||
|       client_version: Default::default(), |       client_version: Default::default(), | ||||||
|       computer: Default::default(), |       computer: whoami::hostname(), | ||||||
|  |       ignore_tls_errors: false, | ||||||
|  |       prefer_default_browser: false, | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -120,18 +135,28 @@ impl GpParamsBuilder { | |||||||
|     self |     self | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   pub fn os_version(&mut self, os_version: &str) -> &mut Self { |   pub fn os_version<T: Into<Option<String>>>(&mut self, os_version: T) -> &mut Self { | ||||||
|     self.os_version = Some(os_version.to_string()); |     self.os_version = os_version.into(); | ||||||
|     self |     self | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   pub fn client_version(&mut self, client_version: &str) -> &mut Self { |   pub fn client_version<T: Into<Option<String>>>(&mut self, client_version: T) -> &mut Self { | ||||||
|     self.client_version = Some(client_version.to_string()); |     self.client_version = client_version.into(); | ||||||
|     self |     self | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   pub fn computer(&mut self, computer: &str) -> &mut Self { |   pub fn computer(&mut self, computer: &str) -> &mut Self { | ||||||
|     self.computer = Some(computer.to_string()); |     self.computer = computer.to_string(); | ||||||
|  |     self | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   pub fn ignore_tls_errors(&mut self, ignore_tls_errors: bool) -> &mut Self { | ||||||
|  |     self.ignore_tls_errors = ignore_tls_errors; | ||||||
|  |     self | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   pub fn prefer_default_browser(&mut self, prefer_default_browser: bool) -> &mut Self { | ||||||
|  |     self.prefer_default_browser = prefer_default_browser; | ||||||
|     self |     self | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -142,6 +167,8 @@ impl GpParamsBuilder { | |||||||
|       os_version: self.os_version.clone(), |       os_version: self.os_version.clone(), | ||||||
|       client_version: self.client_version.clone(), |       client_version: self.client_version.clone(), | ||||||
|       computer: self.computer.clone(), |       computer: self.computer.clone(), | ||||||
|  |       ignore_tls_errors: self.ignore_tls_errors, | ||||||
|  |       prefer_default_browser: self.prefer_default_browser, | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -7,12 +7,17 @@ pub mod process; | |||||||
| pub mod service; | pub mod service; | ||||||
| pub mod utils; | pub mod utils; | ||||||
|  |  | ||||||
|  | #[cfg(feature = "clap")] | ||||||
|  | pub mod clap; | ||||||
|  |  | ||||||
| #[cfg(debug_assertions)] | #[cfg(debug_assertions)] | ||||||
| pub const GP_API_KEY: &[u8; 32] = &[0; 32]; | pub const GP_API_KEY: &[u8; 32] = &[0; 32]; | ||||||
|  |  | ||||||
| pub const GP_USER_AGENT: &str = "PAN GlobalProtect"; | pub const GP_USER_AGENT: &str = "PAN GlobalProtect"; | ||||||
| pub const GP_SERVICE_LOCK_FILE: &str = "/var/run/gpservice.lock"; | pub const GP_SERVICE_LOCK_FILE: &str = "/var/run/gpservice.lock"; | ||||||
|  |  | ||||||
|  | #[cfg(not(debug_assertions))] | ||||||
|  | pub const GP_CLIENT_BINARY: &str = "/usr/bin/gpclient"; | ||||||
| #[cfg(not(debug_assertions))] | #[cfg(not(debug_assertions))] | ||||||
| pub const GP_SERVICE_BINARY: &str = "/usr/bin/gpservice"; | pub const GP_SERVICE_BINARY: &str = "/usr/bin/gpservice"; | ||||||
| #[cfg(not(debug_assertions))] | #[cfg(not(debug_assertions))] | ||||||
| @@ -20,6 +25,8 @@ pub const GP_GUI_BINARY: &str = "/usr/bin/gpgui"; | |||||||
| #[cfg(not(debug_assertions))] | #[cfg(not(debug_assertions))] | ||||||
| pub(crate) const GP_AUTH_BINARY: &str = "/usr/bin/gpauth"; | pub(crate) const GP_AUTH_BINARY: &str = "/usr/bin/gpauth"; | ||||||
|  |  | ||||||
|  | #[cfg(debug_assertions)] | ||||||
|  | pub const GP_CLIENT_BINARY: &str = dotenvy_macro::dotenv!("GP_CLIENT_BINARY"); | ||||||
| #[cfg(debug_assertions)] | #[cfg(debug_assertions)] | ||||||
| pub const GP_SERVICE_BINARY: &str = dotenvy_macro::dotenv!("GP_SERVICE_BINARY"); | pub const GP_SERVICE_BINARY: &str = dotenvy_macro::dotenv!("GP_SERVICE_BINARY"); | ||||||
| #[cfg(debug_assertions)] | #[cfg(debug_assertions)] | ||||||
|   | |||||||
| @@ -132,14 +132,8 @@ pub async fn retrieve_config( | |||||||
|  |  | ||||||
|   info!("Portal config, user_agent: {}", gp_params.user_agent()); |   info!("Portal config, user_agent: {}", gp_params.user_agent()); | ||||||
|  |  | ||||||
|   let res_xml = client |   let res = client.post(&url).form(¶ms).send().await?; | ||||||
|     .post(&url) |   let res_xml = res.error_for_status()?.text().await?; | ||||||
|     .form(¶ms) |  | ||||||
|     .send() |  | ||||||
|     .await? |  | ||||||
|     .error_for_status()? |  | ||||||
|     .text() |  | ||||||
|     .await?; |  | ||||||
|  |  | ||||||
|   ensure!(!res_xml.is_empty(), PortalConfigError::EmptyResponse); |   ensure!(!res_xml.is_empty(), PortalConfigError::EmptyResponse); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -5,13 +5,28 @@ use roxmltree::Document; | |||||||
| use serde::Serialize; | use serde::Serialize; | ||||||
| use specta::Type; | use specta::Type; | ||||||
|  |  | ||||||
| use crate::utils::{base64, normalize_server, xml}; | use crate::{ | ||||||
|  |   gp_params::GpParams, | ||||||
|  |   utils::{base64, normalize_server, xml}, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const REQUIRED_PARAMS: [&str; 8] = [ | ||||||
|  |   "tmp", | ||||||
|  |   "clientVer", | ||||||
|  |   "clientos", | ||||||
|  |   "os-version", | ||||||
|  |   "host-id", | ||||||
|  |   "ipv6-support", | ||||||
|  |   "default-browser", | ||||||
|  |   "cas-support", | ||||||
|  | ]; | ||||||
|  |  | ||||||
| #[derive(Debug, Serialize, Type, Clone)] | #[derive(Debug, Serialize, Type, Clone)] | ||||||
| #[serde(rename_all = "camelCase")] | #[serde(rename_all = "camelCase")] | ||||||
| pub struct SamlPrelogin { | pub struct SamlPrelogin { | ||||||
|   region: String, |   region: String, | ||||||
|   saml_request: String, |   saml_request: String, | ||||||
|  |   support_default_browser: bool, | ||||||
| } | } | ||||||
|  |  | ||||||
| impl SamlPrelogin { | impl SamlPrelogin { | ||||||
| @@ -22,6 +37,10 @@ impl SamlPrelogin { | |||||||
|   pub fn saml_request(&self) -> &str { |   pub fn saml_request(&self) -> &str { | ||||||
|     &self.saml_request |     &self.saml_request | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   pub fn support_default_browser(&self) -> bool { | ||||||
|  |     self.support_default_browser | ||||||
|  |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| #[derive(Debug, Serialize, Type, Clone)] | #[derive(Debug, Serialize, Type, Clone)] | ||||||
| @@ -67,20 +86,33 @@ impl Prelogin { | |||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| pub async fn prelogin(portal: &str, user_agent: &str) -> anyhow::Result<Prelogin> { | pub async fn prelogin(portal: &str, gp_params: &GpParams) -> anyhow::Result<Prelogin> { | ||||||
|  |   let user_agent = gp_params.user_agent(); | ||||||
|   info!("Portal prelogin, user_agent: {}", user_agent); |   info!("Portal prelogin, user_agent: {}", user_agent); | ||||||
|  |  | ||||||
|   let portal = normalize_server(portal)?; |   let portal = normalize_server(portal)?; | ||||||
|   let prelogin_url = format!("{}/global-protect/prelogin.esp", portal); |   let prelogin_url = format!("{}/global-protect/prelogin.esp", portal); | ||||||
|   let client = Client::builder().user_agent(user_agent).build()?; |   let mut params = gp_params.to_params(); | ||||||
|  |  | ||||||
|   let res_xml = client |   params.insert("tmp", "tmp"); | ||||||
|     .get(&prelogin_url) |   params.insert("cas-support", "yes"); | ||||||
|     .send() |   if gp_params.prefer_default_browser() { | ||||||
|     .await? |     params.insert("default-browser", "1"); | ||||||
|     .error_for_status()? |   } | ||||||
|     .text() |  | ||||||
|     .await?; |   params.retain(|k, _| { | ||||||
|  |     REQUIRED_PARAMS | ||||||
|  |       .iter() | ||||||
|  |       .any(|required_param| required_param == k) | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   let client = Client::builder() | ||||||
|  |     .danger_accept_invalid_certs(gp_params.ignore_tls_errors()) | ||||||
|  |     .user_agent(user_agent) | ||||||
|  |     .build()?; | ||||||
|  |  | ||||||
|  |   let res = client.post(&prelogin_url).form(¶ms).send().await?; | ||||||
|  |   let res_xml = res.error_for_status()?.text().await?; | ||||||
|  |  | ||||||
|   trace!("Prelogin response: {}", res_xml); |   trace!("Prelogin response: {}", res_xml); | ||||||
|   let doc = Document::parse(&res_xml)?; |   let doc = Document::parse(&res_xml)?; | ||||||
| @@ -98,12 +130,18 @@ pub async fn prelogin(portal: &str, user_agent: &str) -> anyhow::Result<Prelogin | |||||||
|  |  | ||||||
|   let saml_method = xml::get_child_text(&doc, "saml-auth-method"); |   let saml_method = xml::get_child_text(&doc, "saml-auth-method"); | ||||||
|   let saml_request = xml::get_child_text(&doc, "saml-request"); |   let saml_request = xml::get_child_text(&doc, "saml-request"); | ||||||
|  |   let saml_default_browser = xml::get_child_text(&doc, "saml-default-browser"); | ||||||
|   // Check if the prelogin response is SAML |   // Check if the prelogin response is SAML | ||||||
|   if saml_method.is_some() && saml_request.is_some() { |   if saml_method.is_some() && saml_request.is_some() { | ||||||
|     let saml_request = base64::decode_to_string(&saml_request.unwrap())?; |     let saml_request = base64::decode_to_string(&saml_request.unwrap())?; | ||||||
|  |     let support_default_browser = saml_default_browser | ||||||
|  |       .map(|s| s.to_lowercase() == "yes") | ||||||
|  |       .unwrap_or(false); | ||||||
|  |  | ||||||
|     let saml_prelogin = SamlPrelogin { |     let saml_prelogin = SamlPrelogin { | ||||||
|       region, |       region, | ||||||
|       saml_request, |       saml_request, | ||||||
|  |       support_default_browser, | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     return Ok(Prelogin::Saml(saml_prelogin)); |     return Ok(Prelogin::Saml(saml_prelogin)); | ||||||
|   | |||||||
| @@ -8,10 +8,13 @@ use super::command_traits::CommandExt; | |||||||
|  |  | ||||||
| pub struct SamlAuthLauncher<'a> { | pub struct SamlAuthLauncher<'a> { | ||||||
|   server: &'a str, |   server: &'a str, | ||||||
|   user_agent: Option<&'a str>, |  | ||||||
|   saml_request: Option<&'a str>, |   saml_request: Option<&'a str>, | ||||||
|  |   user_agent: Option<&'a str>, | ||||||
|  |   os: Option<&'a str>, | ||||||
|  |   os_version: Option<&'a str>, | ||||||
|   hidpi: bool, |   hidpi: bool, | ||||||
|   fix_openssl: bool, |   fix_openssl: bool, | ||||||
|  |   ignore_tls_errors: bool, | ||||||
|   clean: bool, |   clean: bool, | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -19,21 +22,34 @@ impl<'a> SamlAuthLauncher<'a> { | |||||||
|   pub fn new(server: &'a str) -> Self { |   pub fn new(server: &'a str) -> Self { | ||||||
|     Self { |     Self { | ||||||
|       server, |       server, | ||||||
|       user_agent: None, |  | ||||||
|       saml_request: None, |       saml_request: None, | ||||||
|  |       user_agent: None, | ||||||
|  |       os: None, | ||||||
|  |       os_version: None, | ||||||
|       hidpi: false, |       hidpi: false, | ||||||
|       fix_openssl: false, |       fix_openssl: false, | ||||||
|  |       ignore_tls_errors: false, | ||||||
|       clean: false, |       clean: false, | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   pub fn saml_request(mut self, saml_request: &'a str) -> Self { | ||||||
|  |     self.saml_request = Some(saml_request); | ||||||
|  |     self | ||||||
|  |   } | ||||||
|  |  | ||||||
|   pub fn user_agent(mut self, user_agent: &'a str) -> Self { |   pub fn user_agent(mut self, user_agent: &'a str) -> Self { | ||||||
|     self.user_agent = Some(user_agent); |     self.user_agent = Some(user_agent); | ||||||
|     self |     self | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   pub fn saml_request(mut self, saml_request: &'a str) -> Self { |   pub fn os(mut self, os: &'a str) -> Self { | ||||||
|     self.saml_request = Some(saml_request); |     self.os = Some(os); | ||||||
|  |     self | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   pub fn os_version(mut self, os_version: Option<&'a str>) -> Self { | ||||||
|  |     self.os_version = os_version; | ||||||
|     self |     self | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -47,6 +63,11 @@ impl<'a> SamlAuthLauncher<'a> { | |||||||
|     self |     self | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   pub fn ignore_tls_errors(mut self, ignore_tls_errors: bool) -> Self { | ||||||
|  |     self.ignore_tls_errors = ignore_tls_errors; | ||||||
|  |     self | ||||||
|  |   } | ||||||
|  |  | ||||||
|   pub fn clean(mut self, clean: bool) -> Self { |   pub fn clean(mut self, clean: bool) -> Self { | ||||||
|     self.clean = clean; |     self.clean = clean; | ||||||
|     self |     self | ||||||
| @@ -57,20 +78,32 @@ impl<'a> SamlAuthLauncher<'a> { | |||||||
|     let mut auth_cmd = Command::new(GP_AUTH_BINARY); |     let mut auth_cmd = Command::new(GP_AUTH_BINARY); | ||||||
|     auth_cmd.arg(self.server); |     auth_cmd.arg(self.server); | ||||||
|  |  | ||||||
|  |     if let Some(saml_request) = self.saml_request { | ||||||
|  |       auth_cmd.arg("--saml-request").arg(saml_request); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     if let Some(user_agent) = self.user_agent { |     if let Some(user_agent) = self.user_agent { | ||||||
|       auth_cmd.arg("--user-agent").arg(user_agent); |       auth_cmd.arg("--user-agent").arg(user_agent); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     if let Some(saml_request) = self.saml_request { |     if let Some(os) = self.os { | ||||||
|       auth_cmd.arg("--saml-request").arg(saml_request); |       auth_cmd.arg("--os").arg(os); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if let Some(os_version) = self.os_version { | ||||||
|  |       auth_cmd.arg("--os-version").arg(os_version); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if self.hidpi { | ||||||
|  |       auth_cmd.arg("--hidpi"); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     if self.fix_openssl { |     if self.fix_openssl { | ||||||
|       auth_cmd.arg("--fix-openssl"); |       auth_cmd.arg("--fix-openssl"); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     if self.hidpi { |     if self.ignore_tls_errors { | ||||||
|       auth_cmd.arg("--hidpi"); |       auth_cmd.arg("--ignore-tls-errors"); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     if self.clean { |     if self.clean { | ||||||
|   | |||||||
							
								
								
									
										34
									
								
								crates/gpapi/src/process/browser_authenticator.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								crates/gpapi/src/process/browser_authenticator.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | |||||||
|  | use std::{env::temp_dir, io::Write}; | ||||||
|  |  | ||||||
|  | pub struct BrowserAuthenticator<'a> { | ||||||
|  |   auth_request: &'a str, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl BrowserAuthenticator<'_> { | ||||||
|  |   pub fn new(auth_request: &str) -> BrowserAuthenticator { | ||||||
|  |     BrowserAuthenticator { auth_request } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   pub fn authenticate(&self) -> anyhow::Result<()> { | ||||||
|  |     if self.auth_request.starts_with("http") { | ||||||
|  |       open::that_detached(self.auth_request)?; | ||||||
|  |     } else { | ||||||
|  |       let html_file = temp_dir().join("gpauth.html"); | ||||||
|  |       let mut file = std::fs::File::create(&html_file)?; | ||||||
|  |  | ||||||
|  |       file.write_all(self.auth_request.as_bytes())?; | ||||||
|  |  | ||||||
|  |       open::that_detached(html_file)?; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     Ok(()) | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl Drop for BrowserAuthenticator<'_> { | ||||||
|  |   fn drop(&mut self) { | ||||||
|  |     // Cleanup the temporary file | ||||||
|  |     let html_file = temp_dir().join("gpauth.html"); | ||||||
|  |     let _ = std::fs::remove_file(html_file); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -1,5 +1,7 @@ | |||||||
| pub(crate) mod command_traits; | pub(crate) mod command_traits; | ||||||
|  |  | ||||||
| pub mod auth_launcher; | pub mod auth_launcher; | ||||||
|  | #[cfg(feature = "browser-auth")] | ||||||
|  | pub mod browser_authenticator; | ||||||
| pub mod gui_launcher; | pub mod gui_launcher; | ||||||
| pub mod service_launcher; | pub mod service_launcher; | ||||||
|   | |||||||
| @@ -7,4 +7,6 @@ use super::vpn_state::VpnState; | |||||||
| pub enum WsEvent { | pub enum WsEvent { | ||||||
|   VpnState(VpnState), |   VpnState(VpnState), | ||||||
|   ActiveGui, |   ActiveGui, | ||||||
|  |   /// External authentication data | ||||||
|  |   AuthData(String), | ||||||
| } | } | ||||||
|   | |||||||
| @@ -27,7 +27,6 @@ pub fn raise_window(win: &Window) -> anyhow::Result<()> { | |||||||
|     } |     } | ||||||
|     let title = win.title()?; |     let title = win.title()?; | ||||||
|     tokio::spawn(async move { |     tokio::spawn(async move { | ||||||
|       info!("Raising window: {}", title); |  | ||||||
|       if let Err(err) = wmctrl_raise_window(&title).await { |       if let Err(err) = wmctrl_raise_window(&title).await { | ||||||
|         warn!("Failed to raise window: {}", err); |         warn!("Failed to raise window: {}", err); | ||||||
|       } |       } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user