Compare commits
22 Commits
v2.0.0-bet
...
f9c0eb43a6
Author | SHA1 | Date | |
---|---|---|---|
|
f9c0eb43a6 | ||
|
db9249bd61 | ||
|
662e4d0b8a | ||
|
13be9179f5 | ||
|
0a55506077 | ||
|
8860efa82e | ||
|
9bc0994a8e | ||
|
1f50e4d82b | ||
|
995d1216ea | ||
|
196e91289c | ||
|
b2bb35994f | ||
|
6fe6a1387a | ||
|
aac401e7ee | ||
|
9655b735a1 | ||
|
c3bd7aeb93 | ||
|
0b55a80317 | ||
|
c6315bf384 | ||
|
87b965f80c | ||
|
b09b21ae0f | ||
|
7e372cd113 | ||
|
1e211e8912 | ||
|
8bc4049a0f |
152
.github/workflows/build.yaml
vendored
@@ -6,10 +6,11 @@ on:
|
|||||||
- "*.md"
|
- "*.md"
|
||||||
- .vscode
|
- .vscode
|
||||||
- .devcontainer
|
- .devcontainer
|
||||||
# branches:
|
branches:
|
||||||
# - main
|
- main
|
||||||
# tags:
|
tags:
|
||||||
# - v*.*.*
|
- latest
|
||||||
|
- v*.*.*
|
||||||
jobs:
|
jobs:
|
||||||
# Include arm64 if ref is a tag
|
# Include arm64 if ref is a tag
|
||||||
setup-matrix:
|
setup-matrix:
|
||||||
@@ -30,7 +31,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout gpgui repo
|
- name: Checkout gpgui repo
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GH_PAT }}
|
token: ${{ secrets.GH_PAT }}
|
||||||
repository: yuezk/gpgui
|
repository: yuezk/gpgui
|
||||||
@@ -54,43 +55,35 @@ jobs:
|
|||||||
pnpm run build
|
pnpm run build
|
||||||
|
|
||||||
- name: Upload artifacts
|
- name: Upload artifacts
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: gpgui-fe
|
name: gpgui-fe
|
||||||
path: app/dist
|
path: app/dist
|
||||||
|
|
||||||
build-tauri:
|
build-tauri-amd64:
|
||||||
needs: [setup-matrix, build-fe]
|
needs: [build-fe]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
arch: ${{ fromJson(needs.setup-matrix.outputs.matrix) }}
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout gpgui repo
|
- name: Checkout gpgui repo
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GH_PAT }}
|
token: ${{ secrets.GH_PAT }}
|
||||||
repository: yuezk/gpgui
|
repository: yuezk/gpgui
|
||||||
path: gpgui
|
path: gpgui
|
||||||
|
|
||||||
- name: Checkout gp repo
|
- name: Checkout gp repo
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GH_PAT }}
|
token: ${{ secrets.GH_PAT }}
|
||||||
repository: yuezk/GlobalProtect-openconnect
|
repository: yuezk/GlobalProtect-openconnect
|
||||||
path: gp
|
path: gp
|
||||||
|
|
||||||
- name: Download gpgui-fe artifact
|
- name: Download gpgui-fe artifact
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: gpgui-fe
|
name: gpgui-fe
|
||||||
path: gpgui/app/dist
|
path: gpgui/app/dist
|
||||||
|
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v3
|
|
||||||
with:
|
|
||||||
platforms: ${{ matrix.arch }}
|
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
@@ -104,35 +97,103 @@ jobs:
|
|||||||
-v $(pwd):/${{ github.workspace }} \
|
-v $(pwd):/${{ github.workspace }} \
|
||||||
-w ${{ github.workspace }} \
|
-w ${{ github.workspace }} \
|
||||||
-e CI=true \
|
-e CI=true \
|
||||||
--platform linux/${{ matrix.arch }} \
|
|
||||||
yuezk/gpdev:main \
|
yuezk/gpdev:main \
|
||||||
"./gpgui/scripts/build.sh"
|
"./gpgui/scripts/build.sh"
|
||||||
|
|
||||||
- name: Upload artifacts
|
- name: Upload artifacts
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: artifact-${{ matrix.arch }}-tauri
|
name: artifact-amd64-tauri
|
||||||
path: |
|
path: |
|
||||||
gpgui/.tmp/artifact
|
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:
|
package-rpm:
|
||||||
needs: [setup-matrix, build-tauri]
|
needs: [setup-matrix, package-tarball]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
arch: ${{ fromJson(needs.setup-matrix.outputs.matrix) }}
|
arch: ${{ fromJson(needs.setup-matrix.outputs.matrix) }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout gpgui repo
|
- name: Checkout gpgui repo
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GH_PAT }}
|
token: ${{ secrets.GH_PAT }}
|
||||||
repository: yuezk/gpgui
|
repository: yuezk/gpgui
|
||||||
path: gpgui
|
path: gpgui
|
||||||
|
|
||||||
- name: Download artifact-${{ matrix.arch }}
|
- name: Download package tarball
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: artifact-${{ matrix.arch }}-tauri
|
name: artifact-tarball
|
||||||
path: gpgui/.tmp/artifact
|
path: gpgui/.tmp/artifact
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
@@ -157,28 +218,28 @@ jobs:
|
|||||||
"./gpgui/scripts/build-rpm.sh"
|
"./gpgui/scripts/build-rpm.sh"
|
||||||
|
|
||||||
- name: Upload rpm artifacts
|
- name: Upload rpm artifacts
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: artifact-${{ matrix.arch }}-rpm
|
name: artifact-${{ matrix.arch }}-rpm
|
||||||
path: |
|
path: |
|
||||||
gpgui/.tmp/artifact/*.rpm
|
gpgui/.tmp/artifact/*.rpm
|
||||||
|
|
||||||
package-pkgbuild:
|
package-pkgbuild:
|
||||||
needs: [setup-matrix, build-tauri]
|
needs: [setup-matrix, build-tauri-amd64, build-tauri-arm64]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
arch: ${{ fromJson(needs.setup-matrix.outputs.matrix) }}
|
arch: ${{ fromJson(needs.setup-matrix.outputs.matrix) }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout gpgui repo
|
- name: Checkout gpgui repo
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GH_PAT }}
|
token: ${{ secrets.GH_PAT }}
|
||||||
repository: yuezk/gpgui
|
repository: yuezk/gpgui
|
||||||
path: gpgui
|
path: gpgui
|
||||||
|
|
||||||
- name: Download artifact-${{ matrix.arch }}
|
- name: Download artifact-${{ matrix.arch }}
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: artifact-${{ matrix.arch }}-tauri
|
name: artifact-${{ matrix.arch }}-tauri
|
||||||
path: gpgui/.tmp/artifact
|
path: gpgui/.tmp/artifact
|
||||||
@@ -196,13 +257,11 @@ jobs:
|
|||||||
|
|
||||||
- name: Generate PKGBUILD
|
- name: Generate PKGBUILD
|
||||||
run: |
|
run: |
|
||||||
|
export CI_ARCH=${{ matrix.arch }}
|
||||||
./gpgui/scripts/generate-pkgbuild.sh
|
./gpgui/scripts/generate-pkgbuild.sh
|
||||||
|
|
||||||
- name: Build PKGBUILD package
|
- name: Build PKGBUILD package
|
||||||
run: |
|
run: |
|
||||||
# Generate PKGBUILD to .tmp/pkgbuild
|
|
||||||
./gpgui/scripts/generate-pkgbuild.sh
|
|
||||||
|
|
||||||
# Build package
|
# Build package
|
||||||
docker run \
|
docker run \
|
||||||
--rm \
|
--rm \
|
||||||
@@ -211,7 +270,7 @@ jobs:
|
|||||||
yuezk/gpdev:pkgbuild
|
yuezk/gpdev:pkgbuild
|
||||||
|
|
||||||
- name: Upload pkgbuild artifacts
|
- name: Upload pkgbuild artifacts
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: artifact-${{ matrix.arch }}-pkgbuild
|
name: artifact-${{ matrix.arch }}-pkgbuild
|
||||||
path: |
|
path: |
|
||||||
@@ -226,25 +285,24 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Download artifact
|
- name: Download artifact
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
path: artifact
|
path: artifact
|
||||||
pattern: artifact-*
|
# pattern: artifact-*
|
||||||
merge-multiple: true
|
# merge-multiple: true
|
||||||
|
|
||||||
- name: Generate checksum
|
# - name: Generate checksum
|
||||||
uses: jmgilman/actions-generate-checksum@v1
|
# uses: jmgilman/actions-generate-checksum@v1
|
||||||
with:
|
# with:
|
||||||
output: checksums.txt
|
# output: checksums.txt
|
||||||
patterns: |
|
# patterns: |
|
||||||
artifact/*
|
# artifact/*
|
||||||
|
|
||||||
- name: Create GH release
|
- name: Create GH release
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v1
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GH_PAT }}
|
token: ${{ secrets.GH_PAT }}
|
||||||
prerelease: contains(github.ref, 'latest')
|
prerelease: ${{ contains(github.ref, 'latest') }}
|
||||||
fail_on_unmatched_files: true
|
fail_on_unmatched_files: true
|
||||||
files: |
|
files: |
|
||||||
checksums.txt
|
artifact/artifact-*/*
|
||||||
artifact/*
|
|
||||||
|
9
.vscode/settings.json
vendored
@@ -4,6 +4,7 @@
|
|||||||
"bincode",
|
"bincode",
|
||||||
"chacha",
|
"chacha",
|
||||||
"clientos",
|
"clientos",
|
||||||
|
"cstring",
|
||||||
"datetime",
|
"datetime",
|
||||||
"disconnectable",
|
"disconnectable",
|
||||||
"distro",
|
"distro",
|
||||||
@@ -11,8 +12,10 @@
|
|||||||
"dotenvy",
|
"dotenvy",
|
||||||
"getconfig",
|
"getconfig",
|
||||||
"globalprotect",
|
"globalprotect",
|
||||||
|
"globalprotectcallback",
|
||||||
"gpapi",
|
"gpapi",
|
||||||
"gpauth",
|
"gpauth",
|
||||||
|
"gpcallback",
|
||||||
"gpclient",
|
"gpclient",
|
||||||
"gpcommon",
|
"gpcommon",
|
||||||
"gpgui",
|
"gpgui",
|
||||||
@@ -48,6 +51,8 @@
|
|||||||
"vpnc",
|
"vpnc",
|
||||||
"vpninfo",
|
"vpninfo",
|
||||||
"wmctrl",
|
"wmctrl",
|
||||||
"XAUTHORITY"
|
"XAUTHORITY",
|
||||||
]
|
"yuezk"
|
||||||
|
],
|
||||||
|
"rust-analyzer.cargo.features": "all",
|
||||||
}
|
}
|
||||||
|
124
Cargo.lock
generated
@@ -126,6 +126,28 @@ version = "1.0.79"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca"
|
checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-stream"
|
||||||
|
version = "0.3.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51"
|
||||||
|
dependencies = [
|
||||||
|
"async-stream-impl",
|
||||||
|
"futures-core",
|
||||||
|
"pin-project-lite",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-stream-impl"
|
||||||
|
version = "0.3.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.48",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-trait"
|
name = "async-trait"
|
||||||
version = "0.1.77"
|
version = "0.1.77"
|
||||||
@@ -903,6 +925,19 @@ dependencies = [
|
|||||||
"syn 1.0.109",
|
"syn 1.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "downloader"
|
||||||
|
version = "0.2.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d05213e96f184578b5f70105d4d0a644a168e99e12d7bea0b200c15d67b5c182"
|
||||||
|
dependencies = [
|
||||||
|
"futures",
|
||||||
|
"rand 0.8.5",
|
||||||
|
"reqwest",
|
||||||
|
"thiserror",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dtoa"
|
name = "dtoa"
|
||||||
version = "1.0.9"
|
version = "1.0.9"
|
||||||
@@ -1423,19 +1458,23 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gpapi"
|
name = "gpapi"
|
||||||
version = "2.0.0-beta2"
|
version = "2.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64 0.21.5",
|
"base64 0.21.5",
|
||||||
"chacha20poly1305",
|
"chacha20poly1305",
|
||||||
|
"clap",
|
||||||
"dotenvy_macro",
|
"dotenvy_macro",
|
||||||
"log",
|
"log",
|
||||||
|
"md5",
|
||||||
|
"open",
|
||||||
"redact-engine",
|
"redact-engine",
|
||||||
"regex",
|
"regex",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"roxmltree",
|
"roxmltree",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"serde_urlencoded",
|
||||||
"specta",
|
"specta",
|
||||||
"specta-macros",
|
"specta-macros",
|
||||||
"tauri",
|
"tauri",
|
||||||
@@ -1450,7 +1489,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gpauth"
|
name = "gpauth"
|
||||||
version = "2.0.0-beta2"
|
version = "2.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
@@ -1470,7 +1509,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gpclient"
|
name = "gpclient"
|
||||||
version = "2.0.0-beta2"
|
version = "2.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
@@ -1489,9 +1528,28 @@ dependencies = [
|
|||||||
"whoami",
|
"whoami",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gpgui-helper"
|
||||||
|
version = "2.0.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"async-stream",
|
||||||
|
"base64 0.21.5",
|
||||||
|
"clap",
|
||||||
|
"compile-time",
|
||||||
|
"downloader",
|
||||||
|
"env_logger",
|
||||||
|
"futures-util",
|
||||||
|
"log",
|
||||||
|
"tauri",
|
||||||
|
"tauri-build",
|
||||||
|
"tempfile",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gpservice"
|
name = "gpservice"
|
||||||
version = "2.0.0-beta2"
|
version = "2.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"axum",
|
"axum",
|
||||||
@@ -1564,9 +1622,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "h2"
|
name = "h2"
|
||||||
version = "0.3.22"
|
version = "0.3.24"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4d6250322ef6e60f93f9a2162799302cd6f68f79f6e5d85c8c16f14d1d958178"
|
checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"fnv",
|
"fnv",
|
||||||
@@ -1583,9 +1641,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "h2"
|
name = "h2"
|
||||||
version = "0.4.0"
|
version = "0.4.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e1d308f63daf4181410c242d34c11f928dcb3aa105852019e043c9d1f4e4368a"
|
checksum = "31d030e59af851932b72ceebadf4a2b5986dba4c3b99dd2493f8273a0f151943"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"fnv",
|
"fnv",
|
||||||
@@ -1743,7 +1801,7 @@ dependencies = [
|
|||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"h2 0.3.22",
|
"h2 0.3.24",
|
||||||
"http 0.2.11",
|
"http 0.2.11",
|
||||||
"http-body 0.4.6",
|
"http-body 0.4.6",
|
||||||
"httparse",
|
"httparse",
|
||||||
@@ -1766,7 +1824,7 @@ dependencies = [
|
|||||||
"bytes",
|
"bytes",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"h2 0.4.0",
|
"h2 0.4.2",
|
||||||
"http 1.0.0",
|
"http 1.0.0",
|
||||||
"http-body 1.0.0",
|
"http-body 1.0.0",
|
||||||
"httparse",
|
"httparse",
|
||||||
@@ -1962,6 +2020,15 @@ version = "2.9.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
|
checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "is-docker"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3"
|
||||||
|
dependencies = [
|
||||||
|
"once_cell",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "is-terminal"
|
name = "is-terminal"
|
||||||
version = "0.4.10"
|
version = "0.4.10"
|
||||||
@@ -1973,6 +2040,16 @@ dependencies = [
|
|||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "is-wsl"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5"
|
||||||
|
dependencies = [
|
||||||
|
"is-docker",
|
||||||
|
"once_cell",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "is_executable"
|
name = "is_executable"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
@@ -2205,6 +2282,12 @@ version = "0.7.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
|
checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "md5"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
version = "2.7.1"
|
version = "2.7.1"
|
||||||
@@ -2444,9 +2527,20 @@ version = "0.3.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
|
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "open"
|
||||||
|
version = "5.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "90878fb664448b54c4e592455ad02831e23a3f7e157374a8b95654731aac7349"
|
||||||
|
dependencies = [
|
||||||
|
"is-wsl",
|
||||||
|
"libc",
|
||||||
|
"pathdiff",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openconnect"
|
name = "openconnect"
|
||||||
version = "2.0.0-beta2"
|
version = "2.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"is_executable",
|
"is_executable",
|
||||||
@@ -2573,6 +2667,12 @@ version = "1.0.14"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
|
checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pathdiff"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "percent-encoding"
|
name = "percent-encoding"
|
||||||
version = "2.3.1"
|
version = "2.3.1"
|
||||||
@@ -3070,7 +3170,7 @@ dependencies = [
|
|||||||
"encoding_rs",
|
"encoding_rs",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"h2 0.3.22",
|
"h2 0.3.24",
|
||||||
"http 0.2.11",
|
"http 0.2.11",
|
||||||
"http-body 0.4.6",
|
"http-body 0.4.6",
|
||||||
"hyper 0.14.28",
|
"hyper 0.14.28",
|
||||||
|
15
Cargo.toml
@@ -1,10 +1,10 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
members = ["crates/*", "apps/gpclient", "apps/gpservice", "apps/gpauth"]
|
members = ["crates/*", "apps/gpclient", "apps/gpservice", "apps/gpauth", "apps/gpgui-helper/src-tauri"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "2.0.0-beta2"
|
version = "2.0.0"
|
||||||
authors = ["Kevin Yue <k3vinyue@gmail.com>"]
|
authors = ["Kevin Yue <k3vinyue@gmail.com>"]
|
||||||
homepage = "https://github.com/yuezk/GlobalProtect-openconnect"
|
homepage = "https://github.com/yuezk/GlobalProtect-openconnect"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
@@ -34,15 +34,20 @@ axum = "0.7"
|
|||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
futures-util = "0.3"
|
futures-util = "0.3"
|
||||||
tokio-tungstenite = "0.20.1"
|
tokio-tungstenite = "0.20.1"
|
||||||
specta = "=2.0.0-rc.1"
|
|
||||||
specta-macros = "=2.0.0-rc.1"
|
|
||||||
uzers = "0.11"
|
uzers = "0.11"
|
||||||
whoami = "1"
|
whoami = "1"
|
||||||
tauri = { version = "1.5" }
|
|
||||||
thiserror = "1"
|
thiserror = "1"
|
||||||
redact-engine = "0.1"
|
redact-engine = "0.1"
|
||||||
dotenvy_macro = "0.15"
|
dotenvy_macro = "0.15"
|
||||||
compile-time = "0.2"
|
compile-time = "0.2"
|
||||||
|
serde_urlencoded = "0.7"
|
||||||
|
md5="0.7"
|
||||||
|
|
||||||
|
# Tauri dependencies
|
||||||
|
tauri = { version = "1.5" }
|
||||||
|
specta = "=2.0.0-rc.1"
|
||||||
|
specta-macros = "=2.0.0-rc.1"
|
||||||
|
rspc = { version = "1.0.0-rc.5", features = ["tauri"] }
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
opt-level = 'z' # Optimize for size
|
opt-level = 'z' # Optimize for size
|
||||||
|
34
README.md
@@ -11,8 +11,11 @@ A GUI for GlobalProtect VPN, based on OpenConnect, supports the SSO authenticati
|
|||||||
- [x] Better Linux support
|
- [x] Better Linux support
|
||||||
- [x] Support both CLI and GUI
|
- [x] Support both CLI and GUI
|
||||||
- [x] Support both SSO and non-SSO authentication
|
- [x] Support both SSO and non-SSO authentication
|
||||||
|
- [x] Support the FIDO2 authentication (e.g., YubiKey)
|
||||||
|
- [x] Support authentication using default browser
|
||||||
- [x] Support multiple portals
|
- [x] Support multiple portals
|
||||||
- [x] Support gateway selection
|
- [x] Support gateway selection
|
||||||
|
- [x] Support connect gateway directly
|
||||||
- [x] Support auto-connect on startup
|
- [x] Support auto-connect on startup
|
||||||
- [x] Support system tray icon
|
- [x] Support system tray icon
|
||||||
|
|
||||||
@@ -33,11 +36,12 @@ Commands:
|
|||||||
|
|
||||||
Options:
|
Options:
|
||||||
--fix-openssl Get around the OpenSSL `unsafe legacy renegotiation` error
|
--fix-openssl Get around the OpenSSL `unsafe legacy renegotiation` error
|
||||||
|
--ignore-tls-errors Ignore the TLS errors
|
||||||
-h, --help Print help
|
-h, --help Print help
|
||||||
-V, --version Print version
|
-V, --version Print version
|
||||||
```
|
|
||||||
|
|
||||||
See `gpclient -h` for help.
|
See 'gpclient help <command>' for more information on a specific command.
|
||||||
|
```
|
||||||
|
|
||||||
### GUI
|
### GUI
|
||||||
|
|
||||||
@@ -49,6 +53,15 @@ The GUI version is also available after you installed it. You can launch it from
|
|||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
> [!Note]
|
||||||
|
>
|
||||||
|
> This instruction is for the 2.x version. The 1.x version is still available on the [1.x](https://github.com/yuezk/GlobalProtect-openconnect/tree/1.x) branch, you can build it from the source code by following the instructions in the `README.md` file.
|
||||||
|
|
||||||
|
> [!Warning]
|
||||||
|
>
|
||||||
|
> The client requires `openconnect >= 8.20`, please make sure you have it installed, you can check it with `openconnect --version`.
|
||||||
|
> Installing the client from PPA will automatically install the required version of `openconnect`.
|
||||||
|
|
||||||
### Debian/Ubuntu based distributions
|
### Debian/Ubuntu based distributions
|
||||||
|
|
||||||
#### Install from PPA
|
#### Install from PPA
|
||||||
@@ -59,6 +72,10 @@ sudo apt-get update
|
|||||||
sudo apt-get install globalprotect-openconnect
|
sudo apt-get install globalprotect-openconnect
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> [!Note]
|
||||||
|
>
|
||||||
|
> For Linux Mint, you might need to import the GPG key with: `sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 7937C393082992E5D6E4A60453FC26B43838D761` if you encountered an error `gpg: keyserver receive failed: General error`.
|
||||||
|
|
||||||
#### Install from deb package
|
#### Install from deb package
|
||||||
|
|
||||||
Download the latest deb package from [releases](https://github.com/yuezk/GlobalProtect-openconnect/releases) page. Then install it with `dpkg`:
|
Download the latest deb package from [releases](https://github.com/yuezk/GlobalProtect-openconnect/releases) page. Then install it with `dpkg`:
|
||||||
@@ -73,6 +90,10 @@ sudo dpkg -i globalprotect-openconnect_*.deb
|
|||||||
|
|
||||||
Install from AUR: [globalprotect-openconnect-git](https://aur.archlinux.org/packages/globalprotect-openconnect-git/)
|
Install from AUR: [globalprotect-openconnect-git](https://aur.archlinux.org/packages/globalprotect-openconnect-git/)
|
||||||
|
|
||||||
|
```
|
||||||
|
yay -S globalprotect-openconnect-git
|
||||||
|
```
|
||||||
|
|
||||||
#### Install from package
|
#### Install from package
|
||||||
|
|
||||||
Download the latest package from [releases](https://github.com/yuezk/GlobalProtect-openconnect/releases) page. Then install it with `pacman`:
|
Download the latest package from [releases](https://github.com/yuezk/GlobalProtect-openconnect/releases) page. Then install it with `pacman`:
|
||||||
@@ -102,11 +123,14 @@ Download the latest RPM package from [releases](https://github.com/yuezk/GlobalP
|
|||||||
|
|
||||||
### Other distributions
|
### Other distributions
|
||||||
|
|
||||||
The project depends on `openconnect`, `webkit2gtk`, `libsecret`, `libayatana-appindicator` or `libappindicator-gtk3`. You can install them first and then download the latest binary release (i.e., `*.bin.tar.gz`) from [releases](https://github.com/yuezk/GlobalProtect-openconnect/releases) page.
|
The project depends on `openconnect >= 8.20`, `webkit2gtk`, `libsecret`, `libayatana-appindicator` or `libappindicator-gtk3`. You can install them first and then download the latest binary release (i.e., `*.bin.tar.gz`) from [releases](https://github.com/yuezk/GlobalProtect-openconnect/releases) page.
|
||||||
|
|
||||||
### Install the Old Version (v1.4.9)
|
## About Trial
|
||||||
|
|
||||||
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.
|
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)
|
||||||
|
|
||||||
|
@@ -8,7 +8,7 @@ license.workspace = true
|
|||||||
tauri-build = { version = "1.5", features = [] }
|
tauri-build = { version = "1.5", features = [] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
gpapi = { path = "../../crates/gpapi", features = ["tauri"] }
|
gpapi = { path = "../../crates/gpapi", features = ["tauri", "clap"] }
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
clap.workspace = true
|
clap.workspace = true
|
||||||
env_logger.workspace = true
|
env_logger.workspace = true
|
||||||
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 974 B After Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 83 KiB |
@@ -7,6 +7,7 @@ use std::{
|
|||||||
use anyhow::bail;
|
use anyhow::bail;
|
||||||
use gpapi::{
|
use gpapi::{
|
||||||
auth::SamlAuthData,
|
auth::SamlAuthData,
|
||||||
|
gp_params::GpParams,
|
||||||
portal::{prelogin, Prelogin},
|
portal::{prelogin, Prelogin},
|
||||||
utils::{redact::redact_uri, window::WindowExt},
|
utils::{redact::redact_uri, window::WindowExt},
|
||||||
};
|
};
|
||||||
@@ -18,11 +19,13 @@ use tokio_util::sync::CancellationToken;
|
|||||||
use webkit2gtk::{
|
use webkit2gtk::{
|
||||||
gio::Cancellable,
|
gio::Cancellable,
|
||||||
glib::{GString, TimeSpan},
|
glib::{GString, TimeSpan},
|
||||||
LoadEvent, SettingsExt, URIResponse, URIResponseExt, WebContextExt, WebResource, WebResourceExt,
|
LoadEvent, SettingsExt, TLSErrorsPolicy, URIResponse, URIResponseExt, WebContextExt, WebResource, WebResourceExt,
|
||||||
WebView, WebViewExt, WebsiteDataManagerExtManual, WebsiteDataTypes,
|
WebView, WebViewExt, WebsiteDataManagerExtManual, WebsiteDataTypes,
|
||||||
};
|
};
|
||||||
|
|
||||||
enum AuthDataError {
|
enum AuthDataError {
|
||||||
|
/// Failed to load page due to TLS error
|
||||||
|
TlsError,
|
||||||
/// 1. Found auth data in headers/body but it's invalid
|
/// 1. Found auth data in headers/body but it's invalid
|
||||||
/// 2. Loaded an empty page, failed to load page. etc.
|
/// 2. Loaded an empty page, failed to load page. etc.
|
||||||
Invalid,
|
Invalid,
|
||||||
@@ -37,6 +40,7 @@ pub(crate) struct AuthWindow<'a> {
|
|||||||
server: &'a str,
|
server: &'a str,
|
||||||
saml_request: &'a str,
|
saml_request: &'a str,
|
||||||
user_agent: &'a str,
|
user_agent: &'a str,
|
||||||
|
gp_params: Option<GpParams>,
|
||||||
clean: bool,
|
clean: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,6 +51,7 @@ impl<'a> AuthWindow<'a> {
|
|||||||
server: "",
|
server: "",
|
||||||
saml_request: "",
|
saml_request: "",
|
||||||
user_agent: "",
|
user_agent: "",
|
||||||
|
gp_params: None,
|
||||||
clean: false,
|
clean: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -66,6 +71,11 @@ impl<'a> AuthWindow<'a> {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn gp_params(mut self, gp_params: GpParams) -> Self {
|
||||||
|
self.gp_params.replace(gp_params);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn clean(mut self, clean: bool) -> Self {
|
pub fn clean(mut self, clean: bool) -> Self {
|
||||||
self.clean = clean;
|
self.clean = clean;
|
||||||
self
|
self
|
||||||
@@ -119,6 +129,12 @@ impl<'a> AuthWindow<'a> {
|
|||||||
let saml_request = self.saml_request.to_string();
|
let saml_request = self.saml_request.to_string();
|
||||||
let (auth_result_tx, mut auth_result_rx) = mpsc::unbounded_channel::<AuthResult>();
|
let (auth_result_tx, mut auth_result_rx) = mpsc::unbounded_channel::<AuthResult>();
|
||||||
let raise_window_cancel_token: Arc<RwLock<Option<CancellationToken>>> = Default::default();
|
let raise_window_cancel_token: Arc<RwLock<Option<CancellationToken>>> = Default::default();
|
||||||
|
let gp_params = self.gp_params.as_ref().unwrap();
|
||||||
|
let tls_err_policy = if gp_params.ignore_tls_errors() {
|
||||||
|
TLSErrorsPolicy::Ignore
|
||||||
|
} else {
|
||||||
|
TLSErrorsPolicy::Fail
|
||||||
|
};
|
||||||
|
|
||||||
if self.clean {
|
if self.clean {
|
||||||
clear_webview_cookies(window).await?;
|
clear_webview_cookies(window).await?;
|
||||||
@@ -128,6 +144,10 @@ impl<'a> AuthWindow<'a> {
|
|||||||
window.with_webview(move |wv| {
|
window.with_webview(move |wv| {
|
||||||
let wv = wv.inner();
|
let wv = wv.inner();
|
||||||
|
|
||||||
|
if let Some(context) = wv.context() {
|
||||||
|
context.set_tls_errors_policy(tls_err_policy);
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(settings) = wv.settings() {
|
if let Some(settings) = wv.settings() {
|
||||||
let ua = settings.user_agent().unwrap_or("".into());
|
let ua = settings.user_agent().unwrap_or("".into());
|
||||||
info!("Auth window user agent: {}", ua);
|
info!("Auth window user agent: {}", ua);
|
||||||
@@ -168,31 +188,35 @@ impl<'a> AuthWindow<'a> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
wv.connect_load_failed_with_tls_errors(|_wv, uri, cert, err| {
|
let auth_result_tx_clone = auth_result_tx.clone();
|
||||||
|
wv.connect_load_failed_with_tls_errors(move |_wv, uri, cert, err| {
|
||||||
let redacted_uri = redact_uri(uri);
|
let redacted_uri = redact_uri(uri);
|
||||||
warn!(
|
warn!(
|
||||||
"Failed to load uri: {} with error: {}, cert: {}",
|
"Failed to load uri: {} with error: {}, cert: {}",
|
||||||
redacted_uri, err, cert
|
redacted_uri, err, cert
|
||||||
);
|
);
|
||||||
|
|
||||||
|
send_auth_result(&auth_result_tx_clone, Err(AuthDataError::TlsError));
|
||||||
true
|
true
|
||||||
});
|
});
|
||||||
|
|
||||||
wv.connect_load_failed(move |_wv, _event, uri, err| {
|
wv.connect_load_failed(move |_wv, _event, uri, err| {
|
||||||
let redacted_uri = redact_uri(uri);
|
let redacted_uri = redact_uri(uri);
|
||||||
warn!("Failed to load uri: {} with error: {}", redacted_uri, err);
|
warn!("Failed to load uri: {} with error: {}", redacted_uri, err);
|
||||||
send_auth_result(&auth_result_tx, Err(AuthDataError::Invalid));
|
// NOTE: Don't send error here, since load_changed event will be triggered after this
|
||||||
|
// send_auth_result(&auth_result_tx, Err(AuthDataError::Invalid));
|
||||||
// true to stop other handlers from being invoked for the event. false to propagate the event further.
|
// true to stop other handlers from being invoked for the event. false to propagate the event further.
|
||||||
true
|
true
|
||||||
});
|
});
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let portal = self.server.to_string();
|
let portal = self.server.to_string();
|
||||||
let user_agent = self.user_agent.to_string();
|
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
if let Some(auth_result) = auth_result_rx.recv().await {
|
if let Some(auth_result) = auth_result_rx.recv().await {
|
||||||
match auth_result {
|
match auth_result {
|
||||||
Ok(auth_data) => return Ok(auth_data),
|
Ok(auth_data) => return Ok(auth_data),
|
||||||
|
Err(AuthDataError::TlsError) => bail!("TLS error: certificate verify failed"),
|
||||||
Err(AuthDataError::NotFound) => {
|
Err(AuthDataError::NotFound) => {
|
||||||
info!("No auth data found, it may not be the /SAML20/SP/ACS endpoint");
|
info!("No auth data found, it may not be the /SAML20/SP/ACS endpoint");
|
||||||
|
|
||||||
@@ -201,10 +225,7 @@ impl<'a> AuthWindow<'a> {
|
|||||||
let window = Arc::clone(window);
|
let window = Arc::clone(window);
|
||||||
let cancel_token = CancellationToken::new();
|
let cancel_token = CancellationToken::new();
|
||||||
|
|
||||||
raise_window_cancel_token
|
raise_window_cancel_token.write().await.replace(cancel_token.clone());
|
||||||
.write()
|
|
||||||
.await
|
|
||||||
.replace(cancel_token.clone());
|
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let delay_secs = 1;
|
let delay_secs = 1;
|
||||||
@@ -237,7 +258,7 @@ impl<'a> AuthWindow<'a> {
|
|||||||
);
|
);
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let saml_request = portal_prelogin(&portal, &user_agent).await?;
|
let saml_request = portal_prelogin(&portal, gp_params).await?;
|
||||||
window.with_webview(move |wv| {
|
window.with_webview(move |wv| {
|
||||||
let wv = wv.inner();
|
let wv = wv.inner();
|
||||||
load_saml_request(&wv, &saml_request);
|
load_saml_request(&wv, &saml_request);
|
||||||
@@ -258,11 +279,10 @@ fn raise_window(window: &Arc<Window>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn portal_prelogin(portal: &str, user_agent: &str) -> anyhow::Result<String> {
|
pub async fn portal_prelogin(portal: &str, gp_params: &GpParams) -> anyhow::Result<String> {
|
||||||
info!("Portal prelogin...");
|
match prelogin(portal, gp_params).await? {
|
||||||
match prelogin(portal, user_agent).await? {
|
|
||||||
Prelogin::Saml(prelogin) => Ok(prelogin.saml_request().to_string()),
|
Prelogin::Saml(prelogin) => Ok(prelogin.saml_request().to_string()),
|
||||||
Prelogin::Standard(_) => Err(anyhow::anyhow!("Received non-SAML prelogin response")),
|
Prelogin::Standard(_) => bail!("Received non-SAML prelogin response"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -397,6 +417,11 @@ fn read_auth_data(main_resource: &WebResource, auth_result_tx: mpsc::UnboundedSe
|
|||||||
send_auth_result(&auth_result_tx, auth_result)
|
send_auth_result(&auth_result_tx, auth_result)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Err(AuthDataError::TlsError) => {
|
||||||
|
// NOTE: This is unreachable
|
||||||
|
info!("TLS error found in headers, trying to read from body...");
|
||||||
|
send_auth_result(&auth_result_tx, Err(AuthDataError::TlsError));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,6 +1,8 @@
|
|||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use gpapi::{
|
use gpapi::{
|
||||||
auth::{SamlAuthData, SamlAuthResult},
|
auth::{SamlAuthData, SamlAuthResult},
|
||||||
|
clap::args::Os,
|
||||||
|
gp_params::{ClientOs, GpParams},
|
||||||
utils::{normalize_server, openssl},
|
utils::{normalize_server, openssl},
|
||||||
GP_USER_AGENT,
|
GP_USER_AGENT,
|
||||||
};
|
};
|
||||||
@@ -11,38 +13,47 @@ use tempfile::NamedTempFile;
|
|||||||
|
|
||||||
use crate::auth_window::{portal_prelogin, AuthWindow};
|
use crate::auth_window::{portal_prelogin, AuthWindow};
|
||||||
|
|
||||||
const VERSION: &str = concat!(
|
const VERSION: &str = concat!(env!("CARGO_PKG_VERSION"), " (", compile_time::date_str!(), ")");
|
||||||
env!("CARGO_PKG_VERSION"),
|
|
||||||
" (",
|
|
||||||
compile_time::date_str!(),
|
|
||||||
")"
|
|
||||||
);
|
|
||||||
|
|
||||||
#[derive(Parser, Clone)]
|
#[derive(Parser, Clone)]
|
||||||
#[command(version = VERSION)]
|
#[command(version = VERSION)]
|
||||||
struct Cli {
|
struct Cli {
|
||||||
server: String,
|
server: String,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
|
gateway: bool,
|
||||||
|
#[arg(long)]
|
||||||
saml_request: Option<String>,
|
saml_request: Option<String>,
|
||||||
#[arg(long, default_value = GP_USER_AGENT)]
|
#[arg(long, default_value = GP_USER_AGENT)]
|
||||||
user_agent: String,
|
user_agent: String,
|
||||||
|
#[arg(long, default_value = "Linux")]
|
||||||
|
os: Os,
|
||||||
|
#[arg(long)]
|
||||||
|
os_version: Option<String>,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
hidpi: bool,
|
hidpi: bool,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
fix_openssl: bool,
|
fix_openssl: bool,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
|
ignore_tls_errors: bool,
|
||||||
|
#[arg(long)]
|
||||||
clean: bool,
|
clean: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Cli {
|
impl Cli {
|
||||||
async fn run(&mut self) -> anyhow::Result<()> {
|
async fn run(&mut self) -> anyhow::Result<()> {
|
||||||
|
if self.ignore_tls_errors {
|
||||||
|
info!("TLS errors will be ignored");
|
||||||
|
}
|
||||||
|
|
||||||
let mut openssl_conf = self.prepare_env()?;
|
let mut openssl_conf = self.prepare_env()?;
|
||||||
|
|
||||||
self.server = normalize_server(&self.server)?;
|
self.server = normalize_server(&self.server)?;
|
||||||
|
let gp_params = self.build_gp_params();
|
||||||
|
|
||||||
// Get the initial SAML request
|
// Get the initial SAML request
|
||||||
let saml_request = match self.saml_request {
|
let saml_request = match self.saml_request {
|
||||||
Some(ref saml_request) => saml_request.clone(),
|
Some(ref saml_request) => saml_request.clone(),
|
||||||
None => portal_prelogin(&self.server, &self.user_agent).await?,
|
None => portal_prelogin(&self.server, &gp_params).await?,
|
||||||
};
|
};
|
||||||
|
|
||||||
self.saml_request.replace(saml_request);
|
self.saml_request.replace(saml_request);
|
||||||
@@ -82,10 +93,23 @@ impl Cli {
|
|||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn build_gp_params(&self) -> GpParams {
|
||||||
|
let gp_params = GpParams::builder()
|
||||||
|
.user_agent(&self.user_agent)
|
||||||
|
.client_os(ClientOs::from(&self.os))
|
||||||
|
.os_version(self.os_version.clone())
|
||||||
|
.ignore_tls_errors(self.ignore_tls_errors)
|
||||||
|
.is_gateway(self.gateway)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
gp_params
|
||||||
|
}
|
||||||
|
|
||||||
async fn saml_auth(&self, app_handle: AppHandle) -> anyhow::Result<SamlAuthData> {
|
async fn saml_auth(&self, app_handle: AppHandle) -> anyhow::Result<SamlAuthData> {
|
||||||
let auth_window = AuthWindow::new(app_handle)
|
let auth_window = AuthWindow::new(app_handle)
|
||||||
.server(&self.server)
|
.server(&self.server)
|
||||||
.user_agent(&self.user_agent)
|
.user_agent(&self.user_agent)
|
||||||
|
.gp_params(self.build_gp_params())
|
||||||
.saml_request(self.saml_request.as_ref().unwrap())
|
.saml_request(self.saml_request.as_ref().unwrap())
|
||||||
.clean(self.clean);
|
.clean(self.clean);
|
||||||
|
|
||||||
|
@@ -6,7 +6,7 @@ edition.workspace = true
|
|||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
gpapi = { path = "../../crates/gpapi" }
|
gpapi = { path = "../../crates/gpapi", features = ["clap"] }
|
||||||
openconnect = { path = "../../crates/openconnect" }
|
openconnect = { path = "../../crates/openconnect" }
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
clap.workspace = true
|
clap.workspace = true
|
||||||
|
@@ -9,12 +9,12 @@ use crate::{
|
|||||||
launch_gui::{LaunchGuiArgs, LaunchGuiHandler},
|
launch_gui::{LaunchGuiArgs, LaunchGuiHandler},
|
||||||
};
|
};
|
||||||
|
|
||||||
const VERSION: &str = concat!(
|
const VERSION: &str = concat!(env!("CARGO_PKG_VERSION"), " (", compile_time::date_str!(), ")");
|
||||||
env!("CARGO_PKG_VERSION"),
|
|
||||||
" (",
|
pub(crate) struct SharedArgs {
|
||||||
compile_time::date_str!(),
|
pub(crate) fix_openssl: bool,
|
||||||
")"
|
pub(crate) ignore_tls_errors: bool,
|
||||||
);
|
}
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
enum CliCommand {
|
enum CliCommand {
|
||||||
@@ -40,17 +40,18 @@ enum CliCommand {
|
|||||||
{usage-heading} {usage}
|
{usage-heading} {usage}
|
||||||
|
|
||||||
{all-args}{after-help}
|
{all-args}{after-help}
|
||||||
|
|
||||||
|
See 'gpclient help <command>' for more information on a specific command.
|
||||||
"
|
"
|
||||||
)]
|
)]
|
||||||
struct Cli {
|
struct Cli {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
command: CliCommand,
|
command: CliCommand,
|
||||||
|
|
||||||
#[arg(
|
#[arg(long, help = "Get around the OpenSSL `unsafe legacy renegotiation` error")]
|
||||||
long,
|
|
||||||
help = "Get around the OpenSSL `unsafe legacy renegotiation` error"
|
|
||||||
)]
|
|
||||||
fix_openssl: bool,
|
fix_openssl: bool,
|
||||||
|
#[arg(long, help = "Ignore the TLS errors")]
|
||||||
|
ignore_tls_errors: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Cli {
|
impl Cli {
|
||||||
@@ -67,9 +68,17 @@ impl Cli {
|
|||||||
// The temp file will be dropped automatically when the file handle is dropped
|
// The temp file will be dropped automatically when the file handle is dropped
|
||||||
// So, declare it here to ensure it's not dropped
|
// So, declare it here to ensure it's not dropped
|
||||||
let _file = self.fix_openssl()?;
|
let _file = self.fix_openssl()?;
|
||||||
|
let shared_args = SharedArgs {
|
||||||
|
fix_openssl: self.fix_openssl,
|
||||||
|
ignore_tls_errors: self.ignore_tls_errors,
|
||||||
|
};
|
||||||
|
|
||||||
|
if self.ignore_tls_errors {
|
||||||
|
info!("TLS errors will be ignored");
|
||||||
|
}
|
||||||
|
|
||||||
match &self.command {
|
match &self.command {
|
||||||
CliCommand::Connect(args) => ConnectHandler::new(args, self.fix_openssl).handle().await,
|
CliCommand::Connect(args) => ConnectHandler::new(args, &shared_args).handle().await,
|
||||||
CliCommand::Disconnect => DisconnectHandler::new().handle(),
|
CliCommand::Disconnect => DisconnectHandler::new().handle(),
|
||||||
CliCommand::LaunchGui(args) => LaunchGuiHandler::new(args).handle().await,
|
CliCommand::LaunchGui(args) => LaunchGuiHandler::new(args).handle().await,
|
||||||
}
|
}
|
||||||
@@ -89,13 +98,22 @@ pub(crate) async fn run() {
|
|||||||
if let Err(err) = cli.run().await {
|
if let Err(err) = cli.run().await {
|
||||||
eprintln!("\nError: {}", err);
|
eprintln!("\nError: {}", err);
|
||||||
|
|
||||||
if err.to_string().contains("unsafe legacy renegotiation") && !cli.fix_openssl {
|
let err = err.to_string();
|
||||||
|
|
||||||
|
if err.contains("unsafe legacy renegotiation") && !cli.fix_openssl {
|
||||||
eprintln!("\nRe-run it with the `--fix-openssl` option to work around this issue, e.g.:\n");
|
eprintln!("\nRe-run it with the `--fix-openssl` option to work around this issue, e.g.:\n");
|
||||||
// Print the command
|
// Print the command
|
||||||
let args = std::env::args().collect::<Vec<_>>();
|
let args = std::env::args().collect::<Vec<_>>();
|
||||||
eprintln!("{} --fix-openssl {}\n", args[0], args[1..].join(" "));
|
eprintln!("{} --fix-openssl {}\n", args[0], args[1..].join(" "));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err.contains("certificate verify failed") && !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);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -2,66 +2,109 @@ use std::{fs, sync::Arc};
|
|||||||
|
|
||||||
use clap::Args;
|
use clap::Args;
|
||||||
use gpapi::{
|
use gpapi::{
|
||||||
|
clap::args::Os,
|
||||||
credential::{Credential, PasswordCredential},
|
credential::{Credential, PasswordCredential},
|
||||||
gateway::gateway_login,
|
gateway::gateway_login,
|
||||||
gp_params::GpParams,
|
gp_params::{ClientOs, GpParams},
|
||||||
portal::{prelogin, retrieve_config, Prelogin},
|
portal::{prelogin, retrieve_config, PortalError, Prelogin},
|
||||||
process::auth_launcher::SamlAuthLauncher,
|
process::{
|
||||||
utils::{self, shutdown_signal},
|
auth_launcher::SamlAuthLauncher,
|
||||||
|
users::{get_non_root_user, get_user_by_name},
|
||||||
|
},
|
||||||
|
utils::shutdown_signal,
|
||||||
GP_USER_AGENT,
|
GP_USER_AGENT,
|
||||||
};
|
};
|
||||||
use inquire::{Password, PasswordDisplayMode, Select, Text};
|
use inquire::{Password, PasswordDisplayMode, Select, Text};
|
||||||
use log::info;
|
use log::info;
|
||||||
use openconnect::Vpn;
|
use openconnect::Vpn;
|
||||||
|
|
||||||
use crate::GP_CLIENT_LOCK_FILE;
|
use crate::{cli::SharedArgs, GP_CLIENT_LOCK_FILE};
|
||||||
|
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
pub(crate) struct ConnectArgs {
|
pub(crate) struct ConnectArgs {
|
||||||
#[arg(help = "The portal server to connect to")]
|
#[arg(help = "The portal server to connect to")]
|
||||||
server: String,
|
server: String,
|
||||||
#[arg(
|
#[arg(short, long, help = "The gateway to connect to, it will prompt if not specified")]
|
||||||
short,
|
|
||||||
long,
|
|
||||||
help = "The gateway to connect to, it will prompt if not specified"
|
|
||||||
)]
|
|
||||||
gateway: Option<String>,
|
gateway: Option<String>,
|
||||||
#[arg(
|
#[arg(short, long, help = "The username to use, it will prompt if not specified")]
|
||||||
short,
|
|
||||||
long,
|
|
||||||
help = "The username to use, it will prompt if not specified"
|
|
||||||
)]
|
|
||||||
user: Option<String>,
|
user: Option<String>,
|
||||||
#[arg(long, short, help = "The VPNC script to use")]
|
#[arg(long, short, help = "The VPNC script to use")]
|
||||||
script: Option<String>,
|
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(long, default_value = GP_USER_AGENT, help = "The user agent to use")]
|
#[arg(long, default_value = GP_USER_AGENT, help = "The user agent to use")]
|
||||||
user_agent: String,
|
user_agent: String,
|
||||||
|
#[arg(long, default_value = "Linux")]
|
||||||
|
os: Os,
|
||||||
|
#[arg(long)]
|
||||||
|
os_version: Option<String>,
|
||||||
#[arg(long, help = "The HiDPI mode, useful for high resolution screens")]
|
#[arg(long, help = "The HiDPI mode, useful for high resolution screens")]
|
||||||
hidpi: bool,
|
hidpi: bool,
|
||||||
#[arg(long, help = "Do not reuse the remembered authentication cookie")]
|
#[arg(long, help = "Do not reuse the remembered authentication cookie")]
|
||||||
clean: bool,
|
clean: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ConnectArgs {
|
||||||
|
fn os_version(&self) -> String {
|
||||||
|
if let Some(os_version) = &self.os_version {
|
||||||
|
return os_version.to_owned();
|
||||||
|
}
|
||||||
|
|
||||||
|
match self.os {
|
||||||
|
Os::Linux => format!("Linux {}", whoami::distro()),
|
||||||
|
Os::Windows => String::from("Microsoft Windows 11 Pro , 64-bit"),
|
||||||
|
Os::Mac => String::from("Apple Mac OS X 13.4.0"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) struct ConnectHandler<'a> {
|
pub(crate) struct ConnectHandler<'a> {
|
||||||
args: &'a ConnectArgs,
|
args: &'a ConnectArgs,
|
||||||
fix_openssl: bool,
|
shared_args: &'a SharedArgs,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> ConnectHandler<'a> {
|
impl<'a> ConnectHandler<'a> {
|
||||||
pub(crate) fn new(args: &'a ConnectArgs, fix_openssl: bool) -> Self {
|
pub(crate) fn new(args: &'a ConnectArgs, shared_args: &'a SharedArgs) -> Self {
|
||||||
Self { args, fix_openssl }
|
Self { args, shared_args }
|
||||||
|
}
|
||||||
|
|
||||||
|
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<()> {
|
pub(crate) async fn handle(&self) -> anyhow::Result<()> {
|
||||||
let portal = utils::normalize_server(self.args.server.as_str())?;
|
let server = self.args.server.as_str();
|
||||||
|
|
||||||
let gp_params = GpParams::builder()
|
let Err(err) = self.connect_portal_with_prelogin(server).await else {
|
||||||
.user_agent(&self.args.user_agent)
|
return Ok(());
|
||||||
.build();
|
};
|
||||||
|
|
||||||
let prelogin = prelogin(&portal, &self.args.user_agent).await?;
|
info!("Failed to connect portal with prelogin: {}", err);
|
||||||
let portal_credential = self.obtain_portal_credential(&prelogin).await?;
|
if err.root_cause().downcast_ref::<PortalError>().is_some() {
|
||||||
let mut portal_config = retrieve_config(&portal, &portal_credential, &gp_params).await?;
|
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 {
|
let selected_gateway = match &self.args.gateway {
|
||||||
Some(gateway) => portal_config
|
Some(gateway) => portal_config
|
||||||
@@ -83,11 +126,38 @@ impl<'a> ConnectHandler<'a> {
|
|||||||
|
|
||||||
let gateway = selected_gateway.server();
|
let gateway = selected_gateway.server();
|
||||||
let cred = portal_config.auth_cookie().into();
|
let cred = portal_config.auth_cookie().into();
|
||||||
let token = gateway_login(gateway, &cred, &gp_params).await?;
|
|
||||||
|
|
||||||
let vpn = Vpn::builder(gateway, &token)
|
let cookie = match gateway_login(gateway, &cred, &gp_params).await {
|
||||||
|
Ok(cookie) => cookie,
|
||||||
|
Err(err) => {
|
||||||
|
info!("Gateway login failed: {}", err);
|
||||||
|
return self.connect_gateway_with_prelogin(gateway).await;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
self.connect_gateway(gateway, &cookie).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn connect_gateway_with_prelogin(&self, gateway: &str) -> anyhow::Result<()> {
|
||||||
|
let mut gp_params = self.build_gp_params();
|
||||||
|
gp_params.set_is_gateway(true);
|
||||||
|
|
||||||
|
let prelogin = prelogin(gateway, &gp_params).await?;
|
||||||
|
let cred = self.obtain_credential(&prelogin, gateway).await?;
|
||||||
|
|
||||||
|
let cookie = gateway_login(gateway, &cred, &gp_params).await?;
|
||||||
|
|
||||||
|
self.connect_gateway(gateway, &cookie).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn connect_gateway(&self, gateway: &str, cookie: &str) -> anyhow::Result<()> {
|
||||||
|
let csd_uid = get_csd_uid(&self.args.csd_user)?;
|
||||||
|
|
||||||
|
let vpn = Vpn::builder(gateway, cookie)
|
||||||
.user_agent(self.args.user_agent.clone())
|
.user_agent(self.args.user_agent.clone())
|
||||||
.script(self.args.script.clone())
|
.script(self.args.script.clone())
|
||||||
|
.csd_uid(csd_uid)
|
||||||
|
.csd_wrapper(self.args.csd_wrapper.clone())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
let vpn = Arc::new(vpn);
|
let vpn = Arc::new(vpn);
|
||||||
@@ -110,20 +180,27 @@ impl<'a> ConnectHandler<'a> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn obtain_portal_credential(&self, prelogin: &Prelogin) -> anyhow::Result<Credential> {
|
async fn obtain_credential(&self, prelogin: &Prelogin, server: &str) -> anyhow::Result<Credential> {
|
||||||
|
let is_gateway = prelogin.is_gateway();
|
||||||
|
|
||||||
match prelogin {
|
match prelogin {
|
||||||
Prelogin::Saml(prelogin) => {
|
Prelogin::Saml(prelogin) => {
|
||||||
SamlAuthLauncher::new(&self.args.server)
|
SamlAuthLauncher::new(&self.args.server)
|
||||||
.user_agent(&self.args.user_agent)
|
.gateway(is_gateway)
|
||||||
.saml_request(prelogin.saml_request())
|
.saml_request(prelogin.saml_request())
|
||||||
|
.user_agent(&self.args.user_agent)
|
||||||
|
.os(self.args.os.as_str())
|
||||||
|
.os_version(Some(&self.args.os_version()))
|
||||||
.hidpi(self.args.hidpi)
|
.hidpi(self.args.hidpi)
|
||||||
.fix_openssl(self.fix_openssl)
|
.fix_openssl(self.shared_args.fix_openssl)
|
||||||
|
.ignore_tls_errors(self.shared_args.ignore_tls_errors)
|
||||||
.clean(self.args.clean)
|
.clean(self.args.clean)
|
||||||
.launch()
|
.launch()
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
Prelogin::Standard(prelogin) => {
|
Prelogin::Standard(prelogin) => {
|
||||||
println!("{}", prelogin.auth_message());
|
let prefix = if is_gateway { "Gateway" } else { "Portal" };
|
||||||
|
println!("{} ({}: {})", prelogin.auth_message(), prefix, server);
|
||||||
|
|
||||||
let user = self.args.user.as_ref().map_or_else(
|
let user = self.args.user.as_ref().map_or_else(
|
||||||
|| Text::new(&format!("{}:", prelogin.label_username())).prompt(),
|
|| Text::new(&format!("{}:", prelogin.label_username())).prompt(),
|
||||||
@@ -148,3 +225,11 @@ fn write_pid_file() {
|
|||||||
fs::write(GP_CLIENT_LOCK_FILE, pid.to_string()).unwrap();
|
fs::write(GP_CLIENT_LOCK_FILE, pid.to_string()).unwrap();
|
||||||
info!("Wrote PID {} to {}", pid, GP_CLIENT_LOCK_FILE);
|
info!("Wrote PID {} to {}", pid, GP_CLIENT_LOCK_FILE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_csd_uid(csd_user: &Option<String>) -> anyhow::Result<u32> {
|
||||||
|
if let Some(csd_user) = csd_user {
|
||||||
|
get_user_by_name(csd_user).map(|user| user.uid())
|
||||||
|
} else {
|
||||||
|
get_non_root_user().map_or_else(|_| Ok(0), |user| Ok(user.uid()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -10,7 +10,12 @@ use log::info;
|
|||||||
|
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
pub(crate) struct LaunchGuiArgs {
|
pub(crate) struct LaunchGuiArgs {
|
||||||
#[clap(long, help = "Launch the GUI minimized")]
|
#[arg(
|
||||||
|
required = false,
|
||||||
|
help = "The authentication data, used for the default browser authentication"
|
||||||
|
)]
|
||||||
|
auth_data: Option<String>,
|
||||||
|
#[arg(long, help = "Launch the GUI minimized")]
|
||||||
minimized: bool,
|
minimized: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,6 +35,12 @@ impl<'a> LaunchGuiHandler<'a> {
|
|||||||
anyhow::bail!("`launch-gui` cannot be run as root");
|
anyhow::bail!("`launch-gui` cannot be run as root");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let auth_data = self.args.auth_data.as_deref().unwrap_or_default();
|
||||||
|
if !auth_data.is_empty() {
|
||||||
|
// Process the authentication data, its format is `globalprotectcallback:<data>`
|
||||||
|
return feed_auth_data(auth_data).await;
|
||||||
|
}
|
||||||
|
|
||||||
if try_active_gui().await.is_ok() {
|
if try_active_gui().await.is_ok() {
|
||||||
info!("The GUI is already running");
|
info!("The GUI is already running");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
@@ -66,6 +77,19 @@ impl<'a> LaunchGuiHandler<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn feed_auth_data(auth_data: &str) -> anyhow::Result<()> {
|
||||||
|
let service_endpoint = http_endpoint().await?;
|
||||||
|
|
||||||
|
reqwest::Client::default()
|
||||||
|
.post(format!("{}/auth-data", service_endpoint))
|
||||||
|
.json(&auth_data)
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.error_for_status()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
async fn try_active_gui() -> anyhow::Result<()> {
|
async fn try_active_gui() -> anyhow::Result<()> {
|
||||||
let service_endpoint = http_endpoint().await?;
|
let service_endpoint = http_endpoint().await?;
|
||||||
|
|
||||||
|
36
apps/gpgui-helper/.eslintrc.cjs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
module.exports = {
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
es2021: true,
|
||||||
|
},
|
||||||
|
extends: [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
"plugin:react/recommended",
|
||||||
|
"plugin:react/jsx-runtime",
|
||||||
|
"plugin:react-hooks/recommended",
|
||||||
|
"prettier",
|
||||||
|
],
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
env: {
|
||||||
|
node: true,
|
||||||
|
},
|
||||||
|
files: [".eslintrc.{js,cjs}"],
|
||||||
|
parserOptions: {
|
||||||
|
sourceType: "script",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
parser: "@typescript-eslint/parser",
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: "latest",
|
||||||
|
sourceType: "module",
|
||||||
|
},
|
||||||
|
plugins: ["@typescript-eslint", "react"],
|
||||||
|
rules: {
|
||||||
|
"react-hooks/rules-of-hooks": "error",
|
||||||
|
"react-hooks/exhaustive-deps": "error",
|
||||||
|
"@typescript-eslint/no-unused-vars": "warn",
|
||||||
|
},
|
||||||
|
};
|
24
apps/gpgui-helper/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
0
apps/gpgui-helper/.prettierignore
Normal file
3
apps/gpgui-helper/.prettierrc
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"printWidth": 100
|
||||||
|
}
|
7
apps/gpgui-helper/README.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# Tauri + React + Typescript
|
||||||
|
|
||||||
|
This template should help get you started developing with Tauri, React and Typescript in Vite.
|
||||||
|
|
||||||
|
## Recommended IDE Setup
|
||||||
|
|
||||||
|
- [VS Code](https://code.visualstudio.com/) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)
|
19
apps/gpgui-helper/index.html
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>GlobalProtect</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script>
|
||||||
|
/* workaround to webview font size auto scaling */
|
||||||
|
var htmlFontSize = getComputedStyle(document.documentElement).fontSize;
|
||||||
|
var ratio = parseInt(htmlFontSize, 10) / 16;
|
||||||
|
document.documentElement.style.fontSize = 16 / ratio + "px";
|
||||||
|
</script>
|
||||||
|
<div id="root" data-tauri-drag-region></div>
|
||||||
|
<script type="module" src="/src/pages/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
34
apps/gpgui-helper/package.json
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"name": "gpgui",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"tauri": "tauri"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@mui/icons-material": "^5.14.18",
|
||||||
|
"@mui/material": "^5.14.18",
|
||||||
|
"@tauri-apps/api": "^1.5.0",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tauri-apps/cli": "^1.5.0",
|
||||||
|
"@types/node": "^20.8.10",
|
||||||
|
"@types/react": "^18.2.15",
|
||||||
|
"@types/react-dom": "^18.2.7",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^6.12.0",
|
||||||
|
"@typescript-eslint/parser": "^6.12.0",
|
||||||
|
"@vitejs/plugin-react": "^4.0.3",
|
||||||
|
"eslint": "^8.54.0",
|
||||||
|
"eslint-config-prettier": "^9.0.0",
|
||||||
|
"eslint-plugin-react": "^7.33.2",
|
||||||
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
|
"prettier": "3.1.0",
|
||||||
|
"typescript": "^5.0.2",
|
||||||
|
"vite": "^4.4.4"
|
||||||
|
}
|
||||||
|
}
|
2932
apps/gpgui-helper/pnpm-lock.yaml
generated
Normal file
6
apps/gpgui-helper/public/tauri.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<svg width="206" height="231" viewBox="0 0 206 231" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M143.143 84C143.143 96.1503 133.293 106 121.143 106C108.992 106 99.1426 96.1503 99.1426 84C99.1426 71.8497 108.992 62 121.143 62C133.293 62 143.143 71.8497 143.143 84Z" fill="#FFC131"/>
|
||||||
|
<ellipse cx="84.1426" cy="147" rx="22" ry="22" transform="rotate(180 84.1426 147)" fill="#24C8DB"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M166.738 154.548C157.86 160.286 148.023 164.269 137.757 166.341C139.858 160.282 141 153.774 141 147C141 144.543 140.85 142.121 140.558 139.743C144.975 138.204 149.215 136.139 153.183 133.575C162.73 127.404 170.292 118.608 174.961 108.244C179.63 97.8797 181.207 86.3876 179.502 75.1487C177.798 63.9098 172.884 53.4021 165.352 44.8883C157.82 36.3744 147.99 30.2165 137.042 27.1546C126.095 24.0926 114.496 24.2568 103.64 27.6274C92.7839 30.998 83.1319 37.4317 75.8437 46.1553C74.9102 47.2727 74.0206 48.4216 73.176 49.5993C61.9292 50.8488 51.0363 54.0318 40.9629 58.9556C44.2417 48.4586 49.5653 38.6591 56.679 30.1442C67.0505 17.7298 80.7861 8.57426 96.2354 3.77762C111.685 -1.01901 128.19 -1.25267 143.769 3.10474C159.348 7.46215 173.337 16.2252 184.056 28.3411C194.775 40.457 201.767 55.4101 204.193 71.404C206.619 87.3978 204.374 103.752 197.73 118.501C191.086 133.25 180.324 145.767 166.738 154.548ZM41.9631 74.275L62.5557 76.8042C63.0459 72.813 63.9401 68.9018 65.2138 65.1274C57.0465 67.0016 49.2088 70.087 41.9631 74.275Z" fill="#FFC131"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M38.4045 76.4519C47.3493 70.6709 57.2677 66.6712 67.6171 64.6132C65.2774 70.9669 64 77.8343 64 85.0001C64 87.1434 64.1143 89.26 64.3371 91.3442C60.0093 92.8732 55.8533 94.9092 51.9599 97.4256C42.4128 103.596 34.8505 112.392 30.1816 122.756C25.5126 133.12 23.9357 144.612 25.6403 155.851C27.3449 167.09 32.2584 177.598 39.7906 186.112C47.3227 194.626 57.153 200.784 68.1003 203.846C79.0476 206.907 90.6462 206.743 101.502 203.373C112.359 200.002 122.011 193.568 129.299 184.845C130.237 183.722 131.131 182.567 131.979 181.383C143.235 180.114 154.132 176.91 164.205 171.962C160.929 182.49 155.596 192.319 148.464 200.856C138.092 213.27 124.357 222.426 108.907 227.222C93.458 232.019 76.9524 232.253 61.3736 227.895C45.7948 223.538 31.8055 214.775 21.0867 202.659C10.3679 190.543 3.37557 175.59 0.949823 159.596C-1.47592 143.602 0.768139 127.248 7.41237 112.499C14.0566 97.7497 24.8183 85.2327 38.4045 76.4519ZM163.062 156.711L163.062 156.711C162.954 156.773 162.846 156.835 162.738 156.897C162.846 156.835 162.954 156.773 163.062 156.711Z" fill="#24C8DB"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.5 KiB |
1
apps/gpgui-helper/public/vite.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
After Width: | Height: | Size: 1.5 KiB |
4
apps/gpgui-helper/src-tauri/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# Generated by Cargo
|
||||||
|
# will have compiled files and executables
|
||||||
|
/target/
|
||||||
|
|
25
apps/gpgui-helper/src-tauri/Cargo.toml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
[package]
|
||||||
|
name = "gpgui-helper"
|
||||||
|
authors.workspace = true
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
tauri-build = { version = "1.5", features = [] }
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tauri = { workspace = true, features = [
|
||||||
|
"window-start-dragging",
|
||||||
|
] }
|
||||||
|
tokio.workspace = true
|
||||||
|
anyhow.workspace = true
|
||||||
|
log.workspace = true
|
||||||
|
clap.workspace = true
|
||||||
|
compile-time.workspace = true
|
||||||
|
env_logger.workspace = true
|
||||||
|
base64.workspace = true
|
||||||
|
async-stream = "0.3"
|
||||||
|
futures-util.workspace = true
|
||||||
|
downloader = "0.2"
|
||||||
|
tempfile.workspace = true
|
3
apps/gpgui-helper/src-tauri/build.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
tauri_build::build()
|
||||||
|
}
|
BIN
apps/gpgui-helper/src-tauri/icons/128x128.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
apps/gpgui-helper/src-tauri/icons/128x128@2x.png
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
apps/gpgui-helper/src-tauri/icons/32x32.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
apps/gpgui-helper/src-tauri/icons/icon.icns
Normal file
BIN
apps/gpgui-helper/src-tauri/icons/icon.ico
Normal file
After Width: | Height: | Size: 44 KiB |
BIN
apps/gpgui-helper/src-tauri/icons/icon.png
Normal file
After Width: | Height: | Size: 83 KiB |
99
apps/gpgui-helper/src-tauri/icons/icon.svg
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
<?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>
|
After Width: | Height: | Size: 6.7 KiB |
92
apps/gpgui-helper/src-tauri/src/app.rs
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
use std::{sync::Arc, time::Duration};
|
||||||
|
|
||||||
|
use downloader::{progress::Reporter, Download, Downloader};
|
||||||
|
use tauri::{window::MenuHandle, Manager};
|
||||||
|
use tempfile::TempDir;
|
||||||
|
|
||||||
|
pub struct App {
|
||||||
|
api_key: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl App {
|
||||||
|
pub fn new(api_key: Vec<u8>) -> Self {
|
||||||
|
Self { api_key }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run(&self) -> anyhow::Result<()> {
|
||||||
|
tauri::Builder::default()
|
||||||
|
.setup(|app| {
|
||||||
|
let win = app.get_window("main").unwrap();
|
||||||
|
|
||||||
|
tauri::async_runtime::spawn(async move {
|
||||||
|
hide_menu(win.menu_handle());
|
||||||
|
let _ = download_gui();
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.run(tauri::generate_context!())?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fix the bug that the menu bar is visible on Ubuntu 18.04
|
||||||
|
fn hide_menu(menu_handle: MenuHandle) {
|
||||||
|
tokio::spawn(async move {
|
||||||
|
loop {
|
||||||
|
let menu_visible = menu_handle.is_visible().unwrap_or(false);
|
||||||
|
|
||||||
|
if !menu_visible {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if menu_visible {
|
||||||
|
let _ = menu_handle.hide();
|
||||||
|
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DownloadProgress {}
|
||||||
|
|
||||||
|
impl Reporter for DownloadProgress {
|
||||||
|
fn setup(&self, max_progress: Option<u64>, message: &str) {
|
||||||
|
println!("{}: {}", message, max_progress.unwrap_or(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn progress(&self, current: u64) {
|
||||||
|
println!("progress: {}", current);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_message(&self, message: &str) {
|
||||||
|
println!("message: {}", message);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn done(&self) {
|
||||||
|
println!("done")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn download_gui() -> anyhow::Result<()> {
|
||||||
|
let tmp_dir = TempDir::new()?;
|
||||||
|
let tmp_dir = tmp_dir.into_path();
|
||||||
|
|
||||||
|
let mut downloader = Downloader::builder().download_folder(&tmp_dir).build()?;
|
||||||
|
|
||||||
|
let dl = Download::new("https://github.com/yuezk/GlobalProtect-openconnect/releases/download/v2.0.0/globalprotect-openconnect_2.0.0_x86_64.bin.tar.gz");
|
||||||
|
let progress = Arc::new(DownloadProgress {});
|
||||||
|
let dl = dl.progress(progress);
|
||||||
|
|
||||||
|
let result = downloader.download(&[dl])?;
|
||||||
|
|
||||||
|
for r in result {
|
||||||
|
match r {
|
||||||
|
Ok(s) => println!("Downloaded: {}", s),
|
||||||
|
Err(e) => println!("Error: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
50
apps/gpgui-helper/src-tauri/src/cli.rs
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
use base64::prelude::*;
|
||||||
|
use clap::Parser;
|
||||||
|
use log::{info, LevelFilter};
|
||||||
|
|
||||||
|
use crate::app::App;
|
||||||
|
|
||||||
|
const VERSION: &str = concat!(env!("CARGO_PKG_VERSION"), " (", compile_time::date_str!(), ")");
|
||||||
|
const GP_API_KEY: &[u8; 32] = &[0; 32];
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
#[command(version = VERSION)]
|
||||||
|
struct Cli {}
|
||||||
|
|
||||||
|
impl Cli {
|
||||||
|
fn run(&self) -> anyhow::Result<()> {
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
let api_key = GP_API_KEY.to_vec();
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
let api_key = self.read_api_key()?;
|
||||||
|
|
||||||
|
let app = App::new(api_key);
|
||||||
|
|
||||||
|
app.run()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_api_key(&self) -> anyhow::Result<Vec<u8>> {
|
||||||
|
let mut api_key = String::new();
|
||||||
|
std::io::stdin().read_line(&mut api_key)?;
|
||||||
|
|
||||||
|
let api_key = BASE64_STANDARD.decode(api_key.trim())?;
|
||||||
|
|
||||||
|
Ok(api_key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init_logger() {
|
||||||
|
env_logger::builder().filter_level(LevelFilter::Info).init();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run() {
|
||||||
|
let cli = Cli::parse();
|
||||||
|
|
||||||
|
init_logger();
|
||||||
|
info!("gpgui-helper started: {}", VERSION);
|
||||||
|
|
||||||
|
if let Err(e) = cli.run() {
|
||||||
|
eprintln!("{}", e);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
3
apps/gpgui-helper/src-tauri/src/lib.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
pub(crate) mod app;
|
||||||
|
|
||||||
|
pub mod cli;
|
8
apps/gpgui-helper/src-tauri/src/main.rs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||||
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
|
use gpgui_helper::cli;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
cli::run()
|
||||||
|
}
|
47
apps/gpgui-helper/src-tauri/tauri.conf.json
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
||||||
|
"build": {
|
||||||
|
"beforeDevCommand": "pnpm dev",
|
||||||
|
"beforeBuildCommand": "echo 'Skipping Vite build command...'",
|
||||||
|
"devPath": "http://localhost:1420",
|
||||||
|
"distDir": "../dist",
|
||||||
|
"withGlobalTauri": false
|
||||||
|
},
|
||||||
|
"package": {
|
||||||
|
"productName": "gpgui-helper"
|
||||||
|
},
|
||||||
|
"tauri": {
|
||||||
|
"allowlist": {
|
||||||
|
"all": false,
|
||||||
|
"window": {
|
||||||
|
"all": false,
|
||||||
|
"startDragging": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bundle": {
|
||||||
|
"active": true,
|
||||||
|
"targets": "deb",
|
||||||
|
"identifier": "com.yuezk.gpgui-helper",
|
||||||
|
"icon": [
|
||||||
|
"icons/32x32.png",
|
||||||
|
"icons/128x128.png",
|
||||||
|
"icons/128x128@2x.png",
|
||||||
|
"icons/icon.icns",
|
||||||
|
"icons/icon.ico"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"csp": null
|
||||||
|
},
|
||||||
|
"windows": [
|
||||||
|
{
|
||||||
|
"title": "GlobalProtect GUI Helper",
|
||||||
|
"center": true,
|
||||||
|
"resizable": true,
|
||||||
|
"width": 480,
|
||||||
|
"height": 100,
|
||||||
|
"label": "main"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
99
apps/gpgui-helper/src/assets/icon.svg
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
<?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>
|
After Width: | Height: | Size: 6.7 KiB |
91
apps/gpgui-helper/src/components/App/App.tsx
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
CssBaseline,
|
||||||
|
LinearProgress,
|
||||||
|
LinearProgressProps,
|
||||||
|
Typography,
|
||||||
|
} from "@mui/material";
|
||||||
|
|
||||||
|
import "./styles.css";
|
||||||
|
|
||||||
|
import logo from "../../assets/icon.svg";
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<CssBaseline />
|
||||||
|
<Box
|
||||||
|
sx={{ position: "absolute", inset: 0 }}
|
||||||
|
display="flex"
|
||||||
|
alignItems="center"
|
||||||
|
px={2}
|
||||||
|
data-tauri-drag-region
|
||||||
|
>
|
||||||
|
<Box display="flex" alignItems="center" flex="1" data-tauri-drag-region>
|
||||||
|
<Box
|
||||||
|
component="img"
|
||||||
|
src={logo}
|
||||||
|
alt="logo"
|
||||||
|
sx={{ width: 64, height: 64 }}
|
||||||
|
data-tauri-drag-region
|
||||||
|
/>
|
||||||
|
<Box flex={1} ml={2}>
|
||||||
|
<DownloadIndicator />
|
||||||
|
{/* <DownloadFailed /> */}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DownloadIndicator() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Typography variant="h1" fontSize="1rem" data-tauri-drag-region>
|
||||||
|
Updating the GUI components...
|
||||||
|
</Typography>
|
||||||
|
<Box mt={1}>
|
||||||
|
<LinearProgressWithLabel value={50} />
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DownloadFailed() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Typography variant="h1" fontSize="1rem" data-tauri-drag-region>
|
||||||
|
Failed to update the GUI components.
|
||||||
|
</Typography>
|
||||||
|
<Box mt={1} data-tauri-drag-region>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
size="small"
|
||||||
|
sx={{
|
||||||
|
textTransform: "none",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Retry
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function LinearProgressWithLabel(props: LinearProgressProps & { value: number }) {
|
||||||
|
return (
|
||||||
|
<Box sx={{ display: "flex", alignItems: "center" }}>
|
||||||
|
<Box sx={{ width: "100%", mr: 1 }}>
|
||||||
|
<LinearProgress variant="determinate" {...props} />
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ minWidth: 35 }}>
|
||||||
|
<Typography variant="body2" color="text.secondary">{`${Math.round(
|
||||||
|
props.value,
|
||||||
|
)}%`}</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
10
apps/gpgui-helper/src/components/App/styles.css
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
html,
|
||||||
|
body,
|
||||||
|
#root {
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
cursor: default;
|
||||||
|
}
|
6
apps/gpgui-helper/src/pages/main.tsx
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { createRoot } from "react-dom/client"
|
||||||
|
import App from "../components/App/App";
|
||||||
|
|
||||||
|
const rootApp = createRoot(document.getElementById('root') as HTMLElement);
|
||||||
|
|
||||||
|
rootApp.render(<App />);
|
0
apps/gpgui-helper/src/types.d.ts
vendored
Normal file
1
apps/gpgui-helper/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
25
apps/gpgui-helper/tsconfig.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
|
}
|
10
apps/gpgui-helper/tsconfig.node.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
30
apps/gpgui-helper/vite.config.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
import { resolve } from "path";
|
||||||
|
import { defineConfig } from "vite";
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig(async () => {
|
||||||
|
return {
|
||||||
|
plugins: [react()],
|
||||||
|
|
||||||
|
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
|
||||||
|
//
|
||||||
|
// 1. prevent vite from obscuring rust errors
|
||||||
|
clearScreen: false,
|
||||||
|
// 2. tauri expects a fixed port, fail if that port is not available
|
||||||
|
server: {
|
||||||
|
port: 1420,
|
||||||
|
strictPort: true,
|
||||||
|
},
|
||||||
|
// 3. to make use of `TAURI_DEBUG` and other env variables
|
||||||
|
// https://tauri.app/v1/api/config#buildconfig.beforedevcommand
|
||||||
|
envPrefix: ["VITE_", "TAURI_"],
|
||||||
|
build: {
|
||||||
|
rollupOptions: {
|
||||||
|
input: {
|
||||||
|
main: resolve(__dirname, "index.html"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
@@ -1,19 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE policyconfig PUBLIC "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN" "http://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd">
|
|
||||||
<policyconfig>
|
|
||||||
<vendor>GlobalProtect-openconnect</vendor>
|
|
||||||
<vendor_url>https://github.com/yuezk/GlobalProtect-openconnect</vendor_url>
|
|
||||||
<icon_name>gpgui</icon_name>
|
|
||||||
<action id="com.yuezk.gpservice">
|
|
||||||
<description>Run GPService as root</description>
|
|
||||||
<message>Authentication is required to run the GPService as root</message>
|
|
||||||
<defaults>
|
|
||||||
<allow_any>yes</allow_any>
|
|
||||||
<allow_inactive>yes</allow_inactive>
|
|
||||||
<allow_active>yes</allow_active>
|
|
||||||
</defaults>
|
|
||||||
<annotate key="org.freedesktop.policykit.exec.path">/home/kevin/Documents/repos/gp/target/debug/gpservice</annotate>
|
|
||||||
<annotate key="org.freedesktop.policykit.exec.argv1">--with-gui</annotate>
|
|
||||||
<annotate key="org.freedesktop.policykit.exec.allow_gui">true</annotate>
|
|
||||||
</action>
|
|
||||||
</policyconfig>
|
|
@@ -6,9 +6,7 @@ use clap::Parser;
|
|||||||
use gpapi::{
|
use gpapi::{
|
||||||
process::gui_launcher::GuiLauncher,
|
process::gui_launcher::GuiLauncher,
|
||||||
service::{request::WsRequest, vpn_state::VpnState},
|
service::{request::WsRequest, vpn_state::VpnState},
|
||||||
utils::{
|
utils::{crypto::generate_key, env_file, lock_file::LockFile, redact::Redaction, shutdown_signal},
|
||||||
crypto::generate_key, env_file, lock_file::LockFile, redact::Redaction, shutdown_signal,
|
|
||||||
},
|
|
||||||
GP_SERVICE_LOCK_FILE,
|
GP_SERVICE_LOCK_FILE,
|
||||||
};
|
};
|
||||||
use log::{info, warn, LevelFilter};
|
use log::{info, warn, LevelFilter};
|
||||||
@@ -16,12 +14,7 @@ use tokio::sync::{mpsc, watch};
|
|||||||
|
|
||||||
use crate::{vpn_task::VpnTask, ws_server::WsServer};
|
use crate::{vpn_task::VpnTask, ws_server::WsServer};
|
||||||
|
|
||||||
const VERSION: &str = concat!(
|
const VERSION: &str = concat!(env!("CARGO_PKG_VERSION"), " (", compile_time::date_str!(), ")");
|
||||||
env!("CARGO_PKG_VERSION"),
|
|
||||||
" (",
|
|
||||||
compile_time::date_str!(),
|
|
||||||
")"
|
|
||||||
);
|
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(version = VERSION)]
|
#[command(version = VERSION)]
|
||||||
@@ -51,13 +44,7 @@ impl Cli {
|
|||||||
let (vpn_state_tx, vpn_state_rx) = watch::channel(VpnState::Disconnected);
|
let (vpn_state_tx, vpn_state_rx) = watch::channel(VpnState::Disconnected);
|
||||||
|
|
||||||
let mut vpn_task = VpnTask::new(ws_req_rx, vpn_state_tx);
|
let mut vpn_task = VpnTask::new(ws_req_rx, vpn_state_tx);
|
||||||
let ws_server = WsServer::new(
|
let ws_server = WsServer::new(api_key.clone(), ws_req_tx, vpn_state_rx, lock_file.clone(), redaction);
|
||||||
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, mut shutdown_rx) = mpsc::channel::<()>(4);
|
||||||
let shutdown_tx_clone = shutdown_tx.clone();
|
let shutdown_tx_clone = shutdown_tx.clone();
|
||||||
@@ -76,11 +63,7 @@ impl Cli {
|
|||||||
if no_gui {
|
if no_gui {
|
||||||
info!("GUI is disabled");
|
info!("GUI is disabled");
|
||||||
} else {
|
} else {
|
||||||
let envs = self
|
let envs = self.env_file.as_ref().map(env_file::load_env_vars).transpose()?;
|
||||||
.env_file
|
|
||||||
.as_ref()
|
|
||||||
.map(env_file::load_env_vars)
|
|
||||||
.transpose()?;
|
|
||||||
|
|
||||||
let minimized = self.minimized;
|
let minimized = self.minimized;
|
||||||
|
|
||||||
|
@@ -21,10 +21,11 @@ pub(crate) async fn active_gui(State(ctx): State<Arc<WsServerContext>>) -> impl
|
|||||||
ctx.send_event(WsEvent::ActiveGui).await;
|
ctx.send_event(WsEvent::ActiveGui).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn ws_handler(
|
pub(crate) async fn auth_data(State(ctx): State<Arc<WsServerContext>>, body: String) -> impl IntoResponse {
|
||||||
ws: WebSocketUpgrade,
|
ctx.send_event(WsEvent::AuthData(body)).await;
|
||||||
State(ctx): State<Arc<WsServerContext>>,
|
}
|
||||||
) -> impl IntoResponse {
|
|
||||||
|
pub(crate) async fn ws_handler(ws: WebSocketUpgrade, State(ctx): State<Arc<WsServerContext>>) -> impl IntoResponse {
|
||||||
ws.on_upgrade(move |socket| handle_socket(socket, ctx))
|
ws.on_upgrade(move |socket| handle_socket(socket, ctx))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -2,8 +2,8 @@ mod cli;
|
|||||||
mod handlers;
|
mod handlers;
|
||||||
mod routes;
|
mod routes;
|
||||||
mod vpn_task;
|
mod vpn_task;
|
||||||
mod ws_server;
|
|
||||||
mod ws_connection;
|
mod ws_connection;
|
||||||
|
mod ws_server;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
|
@@ -1,6 +1,9 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use axum::{routing::{get, post}, Router};
|
use axum::{
|
||||||
|
routing::{get, post},
|
||||||
|
Router,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::{handlers, ws_server::WsServerContext};
|
use crate::{handlers, ws_server::WsServerContext};
|
||||||
|
|
||||||
@@ -8,6 +11,7 @@ pub(crate) fn routes(ctx: Arc<WsServerContext>) -> Router {
|
|||||||
Router::new()
|
Router::new()
|
||||||
.route("/health", get(handlers::health))
|
.route("/health", get(handlers::health))
|
||||||
.route("/active-gui", post(handlers::active_gui))
|
.route("/active-gui", post(handlers::active_gui))
|
||||||
|
.route("/auth-data", post(handlers::auth_data))
|
||||||
.route("/ws", get(handlers::ws_handler))
|
.route("/ws", get(handlers::ws_handler))
|
||||||
.with_state(ctx)
|
.with_state(ctx)
|
||||||
}
|
}
|
||||||
|
@@ -32,11 +32,13 @@ impl VpnTaskContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let info = req.info().clone();
|
let info = req.info().clone();
|
||||||
let vpn_handle = self.vpn_handle.clone();
|
let vpn_handle = Arc::clone(&self.vpn_handle);
|
||||||
let args = req.args();
|
let args = req.args();
|
||||||
let vpn = Vpn::builder(req.gateway().server(), args.cookie())
|
let vpn = Vpn::builder(req.gateway().server(), args.cookie())
|
||||||
.user_agent(args.user_agent())
|
.user_agent(args.user_agent())
|
||||||
.script(args.vpnc_script())
|
.script(args.vpnc_script())
|
||||||
|
.csd_uid(args.csd_uid())
|
||||||
|
.csd_wrapper(args.csd_wrapper())
|
||||||
.os(args.openconnect_os())
|
.os(args.openconnect_os())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
@@ -73,7 +75,9 @@ impl VpnTaskContext {
|
|||||||
|
|
||||||
pub async fn disconnect(&self) {
|
pub async fn disconnect(&self) {
|
||||||
if let Some(disconnect_rx) = self.disconnect_rx.write().await.take() {
|
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() {
|
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();
|
self.vpn_state_tx.send(VpnState::Disconnecting).ok();
|
||||||
vpn.disconnect()
|
vpn.disconnect()
|
||||||
}
|
}
|
||||||
|
@@ -98,12 +98,7 @@ impl WsServer {
|
|||||||
lock_file: Arc<LockFile>,
|
lock_file: Arc<LockFile>,
|
||||||
redaction: Arc<Redaction>,
|
redaction: Arc<Redaction>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let ctx = Arc::new(WsServerContext::new(
|
let ctx = Arc::new(WsServerContext::new(api_key, ws_req_tx, vpn_state_rx, redaction));
|
||||||
api_key,
|
|
||||||
ws_req_tx,
|
|
||||||
vpn_state_rx,
|
|
||||||
redaction,
|
|
||||||
));
|
|
||||||
let cancel_token = CancellationToken::new();
|
let cancel_token = CancellationToken::new();
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
|
@@ -25,8 +25,14 @@ url.workspace = true
|
|||||||
regex.workspace = true
|
regex.workspace = true
|
||||||
dotenvy_macro.workspace = true
|
dotenvy_macro.workspace = true
|
||||||
uzers.workspace = true
|
uzers.workspace = true
|
||||||
|
serde_urlencoded.workspace = true
|
||||||
|
md5.workspace = true
|
||||||
|
|
||||||
tauri = { workspace = true, optional = true }
|
tauri = { workspace = true, optional = true }
|
||||||
|
clap = { workspace = true, optional = true }
|
||||||
|
open = { version = "5", optional = true }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
tauri = ["dep:tauri"]
|
tauri = ["dep:tauri"]
|
||||||
|
clap = ["dep:clap"]
|
||||||
|
browser-auth = ["dep:open"]
|
||||||
|
@@ -1,3 +1,5 @@
|
|||||||
|
use anyhow::bail;
|
||||||
|
use regex::Regex;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
@@ -25,11 +27,7 @@ impl SamlAuthResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl SamlAuthData {
|
impl SamlAuthData {
|
||||||
pub fn new(
|
pub fn new(username: String, prelogin_cookie: Option<String>, portal_userauthcookie: Option<String>) -> Self {
|
||||||
username: String,
|
|
||||||
prelogin_cookie: Option<String>,
|
|
||||||
portal_userauthcookie: Option<String>,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
Self {
|
||||||
username,
|
username,
|
||||||
prelogin_cookie,
|
prelogin_cookie,
|
||||||
@@ -37,6 +35,32 @@ impl SamlAuthData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn parse_html(html: &str) -> anyhow::Result<SamlAuthData> {
|
||||||
|
match parse_xml_tag(html, "saml-auth-status") {
|
||||||
|
Some(saml_status) if saml_status == "1" => {
|
||||||
|
let username = parse_xml_tag(html, "saml-username");
|
||||||
|
let prelogin_cookie = parse_xml_tag(html, "prelogin-cookie");
|
||||||
|
let portal_userauthcookie = parse_xml_tag(html, "portal-userauthcookie");
|
||||||
|
|
||||||
|
if SamlAuthData::check(&username, &prelogin_cookie, &portal_userauthcookie) {
|
||||||
|
return Ok(SamlAuthData::new(
|
||||||
|
username.unwrap(),
|
||||||
|
prelogin_cookie,
|
||||||
|
portal_userauthcookie,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
bail!("Found invalid auth data in HTML");
|
||||||
|
}
|
||||||
|
Some(status) => {
|
||||||
|
bail!("Found invalid SAML status {} in HTML", status);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
bail!("No auth data found in HTML");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn username(&self) -> &str {
|
pub fn username(&self) -> &str {
|
||||||
&self.username
|
&self.username
|
||||||
}
|
}
|
||||||
@@ -50,14 +74,17 @@ impl SamlAuthData {
|
|||||||
prelogin_cookie: &Option<String>,
|
prelogin_cookie: &Option<String>,
|
||||||
portal_userauthcookie: &Option<String>,
|
portal_userauthcookie: &Option<String>,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
let username_valid = username
|
let username_valid = username.as_ref().is_some_and(|username| !username.is_empty());
|
||||||
.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 prelogin_cookie_valid = prelogin_cookie.as_ref().is_some_and(|val| val.len() > 5);
|
||||||
let portal_userauthcookie_valid = portal_userauthcookie
|
let portal_userauthcookie_valid = portal_userauthcookie.as_ref().is_some_and(|val| val.len() > 5);
|
||||||
.as_ref()
|
|
||||||
.is_some_and(|val| val.len() > 5);
|
|
||||||
|
|
||||||
username_valid && (prelogin_cookie_valid || portal_userauthcookie_valid)
|
username_valid && (prelogin_cookie_valid || portal_userauthcookie_valid)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn parse_xml_tag(html: &str, tag: &str) -> Option<String> {
|
||||||
|
let re = Regex::new(&format!("<{}>(.*)</{}>", tag, tag)).unwrap();
|
||||||
|
re.captures(html)
|
||||||
|
.and_then(|captures| captures.get(1))
|
||||||
|
.map(|m| m.as_str().to_string())
|
||||||
|
}
|
||||||
|
64
crates/gpapi/src/clap/args.rs
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
use clap::{builder::PossibleValue, ValueEnum};
|
||||||
|
|
||||||
|
use crate::gp_params::ClientOs;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum Os {
|
||||||
|
Linux,
|
||||||
|
Windows,
|
||||||
|
Mac,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Os {
|
||||||
|
pub fn as_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Os::Linux => "Linux",
|
||||||
|
Os::Windows => "Windows",
|
||||||
|
Os::Mac => "Mac",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&str> for Os {
|
||||||
|
fn from(os: &str) -> Self {
|
||||||
|
match os.to_lowercase().as_str() {
|
||||||
|
"linux" => Os::Linux,
|
||||||
|
"windows" => Os::Windows,
|
||||||
|
"mac" => Os::Mac,
|
||||||
|
_ => Os::Linux,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&Os> for ClientOs {
|
||||||
|
fn from(value: &Os) -> Self {
|
||||||
|
match value {
|
||||||
|
Os::Linux => ClientOs::Linux,
|
||||||
|
Os::Windows => ClientOs::Windows,
|
||||||
|
Os::Mac => ClientOs::Mac,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ValueEnum for Os {
|
||||||
|
fn value_variants<'a>() -> &'a [Self] {
|
||||||
|
&[Os::Linux, Os::Windows, Os::Mac]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
|
||||||
|
match self {
|
||||||
|
Os::Linux => Some(PossibleValue::new("Linux")),
|
||||||
|
Os::Windows => Some(PossibleValue::new("Windows")),
|
||||||
|
Os::Mac => Some(PossibleValue::new("Mac")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_str(input: &str, _: bool) -> Result<Self, String> {
|
||||||
|
match input.to_lowercase().as_str() {
|
||||||
|
"linux" => Ok(Os::Linux),
|
||||||
|
"windows" => Ok(Os::Windows),
|
||||||
|
"mac" => Ok(Os::Mac),
|
||||||
|
_ => Err(format!("Invalid OS: {}", input)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1
crates/gpapi/src/clap/mod.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pub mod args;
|
@@ -3,7 +3,7 @@ use std::collections::HashMap;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use specta::Type;
|
use specta::Type;
|
||||||
|
|
||||||
use crate::auth::SamlAuthData;
|
use crate::{auth::SamlAuthData, utils::base64::decode_to_string};
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Type, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Type, Clone)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
@@ -112,11 +112,7 @@ pub struct CachedCredential {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl CachedCredential {
|
impl CachedCredential {
|
||||||
pub fn new(
|
pub fn new(username: String, password: Option<String>, auth_cookie: AuthCookieCredential) -> Self {
|
||||||
username: String,
|
|
||||||
password: Option<String>,
|
|
||||||
auth_cookie: AuthCookieCredential,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
Self {
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
@@ -139,6 +135,24 @@ impl CachedCredential {
|
|||||||
pub fn set_auth_cookie(&mut self, auth_cookie: AuthCookieCredential) {
|
pub fn set_auth_cookie(&mut self, auth_cookie: AuthCookieCredential) {
|
||||||
self.auth_cookie = auth_cookie;
|
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)]
|
#[derive(Debug, Serialize, Deserialize, Type, Clone)]
|
||||||
@@ -151,6 +165,17 @@ pub enum Credential {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Credential {
|
impl Credential {
|
||||||
|
/// Create a credential from a globalprotectcallback:<base64 encoded string>
|
||||||
|
pub fn parse_gpcallback(auth_data: &str) -> anyhow::Result<Self> {
|
||||||
|
// Remove the surrounding quotes
|
||||||
|
let auth_data = auth_data.trim_matches('"');
|
||||||
|
let auth_data = auth_data.trim_start_matches("globalprotectcallback:");
|
||||||
|
let auth_data = decode_to_string(auth_data)?;
|
||||||
|
let auth_data = SamlAuthData::parse_html(&auth_data)?;
|
||||||
|
|
||||||
|
Self::try_from(auth_data)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn username(&self) -> &str {
|
pub fn username(&self) -> &str {
|
||||||
match self {
|
match self {
|
||||||
Credential::Password(cred) => cred.username(),
|
Credential::Password(cred) => cred.username(),
|
||||||
@@ -164,31 +189,30 @@ impl Credential {
|
|||||||
let mut params = HashMap::new();
|
let mut params = HashMap::new();
|
||||||
params.insert("user", self.username());
|
params.insert("user", self.username());
|
||||||
|
|
||||||
match self {
|
let (passwd, prelogin_cookie, portal_userauthcookie, portal_prelogonuserauthcookie) = match self {
|
||||||
Credential::Password(cred) => {
|
Credential::Password(cred) => (Some(cred.password()), None, None, None),
|
||||||
params.insert("passwd", cred.password());
|
Credential::PreloginCookie(cred) => (None, Some(cred.prelogin_cookie()), None, None),
|
||||||
}
|
Credential::AuthCookie(cred) => (
|
||||||
Credential::PreloginCookie(cred) => {
|
None,
|
||||||
params.insert("prelogin-cookie", cred.prelogin_cookie());
|
None,
|
||||||
}
|
Some(cred.user_auth_cookie()),
|
||||||
Credential::AuthCookie(cred) => {
|
Some(cred.prelogon_user_auth_cookie()),
|
||||||
params.insert("portal-userauthcookie", cred.user_auth_cookie());
|
),
|
||||||
|
Credential::CachedCredential(cred) => (
|
||||||
|
cred.password(),
|
||||||
|
None,
|
||||||
|
Some(cred.auth_cookie.user_auth_cookie()),
|
||||||
|
Some(cred.auth_cookie.prelogon_user_auth_cookie()),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
params.insert("passwd", passwd.unwrap_or_default());
|
||||||
|
params.insert("prelogin-cookie", prelogin_cookie.unwrap_or_default());
|
||||||
|
params.insert("portal-userauthcookie", portal_userauthcookie.unwrap_or_default());
|
||||||
params.insert(
|
params.insert(
|
||||||
"portal-prelogonuserauthcookie",
|
"portal-prelogonuserauthcookie",
|
||||||
cred.prelogon_user_auth_cookie(),
|
portal_prelogonuserauthcookie.unwrap_or_default(),
|
||||||
);
|
);
|
||||||
}
|
|
||||||
Credential::CachedCredential(cred) => {
|
|
||||||
if let Some(password) = cred.password() {
|
|
||||||
params.insert("passwd", password);
|
|
||||||
}
|
|
||||||
params.insert("portal-userauthcookie", cred.auth_cookie.user_auth_cookie());
|
|
||||||
params.insert(
|
|
||||||
"portal-prelogonuserauthcookie",
|
|
||||||
cred.auth_cookie.prelogon_user_auth_cookie(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
params
|
params
|
||||||
}
|
}
|
||||||
|
178
crates/gpapi/src/gateway/hip.rs
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use log::{info, warn};
|
||||||
|
use reqwest::Client;
|
||||||
|
use roxmltree::Document;
|
||||||
|
|
||||||
|
use crate::{gp_params::GpParams, process::hip_launcher::HipLauncher, utils::normalize_server};
|
||||||
|
|
||||||
|
struct HipReporter<'a> {
|
||||||
|
server: String,
|
||||||
|
cookie: &'a str,
|
||||||
|
md5: &'a str,
|
||||||
|
csd_wrapper: &'a str,
|
||||||
|
gp_params: &'a GpParams,
|
||||||
|
client: Client,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HipReporter<'_> {
|
||||||
|
async fn report(&self) -> anyhow::Result<()> {
|
||||||
|
let client_ip = self.retrieve_client_ip().await?;
|
||||||
|
|
||||||
|
let hip_needed = match self.check_hip(&client_ip).await {
|
||||||
|
Ok(hip_needed) => hip_needed,
|
||||||
|
Err(err) => {
|
||||||
|
warn!("Failed to check HIP: {}", err);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if !hip_needed {
|
||||||
|
info!("HIP report not needed");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("HIP report needed, generating report...");
|
||||||
|
let report = self.generate_report(&client_ip).await?;
|
||||||
|
|
||||||
|
if let Err(err) = self.submit_hip(&client_ip, &report).await {
|
||||||
|
warn!("Failed to submit HIP report: {}", err);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn retrieve_client_ip(&self) -> anyhow::Result<String> {
|
||||||
|
let config_url = format!("{}/ssl-vpn/getconfig.esp", self.server);
|
||||||
|
let mut params: HashMap<&str, &str> = HashMap::new();
|
||||||
|
|
||||||
|
params.insert("client-type", "1");
|
||||||
|
params.insert("protocol-version", "p1");
|
||||||
|
params.insert("internal", "no");
|
||||||
|
params.insert("ipv6-support", "yes");
|
||||||
|
params.insert("clientos", self.gp_params.client_os());
|
||||||
|
params.insert("hmac-algo", "sha1,md5,sha256");
|
||||||
|
params.insert("enc-algo", "aes-128-cbc,aes-256-cbc");
|
||||||
|
|
||||||
|
if let Some(os_version) = self.gp_params.os_version() {
|
||||||
|
params.insert("os-version", os_version);
|
||||||
|
}
|
||||||
|
if let Some(client_version) = self.gp_params.client_version() {
|
||||||
|
params.insert("app-version", client_version);
|
||||||
|
}
|
||||||
|
|
||||||
|
let params = merge_cookie_params(self.cookie, ¶ms)?;
|
||||||
|
|
||||||
|
let res = self.client.post(&config_url).form(¶ms).send().await?;
|
||||||
|
let res_xml = res.error_for_status()?.text().await?;
|
||||||
|
let doc = Document::parse(&res_xml)?;
|
||||||
|
|
||||||
|
// Get <ip-address>
|
||||||
|
let ip = doc
|
||||||
|
.descendants()
|
||||||
|
.find(|n| n.has_tag_name("ip-address"))
|
||||||
|
.and_then(|n| n.text())
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("ip-address not found"))?;
|
||||||
|
|
||||||
|
Ok(ip.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn check_hip(&self, client_ip: &str) -> anyhow::Result<bool> {
|
||||||
|
let url = format!("{}/ssl-vpn/hipreportcheck.esp", self.server);
|
||||||
|
let mut params = HashMap::new();
|
||||||
|
|
||||||
|
params.insert("client-role", "global-protect-full");
|
||||||
|
params.insert("client-ip", client_ip);
|
||||||
|
params.insert("md5", self.md5);
|
||||||
|
|
||||||
|
let params = merge_cookie_params(self.cookie, ¶ms)?;
|
||||||
|
let res = self.client.post(&url).form(¶ms).send().await?;
|
||||||
|
let res_xml = res.error_for_status()?.text().await?;
|
||||||
|
|
||||||
|
is_hip_needed(&res_xml)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn generate_report(&self, client_ip: &str) -> anyhow::Result<String> {
|
||||||
|
let launcher = HipLauncher::new(self.csd_wrapper)
|
||||||
|
.cookie(self.cookie)
|
||||||
|
.md5(self.md5)
|
||||||
|
.client_ip(client_ip)
|
||||||
|
.client_os(self.gp_params.client_os())
|
||||||
|
.client_version(self.gp_params.client_version());
|
||||||
|
|
||||||
|
launcher.launch().await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn submit_hip(&self, client_ip: &str, report: &str) -> anyhow::Result<()> {
|
||||||
|
let url = format!("{}/ssl-vpn/hipreport.esp", self.server);
|
||||||
|
|
||||||
|
let mut params = HashMap::new();
|
||||||
|
params.insert("client-role", "global-protect-full");
|
||||||
|
params.insert("client-ip", client_ip);
|
||||||
|
params.insert("report", report);
|
||||||
|
|
||||||
|
let params = merge_cookie_params(self.cookie, ¶ms)?;
|
||||||
|
let res = self.client.post(&url).form(¶ms).send().await?;
|
||||||
|
let res_xml = res.error_for_status()?.text().await?;
|
||||||
|
|
||||||
|
info!("HIP check response: {}", res_xml);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_hip_needed(res_xml: &str) -> anyhow::Result<bool> {
|
||||||
|
let doc = Document::parse(res_xml)?;
|
||||||
|
|
||||||
|
let hip_needed = doc
|
||||||
|
.descendants()
|
||||||
|
.find(|n| n.has_tag_name("hip-report-needed"))
|
||||||
|
.and_then(|n| n.text())
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("hip-report-needed not found"))?;
|
||||||
|
|
||||||
|
Ok(hip_needed == "yes")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn merge_cookie_params(cookie: &str, params: &HashMap<&str, &str>) -> anyhow::Result<HashMap<String, String>> {
|
||||||
|
let cookie_params = serde_urlencoded::from_str::<HashMap<String, String>>(cookie)?;
|
||||||
|
let params = params
|
||||||
|
.iter()
|
||||||
|
.map(|(k, v)| (k.to_string(), v.to_string()))
|
||||||
|
.chain(cookie_params)
|
||||||
|
.collect::<HashMap<String, String>>();
|
||||||
|
|
||||||
|
Ok(params)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute md5 for fields except authcookie,preferred-ip,preferred-ipv6
|
||||||
|
fn build_csd_token(cookie: &str) -> anyhow::Result<String> {
|
||||||
|
let mut cookie_params = serde_urlencoded::from_str::<Vec<(String, String)>>(cookie)?;
|
||||||
|
cookie_params.retain(|(k, _)| k != "authcookie" && k != "preferred-ip" && k != "preferred-ipv6");
|
||||||
|
|
||||||
|
let token = serde_urlencoded::to_string(cookie_params)?;
|
||||||
|
let md5 = format!("{:x}", md5::compute(token));
|
||||||
|
|
||||||
|
Ok(md5)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn hip_report(gateway: &str, cookie: &str, csd_wrapper: &str, gp_params: &GpParams) -> anyhow::Result<()> {
|
||||||
|
let client = Client::builder()
|
||||||
|
.danger_accept_invalid_certs(gp_params.ignore_tls_errors())
|
||||||
|
.user_agent(gp_params.user_agent())
|
||||||
|
.build()?;
|
||||||
|
|
||||||
|
let md5 = build_csd_token(cookie)?;
|
||||||
|
|
||||||
|
info!("Submit HIP report md5: {}", md5);
|
||||||
|
|
||||||
|
let reporter = HipReporter {
|
||||||
|
server: normalize_server(gateway)?,
|
||||||
|
cookie,
|
||||||
|
md5: &md5,
|
||||||
|
csd_wrapper,
|
||||||
|
gp_params,
|
||||||
|
client,
|
||||||
|
};
|
||||||
|
|
||||||
|
reporter.report().await
|
||||||
|
}
|
@@ -1,17 +1,22 @@
|
|||||||
|
use anyhow::bail;
|
||||||
use log::info;
|
use log::info;
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use roxmltree::Document;
|
use roxmltree::Document;
|
||||||
use urlencoding::encode;
|
use urlencoding::encode;
|
||||||
|
|
||||||
use crate::{credential::Credential, gp_params::GpParams};
|
use crate::{
|
||||||
|
credential::Credential,
|
||||||
|
gp_params::GpParams,
|
||||||
|
utils::{normalize_server, remove_url_scheme},
|
||||||
|
};
|
||||||
|
|
||||||
pub async fn gateway_login(
|
pub async fn gateway_login(gateway: &str, cred: &Credential, gp_params: &GpParams) -> anyhow::Result<String> {
|
||||||
gateway: &str,
|
let url = normalize_server(gateway)?;
|
||||||
cred: &Credential,
|
let gateway = remove_url_scheme(&url);
|
||||||
gp_params: &GpParams,
|
|
||||||
) -> anyhow::Result<String> {
|
let login_url = format!("{}/ssl-vpn/login.esp", url);
|
||||||
let login_url = format!("https://{}/ssl-vpn/login.esp", gateway);
|
|
||||||
let client = Client::builder()
|
let client = Client::builder()
|
||||||
|
.danger_accept_invalid_certs(gp_params.ignore_tls_errors())
|
||||||
.user_agent(gp_params.user_agent())
|
.user_agent(gp_params.user_agent())
|
||||||
.build()?;
|
.build()?;
|
||||||
|
|
||||||
@@ -19,19 +24,18 @@ pub async fn gateway_login(
|
|||||||
let extra_params = gp_params.to_params();
|
let extra_params = gp_params.to_params();
|
||||||
|
|
||||||
params.extend(extra_params);
|
params.extend(extra_params);
|
||||||
params.insert("server", gateway);
|
params.insert("server", &gateway);
|
||||||
|
|
||||||
info!("Gateway login, user_agent: {}", gp_params.user_agent());
|
info!("Gateway login, user_agent: {}", gp_params.user_agent());
|
||||||
|
|
||||||
let res_xml = client
|
let res = client.post(&login_url).form(¶ms).send().await?;
|
||||||
.post(&login_url)
|
let status = res.status();
|
||||||
.form(¶ms)
|
|
||||||
.send()
|
|
||||||
.await?
|
|
||||||
.error_for_status()?
|
|
||||||
.text()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
|
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)?;
|
let doc = Document::parse(&res_xml)?;
|
||||||
|
|
||||||
build_gateway_token(&doc, gp_params.computer())
|
build_gateway_token(&doc, gp_params.computer())
|
||||||
@@ -62,11 +66,7 @@ fn build_gateway_token(doc: &Document, computer: &str) -> anyhow::Result<String>
|
|||||||
Ok(token)
|
Ok(token)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_args<'a>(
|
fn read_args<'a>(args: &'a [String], index: usize, key: &'a str) -> anyhow::Result<(&'a str, &'a str)> {
|
||||||
args: &'a [String],
|
|
||||||
index: usize,
|
|
||||||
key: &'a str,
|
|
||||||
) -> anyhow::Result<(&'a str, &'a str)> {
|
|
||||||
args
|
args
|
||||||
.get(index)
|
.get(index)
|
||||||
.ok_or_else(|| anyhow::anyhow!("Failed to read {key} from args"))
|
.ok_or_else(|| anyhow::anyhow!("Failed to read {key} from args"))
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
mod login;
|
mod login;
|
||||||
mod parse_gateways;
|
mod parse_gateways;
|
||||||
|
pub mod hip;
|
||||||
|
|
||||||
pub use login::*;
|
pub use login::*;
|
||||||
pub(crate) use parse_gateways::*;
|
pub(crate) use parse_gateways::*;
|
||||||
@@ -31,6 +32,15 @@ impl Display for Gateway {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Gateway {
|
impl Gateway {
|
||||||
|
pub fn new(name: String, address: String) -> Self {
|
||||||
|
Self {
|
||||||
|
name,
|
||||||
|
address,
|
||||||
|
priority: 0,
|
||||||
|
priority_rules: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn name(&self) -> &str {
|
pub fn name(&self) -> &str {
|
||||||
&self.name
|
&self.name
|
||||||
}
|
}
|
||||||
|
@@ -4,9 +4,7 @@ use super::{Gateway, PriorityRule};
|
|||||||
|
|
||||||
pub(crate) fn parse_gateways(doc: &Document) -> Option<Vec<Gateway>> {
|
pub(crate) fn parse_gateways(doc: &Document) -> Option<Vec<Gateway>> {
|
||||||
let node_gateways = doc.descendants().find(|n| n.has_tag_name("gateways"))?;
|
let node_gateways = doc.descendants().find(|n| n.has_tag_name("gateways"))?;
|
||||||
let list_gateway = node_gateways
|
let list_gateway = node_gateways.descendants().find(|n| n.has_tag_name("list"))?;
|
||||||
.descendants()
|
|
||||||
.find(|n| n.has_tag_name("list"))?;
|
|
||||||
|
|
||||||
let gateways = list_gateway
|
let gateways = list_gateway
|
||||||
.children()
|
.children()
|
||||||
|
@@ -7,23 +7,32 @@ use crate::GP_USER_AGENT;
|
|||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone, Type, Default)]
|
#[derive(Debug, Serialize, Deserialize, Clone, Type, Default)]
|
||||||
pub enum ClientOs {
|
pub enum ClientOs {
|
||||||
Linux,
|
|
||||||
#[default]
|
#[default]
|
||||||
|
Linux,
|
||||||
Windows,
|
Windows,
|
||||||
Mac,
|
Mac,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&ClientOs> for &str {
|
impl From<&str> for ClientOs {
|
||||||
fn from(os: &ClientOs) -> Self {
|
fn from(os: &str) -> Self {
|
||||||
match os {
|
match os {
|
||||||
ClientOs::Linux => "Linux",
|
"Linux" => ClientOs::Linux,
|
||||||
ClientOs::Windows => "Windows",
|
"Windows" => ClientOs::Windows,
|
||||||
ClientOs::Mac => "Mac",
|
"Mac" => ClientOs::Mac,
|
||||||
|
_ => ClientOs::Linux,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ClientOs {
|
impl ClientOs {
|
||||||
|
pub fn as_str(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
ClientOs::Linux => "Linux",
|
||||||
|
ClientOs::Windows => "Windows",
|
||||||
|
ClientOs::Mac => "Mac",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn to_openconnect_os(&self) -> &str {
|
pub fn to_openconnect_os(&self) -> &str {
|
||||||
match self {
|
match self {
|
||||||
ClientOs::Linux => "linux",
|
ClientOs::Linux => "linux",
|
||||||
@@ -35,11 +44,14 @@ impl ClientOs {
|
|||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Type, Default)]
|
#[derive(Debug, Serialize, Deserialize, Type, Default)]
|
||||||
pub struct GpParams {
|
pub struct GpParams {
|
||||||
|
is_gateway: bool,
|
||||||
user_agent: String,
|
user_agent: String,
|
||||||
client_os: ClientOs,
|
client_os: ClientOs,
|
||||||
os_version: Option<String>,
|
os_version: Option<String>,
|
||||||
client_version: Option<String>,
|
client_version: Option<String>,
|
||||||
computer: Option<String>,
|
computer: String,
|
||||||
|
ignore_tls_errors: bool,
|
||||||
|
prefer_default_browser: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GpParams {
|
impl GpParams {
|
||||||
@@ -47,20 +59,45 @@ impl GpParams {
|
|||||||
GpParamsBuilder::new()
|
GpParamsBuilder::new()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn is_gateway(&self) -> bool {
|
||||||
|
self.is_gateway
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_is_gateway(&mut self, is_gateway: bool) {
|
||||||
|
self.is_gateway = is_gateway;
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn user_agent(&self) -> &str {
|
pub(crate) fn user_agent(&self) -> &str {
|
||||||
&self.user_agent
|
&self.user_agent
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn computer(&self) -> &str {
|
pub(crate) fn computer(&self) -> &str {
|
||||||
match self.computer {
|
&self.computer
|
||||||
Some(ref computer) => computer,
|
|
||||||
None => (&self.client_os).into()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn ignore_tls_errors(&self) -> bool {
|
||||||
|
self.ignore_tls_errors
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn prefer_default_browser(&self) -> bool {
|
||||||
|
self.prefer_default_browser
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn client_os(&self) -> &str {
|
||||||
|
self.client_os.as_str()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn os_version(&self) -> Option<&str> {
|
||||||
|
self.os_version.as_deref()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn client_version(&self) -> Option<&str> {
|
||||||
|
self.client_version.as_deref()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn to_params(&self) -> HashMap<&str, &str> {
|
pub(crate) fn to_params(&self) -> HashMap<&str, &str> {
|
||||||
let mut params: HashMap<&str, &str> = HashMap::new();
|
let mut params: HashMap<&str, &str> = HashMap::new();
|
||||||
let client_os: &str = (&self.client_os).into();
|
let client_os = self.client_os.as_str();
|
||||||
|
|
||||||
// Common params
|
// Common params
|
||||||
params.insert("prot", "https:");
|
params.insert("prot", "https:");
|
||||||
@@ -70,46 +107,52 @@ impl GpParams {
|
|||||||
params.insert("ipv6-support", "yes");
|
params.insert("ipv6-support", "yes");
|
||||||
params.insert("inputStr", "");
|
params.insert("inputStr", "");
|
||||||
params.insert("clientVer", "4100");
|
params.insert("clientVer", "4100");
|
||||||
|
|
||||||
params.insert("clientos", client_os);
|
params.insert("clientos", client_os);
|
||||||
|
params.insert("computer", &self.computer);
|
||||||
if let Some(computer) = &self.computer {
|
|
||||||
params.insert("computer", computer);
|
|
||||||
} else {
|
|
||||||
params.insert("computer", client_os);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(os_version) = &self.os_version {
|
if let Some(os_version) = &self.os_version {
|
||||||
params.insert("os-version", os_version);
|
params.insert("os-version", os_version);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(client_version) = &self.client_version {
|
// NOTE: Do not include clientgpversion for now
|
||||||
params.insert("clientgpversion", client_version);
|
// if let Some(client_version) = &self.client_version {
|
||||||
}
|
// params.insert("clientgpversion", client_version);
|
||||||
|
// }
|
||||||
|
|
||||||
params
|
params
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct GpParamsBuilder {
|
pub struct GpParamsBuilder {
|
||||||
|
is_gateway: bool,
|
||||||
user_agent: String,
|
user_agent: String,
|
||||||
client_os: ClientOs,
|
client_os: ClientOs,
|
||||||
os_version: Option<String>,
|
os_version: Option<String>,
|
||||||
client_version: Option<String>,
|
client_version: Option<String>,
|
||||||
computer: Option<String>,
|
computer: String,
|
||||||
|
ignore_tls_errors: bool,
|
||||||
|
prefer_default_browser: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GpParamsBuilder {
|
impl GpParamsBuilder {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
is_gateway: false,
|
||||||
user_agent: GP_USER_AGENT.to_string(),
|
user_agent: GP_USER_AGENT.to_string(),
|
||||||
client_os: ClientOs::Linux,
|
client_os: ClientOs::Linux,
|
||||||
os_version: Default::default(),
|
os_version: Default::default(),
|
||||||
client_version: Default::default(),
|
client_version: Default::default(),
|
||||||
computer: Default::default(),
|
computer: whoami::hostname(),
|
||||||
|
ignore_tls_errors: false,
|
||||||
|
prefer_default_browser: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_gateway(&mut self, is_gateway: bool) -> &mut Self {
|
||||||
|
self.is_gateway = is_gateway;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn user_agent(&mut self, user_agent: &str) -> &mut Self {
|
pub fn user_agent(&mut self, user_agent: &str) -> &mut Self {
|
||||||
self.user_agent = user_agent.to_string();
|
self.user_agent = user_agent.to_string();
|
||||||
self
|
self
|
||||||
@@ -120,28 +163,41 @@ impl GpParamsBuilder {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn os_version(&mut self, os_version: &str) -> &mut Self {
|
pub fn os_version<T: Into<Option<String>>>(&mut self, os_version: T) -> &mut Self {
|
||||||
self.os_version = Some(os_version.to_string());
|
self.os_version = os_version.into();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn client_version(&mut self, client_version: &str) -> &mut Self {
|
pub fn client_version<T: Into<Option<String>>>(&mut self, client_version: T) -> &mut Self {
|
||||||
self.client_version = Some(client_version.to_string());
|
self.client_version = client_version.into();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn computer(&mut self, computer: &str) -> &mut Self {
|
pub fn computer(&mut self, computer: &str) -> &mut Self {
|
||||||
self.computer = Some(computer.to_string());
|
self.computer = computer.to_string();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ignore_tls_errors(&mut self, ignore_tls_errors: bool) -> &mut Self {
|
||||||
|
self.ignore_tls_errors = ignore_tls_errors;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn prefer_default_browser(&mut self, prefer_default_browser: bool) -> &mut Self {
|
||||||
|
self.prefer_default_browser = prefer_default_browser;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build(&self) -> GpParams {
|
pub fn build(&self) -> GpParams {
|
||||||
GpParams {
|
GpParams {
|
||||||
|
is_gateway: self.is_gateway,
|
||||||
user_agent: self.user_agent.clone(),
|
user_agent: self.user_agent.clone(),
|
||||||
client_os: self.client_os.clone(),
|
client_os: self.client_os.clone(),
|
||||||
os_version: self.os_version.clone(),
|
os_version: self.os_version.clone(),
|
||||||
client_version: self.client_version.clone(),
|
client_version: self.client_version.clone(),
|
||||||
computer: self.computer.clone(),
|
computer: self.computer.clone(),
|
||||||
|
ignore_tls_errors: self.ignore_tls_errors,
|
||||||
|
prefer_default_browser: self.prefer_default_browser,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -7,12 +7,17 @@ pub mod process;
|
|||||||
pub mod service;
|
pub mod service;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
|
|
||||||
|
#[cfg(feature = "clap")]
|
||||||
|
pub mod clap;
|
||||||
|
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
pub const GP_API_KEY: &[u8; 32] = &[0; 32];
|
pub const GP_API_KEY: &[u8; 32] = &[0; 32];
|
||||||
|
|
||||||
pub const GP_USER_AGENT: &str = "PAN GlobalProtect";
|
pub const GP_USER_AGENT: &str = "PAN GlobalProtect";
|
||||||
pub const GP_SERVICE_LOCK_FILE: &str = "/var/run/gpservice.lock";
|
pub const GP_SERVICE_LOCK_FILE: &str = "/var/run/gpservice.lock";
|
||||||
|
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
pub const GP_CLIENT_BINARY: &str = "/usr/bin/gpclient";
|
||||||
#[cfg(not(debug_assertions))]
|
#[cfg(not(debug_assertions))]
|
||||||
pub const GP_SERVICE_BINARY: &str = "/usr/bin/gpservice";
|
pub const GP_SERVICE_BINARY: &str = "/usr/bin/gpservice";
|
||||||
#[cfg(not(debug_assertions))]
|
#[cfg(not(debug_assertions))]
|
||||||
@@ -20,6 +25,8 @@ pub const GP_GUI_BINARY: &str = "/usr/bin/gpgui";
|
|||||||
#[cfg(not(debug_assertions))]
|
#[cfg(not(debug_assertions))]
|
||||||
pub(crate) const GP_AUTH_BINARY: &str = "/usr/bin/gpauth";
|
pub(crate) const GP_AUTH_BINARY: &str = "/usr/bin/gpauth";
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
pub const GP_CLIENT_BINARY: &str = dotenvy_macro::dotenv!("GP_CLIENT_BINARY");
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
pub const GP_SERVICE_BINARY: &str = dotenvy_macro::dotenv!("GP_SERVICE_BINARY");
|
pub const GP_SERVICE_BINARY: &str = dotenvy_macro::dotenv!("GP_SERVICE_BINARY");
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
|
@@ -1,16 +1,16 @@
|
|||||||
use anyhow::ensure;
|
use anyhow::bail;
|
||||||
use log::info;
|
use log::info;
|
||||||
use reqwest::Client;
|
use reqwest::{Client, StatusCode};
|
||||||
use roxmltree::Document;
|
use roxmltree::Document;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use specta::Type;
|
use specta::Type;
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
credential::{AuthCookieCredential, Credential},
|
credential::{AuthCookieCredential, Credential},
|
||||||
gateway::{parse_gateways, Gateway},
|
gateway::{parse_gateways, Gateway},
|
||||||
gp_params::GpParams,
|
gp_params::GpParams,
|
||||||
utils::{normalize_server, xml},
|
portal::PortalError,
|
||||||
|
utils::{normalize_server, remove_url_scheme, xml},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Type)]
|
#[derive(Debug, Serialize, Type)]
|
||||||
@@ -18,25 +18,12 @@ use crate::{
|
|||||||
pub struct PortalConfig {
|
pub struct PortalConfig {
|
||||||
portal: String,
|
portal: String,
|
||||||
auth_cookie: AuthCookieCredential,
|
auth_cookie: AuthCookieCredential,
|
||||||
|
config_cred: Credential,
|
||||||
gateways: Vec<Gateway>,
|
gateways: Vec<Gateway>,
|
||||||
config_digest: Option<String>,
|
config_digest: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PortalConfig {
|
impl PortalConfig {
|
||||||
pub fn new(
|
|
||||||
portal: String,
|
|
||||||
auth_cookie: AuthCookieCredential,
|
|
||||||
gateways: Vec<Gateway>,
|
|
||||||
config_digest: Option<String>,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
portal,
|
|
||||||
auth_cookie,
|
|
||||||
gateways,
|
|
||||||
config_digest,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn portal(&self) -> &str {
|
pub fn portal(&self) -> &str {
|
||||||
&self.portal
|
&self.portal
|
||||||
}
|
}
|
||||||
@@ -49,6 +36,10 @@ impl PortalConfig {
|
|||||||
&self.auth_cookie
|
&self.auth_cookie
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn config_cred(&self) -> &Credential {
|
||||||
|
&self.config_cred
|
||||||
|
}
|
||||||
|
|
||||||
/// In-place sort the gateways by region
|
/// In-place sort the gateways by region
|
||||||
pub fn sort_gateways(&mut self, region: &str) {
|
pub fn sort_gateways(&mut self, region: &str) {
|
||||||
let preferred_gateway = self.find_preferred_gateway(region);
|
let preferred_gateway = self.find_preferred_gateway(region);
|
||||||
@@ -88,38 +79,17 @@ impl PortalConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If no gateway is found, return the gateway with the lowest priority
|
// If no gateway is found, return the gateway with the lowest priority
|
||||||
preferred_gateway.unwrap_or_else(|| {
|
preferred_gateway.unwrap_or_else(|| self.gateways.iter().min_by_key(|gateway| gateway.priority).unwrap())
|
||||||
self
|
|
||||||
.gateways
|
|
||||||
.iter()
|
|
||||||
.min_by_key(|gateway| gateway.priority)
|
|
||||||
.unwrap()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
pub async fn retrieve_config(portal: &str, cred: &Credential, gp_params: &GpParams) -> anyhow::Result<PortalConfig> {
|
||||||
pub enum PortalConfigError {
|
|
||||||
#[error("Empty response, retrying can help")]
|
|
||||||
EmptyResponse,
|
|
||||||
#[error("Empty auth cookie, retrying can help")]
|
|
||||||
EmptyAuthCookie,
|
|
||||||
#[error("Invalid auth cookie, retrying can help")]
|
|
||||||
InvalidAuthCookie,
|
|
||||||
#[error("Empty gateways, retrying can help")]
|
|
||||||
EmptyGateways,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn retrieve_config(
|
|
||||||
portal: &str,
|
|
||||||
cred: &Credential,
|
|
||||||
gp_params: &GpParams,
|
|
||||||
) -> anyhow::Result<PortalConfig> {
|
|
||||||
let portal = normalize_server(portal)?;
|
let portal = normalize_server(portal)?;
|
||||||
let server = remove_url_scheme(&portal);
|
let server = remove_url_scheme(&portal);
|
||||||
|
|
||||||
let url = format!("{}/global-protect/getconfig.esp", portal);
|
let url = format!("{}/global-protect/getconfig.esp", portal);
|
||||||
let client = Client::builder()
|
let client = Client::builder()
|
||||||
|
.danger_accept_invalid_certs(gp_params.ignore_tls_errors())
|
||||||
.user_agent(gp_params.user_agent())
|
.user_agent(gp_params.user_agent())
|
||||||
.build()?;
|
.build()?;
|
||||||
|
|
||||||
@@ -132,49 +102,43 @@ pub async fn retrieve_config(
|
|||||||
|
|
||||||
info!("Portal config, user_agent: {}", gp_params.user_agent());
|
info!("Portal config, user_agent: {}", gp_params.user_agent());
|
||||||
|
|
||||||
let res_xml = client
|
let res = client.post(&url).form(¶ms).send().await?;
|
||||||
.post(&url)
|
let status = res.status();
|
||||||
.form(¶ms)
|
|
||||||
.send()
|
|
||||||
.await?
|
|
||||||
.error_for_status()?
|
|
||||||
.text()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
ensure!(!res_xml.is_empty(), PortalConfigError::EmptyResponse);
|
if status == StatusCode::NOT_FOUND {
|
||||||
|
bail!(PortalError::ConfigError("Config endpoint not found".to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
let doc = Document::parse(&res_xml)?;
|
if status.is_client_error() || status.is_server_error() {
|
||||||
let gateways = parse_gateways(&doc).ok_or_else(|| anyhow::anyhow!("Failed to parse gateways"))?;
|
bail!("Portal config error: {}", status)
|
||||||
|
}
|
||||||
|
|
||||||
|
let res_xml = res.text().await.map_err(|e| PortalError::ConfigError(e.to_string()))?;
|
||||||
|
|
||||||
|
if res_xml.is_empty() {
|
||||||
|
bail!(PortalError::ConfigError("Empty portal config response".to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
let doc = Document::parse(&res_xml).map_err(|e| PortalError::ConfigError(e.to_string()))?;
|
||||||
|
|
||||||
|
let mut gateways = parse_gateways(&doc).unwrap_or_else(|| {
|
||||||
|
info!("No gateways found in portal config");
|
||||||
|
vec![]
|
||||||
|
});
|
||||||
|
|
||||||
let user_auth_cookie = xml::get_child_text(&doc, "portal-userauthcookie").unwrap_or_default();
|
let user_auth_cookie = xml::get_child_text(&doc, "portal-userauthcookie").unwrap_or_default();
|
||||||
let prelogon_user_auth_cookie =
|
let prelogon_user_auth_cookie = xml::get_child_text(&doc, "portal-prelogonuserauthcookie").unwrap_or_default();
|
||||||
xml::get_child_text(&doc, "portal-prelogonuserauthcookie").unwrap_or_default();
|
|
||||||
let config_digest = xml::get_child_text(&doc, "config-digest");
|
let config_digest = xml::get_child_text(&doc, "config-digest");
|
||||||
|
|
||||||
ensure!(
|
if gateways.is_empty() {
|
||||||
!user_auth_cookie.is_empty() && !prelogon_user_auth_cookie.is_empty(),
|
gateways.push(Gateway::new(server.to_string(), server.to_string()));
|
||||||
PortalConfigError::EmptyAuthCookie
|
}
|
||||||
);
|
|
||||||
|
|
||||||
ensure!(
|
Ok(PortalConfig {
|
||||||
user_auth_cookie != "empty" && prelogon_user_auth_cookie != "empty",
|
portal: server.to_string(),
|
||||||
PortalConfigError::InvalidAuthCookie
|
auth_cookie: AuthCookieCredential::new(cred.username(), &user_auth_cookie, &prelogon_user_auth_cookie),
|
||||||
);
|
config_cred: cred.clone(),
|
||||||
|
|
||||||
ensure!(!gateways.is_empty(), PortalConfigError::EmptyGateways);
|
|
||||||
|
|
||||||
Ok(PortalConfig::new(
|
|
||||||
server.to_string(),
|
|
||||||
AuthCookieCredential::new(
|
|
||||||
cred.username(),
|
|
||||||
&user_auth_cookie,
|
|
||||||
&prelogon_user_auth_cookie,
|
|
||||||
),
|
|
||||||
gateways,
|
gateways,
|
||||||
config_digest,
|
config_digest,
|
||||||
))
|
})
|
||||||
}
|
|
||||||
|
|
||||||
fn remove_url_scheme(s: &str) -> String {
|
|
||||||
s.replace("http://", "").replace("https://", "")
|
|
||||||
}
|
}
|
||||||
|
@@ -3,3 +3,13 @@ mod prelogin;
|
|||||||
|
|
||||||
pub use config::*;
|
pub use config::*;
|
||||||
pub use prelogin::*;
|
pub use prelogin::*;
|
||||||
|
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum PortalError {
|
||||||
|
#[error("Portal prelogin error: {0}")]
|
||||||
|
PreloginError(String),
|
||||||
|
#[error("Portal config error: {0}")]
|
||||||
|
ConfigError(String),
|
||||||
|
}
|
||||||
|
@@ -1,17 +1,34 @@
|
|||||||
use anyhow::bail;
|
use anyhow::bail;
|
||||||
use log::{info, trace};
|
use log::info;
|
||||||
use reqwest::Client;
|
use reqwest::{Client, StatusCode};
|
||||||
use roxmltree::Document;
|
use roxmltree::Document;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use specta::Type;
|
use specta::Type;
|
||||||
|
|
||||||
use crate::utils::{base64, normalize_server, xml};
|
use crate::{
|
||||||
|
gp_params::GpParams,
|
||||||
|
portal::PortalError,
|
||||||
|
utils::{base64, normalize_server, xml},
|
||||||
|
};
|
||||||
|
|
||||||
|
const REQUIRED_PARAMS: [&str; 8] = [
|
||||||
|
"tmp",
|
||||||
|
"clientVer",
|
||||||
|
"clientos",
|
||||||
|
"os-version",
|
||||||
|
"host-id",
|
||||||
|
"ipv6-support",
|
||||||
|
"default-browser",
|
||||||
|
"cas-support",
|
||||||
|
];
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Type, Clone)]
|
#[derive(Debug, Serialize, Type, Clone)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct SamlPrelogin {
|
pub struct SamlPrelogin {
|
||||||
region: String,
|
region: String,
|
||||||
|
is_gateway: bool,
|
||||||
saml_request: String,
|
saml_request: String,
|
||||||
|
support_default_browser: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SamlPrelogin {
|
impl SamlPrelogin {
|
||||||
@@ -22,12 +39,17 @@ impl SamlPrelogin {
|
|||||||
pub fn saml_request(&self) -> &str {
|
pub fn saml_request(&self) -> &str {
|
||||||
&self.saml_request
|
&self.saml_request
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn support_default_browser(&self) -> bool {
|
||||||
|
self.support_default_browser
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Type, Clone)]
|
#[derive(Debug, Serialize, Type, Clone)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct StandardPrelogin {
|
pub struct StandardPrelogin {
|
||||||
region: String,
|
region: String,
|
||||||
|
is_gateway: bool,
|
||||||
auth_message: String,
|
auth_message: String,
|
||||||
label_username: String,
|
label_username: String,
|
||||||
label_password: String,
|
label_password: String,
|
||||||
@@ -65,24 +87,59 @@ impl Prelogin {
|
|||||||
Prelogin::Standard(standard) => standard.region(),
|
Prelogin::Standard(standard) => standard.region(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_gateway(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
Prelogin::Saml(saml) => saml.is_gateway,
|
||||||
|
Prelogin::Standard(standard) => standard.is_gateway,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn prelogin(portal: &str, user_agent: &str) -> anyhow::Result<Prelogin> {
|
pub async fn prelogin(portal: &str, gp_params: &GpParams) -> anyhow::Result<Prelogin> {
|
||||||
info!("Portal prelogin, user_agent: {}", user_agent);
|
let user_agent = gp_params.user_agent();
|
||||||
|
info!("Prelogin with user_agent: {}", user_agent);
|
||||||
|
|
||||||
let portal = normalize_server(portal)?;
|
let portal = normalize_server(portal)?;
|
||||||
let prelogin_url = format!("{}/global-protect/prelogin.esp", portal);
|
let is_gateway = gp_params.is_gateway();
|
||||||
let client = Client::builder().user_agent(user_agent).build()?;
|
let path = if is_gateway { "ssl-vpn" } else { "global-protect" };
|
||||||
|
let prelogin_url = format!("{portal}/{}/prelogin.esp", path);
|
||||||
|
let mut params = gp_params.to_params();
|
||||||
|
|
||||||
let res_xml = client
|
params.insert("tmp", "tmp");
|
||||||
.get(&prelogin_url)
|
if gp_params.prefer_default_browser() {
|
||||||
.send()
|
params.insert("default-browser", "1");
|
||||||
.await?
|
}
|
||||||
.error_for_status()?
|
|
||||||
|
params.retain(|k, _| REQUIRED_PARAMS.iter().any(|required_param| required_param == k));
|
||||||
|
|
||||||
|
let client = Client::builder()
|
||||||
|
.danger_accept_invalid_certs(gp_params.ignore_tls_errors())
|
||||||
|
.user_agent(user_agent)
|
||||||
|
.build()?;
|
||||||
|
|
||||||
|
let res = client.post(&prelogin_url).form(¶ms).send().await?;
|
||||||
|
let status = res.status();
|
||||||
|
|
||||||
|
if status == StatusCode::NOT_FOUND {
|
||||||
|
bail!(PortalError::PreloginError("Prelogin endpoint not found".to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
if status.is_client_error() || status.is_server_error() {
|
||||||
|
bail!("Prelogin error: {}", status)
|
||||||
|
}
|
||||||
|
|
||||||
|
let res_xml = res
|
||||||
.text()
|
.text()
|
||||||
.await?;
|
.await
|
||||||
|
.map_err(|e| PortalError::PreloginError(e.to_string()))?;
|
||||||
|
|
||||||
trace!("Prelogin response: {}", res_xml);
|
let prelogin = parse_res_xml(res_xml, is_gateway).map_err(|e| PortalError::PreloginError(e.to_string()))?;
|
||||||
|
|
||||||
|
Ok(prelogin)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_res_xml(res_xml: String, is_gateway: bool) -> anyhow::Result<Prelogin> {
|
||||||
let doc = Document::parse(&res_xml)?;
|
let doc = Document::parse(&res_xml)?;
|
||||||
|
|
||||||
let status = xml::get_child_text(&doc, "status")
|
let status = xml::get_child_text(&doc, "status")
|
||||||
@@ -93,17 +150,24 @@ pub async fn prelogin(portal: &str, user_agent: &str) -> anyhow::Result<Prelogin
|
|||||||
bail!("Prelogin failed: {}", msg)
|
bail!("Prelogin failed: {}", msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
let region = xml::get_child_text(&doc, "region")
|
let region = xml::get_child_text(&doc, "region").unwrap_or_else(|| {
|
||||||
.ok_or_else(|| anyhow::anyhow!("Prelogin response does not contain region element"))?;
|
info!("Prelogin response does not contain region element");
|
||||||
|
String::from("Unknown")
|
||||||
|
});
|
||||||
|
|
||||||
let saml_method = xml::get_child_text(&doc, "saml-auth-method");
|
let saml_method = xml::get_child_text(&doc, "saml-auth-method");
|
||||||
let saml_request = xml::get_child_text(&doc, "saml-request");
|
let saml_request = xml::get_child_text(&doc, "saml-request");
|
||||||
|
let saml_default_browser = xml::get_child_text(&doc, "saml-default-browser");
|
||||||
// Check if the prelogin response is SAML
|
// Check if the prelogin response is SAML
|
||||||
if saml_method.is_some() && saml_request.is_some() {
|
if saml_method.is_some() && saml_request.is_some() {
|
||||||
let saml_request = base64::decode_to_string(&saml_request.unwrap())?;
|
let saml_request = base64::decode_to_string(&saml_request.unwrap())?;
|
||||||
|
let support_default_browser = saml_default_browser.map(|s| s.to_lowercase() == "yes").unwrap_or(false);
|
||||||
|
|
||||||
let saml_prelogin = SamlPrelogin {
|
let saml_prelogin = SamlPrelogin {
|
||||||
region,
|
region,
|
||||||
|
is_gateway,
|
||||||
saml_request,
|
saml_request,
|
||||||
|
support_default_browser,
|
||||||
};
|
};
|
||||||
|
|
||||||
return Ok(Prelogin::Saml(saml_prelogin));
|
return Ok(Prelogin::Saml(saml_prelogin));
|
||||||
@@ -113,10 +177,11 @@ pub async fn prelogin(portal: &str, user_agent: &str) -> anyhow::Result<Prelogin
|
|||||||
let label_password = xml::get_child_text(&doc, "password-label");
|
let label_password = xml::get_child_text(&doc, "password-label");
|
||||||
// Check if the prelogin response is standard login
|
// Check if the prelogin response is standard login
|
||||||
if label_username.is_some() && label_password.is_some() {
|
if label_username.is_some() && label_password.is_some() {
|
||||||
let auth_message = xml::get_child_text(&doc, "authentication-message")
|
let auth_message =
|
||||||
.unwrap_or(String::from("Please enter the login credentials"));
|
xml::get_child_text(&doc, "authentication-message").unwrap_or(String::from("Please enter the login credentials"));
|
||||||
let standard_prelogin = StandardPrelogin {
|
let standard_prelogin = StandardPrelogin {
|
||||||
region,
|
region,
|
||||||
|
is_gateway,
|
||||||
auth_message,
|
auth_message,
|
||||||
label_username: label_username.unwrap(),
|
label_username: label_username.unwrap(),
|
||||||
label_password: label_password.unwrap(),
|
label_password: label_password.unwrap(),
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
use std::process::Stdio;
|
use std::process::Stdio;
|
||||||
|
|
||||||
|
use anyhow::bail;
|
||||||
use tokio::process::Command;
|
use tokio::process::Command;
|
||||||
|
|
||||||
use crate::{auth::SamlAuthResult, credential::Credential, GP_AUTH_BINARY};
|
use crate::{auth::SamlAuthResult, credential::Credential, GP_AUTH_BINARY};
|
||||||
@@ -8,10 +9,14 @@ use super::command_traits::CommandExt;
|
|||||||
|
|
||||||
pub struct SamlAuthLauncher<'a> {
|
pub struct SamlAuthLauncher<'a> {
|
||||||
server: &'a str,
|
server: &'a str,
|
||||||
user_agent: Option<&'a str>,
|
gateway: bool,
|
||||||
saml_request: Option<&'a str>,
|
saml_request: Option<&'a str>,
|
||||||
|
user_agent: Option<&'a str>,
|
||||||
|
os: Option<&'a str>,
|
||||||
|
os_version: Option<&'a str>,
|
||||||
hidpi: bool,
|
hidpi: bool,
|
||||||
fix_openssl: bool,
|
fix_openssl: bool,
|
||||||
|
ignore_tls_errors: bool,
|
||||||
clean: bool,
|
clean: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,21 +24,40 @@ impl<'a> SamlAuthLauncher<'a> {
|
|||||||
pub fn new(server: &'a str) -> Self {
|
pub fn new(server: &'a str) -> Self {
|
||||||
Self {
|
Self {
|
||||||
server,
|
server,
|
||||||
user_agent: None,
|
gateway: false,
|
||||||
saml_request: None,
|
saml_request: None,
|
||||||
|
user_agent: None,
|
||||||
|
os: None,
|
||||||
|
os_version: None,
|
||||||
hidpi: false,
|
hidpi: false,
|
||||||
fix_openssl: false,
|
fix_openssl: false,
|
||||||
|
ignore_tls_errors: false,
|
||||||
clean: false,
|
clean: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn gateway(mut self, gateway: bool) -> Self {
|
||||||
|
self.gateway = gateway;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn saml_request(mut self, saml_request: &'a str) -> Self {
|
||||||
|
self.saml_request = Some(saml_request);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn user_agent(mut self, user_agent: &'a str) -> Self {
|
pub fn user_agent(mut self, user_agent: &'a str) -> Self {
|
||||||
self.user_agent = Some(user_agent);
|
self.user_agent = Some(user_agent);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn saml_request(mut self, saml_request: &'a str) -> Self {
|
pub fn os(mut self, os: &'a str) -> Self {
|
||||||
self.saml_request = Some(saml_request);
|
self.os = Some(os);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn os_version(mut self, os_version: Option<&'a str>) -> Self {
|
||||||
|
self.os_version = os_version;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,6 +71,11 @@ impl<'a> SamlAuthLauncher<'a> {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn ignore_tls_errors(mut self, ignore_tls_errors: bool) -> Self {
|
||||||
|
self.ignore_tls_errors = ignore_tls_errors;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn clean(mut self, clean: bool) -> Self {
|
pub fn clean(mut self, clean: bool) -> Self {
|
||||||
self.clean = clean;
|
self.clean = clean;
|
||||||
self
|
self
|
||||||
@@ -57,22 +86,38 @@ impl<'a> SamlAuthLauncher<'a> {
|
|||||||
let mut auth_cmd = Command::new(GP_AUTH_BINARY);
|
let mut auth_cmd = Command::new(GP_AUTH_BINARY);
|
||||||
auth_cmd.arg(self.server);
|
auth_cmd.arg(self.server);
|
||||||
|
|
||||||
if let Some(user_agent) = self.user_agent {
|
if self.gateway {
|
||||||
auth_cmd.arg("--user-agent").arg(user_agent);
|
auth_cmd.arg("--gateway");
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(saml_request) = self.saml_request {
|
if let Some(saml_request) = self.saml_request {
|
||||||
auth_cmd.arg("--saml-request").arg(saml_request);
|
auth_cmd.arg("--saml-request").arg(saml_request);
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.fix_openssl {
|
if let Some(user_agent) = self.user_agent {
|
||||||
auth_cmd.arg("--fix-openssl");
|
auth_cmd.arg("--user-agent").arg(user_agent);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(os) = self.os {
|
||||||
|
auth_cmd.arg("--os").arg(os);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(os_version) = self.os_version {
|
||||||
|
auth_cmd.arg("--os-version").arg(os_version);
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.hidpi {
|
if self.hidpi {
|
||||||
auth_cmd.arg("--hidpi");
|
auth_cmd.arg("--hidpi");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.fix_openssl {
|
||||||
|
auth_cmd.arg("--fix-openssl");
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.ignore_tls_errors {
|
||||||
|
auth_cmd.arg("--ignore-tls-errors");
|
||||||
|
}
|
||||||
|
|
||||||
if self.clean {
|
if self.clean {
|
||||||
auth_cmd.arg("--clean");
|
auth_cmd.arg("--clean");
|
||||||
}
|
}
|
||||||
@@ -85,12 +130,13 @@ impl<'a> SamlAuthLauncher<'a> {
|
|||||||
.wait_with_output()
|
.wait_with_output()
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let auth_result: SamlAuthResult = serde_json::from_slice(&output.stdout)
|
let Ok(auth_result) = serde_json::from_slice::<SamlAuthResult>(&output.stdout) else {
|
||||||
.map_err(|_| anyhow::anyhow!("Failed to parse auth data"))?;
|
bail!("Failed to parse auth data")
|
||||||
|
};
|
||||||
|
|
||||||
match auth_result {
|
match auth_result {
|
||||||
SamlAuthResult::Success(auth_data) => Credential::try_from(auth_data),
|
SamlAuthResult::Success(auth_data) => Credential::try_from(auth_data),
|
||||||
SamlAuthResult::Failure(msg) => Err(anyhow::anyhow!(msg)),
|
SamlAuthResult::Failure(msg) => bail!(msg),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
34
crates/gpapi/src/process/browser_authenticator.rs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
use std::{env::temp_dir, io::Write};
|
||||||
|
|
||||||
|
pub struct BrowserAuthenticator<'a> {
|
||||||
|
auth_request: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BrowserAuthenticator<'_> {
|
||||||
|
pub fn new(auth_request: &str) -> BrowserAuthenticator {
|
||||||
|
BrowserAuthenticator { auth_request }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn authenticate(&self) -> anyhow::Result<()> {
|
||||||
|
if self.auth_request.starts_with("http") {
|
||||||
|
open::that_detached(self.auth_request)?;
|
||||||
|
} else {
|
||||||
|
let html_file = temp_dir().join("gpauth.html");
|
||||||
|
let mut file = std::fs::File::create(&html_file)?;
|
||||||
|
|
||||||
|
file.write_all(self.auth_request.as_bytes())?;
|
||||||
|
|
||||||
|
open::that_detached(html_file)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for BrowserAuthenticator<'_> {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
// Cleanup the temporary file
|
||||||
|
let html_file = temp_dir().join("gpauth.html");
|
||||||
|
let _ = std::fs::remove_file(html_file);
|
||||||
|
}
|
||||||
|
}
|
@@ -1,7 +1,8 @@
|
|||||||
use anyhow::bail;
|
use std::ffi::OsStr;
|
||||||
use std::{env, ffi::OsStr};
|
|
||||||
use tokio::process::Command;
|
use tokio::process::Command;
|
||||||
use uzers::{os::unix::UserExt, User};
|
use uzers::os::unix::UserExt;
|
||||||
|
|
||||||
|
use super::users::get_non_root_user;
|
||||||
|
|
||||||
pub trait CommandExt {
|
pub trait CommandExt {
|
||||||
fn new_pkexec<S: AsRef<OsStr>>(program: S) -> Command;
|
fn new_pkexec<S: AsRef<OsStr>>(program: S) -> Command;
|
||||||
@@ -21,8 +22,7 @@ impl CommandExt for Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn into_non_root(mut self) -> anyhow::Result<Command> {
|
fn into_non_root(mut self) -> anyhow::Result<Command> {
|
||||||
let user =
|
let user = get_non_root_user().map_err(|_| anyhow::anyhow!("{:?} cannot be run as root", self))?;
|
||||||
get_non_root_user().map_err(|_| anyhow::anyhow!("{:?} cannot be run as root", self))?;
|
|
||||||
|
|
||||||
self
|
self
|
||||||
.env("HOME", user.home_dir())
|
.env("HOME", user.home_dir())
|
||||||
@@ -35,30 +35,3 @@ impl CommandExt for Command {
|
|||||||
Ok(self)
|
Ok(self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_non_root_user() -> anyhow::Result<User> {
|
|
||||||
let current_user = whoami::username();
|
|
||||||
|
|
||||||
let user = if current_user == "root" {
|
|
||||||
get_real_user()?
|
|
||||||
} else {
|
|
||||||
uzers::get_user_by_name(¤t_user)
|
|
||||||
.ok_or_else(|| anyhow::anyhow!("User ({}) not found", current_user))?
|
|
||||||
};
|
|
||||||
|
|
||||||
if user.uid() == 0 {
|
|
||||||
bail!("Non-root user not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(user)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_real_user() -> anyhow::Result<User> {
|
|
||||||
// Read the UID from SUDO_UID or PKEXEC_UID environment variable if available.
|
|
||||||
let uid = match env::var("SUDO_UID") {
|
|
||||||
Ok(uid) => uid.parse::<u32>()?,
|
|
||||||
_ => env::var("PKEXEC_UID")?.parse::<u32>()?,
|
|
||||||
};
|
|
||||||
|
|
||||||
uzers::get_user_by_uid(uid).ok_or_else(|| anyhow::anyhow!("User not found"))
|
|
||||||
}
|
|
||||||
|
@@ -66,10 +66,7 @@ impl GuiLauncher {
|
|||||||
|
|
||||||
let mut non_root_cmd = cmd.into_non_root()?;
|
let mut non_root_cmd = cmd.into_non_root()?;
|
||||||
|
|
||||||
let mut child = non_root_cmd
|
let mut child = non_root_cmd.kill_on_drop(true).stdin(Stdio::piped()).spawn()?;
|
||||||
.kill_on_drop(true)
|
|
||||||
.stdin(Stdio::piped())
|
|
||||||
.spawn()?;
|
|
||||||
|
|
||||||
let mut stdin = child
|
let mut stdin = child
|
||||||
.stdin
|
.stdin
|
||||||
|
94
crates/gpapi/src/process/hip_launcher.rs
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
use std::process::Stdio;
|
||||||
|
|
||||||
|
use anyhow::bail;
|
||||||
|
use tokio::process::Command;
|
||||||
|
|
||||||
|
pub struct HipLauncher<'a> {
|
||||||
|
program: &'a str,
|
||||||
|
cookie: Option<&'a str>,
|
||||||
|
client_ip: Option<&'a str>,
|
||||||
|
md5: Option<&'a str>,
|
||||||
|
client_os: Option<&'a str>,
|
||||||
|
client_version: Option<&'a str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> HipLauncher<'a> {
|
||||||
|
pub fn new(program: &'a str) -> Self {
|
||||||
|
Self {
|
||||||
|
program,
|
||||||
|
cookie: None,
|
||||||
|
client_ip: None,
|
||||||
|
md5: None,
|
||||||
|
client_os: None,
|
||||||
|
client_version: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cookie(mut self, cookie: &'a str) -> Self {
|
||||||
|
self.cookie = Some(cookie);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn client_ip(mut self, client_ip: &'a str) -> Self {
|
||||||
|
self.client_ip = Some(client_ip);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn md5(mut self, md5: &'a str) -> Self {
|
||||||
|
self.md5 = Some(md5);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn client_os(mut self, client_os: &'a str) -> Self {
|
||||||
|
self.client_os = Some(client_os);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn client_version(mut self, client_version: Option<&'a str>) -> Self {
|
||||||
|
self.client_version = client_version;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn launch(&self) -> anyhow::Result<String> {
|
||||||
|
let mut cmd = Command::new(self.program);
|
||||||
|
|
||||||
|
if let Some(cookie) = self.cookie {
|
||||||
|
cmd.arg("--cookie").arg(cookie);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(client_ip) = self.client_ip {
|
||||||
|
cmd.arg("--client-ip").arg(client_ip);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(md5) = self.md5 {
|
||||||
|
cmd.arg("--md5").arg(md5);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(client_os) = self.client_os {
|
||||||
|
cmd.arg("--client-os").arg(client_os);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(client_version) = self.client_version {
|
||||||
|
cmd.env("APP_VERSION", client_version);
|
||||||
|
}
|
||||||
|
|
||||||
|
let output = cmd
|
||||||
|
.kill_on_drop(true)
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.spawn()?
|
||||||
|
.wait_with_output()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if let Some(exit_status) = output.status.code() {
|
||||||
|
if exit_status != 0 {
|
||||||
|
bail!("HIP report generation failed with exit code {}", exit_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
let report = String::from_utf8(output.stdout)?;
|
||||||
|
|
||||||
|
Ok(report)
|
||||||
|
} else {
|
||||||
|
bail!("HIP report generation failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,5 +1,9 @@
|
|||||||
pub(crate) mod command_traits;
|
pub(crate) mod command_traits;
|
||||||
|
|
||||||
pub mod auth_launcher;
|
pub mod auth_launcher;
|
||||||
|
#[cfg(feature = "browser-auth")]
|
||||||
|
pub mod browser_authenticator;
|
||||||
pub mod gui_launcher;
|
pub mod gui_launcher;
|
||||||
|
pub mod hip_launcher;
|
||||||
pub mod service_launcher;
|
pub mod service_launcher;
|
||||||
|
pub mod users;
|
||||||
|
39
crates/gpapi/src/process/users.rs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
use std::env;
|
||||||
|
|
||||||
|
use anyhow::bail;
|
||||||
|
use uzers::User;
|
||||||
|
|
||||||
|
pub fn get_user_by_name(username: &str) -> anyhow::Result<User> {
|
||||||
|
uzers::get_user_by_name(username).ok_or_else(|| anyhow::anyhow!("User ({}) not found", username))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_non_root_user() -> anyhow::Result<User> {
|
||||||
|
let current_user = whoami::username();
|
||||||
|
|
||||||
|
let user = if current_user == "root" {
|
||||||
|
get_real_user()?
|
||||||
|
} else {
|
||||||
|
get_user_by_name(¤t_user)?
|
||||||
|
};
|
||||||
|
|
||||||
|
if user.uid() == 0 {
|
||||||
|
bail!("Non-root user not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(user)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_current_user() -> anyhow::Result<User> {
|
||||||
|
let current_user = whoami::username();
|
||||||
|
get_user_by_name(¤t_user)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_real_user() -> anyhow::Result<User> {
|
||||||
|
// Read the UID from SUDO_UID or PKEXEC_UID environment variable if available.
|
||||||
|
let uid = match env::var("SUDO_UID") {
|
||||||
|
Ok(uid) => uid.parse::<u32>()?,
|
||||||
|
_ => env::var("PKEXEC_UID")?.parse::<u32>()?,
|
||||||
|
};
|
||||||
|
|
||||||
|
uzers::get_user_by_uid(uid).ok_or_else(|| anyhow::anyhow!("User not found"))
|
||||||
|
}
|
@@ -7,4 +7,6 @@ use super::vpn_state::VpnState;
|
|||||||
pub enum WsEvent {
|
pub enum WsEvent {
|
||||||
VpnState(VpnState),
|
VpnState(VpnState),
|
||||||
ActiveGui,
|
ActiveGui,
|
||||||
|
/// External authentication data
|
||||||
|
AuthData(String),
|
||||||
}
|
}
|
||||||
|
@@ -32,6 +32,8 @@ pub struct ConnectArgs {
|
|||||||
cookie: String,
|
cookie: String,
|
||||||
vpnc_script: Option<String>,
|
vpnc_script: Option<String>,
|
||||||
user_agent: Option<String>,
|
user_agent: Option<String>,
|
||||||
|
csd_uid: u32,
|
||||||
|
csd_wrapper: Option<String>,
|
||||||
os: Option<ClientOs>,
|
os: Option<ClientOs>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,6 +44,8 @@ impl ConnectArgs {
|
|||||||
vpnc_script: None,
|
vpnc_script: None,
|
||||||
user_agent: None,
|
user_agent: None,
|
||||||
os: None,
|
os: None,
|
||||||
|
csd_uid: 0,
|
||||||
|
csd_wrapper: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,10 +62,15 @@ impl ConnectArgs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn openconnect_os(&self) -> Option<String> {
|
pub fn openconnect_os(&self) -> Option<String> {
|
||||||
self
|
self.os.as_ref().map(|os| os.to_openconnect_os().to_string())
|
||||||
.os
|
}
|
||||||
.as_ref()
|
|
||||||
.map(|os| os.to_openconnect_os().to_string())
|
pub fn csd_uid(&self) -> u32 {
|
||||||
|
self.csd_uid
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn csd_wrapper(&self) -> Option<String> {
|
||||||
|
self.csd_wrapper.clone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,6 +93,16 @@ impl ConnectRequest {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_csd_uid(mut self, csd_uid: u32) -> Self {
|
||||||
|
self.args.csd_uid = csd_uid;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_csd_wrapper<T: Into<Option<String>>>(mut self, csd_wrapper: T) -> Self {
|
||||||
|
self.args.csd_wrapper = csd_wrapper.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn with_user_agent<T: Into<Option<String>>>(mut self, user_agent: T) -> Self {
|
pub fn with_user_agent<T: Into<Option<String>>>(mut self, user_agent: T) -> Self {
|
||||||
self.args.user_agent = user_agent.into();
|
self.args.user_agent = user_agent.into();
|
||||||
self
|
self
|
||||||
|
@@ -30,11 +30,13 @@ pub fn normalize_server(server: &str) -> anyhow::Result<String> {
|
|||||||
.host_str()
|
.host_str()
|
||||||
.ok_or(anyhow::anyhow!("Invalid server URL: missing host"))?;
|
.ok_or(anyhow::anyhow!("Invalid server URL: missing host"))?;
|
||||||
|
|
||||||
let port: String = normalized_url
|
let port: String = normalized_url.port().map_or("".into(), |port| format!(":{}", port));
|
||||||
.port()
|
|
||||||
.map_or("".into(), |port| format!(":{}", port));
|
|
||||||
|
|
||||||
let normalized_url = format!("{}://{}{}", scheme, host, port);
|
let normalized_url = format!("{}://{}{}", scheme, host, port);
|
||||||
|
|
||||||
Ok(normalized_url)
|
Ok(normalized_url)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn remove_url_scheme(s: &str) -> String {
|
||||||
|
s.replace("http://", "").replace("https://", "")
|
||||||
|
}
|
||||||
|
@@ -115,12 +115,7 @@ pub fn redact_uri(uri: &str) -> String {
|
|||||||
.map(|query| format!("?{}", query))
|
.map(|query| format!("?{}", query))
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
return format!(
|
return format!("{}://[**********]{}{}", url.scheme(), url.path(), redacted_query);
|
||||||
"{}://[**********]{}{}",
|
|
||||||
url.scheme(),
|
|
||||||
url.path(),
|
|
||||||
redacted_query
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let redacted_query = redact_query(url.query());
|
let redacted_query = redact_query(url.query());
|
||||||
@@ -165,10 +160,7 @@ mod tests {
|
|||||||
|
|
||||||
redaction.add_value("foo").unwrap();
|
redaction.add_value("foo").unwrap();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(redaction.redact_str("hello, foo, bar"), "hello, [**********], bar");
|
||||||
redaction.redact_str("hello, foo, bar"),
|
|
||||||
"hello, [**********], bar"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@@ -2,9 +2,7 @@ use tokio::signal;
|
|||||||
|
|
||||||
pub async fn shutdown_signal() {
|
pub async fn shutdown_signal() {
|
||||||
let ctrl_c = async {
|
let ctrl_c = async {
|
||||||
signal::ctrl_c()
|
signal::ctrl_c().await.expect("failed to install Ctrl+C handler");
|
||||||
.await
|
|
||||||
.expect("failed to install Ctrl+C handler");
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
|
@@ -27,7 +27,6 @@ pub fn raise_window(win: &Window) -> anyhow::Result<()> {
|
|||||||
}
|
}
|
||||||
let title = win.title()?;
|
let title = win.title()?;
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
info!("Raising window: {}", title);
|
|
||||||
if let Err(err) = wmctrl_raise_window(&title).await {
|
if let Err(err) = wmctrl_raise_window(&title).await {
|
||||||
warn!("Failed to raise window: {}", err);
|
warn!("Failed to raise window: {}", err);
|
||||||
}
|
}
|
||||||
|
@@ -5,8 +5,5 @@ fn main() {
|
|||||||
println!("cargo:rerun-if-changed=src/ffi/vpn.h");
|
println!("cargo:rerun-if-changed=src/ffi/vpn.h");
|
||||||
|
|
||||||
// Compile the vpn.c file
|
// Compile the vpn.c file
|
||||||
cc::Build::new()
|
cc::Build::new().file("src/ffi/vpn.c").include("src/ffi").compile("vpn");
|
||||||
.file("src/ffi/vpn.c")
|
|
||||||
.include("src/ffi")
|
|
||||||
.compile("vpn");
|
|
||||||
}
|
}
|
||||||
|
@@ -15,15 +15,15 @@ pub(crate) struct ConnectOptions {
|
|||||||
pub os: *const c_char,
|
pub os: *const c_char,
|
||||||
pub certificate: *const c_char,
|
pub certificate: *const c_char,
|
||||||
pub servercert: *const c_char,
|
pub servercert: *const c_char,
|
||||||
|
|
||||||
|
pub csd_uid: u32,
|
||||||
|
pub csd_wrapper: *const c_char,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[link(name = "vpn")]
|
#[link(name = "vpn")]
|
||||||
extern "C" {
|
extern "C" {
|
||||||
#[link_name = "vpn_connect"]
|
#[link_name = "vpn_connect"]
|
||||||
fn vpn_connect(
|
fn vpn_connect(options: *const ConnectOptions, callback: extern "C" fn(i32, *mut c_void)) -> c_int;
|
||||||
options: *const ConnectOptions,
|
|
||||||
callback: extern "C" fn(i32, *mut c_void),
|
|
||||||
) -> c_int;
|
|
||||||
|
|
||||||
#[link_name = "vpn_disconnect"]
|
#[link_name = "vpn_disconnect"]
|
||||||
fn vpn_disconnect();
|
fn vpn_disconnect();
|
||||||
|
@@ -61,6 +61,8 @@ int vpn_connect(const vpn_options *options, vpn_connected_callback callback)
|
|||||||
INFO("User agent: %s", options->user_agent);
|
INFO("User agent: %s", options->user_agent);
|
||||||
INFO("VPNC script: %s", options->script);
|
INFO("VPNC script: %s", options->script);
|
||||||
INFO("OS: %s", options->os);
|
INFO("OS: %s", options->os);
|
||||||
|
INFO("CSD_USER: %d", options->csd_uid);
|
||||||
|
INFO("CSD_WRAPPER: %s", options->csd_wrapper);
|
||||||
|
|
||||||
vpninfo = openconnect_vpninfo_new(options->user_agent, validate_peer_cert, NULL, NULL, print_progress, NULL);
|
vpninfo = openconnect_vpninfo_new(options->user_agent, validate_peer_cert, NULL, NULL, print_progress, NULL);
|
||||||
|
|
||||||
@@ -91,6 +93,10 @@ int vpn_connect(const vpn_options *options, vpn_connected_callback callback)
|
|||||||
openconnect_set_system_trust(vpninfo, 0);
|
openconnect_set_system_trust(vpninfo, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options->csd_wrapper) {
|
||||||
|
openconnect_setup_csd(vpninfo, options->csd_uid, 1, options->csd_wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
g_cmd_pipe_fd = openconnect_setup_cmd_pipe(vpninfo);
|
g_cmd_pipe_fd = openconnect_setup_cmd_pipe(vpninfo);
|
||||||
if (g_cmd_pipe_fd < 0)
|
if (g_cmd_pipe_fd < 0)
|
||||||
{
|
{
|
||||||
@@ -137,6 +143,9 @@ int vpn_connect(const vpn_options *options, vpn_connected_callback callback)
|
|||||||
void vpn_disconnect()
|
void vpn_disconnect()
|
||||||
{
|
{
|
||||||
char cmd = OC_CMD_CANCEL;
|
char cmd = OC_CMD_CANCEL;
|
||||||
|
|
||||||
|
INFO("Stopping VPN connection: %d", g_cmd_pipe_fd);
|
||||||
|
|
||||||
if (write(g_cmd_pipe_fd, &cmd, 1) < 0)
|
if (write(g_cmd_pipe_fd, &cmd, 1) < 0)
|
||||||
{
|
{
|
||||||
ERROR("Failed to write to command pipe, VPN connection may not be stopped");
|
ERROR("Failed to write to command pipe, VPN connection may not be stopped");
|
||||||
|
@@ -16,6 +16,9 @@ typedef struct vpn_options
|
|||||||
const char *os;
|
const char *os;
|
||||||
const char *certificate;
|
const char *certificate;
|
||||||
const char *servercert;
|
const char *servercert;
|
||||||
|
|
||||||
|
const uid_t csd_uid;
|
||||||
|
const char *csd_wrapper;
|
||||||
} vpn_options;
|
} vpn_options;
|
||||||
|
|
||||||
int vpn_connect(const vpn_options *options, vpn_connected_callback callback);
|
int vpn_connect(const vpn_options *options, vpn_connected_callback callback);
|
||||||
|
@@ -18,6 +18,9 @@ pub struct Vpn {
|
|||||||
certificate: Option<CString>,
|
certificate: Option<CString>,
|
||||||
servercert: Option<CString>,
|
servercert: Option<CString>,
|
||||||
|
|
||||||
|
csd_uid: u32,
|
||||||
|
csd_wrapper: Option<CString>,
|
||||||
|
|
||||||
callback: OnConnectedCallback,
|
callback: OnConnectedCallback,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,11 +30,7 @@ impl Vpn {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn connect(&self, on_connected: impl FnOnce() + 'static + Send + Sync) -> i32 {
|
pub fn connect(&self, on_connected: impl FnOnce() + 'static + Send + Sync) -> i32 {
|
||||||
self
|
self.callback.write().unwrap().replace(Box::new(on_connected));
|
||||||
.callback
|
|
||||||
.write()
|
|
||||||
.unwrap()
|
|
||||||
.replace(Box::new(on_connected));
|
|
||||||
let options = self.build_connect_options();
|
let options = self.build_connect_options();
|
||||||
|
|
||||||
ffi::connect(&options)
|
ffi::connect(&options)
|
||||||
@@ -60,6 +59,9 @@ impl Vpn {
|
|||||||
os: self.os.as_ptr(),
|
os: self.os.as_ptr(),
|
||||||
certificate: Self::option_to_ptr(&self.certificate),
|
certificate: Self::option_to_ptr(&self.certificate),
|
||||||
servercert: Self::option_to_ptr(&self.servercert),
|
servercert: Self::option_to_ptr(&self.servercert),
|
||||||
|
|
||||||
|
csd_uid: self.csd_uid,
|
||||||
|
csd_wrapper: Self::option_to_ptr(&self.csd_wrapper),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,6 +79,9 @@ pub struct VpnBuilder {
|
|||||||
user_agent: Option<String>,
|
user_agent: Option<String>,
|
||||||
script: Option<String>,
|
script: Option<String>,
|
||||||
os: Option<String>,
|
os: Option<String>,
|
||||||
|
|
||||||
|
csd_uid: u32,
|
||||||
|
csd_wrapper: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VpnBuilder {
|
impl VpnBuilder {
|
||||||
@@ -87,6 +92,8 @@ impl VpnBuilder {
|
|||||||
user_agent: None,
|
user_agent: None,
|
||||||
script: None,
|
script: None,
|
||||||
os: None,
|
os: None,
|
||||||
|
csd_uid: 0,
|
||||||
|
csd_wrapper: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,12 +112,19 @@ impl VpnBuilder {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn csd_uid(mut self, csd_uid: u32) -> Self {
|
||||||
|
self.csd_uid = csd_uid;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn csd_wrapper<T: Into<Option<String>>>(mut self, csd_wrapper: T) -> Self {
|
||||||
|
self.csd_wrapper = csd_wrapper.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn build(self) -> Vpn {
|
pub fn build(self) -> Vpn {
|
||||||
let user_agent = self.user_agent.unwrap_or_default();
|
let user_agent = self.user_agent.unwrap_or_default();
|
||||||
let script = self
|
let script = self.script.or_else(find_default_vpnc_script).unwrap_or_default();
|
||||||
.script
|
|
||||||
.or_else(find_default_vpnc_script)
|
|
||||||
.unwrap_or_default();
|
|
||||||
let os = self.os.unwrap_or("linux".to_string());
|
let os = self.os.unwrap_or("linux".to_string());
|
||||||
|
|
||||||
Vpn {
|
Vpn {
|
||||||
@@ -121,6 +135,10 @@ impl VpnBuilder {
|
|||||||
os: Self::to_cstring(&os),
|
os: Self::to_cstring(&os),
|
||||||
certificate: None,
|
certificate: None,
|
||||||
servercert: None,
|
servercert: None,
|
||||||
|
|
||||||
|
csd_uid: self.csd_uid,
|
||||||
|
csd_wrapper: self.csd_wrapper.as_deref().map(Self::to_cstring),
|
||||||
|
|
||||||
callback: Default::default(),
|
callback: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
max_width = 100
|
max_width = 120
|
||||||
hard_tabs = false
|
hard_tabs = false
|
||||||
tab_spaces = 2
|
tab_spaces = 2
|
||||||
newline_style = "Unix"
|
newline_style = "Unix"
|
||||||
|