mirror of
https://github.com/yuezk/GlobalProtect-openconnect.git
synced 2025-05-20 07:26:58 -04:00
Compare commits
49 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
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 |
79
.github/workflows/build.yaml
vendored
79
.github/workflows/build.yaml
vendored
@@ -9,8 +9,10 @@ on:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
- hotfix/*
|
||||
- feature/*
|
||||
- release/*
|
||||
tags:
|
||||
- latest
|
||||
- v*.*.*
|
||||
jobs:
|
||||
# Include arm64 if ref is a tag
|
||||
@@ -23,9 +25,9 @@ jobs:
|
||||
id: set-matrix
|
||||
run: |
|
||||
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
|
||||
echo "matrix=[\"ubuntu-latest\"]" >> $GITHUB_OUTPUT
|
||||
echo 'matrix=[{"runner": "ubuntu-latest", "arch": "amd64"}]' >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
tarball:
|
||||
@@ -42,10 +44,15 @@ jobs:
|
||||
with:
|
||||
token: ${{ secrets.GH_PAT }}
|
||||
repository: yuezk/GlobalProtect-openconnect
|
||||
ref: ${{ github.ref }}
|
||||
path: source/gp
|
||||
- name: Create tarball
|
||||
run: |
|
||||
cd source/gp
|
||||
# Generate the SNAPSHOT file for non-tagged commits
|
||||
if [[ "${{ github.ref }}" != "refs/tags/"* ]]; then
|
||||
touch SNAPSHOT
|
||||
fi
|
||||
make tarball
|
||||
- name: Upload tarball
|
||||
uses: actions/upload-artifact@v3
|
||||
@@ -61,21 +68,42 @@ jobs:
|
||||
- tarball
|
||||
strategy:
|
||||
matrix:
|
||||
os: ${{fromJson(needs.setup-matrix.outputs.matrix)}}
|
||||
runs-on: ${{ matrix.os }}
|
||||
# Only build gp on amd64, as the arm64 package will be built in release.yaml
|
||||
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:
|
||||
- name: Prepare workspace
|
||||
run: rm -rf build-gp && mkdir build-gp
|
||||
run: |
|
||||
rm -rf build-gp-${{ matrix.package }}
|
||||
mkdir -p build-gp-${{ matrix.package }}
|
||||
- name: Download tarball
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: artifact-source
|
||||
path: build-gp
|
||||
path: build-gp-${{ matrix.package }}
|
||||
- name: Docker Login
|
||||
run: echo ${{ secrets.DOCKER_HUB_TOKEN }} | docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} --password-stdin
|
||||
- name: Build gp in Docker
|
||||
- name: Build ${{ matrix.package }} package in Docker
|
||||
run: |
|
||||
docker run --rm -v $(pwd)/build-gp:/gp yuezk/gpdev:gp-builder
|
||||
docker run --rm \
|
||||
-v $(pwd)/build-gp-${{ matrix.package }}:/${{ matrix.package }} \
|
||||
yuezk/gpdev:${{ matrix.package }}-builder
|
||||
- name: Install ${{ matrix.package }} package in Docker
|
||||
run: |
|
||||
docker run --rm \
|
||||
-e GPGUI_INSTALLED=0 \
|
||||
-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
|
||||
with:
|
||||
name: artifact-gp-${{ matrix.package }}-${{ matrix.os.arch }}
|
||||
if-no-files-found: error
|
||||
path: |
|
||||
build-gp-${{ matrix.package }}/artifacts/*
|
||||
|
||||
build-gpgui:
|
||||
needs:
|
||||
@@ -83,7 +111,8 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os: ${{fromJson(needs.setup-matrix.outputs.matrix)}}
|
||||
runs-on: ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os.runner }}
|
||||
name: build-gpgui (${{ matrix.os.arch }})
|
||||
steps:
|
||||
- uses: pnpm/action-setup@v2
|
||||
with:
|
||||
@@ -95,12 +124,14 @@ jobs:
|
||||
with:
|
||||
token: ${{ secrets.GH_PAT }}
|
||||
repository: yuezk/GlobalProtect-openconnect
|
||||
ref: ${{ github.ref }}
|
||||
path: gpgui-source/gp
|
||||
- name: Checkout gpgui
|
||||
- name: Checkout gpgui@${{ github.ref_name }}
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
token: ${{ secrets.GH_PAT }}
|
||||
repository: yuezk/gpgui
|
||||
ref: ${{ github.ref_name }}
|
||||
path: gpgui-source/gpgui
|
||||
- name: Tarball
|
||||
run: |
|
||||
@@ -120,16 +151,17 @@ jobs:
|
||||
- name: Upload gpgui
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: artifact-gpgui-${{ matrix.os }}
|
||||
name: artifact-gpgui-${{ matrix.os.arch }}
|
||||
if-no-files-found: error
|
||||
path: |
|
||||
gpgui-source/*.bin.tar.xz
|
||||
gpgui-source/*.bin.tar.xz.sha256
|
||||
|
||||
gh-release:
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
if: ${{ github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/') }}
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- tarball
|
||||
- build-gp
|
||||
- build-gpgui
|
||||
|
||||
@@ -141,10 +173,17 @@ jobs:
|
||||
with:
|
||||
path: gh-release
|
||||
- name: Create GH release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
token: ${{ secrets.GH_PAT }}
|
||||
prerelease: ${{ contains(github.ref, 'latest') }}
|
||||
fail_on_unmatched_files: true
|
||||
files: |
|
||||
gh-release/artifact-*/*
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_PAT }}
|
||||
RELEASE_TAG: ${{ github.ref == 'refs/heads/dev' && 'snapshot' || github.ref_name }}
|
||||
REPO: ${{ github.repository }}
|
||||
NOTES: ${{ github.ref == 'refs/heads/dev' && '**!!! DO NOT USE THIS RELEASE IN PRODUCTION !!!**' || format('Release {0}', github.ref_name) }}
|
||||
run: |
|
||||
gh -R "$REPO" release delete $RELEASE_TAG --yes --cleanup-tag || true
|
||||
gh -R "$REPO" release create $RELEASE_TAG \
|
||||
--title "$RELEASE_TAG" \
|
||||
--notes "$NOTES" \
|
||||
${{ github.ref == 'refs/heads/dev' && '--target dev' || '' }} \
|
||||
${{ github.ref == 'refs/heads/dev' && '--prerelease' || '' }} \
|
||||
gh-release/artifact-source/* \
|
||||
gh-release/artifact-gpgui-*/*
|
||||
|
11
.github/workflows/release.yaml
vendored
11
.github/workflows/release.yaml
vendored
@@ -109,11 +109,16 @@ jobs:
|
||||
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
|
||||
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 \
|
||||
docker run --rm \
|
||||
-v $(pwd)/build-${{ matrix.package }}:/${{ matrix.package }} \
|
||||
yuezk/gpdev:${{ matrix.package }}-builder \
|
||||
bash install.sh
|
||||
|
||||
- name: Upload ${{ matrix.package }} package
|
||||
@@ -140,7 +145,7 @@ jobs:
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
token: ${{ secrets.GH_PAT }}
|
||||
prerelease: ${{ contains(github.ref, 'latest') }}
|
||||
prerelease: ${{ contains(github.ref, 'snapshot') }}
|
||||
fail_on_unmatched_files: true
|
||||
tag_name: ${{ inputs.tag }}
|
||||
files: |
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -7,3 +7,4 @@
|
||||
|
||||
.cargo
|
||||
.build
|
||||
SNAPSHOT
|
||||
|
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"cSpell.words": [
|
||||
"authcookie",
|
||||
"badssl",
|
||||
"bincode",
|
||||
"chacha",
|
||||
"clientos",
|
||||
@@ -25,7 +26,9 @@
|
||||
"LOGNAME",
|
||||
"oneshot",
|
||||
"openconnect",
|
||||
"pkcs",
|
||||
"pkexec",
|
||||
"pkey",
|
||||
"Prelogin",
|
||||
"prelogon",
|
||||
"prelogonuserauthcookie",
|
||||
@@ -35,6 +38,7 @@
|
||||
"rspc",
|
||||
"servercert",
|
||||
"specta",
|
||||
"sslkey",
|
||||
"sysinfo",
|
||||
"tanstack",
|
||||
"tauri",
|
||||
|
75
Cargo.lock
generated
75
Cargo.lock
generated
@@ -252,6 +252,12 @@ version = "0.21.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
@@ -564,7 +570,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "common"
|
||||
version = "2.1.1"
|
||||
version = "2.3.1"
|
||||
dependencies = [
|
||||
"is_executable",
|
||||
]
|
||||
@@ -1430,7 +1436,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "gpapi"
|
||||
version = "2.1.1"
|
||||
version = "2.3.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64 0.21.5",
|
||||
@@ -1440,6 +1446,8 @@ dependencies = [
|
||||
"log",
|
||||
"md5",
|
||||
"open",
|
||||
"openssl",
|
||||
"pem",
|
||||
"redact-engine",
|
||||
"regex",
|
||||
"reqwest",
|
||||
@@ -1462,13 +1470,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "gpauth"
|
||||
version = "2.1.1"
|
||||
version = "2.3.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"compile-time",
|
||||
"env_logger",
|
||||
"gpapi",
|
||||
"html-escape",
|
||||
"log",
|
||||
"regex",
|
||||
"serde_json",
|
||||
@@ -1482,7 +1491,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "gpclient"
|
||||
version = "2.1.1"
|
||||
version = "2.3.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
@@ -1504,7 +1513,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "gpgui-helper"
|
||||
version = "2.1.1"
|
||||
version = "2.3.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
@@ -1522,7 +1531,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "gpservice"
|
||||
version = "2.1.1"
|
||||
version = "2.3.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -1598,9 +1607,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.3.24"
|
||||
version = "0.3.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9"
|
||||
checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"fnv",
|
||||
@@ -1617,9 +1626,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.4.2"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "31d030e59af851932b72ceebadf4a2b5986dba4c3b99dd2493f8273a0f151943"
|
||||
checksum = "816ec7294445779408f36fe57bc5b7fc1cf59664059096c65f905c1c61f58069"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"fnv",
|
||||
@@ -1673,6 +1682,15 @@ version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "html5ever"
|
||||
version = "0.26.0"
|
||||
@@ -1777,7 +1795,7 @@ dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2 0.3.24",
|
||||
"h2 0.3.26",
|
||||
"http 0.2.11",
|
||||
"http-body 0.4.6",
|
||||
"httparse",
|
||||
@@ -1800,7 +1818,7 @@ dependencies = [
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
"h2 0.4.2",
|
||||
"h2 0.4.4",
|
||||
"http 1.0.0",
|
||||
"http-body 1.0.0",
|
||||
"httparse",
|
||||
@@ -2527,7 +2545,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openconnect"
|
||||
version = "2.1.1"
|
||||
version = "2.3.1"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"common",
|
||||
@@ -2660,6 +2678,16 @@ version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.1"
|
||||
@@ -3157,7 +3185,7 @@ dependencies = [
|
||||
"encoding_rs",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2 0.3.24",
|
||||
"h2 0.3.26",
|
||||
"http 0.2.11",
|
||||
"http-body 0.4.6",
|
||||
"hyper 0.14.28",
|
||||
@@ -4484,6 +4512,12 @@ version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
||||
|
||||
[[package]]
|
||||
name = "utf8-width"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.1"
|
||||
@@ -4590,6 +4624,12 @@ version = "0.11.0+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||
|
||||
[[package]]
|
||||
name = "wasite"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.89"
|
||||
@@ -4766,11 +4806,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "whoami"
|
||||
version = "1.4.1"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22fc3756b8a9133049b26c7f61ab35416c130e8c09b660f5b3958b446f52cc50"
|
||||
checksum = "a44ab49fad634e88f55bf8f9bb3abd2f27d7204172a112c7c9987e01c1c94ea9"
|
||||
dependencies = [
|
||||
"wasm-bindgen",
|
||||
"redox_syscall",
|
||||
"wasite",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
|
@@ -5,7 +5,7 @@ members = ["crates/*", "apps/gpclient", "apps/gpservice", "apps/gpauth", "apps/g
|
||||
|
||||
[workspace.package]
|
||||
rust-version = "1.70"
|
||||
version = "2.1.1"
|
||||
version = "2.3.1"
|
||||
authors = ["Kevin Yue <k3vinyue@gmail.com>"]
|
||||
homepage = "https://github.com/yuezk/GlobalProtect-openconnect"
|
||||
edition = "2021"
|
||||
@@ -22,6 +22,8 @@ is_executable = "1.0"
|
||||
log = "0.4"
|
||||
regex = "1"
|
||||
reqwest = { version = "0.11", features = ["native-tls-vendored", "json"] }
|
||||
openssl = "0.10"
|
||||
pem = "3"
|
||||
roxmltree = "0.18"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
|
12
Makefile
12
Makefile
@@ -15,6 +15,13 @@ PUBLISH ?= 0
|
||||
|
||||
export DEBEMAIL = k3vinyue@gmail.com
|
||||
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
|
||||
|
||||
@@ -61,7 +68,8 @@ download-gui:
|
||||
if [ $(INCLUDE_GUI) -eq 1 ]; then \
|
||||
echo "Downloading GlobalProtect GUI..."; \
|
||||
mkdir -p .build/gpgui; \
|
||||
curl -sSL https://github.com/yuezk/GlobalProtect-openconnect/releases/download/v$(VERSION)/gpgui_$(VERSION)_$(shell uname -m).bin.tar.xz -o .build/gpgui/gpgui_$(VERSION)_x$(shell uname -m).bin.tar.xz; \
|
||||
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)"; \
|
||||
@@ -195,7 +203,7 @@ init-rpm: clean-rpm
|
||||
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/@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/@DATE@/$(shell LC_ALL=en.US date -u "+%a %b %e %T %Z %Y")/g" .build/rpm/globalprotect-openconnect.changes
|
||||
|
58
README.md
58
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 the FIDO2 authentication (e.g., YubiKey)
|
||||
- [x] Support authentication using default browser
|
||||
- [x] Support client certificate authentication
|
||||
- [x] Support multiple portals
|
||||
- [x] Support gateway selection
|
||||
- [x] Support connect gateway directly
|
||||
@@ -43,6 +44,12 @@ Options:
|
||||
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
|
||||
|
||||
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.
|
||||
@@ -55,7 +62,7 @@ The GUI version is also available after you installed it. You can launch it from
|
||||
|
||||
### 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
|
||||
@@ -68,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`.
|
||||
|
||||
#### 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
|
||||
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
|
||||
@@ -120,6 +144,30 @@ Download the latest RPM package from [releases](https://github.com/yuezk/GlobalP
|
||||
```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
|
||||
|
||||
@@ -151,6 +199,8 @@ You can also build the client from source, steps are as follows:
|
||||
|
||||
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)).
|
||||
|
||||
2. How to deal with error `(gpauth:18869): Gtk-WARNING **: 10:33:37.566: cannot open display:`
|
||||
|
@@ -8,7 +8,11 @@ license.workspace = true
|
||||
tauri-build = { version = "1.5", features = [] }
|
||||
|
||||
[dependencies]
|
||||
gpapi = { path = "../../crates/gpapi", features = ["tauri", "clap"] }
|
||||
gpapi = { path = "../../crates/gpapi", features = [
|
||||
"tauri",
|
||||
"clap",
|
||||
"browser-auth",
|
||||
] }
|
||||
anyhow.workspace = true
|
||||
clap.workspace = true
|
||||
env_logger.workspace = true
|
||||
@@ -18,6 +22,7 @@ serde_json.workspace = true
|
||||
tokio.workspace = true
|
||||
tokio-util.workspace = true
|
||||
tempfile.workspace = true
|
||||
html-escape = "0.2.13"
|
||||
webkit2gtk = "0.18.2"
|
||||
tauri = { workspace = true, features = ["http-all"] }
|
||||
compile-time.workspace = true
|
||||
|
@@ -7,6 +7,7 @@ use std::{
|
||||
use anyhow::bail;
|
||||
use gpapi::{
|
||||
auth::SamlAuthData,
|
||||
error::AuthDataParseError,
|
||||
gp_params::GpParams,
|
||||
portal::{prelogin, Prelogin},
|
||||
utils::{redact::redact_uri, window::WindowExt},
|
||||
@@ -184,6 +185,10 @@ impl<'a> AuthWindow<'a> {
|
||||
}
|
||||
|
||||
info!("Loaded uri: {}", redact_uri(&uri));
|
||||
if uri.starts_with("globalprotectcallback:") {
|
||||
return;
|
||||
}
|
||||
|
||||
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| {
|
||||
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
|
||||
// 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.
|
||||
@@ -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)
|
||||
where
|
||||
F: FnOnce(AuthResult) + Send + 'static,
|
||||
F: FnOnce(Result<SamlAuthData, AuthDataParseError>) + Send + 'static,
|
||||
{
|
||||
main_resource.data(Cancellable::NONE, |data| match data {
|
||||
Ok(data) => {
|
||||
@@ -348,53 +355,41 @@ where
|
||||
}
|
||||
Err(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") {
|
||||
info!("Found 'Temporarily Unavailable' in HTML, auth failed");
|
||||
return Err(AuthDataError::Invalid);
|
||||
return Err(AuthDataParseError::Invalid);
|
||||
}
|
||||
|
||||
match parse_xml_tag(html, "saml-auth-status") {
|
||||
Some(saml_status) if saml_status == "1" => {
|
||||
let username = parse_xml_tag(html, "saml-username");
|
||||
let prelogin_cookie = parse_xml_tag(html, "prelogin-cookie");
|
||||
let portal_userauthcookie = parse_xml_tag(html, "portal-userauthcookie");
|
||||
SamlAuthData::from_html(html).or_else(|err| {
|
||||
if let Some(gpcallback) = extract_gpcallback(html) {
|
||||
info!("Found gpcallback from html...");
|
||||
SamlAuthData::from_gpcallback(&gpcallback)
|
||||
} else {
|
||||
Err(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if SamlAuthData::check(&username, &prelogin_cookie, &portal_userauthcookie) {
|
||||
return Ok(SamlAuthData::new(
|
||||
username.unwrap(),
|
||||
prelogin_cookie,
|
||||
portal_userauthcookie,
|
||||
));
|
||||
}
|
||||
|
||||
info!("Found invalid auth data in HTML");
|
||||
Err(AuthDataError::Invalid)
|
||||
}
|
||||
Some(status) => {
|
||||
info!("Found invalid SAML status {} in HTML", status);
|
||||
Err(AuthDataError::Invalid)
|
||||
}
|
||||
None => {
|
||||
info!("No auth data found in HTML");
|
||||
Err(AuthDataError::NotFound)
|
||||
}
|
||||
}
|
||||
fn extract_gpcallback(html: &str) -> Option<String> {
|
||||
let re = Regex::new(r#"globalprotectcallback:[^"]+"#).unwrap();
|
||||
re.captures(html)
|
||||
.and_then(|captures| captures.get(0))
|
||||
.map(|m| html_escape::decode_html_entities(m.as_str()).to_string())
|
||||
}
|
||||
|
||||
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");
|
||||
send_auth_result(&auth_result_tx, Err(AuthDataError::Invalid));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let response = main_resource.response().unwrap();
|
||||
info!("Trying to read auth data from response headers...");
|
||||
|
||||
match read_auth_data_from_headers(&response) {
|
||||
@@ -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| {
|
||||
// Since we have already found invalid auth data in headers, which means this could be the `/SAML20/SP/ACS` endpoint
|
||||
// any error result from body should be considered as invalid, and trigger a retry
|
||||
let auth_result = auth_result.map_err(|_| AuthDataError::Invalid);
|
||||
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);
|
||||
});
|
||||
}
|
||||
Err(AuthDataError::NotFound) => {
|
||||
info!("No auth data found in headers, trying to read from body...");
|
||||
let url = main_resource.uri().unwrap_or("".into());
|
||||
let is_acs_endpoint = url.contains("/SAML20/SP/ACS");
|
||||
|
||||
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| {
|
||||
// If the endpoint is `/SAML20/SP/ACS` and no auth data found in body, it should be considered as invalid
|
||||
let auth_result = auth_result.map_err(|err| {
|
||||
if matches!(err, AuthDataError::NotFound) && is_acs_endpoint {
|
||||
AuthDataError::Invalid
|
||||
info!("Failed to read auth data from body: {}", err);
|
||||
|
||||
if !is_acs_endpoint && matches!(err, AuthDataParseError::NotFound) {
|
||||
AuthDataError::NotFound
|
||||
} 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<()> {
|
||||
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))
|
||||
}
|
||||
|
||||
#[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},
|
||||
clap::args::Os,
|
||||
gp_params::{ClientOs, GpParams},
|
||||
process::browser_authenticator::BrowserAuthenticator,
|
||||
utils::{normalize_server, openssl},
|
||||
GP_USER_AGENT,
|
||||
};
|
||||
@@ -37,6 +38,8 @@ struct Cli {
|
||||
ignore_tls_errors: bool,
|
||||
#[arg(long)]
|
||||
clean: bool,
|
||||
#[arg(long)]
|
||||
default_browser: bool,
|
||||
}
|
||||
|
||||
impl Cli {
|
||||
@@ -56,6 +59,15 @@ impl Cli {
|
||||
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);
|
||||
|
||||
let app = create_app(self.clone())?;
|
||||
|
@@ -1,4 +1,4 @@
|
||||
use std::{fs, sync::Arc};
|
||||
use std::{cell::RefCell, fs, sync::Arc};
|
||||
|
||||
use clap::Args;
|
||||
use common::vpn_utils::find_csd_wrapper;
|
||||
@@ -6,21 +6,22 @@ use gpapi::{
|
||||
clap::args::Os,
|
||||
credential::{Credential, PasswordCredential},
|
||||
error::PortalError,
|
||||
gateway::gateway_login,
|
||||
gateway::{gateway_login, GatewayLogin},
|
||||
gp_params::{ClientOs, GpParams},
|
||||
portal::{prelogin, retrieve_config, Prelogin},
|
||||
process::{
|
||||
auth_launcher::SamlAuthLauncher,
|
||||
users::{get_non_root_user, get_user_by_name},
|
||||
},
|
||||
utils::shutdown_signal,
|
||||
utils::{request::RequestIdentityError, shutdown_signal},
|
||||
GP_USER_AGENT,
|
||||
};
|
||||
use inquire::{Password, PasswordDisplayMode, Select, Text};
|
||||
use log::info;
|
||||
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)]
|
||||
pub(crate) struct ConnectArgs {
|
||||
@@ -32,6 +33,8 @@ pub(crate) struct ConnectArgs {
|
||||
user: Option<String>,
|
||||
#[arg(long, short, help = "The VPNC script to use")]
|
||||
script: Option<String>,
|
||||
#[arg(long, help = "Connect the server as a gateway, instead of a portal")]
|
||||
as_gateway: bool,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
@@ -39,14 +42,29 @@ pub(crate) struct ConnectArgs {
|
||||
)]
|
||||
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")]
|
||||
csd_user: Option<String>,
|
||||
|
||||
#[arg(long, help = "Same as the '--csd-wrapper' option in the openconnect command")]
|
||||
csd_wrapper: Option<String>,
|
||||
|
||||
#[arg(long, default_value = "300", help = "Reconnection retry timeout in seconds")]
|
||||
reconnect_timeout: u32,
|
||||
#[arg(short, long, help = "Request MTU from server (legacy servers only)")]
|
||||
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")]
|
||||
user_agent: String,
|
||||
@@ -58,6 +76,8 @@ pub(crate) struct ConnectArgs {
|
||||
hidpi: bool,
|
||||
#[arg(long, help = "Do not reuse the remembered authentication cookie")]
|
||||
clean: bool,
|
||||
#[arg(long, help = "Use the default browser to authenticate")]
|
||||
default_browser: bool,
|
||||
}
|
||||
|
||||
impl ConnectArgs {
|
||||
@@ -77,11 +97,16 @@ impl ConnectArgs {
|
||||
pub(crate) struct ConnectHandler<'a> {
|
||||
args: &'a ConnectArgs,
|
||||
shared_args: &'a SharedArgs,
|
||||
latest_key_password: RefCell<Option<String>>,
|
||||
}
|
||||
|
||||
impl<'a> ConnectHandler<'a> {
|
||||
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 {
|
||||
@@ -90,11 +115,52 @@ impl<'a> ConnectHandler<'a> {
|
||||
.client_os(ClientOs::from(&self.args.os))
|
||||
.os_version(self.args.os_version())
|
||||
.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()
|
||||
}
|
||||
|
||||
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 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 {
|
||||
return Ok(());
|
||||
@@ -103,10 +169,15 @@ impl<'a> ConnectHandler<'a> {
|
||||
info!("Failed to connect portal with prelogin: {}", err);
|
||||
if err.root_cause().downcast_ref::<PortalError>().is_some() {
|
||||
info!("Trying the gateway authentication workflow...");
|
||||
return self.connect_gateway_with_prelogin(server).await;
|
||||
}
|
||||
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<()> {
|
||||
@@ -141,7 +212,7 @@ impl<'a> ConnectHandler<'a> {
|
||||
let gateway = selected_gateway.server();
|
||||
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,
|
||||
Err(err) => {
|
||||
info!("Gateway login failed: {}", err);
|
||||
@@ -153,7 +224,7 @@ impl<'a> ConnectHandler<'a> {
|
||||
}
|
||||
|
||||
async fn connect_gateway_with_prelogin(&self, gateway: &str) -> anyhow::Result<()> {
|
||||
info!("Treat the portal as the gateway, connecting...");
|
||||
info!("Performing the gateway authentication...");
|
||||
|
||||
let mut gp_params = self.build_gp_params();
|
||||
gp_params.set_is_gateway(true);
|
||||
@@ -161,11 +232,28 @@ impl<'a> ConnectHandler<'a> {
|
||||
let prelogin = prelogin(gateway, &gp_params).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
|
||||
}
|
||||
|
||||
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<()> {
|
||||
let mtu = self.args.mtu.unwrap_or(0);
|
||||
let csd_uid = get_csd_uid(&self.args.csd_user)?;
|
||||
@@ -180,9 +268,14 @@ impl<'a> ConnectHandler<'a> {
|
||||
let vpn = Vpn::builder(gateway, cookie)
|
||||
.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_wrapper(csd_wrapper)
|
||||
.reconnect_timeout(self.args.reconnect_timeout)
|
||||
.mtu(mtu)
|
||||
.disable_ipv6(self.args.disable_ipv6)
|
||||
.build()?;
|
||||
|
||||
let vpn = Arc::new(vpn);
|
||||
@@ -210,7 +303,9 @@ impl<'a> ConnectHandler<'a> {
|
||||
|
||||
match 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)
|
||||
.saml_request(prelogin.saml_request())
|
||||
.user_agent(&self.args.user_agent)
|
||||
@@ -220,8 +315,21 @@ impl<'a> ConnectHandler<'a> {
|
||||
.fix_openssl(self.shared_args.fix_openssl)
|
||||
.ignore_tls_errors(self.shared_args.ignore_tls_errors)
|
||||
.clean(self.args.clean)
|
||||
.default_browser(use_default_browser)
|
||||
.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) => {
|
||||
let prefix = if is_gateway { "Gateway" } else { "Portal" };
|
||||
@@ -244,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() {
|
||||
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 directories::ProjectDirs;
|
||||
@@ -7,6 +7,9 @@ use gpapi::{
|
||||
utils::{endpoint::http_endpoint, env_file, shutdown_signal},
|
||||
};
|
||||
use log::info;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
|
||||
use crate::GP_CLIENT_PORT_FILE;
|
||||
|
||||
#[derive(Args)]
|
||||
pub(crate) struct LaunchGuiArgs {
|
||||
@@ -78,11 +81,21 @@ impl<'a> LaunchGuiHandler<'a> {
|
||||
}
|
||||
|
||||
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?;
|
||||
|
||||
reqwest::Client::default()
|
||||
.post(format!("{}/auth-data", service_endpoint))
|
||||
.json(&auth_data)
|
||||
.body(auth_data.to_string())
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?;
|
||||
@@ -90,6 +103,15 @@ async fn feed_auth_data(auth_data: &str) -> anyhow::Result<()> {
|
||||
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<()> {
|
||||
let service_endpoint = http_endpoint().await?;
|
||||
|
||||
|
@@ -4,6 +4,7 @@ mod disconnect;
|
||||
mod launch_gui;
|
||||
|
||||
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]
|
||||
async fn main() {
|
||||
|
@@ -31,6 +31,6 @@
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"prettier": "3.1.0",
|
||||
"typescript": "^5.0.2",
|
||||
"vite": "^4.5.2"
|
||||
"vite": "^4.5.3"
|
||||
}
|
||||
}
|
||||
|
14
apps/gpgui-helper/pnpm-lock.yaml
generated
14
apps/gpgui-helper/pnpm-lock.yaml
generated
@@ -48,7 +48,7 @@ devDependencies:
|
||||
version: 6.12.0(eslint@8.54.0)(typescript@5.0.2)
|
||||
'@vitejs/plugin-react':
|
||||
specifier: ^4.0.3
|
||||
version: 4.0.3(vite@4.5.2)
|
||||
version: 4.0.3(vite@4.5.3)
|
||||
eslint:
|
||||
specifier: ^8.54.0
|
||||
version: 8.54.0
|
||||
@@ -68,8 +68,8 @@ devDependencies:
|
||||
specifier: ^5.0.2
|
||||
version: 5.0.2
|
||||
vite:
|
||||
specifier: ^4.5.2
|
||||
version: 4.5.2(@types/node@20.8.10)
|
||||
specifier: ^4.5.3
|
||||
version: 4.5.3(@types/node@20.8.10)
|
||||
|
||||
packages:
|
||||
|
||||
@@ -1229,7 +1229,7 @@ packages:
|
||||
resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==}
|
||||
dev: true
|
||||
|
||||
/@vitejs/plugin-react@4.0.3(vite@4.5.2):
|
||||
/@vitejs/plugin-react@4.0.3(vite@4.5.3):
|
||||
resolution: {integrity: sha512-pwXDog5nwwvSIzwrvYYmA2Ljcd/ZNlcsSG2Q9CNDBwnsd55UGAyr2doXtB5j+2uymRCnCfExlznzzSFbBRcoCg==}
|
||||
engines: {node: ^14.18.0 || >=16.0.0}
|
||||
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-source': 7.22.5(@babel/core@7.23.2)
|
||||
react-refresh: 0.14.0
|
||||
vite: 4.5.2(@types/node@20.8.10)
|
||||
vite: 4.5.3(@types/node@20.8.10)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
@@ -2979,8 +2979,8 @@ packages:
|
||||
punycode: 2.3.1
|
||||
dev: true
|
||||
|
||||
/vite@4.5.2(@types/node@20.8.10):
|
||||
resolution: {integrity: sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==}
|
||||
/vite@4.5.3(@types/node@20.8.10):
|
||||
resolution: {integrity: sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==}
|
||||
engines: {node: ^14.18.0 || >=16.0.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
|
@@ -9,6 +9,12 @@ use tauri::{Manager, Window};
|
||||
|
||||
use crate::downloader::{ChecksumFetcher, FileDownloader};
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
const SNAPSHOT: &str = match option_env!("SNAPSHOT") {
|
||||
Some(val) => val,
|
||||
None => "false"
|
||||
};
|
||||
|
||||
pub struct ProgressNotifier {
|
||||
win: Window,
|
||||
}
|
||||
@@ -81,9 +87,13 @@ impl GuiUpdater {
|
||||
info!("Update GUI, version: {}", self.version);
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
let release_tag = "latest";
|
||||
let release_tag = "snapshot";
|
||||
#[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")]
|
||||
let arch = "x86_64";
|
||||
@@ -91,8 +101,8 @@ impl GuiUpdater {
|
||||
let arch = "aarch64";
|
||||
|
||||
let file_url = format!(
|
||||
"https://github.com/yuezk/GlobalProtect-openconnect/releases/download/{}/gpgui_{}_{}.bin.tar.xz",
|
||||
release_tag, self.version, arch
|
||||
"https://github.com/yuezk/GlobalProtect-openconnect/releases/download/{}/gpgui_{}.bin.tar.xz",
|
||||
release_tag, arch
|
||||
);
|
||||
let checksum_url = format!("{}.sha256", file_url);
|
||||
|
||||
|
@@ -38,10 +38,15 @@ impl VpnTaskContext {
|
||||
let vpn = match Vpn::builder(req.gateway().server(), args.cookie())
|
||||
.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_wrapper(args.csd_wrapper())
|
||||
.reconnect_timeout(args.reconnect_timeout())
|
||||
.mtu(args.mtu())
|
||||
.os(args.openconnect_os())
|
||||
.disable_ipv6(args.disable_ipv6())
|
||||
.build()
|
||||
{
|
||||
Ok(vpn) => vpn,
|
||||
|
@@ -118,28 +118,41 @@ impl WsServer {
|
||||
}
|
||||
|
||||
pub async fn start(&self, shutdown_tx: mpsc::Sender<()>) {
|
||||
if let Ok(listener) = TcpListener::bind("127.0.0.1:0").await {
|
||||
let local_addr = listener.local_addr().unwrap();
|
||||
let listener = match self.start_tcp_server().await {
|
||||
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();
|
||||
|
||||
info!("WS server listening on port: {}", local_addr.port());
|
||||
|
||||
tokio::select! {
|
||||
_ = watch_vpn_state(self.ctx.vpn_state_rx(), Arc::clone(&self.ctx)) => {
|
||||
info!("VPN state watch task completed");
|
||||
}
|
||||
_ = start_server(listener, self.ctx.clone()) => {
|
||||
info!("WS server stopped");
|
||||
}
|
||||
_ = self.cancel_token.cancelled() => {
|
||||
info!("WS server cancelled");
|
||||
}
|
||||
tokio::select! {
|
||||
_ = watch_vpn_state(self.ctx.vpn_state_rx(), Arc::clone(&self.ctx)) => {
|
||||
info!("VPN state watch task completed");
|
||||
}
|
||||
_ = start_server(listener, self.ctx.clone()) => {
|
||||
info!("WS server stopped");
|
||||
}
|
||||
_ = self.cancel_token.cancelled() => {
|
||||
info!("WS server cancelled");
|
||||
}
|
||||
}
|
||||
|
||||
let _ = shutdown_tx.send(()).await;
|
||||
}
|
||||
|
||||
async fn 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>) {
|
||||
|
35
changelog.md
35
changelog.md
@@ -1,5 +1,40 @@
|
||||
# Changelog
|
||||
|
||||
## 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
|
||||
|
@@ -1,7 +1,6 @@
|
||||
use is_executable::IsExecutable;
|
||||
use std::path::Path;
|
||||
use std::{io, path::Path};
|
||||
|
||||
pub use is_executable::is_executable;
|
||||
use is_executable::IsExecutable;
|
||||
|
||||
const VPNC_SCRIPT_LOCATIONS: [&str; 6] = [
|
||||
"/usr/local/share/vpnc-scripts/vpnc-script",
|
||||
@@ -39,3 +38,17 @@ pub fn find_vpnc_script() -> Option<String> {
|
||||
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
|
||||
log.workspace = true
|
||||
reqwest.workspace = true
|
||||
openssl.workspace = true
|
||||
pem.workspace = true
|
||||
roxmltree.workspace = true
|
||||
serde.workspace = true
|
||||
specta.workspace = true
|
||||
|
@@ -1,13 +1,17 @@
|
||||
use anyhow::bail;
|
||||
use log::{info, warn};
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{error::AuthDataParseError, utils::base64::decode_to_string};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SamlAuthData {
|
||||
#[serde(alias = "un")]
|
||||
username: String,
|
||||
prelogin_cookie: Option<String>,
|
||||
portal_userauthcookie: Option<String>,
|
||||
token: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
@@ -32,10 +36,11 @@ impl SamlAuthData {
|
||||
username,
|
||||
prelogin_cookie,
|
||||
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") {
|
||||
Some(saml_status) if saml_status == "1" => {
|
||||
let username = parse_xml_tag(html, "saml-username");
|
||||
@@ -43,21 +48,43 @@ impl SamlAuthData {
|
||||
let portal_userauthcookie = parse_xml_tag(html, "portal-userauthcookie");
|
||||
|
||||
if SamlAuthData::check(&username, &prelogin_cookie, &portal_userauthcookie) {
|
||||
return Ok(SamlAuthData::new(
|
||||
Ok(SamlAuthData::new(
|
||||
username.unwrap(),
|
||||
prelogin_cookie,
|
||||
portal_userauthcookie,
|
||||
));
|
||||
))
|
||||
} else {
|
||||
Err(AuthDataParseError::Invalid)
|
||||
}
|
||||
}
|
||||
Some(_) => Err(AuthDataParseError::Invalid),
|
||||
None => Err(AuthDataParseError::NotFound),
|
||||
}
|
||||
}
|
||||
|
||||
bail!("Found invalid auth data in HTML");
|
||||
}
|
||||
Some(status) => {
|
||||
bail!("Found invalid SAML status {} in HTML", status);
|
||||
}
|
||||
None => {
|
||||
bail!("No auth data found in HTML");
|
||||
}
|
||||
pub fn 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");
|
||||
|
||||
let auth_data: SamlAuthData = serde_urlencoded::from_str(auth_data).map_err(|e| {
|
||||
warn!("Failed to parse token auth data: {}", e);
|
||||
warn!("Auth data: {}", auth_data);
|
||||
AuthDataParseError::Invalid
|
||||
})?;
|
||||
|
||||
Ok(auth_data)
|
||||
} else {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,6 +96,10 @@ impl SamlAuthData {
|
||||
self.prelogin_cookie.as_deref()
|
||||
}
|
||||
|
||||
pub fn token(&self) -> Option<&str> {
|
||||
self.token.as_deref()
|
||||
}
|
||||
|
||||
pub fn check(
|
||||
username: &Option<String>,
|
||||
prelogin_cookie: &Option<String>,
|
||||
@@ -78,7 +109,16 @@ impl SamlAuthData {
|
||||
let prelogin_cookie_valid = prelogin_cookie.as_ref().is_some_and(|val| val.len() > 5);
|
||||
let portal_userauthcookie_valid = portal_userauthcookie.as_ref().is_some_and(|val| val.len() > 5);
|
||||
|
||||
username_valid && (prelogin_cookie_valid || portal_userauthcookie_valid)
|
||||
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 +128,28 @@ pub fn parse_xml_tag(html: &str, tag: &str) -> Option<String> {
|
||||
.and_then(|captures| captures.get(1))
|
||||
.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_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 specta::Type;
|
||||
|
||||
use crate::{auth::SamlAuthData, utils::base64::decode_to_string};
|
||||
use crate::auth::SamlAuthData;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Type, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -37,16 +37,18 @@ impl From<&CachedCredential> for PasswordCredential {
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Type, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PreloginCookieCredential {
|
||||
pub struct PreloginCredential {
|
||||
username: String,
|
||||
prelogin_cookie: String,
|
||||
prelogin_cookie: Option<String>,
|
||||
token: Option<String>,
|
||||
}
|
||||
|
||||
impl PreloginCookieCredential {
|
||||
pub fn new(username: &str, prelogin_cookie: &str) -> Self {
|
||||
impl PreloginCredential {
|
||||
pub fn new(username: &str, prelogin_cookie: Option<&str>, token: Option<&str>) -> Self {
|
||||
Self {
|
||||
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
|
||||
}
|
||||
|
||||
pub fn prelogin_cookie(&self) -> &str {
|
||||
&self.prelogin_cookie
|
||||
pub fn prelogin_cookie(&self) -> Option<&str> {
|
||||
self.prelogin_cookie.as_deref()
|
||||
}
|
||||
|
||||
pub fn token(&self) -> Option<&str> {
|
||||
self.token.as_deref()
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<SamlAuthData> for PreloginCookieCredential {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(value: SamlAuthData) -> Result<Self, Self::Error> {
|
||||
impl From<SamlAuthData> for PreloginCredential {
|
||||
fn from(value: SamlAuthData) -> Self {
|
||||
let username = value.username().to_string();
|
||||
let prelogin_cookie = value
|
||||
.prelogin_cookie()
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing prelogin cookie"))?
|
||||
.to_string();
|
||||
let prelogin_cookie = value.prelogin_cookie();
|
||||
let token = value.token();
|
||||
|
||||
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)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
pub enum Credential {
|
||||
Password(PasswordCredential),
|
||||
PreloginCookie(PreloginCookieCredential),
|
||||
Prelogin(PreloginCredential),
|
||||
AuthCookie(AuthCookieCredential),
|
||||
CachedCredential(CachedCredential),
|
||||
Cached(CachedCredential),
|
||||
}
|
||||
|
||||
impl Credential {
|
||||
/// Create a credential from a globalprotectcallback:<base64 encoded string>
|
||||
pub fn parse_gpcallback(auth_data: &str) -> anyhow::Result<Self> {
|
||||
// Remove the surrounding quotes
|
||||
let auth_data = auth_data.trim_matches('"');
|
||||
let auth_data = auth_data.trim_start_matches("globalprotectcallback:");
|
||||
let auth_data = decode_to_string(auth_data)?;
|
||||
let auth_data = SamlAuthData::parse_html(&auth_data)?;
|
||||
/// Create a credential from a globalprotectcallback:<base64 encoded string>,
|
||||
/// or globalprotectcallback:cas-as=1&un=user@xyz.com&token=very_long_string
|
||||
pub fn from_gpcallback(auth_data: &str) -> anyhow::Result<Self> {
|
||||
let auth_data = SamlAuthData::from_gpcallback(auth_data)?;
|
||||
|
||||
Self::try_from(auth_data)
|
||||
Ok(Self::from(auth_data))
|
||||
}
|
||||
|
||||
pub fn username(&self) -> &str {
|
||||
match self {
|
||||
Credential::Password(cred) => cred.username(),
|
||||
Credential::PreloginCookie(cred) => cred.username(),
|
||||
Credential::Prelogin(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();
|
||||
params.insert("user", self.username());
|
||||
|
||||
let (passwd, prelogin_cookie, portal_userauthcookie, portal_prelogonuserauthcookie) = match self {
|
||||
Credential::Password(cred) => (Some(cred.password()), None, None, None),
|
||||
Credential::PreloginCookie(cred) => (None, Some(cred.prelogin_cookie()), None, None),
|
||||
let (passwd, prelogin_cookie, portal_userauthcookie, portal_prelogonuserauthcookie, token) = match self {
|
||||
Credential::Password(cred) => (Some(cred.password()), None, None, None, None),
|
||||
Credential::Prelogin(cred) => (None, cred.prelogin_cookie(), None, None, cred.token()),
|
||||
Credential::AuthCookie(cred) => (
|
||||
None,
|
||||
None,
|
||||
Some(cred.user_auth_cookie()),
|
||||
Some(cred.prelogon_user_auth_cookie()),
|
||||
None,
|
||||
),
|
||||
Credential::CachedCredential(cred) => (
|
||||
Credential::Cached(cred) => (
|
||||
cred.password(),
|
||||
None,
|
||||
Some(cred.auth_cookie.user_auth_cookie()),
|
||||
Some(cred.auth_cookie.prelogon_user_auth_cookie()),
|
||||
None,
|
||||
),
|
||||
};
|
||||
|
||||
@@ -214,17 +214,19 @@ impl Credential {
|
||||
portal_prelogonuserauthcookie.unwrap_or_default(),
|
||||
);
|
||||
|
||||
if let Some(token) = token {
|
||||
params.insert("token", token);
|
||||
}
|
||||
|
||||
params
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<SamlAuthData> for Credential {
|
||||
type Error = anyhow::Error;
|
||||
impl From<SamlAuthData> for Credential {
|
||||
fn from(value: SamlAuthData) -> Self {
|
||||
let cred = PreloginCredential::from(value);
|
||||
|
||||
fn try_from(value: SamlAuthData) -> Result<Self, Self::Error> {
|
||||
let prelogin_cookie = PreloginCookieCredential::try_from(value)?;
|
||||
|
||||
Ok(Self::PreloginCookie(prelogin_cookie))
|
||||
Self::Prelogin(cred)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -242,6 +244,6 @@ impl From<&AuthCookieCredential> for Credential {
|
||||
|
||||
impl From<&CachedCredential> for Credential {
|
||||
fn from(value: &CachedCredential) -> Self {
|
||||
Self::CachedCredential(value.clone())
|
||||
Self::Cached(value.clone())
|
||||
}
|
||||
}
|
||||
|
@@ -2,10 +2,18 @@ use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum PortalError {
|
||||
#[error("Portal prelogin error: {0}")]
|
||||
#[error("Prelogin error: {0}")]
|
||||
PreloginError(String),
|
||||
#[error("Portal config error: {0}")]
|
||||
ConfigError(String),
|
||||
#[error("Gateway error: {0}")]
|
||||
GatewayError(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<()> {
|
||||
let client = Client::builder()
|
||||
.danger_accept_invalid_certs(gp_params.ignore_tls_errors())
|
||||
.user_agent(gp_params.user_agent())
|
||||
.build()?;
|
||||
|
||||
let client = Client::try_from(gp_params)?;
|
||||
let md5 = build_csd_token(cookie)?;
|
||||
|
||||
info!("Submit HIP report md5: {}", md5);
|
||||
|
@@ -8,18 +8,20 @@ use crate::{
|
||||
credential::Credential,
|
||||
error::PortalError,
|
||||
gp_params::GpParams,
|
||||
utils::{normalize_server, parse_gp_error, 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 gateway = remove_url_scheme(&url);
|
||||
|
||||
let login_url = format!("{}/ssl-vpn/login.esp", url);
|
||||
let client = Client::builder()
|
||||
.danger_accept_invalid_certs(gp_params.ignore_tls_errors())
|
||||
.user_agent(gp_params.user_agent())
|
||||
.build()?;
|
||||
let client = Client::try_from(gp_params)?;
|
||||
|
||||
let mut params = cred.to_params();
|
||||
let extra_params = gp_params.to_params();
|
||||
@@ -34,25 +36,27 @@ pub async fn gateway_login(gateway: &str, cred: &Credential, gp_params: &GpParam
|
||||
.form(¶ms)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!(PortalError::GatewayError(e.to_string())))?;
|
||||
.map_err(|e| anyhow::anyhow!(PortalError::NetworkError(e.to_string())))?;
|
||||
|
||||
let status = res.status();
|
||||
let res = parse_gp_response(res).await.map_err(|err| {
|
||||
warn!("{err}");
|
||||
anyhow::anyhow!("Gateway login error: {}", err.reason)
|
||||
})?;
|
||||
|
||||
if status.is_client_error() || status.is_server_error() {
|
||||
let (reason, res) = parse_gp_error(res).await;
|
||||
// MFA detected
|
||||
if res.contains("Challenge") {
|
||||
let Some((message, input_str)) = parse_mfa(&res) else {
|
||||
bail!("Failed to parse MFA challenge: {res}");
|
||||
};
|
||||
|
||||
warn!(
|
||||
"Gateway login error: reason={}, status={}, response={}",
|
||||
reason, status, res
|
||||
);
|
||||
|
||||
bail!("Gateway login error, reason: {}", reason);
|
||||
return Ok(GatewayLogin::Mfa(message, input_str));
|
||||
}
|
||||
|
||||
let res_xml = res.text().await?;
|
||||
let doc = Document::parse(&res_xml)?;
|
||||
let doc = Document::parse(&res)?;
|
||||
|
||||
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> {
|
||||
@@ -86,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"))
|
||||
.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 log::info;
|
||||
use reqwest::Client;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
|
||||
use crate::GP_USER_AGENT;
|
||||
use crate::{utils::request::create_identity, GP_USER_AGENT};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Type, Default)]
|
||||
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 {
|
||||
is_gateway: bool,
|
||||
user_agent: String,
|
||||
@@ -51,7 +53,12 @@ pub struct GpParams {
|
||||
client_version: Option<String>,
|
||||
computer: String,
|
||||
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 {
|
||||
@@ -79,10 +86,6 @@ impl GpParams {
|
||||
self.ignore_tls_errors
|
||||
}
|
||||
|
||||
pub fn prefer_default_browser(&self) -> bool {
|
||||
self.prefer_default_browser
|
||||
}
|
||||
|
||||
pub fn client_os(&self) -> &str {
|
||||
self.client_os.as_str()
|
||||
}
|
||||
@@ -95,6 +98,14 @@ impl GpParams {
|
||||
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> {
|
||||
let mut params: HashMap<&str, &str> = HashMap::new();
|
||||
let client_os = self.client_os.as_str();
|
||||
@@ -105,11 +116,16 @@ impl GpParams {
|
||||
params.insert("ok", "Login");
|
||||
params.insert("direct", "yes");
|
||||
params.insert("ipv6-support", "yes");
|
||||
params.insert("inputStr", "");
|
||||
params.insert("clientVer", "4100");
|
||||
params.insert("clientos", client_os);
|
||||
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 {
|
||||
params.insert("os-version", os_version);
|
||||
}
|
||||
@@ -131,20 +147,26 @@ pub struct GpParamsBuilder {
|
||||
client_version: Option<String>,
|
||||
computer: String,
|
||||
ignore_tls_errors: bool,
|
||||
prefer_default_browser: bool,
|
||||
certificate: Option<String>,
|
||||
sslkey: Option<String>,
|
||||
key_password: Option<String>,
|
||||
}
|
||||
|
||||
impl GpParamsBuilder {
|
||||
pub fn new() -> Self {
|
||||
let computer = whoami::fallible::hostname().unwrap_or_else(|_| String::from("localhost"));
|
||||
|
||||
Self {
|
||||
is_gateway: false,
|
||||
user_agent: GP_USER_AGENT.to_string(),
|
||||
client_os: ClientOs::Linux,
|
||||
os_version: Default::default(),
|
||||
client_version: Default::default(),
|
||||
computer: whoami::hostname(),
|
||||
computer,
|
||||
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
|
||||
}
|
||||
|
||||
pub fn prefer_default_browser(&mut self, prefer_default_browser: bool) -> &mut Self {
|
||||
self.prefer_default_browser = prefer_default_browser;
|
||||
pub fn certificate<T: Into<Option<String>>>(&mut self, certificate: T) -> &mut Self {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -197,7 +229,11 @@ impl GpParamsBuilder {
|
||||
client_version: self.client_version.clone(),
|
||||
computer: self.computer.clone(),
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@@ -10,7 +10,7 @@ use crate::{
|
||||
error::PortalError,
|
||||
gateway::{parse_gateways, Gateway},
|
||||
gp_params::GpParams,
|
||||
utils::{normalize_server, parse_gp_error, remove_url_scheme, xml},
|
||||
utils::{normalize_server, parse_gp_response, remove_url_scheme, xml},
|
||||
};
|
||||
|
||||
#[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 url = format!("{}/global-protect/getconfig.esp", portal);
|
||||
let client = Client::builder()
|
||||
.danger_accept_invalid_certs(gp_params.ignore_tls_errors())
|
||||
.user_agent(gp_params.user_agent())
|
||||
.build()?;
|
||||
let client = Client::try_from(gp_params)?;
|
||||
|
||||
let mut params = cred.to_params();
|
||||
let extra_params = gp_params.to_params();
|
||||
@@ -102,25 +99,25 @@ pub async fn retrieve_config(portal: &str, cred: &Credential, gp_params: &GpPara
|
||||
|
||||
info!("Portal config, user_agent: {}", gp_params.user_agent());
|
||||
|
||||
let res = client.post(&url).form(¶ms).send().await?;
|
||||
let status = res.status();
|
||||
let res = client
|
||||
.post(&url)
|
||||
.form(¶ms)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!(PortalError::NetworkError(e.to_string())))?;
|
||||
|
||||
if status == StatusCode::NOT_FOUND {
|
||||
bail!(PortalError::ConfigError("Config endpoint not found".to_string()))
|
||||
}
|
||||
let res_xml = parse_gp_response(res).await.or_else(|err| {
|
||||
if err.status == StatusCode::NOT_FOUND {
|
||||
bail!(PortalError::ConfigError("Config endpoint not found".to_string()));
|
||||
}
|
||||
|
||||
if status.is_client_error() || status.is_server_error() {
|
||||
let (reason, res) = parse_gp_error(res).await;
|
||||
if err.is_status_error() {
|
||||
warn!("{err}");
|
||||
bail!("Portal config error: {}", err.reason);
|
||||
}
|
||||
|
||||
warn!(
|
||||
"Portal config error: reason={}, status={}, response={}",
|
||||
reason, status, res
|
||||
);
|
||||
|
||||
bail!("Portal config error, reason: {}", 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() {
|
||||
bail!(PortalError::ConfigError("Empty portal config response".to_string()))
|
||||
|
@@ -1,4 +1,4 @@
|
||||
use anyhow::bail;
|
||||
use anyhow::{anyhow, bail};
|
||||
use log::{info, warn};
|
||||
use reqwest::{Client, StatusCode};
|
||||
use roxmltree::Document;
|
||||
@@ -8,7 +8,7 @@ use specta::Type;
|
||||
use crate::{
|
||||
error::PortalError,
|
||||
gp_params::GpParams,
|
||||
utils::{base64, normalize_server, parse_gp_error, xml},
|
||||
utils::{base64, normalize_server, parse_gp_response, xml},
|
||||
};
|
||||
|
||||
const REQUIRED_PARAMS: [&str; 8] = [
|
||||
@@ -98,60 +98,61 @@ impl Prelogin {
|
||||
|
||||
pub async fn prelogin(portal: &str, gp_params: &GpParams) -> anyhow::Result<Prelogin> {
|
||||
let user_agent = gp_params.user_agent();
|
||||
info!("Prelogin with user_agent: {}", user_agent);
|
||||
let 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 is_gateway = gp_params.is_gateway();
|
||||
let path = if is_gateway { "ssl-vpn" } else { "global-protect" };
|
||||
let prelogin_url = format!("{portal}/{}/prelogin.esp", path);
|
||||
let mut params = gp_params.to_params();
|
||||
|
||||
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));
|
||||
|
||||
let client = Client::builder()
|
||||
.danger_accept_invalid_certs(gp_params.ignore_tls_errors())
|
||||
.user_agent(user_agent)
|
||||
.build()?;
|
||||
let client = Client::try_from(gp_params)?;
|
||||
|
||||
let res = client.post(&prelogin_url).form(¶ms).send().await?;
|
||||
let status = res.status();
|
||||
|
||||
if status == StatusCode::NOT_FOUND {
|
||||
bail!(PortalError::PreloginError("Prelogin endpoint not found".to_string()))
|
||||
}
|
||||
|
||||
if status.is_client_error() || status.is_server_error() {
|
||||
let (reason, res) = parse_gp_error(res).await;
|
||||
|
||||
warn!("Prelogin error: reason={}, status={}, response={}", reason, status, res);
|
||||
|
||||
bail!("Prelogin error: {}", status)
|
||||
}
|
||||
|
||||
let res_xml = res
|
||||
.text()
|
||||
let res = client
|
||||
.post(&prelogin_url)
|
||||
.form(¶ms)
|
||||
.send()
|
||||
.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)
|
||||
}
|
||||
|
||||
fn parse_res_xml(res_xml: String, is_gateway: bool) -> anyhow::Result<Prelogin> {
|
||||
let doc = Document::parse(&res_xml)?;
|
||||
fn parse_res_xml(res_xml: &str, is_gateway: bool) -> anyhow::Result<Prelogin> {
|
||||
let doc = Document::parse(res_xml)?;
|
||||
|
||||
let status = xml::get_child_text(&doc, "status")
|
||||
.ok_or_else(|| anyhow::anyhow!("Prelogin response does not contain status element"))?;
|
||||
// Check the status of the prelogin response
|
||||
if status.to_uppercase() != "SUCCESS" {
|
||||
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(|| {
|
||||
@@ -177,22 +178,24 @@ fn parse_res_xml(res_xml: String, is_gateway: bool) -> anyhow::Result<Prelogin>
|
||||
return Ok(Prelogin::Saml(saml_prelogin));
|
||||
}
|
||||
|
||||
let label_username = xml::get_child_text(&doc, "username-label");
|
||||
let label_password = xml::get_child_text(&doc, "password-label");
|
||||
// Check if the prelogin response is standard login
|
||||
if label_username.is_some() && label_password.is_some() {
|
||||
let auth_message =
|
||||
xml::get_child_text(&doc, "authentication-message").unwrap_or(String::from("Please enter the login credentials"));
|
||||
let standard_prelogin = StandardPrelogin {
|
||||
region,
|
||||
is_gateway,
|
||||
auth_message,
|
||||
label_username: label_username.unwrap(),
|
||||
label_password: label_password.unwrap(),
|
||||
};
|
||||
let label_username = xml::get_child_text(&doc, "username-label").unwrap_or_else(|| {
|
||||
info!("Username label has no value, using default");
|
||||
String::from("Username")
|
||||
});
|
||||
let label_password = xml::get_child_text(&doc, "password-label").unwrap_or_else(|| {
|
||||
info!("Password label has no value, using default");
|
||||
String::from("Password")
|
||||
});
|
||||
|
||||
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,
|
||||
ignore_tls_errors: bool,
|
||||
clean: bool,
|
||||
default_browser: bool,
|
||||
}
|
||||
|
||||
impl<'a> SamlAuthLauncher<'a> {
|
||||
@@ -33,6 +34,7 @@ impl<'a> SamlAuthLauncher<'a> {
|
||||
fix_openssl: false,
|
||||
ignore_tls_errors: false,
|
||||
clean: false,
|
||||
default_browser: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,8 +83,13 @@ impl<'a> SamlAuthLauncher<'a> {
|
||||
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.
|
||||
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);
|
||||
auth_cmd.arg(self.server);
|
||||
|
||||
@@ -122,6 +129,10 @@ impl<'a> SamlAuthLauncher<'a> {
|
||||
auth_cmd.arg("--clean");
|
||||
}
|
||||
|
||||
if self.default_browser {
|
||||
auth_cmd.arg("--default-browser");
|
||||
}
|
||||
|
||||
let mut non_root_cmd = auth_cmd.into_non_root()?;
|
||||
let output = non_root_cmd
|
||||
.kill_on_drop(true)
|
||||
@@ -130,12 +141,16 @@ impl<'a> SamlAuthLauncher<'a> {
|
||||
.wait_with_output()
|
||||
.await?;
|
||||
|
||||
if self.default_browser {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let Ok(auth_result) = serde_json::from_slice::<SamlAuthResult>(&output.stdout) else {
|
||||
bail!("Failed to parse auth data")
|
||||
};
|
||||
|
||||
match auth_result {
|
||||
SamlAuthResult::Success(auth_data) => Credential::try_from(auth_data),
|
||||
SamlAuthResult::Success(auth_data) => Ok(Some(Credential::from(auth_data))),
|
||||
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> {
|
||||
auth_request: &'a str,
|
||||
@@ -14,8 +17,18 @@ impl BrowserAuthenticator<'_> {
|
||||
open::that_detached(self.auth_request)?;
|
||||
} else {
|
||||
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())?;
|
||||
|
||||
open::that_detached(html_file)?;
|
||||
@@ -24,11 +37,3 @@ impl BrowserAuthenticator<'_> {
|
||||
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,
|
||||
vpnc_script: Option<String>,
|
||||
user_agent: Option<String>,
|
||||
os: Option<ClientOs>,
|
||||
certificate: Option<String>,
|
||||
sslkey: Option<String>,
|
||||
key_password: Option<String>,
|
||||
csd_uid: u32,
|
||||
csd_wrapper: Option<String>,
|
||||
reconnect_timeout: u32,
|
||||
mtu: u32,
|
||||
os: Option<ClientOs>,
|
||||
disable_ipv6: bool,
|
||||
}
|
||||
|
||||
impl ConnectArgs {
|
||||
@@ -45,9 +50,14 @@ impl ConnectArgs {
|
||||
vpnc_script: None,
|
||||
user_agent: None,
|
||||
os: None,
|
||||
certificate: None,
|
||||
sslkey: None,
|
||||
key_password: None,
|
||||
csd_uid: 0,
|
||||
csd_wrapper: None,
|
||||
reconnect_timeout: 300,
|
||||
mtu: 0,
|
||||
disable_ipv6: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +77,18 @@ impl ConnectArgs {
|
||||
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 {
|
||||
self.csd_uid
|
||||
}
|
||||
@@ -75,9 +97,17 @@ impl ConnectArgs {
|
||||
self.csd_wrapper.clone()
|
||||
}
|
||||
|
||||
pub fn reconnect_timeout(&self) -> u32 {
|
||||
self.reconnect_timeout
|
||||
}
|
||||
|
||||
pub fn mtu(&self) -> u32 {
|
||||
self.mtu
|
||||
}
|
||||
|
||||
pub fn disable_ipv6(&self) -> bool {
|
||||
self.disable_ipv6
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Type)]
|
||||
@@ -109,11 +139,6 @@ impl ConnectRequest {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_mtu(mut self, mtu: u32) -> Self {
|
||||
self.args.mtu = mtu;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_user_agent<T: Into<Option<String>>>(mut self, user_agent: T) -> Self {
|
||||
self.args.user_agent = user_agent.into();
|
||||
self
|
||||
@@ -124,6 +149,36 @@ impl ConnectRequest {
|
||||
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 {
|
||||
self.info.gateway()
|
||||
}
|
||||
|
@@ -1,5 +1,3 @@
|
||||
use reqwest::{Response, Url};
|
||||
|
||||
pub(crate) mod xml;
|
||||
|
||||
pub mod base64;
|
||||
@@ -10,13 +8,18 @@ pub mod env_file;
|
||||
pub mod lock_file;
|
||||
pub mod openssl;
|
||||
pub mod redact;
|
||||
pub mod request;
|
||||
#[cfg(feature = "tauri")]
|
||||
pub mod window;
|
||||
|
||||
mod shutdown_signal;
|
||||
|
||||
use log::warn;
|
||||
pub use shutdown_signal::shutdown_signal;
|
||||
|
||||
use reqwest::{Response, StatusCode, Url};
|
||||
use thiserror::Error;
|
||||
|
||||
/// Normalize the server URL to the format `https://<host>:<port>`
|
||||
pub fn normalize_server(server: &str) -> anyhow::Result<String> {
|
||||
let server = if server.starts_with("https://") || server.starts_with("http://") {
|
||||
@@ -42,7 +45,41 @@ pub fn remove_url_scheme(s: &str) -> String {
|
||||
s.replace("http://", "").replace("https://", "")
|
||||
}
|
||||
|
||||
pub(crate) async fn parse_gp_error(res: Response) -> (String, String) {
|
||||
#[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")
|
||||
|
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-----
|
@@ -14,12 +14,16 @@ pub(crate) struct ConnectOptions {
|
||||
pub script: *const c_char,
|
||||
pub os: *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 csd_uid: u32,
|
||||
pub csd_wrapper: *const c_char,
|
||||
|
||||
pub reconnect_timeout: u32,
|
||||
pub mtu: u32,
|
||||
pub disable_ipv6: u32,
|
||||
}
|
||||
|
||||
#[link(name = "vpn")]
|
||||
|
@@ -16,7 +16,7 @@ static vpn_connected_callback on_vpn_connected;
|
||||
/* Validate the peer certificate */
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -28,12 +28,9 @@ static void print_progress(__attribute__((unused)) void *_vpninfo, int level, co
|
||||
char *message = format_message(format, args);
|
||||
va_end(args);
|
||||
|
||||
if (message == NULL)
|
||||
{
|
||||
if (message == NULL) {
|
||||
ERROR("Failed to format log message");
|
||||
}
|
||||
else
|
||||
{
|
||||
} else {
|
||||
LOG(level, message);
|
||||
free(message);
|
||||
}
|
||||
@@ -63,12 +60,13 @@ int vpn_connect(const vpn_options *options, vpn_connected_callback callback)
|
||||
INFO("OS: %s", options->os);
|
||||
INFO("CSD_USER: %d", options->csd_uid);
|
||||
INFO("CSD_WRAPPER: %s", options->csd_wrapper);
|
||||
INFO("RECONNECT_TIMEOUT: %d", options->reconnect_timeout);
|
||||
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);
|
||||
|
||||
if (!vpninfo)
|
||||
{
|
||||
if (!vpninfo) {
|
||||
ERROR("openconnect_vpninfo_new failed");
|
||||
return 1;
|
||||
}
|
||||
@@ -83,15 +81,13 @@ int vpn_connect(const vpn_options *options, vpn_connected_callback callback)
|
||||
openconnect_set_reported_os(vpninfo, options->os);
|
||||
}
|
||||
|
||||
if (options->certificate)
|
||||
{
|
||||
if (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) {
|
||||
INFO("Setting server certificate: %s", options->servercert);
|
||||
openconnect_set_system_trust(vpninfo, 0);
|
||||
if (options->key_password) {
|
||||
openconnect_set_key_password(vpninfo, options->key_password);
|
||||
}
|
||||
|
||||
if (options->csd_wrapper) {
|
||||
@@ -103,39 +99,37 @@ int vpn_connect(const vpn_options *options, vpn_connected_callback callback)
|
||||
openconnect_set_reqmtu(vpninfo, mtu);
|
||||
}
|
||||
|
||||
if (options->disable_ipv6) {
|
||||
openconnect_disable_ipv6(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");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!uname(&utsbuf))
|
||||
{
|
||||
if (!uname(&utsbuf)) {
|
||||
openconnect_set_localname(vpninfo, utsbuf.nodename);
|
||||
}
|
||||
|
||||
// Essential step
|
||||
if (openconnect_make_cstp_connection(vpninfo) != 0)
|
||||
{
|
||||
if (openconnect_make_cstp_connection(vpninfo) != 0) {
|
||||
ERROR("openconnect_make_cstp_connection failed");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (openconnect_setup_dtls(vpninfo, 60) != 0)
|
||||
{
|
||||
if (openconnect_setup_dtls(vpninfo, 60) != 0) {
|
||||
openconnect_disable_dtls(vpninfo);
|
||||
}
|
||||
|
||||
// Essential step
|
||||
openconnect_set_setup_tun_handler(vpninfo, setup_tun_handler);
|
||||
|
||||
while (1)
|
||||
{
|
||||
int ret = openconnect_mainloop(vpninfo, 300, 10);
|
||||
while (1) {
|
||||
int ret = openconnect_mainloop(vpninfo, options->reconnect_timeout, 10);
|
||||
|
||||
if (ret)
|
||||
{
|
||||
if (ret) {
|
||||
INFO("openconnect_mainloop returned %d, exiting", ret);
|
||||
openconnect_vpninfo_free(vpninfo);
|
||||
return ret;
|
||||
@@ -152,8 +146,7 @@ void vpn_disconnect()
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
@@ -15,12 +15,16 @@ typedef struct vpn_options
|
||||
const char *script;
|
||||
const char *os;
|
||||
const char *certificate;
|
||||
const char *sslkey;
|
||||
const char *key_password;
|
||||
const char *servercert;
|
||||
|
||||
const uid_t csd_uid;
|
||||
const char *csd_wrapper;
|
||||
|
||||
const int reconnect_timeout;
|
||||
const int mtu;
|
||||
const int disable_ipv6;
|
||||
} vpn_options;
|
||||
|
||||
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);
|
||||
va_end(args_copy);
|
||||
|
||||
char *buffer = malloc(len + 1);
|
||||
char *buffer = (char*)malloc(len + 1);
|
||||
if (buffer == NULL)
|
||||
{
|
||||
return NULL;
|
||||
|
@@ -4,7 +4,7 @@ use std::{
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
|
||||
use common::vpn_utils::{find_vpnc_script, is_executable};
|
||||
use common::vpn_utils::{check_executable, find_vpnc_script};
|
||||
use log::info;
|
||||
|
||||
use crate::ffi;
|
||||
@@ -18,12 +18,16 @@ pub struct Vpn {
|
||||
script: CString,
|
||||
os: CString,
|
||||
certificate: Option<CString>,
|
||||
sslkey: Option<CString>,
|
||||
key_password: Option<CString>,
|
||||
servercert: Option<CString>,
|
||||
|
||||
csd_uid: u32,
|
||||
csd_wrapper: Option<CString>,
|
||||
|
||||
reconnect_timeout: u32,
|
||||
mtu: u32,
|
||||
disable_ipv6: bool,
|
||||
|
||||
callback: OnConnectedCallback,
|
||||
}
|
||||
@@ -61,13 +65,18 @@ impl Vpn {
|
||||
user_agent: self.user_agent.as_ptr(),
|
||||
script: self.script.as_ptr(),
|
||||
os: self.os.as_ptr(),
|
||||
|
||||
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),
|
||||
|
||||
csd_uid: self.csd_uid,
|
||||
csd_wrapper: Self::option_to_ptr(&self.csd_wrapper),
|
||||
|
||||
reconnect_timeout: self.reconnect_timeout,
|
||||
mtu: self.mtu,
|
||||
disable_ipv6: self.disable_ipv6 as u32,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,23 +89,23 @@ impl Vpn {
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct VpnError<'a> {
|
||||
message: &'a str,
|
||||
pub struct VpnError {
|
||||
message: String,
|
||||
}
|
||||
|
||||
impl<'a> VpnError<'a> {
|
||||
fn new(message: &'a str) -> Self {
|
||||
impl VpnError {
|
||||
fn new(message: String) -> Self {
|
||||
Self { message }
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for VpnError<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
impl fmt::Display for VpnError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", self.message)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for VpnError<'_> {}
|
||||
impl std::error::Error for VpnError {}
|
||||
|
||||
pub struct VpnBuilder {
|
||||
server: String,
|
||||
@@ -106,10 +115,16 @@ pub struct VpnBuilder {
|
||||
user_agent: Option<String>,
|
||||
os: Option<String>,
|
||||
|
||||
certificate: Option<String>,
|
||||
sslkey: Option<String>,
|
||||
key_password: Option<String>,
|
||||
|
||||
csd_uid: u32,
|
||||
csd_wrapper: Option<String>,
|
||||
|
||||
reconnect_timeout: u32,
|
||||
mtu: u32,
|
||||
disable_ipv6: bool,
|
||||
}
|
||||
|
||||
impl VpnBuilder {
|
||||
@@ -122,10 +137,16 @@ impl VpnBuilder {
|
||||
user_agent: None,
|
||||
os: None,
|
||||
|
||||
certificate: None,
|
||||
sslkey: None,
|
||||
key_password: None,
|
||||
|
||||
csd_uid: 0,
|
||||
csd_wrapper: None,
|
||||
|
||||
reconnect_timeout: 300,
|
||||
mtu: 0,
|
||||
disable_ipv6: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,6 +165,21 @@ impl VpnBuilder {
|
||||
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 {
|
||||
self.csd_uid = csd_uid;
|
||||
self
|
||||
@@ -154,26 +190,32 @@ impl VpnBuilder {
|
||||
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 {
|
||||
self.mtu = mtu;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> Result<Vpn, VpnError<'static>> {
|
||||
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) => {
|
||||
if !is_executable(&script) {
|
||||
return Err(VpnError::new("vpnc script is not executable"));
|
||||
}
|
||||
check_executable(&script).map_err(|e| VpnError::new(e.to_string()))?;
|
||||
script
|
||||
}
|
||||
None => find_vpnc_script().ok_or_else(|| VpnError::new("Failed to find vpnc-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 {
|
||||
if !is_executable(csd_wrapper) {
|
||||
return Err(VpnError::new("CSD wrapper is not executable"));
|
||||
}
|
||||
check_executable(csd_wrapper).map_err(|e| VpnError::new(e.to_string()))?;
|
||||
}
|
||||
|
||||
let user_agent = self.user_agent.unwrap_or_default();
|
||||
@@ -185,13 +227,18 @@ impl VpnBuilder {
|
||||
user_agent: Self::to_cstring(&user_agent),
|
||||
script: Self::to_cstring(&script),
|
||||
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,
|
||||
|
||||
csd_uid: self.csd_uid,
|
||||
csd_wrapper: self.csd_wrapper.as_deref().map(Self::to_cstring),
|
||||
|
||||
reconnect_timeout: self.reconnect_timeout,
|
||||
mtu: self.mtu,
|
||||
disable_ipv6: self.disable_ipv6,
|
||||
|
||||
callback: Default::default(),
|
||||
})
|
||||
|
@@ -5,7 +5,10 @@ install:
|
||||
install -Dm755 artifacts/usr/bin/gpservice $(DESTDIR)/usr/bin/gpservice
|
||||
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 $(DESTDIR)/usr/bin/gpgui
|
||||
|
||||
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/icons/hicolor/scalable/apps/gpgui.svg $(DESTDIR)/usr/share/icons/hicolor/scalable/apps/gpgui.svg
|
||||
|
Reference in New Issue
Block a user