mirror of
https://github.com/yuezk/GlobalProtect-openconnect.git
synced 2025-05-20 07:26:58 -04:00
Compare commits
64 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
148468eee3 | ||
|
79083e5664 | ||
|
c52d2bc0b6 | ||
|
54d4f2ec57 | ||
|
a25b5cb894 | ||
|
6caa8fcd84 | ||
|
66270eee77 | ||
|
6119976027 | ||
|
a286b5e418 | ||
|
882ab4001d | ||
|
52b6fa6fbd | ||
|
3bb115bd2d | ||
|
e08f239176 | ||
|
a01c55e38d | ||
|
af51bc257b | ||
|
90a8c11acb | ||
|
92b858884c | ||
|
159673652c | ||
|
200d13ef15 | ||
|
ddeef46d2e | ||
|
97c3998383 | ||
|
93aea4ee60 | ||
|
546dbf542e | ||
|
005410d40b | ||
|
3b384a199a | ||
|
b62b024a8b | ||
|
4fbd373e29 | ||
|
ae211a923a | ||
|
d94d730a44 | ||
|
18ae1c5fa5 | ||
|
a0afabeb04 | ||
|
1158ab9095 | ||
|
54ccb761e5 | ||
|
f72dbd1dec | ||
|
0814c3153a | ||
|
9f085e8b8c | ||
|
0188752c0a | ||
|
a884c41813 | ||
|
879b977321 | ||
|
e9cb253be1 | ||
|
07eacae385 | ||
|
8446874290 | ||
|
c347f97b95 | ||
|
29cfa9e24b | ||
|
1b1ce882a5 | ||
|
e9f2dbf9ea | ||
|
7c6ae315e1 | ||
|
cec0d22dc8 | ||
|
b2ca82e105 | ||
|
5ba6b1d5fc | ||
|
a96e77c758 | ||
|
79e0f0c7c1 | ||
|
187ca778f2 | ||
|
2d1aa3ba8c | ||
|
08bd4efefa | ||
|
558485f5a9 | ||
|
cff2ff9dbe | ||
|
d5d92cfbee | ||
|
a00f6a8cba | ||
|
59dee3d767 | ||
|
e94661b213 | ||
|
9dea81bdff | ||
|
6ff552c1ec | ||
|
c1b1ea1a67 |
@@ -8,5 +8,5 @@ end_of_line = lf
|
|||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
[Makefile]
|
[{Makefile,Makefile.in}]
|
||||||
indent_style = tab
|
indent_style = tab
|
||||||
|
194
.github/workflows/build.yaml
vendored
194
.github/workflows/build.yaml
vendored
@@ -9,8 +9,10 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
- dev
|
- dev
|
||||||
|
- hotfix/*
|
||||||
|
- feature/*
|
||||||
|
- release/*
|
||||||
tags:
|
tags:
|
||||||
- latest
|
|
||||||
- v*.*.*
|
- v*.*.*
|
||||||
jobs:
|
jobs:
|
||||||
# Include arm64 if ref is a tag
|
# Include arm64 if ref is a tag
|
||||||
@@ -23,9 +25,9 @@ jobs:
|
|||||||
id: set-matrix
|
id: set-matrix
|
||||||
run: |
|
run: |
|
||||||
if [[ "${{ github.ref }}" == "refs/tags/"* ]]; then
|
if [[ "${{ github.ref }}" == "refs/tags/"* ]]; then
|
||||||
echo "matrix=[\"ubuntu-latest\", \"arm64\"]" >> $GITHUB_OUTPUT
|
echo 'matrix=[{"runner": "ubuntu-latest", "arch": "amd64"}, {"runner": "arm64", "arch": "arm64"}]' >> $GITHUB_OUTPUT
|
||||||
else
|
else
|
||||||
echo "matrix=[\"ubuntu-latest\"]" >> $GITHUB_OUTPUT
|
echo 'matrix=[{"runner": "ubuntu-latest", "arch": "amd64"}]' >> $GITHUB_OUTPUT
|
||||||
fi
|
fi
|
||||||
|
|
||||||
tarball:
|
tarball:
|
||||||
@@ -42,10 +44,15 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
token: ${{ secrets.GH_PAT }}
|
token: ${{ secrets.GH_PAT }}
|
||||||
repository: yuezk/GlobalProtect-openconnect
|
repository: yuezk/GlobalProtect-openconnect
|
||||||
|
ref: ${{ github.ref }}
|
||||||
path: source/gp
|
path: source/gp
|
||||||
- name: Create tarball
|
- name: Create tarball
|
||||||
run: |
|
run: |
|
||||||
cd source/gp
|
cd source/gp
|
||||||
|
# Generate the SNAPSHOT file for non-tagged commits
|
||||||
|
if [[ "${{ github.ref }}" != "refs/tags/"* ]]; then
|
||||||
|
touch SNAPSHOT
|
||||||
|
fi
|
||||||
make tarball
|
make tarball
|
||||||
- name: Upload tarball
|
- name: Upload tarball
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
@@ -55,140 +62,48 @@ jobs:
|
|||||||
path: |
|
path: |
|
||||||
source/gp/.build/tarball/*.tar.gz
|
source/gp/.build/tarball/*.tar.gz
|
||||||
|
|
||||||
build-deb:
|
build-gp:
|
||||||
needs:
|
needs:
|
||||||
- setup-matrix
|
- setup-matrix
|
||||||
- tarball
|
- tarball
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: ${{fromJson(needs.setup-matrix.outputs.matrix)}}
|
# Only build gp on amd64, as the arm64 package will be built in release.yaml
|
||||||
runs-on: ${{ matrix.os }}
|
os: [{runner: ubuntu-latest, arch: amd64}]
|
||||||
|
package: [deb, rpm, pkg, binary]
|
||||||
|
runs-on: ${{ matrix.os.runner }}
|
||||||
|
name: build-gp (${{ matrix.package }}, ${{ matrix.os.arch }})
|
||||||
steps:
|
steps:
|
||||||
- name: Prepare workspace
|
- name: Prepare workspace
|
||||||
run: rm -rf build-deb && mkdir build-deb
|
run: |
|
||||||
|
rm -rf build-gp-${{ matrix.package }}
|
||||||
|
mkdir -p build-gp-${{ matrix.package }}
|
||||||
- name: Download tarball
|
- name: Download tarball
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: artifact-source
|
name: artifact-source
|
||||||
path: build-deb
|
path: build-gp-${{ matrix.package }}
|
||||||
- name: Docker Login
|
- name: Docker Login
|
||||||
run: echo ${{ secrets.DOCKER_HUB_TOKEN }} | docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} --password-stdin
|
run: echo ${{ secrets.DOCKER_HUB_TOKEN }} | docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} --password-stdin
|
||||||
- name: Build DEB package in Docker
|
- name: Build ${{ matrix.package }} package in Docker
|
||||||
run: |
|
run: |
|
||||||
docker run --rm -v $(pwd)/build-deb:/deb yuezk/gpdev:deb-builder
|
docker run --rm \
|
||||||
- name: Install DEB package in Docker
|
-v $(pwd)/build-gp-${{ matrix.package }}:/${{ matrix.package }} \
|
||||||
|
yuezk/gpdev:${{ matrix.package }}-builder
|
||||||
|
- name: Install ${{ matrix.package }} package in Docker
|
||||||
run: |
|
run: |
|
||||||
docker run --rm -v $(pwd)/build-deb:/deb yuezk/gpdev:deb-builder \
|
docker run --rm \
|
||||||
bash -c "sudo dpkg -i /deb/*.deb; gpclient --version; gpservice --version; gpauth --version; gpgui-helper --version;"
|
-e GPGUI_INSTALLED=0 \
|
||||||
- name: Upload DEB package
|
-v $(pwd)/build-gp-${{ matrix.package }}:/${{ matrix.package }} \
|
||||||
|
yuezk/gpdev:${{ matrix.package }}-builder \
|
||||||
|
bash install.sh
|
||||||
|
- name: Upload ${{ matrix.package }} package
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: artifact-deb-${{ matrix.os }}
|
name: artifact-gp-${{ matrix.package }}-${{ matrix.os.arch }}
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
path: |
|
path: |
|
||||||
build-deb/*.deb
|
build-gp-${{ matrix.package }}/artifacts/*
|
||||||
|
|
||||||
build-rpm:
|
|
||||||
needs:
|
|
||||||
- setup-matrix
|
|
||||||
- tarball
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
os: ${{fromJson(needs.setup-matrix.outputs.matrix)}}
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
steps:
|
|
||||||
- name: Prepare workspace
|
|
||||||
run: rm -rf build-rpm && mkdir build-rpm
|
|
||||||
- name: Download tarball
|
|
||||||
uses: actions/download-artifact@v3
|
|
||||||
with:
|
|
||||||
name: artifact-source
|
|
||||||
path: build-rpm
|
|
||||||
- name: Docker Login
|
|
||||||
run: echo ${{ secrets.DOCKER_HUB_TOKEN }} | docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} --password-stdin
|
|
||||||
- name: Build RPM package in Docker
|
|
||||||
run: |
|
|
||||||
docker run --rm -v $(pwd)/build-rpm:/rpm yuezk/gpdev:rpm-builder
|
|
||||||
- name: Install RPM package in Docker
|
|
||||||
run: |
|
|
||||||
docker run --rm -v $(pwd)/build-rpm:/rpm yuezk/gpdev:rpm-builder \
|
|
||||||
bash -c "sudo rpm -i /rpm/*.$(uname -m).rpm; gpclient --version; gpservice --version; gpauth --version; gpgui-helper --version;"
|
|
||||||
- name: Upload RPM package
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: artifact-rpm-${{ matrix.os }}
|
|
||||||
if-no-files-found: error
|
|
||||||
path: |
|
|
||||||
build-rpm/*.rpm
|
|
||||||
|
|
||||||
build-pkgbuild:
|
|
||||||
needs:
|
|
||||||
- setup-matrix
|
|
||||||
- tarball
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
os: ${{fromJson(needs.setup-matrix.outputs.matrix)}}
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
steps:
|
|
||||||
- name: Prepare workspace
|
|
||||||
run: rm -rf build-pkgbuild && mkdir build-pkgbuild
|
|
||||||
- name: Download tarball
|
|
||||||
uses: actions/download-artifact@v3
|
|
||||||
with:
|
|
||||||
name: artifact-source
|
|
||||||
path: build-pkgbuild
|
|
||||||
- name: Docker Login
|
|
||||||
run: echo ${{ secrets.DOCKER_HUB_TOKEN }} | docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} --password-stdin
|
|
||||||
- name: Build PKGBUILD package in Docker
|
|
||||||
run: |
|
|
||||||
docker run --rm -v $(pwd)/build-pkgbuild:/pkgbuild yuezk/gpdev:pkgbuild
|
|
||||||
- name: Install PKGBUILD package in Docker
|
|
||||||
run: |
|
|
||||||
docker run --rm -v $(pwd)/build-pkgbuild:/pkgbuild yuezk/gpdev:pkgbuild \
|
|
||||||
bash -c "sudo pacman -U --noconfirm /pkgbuild/*.pkg.tar.zst; gpclient --version; gpservice --version; gpauth --version; gpgui-helper --version;"
|
|
||||||
- name: Upload PKGBUILD package
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: artifact-pkgbuild-${{ matrix.os }}
|
|
||||||
if-no-files-found: error
|
|
||||||
path: |
|
|
||||||
build-pkgbuild/*.pkg.tar.zst
|
|
||||||
|
|
||||||
build-binary:
|
|
||||||
needs:
|
|
||||||
- setup-matrix
|
|
||||||
- tarball
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
os: ${{fromJson(needs.setup-matrix.outputs.matrix)}}
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
steps:
|
|
||||||
- name: Prepare workspace
|
|
||||||
run: rm -rf build-binary && mkdir build-binary
|
|
||||||
- name: Download tarball
|
|
||||||
uses: actions/download-artifact@v3
|
|
||||||
with:
|
|
||||||
name: artifact-source
|
|
||||||
path: build-binary
|
|
||||||
- name: Docker Login
|
|
||||||
run: echo ${{ secrets.DOCKER_HUB_TOKEN }} | docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} --password-stdin
|
|
||||||
- name: Build binary in Docker
|
|
||||||
run: |
|
|
||||||
docker run --rm -v $(pwd)/build-binary:/binary yuezk/gpdev:binary-builder
|
|
||||||
- name: Install binary in Docker
|
|
||||||
run: |
|
|
||||||
cd build-binary
|
|
||||||
tar -xJf ./*.bin.tar.xz
|
|
||||||
docker run --rm -v $(pwd):/binary yuezk/gpdev:binary-builder \
|
|
||||||
bash -c "cd /binary/globalprotect-openconnect*/ && sudo make install && gpclient --version && gpservice --version && gpauth --version && gpgui-helper --version;"
|
|
||||||
- name: Upload binary
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: artifact-binary-${{ matrix.os }}
|
|
||||||
if-no-files-found: error
|
|
||||||
path: |
|
|
||||||
build-binary/*.bin.tar.xz
|
|
||||||
build-binary/*.bin.tar.xz.sha256
|
|
||||||
|
|
||||||
build-gpgui:
|
build-gpgui:
|
||||||
needs:
|
needs:
|
||||||
@@ -196,7 +111,8 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: ${{fromJson(needs.setup-matrix.outputs.matrix)}}
|
os: ${{fromJson(needs.setup-matrix.outputs.matrix)}}
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os.runner }}
|
||||||
|
name: build-gpgui (${{ matrix.os.arch }})
|
||||||
steps:
|
steps:
|
||||||
- uses: pnpm/action-setup@v2
|
- uses: pnpm/action-setup@v2
|
||||||
with:
|
with:
|
||||||
@@ -208,12 +124,14 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
token: ${{ secrets.GH_PAT }}
|
token: ${{ secrets.GH_PAT }}
|
||||||
repository: yuezk/GlobalProtect-openconnect
|
repository: yuezk/GlobalProtect-openconnect
|
||||||
|
ref: ${{ github.ref }}
|
||||||
path: gpgui-source/gp
|
path: gpgui-source/gp
|
||||||
- name: Checkout gpgui
|
- name: Checkout gpgui@${{ github.ref_name }}
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GH_PAT }}
|
token: ${{ secrets.GH_PAT }}
|
||||||
repository: yuezk/gpgui
|
repository: yuezk/gpgui
|
||||||
|
ref: ${{ github.ref_name }}
|
||||||
path: gpgui-source/gpgui
|
path: gpgui-source/gpgui
|
||||||
- name: Tarball
|
- name: Tarball
|
||||||
run: |
|
run: |
|
||||||
@@ -233,34 +151,40 @@ jobs:
|
|||||||
- name: Upload gpgui
|
- name: Upload gpgui
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: artifact-gpgui-${{ matrix.os }}
|
name: artifact-gpgui-${{ matrix.os.arch }}
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
path: |
|
path: |
|
||||||
gpgui-source/*.bin.tar.xz
|
gpgui-source/*.bin.tar.xz
|
||||||
gpgui-source/*.bin.tar.xz.sha256
|
gpgui-source/*.bin.tar.xz.sha256
|
||||||
|
|
||||||
gh-release:
|
gh-release:
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
if: ${{ github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/') }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs:
|
needs:
|
||||||
- build-deb
|
- tarball
|
||||||
- build-rpm
|
- build-gp
|
||||||
- build-pkgbuild
|
|
||||||
- build-binary
|
|
||||||
- build-gpgui
|
- build-gpgui
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Prepare workspace
|
- name: Prepare workspace
|
||||||
run: rm -rf build-artifact && mkdir build-artifact
|
run: rm -rf gh-release && mkdir gh-release
|
||||||
|
|
||||||
|
- name: Checkout GlobalProtect-openconnect
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GH_PAT }}
|
||||||
|
repository: yuezk/GlobalProtect-openconnect
|
||||||
|
ref: ${{ github.ref }}
|
||||||
|
path: gh-release/gp
|
||||||
|
|
||||||
- name: Download all artifacts
|
- name: Download all artifacts
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
path: build-artifact
|
path: gh-release/gp/.build/artifacts
|
||||||
|
|
||||||
- name: Create GH release
|
- name: Create GH release
|
||||||
uses: softprops/action-gh-release@v1
|
env:
|
||||||
with:
|
GH_TOKEN: ${{ secrets.GH_PAT }}
|
||||||
token: ${{ secrets.GH_PAT }}
|
RELEASE_TAG: ${{ github.ref == 'refs/heads/dev' && 'snapshot' || github.ref_name }}
|
||||||
prerelease: ${{ contains(github.ref, 'latest') }}
|
run: |
|
||||||
fail_on_unmatched_files: true
|
cd gh-release/gp/scripts && ./gh-release.sh "$RELEASE_TAG"
|
||||||
files: |
|
|
||||||
build-artifact/artifact-*/*
|
|
||||||
|
89
.github/workflows/publish.yaml
vendored
Normal file
89
.github/workflows/publish.yaml
vendored
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
name: Publish Packages
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
tag:
|
||||||
|
description: 'Tag to publish'
|
||||||
|
required: true
|
||||||
|
revision:
|
||||||
|
description: 'Package revision'
|
||||||
|
required: true
|
||||||
|
default: "1"
|
||||||
|
ppa:
|
||||||
|
description: 'Publish to PPA'
|
||||||
|
type: boolean
|
||||||
|
required: true
|
||||||
|
default: true
|
||||||
|
obs:
|
||||||
|
description: 'Publish to OBS'
|
||||||
|
type: boolean
|
||||||
|
required: true
|
||||||
|
default: true
|
||||||
|
aur:
|
||||||
|
description: 'Publish to AUR'
|
||||||
|
type: boolean
|
||||||
|
required: true
|
||||||
|
default: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check tag exists
|
||||||
|
uses: mukunku/tag-exists-action@v1.6.0
|
||||||
|
id: check-tag
|
||||||
|
with:
|
||||||
|
tag: ${{ inputs.tag }}
|
||||||
|
- name: Exit if tag does not exist
|
||||||
|
run: |
|
||||||
|
if [[ "${{ steps.check-tag.outputs.exists }}" == "false" ]]; then
|
||||||
|
echo "Tag ${{ inputs.tag }} does not exist"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
publish-ppa:
|
||||||
|
needs: check
|
||||||
|
if: ${{ inputs.ppa }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: pnpm/action-setup@v2
|
||||||
|
with:
|
||||||
|
version: 8
|
||||||
|
- name: Prepare workspace
|
||||||
|
run: rm -rf publish-ppa && mkdir publish-ppa
|
||||||
|
- name: Download ${{ inputs.tag }} source code
|
||||||
|
uses: robinraju/release-downloader@v1.9
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GH_PAT }}
|
||||||
|
tag: ${{ inputs.tag }}
|
||||||
|
fileName: globalprotect-openconnect-*.tar.gz
|
||||||
|
tarBall: false
|
||||||
|
zipBall: false
|
||||||
|
out-file-path: publish-ppa
|
||||||
|
- name: Make the offline tarball
|
||||||
|
run: |
|
||||||
|
cd publish-ppa
|
||||||
|
tar -xf globalprotect-openconnect-*.tar.gz
|
||||||
|
cd globalprotect-openconnect-*/
|
||||||
|
|
||||||
|
make tarball OFFLINE=1
|
||||||
|
|
||||||
|
# Prepare the debian directory with custom files
|
||||||
|
mkdir -p .build/debian
|
||||||
|
sed 's/@RUST@/rust-all(>=1.70)/g' packaging/deb/control.in > .build/debian/control
|
||||||
|
sed 's/@OFFLINE@/1/g' packaging/deb/rules.in > .build/debian/rules
|
||||||
|
cp packaging/deb/postrm .build/debian/postrm
|
||||||
|
|
||||||
|
- name: Publish to PPA
|
||||||
|
uses: yuezk/publish-ppa-package@dev
|
||||||
|
with:
|
||||||
|
repository: "yuezk/globalprotect-openconnect"
|
||||||
|
gpg_private_key: ${{ secrets.PPA_GPG_PRIVATE_KEY }}
|
||||||
|
gpg_passphrase: ${{ secrets.PPA_GPG_PASSPHRASE }}
|
||||||
|
tarball: publish-ppa/globalprotect-openconnect-*/.build/tarball/*.tar.gz
|
||||||
|
debian_dir: publish-ppa/globalprotect-openconnect-*/.build/debian
|
||||||
|
deb_email: "k3vinyue@gmail.com"
|
||||||
|
deb_fullname: "Kevin Yue"
|
||||||
|
extra_ppa: "liushuyu-011/rust-bpo-1.75"
|
||||||
|
revision: ${{ inputs.revision }}
|
153
.github/workflows/release.yaml
vendored
Normal file
153
.github/workflows/release.yaml
vendored
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
name: Release Packages
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
tag:
|
||||||
|
description: 'Tag to release'
|
||||||
|
required: true
|
||||||
|
arch:
|
||||||
|
type: choice
|
||||||
|
description: 'Architecture to build'
|
||||||
|
required: true
|
||||||
|
default: all
|
||||||
|
options:
|
||||||
|
- all
|
||||||
|
- x86_64
|
||||||
|
- arm64
|
||||||
|
release-deb:
|
||||||
|
type: boolean
|
||||||
|
description: 'Build DEB package'
|
||||||
|
required: true
|
||||||
|
default: true
|
||||||
|
release-rpm:
|
||||||
|
type: boolean
|
||||||
|
description: 'Build RPM package'
|
||||||
|
required: true
|
||||||
|
default: true
|
||||||
|
release-pkg:
|
||||||
|
type: boolean
|
||||||
|
description: 'Build PKG package'
|
||||||
|
required: true
|
||||||
|
default: true
|
||||||
|
release-binary:
|
||||||
|
type: boolean
|
||||||
|
description: 'Build binary package'
|
||||||
|
required: true
|
||||||
|
default: true
|
||||||
|
gh-release:
|
||||||
|
type: boolean
|
||||||
|
description: 'Update GitHub release'
|
||||||
|
required: true
|
||||||
|
default: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check tag exists
|
||||||
|
uses: mukunku/tag-exists-action@v1.6.0
|
||||||
|
id: check-tag
|
||||||
|
with:
|
||||||
|
tag: ${{ inputs.tag }}
|
||||||
|
- name: Exit if tag does not exist
|
||||||
|
run: |
|
||||||
|
if [[ "${{ steps.check-tag.outputs.exists }}" == "false" ]]; then
|
||||||
|
echo "Tag ${{ inputs.tag }} does not exist"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
setup-matrix:
|
||||||
|
needs:
|
||||||
|
- check
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
matrix: ${{ steps.set-matrix.outputs.result }}
|
||||||
|
steps:
|
||||||
|
- name: Set up matrix
|
||||||
|
id: set-matrix
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
result-encoding: string
|
||||||
|
script: |
|
||||||
|
const inputs = ${{ toJson(inputs) }}
|
||||||
|
const { arch } = inputs
|
||||||
|
const osMap = {
|
||||||
|
"all": ["ubuntu-latest", "arm64"],
|
||||||
|
"x86_64": ["ubuntu-latest"],
|
||||||
|
"arm64": ["arm64"]
|
||||||
|
}
|
||||||
|
|
||||||
|
const package = Object.entries(inputs)
|
||||||
|
.filter(([key, value]) => key.startsWith('release-') && value)
|
||||||
|
.map(([key, value]) => key.replace('release-', ''))
|
||||||
|
|
||||||
|
return JSON.stringify({
|
||||||
|
os: osMap[arch],
|
||||||
|
package,
|
||||||
|
})
|
||||||
|
|
||||||
|
build:
|
||||||
|
needs:
|
||||||
|
- setup-matrix
|
||||||
|
strategy:
|
||||||
|
matrix: ${{ fromJson(needs.setup-matrix.outputs.matrix) }}
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
steps:
|
||||||
|
- name: Prepare workspace
|
||||||
|
run: rm -rf build-${{ matrix.package }} && mkdir -p build-${{ matrix.package }}
|
||||||
|
- name: Download ${{ inputs.tag }} source code
|
||||||
|
uses: robinraju/release-downloader@v1.9
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GH_PAT }}
|
||||||
|
tag: ${{ inputs.tag }}
|
||||||
|
fileName: globalprotect-openconnect-*.tar.gz
|
||||||
|
tarBall: false
|
||||||
|
zipBall: false
|
||||||
|
out-file-path: build-${{ matrix.package }}
|
||||||
|
- name: Docker Login
|
||||||
|
run: echo ${{ secrets.DOCKER_HUB_TOKEN }} | docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} --password-stdin
|
||||||
|
- name: Build ${{ matrix.package }} package in Docker
|
||||||
|
run: |
|
||||||
|
docker run --rm \
|
||||||
|
-v $(pwd)/build-${{ matrix.package }}:/${{ matrix.package }} \
|
||||||
|
-e INCLUDE_GUI=1 \
|
||||||
|
yuezk/gpdev:${{ matrix.package }}-builder
|
||||||
|
|
||||||
|
- name: Install ${{ matrix.package }} package in Docker
|
||||||
|
run: |
|
||||||
|
docker run --rm \
|
||||||
|
-v $(pwd)/build-${{ matrix.package }}:/${{ matrix.package }} \
|
||||||
|
yuezk/gpdev:${{ matrix.package }}-builder \
|
||||||
|
bash install.sh
|
||||||
|
|
||||||
|
- name: Upload ${{ matrix.package }} package
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: artifact-${{ matrix.os }}-${{ matrix.package }}
|
||||||
|
if-no-files-found: error
|
||||||
|
path: |
|
||||||
|
build-${{ matrix.package }}/artifacts/*
|
||||||
|
|
||||||
|
gh-release:
|
||||||
|
needs:
|
||||||
|
- build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ inputs.gh-release }}
|
||||||
|
steps:
|
||||||
|
- name: Prepare workspace
|
||||||
|
run: rm -rf gh-release && mkdir gh-release
|
||||||
|
- name: Download artifact
|
||||||
|
uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
path: gh-release
|
||||||
|
- name: Update release
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GH_PAT }}
|
||||||
|
prerelease: ${{ contains(github.ref, 'snapshot') }}
|
||||||
|
fail_on_unmatched_files: true
|
||||||
|
tag_name: ${{ inputs.tag }}
|
||||||
|
files: |
|
||||||
|
gh-release/artifact-*/*
|
||||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -7,3 +7,4 @@
|
|||||||
|
|
||||||
.cargo
|
.cargo
|
||||||
.build
|
.build
|
||||||
|
SNAPSHOT
|
||||||
|
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"cSpell.words": [
|
"cSpell.words": [
|
||||||
"authcookie",
|
"authcookie",
|
||||||
|
"badssl",
|
||||||
"bincode",
|
"bincode",
|
||||||
"chacha",
|
"chacha",
|
||||||
"clientos",
|
"clientos",
|
||||||
@@ -25,7 +26,9 @@
|
|||||||
"LOGNAME",
|
"LOGNAME",
|
||||||
"oneshot",
|
"oneshot",
|
||||||
"openconnect",
|
"openconnect",
|
||||||
|
"pkcs",
|
||||||
"pkexec",
|
"pkexec",
|
||||||
|
"pkey",
|
||||||
"Prelogin",
|
"Prelogin",
|
||||||
"prelogon",
|
"prelogon",
|
||||||
"prelogonuserauthcookie",
|
"prelogonuserauthcookie",
|
||||||
@@ -35,6 +38,7 @@
|
|||||||
"rspc",
|
"rspc",
|
||||||
"servercert",
|
"servercert",
|
||||||
"specta",
|
"specta",
|
||||||
|
"sslkey",
|
||||||
"sysinfo",
|
"sysinfo",
|
||||||
"tanstack",
|
"tanstack",
|
||||||
"tauri",
|
"tauri",
|
||||||
|
110
Cargo.lock
generated
110
Cargo.lock
generated
@@ -252,6 +252,12 @@ version = "0.21.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9"
|
checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base64"
|
||||||
|
version = "0.22.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "1.3.2"
|
version = "1.3.2"
|
||||||
@@ -562,6 +568,13 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "common"
|
||||||
|
version = "2.3.2"
|
||||||
|
dependencies = [
|
||||||
|
"is_executable",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "compile-time"
|
name = "compile-time"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -885,24 +898,6 @@ dependencies = [
|
|||||||
"litrs",
|
"litrs",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "dotenvy"
|
|
||||||
version = "0.15.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "dotenvy_macro"
|
|
||||||
version = "0.15.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "cb0235d912a8c749f4e0c9f18ca253b4c28cfefc1d2518096016d6e3230b6424"
|
|
||||||
dependencies = [
|
|
||||||
"dotenvy",
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn 1.0.109",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dtoa"
|
name = "dtoa"
|
||||||
version = "1.0.9"
|
version = "1.0.9"
|
||||||
@@ -1423,16 +1418,17 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gpapi"
|
name = "gpapi"
|
||||||
version = "2.1.0"
|
version = "2.3.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64 0.21.5",
|
"base64 0.21.5",
|
||||||
"chacha20poly1305",
|
"chacha20poly1305",
|
||||||
"clap",
|
"clap",
|
||||||
"dotenvy_macro",
|
|
||||||
"log",
|
"log",
|
||||||
"md5",
|
"md5",
|
||||||
"open",
|
"open",
|
||||||
|
"openssl",
|
||||||
|
"pem",
|
||||||
"redact-engine",
|
"redact-engine",
|
||||||
"regex",
|
"regex",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
@@ -1455,13 +1451,14 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gpauth"
|
name = "gpauth"
|
||||||
version = "2.1.0"
|
version = "2.3.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
"compile-time",
|
"compile-time",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"gpapi",
|
"gpapi",
|
||||||
|
"html-escape",
|
||||||
"log",
|
"log",
|
||||||
"regex",
|
"regex",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -1475,10 +1472,11 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gpclient"
|
name = "gpclient"
|
||||||
version = "2.1.0"
|
version = "2.3.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
|
"common",
|
||||||
"compile-time",
|
"compile-time",
|
||||||
"directories",
|
"directories",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
@@ -1496,7 +1494,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gpgui-helper"
|
name = "gpgui-helper"
|
||||||
version = "2.1.0"
|
version = "2.3.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
@@ -1514,7 +1512,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gpservice"
|
name = "gpservice"
|
||||||
version = "2.1.0"
|
version = "2.3.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"axum",
|
"axum",
|
||||||
@@ -1590,9 +1588,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "h2"
|
name = "h2"
|
||||||
version = "0.3.24"
|
version = "0.3.26"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9"
|
checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"fnv",
|
"fnv",
|
||||||
@@ -1609,9 +1607,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "h2"
|
name = "h2"
|
||||||
version = "0.4.2"
|
version = "0.4.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "31d030e59af851932b72ceebadf4a2b5986dba4c3b99dd2493f8273a0f151943"
|
checksum = "816ec7294445779408f36fe57bc5b7fc1cf59664059096c65f905c1c61f58069"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"fnv",
|
"fnv",
|
||||||
@@ -1665,6 +1663,15 @@ version = "0.4.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "html-escape"
|
||||||
|
version = "0.2.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476"
|
||||||
|
dependencies = [
|
||||||
|
"utf8-width",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "html5ever"
|
name = "html5ever"
|
||||||
version = "0.26.0"
|
version = "0.26.0"
|
||||||
@@ -1769,7 +1776,7 @@ dependencies = [
|
|||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"h2 0.3.24",
|
"h2 0.3.26",
|
||||||
"http 0.2.11",
|
"http 0.2.11",
|
||||||
"http-body 0.4.6",
|
"http-body 0.4.6",
|
||||||
"httparse",
|
"httparse",
|
||||||
@@ -1792,7 +1799,7 @@ dependencies = [
|
|||||||
"bytes",
|
"bytes",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"h2 0.4.2",
|
"h2 0.4.4",
|
||||||
"http 1.0.0",
|
"http 1.0.0",
|
||||||
"http-body 1.0.0",
|
"http-body 1.0.0",
|
||||||
"httparse",
|
"httparse",
|
||||||
@@ -2300,9 +2307,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mio"
|
name = "mio"
|
||||||
version = "0.8.10"
|
version = "0.8.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09"
|
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
@@ -2519,10 +2526,10 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openconnect"
|
name = "openconnect"
|
||||||
version = "2.1.0"
|
version = "2.3.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"is_executable",
|
"common",
|
||||||
"log",
|
"log",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2652,6 +2659,16 @@ version = "0.2.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
|
checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pem"
|
||||||
|
version = "3.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.22.1",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "percent-encoding"
|
name = "percent-encoding"
|
||||||
version = "2.3.1"
|
version = "2.3.1"
|
||||||
@@ -3149,7 +3166,7 @@ dependencies = [
|
|||||||
"encoding_rs",
|
"encoding_rs",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"h2 0.3.24",
|
"h2 0.3.26",
|
||||||
"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",
|
||||||
@@ -4147,9 +4164,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.35.1"
|
version = "1.36.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104"
|
checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"backtrace",
|
"backtrace",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -4476,6 +4493,12 @@ version = "0.7.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "utf8-width"
|
||||||
|
version = "0.1.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "utf8parse"
|
name = "utf8parse"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
@@ -4582,6 +4605,12 @@ version = "0.11.0+wasi-snapshot-preview1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasite"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen"
|
name = "wasm-bindgen"
|
||||||
version = "0.2.89"
|
version = "0.2.89"
|
||||||
@@ -4758,11 +4787,12 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "whoami"
|
name = "whoami"
|
||||||
version = "1.4.1"
|
version = "1.5.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "22fc3756b8a9133049b26c7f61ab35416c130e8c09b660f5b3958b446f52cc50"
|
checksum = "a44ab49fad634e88f55bf8f9bb3abd2f27d7204172a112c7c9987e01c1c94ea9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"wasm-bindgen",
|
"redox_syscall",
|
||||||
|
"wasite",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@@ -5,7 +5,7 @@ members = ["crates/*", "apps/gpclient", "apps/gpservice", "apps/gpauth", "apps/g
|
|||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
rust-version = "1.70"
|
rust-version = "1.70"
|
||||||
version = "2.1.0"
|
version = "2.3.2"
|
||||||
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"
|
||||||
@@ -22,6 +22,8 @@ is_executable = "1.0"
|
|||||||
log = "0.4"
|
log = "0.4"
|
||||||
regex = "1"
|
regex = "1"
|
||||||
reqwest = { version = "0.11", features = ["native-tls-vendored", "json"] }
|
reqwest = { version = "0.11", features = ["native-tls-vendored", "json"] }
|
||||||
|
openssl = "0.10"
|
||||||
|
pem = "3"
|
||||||
roxmltree = "0.18"
|
roxmltree = "0.18"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
@@ -39,7 +41,6 @@ uzers = "0.11"
|
|||||||
whoami = "1"
|
whoami = "1"
|
||||||
thiserror = "1"
|
thiserror = "1"
|
||||||
redact-engine = "0.1"
|
redact-engine = "0.1"
|
||||||
dotenvy_macro = "0.15"
|
|
||||||
compile-time = "0.2"
|
compile-time = "0.2"
|
||||||
serde_urlencoded = "0.7"
|
serde_urlencoded = "0.7"
|
||||||
md5="0.7"
|
md5="0.7"
|
||||||
|
53
Makefile
53
Makefile
@@ -1,5 +1,8 @@
|
|||||||
|
.SHELLFLAGS += -e
|
||||||
|
|
||||||
OFFLINE ?= 0
|
OFFLINE ?= 0
|
||||||
BUILD_FE ?= 1
|
BUILD_FE ?= 1
|
||||||
|
INCLUDE_GUI ?= 0
|
||||||
CARGO ?= cargo
|
CARGO ?= cargo
|
||||||
|
|
||||||
VERSION = $(shell $(CARGO) metadata --no-deps --format-version 1 | jq -r '.packages[0].version')
|
VERSION = $(shell $(CARGO) metadata --no-deps --format-version 1 | jq -r '.packages[0].version')
|
||||||
@@ -12,6 +15,13 @@ PUBLISH ?= 0
|
|||||||
|
|
||||||
export DEBEMAIL = k3vinyue@gmail.com
|
export DEBEMAIL = k3vinyue@gmail.com
|
||||||
export DEBFULLNAME = Kevin Yue
|
export DEBFULLNAME = Kevin Yue
|
||||||
|
export SNAPSHOT = $(shell test -f SNAPSHOT && echo "true" || echo "false")
|
||||||
|
|
||||||
|
ifeq ($(SNAPSHOT), true)
|
||||||
|
RELEASE_TAG = snapshot
|
||||||
|
else
|
||||||
|
RELEASE_TAG = v$(VERSION)
|
||||||
|
endif
|
||||||
|
|
||||||
CARGO_BUILD_ARGS = --release
|
CARGO_BUILD_ARGS = --release
|
||||||
|
|
||||||
@@ -33,6 +43,7 @@ clean-tarball:
|
|||||||
# Create a tarball, include the cargo dependencies if OFFLINE is set to 1
|
# Create a tarball, include the cargo dependencies if OFFLINE is set to 1
|
||||||
tarball: clean-tarball
|
tarball: clean-tarball
|
||||||
if [ $(BUILD_FE) -eq 1 ]; then \
|
if [ $(BUILD_FE) -eq 1 ]; then \
|
||||||
|
echo "Building frontend..."; \
|
||||||
cd apps/gpgui-helper && pnpm install && pnpm build; \
|
cd apps/gpgui-helper && pnpm install && pnpm build; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -48,9 +59,23 @@ tarball: clean-tarball
|
|||||||
tar -cJf vendor.tar.xz .vendor; \
|
tar -cJf vendor.tar.xz .vendor; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
@echo "Creating tarball..."
|
||||||
tar --exclude .vendor --exclude target --transform 's,^,${PKG}/,' -czf .build/tarball/${PKG}.tar.gz * .cargo
|
tar --exclude .vendor --exclude target --transform 's,^,${PKG}/,' -czf .build/tarball/${PKG}.tar.gz * .cargo
|
||||||
|
|
||||||
build: build-fe build-rs
|
download-gui:
|
||||||
|
rm -rf .build/gpgui
|
||||||
|
|
||||||
|
if [ $(INCLUDE_GUI) -eq 1 ]; then \
|
||||||
|
echo "Downloading GlobalProtect GUI..."; \
|
||||||
|
mkdir -p .build/gpgui; \
|
||||||
|
curl -sSL https://github.com/yuezk/GlobalProtect-openconnect/releases/download/$(RELEASE_TAG)/gpgui_$(shell uname -m).bin.tar.xz \
|
||||||
|
-o .build/gpgui/gpgui_$(shell uname -m).bin.tar.xz; \
|
||||||
|
tar -xJf .build/gpgui/*.tar.xz -C .build/gpgui; \
|
||||||
|
else \
|
||||||
|
echo "Skipping GlobalProtect GUI download (INCLUDE_GUI=0)"; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
build: download-gui build-fe build-rs
|
||||||
|
|
||||||
# Install and build the frontend
|
# Install and build the frontend
|
||||||
# If OFFLINE is set to 1, skip it
|
# If OFFLINE is set to 1, skip it
|
||||||
@@ -88,6 +113,10 @@ install:
|
|||||||
install -Dm755 target/release/gpservice $(DESTDIR)/usr/bin/gpservice
|
install -Dm755 target/release/gpservice $(DESTDIR)/usr/bin/gpservice
|
||||||
install -Dm755 target/release/gpgui-helper $(DESTDIR)/usr/bin/gpgui-helper
|
install -Dm755 target/release/gpgui-helper $(DESTDIR)/usr/bin/gpgui-helper
|
||||||
|
|
||||||
|
if [ -f .build/gpgui/gpgui_*/gpgui ]; then \
|
||||||
|
install -Dm755 .build/gpgui/gpgui_*/gpgui $(DESTDIR)/usr/bin/gpgui; \
|
||||||
|
fi
|
||||||
|
|
||||||
install -Dm644 packaging/files/usr/share/applications/gpgui.desktop $(DESTDIR)/usr/share/applications/gpgui.desktop
|
install -Dm644 packaging/files/usr/share/applications/gpgui.desktop $(DESTDIR)/usr/share/applications/gpgui.desktop
|
||||||
install -Dm644 packaging/files/usr/share/icons/hicolor/scalable/apps/gpgui.svg $(DESTDIR)/usr/share/icons/hicolor/scalable/apps/gpgui.svg
|
install -Dm644 packaging/files/usr/share/icons/hicolor/scalable/apps/gpgui.svg $(DESTDIR)/usr/share/icons/hicolor/scalable/apps/gpgui.svg
|
||||||
install -Dm644 packaging/files/usr/share/icons/hicolor/32x32/apps/gpgui.png $(DESTDIR)/usr/share/icons/hicolor/32x32/apps/gpgui.png
|
install -Dm644 packaging/files/usr/share/icons/hicolor/32x32/apps/gpgui.png $(DESTDIR)/usr/share/icons/hicolor/32x32/apps/gpgui.png
|
||||||
@@ -123,7 +152,8 @@ init-debian: clean-debian tarball
|
|||||||
cd .build/deb/${PKG} && debmake
|
cd .build/deb/${PKG} && debmake
|
||||||
|
|
||||||
cp -f packaging/deb/control.in .build/deb/$(PKG)/debian/control
|
cp -f packaging/deb/control.in .build/deb/$(PKG)/debian/control
|
||||||
cp -f packaging/deb/rules .build/deb/$(PKG)/debian/rules
|
cp -f packaging/deb/rules.in .build/deb/$(PKG)/debian/rules
|
||||||
|
cp -f packaging/deb/postrm .build/deb/$(PKG)/debian/postrm
|
||||||
|
|
||||||
sed -i "s/@OFFLINE@/$(OFFLINE)/g" .build/deb/$(PKG)/debian/rules
|
sed -i "s/@OFFLINE@/$(OFFLINE)/g" .build/deb/$(PKG)/debian/rules
|
||||||
|
|
||||||
@@ -144,21 +174,20 @@ check-ppa:
|
|||||||
|
|
||||||
# Usage: make ppa SERIES=focal OFFLINE=1 PUBLISH=1
|
# Usage: make ppa SERIES=focal OFFLINE=1 PUBLISH=1
|
||||||
ppa: check-ppa init-debian
|
ppa: check-ppa init-debian
|
||||||
cd .build/deb/${PKG}
|
sed -i "s/@RUST@/rust-all(>=1.70)/g" .build/deb/$(PKG)/debian/control
|
||||||
|
|
||||||
sed -i "s/@RUST@/rust-all(>=1.70)/g" debian/control
|
|
||||||
|
|
||||||
$(eval SERIES_VER = $(shell distro-info --series $(SERIES) -r | cut -d' ' -f1))
|
$(eval SERIES_VER = $(shell distro-info --series $(SERIES) -r | cut -d' ' -f1))
|
||||||
@echo "Building for $(SERIES) $(SERIES_VER)"
|
@echo "Building for $(SERIES) $(SERIES_VER)"
|
||||||
|
|
||||||
dch --create --distribution $(SERIES) --package $(PKG_NAME) --newversion $(VERSION)-$(REVISION)ppa$(PPA_REVISION)~ubuntu$(SERIES_VER) "Bugfix and improvements."
|
rm -rf .build/deb/$(PKG)/debian/changelog
|
||||||
|
cd .build/deb/$(PKG) && dch --create --distribution $(SERIES) --package $(PKG_NAME) --newversion $(VERSION)-$(REVISION)ppa$(PPA_REVISION)~ubuntu$(SERIES_VER) "Bugfix and improvements."
|
||||||
|
|
||||||
echo "y" | debuild -e PATH -S -sa -k"$(GPG_KEY_ID)" -p"gpg --batch --passphrase $(GPG_KEY_PASS) --pinentry-mode loopback"
|
cd .build/deb/$(PKG) && echo "y" | debuild -e PATH -S -sa -k"$(GPG_KEY_ID)" -p"gpg --batch --passphrase $(GPG_KEY_PASS) --pinentry-mode loopback"
|
||||||
|
|
||||||
if [ $(PUBLISH) -eq 1 ]; then \
|
if [ $(PUBLISH) -eq 1 ]; then \
|
||||||
dput ppa:yuezk/globalprotect-openconnect ../*.changes; \
|
cd .build/deb/$(PKG) && dput ppa:yuezk/globalprotect-openconnect ../*.changes; \
|
||||||
else
|
else \
|
||||||
echo "Skipping ppa publish (PUBLISH=0)"
|
echo "Skipping ppa publish (PUBLISH=0)"; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
clean-rpm:
|
clean-rpm:
|
||||||
@@ -174,7 +203,7 @@ init-rpm: clean-rpm
|
|||||||
sed -i "s/@VERSION@/$(VERSION)/g" .build/rpm/globalprotect-openconnect.spec
|
sed -i "s/@VERSION@/$(VERSION)/g" .build/rpm/globalprotect-openconnect.spec
|
||||||
sed -i "s/@REVISION@/$(REVISION)/g" .build/rpm/globalprotect-openconnect.spec
|
sed -i "s/@REVISION@/$(REVISION)/g" .build/rpm/globalprotect-openconnect.spec
|
||||||
sed -i "s/@OFFLINE@/$(OFFLINE)/g" .build/rpm/globalprotect-openconnect.spec
|
sed -i "s/@OFFLINE@/$(OFFLINE)/g" .build/rpm/globalprotect-openconnect.spec
|
||||||
sed -i "s/@DATE@/$(shell date "+%a %b %d %Y")/g" .build/rpm/globalprotect-openconnect.spec
|
sed -i "s/@DATE@/$(shell LC_ALL=en.US date "+%a %b %d %Y")/g" .build/rpm/globalprotect-openconnect.spec
|
||||||
|
|
||||||
sed -i "s/@VERSION@/$(VERSION)/g" .build/rpm/globalprotect-openconnect.changes
|
sed -i "s/@VERSION@/$(VERSION)/g" .build/rpm/globalprotect-openconnect.changes
|
||||||
sed -i "s/@DATE@/$(shell LC_ALL=en.US date -u "+%a %b %e %T %Z %Y")/g" .build/rpm/globalprotect-openconnect.changes
|
sed -i "s/@DATE@/$(shell LC_ALL=en.US date -u "+%a %b %e %T %Z %Y")/g" .build/rpm/globalprotect-openconnect.changes
|
||||||
@@ -222,7 +251,7 @@ binary: clean-binary tarball
|
|||||||
|
|
||||||
mkdir -p .build/binary/$(PKG_NAME)_$(VERSION)/artifacts
|
mkdir -p .build/binary/$(PKG_NAME)_$(VERSION)/artifacts
|
||||||
|
|
||||||
make -C .build/binary/${PKG} build OFFLINE=$(OFFLINE) BUILD_FE=0
|
make -C .build/binary/${PKG} build OFFLINE=$(OFFLINE) BUILD_FE=0 INCLUDE_GUI=$(INCLUDE_GUI)
|
||||||
make -C .build/binary/${PKG} install DESTDIR=$(PWD)/.build/binary/$(PKG_NAME)_$(VERSION)/artifacts
|
make -C .build/binary/${PKG} install DESTDIR=$(PWD)/.build/binary/$(PKG_NAME)_$(VERSION)/artifacts
|
||||||
|
|
||||||
cp packaging/binary/Makefile.in .build/binary/$(PKG_NAME)_$(VERSION)/Makefile
|
cp packaging/binary/Makefile.in .build/binary/$(PKG_NAME)_$(VERSION)/Makefile
|
||||||
|
108
README.md
108
README.md
@@ -13,6 +13,7 @@ A GUI for GlobalProtect VPN, based on OpenConnect, supports the SSO authenticati
|
|||||||
- [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 the FIDO2 authentication (e.g., YubiKey)
|
||||||
- [x] Support authentication using default browser
|
- [x] Support authentication using default browser
|
||||||
|
- [x] Support client certificate authentication
|
||||||
- [x] Support multiple portals
|
- [x] Support multiple portals
|
||||||
- [x] Support gateway selection
|
- [x] Support gateway selection
|
||||||
- [x] Support connect gateway directly
|
- [x] Support connect gateway directly
|
||||||
@@ -43,6 +44,12 @@ Options:
|
|||||||
See 'gpclient help <command>' for more information on a specific command.
|
See 'gpclient help <command>' for more information on a specific command.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
To use the default browser for authentication with the CLI version, you need to use the following command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo -E gpclient connect --default-browser <portal>
|
||||||
|
```
|
||||||
|
|
||||||
### GUI
|
### GUI
|
||||||
|
|
||||||
The GUI version is also available after you installed it. You can launch it from the application menu or run `gpclient launch-gui` in the terminal.
|
The GUI version is also available after you installed it. You can launch it from the application menu or run `gpclient launch-gui` in the terminal.
|
||||||
@@ -53,18 +60,9 @@ 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, pkexec, and gnome-keyring`, please make sure you have them installed.
|
|
||||||
> Installing the client from PPA will automatically install the required version of `openconnect`.
|
|
||||||
|
|
||||||
### Debian/Ubuntu based distributions
|
### Debian/Ubuntu based distributions
|
||||||
|
|
||||||
#### Install from PPA
|
#### Install from PPA (Ubuntu 18.04 and later, except 24.04)
|
||||||
|
|
||||||
```
|
```
|
||||||
sudo apt-get install gir1.2-gtk-3.0 gir1.2-webkit2-4.0
|
sudo apt-get install gir1.2-gtk-3.0 gir1.2-webkit2-4.0
|
||||||
@@ -77,12 +75,29 @@ sudo apt-get install globalprotect-openconnect
|
|||||||
>
|
>
|
||||||
> 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`.
|
> 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
|
#### **Ubuntu 24.04 and later**
|
||||||
|
|
||||||
Download the latest deb package from [releases](https://github.com/yuezk/GlobalProtect-openconnect/releases) page. Then install it with `dpkg`:
|
The `libwebkit2gtk-4.0-37` package was [removed](https://bugs.launchpad.net/ubuntu/+source/webkit2gtk/+bug/2061914) from its repo, before [the issue](https://github.com/yuezk/GlobalProtect-openconnect/issues/351) gets resolved, you need to install them manually:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo dpkg -i globalprotect-openconnect_*.deb
|
wget http://launchpadlibrarian.net/704701349/libwebkit2gtk-4.0-37_2.43.3-1_amd64.deb
|
||||||
|
wget http://launchpadlibrarian.net/704701345/libjavascriptcoregtk-4.0-18_2.43.3-1_amd64.deb
|
||||||
|
|
||||||
|
sudo dpkg --install *.deb
|
||||||
|
```
|
||||||
|
|
||||||
|
And the latest package is not available in the PPA, you can follow the [Install from deb package](#install-from-deb-package) section to install the latest package.
|
||||||
|
|
||||||
|
#### **Ubuntu 18.04**
|
||||||
|
|
||||||
|
The latest package is not available in the PPA either, but you still needs to add the `ppa:yuezk/globalprotect-openconnect` repo beforehand to use the required `openconnect` package. Then you can follow the [Install from deb package](#install-from-deb-package) section to install the latest package.
|
||||||
|
|
||||||
|
#### Install from deb package
|
||||||
|
|
||||||
|
Download the latest deb package from [releases](https://github.com/yuezk/GlobalProtect-openconnect/releases) page. Then install it with `apt`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt install --fix-broken globalprotect-openconnect_*.deb
|
||||||
```
|
```
|
||||||
|
|
||||||
### Arch Linux / Manjaro
|
### Arch Linux / Manjaro
|
||||||
@@ -103,7 +118,7 @@ Download the latest package from [releases](https://github.com/yuezk/GlobalProte
|
|||||||
sudo pacman -U globalprotect-openconnect-*.pkg.tar.zst
|
sudo pacman -U globalprotect-openconnect-*.pkg.tar.zst
|
||||||
```
|
```
|
||||||
|
|
||||||
### Fedora/OpenSUSE/CentOS/RHEL
|
### Fedora 38 and later / Fedora Rawhide
|
||||||
|
|
||||||
#### Install from COPR
|
#### Install from COPR
|
||||||
|
|
||||||
@@ -114,29 +129,82 @@ sudo dnf copr enable yuezk/globalprotect-openconnect
|
|||||||
sudo dnf install globalprotect-openconnect
|
sudo dnf install globalprotect-openconnect
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Install from OBS (OpenSUSE Build Service)
|
### openSUSE Leap 15.6 / openSUSE Tumbleweed
|
||||||
|
|
||||||
|
#### Install from OBS (openSUSE Build Service)
|
||||||
|
|
||||||
The package is also available on [OBS](https://build.opensuse.org/package/show/home:yuezk/globalprotect-openconnect) for various RPM-based distributions. You can follow the instructions [on this page](https://software.opensuse.org//download.html?project=home%3Ayuezk&package=globalprotect-openconnect) to install it.
|
The package is also available on [OBS](https://build.opensuse.org/package/show/home:yuezk/globalprotect-openconnect) for various RPM-based distributions. You can follow the instructions [on this page](https://software.opensuse.org//download.html?project=home%3Ayuezk&package=globalprotect-openconnect) to install it.
|
||||||
|
|
||||||
|
### Other RPM-based distributions
|
||||||
|
|
||||||
#### Install from RPM package
|
#### Install from RPM package
|
||||||
|
|
||||||
Download the latest RPM package from [releases](https://github.com/yuezk/GlobalProtect-openconnect/releases) page.
|
Download the latest RPM package from [releases](https://github.com/yuezk/GlobalProtect-openconnect/releases) page.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo rpm -i globalprotect-openconnect-*.rpm
|
||||||
|
```
|
||||||
|
### Gentoo
|
||||||
|
|
||||||
|
Install from the ```rios``` or ```slonko``` overlays. Example using rios:
|
||||||
|
|
||||||
|
#### 1. Enable the overlay
|
||||||
|
```
|
||||||
|
sudo eselect repository enable rios
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Sync with the repository
|
||||||
|
|
||||||
|
- If you have eix installed, use it:
|
||||||
|
```
|
||||||
|
sudo eix-sync
|
||||||
|
```
|
||||||
|
- Otherwise, use:
|
||||||
|
```
|
||||||
|
sudo emerge --sync
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Install
|
||||||
|
|
||||||
|
```sudo emerge globalprotect-openconnect```
|
||||||
|
|
||||||
|
|
||||||
### Other distributions
|
### Other distributions
|
||||||
|
|
||||||
- Install `openconnect >= 8.20`, `webkit2gtk`, `libsecret`, `libayatana-appindicator` or `libappindicator-gtk3`.
|
- Install `openconnect >= 8.20`, `webkit2gtk`, `libsecret`, `libayatana-appindicator` or `libappindicator-gtk3`.
|
||||||
- Download `globalprotect-openconnect.tar.gz` from [releases](https://github.com/yuezk/GlobalProtect-openconnect/releases) page.
|
- Download `globalprotect-openconnect_${version}_${arch}.bin.tar.xz` from [releases](https://github.com/yuezk/GlobalProtect-openconnect/releases) page.
|
||||||
- Extract the tarball and run `make build` to build the client.
|
- Extract the tarball with `tar -xJf globalprotect-openconnect_${version}_${arch}.bin.tar.xz`.
|
||||||
- Run `make install` to install the client.
|
- Run `sudo make install` to install the client.
|
||||||
|
|
||||||
|
## Build from source
|
||||||
|
|
||||||
|
You can also build the client from source, steps are as follows:
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- [Install Rust](https://www.rust-lang.org/tools/install)
|
||||||
|
- Install Tauri dependencies: https://tauri.app/v1/guides/getting-started/prerequisites/#setting-up-linux
|
||||||
|
- Install `perl`
|
||||||
|
- Install `openconnect >= 8.20` and `libopenconnect-dev` (or `openconnect-devel` on RPM-based distributions)
|
||||||
|
- Install `pkexec`, `gnome-keyring` (or `pam_kwallet` on KDE)
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
1. Download the source code tarball from [releases](https://github.com/yuezk/GlobalProtect-openconnect/releases) page. Choose `globalprotect-openconnect-${version}.tar.gz`.
|
||||||
|
2. Extract the tarball with `tar -xzf globalprotect-openconnect-${version}.tar.gz`.
|
||||||
|
3. Enter the source directory and run `make build BUILD_FE=0` to build the client.
|
||||||
|
3. Run `sudo make install` to install the client. (Note, `DESTDIR` is not supported)
|
||||||
|
|
||||||
## FAQ
|
## FAQ
|
||||||
|
|
||||||
1. How to deal with error `Secure Storage not ready`
|
1. How to deal with error `Secure Storage not ready`
|
||||||
|
|
||||||
|
Try upgrade the client to `2.2.0` or later, which will use a file-based storage as a fallback.
|
||||||
|
|
||||||
You need to install the `gnome-keyring` package, and restart the system (See [#321](https://github.com/yuezk/GlobalProtect-openconnect/issues/321), [#316](https://github.com/yuezk/GlobalProtect-openconnect/issues/316)).
|
You need to install the `gnome-keyring` package, and restart the system (See [#321](https://github.com/yuezk/GlobalProtect-openconnect/issues/321), [#316](https://github.com/yuezk/GlobalProtect-openconnect/issues/316)).
|
||||||
|
|
||||||
2. How to deal with error `(gpauth:18869): Gtk-WARNING **: 10:33:37.566: cannot open display:`
|
2. How to deal with error `(gpauth:18869): Gtk-WARNING **: 10:33:37.566: cannot open display:`
|
||||||
|
|
||||||
If you encounter this error when using the CLI version, try to run the command with `sudo -E` (See [#316](https://github.com/yuezk/GlobalProtect-openconnect/issues/316)).
|
If you encounter this error when using the CLI version, try to run the command with `sudo -E` (See [#316](https://github.com/yuezk/GlobalProtect-openconnect/issues/316)).
|
||||||
|
|
||||||
## About Trial
|
## About Trial
|
||||||
|
@@ -8,7 +8,11 @@ license.workspace = true
|
|||||||
tauri-build = { version = "1.5", features = [] }
|
tauri-build = { version = "1.5", features = [] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
gpapi = { path = "../../crates/gpapi", features = ["tauri", "clap"] }
|
gpapi = { path = "../../crates/gpapi", features = [
|
||||||
|
"tauri",
|
||||||
|
"clap",
|
||||||
|
"browser-auth",
|
||||||
|
] }
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
clap.workspace = true
|
clap.workspace = true
|
||||||
env_logger.workspace = true
|
env_logger.workspace = true
|
||||||
@@ -18,6 +22,7 @@ serde_json.workspace = true
|
|||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
tokio-util.workspace = true
|
tokio-util.workspace = true
|
||||||
tempfile.workspace = true
|
tempfile.workspace = true
|
||||||
|
html-escape = "0.2.13"
|
||||||
webkit2gtk = "0.18.2"
|
webkit2gtk = "0.18.2"
|
||||||
tauri = { workspace = true, features = ["http-all"] }
|
tauri = { workspace = true, features = ["http-all"] }
|
||||||
compile-time.workspace = true
|
compile-time.workspace = true
|
||||||
|
@@ -7,6 +7,7 @@ use std::{
|
|||||||
use anyhow::bail;
|
use anyhow::bail;
|
||||||
use gpapi::{
|
use gpapi::{
|
||||||
auth::SamlAuthData,
|
auth::SamlAuthData,
|
||||||
|
error::AuthDataParseError,
|
||||||
gp_params::GpParams,
|
gp_params::GpParams,
|
||||||
portal::{prelogin, Prelogin},
|
portal::{prelogin, Prelogin},
|
||||||
utils::{redact::redact_uri, window::WindowExt},
|
utils::{redact::redact_uri, window::WindowExt},
|
||||||
@@ -184,6 +185,10 @@ impl<'a> AuthWindow<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
info!("Loaded uri: {}", redact_uri(&uri));
|
info!("Loaded uri: {}", redact_uri(&uri));
|
||||||
|
if uri.starts_with("globalprotectcallback:") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
read_auth_data(&main_resource, auth_result_tx_clone.clone());
|
read_auth_data(&main_resource, auth_result_tx_clone.clone());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -202,7 +207,9 @@ impl<'a> AuthWindow<'a> {
|
|||||||
|
|
||||||
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);
|
if !uri.starts_with("globalprotectcallback:") {
|
||||||
|
warn!("Failed to load uri: {} with error: {}", redacted_uri, err);
|
||||||
|
}
|
||||||
// NOTE: Don't send error here, since load_changed event will be triggered after this
|
// NOTE: Don't send error here, since load_changed event will be triggered after this
|
||||||
// send_auth_result(&auth_result_tx, Err(AuthDataError::Invalid));
|
// 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.
|
||||||
@@ -339,7 +346,7 @@ fn read_auth_data_from_headers(response: &URIResponse) -> AuthResult {
|
|||||||
|
|
||||||
fn read_auth_data_from_body<F>(main_resource: &WebResource, callback: F)
|
fn read_auth_data_from_body<F>(main_resource: &WebResource, callback: F)
|
||||||
where
|
where
|
||||||
F: FnOnce(AuthResult) + Send + 'static,
|
F: FnOnce(Result<SamlAuthData, AuthDataParseError>) + Send + 'static,
|
||||||
{
|
{
|
||||||
main_resource.data(Cancellable::NONE, |data| match data {
|
main_resource.data(Cancellable::NONE, |data| match data {
|
||||||
Ok(data) => {
|
Ok(data) => {
|
||||||
@@ -348,53 +355,41 @@ where
|
|||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
info!("Failed to read response body: {}", err);
|
info!("Failed to read response body: {}", err);
|
||||||
callback(Err(AuthDataError::Invalid))
|
callback(Err(AuthDataParseError::Invalid))
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_auth_data_from_html(html: &str) -> AuthResult {
|
fn read_auth_data_from_html(html: &str) -> Result<SamlAuthData, AuthDataParseError> {
|
||||||
if html.contains("Temporarily Unavailable") {
|
if html.contains("Temporarily Unavailable") {
|
||||||
info!("Found 'Temporarily Unavailable' in HTML, auth failed");
|
info!("Found 'Temporarily Unavailable' in HTML, auth failed");
|
||||||
return Err(AuthDataError::Invalid);
|
return Err(AuthDataParseError::Invalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
match parse_xml_tag(html, "saml-auth-status") {
|
SamlAuthData::from_html(html).or_else(|err| {
|
||||||
Some(saml_status) if saml_status == "1" => {
|
if let Some(gpcallback) = extract_gpcallback(html) {
|
||||||
let username = parse_xml_tag(html, "saml-username");
|
info!("Found gpcallback from html...");
|
||||||
let prelogin_cookie = parse_xml_tag(html, "prelogin-cookie");
|
SamlAuthData::from_gpcallback(&gpcallback)
|
||||||
let portal_userauthcookie = parse_xml_tag(html, "portal-userauthcookie");
|
} else {
|
||||||
|
Err(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if SamlAuthData::check(&username, &prelogin_cookie, &portal_userauthcookie) {
|
fn extract_gpcallback(html: &str) -> Option<String> {
|
||||||
return Ok(SamlAuthData::new(
|
let re = Regex::new(r#"globalprotectcallback:[^"]+"#).unwrap();
|
||||||
username.unwrap(),
|
re.captures(html)
|
||||||
prelogin_cookie,
|
.and_then(|captures| captures.get(0))
|
||||||
portal_userauthcookie,
|
.map(|m| html_escape::decode_html_entities(m.as_str()).to_string())
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
info!("Found invalid auth data in HTML");
|
|
||||||
Err(AuthDataError::Invalid)
|
|
||||||
}
|
|
||||||
Some(status) => {
|
|
||||||
info!("Found invalid SAML status {} in HTML", status);
|
|
||||||
Err(AuthDataError::Invalid)
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
info!("No auth data found in HTML");
|
|
||||||
Err(AuthDataError::NotFound)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_auth_data(main_resource: &WebResource, auth_result_tx: mpsc::UnboundedSender<AuthResult>) {
|
fn read_auth_data(main_resource: &WebResource, auth_result_tx: mpsc::UnboundedSender<AuthResult>) {
|
||||||
if main_resource.response().is_none() {
|
let Some(response) = main_resource.response() else {
|
||||||
info!("No response found in main resource");
|
info!("No response found in main resource");
|
||||||
send_auth_result(&auth_result_tx, Err(AuthDataError::Invalid));
|
send_auth_result(&auth_result_tx, Err(AuthDataError::Invalid));
|
||||||
return;
|
return;
|
||||||
}
|
};
|
||||||
|
|
||||||
let response = main_resource.response().unwrap();
|
|
||||||
info!("Trying to read auth data from response headers...");
|
info!("Trying to read auth data from response headers...");
|
||||||
|
|
||||||
match read_auth_data_from_headers(&response) {
|
match read_auth_data_from_headers(&response) {
|
||||||
@@ -407,22 +402,27 @@ fn read_auth_data(main_resource: &WebResource, auth_result_tx: mpsc::UnboundedSe
|
|||||||
read_auth_data_from_body(main_resource, move |auth_result| {
|
read_auth_data_from_body(main_resource, move |auth_result| {
|
||||||
// Since we have already found invalid auth data in headers, which means this could be the `/SAML20/SP/ACS` endpoint
|
// Since we have already found invalid auth data in headers, which means this could be the `/SAML20/SP/ACS` endpoint
|
||||||
// any error result from body should be considered as invalid, and trigger a retry
|
// any error result from body should be considered as invalid, and trigger a retry
|
||||||
let auth_result = auth_result.map_err(|_| AuthDataError::Invalid);
|
let auth_result = auth_result.map_err(|err| {
|
||||||
|
info!("Failed to read auth data from body: {}", err);
|
||||||
|
AuthDataError::Invalid
|
||||||
|
});
|
||||||
send_auth_result(&auth_result_tx, auth_result);
|
send_auth_result(&auth_result_tx, auth_result);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Err(AuthDataError::NotFound) => {
|
Err(AuthDataError::NotFound) => {
|
||||||
info!("No auth data found in headers, trying to read from body...");
|
info!("No auth data found in headers, trying to read from body...");
|
||||||
let url = main_resource.uri().unwrap_or("".into());
|
|
||||||
let is_acs_endpoint = url.contains("/SAML20/SP/ACS");
|
let is_acs_endpoint = main_resource.uri().map_or(false, |uri| uri.contains("/SAML20/SP/ACS"));
|
||||||
|
|
||||||
read_auth_data_from_body(main_resource, move |auth_result| {
|
read_auth_data_from_body(main_resource, move |auth_result| {
|
||||||
// If the endpoint is `/SAML20/SP/ACS` and no auth data found in body, it should be considered as invalid
|
// If the endpoint is `/SAML20/SP/ACS` and no auth data found in body, it should be considered as invalid
|
||||||
let auth_result = auth_result.map_err(|err| {
|
let auth_result = auth_result.map_err(|err| {
|
||||||
if matches!(err, AuthDataError::NotFound) && is_acs_endpoint {
|
info!("Failed to read auth data from body: {}", err);
|
||||||
AuthDataError::Invalid
|
|
||||||
|
if !is_acs_endpoint && matches!(err, AuthDataParseError::NotFound) {
|
||||||
|
AuthDataError::NotFound
|
||||||
} else {
|
} else {
|
||||||
err
|
AuthDataError::Invalid
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -437,13 +437,6 @@ fn read_auth_data(main_resource: &WebResource, auth_result_tx: mpsc::UnboundedSe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_xml_tag(html: &str, tag: &str) -> Option<String> {
|
|
||||||
let re = Regex::new(&format!("<{}>(.*)</{}>", tag, tag)).unwrap();
|
|
||||||
re.captures(html)
|
|
||||||
.and_then(|captures| captures.get(1))
|
|
||||||
.map(|m| m.as_str().to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn clear_webview_cookies(window: &Window) -> anyhow::Result<()> {
|
pub(crate) async fn clear_webview_cookies(window: &Window) -> anyhow::Result<()> {
|
||||||
let (tx, rx) = oneshot::channel::<Result<(), String>>();
|
let (tx, rx) = oneshot::channel::<Result<(), String>>();
|
||||||
|
|
||||||
@@ -489,3 +482,42 @@ pub(crate) async fn clear_webview_cookies(window: &Window) -> anyhow::Result<()>
|
|||||||
|
|
||||||
rx.await?.map_err(|err| anyhow::anyhow!(err))
|
rx.await?.map_err(|err| anyhow::anyhow!(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extract_gpcallback_some() {
|
||||||
|
let html = r#"
|
||||||
|
<meta http-equiv="refresh" content="0; URL=globalprotectcallback:PGh0bWw+PCEtLSA8c">
|
||||||
|
<meta http-equiv="refresh" content="0; URL=globalprotectcallback:PGh0bWw+PCEtLSA8c">
|
||||||
|
"#;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
extract_gpcallback(html).as_deref(),
|
||||||
|
Some("globalprotectcallback:PGh0bWw+PCEtLSA8c")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extract_gpcallback_cas() {
|
||||||
|
let html = r#"
|
||||||
|
<meta http-equiv="refresh" content="0; URL=globalprotectcallback:cas-as=1&un=xyz@email.com&token=very_long_string">
|
||||||
|
"#;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
extract_gpcallback(html).as_deref(),
|
||||||
|
Some("globalprotectcallback:cas-as=1&un=xyz@email.com&token=very_long_string")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extract_gpcallback_none() {
|
||||||
|
let html = r#"
|
||||||
|
<meta http-equiv="refresh" content="0; URL=PGh0bWw+PCEtLSA8c">
|
||||||
|
"#;
|
||||||
|
|
||||||
|
assert_eq!(extract_gpcallback(html), None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -3,6 +3,7 @@ use gpapi::{
|
|||||||
auth::{SamlAuthData, SamlAuthResult},
|
auth::{SamlAuthData, SamlAuthResult},
|
||||||
clap::args::Os,
|
clap::args::Os,
|
||||||
gp_params::{ClientOs, GpParams},
|
gp_params::{ClientOs, GpParams},
|
||||||
|
process::browser_authenticator::BrowserAuthenticator,
|
||||||
utils::{normalize_server, openssl},
|
utils::{normalize_server, openssl},
|
||||||
GP_USER_AGENT,
|
GP_USER_AGENT,
|
||||||
};
|
};
|
||||||
@@ -37,6 +38,8 @@ struct Cli {
|
|||||||
ignore_tls_errors: bool,
|
ignore_tls_errors: bool,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
clean: bool,
|
clean: bool,
|
||||||
|
#[arg(long)]
|
||||||
|
default_browser: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Cli {
|
impl Cli {
|
||||||
@@ -56,6 +59,15 @@ impl Cli {
|
|||||||
None => portal_prelogin(&self.server, &gp_params).await?,
|
None => portal_prelogin(&self.server, &gp_params).await?,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if self.default_browser {
|
||||||
|
let browser_auth = BrowserAuthenticator::new(&saml_request);
|
||||||
|
browser_auth.authenticate()?;
|
||||||
|
|
||||||
|
info!("Please continue the authentication process in the default browser");
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
self.saml_request.replace(saml_request);
|
self.saml_request.replace(saml_request);
|
||||||
|
|
||||||
let app = create_app(self.clone())?;
|
let app = create_app(self.clone())?;
|
||||||
|
@@ -22,8 +22,8 @@
|
|||||||
"all": true,
|
"all": true,
|
||||||
"request": true,
|
"request": true,
|
||||||
"scope": [
|
"scope": [
|
||||||
"http://**",
|
"http://*",
|
||||||
"https://**"
|
"https://*"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -6,6 +6,7 @@ edition.workspace = true
|
|||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
common = { path = "../../crates/common" }
|
||||||
gpapi = { path = "../../crates/gpapi", features = ["clap"] }
|
gpapi = { path = "../../crates/gpapi", features = ["clap"] }
|
||||||
openconnect = { path = "../../crates/openconnect" }
|
openconnect = { path = "../../crates/openconnect" }
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
|
@@ -19,7 +19,7 @@ pub(crate) struct SharedArgs {
|
|||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
enum CliCommand {
|
enum CliCommand {
|
||||||
#[command(about = "Connect to a portal server")]
|
#[command(about = "Connect to a portal server")]
|
||||||
Connect(ConnectArgs),
|
Connect(Box<ConnectArgs>),
|
||||||
#[command(about = "Disconnect from the server")]
|
#[command(about = "Disconnect from the server")]
|
||||||
Disconnect,
|
Disconnect,
|
||||||
#[command(about = "Launch the GUI")]
|
#[command(about = "Launch the GUI")]
|
||||||
|
@@ -1,24 +1,27 @@
|
|||||||
use std::{fs, sync::Arc};
|
use std::{cell::RefCell, fs, sync::Arc};
|
||||||
|
|
||||||
use clap::Args;
|
use clap::Args;
|
||||||
|
use common::vpn_utils::find_csd_wrapper;
|
||||||
use gpapi::{
|
use gpapi::{
|
||||||
clap::args::Os,
|
clap::args::Os,
|
||||||
credential::{Credential, PasswordCredential},
|
credential::{Credential, PasswordCredential},
|
||||||
gateway::gateway_login,
|
error::PortalError,
|
||||||
|
gateway::{gateway_login, GatewayLogin},
|
||||||
gp_params::{ClientOs, GpParams},
|
gp_params::{ClientOs, GpParams},
|
||||||
portal::{prelogin, retrieve_config, PortalError, Prelogin},
|
portal::{prelogin, retrieve_config, Prelogin},
|
||||||
process::{
|
process::{
|
||||||
auth_launcher::SamlAuthLauncher,
|
auth_launcher::SamlAuthLauncher,
|
||||||
users::{get_non_root_user, get_user_by_name},
|
users::{get_non_root_user, get_user_by_name},
|
||||||
},
|
},
|
||||||
utils::shutdown_signal,
|
utils::{request::RequestIdentityError, 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 tokio::{io::AsyncReadExt, net::TcpListener};
|
||||||
|
|
||||||
use crate::{cli::SharedArgs, GP_CLIENT_LOCK_FILE};
|
use crate::{cli::SharedArgs, GP_CLIENT_LOCK_FILE, GP_CLIENT_PORT_FILE};
|
||||||
|
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
pub(crate) struct ConnectArgs {
|
pub(crate) struct ConnectArgs {
|
||||||
@@ -30,6 +33,25 @@ pub(crate) struct ConnectArgs {
|
|||||||
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 = "Connect the server as a gateway, instead of a portal")]
|
||||||
|
as_gateway: bool,
|
||||||
|
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
help = "Use the default CSD wrapper to generate the HIP report and send it to the server"
|
||||||
|
)]
|
||||||
|
hip: bool,
|
||||||
|
|
||||||
|
#[arg(
|
||||||
|
short,
|
||||||
|
long,
|
||||||
|
help = "Use SSL client certificate file in pkcs#8 (.pem) or pkcs#12 (.p12, .pfx) format"
|
||||||
|
)]
|
||||||
|
certificate: Option<String>,
|
||||||
|
#[arg(short = 'k', long, help = "Use SSL private key file in pkcs#8 (.pem) format")]
|
||||||
|
sslkey: Option<String>,
|
||||||
|
#[arg(short = 'p', long, help = "The key passphrase of the private key")]
|
||||||
|
key_password: Option<String>,
|
||||||
|
|
||||||
#[arg(long, help = "Same as the '--csd-user' option in the openconnect command")]
|
#[arg(long, help = "Same as the '--csd-user' option in the openconnect command")]
|
||||||
csd_user: Option<String>,
|
csd_user: Option<String>,
|
||||||
@@ -37,8 +59,12 @@ pub(crate) struct ConnectArgs {
|
|||||||
#[arg(long, help = "Same as the '--csd-wrapper' option in the openconnect command")]
|
#[arg(long, help = "Same as the '--csd-wrapper' option in the openconnect command")]
|
||||||
csd_wrapper: Option<String>,
|
csd_wrapper: Option<String>,
|
||||||
|
|
||||||
|
#[arg(long, default_value = "300", help = "Reconnection retry timeout in seconds")]
|
||||||
|
reconnect_timeout: u32,
|
||||||
#[arg(short, long, help = "Request MTU from server (legacy servers only)")]
|
#[arg(short, long, help = "Request MTU from server (legacy servers only)")]
|
||||||
mtu: Option<u32>,
|
mtu: Option<u32>,
|
||||||
|
#[arg(long, help = "Do not ask for IPv6 connectivity")]
|
||||||
|
disable_ipv6: bool,
|
||||||
|
|
||||||
#[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,
|
||||||
@@ -50,6 +76,8 @@ pub(crate) struct ConnectArgs {
|
|||||||
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,
|
||||||
|
#[arg(long, help = "Use the default browser to authenticate")]
|
||||||
|
default_browser: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ConnectArgs {
|
impl ConnectArgs {
|
||||||
@@ -69,11 +97,16 @@ impl ConnectArgs {
|
|||||||
pub(crate) struct ConnectHandler<'a> {
|
pub(crate) struct ConnectHandler<'a> {
|
||||||
args: &'a ConnectArgs,
|
args: &'a ConnectArgs,
|
||||||
shared_args: &'a SharedArgs,
|
shared_args: &'a SharedArgs,
|
||||||
|
latest_key_password: RefCell<Option<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> ConnectHandler<'a> {
|
impl<'a> ConnectHandler<'a> {
|
||||||
pub(crate) fn new(args: &'a ConnectArgs, shared_args: &'a SharedArgs) -> Self {
|
pub(crate) fn new(args: &'a ConnectArgs, shared_args: &'a SharedArgs) -> Self {
|
||||||
Self { args, shared_args }
|
Self {
|
||||||
|
args,
|
||||||
|
shared_args,
|
||||||
|
latest_key_password: Default::default(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_gp_params(&self) -> GpParams {
|
fn build_gp_params(&self) -> GpParams {
|
||||||
@@ -82,11 +115,52 @@ impl<'a> ConnectHandler<'a> {
|
|||||||
.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)
|
||||||
|
.certificate(self.args.certificate.clone())
|
||||||
|
.sslkey(self.args.sslkey.clone())
|
||||||
|
.key_password(self.latest_key_password.borrow().clone())
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn handle(&self) -> anyhow::Result<()> {
|
pub(crate) async fn handle(&self) -> anyhow::Result<()> {
|
||||||
|
self.latest_key_password.replace(self.args.key_password.clone());
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let Err(err) = self.handle_impl().await else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(root_cause) = err.root_cause().downcast_ref::<RequestIdentityError>() else {
|
||||||
|
return Err(err);
|
||||||
|
};
|
||||||
|
|
||||||
|
match root_cause {
|
||||||
|
RequestIdentityError::NoKey => {
|
||||||
|
eprintln!("ERROR: No private key found in the certificate file");
|
||||||
|
eprintln!("ERROR: Please provide the private key file using the `-k` option");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
RequestIdentityError::NoPassphrase(cert_type) | RequestIdentityError::DecryptError(cert_type) => {
|
||||||
|
// Decrypt the private key error, ask for the key password
|
||||||
|
let message = format!("Enter the {} passphrase:", cert_type);
|
||||||
|
let password = Password::new(&message)
|
||||||
|
.without_confirmation()
|
||||||
|
.with_display_mode(PasswordDisplayMode::Masked)
|
||||||
|
.prompt()?;
|
||||||
|
|
||||||
|
self.latest_key_password.replace(Some(password));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn handle_impl(&self) -> anyhow::Result<()> {
|
||||||
let server = self.args.server.as_str();
|
let server = self.args.server.as_str();
|
||||||
|
let as_gateway = self.args.as_gateway;
|
||||||
|
|
||||||
|
if as_gateway {
|
||||||
|
info!("Treating the server as a gateway");
|
||||||
|
return self.connect_gateway_with_prelogin(server).await;
|
||||||
|
}
|
||||||
|
|
||||||
let Err(err) = self.connect_portal_with_prelogin(server).await else {
|
let Err(err) = self.connect_portal_with_prelogin(server).await else {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
@@ -95,10 +169,15 @@ impl<'a> ConnectHandler<'a> {
|
|||||||
info!("Failed to connect portal with prelogin: {}", err);
|
info!("Failed to connect portal with prelogin: {}", err);
|
||||||
if err.root_cause().downcast_ref::<PortalError>().is_some() {
|
if err.root_cause().downcast_ref::<PortalError>().is_some() {
|
||||||
info!("Trying the gateway authentication workflow...");
|
info!("Trying the gateway authentication workflow...");
|
||||||
return self.connect_gateway_with_prelogin(server).await;
|
self.connect_gateway_with_prelogin(server).await?;
|
||||||
}
|
|
||||||
|
|
||||||
Err(err)
|
eprintln!("\nNOTE: the server may be a gateway, not a portal.");
|
||||||
|
eprintln!("NOTE: try to use the `--as-gateway` option if you were authenticated twice.");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn connect_portal_with_prelogin(&self, portal: &str) -> anyhow::Result<()> {
|
async fn connect_portal_with_prelogin(&self, portal: &str) -> anyhow::Result<()> {
|
||||||
@@ -112,16 +191,19 @@ impl<'a> ConnectHandler<'a> {
|
|||||||
let selected_gateway = match &self.args.gateway {
|
let selected_gateway = match &self.args.gateway {
|
||||||
Some(gateway) => portal_config
|
Some(gateway) => portal_config
|
||||||
.find_gateway(gateway)
|
.find_gateway(gateway)
|
||||||
.ok_or_else(|| anyhow::anyhow!("Cannot find gateway {}", gateway))?,
|
.ok_or_else(|| anyhow::anyhow!("Cannot find gateway specified: {}", gateway))?,
|
||||||
None => {
|
None => {
|
||||||
portal_config.sort_gateways(prelogin.region());
|
portal_config.sort_gateways(prelogin.region());
|
||||||
let gateways = portal_config.gateways();
|
let gateways = portal_config.gateways();
|
||||||
|
|
||||||
if gateways.len() > 1 {
|
if gateways.len() > 1 {
|
||||||
Select::new("Which gateway do you want to connect to?", gateways)
|
let gateway = Select::new("Which gateway do you want to connect to?", gateways)
|
||||||
.with_vim_mode(true)
|
.with_vim_mode(true)
|
||||||
.prompt()?
|
.prompt()?;
|
||||||
|
info!("Connecting to the selected gateway: {}", gateway);
|
||||||
|
gateway
|
||||||
} else {
|
} else {
|
||||||
|
info!("Connecting to the only available gateway: {}", gateways[0]);
|
||||||
gateways[0]
|
gateways[0]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -130,7 +212,7 @@ 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 cookie = match gateway_login(gateway, &cred, &gp_params).await {
|
let cookie = match self.login_gateway(gateway, &cred, &gp_params).await {
|
||||||
Ok(cookie) => cookie,
|
Ok(cookie) => cookie,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
info!("Gateway login failed: {}", err);
|
info!("Gateway login failed: {}", err);
|
||||||
@@ -142,28 +224,59 @@ impl<'a> ConnectHandler<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn connect_gateway_with_prelogin(&self, gateway: &str) -> anyhow::Result<()> {
|
async fn connect_gateway_with_prelogin(&self, gateway: &str) -> anyhow::Result<()> {
|
||||||
|
info!("Performing the gateway authentication...");
|
||||||
|
|
||||||
let mut gp_params = self.build_gp_params();
|
let mut gp_params = self.build_gp_params();
|
||||||
gp_params.set_is_gateway(true);
|
gp_params.set_is_gateway(true);
|
||||||
|
|
||||||
let prelogin = prelogin(gateway, &gp_params).await?;
|
let prelogin = prelogin(gateway, &gp_params).await?;
|
||||||
let cred = self.obtain_credential(&prelogin, gateway).await?;
|
let cred = self.obtain_credential(&prelogin, gateway).await?;
|
||||||
|
|
||||||
let cookie = gateway_login(gateway, &cred, &gp_params).await?;
|
let cookie = self.login_gateway(gateway, &cred, &gp_params).await?;
|
||||||
|
|
||||||
self.connect_gateway(gateway, &cookie).await
|
self.connect_gateway(gateway, &cookie).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn login_gateway(&self, gateway: &str, cred: &Credential, gp_params: &GpParams) -> anyhow::Result<String> {
|
||||||
|
let mut gp_params = gp_params.clone();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match gateway_login(gateway, cred, &gp_params).await? {
|
||||||
|
GatewayLogin::Cookie(cookie) => return Ok(cookie),
|
||||||
|
GatewayLogin::Mfa(message, input_str) => {
|
||||||
|
let otp = Text::new(&message).prompt()?;
|
||||||
|
gp_params.set_input_str(&input_str);
|
||||||
|
gp_params.set_otp(&otp);
|
||||||
|
|
||||||
|
info!("Retrying gateway login with MFA...");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn connect_gateway(&self, gateway: &str, cookie: &str) -> anyhow::Result<()> {
|
async fn connect_gateway(&self, gateway: &str, cookie: &str) -> anyhow::Result<()> {
|
||||||
let csd_uid = get_csd_uid(&self.args.csd_user)?;
|
|
||||||
let mtu = self.args.mtu.unwrap_or(0);
|
let mtu = self.args.mtu.unwrap_or(0);
|
||||||
|
let csd_uid = get_csd_uid(&self.args.csd_user)?;
|
||||||
|
let csd_wrapper = if self.args.csd_wrapper.is_some() {
|
||||||
|
self.args.csd_wrapper.clone()
|
||||||
|
} else if self.args.hip {
|
||||||
|
find_csd_wrapper()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
let vpn = Vpn::builder(gateway, cookie)
|
let vpn = Vpn::builder(gateway, cookie)
|
||||||
.user_agent(self.args.user_agent.clone())
|
|
||||||
.script(self.args.script.clone())
|
.script(self.args.script.clone())
|
||||||
|
.user_agent(self.args.user_agent.clone())
|
||||||
|
.certificate(self.args.certificate.clone())
|
||||||
|
.sslkey(self.args.sslkey.clone())
|
||||||
|
.key_password(self.latest_key_password.borrow().clone())
|
||||||
.csd_uid(csd_uid)
|
.csd_uid(csd_uid)
|
||||||
.csd_wrapper(self.args.csd_wrapper.clone())
|
.csd_wrapper(csd_wrapper)
|
||||||
|
.reconnect_timeout(self.args.reconnect_timeout)
|
||||||
.mtu(mtu)
|
.mtu(mtu)
|
||||||
.build();
|
.disable_ipv6(self.args.disable_ipv6)
|
||||||
|
.build()?;
|
||||||
|
|
||||||
let vpn = Arc::new(vpn);
|
let vpn = Arc::new(vpn);
|
||||||
let vpn_clone = vpn.clone();
|
let vpn_clone = vpn.clone();
|
||||||
@@ -190,7 +303,9 @@ impl<'a> ConnectHandler<'a> {
|
|||||||
|
|
||||||
match prelogin {
|
match prelogin {
|
||||||
Prelogin::Saml(prelogin) => {
|
Prelogin::Saml(prelogin) => {
|
||||||
SamlAuthLauncher::new(&self.args.server)
|
let use_default_browser = prelogin.support_default_browser() && self.args.default_browser;
|
||||||
|
|
||||||
|
let cred = SamlAuthLauncher::new(&self.args.server)
|
||||||
.gateway(is_gateway)
|
.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)
|
||||||
@@ -200,8 +315,21 @@ impl<'a> ConnectHandler<'a> {
|
|||||||
.fix_openssl(self.shared_args.fix_openssl)
|
.fix_openssl(self.shared_args.fix_openssl)
|
||||||
.ignore_tls_errors(self.shared_args.ignore_tls_errors)
|
.ignore_tls_errors(self.shared_args.ignore_tls_errors)
|
||||||
.clean(self.args.clean)
|
.clean(self.args.clean)
|
||||||
|
.default_browser(use_default_browser)
|
||||||
.launch()
|
.launch()
|
||||||
.await
|
.await?;
|
||||||
|
|
||||||
|
if let Some(cred) = cred {
|
||||||
|
return Ok(cred);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !use_default_browser {
|
||||||
|
// This should never happen
|
||||||
|
unreachable!("SAML authentication failed without using the default browser");
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Waiting for the browser authentication to complete...");
|
||||||
|
wait_credentials().await
|
||||||
}
|
}
|
||||||
Prelogin::Standard(prelogin) => {
|
Prelogin::Standard(prelogin) => {
|
||||||
let prefix = if is_gateway { "Gateway" } else { "Portal" };
|
let prefix = if is_gateway { "Gateway" } else { "Portal" };
|
||||||
@@ -224,6 +352,27 @@ impl<'a> ConnectHandler<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn wait_credentials() -> anyhow::Result<Credential> {
|
||||||
|
// Start a local server to receive the browser authentication data
|
||||||
|
let listener = TcpListener::bind("127.0.0.1:0").await?;
|
||||||
|
let port = listener.local_addr()?.port();
|
||||||
|
|
||||||
|
// Write the port to a file
|
||||||
|
fs::write(GP_CLIENT_PORT_FILE, port.to_string())?;
|
||||||
|
|
||||||
|
info!("Listening authentication data on port {}", port);
|
||||||
|
let (mut socket, _) = listener.accept().await?;
|
||||||
|
|
||||||
|
info!("Received the browser authentication data from the socket");
|
||||||
|
let mut data = String::new();
|
||||||
|
socket.read_to_string(&mut data).await?;
|
||||||
|
|
||||||
|
// Remove the port file
|
||||||
|
fs::remove_file(GP_CLIENT_PORT_FILE)?;
|
||||||
|
|
||||||
|
Credential::from_gpcallback(&data)
|
||||||
|
}
|
||||||
|
|
||||||
fn write_pid_file() {
|
fn write_pid_file() {
|
||||||
let pid = std::process::id();
|
let pid = std::process::id();
|
||||||
|
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
use std::{collections::HashMap, fs, path::PathBuf};
|
use std::{collections::HashMap, env::temp_dir, fs, path::PathBuf};
|
||||||
|
|
||||||
use clap::Args;
|
use clap::Args;
|
||||||
use directories::ProjectDirs;
|
use directories::ProjectDirs;
|
||||||
@@ -7,6 +7,9 @@ use gpapi::{
|
|||||||
utils::{endpoint::http_endpoint, env_file, shutdown_signal},
|
utils::{endpoint::http_endpoint, env_file, shutdown_signal},
|
||||||
};
|
};
|
||||||
use log::info;
|
use log::info;
|
||||||
|
use tokio::io::AsyncWriteExt;
|
||||||
|
|
||||||
|
use crate::GP_CLIENT_PORT_FILE;
|
||||||
|
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
pub(crate) struct LaunchGuiArgs {
|
pub(crate) struct LaunchGuiArgs {
|
||||||
@@ -78,11 +81,21 @@ impl<'a> LaunchGuiHandler<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn feed_auth_data(auth_data: &str) -> anyhow::Result<()> {
|
async fn feed_auth_data(auth_data: &str) -> anyhow::Result<()> {
|
||||||
|
let _ = tokio::join!(feed_auth_data_gui(auth_data), feed_auth_data_cli(auth_data));
|
||||||
|
|
||||||
|
// Cleanup the temporary file
|
||||||
|
let html_file = temp_dir().join("gpauth.html");
|
||||||
|
let _ = std::fs::remove_file(html_file);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn feed_auth_data_gui(auth_data: &str) -> anyhow::Result<()> {
|
||||||
let service_endpoint = http_endpoint().await?;
|
let service_endpoint = http_endpoint().await?;
|
||||||
|
|
||||||
reqwest::Client::default()
|
reqwest::Client::default()
|
||||||
.post(format!("{}/auth-data", service_endpoint))
|
.post(format!("{}/auth-data", service_endpoint))
|
||||||
.json(&auth_data)
|
.body(auth_data.to_string())
|
||||||
.send()
|
.send()
|
||||||
.await?
|
.await?
|
||||||
.error_for_status()?;
|
.error_for_status()?;
|
||||||
@@ -90,6 +103,15 @@ async fn feed_auth_data(auth_data: &str) -> anyhow::Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn feed_auth_data_cli(auth_data: &str) -> anyhow::Result<()> {
|
||||||
|
let port = tokio::fs::read_to_string(GP_CLIENT_PORT_FILE).await?;
|
||||||
|
let mut stream = tokio::net::TcpStream::connect(format!("127.0.0.1:{}", port.trim())).await?;
|
||||||
|
|
||||||
|
stream.write_all(auth_data.as_bytes()).await?;
|
||||||
|
|
||||||
|
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?;
|
||||||
|
|
||||||
|
@@ -4,6 +4,7 @@ mod disconnect;
|
|||||||
mod launch_gui;
|
mod launch_gui;
|
||||||
|
|
||||||
pub(crate) const GP_CLIENT_LOCK_FILE: &str = "/var/run/gpclient.lock";
|
pub(crate) const GP_CLIENT_LOCK_FILE: &str = "/var/run/gpclient.lock";
|
||||||
|
pub(crate) const GP_CLIENT_PORT_FILE: &str = "/var/run/gpclient.port";
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
|
@@ -18,7 +18,7 @@
|
|||||||
"react-dom": "^18.2.0"
|
"react-dom": "^18.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/cli": "^1.5.0",
|
"@tauri-apps/cli": "^1.5.6",
|
||||||
"@types/node": "^20.8.10",
|
"@types/node": "^20.8.10",
|
||||||
"@types/react": "^18.2.15",
|
"@types/react": "^18.2.15",
|
||||||
"@types/react-dom": "^18.2.7",
|
"@types/react-dom": "^18.2.7",
|
||||||
@@ -31,6 +31,6 @@
|
|||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
"prettier": "3.1.0",
|
"prettier": "3.1.0",
|
||||||
"typescript": "^5.0.2",
|
"typescript": "^5.0.2",
|
||||||
"vite": "^4.4.4"
|
"vite": "^4.5.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
82
apps/gpgui-helper/pnpm-lock.yaml
generated
82
apps/gpgui-helper/pnpm-lock.yaml
generated
@@ -29,8 +29,8 @@ dependencies:
|
|||||||
|
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@tauri-apps/cli':
|
'@tauri-apps/cli':
|
||||||
specifier: ^1.5.0
|
specifier: ^1.5.6
|
||||||
version: 1.5.0
|
version: 1.5.6
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^20.8.10
|
specifier: ^20.8.10
|
||||||
version: 20.8.10
|
version: 20.8.10
|
||||||
@@ -48,7 +48,7 @@ devDependencies:
|
|||||||
version: 6.12.0(eslint@8.54.0)(typescript@5.0.2)
|
version: 6.12.0(eslint@8.54.0)(typescript@5.0.2)
|
||||||
'@vitejs/plugin-react':
|
'@vitejs/plugin-react':
|
||||||
specifier: ^4.0.3
|
specifier: ^4.0.3
|
||||||
version: 4.0.3(vite@4.4.4)
|
version: 4.0.3(vite@4.5.3)
|
||||||
eslint:
|
eslint:
|
||||||
specifier: ^8.54.0
|
specifier: ^8.54.0
|
||||||
version: 8.54.0
|
version: 8.54.0
|
||||||
@@ -68,8 +68,8 @@ devDependencies:
|
|||||||
specifier: ^5.0.2
|
specifier: ^5.0.2
|
||||||
version: 5.0.2
|
version: 5.0.2
|
||||||
vite:
|
vite:
|
||||||
specifier: ^4.4.4
|
specifier: ^4.5.3
|
||||||
version: 4.4.4(@types/node@20.8.10)
|
version: 4.5.3(@types/node@20.8.10)
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
@@ -940,8 +940,8 @@ packages:
|
|||||||
engines: {node: '>= 14.6.0', npm: '>= 6.6.0', yarn: '>= 1.19.1'}
|
engines: {node: '>= 14.6.0', npm: '>= 6.6.0', yarn: '>= 1.19.1'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@tauri-apps/cli-darwin-arm64@1.5.0:
|
/@tauri-apps/cli-darwin-arm64@1.5.6:
|
||||||
resolution: {integrity: sha512-wvEfcSBjlh1G8uBiylMNFgBtAyk4mXfvDFcGyigf/2ui7Wve6fcAFDJdTVwiHOZ4Wxnw6BD3lIkVMQdDpE+nFg==}
|
resolution: {integrity: sha512-NNvG3XLtciCMsBahbDNUEvq184VZmOveTGOuy0So2R33b/6FDkuWaSgWZsR1mISpOuP034htQYW0VITCLelfqg==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
@@ -949,8 +949,8 @@ packages:
|
|||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/@tauri-apps/cli-darwin-x64@1.5.0:
|
/@tauri-apps/cli-darwin-x64@1.5.6:
|
||||||
resolution: {integrity: sha512-pWzLMAMslx3dkLyq1f4PpuEhgUs8mPISM5nQoHfgYYchJk6Ip1YtWupo56h5QjqyRNPUsSYT8DVGIw0Oi8auXQ==}
|
resolution: {integrity: sha512-nkiqmtUQw3N1j4WoVjv81q6zWuZFhBLya/RNGUL94oafORloOZoSY0uTZJAoeieb3Y1YK0rCHSDl02MyV2Fi4A==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
@@ -958,8 +958,8 @@ packages:
|
|||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/@tauri-apps/cli-linux-arm-gnueabihf@1.5.0:
|
/@tauri-apps/cli-linux-arm-gnueabihf@1.5.6:
|
||||||
resolution: {integrity: sha512-hP3nAd0TjxblIAgriXhdX33sKXwbkY3CKsWBVB3O+5DOGy7XzKosIt0KaqiV8BHI2pbHMVOwTZuvjdK8pPLULQ==}
|
resolution: {integrity: sha512-z6SPx+axZexmWXTIVPNs4Tg7FtvdJl9EKxYN6JPjOmDZcqA13iyqWBQal2DA/GMZ1Xqo3vyJf6EoEaKaliymPQ==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
@@ -967,8 +967,8 @@ packages:
|
|||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/@tauri-apps/cli-linux-arm64-gnu@1.5.0:
|
/@tauri-apps/cli-linux-arm64-gnu@1.5.6:
|
||||||
resolution: {integrity: sha512-/AwGSjUplzeiGWbKP8rAW4gQx5umWiaQJ0ifx6NakA/sIrhRXPYTobwzg4ixw31ALQNXaEosJCkmvXyHUvoUYw==}
|
resolution: {integrity: sha512-QuQjMQmpsCbzBrmtQiG4uhnfAbdFx3nzm+9LtqjuZlurc12+Mj5MTgqQ3AOwQedH3f7C+KlvbqD2AdXpwTg7VA==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
@@ -976,8 +976,8 @@ packages:
|
|||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/@tauri-apps/cli-linux-arm64-musl@1.5.0:
|
/@tauri-apps/cli-linux-arm64-musl@1.5.6:
|
||||||
resolution: {integrity: sha512-OFzKMkg3bmBjr/BYQ1kx4QOHL+JSkzX+Cw8RcG7CKnq8QoJyg8N0K0UTskgsVwlCN4l7bxeuSLvEveg4SBA2AQ==}
|
resolution: {integrity: sha512-8j5dH3odweFeom7bRGlfzDApWVOT4jIq8/214Wl+JeiNVehouIBo9lZGeghZBH3XKFRwEvU23i7sRVjuh2s8mg==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
@@ -985,8 +985,8 @@ packages:
|
|||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/@tauri-apps/cli-linux-x64-gnu@1.5.0:
|
/@tauri-apps/cli-linux-x64-gnu@1.5.6:
|
||||||
resolution: {integrity: sha512-V2stfUH3Qrc3cIXAd+cKbJruS1oJqqGd40GTVcKOKlRk9Ef9H3WNUQ5PyWKj1t1rk8AxjcBO/vK+Unkuy1WSCw==}
|
resolution: {integrity: sha512-gbFHYHfdEGW0ffk8SigDsoXks6USpilF6wR0nqB/JbWzbzFR/sBuLVNQlJl1RKNakyJHu+lsFxGy0fcTdoX8xA==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
@@ -994,8 +994,8 @@ packages:
|
|||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/@tauri-apps/cli-linux-x64-musl@1.5.0:
|
/@tauri-apps/cli-linux-x64-musl@1.5.6:
|
||||||
resolution: {integrity: sha512-qpcGeesuksxzE7lC3RCnikTY9DCRMnAYwhWa9i8MA7pKDX1IXaEvAaXrse44XCZUohxLLgn2k2o6Pb+65dDijQ==}
|
resolution: {integrity: sha512-9v688ogoLkeFYQNgqiSErfhTreLUd8B3prIBSYUt+x4+5Kcw91zWvIh+VSxL1n3KCGGsM7cuXhkGPaxwlEh1ug==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
@@ -1003,8 +1003,8 @@ packages:
|
|||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/@tauri-apps/cli-win32-arm64-msvc@1.5.0:
|
/@tauri-apps/cli-win32-arm64-msvc@1.5.6:
|
||||||
resolution: {integrity: sha512-Glt/AEWwbFmFnQuoPRbB6vMzCIT9jYSpD59zRP8ljhRSzwDHE59q5nXOrheLKICwMmaXqtAuCIq9ndDBKghwoQ==}
|
resolution: {integrity: sha512-DRNDXFNZb6y5IZrw+lhTTA9l4wbzO4TNRBAlHAiXUrH+pRFZ/ZJtv5WEuAj9ocVSahVw2NaK5Yaold4NPAxHog==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
@@ -1012,8 +1012,8 @@ packages:
|
|||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/@tauri-apps/cli-win32-ia32-msvc@1.5.0:
|
/@tauri-apps/cli-win32-ia32-msvc@1.5.6:
|
||||||
resolution: {integrity: sha512-k+R2VI8eZJfRjaRS8LwbkjMBKFaKcWtA/byaFnGDDUnb3VM/WFW++3KjC5Ne2wXpxFW9RVaFiBNIpmRcExI0Qw==}
|
resolution: {integrity: sha512-oUYKNR/IZjF4fsOzRpw0xesl2lOjhsQEyWlgbpT25T83EU113Xgck9UjtI7xemNI/OPCv1tPiaM1e7/ABdg5iA==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [ia32]
|
cpu: [ia32]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
@@ -1021,8 +1021,8 @@ packages:
|
|||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/@tauri-apps/cli-win32-x64-msvc@1.5.0:
|
/@tauri-apps/cli-win32-x64-msvc@1.5.6:
|
||||||
resolution: {integrity: sha512-jEwq9UuUldVyDJ/dsYoHFuZfNZIkxbeDYSMELfZXsRvWWEA8xRYeTkH38S++KU1eBl5dTK0LbxhztEB2HjRT1g==}
|
resolution: {integrity: sha512-RmEf1os9C8//uq2hbjXi7Vgz9ne7798ZxqemAZdUwo1pv3oLVZSz1/IvZmUHPdy2e6zSeySqWu1D0Y3QRNN+dg==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
@@ -1030,21 +1030,21 @@ packages:
|
|||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/@tauri-apps/cli@1.5.0:
|
/@tauri-apps/cli@1.5.6:
|
||||||
resolution: {integrity: sha512-usY7Ncfkxl2f/ufjWDtp+eJsodlj8ipMKExIt160KR+tx0GtQgLtxRnrKxe1o7wu18Pkqd5JIuWMaOmT3YZeYA==}
|
resolution: {integrity: sha512-k4Y19oVCnt7WZb2TnDzLqfs7o98Jq0tUoVMv+JQSzuRDJqaVu2xMBZ8dYplEn+EccdR5SOMyzaLBJWu38TVK1A==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@tauri-apps/cli-darwin-arm64': 1.5.0
|
'@tauri-apps/cli-darwin-arm64': 1.5.6
|
||||||
'@tauri-apps/cli-darwin-x64': 1.5.0
|
'@tauri-apps/cli-darwin-x64': 1.5.6
|
||||||
'@tauri-apps/cli-linux-arm-gnueabihf': 1.5.0
|
'@tauri-apps/cli-linux-arm-gnueabihf': 1.5.6
|
||||||
'@tauri-apps/cli-linux-arm64-gnu': 1.5.0
|
'@tauri-apps/cli-linux-arm64-gnu': 1.5.6
|
||||||
'@tauri-apps/cli-linux-arm64-musl': 1.5.0
|
'@tauri-apps/cli-linux-arm64-musl': 1.5.6
|
||||||
'@tauri-apps/cli-linux-x64-gnu': 1.5.0
|
'@tauri-apps/cli-linux-x64-gnu': 1.5.6
|
||||||
'@tauri-apps/cli-linux-x64-musl': 1.5.0
|
'@tauri-apps/cli-linux-x64-musl': 1.5.6
|
||||||
'@tauri-apps/cli-win32-arm64-msvc': 1.5.0
|
'@tauri-apps/cli-win32-arm64-msvc': 1.5.6
|
||||||
'@tauri-apps/cli-win32-ia32-msvc': 1.5.0
|
'@tauri-apps/cli-win32-ia32-msvc': 1.5.6
|
||||||
'@tauri-apps/cli-win32-x64-msvc': 1.5.0
|
'@tauri-apps/cli-win32-x64-msvc': 1.5.6
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@types/json-schema@7.0.14:
|
/@types/json-schema@7.0.14:
|
||||||
@@ -1229,7 +1229,7 @@ packages:
|
|||||||
resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==}
|
resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@vitejs/plugin-react@4.0.3(vite@4.4.4):
|
/@vitejs/plugin-react@4.0.3(vite@4.5.3):
|
||||||
resolution: {integrity: sha512-pwXDog5nwwvSIzwrvYYmA2Ljcd/ZNlcsSG2Q9CNDBwnsd55UGAyr2doXtB5j+2uymRCnCfExlznzzSFbBRcoCg==}
|
resolution: {integrity: sha512-pwXDog5nwwvSIzwrvYYmA2Ljcd/ZNlcsSG2Q9CNDBwnsd55UGAyr2doXtB5j+2uymRCnCfExlznzzSFbBRcoCg==}
|
||||||
engines: {node: ^14.18.0 || >=16.0.0}
|
engines: {node: ^14.18.0 || >=16.0.0}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -1239,7 +1239,7 @@ packages:
|
|||||||
'@babel/plugin-transform-react-jsx-self': 7.22.5(@babel/core@7.23.2)
|
'@babel/plugin-transform-react-jsx-self': 7.22.5(@babel/core@7.23.2)
|
||||||
'@babel/plugin-transform-react-jsx-source': 7.22.5(@babel/core@7.23.2)
|
'@babel/plugin-transform-react-jsx-source': 7.22.5(@babel/core@7.23.2)
|
||||||
react-refresh: 0.14.0
|
react-refresh: 0.14.0
|
||||||
vite: 4.4.4(@types/node@20.8.10)
|
vite: 4.5.3(@types/node@20.8.10)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
dev: true
|
dev: true
|
||||||
@@ -2979,8 +2979,8 @@ packages:
|
|||||||
punycode: 2.3.1
|
punycode: 2.3.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/vite@4.4.4(@types/node@20.8.10):
|
/vite@4.5.3(@types/node@20.8.10):
|
||||||
resolution: {integrity: sha512-4mvsTxjkveWrKDJI70QmelfVqTm+ihFAb6+xf4sjEU2TmUCTlVX87tmg/QooPEMQb/lM9qGHT99ebqPziEd3wg==}
|
resolution: {integrity: sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==}
|
||||||
engines: {node: ^14.18.0 || >=16.0.0}
|
engines: {node: ^14.18.0 || >=16.0.0}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
@@ -9,6 +9,12 @@ use tauri::{Manager, Window};
|
|||||||
|
|
||||||
use crate::downloader::{ChecksumFetcher, FileDownloader};
|
use crate::downloader::{ChecksumFetcher, FileDownloader};
|
||||||
|
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
const SNAPSHOT: &str = match option_env!("SNAPSHOT") {
|
||||||
|
Some(val) => val,
|
||||||
|
None => "false"
|
||||||
|
};
|
||||||
|
|
||||||
pub struct ProgressNotifier {
|
pub struct ProgressNotifier {
|
||||||
win: Window,
|
win: Window,
|
||||||
}
|
}
|
||||||
@@ -81,9 +87,13 @@ impl GuiUpdater {
|
|||||||
info!("Update GUI, version: {}", self.version);
|
info!("Update GUI, version: {}", self.version);
|
||||||
|
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
let release_tag = "latest";
|
let release_tag = "snapshot";
|
||||||
#[cfg(not(debug_assertions))]
|
#[cfg(not(debug_assertions))]
|
||||||
let release_tag = format!("v{}", self.version);
|
let release_tag = if SNAPSHOT == "true" {
|
||||||
|
String::from("snapshot")
|
||||||
|
} else {
|
||||||
|
format!("v{}", self.version)
|
||||||
|
};
|
||||||
|
|
||||||
#[cfg(target_arch = "x86_64")]
|
#[cfg(target_arch = "x86_64")]
|
||||||
let arch = "x86_64";
|
let arch = "x86_64";
|
||||||
@@ -91,8 +101,8 @@ impl GuiUpdater {
|
|||||||
let arch = "aarch64";
|
let arch = "aarch64";
|
||||||
|
|
||||||
let file_url = format!(
|
let file_url = format!(
|
||||||
"https://github.com/yuezk/GlobalProtect-openconnect/releases/download/{}/gpgui_{}_{}.bin.tar.xz",
|
"https://github.com/yuezk/GlobalProtect-openconnect/releases/download/{}/gpgui_{}.bin.tar.xz",
|
||||||
release_tag, self.version, arch
|
release_tag, arch
|
||||||
);
|
);
|
||||||
let checksum_url = format!("{}.sha256", file_url);
|
let checksum_url = format!("{}.sha256", file_url);
|
||||||
|
|
||||||
|
@@ -4,7 +4,7 @@ use gpapi::service::{
|
|||||||
request::{ConnectRequest, WsRequest},
|
request::{ConnectRequest, WsRequest},
|
||||||
vpn_state::VpnState,
|
vpn_state::VpnState,
|
||||||
};
|
};
|
||||||
use log::info;
|
use log::{info, warn};
|
||||||
use openconnect::Vpn;
|
use openconnect::Vpn;
|
||||||
use tokio::sync::{mpsc, oneshot, watch, RwLock};
|
use tokio::sync::{mpsc, oneshot, watch, RwLock};
|
||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
@@ -31,22 +31,34 @@ impl VpnTaskContext {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let vpn_state_tx = self.vpn_state_tx.clone();
|
||||||
let info = req.info().clone();
|
let info = req.info().clone();
|
||||||
let vpn_handle = Arc::clone(&self.vpn_handle);
|
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 = match Vpn::builder(req.gateway().server(), args.cookie())
|
||||||
.user_agent(args.user_agent())
|
|
||||||
.script(args.vpnc_script())
|
.script(args.vpnc_script())
|
||||||
|
.user_agent(args.user_agent())
|
||||||
|
.os(args.openconnect_os())
|
||||||
|
.certificate(args.certificate())
|
||||||
|
.sslkey(args.sslkey())
|
||||||
|
.key_password(args.key_password())
|
||||||
.csd_uid(args.csd_uid())
|
.csd_uid(args.csd_uid())
|
||||||
.csd_wrapper(args.csd_wrapper())
|
.csd_wrapper(args.csd_wrapper())
|
||||||
|
.reconnect_timeout(args.reconnect_timeout())
|
||||||
.mtu(args.mtu())
|
.mtu(args.mtu())
|
||||||
.os(args.openconnect_os())
|
.disable_ipv6(args.disable_ipv6())
|
||||||
.build();
|
.build()
|
||||||
|
{
|
||||||
|
Ok(vpn) => vpn,
|
||||||
|
Err(err) => {
|
||||||
|
warn!("Failed to create VPN: {}", err);
|
||||||
|
vpn_state_tx.send(VpnState::Disconnected).ok();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Save the VPN handle
|
// Save the VPN handle
|
||||||
vpn_handle.write().await.replace(vpn);
|
vpn_handle.write().await.replace(vpn);
|
||||||
|
|
||||||
let vpn_state_tx = self.vpn_state_tx.clone();
|
|
||||||
let connect_info = Box::new(info.clone());
|
let connect_info = Box::new(info.clone());
|
||||||
vpn_state_tx.send(VpnState::Connecting(connect_info)).ok();
|
vpn_state_tx.send(VpnState::Connecting(connect_info)).ok();
|
||||||
|
|
||||||
|
@@ -118,28 +118,41 @@ impl WsServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn start(&self, shutdown_tx: mpsc::Sender<()>) {
|
pub async fn start(&self, shutdown_tx: mpsc::Sender<()>) {
|
||||||
if let Ok(listener) = TcpListener::bind("127.0.0.1:0").await {
|
let listener = match self.start_tcp_server().await {
|
||||||
let local_addr = listener.local_addr().unwrap();
|
Ok(listener) => listener,
|
||||||
|
Err(err) => {
|
||||||
|
warn!("Failed to start WS server: {}", err);
|
||||||
|
let _ = shutdown_tx.send(()).await;
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
self.lock_file.lock(local_addr.port().to_string()).unwrap();
|
tokio::select! {
|
||||||
|
_ = watch_vpn_state(self.ctx.vpn_state_rx(), Arc::clone(&self.ctx)) => {
|
||||||
info!("WS server listening on port: {}", local_addr.port());
|
info!("VPN state watch task completed");
|
||||||
|
}
|
||||||
tokio::select! {
|
_ = start_server(listener, self.ctx.clone()) => {
|
||||||
_ = watch_vpn_state(self.ctx.vpn_state_rx(), Arc::clone(&self.ctx)) => {
|
info!("WS server stopped");
|
||||||
info!("VPN state watch task completed");
|
}
|
||||||
}
|
_ = self.cancel_token.cancelled() => {
|
||||||
_ = start_server(listener, self.ctx.clone()) => {
|
info!("WS server cancelled");
|
||||||
info!("WS server stopped");
|
|
||||||
}
|
|
||||||
_ = self.cancel_token.cancelled() => {
|
|
||||||
info!("WS server cancelled");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let _ = shutdown_tx.send(()).await;
|
let _ = shutdown_tx.send(()).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn start_tcp_server(&self) -> anyhow::Result<TcpListener> {
|
||||||
|
let listener = TcpListener::bind("127.0.0.1:0").await?;
|
||||||
|
let local_addr = listener.local_addr()?;
|
||||||
|
let port = local_addr.port();
|
||||||
|
|
||||||
|
info!("WS server listening on port: {}", port);
|
||||||
|
|
||||||
|
self.lock_file.lock(port.to_string())?;
|
||||||
|
|
||||||
|
Ok(listener)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn watch_vpn_state(mut vpn_state_rx: watch::Receiver<VpnState>, ctx: Arc<WsServerContext>) {
|
async fn watch_vpn_state(mut vpn_state_rx: watch::Receiver<VpnState>, ctx: Arc<WsServerContext>) {
|
||||||
|
49
changelog.md
49
changelog.md
@@ -1,5 +1,54 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2.3.2 - 2024-06-17
|
||||||
|
|
||||||
|
- Fix the CAS callback parsing issue (fix [#372](https://github.com/yuezk/GlobalProtect-openconnect/issues/372))
|
||||||
|
- CLI: fix the `/tmp/gpauth.html` deletion issue (fix [#366](https://github.com/yuezk/GlobalProtect-openconnect/issues/366))
|
||||||
|
- GUI: fix the license not working after reboot (fix [#376](https://github.com/yuezk/GlobalProtect-openconnect/issues/376))
|
||||||
|
- GUI: add the license activation management link
|
||||||
|
|
||||||
|
## 2.3.1 - 2024-05-21
|
||||||
|
|
||||||
|
- Fix the `--sslkey` option not working
|
||||||
|
|
||||||
|
## 2.3.0 - 2024-05-20
|
||||||
|
|
||||||
|
- Support client certificate authentication (fix [#363](https://github.com/yuezk/GlobalProtect-openconnect/issues/363))
|
||||||
|
- Support `--disable-ipv6`, `--reconnect-timeout` parameters (related: [#364](https://github.com/yuezk/GlobalProtect-openconnect/issues/364))
|
||||||
|
- Use default labels if label fields are missing in prelogin response (fix [#357](https://github.com/yuezk/GlobalProtect-openconnect/issues/357))
|
||||||
|
|
||||||
|
## 2.2.1 - 2024-05-07
|
||||||
|
|
||||||
|
- GUI: Restore the default browser auth implementation (fix [#360](https://github.com/yuezk/GlobalProtect-openconnect/issues/360))
|
||||||
|
|
||||||
|
## 2.2.0 - 2024-04-30
|
||||||
|
|
||||||
|
- CLI: support authentication with external browser (fix [#298](https://github.com/yuezk/GlobalProtect-openconnect/issues/298))
|
||||||
|
- GUI: support using file-based storage when the system keyring is not available.
|
||||||
|
|
||||||
|
## 2.1.4 - 2024-04-10
|
||||||
|
|
||||||
|
- Support MFA authentication (fix [#343](https://github.com/yuezk/GlobalProtect-openconnect/issues/343))
|
||||||
|
- Improve the Gateway switcher UI
|
||||||
|
|
||||||
|
## 2.1.3 - 2024-04-07
|
||||||
|
|
||||||
|
- Support CAS authentication (fix [#339](https://github.com/yuezk/GlobalProtect-openconnect/issues/339))
|
||||||
|
- CLI: Add `--as-gateway` option to connect as gateway directly (fix [#318](https://github.com/yuezk/GlobalProtect-openconnect/issues/318))
|
||||||
|
- GUI: Support connect the gateway directly (fix [#318](https://github.com/yuezk/GlobalProtect-openconnect/issues/318))
|
||||||
|
- GUI: Add an option to use symbolic tray icon (fix [#341](https://github.com/yuezk/GlobalProtect-openconnect/issues/341))
|
||||||
|
|
||||||
|
## 2.1.2 - 2024-03-29
|
||||||
|
|
||||||
|
- Treat portal as gateway when the gateway login is failed (fix #338)
|
||||||
|
|
||||||
|
## 2.1.1 - 2024-03-25
|
||||||
|
|
||||||
|
- Add the `--hip` option to enable HIP report
|
||||||
|
- Fix not working in OpenSuse 15.5 (fix #336, #322)
|
||||||
|
- Treat portal as gateway when the gateway login is failed (fix #338)
|
||||||
|
- Improve the error message (fix #327)
|
||||||
|
|
||||||
## 2.1.0 - 2024-02-27
|
## 2.1.0 - 2024-02-27
|
||||||
|
|
||||||
- Update distribution channel for `gpgui` to complaint with the GPL-3 license.
|
- Update distribution channel for `gpgui` to complaint with the GPL-3 license.
|
||||||
|
11
crates/common/Cargo.toml
Normal file
11
crates/common/Cargo.toml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
[package]
|
||||||
|
name = "common"
|
||||||
|
rust-version.workspace = true
|
||||||
|
version.workspace = true
|
||||||
|
authors.workspace = true
|
||||||
|
homepage.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
is_executable.workspace = true
|
1
crates/common/src/lib.rs
Normal file
1
crates/common/src/lib.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pub mod vpn_utils;
|
54
crates/common/src/vpn_utils.rs
Normal file
54
crates/common/src/vpn_utils.rs
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
use std::{io, path::Path};
|
||||||
|
|
||||||
|
use is_executable::IsExecutable;
|
||||||
|
|
||||||
|
const VPNC_SCRIPT_LOCATIONS: [&str; 6] = [
|
||||||
|
"/usr/local/share/vpnc-scripts/vpnc-script",
|
||||||
|
"/usr/local/sbin/vpnc-script",
|
||||||
|
"/usr/share/vpnc-scripts/vpnc-script",
|
||||||
|
"/usr/sbin/vpnc-script",
|
||||||
|
"/etc/vpnc/vpnc-script",
|
||||||
|
"/etc/openconnect/vpnc-script",
|
||||||
|
];
|
||||||
|
|
||||||
|
const CSD_WRAPPER_LOCATIONS: [&str; 3] = [
|
||||||
|
#[cfg(target_arch = "x86_64")]
|
||||||
|
"/usr/lib/x86_64-linux-gnu/openconnect/hipreport.sh",
|
||||||
|
#[cfg(target_arch = "aarch64")]
|
||||||
|
"/usr/lib/aarch64-linux-gnu/openconnect/hipreport.sh",
|
||||||
|
"/usr/lib/openconnect/hipreport.sh",
|
||||||
|
"/usr/libexec/openconnect/hipreport.sh",
|
||||||
|
];
|
||||||
|
|
||||||
|
fn find_executable(locations: &[&str]) -> Option<String> {
|
||||||
|
for location in locations.iter() {
|
||||||
|
let path = Path::new(location);
|
||||||
|
if path.is_executable() {
|
||||||
|
return Some(location.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_vpnc_script() -> Option<String> {
|
||||||
|
find_executable(&VPNC_SCRIPT_LOCATIONS)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_csd_wrapper() -> Option<String> {
|
||||||
|
find_executable(&CSD_WRAPPER_LOCATIONS)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// If file exists, check if it is executable
|
||||||
|
pub fn check_executable(file: &str) -> Result<(), io::Error> {
|
||||||
|
let path = Path::new(file);
|
||||||
|
|
||||||
|
if path.exists() && !path.is_executable() {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::PermissionDenied,
|
||||||
|
format!("{} is not executable", file),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
@@ -9,6 +9,8 @@ anyhow.workspace = true
|
|||||||
base64.workspace = true
|
base64.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
reqwest.workspace = true
|
reqwest.workspace = true
|
||||||
|
openssl.workspace = true
|
||||||
|
pem.workspace = true
|
||||||
roxmltree.workspace = true
|
roxmltree.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
specta.workspace = true
|
specta.workspace = true
|
||||||
@@ -23,7 +25,6 @@ chacha20poly1305 = { version = "0.10", features = ["std"] }
|
|||||||
redact-engine.workspace = true
|
redact-engine.workspace = true
|
||||||
url.workspace = true
|
url.workspace = true
|
||||||
regex.workspace = true
|
regex.workspace = true
|
||||||
dotenvy_macro.workspace = true
|
|
||||||
uzers.workspace = true
|
uzers.workspace = true
|
||||||
serde_urlencoded.workspace = true
|
serde_urlencoded.workspace = true
|
||||||
md5.workspace = true
|
md5.workspace = true
|
||||||
|
19
crates/gpapi/build.rs
Normal file
19
crates/gpapi/build.rs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let manifest_dir = env!("CARGO_MANIFEST_DIR");
|
||||||
|
let workspace_dir = Path::new(manifest_dir).ancestors().nth(2).unwrap();
|
||||||
|
let gpgui_dir = workspace_dir.parent().unwrap().join("gpgui");
|
||||||
|
|
||||||
|
let gp_service_binary = workspace_dir.join("target/debug/gpservice");
|
||||||
|
let gp_client_binary = workspace_dir.join("target/debug/gpclient");
|
||||||
|
let gp_auth_binary = workspace_dir.join("target/debug/gpauth");
|
||||||
|
let gp_gui_helper_binary = workspace_dir.join("target/debug/gpgui-helper");
|
||||||
|
let gp_gui_binary = gpgui_dir.join("target/debug/gpgui");
|
||||||
|
|
||||||
|
println!("cargo:rustc-env=GP_SERVICE_BINARY={}", gp_service_binary.display());
|
||||||
|
println!("cargo:rustc-env=GP_CLIENT_BINARY={}", gp_client_binary.display());
|
||||||
|
println!("cargo:rustc-env=GP_AUTH_BINARY={}", gp_auth_binary.display());
|
||||||
|
println!("cargo:rustc-env=GP_GUI_HELPER_BINARY={}", gp_gui_helper_binary.display());
|
||||||
|
println!("cargo:rustc-env=GP_GUI_BINARY={}", gp_gui_binary.display());
|
||||||
|
}
|
@@ -1,13 +1,19 @@
|
|||||||
use anyhow::bail;
|
use std::borrow::{Borrow, Cow};
|
||||||
|
|
||||||
|
use log::{info, warn};
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::{error::AuthDataParseError, utils::base64::decode_to_string};
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct SamlAuthData {
|
pub struct SamlAuthData {
|
||||||
|
#[serde(alias = "un")]
|
||||||
username: String,
|
username: String,
|
||||||
prelogin_cookie: Option<String>,
|
prelogin_cookie: Option<String>,
|
||||||
portal_userauthcookie: Option<String>,
|
portal_userauthcookie: Option<String>,
|
||||||
|
token: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
@@ -32,10 +38,11 @@ impl SamlAuthData {
|
|||||||
username,
|
username,
|
||||||
prelogin_cookie,
|
prelogin_cookie,
|
||||||
portal_userauthcookie,
|
portal_userauthcookie,
|
||||||
|
token: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_html(html: &str) -> anyhow::Result<SamlAuthData> {
|
pub fn from_html(html: &str) -> anyhow::Result<SamlAuthData, AuthDataParseError> {
|
||||||
match parse_xml_tag(html, "saml-auth-status") {
|
match parse_xml_tag(html, "saml-auth-status") {
|
||||||
Some(saml_status) if saml_status == "1" => {
|
Some(saml_status) if saml_status == "1" => {
|
||||||
let username = parse_xml_tag(html, "saml-username");
|
let username = parse_xml_tag(html, "saml-username");
|
||||||
@@ -43,24 +50,51 @@ impl SamlAuthData {
|
|||||||
let portal_userauthcookie = parse_xml_tag(html, "portal-userauthcookie");
|
let portal_userauthcookie = parse_xml_tag(html, "portal-userauthcookie");
|
||||||
|
|
||||||
if SamlAuthData::check(&username, &prelogin_cookie, &portal_userauthcookie) {
|
if SamlAuthData::check(&username, &prelogin_cookie, &portal_userauthcookie) {
|
||||||
return Ok(SamlAuthData::new(
|
Ok(SamlAuthData::new(
|
||||||
username.unwrap(),
|
username.unwrap(),
|
||||||
prelogin_cookie,
|
prelogin_cookie,
|
||||||
portal_userauthcookie,
|
portal_userauthcookie,
|
||||||
));
|
))
|
||||||
|
} else {
|
||||||
|
Err(AuthDataParseError::Invalid)
|
||||||
}
|
}
|
||||||
|
|
||||||
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");
|
|
||||||
}
|
}
|
||||||
|
Some(_) => Err(AuthDataParseError::Invalid),
|
||||||
|
None => Err(AuthDataParseError::NotFound),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn from_gpcallback(data: &str) -> anyhow::Result<SamlAuthData, AuthDataParseError> {
|
||||||
|
let auth_data = data.trim_start_matches("globalprotectcallback:");
|
||||||
|
|
||||||
|
if auth_data.starts_with("cas-as") {
|
||||||
|
info!("Got CAS auth data from globalprotectcallback");
|
||||||
|
|
||||||
|
// Decode the auth data and use the original value if decoding fails
|
||||||
|
let auth_data = urlencoding::decode(auth_data).unwrap_or_else(|err| {
|
||||||
|
warn!("Failed to decode token auth data: {}", err);
|
||||||
|
Cow::Borrowed(auth_data)
|
||||||
|
});
|
||||||
|
|
||||||
|
let auth_data: SamlAuthData = serde_urlencoded::from_str(auth_data.borrow()).map_err(|e| {
|
||||||
|
warn!("Failed to parse token auth data: {}", e);
|
||||||
|
warn!("Auth data: {}", auth_data);
|
||||||
|
AuthDataParseError::Invalid
|
||||||
|
})?;
|
||||||
|
|
||||||
|
return Ok(auth_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Parsing SAML auth data...");
|
||||||
|
let auth_data = decode_to_string(auth_data).map_err(|e| {
|
||||||
|
warn!("Failed to decode SAML auth data: {}", e);
|
||||||
|
AuthDataParseError::Invalid
|
||||||
|
})?;
|
||||||
|
let auth_data = Self::from_html(&auth_data)?;
|
||||||
|
|
||||||
|
Ok(auth_data)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn username(&self) -> &str {
|
pub fn username(&self) -> &str {
|
||||||
&self.username
|
&self.username
|
||||||
}
|
}
|
||||||
@@ -69,6 +103,10 @@ impl SamlAuthData {
|
|||||||
self.prelogin_cookie.as_deref()
|
self.prelogin_cookie.as_deref()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn token(&self) -> Option<&str> {
|
||||||
|
self.token.as_deref()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn check(
|
pub fn check(
|
||||||
username: &Option<String>,
|
username: &Option<String>,
|
||||||
prelogin_cookie: &Option<String>,
|
prelogin_cookie: &Option<String>,
|
||||||
@@ -78,7 +116,16 @@ impl SamlAuthData {
|
|||||||
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.as_ref().is_some_and(|val| val.len() > 5);
|
let portal_userauthcookie_valid = portal_userauthcookie.as_ref().is_some_and(|val| val.len() > 5);
|
||||||
|
|
||||||
username_valid && (prelogin_cookie_valid || portal_userauthcookie_valid)
|
let is_valid = username_valid && (prelogin_cookie_valid || portal_userauthcookie_valid);
|
||||||
|
|
||||||
|
if !is_valid {
|
||||||
|
warn!(
|
||||||
|
"Invalid SAML auth data: username: {:?}, prelogin-cookie: {:?}, portal-userauthcookie: {:?}",
|
||||||
|
username, prelogin_cookie, portal_userauthcookie
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
is_valid
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,3 +135,38 @@ pub fn parse_xml_tag(html: &str, tag: &str) -> Option<String> {
|
|||||||
.and_then(|captures| captures.get(1))
|
.and_then(|captures| captures.get(1))
|
||||||
.map(|m| m.as_str().to_string())
|
.map(|m| m.as_str().to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn auth_data_from_gpcallback_cas() {
|
||||||
|
let auth_data = "globalprotectcallback:cas-as=1&un=xyz@email.com&token=very_long_string";
|
||||||
|
|
||||||
|
let auth_data = SamlAuthData::from_gpcallback(auth_data).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(auth_data.username(), "xyz@email.com");
|
||||||
|
assert_eq!(auth_data.token(), Some("very_long_string"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn auth_data_from_gpcallback_cas_urlencoded() {
|
||||||
|
let auth_data = "globalprotectcallback:cas-as%3D1%26un%3Dxyz%40email.com%26token%3Dvery_long_string";
|
||||||
|
|
||||||
|
let auth_data = SamlAuthData::from_gpcallback(auth_data).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(auth_data.username(), "xyz@email.com");
|
||||||
|
assert_eq!(auth_data.token(), Some("very_long_string"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn auth_data_from_gpcallback_non_cas() {
|
||||||
|
let auth_data = "PGh0bWw+PCEtLSA8c2FtbC1hdXRoLXN0YXR1cz4xPC9zYW1sLWF1dGgtc3RhdHVzPjxwcmVsb2dpbi1jb29raWU+cHJlbG9naW4tY29va2llPC9wcmVsb2dpbi1jb29raWU+PHNhbWwtdXNlcm5hbWU+eHl6QGVtYWlsLmNvbTwvc2FtbC11c2VybmFtZT48c2FtbC1zbG8+bm88L3NhbWwtc2xvPjxzYW1sLVNlc3Npb25Ob3RPbk9yQWZ0ZXI+PC9zYW1sLVNlc3Npb25Ob3RPbk9yQWZ0ZXI+IC0tPjwvaHRtbD4=";
|
||||||
|
|
||||||
|
let auth_data = SamlAuthData::from_gpcallback(auth_data).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(auth_data.username(), "xyz@email.com");
|
||||||
|
assert_eq!(auth_data.prelogin_cookie(), Some("prelogin-cookie"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -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, utils::base64::decode_to_string};
|
use crate::auth::SamlAuthData;
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Type, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Type, Clone)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
@@ -37,16 +37,18 @@ impl From<&CachedCredential> for PasswordCredential {
|
|||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Type, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Type, Clone)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct PreloginCookieCredential {
|
pub struct PreloginCredential {
|
||||||
username: String,
|
username: String,
|
||||||
prelogin_cookie: String,
|
prelogin_cookie: Option<String>,
|
||||||
|
token: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PreloginCookieCredential {
|
impl PreloginCredential {
|
||||||
pub fn new(username: &str, prelogin_cookie: &str) -> Self {
|
pub fn new(username: &str, prelogin_cookie: Option<&str>, token: Option<&str>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
username: username.to_string(),
|
username: username.to_string(),
|
||||||
prelogin_cookie: prelogin_cookie.to_string(),
|
prelogin_cookie: prelogin_cookie.map(|s| s.to_string()),
|
||||||
|
token: token.map(|s| s.to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,22 +56,22 @@ impl PreloginCookieCredential {
|
|||||||
&self.username
|
&self.username
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn prelogin_cookie(&self) -> &str {
|
pub fn prelogin_cookie(&self) -> Option<&str> {
|
||||||
&self.prelogin_cookie
|
self.prelogin_cookie.as_deref()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn token(&self) -> Option<&str> {
|
||||||
|
self.token.as_deref()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<SamlAuthData> for PreloginCookieCredential {
|
impl From<SamlAuthData> for PreloginCredential {
|
||||||
type Error = anyhow::Error;
|
fn from(value: SamlAuthData) -> Self {
|
||||||
|
|
||||||
fn try_from(value: SamlAuthData) -> Result<Self, Self::Error> {
|
|
||||||
let username = value.username().to_string();
|
let username = value.username().to_string();
|
||||||
let prelogin_cookie = value
|
let prelogin_cookie = value.prelogin_cookie();
|
||||||
.prelogin_cookie()
|
let token = value.token();
|
||||||
.ok_or_else(|| anyhow::anyhow!("Missing prelogin cookie"))?
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
Ok(Self::new(&username, &prelogin_cookie))
|
Self::new(&username, prelogin_cookie, token)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,34 +156,30 @@ impl From<PasswordCredential> for CachedCredential {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Type, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Type, Clone)]
|
||||||
#[serde(tag = "type", rename_all = "camelCase")]
|
#[serde(tag = "type", rename_all = "camelCase")]
|
||||||
pub enum Credential {
|
pub enum Credential {
|
||||||
Password(PasswordCredential),
|
Password(PasswordCredential),
|
||||||
PreloginCookie(PreloginCookieCredential),
|
Prelogin(PreloginCredential),
|
||||||
AuthCookie(AuthCookieCredential),
|
AuthCookie(AuthCookieCredential),
|
||||||
CachedCredential(CachedCredential),
|
Cached(CachedCredential),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Credential {
|
impl Credential {
|
||||||
/// Create a credential from a globalprotectcallback:<base64 encoded string>
|
/// Create a credential from a globalprotectcallback:<base64 encoded string>,
|
||||||
pub fn parse_gpcallback(auth_data: &str) -> anyhow::Result<Self> {
|
/// or globalprotectcallback:cas-as=1&un=user@xyz.com&token=very_long_string
|
||||||
// Remove the surrounding quotes
|
pub fn from_gpcallback(auth_data: &str) -> anyhow::Result<Self> {
|
||||||
let auth_data = auth_data.trim_matches('"');
|
let auth_data = SamlAuthData::from_gpcallback(auth_data)?;
|
||||||
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)
|
Ok(Self::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(),
|
||||||
Credential::PreloginCookie(cred) => cred.username(),
|
Credential::Prelogin(cred) => cred.username(),
|
||||||
Credential::AuthCookie(cred) => cred.username(),
|
Credential::AuthCookie(cred) => cred.username(),
|
||||||
Credential::CachedCredential(cred) => cred.username(),
|
Credential::Cached(cred) => cred.username(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,20 +187,22 @@ 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, token) = match self {
|
||||||
Credential::Password(cred) => (Some(cred.password()), None, None, None),
|
Credential::Password(cred) => (Some(cred.password()), None, None, None, None),
|
||||||
Credential::PreloginCookie(cred) => (None, Some(cred.prelogin_cookie()), None, None),
|
Credential::Prelogin(cred) => (None, cred.prelogin_cookie(), None, None, cred.token()),
|
||||||
Credential::AuthCookie(cred) => (
|
Credential::AuthCookie(cred) => (
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
Some(cred.user_auth_cookie()),
|
Some(cred.user_auth_cookie()),
|
||||||
Some(cred.prelogon_user_auth_cookie()),
|
Some(cred.prelogon_user_auth_cookie()),
|
||||||
|
None,
|
||||||
),
|
),
|
||||||
Credential::CachedCredential(cred) => (
|
Credential::Cached(cred) => (
|
||||||
cred.password(),
|
cred.password(),
|
||||||
None,
|
None,
|
||||||
Some(cred.auth_cookie.user_auth_cookie()),
|
Some(cred.auth_cookie.user_auth_cookie()),
|
||||||
Some(cred.auth_cookie.prelogon_user_auth_cookie()),
|
Some(cred.auth_cookie.prelogon_user_auth_cookie()),
|
||||||
|
None,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -214,17 +214,19 @@ impl Credential {
|
|||||||
portal_prelogonuserauthcookie.unwrap_or_default(),
|
portal_prelogonuserauthcookie.unwrap_or_default(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if let Some(token) = token {
|
||||||
|
params.insert("token", token);
|
||||||
|
}
|
||||||
|
|
||||||
params
|
params
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<SamlAuthData> for Credential {
|
impl From<SamlAuthData> for Credential {
|
||||||
type Error = anyhow::Error;
|
fn from(value: SamlAuthData) -> Self {
|
||||||
|
let cred = PreloginCredential::from(value);
|
||||||
|
|
||||||
fn try_from(value: SamlAuthData) -> Result<Self, Self::Error> {
|
Self::Prelogin(cred)
|
||||||
let prelogin_cookie = PreloginCookieCredential::try_from(value)?;
|
|
||||||
|
|
||||||
Ok(Self::PreloginCookie(prelogin_cookie))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,6 +244,6 @@ impl From<&AuthCookieCredential> for Credential {
|
|||||||
|
|
||||||
impl From<&CachedCredential> for Credential {
|
impl From<&CachedCredential> for Credential {
|
||||||
fn from(value: &CachedCredential) -> Self {
|
fn from(value: &CachedCredential) -> Self {
|
||||||
Self::CachedCredential(value.clone())
|
Self::Cached(value.clone())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
19
crates/gpapi/src/error.rs
Normal file
19
crates/gpapi/src/error.rs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum PortalError {
|
||||||
|
#[error("Prelogin error: {0}")]
|
||||||
|
PreloginError(String),
|
||||||
|
#[error("Portal config error: {0}")]
|
||||||
|
ConfigError(String),
|
||||||
|
#[error("Network error: {0}")]
|
||||||
|
NetworkError(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum AuthDataParseError {
|
||||||
|
#[error("No auth data found")]
|
||||||
|
NotFound,
|
||||||
|
#[error("Invalid auth data")]
|
||||||
|
Invalid,
|
||||||
|
}
|
@@ -156,11 +156,7 @@ fn build_csd_token(cookie: &str) -> anyhow::Result<String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn hip_report(gateway: &str, cookie: &str, csd_wrapper: &str, gp_params: &GpParams) -> anyhow::Result<()> {
|
pub async fn hip_report(gateway: &str, cookie: &str, csd_wrapper: &str, gp_params: &GpParams) -> anyhow::Result<()> {
|
||||||
let client = Client::builder()
|
let client = Client::try_from(gp_params)?;
|
||||||
.danger_accept_invalid_certs(gp_params.ignore_tls_errors())
|
|
||||||
.user_agent(gp_params.user_agent())
|
|
||||||
.build()?;
|
|
||||||
|
|
||||||
let md5 = build_csd_token(cookie)?;
|
let md5 = build_csd_token(cookie)?;
|
||||||
|
|
||||||
info!("Submit HIP report md5: {}", md5);
|
info!("Submit HIP report md5: {}", md5);
|
||||||
|
@@ -1,24 +1,27 @@
|
|||||||
use anyhow::bail;
|
use anyhow::bail;
|
||||||
use log::info;
|
use log::{info, warn};
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use roxmltree::Document;
|
use roxmltree::Document;
|
||||||
use urlencoding::encode;
|
use urlencoding::encode;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
credential::Credential,
|
credential::Credential,
|
||||||
|
error::PortalError,
|
||||||
gp_params::GpParams,
|
gp_params::GpParams,
|
||||||
utils::{normalize_server, remove_url_scheme},
|
utils::{normalize_server, parse_gp_response, remove_url_scheme},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub async fn gateway_login(gateway: &str, cred: &Credential, gp_params: &GpParams) -> anyhow::Result<String> {
|
pub enum GatewayLogin {
|
||||||
|
Cookie(String),
|
||||||
|
Mfa(String, String),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn gateway_login(gateway: &str, cred: &Credential, gp_params: &GpParams) -> anyhow::Result<GatewayLogin> {
|
||||||
let url = normalize_server(gateway)?;
|
let url = normalize_server(gateway)?;
|
||||||
let gateway = remove_url_scheme(&url);
|
let gateway = remove_url_scheme(&url);
|
||||||
|
|
||||||
let login_url = format!("{}/ssl-vpn/login.esp", url);
|
let login_url = format!("{}/ssl-vpn/login.esp", url);
|
||||||
let client = Client::builder()
|
let client = Client::try_from(gp_params)?;
|
||||||
.danger_accept_invalid_certs(gp_params.ignore_tls_errors())
|
|
||||||
.user_agent(gp_params.user_agent())
|
|
||||||
.build()?;
|
|
||||||
|
|
||||||
let mut params = cred.to_params();
|
let mut params = cred.to_params();
|
||||||
let extra_params = gp_params.to_params();
|
let extra_params = gp_params.to_params();
|
||||||
@@ -28,17 +31,32 @@ pub async fn gateway_login(gateway: &str, cred: &Credential, gp_params: &GpParam
|
|||||||
|
|
||||||
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
|
||||||
let status = res.status();
|
.post(&login_url)
|
||||||
|
.form(¶ms)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|e| anyhow::anyhow!(PortalError::NetworkError(e.to_string())))?;
|
||||||
|
|
||||||
if status.is_client_error() || status.is_server_error() {
|
let res = parse_gp_response(res).await.map_err(|err| {
|
||||||
bail!("Gateway login error: {}", status)
|
warn!("{err}");
|
||||||
|
anyhow::anyhow!("Gateway login error: {}", err.reason)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// MFA detected
|
||||||
|
if res.contains("Challenge") {
|
||||||
|
let Some((message, input_str)) = parse_mfa(&res) else {
|
||||||
|
bail!("Failed to parse MFA challenge: {res}");
|
||||||
|
};
|
||||||
|
|
||||||
|
return Ok(GatewayLogin::Mfa(message, input_str));
|
||||||
}
|
}
|
||||||
|
|
||||||
let res_xml = res.text().await?;
|
let doc = Document::parse(&res)?;
|
||||||
let doc = Document::parse(&res_xml)?;
|
|
||||||
|
|
||||||
build_gateway_token(&doc, gp_params.computer())
|
let cookie = build_gateway_token(&doc, gp_params.computer())?;
|
||||||
|
|
||||||
|
Ok(GatewayLogin::Cookie(cookie))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_gateway_token(doc: &Document, computer: &str) -> anyhow::Result<String> {
|
fn build_gateway_token(doc: &Document, computer: &str) -> anyhow::Result<String> {
|
||||||
@@ -72,3 +90,33 @@ fn read_args<'a>(args: &'a [String], index: usize, key: &'a str) -> anyhow::Resu
|
|||||||
.ok_or_else(|| anyhow::anyhow!("Failed to read {key} from args"))
|
.ok_or_else(|| anyhow::anyhow!("Failed to read {key} from args"))
|
||||||
.map(|s| (key, s.as_ref()))
|
.map(|s| (key, s.as_ref()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_mfa(res: &str) -> Option<(String, String)> {
|
||||||
|
let message = res
|
||||||
|
.lines()
|
||||||
|
.find(|l| l.contains("respMsg"))
|
||||||
|
.and_then(|l| l.split('"').nth(1).map(|s| s.to_string()))?;
|
||||||
|
|
||||||
|
let input_str = res
|
||||||
|
.lines()
|
||||||
|
.find(|l| l.contains("inputStr"))
|
||||||
|
.and_then(|l| l.split('"').nth(1).map(|s| s.to_string()))?;
|
||||||
|
|
||||||
|
Some((message, input_str))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mfa() {
|
||||||
|
let res = r#"var respStatus = "Challenge";
|
||||||
|
var respMsg = "MFA message";
|
||||||
|
thisForm.inputStr.value = "5ef64e83000119ed";"#;
|
||||||
|
|
||||||
|
let (message, input_str) = parse_mfa(res).unwrap();
|
||||||
|
assert_eq!(message, "MFA message");
|
||||||
|
assert_eq!(input_str, "5ef64e83000119ed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -1,9 +1,11 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use log::info;
|
||||||
|
use reqwest::Client;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use specta::Type;
|
use specta::Type;
|
||||||
|
|
||||||
use crate::GP_USER_AGENT;
|
use crate::{utils::request::create_identity, GP_USER_AGENT};
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone, Type, Default)]
|
#[derive(Debug, Serialize, Deserialize, Clone, Type, Default)]
|
||||||
pub enum ClientOs {
|
pub enum ClientOs {
|
||||||
@@ -42,7 +44,7 @@ impl ClientOs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Type, Default)]
|
#[derive(Debug, Serialize, Deserialize, Type, Default, Clone)]
|
||||||
pub struct GpParams {
|
pub struct GpParams {
|
||||||
is_gateway: bool,
|
is_gateway: bool,
|
||||||
user_agent: String,
|
user_agent: String,
|
||||||
@@ -51,7 +53,12 @@ pub struct GpParams {
|
|||||||
client_version: Option<String>,
|
client_version: Option<String>,
|
||||||
computer: String,
|
computer: String,
|
||||||
ignore_tls_errors: bool,
|
ignore_tls_errors: bool,
|
||||||
prefer_default_browser: bool,
|
certificate: Option<String>,
|
||||||
|
sslkey: Option<String>,
|
||||||
|
key_password: Option<String>,
|
||||||
|
// Used for MFA
|
||||||
|
input_str: Option<String>,
|
||||||
|
otp: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GpParams {
|
impl GpParams {
|
||||||
@@ -79,10 +86,6 @@ impl GpParams {
|
|||||||
self.ignore_tls_errors
|
self.ignore_tls_errors
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn prefer_default_browser(&self) -> bool {
|
|
||||||
self.prefer_default_browser
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn client_os(&self) -> &str {
|
pub fn client_os(&self) -> &str {
|
||||||
self.client_os.as_str()
|
self.client_os.as_str()
|
||||||
}
|
}
|
||||||
@@ -95,6 +98,14 @@ impl GpParams {
|
|||||||
self.client_version.as_deref()
|
self.client_version.as_deref()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_input_str(&mut self, input_str: &str) {
|
||||||
|
self.input_str = Some(input_str.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_otp(&mut self, otp: &str) {
|
||||||
|
self.otp = Some(otp.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
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();
|
||||||
@@ -105,11 +116,16 @@ impl GpParams {
|
|||||||
params.insert("ok", "Login");
|
params.insert("ok", "Login");
|
||||||
params.insert("direct", "yes");
|
params.insert("direct", "yes");
|
||||||
params.insert("ipv6-support", "yes");
|
params.insert("ipv6-support", "yes");
|
||||||
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);
|
params.insert("computer", &self.computer);
|
||||||
|
|
||||||
|
// MFA
|
||||||
|
params.insert("inputStr", self.input_str.as_deref().unwrap_or_default());
|
||||||
|
if let Some(otp) = &self.otp {
|
||||||
|
params.insert("passwd", otp);
|
||||||
|
}
|
||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
@@ -131,20 +147,26 @@ pub struct GpParamsBuilder {
|
|||||||
client_version: Option<String>,
|
client_version: Option<String>,
|
||||||
computer: String,
|
computer: String,
|
||||||
ignore_tls_errors: bool,
|
ignore_tls_errors: bool,
|
||||||
prefer_default_browser: bool,
|
certificate: Option<String>,
|
||||||
|
sslkey: Option<String>,
|
||||||
|
key_password: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GpParamsBuilder {
|
impl GpParamsBuilder {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
|
let computer = whoami::fallible::hostname().unwrap_or_else(|_| String::from("localhost"));
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
is_gateway: false,
|
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,
|
||||||
ignore_tls_errors: false,
|
ignore_tls_errors: false,
|
||||||
prefer_default_browser: false,
|
certificate: Default::default(),
|
||||||
|
sslkey: Default::default(),
|
||||||
|
key_password: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,8 +205,18 @@ impl GpParamsBuilder {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn prefer_default_browser(&mut self, prefer_default_browser: bool) -> &mut Self {
|
pub fn certificate<T: Into<Option<String>>>(&mut self, certificate: T) -> &mut Self {
|
||||||
self.prefer_default_browser = prefer_default_browser;
|
self.certificate = certificate.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sslkey<T: Into<Option<String>>>(&mut self, sslkey: T) -> &mut Self {
|
||||||
|
self.sslkey = sslkey.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn key_password<T: Into<Option<String>>>(&mut self, password: T) -> &mut Self {
|
||||||
|
self.key_password = password.into();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,7 +229,11 @@ impl GpParamsBuilder {
|
|||||||
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,
|
certificate: self.certificate.clone(),
|
||||||
|
sslkey: self.sslkey.clone(),
|
||||||
|
key_password: self.key_password.clone(),
|
||||||
|
input_str: Default::default(),
|
||||||
|
otp: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -207,3 +243,22 @@ impl Default for GpParamsBuilder {
|
|||||||
Self::new()
|
Self::new()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&GpParams> for Client {
|
||||||
|
type Error = anyhow::Error;
|
||||||
|
|
||||||
|
fn try_from(value: &GpParams) -> Result<Self, Self::Error> {
|
||||||
|
let mut builder = Client::builder()
|
||||||
|
.danger_accept_invalid_certs(value.ignore_tls_errors)
|
||||||
|
.user_agent(&value.user_agent);
|
||||||
|
|
||||||
|
if let Some(cert) = value.certificate.as_deref() {
|
||||||
|
info!("Using client certificate authentication...");
|
||||||
|
let identity = create_identity(cert, value.sslkey.as_deref(), value.key_password.as_deref())?;
|
||||||
|
builder = builder.identity(identity);
|
||||||
|
}
|
||||||
|
|
||||||
|
let client = builder.build()?;
|
||||||
|
Ok(client)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
pub mod auth;
|
pub mod auth;
|
||||||
pub mod credential;
|
pub mod credential;
|
||||||
|
pub mod error;
|
||||||
pub mod gateway;
|
pub mod gateway;
|
||||||
pub mod gp_params;
|
pub mod gp_params;
|
||||||
pub mod portal;
|
pub mod portal;
|
||||||
@@ -28,12 +29,12 @@ pub const GP_GUI_HELPER_BINARY: &str = "/usr/bin/gpgui-helper";
|
|||||||
pub(crate) const GP_AUTH_BINARY: &str = "/usr/bin/gpauth";
|
pub(crate) const GP_AUTH_BINARY: &str = "/usr/bin/gpauth";
|
||||||
|
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
pub const GP_CLIENT_BINARY: &str = dotenvy_macro::dotenv!("GP_CLIENT_BINARY");
|
pub const GP_CLIENT_BINARY: &str = env!("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 = env!("GP_SERVICE_BINARY");
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
pub const GP_GUI_BINARY: &str = dotenvy_macro::dotenv!("GP_GUI_BINARY");
|
pub const GP_GUI_BINARY: &str = env!("GP_GUI_BINARY");
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
pub const GP_GUI_HELPER_BINARY: &str = dotenvy_macro::dotenv!("GP_GUI_HELPER_BINARY");
|
pub const GP_GUI_HELPER_BINARY: &str = env!("GP_GUI_HELPER_BINARY");
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
pub(crate) const GP_AUTH_BINARY: &str = dotenvy_macro::dotenv!("GP_AUTH_BINARY");
|
pub(crate) const GP_AUTH_BINARY: &str = env!("GP_AUTH_BINARY");
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
use anyhow::bail;
|
use anyhow::bail;
|
||||||
use log::info;
|
use log::{info, warn};
|
||||||
use reqwest::{Client, StatusCode};
|
use reqwest::{Client, StatusCode};
|
||||||
use roxmltree::Document;
|
use roxmltree::Document;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
@@ -7,10 +7,10 @@ use specta::Type;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
credential::{AuthCookieCredential, Credential},
|
credential::{AuthCookieCredential, Credential},
|
||||||
|
error::PortalError,
|
||||||
gateway::{parse_gateways, Gateway},
|
gateway::{parse_gateways, Gateway},
|
||||||
gp_params::GpParams,
|
gp_params::GpParams,
|
||||||
portal::PortalError,
|
utils::{normalize_server, parse_gp_response, remove_url_scheme, xml},
|
||||||
utils::{normalize_server, remove_url_scheme, xml},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Type)]
|
#[derive(Debug, Serialize, Type)]
|
||||||
@@ -88,10 +88,7 @@ pub async fn retrieve_config(portal: &str, cred: &Credential, gp_params: &GpPara
|
|||||||
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::try_from(gp_params)?;
|
||||||
.danger_accept_invalid_certs(gp_params.ignore_tls_errors())
|
|
||||||
.user_agent(gp_params.user_agent())
|
|
||||||
.build()?;
|
|
||||||
|
|
||||||
let mut params = cred.to_params();
|
let mut params = cred.to_params();
|
||||||
let extra_params = gp_params.to_params();
|
let extra_params = gp_params.to_params();
|
||||||
@@ -102,18 +99,25 @@ pub async fn retrieve_config(portal: &str, cred: &Credential, gp_params: &GpPara
|
|||||||
|
|
||||||
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
|
||||||
let status = res.status();
|
.post(&url)
|
||||||
|
.form(¶ms)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|e| anyhow::anyhow!(PortalError::NetworkError(e.to_string())))?;
|
||||||
|
|
||||||
if status == StatusCode::NOT_FOUND {
|
let res_xml = parse_gp_response(res).await.or_else(|err| {
|
||||||
bail!(PortalError::ConfigError("Config endpoint not found".to_string()))
|
if err.status == StatusCode::NOT_FOUND {
|
||||||
}
|
bail!(PortalError::ConfigError("Config endpoint not found".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
if status.is_client_error() || status.is_server_error() {
|
if err.is_status_error() {
|
||||||
bail!("Portal config error: {}", status)
|
warn!("{err}");
|
||||||
}
|
bail!("Portal config error: {}", err.reason);
|
||||||
|
}
|
||||||
|
|
||||||
let res_xml = res.text().await.map_err(|e| PortalError::ConfigError(e.to_string()))?;
|
Err(anyhow::anyhow!(PortalError::ConfigError(err.reason)))
|
||||||
|
})?;
|
||||||
|
|
||||||
if res_xml.is_empty() {
|
if res_xml.is_empty() {
|
||||||
bail!(PortalError::ConfigError("Empty portal config response".to_string()))
|
bail!(PortalError::ConfigError("Empty portal config response".to_string()))
|
||||||
|
@@ -3,13 +3,3 @@ 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,14 +1,14 @@
|
|||||||
use anyhow::bail;
|
use anyhow::{anyhow, bail};
|
||||||
use log::info;
|
use log::{info, warn};
|
||||||
use reqwest::{Client, StatusCode};
|
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::{
|
||||||
|
error::PortalError,
|
||||||
gp_params::GpParams,
|
gp_params::GpParams,
|
||||||
portal::PortalError,
|
utils::{base64, normalize_server, parse_gp_response, xml},
|
||||||
utils::{base64, normalize_server, xml},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const REQUIRED_PARAMS: [&str; 8] = [
|
const REQUIRED_PARAMS: [&str; 8] = [
|
||||||
@@ -98,56 +98,61 @@ impl Prelogin {
|
|||||||
|
|
||||||
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!("Prelogin with user_agent: {}", user_agent);
|
let is_gateway = gp_params.is_gateway();
|
||||||
|
let prelogin_type = if is_gateway { "Gateway" } else { "Portal" };
|
||||||
|
|
||||||
|
info!("{} prelogin with user_agent: {}", prelogin_type, user_agent);
|
||||||
|
|
||||||
let portal = normalize_server(portal)?;
|
let portal = normalize_server(portal)?;
|
||||||
let is_gateway = gp_params.is_gateway();
|
|
||||||
let path = if is_gateway { "ssl-vpn" } else { "global-protect" };
|
let path = if is_gateway { "ssl-vpn" } else { "global-protect" };
|
||||||
let prelogin_url = format!("{portal}/{}/prelogin.esp", path);
|
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("tmp", "tmp");
|
||||||
if gp_params.prefer_default_browser() {
|
params.insert("default-browser", "1");
|
||||||
params.insert("default-browser", "1");
|
params.insert("cas-support", "yes");
|
||||||
}
|
|
||||||
|
|
||||||
params.retain(|k, _| REQUIRED_PARAMS.iter().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::try_from(gp_params)?;
|
||||||
.danger_accept_invalid_certs(gp_params.ignore_tls_errors())
|
|
||||||
.user_agent(user_agent)
|
|
||||||
.build()?;
|
|
||||||
|
|
||||||
let res = client.post(&prelogin_url).form(¶ms).send().await?;
|
let res = client
|
||||||
let status = res.status();
|
.post(&prelogin_url)
|
||||||
|
.form(¶ms)
|
||||||
if status == StatusCode::NOT_FOUND {
|
.send()
|
||||||
bail!(PortalError::PreloginError("Prelogin endpoint not found".to_string()))
|
|
||||||
}
|
|
||||||
|
|
||||||
if status.is_client_error() || status.is_server_error() {
|
|
||||||
bail!("Prelogin error: {}", status)
|
|
||||||
}
|
|
||||||
|
|
||||||
let res_xml = res
|
|
||||||
.text()
|
|
||||||
.await
|
.await
|
||||||
.map_err(|e| PortalError::PreloginError(e.to_string()))?;
|
.map_err(|e| anyhow::anyhow!(PortalError::NetworkError(e.to_string())))?;
|
||||||
|
|
||||||
let prelogin = parse_res_xml(res_xml, is_gateway).map_err(|e| PortalError::PreloginError(e.to_string()))?;
|
let res_xml = parse_gp_response(res).await.or_else(|err| {
|
||||||
|
if err.status == StatusCode::NOT_FOUND {
|
||||||
|
bail!(PortalError::PreloginError("Prelogin endpoint not found".to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err.is_status_error() {
|
||||||
|
warn!("{err}");
|
||||||
|
bail!("Prelogin error: {}", err.reason)
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(anyhow!(PortalError::PreloginError(err.reason)))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let prelogin = parse_res_xml(&res_xml, is_gateway).map_err(|err| {
|
||||||
|
warn!("Parse response error, response: {}", res_xml);
|
||||||
|
PortalError::PreloginError(err.to_string())
|
||||||
|
})?;
|
||||||
|
|
||||||
Ok(prelogin)
|
Ok(prelogin)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_res_xml(res_xml: String, is_gateway: bool) -> anyhow::Result<Prelogin> {
|
fn parse_res_xml(res_xml: &str, 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")
|
||||||
.ok_or_else(|| anyhow::anyhow!("Prelogin response does not contain status element"))?;
|
.ok_or_else(|| anyhow::anyhow!("Prelogin response does not contain status element"))?;
|
||||||
// Check the status of the prelogin response
|
// Check the status of the prelogin response
|
||||||
if status.to_uppercase() != "SUCCESS" {
|
if status.to_uppercase() != "SUCCESS" {
|
||||||
let msg = xml::get_child_text(&doc, "msg").unwrap_or(String::from("Unknown error"));
|
let msg = xml::get_child_text(&doc, "msg").unwrap_or(String::from("Unknown error"));
|
||||||
bail!("Prelogin failed: {}", msg)
|
bail!("{}", msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
let region = xml::get_child_text(&doc, "region").unwrap_or_else(|| {
|
let region = xml::get_child_text(&doc, "region").unwrap_or_else(|| {
|
||||||
@@ -173,22 +178,24 @@ fn parse_res_xml(res_xml: String, is_gateway: bool) -> anyhow::Result<Prelogin>
|
|||||||
return Ok(Prelogin::Saml(saml_prelogin));
|
return Ok(Prelogin::Saml(saml_prelogin));
|
||||||
}
|
}
|
||||||
|
|
||||||
let label_username = xml::get_child_text(&doc, "username-label");
|
let label_username = xml::get_child_text(&doc, "username-label").unwrap_or_else(|| {
|
||||||
let label_password = xml::get_child_text(&doc, "password-label");
|
info!("Username label has no value, using default");
|
||||||
// Check if the prelogin response is standard login
|
String::from("Username")
|
||||||
if label_username.is_some() && label_password.is_some() {
|
});
|
||||||
let auth_message =
|
let label_password = xml::get_child_text(&doc, "password-label").unwrap_or_else(|| {
|
||||||
xml::get_child_text(&doc, "authentication-message").unwrap_or(String::from("Please enter the login credentials"));
|
info!("Password label has no value, using default");
|
||||||
let standard_prelogin = StandardPrelogin {
|
String::from("Password")
|
||||||
region,
|
});
|
||||||
is_gateway,
|
|
||||||
auth_message,
|
|
||||||
label_username: label_username.unwrap(),
|
|
||||||
label_password: label_password.unwrap(),
|
|
||||||
};
|
|
||||||
|
|
||||||
return Ok(Prelogin::Standard(standard_prelogin));
|
let auth_message =
|
||||||
}
|
xml::get_child_text(&doc, "authentication-message").unwrap_or(String::from("Please enter the login credentials"));
|
||||||
|
let standard_prelogin = StandardPrelogin {
|
||||||
|
region,
|
||||||
|
is_gateway,
|
||||||
|
auth_message,
|
||||||
|
label_username,
|
||||||
|
label_password,
|
||||||
|
};
|
||||||
|
|
||||||
bail!("Invalid prelogin response");
|
Ok(Prelogin::Standard(standard_prelogin))
|
||||||
}
|
}
|
||||||
|
@@ -18,6 +18,7 @@ pub struct SamlAuthLauncher<'a> {
|
|||||||
fix_openssl: bool,
|
fix_openssl: bool,
|
||||||
ignore_tls_errors: bool,
|
ignore_tls_errors: bool,
|
||||||
clean: bool,
|
clean: bool,
|
||||||
|
default_browser: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> SamlAuthLauncher<'a> {
|
impl<'a> SamlAuthLauncher<'a> {
|
||||||
@@ -33,6 +34,7 @@ impl<'a> SamlAuthLauncher<'a> {
|
|||||||
fix_openssl: false,
|
fix_openssl: false,
|
||||||
ignore_tls_errors: false,
|
ignore_tls_errors: false,
|
||||||
clean: false,
|
clean: false,
|
||||||
|
default_browser: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,8 +83,13 @@ impl<'a> SamlAuthLauncher<'a> {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn default_browser(mut self, default_browser: bool) -> Self {
|
||||||
|
self.default_browser = default_browser;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Launch the authenticator binary as the current user or SUDO_USER if available.
|
/// Launch the authenticator binary as the current user or SUDO_USER if available.
|
||||||
pub async fn launch(self) -> anyhow::Result<Credential> {
|
pub async fn launch(self) -> anyhow::Result<Option<Credential>> {
|
||||||
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);
|
||||||
|
|
||||||
@@ -122,6 +129,10 @@ impl<'a> SamlAuthLauncher<'a> {
|
|||||||
auth_cmd.arg("--clean");
|
auth_cmd.arg("--clean");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.default_browser {
|
||||||
|
auth_cmd.arg("--default-browser");
|
||||||
|
}
|
||||||
|
|
||||||
let mut non_root_cmd = auth_cmd.into_non_root()?;
|
let mut non_root_cmd = auth_cmd.into_non_root()?;
|
||||||
let output = non_root_cmd
|
let output = non_root_cmd
|
||||||
.kill_on_drop(true)
|
.kill_on_drop(true)
|
||||||
@@ -130,12 +141,16 @@ impl<'a> SamlAuthLauncher<'a> {
|
|||||||
.wait_with_output()
|
.wait_with_output()
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
if self.default_browser {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
let Ok(auth_result) = serde_json::from_slice::<SamlAuthResult>(&output.stdout) else {
|
let Ok(auth_result) = serde_json::from_slice::<SamlAuthResult>(&output.stdout) else {
|
||||||
bail!("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) => Ok(Some(Credential::from(auth_data))),
|
||||||
SamlAuthResult::Failure(msg) => bail!(msg),
|
SamlAuthResult::Failure(msg) => bail!(msg),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,7 @@
|
|||||||
use std::{env::temp_dir, io::Write};
|
use std::{env::temp_dir, fs, io::Write, os::unix::fs::PermissionsExt};
|
||||||
|
|
||||||
|
use anyhow::bail;
|
||||||
|
use log::warn;
|
||||||
|
|
||||||
pub struct BrowserAuthenticator<'a> {
|
pub struct BrowserAuthenticator<'a> {
|
||||||
auth_request: &'a str,
|
auth_request: &'a str,
|
||||||
@@ -14,8 +17,18 @@ impl BrowserAuthenticator<'_> {
|
|||||||
open::that_detached(self.auth_request)?;
|
open::that_detached(self.auth_request)?;
|
||||||
} else {
|
} else {
|
||||||
let html_file = temp_dir().join("gpauth.html");
|
let html_file = temp_dir().join("gpauth.html");
|
||||||
let mut file = std::fs::File::create(&html_file)?;
|
|
||||||
|
|
||||||
|
// Remove the file and error if permission denied
|
||||||
|
if let Err(err) = fs::remove_file(&html_file) {
|
||||||
|
if err.kind() != std::io::ErrorKind::NotFound {
|
||||||
|
warn!("Failed to remove the temporary file: {}", err);
|
||||||
|
bail!("Please remove the file manually: {:?}", html_file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut file = fs::File::create(&html_file)?;
|
||||||
|
|
||||||
|
file.set_permissions(fs::Permissions::from_mode(0o600))?;
|
||||||
file.write_all(self.auth_request.as_bytes())?;
|
file.write_all(self.auth_request.as_bytes())?;
|
||||||
|
|
||||||
open::that_detached(html_file)?;
|
open::that_detached(html_file)?;
|
||||||
@@ -24,11 +37,3 @@ impl BrowserAuthenticator<'_> {
|
|||||||
Ok(())
|
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@@ -32,10 +32,15 @@ pub struct ConnectArgs {
|
|||||||
cookie: String,
|
cookie: String,
|
||||||
vpnc_script: Option<String>,
|
vpnc_script: Option<String>,
|
||||||
user_agent: Option<String>,
|
user_agent: Option<String>,
|
||||||
|
os: Option<ClientOs>,
|
||||||
|
certificate: Option<String>,
|
||||||
|
sslkey: Option<String>,
|
||||||
|
key_password: Option<String>,
|
||||||
csd_uid: u32,
|
csd_uid: u32,
|
||||||
csd_wrapper: Option<String>,
|
csd_wrapper: Option<String>,
|
||||||
|
reconnect_timeout: u32,
|
||||||
mtu: u32,
|
mtu: u32,
|
||||||
os: Option<ClientOs>,
|
disable_ipv6: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ConnectArgs {
|
impl ConnectArgs {
|
||||||
@@ -45,9 +50,14 @@ impl ConnectArgs {
|
|||||||
vpnc_script: None,
|
vpnc_script: None,
|
||||||
user_agent: None,
|
user_agent: None,
|
||||||
os: None,
|
os: None,
|
||||||
|
certificate: None,
|
||||||
|
sslkey: None,
|
||||||
|
key_password: None,
|
||||||
csd_uid: 0,
|
csd_uid: 0,
|
||||||
csd_wrapper: None,
|
csd_wrapper: None,
|
||||||
|
reconnect_timeout: 300,
|
||||||
mtu: 0,
|
mtu: 0,
|
||||||
|
disable_ipv6: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,6 +77,18 @@ impl ConnectArgs {
|
|||||||
self.os.as_ref().map(|os| os.to_openconnect_os().to_string())
|
self.os.as_ref().map(|os| os.to_openconnect_os().to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn certificate(&self) -> Option<String> {
|
||||||
|
self.certificate.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sslkey(&self) -> Option<String> {
|
||||||
|
self.sslkey.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn key_password(&self) -> Option<String> {
|
||||||
|
self.key_password.clone()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn csd_uid(&self) -> u32 {
|
pub fn csd_uid(&self) -> u32 {
|
||||||
self.csd_uid
|
self.csd_uid
|
||||||
}
|
}
|
||||||
@@ -75,9 +97,17 @@ impl ConnectArgs {
|
|||||||
self.csd_wrapper.clone()
|
self.csd_wrapper.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn reconnect_timeout(&self) -> u32 {
|
||||||
|
self.reconnect_timeout
|
||||||
|
}
|
||||||
|
|
||||||
pub fn mtu(&self) -> u32 {
|
pub fn mtu(&self) -> u32 {
|
||||||
self.mtu
|
self.mtu
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn disable_ipv6(&self) -> bool {
|
||||||
|
self.disable_ipv6
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, Type)]
|
#[derive(Debug, Deserialize, Serialize, Type)]
|
||||||
@@ -109,11 +139,6 @@ impl ConnectRequest {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_mtu(mut self, mtu: u32) -> Self {
|
|
||||||
self.args.mtu = mtu;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_user_agent<T: Into<Option<String>>>(mut self, user_agent: T) -> Self {
|
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
|
||||||
@@ -124,6 +149,36 @@ impl ConnectRequest {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_certificate<T: Into<Option<String>>>(mut self, certificate: T) -> Self {
|
||||||
|
self.args.certificate = certificate.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_sslkey<T: Into<Option<String>>>(mut self, sslkey: T) -> Self {
|
||||||
|
self.args.sslkey = sslkey.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_key_password<T: Into<Option<String>>>(mut self, key_password: T) -> Self {
|
||||||
|
self.args.key_password = key_password.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_reconnect_timeout(mut self, reconnect_timeout: u32) -> Self {
|
||||||
|
self.args.reconnect_timeout = reconnect_timeout;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_mtu(mut self, mtu: u32) -> Self {
|
||||||
|
self.args.mtu = mtu;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_disable_ipv6(mut self, disable_ipv6: bool) -> Self {
|
||||||
|
self.args.disable_ipv6 = disable_ipv6;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn gateway(&self) -> &Gateway {
|
pub fn gateway(&self) -> &Gateway {
|
||||||
self.info.gateway()
|
self.info.gateway()
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,3 @@
|
|||||||
use reqwest::Url;
|
|
||||||
|
|
||||||
pub(crate) mod xml;
|
pub(crate) mod xml;
|
||||||
|
|
||||||
pub mod base64;
|
pub mod base64;
|
||||||
@@ -10,13 +8,18 @@ pub mod env_file;
|
|||||||
pub mod lock_file;
|
pub mod lock_file;
|
||||||
pub mod openssl;
|
pub mod openssl;
|
||||||
pub mod redact;
|
pub mod redact;
|
||||||
|
pub mod request;
|
||||||
#[cfg(feature = "tauri")]
|
#[cfg(feature = "tauri")]
|
||||||
pub mod window;
|
pub mod window;
|
||||||
|
|
||||||
mod shutdown_signal;
|
mod shutdown_signal;
|
||||||
|
|
||||||
|
use log::warn;
|
||||||
pub use shutdown_signal::shutdown_signal;
|
pub use shutdown_signal::shutdown_signal;
|
||||||
|
|
||||||
|
use reqwest::{Response, StatusCode, Url};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
/// Normalize the server URL to the format `https://<host>:<port>`
|
/// Normalize the server URL to the format `https://<host>:<port>`
|
||||||
pub fn normalize_server(server: &str) -> anyhow::Result<String> {
|
pub fn normalize_server(server: &str) -> anyhow::Result<String> {
|
||||||
let server = if server.starts_with("https://") || server.starts_with("http://") {
|
let server = if server.starts_with("https://") || server.starts_with("http://") {
|
||||||
@@ -41,3 +44,52 @@ pub fn normalize_server(server: &str) -> anyhow::Result<String> {
|
|||||||
pub fn remove_url_scheme(s: &str) -> String {
|
pub fn remove_url_scheme(s: &str) -> String {
|
||||||
s.replace("http://", "").replace("https://", "")
|
s.replace("http://", "").replace("https://", "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
#[error("GP response error: reason={reason}, status={status}, body={body}")]
|
||||||
|
pub(crate) struct GpError {
|
||||||
|
pub status: StatusCode,
|
||||||
|
pub reason: String,
|
||||||
|
body: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GpError {
|
||||||
|
pub fn is_status_error(&self) -> bool {
|
||||||
|
self.status.is_client_error() || self.status.is_server_error()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn parse_gp_response(res: Response) -> anyhow::Result<String, GpError> {
|
||||||
|
let status = res.status();
|
||||||
|
|
||||||
|
if status.is_client_error() || status.is_server_error() {
|
||||||
|
let (reason, body) = parse_gp_error(res).await;
|
||||||
|
|
||||||
|
return Err(GpError { status, reason, body });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.text().await.map_err(|err| {
|
||||||
|
warn!("Failed to read response: {}", err);
|
||||||
|
|
||||||
|
GpError {
|
||||||
|
status,
|
||||||
|
reason: "failed to read response".to_string(),
|
||||||
|
body: "<failed to read response>".to_string(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn parse_gp_error(res: Response) -> (String, String) {
|
||||||
|
let reason = res
|
||||||
|
.headers()
|
||||||
|
.get("x-private-pan-globalprotect")
|
||||||
|
.map_or_else(|| "<none>", |v| v.to_str().unwrap_or("<invalid header>"))
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let res = res.text().await.map_or_else(
|
||||||
|
|_| "<failed to read response>".to_string(),
|
||||||
|
|v| if v.is_empty() { "<empty>".to_string() } else { v },
|
||||||
|
);
|
||||||
|
|
||||||
|
(reason, res)
|
||||||
|
}
|
||||||
|
140
crates/gpapi/src/utils/request.rs
Normal file
140
crates/gpapi/src/utils/request.rs
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
use std::{borrow::Cow, fs};
|
||||||
|
|
||||||
|
use anyhow::bail;
|
||||||
|
use log::warn;
|
||||||
|
use openssl::pkey::PKey;
|
||||||
|
use pem::parse_many;
|
||||||
|
use reqwest::Identity;
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum RequestIdentityError {
|
||||||
|
#[error("Failed to find the private key")]
|
||||||
|
NoKey,
|
||||||
|
#[error("No passphrase provided")]
|
||||||
|
NoPassphrase(&'static str),
|
||||||
|
#[error("Failed to decrypt private key")]
|
||||||
|
DecryptError(&'static str),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create an identity object from a certificate and key
|
||||||
|
/// The file is expected to be the PKCS#8 PEM or PKCS#12 format
|
||||||
|
/// When using a PKCS#12 file, the key is NOT required, but a passphrase is required
|
||||||
|
pub fn create_identity(cert: &str, key: Option<&str>, passphrase: Option<&str>) -> anyhow::Result<Identity> {
|
||||||
|
if cert.ends_with(".p12") || cert.ends_with(".pfx") {
|
||||||
|
create_identity_from_pkcs12(cert, passphrase)
|
||||||
|
} else {
|
||||||
|
create_identity_from_pem(cert, key, passphrase)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_identity_from_pem(cert: &str, key: Option<&str>, passphrase: Option<&str>) -> anyhow::Result<Identity> {
|
||||||
|
let cert_pem = fs::read(cert).map_err(|err| anyhow::anyhow!("Failed to read certificate file: {}", err))?;
|
||||||
|
|
||||||
|
// Use the certificate as the key if no key is provided
|
||||||
|
let key_pem_file = match key {
|
||||||
|
Some(key) => Cow::Owned(fs::read(key).map_err(|err| anyhow::anyhow!("Failed to read key file: {}", err))?),
|
||||||
|
None => Cow::Borrowed(&cert_pem),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Find the private key in the pem file
|
||||||
|
let key_pem = parse_many(key_pem_file.as_ref())?
|
||||||
|
.into_iter()
|
||||||
|
.find(|pem| pem.tag().ends_with("PRIVATE KEY"))
|
||||||
|
.ok_or(RequestIdentityError::NoKey)?;
|
||||||
|
|
||||||
|
// The key pem could be encrypted, so we need to decrypt it
|
||||||
|
let decrypted_key_pem = if key_pem.tag().ends_with("ENCRYPTED PRIVATE KEY") {
|
||||||
|
let passphrase = passphrase.ok_or_else(|| {
|
||||||
|
warn!("Key is encrypted but no passphrase provided");
|
||||||
|
RequestIdentityError::NoPassphrase("PEM")
|
||||||
|
})?;
|
||||||
|
let pem_content = pem::encode(&key_pem);
|
||||||
|
let key = PKey::private_key_from_pem_passphrase(pem_content.as_bytes(), passphrase.as_bytes()).map_err(|err| {
|
||||||
|
warn!("Failed to decrypt key: {}", err);
|
||||||
|
RequestIdentityError::DecryptError("PEM")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
key.private_key_to_pem_pkcs8()?
|
||||||
|
} else {
|
||||||
|
pem::encode(&key_pem).into()
|
||||||
|
};
|
||||||
|
|
||||||
|
let identity = Identity::from_pkcs8_pem(&cert_pem, &decrypted_key_pem)?;
|
||||||
|
Ok(identity)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_identity_from_pkcs12(pkcs12: &str, passphrase: Option<&str>) -> anyhow::Result<Identity> {
|
||||||
|
let pkcs12 = fs::read(pkcs12)?;
|
||||||
|
|
||||||
|
let Some(passphrase) = passphrase else {
|
||||||
|
bail!(RequestIdentityError::NoPassphrase("PKCS#12"));
|
||||||
|
};
|
||||||
|
|
||||||
|
let identity = Identity::from_pkcs12_der(&pkcs12, passphrase)?;
|
||||||
|
Ok(identity)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn create_identity_from_pem_requires_passphrase() {
|
||||||
|
let cert = "tests/files/badssl.com-client.pem";
|
||||||
|
let identity = create_identity_from_pem(cert, None, None);
|
||||||
|
|
||||||
|
assert!(identity.is_err());
|
||||||
|
assert!(identity.unwrap_err().to_string().contains("No passphrase provided"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn create_identity_from_pem_with_passphrase() {
|
||||||
|
let cert = "tests/files/badssl.com-client.pem";
|
||||||
|
let passphrase = "badssl.com";
|
||||||
|
|
||||||
|
let identity = create_identity_from_pem(cert, None, Some(passphrase));
|
||||||
|
|
||||||
|
assert!(identity.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn create_identity_from_pem_unencrypted_key() {
|
||||||
|
let cert = "tests/files/badssl.com-client-unencrypted.pem";
|
||||||
|
let identity = create_identity_from_pem(cert, None, None);
|
||||||
|
println!("{:?}", identity);
|
||||||
|
|
||||||
|
assert!(identity.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn create_identity_from_pem_cert_and_encrypted_key() {
|
||||||
|
let cert = "tests/files/badssl.com-client.pem";
|
||||||
|
let key = "tests/files/badssl.com-client.pem";
|
||||||
|
let passphrase = "badssl.com";
|
||||||
|
|
||||||
|
let identity = create_identity_from_pem(cert, Some(key), Some(passphrase));
|
||||||
|
|
||||||
|
assert!(identity.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn create_identity_from_pem_cert_and_encrypted_key_no_passphrase() {
|
||||||
|
let cert = "tests/files/badssl.com-client.pem";
|
||||||
|
let key = "tests/files/badssl.com-client.pem";
|
||||||
|
|
||||||
|
let identity = create_identity_from_pem(cert, Some(key), None);
|
||||||
|
|
||||||
|
assert!(identity.is_err());
|
||||||
|
assert!(identity.unwrap_err().to_string().contains("No passphrase provided"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn create_identity_from_pem_cert_and_unencrypted_key() {
|
||||||
|
let cert = "tests/files/badssl.com-client.pem";
|
||||||
|
let key = "tests/files/badssl.com-client-unencrypted.pem";
|
||||||
|
|
||||||
|
let identity = create_identity_from_pem(cert, Some(key), None);
|
||||||
|
|
||||||
|
assert!(identity.is_ok());
|
||||||
|
}
|
||||||
|
}
|
62
crates/gpapi/tests/files/badssl.com-client-unencrypted.pem
Normal file
62
crates/gpapi/tests/files/badssl.com-client-unencrypted.pem
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
Bag Attributes
|
||||||
|
localKeyID: AE DC 75 2E 97 28 71 D8 1E 9A 7F 1E 5A AA F4 2E D3 6D 2C 8B
|
||||||
|
subject=/C=US/ST=California/L=San Francisco/O=BadSSL/CN=BadSSL Client Certificate
|
||||||
|
issuer=/C=US/ST=California/L=San Francisco/O=BadSSL/CN=BadSSL Client Root Certificate Authority
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIEnTCCAoWgAwIBAgIJAPfJjkenM2ooMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNV
|
||||||
|
BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNp
|
||||||
|
c2NvMQ8wDQYDVQQKDAZCYWRTU0wxMTAvBgNVBAMMKEJhZFNTTCBDbGllbnQgUm9v
|
||||||
|
dCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcNMjQwNTE3MTc1OTMyWhcNMjYwNTE3
|
||||||
|
MTc1OTMyWjBvMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQG
|
||||||
|
A1UEBwwNU2FuIEZyYW5jaXNjbzEPMA0GA1UECgwGQmFkU1NMMSIwIAYDVQQDDBlC
|
||||||
|
YWRTU0wgQ2xpZW50IENlcnRpZmljYXRlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
|
||||||
|
MIIBCgKCAQEAxzdfEeseTs/rukjly6MSLHM+Rh0enA3Ai4Mj2sdl31x3SbPoen08
|
||||||
|
utVhjPmlxIUdkiMG4+ffe7N+JtDLG75CaxZp9CxytX7kywooRBJsRnQhmQPca8MR
|
||||||
|
WAJBIz+w/L+3AFkTIqWBfyT+1VO8TVKPkEpGdLDovZOmzZAASi9/sj+j6gM7AaCi
|
||||||
|
DeZTf2ES66abA5pOp60Q6OEdwg/vCUJfarhKDpi9tj3P6qToy9Y4DiBUhOct4MG8
|
||||||
|
w5XwmKAC+Vfm8tb7tMiUoU0yvKKOcL6YXBXxB2kPcOYxYNobXavfVBEdwSrjQ7i/
|
||||||
|
s3o6hkGQlm9F7JPEuVgbl/Jdwa64OYIqjQIDAQABoy0wKzAJBgNVHRMEAjAAMBEG
|
||||||
|
CWCGSAGG+EIBAQQEAwIHgDALBgNVHQ8EBAMCBeAwDQYJKoZIhvcNAQELBQADggIB
|
||||||
|
AE6iDW5Lv5I0bJY6TGxJUoB4rcsbbtEP4O4MT14GP7j7I48V09VBG9yjskYze0Ls
|
||||||
|
Xb9mQpEpPyQLTDJIWu/ic/y5SMnelCjUxmfl37cfNLJajQZxc4FDEUSemrPKpEkB
|
||||||
|
UzHNkxw9LSzqsyxnQmMIGoN+ZNCFoV7s5pekzPfgZj5+s7a+oiF/AzhOWZzF7vaM
|
||||||
|
aclX7KCeENQV+q0giDjsGIHI6BevUHYkglocEqff+rIDHjjLxHLPooflV50M+ifc
|
||||||
|
4uJdHgG8hwKxd1uf3LImUsquiBrW5CO6KCgwLrtQNe11pQHpY0urZxK/tnAj7QtD
|
||||||
|
v/O1ryd/3+b0Gx14TyulMtcaLHsE94ppwjcxpYGNcyH+M39OMihuR2aqmkrqcZd/
|
||||||
|
VWop1cNwZgPtCNVvfivRpX52NLI5I0eMfs6jeTMr719hdAby3akoiNLN3YNKrdrp
|
||||||
|
pyRz/sUFGO8AHHECXA15KTeMBNfZnO32ZAZ4jHyyDBO1A5f9iDbErhXfIpeRCrCO
|
||||||
|
gM9MLuO4YEMG1Skp+qaw7SIaG+oi2t4lbVRr3LOv0Hfkjjb7bVjfWSwLBPH/gv0E
|
||||||
|
ZL6G0p7PjeoCh4obS3Y1yxfNlPR6RQwWl1wve+Nkmf5sDCmgr3P0512ZuvqkbKkB
|
||||||
|
/syiAWDsYzFuq2Ntv2ljTYPEPwXEIQcpsagDRL6WzoLR
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
Bag Attributes
|
||||||
|
localKeyID: AE DC 75 2E 97 28 71 D8 1E 9A 7F 1E 5A AA F4 2E D3 6D 2C 8B
|
||||||
|
Key Attributes: <No Attributes>
|
||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDHN18R6x5Oz+u6
|
||||||
|
SOXLoxIscz5GHR6cDcCLgyPax2XfXHdJs+h6fTy61WGM+aXEhR2SIwbj5997s34m
|
||||||
|
0MsbvkJrFmn0LHK1fuTLCihEEmxGdCGZA9xrwxFYAkEjP7D8v7cAWRMipYF/JP7V
|
||||||
|
U7xNUo+QSkZ0sOi9k6bNkABKL3+yP6PqAzsBoKIN5lN/YRLrppsDmk6nrRDo4R3C
|
||||||
|
D+8JQl9quEoOmL22Pc/qpOjL1jgOIFSE5y3gwbzDlfCYoAL5V+by1vu0yJShTTK8
|
||||||
|
oo5wvphcFfEHaQ9w5jFg2htdq99UER3BKuNDuL+zejqGQZCWb0Xsk8S5WBuX8l3B
|
||||||
|
rrg5giqNAgMBAAECggEAVRB/t9b9igmeTlzyQpHPIMvUu3uTpm742JmWpcSe61FA
|
||||||
|
XmhDzInNdLnIfbnb3p44kj4Coy5PbzKlm01sbNxA4BkiBPE1yen1J/2eU/LJ6QuN
|
||||||
|
jRjo9drFfR75UWPQ3xu9uJhQY2rocLILXmvy69FlG+ebThh8SPbTMtNaTFMb47An
|
||||||
|
pk2FrW9+rzPswbklOxls/SDt78usRvfAjslm73IdBTOrbceF+GmYs3/SXz1gu05p
|
||||||
|
LxY2rhC8piBlqnD/QbXBahZbhjb9SkDFn2typMFZKkJIIKDJaOI2E9tIlZ97/0nZ
|
||||||
|
txqchMty8IuU9YYAfLXCmj2IEfnvLtL7thLfKLuWAQKBgQDyXBpEgKFzfy2a1AI0
|
||||||
|
+1qL/u5UN14l7S6/wmyDTgVMXwoxhwPRXWD5PutQ8D6tMfC/y4AYt3OXg1blCvLD
|
||||||
|
XysNj5SK+dpmQR0SyeWjd9zwxJAXvx0McJefCYd86YGcGhJsuX5bkHIeQlEc6df7
|
||||||
|
yoqr1480VQx/+Fk1i6Zr0EIUFQKBgQDSbalUOfXZh2EVRQEgf3VoPlxAiwGGQcVT
|
||||||
|
i+pbjMG3pOwmkVyJZusGtN5HN4Oi7n1oiyfMYGsszKQ5j4TDBGS70pNUzhTv3Vn8
|
||||||
|
0Vsfz0arJRqJxviiv4FfDmsYXwObNKwOjR+LEn1NUPkOYOLdz1lDuWOu11LE90Dy
|
||||||
|
Q6hg8WwCmQKBgQDTy5lI9AAjpqh7/XpQQrhGT2qHPjuQeU25Vnbt6GjI7OVDkvHL
|
||||||
|
LQdpyYprGQgs4s+5TGWNNARYC/cMAh1Ujv5Yw3jUWrR5V73IhZeg20bBQYWKuwDv
|
||||||
|
thVKblFw377cZAxl51R9QCX6O4oW8mRFLiMxORd0bD6YNrf/CyNMZJraYQKBgAE7
|
||||||
|
o0JbFJWxtV/qh5cpKAb0VpYKOngO6pkSuMzQhlINJVUUhPZJJBdl9+dy69KIkzOJ
|
||||||
|
nTIVXotkp5GuxZhe7jgrg7F7g6PkKCLTFzWYgVF/ZihoggxyEs/7xaTe6aZ/KILt
|
||||||
|
UMH/2bwaPVtYNfwWuu8qpurfWBzPVhIVU2c+AuQBAoGAXMbw10vyiznlhyMFw5kx
|
||||||
|
SzlBMqJBLJkzQBtpvXuT0lqqxTSNC3N4WxgVOLCHa6HqXiB0790YL8/RWunsXTk2
|
||||||
|
c7ugThP6iMPNVAycWkIF4vvHTwZ9RCSmEQabRaqGGLz/bhLL3fi3lPGCR+iW2Dxq
|
||||||
|
GTH3fhaM/pZZGdIC75x/69Y=
|
||||||
|
-----END PRIVATE KEY-----
|
64
crates/gpapi/tests/files/badssl.com-client.pem
Normal file
64
crates/gpapi/tests/files/badssl.com-client.pem
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
Bag Attributes
|
||||||
|
localKeyID: AE DC 75 2E 97 28 71 D8 1E 9A 7F 1E 5A AA F4 2E D3 6D 2C 8B
|
||||||
|
subject=/C=US/ST=California/L=San Francisco/O=BadSSL/CN=BadSSL Client Certificate
|
||||||
|
issuer=/C=US/ST=California/L=San Francisco/O=BadSSL/CN=BadSSL Client Root Certificate Authority
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIEnTCCAoWgAwIBAgIJAPfJjkenM2ooMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNV
|
||||||
|
BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNp
|
||||||
|
c2NvMQ8wDQYDVQQKDAZCYWRTU0wxMTAvBgNVBAMMKEJhZFNTTCBDbGllbnQgUm9v
|
||||||
|
dCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcNMjQwNTE3MTc1OTMyWhcNMjYwNTE3
|
||||||
|
MTc1OTMyWjBvMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQG
|
||||||
|
A1UEBwwNU2FuIEZyYW5jaXNjbzEPMA0GA1UECgwGQmFkU1NMMSIwIAYDVQQDDBlC
|
||||||
|
YWRTU0wgQ2xpZW50IENlcnRpZmljYXRlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
|
||||||
|
MIIBCgKCAQEAxzdfEeseTs/rukjly6MSLHM+Rh0enA3Ai4Mj2sdl31x3SbPoen08
|
||||||
|
utVhjPmlxIUdkiMG4+ffe7N+JtDLG75CaxZp9CxytX7kywooRBJsRnQhmQPca8MR
|
||||||
|
WAJBIz+w/L+3AFkTIqWBfyT+1VO8TVKPkEpGdLDovZOmzZAASi9/sj+j6gM7AaCi
|
||||||
|
DeZTf2ES66abA5pOp60Q6OEdwg/vCUJfarhKDpi9tj3P6qToy9Y4DiBUhOct4MG8
|
||||||
|
w5XwmKAC+Vfm8tb7tMiUoU0yvKKOcL6YXBXxB2kPcOYxYNobXavfVBEdwSrjQ7i/
|
||||||
|
s3o6hkGQlm9F7JPEuVgbl/Jdwa64OYIqjQIDAQABoy0wKzAJBgNVHRMEAjAAMBEG
|
||||||
|
CWCGSAGG+EIBAQQEAwIHgDALBgNVHQ8EBAMCBeAwDQYJKoZIhvcNAQELBQADggIB
|
||||||
|
AE6iDW5Lv5I0bJY6TGxJUoB4rcsbbtEP4O4MT14GP7j7I48V09VBG9yjskYze0Ls
|
||||||
|
Xb9mQpEpPyQLTDJIWu/ic/y5SMnelCjUxmfl37cfNLJajQZxc4FDEUSemrPKpEkB
|
||||||
|
UzHNkxw9LSzqsyxnQmMIGoN+ZNCFoV7s5pekzPfgZj5+s7a+oiF/AzhOWZzF7vaM
|
||||||
|
aclX7KCeENQV+q0giDjsGIHI6BevUHYkglocEqff+rIDHjjLxHLPooflV50M+ifc
|
||||||
|
4uJdHgG8hwKxd1uf3LImUsquiBrW5CO6KCgwLrtQNe11pQHpY0urZxK/tnAj7QtD
|
||||||
|
v/O1ryd/3+b0Gx14TyulMtcaLHsE94ppwjcxpYGNcyH+M39OMihuR2aqmkrqcZd/
|
||||||
|
VWop1cNwZgPtCNVvfivRpX52NLI5I0eMfs6jeTMr719hdAby3akoiNLN3YNKrdrp
|
||||||
|
pyRz/sUFGO8AHHECXA15KTeMBNfZnO32ZAZ4jHyyDBO1A5f9iDbErhXfIpeRCrCO
|
||||||
|
gM9MLuO4YEMG1Skp+qaw7SIaG+oi2t4lbVRr3LOv0Hfkjjb7bVjfWSwLBPH/gv0E
|
||||||
|
ZL6G0p7PjeoCh4obS3Y1yxfNlPR6RQwWl1wve+Nkmf5sDCmgr3P0512ZuvqkbKkB
|
||||||
|
/syiAWDsYzFuq2Ntv2ljTYPEPwXEIQcpsagDRL6WzoLR
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
Bag Attributes
|
||||||
|
localKeyID: AE DC 75 2E 97 28 71 D8 1E 9A 7F 1E 5A AA F4 2E D3 6D 2C 8B
|
||||||
|
Key Attributes: <No Attributes>
|
||||||
|
-----BEGIN ENCRYPTED PRIVATE KEY-----
|
||||||
|
MIIFDjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQIET6L0Ht/lYgCAggA
|
||||||
|
MBQGCCqGSIb3DQMHBAi1Xo+JdQ6XvwSCBMgX20Fk3/GzptJ0zjl7ZqX2G3J4LIkM
|
||||||
|
E5qJ4yv2WUkCCOWqz5DjlSrRz4kdCYHqnM1/qyrLa1UWWJlNQ9lBHTE+yp0vtAC/
|
||||||
|
ajQfKt3RFyGxblp6nEKJI7kvhmQHDbITilmVEpcZPbci3gi7asQI3bRSLaHwGtbH
|
||||||
|
DY+8hJ8lZQMRjYGDyGb99qEdYnMMRMW+b44lIRASe6W3EUfrvJlp+OUqRA7hJzn2
|
||||||
|
yha9Zo8KWo9fA9UZDFFKNlXakg76+1HymB+uqvZl14xHHfwhlKPaqzmCb8MUtt7e
|
||||||
|
YJDB9I3y8aHKExXPbRk04bbY5G9o6WdWslDUY4axOZuhUXyn0h6cTZn//qmsjcBH
|
||||||
|
499+j55vW6W7vkMfurt/pmLIBWC9kDWPZVLizbfXxWiWvRmQvKPfzO5TU8oObYyJ
|
||||||
|
qUVjb3Vpa/WPrF5APUVd/DDofurgzdOkmDGomONPvSHxahHSyEZsxpnl52GD6uU/
|
||||||
|
i3oa5qLE9uA1QjyX6wyN9SU5wE2FZKTJwwRJwW4+s4T/2eJjhuJez5q1xhSCes4A
|
||||||
|
A2pufAAY/ctQSmCCKCTW+EkrXtcezx66fkgPpNK/m6bz5KGJkA4QXjl8A05PDAFE
|
||||||
|
Z68VOX/T0IGfXc2BbPgP0u+WpCvvO2cW/pU4sjcwOMxFuT1Bn3TwmDLTZ+zba1rE
|
||||||
|
zFRMMCz/8SKq3I+VkzQ66ureEz0RLwk07JVzE9AJUEm+zCFUdoIaz09OMGVqtf4a
|
||||||
|
V+UgupH0QlffmRNJKQtXPuj6Wjfa43GLaCnN/cpXXq8+2o81dLTsCbEsYu+8DRjC
|
||||||
|
B0iyjzdqgjBBYurIEwEc4iGtPt4Y+4rgAJcpEUgwvWii37xyutOC9V7ansvd6zg3
|
||||||
|
WXiX5Ktj/qS0EzM33WtZfx7jygJIf1MvxrJU+D+HgGii1mHaZ6bHxMX3QGpRsEvh
|
||||||
|
IzBx16XvoHcXARZJG91bC+K1sJ6e05L1PevS7gj4heJTEhtmvABUrn9O1n5fZWPj
|
||||||
|
Q81zRDgplMO7r8aBW/pE+sj4VSTMg0Xu0nlqqvQoWxr9YFcJm0+I9fHQPxewnRus
|
||||||
|
sBZoiTqnWqbTr+uRATRUAp+hU03S4jGZwbzH4ylL2hr/TshGVJk/olBsULAfIiHa
|
||||||
|
dA5H258IEwAoFO6zgI9AvqmTFo3Mnpqb/AS/HuDmmS/3Ud1EF8hFsMLPcV0JdSTY
|
||||||
|
Dl4xgZ6j6jOUlTN5Yt6To2Zg3Q9Bm6qytFaffEP66Jl5aWhksI31Fz/ihzn5wfx9
|
||||||
|
xh91U8+kGVNrpYHlo5y3FR/ywSXynLkJffCbfUciEaTDv9i0JppoIVXyFqcMofHe
|
||||||
|
GUsWTCozAW3O8MwpLaJxcNcfRq0DWziIdiDgbF2tPoCqnNxXtLYSPpdt3jNDcPcx
|
||||||
|
U0Z6ep6FnAXiujtQRSRSP3Ssq23098BxDSM9+eashFOmSbSClAEEn/THRxTp/gMh
|
||||||
|
zmD8kpX1zN1Cm/lerTGjrGjnkXcQ7LY76/+C1uT+tQbw5LjmCfFEYTFtnFyYFlF1
|
||||||
|
GiXFokh9SdLaCzW4vmZok85Fe+7VZ7BAchBTfTIMKlXKmeouf3YVYJ8glPsinrjb
|
||||||
|
cB2pKv3tVrdQwo3moYDwSsDgkd7BNKKHDVdY2O6NgX4/Fyd6pZt7ZAphyC1giEqg
|
||||||
|
pPo=
|
||||||
|
-----END ENCRYPTED PRIVATE KEY-----
|
@@ -6,8 +6,8 @@ license.workspace = true
|
|||||||
links = "openconnect"
|
links = "openconnect"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
common = { path = "../common" }
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
is_executable.workspace = true
|
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
cc = "1"
|
cc = "1"
|
||||||
|
@@ -14,12 +14,16 @@ pub(crate) struct ConnectOptions {
|
|||||||
pub script: *const c_char,
|
pub script: *const c_char,
|
||||||
pub os: *const c_char,
|
pub os: *const c_char,
|
||||||
pub certificate: *const c_char,
|
pub certificate: *const c_char,
|
||||||
|
pub sslkey: *const c_char,
|
||||||
|
pub key_password: *const c_char,
|
||||||
pub servercert: *const c_char,
|
pub servercert: *const c_char,
|
||||||
|
|
||||||
pub csd_uid: u32,
|
pub csd_uid: u32,
|
||||||
pub csd_wrapper: *const c_char,
|
pub csd_wrapper: *const c_char,
|
||||||
|
|
||||||
|
pub reconnect_timeout: u32,
|
||||||
pub mtu: u32,
|
pub mtu: u32,
|
||||||
|
pub disable_ipv6: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[link(name = "vpn")]
|
#[link(name = "vpn")]
|
||||||
|
@@ -16,7 +16,7 @@ static vpn_connected_callback on_vpn_connected;
|
|||||||
/* Validate the peer certificate */
|
/* Validate the peer certificate */
|
||||||
static int validate_peer_cert(__attribute__((unused)) void *_vpninfo, const char *reason)
|
static int validate_peer_cert(__attribute__((unused)) void *_vpninfo, const char *reason)
|
||||||
{
|
{
|
||||||
INFO("Validating peer cert: %s", reason);
|
INFO("Accepting the server certificate though %s", reason);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,12 +28,9 @@ static void print_progress(__attribute__((unused)) void *_vpninfo, int level, co
|
|||||||
char *message = format_message(format, args);
|
char *message = format_message(format, args);
|
||||||
va_end(args);
|
va_end(args);
|
||||||
|
|
||||||
if (message == NULL)
|
if (message == NULL) {
|
||||||
{
|
|
||||||
ERROR("Failed to format log message");
|
ERROR("Failed to format log message");
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{
|
|
||||||
LOG(level, message);
|
LOG(level, message);
|
||||||
free(message);
|
free(message);
|
||||||
}
|
}
|
||||||
@@ -63,12 +60,13 @@ int vpn_connect(const vpn_options *options, vpn_connected_callback callback)
|
|||||||
INFO("OS: %s", options->os);
|
INFO("OS: %s", options->os);
|
||||||
INFO("CSD_USER: %d", options->csd_uid);
|
INFO("CSD_USER: %d", options->csd_uid);
|
||||||
INFO("CSD_WRAPPER: %s", options->csd_wrapper);
|
INFO("CSD_WRAPPER: %s", options->csd_wrapper);
|
||||||
|
INFO("RECONNECT_TIMEOUT: %d", options->reconnect_timeout);
|
||||||
INFO("MTU: %d", options->mtu);
|
INFO("MTU: %d", options->mtu);
|
||||||
|
INFO("DISABLE_IPV6: %d", options->disable_ipv6);
|
||||||
|
|
||||||
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);
|
||||||
|
|
||||||
if (!vpninfo)
|
if (!vpninfo) {
|
||||||
{
|
|
||||||
ERROR("openconnect_vpninfo_new failed");
|
ERROR("openconnect_vpninfo_new failed");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
@@ -83,15 +81,13 @@ int vpn_connect(const vpn_options *options, vpn_connected_callback callback)
|
|||||||
openconnect_set_reported_os(vpninfo, options->os);
|
openconnect_set_reported_os(vpninfo, options->os);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options->certificate)
|
if (options->certificate) {
|
||||||
{
|
|
||||||
INFO("Setting client certificate: %s", options->certificate);
|
INFO("Setting client certificate: %s", options->certificate);
|
||||||
openconnect_set_client_cert(vpninfo, options->certificate, NULL);
|
openconnect_set_client_cert(vpninfo, options->certificate, options->sslkey);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options->servercert) {
|
if (options->key_password) {
|
||||||
INFO("Setting server certificate: %s", options->servercert);
|
openconnect_set_key_password(vpninfo, options->key_password);
|
||||||
openconnect_set_system_trust(vpninfo, 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options->csd_wrapper) {
|
if (options->csd_wrapper) {
|
||||||
@@ -103,39 +99,37 @@ int vpn_connect(const vpn_options *options, vpn_connected_callback callback)
|
|||||||
openconnect_set_reqmtu(vpninfo, mtu);
|
openconnect_set_reqmtu(vpninfo, mtu);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options->disable_ipv6) {
|
||||||
|
openconnect_disable_ipv6(vpninfo);
|
||||||
|
}
|
||||||
|
|
||||||
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) {
|
||||||
{
|
|
||||||
ERROR("openconnect_setup_cmd_pipe failed");
|
ERROR("openconnect_setup_cmd_pipe failed");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!uname(&utsbuf))
|
if (!uname(&utsbuf)) {
|
||||||
{
|
|
||||||
openconnect_set_localname(vpninfo, utsbuf.nodename);
|
openconnect_set_localname(vpninfo, utsbuf.nodename);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Essential step
|
// Essential step
|
||||||
if (openconnect_make_cstp_connection(vpninfo) != 0)
|
if (openconnect_make_cstp_connection(vpninfo) != 0) {
|
||||||
{
|
|
||||||
ERROR("openconnect_make_cstp_connection failed");
|
ERROR("openconnect_make_cstp_connection failed");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (openconnect_setup_dtls(vpninfo, 60) != 0)
|
if (openconnect_setup_dtls(vpninfo, 60) != 0) {
|
||||||
{
|
|
||||||
openconnect_disable_dtls(vpninfo);
|
openconnect_disable_dtls(vpninfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Essential step
|
// Essential step
|
||||||
openconnect_set_setup_tun_handler(vpninfo, setup_tun_handler);
|
openconnect_set_setup_tun_handler(vpninfo, setup_tun_handler);
|
||||||
|
|
||||||
while (1)
|
while (1) {
|
||||||
{
|
int ret = openconnect_mainloop(vpninfo, options->reconnect_timeout, 10);
|
||||||
int ret = openconnect_mainloop(vpninfo, 300, 10);
|
|
||||||
|
|
||||||
if (ret)
|
if (ret) {
|
||||||
{
|
|
||||||
INFO("openconnect_mainloop returned %d, exiting", ret);
|
INFO("openconnect_mainloop returned %d, exiting", ret);
|
||||||
openconnect_vpninfo_free(vpninfo);
|
openconnect_vpninfo_free(vpninfo);
|
||||||
return ret;
|
return ret;
|
||||||
@@ -152,8 +146,7 @@ void vpn_disconnect()
|
|||||||
|
|
||||||
INFO("Stopping VPN connection: %d", g_cmd_pipe_fd);
|
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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -15,12 +15,16 @@ typedef struct vpn_options
|
|||||||
const char *script;
|
const char *script;
|
||||||
const char *os;
|
const char *os;
|
||||||
const char *certificate;
|
const char *certificate;
|
||||||
|
const char *sslkey;
|
||||||
|
const char *key_password;
|
||||||
const char *servercert;
|
const char *servercert;
|
||||||
|
|
||||||
const uid_t csd_uid;
|
const uid_t csd_uid;
|
||||||
const char *csd_wrapper;
|
const char *csd_wrapper;
|
||||||
|
|
||||||
|
const int reconnect_timeout;
|
||||||
const int mtu;
|
const int mtu;
|
||||||
|
const int disable_ipv6;
|
||||||
} 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);
|
||||||
@@ -35,7 +39,7 @@ static char *format_message(const char *format, va_list args)
|
|||||||
int len = vsnprintf(NULL, 0, format, args_copy);
|
int len = vsnprintf(NULL, 0, format, args_copy);
|
||||||
va_end(args_copy);
|
va_end(args_copy);
|
||||||
|
|
||||||
char *buffer = malloc(len + 1);
|
char *buffer = (char*)malloc(len + 1);
|
||||||
if (buffer == NULL)
|
if (buffer == NULL)
|
||||||
{
|
{
|
||||||
return NULL;
|
return NULL;
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
mod ffi;
|
mod ffi;
|
||||||
mod vpn;
|
mod vpn;
|
||||||
mod vpnc_script;
|
|
||||||
|
|
||||||
pub use vpn::*;
|
pub use vpn::*;
|
||||||
|
@@ -1,11 +1,13 @@
|
|||||||
use std::{
|
use std::{
|
||||||
ffi::{c_char, CString},
|
ffi::{c_char, CString},
|
||||||
|
fmt,
|
||||||
sync::{Arc, RwLock},
|
sync::{Arc, RwLock},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use common::vpn_utils::{check_executable, find_vpnc_script};
|
||||||
use log::info;
|
use log::info;
|
||||||
|
|
||||||
use crate::{ffi, vpnc_script::find_default_vpnc_script};
|
use crate::ffi;
|
||||||
|
|
||||||
type OnConnectedCallback = Arc<RwLock<Option<Box<dyn FnOnce() + 'static + Send + Sync>>>>;
|
type OnConnectedCallback = Arc<RwLock<Option<Box<dyn FnOnce() + 'static + Send + Sync>>>>;
|
||||||
|
|
||||||
@@ -16,12 +18,16 @@ pub struct Vpn {
|
|||||||
script: CString,
|
script: CString,
|
||||||
os: CString,
|
os: CString,
|
||||||
certificate: Option<CString>,
|
certificate: Option<CString>,
|
||||||
|
sslkey: Option<CString>,
|
||||||
|
key_password: Option<CString>,
|
||||||
servercert: Option<CString>,
|
servercert: Option<CString>,
|
||||||
|
|
||||||
csd_uid: u32,
|
csd_uid: u32,
|
||||||
csd_wrapper: Option<CString>,
|
csd_wrapper: Option<CString>,
|
||||||
|
|
||||||
|
reconnect_timeout: u32,
|
||||||
mtu: u32,
|
mtu: u32,
|
||||||
|
disable_ipv6: bool,
|
||||||
|
|
||||||
callback: OnConnectedCallback,
|
callback: OnConnectedCallback,
|
||||||
}
|
}
|
||||||
@@ -59,13 +65,18 @@ impl Vpn {
|
|||||||
user_agent: self.user_agent.as_ptr(),
|
user_agent: self.user_agent.as_ptr(),
|
||||||
script: self.script.as_ptr(),
|
script: self.script.as_ptr(),
|
||||||
os: self.os.as_ptr(),
|
os: self.os.as_ptr(),
|
||||||
|
|
||||||
certificate: Self::option_to_ptr(&self.certificate),
|
certificate: Self::option_to_ptr(&self.certificate),
|
||||||
|
sslkey: Self::option_to_ptr(&self.sslkey),
|
||||||
|
key_password: Self::option_to_ptr(&self.key_password),
|
||||||
servercert: Self::option_to_ptr(&self.servercert),
|
servercert: Self::option_to_ptr(&self.servercert),
|
||||||
|
|
||||||
csd_uid: self.csd_uid,
|
csd_uid: self.csd_uid,
|
||||||
csd_wrapper: Self::option_to_ptr(&self.csd_wrapper),
|
csd_wrapper: Self::option_to_ptr(&self.csd_wrapper),
|
||||||
|
|
||||||
|
reconnect_timeout: self.reconnect_timeout,
|
||||||
mtu: self.mtu,
|
mtu: self.mtu,
|
||||||
|
disable_ipv6: self.disable_ipv6 as u32,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,17 +88,43 @@ impl Vpn {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct VpnError {
|
||||||
|
message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VpnError {
|
||||||
|
fn new(message: String) -> Self {
|
||||||
|
Self { message }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for VpnError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(f, "{}", self.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for VpnError {}
|
||||||
|
|
||||||
pub struct VpnBuilder {
|
pub struct VpnBuilder {
|
||||||
server: String,
|
server: String,
|
||||||
cookie: String,
|
cookie: String,
|
||||||
user_agent: Option<String>,
|
|
||||||
script: Option<String>,
|
script: Option<String>,
|
||||||
|
|
||||||
|
user_agent: Option<String>,
|
||||||
os: Option<String>,
|
os: Option<String>,
|
||||||
|
|
||||||
|
certificate: Option<String>,
|
||||||
|
sslkey: Option<String>,
|
||||||
|
key_password: Option<String>,
|
||||||
|
|
||||||
csd_uid: u32,
|
csd_uid: u32,
|
||||||
csd_wrapper: Option<String>,
|
csd_wrapper: Option<String>,
|
||||||
|
|
||||||
|
reconnect_timeout: u32,
|
||||||
mtu: u32,
|
mtu: u32,
|
||||||
|
disable_ipv6: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VpnBuilder {
|
impl VpnBuilder {
|
||||||
@@ -95,18 +132,22 @@ impl VpnBuilder {
|
|||||||
Self {
|
Self {
|
||||||
server: server.to_string(),
|
server: server.to_string(),
|
||||||
cookie: cookie.to_string(),
|
cookie: cookie.to_string(),
|
||||||
user_agent: None,
|
|
||||||
script: None,
|
script: None,
|
||||||
|
|
||||||
|
user_agent: None,
|
||||||
os: None,
|
os: None,
|
||||||
|
|
||||||
|
certificate: None,
|
||||||
|
sslkey: None,
|
||||||
|
key_password: None,
|
||||||
|
|
||||||
csd_uid: 0,
|
csd_uid: 0,
|
||||||
csd_wrapper: None,
|
csd_wrapper: None,
|
||||||
mtu: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn user_agent<T: Into<Option<String>>>(mut self, user_agent: T) -> Self {
|
reconnect_timeout: 300,
|
||||||
self.user_agent = user_agent.into();
|
mtu: 0,
|
||||||
self
|
disable_ipv6: false,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn script<T: Into<Option<String>>>(mut self, script: T) -> Self {
|
pub fn script<T: Into<Option<String>>>(mut self, script: T) -> Self {
|
||||||
@@ -114,11 +155,31 @@ impl VpnBuilder {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn user_agent<T: Into<Option<String>>>(mut self, user_agent: T) -> Self {
|
||||||
|
self.user_agent = user_agent.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn os<T: Into<Option<String>>>(mut self, os: T) -> Self {
|
pub fn os<T: Into<Option<String>>>(mut self, os: T) -> Self {
|
||||||
self.os = os.into();
|
self.os = os.into();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn certificate<T: Into<Option<String>>>(mut self, certificate: T) -> Self {
|
||||||
|
self.certificate = certificate.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sslkey<T: Into<Option<String>>>(mut self, sslkey: T) -> Self {
|
||||||
|
self.sslkey = sslkey.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn key_password<T: Into<Option<String>>>(mut self, key_password: T) -> Self {
|
||||||
|
self.key_password = key_password.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn csd_uid(mut self, csd_uid: u32) -> Self {
|
pub fn csd_uid(mut self, csd_uid: u32) -> Self {
|
||||||
self.csd_uid = csd_uid;
|
self.csd_uid = csd_uid;
|
||||||
self
|
self
|
||||||
@@ -129,32 +190,58 @@ impl VpnBuilder {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn reconnect_timeout(mut self, reconnect_timeout: u32) -> Self {
|
||||||
|
self.reconnect_timeout = reconnect_timeout;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn mtu(mut self, mtu: u32) -> Self {
|
pub fn mtu(mut self, mtu: u32) -> Self {
|
||||||
self.mtu = mtu;
|
self.mtu = mtu;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build(self) -> Vpn {
|
pub fn disable_ipv6(mut self, disable_ipv6: bool) -> Self {
|
||||||
|
self.disable_ipv6 = disable_ipv6;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build(self) -> Result<Vpn, VpnError> {
|
||||||
|
let script = match self.script {
|
||||||
|
Some(script) => {
|
||||||
|
check_executable(&script).map_err(|e| VpnError::new(e.to_string()))?;
|
||||||
|
script
|
||||||
|
}
|
||||||
|
None => find_vpnc_script().ok_or_else(|| VpnError::new(String::from("Failed to find vpnc-script")))?,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(csd_wrapper) = &self.csd_wrapper {
|
||||||
|
check_executable(csd_wrapper).map_err(|e| VpnError::new(e.to_string()))?;
|
||||||
|
}
|
||||||
|
|
||||||
let user_agent = self.user_agent.unwrap_or_default();
|
let user_agent = self.user_agent.unwrap_or_default();
|
||||||
let script = self.script.or_else(find_default_vpnc_script).unwrap_or_default();
|
|
||||||
let os = self.os.unwrap_or("linux".to_string());
|
let os = self.os.unwrap_or("linux".to_string());
|
||||||
|
|
||||||
Vpn {
|
Ok(Vpn {
|
||||||
server: Self::to_cstring(&self.server),
|
server: Self::to_cstring(&self.server),
|
||||||
cookie: Self::to_cstring(&self.cookie),
|
cookie: Self::to_cstring(&self.cookie),
|
||||||
user_agent: Self::to_cstring(&user_agent),
|
user_agent: Self::to_cstring(&user_agent),
|
||||||
script: Self::to_cstring(&script),
|
script: Self::to_cstring(&script),
|
||||||
os: Self::to_cstring(&os),
|
os: Self::to_cstring(&os),
|
||||||
certificate: None,
|
|
||||||
|
certificate: self.certificate.as_deref().map(Self::to_cstring),
|
||||||
|
sslkey: self.sslkey.as_deref().map(Self::to_cstring),
|
||||||
|
key_password: self.key_password.as_deref().map(Self::to_cstring),
|
||||||
servercert: None,
|
servercert: None,
|
||||||
|
|
||||||
csd_uid: self.csd_uid,
|
csd_uid: self.csd_uid,
|
||||||
csd_wrapper: self.csd_wrapper.as_deref().map(Self::to_cstring),
|
csd_wrapper: self.csd_wrapper.as_deref().map(Self::to_cstring),
|
||||||
|
|
||||||
|
reconnect_timeout: self.reconnect_timeout,
|
||||||
mtu: self.mtu,
|
mtu: self.mtu,
|
||||||
|
disable_ipv6: self.disable_ipv6,
|
||||||
|
|
||||||
callback: Default::default(),
|
callback: Default::default(),
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_cstring(value: &str) -> CString {
|
fn to_cstring(value: &str) -> CString {
|
||||||
|
@@ -1,23 +0,0 @@
|
|||||||
use is_executable::IsExecutable;
|
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
const VPNC_SCRIPT_LOCATIONS: [&str; 5] = [
|
|
||||||
"/usr/local/share/vpnc-scripts/vpnc-script",
|
|
||||||
"/usr/local/sbin/vpnc-script",
|
|
||||||
"/usr/share/vpnc-scripts/vpnc-script",
|
|
||||||
"/usr/sbin/vpnc-script",
|
|
||||||
"/etc/vpnc/vpnc-script",
|
|
||||||
];
|
|
||||||
|
|
||||||
pub(crate) fn find_default_vpnc_script() -> Option<String> {
|
|
||||||
for location in VPNC_SCRIPT_LOCATIONS.iter() {
|
|
||||||
let path = Path::new(location);
|
|
||||||
if path.is_executable() {
|
|
||||||
return Some(location.to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log::warn!("vpnc-script not found");
|
|
||||||
|
|
||||||
None
|
|
||||||
}
|
|
@@ -6,6 +6,10 @@ install:
|
|||||||
install -Dm755 artifacts/usr/bin/gpauth $(DESTDIR)/usr/bin/gpauth
|
install -Dm755 artifacts/usr/bin/gpauth $(DESTDIR)/usr/bin/gpauth
|
||||||
install -Dm755 artifacts/usr/bin/gpgui-helper $(DESTDIR)/usr/bin/gpgui-helper
|
install -Dm755 artifacts/usr/bin/gpgui-helper $(DESTDIR)/usr/bin/gpgui-helper
|
||||||
|
|
||||||
|
if [ -f artifacts/usr/bin/gpgui ]; then \
|
||||||
|
install -Dm755 artifacts/usr/bin/gpgui $(DESTDIR)/usr/bin/gpgui; \
|
||||||
|
fi
|
||||||
|
|
||||||
install -Dm644 artifacts/usr/share/applications/gpgui.desktop $(DESTDIR)/usr/share/applications/gpgui.desktop
|
install -Dm644 artifacts/usr/share/applications/gpgui.desktop $(DESTDIR)/usr/share/applications/gpgui.desktop
|
||||||
install -Dm644 artifacts/usr/share/icons/hicolor/scalable/apps/gpgui.svg $(DESTDIR)/usr/share/icons/hicolor/scalable/apps/gpgui.svg
|
install -Dm644 artifacts/usr/share/icons/hicolor/scalable/apps/gpgui.svg $(DESTDIR)/usr/share/icons/hicolor/scalable/apps/gpgui.svg
|
||||||
install -Dm644 artifacts/usr/share/icons/hicolor/32x32/apps/gpgui.png $(DESTDIR)/usr/share/icons/hicolor/32x32/apps/gpgui.png
|
install -Dm644 artifacts/usr/share/icons/hicolor/32x32/apps/gpgui.png $(DESTDIR)/usr/share/icons/hicolor/32x32/apps/gpgui.png
|
||||||
|
@@ -3,7 +3,16 @@ Section: net
|
|||||||
Priority: optional
|
Priority: optional
|
||||||
Maintainer: Kevin Yue <k3vinyue@gmail.com>
|
Maintainer: Kevin Yue <k3vinyue@gmail.com>
|
||||||
Standards-Version: 4.1.4
|
Standards-Version: 4.1.4
|
||||||
Build-Depends: debhelper (>= 9), pkg-config, jq (>= 1), make (>= 4), openconnect (>= 8.20), libxml2, libsecret-1-0, libayatana-appindicator3-1, libwebkit2gtk-4.0-37, libgtk-3-0, gnome-keyring, @RUST@
|
Build-Depends: debhelper (>= 9),
|
||||||
|
pkg-config,
|
||||||
|
jq (>= 1),
|
||||||
|
make (>= 4),
|
||||||
|
libxml2,
|
||||||
|
libsecret-1-0,
|
||||||
|
libayatana-appindicator3-1,
|
||||||
|
gnome-keyring,
|
||||||
|
libwebkit2gtk-4.0-dev,
|
||||||
|
libopenconnect-dev (>= 8.20),@RUST@
|
||||||
Homepage: https://github.com/yuezk/GlobalProtect-openconnect
|
Homepage: https://github.com/yuezk/GlobalProtect-openconnect
|
||||||
|
|
||||||
Package: globalprotect-openconnect
|
Package: globalprotect-openconnect
|
||||||
|
14
packaging/deb/postrm
Executable file
14
packaging/deb/postrm
Executable file
@@ -0,0 +1,14 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
case "$1" in
|
||||||
|
purge|remove|upgrade)
|
||||||
|
# Remove the gpgui binary downloaded at runtime
|
||||||
|
rm -f /usr/bin/gpgui
|
||||||
|
;;
|
||||||
|
|
||||||
|
*)
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
exit 0
|
@@ -1,6 +0,0 @@
|
|||||||
#!/usr/bin/make -f
|
|
||||||
|
|
||||||
export OFFLINE = @OFFLINE@ BUILD_FE=0
|
|
||||||
|
|
||||||
%:
|
|
||||||
dh $@
|
|
7
packaging/deb/rules.in
Executable file
7
packaging/deb/rules.in
Executable file
@@ -0,0 +1,7 @@
|
|||||||
|
#!/usr/bin/make -f
|
||||||
|
|
||||||
|
export OFFLINE = @OFFLINE@
|
||||||
|
export BUILD_FE = 0
|
||||||
|
|
||||||
|
%:
|
||||||
|
dh $@ --no-parallel
|
@@ -15,9 +15,9 @@ BuildRequires: jq
|
|||||||
BuildRequires: pkg-config
|
BuildRequires: pkg-config
|
||||||
BuildRequires: openconnect-devel
|
BuildRequires: openconnect-devel
|
||||||
BuildRequires: openssl-devel
|
BuildRequires: openssl-devel
|
||||||
BuildRequires: curl
|
|
||||||
BuildRequires: wget
|
BuildRequires: wget
|
||||||
BuildRequires: file
|
BuildRequires: file
|
||||||
|
BuildRequires: perl
|
||||||
|
|
||||||
BuildRequires: (webkit2gtk4.0-devel or webkit2gtk3-soup2-devel)
|
BuildRequires: (webkit2gtk4.0-devel or webkit2gtk3-soup2-devel)
|
||||||
BuildRequires: (libappindicator-gtk3-devel or libappindicator3-1)
|
BuildRequires: (libappindicator-gtk3-devel or libappindicator3-1)
|
||||||
@@ -34,6 +34,9 @@ A GUI for GlobalProtect VPN, based on OpenConnect, supports the SSO authenticati
|
|||||||
%prep
|
%prep
|
||||||
%setup
|
%setup
|
||||||
|
|
||||||
|
%postun
|
||||||
|
rm -f %{_bindir}/gpgui
|
||||||
|
|
||||||
%build
|
%build
|
||||||
# The injected RUSTFLAGS could fail the build
|
# The injected RUSTFLAGS could fail the build
|
||||||
unset RUSTFLAGS
|
unset RUSTFLAGS
|
||||||
@@ -44,10 +47,7 @@ make build OFFLINE=@OFFLINE@ BUILD_FE=0
|
|||||||
|
|
||||||
%files
|
%files
|
||||||
%defattr(-,root,root)
|
%defattr(-,root,root)
|
||||||
%{_bindir}/gpclient
|
%{_bindir}/*
|
||||||
%{_bindir}/gpservice
|
|
||||||
%{_bindir}/gpauth
|
|
||||||
%{_bindir}/gpgui-helper
|
|
||||||
%{_datadir}/applications/gpgui.desktop
|
%{_datadir}/applications/gpgui.desktop
|
||||||
%{_datadir}/icons/hicolor/32x32/apps/gpgui.png
|
%{_datadir}/icons/hicolor/32x32/apps/gpgui.png
|
||||||
%{_datadir}/icons/hicolor/128x128/apps/gpgui.png
|
%{_datadir}/icons/hicolor/128x128/apps/gpgui.png
|
||||||
|
51
scripts/gh-release.sh
Executable file
51
scripts/gh-release.sh
Executable file
@@ -0,0 +1,51 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Usage: ./scripts/gh-release.sh <tag>
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
REPO="yuezk/GlobalProtect-openconnect"
|
||||||
|
TAG=$1
|
||||||
|
|
||||||
|
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
|
||||||
|
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||||
|
|
||||||
|
RELEASE_NOTES="Release $TAG"
|
||||||
|
|
||||||
|
if [ -z "$TAG" ]; then
|
||||||
|
echo "Usage: ./scripts/gh-release.sh <tag>"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For snapshot release, we don't create a release, just clear the existing assets and upload new ones.
|
||||||
|
# This is to avoid notification spam.
|
||||||
|
release_snapshot() {
|
||||||
|
RELEASE_NOTES='**!!! DO NOT USE THIS RELEASE IN PRODUCTION !!!**'
|
||||||
|
|
||||||
|
# Get the existing assets
|
||||||
|
gh -R "$REPO" release view "$TAG" --json assets --jq '.assets[].name' \
|
||||||
|
| xargs -I {} gh -R "$REPO" release delete-asset "$TAG" {} --yes
|
||||||
|
|
||||||
|
echo "Uploading new assets..."
|
||||||
|
gh -R "$REPO" release upload "$TAG" \
|
||||||
|
"$PROJECT_DIR"/.build/artifacts/artifact-source/* \
|
||||||
|
"$PROJECT_DIR"/.build/artifacts/artifact-gpgui-*/*
|
||||||
|
}
|
||||||
|
|
||||||
|
release_tag() {
|
||||||
|
echo "Removing existing release..."
|
||||||
|
gh -R "$REPO" release delete $TAG --yes --cleanup-tag || true
|
||||||
|
|
||||||
|
echo "Creating release..."
|
||||||
|
gh -R "$REPO" release create $TAG \
|
||||||
|
--title "$TAG" \
|
||||||
|
--notes "$RELEASE_NOTES" \
|
||||||
|
"$PROJECT_DIR"/.build/artifacts/artifact-source/* \
|
||||||
|
"$PROJECT_DIR"/.build/artifacts/artifact-gpgui-*/*
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ $TAG == *"snapshot" ]]; then
|
||||||
|
release_snapshot
|
||||||
|
else
|
||||||
|
release_tag
|
||||||
|
fi
|
Reference in New Issue
Block a user