Compare commits
10 Commits
v2.0.0-bet
...
8860efa82e
Author | SHA1 | Date | |
---|---|---|---|
|
8860efa82e | ||
|
9bc0994a8e | ||
|
1f50e4d82b | ||
|
995d1216ea | ||
|
196e91289c | ||
|
b2bb35994f | ||
|
6fe6a1387a | ||
|
aac401e7ee | ||
|
9655b735a1 | ||
|
c3bd7aeb93 |
232
.github/workflows/build.yaml
vendored
@@ -8,8 +8,9 @@ on:
|
|||||||
- .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,13 +97,212 @@ 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:
|
||||||
|
needs: [setup-matrix, package-tarball]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
arch: ${{ fromJson(needs.setup-matrix.outputs.matrix) }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout gpgui repo
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GH_PAT }}
|
||||||
|
repository: yuezk/gpgui
|
||||||
|
path: gpgui
|
||||||
|
|
||||||
|
- name: Download package tarball
|
||||||
|
uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
name: artifact-tarball
|
||||||
|
path: gpgui/.tmp/artifact
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
with:
|
||||||
|
platforms: ${{ matrix.arch }}
|
||||||
|
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_HUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Create RPM package
|
||||||
|
run: |
|
||||||
|
docker run \
|
||||||
|
--rm \
|
||||||
|
-v $(pwd):/${{ github.workspace }} \
|
||||||
|
-w ${{ github.workspace }} \
|
||||||
|
--platform linux/${{ matrix.arch }} \
|
||||||
|
yuezk/gpdev:rpm-builder \
|
||||||
|
"./gpgui/scripts/build-rpm.sh"
|
||||||
|
|
||||||
|
- name: Upload rpm artifacts
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: artifact-${{ matrix.arch }}-rpm
|
||||||
|
path: |
|
||||||
|
gpgui/.tmp/artifact/*.rpm
|
||||||
|
|
||||||
|
package-pkgbuild:
|
||||||
|
needs: [setup-matrix, build-tauri-amd64, build-tauri-arm64]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
arch: ${{ fromJson(needs.setup-matrix.outputs.matrix) }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout gpgui repo
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GH_PAT }}
|
||||||
|
repository: yuezk/gpgui
|
||||||
|
path: gpgui
|
||||||
|
|
||||||
|
- name: Download artifact-${{ matrix.arch }}
|
||||||
|
uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
name: artifact-${{ matrix.arch }}-tauri
|
||||||
|
path: gpgui/.tmp/artifact
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
with:
|
||||||
|
platforms: ${{ matrix.arch }}
|
||||||
|
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_HUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Generate PKGBUILD
|
||||||
|
run: |
|
||||||
|
export CI_ARCH=${{ matrix.arch }}
|
||||||
|
./gpgui/scripts/generate-pkgbuild.sh
|
||||||
|
|
||||||
|
- name: Build PKGBUILD package
|
||||||
|
run: |
|
||||||
|
# Build package
|
||||||
|
docker run \
|
||||||
|
--rm \
|
||||||
|
-v $(pwd)/gpgui/.tmp/pkgbuild:/pkgbuild \
|
||||||
|
--platform linux/${{ matrix.arch }} \
|
||||||
|
yuezk/gpdev:pkgbuild
|
||||||
|
|
||||||
|
- name: Upload pkgbuild artifacts
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: artifact-${{ matrix.arch }}-pkgbuild
|
||||||
|
path: |
|
||||||
|
gpgui/.tmp/pkgbuild/*.pkg.tar.zst
|
||||||
|
|
||||||
|
gh-release:
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- package-rpm
|
||||||
|
- package-pkgbuild
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Download artifact
|
||||||
|
uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
path: artifact
|
||||||
|
# pattern: artifact-*
|
||||||
|
# merge-multiple: true
|
||||||
|
|
||||||
|
# - name: Generate checksum
|
||||||
|
# uses: jmgilman/actions-generate-checksum@v1
|
||||||
|
# with:
|
||||||
|
# output: checksums.txt
|
||||||
|
# patterns: |
|
||||||
|
# artifact/*
|
||||||
|
|
||||||
|
- name: Create GH release
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GH_PAT }}
|
||||||
|
prerelease: ${{ contains(github.ref, 'latest') }}
|
||||||
|
fail_on_unmatched_files: true
|
||||||
|
files: |
|
||||||
|
artifact/artifact-*/*
|
||||||
|
5
.vscode/settings.json
vendored
@@ -11,8 +11,10 @@
|
|||||||
"dotenvy",
|
"dotenvy",
|
||||||
"getconfig",
|
"getconfig",
|
||||||
"globalprotect",
|
"globalprotect",
|
||||||
|
"globalprotectcallback",
|
||||||
"gpapi",
|
"gpapi",
|
||||||
"gpauth",
|
"gpauth",
|
||||||
|
"gpcallback",
|
||||||
"gpclient",
|
"gpclient",
|
||||||
"gpcommon",
|
"gpcommon",
|
||||||
"gpgui",
|
"gpgui",
|
||||||
@@ -50,5 +52,6 @@
|
|||||||
"wmctrl",
|
"wmctrl",
|
||||||
"XAUTHORITY",
|
"XAUTHORITY",
|
||||||
"yuezk"
|
"yuezk"
|
||||||
]
|
],
|
||||||
|
"rust-analyzer.cargo.features": "all",
|
||||||
}
|
}
|
||||||
|
47
Cargo.lock
generated
@@ -1423,7 +1423,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gpapi"
|
name = "gpapi"
|
||||||
version = "2.0.0-beta4"
|
version = "2.0.0-beta8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64 0.21.5",
|
"base64 0.21.5",
|
||||||
@@ -1431,6 +1431,7 @@ dependencies = [
|
|||||||
"clap",
|
"clap",
|
||||||
"dotenvy_macro",
|
"dotenvy_macro",
|
||||||
"log",
|
"log",
|
||||||
|
"open",
|
||||||
"redact-engine",
|
"redact-engine",
|
||||||
"regex",
|
"regex",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
@@ -1451,7 +1452,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gpauth"
|
name = "gpauth"
|
||||||
version = "2.0.0-beta4"
|
version = "2.0.0-beta8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
@@ -1471,7 +1472,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gpclient"
|
name = "gpclient"
|
||||||
version = "2.0.0-beta4"
|
version = "2.0.0-beta8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
@@ -1492,7 +1493,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gpservice"
|
name = "gpservice"
|
||||||
version = "2.0.0-beta4"
|
version = "2.0.0-beta8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"axum",
|
"axum",
|
||||||
@@ -1963,6 +1964,15 @@ version = "2.9.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
|
checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "is-docker"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3"
|
||||||
|
dependencies = [
|
||||||
|
"once_cell",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "is-terminal"
|
name = "is-terminal"
|
||||||
version = "0.4.10"
|
version = "0.4.10"
|
||||||
@@ -1974,6 +1984,16 @@ dependencies = [
|
|||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "is-wsl"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5"
|
||||||
|
dependencies = [
|
||||||
|
"is-docker",
|
||||||
|
"once_cell",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "is_executable"
|
name = "is_executable"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
@@ -2445,9 +2465,20 @@ version = "0.3.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
|
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "open"
|
||||||
|
version = "5.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "90878fb664448b54c4e592455ad02831e23a3f7e157374a8b95654731aac7349"
|
||||||
|
dependencies = [
|
||||||
|
"is-wsl",
|
||||||
|
"libc",
|
||||||
|
"pathdiff",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openconnect"
|
name = "openconnect"
|
||||||
version = "2.0.0-beta4"
|
version = "2.0.0-beta8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"is_executable",
|
"is_executable",
|
||||||
@@ -2574,6 +2605,12 @@ version = "1.0.14"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
|
checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pathdiff"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "percent-encoding"
|
name = "percent-encoding"
|
||||||
version = "2.3.1"
|
version = "2.3.1"
|
||||||
|
@@ -4,7 +4,7 @@ resolver = "2"
|
|||||||
members = ["crates/*", "apps/gpclient", "apps/gpservice", "apps/gpauth"]
|
members = ["crates/*", "apps/gpclient", "apps/gpservice", "apps/gpauth"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "2.0.0-beta4"
|
version = "2.0.0-beta8"
|
||||||
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"
|
||||||
|
10
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
|
||||||
|
|
||||||
@@ -122,6 +125,13 @@ Download the latest RPM package from [releases](https://github.com/yuezk/GlobalP
|
|||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
|
## About Trial
|
||||||
|
|
||||||
|
The CLI version is always free, while the GUI version is paid. There are two trial modes for the GUI version:
|
||||||
|
|
||||||
|
1. 10-day trial: You can use the GUI stable release for 10 days after the installation.
|
||||||
|
2. 14-day trial: Each beta release has a fresh trial period (at most 14 days) after released.
|
||||||
|
|
||||||
## [License](./LICENSE)
|
## [License](./LICENSE)
|
||||||
|
|
||||||
GPLv3
|
GPLv3
|
||||||
|
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 |
@@ -19,8 +19,8 @@ use tokio_util::sync::CancellationToken;
|
|||||||
use webkit2gtk::{
|
use webkit2gtk::{
|
||||||
gio::Cancellable,
|
gio::Cancellable,
|
||||||
glib::{GString, TimeSpan},
|
glib::{GString, TimeSpan},
|
||||||
LoadEvent, SettingsExt, TLSErrorsPolicy, URIResponse, URIResponseExt, WebContextExt, WebResource,
|
LoadEvent, SettingsExt, TLSErrorsPolicy, URIResponse, URIResponseExt, WebContextExt, WebResource, WebResourceExt,
|
||||||
WebResourceExt, WebView, WebViewExt, WebsiteDataManagerExtManual, WebsiteDataTypes,
|
WebView, WebViewExt, WebsiteDataManagerExtManual, WebsiteDataTypes,
|
||||||
};
|
};
|
||||||
|
|
||||||
enum AuthDataError {
|
enum AuthDataError {
|
||||||
@@ -216,9 +216,7 @@ impl<'a> AuthWindow<'a> {
|
|||||||
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) => {
|
Err(AuthDataError::TlsError) => bail!("TLS error: certificate verify failed"),
|
||||||
return Err(anyhow::anyhow!("TLS error: certificate verify failed"))
|
|
||||||
}
|
|
||||||
Err(AuthDataError::NotFound) => {
|
Err(AuthDataError::NotFound) => {
|
||||||
info!("No auth data found, it may not be the /SAML20/SP/ACS endpoint");
|
info!("No auth data found, it may not be the /SAML20/SP/ACS endpoint");
|
||||||
|
|
||||||
@@ -227,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;
|
||||||
@@ -284,12 +279,10 @@ fn raise_window(window: &Arc<Window>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn portal_prelogin(portal: &str, gp_params: &GpParams) -> 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, gp_params).await? {
|
||||||
Prelogin::Saml(prelogin) => Ok(prelogin.saml_request().to_string()),
|
Prelogin::Saml(prelogin) => Ok(prelogin.saml_request().to_string()),
|
||||||
Prelogin::Standard(_) => Err(anyhow::anyhow!("Received non-SAML prelogin response")),
|
Prelogin::Standard(_) => bail!("Received non-SAML prelogin response"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -13,18 +13,15 @@ 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,
|
||||||
@@ -102,6 +99,7 @@ impl Cli {
|
|||||||
.client_os(ClientOs::from(&self.os))
|
.client_os(ClientOs::from(&self.os))
|
||||||
.os_version(self.os_version.clone())
|
.os_version(self.os_version.clone())
|
||||||
.ignore_tls_errors(self.ignore_tls_errors)
|
.ignore_tls_errors(self.ignore_tls_errors)
|
||||||
|
.is_gateway(self.gateway)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
gp_params
|
gp_params
|
||||||
|
@@ -9,12 +9,7 @@ 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"),
|
|
||||||
" (",
|
|
||||||
compile_time::date_str!(),
|
|
||||||
")"
|
|
||||||
);
|
|
||||||
|
|
||||||
pub(crate) struct SharedArgs {
|
pub(crate) struct SharedArgs {
|
||||||
pub(crate) fix_openssl: bool,
|
pub(crate) fix_openssl: bool,
|
||||||
@@ -53,10 +48,7 @@ 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")]
|
#[arg(long, help = "Ignore the TLS errors")]
|
||||||
ignore_tls_errors: bool,
|
ignore_tls_errors: bool,
|
||||||
@@ -115,10 +107,8 @@ pub(crate) async fn run() {
|
|||||||
eprintln!("{} --fix-openssl {}\n", args[0], args[1..].join(" "));
|
eprintln!("{} --fix-openssl {}\n", args[0], args[1..].join(" "));
|
||||||
}
|
}
|
||||||
|
|
||||||
if err.contains("certificate verify failed") {
|
if err.contains("certificate verify failed") && !cli.ignore_tls_errors {
|
||||||
eprintln!(
|
eprintln!("\nRe-run it with the `--ignore-tls-errors` option to ignore the certificate error, e.g.:\n");
|
||||||
"\nRe-run it with the `--ignore-tls-errors` option to ignore the certificate error, e.g.:\n"
|
|
||||||
);
|
|
||||||
// Print the command
|
// Print the command
|
||||||
let args = std::env::args().collect::<Vec<_>>();
|
let args = std::env::args().collect::<Vec<_>>();
|
||||||
eprintln!("{} --ignore-tls-errors {}\n", args[0], args[1..].join(" "));
|
eprintln!("{} --ignore-tls-errors {}\n", args[0], args[1..].join(" "));
|
||||||
|
@@ -6,9 +6,9 @@ use gpapi::{
|
|||||||
credential::{Credential, PasswordCredential},
|
credential::{Credential, PasswordCredential},
|
||||||
gateway::gateway_login,
|
gateway::gateway_login,
|
||||||
gp_params::{ClientOs, GpParams},
|
gp_params::{ClientOs, GpParams},
|
||||||
portal::{prelogin, retrieve_config, Prelogin},
|
portal::{prelogin, retrieve_config, PortalError, Prelogin},
|
||||||
process::auth_launcher::SamlAuthLauncher,
|
process::auth_launcher::SamlAuthLauncher,
|
||||||
utils::{self, shutdown_signal},
|
utils::shutdown_signal,
|
||||||
GP_USER_AGENT,
|
GP_USER_AGENT,
|
||||||
};
|
};
|
||||||
use inquire::{Password, PasswordDisplayMode, Select, Text};
|
use inquire::{Password, PasswordDisplayMode, Select, Text};
|
||||||
@@ -21,17 +21,9 @@ use crate::{cli::SharedArgs, GP_CLIENT_LOCK_FILE};
|
|||||||
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>,
|
||||||
@@ -71,19 +63,38 @@ impl<'a> ConnectHandler<'a> {
|
|||||||
Self { args, shared_args }
|
Self { args, shared_args }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn handle(&self) -> anyhow::Result<()> {
|
fn build_gp_params(&self) -> GpParams {
|
||||||
let portal = utils::normalize_server(self.args.server.as_str())?;
|
GpParams::builder()
|
||||||
|
|
||||||
let gp_params = GpParams::builder()
|
|
||||||
.user_agent(&self.args.user_agent)
|
.user_agent(&self.args.user_agent)
|
||||||
.client_os(ClientOs::from(&self.args.os))
|
.client_os(ClientOs::from(&self.args.os))
|
||||||
.os_version(self.args.os_version())
|
.os_version(self.args.os_version())
|
||||||
.ignore_tls_errors(self.shared_args.ignore_tls_errors)
|
.ignore_tls_errors(self.shared_args.ignore_tls_errors)
|
||||||
.build();
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
let prelogin = prelogin(&portal, &gp_params).await?;
|
pub(crate) async fn handle(&self) -> anyhow::Result<()> {
|
||||||
let portal_credential = self.obtain_portal_credential(&prelogin).await?;
|
let server = self.args.server.as_str();
|
||||||
let mut portal_config = retrieve_config(&portal, &portal_credential, &gp_params).await?;
|
|
||||||
|
let Err(err) = self.connect_portal_with_prelogin(server).await else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
info!("Failed to connect portal with prelogin: {}", err);
|
||||||
|
if err.root_cause().downcast_ref::<PortalError>().is_some() {
|
||||||
|
info!("Trying the gateway authentication workflow...");
|
||||||
|
return self.connect_gateway_with_prelogin(server).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn connect_portal_with_prelogin(&self, portal: &str) -> anyhow::Result<()> {
|
||||||
|
let gp_params = self.build_gp_params();
|
||||||
|
|
||||||
|
let prelogin = prelogin(portal, &gp_params).await?;
|
||||||
|
|
||||||
|
let cred = self.obtain_credential(&prelogin, portal).await?;
|
||||||
|
let mut portal_config = retrieve_config(portal, &cred, &gp_params).await?;
|
||||||
|
|
||||||
let selected_gateway = match &self.args.gateway {
|
let selected_gateway = match &self.args.gateway {
|
||||||
Some(gateway) => portal_config
|
Some(gateway) => portal_config
|
||||||
@@ -105,9 +116,32 @@ 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 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())
|
||||||
.build();
|
.build();
|
||||||
@@ -132,10 +166,13 @@ 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)
|
||||||
|
.gateway(is_gateway)
|
||||||
.saml_request(prelogin.saml_request())
|
.saml_request(prelogin.saml_request())
|
||||||
.user_agent(&self.args.user_agent)
|
.user_agent(&self.args.user_agent)
|
||||||
.os(self.args.os.as_str())
|
.os(self.args.os.as_str())
|
||||||
@@ -148,7 +185,8 @@ impl<'a> ConnectHandler<'a> {
|
|||||||
.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(),
|
||||||
|
@@ -10,7 +10,12 @@ use log::info;
|
|||||||
|
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
pub(crate) struct LaunchGuiArgs {
|
pub(crate) struct LaunchGuiArgs {
|
||||||
#[clap(long, help = "Launch the GUI minimized")]
|
#[arg(
|
||||||
|
required = false,
|
||||||
|
help = "The authentication data, used for the default browser authentication"
|
||||||
|
)]
|
||||||
|
auth_data: Option<String>,
|
||||||
|
#[arg(long, help = "Launch the GUI minimized")]
|
||||||
minimized: bool,
|
minimized: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,6 +35,12 @@ impl<'a> LaunchGuiHandler<'a> {
|
|||||||
anyhow::bail!("`launch-gui` cannot be run as root");
|
anyhow::bail!("`launch-gui` cannot be run as root");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let auth_data = self.args.auth_data.as_deref().unwrap_or_default();
|
||||||
|
if !auth_data.is_empty() {
|
||||||
|
// Process the authentication data, its format is `globalprotectcallback:<data>`
|
||||||
|
return feed_auth_data(auth_data).await;
|
||||||
|
}
|
||||||
|
|
||||||
if try_active_gui().await.is_ok() {
|
if try_active_gui().await.is_ok() {
|
||||||
info!("The GUI is already running");
|
info!("The GUI is already running");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
@@ -66,6 +77,19 @@ impl<'a> LaunchGuiHandler<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn feed_auth_data(auth_data: &str) -> anyhow::Result<()> {
|
||||||
|
let service_endpoint = http_endpoint().await?;
|
||||||
|
|
||||||
|
reqwest::Client::default()
|
||||||
|
.post(format!("{}/auth-data", service_endpoint))
|
||||||
|
.json(&auth_data)
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.error_for_status()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
async fn try_active_gui() -> anyhow::Result<()> {
|
async fn try_active_gui() -> anyhow::Result<()> {
|
||||||
let service_endpoint = http_endpoint().await?;
|
let service_endpoint = http_endpoint().await?;
|
||||||
|
|
||||||
|
@@ -1,19 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE policyconfig PUBLIC "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN" "http://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd">
|
|
||||||
<policyconfig>
|
|
||||||
<vendor>GlobalProtect-openconnect</vendor>
|
|
||||||
<vendor_url>https://github.com/yuezk/GlobalProtect-openconnect</vendor_url>
|
|
||||||
<icon_name>gpgui</icon_name>
|
|
||||||
<action id="com.yuezk.gpservice">
|
|
||||||
<description>Run GPService as root</description>
|
|
||||||
<message>Authentication is required to run the GPService as root</message>
|
|
||||||
<defaults>
|
|
||||||
<allow_any>yes</allow_any>
|
|
||||||
<allow_inactive>yes</allow_inactive>
|
|
||||||
<allow_active>yes</allow_active>
|
|
||||||
</defaults>
|
|
||||||
<annotate key="org.freedesktop.policykit.exec.path">/home/kevin/Documents/repos/gp/target/debug/gpservice</annotate>
|
|
||||||
<annotate key="org.freedesktop.policykit.exec.argv1">--with-gui</annotate>
|
|
||||||
<annotate key="org.freedesktop.policykit.exec.allow_gui">true</annotate>
|
|
||||||
</action>
|
|
||||||
</policyconfig>
|
|
@@ -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)
|
||||||
}
|
}
|
||||||
|
@@ -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 {
|
||||||
|
@@ -28,7 +28,9 @@ uzers.workspace = true
|
|||||||
|
|
||||||
tauri = { workspace = true, optional = true }
|
tauri = { workspace = true, optional = true }
|
||||||
clap = { 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"]
|
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())
|
||||||
|
}
|
||||||
|
@@ -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,8 +189,7 @@ impl Credential {
|
|||||||
let mut params = HashMap::new();
|
let mut params = HashMap::new();
|
||||||
params.insert("user", self.username());
|
params.insert("user", self.username());
|
||||||
|
|
||||||
let (passwd, prelogin_cookie, portal_userauthcookie, portal_prelogonuserauthcookie) = match self
|
let (passwd, prelogin_cookie, portal_userauthcookie, portal_prelogonuserauthcookie) = match self {
|
||||||
{
|
|
||||||
Credential::Password(cred) => (Some(cred.password()), None, None, None),
|
Credential::Password(cred) => (Some(cred.password()), None, None, None),
|
||||||
Credential::PreloginCookie(cred) => (None, Some(cred.prelogin_cookie()), None, None),
|
Credential::PreloginCookie(cred) => (None, Some(cred.prelogin_cookie()), None, None),
|
||||||
Credential::AuthCookie(cred) => (
|
Credential::AuthCookie(cred) => (
|
||||||
@@ -184,10 +208,7 @@ impl Credential {
|
|||||||
|
|
||||||
params.insert("passwd", passwd.unwrap_or_default());
|
params.insert("passwd", passwd.unwrap_or_default());
|
||||||
params.insert("prelogin-cookie", prelogin_cookie.unwrap_or_default());
|
params.insert("prelogin-cookie", prelogin_cookie.unwrap_or_default());
|
||||||
params.insert(
|
params.insert("portal-userauthcookie", portal_userauthcookie.unwrap_or_default());
|
||||||
"portal-userauthcookie",
|
|
||||||
portal_userauthcookie.unwrap_or_default(),
|
|
||||||
);
|
|
||||||
params.insert(
|
params.insert(
|
||||||
"portal-prelogonuserauthcookie",
|
"portal-prelogonuserauthcookie",
|
||||||
portal_prelogonuserauthcookie.unwrap_or_default(),
|
portal_prelogonuserauthcookie.unwrap_or_default(),
|
||||||
|
@@ -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,13 +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 = client.post(&login_url).form(¶ms).send().await?;
|
let res = client.post(&login_url).form(¶ms).send().await?;
|
||||||
let res_xml = res.error_for_status()?.text().await?;
|
let status = res.status();
|
||||||
|
|
||||||
|
if status.is_client_error() || status.is_server_error() {
|
||||||
|
bail!("Gateway login error: {}", status)
|
||||||
|
}
|
||||||
|
|
||||||
|
let res_xml = res.text().await?;
|
||||||
let doc = Document::parse(&res_xml)?;
|
let doc = Document::parse(&res_xml)?;
|
||||||
|
|
||||||
build_gateway_token(&doc, gp_params.computer())
|
build_gateway_token(&doc, gp_params.computer())
|
||||||
@@ -56,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"))
|
||||||
|
@@ -31,6 +31,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()
|
||||||
|
@@ -44,12 +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: String,
|
computer: String,
|
||||||
ignore_tls_errors: bool,
|
ignore_tls_errors: bool,
|
||||||
|
prefer_default_browser: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GpParams {
|
impl GpParams {
|
||||||
@@ -57,6 +59,14 @@ 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
|
||||||
}
|
}
|
||||||
@@ -69,6 +79,10 @@ impl GpParams {
|
|||||||
self.ignore_tls_errors
|
self.ignore_tls_errors
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn prefer_default_browser(&self) -> bool {
|
||||||
|
self.prefer_default_browser
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn to_params(&self) -> HashMap<&str, &str> {
|
pub(crate) fn to_params(&self) -> HashMap<&str, &str> {
|
||||||
let mut params: HashMap<&str, &str> = HashMap::new();
|
let mut params: HashMap<&str, &str> = HashMap::new();
|
||||||
let client_os = self.client_os.as_str();
|
let client_os = self.client_os.as_str();
|
||||||
@@ -88,35 +102,45 @@ impl GpParams {
|
|||||||
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: String,
|
computer: String,
|
||||||
ignore_tls_errors: bool,
|
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: whoami::hostname(),
|
computer: whoami::hostname(),
|
||||||
ignore_tls_errors: false,
|
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
|
||||||
@@ -147,14 +171,21 @@ impl GpParamsBuilder {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn prefer_default_browser(&mut self, prefer_default_browser: bool) -> &mut Self {
|
||||||
|
self.prefer_default_browser = prefer_default_browser;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn build(&self) -> GpParams {
|
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,
|
ignore_tls_errors: self.ignore_tls_errors,
|
||||||
|
prefer_default_browser: self.prefer_default_browser,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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()?;
|
||||||
|
|
||||||
@@ -133,42 +103,42 @@ 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 = client.post(&url).form(¶ms).send().await?;
|
let res = client.post(&url).form(¶ms).send().await?;
|
||||||
let res_xml = res.error_for_status()?.text().await?;
|
let status = res.status();
|
||||||
|
|
||||||
ensure!(!res_xml.is_empty(), PortalConfigError::EmptyResponse);
|
if status == StatusCode::NOT_FOUND {
|
||||||
|
bail!(PortalError::ConfigError("Config endpoint not found".to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
let doc = Document::parse(&res_xml)?;
|
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,12 +1,13 @@
|
|||||||
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::{
|
use crate::{
|
||||||
gp_params::GpParams,
|
gp_params::GpParams,
|
||||||
|
portal::PortalError,
|
||||||
utils::{base64, normalize_server, xml},
|
utils::{base64, normalize_server, xml},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -25,7 +26,9 @@ const REQUIRED_PARAMS: [&str; 8] = [
|
|||||||
#[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 {
|
||||||
@@ -36,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,
|
||||||
@@ -79,27 +87,31 @@ 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, gp_params: &GpParams) -> anyhow::Result<Prelogin> {
|
pub async fn prelogin(portal: &str, gp_params: &GpParams) -> anyhow::Result<Prelogin> {
|
||||||
let user_agent = gp_params.user_agent();
|
let user_agent = gp_params.user_agent();
|
||||||
info!("Portal prelogin, user_agent: {}", user_agent);
|
info!("Prelogin with user_agent: {}", user_agent);
|
||||||
|
|
||||||
let portal = normalize_server(portal)?;
|
let portal = normalize_server(portal)?;
|
||||||
let prelogin_url = format!(
|
let is_gateway = gp_params.is_gateway();
|
||||||
"{}/global-protect/prelogin.esp?kerberos-support=yes",
|
let path = if is_gateway { "ssl-vpn" } else { "global-protect" };
|
||||||
portal
|
let prelogin_url = format!("{portal}/{}/prelogin.esp", path);
|
||||||
);
|
|
||||||
let mut params = gp_params.to_params();
|
let mut params = gp_params.to_params();
|
||||||
params.insert("tmp", "tmp");
|
|
||||||
params.insert("default-browser", "0");
|
|
||||||
params.insert("cas-support", "yes");
|
|
||||||
|
|
||||||
params.retain(|k, _| {
|
params.insert("tmp", "tmp");
|
||||||
REQUIRED_PARAMS
|
if gp_params.prefer_default_browser() {
|
||||||
.iter()
|
params.insert("default-browser", "1");
|
||||||
.any(|required_param| required_param == k)
|
}
|
||||||
});
|
|
||||||
|
params.retain(|k, _| REQUIRED_PARAMS.iter().any(|required_param| required_param == k));
|
||||||
|
|
||||||
let client = Client::builder()
|
let client = Client::builder()
|
||||||
.danger_accept_invalid_certs(gp_params.ignore_tls_errors())
|
.danger_accept_invalid_certs(gp_params.ignore_tls_errors())
|
||||||
@@ -107,9 +119,27 @@ pub async fn prelogin(portal: &str, gp_params: &GpParams) -> anyhow::Result<Prel
|
|||||||
.build()?;
|
.build()?;
|
||||||
|
|
||||||
let res = client.post(&prelogin_url).form(¶ms).send().await?;
|
let res = client.post(&prelogin_url).form(¶ms).send().await?;
|
||||||
let res_xml = res.error_for_status()?.text().await?;
|
let status = res.status();
|
||||||
|
|
||||||
trace!("Prelogin response: {}", res_xml);
|
if status == StatusCode::NOT_FOUND {
|
||||||
|
bail!(PortalError::PreloginError("Prelogin endpoint not found".to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
if status.is_client_error() || status.is_server_error() {
|
||||||
|
bail!("Prelogin error: {}", status)
|
||||||
|
}
|
||||||
|
|
||||||
|
let res_xml = res
|
||||||
|
.text()
|
||||||
|
.await
|
||||||
|
.map_err(|e| PortalError::PreloginError(e.to_string()))?;
|
||||||
|
|
||||||
|
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")
|
||||||
@@ -125,12 +155,17 @@ pub async fn prelogin(portal: &str, gp_params: &GpParams) -> anyhow::Result<Prel
|
|||||||
|
|
||||||
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));
|
||||||
@@ -140,10 +175,11 @@ pub async fn prelogin(portal: &str, gp_params: &GpParams) -> anyhow::Result<Prel
|
|||||||
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,6 +9,7 @@ use super::command_traits::CommandExt;
|
|||||||
|
|
||||||
pub struct SamlAuthLauncher<'a> {
|
pub struct SamlAuthLauncher<'a> {
|
||||||
server: &'a str,
|
server: &'a str,
|
||||||
|
gateway: bool,
|
||||||
saml_request: Option<&'a str>,
|
saml_request: Option<&'a str>,
|
||||||
user_agent: Option<&'a str>,
|
user_agent: Option<&'a str>,
|
||||||
os: Option<&'a str>,
|
os: Option<&'a str>,
|
||||||
@@ -22,6 +24,7 @@ impl<'a> SamlAuthLauncher<'a> {
|
|||||||
pub fn new(server: &'a str) -> Self {
|
pub fn new(server: &'a str) -> Self {
|
||||||
Self {
|
Self {
|
||||||
server,
|
server,
|
||||||
|
gateway: false,
|
||||||
saml_request: None,
|
saml_request: None,
|
||||||
user_agent: None,
|
user_agent: None,
|
||||||
os: None,
|
os: None,
|
||||||
@@ -33,6 +36,11 @@ impl<'a> SamlAuthLauncher<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn gateway(mut self, gateway: bool) -> Self {
|
||||||
|
self.gateway = gateway;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn saml_request(mut self, saml_request: &'a str) -> Self {
|
pub fn saml_request(mut self, saml_request: &'a str) -> Self {
|
||||||
self.saml_request = Some(saml_request);
|
self.saml_request = Some(saml_request);
|
||||||
self
|
self
|
||||||
@@ -78,6 +86,10 @@ 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 self.gateway {
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
@@ -118,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);
|
||||||
|
}
|
||||||
|
}
|
@@ -21,8 +21,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())
|
||||||
@@ -42,8 +41,7 @@ fn get_non_root_user() -> anyhow::Result<User> {
|
|||||||
let user = if current_user == "root" {
|
let user = if current_user == "root" {
|
||||||
get_real_user()?
|
get_real_user()?
|
||||||
} else {
|
} else {
|
||||||
uzers::get_user_by_name(¤t_user)
|
uzers::get_user_by_name(¤t_user).ok_or_else(|| anyhow::anyhow!("User ({}) not found", current_user))?
|
||||||
.ok_or_else(|| anyhow::anyhow!("User ({}) not found", current_user))?
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if user.uid() == 0 {
|
if user.uid() == 0 {
|
||||||
|
@@ -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
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
pub(crate) mod command_traits;
|
pub(crate) mod command_traits;
|
||||||
|
|
||||||
pub mod auth_launcher;
|
pub mod auth_launcher;
|
||||||
|
#[cfg(feature = "browser-auth")]
|
||||||
|
pub mod browser_authenticator;
|
||||||
pub mod gui_launcher;
|
pub mod gui_launcher;
|
||||||
pub mod service_launcher;
|
pub mod service_launcher;
|
||||||
|
@@ -7,4 +7,6 @@ use super::vpn_state::VpnState;
|
|||||||
pub enum WsEvent {
|
pub enum WsEvent {
|
||||||
VpnState(VpnState),
|
VpnState(VpnState),
|
||||||
ActiveGui,
|
ActiveGui,
|
||||||
|
/// External authentication data
|
||||||
|
AuthData(String),
|
||||||
}
|
}
|
||||||
|
@@ -58,10 +58,7 @@ 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())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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)]
|
||||||
|
@@ -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");
|
|
||||||
}
|
}
|
||||||
|
@@ -20,10 +20,7 @@ pub(crate) struct ConnectOptions {
|
|||||||
#[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();
|
||||||
|
@@ -27,11 +27,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)
|
||||||
@@ -107,10 +103,7 @@ impl VpnBuilder {
|
|||||||
|
|
||||||
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 {
|
||||||
|
@@ -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"
|
||||||
|