mirror of
				https://github.com/yuezk/GlobalProtect-openconnect.git
				synced 2025-05-20 07:26:58 -04:00 
			
		
		
		
	Compare commits
	
		
			6 Commits
		
	
	
		
			v2.0.0-bet
			...
			v2.0.0-bet
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 1f50e4d82b | ||
|  | 995d1216ea | ||
|  | 196e91289c | ||
|  | b2bb35994f | ||
|  | 6fe6a1387a | ||
|  | aac401e7ee | 
							
								
								
									
										232
									
								
								.github/workflows/build.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										232
									
								
								.github/workflows/build.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -8,8 +8,9 @@ on: | |||||||
|       - .devcontainer |       - .devcontainer | ||||||
|     branches: |     branches: | ||||||
|       - main |       - main | ||||||
|     # tags: |     tags: | ||||||
|     #   - v*.*.* |       - latest | ||||||
|  |       - v*.*.* | ||||||
| jobs: | jobs: | ||||||
|   # Include arm64 if ref is a tag |   # Include arm64 if ref is a tag | ||||||
|   setup-matrix: |   setup-matrix: | ||||||
| @@ -30,7 +31,7 @@ jobs: | |||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     steps: |     steps: | ||||||
|       - name: Checkout gpgui repo |       - name: Checkout gpgui repo | ||||||
|         uses: actions/checkout@v4 |         uses: actions/checkout@v3 | ||||||
|         with: |         with: | ||||||
|           token: ${{ secrets.GH_PAT }} |           token: ${{ secrets.GH_PAT }} | ||||||
|           repository: yuezk/gpgui |           repository: yuezk/gpgui | ||||||
| @@ -54,43 +55,35 @@ jobs: | |||||||
|           pnpm run build |           pnpm run build | ||||||
|  |  | ||||||
|       - name: Upload artifacts |       - name: Upload artifacts | ||||||
|         uses: actions/upload-artifact@v4 |         uses: actions/upload-artifact@v3 | ||||||
|         with: |         with: | ||||||
|           name: gpgui-fe |           name: gpgui-fe | ||||||
|           path: app/dist |           path: app/dist | ||||||
|  |  | ||||||
|   build-tauri: |   build-tauri-amd64: | ||||||
|     needs: [setup-matrix, build-fe] |     needs: [build-fe] | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     strategy: |  | ||||||
|       matrix: |  | ||||||
|         arch: ${{ fromJson(needs.setup-matrix.outputs.matrix) }} |  | ||||||
|     steps: |     steps: | ||||||
|       - name: Checkout gpgui repo |       - name: Checkout gpgui repo | ||||||
|         uses: actions/checkout@v4 |         uses: actions/checkout@v3 | ||||||
|         with: |         with: | ||||||
|           token: ${{ secrets.GH_PAT }} |           token: ${{ secrets.GH_PAT }} | ||||||
|           repository: yuezk/gpgui |           repository: yuezk/gpgui | ||||||
|           path: gpgui |           path: gpgui | ||||||
|  |  | ||||||
|       - name: Checkout gp repo |       - name: Checkout gp repo | ||||||
|         uses: actions/checkout@v4 |         uses: actions/checkout@v3 | ||||||
|         with: |         with: | ||||||
|           token: ${{ secrets.GH_PAT }} |           token: ${{ secrets.GH_PAT }} | ||||||
|           repository: yuezk/GlobalProtect-openconnect |           repository: yuezk/GlobalProtect-openconnect | ||||||
|           path: gp |           path: gp | ||||||
|  |  | ||||||
|       - name: Download gpgui-fe artifact |       - name: Download gpgui-fe artifact | ||||||
|         uses: actions/download-artifact@v4 |         uses: actions/download-artifact@v3 | ||||||
|         with: |         with: | ||||||
|           name: gpgui-fe |           name: gpgui-fe | ||||||
|           path: gpgui/app/dist |           path: gpgui/app/dist | ||||||
|  |  | ||||||
|       - name: Set up QEMU |  | ||||||
|         uses: docker/setup-qemu-action@v3 |  | ||||||
|         with: |  | ||||||
|           platforms: ${{ matrix.arch }} |  | ||||||
|  |  | ||||||
|       - name: Login to Docker Hub |       - name: Login to Docker Hub | ||||||
|         uses: docker/login-action@v3 |         uses: docker/login-action@v3 | ||||||
|         with: |         with: | ||||||
| @@ -104,13 +97,212 @@ jobs: | |||||||
|             -v $(pwd):/${{ github.workspace }} \ |             -v $(pwd):/${{ github.workspace }} \ | ||||||
|             -w ${{ github.workspace }} \ |             -w ${{ github.workspace }} \ | ||||||
|             -e CI=true \ |             -e CI=true \ | ||||||
|             --platform linux/${{ matrix.arch }} \ |  | ||||||
|             yuezk/gpdev:main \ |             yuezk/gpdev:main \ | ||||||
|             "./gpgui/scripts/build.sh" |             "./gpgui/scripts/build.sh" | ||||||
|  |  | ||||||
|       - name: Upload artifacts |       - name: Upload artifacts | ||||||
|         uses: actions/upload-artifact@v4 |         uses: actions/upload-artifact@v3 | ||||||
|         with: |         with: | ||||||
|           name: artifact-${{ matrix.arch }}-tauri |           name: artifact-amd64-tauri | ||||||
|           path: | |           path: | | ||||||
|             gpgui/.tmp/artifact |             gpgui/.tmp/artifact | ||||||
|  |  | ||||||
|  |   build-tauri-arm64: | ||||||
|  |     if: startsWith(github.ref, 'refs/tags/') | ||||||
|  |     needs: [build-fe] | ||||||
|  |     runs-on: self-hosted | ||||||
|  |     steps: | ||||||
|  |       - name: Checkout gpgui repo | ||||||
|  |         uses: actions/checkout@v3 | ||||||
|  |         with: | ||||||
|  |           token: ${{ secrets.GH_PAT }} | ||||||
|  |           repository: yuezk/gpgui | ||||||
|  |           path: gpgui | ||||||
|  |  | ||||||
|  |       - name: Checkout gp repo | ||||||
|  |         uses: actions/checkout@v3 | ||||||
|  |         with: | ||||||
|  |           token: ${{ secrets.GH_PAT }} | ||||||
|  |           repository: yuezk/GlobalProtect-openconnect | ||||||
|  |           path: gp | ||||||
|  |  | ||||||
|  |       - name: Download gpgui-fe artifact | ||||||
|  |         uses: actions/download-artifact@v3 | ||||||
|  |         with: | ||||||
|  |           name: gpgui-fe | ||||||
|  |           path: gpgui/app/dist | ||||||
|  |       - name: Build Tauri | ||||||
|  |         run: | | ||||||
|  |           ./gpgui/scripts/build.sh | ||||||
|  |  | ||||||
|  |       - name: Upload artifacts | ||||||
|  |         uses: actions/upload-artifact@v3 | ||||||
|  |         with: | ||||||
|  |           name: artifact-arm64-tauri | ||||||
|  |           path: | | ||||||
|  |             gpgui/.tmp/artifact | ||||||
|  |  | ||||||
|  |   package-tarball: | ||||||
|  |     needs: [build-tauri-amd64, build-tauri-arm64] | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     steps: | ||||||
|  |       - name: Checkout gpgui repo | ||||||
|  |         uses: actions/checkout@v3 | ||||||
|  |         with: | ||||||
|  |           token: ${{ secrets.GH_PAT }} | ||||||
|  |           repository: yuezk/gpgui | ||||||
|  |           path: gpgui | ||||||
|  |  | ||||||
|  |       - name: Download artifact-amd64-tauri | ||||||
|  |         uses: actions/download-artifact@v3 | ||||||
|  |         with: | ||||||
|  |           name: artifact-amd64-tauri | ||||||
|  |           path: gpgui/.tmp/artifact | ||||||
|  |  | ||||||
|  |       - name: Download artifact-arm64-tauri | ||||||
|  |         uses: actions/download-artifact@v3 | ||||||
|  |         with: | ||||||
|  |           name: artifact-arm64-tauri | ||||||
|  |           path: gpgui/.tmp/artifact | ||||||
|  |  | ||||||
|  |       - name: Create tarball | ||||||
|  |         run: | | ||||||
|  |           ./gpgui/scripts/build-tarball.sh | ||||||
|  |  | ||||||
|  |       - name: Upload tarball | ||||||
|  |         uses: actions/upload-artifact@v3 | ||||||
|  |         with: | ||||||
|  |           name: artifact-tarball | ||||||
|  |           path: | | ||||||
|  |             gpgui/.tmp/tarball/*.tar.gz | ||||||
|  |  | ||||||
|  |   package-rpm: | ||||||
|  |     needs: [setup-matrix, package-tarball] | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     strategy: | ||||||
|  |       matrix: | ||||||
|  |         arch: ${{ fromJson(needs.setup-matrix.outputs.matrix) }} | ||||||
|  |     steps: | ||||||
|  |       - name: Checkout gpgui repo | ||||||
|  |         uses: actions/checkout@v3 | ||||||
|  |         with: | ||||||
|  |           token: ${{ secrets.GH_PAT }} | ||||||
|  |           repository: yuezk/gpgui | ||||||
|  |           path: gpgui | ||||||
|  |  | ||||||
|  |       - name: Download package tarball | ||||||
|  |         uses: actions/download-artifact@v3 | ||||||
|  |         with: | ||||||
|  |           name: artifact-tarball | ||||||
|  |           path: gpgui/.tmp/artifact | ||||||
|  |  | ||||||
|  |       - name: Set up QEMU | ||||||
|  |         uses: docker/setup-qemu-action@v3 | ||||||
|  |         with: | ||||||
|  |           platforms: ${{ matrix.arch }} | ||||||
|  |  | ||||||
|  |       - name: Login to Docker Hub | ||||||
|  |         uses: docker/login-action@v3 | ||||||
|  |         with: | ||||||
|  |           username: ${{ secrets.DOCKER_HUB_USERNAME }} | ||||||
|  |           password: ${{ secrets.DOCKER_HUB_TOKEN }} | ||||||
|  |  | ||||||
|  |       - name: Create RPM package | ||||||
|  |         run: | | ||||||
|  |           docker run \ | ||||||
|  |             --rm \ | ||||||
|  |             -v $(pwd):/${{ github.workspace }} \ | ||||||
|  |             -w ${{ github.workspace }} \ | ||||||
|  |             --platform linux/${{ matrix.arch }} \ | ||||||
|  |             yuezk/gpdev:rpm-builder \ | ||||||
|  |             "./gpgui/scripts/build-rpm.sh" | ||||||
|  |  | ||||||
|  |       - name: Upload rpm artifacts | ||||||
|  |         uses: actions/upload-artifact@v3 | ||||||
|  |         with: | ||||||
|  |           name: artifact-${{ matrix.arch }}-rpm | ||||||
|  |           path: | | ||||||
|  |             gpgui/.tmp/artifact/*.rpm | ||||||
|  |  | ||||||
|  |   package-pkgbuild: | ||||||
|  |     needs: [setup-matrix, build-tauri-amd64, build-tauri-arm64] | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     strategy: | ||||||
|  |       matrix: | ||||||
|  |         arch: ${{ fromJson(needs.setup-matrix.outputs.matrix) }} | ||||||
|  |     steps: | ||||||
|  |       - name: Checkout gpgui repo | ||||||
|  |         uses: actions/checkout@v3 | ||||||
|  |         with: | ||||||
|  |           token: ${{ secrets.GH_PAT }} | ||||||
|  |           repository: yuezk/gpgui | ||||||
|  |           path: gpgui | ||||||
|  |  | ||||||
|  |       - name: Download artifact-${{ matrix.arch }} | ||||||
|  |         uses: actions/download-artifact@v3 | ||||||
|  |         with: | ||||||
|  |           name: artifact-${{ matrix.arch }}-tauri | ||||||
|  |           path: gpgui/.tmp/artifact | ||||||
|  |  | ||||||
|  |       - name: Set up QEMU | ||||||
|  |         uses: docker/setup-qemu-action@v3 | ||||||
|  |         with: | ||||||
|  |           platforms: ${{ matrix.arch }} | ||||||
|  |  | ||||||
|  |       - name: Login to Docker Hub | ||||||
|  |         uses: docker/login-action@v3 | ||||||
|  |         with: | ||||||
|  |           username: ${{ secrets.DOCKER_HUB_USERNAME }} | ||||||
|  |           password: ${{ secrets.DOCKER_HUB_TOKEN }} | ||||||
|  |  | ||||||
|  |       - name: Generate PKGBUILD | ||||||
|  |         run: | | ||||||
|  |           export CI_ARCH=${{ matrix.arch }} | ||||||
|  |           ./gpgui/scripts/generate-pkgbuild.sh | ||||||
|  |  | ||||||
|  |       - name: Build PKGBUILD package | ||||||
|  |         run: | | ||||||
|  |           # Build package | ||||||
|  |           docker run \ | ||||||
|  |             --rm \ | ||||||
|  |             -v $(pwd)/gpgui/.tmp/pkgbuild:/pkgbuild \ | ||||||
|  |             --platform linux/${{ matrix.arch }} \ | ||||||
|  |             yuezk/gpdev:pkgbuild | ||||||
|  |  | ||||||
|  |       - name: Upload pkgbuild artifacts | ||||||
|  |         uses: actions/upload-artifact@v3 | ||||||
|  |         with: | ||||||
|  |           name: artifact-${{ matrix.arch }}-pkgbuild | ||||||
|  |           path: | | ||||||
|  |             gpgui/.tmp/pkgbuild/*.pkg.tar.zst | ||||||
|  |  | ||||||
|  |   gh-release: | ||||||
|  |     if: startsWith(github.ref, 'refs/tags/') | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     needs: | ||||||
|  |       - package-rpm | ||||||
|  |       - package-pkgbuild | ||||||
|  |  | ||||||
|  |     steps: | ||||||
|  |       - name: Download artifact | ||||||
|  |         uses: actions/download-artifact@v3 | ||||||
|  |         with: | ||||||
|  |           path: artifact | ||||||
|  |           # pattern: artifact-* | ||||||
|  |           # merge-multiple: true | ||||||
|  |  | ||||||
|  |       # - name: Generate checksum | ||||||
|  |       #   uses: jmgilman/actions-generate-checksum@v1 | ||||||
|  |       #   with: | ||||||
|  |       #     output: checksums.txt | ||||||
|  |       #     patterns: | | ||||||
|  |       #       artifact/* | ||||||
|  |  | ||||||
|  |       - name: Create GH release | ||||||
|  |         uses: softprops/action-gh-release@v1 | ||||||
|  |         with: | ||||||
|  |           token: ${{ secrets.GH_PAT }} | ||||||
|  |           prerelease: ${{ contains(github.ref, 'latest') }} | ||||||
|  |           fail_on_unmatched_files: true | ||||||
|  |           files: | | ||||||
|  |             artifact/artifact-*/* | ||||||
|   | |||||||
							
								
								
									
										10
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										10
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							| @@ -1423,7 +1423,7 @@ dependencies = [ | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "gpapi" | name = "gpapi" | ||||||
| version = "2.0.0-beta6" | version = "2.0.0-beta8" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "anyhow", |  "anyhow", | ||||||
|  "base64 0.21.5", |  "base64 0.21.5", | ||||||
| @@ -1452,7 +1452,7 @@ dependencies = [ | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "gpauth" | name = "gpauth" | ||||||
| version = "2.0.0-beta6" | version = "2.0.0-beta8" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "anyhow", |  "anyhow", | ||||||
|  "clap", |  "clap", | ||||||
| @@ -1472,7 +1472,7 @@ dependencies = [ | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "gpclient" | name = "gpclient" | ||||||
| version = "2.0.0-beta6" | version = "2.0.0-beta8" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "anyhow", |  "anyhow", | ||||||
|  "clap", |  "clap", | ||||||
| @@ -1493,7 +1493,7 @@ dependencies = [ | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "gpservice" | name = "gpservice" | ||||||
| version = "2.0.0-beta6" | version = "2.0.0-beta8" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "anyhow", |  "anyhow", | ||||||
|  "axum", |  "axum", | ||||||
| @@ -2478,7 +2478,7 @@ dependencies = [ | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "openconnect" | name = "openconnect" | ||||||
| version = "2.0.0-beta6" | version = "2.0.0-beta8" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "cc", |  "cc", | ||||||
|  "is_executable", |  "is_executable", | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ resolver = "2" | |||||||
| members = ["crates/*", "apps/gpclient", "apps/gpservice", "apps/gpauth"] | members = ["crates/*", "apps/gpclient", "apps/gpservice", "apps/gpauth"] | ||||||
|  |  | ||||||
| [workspace.package] | [workspace.package] | ||||||
| version = "2.0.0-beta6" | version = "2.0.0-beta8" | ||||||
| authors = ["Kevin Yue <k3vinyue@gmail.com>"] | authors = ["Kevin Yue <k3vinyue@gmail.com>"] | ||||||
| homepage = "https://github.com/yuezk/GlobalProtect-openconnect" | homepage = "https://github.com/yuezk/GlobalProtect-openconnect" | ||||||
| edition = "2021" | edition = "2021" | ||||||
|   | |||||||
| @@ -11,9 +11,11 @@ A GUI for GlobalProtect VPN, based on OpenConnect, supports the SSO authenticati | |||||||
| - [x] Better Linux support | - [x] Better Linux support | ||||||
| - [x] Support both CLI and GUI | - [x] Support both CLI and GUI | ||||||
| - [x] Support both SSO and non-SSO authentication | - [x] Support both SSO and non-SSO authentication | ||||||
|  | - [x] Support the FIDO2 authentication (e.g., YubiKey) | ||||||
| - [x] Support authentication using default browser | - [x] Support authentication using default browser | ||||||
| - [x] Support multiple portals | - [x] Support multiple portals | ||||||
| - [x] Support gateway selection | - [x] Support gateway selection | ||||||
|  | - [x] Support connect gateway directly | ||||||
| - [x] Support auto-connect on startup | - [x] Support auto-connect on startup | ||||||
| - [x] Support system tray icon | - [x] Support system tray icon | ||||||
|  |  | ||||||
| @@ -123,6 +125,13 @@ Download the latest RPM package from [releases](https://github.com/yuezk/GlobalP | |||||||
|  |  | ||||||
| The project depends on `openconnect >= 8.20`, `webkit2gtk`, `libsecret`, `libayatana-appindicator` or `libappindicator-gtk3`. You can install them first and then download the latest binary release (i.e., `*.bin.tar.gz`) from [releases](https://github.com/yuezk/GlobalProtect-openconnect/releases) page. | The project depends on `openconnect >= 8.20`, `webkit2gtk`, `libsecret`, `libayatana-appindicator` or `libappindicator-gtk3`. You can install them first and then download the latest binary release (i.e., `*.bin.tar.gz`) from [releases](https://github.com/yuezk/GlobalProtect-openconnect/releases) page. | ||||||
|  |  | ||||||
|  | ## About Trial | ||||||
|  |  | ||||||
|  | The CLI version is always free, while the GUI version is paid. There two trial modes for the GUI version: | ||||||
|  |  | ||||||
|  | 1. 10-day trial: You can use the GUI stable release for 10 days after the installation. | ||||||
|  | 2. 14-day trial: Each beta release has a fresh trial period (at most 14 days) after released. | ||||||
|  |  | ||||||
| ## [License](./LICENSE) | ## [License](./LICENSE) | ||||||
|  |  | ||||||
| GPLv3 | GPLv3 | ||||||
|   | |||||||
| @@ -19,8 +19,8 @@ use tokio_util::sync::CancellationToken; | |||||||
| use webkit2gtk::{ | use webkit2gtk::{ | ||||||
|   gio::Cancellable, |   gio::Cancellable, | ||||||
|   glib::{GString, TimeSpan}, |   glib::{GString, TimeSpan}, | ||||||
|   LoadEvent, SettingsExt, TLSErrorsPolicy, URIResponse, URIResponseExt, WebContextExt, WebResource, |   LoadEvent, SettingsExt, TLSErrorsPolicy, URIResponse, URIResponseExt, WebContextExt, WebResource, WebResourceExt, | ||||||
|   WebResourceExt, WebView, WebViewExt, WebsiteDataManagerExtManual, WebsiteDataTypes, |   WebView, WebViewExt, WebsiteDataManagerExtManual, WebsiteDataTypes, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| enum AuthDataError { | enum AuthDataError { | ||||||
| @@ -216,9 +216,7 @@ impl<'a> AuthWindow<'a> { | |||||||
|       if let Some(auth_result) = auth_result_rx.recv().await { |       if let Some(auth_result) = auth_result_rx.recv().await { | ||||||
|         match auth_result { |         match auth_result { | ||||||
|           Ok(auth_data) => return Ok(auth_data), |           Ok(auth_data) => return Ok(auth_data), | ||||||
|           Err(AuthDataError::TlsError) => { |           Err(AuthDataError::TlsError) => bail!("TLS error: certificate verify failed"), | ||||||
|             return Err(anyhow::anyhow!("TLS error: certificate verify failed")) |  | ||||||
|           } |  | ||||||
|           Err(AuthDataError::NotFound) => { |           Err(AuthDataError::NotFound) => { | ||||||
|             info!("No auth data found, it may not be the /SAML20/SP/ACS endpoint"); |             info!("No auth data found, it may not be the /SAML20/SP/ACS endpoint"); | ||||||
|  |  | ||||||
| @@ -227,10 +225,7 @@ impl<'a> AuthWindow<'a> { | |||||||
|               let window = Arc::clone(window); |               let window = Arc::clone(window); | ||||||
|               let cancel_token = CancellationToken::new(); |               let cancel_token = CancellationToken::new(); | ||||||
|  |  | ||||||
|               raise_window_cancel_token |               raise_window_cancel_token.write().await.replace(cancel_token.clone()); | ||||||
|                 .write() |  | ||||||
|                 .await |  | ||||||
|                 .replace(cancel_token.clone()); |  | ||||||
|  |  | ||||||
|               tokio::spawn(async move { |               tokio::spawn(async move { | ||||||
|                 let delay_secs = 1; |                 let delay_secs = 1; | ||||||
| @@ -284,12 +279,10 @@ fn raise_window(window: &Arc<Window>) { | |||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| pub(crate) async fn portal_prelogin(portal: &str, gp_params: &GpParams) -> anyhow::Result<String> { | pub async fn portal_prelogin(portal: &str, gp_params: &GpParams) -> anyhow::Result<String> { | ||||||
|   info!("Portal prelogin..."); |  | ||||||
|  |  | ||||||
|   match prelogin(portal, gp_params).await? { |   match prelogin(portal, gp_params).await? { | ||||||
|     Prelogin::Saml(prelogin) => Ok(prelogin.saml_request().to_string()), |     Prelogin::Saml(prelogin) => Ok(prelogin.saml_request().to_string()), | ||||||
|     Prelogin::Standard(_) => Err(anyhow::anyhow!("Received non-SAML prelogin response")), |     Prelogin::Standard(_) => bail!("Received non-SAML prelogin response"), | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -13,18 +13,15 @@ use tempfile::NamedTempFile; | |||||||
|  |  | ||||||
| use crate::auth_window::{portal_prelogin, AuthWindow}; | use crate::auth_window::{portal_prelogin, AuthWindow}; | ||||||
|  |  | ||||||
| const VERSION: &str = concat!( | const VERSION: &str = concat!(env!("CARGO_PKG_VERSION"), " (", compile_time::date_str!(), ")"); | ||||||
|   env!("CARGO_PKG_VERSION"), |  | ||||||
|   " (", |  | ||||||
|   compile_time::date_str!(), |  | ||||||
|   ")" |  | ||||||
| ); |  | ||||||
|  |  | ||||||
| #[derive(Parser, Clone)] | #[derive(Parser, Clone)] | ||||||
| #[command(version = VERSION)] | #[command(version = VERSION)] | ||||||
| struct Cli { | struct Cli { | ||||||
|   server: String, |   server: String, | ||||||
|   #[arg(long)] |   #[arg(long)] | ||||||
|  |   gateway: bool, | ||||||
|  |   #[arg(long)] | ||||||
|   saml_request: Option<String>, |   saml_request: Option<String>, | ||||||
|   #[arg(long, default_value = GP_USER_AGENT)] |   #[arg(long, default_value = GP_USER_AGENT)] | ||||||
|   user_agent: String, |   user_agent: String, | ||||||
| @@ -102,6 +99,7 @@ impl Cli { | |||||||
|       .client_os(ClientOs::from(&self.os)) |       .client_os(ClientOs::from(&self.os)) | ||||||
|       .os_version(self.os_version.clone()) |       .os_version(self.os_version.clone()) | ||||||
|       .ignore_tls_errors(self.ignore_tls_errors) |       .ignore_tls_errors(self.ignore_tls_errors) | ||||||
|  |       .is_gateway(self.gateway) | ||||||
|       .build(); |       .build(); | ||||||
|  |  | ||||||
|     gp_params |     gp_params | ||||||
|   | |||||||
| @@ -9,12 +9,7 @@ use crate::{ | |||||||
|   launch_gui::{LaunchGuiArgs, LaunchGuiHandler}, |   launch_gui::{LaunchGuiArgs, LaunchGuiHandler}, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const VERSION: &str = concat!( | const VERSION: &str = concat!(env!("CARGO_PKG_VERSION"), " (", compile_time::date_str!(), ")"); | ||||||
|   env!("CARGO_PKG_VERSION"), |  | ||||||
|   " (", |  | ||||||
|   compile_time::date_str!(), |  | ||||||
|   ")" |  | ||||||
| ); |  | ||||||
|  |  | ||||||
| pub(crate) struct SharedArgs { | pub(crate) struct SharedArgs { | ||||||
|   pub(crate) fix_openssl: bool, |   pub(crate) fix_openssl: bool, | ||||||
| @@ -53,10 +48,7 @@ struct Cli { | |||||||
|   #[command(subcommand)] |   #[command(subcommand)] | ||||||
|   command: CliCommand, |   command: CliCommand, | ||||||
|  |  | ||||||
|   #[arg( |   #[arg(long, help = "Get around the OpenSSL `unsafe legacy renegotiation` error")] | ||||||
|     long, |  | ||||||
|     help = "Get around the OpenSSL `unsafe legacy renegotiation` error" |  | ||||||
|   )] |  | ||||||
|   fix_openssl: bool, |   fix_openssl: bool, | ||||||
|   #[arg(long, help = "Ignore the TLS errors")] |   #[arg(long, help = "Ignore the TLS errors")] | ||||||
|   ignore_tls_errors: bool, |   ignore_tls_errors: bool, | ||||||
| @@ -116,9 +108,7 @@ pub(crate) async fn run() { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     if err.contains("certificate verify failed") && !cli.ignore_tls_errors { |     if err.contains("certificate verify failed") && !cli.ignore_tls_errors { | ||||||
|       eprintln!( |       eprintln!("\nRe-run it with the `--ignore-tls-errors` option to ignore the certificate error, e.g.:\n"); | ||||||
|         "\nRe-run it with the `--ignore-tls-errors` option to ignore the certificate error, e.g.:\n" |  | ||||||
|       ); |  | ||||||
|       // Print the command |       // Print the command | ||||||
|       let args = std::env::args().collect::<Vec<_>>(); |       let args = std::env::args().collect::<Vec<_>>(); | ||||||
|       eprintln!("{} --ignore-tls-errors {}\n", args[0], args[1..].join(" ")); |       eprintln!("{} --ignore-tls-errors {}\n", args[0], args[1..].join(" ")); | ||||||
|   | |||||||
| @@ -6,9 +6,9 @@ use gpapi::{ | |||||||
|   credential::{Credential, PasswordCredential}, |   credential::{Credential, PasswordCredential}, | ||||||
|   gateway::gateway_login, |   gateway::gateway_login, | ||||||
|   gp_params::{ClientOs, GpParams}, |   gp_params::{ClientOs, GpParams}, | ||||||
|   portal::{prelogin, retrieve_config, Prelogin}, |   portal::{prelogin, retrieve_config, PortalError, Prelogin}, | ||||||
|   process::auth_launcher::SamlAuthLauncher, |   process::auth_launcher::SamlAuthLauncher, | ||||||
|   utils::{self, shutdown_signal}, |   utils::shutdown_signal, | ||||||
|   GP_USER_AGENT, |   GP_USER_AGENT, | ||||||
| }; | }; | ||||||
| use inquire::{Password, PasswordDisplayMode, Select, Text}; | use inquire::{Password, PasswordDisplayMode, Select, Text}; | ||||||
| @@ -21,17 +21,9 @@ use crate::{cli::SharedArgs, GP_CLIENT_LOCK_FILE}; | |||||||
| pub(crate) struct ConnectArgs { | pub(crate) struct ConnectArgs { | ||||||
|   #[arg(help = "The portal server to connect to")] |   #[arg(help = "The portal server to connect to")] | ||||||
|   server: String, |   server: String, | ||||||
|   #[arg( |   #[arg(short, long, help = "The gateway to connect to, it will prompt if not specified")] | ||||||
|     short, |  | ||||||
|     long, |  | ||||||
|     help = "The gateway to connect to, it will prompt if not specified" |  | ||||||
|   )] |  | ||||||
|   gateway: Option<String>, |   gateway: Option<String>, | ||||||
|   #[arg( |   #[arg(short, long, help = "The username to use, it will prompt if not specified")] | ||||||
|     short, |  | ||||||
|     long, |  | ||||||
|     help = "The username to use, it will prompt if not specified" |  | ||||||
|   )] |  | ||||||
|   user: Option<String>, |   user: Option<String>, | ||||||
|   #[arg(long, short, help = "The VPNC script to use")] |   #[arg(long, short, help = "The VPNC script to use")] | ||||||
|   script: Option<String>, |   script: Option<String>, | ||||||
| @@ -71,19 +63,38 @@ impl<'a> ConnectHandler<'a> { | |||||||
|     Self { args, shared_args } |     Self { args, shared_args } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   pub(crate) async fn handle(&self) -> anyhow::Result<()> { |   fn build_gp_params(&self) -> GpParams { | ||||||
|     let portal = utils::normalize_server(self.args.server.as_str())?; |     GpParams::builder() | ||||||
|  |  | ||||||
|     let gp_params = GpParams::builder() |  | ||||||
|       .user_agent(&self.args.user_agent) |       .user_agent(&self.args.user_agent) | ||||||
|       .client_os(ClientOs::from(&self.args.os)) |       .client_os(ClientOs::from(&self.args.os)) | ||||||
|       .os_version(self.args.os_version()) |       .os_version(self.args.os_version()) | ||||||
|       .ignore_tls_errors(self.shared_args.ignore_tls_errors) |       .ignore_tls_errors(self.shared_args.ignore_tls_errors) | ||||||
|       .build(); |       .build() | ||||||
|  |   } | ||||||
|  |  | ||||||
|     let prelogin = prelogin(&portal, &gp_params).await?; |   pub(crate) async fn handle(&self) -> anyhow::Result<()> { | ||||||
|     let portal_credential = self.obtain_portal_credential(&prelogin).await?; |     let server = self.args.server.as_str(); | ||||||
|     let mut portal_config = retrieve_config(&portal, &portal_credential, &gp_params).await?; |  | ||||||
|  |     let Err(err) = self.connect_portal_with_prelogin(server).await else { | ||||||
|  |       return Ok(()); | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     info!("Failed to connect portal with prelogin: {}", err); | ||||||
|  |     if err.root_cause().downcast_ref::<PortalError>().is_some() { | ||||||
|  |       info!("Trying the gateway authentication workflow..."); | ||||||
|  |       return self.connect_gateway_with_prelogin(server).await; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     Err(err) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   async fn connect_portal_with_prelogin(&self, portal: &str) -> anyhow::Result<()> { | ||||||
|  |     let gp_params = self.build_gp_params(); | ||||||
|  |  | ||||||
|  |     let prelogin = prelogin(portal, &gp_params).await?; | ||||||
|  |  | ||||||
|  |     let cred = self.obtain_credential(&prelogin, portal).await?; | ||||||
|  |     let mut portal_config = retrieve_config(portal, &cred, &gp_params).await?; | ||||||
|  |  | ||||||
|     let selected_gateway = match &self.args.gateway { |     let selected_gateway = match &self.args.gateway { | ||||||
|       Some(gateway) => portal_config |       Some(gateway) => portal_config | ||||||
| @@ -105,9 +116,32 @@ impl<'a> ConnectHandler<'a> { | |||||||
|  |  | ||||||
|     let gateway = selected_gateway.server(); |     let gateway = selected_gateway.server(); | ||||||
|     let cred = portal_config.auth_cookie().into(); |     let cred = portal_config.auth_cookie().into(); | ||||||
|     let token = gateway_login(gateway, &cred, &gp_params).await?; |  | ||||||
|  |  | ||||||
|     let vpn = Vpn::builder(gateway, &token) |     let cookie = match gateway_login(gateway, &cred, &gp_params).await { | ||||||
|  |       Ok(cookie) => cookie, | ||||||
|  |       Err(err) => { | ||||||
|  |         info!("Gateway login failed: {}", err); | ||||||
|  |         return self.connect_gateway_with_prelogin(gateway).await; | ||||||
|  |       } | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     self.connect_gateway(gateway, &cookie).await | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   async fn connect_gateway_with_prelogin(&self, gateway: &str) -> anyhow::Result<()> { | ||||||
|  |     let mut gp_params = self.build_gp_params(); | ||||||
|  |     gp_params.set_is_gateway(true); | ||||||
|  |  | ||||||
|  |     let prelogin = prelogin(gateway, &gp_params).await?; | ||||||
|  |     let cred = self.obtain_credential(&prelogin, &gateway).await?; | ||||||
|  |  | ||||||
|  |     let cookie = gateway_login(gateway, &cred, &gp_params).await?; | ||||||
|  |  | ||||||
|  |     self.connect_gateway(gateway, &cookie).await | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   async fn connect_gateway(&self, gateway: &str, cookie: &str) -> anyhow::Result<()> { | ||||||
|  |     let vpn = Vpn::builder(gateway, cookie) | ||||||
|       .user_agent(self.args.user_agent.clone()) |       .user_agent(self.args.user_agent.clone()) | ||||||
|       .script(self.args.script.clone()) |       .script(self.args.script.clone()) | ||||||
|       .build(); |       .build(); | ||||||
| @@ -132,10 +166,13 @@ impl<'a> ConnectHandler<'a> { | |||||||
|     Ok(()) |     Ok(()) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   async fn obtain_portal_credential(&self, prelogin: &Prelogin) -> anyhow::Result<Credential> { |   async fn obtain_credential(&self, prelogin: &Prelogin, server: &str) -> anyhow::Result<Credential> { | ||||||
|  |     let is_gateway = prelogin.is_gateway(); | ||||||
|  |  | ||||||
|     match prelogin { |     match prelogin { | ||||||
|       Prelogin::Saml(prelogin) => { |       Prelogin::Saml(prelogin) => { | ||||||
|         SamlAuthLauncher::new(&self.args.server) |         SamlAuthLauncher::new(&self.args.server) | ||||||
|  |           .gateway(is_gateway) | ||||||
|           .saml_request(prelogin.saml_request()) |           .saml_request(prelogin.saml_request()) | ||||||
|           .user_agent(&self.args.user_agent) |           .user_agent(&self.args.user_agent) | ||||||
|           .os(self.args.os.as_str()) |           .os(self.args.os.as_str()) | ||||||
| @@ -148,7 +185,8 @@ impl<'a> ConnectHandler<'a> { | |||||||
|           .await |           .await | ||||||
|       } |       } | ||||||
|       Prelogin::Standard(prelogin) => { |       Prelogin::Standard(prelogin) => { | ||||||
|         println!("{}", prelogin.auth_message()); |         let prefix = if is_gateway { "Gateway" } else { "Portal" }; | ||||||
|  |         println!("{} ({}: {})", prelogin.auth_message(), prefix, server); | ||||||
|  |  | ||||||
|         let user = self.args.user.as_ref().map_or_else( |         let user = self.args.user.as_ref().map_or_else( | ||||||
|           || Text::new(&format!("{}:", prelogin.label_username())).prompt(), |           || Text::new(&format!("{}:", prelogin.label_username())).prompt(), | ||||||
|   | |||||||
| @@ -6,9 +6,7 @@ use clap::Parser; | |||||||
| use gpapi::{ | use gpapi::{ | ||||||
|   process::gui_launcher::GuiLauncher, |   process::gui_launcher::GuiLauncher, | ||||||
|   service::{request::WsRequest, vpn_state::VpnState}, |   service::{request::WsRequest, vpn_state::VpnState}, | ||||||
|   utils::{ |   utils::{crypto::generate_key, env_file, lock_file::LockFile, redact::Redaction, shutdown_signal}, | ||||||
|     crypto::generate_key, env_file, lock_file::LockFile, redact::Redaction, shutdown_signal, |  | ||||||
|   }, |  | ||||||
|   GP_SERVICE_LOCK_FILE, |   GP_SERVICE_LOCK_FILE, | ||||||
| }; | }; | ||||||
| use log::{info, warn, LevelFilter}; | use log::{info, warn, LevelFilter}; | ||||||
| @@ -16,12 +14,7 @@ use tokio::sync::{mpsc, watch}; | |||||||
|  |  | ||||||
| use crate::{vpn_task::VpnTask, ws_server::WsServer}; | use crate::{vpn_task::VpnTask, ws_server::WsServer}; | ||||||
|  |  | ||||||
| const VERSION: &str = concat!( | const VERSION: &str = concat!(env!("CARGO_PKG_VERSION"), " (", compile_time::date_str!(), ")"); | ||||||
|   env!("CARGO_PKG_VERSION"), |  | ||||||
|   " (", |  | ||||||
|   compile_time::date_str!(), |  | ||||||
|   ")" |  | ||||||
| ); |  | ||||||
|  |  | ||||||
| #[derive(Parser)] | #[derive(Parser)] | ||||||
| #[command(version = VERSION)] | #[command(version = VERSION)] | ||||||
| @@ -51,13 +44,7 @@ impl Cli { | |||||||
|     let (vpn_state_tx, vpn_state_rx) = watch::channel(VpnState::Disconnected); |     let (vpn_state_tx, vpn_state_rx) = watch::channel(VpnState::Disconnected); | ||||||
|  |  | ||||||
|     let mut vpn_task = VpnTask::new(ws_req_rx, vpn_state_tx); |     let mut vpn_task = VpnTask::new(ws_req_rx, vpn_state_tx); | ||||||
|     let ws_server = WsServer::new( |     let ws_server = WsServer::new(api_key.clone(), ws_req_tx, vpn_state_rx, lock_file.clone(), redaction); | ||||||
|       api_key.clone(), |  | ||||||
|       ws_req_tx, |  | ||||||
|       vpn_state_rx, |  | ||||||
|       lock_file.clone(), |  | ||||||
|       redaction, |  | ||||||
|     ); |  | ||||||
|  |  | ||||||
|     let (shutdown_tx, mut shutdown_rx) = mpsc::channel::<()>(4); |     let (shutdown_tx, mut shutdown_rx) = mpsc::channel::<()>(4); | ||||||
|     let shutdown_tx_clone = shutdown_tx.clone(); |     let shutdown_tx_clone = shutdown_tx.clone(); | ||||||
| @@ -76,11 +63,7 @@ impl Cli { | |||||||
|     if no_gui { |     if no_gui { | ||||||
|       info!("GUI is disabled"); |       info!("GUI is disabled"); | ||||||
|     } else { |     } else { | ||||||
|       let envs = self |       let envs = self.env_file.as_ref().map(env_file::load_env_vars).transpose()?; | ||||||
|         .env_file |  | ||||||
|         .as_ref() |  | ||||||
|         .map(env_file::load_env_vars) |  | ||||||
|         .transpose()?; |  | ||||||
|  |  | ||||||
|       let minimized = self.minimized; |       let minimized = self.minimized; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -21,17 +21,11 @@ pub(crate) async fn active_gui(State(ctx): State<Arc<WsServerContext>>) -> impl | |||||||
|   ctx.send_event(WsEvent::ActiveGui).await; |   ctx.send_event(WsEvent::ActiveGui).await; | ||||||
| } | } | ||||||
|  |  | ||||||
| pub(crate) async fn auth_data( | pub(crate) async fn auth_data(State(ctx): State<Arc<WsServerContext>>, body: String) -> impl IntoResponse { | ||||||
|   State(ctx): State<Arc<WsServerContext>>, |  | ||||||
|   body: String, |  | ||||||
| ) -> impl IntoResponse { |  | ||||||
|   ctx.send_event(WsEvent::AuthData(body)).await; |   ctx.send_event(WsEvent::AuthData(body)).await; | ||||||
| } | } | ||||||
|  |  | ||||||
| pub(crate) async fn ws_handler( | pub(crate) async fn ws_handler(ws: WebSocketUpgrade, State(ctx): State<Arc<WsServerContext>>) -> impl IntoResponse { | ||||||
|   ws: WebSocketUpgrade, |  | ||||||
|   State(ctx): State<Arc<WsServerContext>>, |  | ||||||
| ) -> impl IntoResponse { |  | ||||||
|   ws.on_upgrade(move |socket| handle_socket(socket, ctx)) |   ws.on_upgrade(move |socket| handle_socket(socket, ctx)) | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -2,8 +2,8 @@ mod cli; | |||||||
| mod handlers; | mod handlers; | ||||||
| mod routes; | mod routes; | ||||||
| mod vpn_task; | mod vpn_task; | ||||||
| mod ws_server; |  | ||||||
| mod ws_connection; | mod ws_connection; | ||||||
|  | mod ws_server; | ||||||
|  |  | ||||||
| #[tokio::main] | #[tokio::main] | ||||||
| async fn main() { | async fn main() { | ||||||
|   | |||||||
| @@ -1,6 +1,9 @@ | |||||||
| use std::sync::Arc; | use std::sync::Arc; | ||||||
|  |  | ||||||
| use axum::{routing::{get, post}, Router}; | use axum::{ | ||||||
|  |   routing::{get, post}, | ||||||
|  |   Router, | ||||||
|  | }; | ||||||
|  |  | ||||||
| use crate::{handlers, ws_server::WsServerContext}; | use crate::{handlers, ws_server::WsServerContext}; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -98,12 +98,7 @@ impl WsServer { | |||||||
|     lock_file: Arc<LockFile>, |     lock_file: Arc<LockFile>, | ||||||
|     redaction: Arc<Redaction>, |     redaction: Arc<Redaction>, | ||||||
|   ) -> Self { |   ) -> Self { | ||||||
|     let ctx = Arc::new(WsServerContext::new( |     let ctx = Arc::new(WsServerContext::new(api_key, ws_req_tx, vpn_state_rx, redaction)); | ||||||
|       api_key, |  | ||||||
|       ws_req_tx, |  | ||||||
|       vpn_state_rx, |  | ||||||
|       redaction, |  | ||||||
|     )); |  | ||||||
|     let cancel_token = CancellationToken::new(); |     let cancel_token = CancellationToken::new(); | ||||||
|  |  | ||||||
|     Self { |     Self { | ||||||
|   | |||||||
| @@ -27,11 +27,7 @@ impl SamlAuthResult { | |||||||
| } | } | ||||||
|  |  | ||||||
| impl SamlAuthData { | impl SamlAuthData { | ||||||
|   pub fn new( |   pub fn new(username: String, prelogin_cookie: Option<String>, portal_userauthcookie: Option<String>) -> Self { | ||||||
|     username: String, |  | ||||||
|     prelogin_cookie: Option<String>, |  | ||||||
|     portal_userauthcookie: Option<String>, |  | ||||||
|   ) -> Self { |  | ||||||
|     Self { |     Self { | ||||||
|       username, |       username, | ||||||
|       prelogin_cookie, |       prelogin_cookie, | ||||||
| @@ -78,13 +74,9 @@ impl SamlAuthData { | |||||||
|     prelogin_cookie: &Option<String>, |     prelogin_cookie: &Option<String>, | ||||||
|     portal_userauthcookie: &Option<String>, |     portal_userauthcookie: &Option<String>, | ||||||
|   ) -> bool { |   ) -> bool { | ||||||
|     let username_valid = username |     let username_valid = username.as_ref().is_some_and(|username| !username.is_empty()); | ||||||
|       .as_ref() |  | ||||||
|       .is_some_and(|username| !username.is_empty()); |  | ||||||
|     let prelogin_cookie_valid = prelogin_cookie.as_ref().is_some_and(|val| val.len() > 5); |     let prelogin_cookie_valid = prelogin_cookie.as_ref().is_some_and(|val| val.len() > 5); | ||||||
|     let portal_userauthcookie_valid = portal_userauthcookie |     let portal_userauthcookie_valid = portal_userauthcookie.as_ref().is_some_and(|val| val.len() > 5); | ||||||
|       .as_ref() |  | ||||||
|       .is_some_and(|val| val.len() > 5); |  | ||||||
|  |  | ||||||
|     username_valid && (prelogin_cookie_valid || portal_userauthcookie_valid) |     username_valid && (prelogin_cookie_valid || portal_userauthcookie_valid) | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -112,11 +112,7 @@ pub struct CachedCredential { | |||||||
| } | } | ||||||
|  |  | ||||||
| impl CachedCredential { | impl CachedCredential { | ||||||
|   pub fn new( |   pub fn new(username: String, password: Option<String>, auth_cookie: AuthCookieCredential) -> Self { | ||||||
|     username: String, |  | ||||||
|     password: Option<String>, |  | ||||||
|     auth_cookie: AuthCookieCredential, |  | ||||||
|   ) -> Self { |  | ||||||
|     Self { |     Self { | ||||||
|       username, |       username, | ||||||
|       password, |       password, | ||||||
| @@ -139,6 +135,24 @@ impl CachedCredential { | |||||||
|   pub fn set_auth_cookie(&mut self, auth_cookie: AuthCookieCredential) { |   pub fn set_auth_cookie(&mut self, auth_cookie: AuthCookieCredential) { | ||||||
|     self.auth_cookie = auth_cookie; |     self.auth_cookie = auth_cookie; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   pub fn set_username(&mut self, username: String) { | ||||||
|  |     self.username = username; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   pub fn set_password(&mut self, password: Option<String>) { | ||||||
|  |     self.password = password.map(|s| s.to_string()); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl From<PasswordCredential> for CachedCredential { | ||||||
|  |   fn from(value: PasswordCredential) -> Self { | ||||||
|  |     Self::new( | ||||||
|  |       value.username().to_owned(), | ||||||
|  |       Some(value.password().to_owned()), | ||||||
|  |       AuthCookieCredential::new("", "", ""), | ||||||
|  |     ) | ||||||
|  |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| #[derive(Debug, Serialize, Deserialize, Type, Clone)] | #[derive(Debug, Serialize, Deserialize, Type, Clone)] | ||||||
| @@ -175,8 +189,7 @@ impl Credential { | |||||||
|     let mut params = HashMap::new(); |     let mut params = HashMap::new(); | ||||||
|     params.insert("user", self.username()); |     params.insert("user", self.username()); | ||||||
|  |  | ||||||
|     let (passwd, prelogin_cookie, portal_userauthcookie, portal_prelogonuserauthcookie) = match self |     let (passwd, prelogin_cookie, portal_userauthcookie, portal_prelogonuserauthcookie) = match self { | ||||||
|     { |  | ||||||
|       Credential::Password(cred) => (Some(cred.password()), None, None, None), |       Credential::Password(cred) => (Some(cred.password()), None, None, None), | ||||||
|       Credential::PreloginCookie(cred) => (None, Some(cred.prelogin_cookie()), None, None), |       Credential::PreloginCookie(cred) => (None, Some(cred.prelogin_cookie()), None, None), | ||||||
|       Credential::AuthCookie(cred) => ( |       Credential::AuthCookie(cred) => ( | ||||||
| @@ -195,10 +208,7 @@ impl Credential { | |||||||
|  |  | ||||||
|     params.insert("passwd", passwd.unwrap_or_default()); |     params.insert("passwd", passwd.unwrap_or_default()); | ||||||
|     params.insert("prelogin-cookie", prelogin_cookie.unwrap_or_default()); |     params.insert("prelogin-cookie", prelogin_cookie.unwrap_or_default()); | ||||||
|     params.insert( |     params.insert("portal-userauthcookie", portal_userauthcookie.unwrap_or_default()); | ||||||
|       "portal-userauthcookie", |  | ||||||
|       portal_userauthcookie.unwrap_or_default(), |  | ||||||
|     ); |  | ||||||
|     params.insert( |     params.insert( | ||||||
|       "portal-prelogonuserauthcookie", |       "portal-prelogonuserauthcookie", | ||||||
|       portal_prelogonuserauthcookie.unwrap_or_default(), |       portal_prelogonuserauthcookie.unwrap_or_default(), | ||||||
|   | |||||||
| @@ -1,16 +1,20 @@ | |||||||
|  | use anyhow::bail; | ||||||
| use log::info; | use log::info; | ||||||
| use reqwest::Client; | use reqwest::Client; | ||||||
| use roxmltree::Document; | use roxmltree::Document; | ||||||
| use urlencoding::encode; | use urlencoding::encode; | ||||||
|  |  | ||||||
| use crate::{credential::Credential, gp_params::GpParams}; | use crate::{ | ||||||
|  |   credential::Credential, | ||||||
|  |   gp_params::GpParams, | ||||||
|  |   utils::{normalize_server, remove_url_scheme}, | ||||||
|  | }; | ||||||
|  |  | ||||||
| pub async fn gateway_login( | pub async fn gateway_login(gateway: &str, cred: &Credential, gp_params: &GpParams) -> anyhow::Result<String> { | ||||||
|   gateway: &str, |   let url = normalize_server(gateway)?; | ||||||
|   cred: &Credential, |   let gateway = remove_url_scheme(&url); | ||||||
|   gp_params: &GpParams, |  | ||||||
| ) -> anyhow::Result<String> { |   let login_url = format!("{}/ssl-vpn/login.esp", url); | ||||||
|   let login_url = format!("https://{}/ssl-vpn/login.esp", gateway); |  | ||||||
|   let client = Client::builder() |   let client = Client::builder() | ||||||
|     .danger_accept_invalid_certs(gp_params.ignore_tls_errors()) |     .danger_accept_invalid_certs(gp_params.ignore_tls_errors()) | ||||||
|     .user_agent(gp_params.user_agent()) |     .user_agent(gp_params.user_agent()) | ||||||
| @@ -20,13 +24,18 @@ pub async fn gateway_login( | |||||||
|   let extra_params = gp_params.to_params(); |   let extra_params = gp_params.to_params(); | ||||||
|  |  | ||||||
|   params.extend(extra_params); |   params.extend(extra_params); | ||||||
|   params.insert("server", gateway); |   params.insert("server", &gateway); | ||||||
|  |  | ||||||
|   info!("Gateway login, user_agent: {}", gp_params.user_agent()); |   info!("Gateway login, user_agent: {}", gp_params.user_agent()); | ||||||
|  |  | ||||||
|   let res = client.post(&login_url).form(¶ms).send().await?; |   let res = client.post(&login_url).form(¶ms).send().await?; | ||||||
|   let res_xml = res.error_for_status()?.text().await?; |   let status = res.status(); | ||||||
|  |  | ||||||
|  |   if status.is_client_error() || status.is_server_error() { | ||||||
|  |     bail!("Gateway login error: {}", status) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   let res_xml = res.text().await?; | ||||||
|   let doc = Document::parse(&res_xml)?; |   let doc = Document::parse(&res_xml)?; | ||||||
|  |  | ||||||
|   build_gateway_token(&doc, gp_params.computer()) |   build_gateway_token(&doc, gp_params.computer()) | ||||||
| @@ -57,11 +66,7 @@ fn build_gateway_token(doc: &Document, computer: &str) -> anyhow::Result<String> | |||||||
|   Ok(token) |   Ok(token) | ||||||
| } | } | ||||||
|  |  | ||||||
| fn read_args<'a>( | fn read_args<'a>(args: &'a [String], index: usize, key: &'a str) -> anyhow::Result<(&'a str, &'a str)> { | ||||||
|   args: &'a [String], |  | ||||||
|   index: usize, |  | ||||||
|   key: &'a str, |  | ||||||
| ) -> anyhow::Result<(&'a str, &'a str)> { |  | ||||||
|   args |   args | ||||||
|     .get(index) |     .get(index) | ||||||
|     .ok_or_else(|| anyhow::anyhow!("Failed to read {key} from args")) |     .ok_or_else(|| anyhow::anyhow!("Failed to read {key} from args")) | ||||||
|   | |||||||
| @@ -31,6 +31,15 @@ impl Display for Gateway { | |||||||
| } | } | ||||||
|  |  | ||||||
| impl Gateway { | impl Gateway { | ||||||
|  |   pub fn new(name: String, address: String) -> Self { | ||||||
|  |     Self { | ||||||
|  |       name, | ||||||
|  |       address, | ||||||
|  |       priority: 0, | ||||||
|  |       priority_rules: vec![], | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|   pub fn name(&self) -> &str { |   pub fn name(&self) -> &str { | ||||||
|     &self.name |     &self.name | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -4,9 +4,7 @@ use super::{Gateway, PriorityRule}; | |||||||
|  |  | ||||||
| pub(crate) fn parse_gateways(doc: &Document) -> Option<Vec<Gateway>> { | pub(crate) fn parse_gateways(doc: &Document) -> Option<Vec<Gateway>> { | ||||||
|   let node_gateways = doc.descendants().find(|n| n.has_tag_name("gateways"))?; |   let node_gateways = doc.descendants().find(|n| n.has_tag_name("gateways"))?; | ||||||
|   let list_gateway = node_gateways |   let list_gateway = node_gateways.descendants().find(|n| n.has_tag_name("list"))?; | ||||||
|     .descendants() |  | ||||||
|     .find(|n| n.has_tag_name("list"))?; |  | ||||||
|  |  | ||||||
|   let gateways = list_gateway |   let gateways = list_gateway | ||||||
|     .children() |     .children() | ||||||
|   | |||||||
| @@ -44,6 +44,7 @@ impl ClientOs { | |||||||
|  |  | ||||||
| #[derive(Debug, Serialize, Deserialize, Type, Default)] | #[derive(Debug, Serialize, Deserialize, Type, Default)] | ||||||
| pub struct GpParams { | pub struct GpParams { | ||||||
|  |   is_gateway: bool, | ||||||
|   user_agent: String, |   user_agent: String, | ||||||
|   client_os: ClientOs, |   client_os: ClientOs, | ||||||
|   os_version: Option<String>, |   os_version: Option<String>, | ||||||
| @@ -58,6 +59,14 @@ impl GpParams { | |||||||
|     GpParamsBuilder::new() |     GpParamsBuilder::new() | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   pub(crate) fn is_gateway(&self) -> bool { | ||||||
|  |     self.is_gateway | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   pub fn set_is_gateway(&mut self, is_gateway: bool) { | ||||||
|  |     self.is_gateway = is_gateway; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   pub(crate) fn user_agent(&self) -> &str { |   pub(crate) fn user_agent(&self) -> &str { | ||||||
|     &self.user_agent |     &self.user_agent | ||||||
|   } |   } | ||||||
| @@ -103,6 +112,7 @@ impl GpParams { | |||||||
| } | } | ||||||
|  |  | ||||||
| pub struct GpParamsBuilder { | pub struct GpParamsBuilder { | ||||||
|  |   is_gateway: bool, | ||||||
|   user_agent: String, |   user_agent: String, | ||||||
|   client_os: ClientOs, |   client_os: ClientOs, | ||||||
|   os_version: Option<String>, |   os_version: Option<String>, | ||||||
| @@ -115,6 +125,7 @@ pub struct GpParamsBuilder { | |||||||
| impl GpParamsBuilder { | impl GpParamsBuilder { | ||||||
|   pub fn new() -> Self { |   pub fn new() -> Self { | ||||||
|     Self { |     Self { | ||||||
|  |       is_gateway: false, | ||||||
|       user_agent: GP_USER_AGENT.to_string(), |       user_agent: GP_USER_AGENT.to_string(), | ||||||
|       client_os: ClientOs::Linux, |       client_os: ClientOs::Linux, | ||||||
|       os_version: Default::default(), |       os_version: Default::default(), | ||||||
| @@ -125,6 +136,11 @@ impl GpParamsBuilder { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   pub fn is_gateway(&mut self, is_gateway: bool) -> &mut Self { | ||||||
|  |     self.is_gateway = is_gateway; | ||||||
|  |     self | ||||||
|  |   } | ||||||
|  |  | ||||||
|   pub fn user_agent(&mut self, user_agent: &str) -> &mut Self { |   pub fn user_agent(&mut self, user_agent: &str) -> &mut Self { | ||||||
|     self.user_agent = user_agent.to_string(); |     self.user_agent = user_agent.to_string(); | ||||||
|     self |     self | ||||||
| @@ -162,6 +178,7 @@ impl GpParamsBuilder { | |||||||
|  |  | ||||||
|   pub fn build(&self) -> GpParams { |   pub fn build(&self) -> GpParams { | ||||||
|     GpParams { |     GpParams { | ||||||
|  |       is_gateway: self.is_gateway, | ||||||
|       user_agent: self.user_agent.clone(), |       user_agent: self.user_agent.clone(), | ||||||
|       client_os: self.client_os.clone(), |       client_os: self.client_os.clone(), | ||||||
|       os_version: self.os_version.clone(), |       os_version: self.os_version.clone(), | ||||||
|   | |||||||
| @@ -1,16 +1,16 @@ | |||||||
| use anyhow::ensure; | use anyhow::bail; | ||||||
| use log::info; | use log::info; | ||||||
| use reqwest::Client; | use reqwest::{Client, StatusCode}; | ||||||
| use roxmltree::Document; | use roxmltree::Document; | ||||||
| use serde::Serialize; | use serde::Serialize; | ||||||
| use specta::Type; | use specta::Type; | ||||||
| use thiserror::Error; |  | ||||||
|  |  | ||||||
| use crate::{ | use crate::{ | ||||||
|   credential::{AuthCookieCredential, Credential}, |   credential::{AuthCookieCredential, Credential}, | ||||||
|   gateway::{parse_gateways, Gateway}, |   gateway::{parse_gateways, Gateway}, | ||||||
|   gp_params::GpParams, |   gp_params::GpParams, | ||||||
|   utils::{normalize_server, xml}, |   portal::PortalError, | ||||||
|  |   utils::{normalize_server, remove_url_scheme, xml}, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| #[derive(Debug, Serialize, Type)] | #[derive(Debug, Serialize, Type)] | ||||||
| @@ -18,25 +18,12 @@ use crate::{ | |||||||
| pub struct PortalConfig { | pub struct PortalConfig { | ||||||
|   portal: String, |   portal: String, | ||||||
|   auth_cookie: AuthCookieCredential, |   auth_cookie: AuthCookieCredential, | ||||||
|  |   config_cred: Credential, | ||||||
|   gateways: Vec<Gateway>, |   gateways: Vec<Gateway>, | ||||||
|   config_digest: Option<String>, |   config_digest: Option<String>, | ||||||
| } | } | ||||||
|  |  | ||||||
| impl PortalConfig { | impl PortalConfig { | ||||||
|   pub fn new( |  | ||||||
|     portal: String, |  | ||||||
|     auth_cookie: AuthCookieCredential, |  | ||||||
|     gateways: Vec<Gateway>, |  | ||||||
|     config_digest: Option<String>, |  | ||||||
|   ) -> Self { |  | ||||||
|     Self { |  | ||||||
|       portal, |  | ||||||
|       auth_cookie, |  | ||||||
|       gateways, |  | ||||||
|       config_digest, |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   pub fn portal(&self) -> &str { |   pub fn portal(&self) -> &str { | ||||||
|     &self.portal |     &self.portal | ||||||
|   } |   } | ||||||
| @@ -49,6 +36,10 @@ impl PortalConfig { | |||||||
|     &self.auth_cookie |     &self.auth_cookie | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   pub fn config_cred(&self) -> &Credential { | ||||||
|  |     &self.config_cred | ||||||
|  |   } | ||||||
|  |  | ||||||
|   /// In-place sort the gateways by region |   /// In-place sort the gateways by region | ||||||
|   pub fn sort_gateways(&mut self, region: &str) { |   pub fn sort_gateways(&mut self, region: &str) { | ||||||
|     let preferred_gateway = self.find_preferred_gateway(region); |     let preferred_gateway = self.find_preferred_gateway(region); | ||||||
| @@ -88,33 +79,11 @@ impl PortalConfig { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     // If no gateway is found, return the gateway with the lowest priority |     // If no gateway is found, return the gateway with the lowest priority | ||||||
|     preferred_gateway.unwrap_or_else(|| { |     preferred_gateway.unwrap_or_else(|| self.gateways.iter().min_by_key(|gateway| gateway.priority).unwrap()) | ||||||
|       self |  | ||||||
|         .gateways |  | ||||||
|         .iter() |  | ||||||
|         .min_by_key(|gateway| gateway.priority) |  | ||||||
|         .unwrap() |  | ||||||
|     }) |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| #[derive(Error, Debug)] | pub async fn retrieve_config(portal: &str, cred: &Credential, gp_params: &GpParams) -> anyhow::Result<PortalConfig> { | ||||||
| pub enum PortalConfigError { |  | ||||||
|   #[error("Empty response, retrying can help")] |  | ||||||
|   EmptyResponse, |  | ||||||
|   #[error("Empty auth cookie, retrying can help")] |  | ||||||
|   EmptyAuthCookie, |  | ||||||
|   #[error("Invalid auth cookie, retrying can help")] |  | ||||||
|   InvalidAuthCookie, |  | ||||||
|   #[error("Empty gateways, retrying can help")] |  | ||||||
|   EmptyGateways, |  | ||||||
| } |  | ||||||
|  |  | ||||||
| pub async fn retrieve_config( |  | ||||||
|   portal: &str, |  | ||||||
|   cred: &Credential, |  | ||||||
|   gp_params: &GpParams, |  | ||||||
| ) -> anyhow::Result<PortalConfig> { |  | ||||||
|   let portal = normalize_server(portal)?; |   let portal = normalize_server(portal)?; | ||||||
|   let server = remove_url_scheme(&portal); |   let server = remove_url_scheme(&portal); | ||||||
|  |  | ||||||
| @@ -134,42 +103,42 @@ pub async fn retrieve_config( | |||||||
|   info!("Portal config, user_agent: {}", gp_params.user_agent()); |   info!("Portal config, user_agent: {}", gp_params.user_agent()); | ||||||
|  |  | ||||||
|   let res = client.post(&url).form(¶ms).send().await?; |   let res = client.post(&url).form(¶ms).send().await?; | ||||||
|   let res_xml = res.error_for_status()?.text().await?; |   let status = res.status(); | ||||||
|  |  | ||||||
|   ensure!(!res_xml.is_empty(), PortalConfigError::EmptyResponse); |   if status == StatusCode::NOT_FOUND { | ||||||
|  |     bail!(PortalError::ConfigError("Config endpoint not found".to_string())) | ||||||
|  |   } | ||||||
|  |  | ||||||
|   let doc = Document::parse(&res_xml)?; |   if status.is_client_error() || status.is_server_error() { | ||||||
|   let gateways = parse_gateways(&doc).ok_or_else(|| anyhow::anyhow!("Failed to parse gateways"))?; |     bail!("Portal config error: {}", status) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   let res_xml = res.text().await.map_err(|e| PortalError::ConfigError(e.to_string()))?; | ||||||
|  |  | ||||||
|  |   if res_xml.is_empty() { | ||||||
|  |     bail!(PortalError::ConfigError("Empty portal config response".to_string())) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   let doc = Document::parse(&res_xml).map_err(|e| PortalError::ConfigError(e.to_string()))?; | ||||||
|  |  | ||||||
|  |   let mut gateways = parse_gateways(&doc).unwrap_or_else(|| { | ||||||
|  |     info!("No gateways found in portal config"); | ||||||
|  |     vec![] | ||||||
|  |   }); | ||||||
|  |  | ||||||
|   let user_auth_cookie = xml::get_child_text(&doc, "portal-userauthcookie").unwrap_or_default(); |   let user_auth_cookie = xml::get_child_text(&doc, "portal-userauthcookie").unwrap_or_default(); | ||||||
|   let prelogon_user_auth_cookie = |   let prelogon_user_auth_cookie = xml::get_child_text(&doc, "portal-prelogonuserauthcookie").unwrap_or_default(); | ||||||
|     xml::get_child_text(&doc, "portal-prelogonuserauthcookie").unwrap_or_default(); |  | ||||||
|   let config_digest = xml::get_child_text(&doc, "config-digest"); |   let config_digest = xml::get_child_text(&doc, "config-digest"); | ||||||
|  |  | ||||||
|   ensure!( |   if gateways.is_empty() { | ||||||
|     !user_auth_cookie.is_empty() && !prelogon_user_auth_cookie.is_empty(), |     gateways.push(Gateway::new(server.to_string(), server.to_string())); | ||||||
|     PortalConfigError::EmptyAuthCookie |   } | ||||||
|   ); |  | ||||||
|  |  | ||||||
|   ensure!( |   Ok(PortalConfig { | ||||||
|     user_auth_cookie != "empty" && prelogon_user_auth_cookie != "empty", |     portal: server.to_string(), | ||||||
|     PortalConfigError::InvalidAuthCookie |     auth_cookie: AuthCookieCredential::new(cred.username(), &user_auth_cookie, &prelogon_user_auth_cookie), | ||||||
|   ); |     config_cred: cred.clone(), | ||||||
|  |  | ||||||
|   ensure!(!gateways.is_empty(), PortalConfigError::EmptyGateways); |  | ||||||
|  |  | ||||||
|   Ok(PortalConfig::new( |  | ||||||
|     server.to_string(), |  | ||||||
|     AuthCookieCredential::new( |  | ||||||
|       cred.username(), |  | ||||||
|       &user_auth_cookie, |  | ||||||
|       &prelogon_user_auth_cookie, |  | ||||||
|     ), |  | ||||||
|     gateways, |     gateways, | ||||||
|     config_digest, |     config_digest, | ||||||
|   )) |   }) | ||||||
| } |  | ||||||
|  |  | ||||||
| fn remove_url_scheme(s: &str) -> String { |  | ||||||
|   s.replace("http://", "").replace("https://", "") |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -3,3 +3,13 @@ mod prelogin; | |||||||
|  |  | ||||||
| pub use config::*; | pub use config::*; | ||||||
| pub use prelogin::*; | pub use prelogin::*; | ||||||
|  |  | ||||||
|  | use thiserror::Error; | ||||||
|  |  | ||||||
|  | #[derive(Error, Debug)] | ||||||
|  | pub enum PortalError { | ||||||
|  |   #[error("Portal prelogin error: {0}")] | ||||||
|  |   PreloginError(String), | ||||||
|  |   #[error("Portal config error: {0}")] | ||||||
|  |   ConfigError(String), | ||||||
|  | } | ||||||
|   | |||||||
| @@ -1,12 +1,13 @@ | |||||||
| use anyhow::bail; | use anyhow::bail; | ||||||
| use log::{info, trace}; | use log::info; | ||||||
| use reqwest::Client; | use reqwest::{Client, StatusCode}; | ||||||
| use roxmltree::Document; | use roxmltree::Document; | ||||||
| use serde::Serialize; | use serde::Serialize; | ||||||
| use specta::Type; | use specta::Type; | ||||||
|  |  | ||||||
| use crate::{ | use crate::{ | ||||||
|   gp_params::GpParams, |   gp_params::GpParams, | ||||||
|  |   portal::PortalError, | ||||||
|   utils::{base64, normalize_server, xml}, |   utils::{base64, normalize_server, xml}, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| @@ -25,6 +26,7 @@ const REQUIRED_PARAMS: [&str; 8] = [ | |||||||
| #[serde(rename_all = "camelCase")] | #[serde(rename_all = "camelCase")] | ||||||
| pub struct SamlPrelogin { | pub struct SamlPrelogin { | ||||||
|   region: String, |   region: String, | ||||||
|  |   is_gateway: bool, | ||||||
|   saml_request: String, |   saml_request: String, | ||||||
|   support_default_browser: bool, |   support_default_browser: bool, | ||||||
| } | } | ||||||
| @@ -47,6 +49,7 @@ impl SamlPrelogin { | |||||||
| #[serde(rename_all = "camelCase")] | #[serde(rename_all = "camelCase")] | ||||||
| pub struct StandardPrelogin { | pub struct StandardPrelogin { | ||||||
|   region: String, |   region: String, | ||||||
|  |   is_gateway: bool, | ||||||
|   auth_message: String, |   auth_message: String, | ||||||
|   label_username: String, |   label_username: String, | ||||||
|   label_password: String, |   label_password: String, | ||||||
| @@ -84,27 +87,31 @@ impl Prelogin { | |||||||
|       Prelogin::Standard(standard) => standard.region(), |       Prelogin::Standard(standard) => standard.region(), | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   pub fn is_gateway(&self) -> bool { | ||||||
|  |     match self { | ||||||
|  |       Prelogin::Saml(saml) => saml.is_gateway, | ||||||
|  |       Prelogin::Standard(standard) => standard.is_gateway, | ||||||
|  |     } | ||||||
|  |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| pub async fn prelogin(portal: &str, gp_params: &GpParams) -> anyhow::Result<Prelogin> { | pub async fn prelogin(portal: &str, gp_params: &GpParams) -> anyhow::Result<Prelogin> { | ||||||
|   let user_agent = gp_params.user_agent(); |   let user_agent = gp_params.user_agent(); | ||||||
|   info!("Portal prelogin, user_agent: {}", user_agent); |   info!("Prelogin with user_agent: {}", user_agent); | ||||||
|  |  | ||||||
|   let portal = normalize_server(portal)?; |   let portal = normalize_server(portal)?; | ||||||
|   let prelogin_url = format!("{}/global-protect/prelogin.esp", portal); |   let is_gateway = gp_params.is_gateway(); | ||||||
|  |   let path = if is_gateway { "ssl-vpn" } else { "global-protect" }; | ||||||
|  |   let prelogin_url = format!("{portal}/{}/prelogin.esp", path); | ||||||
|   let mut params = gp_params.to_params(); |   let mut params = gp_params.to_params(); | ||||||
|  |  | ||||||
|   params.insert("tmp", "tmp"); |   params.insert("tmp", "tmp"); | ||||||
|   params.insert("cas-support", "yes"); |  | ||||||
|   if gp_params.prefer_default_browser() { |   if gp_params.prefer_default_browser() { | ||||||
|     params.insert("default-browser", "1"); |     params.insert("default-browser", "1"); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   params.retain(|k, _| { |   params.retain(|k, _| REQUIRED_PARAMS.iter().any(|required_param| required_param == k)); | ||||||
|     REQUIRED_PARAMS |  | ||||||
|       .iter() |  | ||||||
|       .any(|required_param| required_param == k) |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
|   let client = Client::builder() |   let client = Client::builder() | ||||||
|     .danger_accept_invalid_certs(gp_params.ignore_tls_errors()) |     .danger_accept_invalid_certs(gp_params.ignore_tls_errors()) | ||||||
| @@ -112,9 +119,27 @@ pub async fn prelogin(portal: &str, gp_params: &GpParams) -> anyhow::Result<Prel | |||||||
|     .build()?; |     .build()?; | ||||||
|  |  | ||||||
|   let res = client.post(&prelogin_url).form(¶ms).send().await?; |   let res = client.post(&prelogin_url).form(¶ms).send().await?; | ||||||
|   let res_xml = res.error_for_status()?.text().await?; |   let status = res.status(); | ||||||
|  |  | ||||||
|   trace!("Prelogin response: {}", res_xml); |   if status == StatusCode::NOT_FOUND { | ||||||
|  |     bail!(PortalError::PreloginError("Prelogin endpoint not found".to_string())) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if status.is_client_error() || status.is_server_error() { | ||||||
|  |     bail!("Prelogin error: {}", status) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   let res_xml = res | ||||||
|  |     .text() | ||||||
|  |     .await | ||||||
|  |     .map_err(|e| PortalError::PreloginError(e.to_string()))?; | ||||||
|  |  | ||||||
|  |   let prelogin = parse_res_xml(res_xml, is_gateway).map_err(|e| PortalError::PreloginError(e.to_string()))?; | ||||||
|  |  | ||||||
|  |   Ok(prelogin) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fn parse_res_xml(res_xml: String, is_gateway: bool) -> anyhow::Result<Prelogin> { | ||||||
|   let doc = Document::parse(&res_xml)?; |   let doc = Document::parse(&res_xml)?; | ||||||
|  |  | ||||||
|   let status = xml::get_child_text(&doc, "status") |   let status = xml::get_child_text(&doc, "status") | ||||||
| @@ -134,12 +159,11 @@ pub async fn prelogin(portal: &str, gp_params: &GpParams) -> anyhow::Result<Prel | |||||||
|   // Check if the prelogin response is SAML |   // Check if the prelogin response is SAML | ||||||
|   if saml_method.is_some() && saml_request.is_some() { |   if saml_method.is_some() && saml_request.is_some() { | ||||||
|     let saml_request = base64::decode_to_string(&saml_request.unwrap())?; |     let saml_request = base64::decode_to_string(&saml_request.unwrap())?; | ||||||
|     let support_default_browser = saml_default_browser |     let support_default_browser = saml_default_browser.map(|s| s.to_lowercase() == "yes").unwrap_or(false); | ||||||
|       .map(|s| s.to_lowercase() == "yes") |  | ||||||
|       .unwrap_or(false); |  | ||||||
|  |  | ||||||
|     let saml_prelogin = SamlPrelogin { |     let saml_prelogin = SamlPrelogin { | ||||||
|       region, |       region, | ||||||
|  |       is_gateway, | ||||||
|       saml_request, |       saml_request, | ||||||
|       support_default_browser, |       support_default_browser, | ||||||
|     }; |     }; | ||||||
| @@ -151,10 +175,11 @@ pub async fn prelogin(portal: &str, gp_params: &GpParams) -> anyhow::Result<Prel | |||||||
|   let label_password = xml::get_child_text(&doc, "password-label"); |   let label_password = xml::get_child_text(&doc, "password-label"); | ||||||
|   // Check if the prelogin response is standard login |   // Check if the prelogin response is standard login | ||||||
|   if label_username.is_some() && label_password.is_some() { |   if label_username.is_some() && label_password.is_some() { | ||||||
|     let auth_message = xml::get_child_text(&doc, "authentication-message") |     let auth_message = | ||||||
|       .unwrap_or(String::from("Please enter the login credentials")); |       xml::get_child_text(&doc, "authentication-message").unwrap_or(String::from("Please enter the login credentials")); | ||||||
|     let standard_prelogin = StandardPrelogin { |     let standard_prelogin = StandardPrelogin { | ||||||
|       region, |       region, | ||||||
|  |       is_gateway, | ||||||
|       auth_message, |       auth_message, | ||||||
|       label_username: label_username.unwrap(), |       label_username: label_username.unwrap(), | ||||||
|       label_password: label_password.unwrap(), |       label_password: label_password.unwrap(), | ||||||
|   | |||||||
| @@ -8,6 +8,7 @@ use super::command_traits::CommandExt; | |||||||
|  |  | ||||||
| pub struct SamlAuthLauncher<'a> { | pub struct SamlAuthLauncher<'a> { | ||||||
|   server: &'a str, |   server: &'a str, | ||||||
|  |   gateway: bool, | ||||||
|   saml_request: Option<&'a str>, |   saml_request: Option<&'a str>, | ||||||
|   user_agent: Option<&'a str>, |   user_agent: Option<&'a str>, | ||||||
|   os: Option<&'a str>, |   os: Option<&'a str>, | ||||||
| @@ -22,6 +23,7 @@ impl<'a> SamlAuthLauncher<'a> { | |||||||
|   pub fn new(server: &'a str) -> Self { |   pub fn new(server: &'a str) -> Self { | ||||||
|     Self { |     Self { | ||||||
|       server, |       server, | ||||||
|  |       gateway: false, | ||||||
|       saml_request: None, |       saml_request: None, | ||||||
|       user_agent: None, |       user_agent: None, | ||||||
|       os: None, |       os: None, | ||||||
| @@ -33,6 +35,11 @@ impl<'a> SamlAuthLauncher<'a> { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   pub fn gateway(mut self, gateway: bool) -> Self { | ||||||
|  |     self.gateway = gateway; | ||||||
|  |     self | ||||||
|  |   } | ||||||
|  |  | ||||||
|   pub fn saml_request(mut self, saml_request: &'a str) -> Self { |   pub fn saml_request(mut self, saml_request: &'a str) -> Self { | ||||||
|     self.saml_request = Some(saml_request); |     self.saml_request = Some(saml_request); | ||||||
|     self |     self | ||||||
| @@ -78,6 +85,10 @@ impl<'a> SamlAuthLauncher<'a> { | |||||||
|     let mut auth_cmd = Command::new(GP_AUTH_BINARY); |     let mut auth_cmd = Command::new(GP_AUTH_BINARY); | ||||||
|     auth_cmd.arg(self.server); |     auth_cmd.arg(self.server); | ||||||
|  |  | ||||||
|  |     if self.gateway { | ||||||
|  |       auth_cmd.arg("--gateway"); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     if let Some(saml_request) = self.saml_request { |     if let Some(saml_request) = self.saml_request { | ||||||
|       auth_cmd.arg("--saml-request").arg(saml_request); |       auth_cmd.arg("--saml-request").arg(saml_request); | ||||||
|     } |     } | ||||||
| @@ -118,7 +129,7 @@ impl<'a> SamlAuthLauncher<'a> { | |||||||
|       .wait_with_output() |       .wait_with_output() | ||||||
|       .await?; |       .await?; | ||||||
|  |  | ||||||
|     let auth_result: SamlAuthResult = serde_json::from_slice(&output.stdout) |     let auth_result = serde_json::from_slice::<SamlAuthResult>(&output.stdout) | ||||||
|       .map_err(|_| anyhow::anyhow!("Failed to parse auth data"))?; |       .map_err(|_| anyhow::anyhow!("Failed to parse auth data"))?; | ||||||
|  |  | ||||||
|     match auth_result { |     match auth_result { | ||||||
|   | |||||||
| @@ -21,8 +21,7 @@ impl CommandExt for Command { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   fn into_non_root(mut self) -> anyhow::Result<Command> { |   fn into_non_root(mut self) -> anyhow::Result<Command> { | ||||||
|     let user = |     let user = get_non_root_user().map_err(|_| anyhow::anyhow!("{:?} cannot be run as root", self))?; | ||||||
|       get_non_root_user().map_err(|_| anyhow::anyhow!("{:?} cannot be run as root", self))?; |  | ||||||
|  |  | ||||||
|     self |     self | ||||||
|       .env("HOME", user.home_dir()) |       .env("HOME", user.home_dir()) | ||||||
| @@ -42,8 +41,7 @@ fn get_non_root_user() -> anyhow::Result<User> { | |||||||
|   let user = if current_user == "root" { |   let user = if current_user == "root" { | ||||||
|     get_real_user()? |     get_real_user()? | ||||||
|   } else { |   } else { | ||||||
|     uzers::get_user_by_name(¤t_user) |     uzers::get_user_by_name(¤t_user).ok_or_else(|| anyhow::anyhow!("User ({}) not found", current_user))? | ||||||
|       .ok_or_else(|| anyhow::anyhow!("User ({}) not found", current_user))? |  | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   if user.uid() == 0 { |   if user.uid() == 0 { | ||||||
|   | |||||||
| @@ -66,10 +66,7 @@ impl GuiLauncher { | |||||||
|  |  | ||||||
|     let mut non_root_cmd = cmd.into_non_root()?; |     let mut non_root_cmd = cmd.into_non_root()?; | ||||||
|  |  | ||||||
|     let mut child = non_root_cmd |     let mut child = non_root_cmd.kill_on_drop(true).stdin(Stdio::piped()).spawn()?; | ||||||
|       .kill_on_drop(true) |  | ||||||
|       .stdin(Stdio::piped()) |  | ||||||
|       .spawn()?; |  | ||||||
|  |  | ||||||
|     let mut stdin = child |     let mut stdin = child | ||||||
|       .stdin |       .stdin | ||||||
|   | |||||||
| @@ -58,10 +58,7 @@ impl ConnectArgs { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   pub fn openconnect_os(&self) -> Option<String> { |   pub fn openconnect_os(&self) -> Option<String> { | ||||||
|     self |     self.os.as_ref().map(|os| os.to_openconnect_os().to_string()) | ||||||
|       .os |  | ||||||
|       .as_ref() |  | ||||||
|       .map(|os| os.to_openconnect_os().to_string()) |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -30,11 +30,13 @@ pub fn normalize_server(server: &str) -> anyhow::Result<String> { | |||||||
|     .host_str() |     .host_str() | ||||||
|     .ok_or(anyhow::anyhow!("Invalid server URL: missing host"))?; |     .ok_or(anyhow::anyhow!("Invalid server URL: missing host"))?; | ||||||
|  |  | ||||||
|   let port: String = normalized_url |   let port: String = normalized_url.port().map_or("".into(), |port| format!(":{}", port)); | ||||||
|     .port() |  | ||||||
|     .map_or("".into(), |port| format!(":{}", port)); |  | ||||||
|  |  | ||||||
|   let normalized_url = format!("{}://{}{}", scheme, host, port); |   let normalized_url = format!("{}://{}{}", scheme, host, port); | ||||||
|  |  | ||||||
|   Ok(normalized_url) |   Ok(normalized_url) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | pub fn remove_url_scheme(s: &str) -> String { | ||||||
|  |   s.replace("http://", "").replace("https://", "") | ||||||
|  | } | ||||||
|   | |||||||
| @@ -115,12 +115,7 @@ pub fn redact_uri(uri: &str) -> String { | |||||||
|       .map(|query| format!("?{}", query)) |       .map(|query| format!("?{}", query)) | ||||||
|       .unwrap_or_default(); |       .unwrap_or_default(); | ||||||
|  |  | ||||||
|     return format!( |     return format!("{}://[**********]{}{}", url.scheme(), url.path(), redacted_query); | ||||||
|       "{}://[**********]{}{}", |  | ||||||
|       url.scheme(), |  | ||||||
|       url.path(), |  | ||||||
|       redacted_query |  | ||||||
|     ); |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   let redacted_query = redact_query(url.query()); |   let redacted_query = redact_query(url.query()); | ||||||
| @@ -165,10 +160,7 @@ mod tests { | |||||||
|  |  | ||||||
|     redaction.add_value("foo").unwrap(); |     redaction.add_value("foo").unwrap(); | ||||||
|  |  | ||||||
|     assert_eq!( |     assert_eq!(redaction.redact_str("hello, foo, bar"), "hello, [**********], bar"); | ||||||
|       redaction.redact_str("hello, foo, bar"), |  | ||||||
|       "hello, [**********], bar" |  | ||||||
|     ); |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   #[test] |   #[test] | ||||||
|   | |||||||
| @@ -2,9 +2,7 @@ use tokio::signal; | |||||||
|  |  | ||||||
| pub async fn shutdown_signal() { | pub async fn shutdown_signal() { | ||||||
|   let ctrl_c = async { |   let ctrl_c = async { | ||||||
|     signal::ctrl_c() |     signal::ctrl_c().await.expect("failed to install Ctrl+C handler"); | ||||||
|       .await |  | ||||||
|       .expect("failed to install Ctrl+C handler"); |  | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   #[cfg(unix)] |   #[cfg(unix)] | ||||||
|   | |||||||
| @@ -5,8 +5,5 @@ fn main() { | |||||||
|   println!("cargo:rerun-if-changed=src/ffi/vpn.h"); |   println!("cargo:rerun-if-changed=src/ffi/vpn.h"); | ||||||
|  |  | ||||||
|   // Compile the vpn.c file |   // Compile the vpn.c file | ||||||
|   cc::Build::new() |   cc::Build::new().file("src/ffi/vpn.c").include("src/ffi").compile("vpn"); | ||||||
|     .file("src/ffi/vpn.c") |  | ||||||
|     .include("src/ffi") |  | ||||||
|     .compile("vpn"); |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -20,10 +20,7 @@ pub(crate) struct ConnectOptions { | |||||||
| #[link(name = "vpn")] | #[link(name = "vpn")] | ||||||
| extern "C" { | extern "C" { | ||||||
|   #[link_name = "vpn_connect"] |   #[link_name = "vpn_connect"] | ||||||
|   fn vpn_connect( |   fn vpn_connect(options: *const ConnectOptions, callback: extern "C" fn(i32, *mut c_void)) -> c_int; | ||||||
|     options: *const ConnectOptions, |  | ||||||
|     callback: extern "C" fn(i32, *mut c_void), |  | ||||||
|   ) -> c_int; |  | ||||||
|  |  | ||||||
|   #[link_name = "vpn_disconnect"] |   #[link_name = "vpn_disconnect"] | ||||||
|   fn vpn_disconnect(); |   fn vpn_disconnect(); | ||||||
|   | |||||||
| @@ -27,11 +27,7 @@ impl Vpn { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   pub fn connect(&self, on_connected: impl FnOnce() + 'static + Send + Sync) -> i32 { |   pub fn connect(&self, on_connected: impl FnOnce() + 'static + Send + Sync) -> i32 { | ||||||
|     self |     self.callback.write().unwrap().replace(Box::new(on_connected)); | ||||||
|       .callback |  | ||||||
|       .write() |  | ||||||
|       .unwrap() |  | ||||||
|       .replace(Box::new(on_connected)); |  | ||||||
|     let options = self.build_connect_options(); |     let options = self.build_connect_options(); | ||||||
|  |  | ||||||
|     ffi::connect(&options) |     ffi::connect(&options) | ||||||
| @@ -107,10 +103,7 @@ impl VpnBuilder { | |||||||
|  |  | ||||||
|   pub fn build(self) -> Vpn { |   pub fn build(self) -> Vpn { | ||||||
|     let user_agent = self.user_agent.unwrap_or_default(); |     let user_agent = self.user_agent.unwrap_or_default(); | ||||||
|     let script = self |     let script = self.script.or_else(find_default_vpnc_script).unwrap_or_default(); | ||||||
|       .script |  | ||||||
|       .or_else(find_default_vpnc_script) |  | ||||||
|       .unwrap_or_default(); |  | ||||||
|     let os = self.os.unwrap_or("linux".to_string()); |     let os = self.os.unwrap_or("linux".to_string()); | ||||||
|  |  | ||||||
|     Vpn { |     Vpn { | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| max_width = 100 | max_width = 120 | ||||||
| hard_tabs = false | hard_tabs = false | ||||||
| tab_spaces = 2 | tab_spaces = 2 | ||||||
| newline_style = "Unix" | newline_style = "Unix" | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user