mirror of
				https://github.com/yuezk/GlobalProtect-openconnect.git
				synced 2025-05-20 07:26:58 -04:00 
			
		
		
		
	Compare commits
	
		
			34 Commits
		
	
	
		
			a884c41813
			...
			snapshot
		
	
	| 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 | 
							
								
								
									
										7
									
								
								.github/workflows/build.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								.github/workflows/build.yaml
									
									
									
									
										vendored
									
									
								
							@@ -25,7 +25,7 @@ jobs:
 | 
			
		||||
        id: set-matrix
 | 
			
		||||
        run: |
 | 
			
		||||
          if [[ "${{ github.ref }}" == "refs/tags/"* ]]; then
 | 
			
		||||
            echo 'matrix=[{"runner": "ubuntu-latest", "arch": "amd64"}, {"runner": "arm64", "arch": "arm64"]' >> $GITHUB_OUTPUT
 | 
			
		||||
            echo 'matrix=[{"runner": "ubuntu-latest", "arch": "amd64"}, {"runner": "arm64", "arch": "arm64"}]' >> $GITHUB_OUTPUT
 | 
			
		||||
          else
 | 
			
		||||
            echo 'matrix=[{"runner": "ubuntu-latest", "arch": "amd64"}]' >> $GITHUB_OUTPUT
 | 
			
		||||
          fi
 | 
			
		||||
@@ -68,7 +68,8 @@ jobs:
 | 
			
		||||
    - tarball
 | 
			
		||||
    strategy:
 | 
			
		||||
      matrix:
 | 
			
		||||
        os: ${{fromJson(needs.setup-matrix.outputs.matrix)}}
 | 
			
		||||
        # 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 }})
 | 
			
		||||
@@ -182,7 +183,7 @@ jobs:
 | 
			
		||||
        gh -R "$REPO" release create $RELEASE_TAG \
 | 
			
		||||
          --title "$RELEASE_TAG" \
 | 
			
		||||
          --notes "$NOTES" \
 | 
			
		||||
          --target ${{ github.ref }} \
 | 
			
		||||
          ${{ github.ref == 'refs/heads/dev' && '--target dev' || '' }} \
 | 
			
		||||
          ${{ github.ref == 'refs/heads/dev' && '--prerelease' || '' }} \
 | 
			
		||||
          gh-release/artifact-source/* \
 | 
			
		||||
          gh-release/artifact-gpgui-*/*
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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",
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										32
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										32
									
								
								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.2"
 | 
			
		||||
version = "2.3.1"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "is_executable",
 | 
			
		||||
]
 | 
			
		||||
@@ -1430,7 +1436,7 @@ dependencies = [
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "gpapi"
 | 
			
		||||
version = "2.1.2"
 | 
			
		||||
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,7 +1470,7 @@ dependencies = [
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "gpauth"
 | 
			
		||||
version = "2.1.2"
 | 
			
		||||
version = "2.3.1"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "anyhow",
 | 
			
		||||
 "clap",
 | 
			
		||||
@@ -1483,7 +1491,7 @@ dependencies = [
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "gpclient"
 | 
			
		||||
version = "2.1.2"
 | 
			
		||||
version = "2.3.1"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "anyhow",
 | 
			
		||||
 "clap",
 | 
			
		||||
@@ -1505,7 +1513,7 @@ dependencies = [
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "gpgui-helper"
 | 
			
		||||
version = "2.1.2"
 | 
			
		||||
version = "2.3.1"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "anyhow",
 | 
			
		||||
 "clap",
 | 
			
		||||
@@ -1523,7 +1531,7 @@ dependencies = [
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "gpservice"
 | 
			
		||||
version = "2.1.2"
 | 
			
		||||
version = "2.3.1"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "anyhow",
 | 
			
		||||
 "axum",
 | 
			
		||||
@@ -2537,7 +2545,7 @@ dependencies = [
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "openconnect"
 | 
			
		||||
version = "2.1.2"
 | 
			
		||||
version = "2.3.1"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "cc",
 | 
			
		||||
 "common",
 | 
			
		||||
@@ -2670,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"
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,7 @@ members = ["crates/*", "apps/gpclient", "apps/gpservice", "apps/gpauth", "apps/g
 | 
			
		||||
 | 
			
		||||
[workspace.package]
 | 
			
		||||
rust-version = "1.70"
 | 
			
		||||
version = "2.1.2"
 | 
			
		||||
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"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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
 | 
			
		||||
 
 | 
			
		||||
@@ -366,17 +366,14 @@ fn read_auth_data_from_html(html: &str) -> Result<SamlAuthData, AuthDataParseErr
 | 
			
		||||
    return Err(AuthDataParseError::Invalid);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  match SamlAuthData::from_html(html) {
 | 
			
		||||
    Ok(auth_data) => Ok(auth_data),
 | 
			
		||||
    Err(err) => {
 | 
			
		||||
  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)
 | 
			
		||||
    }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn extract_gpcallback(html: &str) -> Option<String> {
 | 
			
		||||
 
 | 
			
		||||
@@ -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,7 +33,7 @@ pub(crate) struct ConnectArgs {
 | 
			
		||||
  user: Option<String>,
 | 
			
		||||
  #[arg(long, short, help = "The VPNC script to use")]
 | 
			
		||||
  script: Option<String>,
 | 
			
		||||
  #[arg(long, help = "Treat the server as a gateway, instead of a portal")]
 | 
			
		||||
  #[arg(long, help = "Connect the server as a gateway, instead of a portal")]
 | 
			
		||||
  as_gateway: bool,
 | 
			
		||||
 | 
			
		||||
  #[arg(
 | 
			
		||||
@@ -41,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,
 | 
			
		||||
@@ -60,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 {
 | 
			
		||||
@@ -79,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 {
 | 
			
		||||
@@ -92,10 +115,45 @@ 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;
 | 
			
		||||
 | 
			
		||||
@@ -154,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);
 | 
			
		||||
@@ -174,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)?;
 | 
			
		||||
@@ -193,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);
 | 
			
		||||
@@ -223,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)
 | 
			
		||||
@@ -233,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" };
 | 
			
		||||
@@ -257,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,6 +81,16 @@ 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()
 | 
			
		||||
@@ -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() {
 | 
			
		||||
 
 | 
			
		||||
@@ -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,12 +118,14 @@ 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();
 | 
			
		||||
 | 
			
		||||
      self.lock_file.lock(local_addr.port().to_string()).unwrap();
 | 
			
		||||
 | 
			
		||||
      info!("WS server listening on port: {}", local_addr.port());
 | 
			
		||||
    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;
 | 
			
		||||
      },
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    tokio::select! {
 | 
			
		||||
      _ = watch_vpn_state(self.ctx.vpn_state_rx(), Arc::clone(&self.ctx)) => {
 | 
			
		||||
@@ -136,10 +138,21 @@ impl WsServer {
 | 
			
		||||
        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>) {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										31
									
								
								changelog.md
									
									
									
									
									
								
							
							
						
						
									
										31
									
								
								changelog.md
									
									
									
									
									
								
							@@ -1,5 +1,36 @@
 | 
			
		||||
# 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)
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -70,6 +70,7 @@ impl SamlAuthData {
 | 
			
		||||
 | 
			
		||||
      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
 | 
			
		||||
      })?;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -162,7 +162,7 @@ pub enum Credential {
 | 
			
		||||
  Password(PasswordCredential),
 | 
			
		||||
  Prelogin(PreloginCredential),
 | 
			
		||||
  AuthCookie(AuthCookieCredential),
 | 
			
		||||
  CachedCredential(CachedCredential),
 | 
			
		||||
  Cached(CachedCredential),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Credential {
 | 
			
		||||
@@ -179,7 +179,7 @@ impl Credential {
 | 
			
		||||
      Credential::Password(cred) => cred.username(),
 | 
			
		||||
      Credential::Prelogin(cred) => cred.username(),
 | 
			
		||||
      Credential::AuthCookie(cred) => cred.username(),
 | 
			
		||||
      Credential::CachedCredential(cred) => cred.username(),
 | 
			
		||||
      Credential::Cached(cred) => cred.username(),
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -197,7 +197,7 @@ impl Credential {
 | 
			
		||||
        Some(cred.prelogon_user_auth_cookie()),
 | 
			
		||||
        None,
 | 
			
		||||
      ),
 | 
			
		||||
      Credential::CachedCredential(cred) => (
 | 
			
		||||
      Credential::Cached(cred) => (
 | 
			
		||||
        cred.password(),
 | 
			
		||||
        None,
 | 
			
		||||
        Some(cred.auth_cookie.user_auth_cookie()),
 | 
			
		||||
@@ -244,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,7 +2,7 @@ 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),
 | 
			
		||||
 
 | 
			
		||||
@@ -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();
 | 
			
		||||
@@ -36,23 +38,25 @@ pub async fn gateway_login(gateway: &str, cred: &Credential, gp_params: &GpParam
 | 
			
		||||
    .await
 | 
			
		||||
    .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,6 +53,12 @@ pub struct GpParams {
 | 
			
		||||
  client_version: Option<String>,
 | 
			
		||||
  computer: String,
 | 
			
		||||
  ignore_tls_errors: bool,
 | 
			
		||||
  certificate: Option<String>,
 | 
			
		||||
  sslkey: Option<String>,
 | 
			
		||||
  key_password: Option<String>,
 | 
			
		||||
  // Used for MFA
 | 
			
		||||
  input_str: Option<String>,
 | 
			
		||||
  otp: Option<String>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl GpParams {
 | 
			
		||||
@@ -90,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();
 | 
			
		||||
@@ -100,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);
 | 
			
		||||
    }
 | 
			
		||||
@@ -126,18 +147,26 @@ pub struct GpParamsBuilder {
 | 
			
		||||
  client_version: Option<String>,
 | 
			
		||||
  computer: String,
 | 
			
		||||
  ignore_tls_errors: 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,
 | 
			
		||||
      certificate: Default::default(),
 | 
			
		||||
      sslkey: Default::default(),
 | 
			
		||||
      key_password: Default::default(),
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -176,6 +205,21 @@ impl GpParamsBuilder {
 | 
			
		||||
    self
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  pub fn build(&self) -> GpParams {
 | 
			
		||||
    GpParams {
 | 
			
		||||
      is_gateway: self.is_gateway,
 | 
			
		||||
@@ -185,6 +229,11 @@ impl GpParamsBuilder {
 | 
			
		||||
      client_version: self.client_version.clone(),
 | 
			
		||||
      computer: self.computer.clone(),
 | 
			
		||||
      ignore_tls_errors: self.ignore_tls_errors,
 | 
			
		||||
      certificate: self.certificate.clone(),
 | 
			
		||||
      sslkey: self.sslkey.clone(),
 | 
			
		||||
      key_password: self.key_password.clone(),
 | 
			
		||||
      input_str: Default::default(),
 | 
			
		||||
      otp: Default::default(),
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -194,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();
 | 
			
		||||
@@ -108,24 +105,19 @@ pub async fn retrieve_config(portal: &str, cred: &Credential, gp_params: &GpPara
 | 
			
		||||
    .send()
 | 
			
		||||
    .await
 | 
			
		||||
    .map_err(|e| anyhow::anyhow!(PortalError::NetworkError(e.to_string())))?;
 | 
			
		||||
  let status = res.status();
 | 
			
		||||
 | 
			
		||||
  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;
 | 
			
		||||
 | 
			
		||||
    warn!(
 | 
			
		||||
      "Portal config error: reason={}, status={}, response={}",
 | 
			
		||||
      reason, status, res
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    bail!("Portal config error, reason: {}", reason);
 | 
			
		||||
    if err.is_status_error() {
 | 
			
		||||
      warn!("{err}");
 | 
			
		||||
      bail!("Portal config error: {}", err.reason);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
  let res_xml = res.text().await.map_err(|e| PortalError::ConfigError(e.to_string()))?;
 | 
			
		||||
    Err(anyhow::anyhow!(PortalError::ConfigError(err.reason)))
 | 
			
		||||
  })?;
 | 
			
		||||
 | 
			
		||||
  if res_xml.is_empty() {
 | 
			
		||||
    bail!(PortalError::ConfigError("Empty portal config response".to_string()))
 | 
			
		||||
 
 | 
			
		||||
@@ -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,10 +98,12 @@ 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();
 | 
			
		||||
@@ -112,12 +114,7 @@ pub async fn prelogin(portal: &str, gp_params: &GpParams) -> anyhow::Result<Prel
 | 
			
		||||
 | 
			
		||||
  params.retain(|k, _| REQUIRED_PARAMS.iter().any(|required_param| required_param == k));
 | 
			
		||||
 | 
			
		||||
  info!("Prelogin with params: {:?}", params);
 | 
			
		||||
 | 
			
		||||
  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)
 | 
			
		||||
@@ -126,38 +123,36 @@ pub async fn prelogin(portal: &str, gp_params: &GpParams) -> anyhow::Result<Prel
 | 
			
		||||
    .await
 | 
			
		||||
    .map_err(|e| anyhow::anyhow!(PortalError::NetworkError(e.to_string())))?;
 | 
			
		||||
 | 
			
		||||
  let status = res.status();
 | 
			
		||||
  if status == StatusCode::NOT_FOUND {
 | 
			
		||||
  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 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)
 | 
			
		||||
    if err.is_status_error() {
 | 
			
		||||
      warn!("{err}");
 | 
			
		||||
      bail!("Prelogin error: {}", err.reason)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
  let res_xml = res
 | 
			
		||||
    .text()
 | 
			
		||||
    .await
 | 
			
		||||
    .map_err(|e| PortalError::PreloginError(e.to_string()))?;
 | 
			
		||||
    Err(anyhow!(PortalError::PreloginError(err.reason)))
 | 
			
		||||
  })?;
 | 
			
		||||
 | 
			
		||||
  let prelogin = parse_res_xml(res_xml, is_gateway).map_err(|e| PortalError::PreloginError(e.to_string()))?;
 | 
			
		||||
  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(|| {
 | 
			
		||||
@@ -183,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 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")
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  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(),
 | 
			
		||||
    label_username,
 | 
			
		||||
    label_password,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  Ok(Prelogin::Standard(standard_prelogin))
 | 
			
		||||
  } else {
 | 
			
		||||
    Err(anyhow!("Invalid prelogin response"))
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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) => Ok(Credential::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(),
 | 
			
		||||
    })
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user