mirror of
				https://github.com/yuezk/GlobalProtect-openconnect.git
				synced 2025-05-20 07:26:58 -04:00 
			
		
		
		
	Compare commits
	
		
			38 Commits
		
	
	
		
			8446874290
			...
			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 | ||
| 
						 | 
					a884c41813 | ||
| 
						 | 
					879b977321 | ||
| 
						 | 
					e9cb253be1 | ||
| 
						 | 
					07eacae385 | 
							
								
								
									
										7
									
								
								.github/workflows/build.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								.github/workflows/build.yaml
									
									
									
									
										vendored
									
									
								
							@@ -25,7 +25,7 @@ jobs:
 | 
				
			|||||||
        id: set-matrix
 | 
					        id: set-matrix
 | 
				
			||||||
        run: |
 | 
					        run: |
 | 
				
			||||||
          if [[ "${{ github.ref }}" == "refs/tags/"* ]]; then
 | 
					          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
 | 
					          else
 | 
				
			||||||
            echo 'matrix=[{"runner": "ubuntu-latest", "arch": "amd64"}]' >> $GITHUB_OUTPUT
 | 
					            echo 'matrix=[{"runner": "ubuntu-latest", "arch": "amd64"}]' >> $GITHUB_OUTPUT
 | 
				
			||||||
          fi
 | 
					          fi
 | 
				
			||||||
@@ -68,7 +68,8 @@ jobs:
 | 
				
			|||||||
    - tarball
 | 
					    - tarball
 | 
				
			||||||
    strategy:
 | 
					    strategy:
 | 
				
			||||||
      matrix:
 | 
					      matrix:
 | 
				
			||||||
        os: ${{fromJson(needs.setup-matrix.outputs.matrix)}}
 | 
					        # Only build gp on amd64, as the arm64 package will be built in release.yaml
 | 
				
			||||||
 | 
					        os: [{runner: ubuntu-latest, arch: amd64}]
 | 
				
			||||||
        package: [deb, rpm, pkg, binary]
 | 
					        package: [deb, rpm, pkg, binary]
 | 
				
			||||||
    runs-on: ${{ matrix.os.runner }}
 | 
					    runs-on: ${{ matrix.os.runner }}
 | 
				
			||||||
    name: build-gp (${{ matrix.package }}, ${{ matrix.os.arch }})
 | 
					    name: build-gp (${{ matrix.package }}, ${{ matrix.os.arch }})
 | 
				
			||||||
@@ -182,7 +183,7 @@ jobs:
 | 
				
			|||||||
        gh -R "$REPO" release create $RELEASE_TAG \
 | 
					        gh -R "$REPO" release create $RELEASE_TAG \
 | 
				
			||||||
          --title "$RELEASE_TAG" \
 | 
					          --title "$RELEASE_TAG" \
 | 
				
			||||||
          --notes "$NOTES" \
 | 
					          --notes "$NOTES" \
 | 
				
			||||||
          --target ${{ github.ref }} \
 | 
					          ${{ github.ref == 'refs/heads/dev' && '--target dev' || '' }} \
 | 
				
			||||||
          ${{ github.ref == 'refs/heads/dev' && '--prerelease' || '' }} \
 | 
					          ${{ github.ref == 'refs/heads/dev' && '--prerelease' || '' }} \
 | 
				
			||||||
          gh-release/artifact-source/* \
 | 
					          gh-release/artifact-source/* \
 | 
				
			||||||
          gh-release/artifact-gpgui-*/*
 | 
					          gh-release/artifact-gpgui-*/*
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										4
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							@@ -1,6 +1,7 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
    "cSpell.words": [
 | 
					    "cSpell.words": [
 | 
				
			||||||
        "authcookie",
 | 
					        "authcookie",
 | 
				
			||||||
 | 
					        "badssl",
 | 
				
			||||||
        "bincode",
 | 
					        "bincode",
 | 
				
			||||||
        "chacha",
 | 
					        "chacha",
 | 
				
			||||||
        "clientos",
 | 
					        "clientos",
 | 
				
			||||||
@@ -25,7 +26,9 @@
 | 
				
			|||||||
        "LOGNAME",
 | 
					        "LOGNAME",
 | 
				
			||||||
        "oneshot",
 | 
					        "oneshot",
 | 
				
			||||||
        "openconnect",
 | 
					        "openconnect",
 | 
				
			||||||
 | 
					        "pkcs",
 | 
				
			||||||
        "pkexec",
 | 
					        "pkexec",
 | 
				
			||||||
 | 
					        "pkey",
 | 
				
			||||||
        "Prelogin",
 | 
					        "Prelogin",
 | 
				
			||||||
        "prelogon",
 | 
					        "prelogon",
 | 
				
			||||||
        "prelogonuserauthcookie",
 | 
					        "prelogonuserauthcookie",
 | 
				
			||||||
@@ -35,6 +38,7 @@
 | 
				
			|||||||
        "rspc",
 | 
					        "rspc",
 | 
				
			||||||
        "servercert",
 | 
					        "servercert",
 | 
				
			||||||
        "specta",
 | 
					        "specta",
 | 
				
			||||||
 | 
					        "sslkey",
 | 
				
			||||||
        "sysinfo",
 | 
					        "sysinfo",
 | 
				
			||||||
        "tanstack",
 | 
					        "tanstack",
 | 
				
			||||||
        "tauri",
 | 
					        "tauri",
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										59
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										59
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							@@ -252,6 +252,12 @@ version = "0.21.5"
 | 
				
			|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9"
 | 
					checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "base64"
 | 
				
			||||||
 | 
					version = "0.22.1"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "bitflags"
 | 
					name = "bitflags"
 | 
				
			||||||
version = "1.3.2"
 | 
					version = "1.3.2"
 | 
				
			||||||
@@ -564,7 +570,7 @@ dependencies = [
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "common"
 | 
					name = "common"
 | 
				
			||||||
version = "2.1.2"
 | 
					version = "2.3.1"
 | 
				
			||||||
dependencies = [
 | 
					dependencies = [
 | 
				
			||||||
 "is_executable",
 | 
					 "is_executable",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
@@ -1430,7 +1436,7 @@ dependencies = [
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "gpapi"
 | 
					name = "gpapi"
 | 
				
			||||||
version = "2.1.2"
 | 
					version = "2.3.1"
 | 
				
			||||||
dependencies = [
 | 
					dependencies = [
 | 
				
			||||||
 "anyhow",
 | 
					 "anyhow",
 | 
				
			||||||
 "base64 0.21.5",
 | 
					 "base64 0.21.5",
 | 
				
			||||||
@@ -1440,6 +1446,8 @@ dependencies = [
 | 
				
			|||||||
 "log",
 | 
					 "log",
 | 
				
			||||||
 "md5",
 | 
					 "md5",
 | 
				
			||||||
 "open",
 | 
					 "open",
 | 
				
			||||||
 | 
					 "openssl",
 | 
				
			||||||
 | 
					 "pem",
 | 
				
			||||||
 "redact-engine",
 | 
					 "redact-engine",
 | 
				
			||||||
 "regex",
 | 
					 "regex",
 | 
				
			||||||
 "reqwest",
 | 
					 "reqwest",
 | 
				
			||||||
@@ -1462,7 +1470,7 @@ dependencies = [
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "gpauth"
 | 
					name = "gpauth"
 | 
				
			||||||
version = "2.1.2"
 | 
					version = "2.3.1"
 | 
				
			||||||
dependencies = [
 | 
					dependencies = [
 | 
				
			||||||
 "anyhow",
 | 
					 "anyhow",
 | 
				
			||||||
 "clap",
 | 
					 "clap",
 | 
				
			||||||
@@ -1483,7 +1491,7 @@ dependencies = [
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "gpclient"
 | 
					name = "gpclient"
 | 
				
			||||||
version = "2.1.2"
 | 
					version = "2.3.1"
 | 
				
			||||||
dependencies = [
 | 
					dependencies = [
 | 
				
			||||||
 "anyhow",
 | 
					 "anyhow",
 | 
				
			||||||
 "clap",
 | 
					 "clap",
 | 
				
			||||||
@@ -1505,7 +1513,7 @@ dependencies = [
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "gpgui-helper"
 | 
					name = "gpgui-helper"
 | 
				
			||||||
version = "2.1.2"
 | 
					version = "2.3.1"
 | 
				
			||||||
dependencies = [
 | 
					dependencies = [
 | 
				
			||||||
 "anyhow",
 | 
					 "anyhow",
 | 
				
			||||||
 "clap",
 | 
					 "clap",
 | 
				
			||||||
@@ -1523,7 +1531,7 @@ dependencies = [
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "gpservice"
 | 
					name = "gpservice"
 | 
				
			||||||
version = "2.1.2"
 | 
					version = "2.3.1"
 | 
				
			||||||
dependencies = [
 | 
					dependencies = [
 | 
				
			||||||
 "anyhow",
 | 
					 "anyhow",
 | 
				
			||||||
 "axum",
 | 
					 "axum",
 | 
				
			||||||
@@ -1599,9 +1607,9 @@ dependencies = [
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "h2"
 | 
					name = "h2"
 | 
				
			||||||
version = "0.3.24"
 | 
					version = "0.3.26"
 | 
				
			||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9"
 | 
					checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8"
 | 
				
			||||||
dependencies = [
 | 
					dependencies = [
 | 
				
			||||||
 "bytes",
 | 
					 "bytes",
 | 
				
			||||||
 "fnv",
 | 
					 "fnv",
 | 
				
			||||||
@@ -1618,9 +1626,9 @@ dependencies = [
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "h2"
 | 
					name = "h2"
 | 
				
			||||||
version = "0.4.2"
 | 
					version = "0.4.4"
 | 
				
			||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
checksum = "31d030e59af851932b72ceebadf4a2b5986dba4c3b99dd2493f8273a0f151943"
 | 
					checksum = "816ec7294445779408f36fe57bc5b7fc1cf59664059096c65f905c1c61f58069"
 | 
				
			||||||
dependencies = [
 | 
					dependencies = [
 | 
				
			||||||
 "bytes",
 | 
					 "bytes",
 | 
				
			||||||
 "fnv",
 | 
					 "fnv",
 | 
				
			||||||
@@ -1787,7 +1795,7 @@ dependencies = [
 | 
				
			|||||||
 "futures-channel",
 | 
					 "futures-channel",
 | 
				
			||||||
 "futures-core",
 | 
					 "futures-core",
 | 
				
			||||||
 "futures-util",
 | 
					 "futures-util",
 | 
				
			||||||
 "h2 0.3.24",
 | 
					 "h2 0.3.26",
 | 
				
			||||||
 "http 0.2.11",
 | 
					 "http 0.2.11",
 | 
				
			||||||
 "http-body 0.4.6",
 | 
					 "http-body 0.4.6",
 | 
				
			||||||
 "httparse",
 | 
					 "httparse",
 | 
				
			||||||
@@ -1810,7 +1818,7 @@ dependencies = [
 | 
				
			|||||||
 "bytes",
 | 
					 "bytes",
 | 
				
			||||||
 "futures-channel",
 | 
					 "futures-channel",
 | 
				
			||||||
 "futures-util",
 | 
					 "futures-util",
 | 
				
			||||||
 "h2 0.4.2",
 | 
					 "h2 0.4.4",
 | 
				
			||||||
 "http 1.0.0",
 | 
					 "http 1.0.0",
 | 
				
			||||||
 "http-body 1.0.0",
 | 
					 "http-body 1.0.0",
 | 
				
			||||||
 "httparse",
 | 
					 "httparse",
 | 
				
			||||||
@@ -2537,7 +2545,7 @@ dependencies = [
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "openconnect"
 | 
					name = "openconnect"
 | 
				
			||||||
version = "2.1.2"
 | 
					version = "2.3.1"
 | 
				
			||||||
dependencies = [
 | 
					dependencies = [
 | 
				
			||||||
 "cc",
 | 
					 "cc",
 | 
				
			||||||
 "common",
 | 
					 "common",
 | 
				
			||||||
@@ -2670,6 +2678,16 @@ version = "0.2.1"
 | 
				
			|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
 | 
					checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "pem"
 | 
				
			||||||
 | 
					version = "3.0.4"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "base64 0.22.1",
 | 
				
			||||||
 | 
					 "serde",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "percent-encoding"
 | 
					name = "percent-encoding"
 | 
				
			||||||
version = "2.3.1"
 | 
					version = "2.3.1"
 | 
				
			||||||
@@ -3167,7 +3185,7 @@ dependencies = [
 | 
				
			|||||||
 "encoding_rs",
 | 
					 "encoding_rs",
 | 
				
			||||||
 "futures-core",
 | 
					 "futures-core",
 | 
				
			||||||
 "futures-util",
 | 
					 "futures-util",
 | 
				
			||||||
 "h2 0.3.24",
 | 
					 "h2 0.3.26",
 | 
				
			||||||
 "http 0.2.11",
 | 
					 "http 0.2.11",
 | 
				
			||||||
 "http-body 0.4.6",
 | 
					 "http-body 0.4.6",
 | 
				
			||||||
 "hyper 0.14.28",
 | 
					 "hyper 0.14.28",
 | 
				
			||||||
@@ -4606,6 +4624,12 @@ version = "0.11.0+wasi-snapshot-preview1"
 | 
				
			|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
 | 
					checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "wasite"
 | 
				
			||||||
 | 
					version = "0.1.0"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "wasm-bindgen"
 | 
					name = "wasm-bindgen"
 | 
				
			||||||
version = "0.2.89"
 | 
					version = "0.2.89"
 | 
				
			||||||
@@ -4782,11 +4806,12 @@ dependencies = [
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "whoami"
 | 
					name = "whoami"
 | 
				
			||||||
version = "1.4.1"
 | 
					version = "1.5.1"
 | 
				
			||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
checksum = "22fc3756b8a9133049b26c7f61ab35416c130e8c09b660f5b3958b446f52cc50"
 | 
					checksum = "a44ab49fad634e88f55bf8f9bb3abd2f27d7204172a112c7c9987e01c1c94ea9"
 | 
				
			||||||
dependencies = [
 | 
					dependencies = [
 | 
				
			||||||
 "wasm-bindgen",
 | 
					 "redox_syscall",
 | 
				
			||||||
 | 
					 "wasite",
 | 
				
			||||||
 "web-sys",
 | 
					 "web-sys",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,7 +5,7 @@ members = ["crates/*", "apps/gpclient", "apps/gpservice", "apps/gpauth", "apps/g
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
[workspace.package]
 | 
					[workspace.package]
 | 
				
			||||||
rust-version = "1.70"
 | 
					rust-version = "1.70"
 | 
				
			||||||
version = "2.1.2"
 | 
					version = "2.3.1"
 | 
				
			||||||
authors = ["Kevin Yue <k3vinyue@gmail.com>"]
 | 
					authors = ["Kevin Yue <k3vinyue@gmail.com>"]
 | 
				
			||||||
homepage = "https://github.com/yuezk/GlobalProtect-openconnect"
 | 
					homepage = "https://github.com/yuezk/GlobalProtect-openconnect"
 | 
				
			||||||
edition = "2021"
 | 
					edition = "2021"
 | 
				
			||||||
@@ -22,6 +22,8 @@ is_executable = "1.0"
 | 
				
			|||||||
log = "0.4"
 | 
					log = "0.4"
 | 
				
			||||||
regex = "1"
 | 
					regex = "1"
 | 
				
			||||||
reqwest = { version = "0.11", features = ["native-tls-vendored", "json"] }
 | 
					reqwest = { version = "0.11", features = ["native-tls-vendored", "json"] }
 | 
				
			||||||
 | 
					openssl = "0.10"
 | 
				
			||||||
 | 
					pem = "3"
 | 
				
			||||||
roxmltree = "0.18"
 | 
					roxmltree = "0.18"
 | 
				
			||||||
serde = { version = "1.0", features = ["derive"] }
 | 
					serde = { version = "1.0", features = ["derive"] }
 | 
				
			||||||
serde_json = "1.0"
 | 
					serde_json = "1.0"
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										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 both SSO and non-SSO authentication
 | 
				
			||||||
- [x] Support the FIDO2 authentication (e.g., YubiKey)
 | 
					- [x] Support the FIDO2 authentication (e.g., YubiKey)
 | 
				
			||||||
- [x] Support authentication using default browser
 | 
					- [x] Support authentication using default browser
 | 
				
			||||||
 | 
					- [x] Support client certificate authentication
 | 
				
			||||||
- [x] Support multiple portals
 | 
					- [x] Support multiple portals
 | 
				
			||||||
- [x] Support gateway selection
 | 
					- [x] Support gateway selection
 | 
				
			||||||
- [x] Support connect gateway directly
 | 
					- [x] Support connect gateway directly
 | 
				
			||||||
@@ -43,6 +44,12 @@ Options:
 | 
				
			|||||||
See 'gpclient help <command>' for more information on a specific command.
 | 
					See 'gpclient help <command>' for more information on a specific command.
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					To use the default browser for authentication with the CLI version, you need to use the following command:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```bash
 | 
				
			||||||
 | 
					sudo -E gpclient connect --default-browser <portal>
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### GUI
 | 
					### GUI
 | 
				
			||||||
 | 
					
 | 
				
			||||||
The GUI version is also available after you installed it. You can launch it from the application menu or run `gpclient launch-gui` in the terminal.
 | 
					The GUI version is also available after you installed it. You can launch it from the application menu or run `gpclient launch-gui` in the terminal.
 | 
				
			||||||
@@ -55,7 +62,7 @@ The GUI version is also available after you installed it. You can launch it from
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
### Debian/Ubuntu based distributions
 | 
					### Debian/Ubuntu based distributions
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#### Install from PPA
 | 
					#### Install from PPA (Ubuntu 18.04 and later, except 24.04)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
sudo apt-get install gir1.2-gtk-3.0 gir1.2-webkit2-4.0
 | 
					sudo apt-get install gir1.2-gtk-3.0 gir1.2-webkit2-4.0
 | 
				
			||||||
@@ -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`.
 | 
					> For Linux Mint, you might need to import the GPG key with: `sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 7937C393082992E5D6E4A60453FC26B43838D761` if you encountered an error `gpg: keyserver receive failed: General error`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#### Install from deb package
 | 
					#### **Ubuntu 24.04 and later**
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Download the latest deb package from [releases](https://github.com/yuezk/GlobalProtect-openconnect/releases) page. Then install it with `dpkg`:
 | 
					The `libwebkit2gtk-4.0-37` package was [removed](https://bugs.launchpad.net/ubuntu/+source/webkit2gtk/+bug/2061914) from its repo, before [the issue](https://github.com/yuezk/GlobalProtect-openconnect/issues/351) gets resolved, you need to install them manually:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
```bash
 | 
					```bash
 | 
				
			||||||
sudo dpkg -i globalprotect-openconnect_*.deb
 | 
					wget http://launchpadlibrarian.net/704701349/libwebkit2gtk-4.0-37_2.43.3-1_amd64.deb
 | 
				
			||||||
 | 
					wget http://launchpadlibrarian.net/704701345/libjavascriptcoregtk-4.0-18_2.43.3-1_amd64.deb
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					sudo dpkg --install *.deb
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					And the latest package is not available in the PPA, you can follow the [Install from deb package](#install-from-deb-package) section to install the latest package.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#### **Ubuntu 18.04**
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The latest package is not available in the PPA either, but you still needs to add the `ppa:yuezk/globalprotect-openconnect` repo beforehand to use the required `openconnect` package. Then you can follow the [Install from deb package](#install-from-deb-package) section to install the latest package.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#### Install from deb package
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Download the latest deb package from [releases](https://github.com/yuezk/GlobalProtect-openconnect/releases) page. Then install it with `apt`:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```bash
 | 
				
			||||||
 | 
					sudo apt install --fix-broken globalprotect-openconnect_*.deb
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Arch Linux / Manjaro
 | 
					### Arch Linux / Manjaro
 | 
				
			||||||
@@ -120,6 +144,30 @@ Download the latest RPM package from [releases](https://github.com/yuezk/GlobalP
 | 
				
			|||||||
```bash
 | 
					```bash
 | 
				
			||||||
sudo rpm -i globalprotect-openconnect-*.rpm
 | 
					sudo rpm -i globalprotect-openconnect-*.rpm
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
 | 
					### Gentoo
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Install from the ```rios``` or ```slonko``` overlays.  Example using rios:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#### 1. Enable the overlay
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					sudo eselect repository enable rios
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#### 2. Sync with the repository
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  - If you have eix installed, use it:
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					sudo eix-sync
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					  - Otherwise, use:
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					sudo emerge --sync
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#### 3. Install
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```sudo emerge globalprotect-openconnect```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Other distributions
 | 
					### Other distributions
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -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`
 | 
					1. How to deal with error `Secure Storage not ready`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   Try upgrade the client to `2.2.0` or later, which will use a file-based storage as a fallback.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
   You need to install the `gnome-keyring` package, and restart the system (See [#321](https://github.com/yuezk/GlobalProtect-openconnect/issues/321), [#316](https://github.com/yuezk/GlobalProtect-openconnect/issues/316)).
 | 
					   You need to install the `gnome-keyring` package, and restart the system (See [#321](https://github.com/yuezk/GlobalProtect-openconnect/issues/321), [#316](https://github.com/yuezk/GlobalProtect-openconnect/issues/316)).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
2. How to deal with error `(gpauth:18869): Gtk-WARNING **: 10:33:37.566: cannot open display:`
 | 
					2. How to deal with error `(gpauth:18869): Gtk-WARNING **: 10:33:37.566: cannot open display:`
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,7 +8,11 @@ license.workspace = true
 | 
				
			|||||||
tauri-build = { version = "1.5", features = [] }
 | 
					tauri-build = { version = "1.5", features = [] }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[dependencies]
 | 
					[dependencies]
 | 
				
			||||||
gpapi = { path = "../../crates/gpapi", features = ["tauri", "clap"] }
 | 
					gpapi = { path = "../../crates/gpapi", features = [
 | 
				
			||||||
 | 
					  "tauri",
 | 
				
			||||||
 | 
					  "clap",
 | 
				
			||||||
 | 
					  "browser-auth",
 | 
				
			||||||
 | 
					] }
 | 
				
			||||||
anyhow.workspace = true
 | 
					anyhow.workspace = true
 | 
				
			||||||
clap.workspace = true
 | 
					clap.workspace = true
 | 
				
			||||||
env_logger.workspace = true
 | 
					env_logger.workspace = true
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -366,17 +366,14 @@ fn read_auth_data_from_html(html: &str) -> Result<SamlAuthData, AuthDataParseErr
 | 
				
			|||||||
    return Err(AuthDataParseError::Invalid);
 | 
					    return Err(AuthDataParseError::Invalid);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  match SamlAuthData::from_html(html) {
 | 
					  SamlAuthData::from_html(html).or_else(|err| {
 | 
				
			||||||
    Ok(auth_data) => Ok(auth_data),
 | 
					    if let Some(gpcallback) = extract_gpcallback(html) {
 | 
				
			||||||
    Err(err) => {
 | 
					      info!("Found gpcallback from html...");
 | 
				
			||||||
      if let Some(gpcallback) = extract_gpcallback(html) {
 | 
					      SamlAuthData::from_gpcallback(&gpcallback)
 | 
				
			||||||
        info!("Found gpcallback from html...");
 | 
					    } else {
 | 
				
			||||||
        SamlAuthData::from_gpcallback(&gpcallback)
 | 
					      Err(err)
 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        Err(err)
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  })
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
fn extract_gpcallback(html: &str) -> Option<String> {
 | 
					fn extract_gpcallback(html: &str) -> Option<String> {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,6 +3,7 @@ use gpapi::{
 | 
				
			|||||||
  auth::{SamlAuthData, SamlAuthResult},
 | 
					  auth::{SamlAuthData, SamlAuthResult},
 | 
				
			||||||
  clap::args::Os,
 | 
					  clap::args::Os,
 | 
				
			||||||
  gp_params::{ClientOs, GpParams},
 | 
					  gp_params::{ClientOs, GpParams},
 | 
				
			||||||
 | 
					  process::browser_authenticator::BrowserAuthenticator,
 | 
				
			||||||
  utils::{normalize_server, openssl},
 | 
					  utils::{normalize_server, openssl},
 | 
				
			||||||
  GP_USER_AGENT,
 | 
					  GP_USER_AGENT,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
@@ -37,6 +38,8 @@ struct Cli {
 | 
				
			|||||||
  ignore_tls_errors: bool,
 | 
					  ignore_tls_errors: bool,
 | 
				
			||||||
  #[arg(long)]
 | 
					  #[arg(long)]
 | 
				
			||||||
  clean: bool,
 | 
					  clean: bool,
 | 
				
			||||||
 | 
					  #[arg(long)]
 | 
				
			||||||
 | 
					  default_browser: bool,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl Cli {
 | 
					impl Cli {
 | 
				
			||||||
@@ -56,6 +59,15 @@ impl Cli {
 | 
				
			|||||||
      None => portal_prelogin(&self.server, &gp_params).await?,
 | 
					      None => portal_prelogin(&self.server, &gp_params).await?,
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if self.default_browser {
 | 
				
			||||||
 | 
					      let browser_auth = BrowserAuthenticator::new(&saml_request);
 | 
				
			||||||
 | 
					      browser_auth.authenticate()?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      info!("Please continue the authentication process in the default browser");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return Ok(());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    self.saml_request.replace(saml_request);
 | 
					    self.saml_request.replace(saml_request);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let app = create_app(self.clone())?;
 | 
					    let app = create_app(self.clone())?;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
use std::{fs, sync::Arc};
 | 
					use std::{cell::RefCell, fs, sync::Arc};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use clap::Args;
 | 
					use clap::Args;
 | 
				
			||||||
use common::vpn_utils::find_csd_wrapper;
 | 
					use common::vpn_utils::find_csd_wrapper;
 | 
				
			||||||
@@ -6,21 +6,22 @@ use gpapi::{
 | 
				
			|||||||
  clap::args::Os,
 | 
					  clap::args::Os,
 | 
				
			||||||
  credential::{Credential, PasswordCredential},
 | 
					  credential::{Credential, PasswordCredential},
 | 
				
			||||||
  error::PortalError,
 | 
					  error::PortalError,
 | 
				
			||||||
  gateway::gateway_login,
 | 
					  gateway::{gateway_login, GatewayLogin},
 | 
				
			||||||
  gp_params::{ClientOs, GpParams},
 | 
					  gp_params::{ClientOs, GpParams},
 | 
				
			||||||
  portal::{prelogin, retrieve_config, Prelogin},
 | 
					  portal::{prelogin, retrieve_config, Prelogin},
 | 
				
			||||||
  process::{
 | 
					  process::{
 | 
				
			||||||
    auth_launcher::SamlAuthLauncher,
 | 
					    auth_launcher::SamlAuthLauncher,
 | 
				
			||||||
    users::{get_non_root_user, get_user_by_name},
 | 
					    users::{get_non_root_user, get_user_by_name},
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  utils::shutdown_signal,
 | 
					  utils::{request::RequestIdentityError, shutdown_signal},
 | 
				
			||||||
  GP_USER_AGENT,
 | 
					  GP_USER_AGENT,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
use inquire::{Password, PasswordDisplayMode, Select, Text};
 | 
					use inquire::{Password, PasswordDisplayMode, Select, Text};
 | 
				
			||||||
use log::info;
 | 
					use log::info;
 | 
				
			||||||
use openconnect::Vpn;
 | 
					use openconnect::Vpn;
 | 
				
			||||||
 | 
					use tokio::{io::AsyncReadExt, net::TcpListener};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{cli::SharedArgs, GP_CLIENT_LOCK_FILE};
 | 
					use crate::{cli::SharedArgs, GP_CLIENT_LOCK_FILE, GP_CLIENT_PORT_FILE};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Args)]
 | 
					#[derive(Args)]
 | 
				
			||||||
pub(crate) struct ConnectArgs {
 | 
					pub(crate) struct ConnectArgs {
 | 
				
			||||||
@@ -32,6 +33,8 @@ pub(crate) struct ConnectArgs {
 | 
				
			|||||||
  user: Option<String>,
 | 
					  user: Option<String>,
 | 
				
			||||||
  #[arg(long, short, help = "The VPNC script to use")]
 | 
					  #[arg(long, short, help = "The VPNC script to use")]
 | 
				
			||||||
  script: Option<String>,
 | 
					  script: Option<String>,
 | 
				
			||||||
 | 
					  #[arg(long, help = "Connect the server as a gateway, instead of a portal")]
 | 
				
			||||||
 | 
					  as_gateway: bool,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  #[arg(
 | 
					  #[arg(
 | 
				
			||||||
    long,
 | 
					    long,
 | 
				
			||||||
@@ -39,14 +42,29 @@ pub(crate) struct ConnectArgs {
 | 
				
			|||||||
  )]
 | 
					  )]
 | 
				
			||||||
  hip: bool,
 | 
					  hip: bool,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  #[arg(
 | 
				
			||||||
 | 
					    short,
 | 
				
			||||||
 | 
					    long,
 | 
				
			||||||
 | 
					    help = "Use SSL client certificate file in pkcs#8 (.pem) or pkcs#12 (.p12, .pfx) format"
 | 
				
			||||||
 | 
					  )]
 | 
				
			||||||
 | 
					  certificate: Option<String>,
 | 
				
			||||||
 | 
					  #[arg(short = 'k', long, help = "Use SSL private key file in pkcs#8 (.pem) format")]
 | 
				
			||||||
 | 
					  sslkey: Option<String>,
 | 
				
			||||||
 | 
					  #[arg(short = 'p', long, help = "The key passphrase of the private key")]
 | 
				
			||||||
 | 
					  key_password: Option<String>,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  #[arg(long, help = "Same as the '--csd-user' option in the openconnect command")]
 | 
					  #[arg(long, help = "Same as the '--csd-user' option in the openconnect command")]
 | 
				
			||||||
  csd_user: Option<String>,
 | 
					  csd_user: Option<String>,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  #[arg(long, help = "Same as the '--csd-wrapper' option in the openconnect command")]
 | 
					  #[arg(long, help = "Same as the '--csd-wrapper' option in the openconnect command")]
 | 
				
			||||||
  csd_wrapper: Option<String>,
 | 
					  csd_wrapper: Option<String>,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  #[arg(long, default_value = "300", help = "Reconnection retry timeout in seconds")]
 | 
				
			||||||
 | 
					  reconnect_timeout: u32,
 | 
				
			||||||
  #[arg(short, long, help = "Request MTU from server (legacy servers only)")]
 | 
					  #[arg(short, long, help = "Request MTU from server (legacy servers only)")]
 | 
				
			||||||
  mtu: Option<u32>,
 | 
					  mtu: Option<u32>,
 | 
				
			||||||
 | 
					  #[arg(long, help = "Do not ask for IPv6 connectivity")]
 | 
				
			||||||
 | 
					  disable_ipv6: bool,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  #[arg(long, default_value = GP_USER_AGENT, help = "The user agent to use")]
 | 
					  #[arg(long, default_value = GP_USER_AGENT, help = "The user agent to use")]
 | 
				
			||||||
  user_agent: String,
 | 
					  user_agent: String,
 | 
				
			||||||
@@ -58,6 +76,8 @@ pub(crate) struct ConnectArgs {
 | 
				
			|||||||
  hidpi: bool,
 | 
					  hidpi: bool,
 | 
				
			||||||
  #[arg(long, help = "Do not reuse the remembered authentication cookie")]
 | 
					  #[arg(long, help = "Do not reuse the remembered authentication cookie")]
 | 
				
			||||||
  clean: bool,
 | 
					  clean: bool,
 | 
				
			||||||
 | 
					  #[arg(long, help = "Use the default browser to authenticate")]
 | 
				
			||||||
 | 
					  default_browser: bool,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl ConnectArgs {
 | 
					impl ConnectArgs {
 | 
				
			||||||
@@ -77,11 +97,16 @@ impl ConnectArgs {
 | 
				
			|||||||
pub(crate) struct ConnectHandler<'a> {
 | 
					pub(crate) struct ConnectHandler<'a> {
 | 
				
			||||||
  args: &'a ConnectArgs,
 | 
					  args: &'a ConnectArgs,
 | 
				
			||||||
  shared_args: &'a SharedArgs,
 | 
					  shared_args: &'a SharedArgs,
 | 
				
			||||||
 | 
					  latest_key_password: RefCell<Option<String>>,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl<'a> ConnectHandler<'a> {
 | 
					impl<'a> ConnectHandler<'a> {
 | 
				
			||||||
  pub(crate) fn new(args: &'a ConnectArgs, shared_args: &'a SharedArgs) -> Self {
 | 
					  pub(crate) fn new(args: &'a ConnectArgs, shared_args: &'a SharedArgs) -> Self {
 | 
				
			||||||
    Self { args, shared_args }
 | 
					    Self {
 | 
				
			||||||
 | 
					      args,
 | 
				
			||||||
 | 
					      shared_args,
 | 
				
			||||||
 | 
					      latest_key_password: Default::default(),
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  fn build_gp_params(&self) -> GpParams {
 | 
					  fn build_gp_params(&self) -> GpParams {
 | 
				
			||||||
@@ -90,11 +115,52 @@ impl<'a> ConnectHandler<'a> {
 | 
				
			|||||||
      .client_os(ClientOs::from(&self.args.os))
 | 
					      .client_os(ClientOs::from(&self.args.os))
 | 
				
			||||||
      .os_version(self.args.os_version())
 | 
					      .os_version(self.args.os_version())
 | 
				
			||||||
      .ignore_tls_errors(self.shared_args.ignore_tls_errors)
 | 
					      .ignore_tls_errors(self.shared_args.ignore_tls_errors)
 | 
				
			||||||
 | 
					      .certificate(self.args.certificate.clone())
 | 
				
			||||||
 | 
					      .sslkey(self.args.sslkey.clone())
 | 
				
			||||||
 | 
					      .key_password(self.latest_key_password.borrow().clone())
 | 
				
			||||||
      .build()
 | 
					      .build()
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  pub(crate) async fn handle(&self) -> anyhow::Result<()> {
 | 
					  pub(crate) async fn handle(&self) -> anyhow::Result<()> {
 | 
				
			||||||
 | 
					    self.latest_key_password.replace(self.args.key_password.clone());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    loop {
 | 
				
			||||||
 | 
					      let Err(err) = self.handle_impl().await else {
 | 
				
			||||||
 | 
					        return Ok(());
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      let Some(root_cause) = err.root_cause().downcast_ref::<RequestIdentityError>() else {
 | 
				
			||||||
 | 
					        return Err(err);
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      match root_cause {
 | 
				
			||||||
 | 
					        RequestIdentityError::NoKey => {
 | 
				
			||||||
 | 
					          eprintln!("ERROR: No private key found in the certificate file");
 | 
				
			||||||
 | 
					          eprintln!("ERROR: Please provide the private key file using the `-k` option");
 | 
				
			||||||
 | 
					          return Ok(());
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        RequestIdentityError::NoPassphrase(cert_type) | RequestIdentityError::DecryptError(cert_type) => {
 | 
				
			||||||
 | 
					          // Decrypt the private key error, ask for the key password
 | 
				
			||||||
 | 
					          let message = format!("Enter the {} passphrase:", cert_type);
 | 
				
			||||||
 | 
					          let password = Password::new(&message)
 | 
				
			||||||
 | 
					            .without_confirmation()
 | 
				
			||||||
 | 
					            .with_display_mode(PasswordDisplayMode::Masked)
 | 
				
			||||||
 | 
					            .prompt()?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          self.latest_key_password.replace(Some(password));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  pub(crate) async fn handle_impl(&self) -> anyhow::Result<()> {
 | 
				
			||||||
    let server = self.args.server.as_str();
 | 
					    let server = self.args.server.as_str();
 | 
				
			||||||
 | 
					    let as_gateway = self.args.as_gateway;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if as_gateway {
 | 
				
			||||||
 | 
					      info!("Treating the server as a gateway");
 | 
				
			||||||
 | 
					      return self.connect_gateway_with_prelogin(server).await;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let Err(err) = self.connect_portal_with_prelogin(server).await else {
 | 
					    let Err(err) = self.connect_portal_with_prelogin(server).await else {
 | 
				
			||||||
      return Ok(());
 | 
					      return Ok(());
 | 
				
			||||||
@@ -103,10 +169,15 @@ impl<'a> ConnectHandler<'a> {
 | 
				
			|||||||
    info!("Failed to connect portal with prelogin: {}", err);
 | 
					    info!("Failed to connect portal with prelogin: {}", err);
 | 
				
			||||||
    if err.root_cause().downcast_ref::<PortalError>().is_some() {
 | 
					    if err.root_cause().downcast_ref::<PortalError>().is_some() {
 | 
				
			||||||
      info!("Trying the gateway authentication workflow...");
 | 
					      info!("Trying the gateway authentication workflow...");
 | 
				
			||||||
      return self.connect_gateway_with_prelogin(server).await;
 | 
					      self.connect_gateway_with_prelogin(server).await?;
 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Err(err)
 | 
					      eprintln!("\nNOTE: the server may be a gateway, not a portal.");
 | 
				
			||||||
 | 
					      eprintln!("NOTE: try to use the `--as-gateway` option if you were authenticated twice.");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      Ok(())
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      Err(err)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async fn connect_portal_with_prelogin(&self, portal: &str) -> anyhow::Result<()> {
 | 
					  async fn connect_portal_with_prelogin(&self, portal: &str) -> anyhow::Result<()> {
 | 
				
			||||||
@@ -141,7 +212,7 @@ impl<'a> ConnectHandler<'a> {
 | 
				
			|||||||
    let gateway = selected_gateway.server();
 | 
					    let gateway = selected_gateway.server();
 | 
				
			||||||
    let cred = portal_config.auth_cookie().into();
 | 
					    let cred = portal_config.auth_cookie().into();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let cookie = match gateway_login(gateway, &cred, &gp_params).await {
 | 
					    let cookie = match self.login_gateway(gateway, &cred, &gp_params).await {
 | 
				
			||||||
      Ok(cookie) => cookie,
 | 
					      Ok(cookie) => cookie,
 | 
				
			||||||
      Err(err) => {
 | 
					      Err(err) => {
 | 
				
			||||||
        info!("Gateway login failed: {}", err);
 | 
					        info!("Gateway login failed: {}", err);
 | 
				
			||||||
@@ -161,11 +232,28 @@ impl<'a> ConnectHandler<'a> {
 | 
				
			|||||||
    let prelogin = prelogin(gateway, &gp_params).await?;
 | 
					    let prelogin = prelogin(gateway, &gp_params).await?;
 | 
				
			||||||
    let cred = self.obtain_credential(&prelogin, gateway).await?;
 | 
					    let cred = self.obtain_credential(&prelogin, gateway).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let cookie = gateway_login(gateway, &cred, &gp_params).await?;
 | 
					    let cookie = self.login_gateway(gateway, &cred, &gp_params).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    self.connect_gateway(gateway, &cookie).await
 | 
					    self.connect_gateway(gateway, &cookie).await
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async fn login_gateway(&self, gateway: &str, cred: &Credential, gp_params: &GpParams) -> anyhow::Result<String> {
 | 
				
			||||||
 | 
					    let mut gp_params = gp_params.clone();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    loop {
 | 
				
			||||||
 | 
					      match gateway_login(gateway, cred, &gp_params).await? {
 | 
				
			||||||
 | 
					        GatewayLogin::Cookie(cookie) => return Ok(cookie),
 | 
				
			||||||
 | 
					        GatewayLogin::Mfa(message, input_str) => {
 | 
				
			||||||
 | 
					          let otp = Text::new(&message).prompt()?;
 | 
				
			||||||
 | 
					          gp_params.set_input_str(&input_str);
 | 
				
			||||||
 | 
					          gp_params.set_otp(&otp);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          info!("Retrying gateway login with MFA...");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async fn connect_gateway(&self, gateway: &str, cookie: &str) -> anyhow::Result<()> {
 | 
					  async fn connect_gateway(&self, gateway: &str, cookie: &str) -> anyhow::Result<()> {
 | 
				
			||||||
    let mtu = self.args.mtu.unwrap_or(0);
 | 
					    let mtu = self.args.mtu.unwrap_or(0);
 | 
				
			||||||
    let csd_uid = get_csd_uid(&self.args.csd_user)?;
 | 
					    let csd_uid = get_csd_uid(&self.args.csd_user)?;
 | 
				
			||||||
@@ -180,9 +268,14 @@ impl<'a> ConnectHandler<'a> {
 | 
				
			|||||||
    let vpn = Vpn::builder(gateway, cookie)
 | 
					    let vpn = Vpn::builder(gateway, cookie)
 | 
				
			||||||
      .script(self.args.script.clone())
 | 
					      .script(self.args.script.clone())
 | 
				
			||||||
      .user_agent(self.args.user_agent.clone())
 | 
					      .user_agent(self.args.user_agent.clone())
 | 
				
			||||||
 | 
					      .certificate(self.args.certificate.clone())
 | 
				
			||||||
 | 
					      .sslkey(self.args.sslkey.clone())
 | 
				
			||||||
 | 
					      .key_password(self.latest_key_password.borrow().clone())
 | 
				
			||||||
      .csd_uid(csd_uid)
 | 
					      .csd_uid(csd_uid)
 | 
				
			||||||
      .csd_wrapper(csd_wrapper)
 | 
					      .csd_wrapper(csd_wrapper)
 | 
				
			||||||
 | 
					      .reconnect_timeout(self.args.reconnect_timeout)
 | 
				
			||||||
      .mtu(mtu)
 | 
					      .mtu(mtu)
 | 
				
			||||||
 | 
					      .disable_ipv6(self.args.disable_ipv6)
 | 
				
			||||||
      .build()?;
 | 
					      .build()?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let vpn = Arc::new(vpn);
 | 
					    let vpn = Arc::new(vpn);
 | 
				
			||||||
@@ -210,7 +303,9 @@ impl<'a> ConnectHandler<'a> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    match prelogin {
 | 
					    match prelogin {
 | 
				
			||||||
      Prelogin::Saml(prelogin) => {
 | 
					      Prelogin::Saml(prelogin) => {
 | 
				
			||||||
        SamlAuthLauncher::new(&self.args.server)
 | 
					        let use_default_browser = prelogin.support_default_browser() && self.args.default_browser;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let cred = SamlAuthLauncher::new(&self.args.server)
 | 
				
			||||||
          .gateway(is_gateway)
 | 
					          .gateway(is_gateway)
 | 
				
			||||||
          .saml_request(prelogin.saml_request())
 | 
					          .saml_request(prelogin.saml_request())
 | 
				
			||||||
          .user_agent(&self.args.user_agent)
 | 
					          .user_agent(&self.args.user_agent)
 | 
				
			||||||
@@ -220,8 +315,21 @@ impl<'a> ConnectHandler<'a> {
 | 
				
			|||||||
          .fix_openssl(self.shared_args.fix_openssl)
 | 
					          .fix_openssl(self.shared_args.fix_openssl)
 | 
				
			||||||
          .ignore_tls_errors(self.shared_args.ignore_tls_errors)
 | 
					          .ignore_tls_errors(self.shared_args.ignore_tls_errors)
 | 
				
			||||||
          .clean(self.args.clean)
 | 
					          .clean(self.args.clean)
 | 
				
			||||||
 | 
					          .default_browser(use_default_browser)
 | 
				
			||||||
          .launch()
 | 
					          .launch()
 | 
				
			||||||
          .await
 | 
					          .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if let Some(cred) = cred {
 | 
				
			||||||
 | 
					          return Ok(cred);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if !use_default_browser {
 | 
				
			||||||
 | 
					          // This should never happen
 | 
				
			||||||
 | 
					          unreachable!("SAML authentication failed without using the default browser");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        info!("Waiting for the browser authentication to complete...");
 | 
				
			||||||
 | 
					        wait_credentials().await
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      Prelogin::Standard(prelogin) => {
 | 
					      Prelogin::Standard(prelogin) => {
 | 
				
			||||||
        let prefix = if is_gateway { "Gateway" } else { "Portal" };
 | 
					        let prefix = if is_gateway { "Gateway" } else { "Portal" };
 | 
				
			||||||
@@ -244,6 +352,27 @@ impl<'a> ConnectHandler<'a> {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async fn wait_credentials() -> anyhow::Result<Credential> {
 | 
				
			||||||
 | 
					  // Start a local server to receive the browser authentication data
 | 
				
			||||||
 | 
					  let listener = TcpListener::bind("127.0.0.1:0").await?;
 | 
				
			||||||
 | 
					  let port = listener.local_addr()?.port();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Write the port to a file
 | 
				
			||||||
 | 
					  fs::write(GP_CLIENT_PORT_FILE, port.to_string())?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  info!("Listening authentication data on port {}", port);
 | 
				
			||||||
 | 
					  let (mut socket, _) = listener.accept().await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  info!("Received the browser authentication data from the socket");
 | 
				
			||||||
 | 
					  let mut data = String::new();
 | 
				
			||||||
 | 
					  socket.read_to_string(&mut data).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Remove the port file
 | 
				
			||||||
 | 
					  fs::remove_file(GP_CLIENT_PORT_FILE)?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Credential::from_gpcallback(&data)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
fn write_pid_file() {
 | 
					fn write_pid_file() {
 | 
				
			||||||
  let pid = std::process::id();
 | 
					  let pid = std::process::id();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
use std::{collections::HashMap, fs, path::PathBuf};
 | 
					use std::{collections::HashMap, env::temp_dir, fs, path::PathBuf};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use clap::Args;
 | 
					use clap::Args;
 | 
				
			||||||
use directories::ProjectDirs;
 | 
					use directories::ProjectDirs;
 | 
				
			||||||
@@ -7,6 +7,9 @@ use gpapi::{
 | 
				
			|||||||
  utils::{endpoint::http_endpoint, env_file, shutdown_signal},
 | 
					  utils::{endpoint::http_endpoint, env_file, shutdown_signal},
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
use log::info;
 | 
					use log::info;
 | 
				
			||||||
 | 
					use tokio::io::AsyncWriteExt;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::GP_CLIENT_PORT_FILE;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Args)]
 | 
					#[derive(Args)]
 | 
				
			||||||
pub(crate) struct LaunchGuiArgs {
 | 
					pub(crate) struct LaunchGuiArgs {
 | 
				
			||||||
@@ -78,6 +81,16 @@ impl<'a> LaunchGuiHandler<'a> {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async fn feed_auth_data(auth_data: &str) -> anyhow::Result<()> {
 | 
					async fn feed_auth_data(auth_data: &str) -> anyhow::Result<()> {
 | 
				
			||||||
 | 
					  let _ = tokio::join!(feed_auth_data_gui(auth_data), feed_auth_data_cli(auth_data));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Cleanup the temporary file
 | 
				
			||||||
 | 
					  let html_file = temp_dir().join("gpauth.html");
 | 
				
			||||||
 | 
					  let _ = std::fs::remove_file(html_file);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async fn feed_auth_data_gui(auth_data: &str) -> anyhow::Result<()> {
 | 
				
			||||||
  let service_endpoint = http_endpoint().await?;
 | 
					  let service_endpoint = http_endpoint().await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  reqwest::Client::default()
 | 
					  reqwest::Client::default()
 | 
				
			||||||
@@ -90,6 +103,15 @@ async fn feed_auth_data(auth_data: &str) -> anyhow::Result<()> {
 | 
				
			|||||||
  Ok(())
 | 
					  Ok(())
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async fn feed_auth_data_cli(auth_data: &str) -> anyhow::Result<()> {
 | 
				
			||||||
 | 
					  let port = tokio::fs::read_to_string(GP_CLIENT_PORT_FILE).await?;
 | 
				
			||||||
 | 
					  let mut stream = tokio::net::TcpStream::connect(format!("127.0.0.1:{}", port.trim())).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  stream.write_all(auth_data.as_bytes()).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async fn try_active_gui() -> anyhow::Result<()> {
 | 
					async fn try_active_gui() -> anyhow::Result<()> {
 | 
				
			||||||
  let service_endpoint = http_endpoint().await?;
 | 
					  let service_endpoint = http_endpoint().await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,6 +4,7 @@ mod disconnect;
 | 
				
			|||||||
mod launch_gui;
 | 
					mod launch_gui;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub(crate) const GP_CLIENT_LOCK_FILE: &str = "/var/run/gpclient.lock";
 | 
					pub(crate) const GP_CLIENT_LOCK_FILE: &str = "/var/run/gpclient.lock";
 | 
				
			||||||
 | 
					pub(crate) const GP_CLIENT_PORT_FILE: &str = "/var/run/gpclient.port";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[tokio::main]
 | 
					#[tokio::main]
 | 
				
			||||||
async fn main() {
 | 
					async fn main() {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -38,10 +38,15 @@ impl VpnTaskContext {
 | 
				
			|||||||
    let vpn = match Vpn::builder(req.gateway().server(), args.cookie())
 | 
					    let vpn = match Vpn::builder(req.gateway().server(), args.cookie())
 | 
				
			||||||
      .script(args.vpnc_script())
 | 
					      .script(args.vpnc_script())
 | 
				
			||||||
      .user_agent(args.user_agent())
 | 
					      .user_agent(args.user_agent())
 | 
				
			||||||
 | 
					      .os(args.openconnect_os())
 | 
				
			||||||
 | 
					      .certificate(args.certificate())
 | 
				
			||||||
 | 
					      .sslkey(args.sslkey())
 | 
				
			||||||
 | 
					      .key_password(args.key_password())
 | 
				
			||||||
      .csd_uid(args.csd_uid())
 | 
					      .csd_uid(args.csd_uid())
 | 
				
			||||||
      .csd_wrapper(args.csd_wrapper())
 | 
					      .csd_wrapper(args.csd_wrapper())
 | 
				
			||||||
 | 
					      .reconnect_timeout(args.reconnect_timeout())
 | 
				
			||||||
      .mtu(args.mtu())
 | 
					      .mtu(args.mtu())
 | 
				
			||||||
      .os(args.openconnect_os())
 | 
					      .disable_ipv6(args.disable_ipv6())
 | 
				
			||||||
      .build()
 | 
					      .build()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
      Ok(vpn) => vpn,
 | 
					      Ok(vpn) => vpn,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -118,28 +118,41 @@ impl WsServer {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  pub async fn start(&self, shutdown_tx: mpsc::Sender<()>) {
 | 
					  pub async fn start(&self, shutdown_tx: mpsc::Sender<()>) {
 | 
				
			||||||
    if let Ok(listener) = TcpListener::bind("127.0.0.1:0").await {
 | 
					    let listener = match self.start_tcp_server().await {
 | 
				
			||||||
      let local_addr = listener.local_addr().unwrap();
 | 
					      Ok(listener) => listener,
 | 
				
			||||||
 | 
					      Err(err) => {
 | 
				
			||||||
 | 
					        warn!("Failed to start WS server: {}", err);
 | 
				
			||||||
 | 
					        let _ = shutdown_tx.send(()).await;
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      self.lock_file.lock(local_addr.port().to_string()).unwrap();
 | 
					    tokio::select! {
 | 
				
			||||||
 | 
					      _ = watch_vpn_state(self.ctx.vpn_state_rx(), Arc::clone(&self.ctx)) => {
 | 
				
			||||||
      info!("WS server listening on port: {}", local_addr.port());
 | 
					        info!("VPN state watch task completed");
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
      tokio::select! {
 | 
					      _ = start_server(listener, self.ctx.clone()) => {
 | 
				
			||||||
        _ = watch_vpn_state(self.ctx.vpn_state_rx(), Arc::clone(&self.ctx)) => {
 | 
					          info!("WS server stopped");
 | 
				
			||||||
          info!("VPN state watch task completed");
 | 
					      }
 | 
				
			||||||
        }
 | 
					      _ = self.cancel_token.cancelled() => {
 | 
				
			||||||
        _ = start_server(listener, self.ctx.clone()) => {
 | 
					        info!("WS server cancelled");
 | 
				
			||||||
            info!("WS server stopped");
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        _ = self.cancel_token.cancelled() => {
 | 
					 | 
				
			||||||
          info!("WS server cancelled");
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let _ = shutdown_tx.send(()).await;
 | 
					    let _ = shutdown_tx.send(()).await;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async fn start_tcp_server(&self) -> anyhow::Result<TcpListener> {
 | 
				
			||||||
 | 
					    let listener = TcpListener::bind("127.0.0.1:0").await?;
 | 
				
			||||||
 | 
					    let local_addr = listener.local_addr()?;
 | 
				
			||||||
 | 
					    let port = local_addr.port();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    info!("WS server listening on port: {}", port);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    self.lock_file.lock(port.to_string())?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(listener)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async fn watch_vpn_state(mut vpn_state_rx: watch::Receiver<VpnState>, ctx: Arc<WsServerContext>) {
 | 
					async fn watch_vpn_state(mut vpn_state_rx: watch::Receiver<VpnState>, ctx: Arc<WsServerContext>) {
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										31
									
								
								changelog.md
									
									
									
									
									
								
							
							
						
						
									
										31
									
								
								changelog.md
									
									
									
									
									
								
							@@ -1,5 +1,36 @@
 | 
				
			|||||||
# Changelog
 | 
					# 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
 | 
					## 2.1.2 - 2024-03-29
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- Treat portal as gateway when the gateway login is failed (fix #338)
 | 
					- Treat portal as gateway when the gateway login is failed (fix #338)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,6 @@
 | 
				
			|||||||
use is_executable::IsExecutable;
 | 
					use std::{io, path::Path};
 | 
				
			||||||
use std::path::Path;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub use is_executable::is_executable;
 | 
					use is_executable::IsExecutable;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const VPNC_SCRIPT_LOCATIONS: [&str; 6] = [
 | 
					const VPNC_SCRIPT_LOCATIONS: [&str; 6] = [
 | 
				
			||||||
  "/usr/local/share/vpnc-scripts/vpnc-script",
 | 
					  "/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> {
 | 
					pub fn find_csd_wrapper() -> Option<String> {
 | 
				
			||||||
  find_executable(&CSD_WRAPPER_LOCATIONS)
 | 
					  find_executable(&CSD_WRAPPER_LOCATIONS)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// If file exists, check if it is executable
 | 
				
			||||||
 | 
					pub fn check_executable(file: &str) -> Result<(), io::Error> {
 | 
				
			||||||
 | 
					  let path = Path::new(file);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if path.exists() && !path.is_executable() {
 | 
				
			||||||
 | 
					    return Err(io::Error::new(
 | 
				
			||||||
 | 
					      io::ErrorKind::PermissionDenied,
 | 
				
			||||||
 | 
					      format!("{} is not executable", file),
 | 
				
			||||||
 | 
					    ));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,6 +9,8 @@ anyhow.workspace = true
 | 
				
			|||||||
base64.workspace = true
 | 
					base64.workspace = true
 | 
				
			||||||
log.workspace = true
 | 
					log.workspace = true
 | 
				
			||||||
reqwest.workspace = true
 | 
					reqwest.workspace = true
 | 
				
			||||||
 | 
					openssl.workspace = true
 | 
				
			||||||
 | 
					pem.workspace = true
 | 
				
			||||||
roxmltree.workspace = true
 | 
					roxmltree.workspace = true
 | 
				
			||||||
serde.workspace = true
 | 
					serde.workspace = true
 | 
				
			||||||
specta.workspace = true
 | 
					specta.workspace = true
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -70,6 +70,7 @@ impl SamlAuthData {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      let auth_data: SamlAuthData = serde_urlencoded::from_str(auth_data).map_err(|e| {
 | 
					      let auth_data: SamlAuthData = serde_urlencoded::from_str(auth_data).map_err(|e| {
 | 
				
			||||||
        warn!("Failed to parse token auth data: {}", e);
 | 
					        warn!("Failed to parse token auth data: {}", e);
 | 
				
			||||||
 | 
					        warn!("Auth data: {}", auth_data);
 | 
				
			||||||
        AuthDataParseError::Invalid
 | 
					        AuthDataParseError::Invalid
 | 
				
			||||||
      })?;
 | 
					      })?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -37,13 +37,13 @@ impl From<&CachedCredential> for PasswordCredential {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
#[derive(Debug, Serialize, Deserialize, Type, Clone)]
 | 
					#[derive(Debug, Serialize, Deserialize, Type, Clone)]
 | 
				
			||||||
#[serde(rename_all = "camelCase")]
 | 
					#[serde(rename_all = "camelCase")]
 | 
				
			||||||
pub struct PreloginCookieCredential {
 | 
					pub struct PreloginCredential {
 | 
				
			||||||
  username: String,
 | 
					  username: String,
 | 
				
			||||||
  prelogin_cookie: Option<String>,
 | 
					  prelogin_cookie: Option<String>,
 | 
				
			||||||
  token: Option<String>,
 | 
					  token: Option<String>,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl PreloginCookieCredential {
 | 
					impl PreloginCredential {
 | 
				
			||||||
  pub fn new(username: &str, prelogin_cookie: Option<&str>, token: Option<&str>) -> Self {
 | 
					  pub fn new(username: &str, prelogin_cookie: Option<&str>, token: Option<&str>) -> Self {
 | 
				
			||||||
    Self {
 | 
					    Self {
 | 
				
			||||||
      username: username.to_string(),
 | 
					      username: username.to_string(),
 | 
				
			||||||
@@ -65,7 +65,7 @@ impl PreloginCookieCredential {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl From<SamlAuthData> for PreloginCookieCredential {
 | 
					impl From<SamlAuthData> for PreloginCredential {
 | 
				
			||||||
  fn from(value: SamlAuthData) -> Self {
 | 
					  fn from(value: SamlAuthData) -> Self {
 | 
				
			||||||
    let username = value.username().to_string();
 | 
					    let username = value.username().to_string();
 | 
				
			||||||
    let prelogin_cookie = value.prelogin_cookie();
 | 
					    let prelogin_cookie = value.prelogin_cookie();
 | 
				
			||||||
@@ -160,9 +160,9 @@ impl From<PasswordCredential> for CachedCredential {
 | 
				
			|||||||
#[serde(tag = "type", rename_all = "camelCase")]
 | 
					#[serde(tag = "type", rename_all = "camelCase")]
 | 
				
			||||||
pub enum Credential {
 | 
					pub enum Credential {
 | 
				
			||||||
  Password(PasswordCredential),
 | 
					  Password(PasswordCredential),
 | 
				
			||||||
  PreloginCookie(PreloginCookieCredential),
 | 
					  Prelogin(PreloginCredential),
 | 
				
			||||||
  AuthCookie(AuthCookieCredential),
 | 
					  AuthCookie(AuthCookieCredential),
 | 
				
			||||||
  CachedCredential(CachedCredential),
 | 
					  Cached(CachedCredential),
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl Credential {
 | 
					impl Credential {
 | 
				
			||||||
@@ -177,9 +177,9 @@ impl Credential {
 | 
				
			|||||||
  pub fn username(&self) -> &str {
 | 
					  pub fn username(&self) -> &str {
 | 
				
			||||||
    match self {
 | 
					    match self {
 | 
				
			||||||
      Credential::Password(cred) => cred.username(),
 | 
					      Credential::Password(cred) => cred.username(),
 | 
				
			||||||
      Credential::PreloginCookie(cred) => cred.username(),
 | 
					      Credential::Prelogin(cred) => cred.username(),
 | 
				
			||||||
      Credential::AuthCookie(cred) => cred.username(),
 | 
					      Credential::AuthCookie(cred) => cred.username(),
 | 
				
			||||||
      Credential::CachedCredential(cred) => cred.username(),
 | 
					      Credential::Cached(cred) => cred.username(),
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -189,7 +189,7 @@ impl Credential {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    let (passwd, prelogin_cookie, portal_userauthcookie, portal_prelogonuserauthcookie, token) = match self {
 | 
					    let (passwd, prelogin_cookie, portal_userauthcookie, portal_prelogonuserauthcookie, token) = match self {
 | 
				
			||||||
      Credential::Password(cred) => (Some(cred.password()), None, None, None, None),
 | 
					      Credential::Password(cred) => (Some(cred.password()), None, None, None, None),
 | 
				
			||||||
      Credential::PreloginCookie(cred) => (None, cred.prelogin_cookie(), None, None, cred.token()),
 | 
					      Credential::Prelogin(cred) => (None, cred.prelogin_cookie(), None, None, cred.token()),
 | 
				
			||||||
      Credential::AuthCookie(cred) => (
 | 
					      Credential::AuthCookie(cred) => (
 | 
				
			||||||
        None,
 | 
					        None,
 | 
				
			||||||
        None,
 | 
					        None,
 | 
				
			||||||
@@ -197,7 +197,7 @@ impl Credential {
 | 
				
			|||||||
        Some(cred.prelogon_user_auth_cookie()),
 | 
					        Some(cred.prelogon_user_auth_cookie()),
 | 
				
			||||||
        None,
 | 
					        None,
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
      Credential::CachedCredential(cred) => (
 | 
					      Credential::Cached(cred) => (
 | 
				
			||||||
        cred.password(),
 | 
					        cred.password(),
 | 
				
			||||||
        None,
 | 
					        None,
 | 
				
			||||||
        Some(cred.auth_cookie.user_auth_cookie()),
 | 
					        Some(cred.auth_cookie.user_auth_cookie()),
 | 
				
			||||||
@@ -224,9 +224,9 @@ impl Credential {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
impl From<SamlAuthData> for Credential {
 | 
					impl From<SamlAuthData> for Credential {
 | 
				
			||||||
  fn from(value: SamlAuthData) -> Self {
 | 
					  fn from(value: SamlAuthData) -> Self {
 | 
				
			||||||
    let cred = PreloginCookieCredential::from(value);
 | 
					    let cred = PreloginCredential::from(value);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Self::PreloginCookie(cred)
 | 
					    Self::Prelogin(cred)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -244,6 +244,6 @@ impl From<&AuthCookieCredential> for Credential {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
impl From<&CachedCredential> for Credential {
 | 
					impl From<&CachedCredential> for Credential {
 | 
				
			||||||
  fn from(value: &CachedCredential) -> Self {
 | 
					  fn from(value: &CachedCredential) -> Self {
 | 
				
			||||||
    Self::CachedCredential(value.clone())
 | 
					    Self::Cached(value.clone())
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,7 +2,7 @@ use thiserror::Error;
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
#[derive(Error, Debug)]
 | 
					#[derive(Error, Debug)]
 | 
				
			||||||
pub enum PortalError {
 | 
					pub enum PortalError {
 | 
				
			||||||
  #[error("Portal prelogin error: {0}")]
 | 
					  #[error("Prelogin error: {0}")]
 | 
				
			||||||
  PreloginError(String),
 | 
					  PreloginError(String),
 | 
				
			||||||
  #[error("Portal config error: {0}")]
 | 
					  #[error("Portal config error: {0}")]
 | 
				
			||||||
  ConfigError(String),
 | 
					  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<()> {
 | 
					pub async fn hip_report(gateway: &str, cookie: &str, csd_wrapper: &str, gp_params: &GpParams) -> anyhow::Result<()> {
 | 
				
			||||||
  let client = Client::builder()
 | 
					  let client = Client::try_from(gp_params)?;
 | 
				
			||||||
    .danger_accept_invalid_certs(gp_params.ignore_tls_errors())
 | 
					 | 
				
			||||||
    .user_agent(gp_params.user_agent())
 | 
					 | 
				
			||||||
    .build()?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  let md5 = build_csd_token(cookie)?;
 | 
					  let md5 = build_csd_token(cookie)?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  info!("Submit HIP report md5: {}", md5);
 | 
					  info!("Submit HIP report md5: {}", md5);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,18 +8,20 @@ use crate::{
 | 
				
			|||||||
  credential::Credential,
 | 
					  credential::Credential,
 | 
				
			||||||
  error::PortalError,
 | 
					  error::PortalError,
 | 
				
			||||||
  gp_params::GpParams,
 | 
					  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 url = normalize_server(gateway)?;
 | 
				
			||||||
  let gateway = remove_url_scheme(&url);
 | 
					  let gateway = remove_url_scheme(&url);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let login_url = format!("{}/ssl-vpn/login.esp", url);
 | 
					  let login_url = format!("{}/ssl-vpn/login.esp", url);
 | 
				
			||||||
  let client = Client::builder()
 | 
					  let client = Client::try_from(gp_params)?;
 | 
				
			||||||
    .danger_accept_invalid_certs(gp_params.ignore_tls_errors())
 | 
					 | 
				
			||||||
    .user_agent(gp_params.user_agent())
 | 
					 | 
				
			||||||
    .build()?;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let mut params = cred.to_params();
 | 
					  let mut params = cred.to_params();
 | 
				
			||||||
  let extra_params = gp_params.to_params();
 | 
					  let extra_params = gp_params.to_params();
 | 
				
			||||||
@@ -36,23 +38,25 @@ pub async fn gateway_login(gateway: &str, cred: &Credential, gp_params: &GpParam
 | 
				
			|||||||
    .await
 | 
					    .await
 | 
				
			||||||
    .map_err(|e| anyhow::anyhow!(PortalError::NetworkError(e.to_string())))?;
 | 
					    .map_err(|e| anyhow::anyhow!(PortalError::NetworkError(e.to_string())))?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let status = res.status();
 | 
					  let res = parse_gp_response(res).await.map_err(|err| {
 | 
				
			||||||
 | 
					    warn!("{err}");
 | 
				
			||||||
 | 
					    anyhow::anyhow!("Gateway login error: {}", err.reason)
 | 
				
			||||||
 | 
					  })?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if status.is_client_error() || status.is_server_error() {
 | 
					  // MFA detected
 | 
				
			||||||
    let (reason, res) = parse_gp_error(res).await;
 | 
					  if res.contains("Challenge") {
 | 
				
			||||||
 | 
					    let Some((message, input_str)) = parse_mfa(&res) else {
 | 
				
			||||||
 | 
					      bail!("Failed to parse MFA challenge: {res}");
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    warn!(
 | 
					    return Ok(GatewayLogin::Mfa(message, input_str));
 | 
				
			||||||
      "Gateway login error: reason={}, status={}, response={}",
 | 
					 | 
				
			||||||
      reason, status, res
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    bail!("Gateway login error, reason: {}", reason);
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let res_xml = res.text().await?;
 | 
					  let doc = Document::parse(&res)?;
 | 
				
			||||||
  let doc = Document::parse(&res_xml)?;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  build_gateway_token(&doc, gp_params.computer())
 | 
					  let cookie = build_gateway_token(&doc, gp_params.computer())?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Ok(GatewayLogin::Cookie(cookie))
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
fn build_gateway_token(doc: &Document, computer: &str) -> anyhow::Result<String> {
 | 
					fn build_gateway_token(doc: &Document, computer: &str) -> anyhow::Result<String> {
 | 
				
			||||||
@@ -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"))
 | 
					    .ok_or_else(|| anyhow::anyhow!("Failed to read {key} from args"))
 | 
				
			||||||
    .map(|s| (key, s.as_ref()))
 | 
					    .map(|s| (key, s.as_ref()))
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn parse_mfa(res: &str) -> Option<(String, String)> {
 | 
				
			||||||
 | 
					  let message = res
 | 
				
			||||||
 | 
					    .lines()
 | 
				
			||||||
 | 
					    .find(|l| l.contains("respMsg"))
 | 
				
			||||||
 | 
					    .and_then(|l| l.split('"').nth(1).map(|s| s.to_string()))?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let input_str = res
 | 
				
			||||||
 | 
					    .lines()
 | 
				
			||||||
 | 
					    .find(|l| l.contains("inputStr"))
 | 
				
			||||||
 | 
					    .and_then(|l| l.split('"').nth(1).map(|s| s.to_string()))?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Some((message, input_str))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[cfg(test)]
 | 
				
			||||||
 | 
					mod tests {
 | 
				
			||||||
 | 
					  use super::*;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  #[test]
 | 
				
			||||||
 | 
					  fn mfa() {
 | 
				
			||||||
 | 
					    let res = r#"var respStatus = "Challenge";
 | 
				
			||||||
 | 
					var respMsg = "MFA message";
 | 
				
			||||||
 | 
					thisForm.inputStr.value = "5ef64e83000119ed";"#;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let (message, input_str) = parse_mfa(res).unwrap();
 | 
				
			||||||
 | 
					    assert_eq!(message, "MFA message");
 | 
				
			||||||
 | 
					    assert_eq!(input_str, "5ef64e83000119ed");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,9 +1,11 @@
 | 
				
			|||||||
use std::collections::HashMap;
 | 
					use std::collections::HashMap;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use log::info;
 | 
				
			||||||
 | 
					use reqwest::Client;
 | 
				
			||||||
use serde::{Deserialize, Serialize};
 | 
					use serde::{Deserialize, Serialize};
 | 
				
			||||||
use specta::Type;
 | 
					use specta::Type;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::GP_USER_AGENT;
 | 
					use crate::{utils::request::create_identity, GP_USER_AGENT};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Debug, Serialize, Deserialize, Clone, Type, Default)]
 | 
					#[derive(Debug, Serialize, Deserialize, Clone, Type, Default)]
 | 
				
			||||||
pub enum ClientOs {
 | 
					pub enum ClientOs {
 | 
				
			||||||
@@ -42,7 +44,7 @@ impl ClientOs {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Debug, Serialize, Deserialize, Type, Default)]
 | 
					#[derive(Debug, Serialize, Deserialize, Type, Default, Clone)]
 | 
				
			||||||
pub struct GpParams {
 | 
					pub struct GpParams {
 | 
				
			||||||
  is_gateway: bool,
 | 
					  is_gateway: bool,
 | 
				
			||||||
  user_agent: String,
 | 
					  user_agent: String,
 | 
				
			||||||
@@ -51,6 +53,12 @@ pub struct GpParams {
 | 
				
			|||||||
  client_version: Option<String>,
 | 
					  client_version: Option<String>,
 | 
				
			||||||
  computer: String,
 | 
					  computer: String,
 | 
				
			||||||
  ignore_tls_errors: bool,
 | 
					  ignore_tls_errors: bool,
 | 
				
			||||||
 | 
					  certificate: Option<String>,
 | 
				
			||||||
 | 
					  sslkey: Option<String>,
 | 
				
			||||||
 | 
					  key_password: Option<String>,
 | 
				
			||||||
 | 
					  // Used for MFA
 | 
				
			||||||
 | 
					  input_str: Option<String>,
 | 
				
			||||||
 | 
					  otp: Option<String>,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl GpParams {
 | 
					impl GpParams {
 | 
				
			||||||
@@ -90,6 +98,14 @@ impl GpParams {
 | 
				
			|||||||
    self.client_version.as_deref()
 | 
					    self.client_version.as_deref()
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  pub fn set_input_str(&mut self, input_str: &str) {
 | 
				
			||||||
 | 
					    self.input_str = Some(input_str.to_string());
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  pub fn set_otp(&mut self, otp: &str) {
 | 
				
			||||||
 | 
					    self.otp = Some(otp.to_string());
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  pub(crate) fn to_params(&self) -> HashMap<&str, &str> {
 | 
					  pub(crate) fn to_params(&self) -> HashMap<&str, &str> {
 | 
				
			||||||
    let mut params: HashMap<&str, &str> = HashMap::new();
 | 
					    let mut params: HashMap<&str, &str> = HashMap::new();
 | 
				
			||||||
    let client_os = self.client_os.as_str();
 | 
					    let client_os = self.client_os.as_str();
 | 
				
			||||||
@@ -100,11 +116,16 @@ impl GpParams {
 | 
				
			|||||||
    params.insert("ok", "Login");
 | 
					    params.insert("ok", "Login");
 | 
				
			||||||
    params.insert("direct", "yes");
 | 
					    params.insert("direct", "yes");
 | 
				
			||||||
    params.insert("ipv6-support", "yes");
 | 
					    params.insert("ipv6-support", "yes");
 | 
				
			||||||
    params.insert("inputStr", "");
 | 
					 | 
				
			||||||
    params.insert("clientVer", "4100");
 | 
					    params.insert("clientVer", "4100");
 | 
				
			||||||
    params.insert("clientos", client_os);
 | 
					    params.insert("clientos", client_os);
 | 
				
			||||||
    params.insert("computer", &self.computer);
 | 
					    params.insert("computer", &self.computer);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // MFA
 | 
				
			||||||
 | 
					    params.insert("inputStr", self.input_str.as_deref().unwrap_or_default());
 | 
				
			||||||
 | 
					    if let Some(otp) = &self.otp {
 | 
				
			||||||
 | 
					      params.insert("passwd", otp);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if let Some(os_version) = &self.os_version {
 | 
					    if let Some(os_version) = &self.os_version {
 | 
				
			||||||
      params.insert("os-version", os_version);
 | 
					      params.insert("os-version", os_version);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -126,18 +147,26 @@ pub struct GpParamsBuilder {
 | 
				
			|||||||
  client_version: Option<String>,
 | 
					  client_version: Option<String>,
 | 
				
			||||||
  computer: String,
 | 
					  computer: String,
 | 
				
			||||||
  ignore_tls_errors: bool,
 | 
					  ignore_tls_errors: bool,
 | 
				
			||||||
 | 
					  certificate: Option<String>,
 | 
				
			||||||
 | 
					  sslkey: Option<String>,
 | 
				
			||||||
 | 
					  key_password: Option<String>,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl GpParamsBuilder {
 | 
					impl GpParamsBuilder {
 | 
				
			||||||
  pub fn new() -> Self {
 | 
					  pub fn new() -> Self {
 | 
				
			||||||
 | 
					    let computer = whoami::fallible::hostname().unwrap_or_else(|_| String::from("localhost"));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Self {
 | 
					    Self {
 | 
				
			||||||
      is_gateway: false,
 | 
					      is_gateway: false,
 | 
				
			||||||
      user_agent: GP_USER_AGENT.to_string(),
 | 
					      user_agent: GP_USER_AGENT.to_string(),
 | 
				
			||||||
      client_os: ClientOs::Linux,
 | 
					      client_os: ClientOs::Linux,
 | 
				
			||||||
      os_version: Default::default(),
 | 
					      os_version: Default::default(),
 | 
				
			||||||
      client_version: Default::default(),
 | 
					      client_version: Default::default(),
 | 
				
			||||||
      computer: whoami::hostname(),
 | 
					      computer,
 | 
				
			||||||
      ignore_tls_errors: false,
 | 
					      ignore_tls_errors: false,
 | 
				
			||||||
 | 
					      certificate: Default::default(),
 | 
				
			||||||
 | 
					      sslkey: Default::default(),
 | 
				
			||||||
 | 
					      key_password: Default::default(),
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -176,6 +205,21 @@ impl GpParamsBuilder {
 | 
				
			|||||||
    self
 | 
					    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 {
 | 
					  pub fn build(&self) -> GpParams {
 | 
				
			||||||
    GpParams {
 | 
					    GpParams {
 | 
				
			||||||
      is_gateway: self.is_gateway,
 | 
					      is_gateway: self.is_gateway,
 | 
				
			||||||
@@ -185,6 +229,11 @@ impl GpParamsBuilder {
 | 
				
			|||||||
      client_version: self.client_version.clone(),
 | 
					      client_version: self.client_version.clone(),
 | 
				
			||||||
      computer: self.computer.clone(),
 | 
					      computer: self.computer.clone(),
 | 
				
			||||||
      ignore_tls_errors: self.ignore_tls_errors,
 | 
					      ignore_tls_errors: self.ignore_tls_errors,
 | 
				
			||||||
 | 
					      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()
 | 
					    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,
 | 
					  error::PortalError,
 | 
				
			||||||
  gateway::{parse_gateways, Gateway},
 | 
					  gateway::{parse_gateways, Gateway},
 | 
				
			||||||
  gp_params::GpParams,
 | 
					  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)]
 | 
					#[derive(Debug, Serialize, Type)]
 | 
				
			||||||
@@ -88,10 +88,7 @@ pub async fn retrieve_config(portal: &str, cred: &Credential, gp_params: &GpPara
 | 
				
			|||||||
  let server = remove_url_scheme(&portal);
 | 
					  let server = remove_url_scheme(&portal);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let url = format!("{}/global-protect/getconfig.esp", portal);
 | 
					  let url = format!("{}/global-protect/getconfig.esp", portal);
 | 
				
			||||||
  let client = Client::builder()
 | 
					  let client = Client::try_from(gp_params)?;
 | 
				
			||||||
    .danger_accept_invalid_certs(gp_params.ignore_tls_errors())
 | 
					 | 
				
			||||||
    .user_agent(gp_params.user_agent())
 | 
					 | 
				
			||||||
    .build()?;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let mut params = cred.to_params();
 | 
					  let mut params = cred.to_params();
 | 
				
			||||||
  let extra_params = gp_params.to_params();
 | 
					  let extra_params = gp_params.to_params();
 | 
				
			||||||
@@ -108,24 +105,19 @@ pub async fn retrieve_config(portal: &str, cred: &Credential, gp_params: &GpPara
 | 
				
			|||||||
    .send()
 | 
					    .send()
 | 
				
			||||||
    .await
 | 
					    .await
 | 
				
			||||||
    .map_err(|e| anyhow::anyhow!(PortalError::NetworkError(e.to_string())))?;
 | 
					    .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| {
 | 
				
			||||||
    bail!(PortalError::ConfigError("Config endpoint not found".to_string()))
 | 
					    if err.status == StatusCode::NOT_FOUND {
 | 
				
			||||||
  }
 | 
					      bail!(PortalError::ConfigError("Config endpoint not found".to_string()));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if status.is_client_error() || status.is_server_error() {
 | 
					    if err.is_status_error() {
 | 
				
			||||||
    let (reason, res) = parse_gp_error(res).await;
 | 
					      warn!("{err}");
 | 
				
			||||||
 | 
					      bail!("Portal config error: {}", err.reason);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    warn!(
 | 
					    Err(anyhow::anyhow!(PortalError::ConfigError(err.reason)))
 | 
				
			||||||
      "Portal config error: reason={}, status={}, response={}",
 | 
					  })?;
 | 
				
			||||||
      reason, status, res
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    bail!("Portal config error, reason: {}", reason);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  let res_xml = res.text().await.map_err(|e| PortalError::ConfigError(e.to_string()))?;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if res_xml.is_empty() {
 | 
					  if res_xml.is_empty() {
 | 
				
			||||||
    bail!(PortalError::ConfigError("Empty portal config response".to_string()))
 | 
					    bail!(PortalError::ConfigError("Empty portal config response".to_string()))
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,7 +8,7 @@ use specta::Type;
 | 
				
			|||||||
use crate::{
 | 
					use crate::{
 | 
				
			||||||
  error::PortalError,
 | 
					  error::PortalError,
 | 
				
			||||||
  gp_params::GpParams,
 | 
					  gp_params::GpParams,
 | 
				
			||||||
  utils::{base64, normalize_server, parse_gp_error, xml},
 | 
					  utils::{base64, normalize_server, parse_gp_response, xml},
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const REQUIRED_PARAMS: [&str; 8] = [
 | 
					const REQUIRED_PARAMS: [&str; 8] = [
 | 
				
			||||||
@@ -98,10 +98,12 @@ impl Prelogin {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
pub async fn prelogin(portal: &str, gp_params: &GpParams) -> anyhow::Result<Prelogin> {
 | 
					pub async fn prelogin(portal: &str, gp_params: &GpParams) -> anyhow::Result<Prelogin> {
 | 
				
			||||||
  let user_agent = gp_params.user_agent();
 | 
					  let user_agent = gp_params.user_agent();
 | 
				
			||||||
  info!("Prelogin with user_agent: {}", user_agent);
 | 
					  let is_gateway = gp_params.is_gateway();
 | 
				
			||||||
 | 
					  let prelogin_type = if is_gateway { "Gateway" } else { "Portal" };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  info!("{} prelogin with user_agent: {}", prelogin_type, user_agent);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let portal = normalize_server(portal)?;
 | 
					  let portal = normalize_server(portal)?;
 | 
				
			||||||
  let is_gateway = gp_params.is_gateway();
 | 
					 | 
				
			||||||
  let path = if is_gateway { "ssl-vpn" } else { "global-protect" };
 | 
					  let path = if is_gateway { "ssl-vpn" } else { "global-protect" };
 | 
				
			||||||
  let prelogin_url = format!("{portal}/{}/prelogin.esp", path);
 | 
					  let prelogin_url = format!("{portal}/{}/prelogin.esp", path);
 | 
				
			||||||
  let mut params = gp_params.to_params();
 | 
					  let mut params = gp_params.to_params();
 | 
				
			||||||
@@ -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));
 | 
					  params.retain(|k, _| REQUIRED_PARAMS.iter().any(|required_param| required_param == k));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  info!("Prelogin with params: {:?}", params);
 | 
					  let client = Client::try_from(gp_params)?;
 | 
				
			||||||
 | 
					 | 
				
			||||||
  let client = Client::builder()
 | 
					 | 
				
			||||||
    .danger_accept_invalid_certs(gp_params.ignore_tls_errors())
 | 
					 | 
				
			||||||
    .user_agent(user_agent)
 | 
					 | 
				
			||||||
    .build()?;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let res = client
 | 
					  let res = client
 | 
				
			||||||
    .post(&prelogin_url)
 | 
					    .post(&prelogin_url)
 | 
				
			||||||
@@ -126,38 +123,36 @@ pub async fn prelogin(portal: &str, gp_params: &GpParams) -> anyhow::Result<Prel
 | 
				
			|||||||
    .await
 | 
					    .await
 | 
				
			||||||
    .map_err(|e| anyhow::anyhow!(PortalError::NetworkError(e.to_string())))?;
 | 
					    .map_err(|e| anyhow::anyhow!(PortalError::NetworkError(e.to_string())))?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let status = res.status();
 | 
					  let res_xml = parse_gp_response(res).await.or_else(|err| {
 | 
				
			||||||
  if status == StatusCode::NOT_FOUND {
 | 
					    if err.status == StatusCode::NOT_FOUND {
 | 
				
			||||||
    bail!(PortalError::PreloginError("Prelogin endpoint not found".to_string()))
 | 
					      bail!(PortalError::PreloginError("Prelogin endpoint not found".to_string()))
 | 
				
			||||||
  }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if status.is_client_error() || status.is_server_error() {
 | 
					    if err.is_status_error() {
 | 
				
			||||||
    let (reason, res) = parse_gp_error(res).await;
 | 
					      warn!("{err}");
 | 
				
			||||||
 | 
					      bail!("Prelogin error: {}", err.reason)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    warn!("Prelogin error: reason={}, status={}, response={}", reason, status, res);
 | 
					    Err(anyhow!(PortalError::PreloginError(err.reason)))
 | 
				
			||||||
 | 
					  })?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    bail!("Prelogin error: {}", status)
 | 
					  let prelogin = parse_res_xml(&res_xml, is_gateway).map_err(|err| {
 | 
				
			||||||
  }
 | 
					    warn!("Parse response error, response: {}", res_xml);
 | 
				
			||||||
 | 
					    PortalError::PreloginError(err.to_string())
 | 
				
			||||||
  let res_xml = res
 | 
					  })?;
 | 
				
			||||||
    .text()
 | 
					 | 
				
			||||||
    .await
 | 
					 | 
				
			||||||
    .map_err(|e| PortalError::PreloginError(e.to_string()))?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  let prelogin = parse_res_xml(res_xml, is_gateway).map_err(|e| PortalError::PreloginError(e.to_string()))?;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Ok(prelogin)
 | 
					  Ok(prelogin)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
fn parse_res_xml(res_xml: String, is_gateway: bool) -> anyhow::Result<Prelogin> {
 | 
					fn parse_res_xml(res_xml: &str, is_gateway: bool) -> anyhow::Result<Prelogin> {
 | 
				
			||||||
  let doc = Document::parse(&res_xml)?;
 | 
					  let doc = Document::parse(res_xml)?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let status = xml::get_child_text(&doc, "status")
 | 
					  let status = xml::get_child_text(&doc, "status")
 | 
				
			||||||
    .ok_or_else(|| anyhow::anyhow!("Prelogin response does not contain status element"))?;
 | 
					    .ok_or_else(|| anyhow::anyhow!("Prelogin response does not contain status element"))?;
 | 
				
			||||||
  // Check the status of the prelogin response
 | 
					  // Check the status of the prelogin response
 | 
				
			||||||
  if status.to_uppercase() != "SUCCESS" {
 | 
					  if status.to_uppercase() != "SUCCESS" {
 | 
				
			||||||
    let msg = xml::get_child_text(&doc, "msg").unwrap_or(String::from("Unknown error"));
 | 
					    let msg = xml::get_child_text(&doc, "msg").unwrap_or(String::from("Unknown error"));
 | 
				
			||||||
    bail!("Prelogin failed: {}", msg)
 | 
					    bail!("{}", msg)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let region = xml::get_child_text(&doc, "region").unwrap_or_else(|| {
 | 
					  let region = xml::get_child_text(&doc, "region").unwrap_or_else(|| {
 | 
				
			||||||
@@ -183,22 +178,24 @@ fn parse_res_xml(res_xml: String, is_gateway: bool) -> anyhow::Result<Prelogin>
 | 
				
			|||||||
    return Ok(Prelogin::Saml(saml_prelogin));
 | 
					    return Ok(Prelogin::Saml(saml_prelogin));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let label_username = xml::get_child_text(&doc, "username-label");
 | 
					  let label_username = xml::get_child_text(&doc, "username-label").unwrap_or_else(|| {
 | 
				
			||||||
  let label_password = xml::get_child_text(&doc, "password-label");
 | 
					    info!("Username label has no value, using default");
 | 
				
			||||||
  // Check if the prelogin response is standard login
 | 
					    String::from("Username")
 | 
				
			||||||
  if label_username.is_some() && label_password.is_some() {
 | 
					  });
 | 
				
			||||||
    let auth_message =
 | 
					  let label_password = xml::get_child_text(&doc, "password-label").unwrap_or_else(|| {
 | 
				
			||||||
      xml::get_child_text(&doc, "authentication-message").unwrap_or(String::from("Please enter the login credentials"));
 | 
					    info!("Password label has no value, using default");
 | 
				
			||||||
    let standard_prelogin = StandardPrelogin {
 | 
					    String::from("Password")
 | 
				
			||||||
      region,
 | 
					  });
 | 
				
			||||||
      is_gateway,
 | 
					 | 
				
			||||||
      auth_message,
 | 
					 | 
				
			||||||
      label_username: label_username.unwrap(),
 | 
					 | 
				
			||||||
      label_password: label_password.unwrap(),
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Ok(Prelogin::Standard(standard_prelogin))
 | 
					  let auth_message =
 | 
				
			||||||
  } else {
 | 
					    xml::get_child_text(&doc, "authentication-message").unwrap_or(String::from("Please enter the login credentials"));
 | 
				
			||||||
    Err(anyhow!("Invalid prelogin response"))
 | 
					  let standard_prelogin = StandardPrelogin {
 | 
				
			||||||
  }
 | 
					    region,
 | 
				
			||||||
 | 
					    is_gateway,
 | 
				
			||||||
 | 
					    auth_message,
 | 
				
			||||||
 | 
					    label_username,
 | 
				
			||||||
 | 
					    label_password,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Ok(Prelogin::Standard(standard_prelogin))
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -18,6 +18,7 @@ pub struct SamlAuthLauncher<'a> {
 | 
				
			|||||||
  fix_openssl: bool,
 | 
					  fix_openssl: bool,
 | 
				
			||||||
  ignore_tls_errors: bool,
 | 
					  ignore_tls_errors: bool,
 | 
				
			||||||
  clean: bool,
 | 
					  clean: bool,
 | 
				
			||||||
 | 
					  default_browser: bool,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl<'a> SamlAuthLauncher<'a> {
 | 
					impl<'a> SamlAuthLauncher<'a> {
 | 
				
			||||||
@@ -33,6 +34,7 @@ impl<'a> SamlAuthLauncher<'a> {
 | 
				
			|||||||
      fix_openssl: false,
 | 
					      fix_openssl: false,
 | 
				
			||||||
      ignore_tls_errors: false,
 | 
					      ignore_tls_errors: false,
 | 
				
			||||||
      clean: false,
 | 
					      clean: false,
 | 
				
			||||||
 | 
					      default_browser: false,
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -81,8 +83,13 @@ impl<'a> SamlAuthLauncher<'a> {
 | 
				
			|||||||
    self
 | 
					    self
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  pub fn default_browser(mut self, default_browser: bool) -> Self {
 | 
				
			||||||
 | 
					    self.default_browser = default_browser;
 | 
				
			||||||
 | 
					    self
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /// Launch the authenticator binary as the current user or SUDO_USER if available.
 | 
					  /// Launch the authenticator binary as the current user or SUDO_USER if available.
 | 
				
			||||||
  pub async fn launch(self) -> anyhow::Result<Credential> {
 | 
					  pub async fn launch(self) -> anyhow::Result<Option<Credential>> {
 | 
				
			||||||
    let mut auth_cmd = Command::new(GP_AUTH_BINARY);
 | 
					    let mut auth_cmd = Command::new(GP_AUTH_BINARY);
 | 
				
			||||||
    auth_cmd.arg(self.server);
 | 
					    auth_cmd.arg(self.server);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -122,6 +129,10 @@ impl<'a> SamlAuthLauncher<'a> {
 | 
				
			|||||||
      auth_cmd.arg("--clean");
 | 
					      auth_cmd.arg("--clean");
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if self.default_browser {
 | 
				
			||||||
 | 
					      auth_cmd.arg("--default-browser");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let mut non_root_cmd = auth_cmd.into_non_root()?;
 | 
					    let mut non_root_cmd = auth_cmd.into_non_root()?;
 | 
				
			||||||
    let output = non_root_cmd
 | 
					    let output = non_root_cmd
 | 
				
			||||||
      .kill_on_drop(true)
 | 
					      .kill_on_drop(true)
 | 
				
			||||||
@@ -130,12 +141,16 @@ impl<'a> SamlAuthLauncher<'a> {
 | 
				
			|||||||
      .wait_with_output()
 | 
					      .wait_with_output()
 | 
				
			||||||
      .await?;
 | 
					      .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if self.default_browser {
 | 
				
			||||||
 | 
					      return Ok(None);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let Ok(auth_result) = serde_json::from_slice::<SamlAuthResult>(&output.stdout) else {
 | 
					    let Ok(auth_result) = serde_json::from_slice::<SamlAuthResult>(&output.stdout) else {
 | 
				
			||||||
      bail!("Failed to parse auth data")
 | 
					      bail!("Failed to parse auth data")
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    match auth_result {
 | 
					    match auth_result {
 | 
				
			||||||
      SamlAuthResult::Success(auth_data) => Ok(Credential::from(auth_data)),
 | 
					      SamlAuthResult::Success(auth_data) => Ok(Some(Credential::from(auth_data))),
 | 
				
			||||||
      SamlAuthResult::Failure(msg) => bail!(msg),
 | 
					      SamlAuthResult::Failure(msg) => bail!(msg),
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,7 @@
 | 
				
			|||||||
use std::{env::temp_dir, io::Write};
 | 
					use std::{env::temp_dir, fs, io::Write, os::unix::fs::PermissionsExt};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use anyhow::bail;
 | 
				
			||||||
 | 
					use log::warn;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub struct BrowserAuthenticator<'a> {
 | 
					pub struct BrowserAuthenticator<'a> {
 | 
				
			||||||
  auth_request: &'a str,
 | 
					  auth_request: &'a str,
 | 
				
			||||||
@@ -14,8 +17,18 @@ impl BrowserAuthenticator<'_> {
 | 
				
			|||||||
      open::that_detached(self.auth_request)?;
 | 
					      open::that_detached(self.auth_request)?;
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      let html_file = temp_dir().join("gpauth.html");
 | 
					      let html_file = temp_dir().join("gpauth.html");
 | 
				
			||||||
      let mut file = std::fs::File::create(&html_file)?;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Remove the file and error if permission denied
 | 
				
			||||||
 | 
					      if let Err(err) = fs::remove_file(&html_file) {
 | 
				
			||||||
 | 
					        if err.kind() != std::io::ErrorKind::NotFound {
 | 
				
			||||||
 | 
					          warn!("Failed to remove the temporary file: {}", err);
 | 
				
			||||||
 | 
					          bail!("Please remove the file manually: {:?}", html_file);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      let mut file = fs::File::create(&html_file)?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      file.set_permissions(fs::Permissions::from_mode(0o600))?;
 | 
				
			||||||
      file.write_all(self.auth_request.as_bytes())?;
 | 
					      file.write_all(self.auth_request.as_bytes())?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      open::that_detached(html_file)?;
 | 
					      open::that_detached(html_file)?;
 | 
				
			||||||
@@ -24,11 +37,3 @@ impl BrowserAuthenticator<'_> {
 | 
				
			|||||||
    Ok(())
 | 
					    Ok(())
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Drop for BrowserAuthenticator<'_> {
 | 
					 | 
				
			||||||
  fn drop(&mut self) {
 | 
					 | 
				
			||||||
    // Cleanup the temporary file
 | 
					 | 
				
			||||||
    let html_file = temp_dir().join("gpauth.html");
 | 
					 | 
				
			||||||
    let _ = std::fs::remove_file(html_file);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -32,10 +32,15 @@ pub struct ConnectArgs {
 | 
				
			|||||||
  cookie: String,
 | 
					  cookie: String,
 | 
				
			||||||
  vpnc_script: Option<String>,
 | 
					  vpnc_script: Option<String>,
 | 
				
			||||||
  user_agent: Option<String>,
 | 
					  user_agent: Option<String>,
 | 
				
			||||||
 | 
					  os: Option<ClientOs>,
 | 
				
			||||||
 | 
					  certificate: Option<String>,
 | 
				
			||||||
 | 
					  sslkey: Option<String>,
 | 
				
			||||||
 | 
					  key_password: Option<String>,
 | 
				
			||||||
  csd_uid: u32,
 | 
					  csd_uid: u32,
 | 
				
			||||||
  csd_wrapper: Option<String>,
 | 
					  csd_wrapper: Option<String>,
 | 
				
			||||||
 | 
					  reconnect_timeout: u32,
 | 
				
			||||||
  mtu: u32,
 | 
					  mtu: u32,
 | 
				
			||||||
  os: Option<ClientOs>,
 | 
					  disable_ipv6: bool,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl ConnectArgs {
 | 
					impl ConnectArgs {
 | 
				
			||||||
@@ -45,9 +50,14 @@ impl ConnectArgs {
 | 
				
			|||||||
      vpnc_script: None,
 | 
					      vpnc_script: None,
 | 
				
			||||||
      user_agent: None,
 | 
					      user_agent: None,
 | 
				
			||||||
      os: None,
 | 
					      os: None,
 | 
				
			||||||
 | 
					      certificate: None,
 | 
				
			||||||
 | 
					      sslkey: None,
 | 
				
			||||||
 | 
					      key_password: None,
 | 
				
			||||||
      csd_uid: 0,
 | 
					      csd_uid: 0,
 | 
				
			||||||
      csd_wrapper: None,
 | 
					      csd_wrapper: None,
 | 
				
			||||||
 | 
					      reconnect_timeout: 300,
 | 
				
			||||||
      mtu: 0,
 | 
					      mtu: 0,
 | 
				
			||||||
 | 
					      disable_ipv6: false,
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -67,6 +77,18 @@ impl ConnectArgs {
 | 
				
			|||||||
    self.os.as_ref().map(|os| os.to_openconnect_os().to_string())
 | 
					    self.os.as_ref().map(|os| os.to_openconnect_os().to_string())
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  pub fn certificate(&self) -> Option<String> {
 | 
				
			||||||
 | 
					    self.certificate.clone()
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  pub fn sslkey(&self) -> Option<String> {
 | 
				
			||||||
 | 
					    self.sslkey.clone()
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  pub fn key_password(&self) -> Option<String> {
 | 
				
			||||||
 | 
					    self.key_password.clone()
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  pub fn csd_uid(&self) -> u32 {
 | 
					  pub fn csd_uid(&self) -> u32 {
 | 
				
			||||||
    self.csd_uid
 | 
					    self.csd_uid
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@@ -75,9 +97,17 @@ impl ConnectArgs {
 | 
				
			|||||||
    self.csd_wrapper.clone()
 | 
					    self.csd_wrapper.clone()
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  pub fn reconnect_timeout(&self) -> u32 {
 | 
				
			||||||
 | 
					    self.reconnect_timeout
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  pub fn mtu(&self) -> u32 {
 | 
					  pub fn mtu(&self) -> u32 {
 | 
				
			||||||
    self.mtu
 | 
					    self.mtu
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  pub fn disable_ipv6(&self) -> bool {
 | 
				
			||||||
 | 
					    self.disable_ipv6
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Debug, Deserialize, Serialize, Type)]
 | 
					#[derive(Debug, Deserialize, Serialize, Type)]
 | 
				
			||||||
@@ -109,11 +139,6 @@ impl ConnectRequest {
 | 
				
			|||||||
    self
 | 
					    self
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  pub fn with_mtu(mut self, mtu: u32) -> Self {
 | 
					 | 
				
			||||||
    self.args.mtu = mtu;
 | 
					 | 
				
			||||||
    self
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  pub fn with_user_agent<T: Into<Option<String>>>(mut self, user_agent: T) -> Self {
 | 
					  pub fn with_user_agent<T: Into<Option<String>>>(mut self, user_agent: T) -> Self {
 | 
				
			||||||
    self.args.user_agent = user_agent.into();
 | 
					    self.args.user_agent = user_agent.into();
 | 
				
			||||||
    self
 | 
					    self
 | 
				
			||||||
@@ -124,6 +149,36 @@ impl ConnectRequest {
 | 
				
			|||||||
    self
 | 
					    self
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  pub fn with_certificate<T: Into<Option<String>>>(mut self, certificate: T) -> Self {
 | 
				
			||||||
 | 
					    self.args.certificate = certificate.into();
 | 
				
			||||||
 | 
					    self
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  pub fn with_sslkey<T: Into<Option<String>>>(mut self, sslkey: T) -> Self {
 | 
				
			||||||
 | 
					    self.args.sslkey = sslkey.into();
 | 
				
			||||||
 | 
					    self
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  pub fn with_key_password<T: Into<Option<String>>>(mut self, key_password: T) -> Self {
 | 
				
			||||||
 | 
					    self.args.key_password = key_password.into();
 | 
				
			||||||
 | 
					    self
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  pub fn with_reconnect_timeout(mut self, reconnect_timeout: u32) -> Self {
 | 
				
			||||||
 | 
					    self.args.reconnect_timeout = reconnect_timeout;
 | 
				
			||||||
 | 
					    self
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  pub fn with_mtu(mut self, mtu: u32) -> Self {
 | 
				
			||||||
 | 
					    self.args.mtu = mtu;
 | 
				
			||||||
 | 
					    self
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  pub fn with_disable_ipv6(mut self, disable_ipv6: bool) -> Self {
 | 
				
			||||||
 | 
					    self.args.disable_ipv6 = disable_ipv6;
 | 
				
			||||||
 | 
					    self
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  pub fn gateway(&self) -> &Gateway {
 | 
					  pub fn gateway(&self) -> &Gateway {
 | 
				
			||||||
    self.info.gateway()
 | 
					    self.info.gateway()
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,3 @@
 | 
				
			|||||||
use reqwest::{Response, Url};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub(crate) mod xml;
 | 
					pub(crate) mod xml;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub mod base64;
 | 
					pub mod base64;
 | 
				
			||||||
@@ -10,13 +8,18 @@ pub mod env_file;
 | 
				
			|||||||
pub mod lock_file;
 | 
					pub mod lock_file;
 | 
				
			||||||
pub mod openssl;
 | 
					pub mod openssl;
 | 
				
			||||||
pub mod redact;
 | 
					pub mod redact;
 | 
				
			||||||
 | 
					pub mod request;
 | 
				
			||||||
#[cfg(feature = "tauri")]
 | 
					#[cfg(feature = "tauri")]
 | 
				
			||||||
pub mod window;
 | 
					pub mod window;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
mod shutdown_signal;
 | 
					mod shutdown_signal;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use log::warn;
 | 
				
			||||||
pub use shutdown_signal::shutdown_signal;
 | 
					pub use shutdown_signal::shutdown_signal;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use reqwest::{Response, StatusCode, Url};
 | 
				
			||||||
 | 
					use thiserror::Error;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Normalize the server URL to the format `https://<host>:<port>`
 | 
					/// Normalize the server URL to the format `https://<host>:<port>`
 | 
				
			||||||
pub fn normalize_server(server: &str) -> anyhow::Result<String> {
 | 
					pub fn normalize_server(server: &str) -> anyhow::Result<String> {
 | 
				
			||||||
  let server = if server.starts_with("https://") || server.starts_with("http://") {
 | 
					  let server = if server.starts_with("https://") || server.starts_with("http://") {
 | 
				
			||||||
@@ -42,7 +45,41 @@ pub fn remove_url_scheme(s: &str) -> String {
 | 
				
			|||||||
  s.replace("http://", "").replace("https://", "")
 | 
					  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
 | 
					  let reason = res
 | 
				
			||||||
    .headers()
 | 
					    .headers()
 | 
				
			||||||
    .get("x-private-pan-globalprotect")
 | 
					    .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 script: *const c_char,
 | 
				
			||||||
  pub os: *const c_char,
 | 
					  pub os: *const c_char,
 | 
				
			||||||
  pub certificate: *const c_char,
 | 
					  pub certificate: *const c_char,
 | 
				
			||||||
 | 
					  pub sslkey: *const c_char,
 | 
				
			||||||
 | 
					  pub key_password: *const c_char,
 | 
				
			||||||
  pub servercert: *const c_char,
 | 
					  pub servercert: *const c_char,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  pub csd_uid: u32,
 | 
					  pub csd_uid: u32,
 | 
				
			||||||
  pub csd_wrapper: *const c_char,
 | 
					  pub csd_wrapper: *const c_char,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  pub reconnect_timeout: u32,
 | 
				
			||||||
  pub mtu: u32,
 | 
					  pub mtu: u32,
 | 
				
			||||||
 | 
					  pub disable_ipv6: u32,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[link(name = "vpn")]
 | 
					#[link(name = "vpn")]
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -16,7 +16,7 @@ static vpn_connected_callback on_vpn_connected;
 | 
				
			|||||||
/* Validate the peer certificate */
 | 
					/* Validate the peer certificate */
 | 
				
			||||||
static int validate_peer_cert(__attribute__((unused)) void *_vpninfo, const char *reason)
 | 
					static int validate_peer_cert(__attribute__((unused)) void *_vpninfo, const char *reason)
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    INFO("Validating peer cert: %s", reason);
 | 
					    INFO("Accepting the server certificate though %s", reason);
 | 
				
			||||||
    return 0;
 | 
					    return 0;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -28,12 +28,9 @@ static void print_progress(__attribute__((unused)) void *_vpninfo, int level, co
 | 
				
			|||||||
    char *message = format_message(format, args);
 | 
					    char *message = format_message(format, args);
 | 
				
			||||||
    va_end(args);
 | 
					    va_end(args);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (message == NULL)
 | 
					    if (message == NULL) {
 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        ERROR("Failed to format log message");
 | 
					        ERROR("Failed to format log message");
 | 
				
			||||||
    }
 | 
					    } else {
 | 
				
			||||||
    else
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        LOG(level, message);
 | 
					        LOG(level, message);
 | 
				
			||||||
        free(message);
 | 
					        free(message);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -63,12 +60,13 @@ int vpn_connect(const vpn_options *options, vpn_connected_callback callback)
 | 
				
			|||||||
    INFO("OS: %s", options->os);
 | 
					    INFO("OS: %s", options->os);
 | 
				
			||||||
    INFO("CSD_USER: %d", options->csd_uid);
 | 
					    INFO("CSD_USER: %d", options->csd_uid);
 | 
				
			||||||
    INFO("CSD_WRAPPER: %s", options->csd_wrapper);
 | 
					    INFO("CSD_WRAPPER: %s", options->csd_wrapper);
 | 
				
			||||||
 | 
					    INFO("RECONNECT_TIMEOUT: %d", options->reconnect_timeout);
 | 
				
			||||||
    INFO("MTU: %d", options->mtu);
 | 
					    INFO("MTU: %d", options->mtu);
 | 
				
			||||||
 | 
					    INFO("DISABLE_IPV6: %d", options->disable_ipv6);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    vpninfo = openconnect_vpninfo_new(options->user_agent, validate_peer_cert, NULL, NULL, print_progress, NULL);
 | 
					    vpninfo = openconnect_vpninfo_new(options->user_agent, validate_peer_cert, NULL, NULL, print_progress, NULL);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!vpninfo)
 | 
					    if (!vpninfo) {
 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        ERROR("openconnect_vpninfo_new failed");
 | 
					        ERROR("openconnect_vpninfo_new failed");
 | 
				
			||||||
        return 1;
 | 
					        return 1;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -83,15 +81,13 @@ int vpn_connect(const vpn_options *options, vpn_connected_callback callback)
 | 
				
			|||||||
        openconnect_set_reported_os(vpninfo, options->os);
 | 
					        openconnect_set_reported_os(vpninfo, options->os);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (options->certificate)
 | 
					    if (options->certificate) {
 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        INFO("Setting client certificate: %s", options->certificate);
 | 
					        INFO("Setting client certificate: %s", options->certificate);
 | 
				
			||||||
        openconnect_set_client_cert(vpninfo, options->certificate, NULL);
 | 
					        openconnect_set_client_cert(vpninfo, options->certificate, options->sslkey);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (options->servercert) {
 | 
					    if (options->key_password) {
 | 
				
			||||||
        INFO("Setting server certificate: %s", options->servercert);
 | 
					        openconnect_set_key_password(vpninfo, options->key_password);
 | 
				
			||||||
        openconnect_set_system_trust(vpninfo, 0);
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (options->csd_wrapper) {
 | 
					    if (options->csd_wrapper) {
 | 
				
			||||||
@@ -103,39 +99,37 @@ int vpn_connect(const vpn_options *options, vpn_connected_callback callback)
 | 
				
			|||||||
        openconnect_set_reqmtu(vpninfo, mtu);
 | 
					        openconnect_set_reqmtu(vpninfo, mtu);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (options->disable_ipv6) {
 | 
				
			||||||
 | 
					        openconnect_disable_ipv6(vpninfo);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    g_cmd_pipe_fd = openconnect_setup_cmd_pipe(vpninfo);
 | 
					    g_cmd_pipe_fd = openconnect_setup_cmd_pipe(vpninfo);
 | 
				
			||||||
    if (g_cmd_pipe_fd < 0)
 | 
					    if (g_cmd_pipe_fd < 0) {
 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        ERROR("openconnect_setup_cmd_pipe failed");
 | 
					        ERROR("openconnect_setup_cmd_pipe failed");
 | 
				
			||||||
        return 1;
 | 
					        return 1;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!uname(&utsbuf))
 | 
					    if (!uname(&utsbuf)) {
 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        openconnect_set_localname(vpninfo, utsbuf.nodename);
 | 
					        openconnect_set_localname(vpninfo, utsbuf.nodename);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Essential step
 | 
					    // Essential step
 | 
				
			||||||
    if (openconnect_make_cstp_connection(vpninfo) != 0)
 | 
					    if (openconnect_make_cstp_connection(vpninfo) != 0) {
 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        ERROR("openconnect_make_cstp_connection failed");
 | 
					        ERROR("openconnect_make_cstp_connection failed");
 | 
				
			||||||
        return 1;
 | 
					        return 1;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (openconnect_setup_dtls(vpninfo, 60) != 0)
 | 
					    if (openconnect_setup_dtls(vpninfo, 60) != 0) {
 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        openconnect_disable_dtls(vpninfo);
 | 
					        openconnect_disable_dtls(vpninfo);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Essential step
 | 
					    // Essential step
 | 
				
			||||||
    openconnect_set_setup_tun_handler(vpninfo, setup_tun_handler);
 | 
					    openconnect_set_setup_tun_handler(vpninfo, setup_tun_handler);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    while (1)
 | 
					    while (1) {
 | 
				
			||||||
    {
 | 
					        int ret = openconnect_mainloop(vpninfo, options->reconnect_timeout, 10);
 | 
				
			||||||
        int ret = openconnect_mainloop(vpninfo, 300, 10);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (ret)
 | 
					        if (ret) {
 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            INFO("openconnect_mainloop returned %d, exiting", ret);
 | 
					            INFO("openconnect_mainloop returned %d, exiting", ret);
 | 
				
			||||||
            openconnect_vpninfo_free(vpninfo);
 | 
					            openconnect_vpninfo_free(vpninfo);
 | 
				
			||||||
            return ret;
 | 
					            return ret;
 | 
				
			||||||
@@ -152,8 +146,7 @@ void vpn_disconnect()
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    INFO("Stopping VPN connection: %d", g_cmd_pipe_fd);
 | 
					    INFO("Stopping VPN connection: %d", g_cmd_pipe_fd);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (write(g_cmd_pipe_fd, &cmd, 1) < 0)
 | 
					    if (write(g_cmd_pipe_fd, &cmd, 1) < 0) {
 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        ERROR("Failed to write to command pipe, VPN connection may not be stopped");
 | 
					        ERROR("Failed to write to command pipe, VPN connection may not be stopped");
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -15,12 +15,16 @@ typedef struct vpn_options
 | 
				
			|||||||
    const char *script;
 | 
					    const char *script;
 | 
				
			||||||
    const char *os;
 | 
					    const char *os;
 | 
				
			||||||
    const char *certificate;
 | 
					    const char *certificate;
 | 
				
			||||||
 | 
					    const char *sslkey;
 | 
				
			||||||
 | 
					    const char *key_password;
 | 
				
			||||||
    const char *servercert;
 | 
					    const char *servercert;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const uid_t csd_uid;
 | 
					    const uid_t csd_uid;
 | 
				
			||||||
    const char *csd_wrapper;
 | 
					    const char *csd_wrapper;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const int reconnect_timeout;
 | 
				
			||||||
    const int mtu;
 | 
					    const int mtu;
 | 
				
			||||||
 | 
					    const int disable_ipv6;
 | 
				
			||||||
} vpn_options;
 | 
					} vpn_options;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
int vpn_connect(const vpn_options *options, vpn_connected_callback callback);
 | 
					int vpn_connect(const vpn_options *options, vpn_connected_callback callback);
 | 
				
			||||||
@@ -35,7 +39,7 @@ static char *format_message(const char *format, va_list args)
 | 
				
			|||||||
    int len = vsnprintf(NULL, 0, format, args_copy);
 | 
					    int len = vsnprintf(NULL, 0, format, args_copy);
 | 
				
			||||||
    va_end(args_copy);
 | 
					    va_end(args_copy);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    char *buffer = malloc(len + 1);
 | 
					    char *buffer = (char*)malloc(len + 1);
 | 
				
			||||||
    if (buffer == NULL)
 | 
					    if (buffer == NULL)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        return NULL;
 | 
					        return NULL;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,7 +4,7 @@ use std::{
 | 
				
			|||||||
  sync::{Arc, RwLock},
 | 
					  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 log::info;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::ffi;
 | 
					use crate::ffi;
 | 
				
			||||||
@@ -18,12 +18,16 @@ pub struct Vpn {
 | 
				
			|||||||
  script: CString,
 | 
					  script: CString,
 | 
				
			||||||
  os: CString,
 | 
					  os: CString,
 | 
				
			||||||
  certificate: Option<CString>,
 | 
					  certificate: Option<CString>,
 | 
				
			||||||
 | 
					  sslkey: Option<CString>,
 | 
				
			||||||
 | 
					  key_password: Option<CString>,
 | 
				
			||||||
  servercert: Option<CString>,
 | 
					  servercert: Option<CString>,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  csd_uid: u32,
 | 
					  csd_uid: u32,
 | 
				
			||||||
  csd_wrapper: Option<CString>,
 | 
					  csd_wrapper: Option<CString>,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  reconnect_timeout: u32,
 | 
				
			||||||
  mtu: u32,
 | 
					  mtu: u32,
 | 
				
			||||||
 | 
					  disable_ipv6: bool,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  callback: OnConnectedCallback,
 | 
					  callback: OnConnectedCallback,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -61,13 +65,18 @@ impl Vpn {
 | 
				
			|||||||
      user_agent: self.user_agent.as_ptr(),
 | 
					      user_agent: self.user_agent.as_ptr(),
 | 
				
			||||||
      script: self.script.as_ptr(),
 | 
					      script: self.script.as_ptr(),
 | 
				
			||||||
      os: self.os.as_ptr(),
 | 
					      os: self.os.as_ptr(),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      certificate: Self::option_to_ptr(&self.certificate),
 | 
					      certificate: Self::option_to_ptr(&self.certificate),
 | 
				
			||||||
 | 
					      sslkey: Self::option_to_ptr(&self.sslkey),
 | 
				
			||||||
 | 
					      key_password: Self::option_to_ptr(&self.key_password),
 | 
				
			||||||
      servercert: Self::option_to_ptr(&self.servercert),
 | 
					      servercert: Self::option_to_ptr(&self.servercert),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      csd_uid: self.csd_uid,
 | 
					      csd_uid: self.csd_uid,
 | 
				
			||||||
      csd_wrapper: Self::option_to_ptr(&self.csd_wrapper),
 | 
					      csd_wrapper: Self::option_to_ptr(&self.csd_wrapper),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      reconnect_timeout: self.reconnect_timeout,
 | 
				
			||||||
      mtu: self.mtu,
 | 
					      mtu: self.mtu,
 | 
				
			||||||
 | 
					      disable_ipv6: self.disable_ipv6 as u32,
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -80,23 +89,23 @@ impl Vpn {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Debug)]
 | 
					#[derive(Debug)]
 | 
				
			||||||
pub struct VpnError<'a> {
 | 
					pub struct VpnError {
 | 
				
			||||||
  message: &'a str,
 | 
					  message: String,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl<'a> VpnError<'a> {
 | 
					impl VpnError {
 | 
				
			||||||
  fn new(message: &'a str) -> Self {
 | 
					  fn new(message: String) -> Self {
 | 
				
			||||||
    Self { message }
 | 
					    Self { message }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl fmt::Display for VpnError<'_> {
 | 
					impl fmt::Display for VpnError {
 | 
				
			||||||
  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
 | 
					  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
 | 
				
			||||||
    write!(f, "{}", self.message)
 | 
					    write!(f, "{}", self.message)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl std::error::Error for VpnError<'_> {}
 | 
					impl std::error::Error for VpnError {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub struct VpnBuilder {
 | 
					pub struct VpnBuilder {
 | 
				
			||||||
  server: String,
 | 
					  server: String,
 | 
				
			||||||
@@ -106,10 +115,16 @@ pub struct VpnBuilder {
 | 
				
			|||||||
  user_agent: Option<String>,
 | 
					  user_agent: Option<String>,
 | 
				
			||||||
  os: Option<String>,
 | 
					  os: Option<String>,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  certificate: Option<String>,
 | 
				
			||||||
 | 
					  sslkey: Option<String>,
 | 
				
			||||||
 | 
					  key_password: Option<String>,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  csd_uid: u32,
 | 
					  csd_uid: u32,
 | 
				
			||||||
  csd_wrapper: Option<String>,
 | 
					  csd_wrapper: Option<String>,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  reconnect_timeout: u32,
 | 
				
			||||||
  mtu: u32,
 | 
					  mtu: u32,
 | 
				
			||||||
 | 
					  disable_ipv6: bool,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl VpnBuilder {
 | 
					impl VpnBuilder {
 | 
				
			||||||
@@ -122,10 +137,16 @@ impl VpnBuilder {
 | 
				
			|||||||
      user_agent: None,
 | 
					      user_agent: None,
 | 
				
			||||||
      os: None,
 | 
					      os: None,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      certificate: None,
 | 
				
			||||||
 | 
					      sslkey: None,
 | 
				
			||||||
 | 
					      key_password: None,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      csd_uid: 0,
 | 
					      csd_uid: 0,
 | 
				
			||||||
      csd_wrapper: None,
 | 
					      csd_wrapper: None,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      reconnect_timeout: 300,
 | 
				
			||||||
      mtu: 0,
 | 
					      mtu: 0,
 | 
				
			||||||
 | 
					      disable_ipv6: false,
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -144,6 +165,21 @@ impl VpnBuilder {
 | 
				
			|||||||
    self
 | 
					    self
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  pub fn certificate<T: Into<Option<String>>>(mut self, certificate: T) -> Self {
 | 
				
			||||||
 | 
					    self.certificate = certificate.into();
 | 
				
			||||||
 | 
					    self
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  pub fn sslkey<T: Into<Option<String>>>(mut self, sslkey: T) -> Self {
 | 
				
			||||||
 | 
					    self.sslkey = sslkey.into();
 | 
				
			||||||
 | 
					    self
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  pub fn key_password<T: Into<Option<String>>>(mut self, key_password: T) -> Self {
 | 
				
			||||||
 | 
					    self.key_password = key_password.into();
 | 
				
			||||||
 | 
					    self
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  pub fn csd_uid(mut self, csd_uid: u32) -> Self {
 | 
					  pub fn csd_uid(mut self, csd_uid: u32) -> Self {
 | 
				
			||||||
    self.csd_uid = csd_uid;
 | 
					    self.csd_uid = csd_uid;
 | 
				
			||||||
    self
 | 
					    self
 | 
				
			||||||
@@ -154,26 +190,32 @@ impl VpnBuilder {
 | 
				
			|||||||
    self
 | 
					    self
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  pub fn reconnect_timeout(mut self, reconnect_timeout: u32) -> Self {
 | 
				
			||||||
 | 
					    self.reconnect_timeout = reconnect_timeout;
 | 
				
			||||||
 | 
					    self
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  pub fn mtu(mut self, mtu: u32) -> Self {
 | 
					  pub fn mtu(mut self, mtu: u32) -> Self {
 | 
				
			||||||
    self.mtu = mtu;
 | 
					    self.mtu = mtu;
 | 
				
			||||||
    self
 | 
					    self
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  pub fn build(self) -> 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 {
 | 
					    let script = match self.script {
 | 
				
			||||||
      Some(script) => {
 | 
					      Some(script) => {
 | 
				
			||||||
        if !is_executable(&script) {
 | 
					        check_executable(&script).map_err(|e| VpnError::new(e.to_string()))?;
 | 
				
			||||||
          return Err(VpnError::new("vpnc script is not executable"));
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        script
 | 
					        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 let Some(csd_wrapper) = &self.csd_wrapper {
 | 
				
			||||||
      if !is_executable(csd_wrapper) {
 | 
					      check_executable(csd_wrapper).map_err(|e| VpnError::new(e.to_string()))?;
 | 
				
			||||||
        return Err(VpnError::new("CSD wrapper is not executable"));
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let user_agent = self.user_agent.unwrap_or_default();
 | 
					    let user_agent = self.user_agent.unwrap_or_default();
 | 
				
			||||||
@@ -185,13 +227,18 @@ impl VpnBuilder {
 | 
				
			|||||||
      user_agent: Self::to_cstring(&user_agent),
 | 
					      user_agent: Self::to_cstring(&user_agent),
 | 
				
			||||||
      script: Self::to_cstring(&script),
 | 
					      script: Self::to_cstring(&script),
 | 
				
			||||||
      os: Self::to_cstring(&os),
 | 
					      os: Self::to_cstring(&os),
 | 
				
			||||||
      certificate: None,
 | 
					
 | 
				
			||||||
 | 
					      certificate: self.certificate.as_deref().map(Self::to_cstring),
 | 
				
			||||||
 | 
					      sslkey: self.sslkey.as_deref().map(Self::to_cstring),
 | 
				
			||||||
 | 
					      key_password: self.key_password.as_deref().map(Self::to_cstring),
 | 
				
			||||||
      servercert: None,
 | 
					      servercert: None,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      csd_uid: self.csd_uid,
 | 
					      csd_uid: self.csd_uid,
 | 
				
			||||||
      csd_wrapper: self.csd_wrapper.as_deref().map(Self::to_cstring),
 | 
					      csd_wrapper: self.csd_wrapper.as_deref().map(Self::to_cstring),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      reconnect_timeout: self.reconnect_timeout,
 | 
				
			||||||
      mtu: self.mtu,
 | 
					      mtu: self.mtu,
 | 
				
			||||||
 | 
					      disable_ipv6: self.disable_ipv6,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      callback: Default::default(),
 | 
					      callback: Default::default(),
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user