Compare commits
	
		
			235 Commits
		
	
	
		
			v1.2.3
			...
			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 | ||
|  | 04a916a3e1 | ||
|  | edc13ed14d | ||
|  | dd737bc8c5 | ||
|  | 939f2bd94a | ||
|  | abffa21268 | ||
|  | 705b03c0bb | ||
|  | 7bef2ccc68 | ||
|  | bffc5d733b | ||
|  | 8ca2610550 | ||
|  | acf184134a | ||
|  | 4a3f74f1c3 | ||
|  | b39983a0f8 | ||
|  | d6fa32d95d | ||
|  | 7c299f6e68 | ||
|  | 25e8ccd07e | ||
|  | 092123b075 | ||
|  | feb2956cc1 | ||
|  | d356839859 | ||
|  | 2ff39fd14e | ||
|  | c3d300c807 | ||
|  | ef43d10a70 | ||
|  | bd73466e48 | ||
|  | cc2c0ae34e | ||
|  | 9207f7a798 | ||
|  | 2069b7fd8e | ||
|  | f552ef6204 | ||
|  | 2761f7521a | ||
|  | c3939a774b | ||
|  | 49e5242bf2 | ||
|  | 3181d37b20 | ||
|  | 6d788a5e91 | ||
|  | 74c7549444 | ||
|  | c52ccb87f1 | ||
|  | fab25848e1 | ||
|  | 75a24c89cd | ||
|  | 15a73b7dba | ||
|  | 0adeaf9c28 | ||
|  | fe64b2cd19 | ||
|  | 5788474d7e | ||
|  | 3559834762 | ||
|  | f9926b4026 | ||
|  | cb457c4b09 | ||
|  | 5ebfe9b0f4 | ||
|  | 35266dd8bf | ||
|  | bf03d375e0 | ||
|  | 6cf909e34f | ||
|  | 343a6d03c1 | ||
|  | fab8e7591e | ||
|  | 5a485197b7 | ||
|  | 7bc02a4208 | ||
|  | 3067e6e911 | ||
|  | 5db77e8404 | ||
|  | 5714063457 | ||
|  | 41f88ed2e0 | ||
|  | 4fada9bd14 | ||
|  | b57fb993ca | ||
|  | f6d06ed978 | ||
|  | cc67de3a2b | ||
|  | e2d28c83b2 | ||
|  | a489c5881b | ||
|  | 44fd2f1d3f | ||
|  | 9c9b42b87f | ||
|  | fb2b148b72 | ||
|  | 64bec9660a | ||
|  | 0619e91bf5 | ||
|  | 048aa4799f | ||
|  | db0e8b801d | ||
|  | d03bbc339e | ||
|  | 1312d54d08 | ||
|  | 39f99d9143 | ||
|  | 7a4eb0def3 | ||
|  | d9b2094edd | ||
|  | e6118af9f3 | ||
|  | 108b4be3ec | ||
|  | 65c59e47ec | ||
|  | 177da7f3a2 | ||
|  | d5cd90373b | ||
|  | ffa99d3783 | ||
|  | 4940830885 | ||
|  | ad178fe56c | ||
|  | 829298bb84 | ||
|  | 8fe717d844 | ||
|  | dffbc64ef5 | ||
|  | b99c5a8391 | ||
|  | c2f7576d10 | ||
|  | 4327235093 | ||
|  | 0699878b92 | ||
|  | e3aba11506 | ||
|  | ff58258d5c | ||
|  | 991cf25a7b | ||
|  | 02c70150ba | ||
|  | 28d8321958 | ||
|  | e1c9180cae | ||
|  | 57df34fd1e | ||
|  | 04d180e11a | ||
|  | 6d3b127569 | ||
|  | e72b25e415 | ||
|  | 37a511c24d | ||
|  | ad7db36c92 | ||
|  | 11dc5920ef | ||
|  | e6383916c7 | ||
|  | 1d9d928b26 | ||
|  | c02ad5d46d | ||
|  | 2319c7c49c | ||
|  | e0c2c14dc3 | ||
|  | 8f27c92e7b | ||
|  | 9d6ec84c14 | ||
|  | dd81ed9519 | ||
|  | 32bd713965 | ||
|  | ba92517141 | ||
|  | 0e4e082594 | ||
|  | 3e590cab7b | ||
|  | 3e0e4cff12 | ||
|  | 692df2f2c5 | ||
|  | f2b9ffddde | ||
|  | ca38925066 | ||
|  | 8591dd7e81 | ||
|  | b07880930e | ||
|  | fceb80e10e | ||
|  | d802c56d8f | ||
|  | 386f08d0e8 | ||
|  | 9e7fb17bd3 | ||
|  | 36d9753008 | ||
|  | e5b3df9cda | ||
|  | 0dd705d0c0 | ||
|  | ce2360be61 | ||
|  | b5b7033eee | ||
|  | 9e7db4eb86 | ||
|  | bc07e3d496 | ||
|  | 452fe2f189 | ||
|  | 8a65099ca7 | ||
|  | 5c97b2df7a | ||
|  | 0d4485d754 | ||
|  | 98e641e99d | ||
|  | 6fa77cdbd2 | ||
|  | 64e6487e7e | ||
|  | e8b2c1606f | ||
|  | 84f1480653 | ||
|  | 3175855122 | ||
|  | fa8b5c1528 | ||
|  | 7b9942c7e6 | ||
|  | 011a1a0dec | ||
|  | 4a53033023 | ||
|  | 9c6ea1c4b5 | ||
|  | 3369ad4c1d | ||
|  | 25c9f2291a | ||
|  | bba3bc7e4f | ||
|  | b12b692090 | ||
|  | 1300a0cc43 | ||
|  | 165080b476 | ||
|  | d6af8a1598 | ||
|  | eef92b1d31 | ||
|  | 946ead24a4 | ||
|  | 39e57c8598 | ||
|  | 4e2e423c27 | ||
|  | 732a62f1ee | ||
|  | 9f9444a72b | ||
|  | 6352e1fb2b | ||
|  | 42cae3ff26 | ||
|  | 53c8572cf6 | ||
|  | 3f6467321f | ||
|  | 563ec48c8c | ||
|  | 3787ae164c | ||
|  | 04a24c34e8 | ||
|  | fe68248b1f | ||
|  | 47013033ec | ||
|  | 05fb9a26bd | ||
|  | 96962f957c | ||
|  | b4f9cfae67 | ||
|  | c8942984a8 | ||
|  | 3907827d0e | ||
|  | f089996cdc | ||
|  | 260b557238 | ||
|  | 3495dbfe18 | ||
|  | cdf193024c | ||
|  | 76de070d78 | ||
|  | 420ae27888 | ||
|  | 6a347746cc | ||
|  | 624babb380 | ||
|  | 511b20fdcd | ||
|  | abe33c7407 | ||
|  | 99a82c8641 | ||
|  | e5d0acad3c | ||
|  | 38a1eded19 | ||
|  | 3e23e7eaae | ||
|  | cf46848e63 | ||
|  | 2e826201d2 | ||
|  | adba408dc3 | ||
|  | 5d613369ee | ||
|  | ebd3de6f63 | ||
|  | 266ab65892 | ||
|  | ccaf93ec31 | ||
|  | e08d7d7c4d | ||
|  | c14a6ad1d2 | ||
|  | d91fad089f | ||
|  | 2c1036ff10 | ||
|  | d5f9283b93 | ||
|  | fe7b96ce9b | ||
|  | 790865c060 | ||
|  | 7f056c98ce | ||
|  | 70816a9600 | ||
|  | 337a94efcd | ||
|  | cf34f9f70f | 
							
								
								
									
										62
									
								
								.devcontainer/Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,62 @@ | |||||||
|  | FROM ubuntu:18.04 | ||||||
|  |  | ||||||
|  | ARG USERNAME=vscode | ||||||
|  | ARG USER_UID=1000 | ||||||
|  | ARG USER_GID=$USER_UID | ||||||
|  |  | ||||||
|  | ENV RUSTUP_HOME=/usr/local/rustup \ | ||||||
|  |     CARGO_HOME=/usr/local/cargo \ | ||||||
|  |     PATH=/usr/local/cargo/bin:$PATH \ | ||||||
|  |     RUST_VERSION=1.75.0 | ||||||
|  |  | ||||||
|  | RUN set -eux; \ | ||||||
|  |   apt-get update; \ | ||||||
|  |   apt-get install -y --no-install-recommends \ | ||||||
|  |     sudo \ | ||||||
|  |     ca-certificates \ | ||||||
|  |     curl \ | ||||||
|  |     gnupg \ | ||||||
|  |     git \ | ||||||
|  |     less \ | ||||||
|  |     software-properties-common \ | ||||||
|  |     # Tauri dependencies | ||||||
|  |     libwebkit2gtk-4.0-dev build-essential wget libssl-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev; \ | ||||||
|  |   # Install openconnect | ||||||
|  |   add-apt-repository ppa:yuezk/globalprotect-openconnect; \ | ||||||
|  |   apt-get update; \ | ||||||
|  |   apt-get install -y openconnect libopenconnect-dev; \ | ||||||
|  |   # Create a non-root user | ||||||
|  |   groupadd --gid $USER_GID $USERNAME; \ | ||||||
|  |   useradd --uid $USER_UID --gid $USER_GID -m $USERNAME; \ | ||||||
|  |   echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME; \ | ||||||
|  |   chmod 0440 /etc/sudoers.d/$USERNAME; \ | ||||||
|  |   # Install Node.js | ||||||
|  |   mkdir -p /etc/apt/keyrings; \ | ||||||
|  |   curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg; \ | ||||||
|  |   echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_16.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list; \ | ||||||
|  |   apt-get update; \ | ||||||
|  |   apt-get install -y nodejs; \ | ||||||
|  |   corepack enable; \ | ||||||
|  |   # Install diff-so-fancy | ||||||
|  |   npm install -g diff-so-fancy; \ | ||||||
|  |   # Install Rust | ||||||
|  |   curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain $RUST_VERSION; \ | ||||||
|  |   chown -R $USERNAME:$USERNAME $RUSTUP_HOME $CARGO_HOME; \ | ||||||
|  |   rustup --version; \ | ||||||
|  |   cargo --version; \ | ||||||
|  |   rustc --version | ||||||
|  |  | ||||||
|  | USER $USERNAME | ||||||
|  |  | ||||||
|  | # Install Oh My Zsh | ||||||
|  | RUN sh -c "$(wget -O- https://github.com/deluan/zsh-in-docker/releases/download/v1.1.5/zsh-in-docker.sh)" -- \ | ||||||
|  |     -t https://github.com/denysdovhan/spaceship-prompt \ | ||||||
|  |     -a 'SPACESHIP_PROMPT_ADD_NEWLINE="false"' \ | ||||||
|  |     -a 'SPACESHIP_PROMPT_SEPARATE_LINE="false"' \ | ||||||
|  |     -p git \ | ||||||
|  |     -p https://github.com/zsh-users/zsh-autosuggestions \ | ||||||
|  |     -p https://github.com/zsh-users/zsh-completions; \ | ||||||
|  |     # Change the default shell | ||||||
|  |     sudo chsh -s /bin/zsh $USERNAME; \ | ||||||
|  |     # Change the XTERM to xterm-256color | ||||||
|  |     sed -i 's/TERM=xterm/TERM=xterm-256color/g' $HOME/.zshrc; | ||||||
							
								
								
									
										10
									
								
								.devcontainer/devcontainer.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,10 @@ | |||||||
|  | { | ||||||
|  |   "build": { | ||||||
|  |     "dockerfile": "Dockerfile" | ||||||
|  |   }, | ||||||
|  |   "runArgs": [ | ||||||
|  |     "--privileged", | ||||||
|  |     "--cap-add=NET_ADMIN", | ||||||
|  |     "--device=/dev/net/tun" | ||||||
|  |   ] | ||||||
|  | } | ||||||
							
								
								
									
										9
									
								
								.editorconfig
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,9 @@ | |||||||
|  | root = true | ||||||
|  |  | ||||||
|  | [*] | ||||||
|  | charset = utf-8 | ||||||
|  | indent_style = space | ||||||
|  | indent_size = 2 | ||||||
|  | end_of_line = lf | ||||||
|  | insert_final_newline = true | ||||||
|  | trim_trailing_whitespace = true | ||||||
							
								
								
									
										2
									
								
								.github/FUNDING.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,2 @@ | |||||||
|  | ko_fi: yuezk | ||||||
|  | custom: ["https://buymeacoffee.com/yuezk", "https://paypal.me/zongkun"] | ||||||
							
								
								
									
										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. | ||||||
							
								
								
									
										308
									
								
								.github/workflows/build.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,308 @@ | |||||||
|  | name: Build GPGUI | ||||||
|  | on: | ||||||
|  |   push: | ||||||
|  |     paths-ignore: | ||||||
|  |       - LICENSE | ||||||
|  |       - "*.md" | ||||||
|  |       - .vscode | ||||||
|  |       - .devcontainer | ||||||
|  |     branches: | ||||||
|  |       - main | ||||||
|  |     tags: | ||||||
|  |       - latest | ||||||
|  |       - v*.*.* | ||||||
|  | jobs: | ||||||
|  |   # Include arm64 if ref is a tag | ||||||
|  |   setup-matrix: | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     outputs: | ||||||
|  |       matrix: ${{ steps.set-matrix.outputs.matrix }} | ||||||
|  |     steps: | ||||||
|  |       - name: Set up matrix | ||||||
|  |         id: set-matrix | ||||||
|  |         run: | | ||||||
|  |           if [[ "${{ github.ref }}" == "refs/tags/"* ]]; then | ||||||
|  |             echo "matrix=[\"amd64\", \"arm64\"]" >> $GITHUB_OUTPUT | ||||||
|  |           else | ||||||
|  |             echo "matrix=[\"amd64\"]" >> $GITHUB_OUTPUT | ||||||
|  |           fi | ||||||
|  |  | ||||||
|  |   build-fe: | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     steps: | ||||||
|  |       - name: Checkout gpgui repo | ||||||
|  |         uses: actions/checkout@v3 | ||||||
|  |         with: | ||||||
|  |           token: ${{ secrets.GH_PAT }} | ||||||
|  |           repository: yuezk/gpgui | ||||||
|  |  | ||||||
|  |       - name: Install Node.js | ||||||
|  |         uses: actions/setup-node@v4 | ||||||
|  |         with: | ||||||
|  |           node-version: 18 | ||||||
|  |  | ||||||
|  |       - uses: pnpm/action-setup@v2 | ||||||
|  |         with: | ||||||
|  |           version: 8 | ||||||
|  |  | ||||||
|  |       - name: Install dependencies | ||||||
|  |         run: | | ||||||
|  |           cd app | ||||||
|  |           pnpm install | ||||||
|  |       - name: Build | ||||||
|  |         run: | | ||||||
|  |           cd app | ||||||
|  |           pnpm run build | ||||||
|  |  | ||||||
|  |       - name: Upload artifacts | ||||||
|  |         uses: actions/upload-artifact@v3 | ||||||
|  |         with: | ||||||
|  |           name: gpgui-fe | ||||||
|  |           path: app/dist | ||||||
|  |  | ||||||
|  |   build-tauri-amd64: | ||||||
|  |     needs: [build-fe] | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     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: Login to Docker Hub | ||||||
|  |         uses: docker/login-action@v3 | ||||||
|  |         with: | ||||||
|  |           username: ${{ secrets.DOCKER_HUB_USERNAME }} | ||||||
|  |           password: ${{ secrets.DOCKER_HUB_TOKEN }} | ||||||
|  |  | ||||||
|  |       - name: Build Tauri in Docker | ||||||
|  |         run: | | ||||||
|  |           docker run \ | ||||||
|  |             --rm \ | ||||||
|  |             -v $(pwd):/${{ github.workspace }} \ | ||||||
|  |             -w ${{ github.workspace }} \ | ||||||
|  |             -e CI=true \ | ||||||
|  |             yuezk/gpdev:main \ | ||||||
|  |             "./gpgui/scripts/build.sh" | ||||||
|  |  | ||||||
|  |       - name: Upload artifacts | ||||||
|  |         uses: actions/upload-artifact@v3 | ||||||
|  |         with: | ||||||
|  |           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, package-tarball] | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     strategy: | ||||||
|  |       matrix: | ||||||
|  |         arch: ${{ fromJson(needs.setup-matrix.outputs.matrix) }} | ||||||
|  |     steps: | ||||||
|  |       - name: Checkout gpgui repo | ||||||
|  |         uses: actions/checkout@v3 | ||||||
|  |         with: | ||||||
|  |           token: ${{ secrets.GH_PAT }} | ||||||
|  |           repository: yuezk/gpgui | ||||||
|  |           path: gpgui | ||||||
|  |  | ||||||
|  |       - name: Download package tarball | ||||||
|  |         uses: actions/download-artifact@v3 | ||||||
|  |         with: | ||||||
|  |           name: artifact-tarball | ||||||
|  |           path: gpgui/.tmp/artifact | ||||||
|  |  | ||||||
|  |       - name: Set up QEMU | ||||||
|  |         uses: docker/setup-qemu-action@v3 | ||||||
|  |         with: | ||||||
|  |           platforms: ${{ matrix.arch }} | ||||||
|  |  | ||||||
|  |       - name: Login to Docker Hub | ||||||
|  |         uses: docker/login-action@v3 | ||||||
|  |         with: | ||||||
|  |           username: ${{ secrets.DOCKER_HUB_USERNAME }} | ||||||
|  |           password: ${{ secrets.DOCKER_HUB_TOKEN }} | ||||||
|  |  | ||||||
|  |       - name: Create RPM package | ||||||
|  |         run: | | ||||||
|  |           docker run \ | ||||||
|  |             --rm \ | ||||||
|  |             -v $(pwd):/${{ github.workspace }} \ | ||||||
|  |             -w ${{ github.workspace }} \ | ||||||
|  |             --platform linux/${{ matrix.arch }} \ | ||||||
|  |             yuezk/gpdev:rpm-builder \ | ||||||
|  |             "./gpgui/scripts/build-rpm.sh" | ||||||
|  |  | ||||||
|  |       - name: Upload rpm artifacts | ||||||
|  |         uses: actions/upload-artifact@v3 | ||||||
|  |         with: | ||||||
|  |           name: artifact-${{ matrix.arch }}-rpm | ||||||
|  |           path: | | ||||||
|  |             gpgui/.tmp/artifact/*.rpm | ||||||
|  |  | ||||||
|  |   package-pkgbuild: | ||||||
|  |     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@v3 | ||||||
|  |         with: | ||||||
|  |           token: ${{ secrets.GH_PAT }} | ||||||
|  |           repository: yuezk/gpgui | ||||||
|  |           path: gpgui | ||||||
|  |  | ||||||
|  |       - name: Download artifact-${{ matrix.arch }} | ||||||
|  |         uses: actions/download-artifact@v3 | ||||||
|  |         with: | ||||||
|  |           name: artifact-${{ matrix.arch }}-tauri | ||||||
|  |           path: gpgui/.tmp/artifact | ||||||
|  |  | ||||||
|  |       - name: Set up QEMU | ||||||
|  |         uses: docker/setup-qemu-action@v3 | ||||||
|  |         with: | ||||||
|  |           platforms: ${{ matrix.arch }} | ||||||
|  |  | ||||||
|  |       - name: Login to Docker Hub | ||||||
|  |         uses: docker/login-action@v3 | ||||||
|  |         with: | ||||||
|  |           username: ${{ secrets.DOCKER_HUB_USERNAME }} | ||||||
|  |           password: ${{ secrets.DOCKER_HUB_TOKEN }} | ||||||
|  |  | ||||||
|  |       - name: Generate PKGBUILD | ||||||
|  |         run: | | ||||||
|  |           export CI_ARCH=${{ matrix.arch }} | ||||||
|  |           ./gpgui/scripts/generate-pkgbuild.sh | ||||||
|  |  | ||||||
|  |       - name: Build PKGBUILD package | ||||||
|  |         run: | | ||||||
|  |           # Build package | ||||||
|  |           docker run \ | ||||||
|  |             --rm \ | ||||||
|  |             -v $(pwd)/gpgui/.tmp/pkgbuild:/pkgbuild \ | ||||||
|  |             --platform linux/${{ matrix.arch }} \ | ||||||
|  |             yuezk/gpdev:pkgbuild | ||||||
|  |  | ||||||
|  |       - name: Upload pkgbuild artifacts | ||||||
|  |         uses: actions/upload-artifact@v3 | ||||||
|  |         with: | ||||||
|  |           name: artifact-${{ matrix.arch }}-pkgbuild | ||||||
|  |           path: | | ||||||
|  |             gpgui/.tmp/pkgbuild/*.pkg.tar.zst | ||||||
|  |  | ||||||
|  |   gh-release: | ||||||
|  |     if: startsWith(github.ref, 'refs/tags/') | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     needs: | ||||||
|  |       - package-rpm | ||||||
|  |       - package-pkgbuild | ||||||
|  |  | ||||||
|  |     steps: | ||||||
|  |       - name: Download artifact | ||||||
|  |         uses: actions/download-artifact@v3 | ||||||
|  |         with: | ||||||
|  |           path: artifact | ||||||
|  |           # pattern: artifact-* | ||||||
|  |           # merge-multiple: true | ||||||
|  |  | ||||||
|  |       # - name: Generate checksum | ||||||
|  |       #   uses: jmgilman/actions-generate-checksum@v1 | ||||||
|  |       #   with: | ||||||
|  |       #     output: checksums.txt | ||||||
|  |       #     patterns: | | ||||||
|  |       #       artifact/* | ||||||
|  |  | ||||||
|  |       - name: Create GH release | ||||||
|  |         uses: softprops/action-gh-release@v1 | ||||||
|  |         with: | ||||||
|  |           token: ${{ secrets.GH_PAT }} | ||||||
|  |           prerelease: ${{ contains(github.ref, 'latest') }} | ||||||
|  |           fail_on_unmatched_files: true | ||||||
|  |           files: | | ||||||
|  |             artifact/artifact-*/* | ||||||
							
								
								
									
										64
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,60 +1,4 @@ | |||||||
| # Binaries | .idea | ||||||
| gpclient | /target | ||||||
| gpservice | .pnpm-store | ||||||
|  | .env | ||||||
| # Auto generated DBus files |  | ||||||
| *_adaptor.cpp |  | ||||||
| *_adaptor.h |  | ||||||
|  |  | ||||||
| # C++ objects and libs |  | ||||||
| *.slo |  | ||||||
| *.lo |  | ||||||
| *.o |  | ||||||
| *.a |  | ||||||
| *.la |  | ||||||
| *.lai |  | ||||||
| *.so |  | ||||||
| *.so.* |  | ||||||
| *.dll |  | ||||||
| *.dylib |  | ||||||
|  |  | ||||||
| # Qt-es |  | ||||||
| object_script.*.Release |  | ||||||
| object_script.*.Debug |  | ||||||
| *_plugin_import.cpp |  | ||||||
| /.qmake.cache |  | ||||||
| /.qmake.stash |  | ||||||
| *.pro.user |  | ||||||
| *.pro.user.* |  | ||||||
| *.qbs.user |  | ||||||
| *.qbs.user.* |  | ||||||
| *.moc |  | ||||||
| moc_*.cpp |  | ||||||
| moc_*.h |  | ||||||
| qrc_*.cpp |  | ||||||
| ui_*.h |  | ||||||
| *.qmlc |  | ||||||
| *.jsc |  | ||||||
| Makefile* |  | ||||||
| *build-* |  | ||||||
| *.qm |  | ||||||
| *.prl |  | ||||||
|  |  | ||||||
| # Qt unit tests |  | ||||||
| target_wrapper.* |  | ||||||
|  |  | ||||||
| # QtCreator |  | ||||||
| *.autosave |  | ||||||
|  |  | ||||||
| # QtCreator Qml |  | ||||||
| *.qmlproject.user |  | ||||||
| *.qmlproject.user.* |  | ||||||
|  |  | ||||||
| # QtCreator CMake |  | ||||||
| CMakeLists.txt.user* |  | ||||||
|  |  | ||||||
| # QtCreator 4.8< compilation database  |  | ||||||
| compile_commands.json |  | ||||||
|  |  | ||||||
| # QtCreator local machine specific files for imported projects |  | ||||||
| *creator.user* |  | ||||||
|   | |||||||
							
								
								
									
										7
									
								
								.gitmodules
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,7 +0,0 @@ | |||||||
| [submodule "singleapplication"] |  | ||||||
| 	path = singleapplication |  | ||||||
| 	url = https://github.com/itay-grudev/SingleApplication.git |  | ||||||
|  |  | ||||||
| [submodule "plog"] |  | ||||||
| 	path = plog |  | ||||||
| 	url = https://github.com/SergiusTheBest/plog.git |  | ||||||
							
								
								
									
										9
									
								
								.vscode/extensions.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,9 @@ | |||||||
|  | { | ||||||
|  |     "recommendations": [ | ||||||
|  |         "rust-lang.rust-analyzer", | ||||||
|  |         "tamasfe.even-better-toml", | ||||||
|  |         "eamodio.gitlens", | ||||||
|  |         "EditorConfig.EditorConfig", | ||||||
|  |         "streetsidesoftware.code-spell-checker", | ||||||
|  |     ] | ||||||
|  | } | ||||||
							
								
								
									
										58
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,58 @@ | |||||||
|  | { | ||||||
|  |     "cSpell.words": [ | ||||||
|  |         "authcookie", | ||||||
|  |         "bincode", | ||||||
|  |         "chacha", | ||||||
|  |         "clientos", | ||||||
|  |         "cstring", | ||||||
|  |         "datetime", | ||||||
|  |         "disconnectable", | ||||||
|  |         "distro", | ||||||
|  |         "dotenv", | ||||||
|  |         "dotenvy", | ||||||
|  |         "getconfig", | ||||||
|  |         "globalprotect", | ||||||
|  |         "globalprotectcallback", | ||||||
|  |         "gpapi", | ||||||
|  |         "gpauth", | ||||||
|  |         "gpcallback", | ||||||
|  |         "gpclient", | ||||||
|  |         "gpcommon", | ||||||
|  |         "gpgui", | ||||||
|  |         "gpservice", | ||||||
|  |         "hidpi", | ||||||
|  |         "jnlp", | ||||||
|  |         "LOGNAME", | ||||||
|  |         "oneshot", | ||||||
|  |         "openconnect", | ||||||
|  |         "pkexec", | ||||||
|  |         "Prelogin", | ||||||
|  |         "prelogon", | ||||||
|  |         "prelogonuserauthcookie", | ||||||
|  |         "repr", | ||||||
|  |         "reqwest", | ||||||
|  |         "roxmltree", | ||||||
|  |         "rspc", | ||||||
|  |         "servercert", | ||||||
|  |         "specta", | ||||||
|  |         "sysinfo", | ||||||
|  |         "tanstack", | ||||||
|  |         "tauri", | ||||||
|  |         "tempfile", | ||||||
|  |         "thiserror", | ||||||
|  |         "tungstenite", | ||||||
|  |         "unistd", | ||||||
|  |         "unlisten", | ||||||
|  |         "urlencoding", | ||||||
|  |         "userauthcookie", | ||||||
|  |         "utsbuf", | ||||||
|  |         "uzers", | ||||||
|  |         "Vite", | ||||||
|  |         "vpnc", | ||||||
|  |         "vpninfo", | ||||||
|  |         "wmctrl", | ||||||
|  |         "XAUTHORITY", | ||||||
|  |         "yuezk" | ||||||
|  |     ], | ||||||
|  |     "rust-analyzer.cargo.features": "all", | ||||||
|  | } | ||||||
							
								
								
									
										5098
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										54
									
								
								Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,54 @@ | |||||||
|  | [workspace] | ||||||
|  | resolver = "2" | ||||||
|  |  | ||||||
|  | members = ["crates/*", "apps/gpclient", "apps/gpservice", "apps/gpauth"] | ||||||
|  |  | ||||||
|  | [workspace.package] | ||||||
|  | version = "2.0.0" | ||||||
|  | authors = ["Kevin Yue <k3vinyue@gmail.com>"] | ||||||
|  | homepage = "https://github.com/yuezk/GlobalProtect-openconnect" | ||||||
|  | edition = "2021" | ||||||
|  | license = "GPL-3.0" | ||||||
|  |  | ||||||
|  | [workspace.dependencies] | ||||||
|  | anyhow = "1.0" | ||||||
|  | base64 = "0.21" | ||||||
|  | clap = { version = "4.4.2", features = ["derive"] } | ||||||
|  | ctrlc = "3.4" | ||||||
|  | directories = "5.0" | ||||||
|  | env_logger = "0.10" | ||||||
|  | is_executable = "1.0" | ||||||
|  | log = "0.4" | ||||||
|  | regex = "1" | ||||||
|  | reqwest = { version = "0.11", features = ["native-tls-vendored", "json"] } | ||||||
|  | roxmltree = "0.18" | ||||||
|  | serde = { version = "1.0", features = ["derive"] } | ||||||
|  | serde_json = "1.0" | ||||||
|  | sysinfo = "0.29" | ||||||
|  | tempfile = "3.8" | ||||||
|  | tokio = { version = "1", features = ["full"] } | ||||||
|  | tokio-util = "0.7" | ||||||
|  | url = "2.4" | ||||||
|  | urlencoding = "2.1.3" | ||||||
|  | axum = "0.7" | ||||||
|  | futures = "0.3" | ||||||
|  | futures-util = "0.3" | ||||||
|  | tokio-tungstenite = "0.20.1" | ||||||
|  | specta = "=2.0.0-rc.1" | ||||||
|  | specta-macros = "=2.0.0-rc.1" | ||||||
|  | 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 | ||||||
|  | lto = true        # Enable link-time optimization | ||||||
|  | codegen-units = 1 # Reduce number of codegen units to increase optimizations | ||||||
|  | panic = 'abort'   # Abort on panic | ||||||
|  | strip = true      # Strip symbols from binary* | ||||||
| @@ -1,78 +0,0 @@ | |||||||
| TARGET = gpclient |  | ||||||
|  |  | ||||||
| QT       += core gui network websockets dbus webenginewidgets |  | ||||||
|  |  | ||||||
| greaterThan(QT_MAJOR_VERSION, 4): QT += widgets |  | ||||||
|  |  | ||||||
| CONFIG += c++11 |  | ||||||
|  |  | ||||||
| include(../singleapplication/singleapplication.pri) |  | ||||||
| DEFINES += QAPPLICATION_CLASS=QApplication |  | ||||||
|  |  | ||||||
| # The following define makes your compiler emit warnings if you use |  | ||||||
| # any Qt feature that has been marked deprecated (the exact warnings |  | ||||||
| # depend on your compiler). Please consult the documentation of the |  | ||||||
| # deprecated API in order to know how to port your code away from it. |  | ||||||
| DEFINES += QT_DEPRECATED_WARNINGS |  | ||||||
|  |  | ||||||
| INCLUDEPATH += ../plog/include |  | ||||||
|  |  | ||||||
| # You can also make your code fail to compile if it uses deprecated APIs. |  | ||||||
| # In order to do so, uncomment the following line. |  | ||||||
| # You can also select to disable deprecated APIs only up to a certain version of Qt. |  | ||||||
| #DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000    # disables all the APIs deprecated before Qt 6.0.0 |  | ||||||
| SOURCES += \ |  | ||||||
|     cdpcommand.cpp \ |  | ||||||
|     cdpcommandmanager.cpp \ |  | ||||||
|     enhancedwebview.cpp \ |  | ||||||
|     gatewayauthenticator.cpp \ |  | ||||||
|     gpgateway.cpp \ |  | ||||||
|     gphelper.cpp \ |  | ||||||
|     loginparams.cpp \ |  | ||||||
|     main.cpp \ |  | ||||||
|     normalloginwindow.cpp \ |  | ||||||
|     portalauthenticator.cpp \ |  | ||||||
|     portalconfigresponse.cpp \ |  | ||||||
|     preloginresponse.cpp \ |  | ||||||
|     samlloginwindow.cpp \ |  | ||||||
|     gpclient.cpp |  | ||||||
|  |  | ||||||
| HEADERS += \ |  | ||||||
|     cdpcommand.h \ |  | ||||||
|     cdpcommandmanager.h \ |  | ||||||
|     enhancedwebview.h \ |  | ||||||
|     gatewayauthenticator.h \ |  | ||||||
|     gpgateway.h \ |  | ||||||
|     gphelper.h \ |  | ||||||
|     loginparams.h \ |  | ||||||
|     normalloginwindow.h \ |  | ||||||
|     portalauthenticator.h \ |  | ||||||
|     portalconfigresponse.h \ |  | ||||||
|     preloginresponse.h \ |  | ||||||
|     samlloginwindow.h \ |  | ||||||
|     gpclient.h |  | ||||||
|  |  | ||||||
| FORMS += \ |  | ||||||
|     gpclient.ui \ |  | ||||||
|     normalloginwindow.ui |  | ||||||
|  |  | ||||||
| DBUS_INTERFACES += ../GPService/gpservice.xml |  | ||||||
|  |  | ||||||
| # Default rules for deployment. |  | ||||||
| target.path = /usr/bin |  | ||||||
| INSTALLS += target |  | ||||||
|  |  | ||||||
| DISTFILES += \ |  | ||||||
|     com.yuezk.qt.GPClient.svg \ |  | ||||||
|     com.yuezk.qt.gpclient.desktop |  | ||||||
|  |  | ||||||
| desktop_entry.path = /usr/share/applications/ |  | ||||||
| desktop_entry.files = com.yuezk.qt.gpclient.desktop |  | ||||||
|  |  | ||||||
| desktop_icon.path = /usr/share/pixmaps/ |  | ||||||
| desktop_icon.files = com.yuezk.qt.GPClient.svg |  | ||||||
|  |  | ||||||
| INSTALLS += desktop_entry desktop_icon |  | ||||||
|  |  | ||||||
| RESOURCES += \ |  | ||||||
|     resources.qrc |  | ||||||
| @@ -1,30 +0,0 @@ | |||||||
| #include "cdpcommand.h" |  | ||||||
|  |  | ||||||
| #include <QVariantMap> |  | ||||||
| #include <QJsonDocument> |  | ||||||
| #include <QJsonObject> |  | ||||||
|  |  | ||||||
| CDPCommand::CDPCommand(QObject *parent) : QObject(parent) |  | ||||||
| { |  | ||||||
| } |  | ||||||
|  |  | ||||||
| CDPCommand::CDPCommand(int id, QString cmd, QVariantMap& params) : |  | ||||||
|     QObject(nullptr), |  | ||||||
|     id(id), |  | ||||||
|     cmd(cmd), |  | ||||||
|     params(¶ms) |  | ||||||
| { |  | ||||||
| } |  | ||||||
|  |  | ||||||
| QByteArray CDPCommand::toJson() |  | ||||||
| { |  | ||||||
|     QVariantMap payloadMap; |  | ||||||
|     payloadMap["id"] = id; |  | ||||||
|     payloadMap["method"] = cmd; |  | ||||||
|     payloadMap["params"] = *params; |  | ||||||
|  |  | ||||||
|     QJsonObject payloadJsonObject = QJsonObject::fromVariantMap(payloadMap); |  | ||||||
|     QJsonDocument payloadJson(payloadJsonObject); |  | ||||||
|  |  | ||||||
|     return payloadJson.toJson(); |  | ||||||
| } |  | ||||||
| @@ -1,24 +0,0 @@ | |||||||
| #ifndef CDPCOMMAND_H |  | ||||||
| #define CDPCOMMAND_H |  | ||||||
|  |  | ||||||
| #include <QObject> |  | ||||||
|  |  | ||||||
| class CDPCommand : public QObject |  | ||||||
| { |  | ||||||
|     Q_OBJECT |  | ||||||
| public: |  | ||||||
|     explicit CDPCommand(QObject *parent = nullptr); |  | ||||||
|     CDPCommand(int id, QString cmd, QVariantMap& params); |  | ||||||
|  |  | ||||||
|     QByteArray toJson(); |  | ||||||
|  |  | ||||||
| signals: |  | ||||||
|     void finished(); |  | ||||||
|  |  | ||||||
| private: |  | ||||||
|     int id; |  | ||||||
|     QString cmd; |  | ||||||
|     QVariantMap *params; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| #endif // CDPCOMMAND_H |  | ||||||
| @@ -1,86 +0,0 @@ | |||||||
| #include "cdpcommandmanager.h" |  | ||||||
| #include <QVariantMap> |  | ||||||
| #include <plog/Log.h> |  | ||||||
|  |  | ||||||
| CDPCommandManager::CDPCommandManager(QObject *parent) |  | ||||||
|     : QObject(parent) |  | ||||||
|     , networkManager(new QNetworkAccessManager) |  | ||||||
|     , socket(new QWebSocket) |  | ||||||
| { |  | ||||||
|     // WebSocket setup |  | ||||||
|     QObject::connect(socket, &QWebSocket::connected, this, &CDPCommandManager::ready); |  | ||||||
|     QObject::connect(socket, &QWebSocket::textMessageReceived, this, &CDPCommandManager::onTextMessageReceived); |  | ||||||
|     QObject::connect(socket, &QWebSocket::disconnected, this, &CDPCommandManager::onSocketDisconnected); |  | ||||||
|     QObject::connect(socket, QOverload<QAbstractSocket::SocketError>::of(&QWebSocket::error), this, &CDPCommandManager::onSocketError); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| CDPCommandManager::~CDPCommandManager() |  | ||||||
| { |  | ||||||
|     delete networkManager; |  | ||||||
|     delete socket; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void CDPCommandManager::initialize(QString endpoint) |  | ||||||
| { |  | ||||||
|     QNetworkReply *reply = networkManager->get(QNetworkRequest(endpoint)); |  | ||||||
|  |  | ||||||
|     QObject::connect( |  | ||||||
|         reply, &QNetworkReply::finished, |  | ||||||
|         [reply, this]() { |  | ||||||
|             if (reply->error()) { |  | ||||||
|                 PLOGE << "CDP request error"; |  | ||||||
|                 return; |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             QJsonDocument doc = QJsonDocument::fromJson(reply->readAll()); |  | ||||||
|             QJsonArray pages = doc.array(); |  | ||||||
|             QJsonObject page = pages.first().toObject(); |  | ||||||
|             QString wsUrl = page.value("webSocketDebuggerUrl").toString(); |  | ||||||
|  |  | ||||||
|             socket->open(wsUrl); |  | ||||||
|         } |  | ||||||
|     ); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| CDPCommand *CDPCommandManager::sendCommand(QString cmd) |  | ||||||
| { |  | ||||||
|     QVariantMap emptyParams; |  | ||||||
|     return sendCommend(cmd, emptyParams); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| CDPCommand *CDPCommandManager::sendCommend(QString cmd, QVariantMap ¶ms) |  | ||||||
| { |  | ||||||
|     int id = ++commandId; |  | ||||||
|     CDPCommand *command = new CDPCommand(id, cmd, params); |  | ||||||
|     socket->sendTextMessage(command->toJson()); |  | ||||||
|     commandPool.insert(id, command); |  | ||||||
|  |  | ||||||
|     return command; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void CDPCommandManager::onTextMessageReceived(QString message) |  | ||||||
| { |  | ||||||
|     QJsonDocument responseDoc = QJsonDocument::fromJson(message.toUtf8()); |  | ||||||
|     QJsonObject response = responseDoc.object(); |  | ||||||
|  |  | ||||||
|     // Response for method |  | ||||||
|     if (response.contains("id")) { |  | ||||||
|         int id = response.value("id").toInt(); |  | ||||||
|         if (commandPool.contains(id)) { |  | ||||||
|             CDPCommand *command = commandPool.take(id); |  | ||||||
|             command->finished(); |  | ||||||
|         } |  | ||||||
|     } else { // Response for event |  | ||||||
|         emit eventReceived(response.value("method").toString(), response.value("params").toObject()); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void CDPCommandManager::onSocketDisconnected() |  | ||||||
| { |  | ||||||
|     PLOGI << "WebSocket disconnected"; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void CDPCommandManager::onSocketError(QAbstractSocket::SocketError error) |  | ||||||
| { |  | ||||||
|     PLOGE << "WebSocket error" << error; |  | ||||||
| } |  | ||||||
| @@ -1,39 +0,0 @@ | |||||||
| #ifndef CDPCOMMANDMANAGER_H |  | ||||||
| #define CDPCOMMANDMANAGER_H |  | ||||||
|  |  | ||||||
| #include "cdpcommand.h" |  | ||||||
| #include <QObject> |  | ||||||
| #include <QHash> |  | ||||||
| #include <QtWebSockets> |  | ||||||
| #include <QNetworkAccessManager> |  | ||||||
|  |  | ||||||
| class CDPCommandManager : public QObject |  | ||||||
| { |  | ||||||
|     Q_OBJECT |  | ||||||
| public: |  | ||||||
|     explicit CDPCommandManager(QObject *parent = nullptr); |  | ||||||
|     ~CDPCommandManager(); |  | ||||||
|  |  | ||||||
|     void initialize(QString endpoint); |  | ||||||
|  |  | ||||||
|     CDPCommand *sendCommand(QString cmd); |  | ||||||
|     CDPCommand *sendCommend(QString cmd, QVariantMap& params); |  | ||||||
|  |  | ||||||
| signals: |  | ||||||
|     void ready(); |  | ||||||
|     void eventReceived(QString eventName, QJsonObject params); |  | ||||||
|  |  | ||||||
| private: |  | ||||||
|     QNetworkAccessManager *networkManager; |  | ||||||
|     QWebSocket *socket; |  | ||||||
|  |  | ||||||
|     int commandId = 0; |  | ||||||
|     QHash<int, CDPCommand*> commandPool; |  | ||||||
|  |  | ||||||
| private slots: |  | ||||||
|     void onTextMessageReceived(QString message); |  | ||||||
|     void onSocketDisconnected(); |  | ||||||
|     void onSocketError(QAbstractSocket::SocketError error); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| #endif // CDPCOMMANDMANAGER_H |  | ||||||
| @@ -1,99 +0,0 @@ | |||||||
| <?xml version="1.0" encoding="UTF-8" standalone="no"?> |  | ||||||
| <!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  --> |  | ||||||
|  |  | ||||||
| <svg |  | ||||||
|    xmlns:dc="http://purl.org/dc/elements/1.1/" |  | ||||||
|    xmlns:cc="http://creativecommons.org/ns#" |  | ||||||
|    xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" |  | ||||||
|    xmlns:svg="http://www.w3.org/2000/svg" |  | ||||||
|    xmlns="http://www.w3.org/2000/svg" |  | ||||||
|    xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" |  | ||||||
|    xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" |  | ||||||
|    version="1.1" |  | ||||||
|    id="Layer_1" |  | ||||||
|    x="0px" |  | ||||||
|    y="0px" |  | ||||||
|    viewBox="0 0 96 96" |  | ||||||
|    style="enable-background:new 0 0 96 96;" |  | ||||||
|    xml:space="preserve" |  | ||||||
|    sodipodi:docname="com.yuezk.qt.GPClient.svg" |  | ||||||
|    inkscape:version="0.92.4 5da689c313, 2019-01-14"><metadata |  | ||||||
|    id="metadata14"><rdf:RDF><cc:Work |  | ||||||
|        rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type |  | ||||||
|          rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><defs |  | ||||||
|    id="defs12" /><sodipodi:namedview |  | ||||||
|    pagecolor="#ffffff" |  | ||||||
|    bordercolor="#666666" |  | ||||||
|    borderopacity="1" |  | ||||||
|    objecttolerance="10" |  | ||||||
|    gridtolerance="10" |  | ||||||
|    guidetolerance="10" |  | ||||||
|    inkscape:pageopacity="0" |  | ||||||
|    inkscape:pageshadow="2" |  | ||||||
|    inkscape:window-width="1920" |  | ||||||
|    inkscape:window-height="1006" |  | ||||||
|    id="namedview10" |  | ||||||
|    showgrid="false" |  | ||||||
|    inkscape:zoom="6.9532168" |  | ||||||
|    inkscape:cx="7.9545315" |  | ||||||
|    inkscape:cy="59.062386" |  | ||||||
|    inkscape:window-x="0" |  | ||||||
|    inkscape:window-y="0" |  | ||||||
|    inkscape:window-maximized="1" |  | ||||||
|    inkscape:current-layer="g8499" /> |  | ||||||
| <style |  | ||||||
|    type="text/css" |  | ||||||
|    id="style2"> |  | ||||||
| 	.st0{fill:#2980B9;} |  | ||||||
| 	.st1{fill:#3498DB;} |  | ||||||
| 	.st2{fill:#2ECC71;} |  | ||||||
| 	.st3{fill:#27AE60;} |  | ||||||
| </style> |  | ||||||
|  |  | ||||||
| <g |  | ||||||
|    id="g8499" |  | ||||||
|    transform="matrix(1.3407388,0,0,1.3407388,-16.409202,-16.355463)"><g |  | ||||||
|      id="XMLID_1_"> |  | ||||||
| 	<circle |  | ||||||
|    r="32.5" |  | ||||||
|    cy="48" |  | ||||||
|    cx="48" |  | ||||||
|    class="st0" |  | ||||||
|    id="XMLID_3_" |  | ||||||
|    style="fill:#2980b9" /> |  | ||||||
| 	<path |  | ||||||
|    d="m 48,15.5 v 65 C 65.9,80.5 80.5,65.7 80.5,48 80.5,30 65.9,15.5 48,15.5 Z" |  | ||||||
|    class="st1" |  | ||||||
|    id="XMLID_4_" |  | ||||||
|    inkscape:connector-curvature="0" |  | ||||||
|    style="fill:#3498db" /> |  | ||||||
| 	<path |  | ||||||
|    d="m 48,15.5 v 0.6 l 1.2,-0.3 c 0.3,-0.3 0.4,-0.3 0.6,-0.3 h -1.1 z m 7.3,0.9 c -0.1,0 0.4,0.9 1.1,1.8 0.8,1.5 1.1,2.1 1.3,2.1 0.3,-0.3 1.9,-1.2 3,-2.1 -1.7,-0.9 -3.5,-1.5 -5.4,-1.8 z m 10.3,6.2 c -0.1,0 -0.4,0 -0.9,0.6 l -0.8,0.9 0.6,0.6 c 0.3,0.6 0.8,0.9 1,1.2 0.5,0.6 0.6,0.6 0.1,1.5 -0.2,0.6 -0.3,0.9 -0.3,0.9 0.1,0.3 0.3,0.3 1.4,0.3 h 1.6 c 0.1,0 0.3,-0.6 0.4,-1.2 l 0.1,-0.9 -1.1,-0.9 c -1,-0.9 -1,-0.9 -1.4,-1.8 -0.3,-0.6 -0.6,-1.2 -0.7,-1.2 z m -3,2.4 c -0.2,0 -1.3,2.1 -1.3,2.4 0,0 0.3,0.6 0.7,0.9 0.4,0.3 0.7,0.6 0.7,0.6 0.1,0 1.2,-1.2 1.4,-1.5 C 64.2,27.1 64,26.8 63.5,26.2 63.1,25.5 62.7,25 62.6,25 Z m 9.5,1.1 0.2,0.3 c 0,0.3 -0.7,0.9 -1.4,1.5 -1.2,0.9 -1.4,1.2 -2,1.2 -0.6,0 -0.9,0.3 -1.8,0.9 -0.6,0.6 -1.2,0.9 -1.2,1.2 0,0 0.2,0.3 0.6,0.9 0.7,0.6 0.7,0.9 0.2,1.8 l -0.4,0.3 h -1.1 c -0.6,0 -1.5,0 -1.8,-0.3 -0.9,0 -0.8,0 -0.1,2.1 1,3 1.1,3.2 1.3,3.2 0.1,0 1.3,-1.2 2.8,-2.4 1.5,-1.2 2.7,-2.4 2.8,-2.4 l 0.6,0.3 c 0.4,0.3 0.5,0 1.3,-0.6 l 0.8,-0.6 0.8,0.6 c 1.9,1.2 2.2,1.5 2.3,2.4 0.2,1.5 0.3,1.8 0.5,1.8 0.1,0 1.3,-1.5 1.6,-1.8 0.1,-0.3 -0.1,-0.6 -1.1,-2.1 -0.7,-0.9 -1.1,-1.8 -1.1,-2.1 0,0 0.1,0 0.3,-0.3 0.2,0 0.4,0.3 1,0.9 -1.6,-2.3 -3.2,-4.7 -5.1,-6.8 z m 2.8,10.7 c -0.2,0 -0.9,0.9 -0.8,1.2 l 0.5,0.3 H 75 c 0.2,0 0.3,0 0.2,-0.3 C 75.1,37.4 75,36.8 74.9,36.8 Z M 72.3,38 h -2.4 l -2.4,0.3 -4.5,3.5 -4.4,3.8 v 3.5 c 0,2.1 0,3.8 0.1,3.8 0.1,0 0.7,0.9 1.5,1.5 0.8,0.9 1.5,1.5 1.8,1.8 0.4,0.3 0.5,0.3 4,0.6 l 3.4,0.3 1.6,0.9 c 0.8,0.6 1.5,1.2 1.6,1.2 0.1,0 -0.3,0.3 -0.6,0.6 l -0.6,0.6 1,1.2 c 0.5,0.6 1.3,1.5 1.7,1.8 l 0.6,0.9 v 1.7 0.9 c 3.7,-5 5.9,-11.5 6.1,-18.3 0.1,-2.7 -0.3,-5.3 -0.8,-8 l -0.6,-0.3 c -0.1,0 -0.5,0.3 -1,0.6 -0.5,0.3 -1,0.9 -1.1,0.9 -0.1,0 -0.8,-0.3 -1.8,-0.6 l -1.8,-0.6 v -0.9 c 0,-0.6 0,-0.9 -0.6,-1.5 z M 48,63.7 V 64 h 0.2 z" |  | ||||||
|    class="st2" |  | ||||||
|    id="XMLID_13_" |  | ||||||
|    inkscape:connector-curvature="0" |  | ||||||
|    style="fill:#2ecc71" /> |  | ||||||
| 	<path |  | ||||||
|    d="m 48,15.5 c -3.1,0 -6.2,0.5 -9,1.3 0.3,0.4 0.3,0.4 0.6,0.9 1.5,2.5 1.7,2.8 2.1,2.9 0.3,0 0.9,0.1 1.6,0.1 h 1.2 l 0.9,-2 0.8,-1.9 1.8,-0.6 z m -16.9,4.7 c -2.8,1.7 -5.4,3.9 -7.6,6.4 -3.8,4.3 -6.3,9.6 -7.4,15.4 0.5,0 0.9,-0.1 1.8,-0.1 2.8,0.1 2.5,0 3.4,1.4 0.5,0.8 0.6,0.8 1.4,0.8 1,0.1 0.9,0 0.5,-1.6 -0.2,-0.6 -0.3,-1.2 -0.3,-1.4 0,-0.2 0.5,-0.7 1.7,-1.6 1.9,-1.5 1.8,-1.3 1.5,-2.9 -0.1,-0.3 0.1,-0.6 0.6,-1.2 0.7,-0.7 0.7,-0.6 1.4,-0.6 h 0.7 l 0.1,-1.2 c 0.1,-0.7 0.1,-1.3 0.2,-1.3 0,0 1.9,-1.1 4.1,-2.3 2.2,-1.2 4.1,-2.2 4.2,-2.3 0.2,-0.2 -0.3,-0.8 -2.7,-3.8 -1.5,-1.9 -2.8,-3.6 -2.9,-3.7 z m -5.8,23 c -0.1,0 -0.1,0.3 -0.1,0.6 0,0.6 0,0.7 0.6,1 0.8,0.4 0.9,0.5 0.8,0.2 -0.1,-0.4 -1.2,-1.9 -1.3,-1.8 z m -3.4,2.1 -0.5,1.8 c 0.1,0.1 0.9,0.3 1.8,0.5 1,0.2 1.6,0.4 1.8,0.3 l 0.5,-1.3 z m -3.8,1 -1.1,0.6 c -0.6,0.3 -1.2,0.6 -1.4,0.6 h -0.1 c 0,1.4 0.1,2.8 0.3,4.2 l 0.6,0.4 1,-0.1 h 1 l 0.6,1.4 c 0.3,0.7 0.7,1.4 0.8,1.5 0.1,0.1 1,0.1 1.8,0.1 h 1.5 L 23,56.2 c 0,1.2 0,1.3 -0.6,2.2 -0.4,0.5 -0.6,1.2 -0.6,1.4 0,0.2 0.7,2.1 1.6,4.3 l 1.5,4 1.6,0.8 c 1.2,0.6 1.5,0.8 1.5,1 0,0.1 -0.4,2.1 -0.6,3.1 3,2.5 6.4,4.5 10.2,5.8 3.5,-3.6 6.8,-7.1 7.3,-7.6 l 0.7,-0.7 0.2,-1.9 c 0.2,-1.1 0.4,-2.1 0.4,-2.2 0,-0.1 0.5,-0.6 1,-1.2 0.5,-0.5 0.8,-1 0.8,-1.1 v -0.2 c -0.1,-0.1 -1.4,-1.1 -3,-2.2 l -3.1,-2.1 -1.1,-0.1 c -0.8,0 -1.2,0 -1.3,-0.2 C 39.4,59.2 39.2,58.5 39.1,57.7 39,56.9 38.9,56.2 38.8,56.1 38.8,56 38,56 37.1,56 36.2,56 35.4,55.9 35.3,55.8 35.2,55.7 35.2,55.1 35.1,54.3 35,53.6 34.9,53 34.8,52.9 34.7,52.8 33.7,52.7 32.5,52.6 30.5,52.5 30.1,52.5 29.1,52 l -1.2,-0.6 -1.6,0.7 -1.7,0.9 -1.8,-0.1 c -2,0 -1.9,0.2 -2.1,-1.6 C 20.6,50.7 20.6,50.1 20.5,50.1 20.4,50 20,50 19.6,49.9 L 18.9,49.7 19,49.2 c 0,-0.3 0,-1 0.1,-1.4 L 19.2,47 18.7,46.5 Z m 9.1,1.1 C 27.1,47.5 27.1,47.8 27,48 l -0.1,0.5 2.9,1.2 c 2.9,1.1 3.4,1.2 3.9,0.7 0.2,-0.2 0.1,-0.2 -0.3,-0.4 -0.3,-0.1 -1.7,-0.9 -3.2,-1.6 -1.7,-0.7 -2.9,-1.1 -3,-1 z" |  | ||||||
|    class="st3" |  | ||||||
|    id="XMLID_20_" |  | ||||||
|    inkscape:connector-curvature="0" |  | ||||||
|    style="fill:#27ae60" /> |  | ||||||
| </g><g |  | ||||||
|      transform="matrix(1.458069,0,0,1.458069,-22.631538,-19.615144)" |  | ||||||
|      id="g7664"><path |  | ||||||
|        inkscape:connector-curvature="0" |  | ||||||
|        id="XMLID_6_" |  | ||||||
|        class="st3" |  | ||||||
|        d="m 38.8,56.1 c 0,1.2 1,2.2 2.2,2.2 h 15.2 c 1.2,0 2.2,-1 2.2,-2.2 V 45.3 c 0,-1.2 -1,-2.2 -2.2,-2.2 H 40.9 c -1.2,0 -2.2,1 -2.2,2.2 v 10.8 z" |  | ||||||
|        style="fill:#f1aa27;fill-opacity:1" /><path |  | ||||||
|        style="fill:#e6e6e6" |  | ||||||
|        inkscape:connector-curvature="0" |  | ||||||
|        id="XMLID_7_" |  | ||||||
|        class="st4" |  | ||||||
|        d="m 55.5,43.1 h -3.3 v -3.7 c 0,-2.1 -1.7,-3.8 -3.8,-3.8 -2.1,0 -3.8,1.7 -3.8,3.8 v 3.8 h -3.1 v -3.8 c 0,-3.9 3.2,-7 7,-7 3.9,0 7,3.2 7,7 z" /><path |  | ||||||
|        style="fill:#e6e6e6;fill-opacity:1" |  | ||||||
|        inkscape:connector-curvature="0" |  | ||||||
|        id="XMLID_8_" |  | ||||||
|        class="st5" |  | ||||||
|        d="m 50.35,48.2 c 0,-1 -0.8,-1.8 -1.8,-1.8 -1,0 -1.8,0.8 -1.8,1.8 0,0.7 0.4,1.3 1,1.6 l -1,5.2 h 3.6 l -1,-5.2 c 0.6,-0.3 1,-0.9 1,-1.6 z" /></g></g></svg> |  | ||||||
| Before Width: | Height: | Size: 6.7 KiB | 
| @@ -1,10 +0,0 @@ | |||||||
| [Desktop Entry] |  | ||||||
|  |  | ||||||
| Type=Application |  | ||||||
| Version=1.0.0 |  | ||||||
| Name=GlobalProtect VPN |  | ||||||
| Comment=GlobalProtect VPN client, supports SAML auth mode |  | ||||||
| Exec=/usr/bin/gpclient |  | ||||||
| Icon=com.yuezk.qt.GPClient |  | ||||||
| Categories=Network;VPN;Utility;Qt; |  | ||||||
| Keywords=GlobalProtect;Openconnect;SAML;connection;VPN; |  | ||||||
| Before Width: | Height: | Size: 18 KiB | 
| @@ -1,36 +0,0 @@ | |||||||
| #include "enhancedwebview.h" |  | ||||||
| #include "cdpcommandmanager.h" |  | ||||||
|  |  | ||||||
| #include <QtWebEngineWidgets/QWebEngineView> |  | ||||||
| #include <QProcessEnvironment> |  | ||||||
|  |  | ||||||
| EnhancedWebView::EnhancedWebView(QWidget *parent) |  | ||||||
|     : QWebEngineView(parent) |  | ||||||
|     , cdp(new CDPCommandManager) |  | ||||||
| { |  | ||||||
|     QObject::connect(cdp, &CDPCommandManager::ready, this, &EnhancedWebView::onCDPReady); |  | ||||||
|     QObject::connect(cdp, &CDPCommandManager::eventReceived, this, &EnhancedWebView::onEventReceived); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| EnhancedWebView::~EnhancedWebView() |  | ||||||
| { |  | ||||||
|     delete cdp; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void EnhancedWebView::initialize() |  | ||||||
| { |  | ||||||
|     QString port = QProcessEnvironment::systemEnvironment().value(ENV_CDP_PORT); |  | ||||||
|     cdp->initialize("http://127.0.0.1:" + port + "/json"); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void EnhancedWebView::onCDPReady() |  | ||||||
| { |  | ||||||
|     cdp->sendCommand("Network.enable"); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void EnhancedWebView::onEventReceived(QString eventName, QJsonObject params) |  | ||||||
| { |  | ||||||
|     if (eventName == "Network.responseReceived") { |  | ||||||
|         emit responseReceived(params); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,29 +0,0 @@ | |||||||
| #ifndef ENHANCEDWEBVIEW_H |  | ||||||
| #define ENHANCEDWEBVIEW_H |  | ||||||
|  |  | ||||||
| #include "cdpcommandmanager.h" |  | ||||||
| #include <QtWebEngineWidgets/QWebEngineView> |  | ||||||
|  |  | ||||||
| #define ENV_CDP_PORT "QTWEBENGINE_REMOTE_DEBUGGING" |  | ||||||
|  |  | ||||||
| class EnhancedWebView : public QWebEngineView |  | ||||||
| { |  | ||||||
|     Q_OBJECT |  | ||||||
| public: |  | ||||||
|     explicit EnhancedWebView(QWidget *parent = nullptr); |  | ||||||
|     ~EnhancedWebView(); |  | ||||||
|  |  | ||||||
|     void initialize(); |  | ||||||
|  |  | ||||||
| signals: |  | ||||||
|     void responseReceived(QJsonObject params); |  | ||||||
|  |  | ||||||
| private slots: |  | ||||||
|     void onCDPReady(); |  | ||||||
|     void onEventReceived(QString eventName, QJsonObject params); |  | ||||||
|  |  | ||||||
| private: |  | ||||||
|     CDPCommandManager *cdp; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| #endif // ENHANCEDWEBVIEW_H |  | ||||||
| @@ -1,175 +0,0 @@ | |||||||
| #include "gatewayauthenticator.h" |  | ||||||
| #include "gphelper.h" |  | ||||||
| #include "loginparams.h" |  | ||||||
| #include "preloginresponse.h" |  | ||||||
|  |  | ||||||
| #include <QNetworkReply> |  | ||||||
| #include <plog/Log.h> |  | ||||||
|  |  | ||||||
| using namespace gpclient::helper; |  | ||||||
|  |  | ||||||
| GatewayAuthenticator::GatewayAuthenticator(const QString& gateway, const PortalConfigResponse& portalConfig) |  | ||||||
|     : QObject() |  | ||||||
|     , preloginUrl("https://" + gateway + "/ssl-vpn/prelogin.esp?tmp=tmp&kerberos-support=yes&ipv6-support=yes&clientVer=4100&clientos=Linux") |  | ||||||
|     , loginUrl("https://" + gateway + "/ssl-vpn/login.esp") |  | ||||||
|     , portalConfig(portalConfig) |  | ||||||
| { |  | ||||||
| } |  | ||||||
|  |  | ||||||
| GatewayAuthenticator::~GatewayAuthenticator() |  | ||||||
| { |  | ||||||
|     delete normalLoginWindow; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void GatewayAuthenticator::authenticate() |  | ||||||
| { |  | ||||||
|     PLOGI << "Start gateway authentication..."; |  | ||||||
|  |  | ||||||
|     LoginParams params; |  | ||||||
|     params.setUser(portalConfig.username()); |  | ||||||
|     params.setPassword(portalConfig.password()); |  | ||||||
|     params.setUserAuthCookie(portalConfig.userAuthCookie()); |  | ||||||
|  |  | ||||||
|     login(params); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void GatewayAuthenticator::login(const LoginParams ¶ms) |  | ||||||
| { |  | ||||||
|     PLOGI << "Trying to login the gateway at " << loginUrl << " with " << params.toUtf8(); |  | ||||||
|  |  | ||||||
|     QNetworkReply *reply = createRequest(loginUrl, params.toUtf8()); |  | ||||||
|     connect(reply, &QNetworkReply::finished, this, &GatewayAuthenticator::onLoginFinished); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void GatewayAuthenticator::onLoginFinished() |  | ||||||
| { |  | ||||||
|     QNetworkReply *reply = qobject_cast<QNetworkReply*>(sender()); |  | ||||||
|  |  | ||||||
|     if (reply->error()) { |  | ||||||
|         PLOGE << QString("Failed to login the gateway at %1, %2").arg(loginUrl).arg(reply->errorString()); |  | ||||||
|  |  | ||||||
|         if (normalLoginWindow) { |  | ||||||
|             normalLoginWindow->setProcessing(false); |  | ||||||
|             openMessageBox("Gateway login failed.", "Please check your credentials and try again."); |  | ||||||
|         } else { |  | ||||||
|             doAuth(); |  | ||||||
|         } |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if (normalLoginWindow) { |  | ||||||
|         normalLoginWindow->close(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const QUrlQuery params = gpclient::helper::parseGatewayResponse(reply->readAll()); |  | ||||||
|     emit success(params.toString()); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void GatewayAuthenticator::doAuth() |  | ||||||
| { |  | ||||||
|     PLOGI << "Perform the gateway prelogin at " << preloginUrl; |  | ||||||
|  |  | ||||||
|     QNetworkReply *reply = createRequest(preloginUrl); |  | ||||||
|     connect(reply, &QNetworkReply::finished, this, &GatewayAuthenticator::onPreloginFinished); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void GatewayAuthenticator::onPreloginFinished() |  | ||||||
| { |  | ||||||
|     QNetworkReply *reply = qobject_cast<QNetworkReply*>(sender()); |  | ||||||
|  |  | ||||||
|     if (reply->error()) { |  | ||||||
|         PLOGE << QString("Failed to prelogin the gateway at %1, %2").arg(preloginUrl).arg(reply->errorString()); |  | ||||||
|  |  | ||||||
|         emit fail("Error occurred on the gateway prelogin interface."); |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     PLOGI << "Gateway prelogin succeeded."; |  | ||||||
|  |  | ||||||
|     PreloginResponse response = PreloginResponse::parse(reply->readAll()); |  | ||||||
|  |  | ||||||
|     if (response.hasSamlAuthFields()) { |  | ||||||
|         samlAuth(response.samlMethod(), response.samlRequest(), reply->url().toString()); |  | ||||||
|     } else if (response.hasNormalAuthFields()) { |  | ||||||
|         normalAuth(response.labelUsername(), response.labelPassword(), response.authMessage()); |  | ||||||
|     } else { |  | ||||||
|         PLOGE << QString("Unknown prelogin response for %1, got %2").arg(preloginUrl).arg(QString::fromUtf8(response.rawResponse())); |  | ||||||
|         emit fail("Unknown response for gateway prelogin interface."); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     delete reply; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void GatewayAuthenticator::normalAuth(QString labelUsername, QString labelPassword, QString authMessage) |  | ||||||
| { |  | ||||||
|     PLOGI << QString("Trying to perform the normal login with %1 / %2 credentials").arg(labelUsername).arg(labelPassword); |  | ||||||
|  |  | ||||||
|     normalLoginWindow = new NormalLoginWindow; |  | ||||||
|     normalLoginWindow->setPortalAddress(gateway); |  | ||||||
|     normalLoginWindow->setAuthMessage(authMessage); |  | ||||||
|     normalLoginWindow->setUsernameLabel(labelUsername); |  | ||||||
|     normalLoginWindow->setPasswordLabel(labelPassword); |  | ||||||
|  |  | ||||||
|     // Do login |  | ||||||
|     connect(normalLoginWindow, &NormalLoginWindow::performLogin, this, &GatewayAuthenticator::onPerformNormalLogin); |  | ||||||
|     connect(normalLoginWindow, &NormalLoginWindow::rejected, this, &GatewayAuthenticator::onLoginWindowRejected); |  | ||||||
|     connect(normalLoginWindow, &NormalLoginWindow::finished, this, &GatewayAuthenticator::onLoginWindowFinished); |  | ||||||
|  |  | ||||||
|     normalLoginWindow->show(); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void GatewayAuthenticator::onPerformNormalLogin(const QString &username, const QString &password) |  | ||||||
| { |  | ||||||
|     PLOGI << "Start to perform normal login..."; |  | ||||||
|  |  | ||||||
|     normalLoginWindow->setProcessing(true); |  | ||||||
|     LoginParams params; |  | ||||||
|     params.setUser(username); |  | ||||||
|     params.setPassword(password); |  | ||||||
|     login(params); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void GatewayAuthenticator::onLoginWindowRejected() |  | ||||||
| { |  | ||||||
|     emit fail(); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void GatewayAuthenticator::onLoginWindowFinished() |  | ||||||
| { |  | ||||||
|     delete normalLoginWindow; |  | ||||||
|     normalLoginWindow = nullptr; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void GatewayAuthenticator::samlAuth(QString samlMethod, QString samlRequest, QString preloginUrl) |  | ||||||
| { |  | ||||||
|     PLOGI << "Trying to perform SAML login with saml-method " << samlMethod; |  | ||||||
|  |  | ||||||
|     SAMLLoginWindow *loginWindow = new SAMLLoginWindow; |  | ||||||
|  |  | ||||||
|     connect(loginWindow, &SAMLLoginWindow::success, this, &GatewayAuthenticator::onSAMLLoginSuccess); |  | ||||||
|     connect(loginWindow, &SAMLLoginWindow::fail, this, &GatewayAuthenticator::onSAMLLoginFail); |  | ||||||
|     connect(loginWindow, &SAMLLoginWindow::rejected, this, &GatewayAuthenticator::onLoginWindowRejected); |  | ||||||
|  |  | ||||||
|     loginWindow->login(samlMethod, samlRequest, preloginUrl); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void GatewayAuthenticator::onSAMLLoginSuccess(const QMap<QString, QString> &samlResult) |  | ||||||
| { |  | ||||||
|     if (samlResult.contains("preloginCookie")) { |  | ||||||
|         PLOGI << "SAML login succeeded, got the prelogin-cookie " << samlResult.value("preloginCookie"); |  | ||||||
|     } else { |  | ||||||
|         PLOGI << "SAML login succeeded, got the portal-userauthcookie " << samlResult.value("userAuthCookie"); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     LoginParams params; |  | ||||||
|     params.setUser(samlResult.value("username")); |  | ||||||
|     params.setPreloginCookie(samlResult.value("preloginCookie")); |  | ||||||
|     params.setUserAuthCookie(samlResult.value("userAuthCookie")); |  | ||||||
|  |  | ||||||
|     login(params); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void GatewayAuthenticator::onSAMLLoginFail(const QString msg) |  | ||||||
| { |  | ||||||
|     emit fail(msg); |  | ||||||
| } |  | ||||||
| @@ -1,46 +0,0 @@ | |||||||
| #ifndef GATEWAYAUTHENTICATOR_H |  | ||||||
| #define GATEWAYAUTHENTICATOR_H |  | ||||||
|  |  | ||||||
| #include "portalconfigresponse.h" |  | ||||||
| #include "normalloginwindow.h" |  | ||||||
| #include "loginparams.h" |  | ||||||
| #include <QObject> |  | ||||||
|  |  | ||||||
| class GatewayAuthenticator : public QObject |  | ||||||
| { |  | ||||||
|     Q_OBJECT |  | ||||||
| public: |  | ||||||
|     explicit GatewayAuthenticator(const QString& gateway, const PortalConfigResponse& portalConfig); |  | ||||||
|     ~GatewayAuthenticator(); |  | ||||||
|  |  | ||||||
|     void authenticate(); |  | ||||||
|  |  | ||||||
| signals: |  | ||||||
|     void success(const QString& authCookie); |  | ||||||
|     void fail(const QString& msg = ""); |  | ||||||
|  |  | ||||||
| private slots: |  | ||||||
|     void onLoginFinished(); |  | ||||||
|     void onPreloginFinished(); |  | ||||||
|     void onPerformNormalLogin(const QString &username, const QString &password); |  | ||||||
|     void onLoginWindowRejected(); |  | ||||||
|     void onLoginWindowFinished(); |  | ||||||
|     void onSAMLLoginSuccess(const QMap<QString, QString> &samlResult); |  | ||||||
|     void onSAMLLoginFail(const QString msg); |  | ||||||
|  |  | ||||||
| private: |  | ||||||
|     QString gateway; |  | ||||||
|     QString preloginUrl; |  | ||||||
|     QString loginUrl; |  | ||||||
|  |  | ||||||
|     const PortalConfigResponse& portalConfig; |  | ||||||
|  |  | ||||||
|     NormalLoginWindow *normalLoginWindow{ nullptr }; |  | ||||||
|  |  | ||||||
|     void login(const LoginParams& params); |  | ||||||
|     void doAuth(); |  | ||||||
|     void normalAuth(QString labelUsername, QString labelPassword, QString authMessage); |  | ||||||
|     void samlAuth(QString samlMethod, QString samlRequest, QString preloginUrl = ""); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| #endif // GATEWAYAUTHENTICATOR_H |  | ||||||
| @@ -1,421 +0,0 @@ | |||||||
| #include "gpclient.h" |  | ||||||
| #include "gphelper.h" |  | ||||||
| #include "ui_gpclient.h" |  | ||||||
| #include "portalauthenticator.h" |  | ||||||
| #include "gatewayauthenticator.h" |  | ||||||
|  |  | ||||||
| #include <plog/Log.h> |  | ||||||
| #include <QIcon> |  | ||||||
|  |  | ||||||
| using namespace gpclient::helper; |  | ||||||
|  |  | ||||||
| GPClient::GPClient(QWidget *parent) |  | ||||||
|     : QMainWindow(parent) |  | ||||||
|     , ui(new Ui::GPClient) |  | ||||||
| { |  | ||||||
|     ui->setupUi(this); |  | ||||||
|     setWindowTitle("GlobalProtect"); |  | ||||||
|     setFixedSize(width(), height()); |  | ||||||
|     gpclient::helper::moveCenter(this); |  | ||||||
|  |  | ||||||
|     // Restore portal from the previous settings |  | ||||||
|     ui->portalInput->setText(settings::get("portal", "").toString()); |  | ||||||
|  |  | ||||||
|     // DBus service setup |  | ||||||
|     vpn = new com::yuezk::qt::GPService("com.yuezk.qt.GPService", "/", QDBusConnection::systemBus(), this); |  | ||||||
|     connect(vpn, &com::yuezk::qt::GPService::connected, this, &GPClient::onVPNConnected); |  | ||||||
|     connect(vpn, &com::yuezk::qt::GPService::disconnected, this, &GPClient::onVPNDisconnected); |  | ||||||
|     connect(vpn, &com::yuezk::qt::GPService::logAvailable, this, &GPClient::onVPNLogAvailable); |  | ||||||
|  |  | ||||||
|     // Initiallize the context menu of system tray. |  | ||||||
|     initSystemTrayIcon(); |  | ||||||
|     initVpnStatus(); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| GPClient::~GPClient() |  | ||||||
| { |  | ||||||
|     delete ui; |  | ||||||
|     delete vpn; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void GPClient::on_connectButton_clicked() |  | ||||||
| { |  | ||||||
|     doConnect(); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void GPClient::on_portalInput_returnPressed() |  | ||||||
| { |  | ||||||
|     doConnect(); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void GPClient::on_portalInput_editingFinished() |  | ||||||
| { |  | ||||||
|     populateGatewayMenu(); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void GPClient::initSystemTrayIcon() |  | ||||||
| { |  | ||||||
|     systemTrayIcon = new QSystemTrayIcon(this); |  | ||||||
|     contextMenu = new QMenu("GlobalProtect", this); |  | ||||||
|  |  | ||||||
|     gatewaySwitchMenu = new QMenu("Switch Gateway", this); |  | ||||||
|     gatewaySwitchMenu->setIcon(QIcon::fromTheme("network-workgroup")); |  | ||||||
|     populateGatewayMenu(); |  | ||||||
|  |  | ||||||
|     systemTrayIcon->setIcon(QIcon(":/images/not_connected.png")); |  | ||||||
|     systemTrayIcon->setToolTip("GlobalProtect"); |  | ||||||
|     systemTrayIcon->setContextMenu(contextMenu); |  | ||||||
|  |  | ||||||
|     connect(systemTrayIcon, &QSystemTrayIcon::activated, this, &GPClient::onSystemTrayActivated); |  | ||||||
|     connect(gatewaySwitchMenu, &QMenu::triggered, this, &GPClient::onGatewayChanged); |  | ||||||
|  |  | ||||||
|     openAction = contextMenu->addAction(QIcon::fromTheme("window-new"), "Open", this, &GPClient::activiate); |  | ||||||
|     connectAction = contextMenu->addAction(QIcon::fromTheme("preferences-system-network"), "Connect", this, &GPClient::doConnect); |  | ||||||
|     contextMenu->addMenu(gatewaySwitchMenu); |  | ||||||
|     contextMenu->addSeparator(); |  | ||||||
|     clearAction = contextMenu->addAction(QIcon::fromTheme("edit-clear"), "Reset Settings", this, &GPClient::clearSettings); |  | ||||||
|     quitAction = contextMenu->addAction(QIcon::fromTheme("application-exit"), "Quit", this, &GPClient::quit); |  | ||||||
|  |  | ||||||
|     systemTrayIcon->show(); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void GPClient::initVpnStatus() { |  | ||||||
|     int status = vpn->status(); |  | ||||||
|  |  | ||||||
|     if (status == 1) { |  | ||||||
|         ui->statusLabel->setText("Connecting..."); |  | ||||||
|         updateConnectionStatus(VpnStatus::pending); |  | ||||||
|     } else if (status == 2) { |  | ||||||
|         updateConnectionStatus(VpnStatus::connected); |  | ||||||
|     } else if (status == 3) { |  | ||||||
|         ui->statusLabel->setText("Disconnecting..."); |  | ||||||
|         updateConnectionStatus(VpnStatus::pending); |  | ||||||
|     } else { |  | ||||||
|         updateConnectionStatus(VpnStatus::disconnected); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void GPClient::populateGatewayMenu() |  | ||||||
| { |  | ||||||
|     PLOGI << "Populating the Switch Gateway menu..."; |  | ||||||
|  |  | ||||||
|     const QList<GPGateway> gateways = allGateways(); |  | ||||||
|     gatewaySwitchMenu->clear(); |  | ||||||
|  |  | ||||||
|     if (gateways.isEmpty()) { |  | ||||||
|         gatewaySwitchMenu->addAction("<None>")->setData(-1); |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const QString currentGatewayName = currentGateway().name(); |  | ||||||
|     for (int i = 0; i < gateways.length(); i++) { |  | ||||||
|         const GPGateway g = gateways.at(i); |  | ||||||
|         QString iconImage = ":/images/radio_unselected.png"; |  | ||||||
|         if (g.name() == currentGatewayName) { |  | ||||||
|             iconImage = ":/images/radio_selected.png"; |  | ||||||
|         } |  | ||||||
|         gatewaySwitchMenu->addAction(QIcon(iconImage), g.name())->setData(i); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void GPClient::updateConnectionStatus(const GPClient::VpnStatus &status) |  | ||||||
| { |  | ||||||
|     switch (status) { |  | ||||||
|         case VpnStatus::disconnected: |  | ||||||
|             ui->statusLabel->setText("Not Connected"); |  | ||||||
|             ui->statusImage->setStyleSheet("image: url(:/images/not_connected.png); padding: 15;"); |  | ||||||
|             ui->connectButton->setText("Connect"); |  | ||||||
|             ui->connectButton->setDisabled(false); |  | ||||||
|             ui->portalInput->setReadOnly(false); |  | ||||||
|  |  | ||||||
|             systemTrayIcon->setIcon(QIcon{ ":/images/not_connected.png" }); |  | ||||||
|             connectAction->setEnabled(true); |  | ||||||
|             connectAction->setText("Connect"); |  | ||||||
|             gatewaySwitchMenu->setEnabled(true); |  | ||||||
|             clearAction->setEnabled(true); |  | ||||||
|             break; |  | ||||||
|         case VpnStatus::pending: |  | ||||||
|             ui->statusImage->setStyleSheet("image: url(:/images/pending.png); padding: 15;"); |  | ||||||
|             ui->connectButton->setDisabled(true); |  | ||||||
|             ui->portalInput->setReadOnly(true); |  | ||||||
|  |  | ||||||
|             systemTrayIcon->setIcon(QIcon{ ":/images/pending.png" }); |  | ||||||
|             connectAction->setEnabled(false); |  | ||||||
|             gatewaySwitchMenu->setEnabled(false); |  | ||||||
|             clearAction->setEnabled(false); |  | ||||||
|             break; |  | ||||||
|         case VpnStatus::connected: |  | ||||||
|             ui->statusLabel->setText("Connected"); |  | ||||||
|             ui->statusImage->setStyleSheet("image: url(:/images/connected.png); padding: 15;"); |  | ||||||
|             ui->connectButton->setText("Disconnect"); |  | ||||||
|             ui->connectButton->setDisabled(false); |  | ||||||
|             ui->portalInput->setReadOnly(true); |  | ||||||
|  |  | ||||||
|             systemTrayIcon->setIcon(QIcon{ ":/images/connected.png" }); |  | ||||||
|             connectAction->setEnabled(true); |  | ||||||
|             connectAction->setText("Disconnect"); |  | ||||||
|             gatewaySwitchMenu->setEnabled(true); |  | ||||||
|             clearAction->setEnabled(false); |  | ||||||
|             break; |  | ||||||
|         default: |  | ||||||
|             break; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void GPClient::onSystemTrayActivated(QSystemTrayIcon::ActivationReason reason) |  | ||||||
| { |  | ||||||
|     switch (reason) { |  | ||||||
|         case QSystemTrayIcon::Trigger: |  | ||||||
|         case QSystemTrayIcon::DoubleClick: |  | ||||||
|             this->activiate(); |  | ||||||
|             break; |  | ||||||
|         default: |  | ||||||
|             break; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void GPClient::onGatewayChanged(QAction *action) |  | ||||||
| { |  | ||||||
|     const int index = action->data().toInt(); |  | ||||||
|  |  | ||||||
|     if (index == -1) { |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const GPGateway g = allGateways().at(index); |  | ||||||
|  |  | ||||||
|     // If the selected gateway is the same as the current gateway |  | ||||||
|     if (g.name() == currentGateway().name()) { |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     setCurrentGateway(g); |  | ||||||
|  |  | ||||||
|     if (connected()) { |  | ||||||
|         ui->statusLabel->setText("Switching Gateway..."); |  | ||||||
|         ui->connectButton->setEnabled(false); |  | ||||||
|  |  | ||||||
|         vpn->disconnect(); |  | ||||||
|         isSwitchingGateway = true; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void GPClient::doConnect() |  | ||||||
| { |  | ||||||
|     PLOGI << "Start connecting..."; |  | ||||||
|  |  | ||||||
|     const QString btnText = ui->connectButton->text(); |  | ||||||
|     const QString portal = this->portal(); |  | ||||||
|  |  | ||||||
|     // Display the main window if portal is empty |  | ||||||
|     if (portal.isEmpty()) { |  | ||||||
|         activiate(); |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if (btnText.endsWith("Connect")) { |  | ||||||
|         settings::save("portal", portal); |  | ||||||
|  |  | ||||||
|         // Login to the previously saved gateway |  | ||||||
|         if (!currentGateway().name().isEmpty()) { |  | ||||||
|             PLOGI << "Start gateway login using the previously saved gateway..."; |  | ||||||
|             isQuickConnect = true; |  | ||||||
|             gatewayLogin(); |  | ||||||
|         } else { |  | ||||||
|             // Perform the portal login |  | ||||||
|             PLOGI << "Start portal login..."; |  | ||||||
|             portalLogin(); |  | ||||||
|         } |  | ||||||
|     } else { |  | ||||||
|         PLOGI << "Start disconnecting the VPN..."; |  | ||||||
|  |  | ||||||
|         ui->statusLabel->setText("Disconnecting..."); |  | ||||||
|         updateConnectionStatus(VpnStatus::pending); |  | ||||||
|         vpn->disconnect(); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Login to the portal interface to get the portal config and preferred gateway |  | ||||||
| void GPClient::portalLogin() |  | ||||||
| { |  | ||||||
|     PortalAuthenticator *portalAuth = new PortalAuthenticator(portal()); |  | ||||||
|  |  | ||||||
|     connect(portalAuth, &PortalAuthenticator::success, this, &GPClient::onPortalSuccess); |  | ||||||
|     // Prelogin failed on the portal interface, try to treat the portal as a gateway interface |  | ||||||
|     connect(portalAuth, &PortalAuthenticator::preloginFailed, this, &GPClient::onPortalPreloginFail); |  | ||||||
|     // Portal login failed |  | ||||||
|     connect(portalAuth, &PortalAuthenticator::fail, this, &GPClient::onPortalFail); |  | ||||||
|  |  | ||||||
|     ui->statusLabel->setText("Authenticating..."); |  | ||||||
|     updateConnectionStatus(VpnStatus::pending); |  | ||||||
|     portalAuth->authenticate(); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void GPClient::onPortalSuccess(const PortalConfigResponse portalConfig, const GPGateway gateway, QList<GPGateway> allGateways) |  | ||||||
| { |  | ||||||
|     PLOGI << "Portal authentication succeeded."; |  | ||||||
|  |  | ||||||
|     this->portalConfig = portalConfig; |  | ||||||
|  |  | ||||||
|     setAllGateways(allGateways); |  | ||||||
|     setCurrentGateway(gateway); |  | ||||||
|  |  | ||||||
|     gatewayLogin(); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void GPClient::onPortalPreloginFail() |  | ||||||
| { |  | ||||||
|     PLOGI << "Portal prelogin failed, try to preform login on the the gateway interface..."; |  | ||||||
|  |  | ||||||
|     // Treat the portal input as the gateway address |  | ||||||
|     GPGateway g; |  | ||||||
|     g.setName(portal()); |  | ||||||
|     g.setAddress(portal()); |  | ||||||
|  |  | ||||||
|     QList<GPGateway> gateways; |  | ||||||
|     gateways.append(g); |  | ||||||
|  |  | ||||||
|     setAllGateways(gateways); |  | ||||||
|     setCurrentGateway(g); |  | ||||||
|  |  | ||||||
|     gatewayLogin(); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void GPClient::onPortalFail(const QString &msg) |  | ||||||
| { |  | ||||||
|     if (!msg.isEmpty()) { |  | ||||||
|         openMessageBox("Portal authentication failed.", msg); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     updateConnectionStatus(VpnStatus::disconnected); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Login to the gateway |  | ||||||
| void GPClient::gatewayLogin() |  | ||||||
| { |  | ||||||
|     PLOGI << "Performing gateway login..."; |  | ||||||
|  |  | ||||||
|     GatewayAuthenticator *gatewayAuth = new GatewayAuthenticator(currentGateway().address(), portalConfig); |  | ||||||
|  |  | ||||||
|     connect(gatewayAuth, &GatewayAuthenticator::success, this, &GPClient::onGatewaySuccess); |  | ||||||
|     connect(gatewayAuth, &GatewayAuthenticator::fail, this, &GPClient::onGatewayFail); |  | ||||||
|  |  | ||||||
|     ui->statusLabel->setText("Authenticating..."); |  | ||||||
|     updateConnectionStatus(VpnStatus::pending); |  | ||||||
|     gatewayAuth->authenticate(); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void GPClient::onGatewaySuccess(const QString &authCookie) |  | ||||||
| { |  | ||||||
|     PLOGI << "Gateway login succeeded, got the cookie " << authCookie; |  | ||||||
|  |  | ||||||
|     isQuickConnect = false; |  | ||||||
|     vpn->connect(currentGateway().address(), portalConfig.username(), authCookie); |  | ||||||
|     ui->statusLabel->setText("Connecting..."); |  | ||||||
|     updateConnectionStatus(VpnStatus::pending); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void GPClient::onGatewayFail(const QString &msg) |  | ||||||
| { |  | ||||||
|     // If the quick connect on gateway failed, perform the portal login |  | ||||||
|     if (isQuickConnect && !msg.isEmpty()) { |  | ||||||
|         isQuickConnect = false; |  | ||||||
|         portalLogin(); |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if (!msg.isEmpty()) { |  | ||||||
|         openMessageBox("Gateway authentication failed.", msg); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     updateConnectionStatus(VpnStatus::disconnected); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void GPClient::activiate() |  | ||||||
| { |  | ||||||
|     activateWindow(); |  | ||||||
|     showNormal(); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| QString GPClient::portal() const |  | ||||||
| { |  | ||||||
|     const QString input = ui->portalInput->text().trimmed(); |  | ||||||
|  |  | ||||||
|     if (input.startsWith("http")) { |  | ||||||
|         return QUrl(input).authority(); |  | ||||||
|     } |  | ||||||
|     return input; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| bool GPClient::connected() const |  | ||||||
| { |  | ||||||
|     const QString statusText = ui->statusLabel->text(); |  | ||||||
|     return statusText.contains("Connected") && !statusText.contains("Not"); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| QList<GPGateway> GPClient::allGateways() const |  | ||||||
| { |  | ||||||
|     const QString gatewaysJson = settings::get(portal() + "_gateways").toString(); |  | ||||||
|     return GPGateway::fromJson(gatewaysJson); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void GPClient::setAllGateways(QList<GPGateway> gateways) |  | ||||||
| { |  | ||||||
|     PLOGI << "Updating all the gateways..."; |  | ||||||
|  |  | ||||||
|     settings::save(portal() + "_gateways", GPGateway::serialize(gateways)); |  | ||||||
|     populateGatewayMenu(); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| GPGateway GPClient::currentGateway() const |  | ||||||
| { |  | ||||||
|     const QString selectedGateway = settings::get(portal() + "_selectedGateway").toString(); |  | ||||||
|  |  | ||||||
|     for (auto g : allGateways()) { |  | ||||||
|         if (g.name() == selectedGateway) { |  | ||||||
|             return g; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|     return GPGateway{}; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void GPClient::setCurrentGateway(const GPGateway gateway) |  | ||||||
| { |  | ||||||
|     PLOGI << "Updating the current gateway to " << gateway.name(); |  | ||||||
|  |  | ||||||
|     settings::save(portal() + "_selectedGateway", gateway.name()); |  | ||||||
|     populateGatewayMenu(); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void GPClient::clearSettings() |  | ||||||
| { |  | ||||||
|     settings::clear(); |  | ||||||
|     populateGatewayMenu(); |  | ||||||
|     ui->portalInput->clear(); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void GPClient::quit() |  | ||||||
| { |  | ||||||
|     vpn->disconnect(); |  | ||||||
|     QApplication::quit(); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void GPClient::onVPNConnected() |  | ||||||
| { |  | ||||||
|     updateConnectionStatus(VpnStatus::connected); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void GPClient::onVPNDisconnected() |  | ||||||
| { |  | ||||||
|     updateConnectionStatus(VpnStatus::disconnected); |  | ||||||
|  |  | ||||||
|     if (isSwitchingGateway) { |  | ||||||
|         gatewayLogin(); |  | ||||||
|         isSwitchingGateway = false; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void GPClient::onVPNLogAvailable(QString log) |  | ||||||
| { |  | ||||||
|     PLOGI << log; |  | ||||||
| } |  | ||||||
| @@ -1,89 +0,0 @@ | |||||||
| #ifndef GPCLIENT_H |  | ||||||
| #define GPCLIENT_H |  | ||||||
|  |  | ||||||
| #include "gpservice_interface.h" |  | ||||||
| #include "portalconfigresponse.h" |  | ||||||
|  |  | ||||||
| #include <QMainWindow> |  | ||||||
| #include <QSystemTrayIcon> |  | ||||||
| #include <QMenu> |  | ||||||
|  |  | ||||||
| QT_BEGIN_NAMESPACE |  | ||||||
| namespace Ui { class GPClient; } |  | ||||||
| QT_END_NAMESPACE |  | ||||||
|  |  | ||||||
| class GPClient : public QMainWindow |  | ||||||
| { |  | ||||||
|     Q_OBJECT |  | ||||||
|  |  | ||||||
| public: |  | ||||||
|     GPClient(QWidget *parent = nullptr); |  | ||||||
|     ~GPClient(); |  | ||||||
|  |  | ||||||
|     void activiate(); |  | ||||||
|  |  | ||||||
| private slots: |  | ||||||
|     void on_connectButton_clicked(); |  | ||||||
|     void on_portalInput_returnPressed(); |  | ||||||
|     void on_portalInput_editingFinished(); |  | ||||||
|  |  | ||||||
|     void onSystemTrayActivated(QSystemTrayIcon::ActivationReason reason); |  | ||||||
|     void onGatewayChanged(QAction *action); |  | ||||||
|  |  | ||||||
|     void onPortalSuccess(const PortalConfigResponse portalConfig, const GPGateway gateway, QList<GPGateway> allGateways); |  | ||||||
|     void onPortalPreloginFail(); |  | ||||||
|     void onPortalFail(const QString &msg); |  | ||||||
|  |  | ||||||
|     void onGatewaySuccess(const QString &authCookie); |  | ||||||
|     void onGatewayFail(const QString &msg); |  | ||||||
|  |  | ||||||
|     void onVPNConnected(); |  | ||||||
|     void onVPNDisconnected(); |  | ||||||
|     void onVPNLogAvailable(QString log); |  | ||||||
|  |  | ||||||
| private: |  | ||||||
|     enum class VpnStatus |  | ||||||
|     { |  | ||||||
|         disconnected, |  | ||||||
|         pending, |  | ||||||
|         connected |  | ||||||
|     }; |  | ||||||
|  |  | ||||||
|     Ui::GPClient *ui; |  | ||||||
|     com::yuezk::qt::GPService *vpn; |  | ||||||
|  |  | ||||||
|     QSystemTrayIcon *systemTrayIcon; |  | ||||||
|     QMenu *contextMenu; |  | ||||||
|     QAction *openAction; |  | ||||||
|     QAction *connectAction; |  | ||||||
|  |  | ||||||
|     QMenu *gatewaySwitchMenu; |  | ||||||
|     QAction *clearAction; |  | ||||||
|     QAction *quitAction; |  | ||||||
|  |  | ||||||
|     bool isQuickConnect { false }; |  | ||||||
|     bool isSwitchingGateway { false }; |  | ||||||
|     PortalConfigResponse portalConfig; |  | ||||||
|  |  | ||||||
|     void initSystemTrayIcon(); |  | ||||||
|     void initVpnStatus(); |  | ||||||
|     void populateGatewayMenu(); |  | ||||||
|     void updateConnectionStatus(const VpnStatus &status); |  | ||||||
|  |  | ||||||
|     void doConnect(); |  | ||||||
|     void portalLogin(); |  | ||||||
|     void gatewayLogin(); |  | ||||||
|  |  | ||||||
|     QString portal() const; |  | ||||||
|     bool connected() const; |  | ||||||
|  |  | ||||||
|     QList<GPGateway> allGateways() const; |  | ||||||
|     void setAllGateways(QList<GPGateway> gateways); |  | ||||||
|  |  | ||||||
|     GPGateway currentGateway() const; |  | ||||||
|     void setCurrentGateway(const GPGateway gateway); |  | ||||||
|  |  | ||||||
|     void clearSettings(); |  | ||||||
|     void quit(); |  | ||||||
| }; |  | ||||||
| #endif // GPCLIENT_H |  | ||||||
| @@ -1,133 +0,0 @@ | |||||||
| <?xml version="1.0" encoding="UTF-8"?> |  | ||||||
| <ui version="4.0"> |  | ||||||
|  <class>GPClient</class> |  | ||||||
|  <widget class="QMainWindow" name="GPClient"> |  | ||||||
|   <property name="geometry"> |  | ||||||
|    <rect> |  | ||||||
|     <x>0</x> |  | ||||||
|     <y>0</y> |  | ||||||
|     <width>260</width> |  | ||||||
|     <height>338</height> |  | ||||||
|    </rect> |  | ||||||
|   </property> |  | ||||||
|   <property name="windowTitle"> |  | ||||||
|    <string>GlobalProtect OpenConnect</string> |  | ||||||
|   </property> |  | ||||||
|   <property name="windowIcon"> |  | ||||||
|    <iconset resource="resources.qrc"> |  | ||||||
|     <normaloff>:/images/logo.svg</normaloff>:/images/logo.svg</iconset> |  | ||||||
|   </property> |  | ||||||
|   <property name="styleSheet"> |  | ||||||
|    <string notr="true"/> |  | ||||||
|   </property> |  | ||||||
|   <property name="iconSize"> |  | ||||||
|    <size> |  | ||||||
|     <width>22</width> |  | ||||||
|     <height>22</height> |  | ||||||
|    </size> |  | ||||||
|   </property> |  | ||||||
|   <widget class="QWidget" name="centralwidget"> |  | ||||||
|    <property name="sizePolicy"> |  | ||||||
|     <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> |  | ||||||
|      <horstretch>0</horstretch> |  | ||||||
|      <verstretch>0</verstretch> |  | ||||||
|     </sizepolicy> |  | ||||||
|    </property> |  | ||||||
|    <property name="layoutDirection"> |  | ||||||
|     <enum>Qt::LeftToRight</enum> |  | ||||||
|    </property> |  | ||||||
|    <layout class="QVBoxLayout" name="verticalLayout_3" stretch="1,0"> |  | ||||||
|     <property name="leftMargin"> |  | ||||||
|      <number>15</number> |  | ||||||
|     </property> |  | ||||||
|     <property name="topMargin"> |  | ||||||
|      <number>15</number> |  | ||||||
|     </property> |  | ||||||
|     <property name="rightMargin"> |  | ||||||
|      <number>15</number> |  | ||||||
|     </property> |  | ||||||
|     <property name="bottomMargin"> |  | ||||||
|      <number>15</number> |  | ||||||
|     </property> |  | ||||||
|     <item> |  | ||||||
|      <layout class="QVBoxLayout" name="verticalLayout" stretch="1,0"> |  | ||||||
|       <property name="bottomMargin"> |  | ||||||
|        <number>15</number> |  | ||||||
|       </property> |  | ||||||
|       <item> |  | ||||||
|        <widget class="QLabel" name="statusImage"> |  | ||||||
|         <property name="styleSheet"> |  | ||||||
|          <string notr="true">#statusImage { |  | ||||||
| 	image: url(:/images/not_connected.png); |  | ||||||
| 	padding: 15 |  | ||||||
| }</string> |  | ||||||
|         </property> |  | ||||||
|         <property name="text"> |  | ||||||
|          <string/> |  | ||||||
|         </property> |  | ||||||
|        </widget> |  | ||||||
|       </item> |  | ||||||
|       <item> |  | ||||||
|        <widget class="QLabel" name="statusLabel"> |  | ||||||
|         <property name="font"> |  | ||||||
|          <font> |  | ||||||
|           <pointsize>14</pointsize> |  | ||||||
|           <weight>50</weight> |  | ||||||
|           <bold>false</bold> |  | ||||||
|           <underline>false</underline> |  | ||||||
|          </font> |  | ||||||
|         </property> |  | ||||||
|         <property name="text"> |  | ||||||
|          <string>Not Connected</string> |  | ||||||
|         </property> |  | ||||||
|         <property name="alignment"> |  | ||||||
|          <set>Qt::AlignCenter</set> |  | ||||||
|         </property> |  | ||||||
|        </widget> |  | ||||||
|       </item> |  | ||||||
|      </layout> |  | ||||||
|     </item> |  | ||||||
|     <item> |  | ||||||
|      <layout class="QVBoxLayout" name="verticalLayout_2"> |  | ||||||
|       <property name="bottomMargin"> |  | ||||||
|        <number>0</number> |  | ||||||
|       </property> |  | ||||||
|       <item> |  | ||||||
|        <widget class="QLineEdit" name="portalInput"> |  | ||||||
|         <property name="text"> |  | ||||||
|          <string/> |  | ||||||
|         </property> |  | ||||||
|         <property name="placeholderText"> |  | ||||||
|          <string>Please enter your portal address</string> |  | ||||||
|         </property> |  | ||||||
|        </widget> |  | ||||||
|       </item> |  | ||||||
|       <item> |  | ||||||
|        <widget class="QPushButton" name="connectButton"> |  | ||||||
|         <property name="sizePolicy"> |  | ||||||
|          <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> |  | ||||||
|           <horstretch>0</horstretch> |  | ||||||
|           <verstretch>0</verstretch> |  | ||||||
|          </sizepolicy> |  | ||||||
|         </property> |  | ||||||
|         <property name="text"> |  | ||||||
|          <string>Connect</string> |  | ||||||
|         </property> |  | ||||||
|         <property name="autoDefault"> |  | ||||||
|          <bool>true</bool> |  | ||||||
|         </property> |  | ||||||
|         <property name="default"> |  | ||||||
|          <bool>false</bool> |  | ||||||
|         </property> |  | ||||||
|        </widget> |  | ||||||
|       </item> |  | ||||||
|      </layout> |  | ||||||
|     </item> |  | ||||||
|    </layout> |  | ||||||
|   </widget> |  | ||||||
|  </widget> |  | ||||||
|  <resources> |  | ||||||
|   <include location="resources.qrc"/> |  | ||||||
|  </resources> |  | ||||||
|  <connections/> |  | ||||||
| </ui> |  | ||||||
| @@ -1,97 +0,0 @@ | |||||||
| #include "gpgateway.h" |  | ||||||
|  |  | ||||||
| #include <QJsonObject> |  | ||||||
| #include <QJsonDocument> |  | ||||||
| #include <QJsonArray> |  | ||||||
|  |  | ||||||
| GPGateway::GPGateway() |  | ||||||
| { |  | ||||||
| } |  | ||||||
|  |  | ||||||
| QString GPGateway::name() const |  | ||||||
| { |  | ||||||
|     return _name; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| QString GPGateway::address() const |  | ||||||
| { |  | ||||||
|     return _address; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void GPGateway::setName(const QString &name) |  | ||||||
| { |  | ||||||
|     _name = name; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void GPGateway::setAddress(const QString &address) |  | ||||||
| { |  | ||||||
|     _address = address; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void GPGateway::setPriorityRules(const QMap<QString, int> &priorityRules) |  | ||||||
| { |  | ||||||
|     _priorityRules = priorityRules; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| int GPGateway::priorityOf(QString ruleName) const |  | ||||||
| { |  | ||||||
|     if (_priorityRules.contains(ruleName)) { |  | ||||||
|         return _priorityRules.value(ruleName); |  | ||||||
|     } |  | ||||||
|     return 0; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| QJsonObject GPGateway::toJsonObject() const |  | ||||||
| { |  | ||||||
|     QJsonObject obj; |  | ||||||
|     obj.insert("name", name()); |  | ||||||
|     obj.insert("address", address()); |  | ||||||
|  |  | ||||||
|     return obj; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| QString GPGateway::toString() const |  | ||||||
| { |  | ||||||
|     QJsonDocument jsonDoc{ toJsonObject() }; |  | ||||||
|     return QString::fromUtf8(jsonDoc.toJson()); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| QString GPGateway::serialize(QList<GPGateway> &gateways) |  | ||||||
| { |  | ||||||
|     QJsonArray arr; |  | ||||||
|  |  | ||||||
|     for (auto g : gateways) { |  | ||||||
|         arr.append(g.toJsonObject()); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     QJsonDocument jsonDoc{ arr }; |  | ||||||
|     return QString::fromUtf8(jsonDoc.toJson()); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| QList<GPGateway> GPGateway::fromJson(const QString &jsonString) |  | ||||||
| { |  | ||||||
|     QList<GPGateway> gateways; |  | ||||||
|  |  | ||||||
|     if (jsonString.isEmpty()) { |  | ||||||
|         return gateways; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonString.toUtf8()); |  | ||||||
|  |  | ||||||
|     for (auto item : jsonDoc.array()) { |  | ||||||
|         GPGateway g = GPGateway::fromJsonObject(item.toObject()); |  | ||||||
|         gateways.append(g); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return gateways; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| GPGateway GPGateway::fromJsonObject(const QJsonObject &jsonObj) |  | ||||||
| { |  | ||||||
|     GPGateway g; |  | ||||||
|  |  | ||||||
|     g.setName(jsonObj.value("name").toString()); |  | ||||||
|     g.setAddress(jsonObj.value("address").toString()); |  | ||||||
|  |  | ||||||
|     return g; |  | ||||||
| } |  | ||||||
| @@ -1,33 +0,0 @@ | |||||||
| #ifndef GPGATEWAY_H |  | ||||||
| #define GPGATEWAY_H |  | ||||||
|  |  | ||||||
| #include <QString> |  | ||||||
| #include <QMap> |  | ||||||
| #include <QJsonObject> |  | ||||||
|  |  | ||||||
| class GPGateway |  | ||||||
| { |  | ||||||
| public: |  | ||||||
|     GPGateway(); |  | ||||||
|  |  | ||||||
|     QString name() const; |  | ||||||
|     QString address() const; |  | ||||||
|  |  | ||||||
|     void setName(const QString &name); |  | ||||||
|     void setAddress(const QString &address); |  | ||||||
|     void setPriorityRules(const QMap<QString, int> &priorityRules); |  | ||||||
|     int priorityOf(QString ruleName) const; |  | ||||||
|     QJsonObject toJsonObject() const; |  | ||||||
|     QString toString() const; |  | ||||||
|  |  | ||||||
|     static QString serialize(QList<GPGateway> &gateways); |  | ||||||
|     static QList<GPGateway> fromJson(const QString &jsonString); |  | ||||||
|     static GPGateway fromJsonObject(const QJsonObject &jsonObj); |  | ||||||
|  |  | ||||||
| private: |  | ||||||
|     QString _name; |  | ||||||
|     QString _address; |  | ||||||
|     QMap<QString, int> _priorityRules; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| #endif // GPGATEWAY_H |  | ||||||
| @@ -1,113 +0,0 @@ | |||||||
| #include "gphelper.h" |  | ||||||
| #include <QNetworkRequest> |  | ||||||
| #include <QXmlStreamReader> |  | ||||||
| #include <QMessageBox> |  | ||||||
| #include <QDesktopWidget> |  | ||||||
| #include <QApplication> |  | ||||||
| #include <QWidget> |  | ||||||
| #include <plog/Log.h> |  | ||||||
|  |  | ||||||
| QNetworkAccessManager* gpclient::helper::networkManager = new QNetworkAccessManager; |  | ||||||
|  |  | ||||||
| QNetworkReply* gpclient::helper::createRequest(QString url, QByteArray params) |  | ||||||
| { |  | ||||||
|     QNetworkRequest request(url); |  | ||||||
|     request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); |  | ||||||
|     request.setHeader(QNetworkRequest::UserAgentHeader, UA); |  | ||||||
|  |  | ||||||
|     if (params == nullptr) { |  | ||||||
|         return networkManager->post(request, QByteArray(nullptr)); |  | ||||||
|     } |  | ||||||
|     return networkManager->post(request, params); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| GPGateway gpclient::helper::filterPreferredGateway(QList<GPGateway> gateways, const QString ruleName) |  | ||||||
| { |  | ||||||
|     PLOGI << gateways.size() << " gateway(s) avaiable, filter the gateways with rule: " << ruleName; |  | ||||||
|  |  | ||||||
|     GPGateway gateway = gateways.first(); |  | ||||||
|  |  | ||||||
|     for (GPGateway g : gateways) { |  | ||||||
|         if (g.priorityOf(ruleName) > gateway.priorityOf(ruleName)) { |  | ||||||
|             PLOGI << "Find a preferred gateway: " << g.name(); |  | ||||||
|             gateway = g; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return gateway; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| QUrlQuery gpclient::helper::parseGatewayResponse(const QByteArray &xml) |  | ||||||
| { |  | ||||||
|     PLOGI << "Start parsing the gateway response..."; |  | ||||||
|  |  | ||||||
|     QXmlStreamReader xmlReader{xml}; |  | ||||||
|     QList<QString> args; |  | ||||||
|  |  | ||||||
|     while (!xmlReader.atEnd()) { |  | ||||||
|         xmlReader.readNextStartElement(); |  | ||||||
|         if (xmlReader.name() == "argument") { |  | ||||||
|             args.append(QUrl::toPercentEncoding(xmlReader.readElementText())); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     QUrlQuery params{}; |  | ||||||
|     params.addQueryItem("authcookie", args.at(1)); |  | ||||||
|     params.addQueryItem("portal", args.at(3)); |  | ||||||
|     params.addQueryItem("user", args.at(4)); |  | ||||||
|     params.addQueryItem("domain", args.at(7)); |  | ||||||
|     params.addQueryItem("preferred-ip", args.at(15)); |  | ||||||
|     params.addQueryItem("computer", QUrl::toPercentEncoding(QSysInfo::machineHostName())); |  | ||||||
|  |  | ||||||
|     return params; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void gpclient::helper::openMessageBox(const QString &message, const QString& informativeText) |  | ||||||
| { |  | ||||||
|     QMessageBox msgBox; |  | ||||||
|     msgBox.setWindowTitle("Notice"); |  | ||||||
|     msgBox.setText(message); |  | ||||||
|     msgBox.setFixedWidth(500); |  | ||||||
|     msgBox.setStyleSheet("QLabel{min-width: 250px}"); |  | ||||||
|     msgBox.setInformativeText(informativeText); |  | ||||||
|     msgBox.exec(); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void gpclient::helper::moveCenter(QWidget *widget) |  | ||||||
| { |  | ||||||
|     QDesktopWidget *desktop = QApplication::desktop(); |  | ||||||
|  |  | ||||||
|     int screenWidth, width; |  | ||||||
|     int screenHeight, height; |  | ||||||
|     int x, y; |  | ||||||
|     QSize windowSize; |  | ||||||
|  |  | ||||||
|     screenWidth = desktop->width(); |  | ||||||
|     screenHeight = desktop->height(); |  | ||||||
|  |  | ||||||
|     windowSize = widget->size(); |  | ||||||
|     width = windowSize.width(); |  | ||||||
|     height = windowSize.height(); |  | ||||||
|  |  | ||||||
|     x = (screenWidth - width) / 2; |  | ||||||
|     y = (screenHeight - height) / 2; |  | ||||||
|     y -= 50; |  | ||||||
|     widget->move(x, y); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| QSettings *gpclient::helper::settings::_settings = new QSettings("com.yuezk.qt", "GPClient"); |  | ||||||
|  |  | ||||||
| QVariant gpclient::helper::settings::get(const QString &key, const QVariant &defaultValue) |  | ||||||
| { |  | ||||||
|     return _settings->value(key, defaultValue); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void gpclient::helper::settings::save(const QString &key, const QVariant &value) |  | ||||||
| { |  | ||||||
|     _settings->setValue(key, value); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void gpclient::helper::settings::clear() |  | ||||||
| { |  | ||||||
|     _settings->clear(); |  | ||||||
| } |  | ||||||
| @@ -1,42 +0,0 @@ | |||||||
| #ifndef GPHELPER_H |  | ||||||
| #define GPHELPER_H |  | ||||||
|  |  | ||||||
| #include "samlloginwindow.h" |  | ||||||
| #include "gpgateway.h" |  | ||||||
|  |  | ||||||
| #include <QObject> |  | ||||||
| #include <QNetworkAccessManager> |  | ||||||
| #include <QNetworkRequest> |  | ||||||
| #include <QNetworkReply> |  | ||||||
| #include <QUrlQuery> |  | ||||||
| #include <QSettings> |  | ||||||
|  |  | ||||||
|  |  | ||||||
| const QString UA = "PAN GlobalProtect"; |  | ||||||
|  |  | ||||||
| namespace gpclient { |  | ||||||
|     namespace helper { |  | ||||||
|         extern QNetworkAccessManager *networkManager; |  | ||||||
|  |  | ||||||
|         QNetworkReply* createRequest(QString url, QByteArray params = nullptr); |  | ||||||
|  |  | ||||||
|         GPGateway filterPreferredGateway(QList<GPGateway> gateways, const QString ruleName); |  | ||||||
|  |  | ||||||
|         QUrlQuery parseGatewayResponse(const QByteArray& xml); |  | ||||||
|  |  | ||||||
|         void openMessageBox(const QString& message, const QString& informativeText = ""); |  | ||||||
|  |  | ||||||
|         void moveCenter(QWidget *widget); |  | ||||||
|  |  | ||||||
|         namespace settings { |  | ||||||
|  |  | ||||||
|             extern QSettings *_settings; |  | ||||||
|  |  | ||||||
|             QVariant get(const QString &key, const QVariant &defaultValue = QVariant()); |  | ||||||
|             void save(const QString &key, const QVariant &value); |  | ||||||
|             void clear(); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #endif // GPHELPER_H |  | ||||||
| @@ -1,25 +0,0 @@ | |||||||
| /* |  | ||||||
|  * This file was generated by qdbusxml2cpp version 0.8 |  | ||||||
|  * Command line was: qdbusxml2cpp -i gpservice_interface.h -p :gpservice_interface.cpp ../GPService/gpservice.xml |  | ||||||
|  * |  | ||||||
|  * qdbusxml2cpp is Copyright (C) 2020 The Qt Company Ltd. |  | ||||||
|  * |  | ||||||
|  * This is an auto-generated file. |  | ||||||
|  * This file may have been hand-edited. Look for HAND-EDIT comments |  | ||||||
|  * before re-generating it. |  | ||||||
|  */ |  | ||||||
|  |  | ||||||
| #include "gpservice_interface.h" |  | ||||||
| /* |  | ||||||
|  * Implementation of interface class ComYuezkQtGPServiceInterface |  | ||||||
|  */ |  | ||||||
|  |  | ||||||
| ComYuezkQtGPServiceInterface::ComYuezkQtGPServiceInterface(const QString &service, const QString &path, const QDBusConnection &connection, QObject *parent) |  | ||||||
|     : QDBusAbstractInterface(service, path, staticInterfaceName(), connection, parent) |  | ||||||
| { |  | ||||||
| } |  | ||||||
|  |  | ||||||
| ComYuezkQtGPServiceInterface::~ComYuezkQtGPServiceInterface() |  | ||||||
| { |  | ||||||
| } |  | ||||||
|  |  | ||||||
| @@ -1,71 +0,0 @@ | |||||||
| /* |  | ||||||
|  * This file was generated by qdbusxml2cpp version 0.8 |  | ||||||
|  * Command line was: qdbusxml2cpp -p gpservice_interface.h: ../GPService/gpservice.xml |  | ||||||
|  * |  | ||||||
|  * qdbusxml2cpp is Copyright (C) 2020 The Qt Company Ltd. |  | ||||||
|  * |  | ||||||
|  * This is an auto-generated file. |  | ||||||
|  * Do not edit! All changes made to it will be lost. |  | ||||||
|  */ |  | ||||||
|  |  | ||||||
| #ifndef GPSERVICE_INTERFACE_H |  | ||||||
| #define GPSERVICE_INTERFACE_H |  | ||||||
|  |  | ||||||
| #include <QtCore/QObject> |  | ||||||
| #include <QtCore/QByteArray> |  | ||||||
| #include <QtCore/QList> |  | ||||||
| #include <QtCore/QMap> |  | ||||||
| #include <QtCore/QString> |  | ||||||
| #include <QtCore/QStringList> |  | ||||||
| #include <QtCore/QVariant> |  | ||||||
| #include <QtDBus/QtDBus> |  | ||||||
|  |  | ||||||
| /* |  | ||||||
|  * Proxy class for interface com.yuezk.qt.GPService |  | ||||||
|  */ |  | ||||||
| class ComYuezkQtGPServiceInterface: public QDBusAbstractInterface |  | ||||||
| { |  | ||||||
|     Q_OBJECT |  | ||||||
| public: |  | ||||||
|     static inline const char *staticInterfaceName() |  | ||||||
|     { return "com.yuezk.qt.GPService"; } |  | ||||||
|  |  | ||||||
| public: |  | ||||||
|     ComYuezkQtGPServiceInterface(const QString &service, const QString &path, const QDBusConnection &connection, QObject *parent = nullptr); |  | ||||||
|  |  | ||||||
|     ~ComYuezkQtGPServiceInterface(); |  | ||||||
|  |  | ||||||
| public Q_SLOTS: // METHODS |  | ||||||
|     inline QDBusPendingReply<> connect(const QString &server, const QString &username, const QString &passwd) |  | ||||||
|     { |  | ||||||
|         QList<QVariant> argumentList; |  | ||||||
|         argumentList << QVariant::fromValue(server) << QVariant::fromValue(username) << QVariant::fromValue(passwd); |  | ||||||
|         return asyncCallWithArgumentList(QStringLiteral("connect"), argumentList); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     inline QDBusPendingReply<> disconnect() |  | ||||||
|     { |  | ||||||
|         QList<QVariant> argumentList; |  | ||||||
|         return asyncCallWithArgumentList(QStringLiteral("disconnect"), argumentList); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     inline QDBusPendingReply<int> status() |  | ||||||
|     { |  | ||||||
|         QList<QVariant> argumentList; |  | ||||||
|         return asyncCallWithArgumentList(QStringLiteral("status"), argumentList); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
| Q_SIGNALS: // SIGNALS |  | ||||||
|     void connected(); |  | ||||||
|     void disconnected(); |  | ||||||
|     void logAvailable(const QString &log); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| namespace com { |  | ||||||
|   namespace yuezk { |  | ||||||
|     namespace qt { |  | ||||||
|       typedef ::ComYuezkQtGPServiceInterface GPService; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| #endif |  | ||||||
| @@ -1,70 +0,0 @@ | |||||||
| #include "loginparams.h" |  | ||||||
|  |  | ||||||
| #include <QUrlQuery> |  | ||||||
|  |  | ||||||
| LoginParams::LoginParams() |  | ||||||
| { |  | ||||||
|     params.addQueryItem("prot", QUrl::toPercentEncoding("https:")); |  | ||||||
|     params.addQueryItem("server", ""); |  | ||||||
|     params.addQueryItem("inputSrc", ""); |  | ||||||
|     params.addQueryItem("jnlpReady", "jnlpReady"); |  | ||||||
|     params.addQueryItem("user", ""); |  | ||||||
|     params.addQueryItem("passwd", ""); |  | ||||||
|     params.addQueryItem("computer", QUrl::toPercentEncoding(QSysInfo::machineHostName())); |  | ||||||
|     params.addQueryItem("ok", "Login"); |  | ||||||
|     params.addQueryItem("direct", "yes"); |  | ||||||
|     params.addQueryItem("clientVer", "4100"); |  | ||||||
|     params.addQueryItem("os-version", QUrl::toPercentEncoding(QSysInfo::prettyProductName())); |  | ||||||
|     params.addQueryItem("clientos", "Linux"); |  | ||||||
|     params.addQueryItem("portal-userauthcookie", ""); |  | ||||||
|     params.addQueryItem("portal-prelogonuserauthcookie", ""); |  | ||||||
|     params.addQueryItem("prelogin-cookie", ""); |  | ||||||
|     params.addQueryItem("ipv6-support", "yes"); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| LoginParams::~LoginParams() |  | ||||||
| { |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void LoginParams::setUser(const QString user) |  | ||||||
| { |  | ||||||
|     updateQueryItem("user", user); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void LoginParams::setServer(const QString server) |  | ||||||
| { |  | ||||||
|     updateQueryItem("server", server); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void LoginParams::setPassword(const QString password) |  | ||||||
| { |  | ||||||
|     updateQueryItem("passwd", password); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void LoginParams::setUserAuthCookie(const QString cookie) |  | ||||||
| { |  | ||||||
|     updateQueryItem("portal-userauthcookie", cookie); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void LoginParams::setPrelogonAuthCookie(const QString cookie) |  | ||||||
| { |  | ||||||
|     updateQueryItem("portal-prelogonuserauthcookie", cookie); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void LoginParams::setPreloginCookie(const QString cookie) |  | ||||||
| { |  | ||||||
|     updateQueryItem("prelogin-cookie", cookie); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| QByteArray LoginParams::toUtf8() const |  | ||||||
| { |  | ||||||
|     return params.toString().toUtf8(); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void LoginParams::updateQueryItem(const QString key, const QString value) |  | ||||||
| { |  | ||||||
|     if (params.hasQueryItem(key)) { |  | ||||||
|         params.removeQueryItem(key); |  | ||||||
|     } |  | ||||||
|     params.addQueryItem(key, QUrl::toPercentEncoding(value)); |  | ||||||
| } |  | ||||||
| @@ -1,27 +0,0 @@ | |||||||
| #ifndef LOGINPARAMS_H |  | ||||||
| #define LOGINPARAMS_H |  | ||||||
|  |  | ||||||
| #include <QUrlQuery> |  | ||||||
|  |  | ||||||
| class LoginParams |  | ||||||
| { |  | ||||||
| public: |  | ||||||
|     LoginParams(); |  | ||||||
|     ~LoginParams(); |  | ||||||
|  |  | ||||||
|     void setUser(const QString user); |  | ||||||
|     void setServer(const QString server); |  | ||||||
|     void setPassword(const QString password); |  | ||||||
|     void setUserAuthCookie(const QString cookie); |  | ||||||
|     void setPrelogonAuthCookie(const QString cookie); |  | ||||||
|     void setPreloginCookie(const QString cookie); |  | ||||||
|  |  | ||||||
|     QByteArray toUtf8() const; |  | ||||||
|  |  | ||||||
| private: |  | ||||||
|     QUrlQuery params; |  | ||||||
|  |  | ||||||
|     void updateQueryItem(const QString key, const QString value); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| #endif // LOGINPARAMS_H |  | ||||||
| @@ -1,37 +0,0 @@ | |||||||
| #include "singleapplication.h" |  | ||||||
| #include "gpclient.h" |  | ||||||
| #include "enhancedwebview.h" |  | ||||||
|  |  | ||||||
| #include <QStandardPaths> |  | ||||||
| #include <plog/Log.h> |  | ||||||
| #include <plog/Appenders/ColorConsoleAppender.h> |  | ||||||
|  |  | ||||||
| static const QString version = "v1.2.3"; |  | ||||||
|  |  | ||||||
| int main(int argc, char *argv[]) |  | ||||||
| { |  | ||||||
|     const QDir path = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + "/GlobalProtect-openconnect"; |  | ||||||
|     const QString logFile = path.path() + "/gpclient.log"; |  | ||||||
|     if (!path.exists()) { |  | ||||||
|         path.mkpath("."); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     static plog::ColorConsoleAppender<plog::TxtFormatter> consoleAppender; |  | ||||||
|     plog::init(plog::debug, logFile.toUtf8()).addAppender(&consoleAppender); |  | ||||||
|  |  | ||||||
|     PLOGI << "GlobalProtect started, version: " << version; |  | ||||||
|  |  | ||||||
|     QString port = QString::fromLocal8Bit(qgetenv(ENV_CDP_PORT)); |  | ||||||
|  |  | ||||||
|     if (port == "") { |  | ||||||
|         qputenv(ENV_CDP_PORT, "12315"); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     SingleApplication app(argc, argv); |  | ||||||
|     GPClient w; |  | ||||||
|     w.show(); |  | ||||||
|  |  | ||||||
|     QObject::connect(&app, &SingleApplication::instanceStarted, &w, &GPClient::activiate); |  | ||||||
|  |  | ||||||
|     return app.exec(); |  | ||||||
| } |  | ||||||
| @@ -1,64 +0,0 @@ | |||||||
| #include "normalloginwindow.h" |  | ||||||
| #include "ui_normalloginwindow.h" |  | ||||||
|  |  | ||||||
| #include <QCloseEvent> |  | ||||||
|  |  | ||||||
| NormalLoginWindow::NormalLoginWindow(QWidget *parent) : |  | ||||||
|     QDialog(parent), |  | ||||||
|     ui(new Ui::NormalLoginWindow) |  | ||||||
| { |  | ||||||
|     ui->setupUi(this); |  | ||||||
|     setWindowTitle("GlobalProtect Login"); |  | ||||||
|     setFixedSize(width(), height()); |  | ||||||
|     setModal(true); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| NormalLoginWindow::~NormalLoginWindow() |  | ||||||
| { |  | ||||||
|     delete ui; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void NormalLoginWindow::setAuthMessage(QString message) |  | ||||||
| { |  | ||||||
|     ui->authMessage->setText(message); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void NormalLoginWindow::setUsernameLabel(QString label) |  | ||||||
| { |  | ||||||
|     ui->username->setPlaceholderText(label); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void NormalLoginWindow::setPasswordLabel(QString label) |  | ||||||
| { |  | ||||||
|     ui->password->setPlaceholderText(label); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void NormalLoginWindow::setPortalAddress(QString portal) |  | ||||||
| { |  | ||||||
|     ui->portalAddress->setText(portal); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void NormalLoginWindow::setProcessing(bool isProcessing) |  | ||||||
| { |  | ||||||
|     ui->username->setReadOnly(isProcessing); |  | ||||||
|     ui->password->setReadOnly(isProcessing); |  | ||||||
|     ui->loginButton->setDisabled(isProcessing); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void NormalLoginWindow::on_loginButton_clicked() |  | ||||||
| { |  | ||||||
|     const QString username = ui->username->text().trimmed(); |  | ||||||
|     const QString password = ui->password->text().trimmed(); |  | ||||||
|  |  | ||||||
|     if (username.isEmpty() || password.isEmpty()) { |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     emit performLogin(username, password); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void NormalLoginWindow::closeEvent(QCloseEvent *event) |  | ||||||
| { |  | ||||||
|     event->accept(); |  | ||||||
|     reject(); |  | ||||||
| } |  | ||||||
| @@ -1,37 +0,0 @@ | |||||||
| #ifndef PORTALAUTHWINDOW_H |  | ||||||
| #define PORTALAUTHWINDOW_H |  | ||||||
|  |  | ||||||
| #include <QDialog> |  | ||||||
|  |  | ||||||
| namespace Ui { |  | ||||||
| class NormalLoginWindow; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| class NormalLoginWindow : public QDialog |  | ||||||
| { |  | ||||||
|     Q_OBJECT |  | ||||||
|  |  | ||||||
| public: |  | ||||||
|     explicit NormalLoginWindow(QWidget *parent = nullptr); |  | ||||||
|     ~NormalLoginWindow(); |  | ||||||
|  |  | ||||||
|     void setAuthMessage(QString); |  | ||||||
|     void setUsernameLabel(QString); |  | ||||||
|     void setPasswordLabel(QString); |  | ||||||
|     void setPortalAddress(QString); |  | ||||||
|  |  | ||||||
|     void setProcessing(bool isProcessing); |  | ||||||
|  |  | ||||||
| private slots: |  | ||||||
|     void on_loginButton_clicked(); |  | ||||||
|  |  | ||||||
| signals: |  | ||||||
|     void performLogin(QString username, QString password); |  | ||||||
|  |  | ||||||
| private: |  | ||||||
|     Ui::NormalLoginWindow *ui; |  | ||||||
|  |  | ||||||
|     void closeEvent(QCloseEvent *event); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| #endif // PORTALAUTHWINDOW_H |  | ||||||
| @@ -1,148 +0,0 @@ | |||||||
| <?xml version="1.0" encoding="UTF-8"?> |  | ||||||
| <ui version="4.0"> |  | ||||||
|  <class>NormalLoginWindow</class> |  | ||||||
|  <widget class="QDialog" name="NormalLoginWindow"> |  | ||||||
|   <property name="geometry"> |  | ||||||
|    <rect> |  | ||||||
|     <x>0</x> |  | ||||||
|     <y>0</y> |  | ||||||
|     <width>255</width> |  | ||||||
|     <height>269</height> |  | ||||||
|    </rect> |  | ||||||
|   </property> |  | ||||||
|   <property name="sizePolicy"> |  | ||||||
|    <sizepolicy hsizetype="Preferred" vsizetype="Minimum"> |  | ||||||
|     <horstretch>0</horstretch> |  | ||||||
|     <verstretch>0</verstretch> |  | ||||||
|    </sizepolicy> |  | ||||||
|   </property> |  | ||||||
|   <property name="cursor"> |  | ||||||
|    <cursorShape>ArrowCursor</cursorShape> |  | ||||||
|   </property> |  | ||||||
|   <property name="windowTitle"> |  | ||||||
|    <string>Login</string> |  | ||||||
|   </property> |  | ||||||
|   <property name="modal"> |  | ||||||
|    <bool>true</bool> |  | ||||||
|   </property> |  | ||||||
|   <layout class="QVBoxLayout" name="verticalLayout_5"> |  | ||||||
|    <item> |  | ||||||
|     <layout class="QVBoxLayout" name="verticalLayout_4" stretch="1,0,0"> |  | ||||||
|      <item> |  | ||||||
|       <layout class="QVBoxLayout" name="verticalLayout"> |  | ||||||
|        <item> |  | ||||||
|         <widget class="QLabel" name="label"> |  | ||||||
|          <property name="font"> |  | ||||||
|           <font> |  | ||||||
|            <pointsize>20</pointsize> |  | ||||||
|           </font> |  | ||||||
|          </property> |  | ||||||
|          <property name="text"> |  | ||||||
|           <string>Login</string> |  | ||||||
|          </property> |  | ||||||
|          <property name="alignment"> |  | ||||||
|           <set>Qt::AlignCenter</set> |  | ||||||
|          </property> |  | ||||||
|         </widget> |  | ||||||
|        </item> |  | ||||||
|        <item> |  | ||||||
|         <widget class="QLabel" name="authMessage"> |  | ||||||
|          <property name="enabled"> |  | ||||||
|           <bool>true</bool> |  | ||||||
|          </property> |  | ||||||
|          <property name="sizePolicy"> |  | ||||||
|           <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> |  | ||||||
|            <horstretch>0</horstretch> |  | ||||||
|            <verstretch>2</verstretch> |  | ||||||
|           </sizepolicy> |  | ||||||
|          </property> |  | ||||||
|          <property name="text"> |  | ||||||
|           <string>Please enter the login credentials</string> |  | ||||||
|          </property> |  | ||||||
|          <property name="alignment"> |  | ||||||
|           <set>Qt::AlignCenter</set> |  | ||||||
|          </property> |  | ||||||
|         </widget> |  | ||||||
|        </item> |  | ||||||
|       </layout> |  | ||||||
|      </item> |  | ||||||
|      <item> |  | ||||||
|       <layout class="QVBoxLayout" name="verticalLayout_2"> |  | ||||||
|        <property name="spacing"> |  | ||||||
|         <number>0</number> |  | ||||||
|        </property> |  | ||||||
|        <property name="leftMargin"> |  | ||||||
|         <number>6</number> |  | ||||||
|        </property> |  | ||||||
|        <item> |  | ||||||
|         <widget class="QLabel" name="portalLabel"> |  | ||||||
|          <property name="sizePolicy"> |  | ||||||
|           <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> |  | ||||||
|            <horstretch>0</horstretch> |  | ||||||
|            <verstretch>0</verstretch> |  | ||||||
|           </sizepolicy> |  | ||||||
|          </property> |  | ||||||
|          <property name="text"> |  | ||||||
|           <string>Portal:</string> |  | ||||||
|          </property> |  | ||||||
|          <property name="margin"> |  | ||||||
|           <number>0</number> |  | ||||||
|          </property> |  | ||||||
|         </widget> |  | ||||||
|        </item> |  | ||||||
|        <item> |  | ||||||
|         <widget class="QLabel" name="portalAddress"> |  | ||||||
|          <property name="sizePolicy"> |  | ||||||
|           <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> |  | ||||||
|            <horstretch>0</horstretch> |  | ||||||
|            <verstretch>0</verstretch> |  | ||||||
|           </sizepolicy> |  | ||||||
|          </property> |  | ||||||
|          <property name="text"> |  | ||||||
|           <string>vpn.example.com</string> |  | ||||||
|          </property> |  | ||||||
|         </widget> |  | ||||||
|        </item> |  | ||||||
|       </layout> |  | ||||||
|      </item> |  | ||||||
|      <item> |  | ||||||
|       <layout class="QVBoxLayout" name="verticalLayout_3"> |  | ||||||
|        <item> |  | ||||||
|         <widget class="QLineEdit" name="username"> |  | ||||||
|          <property name="placeholderText"> |  | ||||||
|           <string>Username</string> |  | ||||||
|          </property> |  | ||||||
|         </widget> |  | ||||||
|        </item> |  | ||||||
|        <item> |  | ||||||
|         <widget class="QLineEdit" name="password"> |  | ||||||
|          <property name="text"> |  | ||||||
|           <string/> |  | ||||||
|          </property> |  | ||||||
|          <property name="echoMode"> |  | ||||||
|           <enum>QLineEdit::Password</enum> |  | ||||||
|          </property> |  | ||||||
|          <property name="placeholderText"> |  | ||||||
|           <string>Password</string> |  | ||||||
|          </property> |  | ||||||
|          <property name="clearButtonEnabled"> |  | ||||||
|           <bool>false</bool> |  | ||||||
|          </property> |  | ||||||
|         </widget> |  | ||||||
|        </item> |  | ||||||
|        <item> |  | ||||||
|         <widget class="QPushButton" name="loginButton"> |  | ||||||
|          <property name="text"> |  | ||||||
|           <string>Login</string> |  | ||||||
|          </property> |  | ||||||
|         </widget> |  | ||||||
|        </item> |  | ||||||
|       </layout> |  | ||||||
|      </item> |  | ||||||
|     </layout> |  | ||||||
|    </item> |  | ||||||
|   </layout> |  | ||||||
|  </widget> |  | ||||||
|  <resources/> |  | ||||||
|  <connections/> |  | ||||||
| </ui> |  | ||||||
| Before Width: | Height: | Size: 16 KiB | 
| Before Width: | Height: | Size: 16 KiB | 
| @@ -1,206 +0,0 @@ | |||||||
| #include "portalauthenticator.h" |  | ||||||
| #include "gphelper.h" |  | ||||||
| #include "normalloginwindow.h" |  | ||||||
| #include "samlloginwindow.h" |  | ||||||
| #include "loginparams.h" |  | ||||||
| #include "preloginresponse.h" |  | ||||||
| #include "portalconfigresponse.h" |  | ||||||
| #include "gpgateway.h" |  | ||||||
|  |  | ||||||
| #include <plog/Log.h> |  | ||||||
| #include <QNetworkReply> |  | ||||||
|  |  | ||||||
| using namespace gpclient::helper; |  | ||||||
|  |  | ||||||
| PortalAuthenticator::PortalAuthenticator(const QString& portal) : QObject() |  | ||||||
|   , portal(portal) |  | ||||||
|   , preloginUrl("https://" + portal + "/global-protect/prelogin.esp?tmp=tmp&kerberos-support=yes&ipv6-support=yes&clientVer=4100&clientos=Linux") |  | ||||||
|   , configUrl("https://" + portal + "/global-protect/getconfig.esp") |  | ||||||
| { |  | ||||||
| } |  | ||||||
|  |  | ||||||
| PortalAuthenticator::~PortalAuthenticator() |  | ||||||
| { |  | ||||||
|     delete normalLoginWindow; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void PortalAuthenticator::authenticate() |  | ||||||
| { |  | ||||||
|     PLOGI << "Preform portal prelogin at " << preloginUrl; |  | ||||||
|  |  | ||||||
|     QNetworkReply *reply = createRequest(preloginUrl); |  | ||||||
|     connect(reply, &QNetworkReply::finished, this, &PortalAuthenticator::onPreloginFinished); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void PortalAuthenticator::onPreloginFinished() |  | ||||||
| { |  | ||||||
|     QNetworkReply *reply = qobject_cast<QNetworkReply*>(sender()); |  | ||||||
|  |  | ||||||
|     if (reply->error()) { |  | ||||||
|         PLOGE << QString("Error occurred while accessing %1, %2").arg(preloginUrl).arg(reply->errorString()); |  | ||||||
|         emit preloginFailed("Error occurred on the portal prelogin interface."); |  | ||||||
|         delete reply; |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     PLOGI << "Portal prelogin succeeded."; |  | ||||||
|  |  | ||||||
|     preloginResponse = PreloginResponse::parse(reply->readAll()); |  | ||||||
|  |  | ||||||
|     PLOGI << "Finished parsing the prelogin response. The region field is: " << preloginResponse.region(); |  | ||||||
|  |  | ||||||
|     if (preloginResponse.hasSamlAuthFields()) { |  | ||||||
|         // Do SAML authentication |  | ||||||
|         samlAuth(); |  | ||||||
|     } else if (preloginResponse.hasNormalAuthFields()) { |  | ||||||
|         // Do normal username/password authentication |  | ||||||
|         tryAutoLogin(); |  | ||||||
|     } else { |  | ||||||
|         PLOGE << QString("Unknown prelogin response for %1 got %2").arg(preloginUrl).arg(QString::fromUtf8(preloginResponse.rawResponse())); |  | ||||||
|         emitFail("Unknown response for portal prelogin interface."); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     delete reply; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void PortalAuthenticator::tryAutoLogin() |  | ||||||
| { |  | ||||||
|     const QString username = settings::get("username").toString(); |  | ||||||
|     const QString password = settings::get("password").toString(); |  | ||||||
|  |  | ||||||
|     if (!username.isEmpty() && !password.isEmpty()) { |  | ||||||
|         PLOGI << "Trying auto login using the saved credentials"; |  | ||||||
|         isAutoLogin = true; |  | ||||||
|         fetchConfig(settings::get("username").toString(), settings::get("password").toString()); |  | ||||||
|     } else { |  | ||||||
|         normalAuth(); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void PortalAuthenticator::normalAuth() |  | ||||||
| { |  | ||||||
|     PLOGI << "Trying to launch the normal login window..."; |  | ||||||
|  |  | ||||||
|     normalLoginWindow = new NormalLoginWindow; |  | ||||||
|     normalLoginWindow->setPortalAddress(portal); |  | ||||||
|     normalLoginWindow->setAuthMessage(preloginResponse.authMessage()); |  | ||||||
|     normalLoginWindow->setUsernameLabel(preloginResponse.labelUsername()); |  | ||||||
|     normalLoginWindow->setPasswordLabel(preloginResponse.labelPassword()); |  | ||||||
|  |  | ||||||
|     // Do login |  | ||||||
|     connect(normalLoginWindow, &NormalLoginWindow::performLogin, this, &PortalAuthenticator::onPerformNormalLogin); |  | ||||||
|     connect(normalLoginWindow, &NormalLoginWindow::rejected, this, &PortalAuthenticator::onLoginWindowRejected); |  | ||||||
|     connect(normalLoginWindow, &NormalLoginWindow::finished, this, &PortalAuthenticator::onLoginWindowFinished); |  | ||||||
|  |  | ||||||
|     normalLoginWindow->show(); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void PortalAuthenticator::onPerformNormalLogin(const QString &username, const QString &password) |  | ||||||
| { |  | ||||||
|     normalLoginWindow->setProcessing(true); |  | ||||||
|     fetchConfig(username, password); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void PortalAuthenticator::onLoginWindowRejected() |  | ||||||
| { |  | ||||||
|     emitFail(); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void PortalAuthenticator::onLoginWindowFinished() |  | ||||||
| { |  | ||||||
|     delete normalLoginWindow; |  | ||||||
|     normalLoginWindow = nullptr; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void PortalAuthenticator::samlAuth() |  | ||||||
| { |  | ||||||
|     PLOGI << "Trying to perform SAML login with saml-method " << preloginResponse.samlMethod(); |  | ||||||
|  |  | ||||||
|     SAMLLoginWindow *loginWindow = new SAMLLoginWindow; |  | ||||||
|  |  | ||||||
|     connect(loginWindow, &SAMLLoginWindow::success, this, &PortalAuthenticator::onSAMLLoginSuccess); |  | ||||||
|     connect(loginWindow, &SAMLLoginWindow::fail, this, &PortalAuthenticator::onSAMLLoginFail); |  | ||||||
|     connect(loginWindow, &SAMLLoginWindow::rejected, this, &PortalAuthenticator::onLoginWindowRejected); |  | ||||||
|  |  | ||||||
|     loginWindow->login(preloginResponse.samlMethod(), preloginResponse.samlRequest(), preloginUrl); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void PortalAuthenticator::onSAMLLoginSuccess(const QMap<QString, QString> samlResult) |  | ||||||
| { |  | ||||||
|     if (samlResult.contains("preloginCookie")) { |  | ||||||
|         PLOGI << "SAML login succeeded, got the prelogin-cookie " << samlResult.value("preloginCookie"); |  | ||||||
|     } else { |  | ||||||
|         PLOGI << "SAML login succeeded, got the portal-userauthcookie " << samlResult.value("userAuthCookie"); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     fetchConfig(samlResult.value("username"), "", samlResult.value("preloginCookie"), samlResult.value("userAuthCookie")); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void PortalAuthenticator::onSAMLLoginFail(const QString msg) |  | ||||||
| { |  | ||||||
|     emitFail(msg); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void PortalAuthenticator::fetchConfig(QString username, QString password, QString preloginCookie, QString userAuthCookie) |  | ||||||
| { |  | ||||||
|     LoginParams params; |  | ||||||
|     params.setServer(portal); |  | ||||||
|     params.setUser(username); |  | ||||||
|     params.setPassword(password); |  | ||||||
|     params.setPreloginCookie(preloginCookie); |  | ||||||
|     params.setUserAuthCookie(userAuthCookie); |  | ||||||
|  |  | ||||||
|     // Save the username and password for future use. |  | ||||||
|     this->username = username; |  | ||||||
|     this->password = password; |  | ||||||
|  |  | ||||||
|     PLOGI << "Fetching the portal config from " << configUrl << " for user: " << username; |  | ||||||
|  |  | ||||||
|     QNetworkReply *reply = createRequest(configUrl, params.toUtf8()); |  | ||||||
|     connect(reply, &QNetworkReply::finished, this, &PortalAuthenticator::onFetchConfigFinished); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void PortalAuthenticator::onFetchConfigFinished() |  | ||||||
| { |  | ||||||
|     QNetworkReply *reply = qobject_cast<QNetworkReply*>(sender()); |  | ||||||
|  |  | ||||||
|     if (reply->error()) { |  | ||||||
|         PLOGE << QString("Failed to fetch the portal config from %1, %2").arg(configUrl).arg(reply->errorString()); |  | ||||||
|  |  | ||||||
|         // Login failed, enable the fields of the normal login window |  | ||||||
|         if (normalLoginWindow) { |  | ||||||
|             normalLoginWindow->setProcessing(false); |  | ||||||
|             openMessageBox("Portal login failed.", "Please check your credentials and try again."); |  | ||||||
|         } else if (isAutoLogin) { |  | ||||||
|             isAutoLogin = false; |  | ||||||
|             normalAuth(); |  | ||||||
|         } else { |  | ||||||
|             emitFail("Failed to fetch the portal config."); |  | ||||||
|         } |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     PLOGI << "Fetch the portal config succeeded."; |  | ||||||
|     PortalConfigResponse response = PortalConfigResponse::parse(reply->readAll()); |  | ||||||
|  |  | ||||||
|     // Add the username & password to the response object |  | ||||||
|     response.setUsername(username); |  | ||||||
|     response.setPassword(password); |  | ||||||
|  |  | ||||||
|     // Close the login window |  | ||||||
|     if (normalLoginWindow) { |  | ||||||
|         PLOGI << "Closing the NormalLoginWindow..."; |  | ||||||
|  |  | ||||||
|         // Save the credentials for reuse |  | ||||||
|         settings::save("username", username); |  | ||||||
|         settings::save("password", password); |  | ||||||
|         normalLoginWindow->close(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     emit success(response, filterPreferredGateway(response.allGateways(), preloginResponse.region()), response.allGateways()); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void PortalAuthenticator::emitFail(const QString& msg) |  | ||||||
| { |  | ||||||
|     emit fail(msg); |  | ||||||
| } |  | ||||||
| @@ -1,54 +0,0 @@ | |||||||
| #ifndef PORTALAUTHENTICATOR_H |  | ||||||
| #define PORTALAUTHENTICATOR_H |  | ||||||
|  |  | ||||||
| #include "portalconfigresponse.h" |  | ||||||
| #include "normalloginwindow.h" |  | ||||||
| #include "samlloginwindow.h" |  | ||||||
| #include "preloginresponse.h" |  | ||||||
|  |  | ||||||
| #include <QObject> |  | ||||||
|  |  | ||||||
| class PortalAuthenticator : public QObject |  | ||||||
| { |  | ||||||
|     Q_OBJECT |  | ||||||
| public: |  | ||||||
|     explicit PortalAuthenticator(const QString& portal); |  | ||||||
|     ~PortalAuthenticator(); |  | ||||||
|  |  | ||||||
|     void authenticate(); |  | ||||||
|  |  | ||||||
| signals: |  | ||||||
|     void success(const PortalConfigResponse, const GPGateway, QList<GPGateway> allGateways); |  | ||||||
|     void fail(const QString& msg); |  | ||||||
|     void preloginFailed(const QString& msg); |  | ||||||
|  |  | ||||||
| private slots: |  | ||||||
|     void onPreloginFinished(); |  | ||||||
|     void onPerformNormalLogin(const QString &username, const QString &password); |  | ||||||
|     void onLoginWindowRejected(); |  | ||||||
|     void onLoginWindowFinished(); |  | ||||||
|     void onSAMLLoginSuccess(const QMap<QString, QString> samlResult); |  | ||||||
|     void onSAMLLoginFail(const QString msg); |  | ||||||
|     void onFetchConfigFinished(); |  | ||||||
|  |  | ||||||
| private: |  | ||||||
|     QString portal; |  | ||||||
|     QString preloginUrl; |  | ||||||
|     QString configUrl; |  | ||||||
|     QString username; |  | ||||||
|     QString password; |  | ||||||
|  |  | ||||||
|     PreloginResponse preloginResponse; |  | ||||||
|  |  | ||||||
|     bool isAutoLogin { false }; |  | ||||||
|  |  | ||||||
|     NormalLoginWindow *normalLoginWindow{ nullptr }; |  | ||||||
|  |  | ||||||
|     void tryAutoLogin(); |  | ||||||
|     void normalAuth(); |  | ||||||
|     void samlAuth(); |  | ||||||
|     void fetchConfig(QString username, QString password, QString preloginCookie = "", QString userAuthCookie = ""); |  | ||||||
|     void emitFail(const QString& msg = ""); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| #endif // PORTALAUTHENTICATOR_H |  | ||||||
| @@ -1,168 +0,0 @@ | |||||||
| #include "portalconfigresponse.h" |  | ||||||
|  |  | ||||||
| #include <QXmlStreamReader> |  | ||||||
| #include <plog/Log.h> |  | ||||||
|  |  | ||||||
| QString PortalConfigResponse::xmlUserAuthCookie = "portal-userauthcookie"; |  | ||||||
| QString PortalConfigResponse::xmlPrelogonUserAuthCookie = "portal-prelogonuserauthcookie"; |  | ||||||
| QString PortalConfigResponse::xmlGateways = "gateways"; |  | ||||||
|  |  | ||||||
| PortalConfigResponse::PortalConfigResponse() |  | ||||||
| { |  | ||||||
| } |  | ||||||
|  |  | ||||||
| PortalConfigResponse::~PortalConfigResponse() |  | ||||||
| { |  | ||||||
| } |  | ||||||
|  |  | ||||||
| PortalConfigResponse PortalConfigResponse::parse(const QByteArray xml) |  | ||||||
| { |  | ||||||
|     PLOGI << "Start parsing the portal configuration..."; |  | ||||||
|  |  | ||||||
|     QXmlStreamReader xmlReader(xml); |  | ||||||
|     PortalConfigResponse response; |  | ||||||
|     response.setRawResponse(xml); |  | ||||||
|  |  | ||||||
|     while (!xmlReader.atEnd()) { |  | ||||||
|         xmlReader.readNextStartElement(); |  | ||||||
|  |  | ||||||
|         QString name = xmlReader.name().toString(); |  | ||||||
|  |  | ||||||
|         if (name == xmlUserAuthCookie) { |  | ||||||
|             PLOGI << "Start reading " << name; |  | ||||||
|             response.setUserAuthCookie(xmlReader.readElementText()); |  | ||||||
|         } else if (name == xmlPrelogonUserAuthCookie) { |  | ||||||
|             PLOGI << "Start reading " << name; |  | ||||||
|             response.setPrelogonUserAuthCookie(xmlReader.readElementText()); |  | ||||||
|         } else if (name == xmlGateways) { |  | ||||||
|             response.setAllGateways(parseGateways(xmlReader)); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     PLOGI << "Finished parsing portal configuration."; |  | ||||||
|  |  | ||||||
|     return response; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| const QByteArray PortalConfigResponse::rawResponse() const |  | ||||||
| { |  | ||||||
|     return _rawResponse; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| QString PortalConfigResponse::username() const |  | ||||||
| { |  | ||||||
|     return _username; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| QString PortalConfigResponse::password() const |  | ||||||
| { |  | ||||||
|     return _password; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| QList<GPGateway> PortalConfigResponse::parseGateways(QXmlStreamReader &xmlReader) |  | ||||||
| { |  | ||||||
|     PLOGI << "Start parsing the gateways from portal configuration..."; |  | ||||||
|  |  | ||||||
|     QList<GPGateway> gateways; |  | ||||||
|  |  | ||||||
|     while (xmlReader.name() != xmlGateways || !xmlReader.isEndElement()) { |  | ||||||
|         xmlReader.readNext(); |  | ||||||
|         // Parse the gateways -> external -> list -> entry |  | ||||||
|         if (xmlReader.name() == "entry" && xmlReader.isStartElement()) { |  | ||||||
|             GPGateway g; |  | ||||||
|             QString address = xmlReader.attributes().value("name").toString(); |  | ||||||
|             g.setAddress(address); |  | ||||||
|             g.setPriorityRules(parsePriorityRules(xmlReader)); |  | ||||||
|             g.setName(parseGatewayName(xmlReader)); |  | ||||||
|             gateways.append(g); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     PLOGI << "Finished parsing the gateways."; |  | ||||||
|  |  | ||||||
|     return gateways; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| QMap<QString, int> PortalConfigResponse::parsePriorityRules(QXmlStreamReader &xmlReader) |  | ||||||
| { |  | ||||||
|     PLOGI << "Start parsing the priority rules..."; |  | ||||||
|  |  | ||||||
|     QMap<QString, int> priorityRules; |  | ||||||
|  |  | ||||||
|     while (xmlReader.name() != "priority-rule" || !xmlReader.isEndElement()) { |  | ||||||
|         xmlReader.readNext(); |  | ||||||
|  |  | ||||||
|         if (xmlReader.name() == "entry" && xmlReader.isStartElement()) { |  | ||||||
|             QString ruleName = xmlReader.attributes().value("name").toString(); |  | ||||||
|             // Read the priority tag |  | ||||||
|             xmlReader.readNextStartElement(); |  | ||||||
|             int ruleValue = xmlReader.readElementText().toUInt(); |  | ||||||
|             priorityRules.insert(ruleName, ruleValue); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     PLOGI << "Finished parsing the priority rules."; |  | ||||||
|  |  | ||||||
|     return priorityRules; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| QString PortalConfigResponse::parseGatewayName(QXmlStreamReader &xmlReader) |  | ||||||
| { |  | ||||||
|     PLOGI << "Start parsing the gateway name..."; |  | ||||||
|  |  | ||||||
|     while (xmlReader.name() != "description" || !xmlReader.isEndElement()) { |  | ||||||
|         xmlReader.readNext(); |  | ||||||
|         if (xmlReader.name() == "description" && xmlReader.tokenType() == xmlReader.StartElement) { |  | ||||||
|             PLOGI << "Finished parsing the gateway name"; |  | ||||||
|             return xmlReader.readElementText(); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     PLOGE << "Error: <description> tag not found"; |  | ||||||
|     return ""; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| QString PortalConfigResponse::userAuthCookie() const |  | ||||||
| { |  | ||||||
|     return _userAuthCookie; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| QString PortalConfigResponse::prelogonUserAuthCookie() const |  | ||||||
| { |  | ||||||
|     return _prelogonAuthCookie; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| QList<GPGateway> PortalConfigResponse::allGateways() |  | ||||||
| { |  | ||||||
|     return _gateways; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void PortalConfigResponse::setAllGateways(QList<GPGateway> gateways) |  | ||||||
| { |  | ||||||
|     _gateways = gateways; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void PortalConfigResponse::setRawResponse(const QByteArray response) |  | ||||||
| { |  | ||||||
|     _rawResponse = response; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void PortalConfigResponse::setUsername(const QString username) |  | ||||||
| { |  | ||||||
|     _username = username; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void PortalConfigResponse::setPassword(const QString password) |  | ||||||
| { |  | ||||||
|     _password = password; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void PortalConfigResponse::setUserAuthCookie(const QString cookie) |  | ||||||
| { |  | ||||||
|     _userAuthCookie = cookie; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void PortalConfigResponse::setPrelogonUserAuthCookie(const QString cookie) |  | ||||||
| { |  | ||||||
|     _prelogonAuthCookie = cookie; |  | ||||||
| } |  | ||||||
| @@ -1,51 +0,0 @@ | |||||||
| #ifndef PORTALCONFIGRESPONSE_H |  | ||||||
| #define PORTALCONFIGRESPONSE_H |  | ||||||
|  |  | ||||||
| #include "gpgateway.h" |  | ||||||
|  |  | ||||||
| #include <QString> |  | ||||||
| #include <QList> |  | ||||||
| #include <QXmlStreamReader> |  | ||||||
|  |  | ||||||
| class PortalConfigResponse |  | ||||||
| { |  | ||||||
| public: |  | ||||||
|     PortalConfigResponse(); |  | ||||||
|     ~PortalConfigResponse(); |  | ||||||
|  |  | ||||||
|     static PortalConfigResponse parse(const QByteArray xml); |  | ||||||
|  |  | ||||||
|     const QByteArray rawResponse() const; |  | ||||||
|     QString username() const; |  | ||||||
|     QString password() const; |  | ||||||
|     QString userAuthCookie() const; |  | ||||||
|     QString prelogonUserAuthCookie() const; |  | ||||||
|     QList<GPGateway> allGateways(); |  | ||||||
|     void setAllGateways(QList<GPGateway> gateways); |  | ||||||
|  |  | ||||||
|     void setUsername(const QString username); |  | ||||||
|     void setPassword(const QString password); |  | ||||||
|  |  | ||||||
| private: |  | ||||||
|     static QString xmlUserAuthCookie; |  | ||||||
|     static QString xmlPrelogonUserAuthCookie; |  | ||||||
|     static QString xmlGateways; |  | ||||||
|  |  | ||||||
|     QByteArray _rawResponse; |  | ||||||
|     QString _username; |  | ||||||
|     QString _password; |  | ||||||
|     QString _userAuthCookie; |  | ||||||
|     QString _prelogonAuthCookie; |  | ||||||
|  |  | ||||||
|     QList<GPGateway> _gateways; |  | ||||||
|  |  | ||||||
|     void setRawResponse(const QByteArray response); |  | ||||||
|     void setUserAuthCookie(const QString cookie); |  | ||||||
|     void setPrelogonUserAuthCookie(const QString cookie); |  | ||||||
|  |  | ||||||
|     static QList<GPGateway> parseGateways(QXmlStreamReader &xmlReader); |  | ||||||
|     static QMap<QString, int> parsePriorityRules(QXmlStreamReader &xmlReader); |  | ||||||
|     static QString parseGatewayName(QXmlStreamReader &xmlReader); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| #endif // PORTALCONFIGRESPONSE_H |  | ||||||
| @@ -1,100 +0,0 @@ | |||||||
| #include "preloginresponse.h" |  | ||||||
|  |  | ||||||
| #include <QXmlStreamReader> |  | ||||||
| #include <QMap> |  | ||||||
| #include <plog/Log.h> |  | ||||||
|  |  | ||||||
| QString PreloginResponse::xmlAuthMessage = "authentication-message"; |  | ||||||
| QString PreloginResponse::xmlLabelUsername = "username-label"; |  | ||||||
| QString PreloginResponse::xmlLabelPassword = "password-label"; |  | ||||||
| QString PreloginResponse::xmlSamlMethod = "saml-auth-method"; |  | ||||||
| QString PreloginResponse::xmlSamlRequest = "saml-request"; |  | ||||||
| QString PreloginResponse::xmlRegion = "region"; |  | ||||||
|  |  | ||||||
| PreloginResponse::PreloginResponse() |  | ||||||
| { |  | ||||||
|     add(xmlAuthMessage, ""); |  | ||||||
|     add(xmlLabelUsername, ""); |  | ||||||
|     add(xmlLabelPassword, ""); |  | ||||||
|     add(xmlSamlMethod, ""); |  | ||||||
|     add(xmlSamlRequest, ""); |  | ||||||
|     add(xmlRegion, ""); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| PreloginResponse PreloginResponse::parse(const QByteArray& xml) |  | ||||||
| { |  | ||||||
|     PLOGI << "Start parsing the prelogin response..."; |  | ||||||
|  |  | ||||||
|     QXmlStreamReader xmlReader(xml); |  | ||||||
|     PreloginResponse response; |  | ||||||
|     response.setRawResponse(xml); |  | ||||||
|  |  | ||||||
|     while (!xmlReader.atEnd()) { |  | ||||||
|         xmlReader.readNextStartElement(); |  | ||||||
|         QString name = xmlReader.name().toString(); |  | ||||||
|         if (response.has(name)) { |  | ||||||
|             response.add(name, xmlReader.readElementText()); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|     return response; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| const QByteArray& PreloginResponse::rawResponse() const |  | ||||||
| { |  | ||||||
|     return _rawResponse; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| QString PreloginResponse::authMessage() const |  | ||||||
| { |  | ||||||
|     return resultMap.value(xmlAuthMessage); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| QString PreloginResponse::labelUsername() const |  | ||||||
| { |  | ||||||
|     return resultMap.value(xmlLabelUsername); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| QString PreloginResponse::labelPassword() const |  | ||||||
| { |  | ||||||
|     return resultMap.value(xmlLabelPassword); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| QString PreloginResponse::samlMethod() const |  | ||||||
| { |  | ||||||
|     return resultMap.value(xmlSamlMethod); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| QString PreloginResponse::samlRequest() const |  | ||||||
| { |  | ||||||
|     return QByteArray::fromBase64(resultMap.value(xmlSamlRequest).toUtf8()); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| QString PreloginResponse::region() const |  | ||||||
| { |  | ||||||
|     return resultMap.value(xmlRegion); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| bool PreloginResponse::hasSamlAuthFields() const |  | ||||||
| { |  | ||||||
|     return !samlMethod().isEmpty() && !samlRequest().isEmpty(); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| bool PreloginResponse::hasNormalAuthFields() const |  | ||||||
| { |  | ||||||
|     return !labelUsername().isEmpty() && !labelPassword().isEmpty(); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void PreloginResponse::setRawResponse(const QByteArray response) |  | ||||||
| { |  | ||||||
|     _rawResponse = response; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| bool PreloginResponse::has(const QString name) const |  | ||||||
| { |  | ||||||
|     return resultMap.contains(name); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void PreloginResponse::add(const QString name, const QString value) |  | ||||||
| { |  | ||||||
|     resultMap.insert(name, value); |  | ||||||
| } |  | ||||||
| @@ -1,41 +0,0 @@ | |||||||
| #ifndef PRELOGINRESPONSE_H |  | ||||||
| #define PRELOGINRESPONSE_H |  | ||||||
|  |  | ||||||
| #include <QString> |  | ||||||
| #include <QMap> |  | ||||||
|  |  | ||||||
| class PreloginResponse |  | ||||||
| { |  | ||||||
| public: |  | ||||||
|     PreloginResponse(); |  | ||||||
|  |  | ||||||
|     static PreloginResponse parse(const QByteArray& xml); |  | ||||||
|  |  | ||||||
|     const QByteArray& rawResponse() const; |  | ||||||
|     QString authMessage() const; |  | ||||||
|     QString labelUsername() const; |  | ||||||
|     QString labelPassword() const; |  | ||||||
|     QString samlMethod() const; |  | ||||||
|     QString samlRequest() const; |  | ||||||
|     QString region() const; |  | ||||||
|  |  | ||||||
|     bool hasSamlAuthFields() const; |  | ||||||
|     bool hasNormalAuthFields() const; |  | ||||||
|  |  | ||||||
| private: |  | ||||||
|     static QString xmlAuthMessage; |  | ||||||
|     static QString xmlLabelUsername; |  | ||||||
|     static QString xmlLabelPassword; |  | ||||||
|     static QString xmlSamlMethod; |  | ||||||
|     static QString xmlSamlRequest; |  | ||||||
|     static QString xmlRegion; |  | ||||||
|  |  | ||||||
|     QMap<QString, QString> resultMap; |  | ||||||
|     QByteArray _rawResponse; |  | ||||||
|  |  | ||||||
|     void setRawResponse(const QByteArray response); |  | ||||||
|     void add(const QString name, const QString value); |  | ||||||
|     bool has(const QString name) const; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| #endif // PRELOGINRESPONSE_H |  | ||||||
| Before Width: | Height: | Size: 1.2 KiB | 
| Before Width: | Height: | Size: 993 B | 
| @@ -1,10 +0,0 @@ | |||||||
| <RCC> |  | ||||||
|     <qresource prefix="/images"> |  | ||||||
|         <file alias="logo.svg">com.yuezk.qt.GPClient.svg</file> |  | ||||||
|         <file>connected.png</file> |  | ||||||
|         <file>pending.png</file> |  | ||||||
|         <file>not_connected.png</file> |  | ||||||
|         <file>radio_unselected.png</file> |  | ||||||
|         <file>radio_selected.png</file> |  | ||||||
|     </qresource> |  | ||||||
| </RCC> |  | ||||||
| @@ -1,99 +0,0 @@ | |||||||
| #include "samlloginwindow.h" |  | ||||||
|  |  | ||||||
| #include <QVBoxLayout> |  | ||||||
| #include <plog/Log.h> |  | ||||||
| #include <QWebEngineProfile> |  | ||||||
| #include <QWebEngineView> |  | ||||||
|  |  | ||||||
| SAMLLoginWindow::SAMLLoginWindow(QWidget *parent) |  | ||||||
|     : QDialog(parent) |  | ||||||
|     , webView(new EnhancedWebView(this)) |  | ||||||
| { |  | ||||||
|     setWindowTitle("GlobalProtect SAML Login"); |  | ||||||
|     setModal(true); |  | ||||||
|     resize(700, 550); |  | ||||||
|  |  | ||||||
|     QVBoxLayout *verticalLayout = new QVBoxLayout(this); |  | ||||||
|     webView->setUrl(QUrl("about:blank")); |  | ||||||
|     // webView->page()->profile()->setPersistentCookiesPolicy(QWebEngineProfile::NoPersistentCookies); |  | ||||||
|     verticalLayout->addWidget(webView); |  | ||||||
|  |  | ||||||
|     webView->initialize(); |  | ||||||
|     connect(webView, &EnhancedWebView::responseReceived, this, &SAMLLoginWindow::onResponseReceived); |  | ||||||
|     connect(webView, &EnhancedWebView::loadFinished, this, &SAMLLoginWindow::onLoadFinished); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| SAMLLoginWindow::~SAMLLoginWindow() |  | ||||||
| { |  | ||||||
|     delete webView; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void SAMLLoginWindow::closeEvent(QCloseEvent *event) |  | ||||||
| { |  | ||||||
|     event->accept(); |  | ||||||
|     reject(); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void SAMLLoginWindow::login(const QString samlMethod, const QString samlRequest, const QString preloingUrl) |  | ||||||
| { |  | ||||||
|     if (samlMethod == "POST") { |  | ||||||
|         webView->setHtml(samlRequest, preloingUrl); |  | ||||||
|     } else if (samlMethod == "REDIRECT") { |  | ||||||
|         webView->load(samlRequest); |  | ||||||
|     } else { |  | ||||||
|         PLOGE << "Unknown saml-auth-method expected POST or REDIRECT, got " << samlMethod; |  | ||||||
|         emit fail("Unknown saml-auth-method, got " + samlMethod); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void SAMLLoginWindow::onResponseReceived(QJsonObject params) |  | ||||||
| { |  | ||||||
|     QString type = params.value("type").toString(); |  | ||||||
|     // Skip non-document response |  | ||||||
|     if (type != "Document") { |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     QJsonObject response = params.value("response").toObject(); |  | ||||||
|     QJsonObject headers = response.value("headers").toObject(); |  | ||||||
|  |  | ||||||
|     const QString username = headers.value("saml-username").toString(); |  | ||||||
|     const QString preloginCookie = headers.value("prelogin-cookie").toString(); |  | ||||||
|     const QString userAuthCookie = headers.value("portal-userauthcookie").toString(); |  | ||||||
|  |  | ||||||
|     LOGI << "Response received from " << response.value("url").toString(); |  | ||||||
|  |  | ||||||
|     if (!username.isEmpty()) { |  | ||||||
|         LOGI << "Got username from SAML response headers " << username; |  | ||||||
|         samlResult.insert("username", username); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if (!preloginCookie.isEmpty()) { |  | ||||||
|         LOGI << "Got prelogin-cookie from SAML response headers " << preloginCookie; |  | ||||||
|         samlResult.insert("preloginCookie", preloginCookie); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if (!userAuthCookie.isEmpty()) { |  | ||||||
|         LOGI << "Got portal-userauthcookie from SAML response headers " << userAuthCookie; |  | ||||||
|         samlResult.insert("userAuthCookie", userAuthCookie); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     // Check the SAML result |  | ||||||
|     if (samlResult.contains("username") |  | ||||||
|             && (samlResult.contains("preloginCookie") || samlResult.contains("userAuthCookie"))) { |  | ||||||
|         LOGI << "Got the SAML authentication information successfully. " |  | ||||||
|              << "username: " << samlResult.value("username") |  | ||||||
|              << ", preloginCookie: " << samlResult.value("preloginCookie") |  | ||||||
|              << ", userAuthCookie: " << samlResult.value("userAuthCookie"); |  | ||||||
|  |  | ||||||
|         emit success(samlResult); |  | ||||||
|         accept(); |  | ||||||
|     } else { |  | ||||||
|         this->show(); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void SAMLLoginWindow::onLoadFinished() |  | ||||||
| { |  | ||||||
|      LOGI << "Load finished " << this->webView->page()->url().toString(); |  | ||||||
| } |  | ||||||
| @@ -1,35 +0,0 @@ | |||||||
| #ifndef SAMLLOGINWINDOW_H |  | ||||||
| #define SAMLLOGINWINDOW_H |  | ||||||
|  |  | ||||||
| #include "enhancedwebview.h" |  | ||||||
|  |  | ||||||
| #include <QDialog> |  | ||||||
| #include <QMap> |  | ||||||
| #include <QCloseEvent> |  | ||||||
|  |  | ||||||
| class SAMLLoginWindow : public QDialog |  | ||||||
| { |  | ||||||
|     Q_OBJECT |  | ||||||
|  |  | ||||||
| public: |  | ||||||
|     explicit SAMLLoginWindow(QWidget *parent = nullptr); |  | ||||||
|     ~SAMLLoginWindow(); |  | ||||||
|  |  | ||||||
|     void login(const QString samlMethod, const QString samlRequest, const QString preloingUrl); |  | ||||||
|  |  | ||||||
| signals: |  | ||||||
|     void success(QMap<QString, QString> samlResult); |  | ||||||
|     void fail(const QString msg); |  | ||||||
|  |  | ||||||
| private slots: |  | ||||||
|     void onResponseReceived(QJsonObject params); |  | ||||||
|     void onLoadFinished(); |  | ||||||
|  |  | ||||||
| private: |  | ||||||
|     EnhancedWebView *webView; |  | ||||||
|     QMap<QString, QString> samlResult; |  | ||||||
|  |  | ||||||
|     void closeEvent(QCloseEvent *event); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| #endif // SAMLLOGINWINDOW_H |  | ||||||
| @@ -1,52 +0,0 @@ | |||||||
| TARGET = gpservice |  | ||||||
|  |  | ||||||
| QT += dbus |  | ||||||
| QT -= gui |  | ||||||
|  |  | ||||||
| CONFIG += c++11 console |  | ||||||
| CONFIG -= app_bundle |  | ||||||
|  |  | ||||||
| include(../singleapplication/singleapplication.pri) |  | ||||||
| DEFINES += QAPPLICATION_CLASS=QCoreApplication |  | ||||||
|  |  | ||||||
| # The following define makes your compiler emit warnings if you use |  | ||||||
| # any Qt feature that has been marked deprecated (the exact warnings |  | ||||||
| # depend on your compiler). Please consult the documentation of the |  | ||||||
| # deprecated API in order to know how to port your code away from it. |  | ||||||
| DEFINES += QT_DEPRECATED_WARNINGS |  | ||||||
|  |  | ||||||
| # You can also make your code fail to compile if it uses deprecated APIs. |  | ||||||
| # In order to do so, uncomment the following line. |  | ||||||
| # You can also select to disable deprecated APIs only up to a certain version of Qt. |  | ||||||
| #DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000    # disables all the APIs deprecated before Qt 6.0.0 |  | ||||||
|  |  | ||||||
| HEADERS += \ |  | ||||||
|     gpservice.h \ |  | ||||||
|     sigwatch.h |  | ||||||
|  |  | ||||||
| SOURCES += \ |  | ||||||
|         gpservice.cpp \ |  | ||||||
|         main.cpp \ |  | ||||||
|         sigwatch.cpp |  | ||||||
|  |  | ||||||
| DBUS_ADAPTORS += gpservice.xml |  | ||||||
|  |  | ||||||
| # Default rules for deployment. |  | ||||||
| target.path = /usr/bin |  | ||||||
| INSTALLS += target |  | ||||||
|  |  | ||||||
| DISTFILES += \ |  | ||||||
|     dbus/com.yuezk.qt.GPService.conf \ |  | ||||||
|     dbus/com.yuezk.qt.GPService.service \ |  | ||||||
|     systemd/gpservice.service |  | ||||||
|  |  | ||||||
| dbus_config.path = /usr/share/dbus-1/system.d/ |  | ||||||
| dbus_config.files = dbus/com.yuezk.qt.GPService.conf |  | ||||||
|  |  | ||||||
| dbus_service.path = /usr/share/dbus-1/system-services/ |  | ||||||
| dbus_service.files = dbus/com.yuezk.qt.GPService.service |  | ||||||
|  |  | ||||||
| systemd_service.path = /etc/systemd/system/ |  | ||||||
| systemd_service.files = systemd/gpservice.service |  | ||||||
|  |  | ||||||
| INSTALLS += dbus_config dbus_service systemd_service |  | ||||||
| @@ -1,18 +0,0 @@ | |||||||
| <?xml version="1.0" encoding="UTF-8"?> |  | ||||||
| <!DOCTYPE busconfig PUBLIC |  | ||||||
| "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN" |  | ||||||
| "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd"> |  | ||||||
| <busconfig> |  | ||||||
|         <policy user="root"> |  | ||||||
|                 <allow own="com.yuezk.qt.GPService"/> |  | ||||||
|         </policy> |  | ||||||
|  |  | ||||||
|         <policy context="default"> |  | ||||||
|                 <allow send_destination="com.yuezk.qt.GPService" |  | ||||||
|                         send_interface="com.yuezk.qt.GPService" |  | ||||||
|                         /> |  | ||||||
|                 <allow send_destination="com.yuezk.qt.GPService" |  | ||||||
|                         send_interface="org.freedesktop.DBus.Introspectable" |  | ||||||
|                         /> |  | ||||||
|         </policy> |  | ||||||
| </busconfig> |  | ||||||
| @@ -1,5 +0,0 @@ | |||||||
| [D-BUS Service] |  | ||||||
| Name=com.yuezk.qt.GPService |  | ||||||
| Exec=/usr/bin/gpservice |  | ||||||
| User=root |  | ||||||
| SystemdService=gpservice.service |  | ||||||
| @@ -1,131 +0,0 @@ | |||||||
| #include "gpservice.h" |  | ||||||
| #include "gpservice_adaptor.h" |  | ||||||
|  |  | ||||||
| #include <QFileInfo> |  | ||||||
| #include <QtDBus> |  | ||||||
| #include <QDateTime> |  | ||||||
| #include <QVariant> |  | ||||||
|  |  | ||||||
| GPService::GPService(QObject *parent) |  | ||||||
|     : QObject(parent) |  | ||||||
|     , openconnect(new QProcess) |  | ||||||
| { |  | ||||||
|     // Register the DBus service |  | ||||||
|     new GPServiceAdaptor(this); |  | ||||||
|     QDBusConnection dbus = QDBusConnection::systemBus(); |  | ||||||
|     dbus.registerObject("/", this); |  | ||||||
|     dbus.registerService("com.yuezk.qt.GPService"); |  | ||||||
|  |  | ||||||
|     // Setup the openconnect process |  | ||||||
|     QObject::connect(openconnect, &QProcess::started, this, &GPService::onProcessStarted); |  | ||||||
|     QObject::connect(openconnect, &QProcess::errorOccurred, this, &GPService::onProcessError); |  | ||||||
|     QObject::connect(openconnect, &QProcess::readyReadStandardOutput, this, &GPService::onProcessStdout); |  | ||||||
|     QObject::connect(openconnect, &QProcess::readyReadStandardError, this, &GPService::onProcessStderr); |  | ||||||
|     QObject::connect(openconnect, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), this, &GPService::onProcessFinished); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| GPService::~GPService() |  | ||||||
| { |  | ||||||
|     delete openconnect; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| QString GPService::findBinary() |  | ||||||
| { |  | ||||||
|     for (int i = 0; i < binaryPaths->length(); i++) { |  | ||||||
|         if (QFileInfo::exists(binaryPaths[i])) { |  | ||||||
|             return binaryPaths[i]; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|     return nullptr; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void GPService::quit() |  | ||||||
| { |  | ||||||
|     if (openconnect->state() == QProcess::NotRunning) { |  | ||||||
|         exit(0); |  | ||||||
|     } else { |  | ||||||
|         aboutToQuit = true; |  | ||||||
|         openconnect->terminate(); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void GPService::connect(QString server, QString username, QString passwd) |  | ||||||
| { |  | ||||||
|     if (vpnStatus != GPService::VpnNotConnected) { |  | ||||||
|         log("VPN status is: " + QVariant::fromValue(vpnStatus).toString()); |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     QString bin = findBinary(); |  | ||||||
|     if (bin == nullptr) { |  | ||||||
|         log("Could not found openconnect binary, make sure openconnect is installed, exiting."); |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     QStringList args; |  | ||||||
|     args << QCoreApplication::arguments().mid(1) |  | ||||||
|      << "--protocol=gp" |  | ||||||
|      << "-u" << username |  | ||||||
|      << "-C" << passwd |  | ||||||
|      << server; |  | ||||||
|  |  | ||||||
|     openconnect->start(bin, args); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void GPService::disconnect() |  | ||||||
| { |  | ||||||
|     if (openconnect->state() != QProcess::NotRunning) { |  | ||||||
|         vpnStatus = GPService::VpnDisconnecting; |  | ||||||
|         openconnect->terminate(); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| int GPService::status() |  | ||||||
| { |  | ||||||
|     return vpnStatus; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void GPService::onProcessStarted() |  | ||||||
| { |  | ||||||
|     log("Openconnect started successfully, PID=" + QString::number(openconnect->processId())); |  | ||||||
|     vpnStatus = GPService::VpnConnecting; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void GPService::onProcessError(QProcess::ProcessError error) |  | ||||||
| { |  | ||||||
|     log("Error occurred: " + QVariant::fromValue(error).toString()); |  | ||||||
|     vpnStatus = GPService::VpnNotConnected; |  | ||||||
|     emit disconnected(); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void GPService::onProcessStdout() |  | ||||||
| { |  | ||||||
|     QString output = openconnect->readAllStandardOutput(); |  | ||||||
|  |  | ||||||
|     log(output); |  | ||||||
|     if (output.indexOf("Connected as") >= 0) { |  | ||||||
|         vpnStatus = GPService::VpnConnected; |  | ||||||
|         emit connected(); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void GPService::onProcessStderr() |  | ||||||
| { |  | ||||||
|     log(openconnect->readAllStandardError()); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void GPService::onProcessFinished(int exitCode, QProcess::ExitStatus exitStatus) |  | ||||||
| { |  | ||||||
|     log("Openconnect process exited with code " + QString::number(exitCode) + " and exit status " + QVariant::fromValue(exitStatus).toString()); |  | ||||||
|     vpnStatus = GPService::VpnNotConnected; |  | ||||||
|     emit disconnected(); |  | ||||||
|  |  | ||||||
|     if (aboutToQuit) { |  | ||||||
|         exit(0); |  | ||||||
|     }; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void GPService::log(QString msg) |  | ||||||
| { |  | ||||||
|     emit logAvailable(msg); |  | ||||||
| } |  | ||||||
| @@ -1,58 +0,0 @@ | |||||||
| #ifndef GLOBALPROTECTSERVICE_H |  | ||||||
| #define GLOBALPROTECTSERVICE_H |  | ||||||
|  |  | ||||||
| #include <QObject> |  | ||||||
| #include <QProcess> |  | ||||||
|  |  | ||||||
| static const QString binaryPaths[] { |  | ||||||
|     "/usr/local/bin/openconnect", |  | ||||||
|     "/usr/local/sbin/openconnect", |  | ||||||
|     "/usr/bin/openconnect", |  | ||||||
|     "/usr/sbin/openconnect", |  | ||||||
|     "/opt/bin/openconnect", |  | ||||||
|     "/opt/sbin/openconnect" |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| class GPService : public QObject |  | ||||||
| { |  | ||||||
|     Q_OBJECT |  | ||||||
|     Q_CLASSINFO("D-Bus Interface", "com.yuezk.qt.GPService") |  | ||||||
| public: |  | ||||||
|     explicit GPService(QObject *parent = nullptr); |  | ||||||
|     ~GPService(); |  | ||||||
|  |  | ||||||
|     enum VpnStatus { |  | ||||||
|         VpnNotConnected, |  | ||||||
|         VpnConnecting, |  | ||||||
|         VpnConnected, |  | ||||||
|         VpnDisconnecting, |  | ||||||
|     }; |  | ||||||
|  |  | ||||||
| signals: |  | ||||||
|     void connected(); |  | ||||||
|     void disconnected(); |  | ||||||
|     void logAvailable(QString log); |  | ||||||
|  |  | ||||||
| public slots: |  | ||||||
|     void connect(QString server, QString username, QString passwd); |  | ||||||
|     void disconnect(); |  | ||||||
|     int status(); |  | ||||||
|     void quit(); |  | ||||||
|  |  | ||||||
| private slots: |  | ||||||
|     void onProcessStarted(); |  | ||||||
|     void onProcessError(QProcess::ProcessError error); |  | ||||||
|     void onProcessStdout(); |  | ||||||
|     void onProcessStderr(); |  | ||||||
|     void onProcessFinished(int exitCode, QProcess::ExitStatus exitStatus); |  | ||||||
|  |  | ||||||
| private: |  | ||||||
|     QProcess *openconnect; |  | ||||||
|     bool aboutToQuit = false; |  | ||||||
|     int vpnStatus = GPService::VpnNotConnected; |  | ||||||
|  |  | ||||||
|     void log(QString msg); |  | ||||||
|     static QString findBinary(); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| #endif // GLOBALPROTECTSERVICE_H |  | ||||||
| @@ -1,22 +0,0 @@ | |||||||
| <!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN" "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd"> |  | ||||||
| <node> |  | ||||||
|   <interface name="com.yuezk.qt.GPService"> |  | ||||||
|     <signal name="connected"> |  | ||||||
|     </signal> |  | ||||||
|     <signal name="disconnected"> |  | ||||||
|     </signal> |  | ||||||
|     <signal name="logAvailable"> |  | ||||||
|       <arg name="log" type="s" /> |  | ||||||
|     </signal> |  | ||||||
|     <method name="connect"> |  | ||||||
|       <arg name="server" type="s" direction="in"/> |  | ||||||
|       <arg name="username" type="s" direction="in"/> |  | ||||||
|       <arg name="passwd" type="s" direction="in"/> |  | ||||||
|     </method> |  | ||||||
|     <method name="disconnect"> |  | ||||||
|     </method> |  | ||||||
|     <method name="status"> |  | ||||||
|       <arg type="i" direction="out"/> |  | ||||||
|     </method> |  | ||||||
|   </interface> |  | ||||||
| </node> |  | ||||||
| @@ -1,26 +0,0 @@ | |||||||
| #include <QtDBus> |  | ||||||
| #include "gpservice.h" |  | ||||||
| #include "singleapplication.h" |  | ||||||
| #include "sigwatch.h" |  | ||||||
|  |  | ||||||
| int main(int argc, char *argv[]) |  | ||||||
| { |  | ||||||
|     SingleApplication app(argc, argv); |  | ||||||
|  |  | ||||||
|     if (!QDBusConnection::systemBus().isConnected()) { |  | ||||||
|         qWarning("Cannot connect to the D-Bus session bus.\n" |  | ||||||
|                  "Please check your system settings and try again.\n"); |  | ||||||
|         return 1; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     GPService service; |  | ||||||
|  |  | ||||||
|     UnixSignalWatcher sigwatch; |  | ||||||
|     sigwatch.watchForSignal(SIGINT); |  | ||||||
|     sigwatch.watchForSignal(SIGTERM); |  | ||||||
|     sigwatch.watchForSignal(SIGQUIT); |  | ||||||
|     sigwatch.watchForSignal(SIGHUP); |  | ||||||
|     QObject::connect(&sigwatch, &UnixSignalWatcher::unixSignal, &service, &GPService::quit); |  | ||||||
|  |  | ||||||
|     return app.exec(); |  | ||||||
| } |  | ||||||
| @@ -1,176 +0,0 @@ | |||||||
| /* |  | ||||||
|  * Unix signal watcher for Qt. |  | ||||||
|  * |  | ||||||
|  * Copyright (C) 2014 Simon Knopp |  | ||||||
|  * |  | ||||||
|  * Permission is hereby granted, free of charge, to any person obtaining a copy |  | ||||||
|  * of this software and associated documentation files (the "Software"), to deal |  | ||||||
|  * in the Software without restriction, including without limitation the rights |  | ||||||
|  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |  | ||||||
|  * copies of the Software, and to permit persons to whom the Software is |  | ||||||
|  * furnished to do so, subject to the following conditions: |  | ||||||
|  * |  | ||||||
|  * The above copyright notice and this permission notice shall be included in |  | ||||||
|  * all copies or substantial portions of the Software. |  | ||||||
|  * |  | ||||||
|  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |  | ||||||
|  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |  | ||||||
|  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |  | ||||||
|  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |  | ||||||
|  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |  | ||||||
|  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |  | ||||||
|  * SOFTWARE. |  | ||||||
|  */ |  | ||||||
|  |  | ||||||
| #include <sys/socket.h> |  | ||||||
| #include <unistd.h> |  | ||||||
| #include <errno.h> |  | ||||||
| #include <QMap> |  | ||||||
| #include <QSocketNotifier> |  | ||||||
| #include <QDebug> |  | ||||||
| #include "sigwatch.h" |  | ||||||
|  |  | ||||||
|  |  | ||||||
| /*! |  | ||||||
|  * \brief The UnixSignalWatcherPrivate class implements the back-end signal |  | ||||||
|  * handling for the UnixSignalWatcher. |  | ||||||
|  * |  | ||||||
|  * \see http://qt-project.org/doc/qt-5.0/qtdoc/unix-signals.html |  | ||||||
|  */ |  | ||||||
| class UnixSignalWatcherPrivate : public QObject |  | ||||||
| { |  | ||||||
|     UnixSignalWatcher * const q_ptr; |  | ||||||
|     Q_DECLARE_PUBLIC(UnixSignalWatcher) |  | ||||||
|  |  | ||||||
| public: |  | ||||||
|     UnixSignalWatcherPrivate(UnixSignalWatcher *q); |  | ||||||
|     ~UnixSignalWatcherPrivate(); |  | ||||||
|  |  | ||||||
|     void watchForSignal(int signal); |  | ||||||
|     static void signalHandler(int signal); |  | ||||||
|  |  | ||||||
|     void _q_onNotify(int sockfd); |  | ||||||
|  |  | ||||||
| private: |  | ||||||
|     static int sockpair[2]; |  | ||||||
|     QSocketNotifier *notifier; |  | ||||||
|     QList<int> watchedSignals; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
|  |  | ||||||
| int UnixSignalWatcherPrivate::sockpair[2]; |  | ||||||
|  |  | ||||||
| UnixSignalWatcherPrivate::UnixSignalWatcherPrivate(UnixSignalWatcher *q) : |  | ||||||
|     q_ptr(q) |  | ||||||
| { |  | ||||||
|     // Create socket pair |  | ||||||
|     if (::socketpair(AF_UNIX, SOCK_STREAM, 0, sockpair)) { |  | ||||||
|         qDebug() << "UnixSignalWatcher: socketpair: " << ::strerror(errno); |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     // Create a notifier for the read end of the pair |  | ||||||
|     notifier = new QSocketNotifier(sockpair[1], QSocketNotifier::Read); |  | ||||||
|     QObject::connect(notifier, SIGNAL(activated(int)), q, SLOT(_q_onNotify(int))); |  | ||||||
|     notifier->setEnabled(true); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| UnixSignalWatcherPrivate::~UnixSignalWatcherPrivate() |  | ||||||
| { |  | ||||||
|     delete notifier; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /*! |  | ||||||
|  * Registers a handler for the given Unix \a signal. The handler will write to |  | ||||||
|  * a socket pair, the other end of which is connected to a QSocketNotifier. |  | ||||||
|  * This provides a way to break out of the asynchronous context from which the |  | ||||||
|  * signal handler is called and back into the Qt event loop. |  | ||||||
|  */ |  | ||||||
| void UnixSignalWatcherPrivate::watchForSignal(int signal) |  | ||||||
| { |  | ||||||
|     if (watchedSignals.contains(signal)) { |  | ||||||
|         qDebug() << "Already watching for signal" << signal; |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     // Register a sigaction which will write to the socket pair |  | ||||||
|     struct sigaction sigact; |  | ||||||
|     sigact.sa_handler = UnixSignalWatcherPrivate::signalHandler; |  | ||||||
|     sigact.sa_flags = 0; |  | ||||||
|     ::sigemptyset(&sigact.sa_mask); |  | ||||||
|     sigact.sa_flags |= SA_RESTART; |  | ||||||
|     if (::sigaction(signal, &sigact, NULL)) { |  | ||||||
|         qDebug() << "UnixSignalWatcher: sigaction: " << ::strerror(errno); |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     watchedSignals.append(signal); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /*! |  | ||||||
|  * Called when a Unix \a signal is received. Write to the socket to wake up the |  | ||||||
|  * QSocketNotifier. |  | ||||||
|  */ |  | ||||||
| void UnixSignalWatcherPrivate::signalHandler(int signal) |  | ||||||
| { |  | ||||||
|     ssize_t nBytes = ::write(sockpair[0], &signal, sizeof(signal)); |  | ||||||
|     Q_UNUSED(nBytes); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /*! |  | ||||||
|  * Called when the signal handler has written to the socket pair. Emits the Unix |  | ||||||
|  * signal as a Qt signal. |  | ||||||
|  */ |  | ||||||
| void UnixSignalWatcherPrivate::_q_onNotify(int sockfd) |  | ||||||
| { |  | ||||||
|     Q_Q(UnixSignalWatcher); |  | ||||||
|  |  | ||||||
|     int signal; |  | ||||||
|     ssize_t nBytes = ::read(sockfd, &signal, sizeof(signal)); |  | ||||||
|     Q_UNUSED(nBytes); |  | ||||||
|     qDebug() << "Caught signal:" << ::strsignal(signal); |  | ||||||
|     emit q->unixSignal(signal); |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  |  | ||||||
| /*! |  | ||||||
|  * Create a new UnixSignalWatcher as a child of the given \a parent. |  | ||||||
|  */ |  | ||||||
| UnixSignalWatcher::UnixSignalWatcher(QObject *parent) : |  | ||||||
|     QObject(parent), |  | ||||||
|     d_ptr(new UnixSignalWatcherPrivate(this)) |  | ||||||
| { |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /*! |  | ||||||
|  * Destroy this UnixSignalWatcher. |  | ||||||
|  */ |  | ||||||
| UnixSignalWatcher::~UnixSignalWatcher() |  | ||||||
| { |  | ||||||
|     delete d_ptr; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /*! |  | ||||||
|  * Register a signal handler for the given \a signal. |  | ||||||
|  * |  | ||||||
|  * After calling this method you can \c connect() to the unixSignal() Qt signal |  | ||||||
|  * to be notified when the Unix signal is received. |  | ||||||
|  */ |  | ||||||
| void UnixSignalWatcher::watchForSignal(int signal) |  | ||||||
| { |  | ||||||
|     Q_D(UnixSignalWatcher); |  | ||||||
|     d->watchForSignal(signal); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /*! |  | ||||||
|  * \fn void UnixSignalWatcher::unixSignal(int signal) |  | ||||||
|  * Emitted when the given Unix \a signal is received. |  | ||||||
|  * |  | ||||||
|  * watchForSignal() must be called for each Unix signal that you want to receive |  | ||||||
|  * via the unixSignal() Qt signal. If a watcher is watching multiple signals, |  | ||||||
|  * unixSignal() will be emitted whenever *any* of the watched Unix signals are |  | ||||||
|  * received, and the \a signal argument can be inspected to find out which one |  | ||||||
|  * was actually received. |  | ||||||
|  */ |  | ||||||
|  |  | ||||||
| #include "moc_sigwatch.cpp" |  | ||||||
| @@ -1,59 +0,0 @@ | |||||||
| /* |  | ||||||
|  * Unix signal watcher for Qt. |  | ||||||
|  * |  | ||||||
|  * Copyright (C) 2014 Simon Knopp |  | ||||||
|  * |  | ||||||
|  * Permission is hereby granted, free of charge, to any person obtaining a copy |  | ||||||
|  * of this software and associated documentation files (the "Software"), to deal |  | ||||||
|  * in the Software without restriction, including without limitation the rights |  | ||||||
|  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |  | ||||||
|  * copies of the Software, and to permit persons to whom the Software is |  | ||||||
|  * furnished to do so, subject to the following conditions: |  | ||||||
|  * |  | ||||||
|  * The above copyright notice and this permission notice shall be included in |  | ||||||
|  * all copies or substantial portions of the Software. |  | ||||||
|  * |  | ||||||
|  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |  | ||||||
|  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |  | ||||||
|  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |  | ||||||
|  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |  | ||||||
|  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |  | ||||||
|  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |  | ||||||
|  * SOFTWARE. |  | ||||||
|  */ |  | ||||||
|  |  | ||||||
| #ifndef SIGWATCH_H |  | ||||||
| #define SIGWATCH_H |  | ||||||
|  |  | ||||||
| #include <QObject> |  | ||||||
| #include <signal.h> |  | ||||||
|  |  | ||||||
| class UnixSignalWatcherPrivate; |  | ||||||
|  |  | ||||||
|  |  | ||||||
| /*! |  | ||||||
|  * \brief The UnixSignalWatcher class converts Unix signals to Qt signals. |  | ||||||
|  * |  | ||||||
|  * To watch for a given signal, e.g. \c SIGINT, call \c watchForSignal(SIGINT) |  | ||||||
|  * and \c connect() your handler to unixSignal(). |  | ||||||
|  */ |  | ||||||
|  |  | ||||||
| class UnixSignalWatcher : public QObject |  | ||||||
| { |  | ||||||
|     Q_OBJECT |  | ||||||
| public: |  | ||||||
|     explicit UnixSignalWatcher(QObject *parent = 0); |  | ||||||
|     ~UnixSignalWatcher(); |  | ||||||
|  |  | ||||||
|     void watchForSignal(int signal); |  | ||||||
|  |  | ||||||
| signals: |  | ||||||
|     void unixSignal(int signal); |  | ||||||
|  |  | ||||||
| private: |  | ||||||
|     UnixSignalWatcherPrivate * const d_ptr; |  | ||||||
|     Q_DECLARE_PRIVATE(UnixSignalWatcher) |  | ||||||
|     Q_PRIVATE_SLOT(d_func(), void _q_onNotify(int)) |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| #endif // SIGWATCH_H |  | ||||||
| @@ -1,11 +0,0 @@ | |||||||
| [Unit] |  | ||||||
| Description=GlobalProtect openconnect DBus service |  | ||||||
|  |  | ||||||
| [Service] |  | ||||||
| Environment="LANG=en_US.utf8" |  | ||||||
| Type=dbus |  | ||||||
| BusName=com.yuezk.qt.GPService |  | ||||||
| ExecStart=/usr/bin/gpservice |  | ||||||
|  |  | ||||||
| [Install] |  | ||||||
| WantedBy=multi-user.target |  | ||||||
| @@ -1,5 +0,0 @@ | |||||||
| TEMPLATE = subdirs |  | ||||||
|  |  | ||||||
| SUBDIRS += \ |  | ||||||
|     GPClient \ |  | ||||||
|     GPService |  | ||||||
							
								
								
									
										160
									
								
								README.md
									
									
									
									
									
								
							
							
						
						| @@ -1,57 +1,151 @@ | |||||||
| # GlobalProtect-openconnect | # 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"> | <p align="center"> | ||||||
|   <img src="screenshot.png"> |   <img width="300" src="https://github.com/yuezk/GlobalProtect-openconnect/assets/3297602/9242df9c-217d-42ab-8c21-8f9f69cd4eb5"> | ||||||
| </p> | </p> | ||||||
|  |  | ||||||
| ## Features | ## Features | ||||||
|  |  | ||||||
| - Similar user experience as the official client in macOS. | - [x] Better Linux support | ||||||
| - Supports both SAML and non-SAML authentication modes. | - [x] Support both CLI and GUI | ||||||
| - Supports automatically selecting the preferred gateway from the multiple gateways. | - [x] Support both SSO and non-SSO authentication | ||||||
| - Supports switching gateway from the system tray menu manually. | - [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 | ||||||
|  |  | ||||||
| ## Prerequisites | ## Usage | ||||||
|  |  | ||||||
| - Openconnect v8.x | ### CLI | ||||||
| - Qt5, qt5-webengine, qt5-websockets |  | ||||||
|  |  | ||||||
| ### Ubuntu | The CLI version is always free and open source in this repo. It has almost the same features as the GUI version. | ||||||
| 1. Install openconnect v8.x |  | ||||||
|  |  | ||||||
|    For Ubuntu 18.04 you might need to [build the latest openconnect from source code](https://gist.github.com/yuezk/ab9a4b87a9fa0182bdb2df41fab5f613). |  | ||||||
|     |  | ||||||
| 2. Install the Qt dependencies |  | ||||||
|     ```sh |  | ||||||
|     sudo apt install qt5-default libqt5websockets5-dev qtwebengine5-dev |  | ||||||
| ``` | ``` | ||||||
| ### OpenSUSE | Usage: gpclient [OPTIONS] <COMMAND> | ||||||
| Install the Qt dependencies |  | ||||||
|  |  | ||||||
| ```sh | Commands: | ||||||
| sudo zypper install libqt5-qtbase-devel libqt5-qtwebsockets-devel libqt5-qtwebengine-devel |   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) | ||||||
|  |  | ||||||
|  | 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 | ||||||
|  |  | ||||||
|  | See 'gpclient help <command>' for more information on a specific command. | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| ## Install | ### GUI | ||||||
|  |  | ||||||
| ### Install from AUR (Arch/Manjaro) | 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. | ||||||
|  |  | ||||||
| Install [globalprotect-openconnect](https://aur.archlinux.org/packages/globalprotect-openconnect/). | > [!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. | ||||||
|  |  | ||||||
| ### Build from source code | ## Installation | ||||||
|  |  | ||||||
| ```sh | > [!Note] | ||||||
| git clone https://github.com/yuezk/GlobalProtect-openconnect.git | > | ||||||
| cd GlobalProtect-openconnect | > 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. | ||||||
| git submodule update --init |  | ||||||
|  | > [!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 | ||||||
|  |  | ||||||
| # qmake or qmake-qt5 |  | ||||||
| qmake CONFIG+=release |  | ||||||
| make |  | ||||||
| sudo make install |  | ||||||
| ``` | ``` | ||||||
| Open `GlobalProtect VPN` in the application dashboard. | 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 | ||||||
|  |  | ||||||
|  | #### Install from AUR | ||||||
|  |  | ||||||
|  | Install from AUR: [globalprotect-openconnect-git](https://aur.archlinux.org/packages/globalprotect-openconnect-git/) | ||||||
|  |  | ||||||
|  | ``` | ||||||
|  | yay -S globalprotect-openconnect-git | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | #### Install from package | ||||||
|  |  | ||||||
|  | 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 | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | #### Install from OBS (OpenSUSE Build Service) | ||||||
|  |  | ||||||
|  | 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. | ||||||
|  |  | ||||||
|  | #### Install from RPM package | ||||||
|  |  | ||||||
|  | Download the latest RPM package from [releases](https://github.com/yuezk/GlobalProtect-openconnect/releases) page. | ||||||
|  |  | ||||||
|  | ### Other distributions | ||||||
|  |  | ||||||
|  | - 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 | ||||||
|  |  | ||||||
|  | 1. How to deal with error `Secure Storage not ready` | ||||||
|  |    | ||||||
|  |    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)). | ||||||
|  |  | ||||||
|  | 2. How to deal with error `(gpauth:18869): Gtk-WARNING **: 10:33:37.566: cannot open display:` | ||||||
|  |    | ||||||
|  |    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)). | ||||||
|  |  | ||||||
|  | ## About Trial | ||||||
|  |  | ||||||
|  | The CLI version is always free, while the GUI version is paid. There are two trial modes for the GUI version: | ||||||
|  |  | ||||||
|  | 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) | ## [License](./LICENSE) | ||||||
|  |  | ||||||
| GPLv3 | GPLv3 | ||||||
|   | |||||||
							
								
								
									
										23
									
								
								apps/gpauth/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,23 @@ | |||||||
|  | [package] | ||||||
|  | name = "gpauth" | ||||||
|  | version.workspace = true | ||||||
|  | edition.workspace = true | ||||||
|  | license.workspace = true | ||||||
|  |  | ||||||
|  | [build-dependencies] | ||||||
|  | tauri-build = { version = "1.5", features = [] } | ||||||
|  |  | ||||||
|  | [dependencies] | ||||||
|  | gpapi = { path = "../../crates/gpapi", features = ["tauri", "clap"] } | ||||||
|  | anyhow.workspace = true | ||||||
|  | clap.workspace = true | ||||||
|  | env_logger.workspace = true | ||||||
|  | log.workspace = true | ||||||
|  | regex.workspace = true | ||||||
|  | serde_json.workspace = true | ||||||
|  | tokio.workspace = true | ||||||
|  | tokio-util.workspace = true | ||||||
|  | tempfile.workspace = true | ||||||
|  | webkit2gtk = "0.18.2" | ||||||
|  | tauri = { workspace = true, features = ["http-all"] } | ||||||
|  | compile-time.workspace = true | ||||||
							
								
								
									
										3
									
								
								apps/gpauth/build.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,3 @@ | |||||||
|  | fn main() { | ||||||
|  |   tauri_build::build() | ||||||
|  | } | ||||||
							
								
								
									
										
											BIN
										
									
								
								apps/gpauth/icons/128x128.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 13 KiB | 
							
								
								
									
										
											BIN
										
									
								
								apps/gpauth/icons/128x128@2x.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 28 KiB | 
							
								
								
									
										
											BIN
										
									
								
								apps/gpauth/icons/32x32.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 2.5 KiB | 
							
								
								
									
										
											BIN
										
									
								
								apps/gpauth/icons/icon.icns
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								apps/gpauth/icons/icon.ico
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 44 KiB | 
							
								
								
									
										
											BIN
										
									
								
								apps/gpauth/icons/icon.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 83 KiB | 
							
								
								
									
										11
									
								
								apps/gpauth/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,11 @@ | |||||||
|  | <!DOCTYPE html> | ||||||
|  | <html lang="en"> | ||||||
|  | <head> | ||||||
|  |   <meta charset="UTF-8"> | ||||||
|  |   <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||||||
|  |   <title>GlobalProtect Login</title> | ||||||
|  | </head> | ||||||
|  | <body> | ||||||
|  |   <p>Redirecting to GlobalProtect Login...</p> | ||||||
|  | </body> | ||||||
|  | </html> | ||||||
							
								
								
									
										491
									
								
								apps/gpauth/src/auth_window.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,491 @@ | |||||||
|  | use std::{ | ||||||
|  |   rc::Rc, | ||||||
|  |   sync::Arc, | ||||||
|  |   time::{Duration, Instant}, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | use anyhow::bail; | ||||||
|  | use gpapi::{ | ||||||
|  |   auth::SamlAuthData, | ||||||
|  |   gp_params::GpParams, | ||||||
|  |   portal::{prelogin, Prelogin}, | ||||||
|  |   utils::{redact::redact_uri, window::WindowExt}, | ||||||
|  | }; | ||||||
|  | use log::{info, warn}; | ||||||
|  | use regex::Regex; | ||||||
|  | use tauri::{AppHandle, Window, WindowEvent, WindowUrl}; | ||||||
|  | use tokio::sync::{mpsc, oneshot, RwLock}; | ||||||
|  | use tokio_util::sync::CancellationToken; | ||||||
|  | use webkit2gtk::{ | ||||||
|  |   gio::Cancellable, | ||||||
|  |   glib::{GString, TimeSpan}, | ||||||
|  |   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, | ||||||
|  |   /// No auth data found in headers/body | ||||||
|  |   NotFound, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type AuthResult = Result<SamlAuthData, AuthDataError>; | ||||||
|  |  | ||||||
|  | pub(crate) struct AuthWindow<'a> { | ||||||
|  |   app_handle: AppHandle, | ||||||
|  |   server: &'a str, | ||||||
|  |   saml_request: &'a str, | ||||||
|  |   user_agent: &'a str, | ||||||
|  |   gp_params: Option<GpParams>, | ||||||
|  |   clean: bool, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl<'a> AuthWindow<'a> { | ||||||
|  |   pub fn new(app_handle: AppHandle) -> Self { | ||||||
|  |     Self { | ||||||
|  |       app_handle, | ||||||
|  |       server: "", | ||||||
|  |       saml_request: "", | ||||||
|  |       user_agent: "", | ||||||
|  |       gp_params: None, | ||||||
|  |       clean: false, | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   pub fn server(mut self, server: &'a str) -> Self { | ||||||
|  |     self.server = server; | ||||||
|  |     self | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   pub fn saml_request(mut self, saml_request: &'a str) -> Self { | ||||||
|  |     self.saml_request = saml_request; | ||||||
|  |     self | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   pub fn user_agent(mut self, user_agent: &'a str) -> Self { | ||||||
|  |     self.user_agent = user_agent; | ||||||
|  |     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 | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   pub async fn open(&self) -> anyhow::Result<SamlAuthData> { | ||||||
|  |     info!("Open auth window, user_agent: {}", self.user_agent); | ||||||
|  |  | ||||||
|  |     let window = Window::builder(&self.app_handle, "auth_window", WindowUrl::default()) | ||||||
|  |       .title("GlobalProtect Login") | ||||||
|  |       // .user_agent(self.user_agent) | ||||||
|  |       .focused(true) | ||||||
|  |       .visible(false) | ||||||
|  |       .center() | ||||||
|  |       .build()?; | ||||||
|  |  | ||||||
|  |     let window = Arc::new(window); | ||||||
|  |  | ||||||
|  |     let cancel_token = CancellationToken::new(); | ||||||
|  |     let cancel_token_clone = cancel_token.clone(); | ||||||
|  |  | ||||||
|  |     window.on_window_event(move |event| { | ||||||
|  |       if let WindowEvent::CloseRequested { .. } = event { | ||||||
|  |         cancel_token_clone.cancel(); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     let window_clone = Arc::clone(&window); | ||||||
|  |     let timeout_secs = 15; | ||||||
|  |     tokio::spawn(async move { | ||||||
|  |       tokio::time::sleep(Duration::from_secs(timeout_secs)).await; | ||||||
|  |       let visible = window_clone.is_visible().unwrap_or(false); | ||||||
|  |       if !visible { | ||||||
|  |         info!("Try to raise auth window after {} seconds", timeout_secs); | ||||||
|  |         raise_window(&window_clone); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     tokio::select! { | ||||||
|  |       _ = cancel_token.cancelled() => { | ||||||
|  |         bail!("Auth cancelled"); | ||||||
|  |       } | ||||||
|  |       saml_result = self.auth_loop(&window) => { | ||||||
|  |         window.close()?; | ||||||
|  |         saml_result | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   async fn auth_loop(&self, window: &Arc<Window>) -> anyhow::Result<SamlAuthData> { | ||||||
|  |     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?; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     let raise_window_cancel_token_clone = Arc::clone(&raise_window_cancel_token); | ||||||
|  |     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); | ||||||
|  |  | ||||||
|  |       let auth_result_tx_clone = auth_result_tx.clone(); | ||||||
|  |       wv.connect_load_changed(move |wv, event| { | ||||||
|  |         if event == LoadEvent::Started { | ||||||
|  |           let Ok(mut cancel_token) = raise_window_cancel_token_clone.try_write() else { | ||||||
|  |             return; | ||||||
|  |           }; | ||||||
|  |  | ||||||
|  |           // Cancel the raise window task | ||||||
|  |           if let Some(cancel_token) = cancel_token.take() { | ||||||
|  |             cancel_token.cancel(); | ||||||
|  |           } | ||||||
|  |           return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if event != LoadEvent::Finished { | ||||||
|  |           return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if let Some(main_resource) = wv.main_resource() { | ||||||
|  |           let uri = main_resource.uri().unwrap_or("".into()); | ||||||
|  |  | ||||||
|  |           if uri.is_empty() { | ||||||
|  |             warn!("Loaded an empty uri"); | ||||||
|  |             send_auth_result(&auth_result_tx_clone, Err(AuthDataError::Invalid)); | ||||||
|  |             return; | ||||||
|  |           } | ||||||
|  |  | ||||||
|  |           info!("Loaded uri: {}", redact_uri(&uri)); | ||||||
|  |           read_auth_data(&main_resource, auth_result_tx_clone.clone()); | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       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); | ||||||
|  |         // 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(); | ||||||
|  |  | ||||||
|  |     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"); | ||||||
|  |  | ||||||
|  |             // The user may need to interact with the auth window, raise it in 3 seconds | ||||||
|  |             if !window.is_visible().unwrap_or(false) { | ||||||
|  |               let window = Arc::clone(window); | ||||||
|  |               let cancel_token = CancellationToken::new(); | ||||||
|  |  | ||||||
|  |               raise_window_cancel_token.write().await.replace(cancel_token.clone()); | ||||||
|  |  | ||||||
|  |               tokio::spawn(async move { | ||||||
|  |                 let delay_secs = 1; | ||||||
|  |  | ||||||
|  |                 info!("Raise window in {} second(s)", delay_secs); | ||||||
|  |                 tokio::select! { | ||||||
|  |                   _ = tokio::time::sleep(Duration::from_secs(delay_secs)) => { | ||||||
|  |                     raise_window(&window); | ||||||
|  |                   } | ||||||
|  |                   _ = cancel_token.cancelled() => { | ||||||
|  |                     info!("Raise window cancelled"); | ||||||
|  |                   } | ||||||
|  |                 } | ||||||
|  |               }); | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |           Err(AuthDataError::Invalid) => { | ||||||
|  |             info!("Got invalid auth data, retrying..."); | ||||||
|  |  | ||||||
|  |             window.with_webview(|wv| { | ||||||
|  |               let wv = wv.inner(); | ||||||
|  |               wv.run_javascript(r#" | ||||||
|  |                   var loading = document.createElement("div"); | ||||||
|  |                   loading.innerHTML = '<div style="position: absolute; width: 100%; text-align: center; font-size: 20px; font-weight: bold; top: 50%; left: 50%; transform: translate(-50%, -50%);">Got invalid token, retrying...</div>'; | ||||||
|  |                   loading.style = "position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(255, 255, 255, 0.85); z-index: 99999;"; | ||||||
|  |                   document.body.appendChild(loading); | ||||||
|  |               "#, | ||||||
|  |                   Cancellable::NONE, | ||||||
|  |                   |_| info!("Injected loading element successfully"), | ||||||
|  |               ); | ||||||
|  |             })?; | ||||||
|  |  | ||||||
|  |             let saml_request = portal_prelogin(&portal, gp_params).await?; | ||||||
|  |             window.with_webview(move |wv| { | ||||||
|  |               let wv = wv.inner(); | ||||||
|  |               load_saml_request(&wv, &saml_request); | ||||||
|  |             })?; | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fn raise_window(window: &Arc<Window>) { | ||||||
|  |   let visible = window.is_visible().unwrap_or(false); | ||||||
|  |   if !visible { | ||||||
|  |     if let Err(err) = window.raise() { | ||||||
|  |       warn!("Failed to raise window: {}", err); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | 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(_) => bail!("Received non-SAML prelogin response"), | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fn send_auth_result(auth_result_tx: &mpsc::UnboundedSender<AuthResult>, auth_result: AuthResult) { | ||||||
|  |   if let Err(err) = auth_result_tx.send(auth_result) { | ||||||
|  |     warn!("Failed to send auth event: {}", err); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fn load_saml_request(wv: &Rc<WebView>, saml_request: &str) { | ||||||
|  |   if saml_request.starts_with("http") { | ||||||
|  |     info!("Load the SAML request as URI..."); | ||||||
|  |     wv.load_uri(saml_request); | ||||||
|  |   } else { | ||||||
|  |     info!("Load the SAML request as HTML..."); | ||||||
|  |     wv.load_html(saml_request, None); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fn read_auth_data_from_headers(response: &URIResponse) -> AuthResult { | ||||||
|  |   response.http_headers().map_or_else( | ||||||
|  |     || { | ||||||
|  |       info!("No headers found in response"); | ||||||
|  |       Err(AuthDataError::NotFound) | ||||||
|  |     }, | ||||||
|  |     |mut headers| match headers.get("saml-auth-status") { | ||||||
|  |       Some(status) if status == "1" => { | ||||||
|  |         let username = headers.get("saml-username").map(GString::into); | ||||||
|  |         let prelogin_cookie = headers.get("prelogin-cookie").map(GString::into); | ||||||
|  |         let portal_userauthcookie = headers.get("portal-userauthcookie").map(GString::into); | ||||||
|  |  | ||||||
|  |         if SamlAuthData::check(&username, &prelogin_cookie, &portal_userauthcookie) { | ||||||
|  |           return Ok(SamlAuthData::new( | ||||||
|  |             username.unwrap(), | ||||||
|  |             prelogin_cookie, | ||||||
|  |             portal_userauthcookie, | ||||||
|  |           )); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         info!("Found invalid auth data in headers"); | ||||||
|  |         Err(AuthDataError::Invalid) | ||||||
|  |       } | ||||||
|  |       Some(status) => { | ||||||
|  |         info!("Found invalid SAML status: {} in headers", status); | ||||||
|  |         Err(AuthDataError::Invalid) | ||||||
|  |       } | ||||||
|  |       None => { | ||||||
|  |         info!("No saml-auth-status header found"); | ||||||
|  |         Err(AuthDataError::NotFound) | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |   ) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fn read_auth_data_from_body<F>(main_resource: &WebResource, callback: F) | ||||||
|  | where | ||||||
|  |   F: FnOnce(AuthResult) + Send + 'static, | ||||||
|  | { | ||||||
|  |   main_resource.data(Cancellable::NONE, |data| match data { | ||||||
|  |     Ok(data) => { | ||||||
|  |       let html = String::from_utf8_lossy(&data); | ||||||
|  |       callback(read_auth_data_from_html(&html)); | ||||||
|  |     } | ||||||
|  |     Err(err) => { | ||||||
|  |       info!("Failed to read response body: {}", err); | ||||||
|  |       callback(Err(AuthDataError::Invalid)) | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fn read_auth_data_from_html(html: &str) -> AuthResult { | ||||||
|  |   if html.contains("Temporarily Unavailable") { | ||||||
|  |     info!("Found 'Temporarily Unavailable' in HTML, auth failed"); | ||||||
|  |     return Err(AuthDataError::Invalid); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   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, | ||||||
|  |         )); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       info!("Found invalid auth data in HTML"); | ||||||
|  |       Err(AuthDataError::Invalid) | ||||||
|  |     } | ||||||
|  |     Some(status) => { | ||||||
|  |       info!("Found invalid SAML status {} in HTML", status); | ||||||
|  |       Err(AuthDataError::Invalid) | ||||||
|  |     } | ||||||
|  |     None => { | ||||||
|  |       info!("No auth data found in HTML"); | ||||||
|  |       Err(AuthDataError::NotFound) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fn read_auth_data(main_resource: &WebResource, auth_result_tx: mpsc::UnboundedSender<AuthResult>) { | ||||||
|  |   if main_resource.response().is_none() { | ||||||
|  |     info!("No response found in main resource"); | ||||||
|  |     send_auth_result(&auth_result_tx, Err(AuthDataError::Invalid)); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   let response = main_resource.response().unwrap(); | ||||||
|  |   info!("Trying to read auth data from response headers..."); | ||||||
|  |  | ||||||
|  |   match read_auth_data_from_headers(&response) { | ||||||
|  |     Ok(auth_data) => { | ||||||
|  |       info!("Got auth data from headers"); | ||||||
|  |       send_auth_result(&auth_result_tx, Ok(auth_data)); | ||||||
|  |     } | ||||||
|  |     Err(AuthDataError::Invalid) => { | ||||||
|  |       info!("Found invalid auth data in headers, trying to read from body..."); | ||||||
|  |       read_auth_data_from_body(main_resource, move |auth_result| { | ||||||
|  |         // Since we have already found invalid auth data in headers, which means this could be the `/SAML20/SP/ACS` endpoint | ||||||
|  |         // any error result from body should be considered as invalid, and trigger a retry | ||||||
|  |         let auth_result = auth_result.map_err(|_| AuthDataError::Invalid); | ||||||
|  |         send_auth_result(&auth_result_tx, auth_result); | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |     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)); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | 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()) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pub(crate) async fn clear_webview_cookies(window: &Window) -> anyhow::Result<()> { | ||||||
|  |   let (tx, rx) = oneshot::channel::<Result<(), String>>(); | ||||||
|  |  | ||||||
|  |   window.with_webview(|wv| { | ||||||
|  |     let send_result = move |result: Result<(), String>| { | ||||||
|  |       if let Err(err) = tx.send(result) { | ||||||
|  |         info!("Failed to send result: {:?}", err); | ||||||
|  |       } | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     let wv = wv.inner(); | ||||||
|  |     let context = match wv.context() { | ||||||
|  |       Some(context) => context, | ||||||
|  |       None => { | ||||||
|  |         send_result(Err("No webview context found".into())); | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |     }; | ||||||
|  |     let data_manager = match context.website_data_manager() { | ||||||
|  |       Some(manager) => manager, | ||||||
|  |       None => { | ||||||
|  |         send_result(Err("No data manager found".into())); | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     let now = Instant::now(); | ||||||
|  |     data_manager.clear( | ||||||
|  |       WebsiteDataTypes::COOKIES, | ||||||
|  |       TimeSpan(0), | ||||||
|  |       Cancellable::NONE, | ||||||
|  |       move |result| match result { | ||||||
|  |         Err(err) => { | ||||||
|  |           send_result(Err(err.to_string())); | ||||||
|  |         } | ||||||
|  |         Ok(_) => { | ||||||
|  |           info!("Cookies cleared in {} ms", now.elapsed().as_millis()); | ||||||
|  |           send_result(Ok(())); | ||||||
|  |         } | ||||||
|  |       }, | ||||||
|  |     ); | ||||||
|  |   })?; | ||||||
|  |  | ||||||
|  |   rx.await?.map_err(|err| anyhow::anyhow!(err)) | ||||||
|  | } | ||||||
							
								
								
									
										162
									
								
								apps/gpauth/src/cli.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,162 @@ | |||||||
|  | use clap::Parser; | ||||||
|  | use gpapi::{ | ||||||
|  |   auth::{SamlAuthData, SamlAuthResult}, | ||||||
|  |   clap::args::Os, | ||||||
|  |   gp_params::{ClientOs, GpParams}, | ||||||
|  |   utils::{normalize_server, openssl}, | ||||||
|  |   GP_USER_AGENT, | ||||||
|  | }; | ||||||
|  | use log::{info, LevelFilter}; | ||||||
|  | use serde_json::json; | ||||||
|  | use tauri::{App, AppHandle, RunEvent}; | ||||||
|  | use tempfile::NamedTempFile; | ||||||
|  |  | ||||||
|  | use crate::auth_window::{portal_prelogin, AuthWindow}; | ||||||
|  |  | ||||||
|  | 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, &gp_params).await?, | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     self.saml_request.replace(saml_request); | ||||||
|  |  | ||||||
|  |     let app = create_app(self.clone())?; | ||||||
|  |  | ||||||
|  |     app.run(move |_app_handle, event| { | ||||||
|  |       if let RunEvent::Exit = event { | ||||||
|  |         if let Some(file) = openssl_conf.take() { | ||||||
|  |           if let Err(err) = file.close() { | ||||||
|  |             info!("Error closing OpenSSL config file: {}", err); | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     Ok(()) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   fn prepare_env(&self) -> anyhow::Result<Option<NamedTempFile>> { | ||||||
|  |     std::env::set_var("WEBKIT_DISABLE_COMPOSITING_MODE", "1"); | ||||||
|  |  | ||||||
|  |     if self.hidpi { | ||||||
|  |       info!("Setting GDK_SCALE=2 and GDK_DPI_SCALE=0.5"); | ||||||
|  |  | ||||||
|  |       std::env::set_var("GDK_SCALE", "2"); | ||||||
|  |       std::env::set_var("GDK_DPI_SCALE", "0.5"); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if self.fix_openssl { | ||||||
|  |       info!("Fixing OpenSSL environment"); | ||||||
|  |       let file = openssl::fix_openssl_env()?; | ||||||
|  |  | ||||||
|  |       return Ok(Some(file)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     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); | ||||||
|  |  | ||||||
|  |     auth_window.open().await | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fn create_app(cli: Cli) -> anyhow::Result<App> { | ||||||
|  |   let app = tauri::Builder::default() | ||||||
|  |     .setup(|app| { | ||||||
|  |       let app_handle = app.handle(); | ||||||
|  |  | ||||||
|  |       tauri::async_runtime::spawn(async move { | ||||||
|  |         let auth_result = match cli.saml_auth(app_handle.clone()).await { | ||||||
|  |           Ok(auth_data) => SamlAuthResult::Success(auth_data), | ||||||
|  |           Err(err) => SamlAuthResult::Failure(format!("{}", err)), | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         println!("{}", json!(auth_result)); | ||||||
|  |       }); | ||||||
|  |       Ok(()) | ||||||
|  |     }) | ||||||
|  |     .build(tauri::generate_context!())?; | ||||||
|  |  | ||||||
|  |   Ok(app) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fn init_logger() { | ||||||
|  |   env_logger::builder().filter_level(LevelFilter::Info).init(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pub async fn run() { | ||||||
|  |   let mut cli = Cli::parse(); | ||||||
|  |  | ||||||
|  |   init_logger(); | ||||||
|  |   info!("gpauth started: {}", VERSION); | ||||||
|  |  | ||||||
|  |   if let Err(err) = cli.run().await { | ||||||
|  |     eprintln!("\nError: {}", err); | ||||||
|  |  | ||||||
|  |     if err.to_string().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(" ")); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     std::process::exit(1); | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										9
									
								
								apps/gpauth/src/main.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,9 @@ | |||||||
|  | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] | ||||||
|  |  | ||||||
|  | mod auth_window; | ||||||
|  | mod cli; | ||||||
|  |  | ||||||
|  | #[tokio::main] | ||||||
|  | async fn main() { | ||||||
|  |   cli::run().await; | ||||||
|  | } | ||||||
							
								
								
									
										47
									
								
								apps/gpauth/tauri.conf.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,47 @@ | |||||||
|  | { | ||||||
|  |   "$schema": "https://cdn.jsdelivr.net/gh/tauri-apps/tauri@tauri-v1.5.0/tooling/cli/schema.json", | ||||||
|  |   "build": { | ||||||
|  |     "distDir": [ | ||||||
|  |       "index.html" | ||||||
|  |     ], | ||||||
|  |     "devPath": [ | ||||||
|  |       "index.html" | ||||||
|  |     ], | ||||||
|  |     "beforeDevCommand": "", | ||||||
|  |     "beforeBuildCommand": "", | ||||||
|  |     "withGlobalTauri": false | ||||||
|  |   }, | ||||||
|  |   "package": { | ||||||
|  |     "productName": "gpauth", | ||||||
|  |     "version": "0.0.0" | ||||||
|  |   }, | ||||||
|  |   "tauri": { | ||||||
|  |     "allowlist": { | ||||||
|  |       "all": false, | ||||||
|  |       "http": { | ||||||
|  |         "all": true, | ||||||
|  |         "request": true, | ||||||
|  |         "scope": [ | ||||||
|  |           "http://**", | ||||||
|  |           "https://**" | ||||||
|  |         ] | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     "bundle": { | ||||||
|  |       "active": true, | ||||||
|  |       "targets": "deb", | ||||||
|  |       "identifier": "com.yuezk.gpauth", | ||||||
|  |       "icon": [ | ||||||
|  |         "icons/32x32.png", | ||||||
|  |         "icons/128x128.png", | ||||||
|  |         "icons/128x128@2x.png", | ||||||
|  |         "icons/icon.icns", | ||||||
|  |         "icons/icon.ico" | ||||||
|  |       ] | ||||||
|  |     }, | ||||||
|  |     "security": { | ||||||
|  |       "csp": null | ||||||
|  |     }, | ||||||
|  |     "windows": [] | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										23
									
								
								apps/gpclient/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,23 @@ | |||||||
|  | [package] | ||||||
|  | name = "gpclient" | ||||||
|  | authors.workspace = true | ||||||
|  | version.workspace = true | ||||||
|  | edition.workspace = true | ||||||
|  | license.workspace = true | ||||||
|  |  | ||||||
|  | [dependencies] | ||||||
|  | gpapi = { path = "../../crates/gpapi", features = ["clap"] } | ||||||
|  | openconnect = { path = "../../crates/openconnect" } | ||||||
|  | anyhow.workspace = true | ||||||
|  | clap.workspace = true | ||||||
|  | env_logger.workspace = true | ||||||
|  | inquire = "0.6.2" | ||||||
|  | log.workspace = true | ||||||
|  | tokio.workspace = true | ||||||
|  | sysinfo.workspace = true | ||||||
|  | serde_json.workspace = true | ||||||
|  | whoami.workspace = true | ||||||
|  | tempfile.workspace = true | ||||||
|  | reqwest.workspace = true | ||||||
|  | directories = "5.0" | ||||||
|  | compile-time.workspace = true | ||||||
							
								
								
									
										119
									
								
								apps/gpclient/src/cli.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,119 @@ | |||||||
|  | use clap::{Parser, Subcommand}; | ||||||
|  | use gpapi::utils::openssl; | ||||||
|  | use log::{info, LevelFilter}; | ||||||
|  | use tempfile::NamedTempFile; | ||||||
|  |  | ||||||
|  | use crate::{ | ||||||
|  |   connect::{ConnectArgs, ConnectHandler}, | ||||||
|  |   disconnect::DisconnectHandler, | ||||||
|  |   launch_gui::{LaunchGuiArgs, LaunchGuiHandler}, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | 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 { | ||||||
|  |   #[command(about = "Connect to a portal server")] | ||||||
|  |   Connect(ConnectArgs), | ||||||
|  |   #[command(about = "Disconnect from the server")] | ||||||
|  |   Disconnect, | ||||||
|  |   #[command(about = "Launch the GUI")] | ||||||
|  |   LaunchGui(LaunchGuiArgs), | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Parser)] | ||||||
|  | #[command( | ||||||
|  |   version = VERSION, | ||||||
|  |   author, | ||||||
|  |   about = "The GlobalProtect VPN client, based on OpenConnect, supports the SSO authentication method.", | ||||||
|  |   help_template = "\ | ||||||
|  | {before-help}{name} {version} | ||||||
|  | {author} | ||||||
|  |  | ||||||
|  | {about} | ||||||
|  |  | ||||||
|  | {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")] | ||||||
|  |   fix_openssl: bool, | ||||||
|  |   #[arg(long, help = "Ignore the TLS errors")] | ||||||
|  |   ignore_tls_errors: bool, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl Cli { | ||||||
|  |   fn fix_openssl(&self) -> anyhow::Result<Option<NamedTempFile>> { | ||||||
|  |     if self.fix_openssl { | ||||||
|  |       let file = openssl::fix_openssl_env()?; | ||||||
|  |       return Ok(Some(file)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     Ok(None) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   async fn run(&self) -> anyhow::Result<()> { | ||||||
|  |     // 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, &shared_args).handle().await, | ||||||
|  |       CliCommand::Disconnect => DisconnectHandler::new().handle(), | ||||||
|  |       CliCommand::LaunchGui(args) => LaunchGuiHandler::new(args).handle().await, | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fn init_logger() { | ||||||
|  |   env_logger::builder().filter_level(LevelFilter::Info).init(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pub(crate) async fn run() { | ||||||
|  |   let cli = Cli::parse(); | ||||||
|  |  | ||||||
|  |   init_logger(); | ||||||
|  |   info!("gpclient started: {}", VERSION); | ||||||
|  |  | ||||||
|  |   if let Err(err) = cli.run().await { | ||||||
|  |     eprintln!("\nError: {}", err); | ||||||
|  |  | ||||||
|  |     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); | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										240
									
								
								apps/gpclient/src/connect.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,240 @@ | |||||||
|  | use std::{fs, sync::Arc}; | ||||||
|  |  | ||||||
|  | use clap::Args; | ||||||
|  | use gpapi::{ | ||||||
|  |   clap::args::Os, | ||||||
|  |   credential::{Credential, PasswordCredential}, | ||||||
|  |   gateway::gateway_login, | ||||||
|  |   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::{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")] | ||||||
|  |   gateway: Option<String>, | ||||||
|  |   #[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, | ||||||
|  |   shared_args: &'a SharedArgs, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl<'a> ConnectHandler<'a> { | ||||||
|  |   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 server = self.args.server.as_str(); | ||||||
|  |  | ||||||
|  |     let Err(err) = self.connect_portal_with_prelogin(server).await else { | ||||||
|  |       return Ok(()); | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     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 | ||||||
|  |         .find_gateway(gateway) | ||||||
|  |         .ok_or_else(|| anyhow::anyhow!("Cannot find gateway {}", gateway))?, | ||||||
|  |       None => { | ||||||
|  |         portal_config.sort_gateways(prelogin.region()); | ||||||
|  |         let gateways = portal_config.gateways(); | ||||||
|  |  | ||||||
|  |         if gateways.len() > 1 { | ||||||
|  |           Select::new("Which gateway do you want to connect to?", gateways) | ||||||
|  |             .with_vim_mode(true) | ||||||
|  |             .prompt()? | ||||||
|  |         } else { | ||||||
|  |           gateways[0] | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     let gateway = selected_gateway.server(); | ||||||
|  |     let cred = portal_config.auth_cookie().into(); | ||||||
|  |  | ||||||
|  |     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); | ||||||
|  |     let vpn_clone = vpn.clone(); | ||||||
|  |  | ||||||
|  |     // Listen for the interrupt signal in the background | ||||||
|  |     tokio::spawn(async move { | ||||||
|  |       shutdown_signal().await; | ||||||
|  |       info!("Received the interrupt signal, disconnecting..."); | ||||||
|  |       vpn_clone.disconnect(); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     vpn.connect(write_pid_file); | ||||||
|  |  | ||||||
|  |     if fs::metadata(GP_CLIENT_LOCK_FILE).is_ok() { | ||||||
|  |       info!("Removing PID file"); | ||||||
|  |       fs::remove_file(GP_CLIENT_LOCK_FILE)?; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     Ok(()) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   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) | ||||||
|  |           .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.shared_args.fix_openssl) | ||||||
|  |           .ignore_tls_errors(self.shared_args.ignore_tls_errors) | ||||||
|  |           .clean(self.args.clean) | ||||||
|  |           .launch() | ||||||
|  |           .await | ||||||
|  |       } | ||||||
|  |       Prelogin::Standard(prelogin) => { | ||||||
|  |         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(), | ||||||
|  |           |user| Ok(user.to_owned()), | ||||||
|  |         )?; | ||||||
|  |         let password = Password::new(&format!("{}:", prelogin.label_password())) | ||||||
|  |           .without_confirmation() | ||||||
|  |           .with_display_mode(PasswordDisplayMode::Masked) | ||||||
|  |           .prompt()?; | ||||||
|  |  | ||||||
|  |         let password_cred = PasswordCredential::new(&user, &password); | ||||||
|  |  | ||||||
|  |         Ok(password_cred.into()) | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fn write_pid_file() { | ||||||
|  |   let pid = std::process::id(); | ||||||
|  |  | ||||||
|  |   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())) | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										31
									
								
								apps/gpclient/src/disconnect.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,31 @@ | |||||||
|  | use crate::GP_CLIENT_LOCK_FILE; | ||||||
|  | use log::{info, warn}; | ||||||
|  | use std::fs; | ||||||
|  | use sysinfo::{Pid, ProcessExt, Signal, System, SystemExt}; | ||||||
|  |  | ||||||
|  | pub(crate) struct DisconnectHandler; | ||||||
|  |  | ||||||
|  | impl DisconnectHandler { | ||||||
|  |   pub(crate) fn new() -> Self { | ||||||
|  |     Self | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   pub(crate) fn handle(&self) -> anyhow::Result<()> { | ||||||
|  |     if fs::metadata(GP_CLIENT_LOCK_FILE).is_err() { | ||||||
|  |       warn!("PID file not found, maybe the client is not running"); | ||||||
|  |       return Ok(()); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     let pid = fs::read_to_string(GP_CLIENT_LOCK_FILE)?; | ||||||
|  |     let pid = pid.trim().parse::<usize>()?; | ||||||
|  |     let s = System::new_all(); | ||||||
|  |  | ||||||
|  |     if let Some(process) = s.process(Pid::from(pid)) { | ||||||
|  |       info!("Found process {}, killing...", pid); | ||||||
|  |       if process.kill_with(Signal::Interrupt).is_none() { | ||||||
|  |         warn!("Failed to kill process {}", pid); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     Ok(()) | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										112
									
								
								apps/gpclient/src/launch_gui.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,112 @@ | |||||||
|  | use std::{collections::HashMap, fs, path::PathBuf}; | ||||||
|  |  | ||||||
|  | use clap::Args; | ||||||
|  | use directories::ProjectDirs; | ||||||
|  | use gpapi::{ | ||||||
|  |   process::service_launcher::ServiceLauncher, | ||||||
|  |   utils::{endpoint::http_endpoint, env_file, shutdown_signal}, | ||||||
|  | }; | ||||||
|  | use log::info; | ||||||
|  |  | ||||||
|  | #[derive(Args)] | ||||||
|  | pub(crate) struct LaunchGuiArgs { | ||||||
|  |   #[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, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pub(crate) struct LaunchGuiHandler<'a> { | ||||||
|  |   args: &'a LaunchGuiArgs, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl<'a> LaunchGuiHandler<'a> { | ||||||
|  |   pub(crate) fn new(args: &'a LaunchGuiArgs) -> Self { | ||||||
|  |     Self { args } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   pub(crate) async fn handle(&self) -> anyhow::Result<()> { | ||||||
|  |     // `launch-gui`cannot be run as root | ||||||
|  |     let user = whoami::username(); | ||||||
|  |     if user == "root" { | ||||||
|  |       anyhow::bail!("`launch-gui` cannot be run as root"); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     let auth_data = self.args.auth_data.as_deref().unwrap_or_default(); | ||||||
|  |     if !auth_data.is_empty() { | ||||||
|  |       // Process the authentication data, its format is `globalprotectcallback:<data>` | ||||||
|  |       return feed_auth_data(auth_data).await; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if try_active_gui().await.is_ok() { | ||||||
|  |       info!("The GUI is already running"); | ||||||
|  |       return Ok(()); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     tokio::spawn(async move { | ||||||
|  |       shutdown_signal().await; | ||||||
|  |       info!("Shutting down..."); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     let log_file = get_log_file()?; | ||||||
|  |     let log_file_path = log_file.to_string_lossy().to_string(); | ||||||
|  |  | ||||||
|  |     info!("Log file: {}", log_file_path); | ||||||
|  |  | ||||||
|  |     let mut extra_envs = HashMap::<String, String>::new(); | ||||||
|  |     extra_envs.insert("GP_LOG_FILE".into(), log_file_path.clone()); | ||||||
|  |  | ||||||
|  |     // Persist the environment variables to a file | ||||||
|  |     let env_file = env_file::persist_env_vars(Some(extra_envs))?; | ||||||
|  |     let env_file = env_file.into_temp_path(); | ||||||
|  |     let env_file_path = env_file.to_string_lossy().to_string(); | ||||||
|  |  | ||||||
|  |     let exit_status = ServiceLauncher::new() | ||||||
|  |       .minimized(self.args.minimized) | ||||||
|  |       .env_file(&env_file_path) | ||||||
|  |       .log_file(&log_file_path) | ||||||
|  |       .launch() | ||||||
|  |       .await?; | ||||||
|  |  | ||||||
|  |     info!("Service exited with status: {}", exit_status); | ||||||
|  |  | ||||||
|  |     Ok(()) | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | 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?; | ||||||
|  |  | ||||||
|  |   reqwest::Client::default() | ||||||
|  |     .post(format!("{}/active-gui", service_endpoint)) | ||||||
|  |     .send() | ||||||
|  |     .await? | ||||||
|  |     .error_for_status()?; | ||||||
|  |  | ||||||
|  |   Ok(()) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pub fn get_log_file() -> anyhow::Result<PathBuf> { | ||||||
|  |   let dirs = ProjectDirs::from("com.yuezk", "GlobalProtect-openconnect", "gpclient") | ||||||
|  |     .ok_or_else(|| anyhow::anyhow!("Failed to get project dirs"))?; | ||||||
|  |  | ||||||
|  |   fs::create_dir_all(dirs.data_dir())?; | ||||||
|  |  | ||||||
|  |   Ok(dirs.data_dir().join("gpclient.log")) | ||||||
|  | } | ||||||
							
								
								
									
										11
									
								
								apps/gpclient/src/main.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,11 @@ | |||||||
|  | mod cli; | ||||||
|  | mod connect; | ||||||
|  | mod disconnect; | ||||||
|  | mod launch_gui; | ||||||
|  |  | ||||||
|  | pub(crate) const GP_CLIENT_LOCK_FILE: &str = "/var/run/gpclient.lock"; | ||||||
|  |  | ||||||
|  | #[tokio::main] | ||||||
|  | async fn main() { | ||||||
|  |   cli::run().await; | ||||||
|  | } | ||||||
							
								
								
									
										19
									
								
								apps/gpservice/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,19 @@ | |||||||
|  | [package] | ||||||
|  | name = "gpservice" | ||||||
|  | version.workspace = true | ||||||
|  | edition.workspace = true | ||||||
|  | license.workspace = true | ||||||
|  |  | ||||||
|  | [dependencies] | ||||||
|  | gpapi = { path = "../../crates/gpapi" } | ||||||
|  | openconnect = { path = "../../crates/openconnect" } | ||||||
|  | clap.workspace = true | ||||||
|  | anyhow.workspace = true | ||||||
|  | tokio.workspace = true | ||||||
|  | tokio-util.workspace = true | ||||||
|  | axum = { workspace = true, features = ["ws"] } | ||||||
|  | futures.workspace = true | ||||||
|  | serde_json.workspace = true | ||||||
|  | env_logger.workspace = true | ||||||
|  | log.workspace = true | ||||||
|  | compile-time.workspace = true | ||||||
							
								
								
									
										165
									
								
								apps/gpservice/src/cli.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,165 @@ | |||||||
|  | use std::sync::Arc; | ||||||
|  | use std::{collections::HashMap, io::Write}; | ||||||
|  |  | ||||||
|  | use anyhow::bail; | ||||||
|  | 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}, | ||||||
|  |   GP_SERVICE_LOCK_FILE, | ||||||
|  | }; | ||||||
|  | use log::{info, warn, LevelFilter}; | ||||||
|  | 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!(), ")"); | ||||||
|  |  | ||||||
|  | #[derive(Parser)] | ||||||
|  | #[command(version = VERSION)] | ||||||
|  | struct Cli { | ||||||
|  |   #[clap(long)] | ||||||
|  |   minimized: bool, | ||||||
|  |   #[clap(long)] | ||||||
|  |   env_file: Option<String>, | ||||||
|  |   #[cfg(debug_assertions)] | ||||||
|  |   #[clap(long)] | ||||||
|  |   no_gui: bool, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl Cli { | ||||||
|  |   async fn run(&mut self, redaction: Arc<Redaction>) -> anyhow::Result<()> { | ||||||
|  |     let lock_file = Arc::new(LockFile::new(GP_SERVICE_LOCK_FILE)); | ||||||
|  |  | ||||||
|  |     if lock_file.check_health().await { | ||||||
|  |       bail!("Another instance of the service is already running"); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     let api_key = self.prepare_api_key(); | ||||||
|  |  | ||||||
|  |     // Channel for sending requests to the VPN task | ||||||
|  |     let (ws_req_tx, ws_req_rx) = mpsc::channel::<WsRequest>(32); | ||||||
|  |     // Channel for receiving the VPN state from the VPN task | ||||||
|  |     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 (shutdown_tx, mut shutdown_rx) = mpsc::channel::<()>(4); | ||||||
|  |     let shutdown_tx_clone = shutdown_tx.clone(); | ||||||
|  |     let vpn_task_token = vpn_task.cancel_token(); | ||||||
|  |     let server_token = ws_server.cancel_token(); | ||||||
|  |  | ||||||
|  |     let vpn_task_handle = tokio::spawn(async move { vpn_task.start(server_token).await }); | ||||||
|  |     let ws_server_handle = tokio::spawn(async move { ws_server.start(shutdown_tx_clone).await }); | ||||||
|  |  | ||||||
|  |     #[cfg(debug_assertions)] | ||||||
|  |     let no_gui = self.no_gui; | ||||||
|  |  | ||||||
|  |     #[cfg(not(debug_assertions))] | ||||||
|  |     let no_gui = false; | ||||||
|  |  | ||||||
|  |     if no_gui { | ||||||
|  |       info!("GUI is disabled"); | ||||||
|  |     } else { | ||||||
|  |       let envs = self.env_file.as_ref().map(env_file::load_env_vars).transpose()?; | ||||||
|  |  | ||||||
|  |       let minimized = self.minimized; | ||||||
|  |  | ||||||
|  |       tokio::spawn(async move { | ||||||
|  |         launch_gui(envs, api_key, minimized).await; | ||||||
|  |         let _ = shutdown_tx.send(()).await; | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     tokio::select! { | ||||||
|  |         _ = shutdown_signal() => { | ||||||
|  |             info!("Shutdown signal received"); | ||||||
|  |         } | ||||||
|  |         _ = shutdown_rx.recv() => { | ||||||
|  |             info!("Shutdown request received, shutting down"); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     vpn_task_token.cancel(); | ||||||
|  |     let _ = tokio::join!(vpn_task_handle, ws_server_handle); | ||||||
|  |  | ||||||
|  |     lock_file.unlock()?; | ||||||
|  |  | ||||||
|  |     info!("gpservice stopped"); | ||||||
|  |  | ||||||
|  |     Ok(()) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   fn prepare_api_key(&self) -> Vec<u8> { | ||||||
|  |     #[cfg(debug_assertions)] | ||||||
|  |     if self.no_gui { | ||||||
|  |       return gpapi::GP_API_KEY.to_vec(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     generate_key().to_vec() | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fn init_logger() -> Arc<Redaction> { | ||||||
|  |   let redaction = Arc::new(Redaction::new()); | ||||||
|  |   let redaction_clone = Arc::clone(&redaction); | ||||||
|  |   // let target = Box::new(File::create("log.txt").expect("Can't create file")); | ||||||
|  |   env_logger::builder() | ||||||
|  |     .filter_level(LevelFilter::Info) | ||||||
|  |     .format(move |buf, record| { | ||||||
|  |       let timestamp = buf.timestamp(); | ||||||
|  |       writeln!( | ||||||
|  |         buf, | ||||||
|  |         "[{} {} {}] {}", | ||||||
|  |         timestamp, | ||||||
|  |         record.level(), | ||||||
|  |         record.module_path().unwrap_or_default(), | ||||||
|  |         redaction_clone.redact_str(&record.args().to_string()) | ||||||
|  |       ) | ||||||
|  |     }) | ||||||
|  |     // .target(env_logger::Target::Pipe(target)) | ||||||
|  |     .init(); | ||||||
|  |  | ||||||
|  |   redaction | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async fn launch_gui(envs: Option<HashMap<String, String>>, api_key: Vec<u8>, mut minimized: bool) { | ||||||
|  |   loop { | ||||||
|  |     let api_key_clone = api_key.clone(); | ||||||
|  |     let gui_launcher = GuiLauncher::new() | ||||||
|  |       .envs(envs.clone()) | ||||||
|  |       .api_key(api_key_clone) | ||||||
|  |       .minimized(minimized); | ||||||
|  |  | ||||||
|  |     match gui_launcher.launch().await { | ||||||
|  |       Ok(exit_status) => { | ||||||
|  |         // Exit code 99 means that the GUI needs to be restarted | ||||||
|  |         if exit_status.code() != Some(99) { | ||||||
|  |           info!("GUI exited with code {:?}", exit_status.code()); | ||||||
|  |           break; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         info!("GUI exited with code 99, restarting"); | ||||||
|  |         minimized = false; | ||||||
|  |       } | ||||||
|  |       Err(err) => { | ||||||
|  |         warn!("Failed to launch GUI: {}", err); | ||||||
|  |         break; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pub async fn run() { | ||||||
|  |   let mut cli = Cli::parse(); | ||||||
|  |  | ||||||
|  |   let redaction = init_logger(); | ||||||
|  |   info!("gpservice started: {}", VERSION); | ||||||
|  |  | ||||||
|  |   if let Err(e) = cli.run(redaction).await { | ||||||
|  |     eprintln!("Error: {}", e); | ||||||
|  |     std::process::exit(1); | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										95
									
								
								apps/gpservice/src/handlers.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,95 @@ | |||||||
|  | use std::{borrow::Cow, ops::ControlFlow, sync::Arc}; | ||||||
|  |  | ||||||
|  | use axum::{ | ||||||
|  |   extract::{ | ||||||
|  |     ws::{self, CloseFrame, Message, WebSocket}, | ||||||
|  |     State, WebSocketUpgrade, | ||||||
|  |   }, | ||||||
|  |   response::IntoResponse, | ||||||
|  | }; | ||||||
|  | use futures::{SinkExt, StreamExt}; | ||||||
|  | use gpapi::service::event::WsEvent; | ||||||
|  | use log::{info, warn}; | ||||||
|  |  | ||||||
|  | use crate::ws_server::WsServerContext; | ||||||
|  |  | ||||||
|  | pub(crate) async fn health() -> impl IntoResponse { | ||||||
|  |   "OK" | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pub(crate) async fn active_gui(State(ctx): State<Arc<WsServerContext>>) -> impl IntoResponse { | ||||||
|  |   ctx.send_event(WsEvent::ActiveGui).await; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pub(crate) async fn auth_data(State(ctx): State<Arc<WsServerContext>>, body: String) -> impl IntoResponse { | ||||||
|  |   ctx.send_event(WsEvent::AuthData(body)).await; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pub(crate) async fn ws_handler(ws: WebSocketUpgrade, State(ctx): State<Arc<WsServerContext>>) -> impl IntoResponse { | ||||||
|  |   ws.on_upgrade(move |socket| handle_socket(socket, ctx)) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async fn handle_socket(mut socket: WebSocket, ctx: Arc<WsServerContext>) { | ||||||
|  |   // Send ping message | ||||||
|  |   if let Err(err) = socket.send(Message::Ping("Hi".into())).await { | ||||||
|  |     warn!("Failed to send ping: {}", err); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Wait for pong message | ||||||
|  |   if socket.recv().await.is_none() { | ||||||
|  |     warn!("Failed to receive pong"); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   info!("New client connected"); | ||||||
|  |  | ||||||
|  |   let (mut sender, mut receiver) = socket.split(); | ||||||
|  |   let (connection, mut msg_rx) = ctx.add_connection().await; | ||||||
|  |  | ||||||
|  |   let send_task = tokio::spawn(async move { | ||||||
|  |     while let Some(msg) = msg_rx.recv().await { | ||||||
|  |       if let Err(err) = sender.send(msg).await { | ||||||
|  |         info!("Failed to send message: {}", err); | ||||||
|  |         break; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     let close_msg = Message::Close(Some(CloseFrame { | ||||||
|  |       code: ws::close_code::NORMAL, | ||||||
|  |       reason: Cow::from("Goodbye"), | ||||||
|  |     })); | ||||||
|  |  | ||||||
|  |     if let Err(err) = sender.send(close_msg).await { | ||||||
|  |       warn!("Failed to close socket: {}", err); | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   let conn = Arc::clone(&connection); | ||||||
|  |   let ctx_clone = Arc::clone(&ctx); | ||||||
|  |   let recv_task = tokio::spawn(async move { | ||||||
|  |     while let Some(Ok(msg)) = receiver.next().await { | ||||||
|  |       let ControlFlow::Continue(ws_req) = conn.recv_msg(msg) else { | ||||||
|  |         break; | ||||||
|  |       }; | ||||||
|  |  | ||||||
|  |       if let Err(err) = ctx_clone.forward_req(ws_req).await { | ||||||
|  |         info!("Failed to forward request: {}", err); | ||||||
|  |         break; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   tokio::select! { | ||||||
|  |     _ = send_task => { | ||||||
|  |         info!("WS server send task completed"); | ||||||
|  |     }, | ||||||
|  |     _ = recv_task => { | ||||||
|  |         info!("WS server recv task completed"); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   info!("Client disconnected"); | ||||||
|  |  | ||||||
|  |   ctx.remove_connection(connection).await; | ||||||
|  | } | ||||||
							
								
								
									
										11
									
								
								apps/gpservice/src/main.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,11 @@ | |||||||
|  | mod cli; | ||||||
|  | mod handlers; | ||||||
|  | mod routes; | ||||||
|  | mod vpn_task; | ||||||
|  | mod ws_connection; | ||||||
|  | mod ws_server; | ||||||
|  |  | ||||||
|  | #[tokio::main] | ||||||
|  | async fn main() { | ||||||
|  |   cli::run().await; | ||||||
|  | } | ||||||
							
								
								
									
										17
									
								
								apps/gpservice/src/routes.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,17 @@ | |||||||
|  | use std::sync::Arc; | ||||||
|  |  | ||||||
|  | use axum::{ | ||||||
|  |   routing::{get, post}, | ||||||
|  |   Router, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | use crate::{handlers, ws_server::WsServerContext}; | ||||||
|  |  | ||||||
|  | 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) | ||||||
|  | } | ||||||
							
								
								
									
										149
									
								
								apps/gpservice/src/vpn_task.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,149 @@ | |||||||
|  | use std::{sync::Arc, thread}; | ||||||
|  |  | ||||||
|  | use gpapi::service::{ | ||||||
|  |   request::{ConnectRequest, WsRequest}, | ||||||
|  |   vpn_state::VpnState, | ||||||
|  | }; | ||||||
|  | use log::info; | ||||||
|  | use openconnect::Vpn; | ||||||
|  | use tokio::sync::{mpsc, oneshot, watch, RwLock}; | ||||||
|  | use tokio_util::sync::CancellationToken; | ||||||
|  |  | ||||||
|  | pub(crate) struct VpnTaskContext { | ||||||
|  |   vpn_handle: Arc<RwLock<Option<Vpn>>>, | ||||||
|  |   vpn_state_tx: Arc<watch::Sender<VpnState>>, | ||||||
|  |   disconnect_rx: RwLock<Option<oneshot::Receiver<()>>>, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl VpnTaskContext { | ||||||
|  |   pub fn new(vpn_state_tx: watch::Sender<VpnState>) -> Self { | ||||||
|  |     Self { | ||||||
|  |       vpn_handle: Default::default(), | ||||||
|  |       vpn_state_tx: Arc::new(vpn_state_tx), | ||||||
|  |       disconnect_rx: Default::default(), | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   pub async fn connect(&self, req: ConnectRequest) { | ||||||
|  |     let vpn_state = self.vpn_state_tx.borrow().clone(); | ||||||
|  |     if !matches!(vpn_state, VpnState::Disconnected) { | ||||||
|  |       info!("VPN is not disconnected, ignore the request"); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     let info = req.info().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(); | ||||||
|  |  | ||||||
|  |     // Save the VPN handle | ||||||
|  |     vpn_handle.write().await.replace(vpn); | ||||||
|  |  | ||||||
|  |     let vpn_state_tx = self.vpn_state_tx.clone(); | ||||||
|  |     let connect_info = Box::new(info.clone()); | ||||||
|  |     vpn_state_tx.send(VpnState::Connecting(connect_info)).ok(); | ||||||
|  |  | ||||||
|  |     let (disconnect_tx, disconnect_rx) = oneshot::channel::<()>(); | ||||||
|  |     self.disconnect_rx.write().await.replace(disconnect_rx); | ||||||
|  |  | ||||||
|  |     // Spawn a new thread to process the VPN connection, cannot use tokio::spawn here. | ||||||
|  |     // Otherwise, it will block the tokio runtime and cannot send the VPN state to the channel | ||||||
|  |     thread::spawn(move || { | ||||||
|  |       let vpn_state_tx_clone = vpn_state_tx.clone(); | ||||||
|  |  | ||||||
|  |       vpn_handle.blocking_read().as_ref().map(|vpn| { | ||||||
|  |         vpn.connect(move || { | ||||||
|  |           let connect_info = Box::new(info.clone()); | ||||||
|  |           vpn_state_tx.send(VpnState::Connected(connect_info)).ok(); | ||||||
|  |         }) | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       // Notify the VPN is disconnected | ||||||
|  |       vpn_state_tx_clone.send(VpnState::Disconnected).ok(); | ||||||
|  |       // Remove the VPN handle | ||||||
|  |       vpn_handle.blocking_write().take(); | ||||||
|  |  | ||||||
|  |       disconnect_tx.send(()).ok(); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   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() | ||||||
|  |       } | ||||||
|  |       // Wait for the VPN to be disconnected | ||||||
|  |       disconnect_rx.await.ok(); | ||||||
|  |       info!("VPN disconnected"); | ||||||
|  |     } else { | ||||||
|  |       info!("VPN is not connected, skip disconnect"); | ||||||
|  |       self.vpn_state_tx.send(VpnState::Disconnected).ok(); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pub(crate) struct VpnTask { | ||||||
|  |   ws_req_rx: mpsc::Receiver<WsRequest>, | ||||||
|  |   ctx: Arc<VpnTaskContext>, | ||||||
|  |   cancel_token: CancellationToken, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl VpnTask { | ||||||
|  |   pub fn new(ws_req_rx: mpsc::Receiver<WsRequest>, vpn_state_tx: watch::Sender<VpnState>) -> Self { | ||||||
|  |     let ctx = Arc::new(VpnTaskContext::new(vpn_state_tx)); | ||||||
|  |     let cancel_token = CancellationToken::new(); | ||||||
|  |  | ||||||
|  |     Self { | ||||||
|  |       ws_req_rx, | ||||||
|  |       ctx, | ||||||
|  |       cancel_token, | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   pub fn cancel_token(&self) -> CancellationToken { | ||||||
|  |     self.cancel_token.clone() | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   pub async fn start(&mut self, server_cancel_token: CancellationToken) { | ||||||
|  |     let cancel_token = self.cancel_token.clone(); | ||||||
|  |  | ||||||
|  |     tokio::select! { | ||||||
|  |         _ = self.recv() => { | ||||||
|  |             info!("VPN task stopped"); | ||||||
|  |         } | ||||||
|  |         _ = cancel_token.cancelled() => { | ||||||
|  |             info!("VPN task cancelled"); | ||||||
|  |             self.ctx.disconnect().await; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     server_cancel_token.cancel(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   async fn recv(&mut self) { | ||||||
|  |     while let Some(req) = self.ws_req_rx.recv().await { | ||||||
|  |       tokio::spawn(process_ws_req(req, self.ctx.clone())); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async fn process_ws_req(req: WsRequest, ctx: Arc<VpnTaskContext>) { | ||||||
|  |   match req { | ||||||
|  |     WsRequest::Connect(req) => { | ||||||
|  |       ctx.connect(*req).await; | ||||||
|  |     } | ||||||
|  |     WsRequest::Disconnect(_) => { | ||||||
|  |       ctx.disconnect().await; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										53
									
								
								apps/gpservice/src/ws_connection.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,53 @@ | |||||||
|  | use std::{ops::ControlFlow, sync::Arc}; | ||||||
|  |  | ||||||
|  | use axum::extract::ws::{CloseFrame, Message}; | ||||||
|  | use gpapi::{ | ||||||
|  |   service::{event::WsEvent, request::WsRequest}, | ||||||
|  |   utils::crypto::Crypto, | ||||||
|  | }; | ||||||
|  | use log::{info, warn}; | ||||||
|  | use tokio::sync::mpsc; | ||||||
|  |  | ||||||
|  | pub(crate) struct WsConnection { | ||||||
|  |   crypto: Arc<Crypto>, | ||||||
|  |   tx: mpsc::Sender<Message>, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl WsConnection { | ||||||
|  |   pub fn new(crypto: Arc<Crypto>, tx: mpsc::Sender<Message>) -> Self { | ||||||
|  |     Self { crypto, tx } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   pub async fn send_event(&self, event: &WsEvent) -> anyhow::Result<()> { | ||||||
|  |     let encrypted = self.crypto.encrypt(event)?; | ||||||
|  |     let msg = Message::Binary(encrypted); | ||||||
|  |  | ||||||
|  |     self.tx.send(msg).await?; | ||||||
|  |  | ||||||
|  |     Ok(()) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   pub fn recv_msg(&self, msg: Message) -> ControlFlow<(), WsRequest> { | ||||||
|  |     match msg { | ||||||
|  |       Message::Binary(data) => match self.crypto.decrypt(data) { | ||||||
|  |         Ok(ws_req) => ControlFlow::Continue(ws_req), | ||||||
|  |         Err(err) => { | ||||||
|  |           info!("Failed to decrypt message: {}", err); | ||||||
|  |           ControlFlow::Break(()) | ||||||
|  |         } | ||||||
|  |       }, | ||||||
|  |       Message::Close(cf) => { | ||||||
|  |         if let Some(CloseFrame { code, reason }) = cf { | ||||||
|  |           info!("Client sent close, code {} and reason `{}`", code, reason); | ||||||
|  |         } else { | ||||||
|  |           info!("Client somehow sent close message without CloseFrame"); | ||||||
|  |         } | ||||||
|  |         ControlFlow::Break(()) | ||||||
|  |       } | ||||||
|  |       _ => { | ||||||
|  |         warn!("WS server received unexpected message: {:?}", msg); | ||||||
|  |         ControlFlow::Break(()) | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										153
									
								
								apps/gpservice/src/ws_server.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,153 @@ | |||||||
|  | use std::sync::Arc; | ||||||
|  |  | ||||||
|  | use axum::extract::ws::Message; | ||||||
|  | use gpapi::{ | ||||||
|  |   service::{event::WsEvent, request::WsRequest, vpn_state::VpnState}, | ||||||
|  |   utils::{crypto::Crypto, lock_file::LockFile, redact::Redaction}, | ||||||
|  | }; | ||||||
|  | use log::{info, warn}; | ||||||
|  | use tokio::{ | ||||||
|  |   net::TcpListener, | ||||||
|  |   sync::{mpsc, watch, RwLock}, | ||||||
|  | }; | ||||||
|  | use tokio_util::sync::CancellationToken; | ||||||
|  |  | ||||||
|  | use crate::{routes, ws_connection::WsConnection}; | ||||||
|  |  | ||||||
|  | pub(crate) struct WsServerContext { | ||||||
|  |   crypto: Arc<Crypto>, | ||||||
|  |   ws_req_tx: mpsc::Sender<WsRequest>, | ||||||
|  |   vpn_state_rx: watch::Receiver<VpnState>, | ||||||
|  |   redaction: Arc<Redaction>, | ||||||
|  |   connections: RwLock<Vec<Arc<WsConnection>>>, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl WsServerContext { | ||||||
|  |   pub fn new( | ||||||
|  |     api_key: Vec<u8>, | ||||||
|  |     ws_req_tx: mpsc::Sender<WsRequest>, | ||||||
|  |     vpn_state_rx: watch::Receiver<VpnState>, | ||||||
|  |     redaction: Arc<Redaction>, | ||||||
|  |   ) -> Self { | ||||||
|  |     Self { | ||||||
|  |       crypto: Arc::new(Crypto::new(api_key)), | ||||||
|  |       ws_req_tx, | ||||||
|  |       vpn_state_rx, | ||||||
|  |       redaction, | ||||||
|  |       connections: Default::default(), | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   pub async fn send_event(&self, event: WsEvent) { | ||||||
|  |     let connections = self.connections.read().await; | ||||||
|  |  | ||||||
|  |     for conn in connections.iter() { | ||||||
|  |       let _ = conn.send_event(&event).await; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   pub async fn add_connection(&self) -> (Arc<WsConnection>, mpsc::Receiver<Message>) { | ||||||
|  |     let (tx, rx) = mpsc::channel::<Message>(32); | ||||||
|  |     let conn = Arc::new(WsConnection::new(Arc::clone(&self.crypto), tx)); | ||||||
|  |  | ||||||
|  |     // Send current VPN state to new client | ||||||
|  |     info!("Sending current VPN state to new client"); | ||||||
|  |     let vpn_state = self.vpn_state_rx.borrow().clone(); | ||||||
|  |     if let Err(err) = conn.send_event(&WsEvent::VpnState(vpn_state)).await { | ||||||
|  |       warn!("Failed to send VPN state to new client: {}", err); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     self.connections.write().await.push(Arc::clone(&conn)); | ||||||
|  |  | ||||||
|  |     (conn, rx) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   pub async fn remove_connection(&self, conn: Arc<WsConnection>) { | ||||||
|  |     let mut connections = self.connections.write().await; | ||||||
|  |     connections.retain(|c| !Arc::ptr_eq(c, &conn)); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   fn vpn_state_rx(&self) -> watch::Receiver<VpnState> { | ||||||
|  |     self.vpn_state_rx.clone() | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   pub async fn forward_req(&self, req: WsRequest) -> anyhow::Result<()> { | ||||||
|  |     if let WsRequest::Connect(ref req) = req { | ||||||
|  |       self | ||||||
|  |         .redaction | ||||||
|  |         .add_values(&[req.gateway().server(), req.args().cookie()])? | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     self.ws_req_tx.send(req).await?; | ||||||
|  |  | ||||||
|  |     Ok(()) | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pub(crate) struct WsServer { | ||||||
|  |   ctx: Arc<WsServerContext>, | ||||||
|  |   cancel_token: CancellationToken, | ||||||
|  |   lock_file: Arc<LockFile>, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl WsServer { | ||||||
|  |   pub fn new( | ||||||
|  |     api_key: Vec<u8>, | ||||||
|  |     ws_req_tx: mpsc::Sender<WsRequest>, | ||||||
|  |     vpn_state_rx: watch::Receiver<VpnState>, | ||||||
|  |     lock_file: Arc<LockFile>, | ||||||
|  |     redaction: Arc<Redaction>, | ||||||
|  |   ) -> Self { | ||||||
|  |     let ctx = Arc::new(WsServerContext::new(api_key, ws_req_tx, vpn_state_rx, redaction)); | ||||||
|  |     let cancel_token = CancellationToken::new(); | ||||||
|  |  | ||||||
|  |     Self { | ||||||
|  |       ctx, | ||||||
|  |       cancel_token, | ||||||
|  |       lock_file, | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   pub fn cancel_token(&self) -> CancellationToken { | ||||||
|  |     self.cancel_token.clone() | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   pub async fn start(&self, shutdown_tx: mpsc::Sender<()>) { | ||||||
|  |     if let Ok(listener) = TcpListener::bind("127.0.0.1:0").await { | ||||||
|  |       let local_addr = listener.local_addr().unwrap(); | ||||||
|  |  | ||||||
|  |       self.lock_file.lock(local_addr.port().to_string()).unwrap(); | ||||||
|  |  | ||||||
|  |       info!("WS server listening on port: {}", local_addr.port()); | ||||||
|  |  | ||||||
|  |       tokio::select! { | ||||||
|  |         _ = watch_vpn_state(self.ctx.vpn_state_rx(), Arc::clone(&self.ctx)) => { | ||||||
|  |           info!("VPN state watch task completed"); | ||||||
|  |         } | ||||||
|  |         _ = start_server(listener, self.ctx.clone()) => { | ||||||
|  |             info!("WS server stopped"); | ||||||
|  |         } | ||||||
|  |         _ = self.cancel_token.cancelled() => { | ||||||
|  |           info!("WS server cancelled"); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     let _ = shutdown_tx.send(()).await; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async fn watch_vpn_state(mut vpn_state_rx: watch::Receiver<VpnState>, ctx: Arc<WsServerContext>) { | ||||||
|  |   while vpn_state_rx.changed().await.is_ok() { | ||||||
|  |     let vpn_state = vpn_state_rx.borrow().clone(); | ||||||
|  |     ctx.send_event(WsEvent::VpnState(vpn_state)).await; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async fn start_server(listener: TcpListener, ctx: Arc<WsServerContext>) -> anyhow::Result<()> { | ||||||
|  |   let routes = routes::routes(ctx); | ||||||
|  |  | ||||||
|  |   axum::serve(listener, routes).await?; | ||||||
|  |  | ||||||
|  |   Ok(()) | ||||||
|  | } | ||||||
							
								
								
									
										38
									
								
								crates/gpapi/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,38 @@ | |||||||
|  | [package] | ||||||
|  | name = "gpapi" | ||||||
|  | version.workspace = true | ||||||
|  | edition.workspace = true | ||||||
|  | license = "MIT" | ||||||
|  |  | ||||||
|  | [dependencies] | ||||||
|  | anyhow.workspace = true | ||||||
|  | base64.workspace = true | ||||||
|  | log.workspace = true | ||||||
|  | reqwest.workspace = true | ||||||
|  | roxmltree.workspace = true | ||||||
|  | serde.workspace = true | ||||||
|  | specta.workspace = true | ||||||
|  | specta-macros.workspace = true | ||||||
|  | urlencoding.workspace = true | ||||||
|  | tokio.workspace = true | ||||||
|  | serde_json.workspace = true | ||||||
|  | whoami.workspace = true | ||||||
|  | tempfile.workspace = true | ||||||
|  | thiserror.workspace = true | ||||||
|  | chacha20poly1305 = { version = "0.10", features = ["std"] } | ||||||
|  | redact-engine.workspace = true | ||||||
|  | url.workspace = true | ||||||
|  | regex.workspace = true | ||||||
|  | dotenvy_macro.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"] | ||||||
							
								
								
									
										90
									
								
								crates/gpapi/src/auth.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,90 @@ | |||||||
|  | use anyhow::bail; | ||||||
|  | use regex::Regex; | ||||||
|  | use serde::{Deserialize, Serialize}; | ||||||
|  |  | ||||||
|  | #[derive(Debug, Serialize, Deserialize)] | ||||||
|  | #[serde(rename_all = "camelCase")] | ||||||
|  | pub struct SamlAuthData { | ||||||
|  |   username: String, | ||||||
|  |   prelogin_cookie: Option<String>, | ||||||
|  |   portal_userauthcookie: Option<String>, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Debug, Serialize, Deserialize)] | ||||||
|  | #[serde(rename_all = "camelCase")] | ||||||
|  | pub enum SamlAuthResult { | ||||||
|  |   Success(SamlAuthData), | ||||||
|  |   Failure(String), | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl SamlAuthResult { | ||||||
|  |   pub fn is_success(&self) -> bool { | ||||||
|  |     match self { | ||||||
|  |       SamlAuthResult::Success(_) => true, | ||||||
|  |       SamlAuthResult::Failure(_) => false, | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl SamlAuthData { | ||||||
|  |   pub fn new(username: String, prelogin_cookie: Option<String>, portal_userauthcookie: Option<String>) -> Self { | ||||||
|  |     Self { | ||||||
|  |       username, | ||||||
|  |       prelogin_cookie, | ||||||
|  |       portal_userauthcookie, | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   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 | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   pub fn prelogin_cookie(&self) -> Option<&str> { | ||||||
|  |     self.prelogin_cookie.as_deref() | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   pub fn check( | ||||||
|  |     username: &Option<String>, | ||||||
|  |     prelogin_cookie: &Option<String>, | ||||||
|  |     portal_userauthcookie: &Option<String>, | ||||||
|  |   ) -> bool { | ||||||
|  |     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); | ||||||
|  |  | ||||||
|  |     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; | ||||||
							
								
								
									
										247
									
								
								crates/gpapi/src/credential.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,247 @@ | |||||||
|  | use std::collections::HashMap; | ||||||
|  |  | ||||||
|  | use serde::{Deserialize, Serialize}; | ||||||
|  | use specta::Type; | ||||||
|  |  | ||||||
|  | use crate::{auth::SamlAuthData, utils::base64::decode_to_string}; | ||||||
|  |  | ||||||
|  | #[derive(Debug, Serialize, Deserialize, Type, Clone)] | ||||||
|  | #[serde(rename_all = "camelCase")] | ||||||
|  | pub struct PasswordCredential { | ||||||
|  |   username: String, | ||||||
|  |   password: String, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl PasswordCredential { | ||||||
|  |   pub fn new(username: &str, password: &str) -> Self { | ||||||
|  |     Self { | ||||||
|  |       username: username.to_string(), | ||||||
|  |       password: password.to_string(), | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   pub fn username(&self) -> &str { | ||||||
|  |     &self.username | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   pub fn password(&self) -> &str { | ||||||
|  |     &self.password | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl From<&CachedCredential> for PasswordCredential { | ||||||
|  |   fn from(value: &CachedCredential) -> Self { | ||||||
|  |     Self::new(value.username(), value.password().unwrap_or_default()) | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Debug, Serialize, Deserialize, Type, Clone)] | ||||||
|  | #[serde(rename_all = "camelCase")] | ||||||
|  | pub struct PreloginCookieCredential { | ||||||
|  |   username: String, | ||||||
|  |   prelogin_cookie: String, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl PreloginCookieCredential { | ||||||
|  |   pub fn new(username: &str, prelogin_cookie: &str) -> Self { | ||||||
|  |     Self { | ||||||
|  |       username: username.to_string(), | ||||||
|  |       prelogin_cookie: prelogin_cookie.to_string(), | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   pub fn username(&self) -> &str { | ||||||
|  |     &self.username | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   pub fn prelogin_cookie(&self) -> &str { | ||||||
|  |     &self.prelogin_cookie | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl TryFrom<SamlAuthData> for PreloginCookieCredential { | ||||||
|  |   type Error = anyhow::Error; | ||||||
|  |  | ||||||
|  |   fn try_from(value: SamlAuthData) -> Result<Self, Self::Error> { | ||||||
|  |     let username = value.username().to_string(); | ||||||
|  |     let prelogin_cookie = value | ||||||
|  |       .prelogin_cookie() | ||||||
|  |       .ok_or_else(|| anyhow::anyhow!("Missing prelogin cookie"))? | ||||||
|  |       .to_string(); | ||||||
|  |  | ||||||
|  |     Ok(Self::new(&username, &prelogin_cookie)) | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Debug, Serialize, Deserialize, Type, Clone)] | ||||||
|  | #[serde(rename_all = "camelCase")] | ||||||
|  | pub struct AuthCookieCredential { | ||||||
|  |   username: String, | ||||||
|  |   user_auth_cookie: String, | ||||||
|  |   prelogon_user_auth_cookie: String, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl AuthCookieCredential { | ||||||
|  |   pub fn new(username: &str, user_auth_cookie: &str, prelogon_user_auth_cookie: &str) -> Self { | ||||||
|  |     Self { | ||||||
|  |       username: username.to_string(), | ||||||
|  |       user_auth_cookie: user_auth_cookie.to_string(), | ||||||
|  |       prelogon_user_auth_cookie: prelogon_user_auth_cookie.to_string(), | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   pub fn username(&self) -> &str { | ||||||
|  |     &self.username | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   pub fn user_auth_cookie(&self) -> &str { | ||||||
|  |     &self.user_auth_cookie | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   pub fn prelogon_user_auth_cookie(&self) -> &str { | ||||||
|  |     &self.prelogon_user_auth_cookie | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Debug, Serialize, Deserialize, Type, Clone)] | ||||||
|  | #[serde(rename_all = "camelCase")] | ||||||
|  | pub struct CachedCredential { | ||||||
|  |   username: String, | ||||||
|  |   password: Option<String>, | ||||||
|  |   auth_cookie: AuthCookieCredential, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl CachedCredential { | ||||||
|  |   pub fn new(username: String, password: Option<String>, auth_cookie: AuthCookieCredential) -> Self { | ||||||
|  |     Self { | ||||||
|  |       username, | ||||||
|  |       password, | ||||||
|  |       auth_cookie, | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   pub fn username(&self) -> &str { | ||||||
|  |     &self.username | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   pub fn password(&self) -> Option<&str> { | ||||||
|  |     self.password.as_deref() | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   pub fn auth_cookie(&self) -> &AuthCookieCredential { | ||||||
|  |     &self.auth_cookie | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   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)] | ||||||
|  | #[serde(tag = "type", rename_all = "camelCase")] | ||||||
|  | pub enum Credential { | ||||||
|  |   Password(PasswordCredential), | ||||||
|  |   PreloginCookie(PreloginCookieCredential), | ||||||
|  |   AuthCookie(AuthCookieCredential), | ||||||
|  |   CachedCredential(CachedCredential), | ||||||
|  | } | ||||||
|  |  | ||||||
|  | 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(), | ||||||
|  |       Credential::PreloginCookie(cred) => cred.username(), | ||||||
|  |       Credential::AuthCookie(cred) => cred.username(), | ||||||
|  |       Credential::CachedCredential(cred) => cred.username(), | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   pub fn to_params(&self) -> HashMap<&str, &str> { | ||||||
|  |     let mut params = HashMap::new(); | ||||||
|  |     params.insert("user", self.username()); | ||||||
|  |  | ||||||
|  |     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 | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl TryFrom<SamlAuthData> for Credential { | ||||||
|  |   type Error = anyhow::Error; | ||||||
|  |  | ||||||
|  |   fn try_from(value: SamlAuthData) -> Result<Self, Self::Error> { | ||||||
|  |     let prelogin_cookie = PreloginCookieCredential::try_from(value)?; | ||||||
|  |  | ||||||
|  |     Ok(Self::PreloginCookie(prelogin_cookie)) | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl From<PasswordCredential> for Credential { | ||||||
|  |   fn from(value: PasswordCredential) -> Self { | ||||||
|  |     Self::Password(value) | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl From<&AuthCookieCredential> for Credential { | ||||||
|  |   fn from(value: &AuthCookieCredential) -> Self { | ||||||
|  |     Self::AuthCookie(value.clone()) | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl From<&CachedCredential> for Credential { | ||||||
|  |   fn from(value: &CachedCredential) -> Self { | ||||||
|  |     Self::CachedCredential(value.clone()) | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										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 | ||||||
|  | } | ||||||
							
								
								
									
										74
									
								
								crates/gpapi/src/gateway/login.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,74 @@ | |||||||
|  | use anyhow::bail; | ||||||
|  | use log::info; | ||||||
|  | use reqwest::Client; | ||||||
|  | use roxmltree::Document; | ||||||
|  | use urlencoding::encode; | ||||||
|  |  | ||||||
|  | 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 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()?; | ||||||
|  |  | ||||||
|  |   let mut params = cred.to_params(); | ||||||
|  |   let extra_params = gp_params.to_params(); | ||||||
|  |  | ||||||
|  |   params.extend(extra_params); | ||||||
|  |   params.insert("server", &gateway); | ||||||
|  |  | ||||||
|  |   info!("Gateway login, user_agent: {}", gp_params.user_agent()); | ||||||
|  |  | ||||||
|  |   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()) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fn build_gateway_token(doc: &Document, computer: &str) -> anyhow::Result<String> { | ||||||
|  |   let args = doc | ||||||
|  |     .descendants() | ||||||
|  |     .filter(|n| n.has_tag_name("argument")) | ||||||
|  |     .map(|n| n.text().unwrap_or("").to_string()) | ||||||
|  |     .collect::<Vec<_>>(); | ||||||
|  |  | ||||||
|  |   let params = [ | ||||||
|  |     read_args(&args, 1, "authcookie")?, | ||||||
|  |     read_args(&args, 3, "portal")?, | ||||||
|  |     read_args(&args, 4, "user")?, | ||||||
|  |     read_args(&args, 7, "domain")?, | ||||||
|  |     read_args(&args, 15, "preferred-ip")?, | ||||||
|  |     ("computer", computer), | ||||||
|  |   ]; | ||||||
|  |  | ||||||
|  |   let token = params | ||||||
|  |     .iter() | ||||||
|  |     .map(|(k, v)| format!("{}={}", k, encode(v))) | ||||||
|  |     .collect::<Vec<_>>() | ||||||
|  |     .join("&"); | ||||||
|  |  | ||||||
|  |   Ok(token) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | 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")) | ||||||
|  |     .map(|s| (key, s.as_ref())) | ||||||
|  | } | ||||||
							
								
								
									
										51
									
								
								crates/gpapi/src/gateway/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,51 @@ | |||||||
|  | mod login; | ||||||
|  | mod parse_gateways; | ||||||
|  | pub mod hip; | ||||||
|  |  | ||||||
|  | pub use login::*; | ||||||
|  | pub(crate) use parse_gateways::*; | ||||||
|  |  | ||||||
|  | use serde::{Deserialize, Serialize}; | ||||||
|  | use specta::Type; | ||||||
|  |  | ||||||
|  | use std::fmt::Display; | ||||||
|  |  | ||||||
|  | #[derive(Debug, Serialize, Deserialize, Type, Clone)] | ||||||
|  | pub(crate) struct PriorityRule { | ||||||
|  |   pub(crate) name: String, | ||||||
|  |   pub(crate) priority: u32, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Debug, Serialize, Deserialize, Type, Clone)] | ||||||
|  | #[serde(rename_all = "camelCase")] | ||||||
|  | pub struct Gateway { | ||||||
|  |   pub(crate) name: String, | ||||||
|  |   pub(crate) address: String, | ||||||
|  |   pub(crate) priority: u32, | ||||||
|  |   pub(crate) priority_rules: Vec<PriorityRule>, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl Display for Gateway { | ||||||
|  |   fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||||||
|  |     write!(f, "{} ({})", self.name, self.address) | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | 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 | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   pub fn server(&self) -> &str { | ||||||
|  |     &self.address | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										61
									
								
								crates/gpapi/src/gateway/parse_gateways.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,61 @@ | |||||||
|  | use roxmltree::Document; | ||||||
|  |  | ||||||
|  | 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 gateways = list_gateway | ||||||
|  |     .children() | ||||||
|  |     .filter_map(|gateway_item| { | ||||||
|  |       if !gateway_item.has_tag_name("entry") { | ||||||
|  |         return None; | ||||||
|  |       } | ||||||
|  |       let address = gateway_item.attribute("name").unwrap_or("").to_string(); | ||||||
|  |       let name = gateway_item | ||||||
|  |         .children() | ||||||
|  |         .find(|n| n.has_tag_name("description")) | ||||||
|  |         .and_then(|n| n.text()) | ||||||
|  |         .unwrap_or("") | ||||||
|  |         .to_string(); | ||||||
|  |       let priority = gateway_item | ||||||
|  |         .children() | ||||||
|  |         .find(|n| n.has_tag_name("priority")) | ||||||
|  |         .and_then(|n| n.text()) | ||||||
|  |         .and_then(|s| s.parse().ok()) | ||||||
|  |         .unwrap_or(u32::MAX); | ||||||
|  |       let priority_rules = gateway_item | ||||||
|  |         .children() | ||||||
|  |         .find(|n| n.has_tag_name("priority-rule")) | ||||||
|  |         .map(|n| { | ||||||
|  |           n.children() | ||||||
|  |             .filter_map(|n| { | ||||||
|  |               if !n.has_tag_name("entry") { | ||||||
|  |                 return None; | ||||||
|  |               } | ||||||
|  |               let name = n.attribute("name").unwrap_or("").to_string(); | ||||||
|  |               let priority: u32 = n | ||||||
|  |                 .children() | ||||||
|  |                 .find(|n| n.has_tag_name("priority")) | ||||||
|  |                 .and_then(|n| n.text()) | ||||||
|  |                 .and_then(|s| s.parse().ok()) | ||||||
|  |                 .unwrap_or(u32::MAX); | ||||||
|  |  | ||||||
|  |               Some(PriorityRule { name, priority }) | ||||||
|  |             }) | ||||||
|  |             .collect() | ||||||
|  |         }) | ||||||
|  |         .unwrap_or_default(); | ||||||
|  |  | ||||||
|  |       Some(Gateway { | ||||||
|  |         name, | ||||||
|  |         address, | ||||||
|  |         priority, | ||||||
|  |         priority_rules, | ||||||
|  |       }) | ||||||
|  |     }) | ||||||
|  |     .collect(); | ||||||
|  |  | ||||||
|  |   Some(gateways) | ||||||
|  | } | ||||||