Compare commits
	
		
			32 Commits
		
	
	
		
			v2.0.0-bet
			...
			5767c252b7
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 5767c252b7 | ||
|  | a2efcada02 | ||
|  | e68aa0ffa6 | ||
|  | 66bcccabe4 | ||
|  | 3736189308 | ||
|  | c408482c55 | ||
|  | 00b0b8eb84 | ||
|  | b14294f131 | ||
|  | db9249bd61 | ||
|  | 662e4d0b8a | ||
|  | 13be9179f5 | ||
|  | 0a55506077 | ||
|  | 8860efa82e | ||
|  | 9bc0994a8e | ||
|  | 1f50e4d82b | ||
|  | 995d1216ea | ||
|  | 196e91289c | ||
|  | b2bb35994f | ||
|  | 6fe6a1387a | ||
|  | aac401e7ee | ||
|  | 9655b735a1 | ||
|  | c3bd7aeb93 | ||
|  | 0b55a80317 | ||
|  | c6315bf384 | ||
|  | 87b965f80c | ||
|  | b09b21ae0f | ||
|  | 7e372cd113 | ||
|  | 1e211e8912 | ||
|  | 8bc4049a0f | ||
|  | 03f8c98cb5 | ||
|  | 5c56acc677 | ||
|  | 2d8393dcf7 | 
							
								
								
									
										30
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,30 @@ | ||||
| --- | ||||
| name: Bug report | ||||
| about: Create a report to help us improve | ||||
| title: '' | ||||
| labels: '' | ||||
| assignees: '' | ||||
|  | ||||
| --- | ||||
|  | ||||
| **Describe the bug** | ||||
| A clear and concise description of what the bug is. | ||||
|  | ||||
| **Expected behavior** | ||||
| A clear and concise description of what you expected to happen. | ||||
|  | ||||
| **Screenshots** | ||||
| If applicable, add screenshots to help explain your problem. | ||||
|  | ||||
| **Logs** | ||||
| - For the GUI version, you can find the logs at `~/.local/share/gpclient/gpclient.log` | ||||
| - For the CLI version, copy the output of the `gpclient` command. | ||||
|  | ||||
| **Environment:** | ||||
|  - OS: [e.g. Ubuntu 22.04] | ||||
|  - Desktop Environment: [e.g. GNOME or KDE] | ||||
|  - Output of `ps aux | grep 'gnome-keyring\|kwalletd5' | grep -v grep`: [Required for secure store error] | ||||
|  - Is remote SSH? [Yes/No] | ||||
|  | ||||
| **Additional context** | ||||
| Add any other context about the problem here. | ||||
							
								
								
									
										148
									
								
								.github/workflows/build.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -8,8 +8,9 @@ on: | ||||
|       - .devcontainer | ||||
|     branches: | ||||
|       - main | ||||
|     # tags: | ||||
|     #   - v*.*.* | ||||
|     tags: | ||||
|       - latest | ||||
|       - v*.*.* | ||||
| jobs: | ||||
|   # Include arm64 if ref is a tag | ||||
|   setup-matrix: | ||||
| @@ -30,7 +31,7 @@ jobs: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Checkout gpgui repo | ||||
|         uses: actions/checkout@v4 | ||||
|         uses: actions/checkout@v3 | ||||
|         with: | ||||
|           token: ${{ secrets.GH_PAT }} | ||||
|           repository: yuezk/gpgui | ||||
| @@ -54,43 +55,35 @@ jobs: | ||||
|           pnpm run build | ||||
|  | ||||
|       - name: Upload artifacts | ||||
|         uses: actions/upload-artifact@v4 | ||||
|         uses: actions/upload-artifact@v3 | ||||
|         with: | ||||
|           name: gpgui-fe | ||||
|           path: app/dist | ||||
|  | ||||
|   build-tauri: | ||||
|     needs: [setup-matrix, build-fe] | ||||
|   build-tauri-amd64: | ||||
|     needs: [build-fe] | ||||
|     runs-on: ubuntu-latest | ||||
|     strategy: | ||||
|       matrix: | ||||
|         arch: ${{ fromJson(needs.setup-matrix.outputs.matrix) }} | ||||
|     steps: | ||||
|       - name: Checkout gpgui repo | ||||
|         uses: actions/checkout@v4 | ||||
|         uses: actions/checkout@v3 | ||||
|         with: | ||||
|           token: ${{ secrets.GH_PAT }} | ||||
|           repository: yuezk/gpgui | ||||
|           path: gpgui | ||||
|  | ||||
|       - name: Checkout gp repo | ||||
|         uses: actions/checkout@v4 | ||||
|         uses: actions/checkout@v3 | ||||
|         with: | ||||
|           token: ${{ secrets.GH_PAT }} | ||||
|           repository: yuezk/GlobalProtect-openconnect | ||||
|           path: gp | ||||
|  | ||||
|       - name: Download gpgui-fe artifact | ||||
|         uses: actions/download-artifact@v4 | ||||
|         uses: actions/download-artifact@v3 | ||||
|         with: | ||||
|           name: gpgui-fe | ||||
|           path: gpgui/app/dist | ||||
|  | ||||
|       - 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: | ||||
| @@ -104,35 +97,103 @@ jobs: | ||||
|             -v $(pwd):/${{ github.workspace }} \ | ||||
|             -w ${{ github.workspace }} \ | ||||
|             -e CI=true \ | ||||
|             --platform linux/${{ matrix.arch }} \ | ||||
|             yuezk/gpdev:main \ | ||||
|             "./gpgui/scripts/build.sh" | ||||
|  | ||||
|       - name: Upload artifacts | ||||
|         uses: actions/upload-artifact@v4 | ||||
|         uses: actions/upload-artifact@v3 | ||||
|         with: | ||||
|           name: artifact-${{ matrix.arch }}-tauri | ||||
|           name: artifact-amd64-tauri | ||||
|           path: | | ||||
|             gpgui/.tmp/artifact | ||||
|  | ||||
|   build-tauri-arm64: | ||||
|     if: startsWith(github.ref, 'refs/tags/') | ||||
|     needs: [build-fe] | ||||
|     runs-on: self-hosted | ||||
|     steps: | ||||
|       - name: Checkout gpgui repo | ||||
|         uses: actions/checkout@v3 | ||||
|         with: | ||||
|           token: ${{ secrets.GH_PAT }} | ||||
|           repository: yuezk/gpgui | ||||
|           path: gpgui | ||||
|  | ||||
|       - name: Checkout gp repo | ||||
|         uses: actions/checkout@v3 | ||||
|         with: | ||||
|           token: ${{ secrets.GH_PAT }} | ||||
|           repository: yuezk/GlobalProtect-openconnect | ||||
|           path: gp | ||||
|  | ||||
|       - name: Download gpgui-fe artifact | ||||
|         uses: actions/download-artifact@v3 | ||||
|         with: | ||||
|           name: gpgui-fe | ||||
|           path: gpgui/app/dist | ||||
|       - name: Build Tauri | ||||
|         run: | | ||||
|           ./gpgui/scripts/build.sh | ||||
|  | ||||
|       - name: Upload artifacts | ||||
|         uses: actions/upload-artifact@v3 | ||||
|         with: | ||||
|           name: artifact-arm64-tauri | ||||
|           path: | | ||||
|             gpgui/.tmp/artifact | ||||
|  | ||||
|   package-tarball: | ||||
|     needs: [build-tauri-amd64, build-tauri-arm64] | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Checkout gpgui repo | ||||
|         uses: actions/checkout@v3 | ||||
|         with: | ||||
|           token: ${{ secrets.GH_PAT }} | ||||
|           repository: yuezk/gpgui | ||||
|           path: gpgui | ||||
|  | ||||
|       - name: Download artifact-amd64-tauri | ||||
|         uses: actions/download-artifact@v3 | ||||
|         with: | ||||
|           name: artifact-amd64-tauri | ||||
|           path: gpgui/.tmp/artifact | ||||
|  | ||||
|       - name: Download artifact-arm64-tauri | ||||
|         uses: actions/download-artifact@v3 | ||||
|         with: | ||||
|           name: artifact-arm64-tauri | ||||
|           path: gpgui/.tmp/artifact | ||||
|  | ||||
|       - name: Create tarball | ||||
|         run: | | ||||
|           ./gpgui/scripts/build-tarball.sh | ||||
|  | ||||
|       - name: Upload tarball | ||||
|         uses: actions/upload-artifact@v3 | ||||
|         with: | ||||
|           name: artifact-tarball | ||||
|           path: | | ||||
|             gpgui/.tmp/tarball/*.tar.gz | ||||
|  | ||||
|   package-rpm: | ||||
|     needs: [setup-matrix, build-tauri] | ||||
|     needs: [setup-matrix, package-tarball] | ||||
|     runs-on: ubuntu-latest | ||||
|     strategy: | ||||
|       matrix: | ||||
|         arch: ${{ fromJson(needs.setup-matrix.outputs.matrix) }} | ||||
|     steps: | ||||
|       - name: Checkout gpgui repo | ||||
|         uses: actions/checkout@v4 | ||||
|         uses: actions/checkout@v3 | ||||
|         with: | ||||
|           token: ${{ secrets.GH_PAT }} | ||||
|           repository: yuezk/gpgui | ||||
|           path: gpgui | ||||
|  | ||||
|       - name: Download artifact-${{ matrix.arch }} | ||||
|         uses: actions/download-artifact@v4 | ||||
|       - name: Download package tarball | ||||
|         uses: actions/download-artifact@v3 | ||||
|         with: | ||||
|           name: artifact-${{ matrix.arch }}-tauri | ||||
|           name: artifact-tarball | ||||
|           path: gpgui/.tmp/artifact | ||||
|  | ||||
|       - name: Set up QEMU | ||||
| @@ -157,28 +218,28 @@ jobs: | ||||
|             "./gpgui/scripts/build-rpm.sh" | ||||
|  | ||||
|       - name: Upload rpm artifacts | ||||
|         uses: actions/upload-artifact@v4 | ||||
|         uses: actions/upload-artifact@v3 | ||||
|         with: | ||||
|           name: artifact-${{ matrix.arch }}-rpm | ||||
|           path: | | ||||
|             gpgui/.tmp/artifact/*.rpm | ||||
|  | ||||
|   package-pkgbuild: | ||||
|     needs: [setup-matrix, build-tauri] | ||||
|     needs: [setup-matrix, build-tauri-amd64, build-tauri-arm64] | ||||
|     runs-on: ubuntu-latest | ||||
|     strategy: | ||||
|       matrix: | ||||
|         arch: ${{ fromJson(needs.setup-matrix.outputs.matrix) }} | ||||
|     steps: | ||||
|       - name: Checkout gpgui repo | ||||
|         uses: actions/checkout@v4 | ||||
|         uses: actions/checkout@v3 | ||||
|         with: | ||||
|           token: ${{ secrets.GH_PAT }} | ||||
|           repository: yuezk/gpgui | ||||
|           path: gpgui | ||||
|  | ||||
|       - name: Download artifact-${{ matrix.arch }} | ||||
|         uses: actions/download-artifact@v4 | ||||
|         uses: actions/download-artifact@v3 | ||||
|         with: | ||||
|           name: artifact-${{ matrix.arch }}-tauri | ||||
|           path: gpgui/.tmp/artifact | ||||
| @@ -196,13 +257,11 @@ jobs: | ||||
|  | ||||
|       - name: Generate PKGBUILD | ||||
|         run: | | ||||
|           export CI_ARCH=${{ matrix.arch }} | ||||
|           ./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 \ | ||||
| @@ -211,7 +270,7 @@ jobs: | ||||
|             yuezk/gpdev:pkgbuild | ||||
|  | ||||
|       - name: Upload pkgbuild artifacts | ||||
|         uses: actions/upload-artifact@v4 | ||||
|         uses: actions/upload-artifact@v3 | ||||
|         with: | ||||
|           name: artifact-${{ matrix.arch }}-pkgbuild | ||||
|           path: | | ||||
| @@ -226,25 +285,24 @@ jobs: | ||||
|  | ||||
|     steps: | ||||
|       - name: Download artifact | ||||
|         uses: actions/download-artifact@v4 | ||||
|         uses: actions/download-artifact@v3 | ||||
|         with: | ||||
|           path: artifact | ||||
|           pattern: artifact-* | ||||
|           merge-multiple: true | ||||
|           # pattern: artifact-* | ||||
|           # merge-multiple: true | ||||
|  | ||||
|       - name: Generate checksum | ||||
|         uses: jmgilman/actions-generate-checksum@v1 | ||||
|         with: | ||||
|           output: checksums.txt | ||||
|           patterns: | | ||||
|             artifact/* | ||||
|       # - 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') | ||||
|           prerelease: ${{ contains(github.ref, 'latest') }} | ||||
|           fail_on_unmatched_files: true | ||||
|           files: | | ||||
|             checksums.txt | ||||
|             artifact/* | ||||
|             artifact/artifact-*/* | ||||
|   | ||||
							
								
								
									
										11
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -4,14 +4,18 @@ | ||||
|         "bincode", | ||||
|         "chacha", | ||||
|         "clientos", | ||||
|         "cstring", | ||||
|         "datetime", | ||||
|         "disconnectable", | ||||
|         "distro", | ||||
|         "dotenv", | ||||
|         "dotenvy", | ||||
|         "getconfig", | ||||
|         "globalprotect", | ||||
|         "globalprotectcallback", | ||||
|         "gpapi", | ||||
|         "gpauth", | ||||
|         "gpcallback", | ||||
|         "gpclient", | ||||
|         "gpcommon", | ||||
|         "gpgui", | ||||
| @@ -42,10 +46,13 @@ | ||||
|         "urlencoding", | ||||
|         "userauthcookie", | ||||
|         "utsbuf", | ||||
|         "uzers", | ||||
|         "Vite", | ||||
|         "vpnc", | ||||
|         "vpninfo", | ||||
|         "wmctrl", | ||||
|         "XAUTHORITY" | ||||
|     ] | ||||
|         "XAUTHORITY", | ||||
|         "yuezk" | ||||
|     ], | ||||
|     "rust-analyzer.cargo.features": "all", | ||||
| } | ||||
|   | ||||
							
								
								
									
										92
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						| @@ -1423,19 +1423,23 @@ dependencies = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "gpapi" | ||||
| version = "2.0.0-beta.1" | ||||
| version = "2.0.0" | ||||
| dependencies = [ | ||||
|  "anyhow", | ||||
|  "base64 0.21.5", | ||||
|  "chacha20poly1305", | ||||
|  "clap", | ||||
|  "dotenvy_macro", | ||||
|  "log", | ||||
|  "md5", | ||||
|  "open", | ||||
|  "redact-engine", | ||||
|  "regex", | ||||
|  "reqwest", | ||||
|  "roxmltree", | ||||
|  "serde", | ||||
|  "serde_json", | ||||
|  "serde_urlencoded", | ||||
|  "specta", | ||||
|  "specta-macros", | ||||
|  "tauri", | ||||
| @@ -1444,13 +1448,13 @@ dependencies = [ | ||||
|  "tokio", | ||||
|  "url", | ||||
|  "urlencoding", | ||||
|  "users", | ||||
|  "uzers", | ||||
|  "whoami", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "gpauth" | ||||
| version = "2.0.0-beta.1" | ||||
| version = "2.0.0" | ||||
| dependencies = [ | ||||
|  "anyhow", | ||||
|  "clap", | ||||
| @@ -1470,7 +1474,7 @@ dependencies = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "gpclient" | ||||
| version = "2.0.0-beta.1" | ||||
| version = "2.0.0" | ||||
| dependencies = [ | ||||
|  "anyhow", | ||||
|  "clap", | ||||
| @@ -1491,7 +1495,7 @@ dependencies = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "gpservice" | ||||
| version = "2.0.0-beta.1" | ||||
| version = "2.0.0" | ||||
| dependencies = [ | ||||
|  "anyhow", | ||||
|  "axum", | ||||
| @@ -1564,9 +1568,9 @@ dependencies = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "h2" | ||||
| version = "0.3.22" | ||||
| version = "0.3.24" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "4d6250322ef6e60f93f9a2162799302cd6f68f79f6e5d85c8c16f14d1d958178" | ||||
| checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9" | ||||
| dependencies = [ | ||||
|  "bytes", | ||||
|  "fnv", | ||||
| @@ -1583,9 +1587,9 @@ dependencies = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "h2" | ||||
| version = "0.4.0" | ||||
| version = "0.4.2" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "e1d308f63daf4181410c242d34c11f928dcb3aa105852019e043c9d1f4e4368a" | ||||
| checksum = "31d030e59af851932b72ceebadf4a2b5986dba4c3b99dd2493f8273a0f151943" | ||||
| dependencies = [ | ||||
|  "bytes", | ||||
|  "fnv", | ||||
| @@ -1743,7 +1747,7 @@ dependencies = [ | ||||
|  "futures-channel", | ||||
|  "futures-core", | ||||
|  "futures-util", | ||||
|  "h2 0.3.22", | ||||
|  "h2 0.3.24", | ||||
|  "http 0.2.11", | ||||
|  "http-body 0.4.6", | ||||
|  "httparse", | ||||
| @@ -1766,7 +1770,7 @@ dependencies = [ | ||||
|  "bytes", | ||||
|  "futures-channel", | ||||
|  "futures-util", | ||||
|  "h2 0.4.0", | ||||
|  "h2 0.4.2", | ||||
|  "http 1.0.0", | ||||
|  "http-body 1.0.0", | ||||
|  "httparse", | ||||
| @@ -1962,6 +1966,15 @@ version = "2.9.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| 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]] | ||||
| name = "is-terminal" | ||||
| version = "0.4.10" | ||||
| @@ -1973,6 +1986,16 @@ dependencies = [ | ||||
|  "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]] | ||||
| name = "is_executable" | ||||
| version = "1.0.1" | ||||
| @@ -2205,6 +2228,12 @@ version = "0.7.3" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" | ||||
|  | ||||
| [[package]] | ||||
| name = "md5" | ||||
| version = "0.7.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" | ||||
|  | ||||
| [[package]] | ||||
| name = "memchr" | ||||
| version = "2.7.1" | ||||
| @@ -2444,9 +2473,20 @@ version = "0.3.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| 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]] | ||||
| name = "openconnect" | ||||
| version = "2.0.0-beta.1" | ||||
| version = "2.0.0" | ||||
| dependencies = [ | ||||
|  "cc", | ||||
|  "is_executable", | ||||
| @@ -2573,6 +2613,12 @@ version = "1.0.14" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" | ||||
|  | ||||
| [[package]] | ||||
| name = "pathdiff" | ||||
| version = "0.2.1" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" | ||||
|  | ||||
| [[package]] | ||||
| name = "percent-encoding" | ||||
| version = "2.3.1" | ||||
| @@ -3070,7 +3116,7 @@ dependencies = [ | ||||
|  "encoding_rs", | ||||
|  "futures-core", | ||||
|  "futures-util", | ||||
|  "h2 0.3.22", | ||||
|  "h2 0.3.24", | ||||
|  "http 0.2.11", | ||||
|  "http-body 0.4.6", | ||||
|  "hyper 0.14.28", | ||||
| @@ -4378,16 +4424,6 @@ version = "2.1.3" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" | ||||
|  | ||||
| [[package]] | ||||
| name = "users" | ||||
| version = "0.11.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "24cc0f6d6f267b73e5a2cadf007ba8f9bc39c6a6f9666f8cf25ea809a153b032" | ||||
| dependencies = [ | ||||
|  "libc", | ||||
|  "log", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "utf-8" | ||||
| version = "0.7.6" | ||||
| @@ -4409,6 +4445,16 @@ dependencies = [ | ||||
|  "getrandom 0.2.11", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "uzers" | ||||
| version = "0.11.3" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "76d283dc7e8c901e79e32d077866eaf599156cbf427fffa8289aecc52c5c3f63" | ||||
| dependencies = [ | ||||
|  "libc", | ||||
|  "log", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "valuable" | ||||
| version = "0.1.0" | ||||
|   | ||||
| @@ -4,7 +4,7 @@ resolver = "2" | ||||
| members = ["crates/*", "apps/gpclient", "apps/gpservice", "apps/gpauth"] | ||||
|  | ||||
| [workspace.package] | ||||
| version = "2.0.0-beta.1" | ||||
| version = "2.0.0" | ||||
| authors = ["Kevin Yue <k3vinyue@gmail.com>"] | ||||
| homepage = "https://github.com/yuezk/GlobalProtect-openconnect" | ||||
| edition = "2021" | ||||
| @@ -36,13 +36,15 @@ futures-util = "0.3" | ||||
| tokio-tungstenite = "0.20.1" | ||||
| specta = "=2.0.0-rc.1" | ||||
| specta-macros = "=2.0.0-rc.1" | ||||
| users = "0.11" | ||||
| uzers = "0.11" | ||||
| whoami = "1" | ||||
| tauri = { version = "1.5" } | ||||
| thiserror = "1" | ||||
| redact-engine = "0.1" | ||||
| dotenvy_macro = "0.15" | ||||
| compile-time = "0.2" | ||||
| serde_urlencoded = "0.7" | ||||
| md5="0.7" | ||||
|  | ||||
| [profile.release] | ||||
| opt-level = 'z'   # Optimize for size | ||||
|   | ||||
							
								
								
									
										249
									
								
								README.md
									
									
									
									
									
								
							
							
						
						| @@ -1,194 +1,151 @@ | ||||
| # 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). | ||||
|  | ||||
| A GUI for GlobalProtect VPN, based on OpenConnect, supports the SSO authentication method. Inspired by [gp-saml-gui](https://github.com/dlenski/gp-saml-gui). | ||||
|  | ||||
| <p align="center"> | ||||
|   <img src="https://user-images.githubusercontent.com/3297602/133869036-5c02b0d9-c2d9-4f87-8c81-e44f68cfd6ac.png"> | ||||
|   <img width="300" src="https://github.com/yuezk/GlobalProtect-openconnect/assets/3297602/9242df9c-217d-42ab-8c21-8f9f69cd4eb5"> | ||||
| </p> | ||||
|  | ||||
| <a href="https://paypal.me/zongkun" target="_blank"><img src="https://cdn.jsdelivr.net/gh/everdrone/coolbadge@5ea5937cabca5ecbfc45d6b30592bd81f219bc8d/badges/Paypal/Coffee/Blue/Small.png" alt="Buy me a coffee via Paypal" style="height: 32px; width: 268px;" ></a> | ||||
| <a href="https://ko-fi.com/M4M75PYKZ" target="_blank"><img src="https://ko-fi.com/img/githubbutton_sm.svg" alt="Support me on Ko-fi" style="height: 32px; width: 238px;"></a> | ||||
| <a href="https://www.buymeacoffee.com/yuezk" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="height: 32px; width: 114px;" ></a> | ||||
|  | ||||
|  | ||||
| ## Features | ||||
|  | ||||
| - Similar user experience as the official client in macOS. | ||||
| - Supports both SAML and non-SAML authentication modes. | ||||
| - Supports automatically selecting the preferred gateway from the multiple gateways. | ||||
| - Supports switching gateway from the system tray menu manually. | ||||
| - [x] Better Linux support | ||||
| - [x] Support both CLI and GUI | ||||
| - [x] Support both SSO and non-SSO authentication | ||||
| - [x] Support the FIDO2 authentication (e.g., YubiKey) | ||||
| - [x] Support authentication using default browser | ||||
| - [x] Support multiple portals | ||||
| - [x] Support gateway selection | ||||
| - [x] Support connect gateway directly | ||||
| - [x] Support auto-connect on startup | ||||
| - [x] Support system tray icon | ||||
|  | ||||
| ## Usage | ||||
|  | ||||
| ## Install | ||||
| ### CLI | ||||
|  | ||||
| |OS|Stable version | Development version| | ||||
| |---|--------------|--------------------| | ||||
| |Linux Mint, Ubuntu 18.04 or later|[ppa:yuezk/globalprotect-openconnect](https://launchpad.net/~yuezk/+archive/ubuntu/globalprotect-openconnect)|[ppa:yuezk/globalprotect-openconnect-snapshot](https://launchpad.net/~yuezk/+archive/ubuntu/globalprotect-openconnect-snapshot)| | ||||
| |Arch, Manjaro|[globalprotect-openconnect](https://archlinux.org/packages/extra/x86_64/globalprotect-openconnect/)|[AUR: globalprotect-openconnect-git](https://aur.archlinux.org/packages/globalprotect-openconnect-git/)| | ||||
| |Fedora|[copr: yuezk/globalprotect-openconnect](https://copr.fedorainfracloud.org/coprs/yuezk/globalprotect-openconnect/)|[copr: yuezk/globalprotect-openconnect](https://copr.fedorainfracloud.org/coprs/yuezk/globalprotect-openconnect/)| | ||||
| |openSUSE, CentOS 8|[OBS: globalprotect-openconnect](https://build.opensuse.org/package/show/home:yuezk/globalprotect-openconnect)|[OBS: globalprotect-openconnect-snapshot](https://build.opensuse.org/package/show/home:yuezk/globalprotect-openconnect-snapshot)| | ||||
| The CLI version is always free and open source in this repo. It has almost the same features as the GUI version. | ||||
|  | ||||
| Add the repository in the above table and install it with your favorite package manager tool. | ||||
| ``` | ||||
| Usage: gpclient [OPTIONS] <COMMAND> | ||||
|  | ||||
| [](https://repology.org/project/globalprotect-openconnect/versions) | ||||
| [](https://repology.org/project/globalprotect-openconnect/versions) | ||||
| [](https://repology.org/project/globalprotect-openconnect/versions) | ||||
| [](https://repology.org/project/globalprotect-openconnect/versions) | ||||
| [](https://repology.org/project/globalprotect-openconnect/versions) | ||||
| [](https://repology.org/project/globalprotect-openconnect/versions) | ||||
| [](https://repology.org/project/globalprotect-openconnect/versions) | ||||
| Commands: | ||||
|   connect     Connect to a portal server | ||||
|   disconnect  Disconnect from the server | ||||
|   launch-gui  Launch the GUI | ||||
|   help        Print this message or the help of the given subcommand(s) | ||||
|  | ||||
| ### Linux Mint, Ubuntu 18.04 or later | ||||
| Options: | ||||
|       --fix-openssl        Get around the OpenSSL `unsafe legacy renegotiation` error | ||||
|       --ignore-tls-errors  Ignore the TLS errors | ||||
|   -h, --help               Print help | ||||
|   -V, --version            Print version | ||||
|  | ||||
| ```sh | ||||
| See 'gpclient help <command>' for more information on a specific command. | ||||
| ``` | ||||
|  | ||||
| ### GUI | ||||
|  | ||||
| The GUI version is also available after you installed it. You can launch it from the application menu or run `gpclient launch-gui` in the terminal. | ||||
|  | ||||
| > [!Note] | ||||
| > | ||||
| > The GUI version is partially open source. Its background service is open sourced in this repo as [gpservice](./apps/gpservice/). The GUI part is a wrapper of the background service, which is not open sourced. | ||||
|  | ||||
| ## 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, pkexec, and gnome-keyring`, please make sure you have them installed. | ||||
| > Installing the client from PPA will automatically install the required version of `openconnect`. | ||||
|  | ||||
| ### Debian/Ubuntu based distributions | ||||
|  | ||||
| #### Install from PPA | ||||
|  | ||||
| ``` | ||||
| sudo apt-get install gir1.2-gtk-3.0 gir1.2-webkit2-4.0 | ||||
| sudo add-apt-repository ppa:yuezk/globalprotect-openconnect | ||||
| sudo apt-get update | ||||
| 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 | ||||
|  | ||||
| Download the latest deb package from [releases](https://github.com/yuezk/GlobalProtect-openconnect/releases) page. Then install it with `dpkg`: | ||||
|  | ||||
| ```bash | ||||
| sudo dpkg -i globalprotect-openconnect_*.deb | ||||
| ``` | ||||
|  | ||||
| ### Arch Linux / Manjaro | ||||
|  | ||||
| ```sh | ||||
| sudo pacman -S globalprotect-openconnect | ||||
| #### Install from AUR | ||||
|  | ||||
| Install from AUR: [globalprotect-openconnect-git](https://aur.archlinux.org/packages/globalprotect-openconnect-git/) | ||||
|  | ||||
| ``` | ||||
|  | ||||
| ### AUR snapshot version | ||||
|  | ||||
| ```sh | ||||
| yay -S globalprotect-openconnect-git | ||||
| ``` | ||||
|  | ||||
| ### Fedora | ||||
| #### Install from package | ||||
|  | ||||
| ```sh | ||||
| Download the latest package from [releases](https://github.com/yuezk/GlobalProtect-openconnect/releases) page. Then install it with `pacman`: | ||||
|  | ||||
| ```bash | ||||
| sudo pacman -U globalprotect-openconnect-*.pkg.tar.zst | ||||
| ``` | ||||
|  | ||||
| ### Fedora/OpenSUSE/CentOS/RHEL | ||||
|  | ||||
| #### Install from COPR | ||||
|  | ||||
| The package is available on [COPR](https://copr.fedorainfracloud.org/coprs/yuezk/globalprotect-openconnect/) for various RPM-based distributions. You can install it with the following commands: | ||||
|  | ||||
| ``` | ||||
| sudo dnf copr enable yuezk/globalprotect-openconnect | ||||
| sudo dnf install globalprotect-openconnect | ||||
| ``` | ||||
|  | ||||
| ### openSUSE | ||||
| #### Install from OBS (OpenSUSE Build Service) | ||||
|  | ||||
| - openSUSE Tumbleweed | ||||
|   ```sh | ||||
|   sudo zypper ar https://download.opensuse.org/repositories/home:/yuezk/openSUSE_Tumbleweed/home:yuezk.repo | ||||
|   sudo zypper ref | ||||
|   sudo zypper install globalprotect-openconnect | ||||
|   ``` | ||||
| The package is also available on [OBS](https://build.opensuse.org/package/show/home:yuezk/globalprotect-openconnect) for various RPM-based distributions. You can follow the instructions [on this page](https://software.opensuse.org//download.html?project=home%3Ayuezk&package=globalprotect-openconnect) to install it. | ||||
|  | ||||
| - openSUSE Leap | ||||
| #### Install from RPM package | ||||
|  | ||||
|   ```sh | ||||
|   sudo zypper ar https://download.opensuse.org/repositories/home:/yuezk/15.4/home:yuezk.repo | ||||
| Download the latest RPM package from [releases](https://github.com/yuezk/GlobalProtect-openconnect/releases) page. | ||||
|  | ||||
|   sudo zypper ref | ||||
|   sudo zypper install globalprotect-openconnect | ||||
|   ``` | ||||
| ### CentOS 8 | ||||
| ### Other distributions | ||||
|  | ||||
| 1. Add the repository: `https://download.opensuse.org/repositories/home:/yuezk/CentOS_8/home:yuezk.repo` | ||||
| 1. Install `globalprotect-openconnect` | ||||
| - Install `openconnect >= 8.20`, `webkit2gtk`, `libsecret`, `libayatana-appindicator` or `libappindicator-gtk3`. | ||||
| - Download `globalprotect-openconnect.tar.gz` from [releases](https://github.com/yuezk/GlobalProtect-openconnect/releases) page. | ||||
| - Extract the tarball and run `make build` to build the client. | ||||
| - Run `make install` to install the client. | ||||
|  | ||||
| ## FAQ | ||||
|  | ||||
| ## Build & Install from source code | ||||
| 1. How to deal with error `Secure Storage not ready` | ||||
|    | ||||
| Clone this repo with: | ||||
|    You need to install the `gnome-keyring` package, and restart the system (See [#321](https://github.com/yuezk/GlobalProtect-openconnect/issues/321), [#316](https://github.com/yuezk/GlobalProtect-openconnect/issues/316)). | ||||
|  | ||||
| ```sh | ||||
| git clone https://github.com/yuezk/GlobalProtect-openconnect.git | ||||
| cd GlobalProtect-openconnect | ||||
| ``` | ||||
| 2. How to deal with error `(gpauth:18869): Gtk-WARNING **: 10:33:37.566: cannot open display:` | ||||
|    | ||||
| ### MX Linux | ||||
| The following instructions are for **MX-21.2.1_x64 KDE**. | ||||
|    If you encounter this error when using the CLI version, try to run the command with `sudo -E` (See [#316](https://github.com/yuezk/GlobalProtect-openconnect/issues/316)). | ||||
|  | ||||
| ```sh | ||||
| sudo apt install qttools5-dev libsecret-1-dev libqt5keychain1 | ||||
| ./scripts/install-debian.sh | ||||
| ``` | ||||
| ## About Trial | ||||
|  | ||||
| ### Ubuntu/Mint | ||||
| The CLI version is always free, while the GUI version is paid. There are two trial modes for the GUI version: | ||||
|  | ||||
| > **⚠️ REQUIRED for Ubuntu 18.04 ⚠️** | ||||
| > | ||||
| > Add this [dwmw2/openconnect](https://launchpad.net/~dwmw2/+archive/ubuntu/openconnect) PPA first to install the latest openconnect. | ||||
| > | ||||
| > ```sh | ||||
| > sudo add-apt-repository ppa:dwmw2/openconnect | ||||
| > sudo apt-get update | ||||
| > ``` | ||||
|  | ||||
| Build and install with: | ||||
|  | ||||
| ```sh | ||||
| ./scripts/install-ubuntu.sh | ||||
| ``` | ||||
| ### openSUSE | ||||
|  | ||||
| Build and install with: | ||||
|  | ||||
| ```sh | ||||
| ./scripts/install-opensuse.sh | ||||
| ``` | ||||
|  | ||||
| ### Fedora | ||||
|  | ||||
| Build and install with: | ||||
|  | ||||
| ```sh | ||||
| ./scripts/install-fedora.sh | ||||
| ``` | ||||
|  | ||||
| ### Other Linux | ||||
|  | ||||
| Install the Qt5 dependencies and OpenConnect: | ||||
|  | ||||
| - QtCore | ||||
| - QtWebEngine | ||||
| - QtWebSockets | ||||
| - QtDBus | ||||
| - openconnect v8.x | ||||
| - qtkeychain | ||||
|  | ||||
| ...then build and install with: | ||||
|  | ||||
| ```sh | ||||
| ./scripts/install.sh | ||||
| ``` | ||||
|  | ||||
|  | ||||
| ### NixOS | ||||
|   In `configuration.nix`: | ||||
|  | ||||
|   ``` | ||||
|   services.globalprotect = { | ||||
|     enable = true; | ||||
|     # if you need a Host Integrity Protection report | ||||
|     csdWrapper = "${pkgs.openconnect}/libexec/openconnect/hipreport.sh"; | ||||
|   }; | ||||
|  | ||||
|   environment.systemPackages = [ globalprotect-openconnect ]; | ||||
|   ``` | ||||
|  | ||||
| ## Run | ||||
|  | ||||
| Once the software is installed, you can run `gpclient` to start the UI. | ||||
|  | ||||
| ## Passing the Custom Parameters to `OpenConnect` CLI | ||||
|  | ||||
| See [Configuration](https://github.com/yuezk/GlobalProtect-openconnect/wiki/Configuration) | ||||
|  | ||||
| ## Display the system tray icon on Gnome 40 | ||||
|  | ||||
| Install the [AppIndicator and KStatusNotifierItem Support](https://extensions.gnome.org/extension/615/appindicator-support/) extension and you will see the system try icon (Restart the system after the installation). | ||||
|  | ||||
| <p align="center"> | ||||
|   <img src="https://user-images.githubusercontent.com/3297602/130831022-b93492fd-46dd-4a8e-94a4-13b5747120b7.png" /> | ||||
| <p> | ||||
|  | ||||
|  | ||||
| ## Troubleshooting | ||||
|  | ||||
| Run `gpclient` in the Terminal and collect the logs. | ||||
| 1. 10-day trial: You can use the GUI stable release for 10 days after the installation. | ||||
| 2. 14-day trial: Each beta release has a fresh trial period (at most 14 days) after released. | ||||
|  | ||||
| ## [License](./LICENSE) | ||||
|  | ||||
| GPLv3 | ||||
|   | ||||
| @@ -8,7 +8,7 @@ license.workspace = true | ||||
| tauri-build = { version = "1.5", features = [] } | ||||
|  | ||||
| [dependencies] | ||||
| gpapi = { path = "../../crates/gpapi", features = ["tauri"] } | ||||
| gpapi = { path = "../../crates/gpapi", features = ["tauri", "clap"] } | ||||
| anyhow.workspace = true | ||||
| clap.workspace = true | ||||
| env_logger.workspace = true | ||||
|   | ||||
| Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 13 KiB | 
| Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 28 KiB | 
| Before Width: | Height: | Size: 974 B After Width: | Height: | Size: 2.5 KiB | 
| Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 44 KiB | 
| Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 83 KiB | 
| @@ -7,6 +7,7 @@ use std::{ | ||||
| use anyhow::bail; | ||||
| use gpapi::{ | ||||
|   auth::SamlAuthData, | ||||
|   gp_params::GpParams, | ||||
|   portal::{prelogin, Prelogin}, | ||||
|   utils::{redact::redact_uri, window::WindowExt}, | ||||
| }; | ||||
| @@ -18,11 +19,13 @@ use tokio_util::sync::CancellationToken; | ||||
| use webkit2gtk::{ | ||||
|   gio::Cancellable, | ||||
|   glib::{GString, TimeSpan}, | ||||
|   LoadEvent, URIResponse, URIResponseExt, WebContextExt, WebResource, WebResourceExt, WebView, | ||||
|   WebViewExt, WebsiteDataManagerExtManual, WebsiteDataTypes, | ||||
|   LoadEvent, SettingsExt, TLSErrorsPolicy, URIResponse, URIResponseExt, WebContextExt, WebResource, WebResourceExt, | ||||
|   WebView, WebViewExt, WebsiteDataManagerExtManual, WebsiteDataTypes, | ||||
| }; | ||||
|  | ||||
| enum AuthDataError { | ||||
|   /// Failed to load page due to TLS error | ||||
|   TlsError, | ||||
|   /// 1. Found auth data in headers/body but it's invalid | ||||
|   /// 2. Loaded an empty page, failed to load page. etc. | ||||
|   Invalid, | ||||
| @@ -37,6 +40,7 @@ pub(crate) struct AuthWindow<'a> { | ||||
|   server: &'a str, | ||||
|   saml_request: &'a str, | ||||
|   user_agent: &'a str, | ||||
|   gp_params: Option<GpParams>, | ||||
|   clean: bool, | ||||
| } | ||||
|  | ||||
| @@ -47,6 +51,7 @@ impl<'a> AuthWindow<'a> { | ||||
|       server: "", | ||||
|       saml_request: "", | ||||
|       user_agent: "", | ||||
|       gp_params: None, | ||||
|       clean: false, | ||||
|     } | ||||
|   } | ||||
| @@ -66,6 +71,11 @@ impl<'a> AuthWindow<'a> { | ||||
|     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 { | ||||
|     self.clean = clean; | ||||
|     self | ||||
| @@ -76,7 +86,7 @@ impl<'a> AuthWindow<'a> { | ||||
|  | ||||
|     let window = Window::builder(&self.app_handle, "auth_window", WindowUrl::default()) | ||||
|       .title("GlobalProtect Login") | ||||
|       .user_agent(self.user_agent) | ||||
|       // .user_agent(self.user_agent) | ||||
|       .focused(true) | ||||
|       .visible(false) | ||||
|       .center() | ||||
| @@ -119,6 +129,12 @@ impl<'a> AuthWindow<'a> { | ||||
|     let saml_request = self.saml_request.to_string(); | ||||
|     let (auth_result_tx, mut auth_result_rx) = mpsc::unbounded_channel::<AuthResult>(); | ||||
|     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 { | ||||
|       clear_webview_cookies(window).await?; | ||||
| @@ -128,6 +144,15 @@ impl<'a> AuthWindow<'a> { | ||||
|     window.with_webview(move |wv| { | ||||
|       let wv = wv.inner(); | ||||
|  | ||||
|       if let Some(context) = wv.context() { | ||||
|         context.set_tls_errors_policy(tls_err_policy); | ||||
|       } | ||||
|  | ||||
|       if let Some(settings) = wv.settings() { | ||||
|         let ua = settings.user_agent().unwrap_or("".into()); | ||||
|         info!("Auth window user agent: {}", ua); | ||||
|       } | ||||
|  | ||||
|       // Load the initial SAML request | ||||
|       load_saml_request(&wv, &saml_request); | ||||
|  | ||||
| @@ -163,31 +188,35 @@ 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); | ||||
|         warn!( | ||||
|           "Failed to load uri: {} with error: {}, cert: {}", | ||||
|           redacted_uri, err, cert | ||||
|         ); | ||||
|  | ||||
|         send_auth_result(&auth_result_tx_clone, Err(AuthDataError::TlsError)); | ||||
|         true | ||||
|       }); | ||||
|  | ||||
|       wv.connect_load_failed(move |_wv, _event, uri, err| { | ||||
|         let redacted_uri = redact_uri(uri); | ||||
|         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 | ||||
|       }); | ||||
|     })?; | ||||
|  | ||||
|     let portal = self.server.to_string(); | ||||
|     let user_agent = self.user_agent.to_string(); | ||||
|  | ||||
|     loop { | ||||
|       if let Some(auth_result) = auth_result_rx.recv().await { | ||||
|         match auth_result { | ||||
|           Ok(auth_data) => return Ok(auth_data), | ||||
|           Err(AuthDataError::TlsError) => bail!("TLS error: certificate verify failed"), | ||||
|           Err(AuthDataError::NotFound) => { | ||||
|             info!("No auth data found, it may not be the /SAML20/SP/ACS endpoint"); | ||||
|  | ||||
| @@ -196,10 +225,7 @@ impl<'a> AuthWindow<'a> { | ||||
|               let window = Arc::clone(window); | ||||
|               let cancel_token = CancellationToken::new(); | ||||
|  | ||||
|               raise_window_cancel_token | ||||
|                 .write() | ||||
|                 .await | ||||
|                 .replace(cancel_token.clone()); | ||||
|               raise_window_cancel_token.write().await.replace(cancel_token.clone()); | ||||
|  | ||||
|               tokio::spawn(async move { | ||||
|                 let delay_secs = 1; | ||||
| @@ -232,7 +258,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| { | ||||
|               let wv = wv.inner(); | ||||
|               load_saml_request(&wv, &saml_request); | ||||
| @@ -253,11 +279,10 @@ fn raise_window(window: &Arc<Window>) { | ||||
|   } | ||||
| } | ||||
|  | ||||
| pub(crate) async fn portal_prelogin(portal: &str, user_agent: &str) -> anyhow::Result<String> { | ||||
|   info!("Portal prelogin..."); | ||||
|   match prelogin(portal, user_agent).await? { | ||||
| pub async fn portal_prelogin(portal: &str, gp_params: &GpParams) -> anyhow::Result<String> { | ||||
|   match prelogin(portal, gp_params).await? { | ||||
|     Prelogin::Saml(prelogin) => Ok(prelogin.saml_request().to_string()), | ||||
|     Prelogin::Standard(_) => Err(anyhow::anyhow!("Received non-SAML prelogin response")), | ||||
|     Prelogin::Standard(_) => bail!("Received non-SAML prelogin response"), | ||||
|   } | ||||
| } | ||||
|  | ||||
| @@ -388,10 +413,27 @@ fn read_auth_data(main_resource: &WebResource, auth_result_tx: mpsc::UnboundedSe | ||||
|     } | ||||
|     Err(AuthDataError::NotFound) => { | ||||
|       info!("No auth data found in headers, trying to read from body..."); | ||||
|       let url = main_resource.uri().unwrap_or("".into()); | ||||
|       let is_acs_endpoint = url.contains("/SAML20/SP/ACS"); | ||||
|  | ||||
|       read_auth_data_from_body(main_resource, move |auth_result| { | ||||
|         // If the endpoint is `/SAML20/SP/ACS` and no auth data found in body, it should be considered as invalid | ||||
|         let auth_result = auth_result.map_err(|err| { | ||||
|           if matches!(err, AuthDataError::NotFound) && is_acs_endpoint { | ||||
|             AuthDataError::Invalid | ||||
|           } else { | ||||
|             err | ||||
|           } | ||||
|         }); | ||||
|  | ||||
|         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 gpapi::{ | ||||
|   auth::{SamlAuthData, SamlAuthResult}, | ||||
|   clap::args::Os, | ||||
|   gp_params::{ClientOs, GpParams}, | ||||
|   utils::{normalize_server, openssl}, | ||||
|   GP_USER_AGENT, | ||||
| }; | ||||
| @@ -11,38 +13,47 @@ use tempfile::NamedTempFile; | ||||
|  | ||||
| use crate::auth_window::{portal_prelogin, AuthWindow}; | ||||
|  | ||||
| const VERSION: &str = concat!( | ||||
|   env!("CARGO_PKG_VERSION"), | ||||
|   " (", | ||||
|   compile_time::date_str!(), | ||||
|   ")" | ||||
| ); | ||||
| const VERSION: &str = concat!(env!("CARGO_PKG_VERSION"), " (", compile_time::date_str!(), ")"); | ||||
|  | ||||
| #[derive(Parser, Clone)] | ||||
| #[command(version = VERSION)] | ||||
| struct Cli { | ||||
|   server: String, | ||||
|   #[arg(long)] | ||||
|   gateway: bool, | ||||
|   #[arg(long)] | ||||
|   saml_request: Option<String>, | ||||
|   #[arg(long, default_value = GP_USER_AGENT)] | ||||
|   user_agent: String, | ||||
|   #[arg(long, default_value = "Linux")] | ||||
|   os: Os, | ||||
|   #[arg(long)] | ||||
|   os_version: Option<String>, | ||||
|   #[arg(long)] | ||||
|   hidpi: bool, | ||||
|   #[arg(long)] | ||||
|   fix_openssl: bool, | ||||
|   #[arg(long)] | ||||
|   ignore_tls_errors: bool, | ||||
|   #[arg(long)] | ||||
|   clean: bool, | ||||
| } | ||||
|  | ||||
| impl Cli { | ||||
|   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()?; | ||||
|  | ||||
|     self.server = normalize_server(&self.server)?; | ||||
|     let gp_params = self.build_gp_params(); | ||||
|  | ||||
|     // Get the initial SAML request | ||||
|     let saml_request = match self.saml_request { | ||||
|       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); | ||||
| @@ -82,10 +93,23 @@ impl Cli { | ||||
|     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) | ||||
|       .is_gateway(self.gateway) | ||||
|       .build(); | ||||
|  | ||||
|     gp_params | ||||
|   } | ||||
|  | ||||
|   async fn saml_auth(&self, app_handle: AppHandle) -> anyhow::Result<SamlAuthData> { | ||||
|     let auth_window = AuthWindow::new(app_handle) | ||||
|       .server(&self.server) | ||||
|       .user_agent(&self.user_agent) | ||||
|       .gp_params(self.build_gp_params()) | ||||
|       .saml_request(self.saml_request.as_ref().unwrap()) | ||||
|       .clean(self.clean); | ||||
|  | ||||
|   | ||||
| @@ -6,7 +6,7 @@ edition.workspace = true | ||||
| license.workspace = true | ||||
|  | ||||
| [dependencies] | ||||
| gpapi = { path = "../../crates/gpapi" } | ||||
| gpapi = { path = "../../crates/gpapi", features = ["clap"] } | ||||
| openconnect = { path = "../../crates/openconnect" } | ||||
| anyhow.workspace = true | ||||
| clap.workspace = true | ||||
|   | ||||
| @@ -9,12 +9,12 @@ use crate::{ | ||||
|   launch_gui::{LaunchGuiArgs, LaunchGuiHandler}, | ||||
| }; | ||||
|  | ||||
| const VERSION: &str = concat!( | ||||
|   env!("CARGO_PKG_VERSION"), | ||||
|   " (", | ||||
|   compile_time::date_str!(), | ||||
|   ")" | ||||
| ); | ||||
| const VERSION: &str = concat!(env!("CARGO_PKG_VERSION"), " (", compile_time::date_str!(), ")"); | ||||
|  | ||||
| pub(crate) struct SharedArgs { | ||||
|   pub(crate) fix_openssl: bool, | ||||
|   pub(crate) ignore_tls_errors: bool, | ||||
| } | ||||
|  | ||||
| #[derive(Subcommand)] | ||||
| enum CliCommand { | ||||
| @@ -40,17 +40,18 @@ enum CliCommand { | ||||
| {usage-heading} {usage} | ||||
|  | ||||
| {all-args}{after-help} | ||||
|  | ||||
| See 'gpclient help <command>' for more information on a specific command. | ||||
| " | ||||
| )] | ||||
| struct Cli { | ||||
|   #[command(subcommand)] | ||||
|   command: CliCommand, | ||||
|  | ||||
|   #[arg( | ||||
|     long, | ||||
|     help = "Get around the OpenSSL `unsafe legacy renegotiation` error" | ||||
|   )] | ||||
|   #[arg(long, help = "Get around the OpenSSL `unsafe legacy renegotiation` error")] | ||||
|   fix_openssl: bool, | ||||
|   #[arg(long, help = "Ignore the TLS errors")] | ||||
|   ignore_tls_errors: bool, | ||||
| } | ||||
|  | ||||
| impl Cli { | ||||
| @@ -67,9 +68,17 @@ impl Cli { | ||||
|     // The temp file will be dropped automatically when the file handle is dropped | ||||
|     // So, declare it here to ensure it's not dropped | ||||
|     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 { | ||||
|       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::LaunchGui(args) => LaunchGuiHandler::new(args).handle().await, | ||||
|     } | ||||
| @@ -89,13 +98,22 @@ pub(crate) async fn run() { | ||||
|   if let Err(err) = cli.run().await { | ||||
|     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"); | ||||
|       // Print the command | ||||
|       let args = std::env::args().collect::<Vec<_>>(); | ||||
|       eprintln!("{} --fix-openssl {}\n", args[0], args[1..].join(" ")); | ||||
|     } | ||||
|  | ||||
|     if err.contains("certificate verify failed") && !cli.ignore_tls_errors { | ||||
|       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); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -2,66 +2,112 @@ use std::{fs, sync::Arc}; | ||||
|  | ||||
| use clap::Args; | ||||
| use gpapi::{ | ||||
|   clap::args::Os, | ||||
|   credential::{Credential, PasswordCredential}, | ||||
|   gateway::gateway_login, | ||||
|   gp_params::GpParams, | ||||
|   portal::{prelogin, retrieve_config, Prelogin}, | ||||
|   process::auth_launcher::SamlAuthLauncher, | ||||
|   utils::{self, shutdown_signal}, | ||||
|   gp_params::{ClientOs, GpParams}, | ||||
|   portal::{prelogin, retrieve_config, PortalError, Prelogin}, | ||||
|   process::{ | ||||
|     auth_launcher::SamlAuthLauncher, | ||||
|     users::{get_non_root_user, get_user_by_name}, | ||||
|   }, | ||||
|   utils::shutdown_signal, | ||||
|   GP_USER_AGENT, | ||||
| }; | ||||
| use inquire::{Password, PasswordDisplayMode, Select, Text}; | ||||
| use log::info; | ||||
| use openconnect::Vpn; | ||||
|  | ||||
| use crate::GP_CLIENT_LOCK_FILE; | ||||
| use crate::{cli::SharedArgs, GP_CLIENT_LOCK_FILE}; | ||||
|  | ||||
| #[derive(Args)] | ||||
| pub(crate) struct ConnectArgs { | ||||
|   #[arg(help = "The portal server to connect to")] | ||||
|   server: String, | ||||
|   #[arg( | ||||
|     short, | ||||
|     long, | ||||
|     help = "The gateway to connect to, it will prompt if not specified" | ||||
|   )] | ||||
|   #[arg(short, long, help = "The gateway to connect to, it will prompt if not specified")] | ||||
|   gateway: Option<String>, | ||||
|   #[arg( | ||||
|     short, | ||||
|     long, | ||||
|     help = "The username to use, it will prompt if not specified" | ||||
|   )] | ||||
|   #[arg(short, long, help = "The username to use, it will prompt if not specified")] | ||||
|   user: Option<String>, | ||||
|   #[arg(long, short, help = "The VPNC script to use")] | ||||
|   script: Option<String>, | ||||
|  | ||||
|   #[arg(long, help = "Same as the '--csd-user' option in the openconnect command")] | ||||
|   csd_user: Option<String>, | ||||
|  | ||||
|   #[arg(long, help = "Same as the '--csd-wrapper' option in the openconnect command")] | ||||
|   csd_wrapper: Option<String>, | ||||
|  | ||||
|   #[arg(short, long, help = "Request MTU from server (legacy servers only)")] | ||||
|   mtu: Option<u32>, | ||||
|  | ||||
|   #[arg(long, default_value = GP_USER_AGENT, help = "The user agent to use")] | ||||
|   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")] | ||||
|   hidpi: bool, | ||||
|   #[arg(long, help = "Do not reuse the remembered authentication cookie")] | ||||
|   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> { | ||||
|   args: &'a ConnectArgs, | ||||
|   fix_openssl: bool, | ||||
|   shared_args: &'a SharedArgs, | ||||
| } | ||||
|  | ||||
| impl<'a> ConnectHandler<'a> { | ||||
|   pub(crate) fn new(args: &'a ConnectArgs, fix_openssl: bool) -> Self { | ||||
|     Self { args, fix_openssl } | ||||
|   pub(crate) fn new(args: &'a ConnectArgs, shared_args: &'a SharedArgs) -> Self { | ||||
|     Self { args, shared_args } | ||||
|   } | ||||
|  | ||||
|   fn build_gp_params(&self) -> GpParams { | ||||
|     GpParams::builder() | ||||
|       .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() | ||||
|   } | ||||
|  | ||||
|   pub(crate) async fn handle(&self) -> anyhow::Result<()> { | ||||
|     let portal = utils::normalize_server(self.args.server.as_str())?; | ||||
|     let server = self.args.server.as_str(); | ||||
|  | ||||
|     let gp_params = GpParams::builder() | ||||
|       .user_agent(&self.args.user_agent) | ||||
|       .build(); | ||||
|     let Err(err) = self.connect_portal_with_prelogin(server).await else { | ||||
|       return Ok(()); | ||||
|     }; | ||||
|  | ||||
|     let prelogin = prelogin(&portal, &self.args.user_agent).await?; | ||||
|     let portal_credential = self.obtain_portal_credential(&prelogin).await?; | ||||
|     let mut portal_config = retrieve_config(&portal, &portal_credential, &gp_params).await?; | ||||
|     info!("Failed to connect portal with prelogin: {}", err); | ||||
|     if err.root_cause().downcast_ref::<PortalError>().is_some() { | ||||
|       info!("Trying the gateway authentication workflow..."); | ||||
|       return self.connect_gateway_with_prelogin(server).await; | ||||
|     } | ||||
|  | ||||
|     Err(err) | ||||
|   } | ||||
|  | ||||
|   async fn connect_portal_with_prelogin(&self, portal: &str) -> anyhow::Result<()> { | ||||
|     let gp_params = self.build_gp_params(); | ||||
|  | ||||
|     let prelogin = prelogin(portal, &gp_params).await?; | ||||
|  | ||||
|     let cred = self.obtain_credential(&prelogin, portal).await?; | ||||
|     let mut portal_config = retrieve_config(portal, &cred, &gp_params).await?; | ||||
|  | ||||
|     let selected_gateway = match &self.args.gateway { | ||||
|       Some(gateway) => portal_config | ||||
| @@ -83,11 +129,40 @@ impl<'a> ConnectHandler<'a> { | ||||
|  | ||||
|     let gateway = selected_gateway.server(); | ||||
|     let cred = portal_config.auth_cookie().into(); | ||||
|     let token = gateway_login(gateway, &cred, &gp_params).await?; | ||||
|  | ||||
|     let vpn = Vpn::builder(gateway, &token) | ||||
|     let cookie = match gateway_login(gateway, &cred, &gp_params).await { | ||||
|       Ok(cookie) => cookie, | ||||
|       Err(err) => { | ||||
|         info!("Gateway login failed: {}", err); | ||||
|         return self.connect_gateway_with_prelogin(gateway).await; | ||||
|       } | ||||
|     }; | ||||
|  | ||||
|     self.connect_gateway(gateway, &cookie).await | ||||
|   } | ||||
|  | ||||
|   async fn connect_gateway_with_prelogin(&self, gateway: &str) -> anyhow::Result<()> { | ||||
|     let mut gp_params = self.build_gp_params(); | ||||
|     gp_params.set_is_gateway(true); | ||||
|  | ||||
|     let prelogin = prelogin(gateway, &gp_params).await?; | ||||
|     let cred = self.obtain_credential(&prelogin, gateway).await?; | ||||
|  | ||||
|     let cookie = gateway_login(gateway, &cred, &gp_params).await?; | ||||
|  | ||||
|     self.connect_gateway(gateway, &cookie).await | ||||
|   } | ||||
|  | ||||
|   async fn connect_gateway(&self, gateway: &str, cookie: &str) -> anyhow::Result<()> { | ||||
|     let csd_uid = get_csd_uid(&self.args.csd_user)?; | ||||
|     let mtu = self.args.mtu.unwrap_or(0); | ||||
|  | ||||
|     let vpn = Vpn::builder(gateway, cookie) | ||||
|       .user_agent(self.args.user_agent.clone()) | ||||
|       .script(self.args.script.clone()) | ||||
|       .csd_uid(csd_uid) | ||||
|       .csd_wrapper(self.args.csd_wrapper.clone()) | ||||
|       .mtu(mtu) | ||||
|       .build(); | ||||
|  | ||||
|     let vpn = Arc::new(vpn); | ||||
| @@ -110,20 +185,27 @@ impl<'a> ConnectHandler<'a> { | ||||
|     Ok(()) | ||||
|   } | ||||
|  | ||||
|   async fn obtain_portal_credential(&self, prelogin: &Prelogin) -> anyhow::Result<Credential> { | ||||
|   async fn obtain_credential(&self, prelogin: &Prelogin, server: &str) -> anyhow::Result<Credential> { | ||||
|     let is_gateway = prelogin.is_gateway(); | ||||
|  | ||||
|     match prelogin { | ||||
|       Prelogin::Saml(prelogin) => { | ||||
|         SamlAuthLauncher::new(&self.args.server) | ||||
|           .user_agent(&self.args.user_agent) | ||||
|           .gateway(is_gateway) | ||||
|           .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) | ||||
|           .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) | ||||
|           .launch() | ||||
|           .await | ||||
|       } | ||||
|       Prelogin::Standard(prelogin) => { | ||||
|         println!("{}", prelogin.auth_message()); | ||||
|         let prefix = if is_gateway { "Gateway" } else { "Portal" }; | ||||
|         println!("{} ({}: {})", prelogin.auth_message(), prefix, server); | ||||
|  | ||||
|         let user = self.args.user.as_ref().map_or_else( | ||||
|           || Text::new(&format!("{}:", prelogin.label_username())).prompt(), | ||||
| @@ -148,3 +230,11 @@ fn write_pid_file() { | ||||
|   fs::write(GP_CLIENT_LOCK_FILE, pid.to_string()).unwrap(); | ||||
|   info!("Wrote PID {} to {}", pid, GP_CLIENT_LOCK_FILE); | ||||
| } | ||||
|  | ||||
| fn get_csd_uid(csd_user: &Option<String>) -> anyhow::Result<u32> { | ||||
|   if let Some(csd_user) = csd_user { | ||||
|     get_user_by_name(csd_user).map(|user| user.uid()) | ||||
|   } else { | ||||
|     get_non_root_user().map_or_else(|_| Ok(0), |user| Ok(user.uid())) | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -10,7 +10,12 @@ use log::info; | ||||
|  | ||||
| #[derive(Args)] | ||||
| 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, | ||||
| } | ||||
|  | ||||
| @@ -30,6 +35,12 @@ impl<'a> LaunchGuiHandler<'a> { | ||||
|       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() { | ||||
|       info!("The GUI is already running"); | ||||
|       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<()> { | ||||
|   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> | ||||
| @@ -6,9 +6,7 @@ use clap::Parser; | ||||
| use gpapi::{ | ||||
|   process::gui_launcher::GuiLauncher, | ||||
|   service::{request::WsRequest, vpn_state::VpnState}, | ||||
|   utils::{ | ||||
|     crypto::generate_key, env_file, lock_file::LockFile, redact::Redaction, shutdown_signal, | ||||
|   }, | ||||
|   utils::{crypto::generate_key, env_file, lock_file::LockFile, redact::Redaction, shutdown_signal}, | ||||
|   GP_SERVICE_LOCK_FILE, | ||||
| }; | ||||
| use log::{info, warn, LevelFilter}; | ||||
| @@ -16,12 +14,7 @@ use tokio::sync::{mpsc, watch}; | ||||
|  | ||||
| use crate::{vpn_task::VpnTask, ws_server::WsServer}; | ||||
|  | ||||
| const VERSION: &str = concat!( | ||||
|   env!("CARGO_PKG_VERSION"), | ||||
|   " (", | ||||
|   compile_time::date_str!(), | ||||
|   ")" | ||||
| ); | ||||
| const VERSION: &str = concat!(env!("CARGO_PKG_VERSION"), " (", compile_time::date_str!(), ")"); | ||||
|  | ||||
| #[derive(Parser)] | ||||
| #[command(version = VERSION)] | ||||
| @@ -51,13 +44,7 @@ impl Cli { | ||||
|     let (vpn_state_tx, vpn_state_rx) = watch::channel(VpnState::Disconnected); | ||||
|  | ||||
|     let mut vpn_task = VpnTask::new(ws_req_rx, vpn_state_tx); | ||||
|     let ws_server = WsServer::new( | ||||
|       api_key.clone(), | ||||
|       ws_req_tx, | ||||
|       vpn_state_rx, | ||||
|       lock_file.clone(), | ||||
|       redaction, | ||||
|     ); | ||||
|     let ws_server = WsServer::new(api_key.clone(), ws_req_tx, vpn_state_rx, lock_file.clone(), redaction); | ||||
|  | ||||
|     let (shutdown_tx, mut shutdown_rx) = mpsc::channel::<()>(4); | ||||
|     let shutdown_tx_clone = shutdown_tx.clone(); | ||||
| @@ -76,11 +63,7 @@ impl Cli { | ||||
|     if no_gui { | ||||
|       info!("GUI is disabled"); | ||||
|     } else { | ||||
|       let envs = self | ||||
|         .env_file | ||||
|         .as_ref() | ||||
|         .map(env_file::load_env_vars) | ||||
|         .transpose()?; | ||||
|       let envs = self.env_file.as_ref().map(env_file::load_env_vars).transpose()?; | ||||
|  | ||||
|       let minimized = self.minimized; | ||||
|  | ||||
|   | ||||
| @@ -21,10 +21,11 @@ pub(crate) async fn active_gui(State(ctx): State<Arc<WsServerContext>>) -> impl | ||||
|   ctx.send_event(WsEvent::ActiveGui).await; | ||||
| } | ||||
|  | ||||
| pub(crate) async fn ws_handler( | ||||
|   ws: WebSocketUpgrade, | ||||
|   State(ctx): State<Arc<WsServerContext>>, | ||||
| ) -> impl IntoResponse { | ||||
| 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(ws: WebSocketUpgrade, State(ctx): State<Arc<WsServerContext>>) -> impl IntoResponse { | ||||
|   ws.on_upgrade(move |socket| handle_socket(socket, ctx)) | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -2,8 +2,8 @@ mod cli; | ||||
| mod handlers; | ||||
| mod routes; | ||||
| mod vpn_task; | ||||
| mod ws_server; | ||||
| mod ws_connection; | ||||
| mod ws_server; | ||||
|  | ||||
| #[tokio::main] | ||||
| async fn main() { | ||||
|   | ||||
| @@ -1,6 +1,9 @@ | ||||
| use std::sync::Arc; | ||||
|  | ||||
| use axum::{routing::{get, post}, Router}; | ||||
| use axum::{ | ||||
|   routing::{get, post}, | ||||
|   Router, | ||||
| }; | ||||
|  | ||||
| use crate::{handlers, ws_server::WsServerContext}; | ||||
|  | ||||
| @@ -8,6 +11,7 @@ pub(crate) fn routes(ctx: Arc<WsServerContext>) -> Router { | ||||
|   Router::new() | ||||
|     .route("/health", get(handlers::health)) | ||||
|     .route("/active-gui", post(handlers::active_gui)) | ||||
|     .route("/auth-data", post(handlers::auth_data)) | ||||
|     .route("/ws", get(handlers::ws_handler)) | ||||
|     .with_state(ctx) | ||||
| } | ||||
|   | ||||
| @@ -32,11 +32,14 @@ impl VpnTaskContext { | ||||
|     } | ||||
|  | ||||
|     let info = req.info().clone(); | ||||
|     let vpn_handle = self.vpn_handle.clone(); | ||||
|     let vpn_handle = Arc::clone(&self.vpn_handle); | ||||
|     let args = req.args(); | ||||
|     let vpn = Vpn::builder(req.gateway().server(), args.cookie()) | ||||
|       .user_agent(args.user_agent()) | ||||
|       .script(args.vpnc_script()) | ||||
|       .csd_uid(args.csd_uid()) | ||||
|       .csd_wrapper(args.csd_wrapper()) | ||||
|       .mtu(args.mtu()) | ||||
|       .os(args.openconnect_os()) | ||||
|       .build(); | ||||
|  | ||||
| @@ -73,7 +76,9 @@ impl VpnTaskContext { | ||||
|  | ||||
|   pub async fn disconnect(&self) { | ||||
|     if let Some(disconnect_rx) = self.disconnect_rx.write().await.take() { | ||||
|       info!("Disconnecting VPN..."); | ||||
|       if let Some(vpn) = self.vpn_handle.read().await.as_ref() { | ||||
|         info!("VPN is connected, start disconnecting..."); | ||||
|         self.vpn_state_tx.send(VpnState::Disconnecting).ok(); | ||||
|         vpn.disconnect() | ||||
|       } | ||||
|   | ||||
| @@ -98,12 +98,7 @@ impl WsServer { | ||||
|     lock_file: Arc<LockFile>, | ||||
|     redaction: Arc<Redaction>, | ||||
|   ) -> Self { | ||||
|     let ctx = Arc::new(WsServerContext::new( | ||||
|       api_key, | ||||
|       ws_req_tx, | ||||
|       vpn_state_rx, | ||||
|       redaction, | ||||
|     )); | ||||
|     let ctx = Arc::new(WsServerContext::new(api_key, ws_req_tx, vpn_state_rx, redaction)); | ||||
|     let cancel_token = CancellationToken::new(); | ||||
|  | ||||
|     Self { | ||||
|   | ||||
| @@ -24,9 +24,15 @@ redact-engine.workspace = true | ||||
| url.workspace = true | ||||
| regex.workspace = true | ||||
| dotenvy_macro.workspace = true | ||||
| users.workspace = true | ||||
| uzers.workspace = true | ||||
| serde_urlencoded.workspace = true | ||||
| md5.workspace = true | ||||
|  | ||||
| tauri = { workspace = true, optional = true } | ||||
| clap = { workspace = true, optional = true } | ||||
| open = { version = "5", optional = true } | ||||
|  | ||||
| [features] | ||||
| tauri = ["dep:tauri"] | ||||
| clap = ["dep:clap"] | ||||
| browser-auth = ["dep:open"] | ||||
|   | ||||
| @@ -1,3 +1,5 @@ | ||||
| use anyhow::bail; | ||||
| use regex::Regex; | ||||
| use serde::{Deserialize, Serialize}; | ||||
|  | ||||
| #[derive(Debug, Serialize, Deserialize)] | ||||
| @@ -25,11 +27,7 @@ impl SamlAuthResult { | ||||
| } | ||||
|  | ||||
| impl SamlAuthData { | ||||
|   pub fn new( | ||||
|     username: String, | ||||
|     prelogin_cookie: Option<String>, | ||||
|     portal_userauthcookie: Option<String>, | ||||
|   ) -> Self { | ||||
|   pub fn new(username: String, prelogin_cookie: Option<String>, portal_userauthcookie: Option<String>) -> Self { | ||||
|     Self { | ||||
|       username, | ||||
|       prelogin_cookie, | ||||
| @@ -37,6 +35,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 { | ||||
|     &self.username | ||||
|   } | ||||
| @@ -50,14 +74,17 @@ impl SamlAuthData { | ||||
|     prelogin_cookie: &Option<String>, | ||||
|     portal_userauthcookie: &Option<String>, | ||||
|   ) -> bool { | ||||
|     let username_valid = username | ||||
|       .as_ref() | ||||
|       .is_some_and(|username| !username.is_empty()); | ||||
|     let username_valid = username.as_ref().is_some_and(|username| !username.is_empty()); | ||||
|     let prelogin_cookie_valid = prelogin_cookie.as_ref().is_some_and(|val| val.len() > 5); | ||||
|     let portal_userauthcookie_valid = portal_userauthcookie | ||||
|       .as_ref() | ||||
|       .is_some_and(|val| val.len() > 5); | ||||
|     let portal_userauthcookie_valid = portal_userauthcookie.as_ref().is_some_and(|val| val.len() > 5); | ||||
|  | ||||
|     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
									
								
							
							
						
						| @@ -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
									
								
							
							
						
						| @@ -0,0 +1 @@ | ||||
| pub mod args; | ||||
| @@ -3,7 +3,7 @@ use std::collections::HashMap; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use specta::Type; | ||||
|  | ||||
| use crate::auth::SamlAuthData; | ||||
| use crate::{auth::SamlAuthData, utils::base64::decode_to_string}; | ||||
|  | ||||
| #[derive(Debug, Serialize, Deserialize, Type, Clone)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| @@ -112,11 +112,7 @@ pub struct CachedCredential { | ||||
| } | ||||
|  | ||||
| impl CachedCredential { | ||||
|   pub fn new( | ||||
|     username: String, | ||||
|     password: Option<String>, | ||||
|     auth_cookie: AuthCookieCredential, | ||||
|   ) -> Self { | ||||
|   pub fn new(username: String, password: Option<String>, auth_cookie: AuthCookieCredential) -> Self { | ||||
|     Self { | ||||
|       username, | ||||
|       password, | ||||
| @@ -139,6 +135,24 @@ impl CachedCredential { | ||||
|   pub fn set_auth_cookie(&mut self, auth_cookie: AuthCookieCredential) { | ||||
|     self.auth_cookie = auth_cookie; | ||||
|   } | ||||
|  | ||||
|   pub fn set_username(&mut self, username: String) { | ||||
|     self.username = username; | ||||
|   } | ||||
|  | ||||
|   pub fn set_password(&mut self, password: Option<String>) { | ||||
|     self.password = password.map(|s| s.to_string()); | ||||
|   } | ||||
| } | ||||
|  | ||||
| impl From<PasswordCredential> for CachedCredential { | ||||
|   fn from(value: PasswordCredential) -> Self { | ||||
|     Self::new( | ||||
|       value.username().to_owned(), | ||||
|       Some(value.password().to_owned()), | ||||
|       AuthCookieCredential::new("", "", ""), | ||||
|     ) | ||||
|   } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Serialize, Deserialize, Type, Clone)] | ||||
| @@ -151,6 +165,17 @@ pub enum 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 { | ||||
|     match self { | ||||
|       Credential::Password(cred) => cred.username(), | ||||
| @@ -164,31 +189,30 @@ impl Credential { | ||||
|     let mut params = HashMap::new(); | ||||
|     params.insert("user", self.username()); | ||||
|  | ||||
|     match self { | ||||
|       Credential::Password(cred) => { | ||||
|         params.insert("passwd", cred.password()); | ||||
|       } | ||||
|       Credential::PreloginCookie(cred) => { | ||||
|         params.insert("prelogin-cookie", cred.prelogin_cookie()); | ||||
|       } | ||||
|       Credential::AuthCookie(cred) => { | ||||
|         params.insert("portal-userauthcookie", cred.user_auth_cookie()); | ||||
|         params.insert( | ||||
|           "portal-prelogonuserauthcookie", | ||||
|           cred.prelogon_user_auth_cookie(), | ||||
|         ); | ||||
|       } | ||||
|       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(), | ||||
|         ); | ||||
|       } | ||||
|     } | ||||
|     let (passwd, prelogin_cookie, portal_userauthcookie, portal_prelogonuserauthcookie) = match self { | ||||
|       Credential::Password(cred) => (Some(cred.password()), None, None, None), | ||||
|       Credential::PreloginCookie(cred) => (None, Some(cred.prelogin_cookie()), None, None), | ||||
|       Credential::AuthCookie(cred) => ( | ||||
|         None, | ||||
|         None, | ||||
|         Some(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( | ||||
|       "portal-prelogonuserauthcookie", | ||||
|       portal_prelogonuserauthcookie.unwrap_or_default(), | ||||
|     ); | ||||
|  | ||||
|     params | ||||
|   } | ||||
|   | ||||
							
								
								
									
										178
									
								
								crates/gpapi/src/gateway/hip.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,178 @@ | ||||
| use std::collections::HashMap; | ||||
|  | ||||
| use log::{info, warn}; | ||||
| use reqwest::Client; | ||||
| use roxmltree::Document; | ||||
|  | ||||
| use crate::{gp_params::GpParams, process::hip_launcher::HipLauncher, utils::normalize_server}; | ||||
|  | ||||
| struct HipReporter<'a> { | ||||
|   server: String, | ||||
|   cookie: &'a str, | ||||
|   md5: &'a str, | ||||
|   csd_wrapper: &'a str, | ||||
|   gp_params: &'a GpParams, | ||||
|   client: Client, | ||||
| } | ||||
|  | ||||
| impl HipReporter<'_> { | ||||
|   async fn report(&self) -> anyhow::Result<()> { | ||||
|     let client_ip = self.retrieve_client_ip().await?; | ||||
|  | ||||
|     let hip_needed = match self.check_hip(&client_ip).await { | ||||
|       Ok(hip_needed) => hip_needed, | ||||
|       Err(err) => { | ||||
|         warn!("Failed to check HIP: {}", err); | ||||
|         return Ok(()); | ||||
|       } | ||||
|     }; | ||||
|  | ||||
|     if !hip_needed { | ||||
|       info!("HIP report not needed"); | ||||
|       return Ok(()); | ||||
|     } | ||||
|  | ||||
|     info!("HIP report needed, generating report..."); | ||||
|     let report = self.generate_report(&client_ip).await?; | ||||
|  | ||||
|     if let Err(err) = self.submit_hip(&client_ip, &report).await { | ||||
|       warn!("Failed to submit HIP report: {}", err); | ||||
|     } | ||||
|  | ||||
|     Ok(()) | ||||
|   } | ||||
|  | ||||
|   async fn retrieve_client_ip(&self) -> anyhow::Result<String> { | ||||
|     let config_url = format!("{}/ssl-vpn/getconfig.esp", self.server); | ||||
|     let mut params: HashMap<&str, &str> = HashMap::new(); | ||||
|  | ||||
|     params.insert("client-type", "1"); | ||||
|     params.insert("protocol-version", "p1"); | ||||
|     params.insert("internal", "no"); | ||||
|     params.insert("ipv6-support", "yes"); | ||||
|     params.insert("clientos", self.gp_params.client_os()); | ||||
|     params.insert("hmac-algo", "sha1,md5,sha256"); | ||||
|     params.insert("enc-algo", "aes-128-cbc,aes-256-cbc"); | ||||
|  | ||||
|     if let Some(os_version) = self.gp_params.os_version() { | ||||
|       params.insert("os-version", os_version); | ||||
|     } | ||||
|     if let Some(client_version) = self.gp_params.client_version() { | ||||
|       params.insert("app-version", client_version); | ||||
|     } | ||||
|  | ||||
|     let params = merge_cookie_params(self.cookie, ¶ms)?; | ||||
|  | ||||
|     let res = self.client.post(&config_url).form(¶ms).send().await?; | ||||
|     let res_xml = res.error_for_status()?.text().await?; | ||||
|     let doc = Document::parse(&res_xml)?; | ||||
|  | ||||
|     // Get <ip-address> | ||||
|     let ip = doc | ||||
|       .descendants() | ||||
|       .find(|n| n.has_tag_name("ip-address")) | ||||
|       .and_then(|n| n.text()) | ||||
|       .ok_or_else(|| anyhow::anyhow!("ip-address not found"))?; | ||||
|  | ||||
|     Ok(ip.to_string()) | ||||
|   } | ||||
|  | ||||
|   async fn check_hip(&self, client_ip: &str) -> anyhow::Result<bool> { | ||||
|     let url = format!("{}/ssl-vpn/hipreportcheck.esp", self.server); | ||||
|     let mut params = HashMap::new(); | ||||
|  | ||||
|     params.insert("client-role", "global-protect-full"); | ||||
|     params.insert("client-ip", client_ip); | ||||
|     params.insert("md5", self.md5); | ||||
|  | ||||
|     let params = merge_cookie_params(self.cookie, ¶ms)?; | ||||
|     let res = self.client.post(&url).form(¶ms).send().await?; | ||||
|     let res_xml = res.error_for_status()?.text().await?; | ||||
|  | ||||
|     is_hip_needed(&res_xml) | ||||
|   } | ||||
|  | ||||
|   async fn generate_report(&self, client_ip: &str) -> anyhow::Result<String> { | ||||
|     let launcher = HipLauncher::new(self.csd_wrapper) | ||||
|       .cookie(self.cookie) | ||||
|       .md5(self.md5) | ||||
|       .client_ip(client_ip) | ||||
|       .client_os(self.gp_params.client_os()) | ||||
|       .client_version(self.gp_params.client_version()); | ||||
|  | ||||
|     launcher.launch().await | ||||
|   } | ||||
|  | ||||
|   async fn submit_hip(&self, client_ip: &str, report: &str) -> anyhow::Result<()> { | ||||
|     let url = format!("{}/ssl-vpn/hipreport.esp", self.server); | ||||
|  | ||||
|     let mut params = HashMap::new(); | ||||
|     params.insert("client-role", "global-protect-full"); | ||||
|     params.insert("client-ip", client_ip); | ||||
|     params.insert("report", report); | ||||
|  | ||||
|     let params = merge_cookie_params(self.cookie, ¶ms)?; | ||||
|     let res = self.client.post(&url).form(¶ms).send().await?; | ||||
|     let res_xml = res.error_for_status()?.text().await?; | ||||
|  | ||||
|     info!("HIP check response: {}", res_xml); | ||||
|  | ||||
|     Ok(()) | ||||
|   } | ||||
| } | ||||
|  | ||||
| fn is_hip_needed(res_xml: &str) -> anyhow::Result<bool> { | ||||
|   let doc = Document::parse(res_xml)?; | ||||
|  | ||||
|   let hip_needed = doc | ||||
|     .descendants() | ||||
|     .find(|n| n.has_tag_name("hip-report-needed")) | ||||
|     .and_then(|n| n.text()) | ||||
|     .ok_or_else(|| anyhow::anyhow!("hip-report-needed not found"))?; | ||||
|  | ||||
|   Ok(hip_needed == "yes") | ||||
| } | ||||
|  | ||||
| fn merge_cookie_params(cookie: &str, params: &HashMap<&str, &str>) -> anyhow::Result<HashMap<String, String>> { | ||||
|   let cookie_params = serde_urlencoded::from_str::<HashMap<String, String>>(cookie)?; | ||||
|   let params = params | ||||
|     .iter() | ||||
|     .map(|(k, v)| (k.to_string(), v.to_string())) | ||||
|     .chain(cookie_params) | ||||
|     .collect::<HashMap<String, String>>(); | ||||
|  | ||||
|   Ok(params) | ||||
| } | ||||
|  | ||||
| // Compute md5 for fields except authcookie,preferred-ip,preferred-ipv6 | ||||
| fn build_csd_token(cookie: &str) -> anyhow::Result<String> { | ||||
|   let mut cookie_params = serde_urlencoded::from_str::<Vec<(String, String)>>(cookie)?; | ||||
|   cookie_params.retain(|(k, _)| k != "authcookie" && k != "preferred-ip" && k != "preferred-ipv6"); | ||||
|  | ||||
|   let token = serde_urlencoded::to_string(cookie_params)?; | ||||
|   let md5 = format!("{:x}", md5::compute(token)); | ||||
|  | ||||
|   Ok(md5) | ||||
| } | ||||
|  | ||||
| pub async fn hip_report(gateway: &str, cookie: &str, csd_wrapper: &str, gp_params: &GpParams) -> anyhow::Result<()> { | ||||
|   let client = Client::builder() | ||||
|     .danger_accept_invalid_certs(gp_params.ignore_tls_errors()) | ||||
|     .user_agent(gp_params.user_agent()) | ||||
|     .build()?; | ||||
|  | ||||
|   let md5 = build_csd_token(cookie)?; | ||||
|  | ||||
|   info!("Submit HIP report md5: {}", md5); | ||||
|  | ||||
|   let reporter = HipReporter { | ||||
|     server: normalize_server(gateway)?, | ||||
|     cookie, | ||||
|     md5: &md5, | ||||
|     csd_wrapper, | ||||
|     gp_params, | ||||
|     client, | ||||
|   }; | ||||
|  | ||||
|   reporter.report().await | ||||
| } | ||||
| @@ -1,17 +1,22 @@ | ||||
| use anyhow::bail; | ||||
| use log::info; | ||||
| use reqwest::Client; | ||||
| use roxmltree::Document; | ||||
| use urlencoding::encode; | ||||
|  | ||||
| use crate::{credential::Credential, gp_params::GpParams}; | ||||
| use crate::{ | ||||
|   credential::Credential, | ||||
|   gp_params::GpParams, | ||||
|   utils::{normalize_server, remove_url_scheme}, | ||||
| }; | ||||
|  | ||||
| pub async fn gateway_login( | ||||
|   gateway: &str, | ||||
|   cred: &Credential, | ||||
|   gp_params: &GpParams, | ||||
| ) -> anyhow::Result<String> { | ||||
|   let login_url = format!("https://{}/ssl-vpn/login.esp", gateway); | ||||
| pub async fn gateway_login(gateway: &str, cred: &Credential, gp_params: &GpParams) -> anyhow::Result<String> { | ||||
|   let url = normalize_server(gateway)?; | ||||
|   let gateway = remove_url_scheme(&url); | ||||
|  | ||||
|   let login_url = format!("{}/ssl-vpn/login.esp", url); | ||||
|   let client = Client::builder() | ||||
|     .danger_accept_invalid_certs(gp_params.ignore_tls_errors()) | ||||
|     .user_agent(gp_params.user_agent()) | ||||
|     .build()?; | ||||
|  | ||||
| @@ -19,19 +24,18 @@ pub async fn gateway_login( | ||||
|   let extra_params = gp_params.to_params(); | ||||
|  | ||||
|   params.extend(extra_params); | ||||
|   params.insert("server", gateway); | ||||
|   params.insert("server", &gateway); | ||||
|  | ||||
|   info!("Gateway login, user_agent: {}", gp_params.user_agent()); | ||||
|  | ||||
|   let res_xml = client | ||||
|     .post(&login_url) | ||||
|     .form(¶ms) | ||||
|     .send() | ||||
|     .await? | ||||
|     .error_for_status()? | ||||
|     .text() | ||||
|     .await?; | ||||
|   let res = client.post(&login_url).form(¶ms).send().await?; | ||||
|   let status = res.status(); | ||||
|  | ||||
|   if status.is_client_error() || status.is_server_error() { | ||||
|     bail!("Gateway login error: {}", status) | ||||
|   } | ||||
|  | ||||
|   let res_xml = res.text().await?; | ||||
|   let doc = Document::parse(&res_xml)?; | ||||
|  | ||||
|   build_gateway_token(&doc, gp_params.computer()) | ||||
| @@ -62,11 +66,7 @@ fn build_gateway_token(doc: &Document, computer: &str) -> anyhow::Result<String> | ||||
|   Ok(token) | ||||
| } | ||||
|  | ||||
| fn read_args<'a>( | ||||
|   args: &'a [String], | ||||
|   index: usize, | ||||
|   key: &'a str, | ||||
| ) -> anyhow::Result<(&'a str, &'a str)> { | ||||
| fn read_args<'a>(args: &'a [String], index: usize, key: &'a str) -> anyhow::Result<(&'a str, &'a str)> { | ||||
|   args | ||||
|     .get(index) | ||||
|     .ok_or_else(|| anyhow::anyhow!("Failed to read {key} from args")) | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| mod login; | ||||
| mod parse_gateways; | ||||
| pub mod hip; | ||||
|  | ||||
| pub use login::*; | ||||
| pub(crate) use parse_gateways::*; | ||||
| @@ -31,6 +32,15 @@ impl Display for Gateway { | ||||
| } | ||||
|  | ||||
| impl Gateway { | ||||
|   pub fn new(name: String, address: String) -> Self { | ||||
|     Self { | ||||
|       name, | ||||
|       address, | ||||
|       priority: 0, | ||||
|       priority_rules: vec![], | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   pub fn name(&self) -> &str { | ||||
|     &self.name | ||||
|   } | ||||
|   | ||||
| @@ -4,9 +4,7 @@ use super::{Gateway, PriorityRule}; | ||||
|  | ||||
| pub(crate) fn parse_gateways(doc: &Document) -> Option<Vec<Gateway>> { | ||||
|   let node_gateways = doc.descendants().find(|n| n.has_tag_name("gateways"))?; | ||||
|   let list_gateway = node_gateways | ||||
|     .descendants() | ||||
|     .find(|n| n.has_tag_name("list"))?; | ||||
|   let list_gateway = node_gateways.descendants().find(|n| n.has_tag_name("list"))?; | ||||
|  | ||||
|   let gateways = list_gateway | ||||
|     .children() | ||||
|   | ||||
| @@ -7,23 +7,32 @@ use crate::GP_USER_AGENT; | ||||
|  | ||||
| #[derive(Debug, Serialize, Deserialize, Clone, Type, Default)] | ||||
| pub enum ClientOs { | ||||
|   Linux, | ||||
|   #[default] | ||||
|   Linux, | ||||
|   Windows, | ||||
|   Mac, | ||||
| } | ||||
|  | ||||
| impl From<&ClientOs> for &str { | ||||
|   fn from(os: &ClientOs) -> Self { | ||||
| impl From<&str> for ClientOs { | ||||
|   fn from(os: &str) -> Self { | ||||
|     match os { | ||||
|       ClientOs::Linux => "Linux", | ||||
|       ClientOs::Windows => "Windows", | ||||
|       ClientOs::Mac => "Mac", | ||||
|       "Linux" => ClientOs::Linux, | ||||
|       "Windows" => ClientOs::Windows, | ||||
|       "Mac" => ClientOs::Mac, | ||||
|       _ => ClientOs::Linux, | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| 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 { | ||||
|     match self { | ||||
|       ClientOs::Linux => "linux", | ||||
| @@ -35,11 +44,14 @@ impl ClientOs { | ||||
|  | ||||
| #[derive(Debug, Serialize, Deserialize, Type, Default)] | ||||
| pub struct GpParams { | ||||
|   is_gateway: bool, | ||||
|   user_agent: String, | ||||
|   client_os: ClientOs, | ||||
|   os_version: Option<String>, | ||||
|   client_version: Option<String>, | ||||
|   computer: Option<String>, | ||||
|   computer: String, | ||||
|   ignore_tls_errors: bool, | ||||
|   prefer_default_browser: bool, | ||||
| } | ||||
|  | ||||
| impl GpParams { | ||||
| @@ -47,20 +59,45 @@ impl GpParams { | ||||
|     GpParamsBuilder::new() | ||||
|   } | ||||
|  | ||||
|   pub(crate) fn is_gateway(&self) -> bool { | ||||
|     self.is_gateway | ||||
|   } | ||||
|  | ||||
|   pub fn set_is_gateway(&mut self, is_gateway: bool) { | ||||
|     self.is_gateway = is_gateway; | ||||
|   } | ||||
|  | ||||
|   pub(crate) fn user_agent(&self) -> &str { | ||||
|     &self.user_agent | ||||
|   } | ||||
|  | ||||
|   pub(crate) fn computer(&self) -> &str { | ||||
|     match self.computer { | ||||
|       Some(ref computer) => computer, | ||||
|       None => (&self.client_os).into() | ||||
|     } | ||||
|     &self.computer | ||||
|   } | ||||
|  | ||||
|   pub fn ignore_tls_errors(&self) -> bool { | ||||
|     self.ignore_tls_errors | ||||
|   } | ||||
|  | ||||
|   pub fn prefer_default_browser(&self) -> bool { | ||||
|     self.prefer_default_browser | ||||
|   } | ||||
|  | ||||
|   pub fn client_os(&self) -> &str { | ||||
|     self.client_os.as_str() | ||||
|   } | ||||
|  | ||||
|   pub fn os_version(&self) -> Option<&str> { | ||||
|     self.os_version.as_deref() | ||||
|   } | ||||
|  | ||||
|   pub fn client_version(&self) -> Option<&str> { | ||||
|     self.client_version.as_deref() | ||||
|   } | ||||
|  | ||||
|   pub(crate) fn to_params(&self) -> HashMap<&str, &str> { | ||||
|     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 | ||||
|     params.insert("prot", "https:"); | ||||
| @@ -70,46 +107,52 @@ impl GpParams { | ||||
|     params.insert("ipv6-support", "yes"); | ||||
|     params.insert("inputStr", ""); | ||||
|     params.insert("clientVer", "4100"); | ||||
|  | ||||
|     params.insert("clientos", client_os); | ||||
|  | ||||
|     if let Some(computer) = &self.computer { | ||||
|       params.insert("computer", computer); | ||||
|     } else { | ||||
|       params.insert("computer", client_os); | ||||
|     } | ||||
|     params.insert("computer", &self.computer); | ||||
|  | ||||
|     if let Some(os_version) = &self.os_version { | ||||
|       params.insert("os-version", os_version); | ||||
|     } | ||||
|  | ||||
|     if let Some(client_version) = &self.client_version { | ||||
|       params.insert("clientgpversion", client_version); | ||||
|     } | ||||
|     // NOTE: Do not include clientgpversion for now | ||||
|     // if let Some(client_version) = &self.client_version { | ||||
|     //   params.insert("clientgpversion", client_version); | ||||
|     // } | ||||
|  | ||||
|     params | ||||
|   } | ||||
| } | ||||
|  | ||||
| pub struct GpParamsBuilder { | ||||
|   is_gateway: bool, | ||||
|   user_agent: String, | ||||
|   client_os: ClientOs, | ||||
|   os_version: Option<String>, | ||||
|   client_version: Option<String>, | ||||
|   computer: Option<String>, | ||||
|   computer: String, | ||||
|   ignore_tls_errors: bool, | ||||
|   prefer_default_browser: bool, | ||||
| } | ||||
|  | ||||
| impl GpParamsBuilder { | ||||
|   pub fn new() -> Self { | ||||
|     Self { | ||||
|       is_gateway: false, | ||||
|       user_agent: GP_USER_AGENT.to_string(), | ||||
|       client_os: ClientOs::Linux, | ||||
|       os_version: Default::default(), | ||||
|       client_version: Default::default(), | ||||
|       computer: Default::default(), | ||||
|       computer: whoami::hostname(), | ||||
|       ignore_tls_errors: false, | ||||
|       prefer_default_browser: false, | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   pub fn is_gateway(&mut self, is_gateway: bool) -> &mut Self { | ||||
|     self.is_gateway = is_gateway; | ||||
|     self | ||||
|   } | ||||
|  | ||||
|   pub fn user_agent(&mut self, user_agent: &str) -> &mut Self { | ||||
|     self.user_agent = user_agent.to_string(); | ||||
|     self | ||||
| @@ -120,28 +163,41 @@ impl GpParamsBuilder { | ||||
|     self | ||||
|   } | ||||
|  | ||||
|   pub fn os_version(&mut self, os_version: &str) -> &mut Self { | ||||
|     self.os_version = Some(os_version.to_string()); | ||||
|   pub fn os_version<T: Into<Option<String>>>(&mut self, os_version: T) -> &mut Self { | ||||
|     self.os_version = os_version.into(); | ||||
|     self | ||||
|   } | ||||
|  | ||||
|   pub fn client_version(&mut self, client_version: &str) -> &mut Self { | ||||
|     self.client_version = Some(client_version.to_string()); | ||||
|   pub fn client_version<T: Into<Option<String>>>(&mut self, client_version: T) -> &mut Self { | ||||
|     self.client_version = client_version.into(); | ||||
|     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 | ||||
|   } | ||||
|  | ||||
|   pub fn build(&self) -> GpParams { | ||||
|     GpParams { | ||||
|       is_gateway: self.is_gateway, | ||||
|       user_agent: self.user_agent.clone(), | ||||
|       client_os: self.client_os.clone(), | ||||
|       os_version: self.os_version.clone(), | ||||
|       client_version: self.client_version.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 utils; | ||||
|  | ||||
| #[cfg(feature = "clap")] | ||||
| pub mod clap; | ||||
|  | ||||
| #[cfg(debug_assertions)] | ||||
| pub const GP_API_KEY: &[u8; 32] = &[0; 32]; | ||||
|  | ||||
| pub const GP_USER_AGENT: &str = "PAN GlobalProtect"; | ||||
| 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))] | ||||
| pub const GP_SERVICE_BINARY: &str = "/usr/bin/gpservice"; | ||||
| #[cfg(not(debug_assertions))] | ||||
| @@ -20,6 +25,8 @@ pub const GP_GUI_BINARY: &str = "/usr/bin/gpgui"; | ||||
| #[cfg(not(debug_assertions))] | ||||
| 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)] | ||||
| pub const GP_SERVICE_BINARY: &str = dotenvy_macro::dotenv!("GP_SERVICE_BINARY"); | ||||
| #[cfg(debug_assertions)] | ||||
|   | ||||
| @@ -1,16 +1,16 @@ | ||||
| use anyhow::ensure; | ||||
| use anyhow::bail; | ||||
| use log::info; | ||||
| use reqwest::Client; | ||||
| use reqwest::{Client, StatusCode}; | ||||
| use roxmltree::Document; | ||||
| use serde::Serialize; | ||||
| use specta::Type; | ||||
| use thiserror::Error; | ||||
|  | ||||
| use crate::{ | ||||
|   credential::{AuthCookieCredential, Credential}, | ||||
|   gateway::{parse_gateways, Gateway}, | ||||
|   gp_params::GpParams, | ||||
|   utils::{normalize_server, xml}, | ||||
|   portal::PortalError, | ||||
|   utils::{normalize_server, remove_url_scheme, xml}, | ||||
| }; | ||||
|  | ||||
| #[derive(Debug, Serialize, Type)] | ||||
| @@ -18,25 +18,12 @@ use crate::{ | ||||
| pub struct PortalConfig { | ||||
|   portal: String, | ||||
|   auth_cookie: AuthCookieCredential, | ||||
|   config_cred: Credential, | ||||
|   gateways: Vec<Gateway>, | ||||
|   config_digest: Option<String>, | ||||
| } | ||||
|  | ||||
| impl PortalConfig { | ||||
|   pub fn new( | ||||
|     portal: String, | ||||
|     auth_cookie: AuthCookieCredential, | ||||
|     gateways: Vec<Gateway>, | ||||
|     config_digest: Option<String>, | ||||
|   ) -> Self { | ||||
|     Self { | ||||
|       portal, | ||||
|       auth_cookie, | ||||
|       gateways, | ||||
|       config_digest, | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   pub fn portal(&self) -> &str { | ||||
|     &self.portal | ||||
|   } | ||||
| @@ -49,6 +36,10 @@ impl PortalConfig { | ||||
|     &self.auth_cookie | ||||
|   } | ||||
|  | ||||
|   pub fn config_cred(&self) -> &Credential { | ||||
|     &self.config_cred | ||||
|   } | ||||
|  | ||||
|   /// In-place sort the gateways by region | ||||
|   pub fn sort_gateways(&mut self, region: &str) { | ||||
|     let preferred_gateway = self.find_preferred_gateway(region); | ||||
| @@ -88,38 +79,17 @@ impl PortalConfig { | ||||
|     } | ||||
|  | ||||
|     // If no gateway is found, return the gateway with the lowest priority | ||||
|     preferred_gateway.unwrap_or_else(|| { | ||||
|       self | ||||
|         .gateways | ||||
|         .iter() | ||||
|         .min_by_key(|gateway| gateway.priority) | ||||
|         .unwrap() | ||||
|     }) | ||||
|     preferred_gateway.unwrap_or_else(|| self.gateways.iter().min_by_key(|gateway| gateway.priority).unwrap()) | ||||
|   } | ||||
| } | ||||
|  | ||||
| #[derive(Error, Debug)] | ||||
| pub enum PortalConfigError { | ||||
|   #[error("Empty response, retrying can help")] | ||||
|   EmptyResponse, | ||||
|   #[error("Empty auth cookie, retrying can help")] | ||||
|   EmptyAuthCookie, | ||||
|   #[error("Invalid auth cookie, retrying can help")] | ||||
|   InvalidAuthCookie, | ||||
|   #[error("Empty gateways, retrying can help")] | ||||
|   EmptyGateways, | ||||
| } | ||||
|  | ||||
| pub async fn retrieve_config( | ||||
|   portal: &str, | ||||
|   cred: &Credential, | ||||
|   gp_params: &GpParams, | ||||
| ) -> anyhow::Result<PortalConfig> { | ||||
| pub async fn retrieve_config(portal: &str, cred: &Credential, gp_params: &GpParams) -> anyhow::Result<PortalConfig> { | ||||
|   let portal = normalize_server(portal)?; | ||||
|   let server = remove_url_scheme(&portal); | ||||
|  | ||||
|   let url = format!("{}/global-protect/getconfig.esp", portal); | ||||
|   let client = Client::builder() | ||||
|     .danger_accept_invalid_certs(gp_params.ignore_tls_errors()) | ||||
|     .user_agent(gp_params.user_agent()) | ||||
|     .build()?; | ||||
|  | ||||
| @@ -132,49 +102,43 @@ pub async fn retrieve_config( | ||||
|  | ||||
|   info!("Portal config, user_agent: {}", gp_params.user_agent()); | ||||
|  | ||||
|   let res_xml = client | ||||
|     .post(&url) | ||||
|     .form(¶ms) | ||||
|     .send() | ||||
|     .await? | ||||
|     .error_for_status()? | ||||
|     .text() | ||||
|     .await?; | ||||
|   let res = client.post(&url).form(¶ms).send().await?; | ||||
|   let status = res.status(); | ||||
|  | ||||
|   ensure!(!res_xml.is_empty(), PortalConfigError::EmptyResponse); | ||||
|   if status == StatusCode::NOT_FOUND { | ||||
|     bail!(PortalError::ConfigError("Config endpoint not found".to_string())) | ||||
|   } | ||||
|  | ||||
|   let doc = Document::parse(&res_xml)?; | ||||
|   let gateways = parse_gateways(&doc).ok_or_else(|| anyhow::anyhow!("Failed to parse gateways"))?; | ||||
|   if status.is_client_error() || status.is_server_error() { | ||||
|     bail!("Portal config error: {}", status) | ||||
|   } | ||||
|  | ||||
|   let res_xml = res.text().await.map_err(|e| PortalError::ConfigError(e.to_string()))?; | ||||
|  | ||||
|   if res_xml.is_empty() { | ||||
|     bail!(PortalError::ConfigError("Empty portal config response".to_string())) | ||||
|   } | ||||
|  | ||||
|   let doc = Document::parse(&res_xml).map_err(|e| PortalError::ConfigError(e.to_string()))?; | ||||
|  | ||||
|   let mut gateways = parse_gateways(&doc).unwrap_or_else(|| { | ||||
|     info!("No gateways found in portal config"); | ||||
|     vec![] | ||||
|   }); | ||||
|  | ||||
|   let user_auth_cookie = xml::get_child_text(&doc, "portal-userauthcookie").unwrap_or_default(); | ||||
|   let prelogon_user_auth_cookie = | ||||
|     xml::get_child_text(&doc, "portal-prelogonuserauthcookie").unwrap_or_default(); | ||||
|   let prelogon_user_auth_cookie = xml::get_child_text(&doc, "portal-prelogonuserauthcookie").unwrap_or_default(); | ||||
|   let config_digest = xml::get_child_text(&doc, "config-digest"); | ||||
|  | ||||
|   ensure!( | ||||
|     !user_auth_cookie.is_empty() && !prelogon_user_auth_cookie.is_empty(), | ||||
|     PortalConfigError::EmptyAuthCookie | ||||
|   ); | ||||
|   if gateways.is_empty() { | ||||
|     gateways.push(Gateway::new(server.to_string(), server.to_string())); | ||||
|   } | ||||
|  | ||||
|   ensure!( | ||||
|     user_auth_cookie != "empty" && prelogon_user_auth_cookie != "empty", | ||||
|     PortalConfigError::InvalidAuthCookie | ||||
|   ); | ||||
|  | ||||
|   ensure!(!gateways.is_empty(), PortalConfigError::EmptyGateways); | ||||
|  | ||||
|   Ok(PortalConfig::new( | ||||
|     server.to_string(), | ||||
|     AuthCookieCredential::new( | ||||
|       cred.username(), | ||||
|       &user_auth_cookie, | ||||
|       &prelogon_user_auth_cookie, | ||||
|     ), | ||||
|   Ok(PortalConfig { | ||||
|     portal: server.to_string(), | ||||
|     auth_cookie: AuthCookieCredential::new(cred.username(), &user_auth_cookie, &prelogon_user_auth_cookie), | ||||
|     config_cred: cred.clone(), | ||||
|     gateways, | ||||
|     config_digest, | ||||
|   )) | ||||
| } | ||||
|  | ||||
| fn remove_url_scheme(s: &str) -> String { | ||||
|   s.replace("http://", "").replace("https://", "") | ||||
|   }) | ||||
| } | ||||
|   | ||||
| @@ -3,3 +3,13 @@ mod prelogin; | ||||
|  | ||||
| pub use config::*; | ||||
| pub use prelogin::*; | ||||
|  | ||||
| use thiserror::Error; | ||||
|  | ||||
| #[derive(Error, Debug)] | ||||
| pub enum PortalError { | ||||
|   #[error("Portal prelogin error: {0}")] | ||||
|   PreloginError(String), | ||||
|   #[error("Portal config error: {0}")] | ||||
|   ConfigError(String), | ||||
| } | ||||
|   | ||||
| @@ -1,17 +1,34 @@ | ||||
| use anyhow::bail; | ||||
| use log::{info, trace}; | ||||
| use reqwest::Client; | ||||
| use log::info; | ||||
| use reqwest::{Client, StatusCode}; | ||||
| use roxmltree::Document; | ||||
| use serde::Serialize; | ||||
| use specta::Type; | ||||
|  | ||||
| use crate::utils::{base64, normalize_server, xml}; | ||||
| use crate::{ | ||||
|   gp_params::GpParams, | ||||
|   portal::PortalError, | ||||
|   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)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| pub struct SamlPrelogin { | ||||
|   region: String, | ||||
|   is_gateway: bool, | ||||
|   saml_request: String, | ||||
|   support_default_browser: bool, | ||||
| } | ||||
|  | ||||
| impl SamlPrelogin { | ||||
| @@ -22,12 +39,17 @@ impl SamlPrelogin { | ||||
|   pub fn saml_request(&self) -> &str { | ||||
|     &self.saml_request | ||||
|   } | ||||
|  | ||||
|   pub fn support_default_browser(&self) -> bool { | ||||
|     self.support_default_browser | ||||
|   } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Serialize, Type, Clone)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| pub struct StandardPrelogin { | ||||
|   region: String, | ||||
|   is_gateway: bool, | ||||
|   auth_message: String, | ||||
|   label_username: String, | ||||
|   label_password: String, | ||||
| @@ -65,24 +87,59 @@ impl Prelogin { | ||||
|       Prelogin::Standard(standard) => standard.region(), | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   pub fn is_gateway(&self) -> bool { | ||||
|     match self { | ||||
|       Prelogin::Saml(saml) => saml.is_gateway, | ||||
|       Prelogin::Standard(standard) => standard.is_gateway, | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| pub async fn prelogin(portal: &str, user_agent: &str) -> anyhow::Result<Prelogin> { | ||||
|   info!("Portal prelogin, user_agent: {}", user_agent); | ||||
| pub async fn prelogin(portal: &str, gp_params: &GpParams) -> anyhow::Result<Prelogin> { | ||||
|   let user_agent = gp_params.user_agent(); | ||||
|   info!("Prelogin with user_agent: {}", user_agent); | ||||
|  | ||||
|   let portal = normalize_server(portal)?; | ||||
|   let prelogin_url = format!("{}/global-protect/prelogin.esp", portal); | ||||
|   let client = Client::builder().user_agent(user_agent).build()?; | ||||
|   let is_gateway = gp_params.is_gateway(); | ||||
|   let path = if is_gateway { "ssl-vpn" } else { "global-protect" }; | ||||
|   let prelogin_url = format!("{portal}/{}/prelogin.esp", path); | ||||
|   let mut params = gp_params.to_params(); | ||||
|  | ||||
|   let res_xml = client | ||||
|     .get(&prelogin_url) | ||||
|     .send() | ||||
|     .await? | ||||
|     .error_for_status()? | ||||
|   params.insert("tmp", "tmp"); | ||||
|   if gp_params.prefer_default_browser() { | ||||
|     params.insert("default-browser", "1"); | ||||
|   } | ||||
|  | ||||
|   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 status = res.status(); | ||||
|  | ||||
|   if status == StatusCode::NOT_FOUND { | ||||
|     bail!(PortalError::PreloginError("Prelogin endpoint not found".to_string())) | ||||
|   } | ||||
|  | ||||
|   if status.is_client_error() || status.is_server_error() { | ||||
|     bail!("Prelogin error: {}", status) | ||||
|   } | ||||
|  | ||||
|   let res_xml = res | ||||
|     .text() | ||||
|     .await?; | ||||
|     .await | ||||
|     .map_err(|e| PortalError::PreloginError(e.to_string()))?; | ||||
|  | ||||
|   trace!("Prelogin response: {}", res_xml); | ||||
|   let prelogin = parse_res_xml(res_xml, is_gateway).map_err(|e| PortalError::PreloginError(e.to_string()))?; | ||||
|  | ||||
|   Ok(prelogin) | ||||
| } | ||||
|  | ||||
| fn parse_res_xml(res_xml: String, is_gateway: bool) -> anyhow::Result<Prelogin> { | ||||
|   let doc = Document::parse(&res_xml)?; | ||||
|  | ||||
|   let status = xml::get_child_text(&doc, "status") | ||||
| @@ -93,17 +150,24 @@ pub async fn prelogin(portal: &str, user_agent: &str) -> anyhow::Result<Prelogin | ||||
|     bail!("Prelogin failed: {}", msg) | ||||
|   } | ||||
|  | ||||
|   let region = xml::get_child_text(&doc, "region") | ||||
|     .ok_or_else(|| anyhow::anyhow!("Prelogin response does not contain region element"))?; | ||||
|   let region = xml::get_child_text(&doc, "region").unwrap_or_else(|| { | ||||
|     info!("Prelogin response does not contain region element"); | ||||
|     String::from("Unknown") | ||||
|   }); | ||||
|  | ||||
|   let saml_method = xml::get_child_text(&doc, "saml-auth-method"); | ||||
|   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 | ||||
|   if saml_method.is_some() && saml_request.is_some() { | ||||
|     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 { | ||||
|       region, | ||||
|       is_gateway, | ||||
|       saml_request, | ||||
|       support_default_browser, | ||||
|     }; | ||||
|  | ||||
|     return Ok(Prelogin::Saml(saml_prelogin)); | ||||
| @@ -113,10 +177,11 @@ pub async fn prelogin(portal: &str, user_agent: &str) -> anyhow::Result<Prelogin | ||||
|   let label_password = xml::get_child_text(&doc, "password-label"); | ||||
|   // Check if the prelogin response is standard login | ||||
|   if label_username.is_some() && label_password.is_some() { | ||||
|     let auth_message = xml::get_child_text(&doc, "authentication-message") | ||||
|       .unwrap_or(String::from("Please enter the login credentials")); | ||||
|     let auth_message = | ||||
|       xml::get_child_text(&doc, "authentication-message").unwrap_or(String::from("Please enter the login credentials")); | ||||
|     let standard_prelogin = StandardPrelogin { | ||||
|       region, | ||||
|       is_gateway, | ||||
|       auth_message, | ||||
|       label_username: label_username.unwrap(), | ||||
|       label_password: label_password.unwrap(), | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| use std::process::Stdio; | ||||
|  | ||||
| use anyhow::bail; | ||||
| use tokio::process::Command; | ||||
|  | ||||
| use crate::{auth::SamlAuthResult, credential::Credential, GP_AUTH_BINARY}; | ||||
| @@ -8,10 +9,14 @@ use super::command_traits::CommandExt; | ||||
|  | ||||
| pub struct SamlAuthLauncher<'a> { | ||||
|   server: &'a str, | ||||
|   user_agent: Option<&'a str>, | ||||
|   gateway: bool, | ||||
|   saml_request: Option<&'a str>, | ||||
|   user_agent: Option<&'a str>, | ||||
|   os: Option<&'a str>, | ||||
|   os_version: Option<&'a str>, | ||||
|   hidpi: bool, | ||||
|   fix_openssl: bool, | ||||
|   ignore_tls_errors: bool, | ||||
|   clean: bool, | ||||
| } | ||||
|  | ||||
| @@ -19,21 +24,40 @@ impl<'a> SamlAuthLauncher<'a> { | ||||
|   pub fn new(server: &'a str) -> Self { | ||||
|     Self { | ||||
|       server, | ||||
|       user_agent: None, | ||||
|       gateway: false, | ||||
|       saml_request: None, | ||||
|       user_agent: None, | ||||
|       os: None, | ||||
|       os_version: None, | ||||
|       hidpi: false, | ||||
|       fix_openssl: false, | ||||
|       ignore_tls_errors: false, | ||||
|       clean: false, | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   pub fn gateway(mut self, gateway: bool) -> Self { | ||||
|     self.gateway = gateway; | ||||
|     self | ||||
|   } | ||||
|  | ||||
|   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 { | ||||
|     self.user_agent = Some(user_agent); | ||||
|     self | ||||
|   } | ||||
|  | ||||
|   pub fn saml_request(mut self, saml_request: &'a str) -> Self { | ||||
|     self.saml_request = Some(saml_request); | ||||
|   pub fn os(mut self, os: &'a str) -> Self { | ||||
|     self.os = Some(os); | ||||
|     self | ||||
|   } | ||||
|  | ||||
|   pub fn os_version(mut self, os_version: Option<&'a str>) -> Self { | ||||
|     self.os_version = os_version; | ||||
|     self | ||||
|   } | ||||
|  | ||||
| @@ -47,6 +71,11 @@ impl<'a> SamlAuthLauncher<'a> { | ||||
|     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 { | ||||
|     self.clean = clean; | ||||
|     self | ||||
| @@ -57,22 +86,38 @@ impl<'a> SamlAuthLauncher<'a> { | ||||
|     let mut auth_cmd = Command::new(GP_AUTH_BINARY); | ||||
|     auth_cmd.arg(self.server); | ||||
|  | ||||
|     if let Some(user_agent) = self.user_agent { | ||||
|       auth_cmd.arg("--user-agent").arg(user_agent); | ||||
|     if self.gateway { | ||||
|       auth_cmd.arg("--gateway"); | ||||
|     } | ||||
|  | ||||
|     if let Some(saml_request) = self.saml_request { | ||||
|       auth_cmd.arg("--saml-request").arg(saml_request); | ||||
|     } | ||||
|  | ||||
|     if self.fix_openssl { | ||||
|       auth_cmd.arg("--fix-openssl"); | ||||
|     if let Some(user_agent) = self.user_agent { | ||||
|       auth_cmd.arg("--user-agent").arg(user_agent); | ||||
|     } | ||||
|  | ||||
|     if let Some(os) = self.os { | ||||
|       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 { | ||||
|       auth_cmd.arg("--fix-openssl"); | ||||
|     } | ||||
|  | ||||
|     if self.ignore_tls_errors { | ||||
|       auth_cmd.arg("--ignore-tls-errors"); | ||||
|     } | ||||
|  | ||||
|     if self.clean { | ||||
|       auth_cmd.arg("--clean"); | ||||
|     } | ||||
| @@ -85,12 +130,13 @@ impl<'a> SamlAuthLauncher<'a> { | ||||
|       .wait_with_output() | ||||
|       .await?; | ||||
|  | ||||
|     let auth_result: SamlAuthResult = serde_json::from_slice(&output.stdout) | ||||
|       .map_err(|_| anyhow::anyhow!("Failed to parse auth data"))?; | ||||
|     let Ok(auth_result) = serde_json::from_slice::<SamlAuthResult>(&output.stdout) else { | ||||
|       bail!("Failed to parse auth data") | ||||
|     }; | ||||
|  | ||||
|     match auth_result { | ||||
|       SamlAuthResult::Success(auth_data) => Credential::try_from(auth_data), | ||||
|       SamlAuthResult::Failure(msg) => Err(anyhow::anyhow!(msg)), | ||||
|       SamlAuthResult::Failure(msg) => bail!(msg), | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
							
								
								
									
										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,7 +1,8 @@ | ||||
| use anyhow::bail; | ||||
| use std::{env, ffi::OsStr}; | ||||
| use std::ffi::OsStr; | ||||
| use tokio::process::Command; | ||||
| use users::{os::unix::UserExt, User}; | ||||
| use uzers::os::unix::UserExt; | ||||
|  | ||||
| use super::users::get_non_root_user; | ||||
|  | ||||
| pub trait CommandExt { | ||||
|   fn new_pkexec<S: AsRef<OsStr>>(program: S) -> Command; | ||||
| @@ -21,8 +22,7 @@ impl CommandExt for Command { | ||||
|   } | ||||
|  | ||||
|   fn into_non_root(mut self) -> anyhow::Result<Command> { | ||||
|     let user = | ||||
|       get_non_root_user().map_err(|_| anyhow::anyhow!("{:?} cannot be run as root", self))?; | ||||
|     let user = get_non_root_user().map_err(|_| anyhow::anyhow!("{:?} cannot be run as root", self))?; | ||||
|  | ||||
|     self | ||||
|       .env("HOME", user.home_dir()) | ||||
| @@ -35,30 +35,3 @@ impl CommandExt for Command { | ||||
|     Ok(self) | ||||
|   } | ||||
| } | ||||
|  | ||||
| fn get_non_root_user() -> anyhow::Result<User> { | ||||
|   let current_user = whoami::username(); | ||||
|  | ||||
|   let user = if current_user == "root" { | ||||
|     get_real_user()? | ||||
|   } else { | ||||
|     users::get_user_by_name(¤t_user) | ||||
|       .ok_or_else(|| anyhow::anyhow!("User ({}) not found", current_user))? | ||||
|   }; | ||||
|  | ||||
|   if user.uid() == 0 { | ||||
|     bail!("Non-root user not found") | ||||
|   } | ||||
|  | ||||
|   Ok(user) | ||||
| } | ||||
|  | ||||
| fn get_real_user() -> anyhow::Result<User> { | ||||
|   // Read the UID from SUDO_UID or PKEXEC_UID environment variable if available. | ||||
|   let uid = match env::var("SUDO_UID") { | ||||
|     Ok(uid) => uid.parse::<u32>()?, | ||||
|     _ => env::var("PKEXEC_UID")?.parse::<u32>()?, | ||||
|   }; | ||||
|  | ||||
|   users::get_user_by_uid(uid).ok_or_else(|| anyhow::anyhow!("User not found")) | ||||
| } | ||||
|   | ||||
| @@ -66,10 +66,7 @@ impl GuiLauncher { | ||||
|  | ||||
|     let mut non_root_cmd = cmd.into_non_root()?; | ||||
|  | ||||
|     let mut child = non_root_cmd | ||||
|       .kill_on_drop(true) | ||||
|       .stdin(Stdio::piped()) | ||||
|       .spawn()?; | ||||
|     let mut child = non_root_cmd.kill_on_drop(true).stdin(Stdio::piped()).spawn()?; | ||||
|  | ||||
|     let mut stdin = child | ||||
|       .stdin | ||||
|   | ||||
							
								
								
									
										94
									
								
								crates/gpapi/src/process/hip_launcher.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,94 @@ | ||||
| use std::process::Stdio; | ||||
|  | ||||
| use anyhow::bail; | ||||
| use tokio::process::Command; | ||||
|  | ||||
| pub struct HipLauncher<'a> { | ||||
|   program: &'a str, | ||||
|   cookie: Option<&'a str>, | ||||
|   client_ip: Option<&'a str>, | ||||
|   md5: Option<&'a str>, | ||||
|   client_os: Option<&'a str>, | ||||
|   client_version: Option<&'a str>, | ||||
| } | ||||
|  | ||||
| impl<'a> HipLauncher<'a> { | ||||
|   pub fn new(program: &'a str) -> Self { | ||||
|     Self { | ||||
|       program, | ||||
|       cookie: None, | ||||
|       client_ip: None, | ||||
|       md5: None, | ||||
|       client_os: None, | ||||
|       client_version: None, | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   pub fn cookie(mut self, cookie: &'a str) -> Self { | ||||
|     self.cookie = Some(cookie); | ||||
|     self | ||||
|   } | ||||
|  | ||||
|   pub fn client_ip(mut self, client_ip: &'a str) -> Self { | ||||
|     self.client_ip = Some(client_ip); | ||||
|     self | ||||
|   } | ||||
|  | ||||
|   pub fn md5(mut self, md5: &'a str) -> Self { | ||||
|     self.md5 = Some(md5); | ||||
|     self | ||||
|   } | ||||
|  | ||||
|   pub fn client_os(mut self, client_os: &'a str) -> Self { | ||||
|     self.client_os = Some(client_os); | ||||
|     self | ||||
|   } | ||||
|  | ||||
|   pub fn client_version(mut self, client_version: Option<&'a str>) -> Self { | ||||
|     self.client_version = client_version; | ||||
|     self | ||||
|   } | ||||
|  | ||||
|   pub async fn launch(&self) -> anyhow::Result<String> { | ||||
|     let mut cmd = Command::new(self.program); | ||||
|  | ||||
|     if let Some(cookie) = self.cookie { | ||||
|       cmd.arg("--cookie").arg(cookie); | ||||
|     } | ||||
|  | ||||
|     if let Some(client_ip) = self.client_ip { | ||||
|       cmd.arg("--client-ip").arg(client_ip); | ||||
|     } | ||||
|  | ||||
|     if let Some(md5) = self.md5 { | ||||
|       cmd.arg("--md5").arg(md5); | ||||
|     } | ||||
|  | ||||
|     if let Some(client_os) = self.client_os { | ||||
|       cmd.arg("--client-os").arg(client_os); | ||||
|     } | ||||
|  | ||||
|     if let Some(client_version) = self.client_version { | ||||
|       cmd.env("APP_VERSION", client_version); | ||||
|     } | ||||
|  | ||||
|     let output = cmd | ||||
|       .kill_on_drop(true) | ||||
|       .stdout(Stdio::piped()) | ||||
|       .spawn()? | ||||
|       .wait_with_output() | ||||
|       .await?; | ||||
|  | ||||
|     if let Some(exit_status) = output.status.code() { | ||||
|       if exit_status != 0 { | ||||
|         bail!("HIP report generation failed with exit code {}", exit_status); | ||||
|       } | ||||
|  | ||||
|       let report = String::from_utf8(output.stdout)?; | ||||
|  | ||||
|       Ok(report) | ||||
|     } else { | ||||
|       bail!("HIP report generation failed"); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -1,5 +1,9 @@ | ||||
| pub(crate) mod command_traits; | ||||
|  | ||||
| pub mod auth_launcher; | ||||
| #[cfg(feature = "browser-auth")] | ||||
| pub mod browser_authenticator; | ||||
| pub mod gui_launcher; | ||||
| pub mod hip_launcher; | ||||
| pub mod service_launcher; | ||||
| pub mod users; | ||||
|   | ||||
							
								
								
									
										39
									
								
								crates/gpapi/src/process/users.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,39 @@ | ||||
| use std::env; | ||||
|  | ||||
| use anyhow::bail; | ||||
| use uzers::User; | ||||
|  | ||||
| pub fn get_user_by_name(username: &str) -> anyhow::Result<User> { | ||||
|   uzers::get_user_by_name(username).ok_or_else(|| anyhow::anyhow!("User ({}) not found", username)) | ||||
| } | ||||
|  | ||||
| pub fn get_non_root_user() -> anyhow::Result<User> { | ||||
|   let current_user = whoami::username(); | ||||
|  | ||||
|   let user = if current_user == "root" { | ||||
|     get_real_user()? | ||||
|   } else { | ||||
|     get_user_by_name(¤t_user)? | ||||
|   }; | ||||
|  | ||||
|   if user.uid() == 0 { | ||||
|     bail!("Non-root user not found") | ||||
|   } | ||||
|  | ||||
|   Ok(user) | ||||
| } | ||||
|  | ||||
| pub fn get_current_user() -> anyhow::Result<User> { | ||||
|   let current_user = whoami::username(); | ||||
|   get_user_by_name(¤t_user) | ||||
| } | ||||
|  | ||||
| fn get_real_user() -> anyhow::Result<User> { | ||||
|   // Read the UID from SUDO_UID or PKEXEC_UID environment variable if available. | ||||
|   let uid = match env::var("SUDO_UID") { | ||||
|     Ok(uid) => uid.parse::<u32>()?, | ||||
|     _ => env::var("PKEXEC_UID")?.parse::<u32>()?, | ||||
|   }; | ||||
|  | ||||
|   uzers::get_user_by_uid(uid).ok_or_else(|| anyhow::anyhow!("User not found")) | ||||
| } | ||||
| @@ -7,4 +7,6 @@ use super::vpn_state::VpnState; | ||||
| pub enum WsEvent { | ||||
|   VpnState(VpnState), | ||||
|   ActiveGui, | ||||
|   /// External authentication data | ||||
|   AuthData(String), | ||||
| } | ||||
|   | ||||
| @@ -32,6 +32,9 @@ pub struct ConnectArgs { | ||||
|   cookie: String, | ||||
|   vpnc_script: Option<String>, | ||||
|   user_agent: Option<String>, | ||||
|   csd_uid: u32, | ||||
|   csd_wrapper: Option<String>, | ||||
|   mtu: u32, | ||||
|   os: Option<ClientOs>, | ||||
| } | ||||
|  | ||||
| @@ -42,6 +45,9 @@ impl ConnectArgs { | ||||
|       vpnc_script: None, | ||||
|       user_agent: None, | ||||
|       os: None, | ||||
|       csd_uid: 0, | ||||
|       csd_wrapper: None, | ||||
|       mtu: 0, | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -58,10 +64,19 @@ impl ConnectArgs { | ||||
|   } | ||||
|  | ||||
|   pub fn openconnect_os(&self) -> Option<String> { | ||||
|     self | ||||
|       .os | ||||
|       .as_ref() | ||||
|       .map(|os| os.to_openconnect_os().to_string()) | ||||
|     self.os.as_ref().map(|os| os.to_openconnect_os().to_string()) | ||||
|   } | ||||
|  | ||||
|   pub fn csd_uid(&self) -> u32 { | ||||
|     self.csd_uid | ||||
|   } | ||||
|  | ||||
|   pub fn csd_wrapper(&self) -> Option<String> { | ||||
|     self.csd_wrapper.clone() | ||||
|   } | ||||
|  | ||||
|   pub fn mtu(&self) -> u32 { | ||||
|     self.mtu | ||||
|   } | ||||
| } | ||||
|  | ||||
| @@ -84,6 +99,21 @@ impl ConnectRequest { | ||||
|     self | ||||
|   } | ||||
|  | ||||
|   pub fn with_csd_uid(mut self, csd_uid: u32) -> Self { | ||||
|     self.args.csd_uid = csd_uid; | ||||
|     self | ||||
|   } | ||||
|  | ||||
|   pub fn with_csd_wrapper<T: Into<Option<String>>>(mut self, csd_wrapper: T) -> Self { | ||||
|     self.args.csd_wrapper = csd_wrapper.into(); | ||||
|     self | ||||
|   } | ||||
|  | ||||
|   pub fn with_mtu(mut self, mtu: u32) -> Self { | ||||
|     self.args.mtu = mtu; | ||||
|     self | ||||
|   } | ||||
|  | ||||
|   pub fn with_user_agent<T: Into<Option<String>>>(mut self, user_agent: T) -> Self { | ||||
|     self.args.user_agent = user_agent.into(); | ||||
|     self | ||||
|   | ||||
| @@ -30,11 +30,13 @@ pub fn normalize_server(server: &str) -> anyhow::Result<String> { | ||||
|     .host_str() | ||||
|     .ok_or(anyhow::anyhow!("Invalid server URL: missing host"))?; | ||||
|  | ||||
|   let port: String = normalized_url | ||||
|     .port() | ||||
|     .map_or("".into(), |port| format!(":{}", port)); | ||||
|   let port: String = normalized_url.port().map_or("".into(), |port| format!(":{}", port)); | ||||
|  | ||||
|   let normalized_url = format!("{}://{}{}", scheme, host, port); | ||||
|  | ||||
|   Ok(normalized_url) | ||||
| } | ||||
|  | ||||
| pub fn remove_url_scheme(s: &str) -> String { | ||||
|   s.replace("http://", "").replace("https://", "") | ||||
| } | ||||
|   | ||||
| @@ -115,12 +115,7 @@ pub fn redact_uri(uri: &str) -> String { | ||||
|       .map(|query| format!("?{}", query)) | ||||
|       .unwrap_or_default(); | ||||
|  | ||||
|     return format!( | ||||
|       "{}://[**********]{}{}", | ||||
|       url.scheme(), | ||||
|       url.path(), | ||||
|       redacted_query | ||||
|     ); | ||||
|     return format!("{}://[**********]{}{}", url.scheme(), url.path(), redacted_query); | ||||
|   } | ||||
|  | ||||
|   let redacted_query = redact_query(url.query()); | ||||
| @@ -165,10 +160,7 @@ mod tests { | ||||
|  | ||||
|     redaction.add_value("foo").unwrap(); | ||||
|  | ||||
|     assert_eq!( | ||||
|       redaction.redact_str("hello, foo, bar"), | ||||
|       "hello, [**********], bar" | ||||
|     ); | ||||
|     assert_eq!(redaction.redact_str("hello, foo, bar"), "hello, [**********], bar"); | ||||
|   } | ||||
|  | ||||
|   #[test] | ||||
|   | ||||
| @@ -2,9 +2,7 @@ use tokio::signal; | ||||
|  | ||||
| pub async fn shutdown_signal() { | ||||
|   let ctrl_c = async { | ||||
|     signal::ctrl_c() | ||||
|       .await | ||||
|       .expect("failed to install Ctrl+C handler"); | ||||
|     signal::ctrl_c().await.expect("failed to install Ctrl+C handler"); | ||||
|   }; | ||||
|  | ||||
|   #[cfg(unix)] | ||||
|   | ||||
| @@ -27,7 +27,6 @@ pub fn raise_window(win: &Window) -> anyhow::Result<()> { | ||||
|     } | ||||
|     let title = win.title()?; | ||||
|     tokio::spawn(async move { | ||||
|       info!("Raising window: {}", title); | ||||
|       if let Err(err) = wmctrl_raise_window(&title).await { | ||||
|         warn!("Failed to raise window: {}", err); | ||||
|       } | ||||
|   | ||||
| @@ -5,8 +5,5 @@ fn main() { | ||||
|   println!("cargo:rerun-if-changed=src/ffi/vpn.h"); | ||||
|  | ||||
|   // Compile the vpn.c file | ||||
|   cc::Build::new() | ||||
|     .file("src/ffi/vpn.c") | ||||
|     .include("src/ffi") | ||||
|     .compile("vpn"); | ||||
|   cc::Build::new().file("src/ffi/vpn.c").include("src/ffi").compile("vpn"); | ||||
| } | ||||
|   | ||||
| @@ -15,15 +15,17 @@ pub(crate) struct ConnectOptions { | ||||
|   pub os: *const c_char, | ||||
|   pub certificate: *const c_char, | ||||
|   pub servercert: *const c_char, | ||||
|  | ||||
|   pub csd_uid: u32, | ||||
|   pub csd_wrapper: *const c_char, | ||||
|  | ||||
|   pub mtu: u32, | ||||
| } | ||||
|  | ||||
| #[link(name = "vpn")] | ||||
| extern "C" { | ||||
|   #[link_name = "vpn_connect"] | ||||
|   fn vpn_connect( | ||||
|     options: *const ConnectOptions, | ||||
|     callback: extern "C" fn(i32, *mut c_void), | ||||
|   ) -> c_int; | ||||
|   fn vpn_connect(options: *const ConnectOptions, callback: extern "C" fn(i32, *mut c_void)) -> c_int; | ||||
|  | ||||
|   #[link_name = "vpn_disconnect"] | ||||
|   fn vpn_disconnect(); | ||||
|   | ||||
| @@ -61,6 +61,9 @@ int vpn_connect(const vpn_options *options, vpn_connected_callback callback) | ||||
|     INFO("User agent: %s", options->user_agent); | ||||
|     INFO("VPNC script: %s", options->script); | ||||
|     INFO("OS: %s", options->os); | ||||
|     INFO("CSD_USER: %d", options->csd_uid); | ||||
|     INFO("CSD_WRAPPER: %s", options->csd_wrapper); | ||||
|     INFO("MTU: %d", options->mtu); | ||||
|  | ||||
|     vpninfo = openconnect_vpninfo_new(options->user_agent, validate_peer_cert, NULL, NULL, print_progress, NULL); | ||||
|  | ||||
| @@ -91,6 +94,15 @@ int vpn_connect(const vpn_options *options, vpn_connected_callback callback) | ||||
|         openconnect_set_system_trust(vpninfo, 0); | ||||
|     } | ||||
|  | ||||
|     if (options->csd_wrapper) { | ||||
|         openconnect_setup_csd(vpninfo, options->csd_uid, 1, options->csd_wrapper); | ||||
|     } | ||||
|  | ||||
|     if (options->mtu > 0) { | ||||
|         int mtu = options->mtu < 576 ? 576 : options->mtu; | ||||
|         openconnect_set_reqmtu(vpninfo, mtu); | ||||
|     } | ||||
|  | ||||
|     g_cmd_pipe_fd = openconnect_setup_cmd_pipe(vpninfo); | ||||
|     if (g_cmd_pipe_fd < 0) | ||||
|     { | ||||
| @@ -137,6 +149,9 @@ int vpn_connect(const vpn_options *options, vpn_connected_callback callback) | ||||
| void vpn_disconnect() | ||||
| { | ||||
|     char cmd = OC_CMD_CANCEL; | ||||
|  | ||||
|     INFO("Stopping VPN connection: %d", g_cmd_pipe_fd); | ||||
|  | ||||
|     if (write(g_cmd_pipe_fd, &cmd, 1) < 0) | ||||
|     { | ||||
|         ERROR("Failed to write to command pipe, VPN connection may not be stopped"); | ||||
|   | ||||
| @@ -16,6 +16,11 @@ typedef struct vpn_options | ||||
|     const char *os; | ||||
|     const char *certificate; | ||||
|     const char *servercert; | ||||
|  | ||||
|     const uid_t csd_uid; | ||||
|     const char *csd_wrapper; | ||||
|  | ||||
|     const int mtu; | ||||
| } vpn_options; | ||||
|  | ||||
| int vpn_connect(const vpn_options *options, vpn_connected_callback callback); | ||||
|   | ||||
| @@ -18,6 +18,11 @@ pub struct Vpn { | ||||
|   certificate: Option<CString>, | ||||
|   servercert: Option<CString>, | ||||
|  | ||||
|   csd_uid: u32, | ||||
|   csd_wrapper: Option<CString>, | ||||
|  | ||||
|   mtu: u32, | ||||
|  | ||||
|   callback: OnConnectedCallback, | ||||
| } | ||||
|  | ||||
| @@ -27,11 +32,7 @@ impl Vpn { | ||||
|   } | ||||
|  | ||||
|   pub fn connect(&self, on_connected: impl FnOnce() + 'static + Send + Sync) -> i32 { | ||||
|     self | ||||
|       .callback | ||||
|       .write() | ||||
|       .unwrap() | ||||
|       .replace(Box::new(on_connected)); | ||||
|     self.callback.write().unwrap().replace(Box::new(on_connected)); | ||||
|     let options = self.build_connect_options(); | ||||
|  | ||||
|     ffi::connect(&options) | ||||
| @@ -60,6 +61,11 @@ impl Vpn { | ||||
|       os: self.os.as_ptr(), | ||||
|       certificate: Self::option_to_ptr(&self.certificate), | ||||
|       servercert: Self::option_to_ptr(&self.servercert), | ||||
|  | ||||
|       csd_uid: self.csd_uid, | ||||
|       csd_wrapper: Self::option_to_ptr(&self.csd_wrapper), | ||||
|  | ||||
|       mtu: self.mtu, | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -77,6 +83,11 @@ pub struct VpnBuilder { | ||||
|   user_agent: Option<String>, | ||||
|   script: Option<String>, | ||||
|   os: Option<String>, | ||||
|  | ||||
|   csd_uid: u32, | ||||
|   csd_wrapper: Option<String>, | ||||
|  | ||||
|   mtu: u32, | ||||
| } | ||||
|  | ||||
| impl VpnBuilder { | ||||
| @@ -87,6 +98,9 @@ impl VpnBuilder { | ||||
|       user_agent: None, | ||||
|       script: None, | ||||
|       os: None, | ||||
|       csd_uid: 0, | ||||
|       csd_wrapper: None, | ||||
|       mtu: 0, | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -105,12 +119,24 @@ impl VpnBuilder { | ||||
|     self | ||||
|   } | ||||
|  | ||||
|   pub fn csd_uid(mut self, csd_uid: u32) -> Self { | ||||
|     self.csd_uid = csd_uid; | ||||
|     self | ||||
|   } | ||||
|  | ||||
|   pub fn csd_wrapper<T: Into<Option<String>>>(mut self, csd_wrapper: T) -> Self { | ||||
|     self.csd_wrapper = csd_wrapper.into(); | ||||
|     self | ||||
|   } | ||||
|  | ||||
|   pub fn mtu(mut self, mtu: u32) -> Self { | ||||
|     self.mtu = mtu; | ||||
|     self | ||||
|   } | ||||
|  | ||||
|   pub fn build(self) -> Vpn { | ||||
|     let user_agent = self.user_agent.unwrap_or_default(); | ||||
|     let script = self | ||||
|       .script | ||||
|       .or_else(find_default_vpnc_script) | ||||
|       .unwrap_or_default(); | ||||
|     let script = self.script.or_else(find_default_vpnc_script).unwrap_or_default(); | ||||
|     let os = self.os.unwrap_or("linux".to_string()); | ||||
|  | ||||
|     Vpn { | ||||
| @@ -121,6 +147,12 @@ impl VpnBuilder { | ||||
|       os: Self::to_cstring(&os), | ||||
|       certificate: None, | ||||
|       servercert: None, | ||||
|  | ||||
|       csd_uid: self.csd_uid, | ||||
|       csd_wrapper: self.csd_wrapper.as_deref().map(Self::to_cstring), | ||||
|  | ||||
|       mtu: self.mtu, | ||||
|  | ||||
|       callback: Default::default(), | ||||
|     } | ||||
|   } | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| max_width = 100 | ||||
| max_width = 120 | ||||
| hard_tabs = false | ||||
| tab_spaces = 2 | ||||
| newline_style = "Unix" | ||||
|   | ||||